Files
penracourses/atlas/forms.py
T
Ross 12b7fa6629 Refactor CaseDetail references to Casedetail across templates and views
- Updated all instances of 'case_detail' to 'casedetail' in templates to maintain consistency.
- Adjusted view functions to use 'casedetail' instead of 'case_detail' for better clarity.
- Renamed the field 'case_detail' to 'casedetail' in the CasePrior model to align with the new naming convention.
- Ensured that all related logic and references in forms and data handling reflect this change.
2025-11-17 10:34:27 +00:00

1254 lines
41 KiB
Python
Executable File

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 <form> 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
# <form> 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 <script src="/admin/jsi18n"></script>
# 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 <script src="/admin/jsi18n"></script>
# 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"""
<details class="mb-3">
<summary class="h6" style="cursor:pointer;"><i class="bi bi-info-circle"></i> Show original case history</summary>
<div class="mt-2 p-2 bg-dark text-light border rounded">
<label class="form-label fw-bold">Original history (read-only)</label>
<div id="original-history" style="white-space: pre-wrap;">{case_history_html}</div>
<div class="form-text text-secondary">Use the buttons below to copy this into the override field or clear the override.</div>
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-light" id="use-original-history">Copy original into override</button>
<button type="button" class="btn btn-sm btn-outline-danger ms-2" id="clear-override-history">Clear override</button>
</div>
</div>
</details>
<script>
document.addEventListener('DOMContentLoaded', function() {{
var copyBtn = document.getElementById('use-original-history');
var clearBtn = document.getElementById('clear-override-history');
var original = '{case_history_html.replace("'", "\\'").replace("\\n", "\\\\n")}';
var overrideField = document.getElementById('{override_id}');
function setOverride(val) {{
if(!overrideField) return; overrideField.value = val; overrideField.dispatchEvent(new Event('input',{{bubbles:true}})); }}
if(copyBtn) copyBtn.addEventListener('click', function(e){{ setOverride(original); }});
if(clearBtn) clearBtn.addEventListener('click', function(e){{ setOverride(''); }});
}});
</script>
"""
# 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