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 ) 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, SeriesImage, SeriesFinding, Condition, Structure, Subspecialty, UncategorisedDicom, UserReportAnswer, ) 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 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 import logging from generic.forms import ExamAuthorFormMixin, ExamGroupsFormMixin 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 CaseSelect(Select): template_name = "atlas/case_select_widget.html" def create_option(self, *args, **kwargs): option = super().create_option(*args, **kwargs) print(option) 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" ) # 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(CaseCollectionForm, self).__init__(*args, **kwargs) 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() 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"] 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}), } class CaseForm(ModelForm): # exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False)) 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 # 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") logging.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", "condition", "pathological_process", "report", "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}), "description": Textarea(attrs={"cols": 80, "rows": 5}), "history": Textarea(attrs={"cols": 80, "rows": 5}), "discussion": Textarea(attrs={"cols": 80, "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() logging.debug(f"{self.collection=}") if self.collection is not None: logging.debug(f"{self.collection=}") case_no = self.collection.cases.count() + 1 logging.debug(f"{case_no=}") 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}) 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"] 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() 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, 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' ) class Meta: fields = ["json_answer"] labels = {"json_answer": ""} # help_texts = { # "answer": "Write your answer in here." # } def __init__(self, *args, case_detail, **kwargs): super(BaseQuestionAnswerForm, self).__init__(*args, **kwargs) #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"].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) # print("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) print("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): 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 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 CaseDetailForm(ModelForm): class Meta: model = CaseDetail fields = ["question_schema", "question_answers"]#, "user"] widgets = { "question_schema": SvelteJSONEditorWidgetOverride(), "question_answers": SvelteJSONEditorWidgetOverride(), } 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) #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 # super(PriorCaseForm, self).__init__(*args, **kwargs) # prior_cases = self.case_detail.case.get_all_prior_cases() # if not prior_cases: # prior_cases = Case.objects.none() # self.fields["case"] = ChoiceField( # required=False, # # widget=Select(verbose_name="Series", is_stacked=False), # )