feat: Implement per-case query and messaging functionality with UI updates and tests
This commit is contained in:
+1
-1
@@ -120,7 +120,7 @@ class CaseSelect(Select):
|
|||||||
class MoveSelectedSeriesForm(Form):
|
class MoveSelectedSeriesForm(Form):
|
||||||
destination_case = ModelChoiceField(
|
destination_case = ModelChoiceField(
|
||||||
queryset=Case.objects.none(),
|
queryset=Case.objects.none(),
|
||||||
widget=CaseSelect(attrs={"class": "form-select form-select-sm"}),
|
widget=HiddenInput(),
|
||||||
label="Move selected series to case",
|
label="Move selected series to case",
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-04-27 21:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('atlas', '0097_casereviewmessage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='casecollection',
|
||||||
|
name='case_query_messaging_enabled',
|
||||||
|
field=models.BooleanField(default=True, help_text='Enable per-case query and messaging for this collection.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1356,6 +1356,10 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
|||||||
cid_user_exam = GenericRelation("generic.CidUserExam", related_query_name="atlas_exams")
|
cid_user_exam = GenericRelation("generic.CidUserExam", related_query_name="atlas_exams")
|
||||||
|
|
||||||
feedback_once_collection_complete = models.BooleanField(default=True, help_text="If true feedback is only given once the collection is complete. If false feedback is given after each case.")
|
feedback_once_collection_complete = models.BooleanField(default=True, help_text="If true feedback is only given once the collection is complete. If false feedback is given after each case.")
|
||||||
|
case_query_messaging_enabled = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Enable per-case query and messaging for this collection.",
|
||||||
|
)
|
||||||
|
|
||||||
question_time_limit = models.PositiveIntegerField(
|
question_time_limit = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
{% load case_widgets %}
|
||||||
|
|
||||||
{% partialdef case-series %}
|
{% partialdef case-series %}
|
||||||
{% for series in case.get_ordered_series %}
|
{% for series in case.get_ordered_series %}
|
||||||
<span class="series-block" id="series-block-{{ series.pk }}">
|
<span class="series-block" id="series-block-{{ series.pk }}">
|
||||||
@@ -471,13 +473,18 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="series-move-panel border rounded p-2">
|
<details class="series-move-panel border rounded p-2">
|
||||||
<div class="small fw-semibold mb-2">Move selected series to another case</div>
|
<summary class="small fw-semibold">Move selected series to another case</summary>
|
||||||
{% if move_series_form %}
|
{% if move_series_form %}
|
||||||
<form id="move-selected-series-form" class="d-flex flex-column gap-2">
|
<form id="move-selected-series-form" class="d-flex flex-column gap-2 mt-2">
|
||||||
{{ move_series_form.destination_case }}
|
{{ move_series_form.destination_case }}
|
||||||
|
<div class="small text-muted">Click any case row below to select the destination case.</div>
|
||||||
|
<div id="selected-move-case" class="small text-muted">No destination case selected.</div>
|
||||||
|
{% case_search_widget collection=None input_id='move-series-case-search-input' target_id='move-series-case-search-results' %}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
id="move-selected-series-submit"
|
||||||
class="btn btn-sm btn-outline-primary text-start"
|
class="btn btn-sm btn-outline-primary text-start"
|
||||||
|
disabled
|
||||||
hx-post="{% url 'atlas:move_selected_series_to_case' case.pk %}"
|
hx-post="{% url 'atlas:move_selected_series_to_case' case.pk %}"
|
||||||
hx-include="[name='series-ids']:checked, #move-selected-series-form [name='destination_case']"
|
hx-include="[name='series-ids']:checked, #move-selected-series-form [name='destination_case']"
|
||||||
hx-target="#series-action-results"
|
hx-target="#series-action-results"
|
||||||
@@ -487,9 +494,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-muted small">Case move options are unavailable in this view.</div>
|
<div class="text-muted small mt-2">Case move options are unavailable in this view.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info mt-2" id="series-action-alert" style="display:none;">
|
<div class="alert alert-info mt-2" id="series-action-alert" style="display:none;">
|
||||||
<span id="series-action-results" ></span>
|
<span id="series-action-results" ></span>
|
||||||
@@ -1024,6 +1031,39 @@
|
|||||||
detailsElement.addEventListener("toggle", toggleCheckboxes);
|
detailsElement.addEventListener("toggle", toggleCheckboxes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moveSeriesForm = document.getElementById("move-selected-series-form");
|
||||||
|
if (moveSeriesForm) {
|
||||||
|
const hiddenInput = moveSeriesForm.querySelector("[name='destination_case']");
|
||||||
|
const moveSubmitButton = document.getElementById("move-selected-series-submit");
|
||||||
|
|
||||||
|
function syncMoveSeriesSubmitState() {
|
||||||
|
if (!moveSubmitButton || !hiddenInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
moveSubmitButton.disabled = !hiddenInput.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveSeriesForm.addEventListener("case:selected", function(event) {
|
||||||
|
const selectionLabel = document.getElementById("selected-move-case");
|
||||||
|
if (!hiddenInput || !event.detail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const casePk = event.detail.casePk;
|
||||||
|
const caseTitle = event.detail.caseTitle || ("Case #" + casePk);
|
||||||
|
hiddenInput.value = casePk;
|
||||||
|
|
||||||
|
if (selectionLabel) {
|
||||||
|
selectionLabel.innerHTML = "Selected destination: <strong>" + caseTitle + " (" + casePk + ")</strong>";
|
||||||
|
selectionLabel.classList.remove("text-muted");
|
||||||
|
}
|
||||||
|
|
||||||
|
syncMoveSeriesSubmitState();
|
||||||
|
});
|
||||||
|
|
||||||
|
syncMoveSeriesSubmitState();
|
||||||
|
}
|
||||||
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById("findingModal"));
|
const modal = new bootstrap.Modal(document.getElementById("findingModal"));
|
||||||
const modalBody = document.getElementById("findingModalBody");
|
const modalBody = document.getElementById("findingModalBody");
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if question_completed %}
|
{% if question_completed and collection.case_query_messaging_enabled %}
|
||||||
<details class="mt-3" {% if case_review_stats.has_outstanding_any %}open{% endif %}>
|
<details class="mt-3" {% if case_review_stats.has_outstanding_any %}open{% endif %}>
|
||||||
<summary class="small fw-semibold d-flex flex-wrap align-items-center gap-2">
|
<summary class="small fw-semibold d-flex flex-wrap align-items-center gap-2">
|
||||||
<span>Case conversation</span>
|
<span>Case conversation</span>
|
||||||
|
|||||||
@@ -88,6 +88,7 @@
|
|||||||
|
|
||||||
<div class="card border-secondary w-100 mt-3">
|
<div class="card border-secondary w-100 mt-3">
|
||||||
<div class="card-body py-2 px-3 text-start text-md-end">
|
<div class="card-body py-2 px-3 text-start text-md-end">
|
||||||
|
{% if collection.case_query_messaging_enabled %}
|
||||||
<div class="small text-muted">Outstanding messages</div>
|
<div class="small text-muted">Outstanding messages</div>
|
||||||
<div class="fw-semibold {% if total_outstanding_messages %}text-danger{% endif %}">
|
<div class="fw-semibold {% if total_outstanding_messages %}text-danger{% endif %}">
|
||||||
{{ total_outstanding_messages }} pending acknowledgement
|
{{ total_outstanding_messages }} pending acknowledgement
|
||||||
@@ -128,6 +129,13 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="small text-muted">Case query/messaging</div>
|
||||||
|
<div class="fw-semibold text-muted">Disabled for this collection</div>
|
||||||
|
<div class="mt-2 d-flex flex-wrap gap-2 justify-content-md-end">
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:collection_history' collection.pk %}">Open History</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:collection_detail' collection.pk %}">Collection Detail</a>
|
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:collection_detail' collection.pk %}">Collection Detail</a>
|
||||||
|
{% if collection.case_query_messaging_enabled %}
|
||||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:user_messages_inbox' %}">Global Messages Inbox</a>
|
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:user_messages_inbox' %}">Global Messages Inbox</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -40,8 +42,13 @@
|
|||||||
<div class="col-md-3 col-sm-6">
|
<div class="col-md-3 col-sm-6">
|
||||||
<div class="card border-secondary h-100 {% if total_outstanding_messages %}border-danger-subtle{% endif %}">
|
<div class="card border-secondary h-100 {% if total_outstanding_messages %}border-danger-subtle{% endif %}">
|
||||||
<div class="card-body py-3">
|
<div class="card-body py-3">
|
||||||
|
{% if collection.case_query_messaging_enabled %}
|
||||||
<div class="small text-muted">Outstanding Messages</div>
|
<div class="small text-muted">Outstanding Messages</div>
|
||||||
<div class="h4 mb-0 {% if total_outstanding_messages %}text-danger{% endif %}">{{ total_outstanding_messages }}</div>
|
<div class="h4 mb-0 {% if total_outstanding_messages %}text-danger{% endif %}">{{ total_outstanding_messages }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="small text-muted">Case Messaging</div>
|
||||||
|
<div class="h6 mb-0 text-muted">Disabled</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,6 +78,7 @@
|
|||||||
<div class="small text-muted mt-1">Started: {{ userexam.start_time|default:"-" }}</div>
|
<div class="small text-muted mt-1">Started: {{ userexam.start_time|default:"-" }}</div>
|
||||||
<div class="small text-muted">Ended: {{ userexam.end_time|default:"-" }}</div>
|
<div class="small text-muted">Ended: {{ userexam.end_time|default:"-" }}</div>
|
||||||
<div class="mt-2 d-flex flex-wrap gap-2">
|
<div class="mt-2 d-flex flex-wrap gap-2">
|
||||||
|
{% if collection.case_query_messaging_enabled %}
|
||||||
{% if userexam.outstanding_feedback_count %}
|
{% if userexam.outstanding_feedback_count %}
|
||||||
<span class="badge bg-danger">{{ userexam.outstanding_feedback_count }} outstanding message{{ userexam.outstanding_feedback_count|pluralize }}</span>
|
<span class="badge bg-danger">{{ userexam.outstanding_feedback_count }} outstanding message{{ userexam.outstanding_feedback_count|pluralize }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -79,11 +87,14 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-dark-subtle text-body-secondary">No conversation yet</span>
|
<span class="badge bg-dark-subtle text-body-secondary">No conversation yet</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<a class="btn btn-sm btn-primary" href="{% url 'atlas:collection_history_user' collection.pk userexam.user_user.pk %}">Open Attempt</a>
|
<a class="btn btn-sm btn-primary" href="{% url 'atlas:collection_history_user' collection.pk userexam.user_user.pk %}">Open Attempt</a>
|
||||||
|
{% if collection.case_query_messaging_enabled %}
|
||||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:collection_user_messages' collection.pk userexam.user_user.pk %}">Messages</a>
|
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:collection_user_messages' collection.pk userexam.user_user.pk %}">Messages</a>
|
||||||
|
{% endif %}
|
||||||
<button
|
<button
|
||||||
hx-post="{% url 'atlas:collection_reset_answers_user' collection.pk userexam.user_user.pk %}"
|
hx-post="{% url 'atlas:collection_reset_answers_user' collection.pk userexam.user_user.pk %}"
|
||||||
hx-target="#user-history-{{ userexam.user_user.pk }}"
|
hx-target="#user-history-{{ userexam.user_user.pk }}"
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
<div class="small text-muted mt-1">Started: {{ cidexam.start_time|default:"-" }}</div>
|
<div class="small text-muted mt-1">Started: {{ cidexam.start_time|default:"-" }}</div>
|
||||||
<div class="small text-muted">Ended: {{ cidexam.end_time|default:"-" }}</div>
|
<div class="small text-muted">Ended: {{ cidexam.end_time|default:"-" }}</div>
|
||||||
<div class="mt-2 d-flex flex-wrap gap-2">
|
<div class="mt-2 d-flex flex-wrap gap-2">
|
||||||
|
{% if collection.case_query_messaging_enabled %}
|
||||||
{% if cidexam.outstanding_feedback_count %}
|
{% if cidexam.outstanding_feedback_count %}
|
||||||
<span class="badge bg-danger">{{ cidexam.outstanding_feedback_count }} outstanding message{{ cidexam.outstanding_feedback_count|pluralize }}</span>
|
<span class="badge bg-danger">{{ cidexam.outstanding_feedback_count }} outstanding message{{ cidexam.outstanding_feedback_count|pluralize }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -132,11 +144,14 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-dark-subtle text-body-secondary">No conversation yet</span>
|
<span class="badge bg-dark-subtle text-body-secondary">No conversation yet</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<a class="btn btn-sm btn-primary" href="{% url 'atlas:collection_history_ciduser' collection.pk cidexam.cid_user.cid %}">Open Attempt</a>
|
<a class="btn btn-sm btn-primary" href="{% url 'atlas:collection_history_ciduser' collection.pk cidexam.cid_user.cid %}">Open Attempt</a>
|
||||||
|
{% if collection.case_query_messaging_enabled %}
|
||||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:collection_cid_messages' collection.pk cidexam.cid_user.cid %}">Messages</a>
|
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:collection_cid_messages' collection.pk cidexam.cid_user.cid %}">Messages</a>
|
||||||
|
{% endif %}
|
||||||
<button
|
<button
|
||||||
hx-post="{% url 'atlas:collection_reset_answers_ciduser' collection.pk cidexam.cid_user.cid %}"
|
hx-post="{% url 'atlas:collection_reset_answers_ciduser' collection.pk cidexam.cid_user.cid %}"
|
||||||
hx-target="#cid-history-{{ cidexam.cid_user.cid }}"
|
hx-target="#cid-history-{{ cidexam.cid_user.cid }}"
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if collection.case_query_messaging_enabled %}
|
||||||
<details class="mt-3" {% if row.case_review_stats.has_outstanding_any %}open{% endif %}>
|
<details class="mt-3" {% if row.case_review_stats.has_outstanding_any %}open{% endif %}>
|
||||||
<summary class="small fw-semibold d-flex flex-wrap align-items-center gap-2">
|
<summary class="small fw-semibold d-flex flex-wrap align-items-center gap-2">
|
||||||
<span>Case conversation</span>
|
<span>Case conversation</span>
|
||||||
@@ -151,6 +152,7 @@
|
|||||||
<div class="small text-muted">Loading case conversation...</div>
|
<div class="small text-muted">Loading case conversation...</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% if collection.in_review_mode or cid_user_exam.completed %}
|
{% if collection.in_review_mode or cid_user_exam.completed %}
|
||||||
<span class="badge bg-light text-dark ms-2">Review</span>
|
<span class="badge bg-light text-dark ms-2">Review</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if total_outstanding_feedback %}
|
{% if collection.case_query_messaging_enabled and total_outstanding_feedback %}
|
||||||
<span class="badge bg-danger ms-2">{{ total_outstanding_feedback }} outstanding feedback</span>
|
<span class="badge bg-danger ms-2">{{ total_outstanding_feedback }} outstanding feedback</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
{% if cid_user_exam.completed %}
|
{% if cid_user_exam.completed %}
|
||||||
<div class="mt-1 small">End time: {{ cid_user_exam.end_time|default:"-" }}</div>
|
<div class="mt-1 small">End time: {{ cid_user_exam.end_time|default:"-" }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if collection.in_review_mode or cid_user_exam.completed %}
|
{% if collection.case_query_messaging_enabled and (collection.in_review_mode or cid_user_exam.completed) %}
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<a class="btn btn-sm {% if total_outstanding_feedback %}btn-danger{% else %}btn-outline-secondary{% endif %}"
|
<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 %}">
|
||||||
@@ -68,11 +68,13 @@
|
|||||||
{% if self_review %}
|
{% if self_review %}
|
||||||
<span class="badge bg-info-subtle text-info-emphasis ms-1">Self review added</span>
|
<span class="badge bg-info-subtle text-info-emphasis ms-1">Self review added</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if collection.case_query_messaging_enabled %}
|
||||||
{% if review_stats.has_outstanding_feedback %}
|
{% if review_stats.has_outstanding_feedback %}
|
||||||
<span class="badge bg-danger ms-1">{{ review_stats.outstanding_feedback_count }} feedback item{{ review_stats.outstanding_feedback_count|pluralize }} to review</span>
|
<span class="badge bg-danger ms-1">{{ review_stats.outstanding_feedback_count }} feedback item{{ review_stats.outstanding_feedback_count|pluralize }} to review</span>
|
||||||
{% elif review_stats.has_messages %}
|
{% elif review_stats.has_messages %}
|
||||||
<span class="badge bg-secondary ms-1">{{ review_stats.message_count }} conversation message{{ review_stats.message_count|pluralize }}</span>
|
<span class="badge bg-secondary ms-1">{{ review_stats.message_count }} conversation message{{ review_stats.message_count|pluralize }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if collection.show_title_post %}
|
{% if collection.show_title_post %}
|
||||||
<div class="small text-muted">
|
<div class="small text-muted">
|
||||||
@@ -90,7 +92,7 @@
|
|||||||
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 %}
|
{% if self_review %}Add New Self Review{% else %}Add Self Review{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% if review_stats.has_messages %}
|
{% 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 %}"
|
<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
|
View Conversation
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import pytest
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from atlas.models import CaseCollection, CaseDetail, CaseReviewMessage
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_collection_case_query_messaging_enabled_default_true():
|
||||||
|
collection = CaseCollection.objects.create(name="Default messaging collection")
|
||||||
|
|
||||||
|
assert collection.case_query_messaging_enabled is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_collection_case_review_thread_returns_404_when_messaging_disabled(create_case, client):
|
||||||
|
user_model = get_user_model()
|
||||||
|
author = user_model.objects.create_user(username="author-thread", password="testpass")
|
||||||
|
|
||||||
|
collection = CaseCollection.objects.create(
|
||||||
|
name="Thread disabled",
|
||||||
|
case_query_messaging_enabled=False,
|
||||||
|
)
|
||||||
|
collection.author.add(author)
|
||||||
|
|
||||||
|
case = create_case
|
||||||
|
CaseDetail.objects.create(collection=collection, case=case, sort_order=0)
|
||||||
|
attempt = collection.get_or_create_cid_user_exam(user_user=author)
|
||||||
|
|
||||||
|
client.force_login(author)
|
||||||
|
url = reverse(
|
||||||
|
"atlas:collection_case_review_thread",
|
||||||
|
kwargs={
|
||||||
|
"exam_id": collection.pk,
|
||||||
|
"cid_user_exam_id": attempt.pk,
|
||||||
|
"case_id": case.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_collection_user_messages_returns_404_when_messaging_disabled(create_case, client):
|
||||||
|
user_model = get_user_model()
|
||||||
|
author = user_model.objects.create_user(username="author-user-messages", password="testpass")
|
||||||
|
learner = user_model.objects.create_user(username="learner-user-messages", password="testpass")
|
||||||
|
|
||||||
|
collection = CaseCollection.objects.create(
|
||||||
|
name="User messages disabled",
|
||||||
|
case_query_messaging_enabled=False,
|
||||||
|
)
|
||||||
|
collection.author.add(author)
|
||||||
|
|
||||||
|
case = create_case
|
||||||
|
CaseDetail.objects.create(collection=collection, case=case, sort_order=0)
|
||||||
|
collection.get_or_create_cid_user_exam(user_user=learner)
|
||||||
|
|
||||||
|
client.force_login(author)
|
||||||
|
url = reverse(
|
||||||
|
"atlas:collection_user_messages",
|
||||||
|
kwargs={"exam_id": collection.pk, "user_pk": learner.pk},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_messages_inbox_ignores_disabled_collection_messages(create_case, client):
|
||||||
|
user_model = get_user_model()
|
||||||
|
moderator = user_model.objects.create_user(username="moderator", password="testpass")
|
||||||
|
learner = user_model.objects.create_user(username="learner", password="testpass")
|
||||||
|
|
||||||
|
enabled_collection = CaseCollection.objects.create(
|
||||||
|
name="Enabled messaging collection",
|
||||||
|
case_query_messaging_enabled=True,
|
||||||
|
)
|
||||||
|
disabled_collection = CaseCollection.objects.create(
|
||||||
|
name="Disabled messaging collection",
|
||||||
|
case_query_messaging_enabled=False,
|
||||||
|
)
|
||||||
|
enabled_collection.author.add(moderator)
|
||||||
|
disabled_collection.author.add(moderator)
|
||||||
|
|
||||||
|
case = create_case
|
||||||
|
CaseDetail.objects.create(collection=enabled_collection, case=case, sort_order=0)
|
||||||
|
CaseDetail.objects.create(collection=disabled_collection, case=case, sort_order=0)
|
||||||
|
|
||||||
|
enabled_attempt = enabled_collection.get_or_create_cid_user_exam(user_user=learner)
|
||||||
|
disabled_attempt = disabled_collection.get_or_create_cid_user_exam(user_user=learner)
|
||||||
|
|
||||||
|
CaseReviewMessage.objects.create(
|
||||||
|
cid_user_exam=enabled_attempt,
|
||||||
|
case=case,
|
||||||
|
author=learner,
|
||||||
|
message_type=CaseReviewMessage.MessageType.QUERY,
|
||||||
|
message="Enabled message",
|
||||||
|
requires_acknowledgement=True,
|
||||||
|
)
|
||||||
|
CaseReviewMessage.objects.create(
|
||||||
|
cid_user_exam=disabled_attempt,
|
||||||
|
case=case,
|
||||||
|
author=learner,
|
||||||
|
message_type=CaseReviewMessage.MessageType.QUERY,
|
||||||
|
message="Disabled message",
|
||||||
|
requires_acknowledgement=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
client.force_login(moderator)
|
||||||
|
response = client.get(reverse("atlas:user_messages_inbox"))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Enabled messaging collection" in response.content.decode()
|
||||||
|
assert "Disabled messaging collection" not in response.content.decode()
|
||||||
+47
-2
@@ -4024,6 +4024,7 @@ def collection_viva_series(request, pk, series_id):
|
|||||||
def collection_detail(request, pk):
|
def collection_detail(request, pk):
|
||||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||||
collection_content_type = ContentType.objects.get_for_model(collection)
|
collection_content_type = ContentType.objects.get_for_model(collection)
|
||||||
|
messaging_enabled = _collection_case_query_messaging_enabled(collection)
|
||||||
|
|
||||||
# cases = collection.cases.all().order_by("casedetail__sort_order")
|
# cases = collection.cases.all().order_by("casedetail__sort_order")
|
||||||
casedetails = (
|
casedetails = (
|
||||||
@@ -4034,6 +4035,7 @@ def collection_detail(request, pk):
|
|||||||
.order_by("sort_order")
|
.order_by("sort_order")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if messaging_enabled:
|
||||||
outstanding_messages = CaseReviewMessage.objects.filter(
|
outstanding_messages = CaseReviewMessage.objects.filter(
|
||||||
cid_user_exam__content_type=collection_content_type,
|
cid_user_exam__content_type=collection_content_type,
|
||||||
cid_user_exam__object_id=collection.pk,
|
cid_user_exam__object_id=collection.pk,
|
||||||
@@ -4077,6 +4079,10 @@ def collection_detail(request, pk):
|
|||||||
.filter(Q(outstanding_message_count__gt=0) | Q(total_review_message_count__gt=0))
|
.filter(Q(outstanding_message_count__gt=0) | Q(total_review_message_count__gt=0))
|
||||||
.order_by("-outstanding_message_count", "-total_review_message_count", "-start_time")
|
.order_by("-outstanding_message_count", "-total_review_message_count", "-start_time")
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
total_outstanding_messages = 0
|
||||||
|
user_message_rows = []
|
||||||
|
cid_message_rows = []
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -4100,6 +4106,7 @@ def collection_history(request, exam_id: int):
|
|||||||
|
|
||||||
casedetails = list(CaseDetail.objects.filter(collection=collection).order_by("sort_order"))
|
casedetails = list(CaseDetail.objects.filter(collection=collection).order_by("sort_order"))
|
||||||
|
|
||||||
|
if _collection_case_query_messaging_enabled(collection):
|
||||||
userexams = list(
|
userexams = list(
|
||||||
collection.get_user_exams()
|
collection.get_user_exams()
|
||||||
.select_related("user_user")
|
.select_related("user_user")
|
||||||
@@ -4132,6 +4139,15 @@ def collection_history(request, exam_id: int):
|
|||||||
)
|
)
|
||||||
.order_by("-outstanding_feedback_count", "completed", "-start_time")
|
.order_by("-outstanding_feedback_count", "completed", "-start_time")
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
userexams = list(collection.get_user_exams().select_related("user_user").order_by("completed", "-start_time"))
|
||||||
|
cidexams = list(collection.get_cid_exams().select_related("cid_user").order_by("completed", "-start_time"))
|
||||||
|
for userexam in userexams:
|
||||||
|
userexam.outstanding_feedback_count = 0
|
||||||
|
userexam.total_review_message_count = 0
|
||||||
|
for cidexam in cidexams:
|
||||||
|
cidexam.outstanding_feedback_count = 0
|
||||||
|
cidexam.total_review_message_count = 0
|
||||||
|
|
||||||
total_outstanding_messages = sum(item.outstanding_feedback_count for item in userexams) + sum(
|
total_outstanding_messages = sum(item.outstanding_feedback_count for item in userexams) + sum(
|
||||||
item.outstanding_feedback_count for item in cidexams
|
item.outstanding_feedback_count for item in cidexams
|
||||||
@@ -4292,6 +4308,7 @@ def collection_history_user(request, exam_id: int, user_pk: int):
|
|||||||
case_review_stats, _, _ = _get_case_review_stats_for_exam(
|
case_review_stats, _, _ = _get_case_review_stats_for_exam(
|
||||||
cid_user_exam=cid_user_exam,
|
cid_user_exam=cid_user_exam,
|
||||||
casedetails=casedetails,
|
casedetails=casedetails,
|
||||||
|
messaging_enabled=_collection_case_query_messaging_enabled(collection),
|
||||||
)
|
)
|
||||||
for casedetail in casedetails:
|
for casedetail in casedetails:
|
||||||
user_answer = casedetail.get_user_answers(user)
|
user_answer = casedetail.get_user_answers(user)
|
||||||
@@ -4348,6 +4365,7 @@ def collection_history_ciduser(request, exam_id: int, cid: int):
|
|||||||
case_review_stats, _, _ = _get_case_review_stats_for_exam(
|
case_review_stats, _, _ = _get_case_review_stats_for_exam(
|
||||||
cid_user_exam=cid_user_exam,
|
cid_user_exam=cid_user_exam,
|
||||||
casedetails=casedetails,
|
casedetails=casedetails,
|
||||||
|
messaging_enabled=_collection_case_query_messaging_enabled(collection),
|
||||||
)
|
)
|
||||||
for casedetail in casedetails:
|
for casedetail in casedetails:
|
||||||
user_answer = casedetail.get_cid_answers(cid)
|
user_answer = casedetail.get_cid_answers(cid)
|
||||||
@@ -4396,6 +4414,7 @@ def collection_user_messages(request, exam_id: int, user_pk: int):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||||
|
_enforce_collection_case_query_messaging_enabled(collection)
|
||||||
user = get_object_or_404(User, pk=user_pk)
|
user = get_object_or_404(User, pk=user_pk)
|
||||||
cid_user_exam = collection.get_or_create_cid_user_exam(user_user=user)
|
cid_user_exam = collection.get_or_create_cid_user_exam(user_user=user)
|
||||||
|
|
||||||
@@ -4436,6 +4455,7 @@ def collection_cid_messages(request, exam_id: int, cid: int):
|
|||||||
"""Supervisor page showing case conversations for one CID attempt."""
|
"""Supervisor page showing case conversations for one CID attempt."""
|
||||||
|
|
||||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||||
|
_enforce_collection_case_query_messaging_enabled(collection)
|
||||||
cid_user_exam = collection.get_or_create_cid_user_exam(cid=cid)
|
cid_user_exam = collection.get_or_create_cid_user_exam(cid=cid)
|
||||||
|
|
||||||
casedetails = list(
|
casedetails = list(
|
||||||
@@ -4517,10 +4537,31 @@ def user_messages_inbox(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
attempts = [item["attempt"] for item in attempts_map.values()]
|
attempts = [item["attempt"] for item in attempts_map.values()]
|
||||||
attempt_ids = [attempt.pk for attempt in attempts]
|
|
||||||
collection_ids = sorted({attempt.object_id for attempt in attempts})
|
collection_ids = sorted({attempt.object_id for attempt in attempts})
|
||||||
|
|
||||||
collections_by_id = CaseCollection.objects.in_bulk(collection_ids)
|
collections_by_id = CaseCollection.objects.in_bulk(collection_ids)
|
||||||
|
enabled_collection_ids = {
|
||||||
|
collection_id
|
||||||
|
for collection_id, collection in collections_by_id.items()
|
||||||
|
if collection is not None and _collection_case_query_messaging_enabled(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts = [attempt for attempt in attempts if attempt.object_id in enabled_collection_ids]
|
||||||
|
if not attempts:
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"atlas/user_messages_inbox.html",
|
||||||
|
{
|
||||||
|
"inbox_rows": [],
|
||||||
|
"total_outstanding": 0,
|
||||||
|
"total_needs_your_ack": 0,
|
||||||
|
"total_waiting_on_other": 0,
|
||||||
|
"show_all": request.GET.get("show") == "all",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
attempt_ids = [attempt.pk for attempt in attempts]
|
||||||
|
collection_ids = sorted(enabled_collection_ids)
|
||||||
|
|
||||||
casedetails_by_collection = defaultdict(list)
|
casedetails_by_collection = defaultdict(list)
|
||||||
all_case_ids = set()
|
all_case_ids = set()
|
||||||
@@ -4575,7 +4616,7 @@ def user_messages_inbox(request):
|
|||||||
attempt = item["attempt"]
|
attempt = item["attempt"]
|
||||||
viewer_role = item["viewer_role"]
|
viewer_role = item["viewer_role"]
|
||||||
collection = collections_by_id.get(attempt.object_id)
|
collection = collections_by_id.get(attempt.object_id)
|
||||||
if collection is None:
|
if collection is None or not _collection_case_query_messaging_enabled(collection):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
casedetails = casedetails_by_collection.get(collection.pk, [])
|
casedetails = casedetails_by_collection.get(collection.pk, [])
|
||||||
@@ -4822,6 +4863,7 @@ def collection_case_review_thread(request, exam_id: int, cid_user_exam_id: int,
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||||
|
_enforce_collection_case_query_messaging_enabled(collection)
|
||||||
collection_content_type = ContentType.objects.get_for_model(collection)
|
collection_content_type = ContentType.objects.get_for_model(collection)
|
||||||
cid_user_exam = get_object_or_404(
|
cid_user_exam = get_object_or_404(
|
||||||
CidUserExam,
|
CidUserExam,
|
||||||
@@ -5517,6 +5559,7 @@ def collection_feedback_overview(
|
|||||||
):
|
):
|
||||||
"""Show all case conversations for one learner attempt with outstanding items first."""
|
"""Show all case conversations for one learner attempt with outstanding items first."""
|
||||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||||
|
_enforce_collection_case_query_messaging_enabled(collection)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
collection.check_user_can_take(cid, passcode, request.user)
|
collection.check_user_can_take(cid, passcode, request.user)
|
||||||
@@ -5620,6 +5663,7 @@ def collection_take_overview(
|
|||||||
case_review_stats, outstanding_case_count, total_outstanding_feedback = _get_case_review_stats_for_exam(
|
case_review_stats, outstanding_case_count, total_outstanding_feedback = _get_case_review_stats_for_exam(
|
||||||
cid_user_exam=cid_user_exam,
|
cid_user_exam=cid_user_exam,
|
||||||
casedetails=casedetails,
|
casedetails=casedetails,
|
||||||
|
messaging_enabled=_collection_case_query_messaging_enabled(collection),
|
||||||
)
|
)
|
||||||
|
|
||||||
question_answer_tuples = []
|
question_answer_tuples = []
|
||||||
@@ -5940,6 +5984,7 @@ def collection_case_view_take(
|
|||||||
case_review_stats, _, _ = _get_case_review_stats_for_exam(
|
case_review_stats, _, _ = _get_case_review_stats_for_exam(
|
||||||
cid_user_exam=cid_user_exam,
|
cid_user_exam=cid_user_exam,
|
||||||
casedetails=[casedetail],
|
casedetails=[casedetail],
|
||||||
|
messaging_enabled=_collection_case_query_messaging_enabled(collection),
|
||||||
)
|
)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|||||||
Reference in New Issue
Block a user