Enhance question overview filters to support multi-selection for categories, findings, structures, conditions, subspecialties, and presentations
This commit is contained in:
@@ -19,10 +19,9 @@
|
|||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label class="form-label small">Category</label>
|
<label class="form-label small">Category</label>
|
||||||
<select name="category" class="form-select form-select-sm">
|
<select name="category" multiple class="form-select form-select-sm" size="4">
|
||||||
<option value="">(All)</option>
|
|
||||||
{% for c in categories %}
|
{% for c in categories %}
|
||||||
<option value="{{ c.id }}" {% if selected_category|default:'' == c.id|stringformat:"s" %}selected{% endif %}>{{ c.category }}</option>
|
<option value="{{ c.id }}" {% if c.id|stringformat:"s" in selected_category %}selected{% endif %}>{{ c.category }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,54 +37,60 @@
|
|||||||
|
|
||||||
<div class="w-100"></div>
|
<div class="w-100"></div>
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-6">
|
<div class="col-12">
|
||||||
<label class="form-label small">Finding</label>
|
<p>
|
||||||
<select name="finding" class="form-select form-select-sm">
|
<a class="btn btn-sm btn-outline-secondary" data-bs-toggle="collapse" href="#advanced-filters" role="button" aria-expanded="false" aria-controls="advanced-filters">Advanced filters</a>
|
||||||
<option value="">(Any)</option>
|
</p>
|
||||||
{% for f in findings %}
|
<div class="collapse" id="advanced-filters">
|
||||||
<option value="{{ f.id }}" {% if selected_finding|default:'' == f.id|stringformat:"s" %}selected{% endif %}>{{ f.name }}</option>
|
<div class="card card-body">
|
||||||
{% endfor %}
|
<div class="row g-2">
|
||||||
</select>
|
<div class="col-md-4 col-sm-6">
|
||||||
</div>
|
<label class="form-label small">Finding</label>
|
||||||
|
<select name="finding" multiple class="form-select form-select-sm" size="6">
|
||||||
|
{% for f in findings %}
|
||||||
|
<option value="{{ f.id }}" {% if f.id|stringformat:"s" in selected_findings %}selected{% endif %}>{{ f.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-6">
|
<div class="col-md-4 col-sm-6">
|
||||||
<label class="form-label small">Structure</label>
|
<label class="form-label small">Structure</label>
|
||||||
<select name="structure" class="form-select form-select-sm">
|
<select name="structure" multiple class="form-select form-select-sm" size="6">
|
||||||
<option value="">(Any)</option>
|
{% for s in structures %}
|
||||||
{% for s in structures %}
|
<option value="{{ s.id }}" {% if s.id|stringformat:"s" in selected_structures %}selected{% endif %}>{{ s.name }}</option>
|
||||||
<option value="{{ s.id }}" {% if selected_structure|default:'' == s.id|stringformat:"s" %}selected{% endif %}>{{ s.name }}</option>
|
{% endfor %}
|
||||||
{% endfor %}
|
</select>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-6">
|
<div class="col-md-4 col-sm-6">
|
||||||
<label class="form-label small">Condition</label>
|
<label class="form-label small">Condition</label>
|
||||||
<select name="condition" class="form-select form-select-sm">
|
<select name="condition" multiple class="form-select form-select-sm" size="6">
|
||||||
<option value="">(Any)</option>
|
{% for c in conditions %}
|
||||||
{% for c in conditions %}
|
<option value="{{ c.id }}" {% if c.id|stringformat:"s" in selected_conditions %}selected{% endif %}>{{ c.name }}</option>
|
||||||
<option value="{{ c.id }}" {% if selected_condition|default:'' == c.id|stringformat:"s" %}selected{% endif %}>{{ c.name }}</option>
|
{% endfor %}
|
||||||
{% endfor %}
|
</select>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-6">
|
<div class="col-md-4 col-sm-6">
|
||||||
<label class="form-label small">Subspecialty</label>
|
<label class="form-label small">Subspecialty</label>
|
||||||
<select name="subspecialty" class="form-select form-select-sm">
|
<select name="subspecialty" multiple class="form-select form-select-sm" size="6">
|
||||||
<option value="">(Any)</option>
|
{% for ss in subspecialties %}
|
||||||
{% for ss in subspecialties %}
|
<option value="{{ ss.id }}" {% if ss.id|stringformat:"s" in selected_subspecialties %}selected{% endif %}>{{ ss.name }}</option>
|
||||||
<option value="{{ ss.id }}" {% if selected_subspecialty|default:'' == ss.id|stringformat:"s" %}selected{% endif %}>{{ ss.name }}</option>
|
{% endfor %}
|
||||||
{% endfor %}
|
</select>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-6">
|
<div class="col-md-4 col-sm-6">
|
||||||
<label class="form-label small">Presentation</label>
|
<label class="form-label small">Presentation</label>
|
||||||
<select name="presentation" class="form-select form-select-sm">
|
<select name="presentation" multiple class="form-select form-select-sm" size="6">
|
||||||
<option value="">(Any)</option>
|
{% for p in presentations %}
|
||||||
{% for p in presentations %}
|
<option value="{{ p.id }}" {% if p.id|stringformat:"s" in selected_presentations %}selected{% endif %}>{{ p.name }}</option>
|
||||||
<option value="{{ p.id }}" {% if selected_presentation|default:'' == p.id|stringformat:"s" %}selected{% endif %}>{{ p.name }}</option>
|
{% endfor %}
|
||||||
{% endfor %}
|
</select>
|
||||||
</select>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto align-self-end">
|
<div class="col-auto align-self-end">
|
||||||
@@ -99,10 +104,10 @@
|
|||||||
|
|
||||||
{% if counts %}
|
{% if counts %}
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
{% for name, count in counts %}
|
{% for row in counts %}
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<div>{{ name }}</div>
|
<div><a href="{{ row.link }}">{{ row.cat_name }}</a></div>
|
||||||
<div class="text-muted">{{ count }}</div>
|
<div class="text-muted">{{ row.count }}</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+103
-77
@@ -531,15 +531,15 @@ def question_overview(request):
|
|||||||
|
|
||||||
status = request.GET.get("status") or QuestionReview.StatusChoices.ACCEPTED
|
status = request.GET.get("status") or QuestionReview.StatusChoices.ACCEPTED
|
||||||
group_by = request.GET.get("group_by") or "category"
|
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")
|
open_access = request.GET.get("open_access")
|
||||||
|
|
||||||
# Atlas model filters (single-select for now)
|
finding_filters = request.GET.getlist("finding")
|
||||||
finding_filter = request.GET.get("finding")
|
structure_filters = request.GET.getlist("structure")
|
||||||
structure_filter = request.GET.get("structure")
|
condition_filters = request.GET.getlist("condition")
|
||||||
condition_filter = request.GET.get("condition")
|
subspecialty_filters = request.GET.getlist("subspecialty")
|
||||||
subspecialty_filter = request.GET.get("subspecialty")
|
presentation_filters = request.GET.getlist("presentation")
|
||||||
presentation_filter = request.GET.get("presentation")
|
|
||||||
|
|
||||||
qs = Question.objects.all().order_by("pk")
|
qs = Question.objects.all().order_by("pk")
|
||||||
|
|
||||||
@@ -552,10 +552,12 @@ def question_overview(request):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Optional category filter
|
# Optional category filter (multi)
|
||||||
if category_filter:
|
if category_filters:
|
||||||
try:
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -563,89 +565,113 @@ def question_overview(request):
|
|||||||
if open_access in ("0", "1"):
|
if open_access in ("0", "1"):
|
||||||
qs = qs.filter(open_access=(open_access == "1"))
|
qs = qs.filter(open_access=(open_access == "1"))
|
||||||
|
|
||||||
# Optional atlas M2M filters
|
# Optional atlas M2M filters (multi-select)
|
||||||
try:
|
try:
|
||||||
if finding_filter:
|
finding_ids = [int(x) for x in finding_filters if x]
|
||||||
qs = qs.filter(finding__id=int(finding_filter))
|
if finding_ids:
|
||||||
|
qs = qs.filter(finding__id__in=finding_ids)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
finding_ids = []
|
||||||
try:
|
try:
|
||||||
if structure_filter:
|
structure_ids = [int(x) for x in structure_filters if x]
|
||||||
qs = qs.filter(structure__id=int(structure_filter))
|
if structure_ids:
|
||||||
|
qs = qs.filter(structure__id__in=structure_ids)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
structure_ids = []
|
||||||
try:
|
try:
|
||||||
if condition_filter:
|
condition_ids = [int(x) for x in condition_filters if x]
|
||||||
qs = qs.filter(condition__id=int(condition_filter))
|
if condition_ids:
|
||||||
|
qs = qs.filter(condition__id__in=condition_ids)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
condition_ids = []
|
||||||
try:
|
try:
|
||||||
if subspecialty_filter:
|
subspecialty_ids = [int(x) for x in subspecialty_filters if x]
|
||||||
qs = qs.filter(subspecialty__id=int(subspecialty_filter))
|
if subspecialty_ids:
|
||||||
|
qs = qs.filter(subspecialty__id__in=subspecialty_ids)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
subspecialty_ids = []
|
||||||
try:
|
try:
|
||||||
if presentation_filter:
|
presentation_ids = [int(x) for x in presentation_filters if x]
|
||||||
qs = qs.filter(presentation__id=int(presentation_filter))
|
if presentation_ids:
|
||||||
|
qs = qs.filter(presentation__id__in=presentation_ids)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
presentation_ids = []
|
||||||
|
|
||||||
# Build counts by category (default)
|
|
||||||
counts = {}
|
|
||||||
total = 0
|
|
||||||
|
|
||||||
|
# Optimize status filtering with a DB subquery to get latest review status per question
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import OuterRef, Subquery, Count
|
||||||
|
|
||||||
# Import atlas models for populating filter choices
|
ct = ContentType.objects.get_for_model(Question)
|
||||||
from atlas.models import Finding, Structure, Condition as AtlasCondition, Subspecialty, Presentation as AtlasPresentation
|
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:
|
if status == "ANY":
|
||||||
# Respect authors_only flag
|
pass
|
||||||
try:
|
elif status == "UNREVIEWED":
|
||||||
if question.authors_only and not (
|
qs = qs.filter(latest_review_status__isnull=True)
|
||||||
request.user.is_superuser or request.user in question.author.all()
|
else:
|
||||||
):
|
qs = qs.filter(latest_review_status=status)
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
ct = ContentType.objects.get_for_model(question)
|
# Respect authors_only: non-owners should be excluded
|
||||||
latest = (
|
# We applied permission filter earlier; ensure authors_only exclusion
|
||||||
QuestionReview.objects.filter(content_type=ct, object_id=question.pk)
|
from django.db.models import Q
|
||||||
.order_by("-created_on")
|
try:
|
||||||
.first()
|
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
|
# Aggregate counts grouped by category (DB-side)
|
||||||
match = False
|
grouped = qs.values("category__id", "category__category").annotate(count=Count("pk")).order_by("-count", "category__category")
|
||||||
if status == "ANY":
|
|
||||||
match = True
|
|
||||||
elif status == "UNREVIEWED":
|
|
||||||
match = latest is None
|
|
||||||
else:
|
|
||||||
if latest is not None and latest.status == status:
|
|
||||||
match = True
|
|
||||||
|
|
||||||
if not match:
|
counts_rows = []
|
||||||
continue
|
total = qs.count()
|
||||||
|
|
||||||
# Grouping
|
# Build per-row link (to question list) preserving current filters but swapping category
|
||||||
if group_by == "category":
|
from urllib.parse import urlencode
|
||||||
key = question.category.category if question.category else "Uncategorized"
|
from django.urls import reverse
|
||||||
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
|
|
||||||
|
|
||||||
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
|
for row in grouped:
|
||||||
sorted_counts = sorted(counts.items(), key=lambda x: (-x[1], x[0]))
|
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
|
# 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")
|
findings = Finding.objects.all().order_by("name")
|
||||||
structures = Structure.objects.all().order_by("name")
|
structures = Structure.objects.all().order_by("name")
|
||||||
conditions = AtlasCondition.objects.all().order_by("name")
|
conditions = AtlasCondition.objects.all().order_by("name")
|
||||||
@@ -656,21 +682,21 @@ def question_overview(request):
|
|||||||
"status_options": status_options,
|
"status_options": status_options,
|
||||||
"selected_status": status,
|
"selected_status": status,
|
||||||
"group_by": group_by,
|
"group_by": group_by,
|
||||||
"counts": sorted_counts,
|
"counts": counts_rows,
|
||||||
"total": total,
|
"total": total,
|
||||||
"categories": categories,
|
"categories": categories,
|
||||||
"selected_category": category_filter,
|
"selected_category": selected_category,
|
||||||
"selected_open_access": open_access,
|
"selected_open_access": open_access,
|
||||||
"findings": findings,
|
"findings": findings,
|
||||||
"structures": structures,
|
"structures": structures,
|
||||||
"conditions": conditions,
|
"conditions": conditions,
|
||||||
"subspecialties": subspecialties,
|
"subspecialties": subspecialties,
|
||||||
"presentations": presentations,
|
"presentations": presentations,
|
||||||
"selected_finding": finding_filter,
|
"selected_findings": selected_findings,
|
||||||
"selected_structure": structure_filter,
|
"selected_structures": selected_structures,
|
||||||
"selected_condition": condition_filter,
|
"selected_conditions": selected_conditions,
|
||||||
"selected_subspecialty": subspecialty_filter,
|
"selected_subspecialties": selected_subspecialties,
|
||||||
"selected_presentation": presentation_filter,
|
"selected_presentations": selected_presentations,
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "sbas/question_overview.html", context)
|
return render(request, "sbas/question_overview.html", context)
|
||||||
|
|||||||
Reference in New Issue
Block a user