This commit is contained in:
Ross
2026-07-13 10:03:33 +01:00
parent 89bf6eedc0
commit 3a3ec75b1d
18 changed files with 514 additions and 24 deletions
+117 -6
View File
@@ -4038,8 +4038,21 @@ class ExamViews(View, LoginRequiredMixin):
max_score = self.get_max_score(questions)
from django.contrib.contenttypes.models import ContentType
from generic.models import CidUserExam
ct = ContentType.objects.get_for_model(self.Exam)
excluded_keys = set()
for cux in CidUserExam.objects.filter(content_type=ct, object_id=exam.pk, exclude_from_stats=True):
if cux.user_user_id:
excluded_keys.add(f"u/{cux.user_user_id}")
elif cux.cid_user_id:
excluded_keys.add(f"c/{cux.cid_user.cid}")
pass_mark_threshold = exam.pass_mark if exam.pass_mark is not None else (max_score / 2)
if stats_only:
user_scores_list = [i for i in user_scores.values() if i > 0]
user_scores_list = [score for user_key, score in user_scores.items() if user_key not in excluded_keys and score > 0]
mean = 0
median = 0
mode = 0
@@ -4080,8 +4093,9 @@ class ExamViews(View, LoginRequiredMixin):
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
passed_candidates_count = sum(1 for user_key, score in user_scores.items() if user_key not in excluded_keys and score >= pass_mark_threshold)
stats_cids = [c for c in cids if c not in excluded_keys]
pass_rate = round((passed_candidates_count / len(stats_cids)) * 100, 1) if stats_cids else 0.0
df = user_scores_list
fig = px.histogram(
@@ -4120,8 +4134,9 @@ class ExamViews(View, LoginRequiredMixin):
exam.user_scores = user_data
exam.save()
expected_answer_count = len(cids) * question_number
user_answer_count_total = sum(user_answer_count.values())
stats_cids = [c for c in cids if c not in excluded_keys]
expected_answer_count = len(stats_cids) * question_number
user_answer_count_total = sum(user_answer_count[u] for u in stats_cids if u in user_answer_count)
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"):
@@ -4135,7 +4150,7 @@ class ExamViews(View, LoginRequiredMixin):
context = {
"exam": exam,
"cids": sorted(cids),
"cids": sorted(stats_cids),
"question_number": question_number,
"max_score": max_score,
"mean": mean,
@@ -4153,9 +4168,20 @@ class ExamViews(View, LoginRequiredMixin):
"completion_rate": completion_rate,
"unmarked_count": unmarked_count,
"plot": fig_html,
"pass_mark_threshold": pass_mark_threshold,
}
return render(request, "generic/partials/_exam_scores_stats.html", context)
pass_status = {}
for cid, score in user_scores.items():
try:
val = float(score)
pass_status[cid] = "Pass" if val >= pass_mark_threshold else "Fail"
except (ValueError, TypeError):
pass_status[cid] = "Fail"
excluded_map = {k: True for k in excluded_keys}
template_variables = {
"cids": sorted(cids),
"missing_cids": valid_cid_users - plain_cids,
@@ -4172,6 +4198,10 @@ class ExamViews(View, LoginRequiredMixin):
"max_score": max_score,
"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",
"excluded_map": excluded_map,
"pass_status": pass_status,
"pass_mark_threshold": pass_mark_threshold,
"default_pass_mark": max_score / 2,
}
if self.app_name == "rapids":
@@ -4202,6 +4232,87 @@ class ExamViews(View, LoginRequiredMixin):
template_variables,
)
def update_pass_mark(self, request, pk):
exam = get_object_or_404(self.Exam, pk=pk)
if not exam.check_user_can_edit(request.user):
raise PermissionDenied
pass_mark_str = request.POST.get("pass_mark")
if pass_mark_str == "" or pass_mark_str is None:
exam.pass_mark = None
else:
try:
exam.pass_mark = float(pass_mark_str)
except ValueError:
return HttpResponse("Invalid pass mark", status=400)
exam.save()
from django.urls import reverse
from django.http import HttpResponseRedirect
url = reverse(f"{self.app_name}:exam_scores_all", kwargs={"pk": pk})
response = HttpResponseRedirect(url)
if request.htmx:
response["HX-Redirect"] = url
return response
def toggle_candidate_stats_exclusion(self, request, pk):
exam = get_object_or_404(self.Exam, pk=pk)
if not exam.check_user_can_edit(request.user):
raise PermissionDenied
candidate_key = request.GET.get("candidate")
if not candidate_key:
return HttpResponse("Candidate ID required", status=400)
from django.contrib.contenttypes.models import ContentType
from generic.models import CidUserExam, CidUser
ct = ContentType.objects.get_for_model(self.Exam)
cux = None
if candidate_key.startswith("u/"):
try:
user_id = int(candidate_key[2:])
cux, created = CidUserExam.objects.get_or_create(
content_type=ct,
object_id=exam.pk,
user_user_id=user_id,
)
except ValueError:
return HttpResponse("Invalid user ID", status=400)
elif candidate_key.startswith("c/"):
try:
cid = int(candidate_key[2:])
cid_user = CidUser.objects.filter(cid=cid).first()
if cid_user:
cux, created = CidUserExam.objects.get_or_create(
content_type=ct,
object_id=exam.pk,
cid_user=cid_user,
)
except ValueError:
return HttpResponse("Invalid CID", status=400)
else:
return HttpResponse("Invalid candidate key format", status=400)
if cux:
cux.exclude_from_stats = not cux.exclude_from_stats
cux.save()
response = render(
request,
"generic/partials/_candidate_stats_checkbox.html",
{
"cid": candidate_key,
"is_excluded": cux.exclude_from_stats,
"exam": exam,
}
)
response["HX-Trigger"] = "refreshStats"
return response
return HttpResponse("Candidate not found", status=404)
class GenericViewBase:
def __init__(self, app_name, QuestionObject, UserAnswerObject, ExamObject):