diff --git a/atlas/templates/atlas/collection_take_start.html b/atlas/templates/atlas/collection_take_start.html index 1350220e..fa937346 100644 --- a/atlas/templates/atlas/collection_take_start.html +++ b/atlas/templates/atlas/collection_take_start.html @@ -73,6 +73,50 @@ {% endif %} + + {# ---- Who can see your results ---- #} + {% if sharing_summary %} +
+
Who can see your results
+ +
+ {% endif %} {% else %}

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 %} -
- - - - - - - - - - - - {% for attempt, collection in attempt_list %} + {% if attempt_list %} +
+
CollectionLearnerStartedStatus
+ - - - - - + + + + + - {% endfor %} - -
- {% if collection %} - {{ collection.name }} - {% else %} - Unknown collection - {% endif %} - - {% if attempt.user_user %} - {{ attempt.user_user.get_full_name|default:attempt.user_user.username }} - {% if attempt.user_user.email %} -
{{ attempt.user_user.email }} - {% endif %} - {% elif attempt.cid_user %} - CID {{ attempt.cid_user.cid }} - {% if attempt.cid_user.name %}
{{ attempt.cid_user.name }}{% endif %} - {% else %} - Unknown - {% endif %} -
{{ attempt.start_time|default:"-" }} - {% if attempt.completed %} - Completed - {% else %} - In progress - {% endif %} - - - View attempt - - CollectionLearnerStartedStatus
-
-{% else %} -
- No collection attempts have been shared with you yet. -
-{% endif %} + + + {% for attempt, collection in attempt_list %} + + + {% if collection %} + {{ collection.name }} + {% else %} + Unknown collection + {% endif %} + + + {% if attempt.user_user %} + {{ attempt.user_user.get_full_name|default:attempt.user_user.username }} + {% if attempt.user_user.email %} +
{{ attempt.user_user.email }} + {% endif %} + {% elif attempt.cid_user %} + CID {{ attempt.cid_user.cid }} + {% if attempt.cid_user.name %}
{{ attempt.cid_user.name }}{% endif %} + {% else %} + Unknown + {% endif %} + + {{ attempt.start_time|default:"-" }} + + {% if attempt.completed %} + Completed + {% else %} + In progress + {% endif %} + + + + View attempt + + + + {% endfor %} + + + + {% else %} +
+ No collection attempts have been shared with you yet. +
+ {% endif %} {% endblock %} diff --git a/atlas/templates/atlas/supervision.html b/atlas/templates/atlas/supervision.html new file mode 100644 index 00000000..a6bc2560 --- /dev/null +++ b/atlas/templates/atlas/supervision.html @@ -0,0 +1,136 @@ +{% extends 'atlas/exams.html' %} +{% load static %} + +{% block content %} +
+
+

Supervision

+

+ Collection attempts shared with you as author, marker, supervisor or by direct invitation. + {{ total_attempts }} attempt{{ total_attempts|pluralize }} found. +

+
+
+ +{# ---- Filter / group toolbar ---- #} +
+ +
+ + +
+
+ + +
+ +
+ + Clear +
+
+ +{# ---- Grouped results ---- #} +{% if groups %} + {% for group_label, attempt_rows in groups %} +
+
+ {% if group_by == 'user' %} + + {% else %} + + {% endif %} + {{ group_label }} + {{ attempt_rows|length }} attempt{{ attempt_rows|length|pluralize }} +
+
+ + + + {% if group_by == 'user' %} + + {% else %} + + {% endif %} + + + + + + + + + {% for attempt, collection in attempt_rows %} + + {% if group_by == 'user' %} + + {% else %} + + {% endif %} + + + + + + {% endfor %} + +
CollectionLearnerStartedEndedStatusProgress
+ {% if collection %} + {{ collection.name }} + {% else %} + Unknown + {% endif %} + + {% if attempt.user_user %} + {{ attempt.user_user.get_full_name|default:attempt.user_user.username }} + {% if attempt.user_user.email %} +
{{ attempt.user_user.email }} + {% endif %} + {% elif attempt.cid_user %} + CID {{ attempt.cid_user.cid }} + {% if attempt.cid_user.name %}
{{ attempt.cid_user.name }}{% endif %} + {% else %} + Unknown + {% endif %} +
{{ attempt.start_time|default:"-" }}{{ attempt.end_time|default:"-" }} + {% if attempt.completed %} + Completed + {% else %} + In progress + {% endif %} + + {% if attempt.user_user_id %} + + View attempt + + {% else %} + + View attempt + + {% endif %} +
+
+
+ {% endfor %} +{% else %} +
+ 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 @@ -