From 682e8160b54bd51089cf4b3e498e2ab5254046af Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 6 Oct 2025 13:51:55 +0100 Subject: [PATCH] Refactor user collections view to categorize collections into available, in-progress, and finished; update templates for improved display of collection statuses and question rendering. --- atlas/models.py | 35 +++++++++-- atlas/templates/atlas/case_display_block.html | 7 +++ .../atlas/collection_case_view_take.html | 30 +-------- atlas/templates/atlas/collection_viva.html | 38 +++++++++++ .../collection_question_answer_block.html | 27 ++++++++ .../partials/collection_question_block.html | 63 +++++++++++++++++++ atlas/templates/atlas/user_collections.html | 55 +++++++++++++--- atlas/views.py | 42 ++++++++++++- 8 files changed, 250 insertions(+), 47 deletions(-) create mode 100644 atlas/templates/atlas/partials/collection_question_answer_block.html create mode 100644 atlas/templates/atlas/partials/collection_question_block.html diff --git a/atlas/models.py b/atlas/models.py index 4767cad3..afb57d18 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -1391,18 +1391,25 @@ class BaseReportAnswer(models.Model): raise ValueError("No cid or user specified") def get_correct_json_answers(self): + logger.debug(f"Getting correct json answers for question {self.question.pk}") if self.question.question_schema is None or not "properties" in self.question.question_schema: return [] answers = [] for name, value in self.question.question_schema["properties"].items(): + logger.debug(f"Processing question property '{name}' with schema {value}") - try: - user_answer = self.json_answer[name] - except KeyError: # If the schema has changed? + # Safely retrieve the user's answer and the stored correct answer. + # json_answer or question_answers may be None (or not a dict) if not set, + # so check before subscripting to avoid TypeError. + if isinstance(self.json_answer, dict): + user_answer = self.json_answer.get(name, "") + else: user_answer = "" - try: - correct_answer = self.question.question_answers[name] - except KeyError: # If the schema has changed? + + qa = getattr(self.question, "question_answers", None) + if isinstance(qa, dict): + correct_answer = qa.get(name, "") + else: correct_answer = "" match value: @@ -1423,6 +1430,22 @@ class BaseReportAnswer(models.Model): return answers + #def get_marked_answers_html(self): + # """Returns an HTML representation of the users answers with correct answers highlighted""" + # html = "
" + # for value, user_answer, correct_answer, answer_is_correct, automark in self.get_correct_json_answers(): + # if automark: + # if answer_is_correct: + # html += f"
{user_answer}
" + # else: + # html += f"
Your answer: {user_answer}
Correct answer: {correct_answer}
" + # else: + # html += f"
Your answer: {user_answer} (Not auto-marked)
" + + # html += "
" + + # return format_html(html) + class Meta: abstract = True diff --git a/atlas/templates/atlas/case_display_block.html b/atlas/templates/atlas/case_display_block.html index d3c6f359..71e41093 100755 --- a/atlas/templates/atlas/case_display_block.html +++ b/atlas/templates/atlas/case_display_block.html @@ -416,6 +416,13 @@

+ + {% if casedetail %} + {% include 'atlas/partials/collection_question_block.html' with case_detail=casedetail can_edit=can_edit %} + + {% endif %} + +

Previous case: {{ case.previous_case.get_link }}

diff --git a/atlas/templates/atlas/collection_case_view_take.html b/atlas/templates/atlas/collection_case_view_take.html index 50f39466..025b5dfe 100644 --- a/atlas/templates/atlas/collection_case_view_take.html +++ b/atlas/templates/atlas/collection_case_view_take.html @@ -158,35 +158,7 @@ {% if collection.collection_type == "QUE" %} {% if question_completed %}

Answers

- -
- {% for value, user_answer, correct_answer, answer_is_correct, automark in answer.get_correct_json_answers %} -
-

{{value.title}}

- {{value.description}} -
- Answer : {{user_answer}} - - {% if not answer_is_correct %} -
Correct answer: {{correct_answer}} - {% endif %} - -
-
- - {% endfor %} -
- + {% include "atlas/partials/collection_question_answer_block.html" %} {% if collection.self_review %}
diff --git a/atlas/templates/atlas/collection_viva.html b/atlas/templates/atlas/collection_viva.html index 14e7ab74..601d5579 100644 --- a/atlas/templates/atlas/collection_viva.html +++ b/atlas/templates/atlas/collection_viva.html @@ -67,6 +67,10 @@ {% endfor %}
+ {# Hidden per-case question snippet to inject when case is loaded #} + {% if case.display_sets.all %}
Display Sets: {% for ds in case.display_sets.all %} @@ -116,6 +120,7 @@
+
@@ -221,6 +226,14 @@ "viewerstate": viewerstate }); + // Inject the per-case question snippet into the current case panel + try { + var snippet = $(c).find('.question-block-snippet').html(); + $('#current-case-questions').html(snippet || ''); + } catch (e) { + console.warn('Unable to inject question snippet', e); + } + }); $('.open-displayset').click(function() { let c = this; @@ -242,6 +255,15 @@ "annotations": annotations }); + // Inject the per-case question snippet + try { + var caseItem = $(this).closest('.case-item'); + var snippet = caseItem.find('.question-block-snippet').html(); + $('#current-case-questions').html(snippet || ''); + } catch (e) { + console.warn('Unable to inject question snippet for displayset', e); + } + }); $('.open-series-local').click(function() { let c = $(this).closest(".case-item")[0]; @@ -264,6 +286,14 @@ "series": seriesPk, "images": JSON.stringify(images), }); + + // Inject the per-case question snippet + try { + var snippet = $(c).find('.question-block-snippet').html(); + $('#current-case-questions').html(snippet || ''); + } catch (e) { + console.warn('Unable to inject question snippet for series', e); + } }); $('.open-case, .open-series').click(function() { var url = $(this).data('target'); @@ -289,6 +319,14 @@ $('#current-case-series').html("Series: " + $(this).data('series')); } bc.postMessage({"type": "open", "url" : url}); + // Inject question snippet for this case + try { + var caseItem = $(this).closest('.case-item'); + var snippet = caseItem.find('.question-block-snippet').html(); + $('#current-case-questions').html(snippet || ''); + } catch (e) { + console.warn('Unable to inject question snippet', e); + } //if (win2 == false || win2.closed) { // win2 = openSecondaryWindow(url); // console.log('opened', win2) diff --git a/atlas/templates/atlas/partials/collection_question_answer_block.html b/atlas/templates/atlas/partials/collection_question_answer_block.html new file mode 100644 index 00000000..b00e3f31 --- /dev/null +++ b/atlas/templates/atlas/partials/collection_question_answer_block.html @@ -0,0 +1,27 @@ +
+ {% for value, user_answer, correct_answer, answer_is_correct, automark in answer.get_correct_json_answers %} +
+

{{value.title}}

+ {{value.description}} +
+ Answer : {{user_answer}} + + {% if not answer_is_correct %} +
Correct answer: {{correct_answer}} + {% endif %} + +
+
+ + {% endfor %} +
\ No newline at end of file diff --git a/atlas/templates/atlas/partials/collection_question_block.html b/atlas/templates/atlas/partials/collection_question_block.html new file mode 100644 index 00000000..e606a1cf --- /dev/null +++ b/atlas/templates/atlas/partials/collection_question_block.html @@ -0,0 +1,63 @@ +{# Partial: render questions and answers as HTML. #} + +{% if case_detail.question_schema %} +
+

Questions

+
+ {% for name, prop in case_detail.question_schema.properties.items %} +
{{ prop.title|default:name }}
+
+ {% if prop.description %} +
{{ prop.description }}
+ {% endif %} + + {% if prop.type == "string" and prop.enum %} +
Options: {{ prop.enum|join:", " }}
+ {% else %} +
Type: {{ prop.type }}
+ {% endif %} + + {# Safely fetch the stored/example answer using the project's `get_item` filter. #} + {% if case_detail.question_answers %} + {% with correct=case_detail.question_answers|get_item:name %} +
+ Example / Correct: + {% if correct %} + {{ correct }} + {% else %} + (no answer provided) + {% endif %} +
+ {% endwith %} + {% else %} +
Example / Correct: (no answer provided)
+ {% endif %} + + {# Optionally render and compare a user's answer passed as `user_answer` in the context #} + {% if user_answer %} + {% if user_answer|get_item:name %} + {% with ua=user_answer|get_item:name %} + {% with correct=case_detail.question_answers|get_item:name %} +
+ Your answer: + {{ ua }} + {% if ua == correct %} + Correct + {% else %} + Incorrect + {% endif %} +
+ {% endwith %} + {% endwith %} + {% else %} +
Your answer: (no answer)
+ {% endif %} + {% endif %} + +
+ {% endfor %} +
+
+{% else %} +
No questions defined for this case.
+{% endif %} diff --git a/atlas/templates/atlas/user_collections.html b/atlas/templates/atlas/user_collections.html index 6288266e..13df6cd5 100644 --- a/atlas/templates/atlas/user_collections.html +++ b/atlas/templates/atlas/user_collections.html @@ -1,18 +1,53 @@ {% extends 'atlas/base.html' %} {% block content %} -

Collections

+

Collections

-The following collections are available for you to view / take. + The following collections are available for you to view / take. If you received a directly link to a collection it will not appear here unless you have started it. - + +

Available to start

- + +

In progress

+ + +

Finished

+ + {% endblock %} diff --git a/atlas/views.py b/atlas/views.py index b00ba018..eea143c7 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -38,6 +38,7 @@ from django.urls import reverse_lazy, reverse from django.http import Http404, JsonResponse from django.http import HttpResponseRedirect, HttpResponse +from django.contrib.contenttypes.models import ContentType from generic.models import CidUser, CidUserExam, CimarCase from atlas.helpers import get_cases_available_to_user @@ -641,9 +642,46 @@ def index(request): def user_collections(request): - collections = request.user.user_casecollection_exams.all() + # Collections the user is explicitly allowed to take (via CaseCollection.valid_user_users) + available_collections = request.user.user_casecollection_exams.all() + # Collections the user has already taken (recorded in generic.CidUserExam) + in_progress_ids = [] + finished_ids = [] + try: + ct = ContentType.objects.get_for_model(CaseCollection) + taken_exams = CidUserExam.objects.filter(user_user=request.user, content_type=ct) + in_progress_ids = list( + taken_exams.filter(completed=False).values_list("object_id", flat=True).distinct() + ) + finished_ids = list( + taken_exams.filter(completed=True).values_list("object_id", flat=True).distinct() + ) + except Exception: + # If anything goes wrong determining taken/finished exams, treat as none + in_progress_ids = [] + finished_ids = [] - return render(request, "atlas/user_collections.html", {"collections": collections}) + # Build querysets for the three groups + taken_all_ids = set(in_progress_ids) | set(finished_ids) + + # Available to start: explicitly allowed collections that the user hasn't taken at all + available_qs = available_collections.exclude(pk__in=taken_all_ids).order_by("name") + + # In-progress (taken but not completed) + in_progress_qs = CaseCollection.objects.filter(pk__in=list(in_progress_ids)).order_by("name") + + # Finished (completed) + finished_qs = CaseCollection.objects.filter(pk__in=list(finished_ids)).order_by("name") + + return render( + request, + "atlas/user_collections.html", + { + "available_collections": available_qs, + "in_progress_collections": in_progress_qs, + "finished_collections": finished_qs, + }, + ) @login_required def collection_options(request):