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:
+29
-6
@@ -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 %}
|
||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user