Refactor user collections view to categorize collections into available, in-progress, and finished; update templates for improved display of collection statuses and question rendering.

This commit is contained in:
Ross
2025-10-06 13:51:55 +01:00
parent 569ef760fa
commit 682e8160b5
8 changed files with 250 additions and 47 deletions
+29 -6
View File
@@ -1391,18 +1391,25 @@ class BaseReportAnswer(models.Model):
raise ValueError("No cid or user specified") raise ValueError("No cid or user specified")
def get_correct_json_answers(self): 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: if self.question.question_schema is None or not "properties" in self.question.question_schema:
return [] return []
answers = [] answers = []
for name, value in self.question.question_schema["properties"].items(): for name, value in self.question.question_schema["properties"].items():
logger.debug(f"Processing question property '{name}' with schema {value}")
try: # Safely retrieve the user's answer and the stored correct answer.
user_answer = self.json_answer[name] # json_answer or question_answers may be None (or not a dict) if not set,
except KeyError: # If the schema has changed? # so check before subscripting to avoid TypeError.
if isinstance(self.json_answer, dict):
user_answer = self.json_answer.get(name, "")
else:
user_answer = "" user_answer = ""
try:
correct_answer = self.question.question_answers[name] qa = getattr(self.question, "question_answers", None)
except KeyError: # If the schema has changed? if isinstance(qa, dict):
correct_answer = qa.get(name, "")
else:
correct_answer = "" correct_answer = ""
match value: match value:
@@ -1423,6 +1430,22 @@ class BaseReportAnswer(models.Model):
return answers return answers
#def get_marked_answers_html(self):
# """Returns an HTML representation of the users answers with correct answers highlighted"""
# html = "<div class='json-answers'>"
# for value, user_answer, correct_answer, answer_is_correct, automark in self.get_correct_json_answers():
# if automark:
# if answer_is_correct:
# html += f"<div class='answer correct'>{user_answer}</div>"
# else:
# html += f"<div class='answer incorrect'>Your answer: {user_answer}<br/>Correct answer: {correct_answer}</div>"
# else:
# html += f"<div class='answer unmarked'>Your answer: {user_answer} (Not auto-marked)</div>"
# html += "</div>"
# return format_html(html)
class Meta: class Meta:
abstract = True abstract = True
@@ -416,6 +416,13 @@
</ul> </ul>
</p> </p>
{% if casedetail %}
{% include 'atlas/partials/collection_question_block.html' with case_detail=casedetail can_edit=can_edit %}
{% endif %}
<p class="pre-whitespace"><b>Previous case:</b> {{ case.previous_case.get_link }}</p> <p class="pre-whitespace"><b>Previous case:</b> {{ case.previous_case.get_link }}</p>
@@ -158,35 +158,7 @@
{% if collection.collection_type == "QUE" %} {% if collection.collection_type == "QUE" %}
{% if question_completed %} {% if question_completed %}
<h3>Answers</h3> <h3>Answers</h3>
{% include "atlas/partials/collection_question_answer_block.html" %}
<div class="answer-block">
{% for value, user_answer, correct_answer, answer_is_correct, automark in answer.get_correct_json_answers %}
<div class="{% if answer_is_correct %}
correct
{% else %}
incorrect
{% endif %}
{% if automark %}
automark
{% endif %}
">
<h4>{{value.title}}</h4>
{{value.description}}
<div
>
Answer : {{user_answer}}
{% if not answer_is_correct %}
<br/>Correct answer: {{correct_answer}}
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% if collection.self_review %} {% if collection.self_review %}
<div> <div>
@@ -67,6 +67,10 @@
{% endfor %} {% endfor %}
</details> </details>
</div> </div>
{# Hidden per-case question snippet to inject when case is loaded #}
<div class="question-block-snippet" style="display:none;">
{% include 'atlas/partials/collection_question_block.html' with case_detail=casedetail %}
</div>
{% if case.display_sets.all %} {% if case.display_sets.all %}
<details class="displayset-detail"><summary>Display Sets:</summary> <details class="displayset-detail"><summary>Display Sets:</summary>
{% for ds in case.display_sets.all %} {% for ds in case.display_sets.all %}
@@ -116,6 +120,7 @@
<div id="current-case-history"></div> <div id="current-case-history"></div>
<div id="current-case-discussion"></div> <div id="current-case-discussion"></div>
<div id="current-case-report"></div> <div id="current-case-report"></div>
<div id="current-case-questions" class="mt-3"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -221,6 +226,14 @@
"viewerstate": viewerstate "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() { $('.open-displayset').click(function() {
let c = this; let c = this;
@@ -242,6 +255,15 @@
"annotations": annotations "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() { $('.open-series-local').click(function() {
let c = $(this).closest(".case-item")[0]; let c = $(this).closest(".case-item")[0];
@@ -264,6 +286,14 @@
"series": seriesPk, "series": seriesPk,
"images": JSON.stringify(images), "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() { $('.open-case, .open-series').click(function() {
var url = $(this).data('target'); var url = $(this).data('target');
@@ -289,6 +319,14 @@
$('#current-case-series').html("<span class='title'>Series:</span> " + $(this).data('series')); $('#current-case-series').html("<span class='title'>Series:</span> " + $(this).data('series'));
} }
bc.postMessage({"type": "open", "url" : url}); 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) { //if (win2 == false || win2.closed) {
// win2 = openSecondaryWindow(url); // win2 = openSecondaryWindow(url);
// console.log('opened', win2) // console.log('opened', win2)
@@ -0,0 +1,27 @@
<div class="answer-block">
{% for value, user_answer, correct_answer, answer_is_correct, automark in answer.get_correct_json_answers %}
<div class="{% if answer_is_correct %}
correct
{% else %}
incorrect
{% endif %}
{% if automark %}
automark
{% endif %}
">
<h4>{{value.title}}</h4>
{{value.description}}
<div
>
Answer : {{user_answer}}
{% if not answer_is_correct %}
<br/>Correct answer: {{correct_answer}}
{% endif %}
</div>
</div>
{% endfor %}
</div>
@@ -0,0 +1,63 @@
{# Partial: render questions and answers as HTML. #}
{% if case_detail.question_schema %}
<div class="collection-question-block">
<h4>Questions</h4>
<dl class="row">
{% for name, prop in case_detail.question_schema.properties.items %}
<dt class="col-sm-4">{{ prop.title|default:name }}</dt>
<dd class="col-sm-8">
{% if prop.description %}
<div class="mb-1 text-muted small">{{ prop.description }}</div>
{% endif %}
{% if prop.type == "string" and prop.enum %}
<div class="mt-1 small text-muted">Options: {{ prop.enum|join:", " }}</div>
{% else %}
<div class="mt-1 small text-muted">Type: {{ prop.type }}</div>
{% 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 %}
<div class="mt-2">
<strong>Example / Correct:</strong>
{% if correct %}
<span class="ms-2">{{ correct }}</span>
{% else %}
<span class="text-muted ms-2">(no answer provided)</span>
{% endif %}
</div>
{% endwith %}
{% else %}
<div class="mt-2"><strong>Example / Correct:</strong> <span class="text-muted ms-2">(no answer provided)</span></div>
{% 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 %}
<div class="mt-1">
<strong>Your answer:</strong>
<span class="ms-2 {% if ua == correct %}text-success{% else %}text-danger{% endif %}">{{ ua }}</span>
{% if ua == correct %}
<span class="badge bg-success ms-2">Correct</span>
{% else %}
<span class="badge bg-danger ms-2">Incorrect</span>
{% endif %}
</div>
{% endwith %}
{% endwith %}
{% else %}
<div class="mt-1"><strong>Your answer:</strong> <span class="text-muted ms-2">(no answer)</span></div>
{% endif %}
{% endif %}
</dd>
{% endfor %}
</dl>
</div>
{% else %}
<div class="collection-question-block text-muted">No questions defined for this case.</div>
{% endif %}
+45 -10
View File
@@ -1,18 +1,53 @@
{% extends 'atlas/base.html' %} {% extends 'atlas/base.html' %}
{% block content %} {% block content %}
<h2>Collections</h2> <h2>Collections</h2>
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.
<h3>Available to start</h3>
<ul> <ul>
{% for collection in collections %} {% for collection in available_collections %}
<li> <li>
<a href="{% url 'atlas:collection_take_start' collection.pk %}">{{collection}} {{collelction.is_complete}}</a> <a href="{% url 'atlas:collection_take_start' collection.pk %}">{{ collection.name }}</a>
</li> {% if collection.description %}
<small class="text-muted">— {{ collection.description }}</small>
{% endfor %} {% endif %}
</li>
{% empty %}
<li>No collections available to start.</li>
{% endfor %}
</ul> </ul>
<h3>In progress</h3>
<ul>
{% for collection in in_progress_collections %}
<li>
<a href="{% url 'atlas:collection_take_start' collection.pk %}">{{ collection.name }}</a>
<span class="badge bg-warning text-dark">In progress</span>
{% if collection.description %}
<small class="text-muted">— {{ collection.description }}</small>
{% endif %}
</li>
{% empty %}
<li>No in-progress collections.</li>
{% endfor %}
</ul>
<h3>Finished</h3>
<ul>
{% for collection in finished_collections %}
<li>
<a href="{% url 'atlas:collection_take_start' collection.pk %}">{{ collection.name }}</a>
<span class="badge bg-success">Completed</span>
{% if collection.description %}
<small class="text-muted">— {{ collection.description }}</small>
{% endif %}
</li>
{% empty %}
<li>No finished collections.</li>
{% endfor %}
</ul>
{% endblock %} {% endblock %}
+40 -2
View File
@@ -38,6 +38,7 @@ from django.urls import reverse_lazy, reverse
from django.http import Http404, JsonResponse from django.http import Http404, JsonResponse
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse
from django.contrib.contenttypes.models import ContentType
from generic.models import CidUser, CidUserExam, CimarCase from generic.models import CidUser, CidUserExam, CimarCase
from atlas.helpers import get_cases_available_to_user from atlas.helpers import get_cases_available_to_user
@@ -641,9 +642,46 @@ def index(request):
def user_collections(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 @login_required
def collection_options(request): def collection_options(request):