From 8d638cc08b96dc9821ddf55e52969a7b018ee1de Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 5 Jan 2026 14:13:52 +0000 Subject: [PATCH] Enhance exam overview and fragment by adding question status indicators for answered and flagged questions --- physics/templates/physics/exam_take.html | 27 +++++++++++++++++ .../templates/physics/exam_take_overview.html | 13 ++++++-- .../physics/partials/exam_take_fragment.html | 11 ++++++- physics/views.py | 30 ++++++++++++++++--- 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/physics/templates/physics/exam_take.html b/physics/templates/physics/exam_take.html index 357a6d56..b8e74a83 100755 --- a/physics/templates/physics/exam_take.html +++ b/physics/templates/physics/exam_take.html @@ -49,6 +49,12 @@

Questions

+ + {% 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; + } {% endblock %} diff --git a/physics/templates/physics/exam_take_overview.html b/physics/templates/physics/exam_take_overview.html index 6e6cb7b4..3ff7987d 100644 --- a/physics/templates/physics/exam_take_overview.html +++ b/physics/templates/physics/exam_take_overview.html @@ -42,11 +42,11 @@
{{answer_count}} out of {{exam_length}} questions answered. Unanswered questions are shown in red. Click any tile to jump to that question.
- {% 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 %} {{forloop.counter}}{% if answer and answer.answer %}: {{answer.answer}}{% endif %} @@ -54,7 +54,7 @@ {% else %} {{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; diff --git a/physics/templates/physics/partials/exam_take_fragment.html b/physics/templates/physics/partials/exam_take_fragment.html index 8194fb65..08410727 100644 --- a/physics/templates/physics/partials/exam_take_fragment.html +++ b/physics/templates/physics/partials/exam_take_fragment.html @@ -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 = $(``); + const qbutton = $(``); + // 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(''); + } if (i == {{ pos }}) { qbutton.addClass('current-question'); } $('#menu-list').append(qbutton); } diff --git a/physics/views.py b/physics/views.py index db7fa189..4733828d 100644 --- a/physics/views.py +++ b/physics/views.py @@ -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 + ]), }, )