6575c50507
Co-authored-by: Copilot <copilot@github.com>
1504 lines
50 KiB
Python
Executable File
1504 lines
50 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,
|
|
Presentation,
|
|
PathologicalProcess,
|
|
Procedure,
|
|
UncategorisedDicom,
|
|
UserReportAnswer,
|
|
CaseDisplaySet,
|
|
CaseReviewMessage,
|
|
)
|
|
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, Button
|
|
from django.urls import reverse
|
|
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 MoveSelectedSeriesForm(Form):
|
|
destination_case = ModelChoiceField(
|
|
queryset=Case.objects.none(),
|
|
widget=HiddenInput(),
|
|
label="Move selected series to case",
|
|
required=True,
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop("user", None)
|
|
source_case = kwargs.pop("source_case", None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
available_cases = get_cases_available_to_user(user)
|
|
if source_case is not None:
|
|
available_cases = available_cases.exclude(pk=source_case.pk)
|
|
|
|
self.fields["destination_case"].queryset = available_cases.order_by("pk")
|
|
|
|
|
|
class ConditionForm(ModelForm):
|
|
class Meta:
|
|
model = Condition
|
|
exclude = ["rcr_curriculum"]
|
|
widgets = {
|
|
# Use an AJAX/autocomplete single-select for canonical so users
|
|
# can search for existing canonical Conditions by name.
|
|
"canonical": autocomplete.ModelSelect2(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
|
|
# Primary submit
|
|
self.helper.add_input(Submit("submit", "Submit"))
|
|
# Cancel as a button that redirects back to the condition list
|
|
try:
|
|
cancel_url = reverse("atlas:condition_view")
|
|
except Exception:
|
|
cancel_url = "#"
|
|
self.helper.add_input(
|
|
Button(
|
|
"cancel",
|
|
"Cancel",
|
|
css_class="btn btn-secondary ms-2",
|
|
onclick=f"window.location='{cancel_url}'",
|
|
)
|
|
)
|
|
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",
|
|
"show_case_link_post",
|
|
"show_findings_post",
|
|
"show_displaysets_post",
|
|
"show_differentials_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",
|
|
"case_query_messaging_enabled",
|
|
"show_results_sharing_information",
|
|
"viewer_mode",
|
|
"question_time_limit",
|
|
"answer_entry_grace_period",
|
|
"prerequisites",
|
|
Fieldset(
|
|
"Custom Start Screen",
|
|
"start_screen_content",
|
|
"start_screen_resources",
|
|
),
|
|
Fieldset(
|
|
"Custom End Overview",
|
|
"end_overview_content",
|
|
"end_overview_resources",
|
|
),
|
|
),
|
|
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)
|
|
# If editing an existing NormalCase, prepopulate age_value/age_unit
|
|
try:
|
|
instance = kwargs.get('instance')
|
|
except Exception:
|
|
instance = None
|
|
if instance is None:
|
|
# also support self.instance when ModelForm sets it
|
|
instance = getattr(self, 'instance', None)
|
|
if instance is not None and getattr(instance, 'age_days', None) is not None:
|
|
try:
|
|
val, unit = NormalCase.days_to_value_unit(instance.age_days)
|
|
if val is not None and unit is not None:
|
|
self.fields['age_value'].initial = val
|
|
self.fields['age_unit'].initial = unit
|
|
except Exception:
|
|
pass
|
|
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 button rendered in modal footer; avoid duplicate button here
|
|
)
|
|
|
|
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 = []
|
|
# Use the canonical FK in forms (if users want to mark this Finding
|
|
# as an alias of another). The old `synonym` M2M has been removed.
|
|
widgets = {
|
|
"canonical": autocomplete.ModelSelect2(
|
|
url="atlas:finding-autocomplete"
|
|
),
|
|
}
|
|
|
|
|
|
class StructureForm(ModelForm):
|
|
class Meta:
|
|
model = Structure
|
|
exclude = []
|
|
widgets = {
|
|
"canonical": autocomplete.ModelSelect2(
|
|
url="atlas:structure-autocomplete"
|
|
),
|
|
}
|
|
|
|
|
|
class SubspecialtyForm(ModelForm):
|
|
class Meta:
|
|
model = Subspecialty
|
|
exclude = []
|
|
|
|
|
|
class PresentationForm(ModelForm):
|
|
class Meta:
|
|
model = Presentation
|
|
exclude = []
|
|
|
|
|
|
class PathologicalProcessForm(ModelForm):
|
|
class Meta:
|
|
model = PathologicalProcess
|
|
exclude = []
|
|
|
|
|
|
class ProcedureForm(ModelForm):
|
|
class Meta:
|
|
model = Procedure
|
|
exclude = []
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
try:
|
|
self.helper = FormHelper()
|
|
self.helper.form_method = "post"
|
|
# Prevent crispy from rendering its own <form> tag so our
|
|
# template-level form wrapper contains the submit buttons.
|
|
self.helper.form_tag = False
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
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",
|
|
"procedures",
|
|
"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",
|
|
"procedures",
|
|
"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(),
|
|
"procedures": autocomplete.ModelSelect2Multiple(
|
|
url="atlas:procedure-autocomplete"
|
|
),
|
|
}
|
|
|
|
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):
|
|
SCORE_CHOICES = [
|
|
(1, "1 - Poor"),
|
|
(2, "2 - Limited"),
|
|
(3, "3 - Adequate"),
|
|
(4, "4 - Good"),
|
|
(5, "5 - Excellent"),
|
|
]
|
|
|
|
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)
|
|
|
|
# Render scoring fields as explicit 1-5 selector choices for cleaner UX.
|
|
self.fields["findings"] = forms.TypedChoiceField(
|
|
choices=self.SCORE_CHOICES,
|
|
coerce=int,
|
|
empty_value=None,
|
|
required=False,
|
|
widget=RadioSelect,
|
|
label="Findings",
|
|
help_text="How well did you identify key findings?",
|
|
)
|
|
self.fields["interpretation"] = forms.TypedChoiceField(
|
|
choices=self.SCORE_CHOICES,
|
|
coerce=int,
|
|
empty_value=None,
|
|
required=False,
|
|
widget=RadioSelect,
|
|
label="Interpretation",
|
|
help_text="How strong was your overall interpretation?",
|
|
)
|
|
|
|
self.fields["comments"].widget.attrs.update(
|
|
{
|
|
"rows": 5,
|
|
"placeholder": "What went well? What would you do differently next time?",
|
|
}
|
|
)
|
|
|
|
self.helper = FormHelper()
|
|
self.helper.form_method = "post"
|
|
self.helper.form_tag = False
|
|
self.helper.layout = Layout(
|
|
Field("user_exam"),
|
|
Field("case"),
|
|
Field("comments"),
|
|
Field("findings"),
|
|
Field("interpretation"),
|
|
)
|
|
|
|
|
|
class CaseReviewMessageForm(ModelForm):
|
|
class Meta:
|
|
model = CaseReviewMessage
|
|
fields = ["message", "requires_acknowledgement"]
|
|
|
|
def __init__(self, *args, allow_ack=False, message_placeholder=None, **kwargs):
|
|
super(CaseReviewMessageForm, self).__init__(*args, **kwargs)
|
|
|
|
self.fields["message"].widget.attrs.update(
|
|
{
|
|
"rows": 3,
|
|
"placeholder": message_placeholder or "Write a message...",
|
|
"class": "form-control",
|
|
}
|
|
)
|
|
|
|
self.fields["requires_acknowledgement"].widget.attrs.update(
|
|
{
|
|
"class": "form-check-input",
|
|
}
|
|
)
|
|
|
|
if not allow_ack:
|
|
self.fields["requires_acknowledgement"].widget = HiddenInput()
|
|
self.fields["requires_acknowledgement"].required = False
|
|
|
|
self.helper = FormHelper()
|
|
self.helper.form_method = "post"
|
|
self.helper.form_tag = False
|
|
|
|
|
|
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"]
|
|
|
|
@classmethod
|
|
def get_items_from_keys(cls, keys, context_obj=None):
|
|
"""Return items for given keys, ignoring empty strings.
|
|
|
|
The autocomplete frontend sometimes sends empty string keys which
|
|
causes the ORM to raise ValueError when filtering numeric PK fields.
|
|
Filter out falsy/blank keys before querying. If no valid keys remain
|
|
return an empty queryset.
|
|
"""
|
|
# normalize and remove empty/blank values
|
|
clean_keys = [k for k in keys if k is not None and str(k).strip() != ""]
|
|
if not clean_keys:
|
|
return cls.model.objects.none()
|
|
|
|
# try to convert numeric keys to ints where possible
|
|
parsed_keys = []
|
|
for k in clean_keys:
|
|
try:
|
|
parsed_keys.append(int(k))
|
|
except Exception:
|
|
parsed_keys.append(k)
|
|
|
|
qs = cls.model.objects.filter(pk__in=parsed_keys)
|
|
|
|
# Return a list of dicts matching the autocomplete core expectations
|
|
# (items subscriptable by keys like 'key' / 'label').
|
|
return [
|
|
{"key": obj.pk, "label": getattr(obj, "name", str(obj)), "value": str(obj)}
|
|
for obj in qs
|
|
]
|
|
|
|
|
|
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",
|
|
"question_time_limit_override",
|
|
"answer_entry_grace_period_override",
|
|
]
|
|
|
|
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")
|
|
|
|
if "question_time_limit_override" in self.fields:
|
|
self.fields["question_time_limit_override"].help_text = (
|
|
"Leave blank to use the collection default answer time limit."
|
|
)
|
|
|
|
if "answer_entry_grace_period_override" in self.fields:
|
|
self.fields["answer_entry_grace_period_override"].help_text = (
|
|
"Leave blank to use the collection default answer-only grace period."
|
|
)
|
|
|
|
# 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"),
|
|
Fieldset(
|
|
"Case Timing Overrides",
|
|
Field("question_time_limit_override"),
|
|
Field("answer_entry_grace_period_override"),
|
|
),
|
|
)
|
|
|
|
|
|
|
|
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
|