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 #}
+
+ {% include 'atlas/partials/collection_question_block.html' with case_detail=casedetail %}
+
{% 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
- {% for collection in collections %}
- -
- {{collection}} {{collelction.is_complete}}
-
-
- {% endfor %}
+ {% for collection in available_collections %}
+ -
+ {{ collection.name }}
+ {% if collection.description %}
+ — {{ collection.description }}
+ {% endif %}
+
+ {% empty %}
+ - No collections available to start.
+ {% endfor %}
-
+
+ In progress
+
+ {% for collection in in_progress_collections %}
+ -
+ {{ collection.name }}
+ In progress
+ {% if collection.description %}
+ — {{ collection.description }}
+ {% endif %}
+
+ {% empty %}
+ - No in-progress collections.
+ {% endfor %}
+
+
+ Finished
+
+ {% for collection in finished_collections %}
+ -
+ {{ collection.name }}
+ Completed
+ {% if collection.description %}
+ — {{ collection.description }}
+ {% endif %}
+
+ {% empty %}
+ - No finished collections.
+ {% endfor %}
+
+
{% 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):