Add sync functionality for prerequisite users in collection detail

This commit is contained in:
Ross
2025-10-13 14:14:17 +01:00
parent eeef34bffc
commit 2c30145d3a
5 changed files with 118 additions and 29 deletions
+44 -22
View File
@@ -4,30 +4,30 @@
{% partialdef casedetails-management-links %}
(<a href="{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk casedetail.case.pk %}"><i class="bi bi-display" title="Setup default display"></i></a>
(<a href="{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk casedetail.case.pk %}"><i class="bi bi-display" title="Setup default display"></i></a>
{% if casedetail.default_viewerstate %}
<i class="bi bi-check text-success" title="This case has a default viewerstate defined"></i>
{% endif %}
)
{% if casedetail.default_viewerstate %}
<i class="bi bi-check text-success" title="This case has a default viewerstate defined"></i>
{% endif %}
)
(<a href="{% url 'atlas:collection_case_details' casedetail.collection.pk casedetail.case.pk %}"><i class="bi bi-info-square" title="Case details"></i></a>)
(<a href="{% url 'atlas:collection_case_details' casedetail.collection.pk casedetail.case.pk %}"><i class="bi bi-info-square" title="Case details"></i></a>)
{% if collection.collection_type == "QUE" %}
(<a href='{% url "atlas:collection_case_questions" casedetail.collection.pk casedetail.case.pk %}'>
{% if casedetail.question_schema %}
<i class="bi bi-question-square text-success" title="This case has questions defined."></i>
{% else %}
<i class="bi bi-question-square text-danger" title="This case has no questions defined."></i>
{% endif %}
</a>
)
{% endif %}
{% if collection.collection_type == "QUE" %}
(<a href='{% url "atlas:collection_case_questions" casedetail.collection.pk casedetail.case.pk %}'>
{% if casedetail.question_schema %}
<i class="bi bi-question-square text-success" title="This case has questions defined."></i>
{% else %}
<i class="bi bi-question-square text-danger" title="This case has no questions defined."></i>
{% endif %}
</a>
)
{% endif %}
{% if casedetail.case.previous_case %}
(<a href='{% url "atlas:collection_case_priors" casedetail.collection.pk casedetail.case.pk %}'>
<i class="bi bi-link-45deg" title="Manage priors"></i></a>)
{% endif %}
{% if casedetail.case.previous_case %}
(<a href='{% url "atlas:collection_case_priors" casedetail.collection.pk casedetail.case.pk %}'>
<i class="bi bi-link-45deg" title="Manage priors"></i></a>)
{% endif %}
{% endpartialdef %}
{% block content %}
@@ -42,6 +42,17 @@
Collection Type: {{collection.get_collection_type_display}}<br />
Self review: {{collection.self_review}}<br />
Open access: {{collection.open_access}}<br />
{% if collection.prerequisites.exists %}
Prerequisite collections:
<ul>
{% for prereq in collection.prerequisites.all %}
<li>
<a href="{% url 'atlas:collection_detail' prereq.pk %}">{{ prereq.name }}</a>
&nbsp;
</li>
{% endfor %}
</ul>
{% endif %}
</div>
@@ -58,14 +69,17 @@
<p>Review collection <a href='{% url "atlas:collection_viva" collection.pk %}'>here</a>
{% endif %}
<h3>Cases</h3>
<ol id="full-question-list" class="sortable">
{% for casedetail in casesdetails %}
<li data-question_pk={{casedetail.case.pk}}>
<a href="{% url 'atlas:collection_case_view' pk=collection.pk case_number=forloop.counter0 %}">Case {{forloop.counter}}</a>
:
:
{% if casedetail.case.series.count == 0 %}
&nbsp;<i class="bi bi-exclamation-circle text-warning" title="No series attached to this case"></i>&nbsp;
{% endif %}
@@ -114,6 +128,14 @@
<div id="user-list"></div>
</details>
<p>
<button
title="Sync users from all prerequisite collections into this collection"
hx-post="{% url 'atlas:collection_sync_prerequisite_users' collection.pk %}"
hx-swap="outerHTML"
class="btn btn-sm btn-secondary"
>Sync prerequisite users</button>
</p>
{% endif %}
{% include 'exam_overview_js.html' %}
{% endblock %}
@@ -6,6 +6,7 @@
{% if request.user.is_authenticated and valid_user %}
User: {{request.user}}<br/>
{% if cid_exam %}
Started: {{cid_exam.start_time}} <br/>
@@ -5,7 +5,12 @@
<p>{{ message }}</p>
{% if prereq %}
<p>
You must complete the collection: <a href="{% url 'atlas:collection_take_start' prereq.pk %}">{{ prereq.name }}</a>
You must complete the collection:
{% if cid and passcode %}
<a href="{% url 'atlas:collection_take_start' prereq.pk %}?cid={{ cid }}&passcode={{ passcode }}">{{ prereq.name }}</a>
{% else %}
<a href="{% url 'atlas:collection_take_start' prereq.pk %}">{{ prereq.name }}</a>
{% endif %}
</p>
{% endif %}
<p>
+5
View File
@@ -80,6 +80,11 @@ urlpatterns = [
views.collection_take_start,
name="collection_take_start",
),
path(
"collection/<int:pk>/sync_prerequisite_users",
views.collection_sync_prerequisite_users,
name="collection_sync_prerequisite_users",
),
path("collection/<int:pk>/authors", views.CaseCollectionAuthorUpdate.as_view(), name="collection_authors"),
path(
"collection/<int:pk>/toggle_results_published",
+62 -6
View File
@@ -2189,9 +2189,7 @@ def collection_take_start(request, pk, cid=None, passcode=None):
Returns:
_type_: _description_
"""
print(f"Collection take start: {pk}, cid: {cid}, passcode: {passcode}")
collection = get_object_or_404(CaseCollection, pk=pk)
print(f"Collection: {collection}")
try:
collection.check_user_can_take(cid, passcode, request.user)
@@ -2204,7 +2202,7 @@ def collection_take_start(request, pk, cid=None, passcode=None):
return render(
request,
'atlas/prerequisite_required.html',
{'message': str(e), 'prereq': prereq},
{'message': str(e), 'prereq': prereq, 'cid': cid, 'passcode': passcode},
)
@@ -2626,7 +2624,7 @@ def collection_take_overview(
collection.check_user_can_take(cid, passcode, request.user)
except PrerequisiteRequired as e:
prereq = getattr(e, 'prereq', None)
return render(request, 'atlas/prerequisite_required.html', {'message': str(e), 'prereq': prereq})
return render(request, 'atlas/prerequisite_required.html', {'message': str(e), 'prereq': prereq, 'cid': cid, 'passcode': passcode})
cid_user_exam = collection.get_or_create_cid_user_exam(
cid=cid, user_user=request.user
@@ -2703,7 +2701,7 @@ def collection_case_view_take_answers(
collection.check_user_can_take(cid, passcode, request.user)
except PrerequisiteRequired as e:
prereq = getattr(e, 'prereq', None)
return render(request, 'atlas/prerequisite_required.html', {'message': str(e), 'prereq': prereq})
return render(request, 'atlas/prerequisite_required.html', {'message': str(e), 'prereq': prereq, 'cid': cid, 'passcode': passcode})
case, case_count = collection.get_case_by_index(case_number, case_count=True)
@@ -2756,7 +2754,7 @@ def collection_case_view_take(
collection.check_user_can_take(cid, passcode, request.user)
except PrerequisiteRequired as e:
prereq = getattr(e, 'prereq', None)
return render(request, 'atlas/prerequisite_required.html', {'message': str(e), 'prereq': prereq})
return render(request, 'atlas/prerequisite_required.html', {'message': str(e), 'prereq': prereq, 'cid': cid, 'passcode': passcode})
case, case_count = collection.get_case_by_index(case_number, case_count=True)
@@ -3563,6 +3561,64 @@ def collection_question_schemas(request, exam_id: int):
)
@user_is_collection_author_or_atlas_editor
def collection_sync_prerequisite_users(request, pk: int):
"""Copy valid cid_users and user_users from all prerequisite collections into this collection.
This endpoint is intended to be called via HTMX from the collection detail page.
"""
if not request.htmx:
raise Http404("Invalid request")
collection = get_object_or_404(CaseCollection, pk=pk)
# Collect the closure of collections: the target collection and all prerequisites (recursively)
to_visit = [collection]
visited = {}
while to_visit:
col = to_visit.pop()
if col.pk in visited:
continue
visited[col.pk] = col
for p in col.prerequisites.all():
if p.pk not in visited:
to_visit.append(p)
collections = list(visited.values())
# Compute the union of all cid users and user users across the set
union_cid_users = set()
union_user_users = set()
for col in collections:
union_cid_users.update(list(col.valid_cid_users.all()))
union_user_users.update(list(col.valid_user_users.all()))
# Apply the union to each collection and count additions
total_added_cid = 0
total_added_user = 0
per_collection_added = []
for col in collections:
added_cid = 0
added_user = 0
for cid_user in union_cid_users:
if not col.valid_cid_users.filter(pk=cid_user.pk).exists():
col.valid_cid_users.add(cid_user)
added_cid += 1
for user in union_user_users:
if not col.valid_user_users.filter(pk=user.pk).exists():
col.valid_user_users.add(user)
added_user += 1
if added_cid or added_user:
col.save()
total_added_cid += added_cid
total_added_user += added_user
per_collection_added.append((col.pk, added_cid, added_user))
return HttpResponse(
f"Two-way sync complete — total added: {total_added_cid} cid(s), {total_added_user} user(s)."
)
def collection_reset_answers_user_list(request, exam_id: int):
if request.htmx:
collection = get_object_or_404(CaseCollection, pk=exam_id)