Add question overview page with filtering options and counts by category
This commit is contained in:
@@ -25,6 +25,8 @@
|
||||
<li><a class="dropdown-item" href="{% url 'sbas:question_view' %}"><i class="bi bi-list-ul"></i> All Questions</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'sbas:question_review_start' %}"><i class="bi bi-bookmarks-fill"></i> Review Questions</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'sbas:generate_exam' %}"><i class="bi bi-journal-plus"></i> Generate Exam</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'sbas:question_overview' %}"><i class="bi bi-journal-plus"></i> Questions Overview`</a></li>
|
||||
|
||||
<!-- Additional items can be added here -->
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="py-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h5">Question overview</h2>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'sbas:question_view' %}">All questions</a>
|
||||
</div>
|
||||
|
||||
<form method="get" class="row g-2 mb-3">
|
||||
<div class="col-auto">
|
||||
<label class="form-label small">Status</label>
|
||||
<select name="status" class="form-select form-select-sm">
|
||||
{% for v, label in status_options %}
|
||||
<option value="{{ v }}" {% if selected_status == v %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<label class="form-label small">Category</label>
|
||||
<select name="category" class="form-select form-select-sm">
|
||||
<option value="">(All)</option>
|
||||
{% for c in categories %}
|
||||
<option value="{{ c.id }}" {% if selected_category|default:'' == c.id|stringformat:"s" %}selected{% endif %}>{{ c.category }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<label class="form-label small">Open access</label>
|
||||
<select name="open_access" class="form-select form-select-sm">
|
||||
<option value="">(Any)</option>
|
||||
<option value="1" {% if selected_open_access == '1' %}selected{% endif %}>Yes</option>
|
||||
<option value="0" {% if selected_open_access == '0' %}selected{% endif %}>No</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-auto align-self-end">
|
||||
<button class="btn btn-sm btn-primary">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Counts by category <small class="text-muted">({{ total }} questions)</small></h6>
|
||||
|
||||
{% if counts %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for name, count in counts %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>{{ name }}</div>
|
||||
<div class="text-muted">{{ count }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="small text-muted">No matching questions.</p>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}{% endblock %}
|
||||
@@ -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/<int:pk>/scores/<int:cid>/<str:passcode>/",
|
||||
|
||||
+111
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user