From dfdba4cce61f3ac34e0202175fcff9da93ef070b Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 5 Nov 2025 22:19:24 +0000 Subject: [PATCH] Add question search functionality with HTMX support for dynamic results --- .../partials/_question_search_results.html | 37 ++++++++++++++ sbas/templates/sbas/search.html | 41 ++++++++++++++++ sbas/urls.py | 2 + sbas/views.py | 48 +++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 sbas/templates/sbas/partials/_question_search_results.html create mode 100644 sbas/templates/sbas/search.html diff --git a/sbas/templates/sbas/partials/_question_search_results.html b/sbas/templates/sbas/partials/_question_search_results.html new file mode 100644 index 00000000..a4eb2b80 --- /dev/null +++ b/sbas/templates/sbas/partials/_question_search_results.html @@ -0,0 +1,37 @@ +{# partial: list of questions for HTMX search results #} +
+{% if questions %} + {% for question in questions %} + +
+
{{ question.title|default:question.get_stem_stripped|truncatechars:80 }}
+ #{{ question.pk }} +
+ +
+ {% if question.author.all %} + By {{ question.author.all|join:", " }} + {% endif %} + {% if question.category %}{{ question.category.category }}{% endif %} + {% if question.frcr_appropriate %}FRCR{% endif %} +
+ +
+ {% for c in question.condition.all|slice:":3" %} + {{ c.name }} + {% endfor %} + {% for p in question.presentation.all|slice:":3" %} + {{ p.name }} + {% endfor %} + {% for s in question.subspecialty.all|slice:":3" %} + {{ s.name }} + {% endfor %} +
+ +

{{ question.stem|striptags|truncatechars:160 }}

+
+ {% endfor %} +{% else %} +
No questions found
+{% endif %} +
diff --git a/sbas/templates/sbas/search.html b/sbas/templates/sbas/search.html new file mode 100644 index 00000000..852a1ee0 --- /dev/null +++ b/sbas/templates/sbas/search.html @@ -0,0 +1,41 @@ +{% extends 'sbas/base.html' %} + +{% block content %} +
+
+
+

SBAs Question Search

+
+
+
+
+
+
+
Search questions
+
+
+ + +
+
+ +
+ {% include 'sbas/partials/_question_search_results.html' with questions=None %} +
+
+
+
+
+
+{% endblock %} diff --git a/sbas/urls.py b/sbas/urls.py index 91d35feb..cf911d4d 100644 --- a/sbas/urls.py +++ b/sbas/urls.py @@ -115,6 +115,8 @@ urlpatterns = [ views.question_overview, name="question_overview", ), + path("search/", views.question_search_page, name="question_search_page"), + path("search/questions/", views.question_search_partial, name="question_search_partial"), path("question//toggle_frcr/", views.toggle_frcr_appropriate, name="toggle_frcr"), path("category/merge/", views.merge_category, name="merge_category"), #path( diff --git a/sbas/views.py b/sbas/views.py index cddea9ce..5941aa14 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -120,6 +120,54 @@ def active_exams(request): return render(request, "sbas/available_exam_list.html", {"exams": active_exams}) +@login_required +def question_search_page(request): + """Render the SBAs search page which contains the HTMX search input. + + The actual results are returned by `question_search_partial` and swapped + into the results container via HTMX. + """ + return render(request, "sbas/search.html", {}) + + +@login_required +def question_search_partial(request): + """Return an HTMX partial listing questions matching `q`. + + Searches several Question fields and related names. Uses `distinct()` to + avoid duplicates from joins. The template expects `questions` in context. + """ + q = request.GET.get("q", "") or request.GET.get("sbas-question-search-input", "") + q = q.strip() + + qs = Question.objects.all() + + if q: + from django.db.models import Q + + qs = qs.filter( + Q(title__icontains=q) + | Q(stem__icontains=q) + | Q(a_answer__icontains=q) + | Q(b_answer__icontains=q) + | Q(c_answer__icontains=q) + | Q(d_answer__icontains=q) + | Q(e_answer__icontains=q) + | Q(category__category__icontains=q) + | Q(author__first_name__icontains=q) + | Q(author__last_name__icontains=q) + | Q(author__username__icontains=q) + | Q(condition__name__icontains=q) + | Q(presentation__name__icontains=q) + | Q(subspecialty__name__icontains=q) + ).distinct() + + # Prefetch related data used in the partial to avoid N+1 queries + qs = qs.select_related("category").prefetch_related("author", "condition", "presentation", "subspecialty")[:50] + + return render(request, "sbas/partials/_question_search_results.html", {"questions": qs}) + + def exam_start(request, pk): exam = get_object_or_404(Exam, pk=pk)