From 293b5b6682d41e912662f0bce1aa4e45deaf581b Mon Sep 17 00:00:00 2001 From: Ross Date: Tue, 28 Oct 2025 08:15:10 +0000 Subject: [PATCH] Add question overview page with filtering options and counts by category --- sbas/templates/sbas/base.html | 2 + sbas/templates/sbas/question_overview.html | 66 ++++++++++++ sbas/urls.py | 5 + sbas/views.py | 111 +++++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 sbas/templates/sbas/question_overview.html diff --git a/sbas/templates/sbas/base.html b/sbas/templates/sbas/base.html index 12e426ff..46465217 100644 --- a/sbas/templates/sbas/base.html +++ b/sbas/templates/sbas/base.html @@ -25,6 +25,8 @@
  • All Questions
  • Review Questions
  • Generate Exam
  • +
  • Questions Overview`
  • + diff --git a/sbas/templates/sbas/question_overview.html b/sbas/templates/sbas/question_overview.html new file mode 100644 index 00000000..fbcbc0e9 --- /dev/null +++ b/sbas/templates/sbas/question_overview.html @@ -0,0 +1,66 @@ +{% extends 'base.html' %} + +{% block content %} +
    +
    +

    Question overview

    + All questions +
    + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    + +
    +
    +
    Counts by category ({{ total }} questions)
    + + {% if counts %} +
      + {% for name, count in counts %} +
    • +
      {{ name }}
      +
      {{ count }}
      +
    • + {% endfor %} +
    + {% else %} +

    No matching questions.

    + {% endif %} + +
    +
    +
    +{% endblock %} + +{% block js %}{% endblock %} diff --git a/sbas/urls.py b/sbas/urls.py index 48b8b6e7..f4f5ec8c 100644 --- a/sbas/urls.py +++ b/sbas/urls.py @@ -110,6 +110,11 @@ urlpatterns = [ views.generate_exam, name="generate_exam", ), + path( + "question/overview/", + views.question_overview, + name="question_overview", + ), path("category/merge/", views.merge_category, name="merge_category"), #path( # "exam//scores///", diff --git a/sbas/views.py b/sbas/views.py index 5bbf43e4..68519420 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -511,6 +511,117 @@ def generate_exam(request): return render(request, "sbas/generate_exam.html", {"categories": categories, "status_options": status_options}) +@login_required +def question_overview(request): + """Show counts of questions grouped by category (default) with simple filters. + + Default behaviour: + - group_by=category + - status=AC (Accepted) + + Available GET params: + - status: AC, OD, ER, RJ, IP, ANY, UNREVIEWED + - category: category id to filter (optional) + - open_access: '1' or '0' to filter + """ + # Prepare status options as elsewhere + status_options = [("ANY", "Any"), ("UNREVIEWED", "Unreviewed")] + for v, label in QuestionReview.StatusChoices.choices: + status_options.append((v, label)) + + status = request.GET.get("status") or QuestionReview.StatusChoices.ACCEPTED + group_by = request.GET.get("group_by") or "category" + category_filter = request.GET.get("category") + open_access = request.GET.get("open_access") + + qs = Question.objects.all().order_by("pk") + + # Permission filter: non-checkers see open_access or their own questions + try: + if not request.user.groups.filter(name="sbas_checker").exists(): + from django.db.models import Q + + qs = qs.filter(Q(open_access=True) | Q(author__id=request.user.id)) + except Exception: + pass + + # Optional category filter + if category_filter: + try: + qs = qs.filter(category__id=int(category_filter)) + except Exception: + pass + + # Optional open_access filter + if open_access in ("0", "1"): + qs = qs.filter(open_access=(open_access == "1")) + + # Build counts by category (default) + counts = {} + total = 0 + + from django.contrib.contenttypes.models import ContentType + + 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 + + ct = ContentType.objects.get_for_model(question) + latest = ( + QuestionReview.objects.filter(content_type=ct, object_id=question.pk) + .order_by("-created_on") + .first() + ) + + # 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 + + if not match: + continue + + # 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 + + total += 1 + + # Sort counts by descending + sorted_counts = sorted(counts.items(), key=lambda x: (-x[1], x[0])) + + categories = Category.objects.all().order_by("category") + + context = { + "status_options": status_options, + "selected_status": status, + "group_by": group_by, + "counts": sorted_counts, + "total": total, + "categories": categories, + "selected_category": category_filter, + "selected_open_access": open_access, + } + + return render(request, "sbas/question_overview.html", context) + + class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView): model = UserAnswer table_class = UserAnswerTable