From 24c35148b0ff1ae37a83729e62cb3d357b9a4323 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 10 Nov 2025 12:54:29 +0000 Subject: [PATCH] Refactor physics exam review scoring to count true/false sub-questions individually and improve accuracy of correct answer calculations --- generic/views.py | 54 ++++++++++++++++++++++++++++++++++++++++-------- physics/views.py | 33 ++++++++++++++++------------- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/generic/views.py b/generic/views.py index d31b94eb..6c164409 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1283,17 +1283,53 @@ class ExamViews(View, LoginRequiredMixin): # 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: + # Special-case physics: count each true/false sub-question individually. + if self.app_name == "physics": + # Determine number of sub-items per question try: - score = ua.get_answer_score() + n_sub = len(q.get_questions()) 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 + n_sub = None + + total_subitems = 0 + correct_subitems = 0 + for ua in ua_qs: + try: + score = ua.get_answer_score() + except Exception: + continue + # score is expected to be iterable (tuple/list) for physics + if isinstance(score, (list, tuple)) and score: + # count numeric-typed positive/1 values as correct + for s in score: + if isinstance(s, (int, float)) and s > 0: + correct_subitems += 1 + total_subitems += len(score) + else: + # fallback: treat scalar score as single subitem + if isinstance(score, (int, float)): + if score > 0: + correct_subitems += 1 + total_subitems += 1 + + # Compute percent over all subitems seen + correct_count = correct_subitems + if total_subitems: + correct_pct = round(100.0 * correct_subitems / total_subitems, 1) + else: + correct_pct = 0.0 + else: + 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 else: correct_count = 0 correct_pct = 0.0 diff --git a/physics/views.py b/physics/views.py index 59f0a04c..e24c911e 100644 --- a/physics/views.py +++ b/physics/views.py @@ -522,27 +522,32 @@ def exam_review_question_summary(request, pk: int, q_index: int): correct_count = None correct_pct = None try: - # per-question max for physics is 5 (see generic.get_max_score mapping) - per_question_max = 5 + # For physics, count true/false sub-questions individually rather than + # requiring a full-match across all parts. if total_responses: - cnt = 0 + correct_subitems = 0 + total_subitems = 0 for ua in ua_qs: try: score = ua.get_answer_score() except Exception: continue - 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 + if isinstance(score, (list, tuple)) and score: + for s in score: + if isinstance(s, (int, float)) and s > 0: + correct_subitems += 1 + total_subitems += len(score) else: - 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 + if isinstance(score, (int, float)): + if score > 0: + correct_subitems += 1 + total_subitems += 1 + + correct_count = correct_subitems + if total_subitems: + correct_pct = round(100.0 * correct_subitems / total_subitems, 1) + else: + correct_pct = 0.0 else: correct_count = 0 correct_pct = 0.0