diff --git a/rad/urls.py b/rad/urls.py index 3f892463..30c61cec 100644 --- a/rad/urls.py +++ b/rad/urls.py @@ -106,6 +106,8 @@ urlpatterns = [ path("cid//", views.cid_scores_admin, name="cid_scores_admin"), path("user/scores", views.user_scores, name="user_scores"), path("user/scores/", views.user_scores_admin, name="user_scores_admin"), + path("user/marking", views.user_marking, name="user_marking"), + path("user/marking/", 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 diff --git a/rad/views.py b/rad/views.py index 6ce1c160..6b3c500a 100644 --- a/rad/views.py +++ b/rad/views.py @@ -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) diff --git a/templates/generic/partials/user_marking_exam_list.html b/templates/generic/partials/user_marking_exam_list.html new file mode 100644 index 00000000..c8c7329f --- /dev/null +++ b/templates/generic/partials/user_marking_exam_list.html @@ -0,0 +1,26 @@ +
+ {% if exams_data %} +
    + {% for item in exams_data %} + {% with exam=item.exam %} +
  • +
    + {{ exam.name }} +
    Authors: {% for a in exam.author.all %}{{ a.get_full_name|default:a.username }}{% if not forloop.last %}, {% endif %}{% endfor %}
    +
    +
    +
    Markers: {{ exam.markers.all.count }}
    + {% if item.has_unmarked %} + Needs marking ({{ item.unmarked_count }}) + {% else %} + No unmarked + {% endif %} +
    +
  • + {% endwith %} + {% endfor %} +
+ {% else %} +
No exams found for this exam type.
+ {% endif %} +
diff --git a/templates/index.html b/templates/index.html index 18ed08e7..8f6eba28 100644 --- a/templates/index.html +++ b/templates/index.html @@ -91,8 +91,11 @@
Quick links
-
    +
    • RTS platform
    • + {% if request.user.is_authenticated %} +
    • Marking overview
    • + {% endif %} {% if not request.user.is_authenticated %}
    • CID login
    • {% endif %} {% if request.user.supervisor %}
    • Supervisor overview
    • {% endif %} {% if request.user|has_group:"cid_user_manager" %} diff --git a/templates/rad/user_marking.html b/templates/rad/user_marking.html new file mode 100644 index 00000000..82875cd7 --- /dev/null +++ b/templates/rad/user_marking.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% block content %} +

      Marking: Exams you can mark

      + + + +
      +
      +

      Click an exam type to load exams — each section loads independently.

      +
      +
      + +
      + {% for app in apps %} +
      +
      +
      +
      {{ app|capfirst }}
      +
      + +
      Loading...
      +
      +
      +
      +
      No data loaded — click the button to load.
      +
      +
      +
      + {% endfor %} +
      + + + + + +{% endblock %}