From c61f92ae9e77a8ff86dfff19d5e3f97d2fbf4ec2 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 16 Feb 2026 13:49:17 +0000 Subject: [PATCH] Refactor exam search widget: streamline filtering logic and enhance queryset efficiency --- generic/views.py | 44 ++++++++++++++++++++++---------------------- generic/widgets.py | 11 ----------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/generic/views.py b/generic/views.py index 5abf9171..f02f4964 100644 --- a/generic/views.py +++ b/generic/views.py @@ -6757,11 +6757,6 @@ def exam_search_widget(request): qobj |= Q(**{lk: q}) qs = model.objects.filter(qobj) - # Limit results - limit = 50 if q in ("*", "%") else 10 - qs = qs.order_by("name")[:limit] - - # Apply optional filters from GET params to the per-model queryset # Only include filters that were supplied in GET _and_ where the model # actually exposes the field. This prevents models without a given @@ -6777,26 +6772,31 @@ def exam_search_widget(request): continue extra_filters[pname] = val + # Apply filters at the queryset level for efficiency + if extra_filters: + try: + qs = qs.filter(**extra_filters) + except Exception: + # If a filter cannot be applied at DB level, fall back to leaving qs unchanged + pass + + # Limit results (wildcard returns up to 50) + limit = 50 if q in ("*", "%") else 10 + qs = qs.order_by("name")[:limit] + results = [] - logger.error(f"Exam search widget: model={model}, q='{q}', extra_filters={extra_filters}, initial_count={qs.count() if qs is not None else 'N/A'}") + logger.error(f"Exam search widget: model={model}, q='{q}', extra_filters={extra_filters}, post_filter_count={qs.count() if qs is not None else 'N/A'}") for e in qs: - # apply any extra_filters defensively (models may not have fields) - skip = False - for k, v in extra_filters.items(): - if getattr(e, k, None) != v: - skip = True - break - if skip: + if not user_can_view_exam(e, request.user): continue - if user_can_view_exam(e, request.user): - results.append({ - "id": e.pk, - "text": str(e), - "archive": getattr(e, "archive", False), - "open_access": getattr(e, "open_access", False), - "exam_mode": getattr(e, "exam_mode", False), - "candidates_only": getattr(e, "candidates_only", False), - }) + results.append({ + "id": e.pk, + "text": str(e), + "archive": getattr(e, "archive", False), + "open_access": getattr(e, "open_access", False), + "exam_mode": getattr(e, "exam_mode", False), + "candidates_only": getattr(e, "candidates_only", False), + }) logger.error(f"Exam search widget: returning {len(results)} results after filtering") diff --git a/generic/widgets.py b/generic/widgets.py index 5142ae53..10c682f1 100644 --- a/generic/widgets.py +++ b/generic/widgets.py @@ -564,17 +564,6 @@ class ExamSearchSelectMultipleWidget: return {{ cleaned: cleaned, filters: filters }}; }} - function parseFilterTokens(q) {{ - const filters = {{}}; - if (!q) return {{ cleaned: '', filters: filters }}; - let cleaned = q.replace(tokenRe, function(m, name, val) {{ - filters[name] = /^(1|true|yes)$/i.test(val); - return ' '; - }}); - cleaned = cleaned.replace(/\s+/g,' ').trim(); - return {{ cleaned: cleaned, filters: filters }}; - }} - function fetchResults(rawQ) {{ const parsed = parseFilterTokens(rawQ || ''); const q = parsed.cleaned;