Add loading spinner and blur effect for question fragment during HTMX requests; optimize answered and flagged state queries

This commit is contained in:
Ross
2026-01-05 14:48:21 +00:00
parent 569ddb4343
commit b89ca918fa
2 changed files with 101 additions and 38 deletions
+40 -38
View File
@@ -179,29 +179,28 @@ def exam_take_overview(request, pk, cid=None, passcode=None):
else:
answers = UserAnswer.objects.filter(user=request.user, exam=exam)
answer_question_map = {}
for ans in answers:
answer_question_map[ans.question] = ans
# Map answers by question id for quick lookup
answer_question_map = {ans.question_id: ans for ans in answers}
# Prepare bulk flagged set to avoid per-question queries
q_ids = [q.pk for q in questions]
try:
ct = ContentType.objects.get_for_model(Question)
if cid is not None:
flags_qs = Flag.objects.filter(content_type=ct, object_id__in=q_ids, cid_user__cid=cid)
else:
flags_qs = Flag.objects.filter(content_type=ct, object_id__in=q_ids, user=request.user)
flagged_set = set(flags_qs.values_list('object_id', flat=True))
except Exception:
flagged_set = set()
question_answer_tuples = []
answer_count = 0
for q in questions:
if q in answer_question_map:
ans = answer_question_map[q]
ans = answer_question_map.get(q.pk)
if ans is not None:
answer_count += 1
else:
ans = None
# compute flagged state for this question+actor
try:
ct = ContentType.objects.get_for_model(q)
flags_qs = Flag.objects.filter(content_type=ct, object_id=q.pk)
if cid is not None:
flags_qs = flags_qs.filter(cid_user__cid=cid)
else:
flags_qs = flags_qs.filter(user=request.user)
flagged = flags_qs.exists()
except Exception:
flagged = False
flagged = q.pk in flagged_set
question_answer_tuples.append((q, ans, flagged))
cid_user_exam = exam.get_or_create_cid_user_exam(cid=cid, user_user=request.user)
@@ -375,6 +374,15 @@ def exam_take(request, pk: int, sk: int, cid: str | None = None, passcode: str |
except Exception:
flagged = False
# Precompute answered and flagged sets in bulk to avoid repeated queries
q_ids = [q.pk for q in questions]
if cid is not None:
answered_set = set(UserAnswer.objects.filter(cid=cid, exam=exam, question_id__in=q_ids).values_list('question_id', flat=True))
flagged_set = set(Flag.objects.filter(content_type=ContentType.objects.get_for_model(Question), object_id__in=q_ids, cid_user__cid=cid).values_list('object_id', flat=True))
else:
answered_set = set(UserAnswer.objects.filter(user=request.user, exam=exam, question_id__in=q_ids).values_list('question_id', flat=True))
flagged_set = set(Flag.objects.filter(content_type=ContentType.objects.get_for_model(Question), object_id__in=q_ids, user=request.user).values_list('object_id', flat=True))
return render(
request,
"physics/exam_take.html",
@@ -392,16 +400,9 @@ def exam_take(request, pk: int, sk: int, cid: str | None = None, passcode: str |
"cid_user_exam": cid_user_exam,
"flagged": flagged,
# minimal per-question status for client-side menu: answered/flagged lists
"answered_json": json.dumps([
True if (question.cid_user_answers.filter(cid=cid, exam=exam).exists() if cid is not None else question.cid_user_answers.filter(user=request.user, exam=exam).exists()) else False
for question in questions
]),
"flagged_json": json.dumps([
(Flag.objects.filter(content_type=ContentType.objects.get_for_model(question), object_id=question.pk).filter(cid_user__cid=cid).exists()
if cid is not None
else Flag.objects.filter(content_type=ContentType.objects.get_for_model(question), object_id=question.pk).filter(user=request.user).exists())
for question in questions
]),
# compute answered/flagged sets in bulk to avoid N+1 queries
"answered_json": json.dumps([q.pk in answered_set for q in questions]),
"flagged_json": json.dumps([q.pk in flagged_set for q in questions]),
},
)
@@ -470,6 +471,15 @@ def exam_take_fragment(request, pk: int, sk: int, cid: str | None = None, passco
if sk == exam_length - 1:
next = False
# Bulk compute answered and flagged sets for the fragment
q_ids = [q.pk for q in questions]
if cid is not None:
frag_answered_set = set(UserAnswer.objects.filter(cid=cid, exam=exam, question_id__in=q_ids).values_list('question_id', flat=True))
frag_flagged_set = set(Flag.objects.filter(content_type=ContentType.objects.get_for_model(Question), object_id__in=q_ids, cid_user__cid=cid).values_list('object_id', flat=True))
else:
frag_answered_set = set(UserAnswer.objects.filter(user=request.user, exam=exam, question_id__in=q_ids).values_list('question_id', flat=True))
frag_flagged_set = set(Flag.objects.filter(content_type=ContentType.objects.get_for_model(Question), object_id__in=q_ids, user=request.user).values_list('object_id', flat=True))
return render(
request,
"physics/partials/exam_take_fragment.html",
@@ -485,16 +495,8 @@ def exam_take_fragment(request, pk: int, sk: int, cid: str | None = None, passco
"saved_answer": saved_answer,
"answer": answer,
"flagged": flagged,
"answered_json": json.dumps([
True if (q.cid_user_answers.filter(cid=cid, exam=exam).exists() if cid is not None else q.cid_user_answers.filter(user=request.user, exam=exam).exists()) else False
for q in questions
]),
"flagged_json": json.dumps([
(Flag.objects.filter(content_type=ContentType.objects.get_for_model(q), object_id=q.pk).filter(cid_user__cid=cid).exists()
if cid is not None
else Flag.objects.filter(content_type=ContentType.objects.get_for_model(q), object_id=q.pk).filter(user=request.user).exists())
for q in questions
]),
"answered_json": json.dumps([q.pk in frag_answered_set for q in questions]),
"flagged_json": json.dumps([q.pk in frag_flagged_set for q in questions]),
"passcode": passcode,
"cid_user_exam": cid_user_exam,
},