265 lines
9.1 KiB
Python
Executable File
265 lines
9.1 KiB
Python
Executable File
from django.forms import (
|
|
Form,
|
|
ModelForm,
|
|
ModelMultipleChoiceField,
|
|
ModelChoiceField,
|
|
CheckboxSelectMultiple,
|
|
ChoiceField,
|
|
CharField,
|
|
)
|
|
from django.forms import inlineformset_factory
|
|
|
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixin, ExamMarkerFormMixin
|
|
|
|
from .models import (
|
|
Question,
|
|
UserAnswer,
|
|
Exam,
|
|
Category
|
|
)
|
|
|
|
from django.contrib.admin.widgets import FilteredSelectMultiple
|
|
from django.forms.widgets import RadioSelect, TextInput, Textarea
|
|
|
|
from tinymce.widgets import TinyMCE
|
|
import re
|
|
|
|
from dal import autocomplete
|
|
from crispy_forms.helper import FormHelper
|
|
from crispy_forms.layout import Layout, Fieldset, Row, Column, Submit
|
|
|
|
|
|
class UserAnswerForm(ModelForm):
|
|
class Meta:
|
|
model = UserAnswer
|
|
fields = ["answer"]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(UserAnswerForm, self).__init__(*args, **kwargs)
|
|
self.fields['answer'].required = False
|
|
|
|
# This should be made generic?
|
|
class ExamForm(ExamFormMixin, ModelForm):
|
|
class Meta(ExamFormMixin.Meta):
|
|
model = Exam
|
|
|
|
class ExamAuthorForm(ExamAuthorFormMixin):
|
|
class Meta(ExamAuthorFormMixin.Meta):
|
|
model = Exam
|
|
|
|
class ExamMarkerForm(ExamMarkerFormMixin):
|
|
class Meta(ExamMarkerFormMixin.Meta):
|
|
model = Exam
|
|
|
|
|
|
class ExamGroupsForm(ExamGroupsFormMixin):
|
|
class Meta(ExamGroupsFormMixin.Meta):
|
|
model = Exam
|
|
|
|
class QuestionForm(ModelForm):
|
|
|
|
# exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all())
|
|
|
|
class Media:
|
|
# Django also includes a few javascript files necessary
|
|
# for the operation of this form element. You need to
|
|
# include <script src="/admin/jsi18n"></script>
|
|
# in the template.
|
|
css = {
|
|
"all": ["css/widgets.css"],
|
|
}
|
|
# Adding this javascript is crucial
|
|
js = ["jsi18n.js", "tesseract.min.js"]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.user = kwargs.pop(
|
|
"user"
|
|
) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole
|
|
if kwargs.get("instance"):
|
|
# We get the 'initial' keyword argument or initialize it
|
|
# as a dict if it didn't exist.
|
|
initial = kwargs.setdefault("initial", {})
|
|
# The widget for a ModelMultipleChoiceField expects
|
|
# a list of primary key for the selected data.
|
|
initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()]
|
|
|
|
ModelForm.__init__(self, *args, **kwargs)
|
|
|
|
super(QuestionForm, self).__init__(*args, **kwargs)
|
|
# self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
|
|
# self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
|
|
self.fields["category"] = ModelChoiceField(
|
|
required=True,
|
|
queryset=Category.objects.all(),
|
|
initial=1,
|
|
)
|
|
|
|
|
|
if self.user.groups.filter(name="sba_checker").exists():
|
|
exam_queryset = Exam.objects.all()
|
|
else:
|
|
exam_queryset = Exam.objects.filter(
|
|
author__id=self.user.id
|
|
) | Exam.objects.filter(open_access=True)
|
|
|
|
self.fields["exams"] = ModelMultipleChoiceField(
|
|
required=False,
|
|
queryset=exam_queryset,
|
|
widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False),
|
|
)
|
|
|
|
# Crispy Forms helper and layout
|
|
self.helper = FormHelper()
|
|
self.helper.form_method = "post"
|
|
self.helper.form_enctype = "multipart/form-data"
|
|
self.helper.template_pack = "bootstrap5"
|
|
self.helper.layout = Layout(
|
|
Fieldset(
|
|
"Question",
|
|
"title",
|
|
"stem",
|
|
),
|
|
Fieldset(
|
|
"Answers",
|
|
# Use responsive columns so they stack on small screens and sit side-by-side on md+
|
|
Row(Column("a_answer", css_class="col-md-6"), Column("a_feedback", css_class="col-md-6")),
|
|
Row(Column("b_answer", css_class="col-md-6"), Column("b_feedback", css_class="col-md-6")),
|
|
Row(Column("c_answer", css_class="col-md-6"), Column("c_feedback", css_class="col-md-6")),
|
|
Row(Column("d_answer", css_class="col-md-6"), Column("d_feedback", css_class="col-md-6")),
|
|
Row(Column("e_answer", css_class="col-md-6"), Column("e_feedback", css_class="col-md-6")),
|
|
"best_answer",
|
|
),
|
|
Fieldset(
|
|
"Feedback",
|
|
"feedback",
|
|
),
|
|
Fieldset(
|
|
"Metadata",
|
|
Row(Column("category", css_class="col-md-6"), Column("exams", css_class="col-md-6")),
|
|
Row(Column("open_access", css_class="col-md-4"), Column("frcr_appropriate", css_class="col-md-4")),
|
|
),
|
|
)
|
|
self.helper.add_input(Submit("submit", "Submit", css_class="btn btn-primary"))
|
|
|
|
def save(self, commit=True):
|
|
# Get the unsaved Pizza instance
|
|
instance = ModelForm.save(self, False)
|
|
instance.save()
|
|
|
|
old_exams = instance.exams.all()
|
|
|
|
new_exams = self.cleaned_data["exams"]
|
|
|
|
for exam in old_exams:
|
|
if exam not in new_exams:
|
|
exam.exam_questions.remove(instance)
|
|
|
|
for exam in new_exams:
|
|
exam.exam_questions.add(instance)
|
|
|
|
self.save_m2m()
|
|
|
|
return instance
|
|
|
|
def clean(self):
|
|
"""Strip a single wrapping <p>...</p> from TinyMCE-provided HTML fields.
|
|
|
|
This handles the common TinyMCE behaviour where the editor wraps
|
|
inline content in a root <p>. We only unwrap when the entire value
|
|
is a single <p> element (so multi-paragraph content is preserved).
|
|
"""
|
|
cleaned = super(QuestionForm, self).clean()
|
|
|
|
def _unwrap_single_p(html: str) -> str:
|
|
if not html or not isinstance(html, str):
|
|
return html
|
|
# Match a single <p ...>...</p> that spans the whole string
|
|
m = re.fullmatch(r"\s*<p\b[^>]*>(.*)</p>\s*", html, flags=re.DOTALL | re.IGNORECASE)
|
|
if m:
|
|
return m.group(1)
|
|
return html
|
|
|
|
fields_to_unwrap = [
|
|
"stem",
|
|
"feedback",
|
|
"a_answer",
|
|
"a_feedback",
|
|
"b_answer",
|
|
"b_feedback",
|
|
"c_answer",
|
|
"c_feedback",
|
|
"d_answer",
|
|
"d_feedback",
|
|
"e_answer",
|
|
"e_feedback",
|
|
]
|
|
|
|
for fname in fields_to_unwrap:
|
|
if fname in cleaned:
|
|
cleaned[fname] = _unwrap_single_p(cleaned.get(fname))
|
|
|
|
return cleaned
|
|
|
|
class Meta:
|
|
model = Question
|
|
|
|
fields = [
|
|
"title",
|
|
"stem",
|
|
"feedback",
|
|
"category",
|
|
"a_answer",
|
|
"a_feedback",
|
|
"b_answer",
|
|
"b_feedback",
|
|
"c_answer",
|
|
"c_feedback",
|
|
"d_answer",
|
|
"d_feedback",
|
|
"e_answer",
|
|
"e_feedback",
|
|
"best_answer",
|
|
"finding",
|
|
"structure",
|
|
"condition",
|
|
"presentation",
|
|
"subspecialty",
|
|
"open_access",
|
|
"frcr_appropriate",
|
|
]
|
|
|
|
widgets = {
|
|
# "normal": RadioSelect(
|
|
# choices=[(True, 'Yes'),
|
|
# (False, 'No')])
|
|
"stem" : TinyMCE(attrs={'cols': 80, 'rows': 30}),
|
|
"feedback" : TinyMCE(attrs={'cols': 80, 'rows': 30}),
|
|
"a_answer" : TinyMCE(attrs={'cols': 80, 'rows': 5}, mce_attrs={'height': 200}),
|
|
"a_feedback" : TinyMCE(attrs={'cols': 80, 'rows': 5}, mce_attrs={'height': 200}),
|
|
"b_answer" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 200}),
|
|
"c_answer" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 200}),
|
|
"d_answer" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 200}),
|
|
"e_answer" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 200}),
|
|
"b_feedback" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 200}),
|
|
"c_feedback" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 200}),
|
|
"d_feedback" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 200}),
|
|
"e_feedback" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 200}),
|
|
"structure": autocomplete.ModelSelect2Multiple(
|
|
url="atlas:structure-autocomplete"
|
|
),
|
|
"finding": autocomplete.ModelSelect2Multiple(
|
|
url="atlas:finding-autocomplete"
|
|
),
|
|
"condition": autocomplete.ModelSelect2Multiple(
|
|
url="atlas:condition-autocomplete"
|
|
),
|
|
"presentation": autocomplete.ModelSelect2Multiple(
|
|
url="atlas:presentation-autocomplete"
|
|
),
|
|
"subspecialty": CheckboxSelectMultiple(),
|
|
}
|
|
|
|
#widgets = {
|
|
# "structure": autocomplete.ModelSelect2(url="anatomy:structure-autocomplete")
|
|
#}
|