diff --git a/atlas/forms.py b/atlas/forms.py index d72d8170..55e07acb 100755 --- a/atlas/forms.py +++ b/atlas/forms.py @@ -12,7 +12,7 @@ from django.forms import ( CharField, ValidationError, modelformset_factory, - CheckboxSelectMultiple + CheckboxSelectMultiple, ) from django.forms import inlineformset_factory from django.shortcuts import get_object_or_404 @@ -45,7 +45,7 @@ from atlas.models import ( from anatomy.models import Modality -from generic.models import Examination#, Sign +from generic.models import Examination # , Sign # from generic.models import Examination, Site, Condition, Sign @@ -58,7 +58,13 @@ from tinymce.widgets import TinyMCE from dal import autocomplete -from autocomplete import widgets as htmx_widgets, Autocomplete, AutocompleteWidget, ModelAutocomplete, register as autocomplete_register +from autocomplete import ( + widgets as htmx_widgets, + Autocomplete, + AutocompleteWidget, + ModelAutocomplete, + register as autocomplete_register, +) import logging @@ -73,28 +79,30 @@ from typing import Any from helpers.cimar import CimarAPI, NotFoundError from rad.settings import CIMAR_USERNAME, CIMAR_PASSWORD + class ClearableInput(TextInput): template_name = "crispy_forms/widgets/clearable_input_single_line.html" + class ClearableTextarea(Textarea): template_name = "crispy_forms/widgets/clearable_input.html" + class CaseSelect(Select): template_name = "atlas/case_select_widget.html" - def create_option(self, *args, **kwargs): option = super().create_option(*args, **kwargs) return option def get_context(self, name: str, value, attrs) -> dict[str, Any]: - context = super().get_context(name, value, attrs) + context = super().get_context(name, value, attrs) return context - pass + class ConditionForm(ModelForm): class Meta: model = Condition @@ -119,26 +127,56 @@ class CaseCollectionForm(ModelForm): exclude = ["cases", "valid_cid_users", "valid_user_users", "author"] 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 + self.user = kwargs.pop("user") 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(CaseCollectionForm, self).__init__(*args, **kwargs) + # Identify show_ fields + show_fields = [f for f in self.fields if f.startswith("show_")] + show_pre_fields = [f for f in show_fields if f.endswith("_pre")] + show_post_fields = [f for f in show_fields if f.endswith("_post")] + + self.helper = FormHelper() + self.helper.form_id = "id-casecollection-form" + self.helper.form_class = "casecollection-form" + self.helper.form_method = "post" + self.helper.form_action = "" + self.helper.form_tag = True + # Group cid_user_groups and user_user_groups together if they exist in the form + user_fields = [] + for f in ["cid_user_groups", "user_user_groups"]: + if f in self.fields: + user_fields.append(f) + + other_fields = [f for f in self.fields if f not in show_fields and f not in user_fields] + + self.helper.layout = Layout( + Fieldset("Collection Details", *other_fields), + # Add grouped user fields if present + Fieldset("Valid User Groups", *user_fields) if user_fields else None, + Div( + Div( + Fieldset("Show fields (pre-exam)", *show_pre_fields), + css_class="col-md-6", + style="float:left;", + ), + Div( + Fieldset("Show fields (post-exam)", *show_post_fields), + css_class="col-md-6", + style="float:left;", + ), + css_class="row", + style="display: flex; flex-wrap: wrap;", + ), + Submit("submit", "Save Collection", css_class="btn btn-primary"), + ) + def save(self, commit=True): # Get the unsaved Case instance instance = ModelForm.save(self, False) - # Do we need to save all changes now? # if commit: instance.save() @@ -174,7 +212,14 @@ class StructureForm(ModelForm): class SeriesFindingForm(ModelForm): class Meta: model = SeriesFinding - exclude = ["series", "annotation_json", "viewport_json", "current_image_id_index", "annotation_json_3d", "viewer_state_3d"] + exclude = [ + "series", + "annotation_json", + "viewport_json", + "current_image_id_index", + "annotation_json_3d", + "viewer_state_3d", + ] widgets = { "findings": autocomplete.ModelSelect2Multiple( @@ -186,7 +231,6 @@ class SeriesFindingForm(ModelForm): "conditions": autocomplete.ModelSelect2Multiple( url="atlas:condition-autocomplete" ), - } def __init__(self, *args, **kwargs): @@ -297,12 +341,10 @@ class SeriesForm(ModelForm): "description": Textarea(attrs={"maxlength": 1000, "rows": 5, "cols": 80}), } - - #def save(self, commit=True): + # def save(self, commit=True): # # Get the unsaved Case instance # instance = ModelForm.save(self, False) - # # Prepare a 'save_m2m' method for the form, # old_save_m2m = self.save_m2m @@ -320,20 +362,19 @@ class SeriesForm(ModelForm): # instance.save() # self.save_m2m() - # return instance -class CaseForm(ModelForm): +class CaseForm(ModelForm): class Media: # Django also includes a few javascript files necessary # for the operation of this form element. You need to # include # in the template. - #history = forms.CharField( + # history = forms.CharField( # widget=ClearableTextarea(attrs={"maxlength": 1000, "rows": 5, "cols": 80}), # required=False, - #) + # ) css = { "all": ["css/widgets.css"], @@ -348,7 +389,7 @@ class CaseForm(ModelForm): # This is populated if we create the case from an exam/collection self.collection = None - if kwargs["initial"] is not None and "exams" in kwargs["initial"]: + if kwargs["initial"] is not None and "exams" in kwargs["initial"]: collections = kwargs["initial"].pop("exams") logging.debug(collections) @@ -362,20 +403,16 @@ class CaseForm(ModelForm): # 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(CaseForm, self).__init__(*args, **kwargs) # self.fields["series"].queryset = if self.user.groups.filter(name="atlas_editor").exists(): - case_queryset = Case.objects.all().order_by("pk").prefetch_related( - ) + case_queryset = Case.objects.all().order_by("pk").prefetch_related() else: case_queryset = get_cases_available_to_user(self.user) - self.fields["previous_case"].queryset=case_queryset - + self.fields["previous_case"].queryset = case_queryset self.helper = FormHelper() self.helper.form_id = "id-case-form" @@ -401,10 +438,8 @@ class CaseForm(ModelForm): "diagnostic_certainty", "cimar_uuid", ), - ) - def get_queryset(self, request): return ( super() @@ -433,7 +468,7 @@ class CaseForm(ModelForm): "open_access", "previous_case", "diagnostic_certainty", - "cimar_uuid" + "cimar_uuid", ] # fields = ['question', 'findings', 'subspecialty', 'references'] widgets = { @@ -457,7 +492,7 @@ class CaseForm(ModelForm): "pathological_process": CheckboxSelectMultiple(), "previous_case": CaseSelect(), } - + def clean_cimar_uuid(self): cimar_uuid = self.cleaned_data["cimar_uuid"] if cimar_uuid: @@ -474,7 +509,6 @@ class CaseForm(ModelForm): # Get the unsaved Case instance instance = ModelForm.save(self, False) - # Prepare a 'save_m2m' method for the form, old_save_m2m = self.save_m2m @@ -501,8 +535,10 @@ class CaseForm(ModelForm): logging.debug(f"{instance=}") # Create through model - #cd = CaseDetail(case=instance, collection=exam, sort_order=case_no) - self.collection.cases.add(instance, through_defaults={"sort_order": case_no}) + # cd = CaseDetail(case=instance, collection=exam, sort_order=case_no) + self.collection.cases.add( + instance, through_defaults={"sort_order": case_no} + ) return instance @@ -520,6 +556,7 @@ CaseDifferentialFormSet = inlineformset_factory( }, ) + class ResourceForm(ModelForm): class Meta: model = Resource @@ -532,7 +569,6 @@ class ResourceForm(ModelForm): class CaseResourceForm(ModelForm): - def __init__(self, *args, user, **kwargs): super(CaseResourceForm, self).__init__(*args, **kwargs) @@ -550,7 +586,7 @@ class CaseResourceForm(ModelForm): self.fields["pre_review"] = BooleanField(required=False) -#class CaseSeriesForm(ModelForm): +# class CaseSeriesForm(ModelForm): # # def __init__(self, *args, user, **kwargs): # super(CaseSeriesForm, self).__init__(*args, **kwargs) @@ -574,7 +610,6 @@ class CaseResourceForm(ModelForm): # ) - class CaseCollectionCaseForm(ModelForm): def __init__(self, *args, user, collection=None, **kwargs): super(CaseCollectionCaseForm, self).__init__(*args, **kwargs) @@ -588,7 +623,7 @@ class CaseCollectionCaseForm(ModelForm): ) -#SeriesFormSet = inlineformset_factory( +# SeriesFormSet = inlineformset_factory( # Case, # Series.case.through, # form=CaseSeriesForm, @@ -596,7 +631,7 @@ class CaseCollectionCaseForm(ModelForm): # can_delete=True, # extra=0, # max_num=10, -#) +# ) CaseCollectionCaseFormSet = inlineformset_factory( CaseCollection, @@ -621,7 +656,13 @@ CaseResourceFormSet = inlineformset_factory( SeriesImageFormSet = inlineformset_factory( Series, SeriesImage, - exclude=["dicom_tags_ohif", "replaced", "image_md5_hash", "image_blake3_hash", "is_dicom"], + exclude=[ + "dicom_tags_ohif", + "replaced", + "image_md5_hash", + "image_blake3_hash", + "is_dicom", + ], can_delete=True, extra=0, max_num=2000, @@ -630,19 +671,23 @@ SeriesImageFormSet = inlineformset_factory( class JsonAnswerForm(Form): json_answer = JSONSchemaField( - schema = {}, - #options = 'schema/options.json' - options = 'schema/options.json' + schema={}, + # options = 'schema/options.json' + options="schema/options.json", ) + class Meta: fields = ["json_answer"] - def __init__(self, *args, question_schema=None, **kwargs): + def __init__(self, *args, question_schema=None, **kwargs): super(JsonAnswerForm, self).__init__(*args, **kwargs) if question_schema is not None: - self.fields["json_answer"] = JSONSchemaField(schema=question_schema, options='schema/options.json') + self.fields["json_answer"] = JSONSchemaField( + schema=question_schema, options="schema/options.json" + ) self.fields["json_answer"].label = False + class BaseReportAnswerForm(ModelForm): class Meta: fields = ["answer"] @@ -653,22 +698,23 @@ class BaseReportAnswerForm(ModelForm): # (False, 'No')]) # "findings": TinyMCE(attrs={"cols": 80, "rows": 20}), # "mark_scheme": TinyMCE(attrs={"cols": 80, "rows": 30}), - "answer": Textarea(attrs={"cols": 100, "rows": 5, "placeholder":"Report text"}), + "answer": Textarea( + attrs={"cols": 100, "rows": 5, "placeholder": "Report text"} + ), } # help_texts = { # "answer": "Write your answer in here." # } - def __init__(self, *args, case_detail, **kwargs): + def __init__(self, *args, case_detail, **kwargs): super(BaseReportAnswerForm, self).__init__(*args, **kwargs) self.fields["answer"].required = False + class BaseQuestionAnswerForm(ModelForm): - json_answer = JSONSchemaField( - schema = {}, - options = 'schema/options.json' - ) + json_answer = JSONSchemaField(schema={}, options="schema/options.json") + class Meta: fields = ["json_answer"] labels = {"json_answer": ""} @@ -677,11 +723,13 @@ class BaseQuestionAnswerForm(ModelForm): # "answer": "Write your answer in here." # } - def __init__(self, *args, case_detail, **kwargs): + def __init__(self, *args, case_detail, **kwargs): super(BaseQuestionAnswerForm, self).__init__(*args, **kwargs) - #self.fields["json_answer"].schema = case_detail.question_schema + # self.fields["json_answer"].schema = case_detail.question_schema if case_detail.question_schema is not None: - self.fields["json_answer"] = JSONSchemaField(schema=case_detail.question_schema, options="schema/options.json") + self.fields["json_answer"] = JSONSchemaField( + schema=case_detail.question_schema, options="schema/options.json" + ) self.fields["json_answer"].label = "" @@ -689,18 +737,22 @@ class CidReportAnswerForm(BaseReportAnswerForm): class Meta(BaseReportAnswerForm.Meta): model = CidReportAnswer + class UserReportAnswerForm(BaseReportAnswerForm): class Meta(BaseReportAnswerForm.Meta): model = UserReportAnswer + class CidQuestionAnswerForm(BaseQuestionAnswerForm): class Meta(BaseQuestionAnswerForm.Meta): model = CidReportAnswer + class UserQuestionAnswerForm(BaseQuestionAnswerForm): class Meta(BaseQuestionAnswerForm.Meta): model = UserReportAnswer + class CidReportAnswerMarkForm(ModelForm): class Meta: model = CidReportAnswer @@ -720,7 +772,7 @@ class CidReportAnswerMarkForm(ModelForm): self.fields["score"].required = False -#class AddCollectionToCaseForm(Form): +# class AddCollectionToCaseForm(Form): # # def __init__(self, *args, user, **kwargs): # super(AddCollectionToCaseForm, self).__init__(*args, **kwargs) @@ -729,7 +781,7 @@ class CidReportAnswerMarkForm(ModelForm): # queryset = CaseCollection.objects.filter(author__id=user.id) # else: # queryset = CaseCollection.objects.all() -# +# # self.valid_collections = queryset # self.case = get_object_or_404(Case, pk=pk) # @@ -761,8 +813,8 @@ class CidReportAnswerMarkForm(ModelForm): # # return collection_set -class AddCollectionToCaseForm(Form): +class AddCollectionToCaseForm(Form): def __init__(self, *args, user, **kwargs): super(AddCollectionToCaseForm, self).__init__(*args, **kwargs) @@ -770,7 +822,7 @@ class AddCollectionToCaseForm(Form): queryset = CaseCollection.objects.filter(author__id=user.id) else: queryset = CaseCollection.objects.all() - + self.valid_collections = queryset self.fields["casecollection"] = ModelMultipleChoiceField( @@ -783,7 +835,7 @@ class AddCollectionToCaseForm(Form): instance = super(AddCollectionToCaseForm, self).save(*args, **kwargs) # Here we save the modified project selection back into the database - instance.casecollection_set.set(self.cleaned_data['casecollection']) + instance.casecollection_set.set(self.cleaned_data["casecollection"]) return instance @@ -798,7 +850,6 @@ class AddCollectionToCaseForm(Form): class SelfReviewForm(ModelForm): - class Meta: model = SelfReview fields = ["user_exam", "case", "comments", "findings", "interpretation"] @@ -813,16 +864,16 @@ class SelfReviewForm(ModelForm): ModelForm.__init__(self, *args, **kwargs) super(SelfReviewForm, self).__init__(*args, **kwargs) - + class UncategorisedDicomForm(ModelForm): class Meta: model = UncategorisedDicom - fields = ["image"]#, "user"] + fields = ["image"] # , "user"] -#UncategorisedDicomFormset = formset_factory(UncategorisedDicomForm) +# UncategorisedDicomFormset = formset_factory(UncategorisedDicomForm) -#class ConditionAutocompleteFormModel(forms.ModelForm): +# class ConditionAutocompleteFormModel(forms.ModelForm): # class Meta: # model = Condition # fields = ["name"] @@ -833,49 +884,56 @@ class UncategorisedDicomForm(ModelForm): # ) # } + class ConditionAutocomplete(ModelAutocomplete): model = Condition search_attrs = ["name"] -class ConditionAutocompleteForm(Form): - condition = ModelChoiceField(queryset=Condition.objects.all(), widget=AutocompleteWidget( - ac_class=ConditionAutocomplete#, options=dict(model=Condition) - )) +class ConditionAutocompleteForm(Form): + condition = ModelChoiceField( + queryset=Condition.objects.all(), + widget=AutocompleteWidget( + ac_class=ConditionAutocomplete # , options=dict(model=Condition) + ), + ) + autocomplete_register(ConditionAutocomplete) + class CaseCollectionAuthorForm(ExamAuthorFormMixin): class Meta(ExamAuthorFormMixin.Meta): model = CaseCollection + class CaseAuthorForm(ExamAuthorFormMixin): class Meta(ExamAuthorFormMixin.Meta): model = Case + + class SeriesAuthorForm(ExamAuthorFormMixin): class Meta(ExamAuthorFormMixin.Meta): model = Series + class ExamGroupsForm(ExamGroupsFormMixin): class Meta(ExamGroupsFormMixin.Meta): model = CaseCollection -class AnswerJSONForm(Form): - json = JSONSchemaField( - schema = 'schema/schema.json', - options = 'schema/options.json' - ) +class AnswerJSONForm(Form): + json = JSONSchemaField(schema="schema/schema.json", options="schema/options.json") class SvelteJSONEditorWidgetOverride(SvelteJSONEditorWidget): template_name = "atlas/svelte_jsoneditor_widget_override.html" -class CaseDetailForm(ModelForm): +class CaseDetailForm(ModelForm): class Meta: model = CaseDetail - fields = ["question_schema", "question_answers"]#, "user"] + fields = ["question_schema", "question_answers"] # , "user"] widgets = { "question_schema": SvelteJSONEditorWidgetOverride(), @@ -892,20 +950,19 @@ class QuestionSchemaForm(ModelForm): "schema": SvelteJSONEditorWidgetOverride(), } -#class ModelPriorCaseForm(ModelForm): + +# class ModelPriorCaseForm(ModelForm): # class Meta: # model = CasePrior # fields = ["case_detail", "prior_case", "relation_text", "prior_visibility"] -class PriorCaseForm(Form): +class PriorCaseForm(Form): relation = CharField(max_length=255, required=True) prior_visibility = CharField(max_length=2, required=True) - - - #def __init__(self, *args, **kwargs): + # def __init__(self, *args, **kwargs): # self.case_detail = kwargs.pop( # "case_detail" # ) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole @@ -922,6 +979,7 @@ class PriorCaseForm(Form): # # widget=Select(verbose_name="Series", is_stacked=False), # ) + class CaseSeriesForm(forms.ModelForm): def __init__(self, *args, queryset, user=None, is_atlas_editor=False, **kwargs): super().__init__(*args, **kwargs) @@ -931,6 +989,7 @@ class CaseSeriesForm(forms.ModelForm): model = SeriesDetail # through model for Case.series fields = ["series", "sort_order", "feedback"] + class CaseUpdateSeriesForm(forms.ModelForm): def __init__(self, *args, **kwargs): self.user = kwargs.pop("user") @@ -942,10 +1001,12 @@ class CaseUpdateSeriesForm(forms.ModelForm): self.helper.form_action = "submit" self.helper.form_tag = False self.helper.layout = Layout() + class Meta: model = Case fields = [] # No fields, just for compatibility with UpdateView + class BaseSeriesFormSet(BaseInlineFormSet): def __init__(self, *args, queryset=None, **kwargs): self.series_queryset = queryset @@ -953,7 +1014,7 @@ class BaseSeriesFormSet(BaseInlineFormSet): def get_form_kwargs(self, index): kwargs = super().get_form_kwargs(index) - kwargs['queryset'] = self.series_queryset + kwargs["queryset"] = self.series_queryset return kwargs @@ -966,6 +1027,7 @@ SeriesFormSet = inlineformset_factory( can_delete=True, ) + class CaseDisplaySetForm(forms.ModelForm): class Meta: model = CaseDisplaySet @@ -977,8 +1039,12 @@ class CaseDisplaySetForm(forms.ModelForm): "conditions", ] widgets = { - "name": forms.TextInput(attrs={"class": "form-control", "placeholder": "Display Set Name"}), - "description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Description"}), + "name": forms.TextInput( + attrs={"class": "form-control", "placeholder": "Display Set Name"} + ), + "description": forms.Textarea( + attrs={"class": "form-control", "rows": 3, "placeholder": "Description"} + ), "findings": autocomplete.ModelSelect2Multiple( url="atlas:finding-autocomplete" ), @@ -1007,6 +1073,7 @@ class CaseCollectionUpdateCaseForm(forms.ModelForm): self.helper.form_action = "submit" self.helper.form_tag = False self.helper.layout = Layout() + class Meta: model = CaseCollection - fields = [] # No fields, just for compatibility with UpdateView \ No newline at end of file + fields = [] # No fields, just for compatibility with UpdateView diff --git a/atlas/templates/atlas/casecollection_form.html b/atlas/templates/atlas/casecollection_form.html index 5bef2507..cc6d02cc 100755 --- a/atlas/templates/atlas/casecollection_form.html +++ b/atlas/templates/atlas/casecollection_form.html @@ -1,5 +1,6 @@ {% extends "atlas/exams.html" %} +{% load crispy_forms_tags %} {% block css %} {% endblock %} @@ -36,7 +37,7 @@
{% csrf_token %} - {{ form }} + {% crispy form form.helper %} {% comment %}

Cases:

Add cases here. These can only be added once created (they can also be added to cases on creation). Click and drag to change order. @@ -60,7 +61,7 @@ {{ case_formset.empty_form }} {% endcomment %} - + {% comment %} {% endcomment %}