add user marknig pages

This commit is contained in:
Ross
2026-01-04 12:39:33 +00:00
parent fa18c33e8a
commit 4ffadd3914
5 changed files with 183 additions and 1 deletions
+2
View File
@@ -106,6 +106,8 @@ urlpatterns = [
path("cid/<int:cid>/", views.cid_scores_admin, name="cid_scores_admin"),
path("user/scores", views.user_scores, name="user_scores"),
path("user/scores/<int:user_id>", views.user_scores_admin, name="user_scores_admin"),
path("user/marking", views.user_marking, name="user_marking"),
path("user/marking/<str:exam_type>", views.user_marking_partial, name="user_marking_partial"),
path("cid/", views.cid_selector, name="cid_selector"),
path("cid/request_cid_details/", views.request_cid_details, name="request_cid_details"),
# Global url that registers RTS compatible exams
+86
View File
@@ -12,6 +12,7 @@ from generic.views import (
RedirectMixin,
get_question_and_content_type,
get_user_exams,
get_exam_model_from_app_name,
)
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
from django.shortcuts import render, get_object_or_404, redirect
@@ -266,6 +267,91 @@ def user_scores(request, user=None):
)
@login_required
def user_marking(request):
"""Main page for user marking overview. Uses HTMX to fetch per-app partials."""
apps = ["physics", "anatomy", "rapids", "shorts", "longs", "sbas"]
return render(request, "rad/user_marking.html", {"apps": apps})
@login_required
def user_marking_partial(request, exam_type: str):
"""Return an HTMX partial listing exams the user can mark for a given exam type.
Each exam is checked for whether it has any unmarked answers (using
question.get_unmarked_user_answer_count when available).
"""
user = request.user
try:
ExamModel = get_exam_model_from_app_name(exam_type)
except Exception:
return HttpResponse("Unknown exam type", status=400)
# Exams where user is an author or an authorised marker
exams_qs = ExamModel.objects.filter(
archive=False, exam_mode=True
).filter(Q(author=user) | Q(markers=user)).prefetch_related("author", "markers")
exams_data = []
for exam in exams_qs.order_by("name"):
has_unmarked = False
unmarked_count = 0
# iterate questions and use their helper to count unmarked answers when available
for q in exam.get_questions():
count = 0
# Prefer question-provided helper if present
helper = getattr(q, "get_unmarked_user_answer_count", None)
if callable(helper):
try:
count = helper(exam_pk=exam.pk, marker=user)
except TypeError:
count = helper(exam_pk=exam.pk)
except Exception:
count = 0
else:
# Fallback: inspect related answer manager if present
rel_manager = None
for rel_name in ("cid_user_answers", "user_answers", "answers", "useranswer_set"):
if hasattr(q, rel_name):
rel_manager = getattr(q, rel_name)
break
if rel_manager is not None:
try:
ans_model = rel_manager.model
# If the answer model has a `score` field, count answers that are unmarked
field_names = [f.name for f in ans_model._meta.get_fields()]
if "score" in field_names:
# Try to detect an UNMARKED constant on the model
unmarked_vals = [None, ""]
model_score_const = getattr(ans_model, "ScoreOptions", None) or getattr(ans_model, "MarkOptions", None)
if model_score_const is not None:
# attempt to use any UNMARKED attribute
unmarked_val = getattr(model_score_const, "UNMARKED", None)
if unmarked_val is not None:
unmarked_vals.append(unmarked_val)
count = rel_manager.filter(exam__id=exam.pk).filter(score__in=unmarked_vals).count()
else:
# No score field — likely auto-marked; treat as 0
count = 0
except Exception:
count = 0
try:
c = int(count)
except Exception:
c = 0
if c:
has_unmarked = True
unmarked_count += c
exams_data.append({"exam": exam, "has_unmarked": has_unmarked, "unmarked_count": unmarked_count})
return render(request, "generic/partials/user_marking_exam_list.html", {"exams_data": exams_data, "exam_type": exam_type})
def cid_scores(request, cid, passcode):
# exam = get_object_or_404(Exam, pk=pk)
@@ -0,0 +1,26 @@
<div class="user-marking-list">
{% if exams_data %}
<ul class="list-group">
{% for item in exams_data %}
{% with exam=item.exam %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<a href="{{ exam.get_absolute_url }}" class="fw-semibold">{{ exam.name }}</a>
<div class="small text-muted">Authors: {% for a in exam.author.all %}{{ a.get_full_name|default:a.username }}{% if not forloop.last %}, {% endif %}{% endfor %}</div>
</div>
<div class="text-end">
<div class="small text-muted">Markers: {{ exam.markers.all.count }}</div>
{% if item.has_unmarked %}
<span class="badge bg-danger mt-1">Needs marking ({{ item.unmarked_count }})</span>
{% else %}
<span class="badge bg-secondary mt-1">No unmarked</span>
{% endif %}
</div>
</li>
{% endwith %}
{% endfor %}
</ul>
{% else %}
<div class="alert alert-info mb-0">No exams found for this exam type.</div>
{% endif %}
</div>
+4 -1
View File
@@ -91,8 +91,11 @@
<div class="card mb-3">
<div class="card-body">
<h6 class="card-title">Quick links</h6>
<ul class="list-unstyled mb-0">
<ul class="list-unstyled mb-0">
<li><a href="http://www.penracourses.org.uk/rts" class="link-primary">RTS platform</a></li>
{% if request.user.is_authenticated %}
<li><a href="{% url 'user_marking' %}" class="link-primary">Marking overview</a></li>
{% endif %}
{% if not request.user.is_authenticated %}<li><a href="{% url 'cid_selector' %}" class="link-primary">CID login</a></li>{% endif %}
{% if request.user.supervisor %}<li><a href='{% url "generic:supervisor_overview" request.user.supervisor.pk %}' class="link-primary">Supervisor overview</a></li>{% endif %}
{% if request.user|has_group:"cid_user_manager" %}
+65
View File
@@ -0,0 +1,65 @@
{% extends "base.html" %}
{% block content %}
<h1 class="mb-4">Marking: Exams you can mark</h1>
<style>
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
</style>
<div class="row">
<div class="col-12">
<p class="text-muted">Click an exam type to load exams — each section loads independently.</p>
</div>
</div>
<div class="row g-4">
{% for app in apps %}
<div class="col-md-6">
<div class="card">
<div class="card-body d-flex align-items-center justify-content-between">
<h5 class="card-title mb-0">{{ app|capfirst }}</h5>
<div>
<button class="btn btn-sm btn-primary" hx-get="{% url 'user_marking_partial' app %}" hx-target="#marking-{{ app }}" hx-swap="innerHTML" hx-indicator="#indicator-{{ app }}">Load / Refresh</button>
<div id="indicator-{{ app }}" class="htmx-indicator ms-2 spinner-border spinner-border-sm text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
</div>
</div>
<div id="marking-{{ app }}" class="card-body border-top">
<div class="text-muted small">No data loaded — click the button to load.</div>
</div>
</div>
</div>
{% endfor %}
</div>
<script>
// Show/hide indicators referenced by triggering element's hx-indicator attribute
document.body.addEventListener('htmx:beforeRequest', function(evt){
try{
var elt = evt.detail.elt || evt.target;
var sel = elt && elt.getAttribute && elt.getAttribute('hx-indicator');
if(sel){
var indicator = document.querySelector(sel);
if(indicator) indicator.classList.add('visible');
}
}catch(e){console.warn(e)}
});
document.body.addEventListener('htmx:afterRequest', function(evt){
try{
var elt = evt.detail.elt || evt.target;
var sel = elt && elt.getAttribute && elt.getAttribute('hx-indicator');
if(sel){
var indicator = document.querySelector(sel);
if(indicator) indicator.classList.remove('visible');
}
}catch(e){console.warn(e)}
});
</script>
<style>
.htmx-indicator { display: none; }
.htmx-indicator.visible { display: inline-block !important; }
</style>
{% endblock %}