from django import forms from django.contrib.admin import widgets from django.forms import ( BaseInlineFormSet, BooleanField, Form, HiddenInput, ModelForm, ModelMultipleChoiceField, ModelChoiceField, ChoiceField, CharField, ValidationError, modelformset_factory, CheckboxSelectMultiple, SplitDateTimeWidget, ) from django.utils.html import escape from django.forms import inlineformset_factory from django.shortcuts import get_object_or_404 from django_jsonforms.forms import JSONSchemaField from django_svelte_jsoneditor.widgets import SvelteJSONEditorWidget from atlas.models import ( Case, CaseCollection, CaseDetail, CasePrior, CidReportAnswer, Differential, Finding, QuestionSchema, Resource, SelfReview, Series, SeriesDetail, SeriesImage, SeriesFinding, Condition, Structure, Subspecialty, UncategorisedDicom, UserReportAnswer, CaseDisplaySet, ) from .models import NormalCase from anatomy.models import Modality from generic.models import Examination # , Sign # from generic.models import Examination, Site, Condition, Sign from django.contrib.admin.widgets import FilteredSelectMultiple from django.forms.widgets import RadioSelect, TextInput, Textarea, Select from crispy_forms.layout import Submit, Layout, Fieldset, Field, Div, HTML from crispy_forms.bootstrap import Accordion, AccordionGroup from tinymce.widgets import TinyMCE from dal import autocomplete from autocomplete import ( widgets as htmx_widgets, Autocomplete, AutocompleteWidget, ModelAutocomplete, register as autocomplete_register, ) from loguru import logger from generic.forms import ( ExamAuthorFormMixin, ExamGroupsFormMixin, SplitDateTimeFieldDefaultTime, SplitDateTimeFieldDefaultTimeEnd, ) from crispy_forms.helper import FormHelper from atlas.helpers import get_cases_available_to_user 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) return context pass class ConditionForm(ModelForm): class Meta: model = Condition exclude = ["rcr_curriculum"] widgets = { "synonym": autocomplete.ModelSelect2Multiple( url="atlas:condition-autocomplete" ), "parent": autocomplete.ModelSelect2Multiple( url="atlas:condition-autocomplete" ), "rcr_curriculum_map": autocomplete.ModelSelect2Multiple( url="atlas:condition-autocomplete-rcr-curriculum" ), } class CaseCollectionForm(ModelForm): class Meta: model = CaseCollection exclude = ["cases", "valid_cid_users", "valid_user_users", "author"] def __init__(self, *args, **kwargs): self.user = kwargs.pop("user") if kwargs.get("instance"): initial = kwargs.setdefault("initial", {}) ModelForm.__init__(self, *args, **kwargs) super(CaseCollectionForm, self).__init__(*args, **kwargs) # Render collection_type as radio buttons and ensure it appears near the top if "collection_type" in self.fields: # Replace the model field with an explicit ChoiceField that uses RadioSelect original = self.fields["collection_type"] choices = getattr(original, "choices", ()) initial = original.initial if hasattr(original, "initial") else None required = original.required label = original.label widget = RadioSelect(attrs={"class": "btn-check"}) # assign custom template for rendering as button group #widget.template_name = "widgets/radioselect_buttons.html" self.fields["collection_type"] = forms.ChoiceField( choices=choices, widget=widget, initial=initial, required=required, label=label, ) # 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) # Explicitly define show fields for pre and post show_pre_fields = [ f for f in [ "show_title_pre", "show_history_pre", "show_description_pre", "show_discussion_pre", "show_report_pre", ] if f in self.fields ] show_post_fields = [ f for f in [ "show_title_post", "show_history_post", "show_description_post", "show_discussion_post", "show_report_post", ] if f in self.fields ] self.helper.layout = Layout( Fieldset( "Collection Details", "name", Field("collection_type", css_class="mb-2"), Fieldset( "Dates", "restrict_to_dates", Div( Field( "start_date", wrapper_class="col-md-6", css_class="form-group", ), Field( "end_date", wrapper_class="col-md-6", css_class="form-group" ), css_class="form-row d-flex", ), ), "active", "publish_results", "archive", "open_access", "candidates_only", "authors_only", "results_supervisor_visible", "exam_open_access", "markers", "exam_mode", "self_review", "feedback_once_collection_complete", "viewer_mode", "question_time_limit", "prerequisites", ), 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"), ) class NormalCaseForm(ModelForm): # Keep non-model input fields for age entry so users can provide a # value + unit (e.g. 6 months) which will be converted to canonical # days on save. The model now stores only `age_days`. age_value = forms.IntegerField(required=False, min_value=0) age_unit = forms.ChoiceField(required=False, choices=NormalCase.AGE_UNIT_CHOICES) class Meta: model = NormalCase fields = ("examination", "modality", "notes") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() # Do not render a
tag from crispy here because the template # already provides the surrounding form element (we inject this # fragment into a modal that contains the form). Rendering a second # would nest forms and cause fields to be outside the # submitted form (HTMX would only send the outer form's fields). self.helper.form_method = "post" self.helper.form_tag = False self.helper.layout = Layout( Div( Field("age_value", wrapper_class="col-md-3"), Field("age_unit", wrapper_class="col-md-3"), Field("examination", wrapper_class="col-md-3"), Field("modality", wrapper_class="col-md-3"), css_class="row", ), Field("notes"), Submit("submit", "Save", css_class="btn btn-primary"), ) def save(self, commit=True): # Respect commit flag. Compute canonical `age_days` from the # provided age_value/age_unit and set it on the instance. instance = super().save(commit=False) age_value = self.cleaned_data.get("age_value") age_unit = self.cleaned_data.get("age_unit") if age_value is not None and age_unit: instance.age_days = NormalCase.to_days(age_value, age_unit) else: # If the user left fields blank, clear any previous value. instance.age_days = None if commit: instance.save() self.save_m2m() return instance class FindingForm(ModelForm): class Meta: model = Finding exclude = [] widgets = { "synonym": autocomplete.ModelSelect2Multiple( url="atlas:finding-autocomplete" ), } class StructureForm(ModelForm): class Meta: model = Structure exclude = [] widgets = { "synonym": autocomplete.ModelSelect2Multiple( url="atlas:structure-autocomplete" ), } class SeriesFindingForm(ModelForm): class Meta: model = SeriesFinding exclude = [ "series", "annotation_json", "viewport_json", "current_image_id_index", "annotation_json_3d", "viewer_state_3d", ] widgets = { "findings": autocomplete.ModelSelect2Multiple( url="atlas:finding-autocomplete" ), "structures": autocomplete.ModelSelect2Multiple( url="atlas:structure-autocomplete" ), "conditions": autocomplete.ModelSelect2Multiple( url="atlas:condition-autocomplete" ), } def __init__(self, *args, **kwargs): if kwargs.get("series_id"): # 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["series"] = kwargs.pop("series_id") # elif 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["structures"] = [t.pk for t in kwargs["structures"].case.all()] # #initial["structures"] = [t.pk for t in kwargs["structures"].case.all()] super(SeriesFindingForm, self).__init__(*args, **kwargs) ModelForm.__init__(self, *args, **kwargs) class SeriesForm(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. css = { "all": ["css/widgets.css"], } # Adding this javascript is crucial js = ["jsi18n.js", "tesseract.min.js", "js/upload_form_helpers.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["case"] = [t.pk for t in kwargs["instance"].case.all()] super(SeriesForm, self).__init__(*args, **kwargs) # self.fields["examination"] = ModelChoiceField( # queryset=Examination.objects.all(), # widget=FilteredSelectMultiple(verbose_name="Examination", is_stacked=False), # ) self.fields["modality"] = ModelChoiceField( required=True, queryset=Modality.objects.all(), ) ModelForm.__init__(self, *args, **kwargs) if self.user.groups.filter(name="atlas_editor").exists(): case_queryset = Case.objects.all() else: case_queryset = Case.objects.filter( author__id=self.user.id ) # | Case.objects.filter(open_access=True) self.fields["case"] = ModelMultipleChoiceField( required=False, queryset=case_queryset, widget=FilteredSelectMultiple(verbose_name="Case", is_stacked=False), ) def save(self, commit=True): # Get the unsaved Long instance instance = ModelForm.save(self, False) # Prepare a 'save_m2m' method for the form, old_save_m2m = self.save_m2m def save_m2m(): old_save_m2m() instance.case.clear() for case in self.cleaned_data["case"]: # We need the id here instance.case.add(case.pk) self.save_m2m = save_m2m # Do we need to save all changes now? # if commit: instance.save() self.save_m2m() # instance.anonymise_images() return instance class Meta: model = Series exclude = ["author", "series_instance_uid"] widgets = { "examination": autocomplete.ModelSelect2( url="generic:examination-autocomplete" ), "description": Textarea(attrs={"maxlength": 1000, "rows": 5, "cols": 80}), } # 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 # def save_m2m(): # old_save_m2m() # # instance.exams.clear() # # for exam in self.cleaned_data["exams"]: # # # We need the id here # # instance.exams.add(exam.pk) # self.save_m2m = save_m2m # # Do we need to save all changes now? # # if commit: # instance.save() # self.save_m2m() # return instance 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( # widget=ClearableTextarea(attrs={"maxlength": 1000, "rows": 5, "cols": 80}), # required=False, # ) css = { "all": ["css/widgets.css"], } # Adding this javascript is crucial js = ["jsi18n.js", "tesseract.min.js", "js/upload_form_helpers.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 # 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"]: collections = kwargs["initial"].pop("exams") logger.debug(collections) self.collection = get_object_or_404(CaseCollection, pk=collections[0]) 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(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() else: case_queryset = get_cases_available_to_user(self.user) self.fields["previous_case"].queryset = case_queryset self.helper = FormHelper() self.helper.form_id = "id-case-form" self.helper.form_class = "case-form" self.helper.form_method = "post" self.helper.form_action = "submit" self.helper.form_tag = False self.helper.layout = Layout( Fieldset( "Case details", "title", "subspecialty", "description", "history", "presentation", "discussion", "report", "condition", "pathological_process", "open_access", "previous_case", "diagnostic_certainty", "cimar_uuid", ), ) def get_queryset(self, request): return ( super() .get_queryset(request) .prefetch_related( "condition", "subspecialty", "pathological_process", "series" ) ) class Meta: model = Case # fields = ['due_back'] # fields = '__all__' fields = [ # "examination", # "site", "title", "subspecialty", "description", "history", "presentation", "discussion", "condition", "pathological_process", "report", "open_access", "previous_case", "diagnostic_certainty", "cimar_uuid", ] # fields = ['question', 'findings', 'subspecialty', 'references'] widgets = { # "normal": RadioSelect( # choices=[(True, 'Yes'), # (False, 'No')]) # "findings": TinyMCE(attrs={"cols": 80, "rows": 20}), # "mark_scheme": TinyMCE(attrs={"cols": 80, "rows": 30}), "title": ClearableInput(attrs={"rows": 1}), "description": ClearableTextarea(attrs={"cols": 80, "rows": 5}), "history": ClearableTextarea(attrs={"maxlength": 2000, "rows": 5}), "discussion": ClearableTextarea(attrs={"maxlength": 2000, "rows": 5}), "report": ClearableTextarea(attrs={"maxlength": 2000, "rows": 5}), "condition": autocomplete.ModelSelect2Multiple( url="atlas:condition-autocomplete" ), "presentation": autocomplete.ModelSelect2Multiple( url="atlas:presentation-autocomplete" ), "subspecialty": CheckboxSelectMultiple(), "pathological_process": CheckboxSelectMultiple(), "previous_case": CaseSelect(), } def clean_cimar_uuid(self): cimar_uuid = self.cleaned_data["cimar_uuid"] if cimar_uuid: with CimarAPI(username=CIMAR_USERNAME, password=CIMAR_PASSWORD) as api: try: study = api.get_study_by_uuid(cimar_uuid) except NotFoundError: raise ValidationError("Study not found in CIMAR") return cimar_uuid 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 def save_m2m(): old_save_m2m() # instance.exams.clear() # for exam in self.cleaned_data["exams"]: # # We need the id here # instance.exams.add(exam.pk) self.save_m2m = save_m2m # Do we need to save all changes now? # if commit: instance.save() self.save_m2m() logger.debug(f"{self.collection=}") if self.collection is not None: logger.debug(f"{self.collection=}") case_no = self.collection.cases.count() + 1 logger.debug(f"{case_no=}") logger.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} ) return instance CaseDifferentialFormSet = inlineformset_factory( Case, Differential, exclude=[], can_delete=True, extra=1, max_num=10, widgets={ "condition": autocomplete.ModelSelect2(url="atlas:condition-autocomplete"), "text": Textarea(attrs={"maxlength": 1000, "rows": 3, "cols": 80}), }, ) class ResourceForm(ModelForm): class Meta: model = Resource exclude = ["author", "created_date"] widgets = { "description": Textarea(attrs={"cols": 80, "rows": 5}), "url": TextInput(attrs={"cols": 80, "rows": 1}), } class CaseResourceForm(ModelForm): def __init__(self, *args, user, **kwargs): super(CaseResourceForm, self).__init__(*args, **kwargs) if not user.groups.filter(name="atlas_editor").exists(): queryset = Resource.objects.filter(author__id=user.id) else: queryset = Resource.objects.all() self.fields["resource"] = ModelChoiceField( required=False, queryset=queryset, # widget=Select(verbose_name="Series", is_stacked=False), ) self.fields["pre_review"] = BooleanField(required=False) # class CaseSeriesForm(ModelForm): # # def __init__(self, *args, user, **kwargs): # super(CaseSeriesForm, self).__init__(*args, **kwargs) # # if not user.groups.filter(name="atlas_editor").exists(): # queryset = Series.objects.filter(author__id=user.id) # else: # queryset = Series.objects.all() # # # Optimize the queryset for the ModelChoiceField # queryset = queryset.select_related( # "modality" # ).prefetch_related( # "examination", "plane", "contrast", "images" # ) # # self.fields["series"] = ModelChoiceField( # required=False, # queryset=queryset, # # widget=Select(verbose_name="Series", is_stacked=False), # ) class CaseCollectionCaseForm(ModelForm): def __init__(self, *args, user, collection=None, **kwargs): super(CaseCollectionCaseForm, self).__init__(*args, **kwargs) queryset = get_cases_available_to_user(user, collection=collection) self.fields["case"] = ModelChoiceField( required=False, queryset=queryset, # widget=Select(verbose_name="Series", is_stacked=False), ) # SeriesFormSet = inlineformset_factory( # Case, # Series.case.through, # form=CaseSeriesForm, # exclude=[], # can_delete=True, # extra=0, # max_num=10, # ) CaseCollectionCaseFormSet = inlineformset_factory( CaseCollection, Case.casecollection_set.through, form=CaseCollectionCaseForm, exclude=["question_schema", "question_answers"], can_delete=True, extra=0, max_num=50, ) CaseResourceFormSet = inlineformset_factory( Case, Resource.case_set.through, form=CaseResourceForm, exclude=[], can_delete=True, extra=1, max_num=10, ) SeriesImageFormSet = inlineformset_factory( Series, SeriesImage, exclude=[ "dicom_tags_ohif", "replaced", "image_md5_hash", "image_blake3_hash", "is_dicom", ], can_delete=True, extra=0, max_num=2000, ) class JsonAnswerForm(Form): json_answer = JSONSchemaField( schema={}, # options = 'schema/options.json' options="schema/options.json", ) class Meta: fields = ["json_answer"] 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"].label = False class BaseReportAnswerForm(ModelForm): class Meta: fields = ["answer"] widgets = { # "normal": RadioSelect( # choices=[(True, 'Yes'), # (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"} ), } # help_texts = { # "answer": "Write your answer in here." # } def __init__(self, *args, casedetail, **kwargs): super(BaseReportAnswerForm, self).__init__(*args, **kwargs) self.fields["answer"].required = False class BaseQuestionAnswerForm(ModelForm): json_answer = JSONSchemaField(schema={}, options="schema/options.json") class Meta: fields = ["json_answer"] labels = {"json_answer": ""} # help_texts = { # "answer": "Write your answer in here." # } def __init__(self, *args, casedetail, **kwargs): super(BaseQuestionAnswerForm, self).__init__(*args, **kwargs) # self.fields["json_answer"].schema = casedetail.question_schema if casedetail.question_schema is not None: self.fields["json_answer"] = JSONSchemaField( schema=casedetail.question_schema, options="schema/options.json" ) self.fields["json_answer"].label = "" 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 fields = ["score", "feedback"] widgets = { # "normal": RadioSelect( # choices=[(True, 'Yes'), # (False, 'No')]) # "findings": TinyMCE(attrs={"cols": 80, "rows": 20}), # "mark_scheme": TinyMCE(attrs={"cols": 80, "rows": 30}), "feedback": Textarea(attrs={"cols": 80, "rows": 4}), } def __init__(self, *args, **kwargs): super(CidReportAnswerMarkForm, self).__init__(*args, **kwargs) self.fields["score"].required = False # class AddCollectionToCaseForm(Form): # # def __init__(self, *args, user, **kwargs): # super(AddCollectionToCaseForm, self).__init__(*args, **kwargs) # # if not user.groups.filter(name="atlas_editor").exists(): # 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) # # self.fields["casecollection"] = ModelMultipleChoiceField( # required=False, # queryset=queryset, # # widget=Select(verbose_name="Series", is_stacked=False), # ) # # def save(self, *args, **kwargs): # print("SAVE", args, kwargs) # 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']) # # print(f"{instance=}") # # return instance # # def clean_casecollection(self): # collection_set = self.cleaned_data["casecollection"] # # print(f"{collection_set=}") # # for col in collection_set: # if col not in self.valid_collections: # raise ValidationError("Invalid collection") # # return collection_set class AddCollectionToCaseForm(Form): def __init__(self, *args, user, **kwargs): super(AddCollectionToCaseForm, self).__init__(*args, **kwargs) if not user.groups.filter(name="atlas_editor").exists(): queryset = CaseCollection.objects.filter(author__id=user.id) else: queryset = CaseCollection.objects.all() self.valid_collections = queryset self.fields["casecollection"] = ModelMultipleChoiceField( required=False, queryset=queryset, # widget=Select(verbose_name="Series", is_stacked=False), ) def save(self, *args, **kwargs): 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"]) return instance def clean_casecollection(self): collection_set = self.cleaned_data["casecollection"] for col in collection_set: if col not in self.valid_collections: raise ValidationError("Invalid collection") return collection_set class SelfReviewForm(ModelForm): class Meta: model = SelfReview fields = ["user_exam", "case", "comments", "findings", "interpretation"] widgets = {"user_exam": HiddenInput(), "case": HiddenInput()} def __init__(self, *args, **kwargs): if "user_exam_id" in kwargs: user_exam_id = kwargs.pop("user_exam_id") if "case_id" in kwargs: case_id = kwargs.pop("case_id") ModelForm.__init__(self, *args, **kwargs) super(SelfReviewForm, self).__init__(*args, **kwargs) class UncategorisedDicomForm(ModelForm): class Meta: model = UncategorisedDicom fields = ["image"] # , "user"] # UncategorisedDicomFormset = formset_factory(UncategorisedDicomForm) # class ConditionAutocompleteFormModel(forms.ModelForm): # class Meta: # model = Condition # fields = ["name"] # widgets = { # 'members': widgets.Autocomplete( # name='members', # options=dict(multiselect=True, model=Condition) # ) # } 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) ), ) 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 SvelteJSONEditorWidgetOverride(SvelteJSONEditorWidget): template_name = "atlas/svelte_jsoneditor_widget_override.html" class CaseQuestionForm(ModelForm): class Meta: model = CaseDetail fields = ["question_schema", "question_answers"] # , "user"] widgets = { "question_schema": SvelteJSONEditorWidgetOverride(), "question_answers": SvelteJSONEditorWidgetOverride(), } class CaseDetailForm(ModelForm): class Meta: model = CaseDetail fields = ["redact_history", "override_history"] def __init__(self, *args, case_history: str = None, **kwargs): """ case_history: optional explicit history text to show (falls back to instance.case.history) """ super().__init__(*args, **kwargs) # Determine original history text (safe-escaped for HTML) if case_history is None: try: case_history = self.instance.case.history if self.instance and getattr(self.instance, "case", None) else "" except Exception: case_history = "" case_history_html = escape(case_history or "No history available.") # Ensure override_history has a stable id we can reference from the inline script override_id = self.fields["override_history"].widget.attrs.get("id", "id_override_history") self.fields["override_history"].widget.attrs["id"] = override_id # Optionally make it a textarea style appearance if not already self.fields["override_history"].widget.attrs.setdefault("rows", 6) self.fields["override_history"].widget.attrs.setdefault("class", "form-control") # Build crispy helper/layout embedding the original history and buttons tied to override_history self.helper = FormHelper() self.helper.form_tag = False # Inline HTML block with buttons and a script that copies/clears the override field. # The script references the explicit override_id above. history_block = f"""
Show original case history
{case_history_html}
Use the buttons below to copy this into the override field or clear the override.
""" # Compose layout: history block then the override field and redact checkbox self.helper.layout = Layout( HTML(history_block), Field("override_history"), Field("redact_history") ) class QuestionSchemaForm(ModelForm): class Meta: model = QuestionSchema fields = ["name", "description", "schema"] widgets = { "schema": SvelteJSONEditorWidgetOverride(), } # class ModelPriorCaseForm(ModelForm): # class Meta: # model = CasePrior # fields = ["case_detail", "prior_case", "relation_text", "prior_visibility"] class PriorCaseForm(Form): relation = CharField(max_length=255, required=True) prior_visibility = CharField(max_length=2, required=True) class CaseSeriesForm(forms.ModelForm): def __init__(self, *args, queryset, user=None, is_atlas_editor=False, **kwargs): super().__init__(*args, **kwargs) self.fields["series"].queryset = queryset class Meta: 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") super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_id = "id-case-form" self.helper.form_class = "case-form" self.helper.form_method = "post" 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 super().__init__(*args, **kwargs) def get_form_kwargs(self, index): kwargs = super().get_form_kwargs(index) kwargs["queryset"] = self.series_queryset return kwargs SeriesFormSet = inlineformset_factory( Case, SeriesDetail, form=CaseSeriesForm, formset=BaseSeriesFormSet, extra=1, can_delete=True, ) class CaseDisplaySetForm(forms.ModelForm): class Meta: model = CaseDisplaySet fields = [ "name", "description", "findings", "structures", "conditions", ] widgets = { "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" ), "structures": autocomplete.ModelSelect2Multiple( url="atlas:structure-autocomplete" ), "conditions": autocomplete.ModelSelect2Multiple( url="atlas:condition-autocomplete" ), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Optionally, you can filter queryset for findings/structures/conditions here if needed class CaseCollectionUpdateCaseForm(forms.ModelForm): def __init__(self, *args, **kwargs): self.user = kwargs.pop("user") self.collection = kwargs.pop("collection", None) super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_id = "id-case-form" self.helper.form_class = "case-form" self.helper.form_method = "post" 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