diff --git a/generic/views.py b/generic/views.py
index d9c7db91..8e26e074 100644
--- a/generic/views.py
+++ b/generic/views.py
@@ -6084,6 +6084,13 @@ def exam_search_widget(request):
except Exception:
model = None
+ # Helper to parse optional boolean GET params
+ def _get_bool_param(name):
+ v = request.GET.get(name)
+ if v is None:
+ return None
+ return str(v).lower() in ("1", "true", "yes", "on")
+
# Build queryset
qs = None
if model is not None:
@@ -6118,20 +6125,34 @@ def exam_search_widget(request):
"sbas.Exam",
]
results = []
+
+ # gather optional filters from GET params
+ extra_filters = {}
+ for pname in ("archive", "open_access", "exam_mode", "candidates_only"):
+ val = _get_bool_param(pname)
+ if val is not None:
+ extra_filters[pname] = val
+
for ml in possible_models:
try:
m = apps.get_model(*ml.split("."))
except Exception:
continue
try:
- rqs = m.objects.filter(name__icontains=q)[:5]
- except Exception:
+ # try common name-like fields and apply extra_filters when possible
try:
- rqs = m.objects.filter(title__icontains=q)[:5]
+ rqs = m.objects.filter(name__icontains=q, **extra_filters)[:5]
except Exception:
- rqs = m.objects.none()
+ try:
+ rqs = m.objects.filter(title__icontains=q, **extra_filters)[:5]
+ except Exception:
+ # fallback to unfiltered small slice
+ rqs = m.objects.filter(name__icontains=q)[:5]
+ except Exception:
+ rqs = m.objects.none()
for o in rqs:
results.append(o)
+
# Render combined results, but restrict to exams the user can access
def user_can_view_exam(exam, user):
try:
@@ -6160,7 +6181,14 @@ def exam_search_widget(request):
rendered_results = []
for o in results[:50]:
if user_can_view_exam(o, request.user):
- rendered_results.append({"id": getattr(o, "pk", None), "text": str(o)})
+ rendered_results.append({
+ "id": getattr(o, "pk", None),
+ "text": str(o),
+ "archive": getattr(o, "archive", False),
+ "open_access": getattr(o, "open_access", False),
+ "exam_mode": getattr(o, "exam_mode", False),
+ "candidates_only": getattr(o, "candidates_only", False),
+ })
return render(request, "generic/partials/exam_search_widget_results.html", {"results": rendered_results, "field": field, "q": q_raw})
# Limit results
@@ -6200,10 +6228,36 @@ def exam_search_widget(request):
pass
return False
+ # Apply optional filters from GET params to the per-model queryset
+ extra_filters = {}
+ for pname in ("archive", "open_access", "exam_mode", "candidates_only"):
+ val = _get_bool_param(pname)
+ if val is not None:
+ extra_filters[pname] = val
+
results = []
for e in qs:
+ # apply any extra_filters defensively (models may not have fields)
+ skip = False
+ for k, v in extra_filters.items():
+ try:
+ if getattr(e, k, None) != v:
+ skip = True
+ break
+ except Exception:
+ # if attribute access fails, don't skip
+ continue
+ if skip:
+ continue
if user_can_view_exam(e, request.user):
- results.append({"id": e.pk, "text": str(e)})
+ 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),
+ })
return render(request, "generic/partials/exam_search_widget_results.html", {"results": results, "field": field, "q": q_raw})
@@ -6223,14 +6277,85 @@ def supervisor_search(request):
q = (request.GET.get("q") or "").strip()
row = request.GET.get("row")
if not q:
- return HttpResponse("")
+ else:
+ # If no model specified, try to search across several known exam models
+ possible_models = [
+ "anatomy.Exam",
+ "longs.Exam",
+ "rapids.Exam",
+ "shorts.Exam",
+ "physics.Exam",
+ "sbas.Exam",
+ ]
+ results = []
- supervisors_qs = Supervisor.objects.filter(
- Q(name__icontains=q) | Q(email__icontains=q)
- ).order_by("name")[:10]
+ # gather optional filters from GET params
+ extra_filters = {}
+ for pname in ("archive", "open_access", "exam_mode", "candidates_only"):
+ val = _get_bool_param(pname)
+ if val is not None:
+ extra_filters[pname] = val
- results = [{"id": s.pk, "text": f"{s.name} - {s.email or ''}"} for s in supervisors_qs]
+ for ml in possible_models:
+ try:
+ m = apps.get_model(*ml.split("."))
+ except Exception:
+ continue
+ try:
+ # try common name-like fields and apply extra_filters when possible
+ try:
+ rqs = m.objects.filter(name__icontains=q, **extra_filters)[:5]
+ except Exception:
+ try:
+ rqs = m.objects.filter(title__icontains=q, **extra_filters)[:5]
+ except Exception:
+ # fallback to unfiltered small slice
+ rqs = m.objects.filter(name__icontains=q)[:5]
+ except Exception:
+ rqs = m.objects.none()
+ for o in rqs:
+ results.append(o)
- return render(request, "generic/partials/supervisor_search_results.html", {"results": results, "row": row, "q": q})
+ # Render combined results, but restrict to exams the user can access
+ def user_can_view_exam(exam, user):
+ try:
+ if user.is_superuser:
+ return True
+ except Exception:
+ pass
+ try:
+ if getattr(exam, "open_access", False) and getattr(exam, "active", False):
+ return True
+ except Exception:
+ pass
+ try:
+ authors = exam.get_author_objects()
+ if user in authors:
+ return True
+ except Exception:
+ pass
+ try:
+ if hasattr(exam, "cid_user_exam") and exam.cid_user_exam.filter(user_user=user).exists():
+ return True
+ except Exception:
+ pass
+ try:
+ if hasattr(exam, "markers") and user in exam.markers.all():
+ return True
+ except Exception:
+ pass
+ return False
+
+ rendered_results = []
+ for o in results[:50]:
+ if user_can_view_exam(o, request.user):
+ rendered_results.append({
+ "id": getattr(o, "pk", None),
+ "text": str(o),
+ "archive": getattr(o, "archive", False),
+ "open_access": getattr(o, "open_access", False),
+ "exam_mode": getattr(o, "exam_mode", False),
+ "candidates_only": getattr(o, "candidates_only", False),
+ })
+ return render(request, "generic/partials/exam_search_widget_results.html", {"results": rendered_results, "field": field, "q": q_raw})
-
\ No newline at end of file
diff --git a/generic/widgets.py b/generic/widgets.py
index ca86d03c..a9a5d458 100644
--- a/generic/widgets.py
+++ b/generic/widgets.py
@@ -353,6 +353,27 @@ class ExamSearchSelectMultipleWidget:
Advanced search tips
+
- Basic: type any part of an exam's name, code or identifier (case-insensitive substring match).
- Field-specific searches: prefix with
field:term to restrict the search. Supported fields:
@@ -364,7 +385,6 @@ class ExamSearchSelectMultipleWidget:
type: — narrow to an exam type (e.g. type:anatomy).
-
Type filter: use the dropdown to restrict results to a specific exam type in addition to your query.
To add exams, click the Add button beside a result. Use Add all results to import visible matches.
Wildcard: use * to broaden results; wildcards may return more results and are rate-limited.
@@ -396,8 +416,17 @@ class ExamSearchSelectMultipleWidget:
results.innerHTML = '';
return;
}}
- const modelParam = defaultModel ? '&model=' + encodeURIComponent(defaultModel) : '';
- const url = '{url}?field=' + encodeURIComponent(widgetFieldName) + '&q=' + encodeURIComponent(q) + modelParam;
+ const modelParam = defaultModel ? '&model=' + encodeURIComponent(defaultModel) : '';
+ const f_archive = document.getElementById('filter_archive_{self.name}');
+ const f_open = document.getElementById('filter_open_access_{self.name}');
+ const f_exam_mode = document.getElementById('filter_exam_mode_{self.name}');
+ const f_candidates = document.getElementById('filter_candidates_only_{self.name}');
+ let extra = '';
+ if (f_archive && f_archive.checked) extra += '&archive=1';
+ if (f_open && f_open.checked) extra += '&open_access=1';
+ if (f_exam_mode && f_exam_mode.checked) extra += '&exam_mode=1';
+ if (f_candidates && f_candidates.checked) extra += '&candidates_only=1';
+ const url = '{url}?field=' + encodeURIComponent(widgetFieldName) + '&q=' + encodeURIComponent(q) + modelParam + extra;
fetch(url)
.then(r => r.text())
.then(html => {{ results.innerHTML = html; }});