.
This commit is contained in:
+117
-6
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user