12b7fa6629
- 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.
1254 lines
41 KiB
Python
Executable File
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
|