Enhance exam search widget: refactor query handling to support wildcard searches and improve filter options rendering

This commit is contained in:
Ross
2026-02-16 13:46:09 +00:00
parent 36da045d71
commit 6e30fcc19f
2 changed files with 147 additions and 96 deletions
+65 -63
View File
@@ -6661,10 +6661,6 @@ def exam_search_widget(request):
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
@@ -6687,10 +6683,6 @@ def exam_search_widget(request):
return False
# Build queryset
qs = model.objects.none()
# Support field-prefixed queries like `name:term`, `code:term`, `id:123`, `date:YYYY-MM-DD`, `type:app`
field_match = re.match(r'^(?P<field>name|code|id|pk|date|type):\s*(?P<term>.+)$', q, flags=re.I)
def _field_exists(mdl, field_name):
try:
mdl._meta.get_field(field_name)
@@ -6698,62 +6690,72 @@ def exam_search_widget(request):
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")
qs = model.objects.none()
from django.db.models import Q
qobj = Q()
for lk in field_lookups:
qobj |= Q(**{lk: q})
qs = model.objects.filter(qobj)
# Support field-prefixed queries like `name:term`, `code:term`, `id:123`, `date:YYYY-MM-DD`.
# Treat '*' or '%' as a wildcard meaning "match everything" for the model.
field_match = re.match(r'^(?P<field>name|code|id|pk|date|type):\s*(?P<term>.+)$', q, flags=re.I)
# Wildcard query: return all objects for this model (filters still apply later)
if q in ("*", "%"):
qs = model.objects.all()
else:
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