diff --git a/atlas/templates/atlas/shared_with_me.html b/atlas/templates/atlas/shared_with_me.html
index f9c7fec7..3dca8f89 100644
--- a/atlas/templates/atlas/shared_with_me.html
+++ b/atlas/templates/atlas/shared_with_me.html
@@ -2,70 +2,70 @@
{% load static %}
{% block content %}
-
-
Shared with me
-
- Collection attempts shared with you as an author, marker, supervisor, or by direct invitation.
-
-
+
+
Shared with me
+
+ Collection attempts shared with you as an author, marker, supervisor, or by direct invitation.
+
+
-{% if attempt_list %}
-
+
+
Supervision
+
+ Collection attempts shared with you as author, marker, supervisor or by direct invitation.
+ {{ total_attempts }} attempt{{ total_attempts|pluralize }} found.
+
+
+
+
+{# ---- Filter / group toolbar ---- #}
+
+ No collection attempts have been shared with you yet. Try adjusting the date filter.
+
+{% endif %}
+{% endblock %}
diff --git a/atlas/urls.py b/atlas/urls.py
index b0b39fc8..1e6e209a 100755
--- a/atlas/urls.py
+++ b/atlas/urls.py
@@ -773,4 +773,9 @@ urlpatterns = [
views.collection_shared_attempt_overview,
name="collection_shared_attempt_overview",
),
+ path(
+ "supervision/",
+ views.collection_supervision,
+ name="collection_supervision",
+ ),
]
diff --git a/atlas/views.py b/atlas/views.py
index 1c613a16..d1613cbc 100755
--- a/atlas/views.py
+++ b/atlas/views.py
@@ -5013,10 +5013,14 @@ def collection_take_start(request, pk, cid=None, passcode=None):
if collection.collection_type == "REV" or valid_user:
cid_exam = collection.get_cid_and_user_exams(user_user=request.user).first()
+ # Build a lightweight summary of who results will be shared with.
+ sharing_summary = _collection_sharing_summary(collection, request.user)
+
template_variables = {
"collection": collection,
"valid_user": valid_user,
"cid_exam": cid_exam,
+ "sharing_summary": sharing_summary,
}
match collection.collection_type:
@@ -5703,6 +5707,46 @@ def collection_take_overview(
)
+def _collection_sharing_summary(collection, user):
+ """Return a dict describing who a registered user's results will be shared with
+ for the given collection, before an attempt is created.
+
+ Keys:
+ always_visible_to – list of User objects (authors + markers)
+ supervisor – User | None (linked supervisor account, if any)
+ supervisor_always – bool (results_supervisor_visible on the collection)
+ supervisor_opt_in – bool (user can choose to share with supervisor)
+ """
+ always_visible = []
+ seen_pks = set()
+ for attr in ("author", "markers"):
+ if hasattr(collection, attr):
+ for u in getattr(collection, attr).select_related():
+ if u.pk not in seen_pks:
+ seen_pks.add(u.pk)
+ always_visible.append(u)
+
+ supervisor_always = getattr(collection, "results_supervisor_visible", False)
+ supervisor_user = None
+ supervisor_opt_in = False
+ if user and user.is_authenticated:
+ try:
+ sup = user.userprofile.supervisor
+ if sup:
+ supervisor_opt_in = True
+ if sup.user_id:
+ supervisor_user = sup.user
+ except Exception:
+ pass
+
+ return {
+ "always_visible_to": always_visible,
+ "supervisor": supervisor_user,
+ "supervisor_always": supervisor_always,
+ "supervisor_opt_in": supervisor_opt_in,
+ }
+
+
@login_required
def collection_exam_sharing_panel(request, pk):
"""HTMX view to display and manage sharing settings for a CidUserExam attempt.
@@ -5940,6 +5984,133 @@ def collection_shared_attempt_overview(request, cid_user_exam_pk):
)
+@login_required
+def collection_supervision(request):
+ """Supervision page: lists all collection attempts visible to the current user as
+ an author, marker, supervisor, or manual-share recipient.
+
+ Supports:
+ - GET param ``group_by``: ``user`` (default) or ``collection``
+ - GET params ``date_from`` / ``date_to``: ISO date strings to filter by
+ attempt start_time (inclusive).
+ """
+ from django.db.models import Q
+ from django.contrib.contenttypes.models import ContentType
+
+ collection_ct = ContentType.objects.get_for_model(CaseCollection)
+
+ # ---- build base queryset of accessible attempts ----
+ manual_qs = CidUserExam.objects.filter(
+ shared_with_users=request.user,
+ content_type=collection_ct,
+ ).exclude(user_user=request.user)
+
+ managed_collection_ids = list(
+ CaseCollection.objects.filter(
+ Q(author=request.user) | Q(markers=request.user)
+ ).values_list("pk", flat=True)
+ )
+ authored_qs = CidUserExam.objects.filter(
+ content_type=collection_ct,
+ object_id__in=managed_collection_ids,
+ ).exclude(user_user=request.user)
+
+ supervisor_qs = CidUserExam.objects.none()
+ supervisor_exam_qs = CidUserExam.objects.none()
+ try:
+ supervisor = request.user.supervisor
+ trainee_users = User.objects.filter(userprofile__supervisor=supervisor)
+ trainee_cid_users = CidUser.objects.filter(supervisor=supervisor)
+ supervisor_qs = CidUserExam.objects.filter(
+ content_type=collection_ct,
+ share_with_supervisor=True,
+ ).filter(
+ Q(user_user__in=trainee_users) | Q(cid_user__in=trainee_cid_users)
+ ).exclude(user_user=request.user)
+
+ visible_collection_ids = list(
+ CaseCollection.objects.filter(results_supervisor_visible=True).values_list("pk", flat=True)
+ )
+ supervisor_exam_qs = CidUserExam.objects.filter(
+ content_type=collection_ct,
+ object_id__in=visible_collection_ids,
+ ).filter(
+ Q(user_user__in=trainee_users) | Q(cid_user__in=trainee_cid_users)
+ ).exclude(user_user=request.user)
+ except Exception:
+ pass
+
+ all_attempts = (
+ manual_qs | authored_qs | supervisor_qs | supervisor_exam_qs
+ ).distinct().select_related("user_user", "cid_user")
+
+ # ---- date filter ----
+ date_from_raw = request.GET.get("date_from", "").strip()
+ date_to_raw = request.GET.get("date_to", "").strip()
+ date_from = None
+ date_to = None
+ try:
+ if date_from_raw:
+ from django.utils.dateparse import parse_date
+ date_from = parse_date(date_from_raw)
+ if date_from:
+ all_attempts = all_attempts.filter(start_time__date__gte=date_from)
+ except Exception:
+ pass
+ try:
+ if date_to_raw:
+ from django.utils.dateparse import parse_date
+ date_to = parse_date(date_to_raw)
+ if date_to:
+ all_attempts = all_attempts.filter(start_time__date__lte=date_to)
+ except Exception:
+ pass
+
+ all_attempts = all_attempts.order_by("-start_time")
+
+ # ---- grouping ----
+ group_by = request.GET.get("group_by", "user")
+ if group_by not in ("user", "collection"):
+ group_by = "user"
+
+ grouped = {}
+ for attempt in all_attempts:
+ collection_obj = attempt.exam
+ if group_by == "collection":
+ key_label = str(collection_obj) if collection_obj else "Unknown collection"
+ key_id = getattr(collection_obj, "pk", 0)
+ key = (key_id, key_label)
+ else:
+ if attempt.user_user_id:
+ key_label = attempt.user_user.get_full_name() or attempt.user_user.username
+ key_id = attempt.user_user_id
+ elif attempt.cid_user_id:
+ name = attempt.cid_user.name or ""
+ key_label = f"CID {attempt.cid_user.cid}" + (f" – {name}" if name else "")
+ key_id = f"cid_{attempt.cid_user_id}"
+ else:
+ key_label = "Unknown learner"
+ key_id = 0
+ key = (key_id, key_label)
+ grouped.setdefault(key, []).append((attempt, collection_obj))
+
+ # Convert to sorted list of (key_label, [(attempt, collection), ...])
+ groups = sorted(grouped.items(), key=lambda x: str(x[0][1]).lower())
+ groups = [(label, rows) for (_, label), rows in groups]
+
+ return render(
+ request,
+ "atlas/supervision.html",
+ {
+ "groups": groups,
+ "group_by": group_by,
+ "date_from": date_from_raw,
+ "date_to": date_to_raw,
+ "total_attempts": all_attempts.count(),
+ },
+ )
+
+
def collection_case_view_take_user_answers(request, pk: int, case_number: int):
return collection_case_view_take_answers(request, pk, case_number)
diff --git a/templates/base.html b/templates/base.html
index e6204479..a87a665e 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -203,8 +203,21 @@