Implement HTMX lazy-loading for exam collections and optimize queryset annotations for performance
This commit is contained in:
+107
-58
@@ -130,6 +130,89 @@ def cid_user_exam_partial(request, app_name: str, exam_id: int, cid: int):
|
||||
return render(request, "generic/partials/ciduserexam_detail_partial.html", {"cid_user_exam": cux})
|
||||
|
||||
|
||||
def exam_collection_exams_partial(request, collection_id: int, exam_type: str):
|
||||
"""Return the `exam-list` partial for a given collection and exam type.
|
||||
|
||||
This endpoint is intended for HTMX lazy-loading. It annotates candidate counts
|
||||
and prefetches authors to avoid per-exam DB queries in the template.
|
||||
"""
|
||||
collection = get_object_or_404(ExamCollection, pk=collection_id)
|
||||
|
||||
# Map exam_type to related name and model
|
||||
EXAM_MAP = {
|
||||
"anatomy": ("anatomy_exams", "anatomy"),
|
||||
"longs": ("longs_exams", "longs"),
|
||||
"rapids": ("rapids_exams", "rapids"),
|
||||
"shorts": ("shorts_exams", "shorts"),
|
||||
"physics": ("physics_exams", "physics"),
|
||||
"sbas": ("sbas_exams", "sbas"),
|
||||
}
|
||||
|
||||
if exam_type not in EXAM_MAP:
|
||||
return HttpResponse("Unknown exam type", status=400)
|
||||
|
||||
rel_name, app_label = EXAM_MAP[exam_type]
|
||||
|
||||
# Attempt to import the model class for the app to annotate counts
|
||||
ExamModel = None
|
||||
try:
|
||||
if exam_type == "anatomy":
|
||||
from anatomy.models import Exam as AnatomyExam
|
||||
|
||||
ExamModel = AnatomyExam
|
||||
elif exam_type == "longs":
|
||||
from longs.models import Exam as LongsExam
|
||||
|
||||
ExamModel = LongsExam
|
||||
elif exam_type == "rapids":
|
||||
from rapids.models import Exam as RapidsExam
|
||||
|
||||
ExamModel = RapidsExam
|
||||
elif exam_type == "shorts":
|
||||
from shorts.models import Exam as ShortsExam
|
||||
|
||||
ExamModel = ShortsExam
|
||||
elif exam_type == "physics":
|
||||
from physics.models import Exam as PhysicsExam
|
||||
|
||||
ExamModel = PhysicsExam
|
||||
elif exam_type == "sbas":
|
||||
from sbas.models import Exam as SbasExam
|
||||
|
||||
ExamModel = SbasExam
|
||||
except Exception:
|
||||
ExamModel = None
|
||||
|
||||
# Build queryset on the related manager; filter archived false by default
|
||||
related_qs = getattr(collection, rel_name).all().filter(archive=False)
|
||||
|
||||
if ExamModel is not None:
|
||||
# Avoid evaluating the related manager multiple times; use ids
|
||||
pks = list(related_qs.values_list("pk", flat=True))
|
||||
related_qs = (
|
||||
ExamModel.objects.filter(pk__in=pks)
|
||||
.annotate(
|
||||
valid_cid_users_count=Count("valid_cid_users"),
|
||||
valid_user_users_count=Count("valid_user_users"),
|
||||
)
|
||||
.prefetch_related("author")
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
context = {
|
||||
"exams": related_qs,
|
||||
"app_name": app_label,
|
||||
"marking": request.user.has_perm("generic.can_mark") if request.user.is_authenticated else False,
|
||||
"collection": collection,
|
||||
}
|
||||
|
||||
# Render only the fragment template which includes the `exam-list` partialdef
|
||||
html = render_to_string(
|
||||
"generic/partials/exam_list_fragment.html", context, request=request
|
||||
)
|
||||
return HttpResponse(html)
|
||||
|
||||
|
||||
from rapids.models import (
|
||||
Rapid as RapidQuestion,
|
||||
ExamQuestionDetail as RapidsExamQuestionDetail,
|
||||
@@ -5875,68 +5958,34 @@ class ExamCollectionList(LoginRequiredMixin, ListView):
|
||||
|
||||
class ExamCollectionDetail(AuthorRequiredMixin, DetailView):
|
||||
model = ExamCollection
|
||||
|
||||
# Annotate counts for each exam type (non-archived) so template can check presence
|
||||
# without evaluating entire related querysets.
|
||||
def get_queryset(self):
|
||||
# Prefetch related exam sets and annotate candidate counts to avoid per-exam DB hits in templates
|
||||
qs = super().get_queryset()
|
||||
counts = {
|
||||
"anatomy_count": Count(
|
||||
"anatomy_exams", filter=Q(anatomy_exams__archive=False), distinct=True
|
||||
),
|
||||
"longs_count": Count(
|
||||
"longs_exams", filter=Q(longs_exams__archive=False), distinct=True
|
||||
),
|
||||
"physics_count": Count(
|
||||
"physics_exams", filter=Q(physics_exams__archive=False), distinct=True
|
||||
),
|
||||
"rapids_count": Count(
|
||||
"rapids_exams", filter=Q(rapids_exams__archive=False), distinct=True
|
||||
),
|
||||
"sbas_count": Count(
|
||||
"sbas_exams", filter=Q(sbas_exams__archive=False), distinct=True
|
||||
),
|
||||
"shorts_count": Count(
|
||||
"shorts_exams", filter=Q(shorts_exams__archive=False), distinct=True
|
||||
),
|
||||
}
|
||||
try:
|
||||
from anatomy.models import Exam as AnatomyExam
|
||||
from longs.models import Exam as LongsExam
|
||||
from rapids.models import Exam as RapidsExam
|
||||
from shorts.models import Exam as ShortsExam
|
||||
from physics.models import Exam as PhysicsExam
|
||||
from sbas.models import Exam as SbasExam
|
||||
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
'anatomy_exams',
|
||||
queryset=AnatomyExam.objects.annotate(
|
||||
valid_cid_users_count=Count('valid_cid_users'),
|
||||
valid_user_users_count=Count('valid_user_users'),
|
||||
).prefetch_related('author'),
|
||||
),
|
||||
Prefetch(
|
||||
'longs_exams',
|
||||
queryset=LongsExam.objects.annotate(
|
||||
valid_cid_users_count=Count('valid_cid_users'),
|
||||
valid_user_users_count=Count('valid_user_users'),
|
||||
).prefetch_related('author'),
|
||||
),
|
||||
Prefetch(
|
||||
'rapids_exams',
|
||||
queryset=RapidsExam.objects.annotate(
|
||||
valid_cid_users_count=Count('valid_cid_users'),
|
||||
valid_user_users_count=Count('valid_user_users'),
|
||||
).prefetch_related('author'),
|
||||
),
|
||||
Prefetch(
|
||||
'shorts_exams',
|
||||
queryset=ShortsExam.objects.annotate(
|
||||
valid_cid_users_count=Count('valid_cid_users'),
|
||||
valid_user_users_count=Count('valid_user_users'),
|
||||
).prefetch_related('author'),
|
||||
),
|
||||
Prefetch(
|
||||
'physics_exams',
|
||||
queryset=PhysicsExam.objects.annotate(
|
||||
valid_cid_users_count=Count('valid_cid_users'),
|
||||
valid_user_users_count=Count('valid_user_users'),
|
||||
).prefetch_related('author'),
|
||||
),
|
||||
Prefetch(
|
||||
'sbas_exams',
|
||||
queryset=SbasExam.objects.annotate(
|
||||
valid_cid_users_count=Count('valid_cid_users'),
|
||||
valid_user_users_count=Count('valid_user_users'),
|
||||
).prefetch_related('author'),
|
||||
),
|
||||
'author',
|
||||
)
|
||||
return qs.annotate(**counts)
|
||||
except Exception:
|
||||
# If any app is missing or import fails, fall back to default queryset
|
||||
pass
|
||||
|
||||
return qs
|
||||
return qs
|
||||
|
||||
|
||||
class ExamCollectionEdit(AuthorRequiredMixin, UpdateView):
|
||||
|
||||
Reference in New Issue
Block a user