From 4382c6070ebaffbcb206f5e5ad89d60758dad107 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 16 Feb 2026 12:10:00 +0000 Subject: [PATCH] Enhance exam search widget: add error handling for model retrieval and improve query parsing --- generic/views.py | 82 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/generic/views.py b/generic/views.py index 44213eec..6d12c07e 100644 --- a/generic/views.py +++ b/generic/views.py @@ -6658,7 +6658,14 @@ def exam_search_widget(request): # Support wildcard queries q = q_raw - model = apps.get_model(model_label) + try: + model = apps.get_model(model_label) if model_label else None + except Exception: + model = None + + # If we couldn't resolve a model, return empty fragment rather than raising + if model is None: + return HttpResponse("") # Helper to parse optional boolean GET params def _get_bool_param(name): @@ -6680,14 +6687,73 @@ def exam_search_widget(request): return False # Build queryset - qs = None - field_lookups = ["name__icontains"] - for lookup in field_lookups: - qs = model.objects.filter(**{lookup: q}) - if qs.exists(): - break + qs = model.objects.none() - logger.error(qs) + # Support field-prefixed queries like `name:term`, `code:term`, `id:123`, `date:YYYY-MM-DD`, `type:app` + field_match = re.match(r'^(?Pname|code|id|pk|date|type):\s*(?P.+)$', q, flags=re.I) + def _field_exists(mdl, field_name): + try: + mdl._meta.get_field(field_name) + return True + except Exception: + return False + + if field_match: + f = field_match.group('field').lower() + term = field_match.group('term').strip() + try: + if f in ('id', 'pk'): + qs = model.objects.filter(pk=int(term)) + elif f == 'name': + qs = model.objects.filter(name__icontains=term) + elif f == 'code': + # try a few plausible code field names if present + lookups = [] + for fn in ('code', 'short_code', 'identifier'): + if _field_exists(model, fn): + lookups.append(f"{fn}__icontains") + if lookups: + from django.db.models import Q + qobj = Q() + for lk in lookups: + qobj |= Q(**{lk: term}) + qs = model.objects.filter(qobj) + else: + qs = model.objects.none() + elif f == 'date': + # try a few common date field names + date_lookups = [] + for fn in ('date', 'exam_date', 'start_date'): + if _field_exists(model, fn): + date_lookups.append(fn) + if date_lookups: + from django.db.models import Q + qobj = Q() + for fn in date_lookups: + qobj |= Q(**{f"{fn}__startswith": term}) + qs = model.objects.filter(qobj) + else: + qs = model.objects.none() + elif f == 'type': + # restrict by app_label or model name match + if term.lower() in (model._meta.app_label.lower(), model._meta.model_name.lower()): + qs = model.objects.all() + else: + qs = model.objects.none() + except Exception: + qs = model.objects.none() + else: + # Default: search name and, where available, code-like fields + field_lookups = ["name__icontains"] + for fn in ('code', 'short_code', 'identifier'): + if _field_exists(model, fn): + field_lookups.append(f"{fn}__icontains") + + from django.db.models import Q + qobj = Q() + for lk in field_lookups: + qobj |= Q(**{lk: q}) + qs = model.objects.filter(qobj) # Limit results limit = 50 if q in ("*", "%") else 10