Optimize exam retrieval by prefetching related data to reduce N+1 queries and enhance access checks for user permissions

This commit is contained in:
Ross
2026-01-04 13:15:15 +00:00
parent 36a809ea40
commit 429adfc376
+53 -17
View File
@@ -3044,7 +3044,12 @@ class ExamViews(View, LoginRequiredMixin):
cid: int | None = None, cid: int | None = None,
passcode: str | None = None, passcode: str | None = None,
): ):
exams = self.Exam.objects.filter(archive=False) # Prefetch related relations used during the active_exams check to
# avoid N+1 queries when iterating many exams.
exams = (
self.Exam.objects.filter(archive=False)
.prefetch_related("valid_cid_users", "valid_user_users", "exam_questions")
)
active_exams = {"exams": []} active_exams = {"exams": []}
@@ -3056,22 +3061,52 @@ class ExamViews(View, LoginRequiredMixin):
return JsonResponse({"status": "invalid"}, status=401) return JsonResponse({"status": "invalid"}, status=401)
exam: ExamBase exam: ExamBase
# Prepare cid/user quick lookup when cid/user provided to avoid DB hits
user_id = request.user.pk if hasattr(request, "user") and request.user.is_authenticated else None
for exam in exams: for exam in exams:
if exam.active or self.check_user_access(request.user, exam.pk): # Skip exams the user cannot see unless it's active
if exam.exam_mode and not exam.check_cid_user( if not (exam.active or self.check_user_access(request.user, exam.pk)):
cid, passcode, user=request.user, user_id=request.user.pk
):
continue continue
if exam.json_creation_time: # If exam is in exam_mode, ensure the cid/user is allowed. Use the
creation_time = exam.json_creation_time.isoformat() # prefetched relations to perform in-memory checks rather than extra
else: # DB queries via `check_cid_user` which issues its own queries.
creation_time = "None" if exam.exam_mode:
allowed = False
# superusers always allowed
if hasattr(request, "user") and getattr(request.user, "is_superuser", False):
allowed = True
url = request.build_absolute_uri( # open access allows any logged-in user
exam.get_json_url(cid=cid, passcode=passcode) if not allowed and exam.exam_open_access and user_id is not None:
) allowed = True
# hacky
# check valid_user_users (prefetched)
if not allowed and user_id is not None:
try:
valid_user_ids = {u.pk for u in exam.valid_user_users.all()}
if user_id in valid_user_ids:
allowed = True
except Exception:
allowed = False
# check CID users when cid provided
if not allowed and cid is not None:
try:
for cu in exam.valid_cid_users.all():
if getattr(cu, "cid", None) == cid and getattr(cu, "passcode", None) == passcode:
allowed = True
break
except Exception:
allowed = False
if not allowed:
continue
creation_time = exam.json_creation_time.isoformat() if exam.json_creation_time else "None"
url = request.build_absolute_uri(exam.get_json_url(cid=cid, passcode=passcode))
if not based: if not based:
url = url + "/unbased" url = url + "/unbased"
@@ -3086,14 +3121,15 @@ class ExamViews(View, LoginRequiredMixin):
"exam_mode": exam.exam_mode, "exam_mode": exam.exam_mode,
} }
# Avoid generating question JSON in this endpoint; return existing ids
# to keep CPU usage low. Generation should be handled asynchronously
# or on-demand by the request that needs it.
if self.question_type == "long": if self.question_type == "long":
h = {} h = {}
for question in exam.exam_questions.all(): for question in exam.exam_questions.all():
# Generate json if needed h[question.pk] = getattr(question, "question_json_id", None)
if question.json_creation_time is None:
question.get_question_json()
h[question.pk] = question.question_json_id
obj["multi_question_json"] = h obj["multi_question_json"] = h
active_exams["exams"].append(obj) active_exams["exams"].append(obj)
if json is False: if json is False: