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