add user marknig pages
This commit is contained in:
@@ -106,6 +106,8 @@ urlpatterns = [
|
|||||||
path("cid/<int:cid>/", views.cid_scores_admin, name="cid_scores_admin"),
|
path("cid/<int:cid>/", views.cid_scores_admin, name="cid_scores_admin"),
|
||||||
path("user/scores", views.user_scores, name="user_scores"),
|
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/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/", views.cid_selector, name="cid_selector"),
|
||||||
path("cid/request_cid_details/", views.request_cid_details, name="request_cid_details"),
|
path("cid/request_cid_details/", views.request_cid_details, name="request_cid_details"),
|
||||||
# Global url that registers RTS compatible exams
|
# Global url that registers RTS compatible exams
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from generic.views import (
|
|||||||
RedirectMixin,
|
RedirectMixin,
|
||||||
get_question_and_content_type,
|
get_question_and_content_type,
|
||||||
get_user_exams,
|
get_user_exams,
|
||||||
|
get_exam_model_from_app_name,
|
||||||
)
|
)
|
||||||
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
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):
|
def cid_scores(request, cid, passcode):
|
||||||
# exam = get_object_or_404(Exam, pk=pk)
|
# 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>
|
||||||
@@ -93,6 +93,9 @@
|
|||||||
<h6 class="card-title">Quick links</h6>
|
<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>
|
<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 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.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" %}
|
{% if request.user|has_group:"cid_user_manager" %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
Reference in New Issue
Block a user