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, Presentation, PathologicalProcess, 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" ), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Crispy helper to render a submit button consistently try: self.helper = FormHelper() self.helper.form_method = "post" self.helper.form_tag = True self.helper.add_input(Submit("submit", "Submit")) except Exception: # If crispy not available or something goes wrong, don't break form pass 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