From f2eaf6197e55ec0332e320401d15c02bbdf6837f Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 16 Feb 2026 11:47:49 +0000 Subject: [PATCH] Refactor exam management: update exam edit form and add exam links partial --- .../anatomy/partials/exam_edit_fragment.html | 5 +- .../anatomy/partials/exam_links.html | 9 ++ .../templates/anatomy/question_detail.html | 8 +- anatomy/views.py | 40 ++++++- generic/views.py | 108 ++++-------------- 5 files changed, 72 insertions(+), 98 deletions(-) create mode 100644 anatomy/templates/anatomy/partials/exam_links.html diff --git a/anatomy/templates/anatomy/partials/exam_edit_fragment.html b/anatomy/templates/anatomy/partials/exam_edit_fragment.html index 36da3249..ca4185d6 100644 --- a/anatomy/templates/anatomy/partials/exam_edit_fragment.html +++ b/anatomy/templates/anatomy/partials/exam_edit_fragment.html @@ -1,10 +1,11 @@ {% load static %}
-
+ {% csrf_token %} + {{ form.media }}
{{ form.exam }} - +
diff --git a/anatomy/templates/anatomy/partials/exam_links.html b/anatomy/templates/anatomy/partials/exam_links.html new file mode 100644 index 00000000..6a19be84 --- /dev/null +++ b/anatomy/templates/anatomy/partials/exam_links.html @@ -0,0 +1,9 @@ + diff --git a/anatomy/templates/anatomy/question_detail.html b/anatomy/templates/anatomy/question_detail.html index dd4c983c..b1f217a5 100644 --- a/anatomy/templates/anatomy/question_detail.html +++ b/anatomy/templates/anatomy/question_detail.html @@ -156,13 +156,7 @@
Exams
-
- {% for exam in question.exams.all %} - {{ exam }} - {% empty %} - No exams - {% endfor %} -
+ {% 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})