Enhance exam overview and fragment by adding question status indicators for answered and flagged questions

This commit is contained in:
Ross
2026-01-05 14:13:52 +00:00
parent 20a06323d5
commit 8d638cc08b
4 changed files with 73 additions and 8 deletions
+27
View File
@@ -49,6 +49,12 @@
<h4>Questions</h4>
<div id="menu-list"></div>
<script>
// Minimal per-question status injected by view: answered/flagged arrays
window.examQuestionAnswered = {{ answered_json|safe }};
window.examQuestionFlagged = {{ flagged_json|safe }};
</script>
{% include "physics/exam_take_help.html" %}
{% endblock %}
@@ -197,6 +203,27 @@
.postinput { margin-top: 0.4rem; text-align: left; }
.physics-answer-list li { padding: 0.6rem; }
}
/* Question menu item states (small pill buttons) */
button.question-menu-item {
color: var(--text);
border: 1px solid var(--card-border);
background: var(--card-bg);
padding: 0.25rem 0.5rem;
border-radius: 0.35rem;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
button.question-menu-item.answered {
border-color: var(--success) !important; /* answered -> success border */
}
/* flagged items show an icon only; no border change here */
button.question-menu-item .question-flag {
margin-left: 0.25rem;
font-size: 0.85em;
color: #ffc107;
line-height: 1;
}
</style>
{% endblock %}
@@ -42,11 +42,11 @@
<div class="overview-text">{{answer_count}} out of {{exam_length}} questions answered. Unanswered questions are shown in red. <span class="d-block">Click any tile to jump to that question.</span></div>
<div class="physics-finish-list" role="list">
{% for question, answer in question_answer_tuples %}
{% for question, answer, flagged in question_answer_tuples %}
{% comment %} Use an anchor styled as a button rather than nesting button inside anchor (invalid HTML). Keep link targets unchanged. {% endcomment %}
{% if not cid %}
<a role="listitem" href="{% url 'physics:exam_take_user' pk=exam.id sk=forloop.counter0 %}"
class="overview-question-btn btn btn-outline-secondary"
class="overview-question-btn btn btn-outline-secondary {% if flagged %}flagged{% endif %}"
aria-label="Go to question {{ forloop.counter }}"
{% if not answer %}title="You have not answered this question" aria-current="false"{% endif %}>
{{forloop.counter}}{% if answer and answer.answer %}: {{answer.answer}}{% endif %}
@@ -54,7 +54,7 @@
{% else %}
<a role="listitem" href="{% url 'physics:exam_take' pk=exam.id sk=forloop.counter0 cid=cid passcode=passcode %}"
class="overview-question-btn btn btn-outline-secondary"
class="overview-question-btn btn btn-outline-secondary {% if flagged %}flagged{% endif %}"
aria-label="Go to question {{ forloop.counter }}"
{% if not answer %}title="You have not answered this question" aria-current="false"{% endif %}>
{{forloop.counter}}{% if answer and answer.answer %}: {{answer.answer}}{% endif %}
@@ -129,6 +129,13 @@
justify-content: flex-start;
}
/* Flagged question indicator: use border colour to signal a flagged item */
.overview-question-btn.flagged {
border-color: #ffc107 !important;
box-shadow: 0 1px 2px rgba(255,193,7,0.06);
background: rgba(255,193,7,0.03);
}
/* Unanswered questions should draw attention */
.overview-question-btn[title] {
border-color: #dc3545 !important;
@@ -124,8 +124,17 @@
// build the question menu locally so it is available after fragment swaps
$('#menu-list').empty();
const answeredArr = (window.examQuestionAnswered && Array.isArray(window.examQuestionAnswered)) ? window.examQuestionAnswered : null;
const flaggedArr = (window.examQuestionFlagged && Array.isArray(window.examQuestionFlagged)) ? window.examQuestionFlagged : null;
for (let i = 0; i < {{ exam_length }}; i++) {
const qbutton = $(`<button class="question-menu-item" name="goto-${i}" data-qn="${i}">${i+1}</button>`);
const qbutton = $(`<button class="question-menu-item" name="goto-${i}" data-qn="${i}"><span class="qnum">${i+1}</span></button>`);
// answered: indicate with border colour but keep text colour unchanged (white)
if (answeredArr && answeredArr[i]) { qbutton.addClass('answered'); }
// flagged: add flag icon and flagged class which alters border
if (flaggedArr && flaggedArr[i]) {
qbutton.addClass('flagged');
qbutton.append('<span class="question-flag" aria-hidden="true">⚑</span>');
}
if (i == {{ pos }}) { qbutton.addClass('current-question'); }
$('#menu-list').append(qbutton);
}
+26 -4
View File
@@ -186,12 +186,23 @@ def exam_take_overview(request, pk, cid=None, passcode=None):
question_answer_tuples = []
answer_count = 0
for q in questions:
# if q in answer_question_map and answer_question_map[q].answer:
if q in answer_question_map: # might need to improve this
question_answer_tuples.append((q, answer_question_map[q]))
if q in answer_question_map:
ans = answer_question_map[q]
answer_count += 1
else:
question_answer_tuples.append((q, None))
ans = None
# compute flagged state for this question+actor
try:
ct = ContentType.objects.get_for_model(q)
flags_qs = Flag.objects.filter(content_type=ct, object_id=q.pk)
if cid is not None:
flags_qs = flags_qs.filter(cid_user__cid=cid)
else:
flags_qs = flags_qs.filter(user=request.user)
flagged = flags_qs.exists()
except Exception:
flagged = False
question_answer_tuples.append((q, ans, flagged))
cid_user_exam = exam.get_or_create_cid_user_exam(cid=cid, user_user=request.user)
@@ -380,6 +391,17 @@ def exam_take(request, pk: int, sk: int, cid: str | None = None, passcode: str |
"passcode": passcode,
"cid_user_exam": cid_user_exam,
"flagged": flagged,
# minimal per-question status for client-side menu: answered/flagged lists
"answered_json": json.dumps([
True if (question.cid_user_answers.filter(cid=cid, exam=exam).exists() if cid is not None else question.cid_user_answers.filter(user=request.user, exam=exam).exists()) else False
for question in questions
]),
"flagged_json": json.dumps([
(Flag.objects.filter(content_type=ContentType.objects.get_for_model(question), object_id=question.pk).filter(cid_user__cid=cid).exists()
if cid is not None
else Flag.objects.filter(content_type=ContentType.objects.get_for_model(question), object_id=question.pk).filter(user=request.user).exists())
for question in questions
]),
},
)