From 15791fa8832db1120be81877e76842dc0844fd07 Mon Sep 17 00:00:00 2001 From: Ross Date: Tue, 28 Oct 2025 08:35:03 +0000 Subject: [PATCH] Enhance question overview filters to support multi-selection for categories, findings, structures, conditions, subspecialties, and presentations --- sbas/templates/sbas/question_overview.html | 105 ++++++------ sbas/views.py | 180 ++++++++++++--------- 2 files changed, 158 insertions(+), 127 deletions(-) diff --git a/sbas/templates/sbas/question_overview.html b/sbas/templates/sbas/question_overview.html index c682f8e5..01376885 100644 --- a/sbas/templates/sbas/question_overview.html +++ b/sbas/templates/sbas/question_overview.html @@ -19,10 +19,9 @@
- {% for c in categories %} - + {% endfor %}
@@ -38,54 +37,60 @@
-
- - -
+
+

+ +

+
+
+
+
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- - +
+ + +
+
+
+
@@ -99,10 +104,10 @@ {% if counts %}
    - {% for name, count in counts %} + {% for row in counts %}
  • -
    {{ name }}
    -
    {{ count }}
    + +
    {{ row.count }}
  • {% endfor %}
diff --git a/sbas/views.py b/sbas/views.py index 70ed4835..5c7e3225 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -531,15 +531,15 @@ def question_overview(request): status = request.GET.get("status") or QuestionReview.StatusChoices.ACCEPTED group_by = request.GET.get("group_by") or "category" - category_filter = request.GET.get("category") + # allow multi-selects for category and atlas filters + category_filters = request.GET.getlist("category") open_access = request.GET.get("open_access") - # Atlas model filters (single-select for now) - finding_filter = request.GET.get("finding") - structure_filter = request.GET.get("structure") - condition_filter = request.GET.get("condition") - subspecialty_filter = request.GET.get("subspecialty") - presentation_filter = request.GET.get("presentation") + finding_filters = request.GET.getlist("finding") + structure_filters = request.GET.getlist("structure") + condition_filters = request.GET.getlist("condition") + subspecialty_filters = request.GET.getlist("subspecialty") + presentation_filters = request.GET.getlist("presentation") qs = Question.objects.all().order_by("pk") @@ -552,10 +552,12 @@ def question_overview(request): except Exception: pass - # Optional category filter - if category_filter: + # Optional category filter (multi) + if category_filters: try: - qs = qs.filter(category__id=int(category_filter)) + ids = [int(x) for x in category_filters if x] + if ids: + qs = qs.filter(category__id__in=ids) except Exception: pass @@ -563,89 +565,113 @@ def question_overview(request): if open_access in ("0", "1"): qs = qs.filter(open_access=(open_access == "1")) - # Optional atlas M2M filters + # Optional atlas M2M filters (multi-select) try: - if finding_filter: - qs = qs.filter(finding__id=int(finding_filter)) + finding_ids = [int(x) for x in finding_filters if x] + if finding_ids: + qs = qs.filter(finding__id__in=finding_ids) except Exception: - pass + finding_ids = [] try: - if structure_filter: - qs = qs.filter(structure__id=int(structure_filter)) + structure_ids = [int(x) for x in structure_filters if x] + if structure_ids: + qs = qs.filter(structure__id__in=structure_ids) except Exception: - pass + structure_ids = [] try: - if condition_filter: - qs = qs.filter(condition__id=int(condition_filter)) + condition_ids = [int(x) for x in condition_filters if x] + if condition_ids: + qs = qs.filter(condition__id__in=condition_ids) except Exception: - pass + condition_ids = [] try: - if subspecialty_filter: - qs = qs.filter(subspecialty__id=int(subspecialty_filter)) + subspecialty_ids = [int(x) for x in subspecialty_filters if x] + if subspecialty_ids: + qs = qs.filter(subspecialty__id__in=subspecialty_ids) except Exception: - pass + subspecialty_ids = [] try: - if presentation_filter: - qs = qs.filter(presentation__id=int(presentation_filter)) + presentation_ids = [int(x) for x in presentation_filters if x] + if presentation_ids: + qs = qs.filter(presentation__id__in=presentation_ids) except Exception: - pass - - # Build counts by category (default) - counts = {} - total = 0 + presentation_ids = [] + # Optimize status filtering with a DB subquery to get latest review status per question from django.contrib.contenttypes.models import ContentType + from django.db.models import OuterRef, Subquery, Count - # Import atlas models for populating filter choices - from atlas.models import Finding, Structure, Condition as AtlasCondition, Subspecialty, Presentation as AtlasPresentation + ct = ContentType.objects.get_for_model(Question) + latest_status_subq = QuestionReview.objects.filter(content_type=ct, object_id=OuterRef("pk")).order_by("-created_on").values("status")[:1] + qs = qs.annotate(latest_review_status=Subquery(latest_status_subq)) - for question in qs: - # Respect authors_only flag - try: - if question.authors_only and not ( - request.user.is_superuser or request.user in question.author.all() - ): - continue - except Exception: - pass + if status == "ANY": + pass + elif status == "UNREVIEWED": + qs = qs.filter(latest_review_status__isnull=True) + else: + qs = qs.filter(latest_review_status=status) - ct = ContentType.objects.get_for_model(question) - latest = ( - QuestionReview.objects.filter(content_type=ct, object_id=question.pk) - .order_by("-created_on") - .first() - ) + # Respect authors_only: non-owners should be excluded + # We applied permission filter earlier; ensure authors_only exclusion + from django.db.models import Q + try: + if not request.user.is_superuser: + qs = qs.exclude(Q(authors_only=True) & ~Q(author__in=[request.user])) + except Exception: + pass - # Determine whether this question matches requested status - match = False - if status == "ANY": - match = True - elif status == "UNREVIEWED": - match = latest is None - else: - if latest is not None and latest.status == status: - match = True + # Aggregate counts grouped by category (DB-side) + grouped = qs.values("category__id", "category__category").annotate(count=Count("pk")).order_by("-count", "category__category") - if not match: - continue + counts_rows = [] + total = qs.count() - # Grouping - if group_by == "category": - key = question.category.category if question.category else "Uncategorized" - counts[key] = counts.get(key, 0) + 1 - else: - # fallback to category grouping if unknown - key = question.category.category if question.category else "Uncategorized" - counts[key] = counts.get(key, 0) + 1 + # Build per-row link (to question list) preserving current filters but swapping category + from urllib.parse import urlencode + from django.urls import reverse - total += 1 + # Convert selected filters to strings for template membership tests + selected_category = [str(x) for x in category_filters] + selected_findings = [str(x) for x in finding_filters] + selected_structures = [str(x) for x in structure_filters] + selected_conditions = [str(x) for x in condition_filters] + selected_subspecialties = [str(x) for x in subspecialty_filters] + selected_presentations = [str(x) for x in presentation_filters] - # Sort counts by descending - sorted_counts = sorted(counts.items(), key=lambda x: (-x[1], x[0])) + for row in grouped: + cat_id = row.get("category__id") + cat_name = row.get("category__category") or "Uncategorized" + count = row.get("count") - categories = Category.objects.all().order_by("category") + params = [] + # category param points to this row + if cat_id: + params.append(("category", str(cat_id))) + # preserve other filters + if status: + params.append(("status", status)) + if open_access is not None and open_access != "": + params.append(("open_access", open_access)) + for fid in selected_findings: + params.append(("finding", fid)) + for sid in selected_structures: + params.append(("structure", sid)) + for cid in selected_conditions: + params.append(("condition", cid)) + for ss in selected_subspecialties: + params.append(("subspecialty", ss)) + for p in selected_presentations: + params.append(("presentation", p)) + + link = reverse("sbas:question_view") + ("?" + urlencode(params, doseq=True) if params else "") + + counts_rows.append({"cat_id": cat_id or "", "cat_name": cat_name, "count": count, "link": link}) # Populate atlas filter lists + from atlas.models import Finding, Structure, Condition as AtlasCondition, Subspecialty, Presentation as AtlasPresentation + + categories = Category.objects.all().order_by("category") findings = Finding.objects.all().order_by("name") structures = Structure.objects.all().order_by("name") conditions = AtlasCondition.objects.all().order_by("name") @@ -656,21 +682,21 @@ def question_overview(request): "status_options": status_options, "selected_status": status, "group_by": group_by, - "counts": sorted_counts, + "counts": counts_rows, "total": total, "categories": categories, - "selected_category": category_filter, + "selected_category": selected_category, "selected_open_access": open_access, "findings": findings, "structures": structures, "conditions": conditions, "subspecialties": subspecialties, "presentations": presentations, - "selected_finding": finding_filter, - "selected_structure": structure_filter, - "selected_condition": condition_filter, - "selected_subspecialty": subspecialty_filter, - "selected_presentation": presentation_filter, + "selected_findings": selected_findings, + "selected_structures": selected_structures, + "selected_conditions": selected_conditions, + "selected_subspecialties": selected_subspecialties, + "selected_presentations": selected_presentations, } return render(request, "sbas/question_overview.html", context)