feat: Consolidate shared attempts functionality into supervision page and remove legacy shared-with-me page
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -48,6 +48,7 @@ Django-based radiology education platform. Multi-app, HTMX-driven, PostgreSQL +
|
|||||||
- **Template tags**: always load `{% load static %}`, `{% load django_htmx %}`, `{% load crispy_forms_tags %}` as needed at the top of the file.
|
- **Template tags**: always load `{% load static %}`, `{% load django_htmx %}`, `{% load crispy_forms_tags %}` as needed at the top of the file.
|
||||||
- **Bootstrap 5** (dark theme). Use existing utility classes; do not add inline styles.
|
- **Bootstrap 5** (dark theme). Use existing utility classes; do not add inline styles.
|
||||||
- To include a partial: `{% include 'app/partials/_fragment.html' %}`. Pass context explicitly when using `with` keyword.
|
- To include a partial: `{% include 'app/partials/_fragment.html' %}`. Pass context explicitly when using `with` keyword.
|
||||||
|
- **Multi line comment** NEVER use {# ... #} style comments for multiline comments as it will render into the html.
|
||||||
|
|
||||||
## Forms
|
## Forms
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div id="sharing-panel-{{ cid_user_exam.pk }}" class="card border-secondary mt-3">
|
<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">
|
<div class="card-header py-2 px-3 d-flex justify-content-between align-items-center">
|
||||||
<span class="fw-semibold small">Result Sharing</span>
|
<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>
|
<a href="{% url 'atlas:collection_supervision' %}" class="btn btn-sm btn-outline-secondary">Supervision</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body py-2 px-3">
|
<div class="card-body py-2 px-3">
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -2,135 +2,155 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="h4 mb-1">Supervision</h2>
|
<h2 class="h4 mb-1">Supervision</h2>
|
||||||
<p class="text-muted small mb-0">
|
<p class="text-muted small mb-0">
|
||||||
Collection attempts shared with you as author, marker, supervisor or by direct invitation.
|
Collection attempts shared with you as author, marker, supervisor or by direct invitation.
|
||||||
<strong>{{ total_attempts }}</strong> attempt{{ total_attempts|pluralize }} found.
|
<strong>{{ total_attempts }}</strong> attempt{{ total_attempts|pluralize }} found.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# ---- Filter / group toolbar ---- #}
|
|
||||||
<form method="get" class="row g-2 align-items-end mb-4">
|
|
||||||
<div class="col-12 col-md-auto">
|
|
||||||
<label class="form-label small mb-1">Group by</label>
|
|
||||||
<div class="btn-group d-flex" role="group">
|
|
||||||
<a href="?group_by=user&date_from={{ date_from }}&date_to={{ date_to }}"
|
|
||||||
class="btn btn-sm {% if group_by == 'user' %}btn-primary{% else %}btn-outline-secondary{% endif %}">
|
|
||||||
By learner
|
|
||||||
</a>
|
|
||||||
<a href="?group_by=collection&date_from={{ date_from }}&date_to={{ date_to }}"
|
|
||||||
class="btn btn-sm {% if group_by == 'collection' %}btn-primary{% else %}btn-outline-secondary{% endif %}">
|
|
||||||
By collection
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-auto">
|
|
||||||
<label class="form-label small mb-1" for="date_from">From</label>
|
|
||||||
<input type="date" id="date_from" name="date_from" class="form-control form-control-sm"
|
|
||||||
value="{{ date_from }}">
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-auto">
|
|
||||||
<label class="form-label small mb-1" for="date_to">To</label>
|
|
||||||
<input type="date" id="date_to" name="date_to" class="form-control form-control-sm"
|
|
||||||
value="{{ date_to }}">
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="group_by" value="{{ group_by }}">
|
|
||||||
<div class="col-12 col-md-auto">
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-primary">Apply filter</button>
|
|
||||||
<a href="{% url 'atlas:collection_supervision' %}" class="btn btn-sm btn-outline-secondary ms-1">Clear</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{# ---- Grouped results ---- #}
|
{# ---- Filter / group toolbar ---- #}
|
||||||
{% if groups %}
|
<form method="get" class="row g-2 align-items-end mb-4">
|
||||||
{% for group_label, attempt_rows in groups %}
|
{# Group by #}
|
||||||
<div class="mb-4">
|
<div class="col-12 col-md-auto">
|
||||||
<h5 class="h6 fw-semibold border-bottom pb-1 mb-2">
|
<label class="form-label small mb-1">Group by</label>
|
||||||
{% if group_by == 'user' %}
|
<div class="btn-group d-flex" role="group">
|
||||||
<i class="bi bi-person me-1" aria-hidden="true"></i>
|
<a href="?group_by=user&date_from={{ date_from }}&date_to={{ date_to }}&q={{ q }}{% if supervisor_only %}&supervisor_only=1{% endif %}"
|
||||||
{% else %}
|
class="btn btn-sm {% if group_by == 'user' %}btn-primary{% else %}btn-outline-secondary{% endif %}">
|
||||||
<i class="bi bi-collection me-1" aria-hidden="true"></i>
|
By learner
|
||||||
{% endif %}
|
</a>
|
||||||
{{ group_label }}
|
<a href="?group_by=collection&date_from={{ date_from }}&date_to={{ date_to }}&q={{ q }}{% if supervisor_only %}&supervisor_only=1{% endif %}"
|
||||||
<span class="badge bg-secondary fw-normal ms-1">{{ attempt_rows|length }} attempt{{ attempt_rows|length|pluralize }}</span>
|
class="btn btn-sm {% if group_by == 'collection' %}btn-primary{% else %}btn-outline-secondary{% endif %}">
|
||||||
</h5>
|
By collection
|
||||||
<div class="table-responsive">
|
</a>
|
||||||
<table class="table table-hover table-sm align-middle mb-0">
|
|
||||||
<thead class="table-dark">
|
|
||||||
<tr>
|
|
||||||
{% if group_by == 'user' %}
|
|
||||||
<th>Collection</th>
|
|
||||||
{% else %}
|
|
||||||
<th>Learner</th>
|
|
||||||
{% endif %}
|
|
||||||
<th>Started</th>
|
|
||||||
<th>Ended</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Progress</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for attempt, collection in attempt_rows %}
|
|
||||||
<tr>
|
|
||||||
{% if group_by == 'user' %}
|
|
||||||
<td>
|
|
||||||
{% if collection %}
|
|
||||||
{{ collection.name }}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">Unknown</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
{% else %}
|
|
||||||
<td>
|
|
||||||
{% if attempt.user_user %}
|
|
||||||
{{ attempt.user_user.get_full_name|default:attempt.user_user.username }}
|
|
||||||
{% if attempt.user_user.email %}
|
|
||||||
<br><span class="text-muted small">{{ attempt.user_user.email }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% elif attempt.cid_user %}
|
|
||||||
CID {{ attempt.cid_user.cid }}
|
|
||||||
{% if attempt.cid_user.name %}<br><span class="text-muted small">{{ attempt.cid_user.name }}</span>{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">Unknown</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
<td class="small text-nowrap">{{ attempt.start_time|default:"-" }}</td>
|
|
||||||
<td class="small text-nowrap">{{ attempt.end_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>
|
|
||||||
{% if attempt.user_user_id %}
|
|
||||||
<a class="btn btn-sm btn-outline-primary"
|
|
||||||
href="{% url 'atlas:collection_history_user' exam_id=collection.pk user_pk=attempt.user_user_id %}">
|
|
||||||
View attempt
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<a class="btn btn-sm btn-outline-primary"
|
|
||||||
href="{% url 'atlas:collection_shared_attempt_overview' attempt.pk %}">
|
|
||||||
View attempt
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
{# Learner search #}
|
||||||
<div class="alert alert-info">
|
<div class="col-12 col-md-3">
|
||||||
No collection attempts have been shared with you yet. Try adjusting the date filter.
|
<label class="form-label small mb-1" for="q">Search learner</label>
|
||||||
</div>
|
<input type="text" id="q" name="q" class="form-control form-control-sm"
|
||||||
{% endif %}
|
placeholder="Name, username or email…" value="{{ q }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Date range #}
|
||||||
|
<div class="col-12 col-md-auto">
|
||||||
|
<label class="form-label small mb-1" for="date_from">From</label>
|
||||||
|
<input type="date" id="date_from" name="date_from" class="form-control form-control-sm"
|
||||||
|
value="{{ date_from }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-auto">
|
||||||
|
<label class="form-label small mb-1" for="date_to">To</label>
|
||||||
|
<input type="date" id="date_to" name="date_to" class="form-control form-control-sm"
|
||||||
|
value="{{ date_to }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="group_by" value="{{ group_by }}">
|
||||||
|
|
||||||
|
{# Supervisor-only toggle (only visible when user is a supervisor) #}
|
||||||
|
{% if is_supervisor %}
|
||||||
|
<div class="col-12 col-md-auto d-flex align-items-end">
|
||||||
|
<div class="form-check form-switch mb-0">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch"
|
||||||
|
id="supervisor_only" name="supervisor_only" value="1"
|
||||||
|
{% if supervisor_only %}checked{% endif %}
|
||||||
|
onchange="this.form.submit()">
|
||||||
|
<label class="form-check-label small" for="supervisor_only">My trainees only</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="col-12 col-md-auto">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary">Apply</button>
|
||||||
|
<a href="{% url 'atlas:collection_supervision' %}" class="btn btn-sm btn-outline-secondary ms-1">Clear</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{# ---- Grouped results ---- #}
|
||||||
|
{% if groups %}
|
||||||
|
{% for group_label, attempt_rows in groups %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="h6 fw-semibold border-bottom pb-1 mb-2">
|
||||||
|
{% if group_by == 'user' %}
|
||||||
|
<i class="bi bi-person me-1" aria-hidden="true"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-collection me-1" aria-hidden="true"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ group_label }}
|
||||||
|
<span class="badge bg-secondary fw-normal ms-1">{{ attempt_rows|length }} attempt{{ attempt_rows|length|pluralize }}</span>
|
||||||
|
</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-sm align-middle mb-0">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
{% if group_by == 'user' %}
|
||||||
|
<th>Collection</th>
|
||||||
|
{% else %}
|
||||||
|
<th>Learner</th>
|
||||||
|
{% endif %}
|
||||||
|
<th>Started</th>
|
||||||
|
<th>Ended</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for attempt, collection in attempt_rows %}
|
||||||
|
<tr>
|
||||||
|
{% if group_by == 'user' %}
|
||||||
|
<td>
|
||||||
|
{% if collection %}{{ collection.name }}{% else %}<span class="text-muted">Unknown</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td>
|
||||||
|
{% if attempt.user_user %}
|
||||||
|
{{ attempt.user_user.get_full_name|default:attempt.user_user.username }}
|
||||||
|
{% if attempt.user_user.email %}
|
||||||
|
<br><span class="text-muted small">{{ attempt.user_user.email }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% elif attempt.cid_user %}
|
||||||
|
CID {{ attempt.cid_user.cid }}
|
||||||
|
{% if attempt.cid_user.name %}<br><span class="text-muted small">{{ attempt.cid_user.name }}</span>{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Unknown</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td class="small text-nowrap">{{ attempt.start_time|default:"-" }}</td>
|
||||||
|
<td class="small text-nowrap">{{ attempt.end_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>
|
||||||
|
{% if attempt.user_user_id and collection %}
|
||||||
|
<a class="btn btn-sm btn-outline-primary"
|
||||||
|
href="{% url 'atlas:collection_history_user' exam_id=collection.pk user_pk=attempt.user_user_id %}">
|
||||||
|
View attempt
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-sm btn-outline-primary"
|
||||||
|
href="{% url 'atlas:collection_shared_attempt_overview' attempt.pk %}">
|
||||||
|
View attempt
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
No collection attempts found. Try adjusting the filters.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+45
-80
@@ -5844,72 +5844,9 @@ def collection_exam_user_search(request, pk):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def collection_shared_with_me(request):
|
def collection_shared_with_me(request):
|
||||||
"""Full page listing all collection attempts that have been shared with the current user.
|
"""Legacy redirect — the shared-with-me page is now part of the supervision page."""
|
||||||
|
return redirect(reverse("atlas:collection_supervision"))
|
||||||
|
|
||||||
Includes attempts where the user is:
|
|
||||||
- An author or marker of the collection
|
|
||||||
- A supervisor of the attempt owner (share_with_supervisor=True or exam results_supervisor_visible)
|
|
||||||
- Manually listed in CidUserExam.shared_with_users
|
|
||||||
|
|
||||||
The user's own attempts are excluded.
|
|
||||||
"""
|
|
||||||
collection_ct = ContentType.objects.get_for_model(CaseCollection)
|
|
||||||
|
|
||||||
# Manual shares
|
|
||||||
manual_qs = CidUserExam.objects.filter(
|
|
||||||
shared_with_users=request.user,
|
|
||||||
content_type=collection_ct,
|
|
||||||
).exclude(user_user=request.user)
|
|
||||||
|
|
||||||
# Author or marker of the collection
|
|
||||||
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 (per-attempt flag)
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Exam-level supervisor visibility
|
|
||||||
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").order_by("-start_time")
|
|
||||||
|
|
||||||
attempt_list = [(attempt, attempt.exam) for attempt in all_attempts]
|
|
||||||
|
|
||||||
return render(
|
|
||||||
request,
|
|
||||||
"atlas/shared_with_me.html",
|
|
||||||
{"attempt_list": attempt_list},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -5993,13 +5930,31 @@ def collection_supervision(request):
|
|||||||
- GET param ``group_by``: ``user`` (default) or ``collection``
|
- GET param ``group_by``: ``user`` (default) or ``collection``
|
||||||
- GET params ``date_from`` / ``date_to``: ISO date strings to filter by
|
- GET params ``date_from`` / ``date_to``: ISO date strings to filter by
|
||||||
attempt start_time (inclusive).
|
attempt start_time (inclusive).
|
||||||
|
- GET param ``q``: free-text search over learner name / username / email.
|
||||||
|
- GET param ``supervisor_only``: ``1`` to restrict to attempts where the current
|
||||||
|
user is the assigned supervisor (only shown when the user is a supervisor).
|
||||||
"""
|
"""
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.utils.dateparse import parse_date
|
||||||
|
|
||||||
collection_ct = ContentType.objects.get_for_model(CaseCollection)
|
collection_ct = ContentType.objects.get_for_model(CaseCollection)
|
||||||
|
|
||||||
# ---- build base queryset of accessible attempts ----
|
# ---- determine supervisor status ----
|
||||||
|
is_supervisor = False
|
||||||
|
trainee_users = User.objects.none()
|
||||||
|
trainee_cid_users = CidUser.objects.none()
|
||||||
|
try:
|
||||||
|
supervisor = request.user.supervisor
|
||||||
|
is_supervisor = True
|
||||||
|
trainee_users = User.objects.filter(userprofile__supervisor=supervisor)
|
||||||
|
trainee_cid_users = CidUser.objects.filter(supervisor=supervisor)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
supervisor_only = request.GET.get("supervisor_only", "") == "1" and is_supervisor
|
||||||
|
|
||||||
|
# ---- build base querysets of accessible attempts ----
|
||||||
manual_qs = CidUserExam.objects.filter(
|
manual_qs = CidUserExam.objects.filter(
|
||||||
shared_with_users=request.user,
|
shared_with_users=request.user,
|
||||||
content_type=collection_ct,
|
content_type=collection_ct,
|
||||||
@@ -6017,10 +5972,7 @@ def collection_supervision(request):
|
|||||||
|
|
||||||
supervisor_qs = CidUserExam.objects.none()
|
supervisor_qs = CidUserExam.objects.none()
|
||||||
supervisor_exam_qs = CidUserExam.objects.none()
|
supervisor_exam_qs = CidUserExam.objects.none()
|
||||||
try:
|
if is_supervisor:
|
||||||
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(
|
supervisor_qs = CidUserExam.objects.filter(
|
||||||
content_type=collection_ct,
|
content_type=collection_ct,
|
||||||
share_with_supervisor=True,
|
share_with_supervisor=True,
|
||||||
@@ -6037,21 +5989,21 @@ def collection_supervision(request):
|
|||||||
).filter(
|
).filter(
|
||||||
Q(user_user__in=trainee_users) | Q(cid_user__in=trainee_cid_users)
|
Q(user_user__in=trainee_users) | Q(cid_user__in=trainee_cid_users)
|
||||||
).exclude(user_user=request.user)
|
).exclude(user_user=request.user)
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
all_attempts = (
|
if supervisor_only:
|
||||||
manual_qs | authored_qs | supervisor_qs | supervisor_exam_qs
|
all_attempts = (supervisor_qs | supervisor_exam_qs).distinct()
|
||||||
).distinct().select_related("user_user", "cid_user")
|
else:
|
||||||
|
all_attempts = (
|
||||||
|
manual_qs | authored_qs | supervisor_qs | supervisor_exam_qs
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
all_attempts = all_attempts.select_related("user_user", "cid_user")
|
||||||
|
|
||||||
# ---- date filter ----
|
# ---- date filter ----
|
||||||
date_from_raw = request.GET.get("date_from", "").strip()
|
date_from_raw = request.GET.get("date_from", "").strip()
|
||||||
date_to_raw = request.GET.get("date_to", "").strip()
|
date_to_raw = request.GET.get("date_to", "").strip()
|
||||||
date_from = None
|
|
||||||
date_to = None
|
|
||||||
try:
|
try:
|
||||||
if date_from_raw:
|
if date_from_raw:
|
||||||
from django.utils.dateparse import parse_date
|
|
||||||
date_from = parse_date(date_from_raw)
|
date_from = parse_date(date_from_raw)
|
||||||
if date_from:
|
if date_from:
|
||||||
all_attempts = all_attempts.filter(start_time__date__gte=date_from)
|
all_attempts = all_attempts.filter(start_time__date__gte=date_from)
|
||||||
@@ -6059,13 +6011,24 @@ def collection_supervision(request):
|
|||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
if date_to_raw:
|
if date_to_raw:
|
||||||
from django.utils.dateparse import parse_date
|
|
||||||
date_to = parse_date(date_to_raw)
|
date_to = parse_date(date_to_raw)
|
||||||
if date_to:
|
if date_to:
|
||||||
all_attempts = all_attempts.filter(start_time__date__lte=date_to)
|
all_attempts = all_attempts.filter(start_time__date__lte=date_to)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# ---- user / learner search ----
|
||||||
|
q = request.GET.get("q", "").strip()
|
||||||
|
if q:
|
||||||
|
all_attempts = all_attempts.filter(
|
||||||
|
Q(user_user__first_name__icontains=q)
|
||||||
|
| Q(user_user__last_name__icontains=q)
|
||||||
|
| Q(user_user__username__icontains=q)
|
||||||
|
| Q(user_user__email__icontains=q)
|
||||||
|
| Q(cid_user__name__icontains=q)
|
||||||
|
| Q(cid_user__email__icontains=q)
|
||||||
|
)
|
||||||
|
|
||||||
all_attempts = all_attempts.order_by("-start_time")
|
all_attempts = all_attempts.order_by("-start_time")
|
||||||
|
|
||||||
# ---- grouping ----
|
# ---- grouping ----
|
||||||
@@ -6094,7 +6057,6 @@ def collection_supervision(request):
|
|||||||
key = (key_id, key_label)
|
key = (key_id, key_label)
|
||||||
grouped.setdefault(key, []).append((attempt, collection_obj))
|
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 = sorted(grouped.items(), key=lambda x: str(x[0][1]).lower())
|
||||||
groups = [(label, rows) for (_, label), rows in groups]
|
groups = [(label, rows) for (_, label), rows in groups]
|
||||||
|
|
||||||
@@ -6106,6 +6068,9 @@ def collection_supervision(request):
|
|||||||
"group_by": group_by,
|
"group_by": group_by,
|
||||||
"date_from": date_from_raw,
|
"date_from": date_from_raw,
|
||||||
"date_to": date_to_raw,
|
"date_to": date_to_raw,
|
||||||
|
"q": q,
|
||||||
|
"is_supervisor": is_supervisor,
|
||||||
|
"supervisor_only": supervisor_only,
|
||||||
"total_attempts": all_attempts.count(),
|
"total_attempts": all_attempts.count(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -214,9 +214,6 @@
|
|||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'atlas:collection_supervision' %}">Supervision</a>
|
<a class="dropdown-item" href="{% url 'atlas:collection_supervision' %}">Supervision</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="{% url 'atlas:collection_shared_with_me' %}">Shared with me</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown pt-0">
|
<li class="nav-item dropdown pt-0">
|
||||||
|
|||||||
Reference in New Issue
Block a user