Refactor collection case navigation and question handling
- Updated links in collection_case_priors.html to point to the new collection_case_questions view. - Modified collection_case_view_take.html to display case history using the new method. - Changed collection_detail.html to include links to the new collection_case_questions view. - Added new URL pattern for collection_case_questions in urls.py. - Refactored collection_case_details view to handle question-related logic and rendering. - Introduced new CaseQuestionForm for handling question submissions. - Created new migration to add override_history and redact_history fields to CaseDetail model. - Added new templates for collection_case_details.html and collection_case_questions.html to manage case questions. - Enhanced JavaScript functionality for dynamic question management in collection_case_questions.html. - Updated useranswer model fields in shorts app to improve feedback and scoring.
This commit is contained in:
+76
-7
@@ -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"""
|
||||
<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:
|
||||
|
||||
@@ -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.'),
|
||||
),
|
||||
]
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends 'atlas/exams.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div>
|
||||
{% if previous %}
|
||||
<a href="{% url 'atlas:collection_case_questions' collection.id previous.id %}">Previous question</a>
|
||||
{% endif %}
|
||||
Viewing question as part of collection: <a href="{% url 'atlas:collection_detail' collection.id %}">{{collection.name}}</a> [{{case_number|add:1}}/{{collection_length}}]
|
||||
{% if next %}
|
||||
<a href="{% url 'atlas:collection_case_questions' collection.id next.id %}">Next question</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2>Case: <a href="{% url 'atlas:case_detail' case_detail.case.pk %}">{{case_detail.case.title}}</a></h2>
|
||||
|
||||
<p>This page allows you to configure how the case is displayed as part of the collection.</p>
|
||||
|
||||
|
||||
|
||||
<form method="POST" class="post-form">
|
||||
{% csrf_token %}
|
||||
{% crispy form form.helper %}
|
||||
<button type="submit" value="answer" name="submit" class="btn btn-primary">Save Changes</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
</script>
|
||||
<style></style>
|
||||
{% endblock %}
|
||||
@@ -7,11 +7,11 @@
|
||||
<div>
|
||||
|
||||
{% if previous %}
|
||||
<a href="{% url 'atlas:collection_case_details' collection.id previous.id %}">Previous question</a>
|
||||
<a href="{% url 'atlas:collection_case_questions' collection.id previous.id %}">Previous question</a>
|
||||
{% endif %}
|
||||
Viewing question as part of collection: <a href="{% url 'atlas:collection_detail' collection.id %}">{{collection.name}}</a> [{{case_number|add:1}}/{{collection_length}}]
|
||||
{% if next %}
|
||||
<a href="{% url 'atlas:collection_case_details' collection.id next.id %}">Next question</a>
|
||||
<a href="{% url 'atlas:collection_case_questions' collection.id next.id %}">Next question</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
+2
-2
@@ -7,11 +7,11 @@
|
||||
<div>
|
||||
|
||||
{% if previous %}
|
||||
<a href="{% url 'atlas:collection_case_details' collection.id previous.id %}">Previous question</a>
|
||||
<a href="{% url 'atlas:collection_case_questions' collection.id previous.id %}">Previous question</a>
|
||||
{% endif %}
|
||||
Viewing question as part of collection: <a href="{% url 'atlas:collection_detail' collection.id %}">{{collection.name}}</a> [{{case_number|add:1}}/{{collection_length}}]
|
||||
{% if next %}
|
||||
<a href="{% url 'atlas:collection_case_details' collection.id next.id %}">Next question</a>
|
||||
<a href="{% url 'atlas:collection_case_questions' collection.id next.id %}">Next question</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
Description: {{case.description}}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if show_history and case.history %}
|
||||
{% if show_history %}
|
||||
<div>
|
||||
History: {{case.history|linebreaks}}
|
||||
History: {{case_detail.get_history_pre|linebreaks}}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -44,14 +44,16 @@
|
||||
{{casedetail.case.title}}
|
||||
|
||||
|
||||
(<a href="{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk casedetail.case.pk %}"><i class="bi bi-display" title="Setup default display"></i></a>
|
||||
(<a href="{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk casedetail.case.pk %}"><i class="bi bi-display" title="Setup default display"></i></a>)
|
||||
(<a href="{% url 'atlas:collection_case_details' casedetail.collection.pk casedetail.case.pk %}"><i class="bi bi-info-square" title="Case details"></i></a>)
|
||||
|
||||
{% if casedetail.default_viewerstate %}
|
||||
<i class="bi bi-check text-success" title="This case has a default viewerstate defined"></i>
|
||||
{% endif %}
|
||||
)
|
||||
|
||||
{% if collection.collection_type == "QUE" %}
|
||||
(<a href='{% url "atlas:collection_case_details" casedetail.collection.pk casedetail.case.pk %}'>
|
||||
(<a href='{% url "atlas:collection_case_questions" casedetail.collection.pk casedetail.case.pk %}'>
|
||||
{% if casedetail.question_schema %}
|
||||
<i class="bi bi-question-square text-success" title="This case has questions defined."></i>
|
||||
{% else %}
|
||||
|
||||
@@ -161,6 +161,11 @@ urlpatterns = [
|
||||
views.collection_case_details,
|
||||
name="collection_case_details",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_id>/questions",
|
||||
views.collection_case_questions,
|
||||
name="collection_case_questions",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_id>/priors",
|
||||
views.collection_case_priors,
|
||||
|
||||
+40
-8
@@ -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,
|
||||
|
||||
@@ -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)]),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user