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 from generic.widgets import ExamSearchWidget 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 # 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=ExamSearchWidget(exam_model=Exam), ) # 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
...
from TinyMCE-provided HTML fields. This handles the common TinyMCE behaviour where the editor wraps inline content in a root. We only unwrap when the entire value is a single
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
...
that spans the whole string m = re.fullmatch(r"\s*]*>(.*)
\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") #}