Add primary answer display to exam review question fragment and ensure safe rendering of question text
This commit is contained in:
@@ -4,6 +4,9 @@
|
|||||||
<div id="anatomy-dicom-image" class="dicom-image-legacy" data-url="{{ question.get_image_url }}"
|
<div id="anatomy-dicom-image" class="dicom-image-legacy" data-url="{{ question.get_image_url }}"
|
||||||
data-annotations='{{question.image_annotations}}' data-edit_annotation=true>
|
data-annotations='{{question.image_annotations}}' data-edit_annotation=true>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2">
|
||||||
|
Answer: {{ question.get_primary_answer }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a class="btn btn-sm btn-outline-secondary" href="{% url app_name|add:':question_detail' question.pk %}" target="_blank">View full question details</a>
|
<a class="btn btn-sm btn-outline-secondary" href="{% url app_name|add:':question_detail' question.pk %}" target="_blank">View full question details</a>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="card border-{{ qs.colour }} h-100">
|
<div class="card border-{{ qs.colour }} h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<h5 class="card-title">Question {{ qs.q_index|add:1 }}: {{ qs.question }}</h5>
|
<h5 class="card-title">Question {{ qs.q_index|add:1 }}: {{ qs.question |safe }}</h5>
|
||||||
<span class="badge bg-{{ qs.colour }} align-self-start">{% if qs.correct_pct is not None %}{{ qs.correct_pct }}% correct{% elif qs.answered_pct is not None %}{{ qs.answered_pct }}% answered{% else %}No data{% endif %}</span>
|
<span class="badge bg-{{ qs.colour }} align-self-start">{% if qs.correct_pct is not None %}{{ qs.correct_pct }}% correct{% elif qs.answered_pct is not None %}{{ qs.answered_pct }}% answered{% else %}No data{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+78
-18
@@ -1214,15 +1214,44 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
else:
|
else:
|
||||||
pcts = {k: 0.0 for k in counts.keys()}
|
pcts = {k: 0.0 for k in counts.keys()}
|
||||||
|
|
||||||
# Compute percent correct where a best_answer exists
|
# Compute percent correct using per-answer scoring when available.
|
||||||
|
# Different apps use different scoring scales (e.g. anatomy/rapids=2, longs=8,
|
||||||
|
# physics/shorts=5, sbas=1). We determine the per-question maximum based on
|
||||||
|
# the app and count user answers whose get_answer_score() equals that max.
|
||||||
correct_pct = None
|
correct_pct = None
|
||||||
correct_count = None
|
correct_count = None
|
||||||
if hasattr(q, "best_answer") and getattr(q, "best_answer", None):
|
try:
|
||||||
try:
|
# Determine per-question maximum score for this app
|
||||||
correct_count = ua_qs.filter(answer=q.best_answer).count()
|
if self.app_name in ("rapids", "anatomy"):
|
||||||
|
per_question_max = 2
|
||||||
|
elif self.app_name == "longs":
|
||||||
|
per_question_max = 8
|
||||||
|
elif self.app_name in ("physics", "shorts"):
|
||||||
|
per_question_max = 5
|
||||||
|
else:
|
||||||
|
per_question_max = 1
|
||||||
|
|
||||||
|
# If we have responses, iterate and count fully-correct answers using
|
||||||
|
# the model's get_answer_score() which encapsulates per-app logic.
|
||||||
|
if total_responses:
|
||||||
|
cnt = 0
|
||||||
|
for ua in ua_qs:
|
||||||
|
try:
|
||||||
|
score = ua.get_answer_score()
|
||||||
|
except Exception:
|
||||||
|
# If get_answer_score fails for any answer, skip it
|
||||||
|
continue
|
||||||
|
if score == per_question_max:
|
||||||
|
cnt += 1
|
||||||
|
correct_count = cnt
|
||||||
correct_pct = round(100.0 * correct_count / total_responses, 1) if total_responses else 0.0
|
correct_pct = round(100.0 * correct_count / total_responses, 1) if total_responses else 0.0
|
||||||
except Exception:
|
else:
|
||||||
correct_pct = None
|
correct_count = 0
|
||||||
|
correct_pct = 0.0
|
||||||
|
except Exception:
|
||||||
|
# Best-effort: fall back to None if something unexpected happens
|
||||||
|
correct_pct = None
|
||||||
|
correct_count = None
|
||||||
|
|
||||||
# Percent answered relative to candidate pool (if known)
|
# Percent answered relative to candidate pool (if known)
|
||||||
answered_pct = None
|
answered_pct = None
|
||||||
@@ -1343,20 +1372,51 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
for k in counts.keys():
|
for k in counts.keys():
|
||||||
pcts[k] = 0.0
|
pcts[k] = 0.0
|
||||||
|
|
||||||
# If the question exposes a best_answer, compute correct count
|
# Compute correct count using per-answer scoring when available.
|
||||||
|
# Different apps use different scoring scales (e.g. anatomy/rapids=2,
|
||||||
|
# longs=8, physics/shorts=5, sbas=1). Determine per-question max
|
||||||
|
# and count answers where get_answer_score() equals that max.
|
||||||
correct_count = None
|
correct_count = None
|
||||||
if hasattr(question, "best_answer") and question.best_answer:
|
|
||||||
correct_count = ua_qs.filter(answer=question.best_answer).count()
|
|
||||||
else:
|
|
||||||
correct_count = None
|
|
||||||
|
|
||||||
# percent correct
|
|
||||||
correct_pct = None
|
correct_pct = None
|
||||||
if correct_count is not None and total_responses:
|
try:
|
||||||
try:
|
if self.app_name in ("rapids", "anatomy"):
|
||||||
correct_pct = round(100.0 * correct_count / total_responses, 1)
|
per_question_max = 2
|
||||||
except Exception:
|
elif self.app_name == "longs":
|
||||||
correct_pct = None
|
per_question_max = 8
|
||||||
|
elif self.app_name in ("physics", "shorts"):
|
||||||
|
per_question_max = 5
|
||||||
|
else:
|
||||||
|
per_question_max = 1
|
||||||
|
|
||||||
|
if total_responses:
|
||||||
|
cnt = 0
|
||||||
|
for ua in ua_qs:
|
||||||
|
try:
|
||||||
|
score = ua.get_answer_score()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle tuple/list scores (physics) by summing parts
|
||||||
|
if isinstance(score, (list, tuple)):
|
||||||
|
try:
|
||||||
|
ssum = sum([s for s in score if isinstance(s, (int, float))])
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if ssum == per_question_max:
|
||||||
|
cnt += 1
|
||||||
|
else:
|
||||||
|
# Numeric scores only; skip 'unmarked' / None
|
||||||
|
if isinstance(score, (int, float)) and score == per_question_max:
|
||||||
|
cnt += 1
|
||||||
|
|
||||||
|
correct_count = cnt
|
||||||
|
correct_pct = round(100.0 * correct_count / total_responses, 1) if total_responses else 0.0
|
||||||
|
else:
|
||||||
|
correct_count = 0
|
||||||
|
correct_pct = 0.0
|
||||||
|
except Exception:
|
||||||
|
correct_count = None
|
||||||
|
correct_pct = None
|
||||||
|
|
||||||
context.update(
|
context.update(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user