diff --git a/atlas/forms.py b/atlas/forms.py index e9eb1af2..5f23e2e0 100755 --- a/atlas/forms.py +++ b/atlas/forms.py @@ -15,6 +15,7 @@ from django.forms import ( 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 @@ -67,7 +68,8 @@ from autocomplete import ( register as autocomplete_register, ) -import logging +from loguru import logger + from generic.forms import ( ExamAuthorFormMixin, @@ -471,7 +473,7 @@ class CaseForm(ModelForm): self.collection = None if kwargs["initial"] is not None and "exams" in kwargs["initial"]: collections = kwargs["initial"].pop("exams") - logging.debug(collections) + logger.debug(collections) self.collection = get_object_or_404(CaseCollection, pk=collections[0]) @@ -606,13 +608,13 @@ class CaseForm(ModelForm): instance.save() self.save_m2m() - logging.debug(f"{self.collection=}") + logger.debug(f"{self.collection=}") if self.collection is not None: - logging.debug(f"{self.collection=}") + logger.debug(f"{self.collection=}") case_no = self.collection.cases.count() + 1 - logging.debug(f"{case_no=}") - logging.debug(f"{instance=}") + logger.debug(f"{case_no=}") + logger.debug(f"{instance=}") # Create through model # cd = CaseDetail(case=instance, collection=exam, sort_order=case_no) @@ -1010,7 +1012,7 @@ class SvelteJSONEditorWidgetOverride(SvelteJSONEditorWidget): template_name = "atlas/svelte_jsoneditor_widget_override.html" -class CaseDetailForm(ModelForm): +class CaseQuestionForm(ModelForm): class Meta: model = CaseDetail fields = ["question_schema", "question_answers"] # , "user"] @@ -1020,6 +1022,73 @@ class CaseDetailForm(ModelForm): "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""" +
+ Show original case history +
+ +
{case_history_html}
+
Use the buttons below to copy this into the override field or clear the override.
+
+ + +
+
+
+ + """ + + # 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: diff --git a/atlas/migrations/0075_casedetail_override_history_and_more.py b/atlas/migrations/0075_casedetail_override_history_and_more.py new file mode 100644 index 00000000..b65908cc --- /dev/null +++ b/atlas/migrations/0075_casedetail_override_history_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.4 on 2025-09-15 09:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('atlas', '0074_alter_seriesimage_image_md5_hash'), + ] + + operations = [ + migrations.AddField( + model_name='casedetail', + name='override_history', + field=models.TextField(blank=True, help_text='This will override the case history for the purpose of the exam/collection.', null=True), + ), + migrations.AddField( + model_name='casedetail', + name='redact_history', + field=models.BooleanField(default=False, help_text='Set to true if the history should be redacted whilst taking the case.'), + ), + ] diff --git a/atlas/models.py b/atlas/models.py index 534bb7d1..dbd92e38 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -1190,6 +1190,16 @@ class CaseDetail(models.Model): sort_order = models.IntegerField(default=1000) + redact_history = models.BooleanField( + default=False, + help_text="Set to true if the history should be redacted whilst taking the case." + ) + + override_history = models.TextField( + null=True, blank=True, + help_text="This will override the case history for the purpose of the exam/collection." + ) + class Meta: ordering = ("sort_order",) @@ -1213,6 +1223,15 @@ class CaseDetail(models.Model): def default_viewerstate_string(self): return json.dumps(self.default_viewerstate) if self.default_viewerstate else "{}" + def get_history_pre(self): + if self.collection.show_history_pre: + if self.redact_history: + return "[Redacted]" + if self.override_history and self.override_history != "": + return self.override_history + return self.case.history + return "" + class CasePrior(models.Model): case_detail = models.ForeignKey(CaseDetail, on_delete=models.CASCADE) prior_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name="prior_case") diff --git a/atlas/templates/atlas/collection_case_details.html b/atlas/templates/atlas/collection_case_details.html new file mode 100644 index 00000000..cdbb0bef --- /dev/null +++ b/atlas/templates/atlas/collection_case_details.html @@ -0,0 +1,34 @@ +{% extends 'atlas/exams.html' %} +{% load crispy_forms_tags %} + +{% block content %} + +
+ {% if previous %} + Previous question + {% endif %} + Viewing question as part of collection: {{collection.name}} [{{case_number|add:1}}/{{collection_length}}] + {% if next %} + Next question + {% endif %} +
+ +

Case: {{case_detail.case.title}}

+ +

This page allows you to configure how the case is displayed as part of the collection.

+ + + +
+ {% csrf_token %} + {% crispy form form.helper %} + +
+ +{% endblock %} + +{% block js %} + + +{% endblock %} \ No newline at end of file diff --git a/atlas/templates/atlas/collection_case_priors.html b/atlas/templates/atlas/collection_case_priors.html index 9b00823b..fde90ee0 100644 --- a/atlas/templates/atlas/collection_case_priors.html +++ b/atlas/templates/atlas/collection_case_priors.html @@ -7,11 +7,11 @@
{% if previous %} - Previous question + Previous question {% endif %} Viewing question as part of collection: {{collection.name}} [{{case_number|add:1}}/{{collection_length}}] {% if next %} - Next question + Next question {% endif %}
diff --git a/atlas/templates/atlas/collection_case_detail.html b/atlas/templates/atlas/collection_case_questions.html similarity index 99% rename from atlas/templates/atlas/collection_case_detail.html rename to atlas/templates/atlas/collection_case_questions.html index d17c5113..4b3f2f00 100644 --- a/atlas/templates/atlas/collection_case_detail.html +++ b/atlas/templates/atlas/collection_case_questions.html @@ -7,11 +7,11 @@
{% if previous %} - Previous question + Previous question {% endif %} Viewing question as part of collection: {{collection.name}} [{{case_number|add:1}}/{{collection_length}}] {% if next %} - Next question + Next question {% endif %}
diff --git a/atlas/templates/atlas/collection_case_view_take.html b/atlas/templates/atlas/collection_case_view_take.html index 694f9da8..62d1220d 100644 --- a/atlas/templates/atlas/collection_case_view_take.html +++ b/atlas/templates/atlas/collection_case_view_take.html @@ -40,9 +40,9 @@ Description: {{case.description}} {% endif %} - {% if show_history and case.history %} + {% if show_history %}
- History: {{case.history|linebreaks}} + History: {{case_detail.get_history_pre|linebreaks}}
{% endif %} diff --git a/atlas/templates/atlas/collection_detail.html b/atlas/templates/atlas/collection_detail.html index 1043a25c..f5e022e5 100644 --- a/atlas/templates/atlas/collection_detail.html +++ b/atlas/templates/atlas/collection_detail.html @@ -44,14 +44,16 @@ {{casedetail.case.title}} - ( + () + () + {% if casedetail.default_viewerstate %} {% endif %} ) {% if collection.collection_type == "QUE" %} - ( + ( {% if casedetail.question_schema %} {% else %} diff --git a/atlas/urls.py b/atlas/urls.py index 5c09cb2e..0ed711e2 100755 --- a/atlas/urls.py +++ b/atlas/urls.py @@ -161,6 +161,11 @@ urlpatterns = [ views.collection_case_details, name="collection_case_details", ), + path( + "collection//case//questions", + views.collection_case_questions, + name="collection_case_questions", + ), path( "collection//case//priors", views.collection_case_priors, diff --git a/atlas/views.py b/atlas/views.py index 42fff86b..6d6a91f1 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -51,6 +51,7 @@ from .forms import ( CaseCollectionForm, CaseCollectionUpdateCaseForm, CaseDetailForm, + CaseQuestionForm, CaseDisplaySetForm, CaseForm, CaseResourceFormSet, @@ -2227,7 +2228,7 @@ def collection_case_priors(request, exam_id, case_id): @user_is_collection_author_or_atlas_editor -def collection_case_details(request, exam_id, case_id): +def collection_case_questions(request, exam_id, case_id): case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id) collection = case_detail.collection @@ -2244,10 +2245,10 @@ def collection_case_details(request, exam_id, case_id): example_form = JsonAnswerForm( request.POST, question_schema=case_detail.question_schema ) - form = CaseDetailForm(instance=case_detail) + form = CaseQuestionForm(instance=case_detail) # Called if the user saves the main form elif request.POST.get("submit") == "save": - form = CaseDetailForm(request.POST, instance=case_detail) + form = CaseQuestionForm(request.POST, instance=case_detail) if form.is_valid(): form.save() # Add any additional logic or redirection here @@ -2277,11 +2278,11 @@ def collection_case_details(request, exam_id, case_id): post_data, question_schema=case_detail.question_schema ) - form = CaseDetailForm(instance=case_detail) + form = CaseQuestionForm(instance=case_detail) # This shouldn't happen else: assert False - form = CaseDetailForm(request.POST, instance=case_detail) + form = CaseQuestionForm(request.POST, instance=case_detail) else: post_data = request.POST.copy() @@ -2290,7 +2291,7 @@ def collection_case_details(request, exam_id, case_id): post_data, question_schema=case_detail.question_schema ) - form = CaseDetailForm(instance=case_detail) + form = CaseQuestionForm(instance=case_detail) pass # post_data = None @@ -2313,7 +2314,7 @@ def collection_case_details(request, exam_id, case_id): # case_detail.question_answers = answers # example_form = JsonAnswerForm(post_data, question_schema=case_detail.question_schema) - # form = CaseDetailForm(instance=case_detail) + # form = CaseQuestionForm(instance=case_detail) # blank_form = JsonAnswerForm(question_schema=case_detail.question_schema) case_number, case_count = collection.get_index_of_case( @@ -2325,7 +2326,7 @@ def collection_case_details(request, exam_id, case_id): return render( request, - "atlas/collection_case_detail.html", + "atlas/collection_case_questions.html", { "case_detail": case_detail, "form": form, @@ -2339,6 +2340,36 @@ def collection_case_details(request, exam_id, case_id): }, ) +@user_is_collection_author_or_atlas_editor +def collection_case_details(request, exam_id, case_id): + case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id) + + collection = case_detail.collection + + form = CaseDetailForm(instance=case_detail) + + case_number, case_count = collection.get_index_of_case( + case_detail.case, case_count=True + ) + + previous = collection.get_previous_case(case_detail.case) + next = collection.get_next_case(case_detail.case) + + return render( + request, + "atlas/collection_case_details.html", + { + "case_detail": case_detail, + "form": form, + "collection": collection, + "case": case_detail.case, + "previous": previous, + "next": next, + "collection_length": case_count, + "case_number": case_number, + }, + ) + @user_is_collection_author_or_atlas_editor def collection_mark_overview(request, pk): @@ -2768,6 +2799,7 @@ def collection_case_view_take( "form": form, "collection": collection, "case": case, + "case_detail": case_detail, "series_list": series_list, "series_to_load": series_to_load, "case_number": case_number, diff --git a/shorts/migrations/0010_alter_useranswer_candidate_feedback_and_more.py b/shorts/migrations/0010_alter_useranswer_candidate_feedback_and_more.py new file mode 100644 index 00000000..8f3ac28b --- /dev/null +++ b/shorts/migrations/0010_alter_useranswer_candidate_feedback_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.4 on 2025-09-15 09:31 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shorts', '0009_questionfinding_annotation_json_3d_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='useranswer', + name='candidate_feedback', + field=models.TextField(blank=True, help_text='Feedback for the candidate, this is optional but WILL be shown to the candidate.', null=True), + ), + migrations.AlterField( + model_name='useranswer', + name='score', + field=models.IntegerField(blank=True, default=0, help_text='Score for the answer. If null then the answer is unmarked. This should be number 0-5.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)]), + ), + ]