+ {% include 'anatomy/partials/exam_links.html' %}
{% if can_edit %}
diff --git a/anatomy/views.py b/anatomy/views.py
index eec8d58d..f17859d5 100644
--- a/anatomy/views.py
+++ b/anatomy/views.py
@@ -30,6 +30,7 @@ from dal import autocomplete
from django.conf import settings
from django.utils.html import format_html_join
from django.utils.safestring import mark_safe
+from django.template.loader import render_to_string
from .forms import (
@@ -1653,11 +1654,37 @@ def question_add_exam(request, question_id: int):
else:
question.exams.add(exam)
- # return updated fragment
- form = AddExamForm(user=request.user)
- return render(request, "anatomy/partials/exam_edit_fragment.html", {"form": form, "question": question})
+ # return out-of-band swap: update exam links and remove the editor
+ links_html = render_to_string("anatomy/partials/exam_links.html", {"question": question}, request=request)
+ oob_remove = '
'
+ return HttpResponse(mark_safe(links_html + oob_remove))
- # else try to process from the search form
+ # else try to process from the search form or widget-provided values
+ # The ExamSearchWidget may submit a list; accept first value if present.
+ exam_vals = request.POST.getlist('exam') if hasattr(request.POST, 'getlist') else []
+ if not exam_vals:
+ # also accept single 'exam' param
+ single = request.POST.get('exam')
+ if single:
+ exam_vals = [single]
+
+ if exam_vals:
+ try:
+ exam = get_object_or_404(Exam, pk=int(exam_vals[0]))
+ except (ValueError, TypeError):
+ exam = None
+
+ if exam:
+ if request.POST.get("remove", False) == "true":
+ question.exams.remove(exam)
+ else:
+ question.exams.add(exam)
+
+ links_html = render_to_string("anatomy/partials/exam_links.html", {"question": question}, request=request)
+ oob_remove = '
'
+ return HttpResponse(mark_safe(links_html + oob_remove))
+
+ # else try to process via bound form (fallback)
form = AddExamForm(request.POST, user=request.user)
if form.is_valid():
exam = form.cleaned_data.get("exam")
@@ -1666,8 +1693,9 @@ def question_add_exam(request, question_id: int):
else:
question.exams.add(exam)
- form = AddExamForm(user=request.user)
- return render(request, "anatomy/partials/exam_edit_fragment.html", {"form": form, "question": question})
+ links_html = render_to_string("anatomy/partials/exam_links.html", {"question": question}, request=request)
+ oob_remove = '
'
+ return HttpResponse(mark_safe(links_html + oob_remove))
else:
# render form with errors
return render(request, "anatomy/partials/exam_edit_fragment.html", {"form": form, "question": question})
diff --git a/generic/views.py b/generic/views.py
index 6d82ade0..d5d36c76 100644
--- a/generic/views.py
+++ b/generic/views.py
@@ -6675,30 +6675,27 @@ def exam_search_widget(request):
if v is None:
return None
return str(v).lower() in ("1", "true", "yes", "on")
+ # Render combined results, but restrict to exams the user can access
+ def user_can_view_exam(exam, user):
+ if user.is_superuser:
+ return True
+ if exam.open_access:
+ return True
+ authors = exam.get_author_objects()
+ if user in authors:
+ return True
+ if hasattr(exam, "markers") and user in exam.markers.all():
+ return True
+ return False
# Build queryset
qs = None
if model is not None:
- # Try common name-like fields for exams
- from django.db.models import Q
-
- field_lookups = ["name__icontains", "title__icontains", "examination__icontains", "modality__icontains"]
+ field_lookups = ["name__icontains"]
for lookup in field_lookups:
- try:
- qs = model.objects.filter(**{lookup: q})
- if qs.exists():
- break
- except Exception:
- qs = None
- # fallback: try icontains on str() via name if present
- if qs is None or not qs.exists():
- try:
- qs = model.objects.filter(Q(name__icontains=q) | Q(title__icontains=q))
- except Exception:
- try:
- qs = model.objects.all()
- except Exception:
- qs = model.objects.none()
+ qs = model.objects.filter(**{lookup: q})
+ if qs.exists():
+ break
else:
# If no model specified, try to search across several known exam models
possible_models = [
@@ -6738,30 +6735,6 @@ def exam_search_widget(request):
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:
- if user.is_superuser:
- return True
- except Exception:
- pass
- try:
- if exam.open_access:
- return True
- except Exception:
- pass
- try:
- authors = exam.get_author_objects()
- if user in authors:
- 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]:
@@ -6776,42 +6749,12 @@ def exam_search_widget(request):
})
return render(request, "generic/partials/exam_search_widget_results.html", {"results": rendered_results, "field": field, "q": q_raw})
+ logger.error(qs)
+
# Limit results
limit = 50 if q in ("*", "%") else 10
- try:
- qs = qs.order_by("name")[:limit]
- except Exception:
- qs = qs[:limit]
+ qs = qs.order_by("name")[:limit]
- # Restrict results to exams the requesting 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
# Apply optional filters from GET params to the per-model queryset
extra_filters = {}
@@ -6821,17 +6764,14 @@ def exam_search_widget(request):
extra_filters[pname] = val
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'}")
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 getattr(e, k, None) != v:
+ skip = True
+ break
if skip:
continue
if user_can_view_exam(e, request.user):
@@ -6844,6 +6784,8 @@ def exam_search_widget(request):
"candidates_only": getattr(e, "candidates_only", False),
})
+ logger.error(f"Exam search widget: returning {len(results)} results after filtering")
+
return render(request, "generic/partials/exam_search_widget_results.html", {"results": results, "field": field, "q": q_raw})