This commit is contained in:
Ross
2026-07-06 22:06:58 +01:00
parent 50b80fd0c7
commit fa98541e31
24 changed files with 1258 additions and 98 deletions
+85 -61
View File
@@ -2370,33 +2370,7 @@ class ExamViews(View, LoginRequiredMixin):
},
)
@method_decorator(login_required)
def exam_scores_refresh(self, request, pk):
exam = get_object_or_404(self.Exam, pk=pk)
if not exam.exam_mode:
raise Http404("Packet not in exam mode")
# We don't worry about order here
questions = exam.exam_questions.all()
cids = self.UserAnswer.objects.filter(question__in=questions, exam__id=pk)
# Force a score update
for c in cids:
c.get_answer_score(cached=False)
return render(
request,
f"{self.app_name}/base.html",
{
"simple_content": format_html(
"""Answer scores updated <br/>
<a href='{}'>Return</a>""",
reverse(f"{self.app_name}:exam_scores_all", kwargs={"pk": exam.pk}),
)
},
)
def exam_cids(self, request, exam_id):
"""View that displays an overview of users that have been added to the exam.
@@ -3941,11 +3915,12 @@ class ExamViews(View, LoginRequiredMixin):
raise PermissionDenied
table_only = request.GET.get("table_only") == "true" or request.headers.get("HX-Target") == "answers-table-container"
stats_only = request.GET.get("stats_only") == "true" or request.headers.get("HX-Target") == "stats-container"
user_answers_marks = defaultdict(list)
by_question = defaultdict(dict)
unmarked = set()
cached_scores = True
cached_scores = False
valid_cid_users = set(exam.valid_cid_users.all().values_list("cid", flat=True))
valid_user_users = set(exam.valid_user_users.all().values_list("pk", flat=True))
@@ -3955,7 +3930,6 @@ class ExamViews(View, LoginRequiredMixin):
user_answers_callstates_counted = {}
if self.app_name in ("physics", "sbas", "longs", "shorts"):
cached_scores = False
questions_qs = exam.get_questions()
else:
questions_qs = exam.get_questions().prefetch_related("answers")
@@ -4004,6 +3978,7 @@ class ExamViews(View, LoginRequiredMixin):
index = question_index_map.get(q)
if index is not None:
unmarked.add(index)
user_answers_marks[cid].append(answer_score)
if self.app_name == "rapids":
@@ -4051,34 +4026,63 @@ class ExamViews(View, LoginRequiredMixin):
user_answer_count[user] = len(user_answers_marks[user])
# ignore scores of 0 for stats
user_scores_list = [i for i in user_scores.values() if i > 0]
# If table_only and rapids, populate user_answers_callstates_counted
if table_only and self.app_name == "rapids":
for u in cids:
user_answers_callstates_counted[u] = dict(Counter(user_answers_callstates[u]))
missing_user_ids = valid_user_users - user_ids
missing_users = []
if missing_user_ids:
missing_users = User.objects.filter(pk__in=missing_user_ids)
max_score = self.get_max_score(questions)
mean = 0
median = 0
mode = 0
fig_html = ""
if stats_only:
user_scores_list = [i for i in user_scores.values() if i > 0]
mean = 0
median = 0
mode = 0
fig_html = ""
std_dev = 0
q1 = 0
q3 = 0
min_score = 0
max_score_achieved = 0
passed_candidates_count = 0
pass_rate = 0.0
if not table_only:
if len(user_scores_list) < 1:
mean = 0
median = 0
mode = 0
fig_html = ""
else:
# Exclude very low scores from statistics
if len(user_scores_list) >= 1:
lower_score_bound = max_score / 5
bounded_scores = [i for i in user_scores_list if i > lower_score_bound]
if bounded_scores:
user_scores_list = bounded_scores
mean = statistics.mean(user_scores_list)
median = statistics.median(user_scores_list)
mean = round(statistics.mean(user_scores_list), 2)
median = round(statistics.median(user_scores_list), 2)
try:
mode = statistics.mode(user_scores_list)
if isinstance(mode, (int, float)):
mode = round(mode, 2)
except statistics.StatisticsError:
mode = "No unique mode"
std_dev = round(statistics.pstdev(user_scores_list), 2)
if len(user_scores_list) >= 2:
quants = statistics.quantiles(user_scores_list, n=4)
q1 = round(quants[0], 2)
q3 = round(quants[2], 2)
else:
q1 = round(user_scores_list[0], 2)
q3 = round(user_scores_list[0], 2)
min_score = round(min(user_scores_list), 2)
max_score_achieved = round(max(user_scores_list), 2)
passed_candidates_count = sum(1 for score in user_scores.values() if score >= max_score / 2)
pass_rate = round((passed_candidates_count / len(cids)) * 100, 1) if cids else 0.0
df = user_scores_list
fig = px.histogram(
df,
@@ -4097,8 +4101,8 @@ class ExamViews(View, LoginRequiredMixin):
exam.stats_candidates = len(user_scores_list)
exam.stats_max_possible = max_score
exam.stats_min = min(user_scores_list)
exam.stats_max = max(user_scores_list)
exam.stats_min = min_score
exam.stats_max = max_score_achieved
exam.stats_graph = fig_html
@@ -4112,20 +4116,45 @@ class ExamViews(View, LoginRequiredMixin):
if self.app_name == "rapids":
counted_callstates = dict(Counter(user_answers_callstates[u]))
user_data[u]["callstates"] = str(counted_callstates)
user_answers_callstates_counted[u] = counted_callstates
exam.user_scores = user_data
exam.save()
else:
# If table_only and rapids, populate user_answers_callstates_counted
if self.app_name == "rapids":
for u in cids:
user_answers_callstates_counted[u] = dict(Counter(user_answers_callstates[u]))
missing_user_ids = valid_user_users - user_ids
missing_users = []
if missing_user_ids:
missing_users = User.objects.filter(pk__in=missing_user_ids)
expected_answer_count = len(cids) * question_number
user_answer_count_total = sum(user_answer_count.values())
completion_rate = round((user_answer_count_total / expected_answer_count) * 100, 1) if expected_answer_count > 0 else 0.0
if hasattr(self.Answer, "MarkOptions") and hasattr(self.UserAnswer, "score"):
unmarked_count = sum(1 for c in cid_user_answers if c.score == self.Answer.MarkOptions.UNMARKED or c.score is None)
elif hasattr(self.UserAnswer, "is_marked"):
unmarked_count = sum(1 for c in cid_user_answers if not c.is_marked())
elif hasattr(self.UserAnswer, "score"):
unmarked_count = sum(1 for c in cid_user_answers if c.score == "" or c.score is None)
else:
unmarked_count = 0
context = {
"exam": exam,
"cids": sorted(cids),
"question_number": question_number,
"max_score": max_score,
"mean": mean,
"median": median,
"mode": mode,
"std_dev": std_dev,
"q1": q1,
"q3": q3,
"min_score": min_score,
"max_score_achieved": max_score_achieved,
"passed_candidates_count": passed_candidates_count,
"pass_rate": pass_rate,
"expected_answer_count": expected_answer_count,
"user_answer_count_total": user_answer_count_total,
"completion_rate": completion_rate,
"unmarked_count": unmarked_count,
"plot": fig_html,
}
return render(request, "generic/partials/_exam_scores_stats.html", context)
template_variables = {
"cids": sorted(cids),
@@ -4141,11 +4170,6 @@ class ExamViews(View, LoginRequiredMixin):
"user_answer_count": user_answer_count,
"cids_user_id_map": cids_user_id_map,
"max_score": max_score,
"mean": mean,
"median": median,
"mode": mode,
"plot": fig_html,
"cached_scores": cached_scores,
"can_edit": exam.check_user_can_edit(request.user),
"base_template": "generic/exam_scores_partial_base.html" if table_only else "generic/exam_scores_base.html",
}