feat: Implement sharing functionality for CidUserExam attempts, including user management and sharing settings
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -38,7 +38,7 @@
|
||||
{% if collection.case_query_messaging_enabled and collection.in_review_mode or collection.case_query_messaging_enabled and cid_user_exam.completed %}
|
||||
<div class="mt-2">
|
||||
<a class="btn btn-sm {% if total_outstanding_feedback %}btn-danger{% else %}btn-outline-secondary{% endif %}"
|
||||
href="{% if cid is not None %}{% url 'atlas:collection_feedback_overview' pk=collection.id cid=cid passcode=passcode %}{% else %}{% url 'atlas:collection_feedback_overview_user' pk=collection.id %}{% endif %}">
|
||||
href="{% if cid is not None %}{% url 'atlas:collection_feedback_overview' pk=collection.id cid=cid passcode=passcode %}{% else %}{% url 'atlas:collection_feedback_overview_user' pk=collection.id %}{% endif %}">
|
||||
{% if total_outstanding_feedback %}
|
||||
Review Feedback Hub
|
||||
{% else %}
|
||||
@@ -89,12 +89,12 @@
|
||||
</a>
|
||||
{% if collection.in_review_mode or cid_user_exam.completed %}
|
||||
<a class="btn btn-sm btn-outline-info"
|
||||
href="{% url 'atlas:add_self_review' cid_user_exam.id question.case.id %}">
|
||||
href="{% url 'atlas:add_self_review' cid_user_exam.id question.case.id %}">
|
||||
{% if self_review %}Add New Self Review{% else %}Add Self Review{% endif %}
|
||||
</a>
|
||||
{% if collection.case_query_messaging_enabled and review_stats.has_messages %}
|
||||
<a class="btn btn-sm {% if review_stats.has_outstanding_feedback %}btn-danger{% else %}btn-outline-dark{% endif %}"
|
||||
href="{% if cid is not None %}{% url 'atlas:collection_feedback_overview' pk=collection.id cid=cid passcode=passcode %}{% else %}{% url 'atlas:collection_feedback_overview_user' pk=collection.id %}{% endif %}#case-feedback-{{ question.case.id }}">
|
||||
href="{% if cid is not None %}{% url 'atlas:collection_feedback_overview' pk=collection.id cid=cid passcode=passcode %}{% else %}{% url 'atlas:collection_feedback_overview_user' pk=collection.id %}{% endif %}#case-feedback-{{ question.case.id }}">
|
||||
View Conversation
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -159,6 +159,15 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Sharing panel – only for registered-user attempts (not CID candidates) #}
|
||||
{% if cid_user_exam.user_user_id %}
|
||||
<div hx-get="{% url 'atlas:collection_exam_sharing_panel' cid_user_exam.pk %}"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="text-muted small mt-3">Loading sharing settings…</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
{% load django_htmx %}
|
||||
{# Sharing panel for a CidUserExam attempt.
|
||||
Context:
|
||||
cid_user_exam – CidUserExam instance
|
||||
sharing_info – dict from cid_user_exam.get_sharing_info()
|
||||
is_owner – bool: current user is the attempt owner
|
||||
#}
|
||||
<div id="sharing-panel-{{ cid_user_exam.pk }}" class="card border-secondary mt-3">
|
||||
<div class="card-header py-2 px-3 d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold small">Result Sharing</span>
|
||||
<a href="{% url 'atlas:collection_shared_with_me' %}" class="btn btn-sm btn-outline-secondary">Shared with me</a>
|
||||
</div>
|
||||
<div class="card-body py-2 px-3">
|
||||
|
||||
{# Always-visible users #}
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted mb-1">Always shared with (authors & markers)</div>
|
||||
{% if sharing_info.always_visible_to %}
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for u in sharing_info.always_visible_to %}
|
||||
<li class="small">
|
||||
<span class="badge bg-secondary me-1">Author/Marker</span>
|
||||
{{ u.get_full_name|default:u.username }}
|
||||
{% if u.email %}<span class="text-muted"><{{ u.email }}></span>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="small text-muted fst-italic">No authors or markers assigned.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Supervisor sharing #}
|
||||
{% if not sharing_info.exam_supervisor_visible %}
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted mb-1">Share with supervisor</div>
|
||||
{% if is_owner %}
|
||||
<button class="btn btn-sm {% if sharing_info.supervisor_enabled %}btn-success{% else %}btn-outline-secondary{% endif %}"
|
||||
hx-post="{% url 'atlas:collection_exam_sharing_panel' cid_user_exam.pk %}"
|
||||
hx-vals='{"action": "toggle_supervisor"}'
|
||||
hx-target="#sharing-panel-{{ cid_user_exam.pk }}"
|
||||
hx-swap="outerHTML">
|
||||
{% if sharing_info.supervisor_enabled %}Supervisor sharing ON{% else %}Supervisor sharing OFF{% endif %}
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="badge {% if sharing_info.supervisor_enabled %}bg-success{% else %}bg-secondary{% endif %}">
|
||||
{% if sharing_info.supervisor_enabled %}ON{% else %}OFF{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if sharing_info.supervisor %}
|
||||
<div class="small text-muted mt-1">
|
||||
Supervisor:
|
||||
<strong>{{ sharing_info.supervisor.get_full_name|default:sharing_info.supervisor.username }}</strong>
|
||||
{% if sharing_info.supervisor.email %}<{{ sharing_info.supervisor.email }}>{% endif %}
|
||||
</div>
|
||||
{% elif sharing_info.supervisor_enabled %}
|
||||
<div class="small text-warning mt-1">No supervisor account linked to your profile — results cannot be shared automatically.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted mb-1">Supervisor</div>
|
||||
<span class="badge bg-success">Sharing always enabled (exam setting)</span>
|
||||
{% if sharing_info.supervisor %}
|
||||
<div class="small text-muted mt-1">
|
||||
Supervisor:
|
||||
<strong>{{ sharing_info.supervisor.get_full_name|default:sharing_info.supervisor.username }}</strong>
|
||||
{% if sharing_info.supervisor.email %}<{{ sharing_info.supervisor.email }}>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Manual shares — only for registered-user attempts #}
|
||||
{% if cid_user_exam.user_user_id %}
|
||||
<div>
|
||||
<div class="small text-muted mb-1">Also shared with</div>
|
||||
{% if sharing_info.manual_shares %}
|
||||
<ul class="list-unstyled mb-2">
|
||||
{% for u in sharing_info.manual_shares %}
|
||||
<li class="small d-flex align-items-center gap-2 mb-1">
|
||||
<span>{{ u.get_full_name|default:u.username }}
|
||||
{% if u.email %}<span class="text-muted"><{{ u.email }}></span>{% endif %}
|
||||
</span>
|
||||
{% if is_owner %}
|
||||
<button class="btn btn-sm btn-outline-danger py-0 px-1"
|
||||
hx-post="{% url 'atlas:collection_exam_sharing_panel' cid_user_exam.pk %}"
|
||||
hx-vals='{"action": "remove_user", "user_id": "{{ u.pk }}"}'
|
||||
hx-target="#sharing-panel-{{ cid_user_exam.pk }}"
|
||||
hx-swap="outerHTML">
|
||||
× Remove
|
||||
</button>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="small text-muted fst-italic mb-2">No additional users.</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_owner %}
|
||||
{# User search input #}
|
||||
<div id="sharing-user-search-{{ cid_user_exam.pk }}">
|
||||
<input type="text"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Search user by name or email…"
|
||||
name="q"
|
||||
autocomplete="off"
|
||||
hx-get="{% url 'atlas:collection_exam_user_search' cid_user_exam.pk %}"
|
||||
hx-trigger="input changed delay:300ms, focus"
|
||||
hx-target="#sharing-search-results-{{ cid_user_exam.pk }}"
|
||||
hx-swap="innerHTML">
|
||||
<div id="sharing-search-results-{{ cid_user_exam.pk }}" class="mt-1"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
{# HTMX partial: user search results for the sharing panel.
|
||||
Context:
|
||||
users – list of User objects matching the query
|
||||
cid_user_exam – CidUserExam instance
|
||||
q – search query string
|
||||
#}
|
||||
{% if users %}
|
||||
<ul class="list-group list-group-flush small border rounded">
|
||||
{% for u in users %}
|
||||
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-1 px-2">
|
||||
<span>
|
||||
{{ u.get_full_name|default:u.username }}
|
||||
{% if u.email %}<span class="text-muted"><{{ u.email }}></span>{% endif %}
|
||||
</span>
|
||||
<button class="btn btn-sm btn-outline-primary py-0 px-2"
|
||||
hx-post="{% url 'atlas:collection_exam_sharing_panel' cid_user_exam.pk %}"
|
||||
hx-vals='{"action": "add_user", "user_id": "{{ u.pk }}"}'
|
||||
hx-target="#sharing-panel-{{ cid_user_exam.pk }}"
|
||||
hx-swap="outerHTML">
|
||||
Add
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif q|length >= 2 %}
|
||||
<div class="small text-muted fst-italic">No matching users found.</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends 'atlas/exams.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-3">
|
||||
<h2 class="h4 mb-1">Shared with me</h2>
|
||||
<p class="text-muted small mb-0">
|
||||
Collection attempts shared with you as an author, marker, supervisor, or by direct invitation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if attempt_list %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Collection</th>
|
||||
<th>Learner</th>
|
||||
<th>Started</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for attempt, collection in attempt_list %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if collection %}
|
||||
<strong>{{ collection.name }}</strong>
|
||||
{% else %}
|
||||
<span class="text-muted">Unknown collection</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if attempt.user_user %}
|
||||
{{ attempt.user_user.get_full_name|default:attempt.user_user.username }}
|
||||
{% if attempt.user_user.email %}
|
||||
<br><span class="small text-muted">{{ attempt.user_user.email }}</span>
|
||||
{% endif %}
|
||||
{% elif attempt.cid_user %}
|
||||
CID {{ attempt.cid_user.cid }}
|
||||
{% if attempt.cid_user.name %}<br><span class="small text-muted">{{ attempt.cid_user.name }}</span>{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">Unknown</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="small">{{ attempt.start_time|default:"-" }}</td>
|
||||
<td>
|
||||
{% if attempt.completed %}
|
||||
<span class="badge bg-success">Completed</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark">In progress</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-outline-primary"
|
||||
href="{% url 'atlas:collection_shared_attempt_overview' attempt.pk %}">
|
||||
View attempt
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
No collection attempts have been shared with you yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user