feat: Add case review messaging system with feedback and query capabilities

- Introduced CaseReviewMessage model to handle feedback and queries for cases.
- Implemented views for displaying user and CID messages, including outstanding feedback.
- Created templates for collection feedback overview and user messages.
- Added functionality for acknowledging messages and sending feedback or queries.
- Enhanced collection detail view to show outstanding messages and user message statistics.
- Added tests for handling duplicate DICOM uploads without series.
This commit is contained in:
Ross
2026-04-27 12:55:27 +01:00
parent b02120d5e7
commit dc479cafd0
15 changed files with 1354 additions and 113 deletions
@@ -289,6 +289,14 @@
</div>
</form>
<div class="mt-3"
hx-get="{% url 'atlas:collection_case_review_thread' exam_id=collection.id cid_user_exam_id=cid_user_exam.id case_id=case.id %}{% if cid %}?cid={{ cid }}&passcode={{ passcode }}{% endif %}"
hx-trigger="load"
hx-target="this"
hx-swap="innerHTML">
<div class="small text-muted">Loading case conversation...</div>
</div>
<aside id="selfReviewSidebar" class="self-review-sidebar" aria-live="polite" aria-label="Self review sidebar">
<div class="self-review-sidebar-header">
<h5 class="mb-0" id="selfReviewSidebarLabel">Self Review</h5>
@@ -85,6 +85,50 @@
>Sync prerequisite users</button>
</div>
{% endif %}
<div class="card border-secondary w-100 mt-3">
<div class="card-body py-2 px-3 text-start text-md-end">
<div class="small text-muted">Outstanding messages</div>
<div class="fw-semibold {% if total_outstanding_messages %}text-danger{% endif %}">
{{ total_outstanding_messages }} pending acknowledgement
</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>
{% if user_message_rows %}
<div class="mt-2 small text-muted">Users</div>
<div class="d-flex flex-column gap-1">
{% for row in user_message_rows|slice:":5" %}
<a class="small {% if row.outstanding_message_count %}text-danger{% endif %}" href="{% url 'atlas:collection_user_messages' collection.pk row.user_user.pk %}">
{{ row.get_user_name }}
{% if row.outstanding_message_count %}
({{ row.outstanding_message_count }} outstanding)
{% else %}
({{ row.total_review_message_count }} history)
{% endif %}
</a>
{% endfor %}
</div>
{% endif %}
{% if cid_message_rows %}
<div class="mt-2 small text-muted">CID users</div>
<div class="d-flex flex-column gap-1">
{% for row in cid_message_rows|slice:":5" %}
<a class="small {% if row.outstanding_message_count %}text-danger{% endif %}" href="{% url 'atlas:collection_cid_messages' collection.pk row.cid_user.cid %}">
{{ row.cid_user }}
{% if row.outstanding_message_count %}
({{ row.outstanding_message_count }} outstanding)
{% else %}
({{ row.total_review_message_count }} history)
{% endif %}
</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,95 @@
{% extends 'base.html' %}
{% block content %}
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
<div>
<h2 class="h4 mb-1">{{ collection.name }}</h2>
<div class="text-muted small">
{% if cid is not None %}
CID: <strong>{{ cid }}</strong>
{% else %}
Feedback hub
{% endif %}
</div>
</div>
<div class="card border-secondary" style="min-width: 320px;">
<div class="card-body py-2 px-3">
<div class="small text-muted">Feedback Summary</div>
<div class="fw-semibold">{{ total_outstanding_feedback }} outstanding feedback item{{ total_outstanding_feedback|pluralize }}</div>
<div class="small text-muted mt-1">Across {{ outstanding_case_count }} case{{ outstanding_case_count|pluralize }}</div>
<div class="mt-2">
<a class="btn btn-sm btn-outline-secondary"
href="{% if cid is not None %}{% url 'atlas:collection_take_overview' pk=collection.id cid=cid passcode=passcode %}{% else %}{% url 'atlas:collection_take_overview_user' pk=collection.id %}{% endif %}">
Back to Overview
</a>
</div>
</div>
</div>
</div>
{% if outstanding_rows %}
<div class="mb-4">
<h3 class="h5 mb-3">Outstanding Feedback</h3>
<div class="d-flex flex-column gap-3">
{% for row in outstanding_rows %}
<section id="case-feedback-{{ row.casedetail.case.id }}" class="card border-danger-subtle">
<div class="card-header d-flex justify-content-between align-items-center gap-2">
<div>
<strong>Case {{ row.casedetail.sort_order|add:1 }}</strong>
{% if collection.show_title_post %}
<span class="ms-1">{{ row.casedetail.case.title|default:row.casedetail.case.pk|truncatechars:80 }}</span>
{% endif %}
</div>
<span class="badge bg-danger">{{ row.stats.outstanding_feedback_count }} awaiting acknowledgement</span>
</div>
<div class="card-body pt-0">
<div hx-get="{{ row.thread_url }}{% if row.cid %}?cid={{ row.cid }}&passcode={{ passcode }}{% endif %}"
hx-trigger="load"
hx-target="this"
hx-swap="innerHTML">
<div class="small text-muted p-3">Loading conversation...</div>
</div>
</div>
</section>
{% endfor %}
</div>
</div>
{% else %}
<div class="alert alert-success">No outstanding feedback needs acknowledgement.</div>
{% endif %}
{% if history_rows %}
<div>
<h3 class="h5 mb-3">Previous Conversations</h3>
<div class="accordion" id="feedbackHistoryAccordion">
{% for row in history_rows %}
<div class="accordion-item" id="case-feedback-{{ row.casedetail.case.id }}">
<h2 class="accordion-header" id="heading-{{ row.casedetail.case.id }}">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-{{ row.casedetail.case.id }}" aria-expanded="false" aria-controls="collapse-{{ row.casedetail.case.id }}">
Case {{ row.casedetail.sort_order|add:1 }}
{% if collection.show_title_post %}
: {{ row.casedetail.case.title|default:row.casedetail.case.pk|truncatechars:80 }}
{% endif %}
<span class="badge bg-secondary ms-2">{{ row.stats.message_count }} message{{ row.stats.message_count|pluralize }}</span>
</button>
</h2>
<div id="collapse-{{ row.casedetail.case.id }}" class="accordion-collapse collapse" aria-labelledby="heading-{{ row.casedetail.case.id }}" data-bs-parent="#feedbackHistoryAccordion">
<div class="accordion-body pt-0">
<div hx-get="{{ row.thread_url }}{% if row.cid %}?cid={{ row.cid }}&passcode={{ passcode }}{% endif %}"
hx-trigger="revealed once"
hx-target="this"
hx-swap="innerHTML">
<div class="small text-muted p-3">Open to load conversation history...</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% elif not outstanding_rows %}
<div class="card border-secondary">
<div class="card-body text-muted small">No case conversations have been started yet.</div>
</div>
{% endif %}
{% endblock %}
@@ -13,6 +13,11 @@
<b><a href="{% url 'atlas:collection_history_user' collection.pk userexam.user_user.pk %}">{{userexam.get_user_name}}</a><b><br/>
Completed: {{userexam.completed}}<br/>
Started: {{userexam.start_time}}, Ended: {{userexam.end_time}}<br/>
{% if userexam.outstanding_feedback_count %}
<span class="badge bg-danger">{{ userexam.outstanding_feedback_count }} outstanding feedback</span><br/>
{% elif userexam.total_review_message_count %}
<span class="badge bg-secondary">{{ userexam.total_review_message_count }} conversation message{{ userexam.total_review_message_count|pluralize }}</span><br/>
{% endif %}
<button
hx-post="{% url 'atlas:collection_reset_answers_user' collection.pk userexam.user_user.pk %}"
@@ -37,6 +42,11 @@
<b><a href="{% url 'atlas:collection_history_ciduser' collection.pk cidexam.cid_user.cid %}">{{cidexam.cid_user}}</a><b><br/>
Completed: {{cidexam.completed}}<br/>
Started: {{cidexam.start_time}}, Ended: {{cidexam.end_time}}<br/>
{% if cidexam.outstanding_feedback_count %}
<span class="badge bg-danger">{{ cidexam.outstanding_feedback_count }} outstanding feedback</span><br/>
{% elif cidexam.total_review_message_count %}
<span class="badge bg-secondary">{{ cidexam.total_review_message_count }} conversation message{{ cidexam.total_review_message_count|pluralize }}</span><br/>
{% endif %}
<button
hx-post="{% url 'atlas:collection_reset_answers_ciduser' collection.pk cidexam.cid_user.cid %}"
@@ -1,101 +1,157 @@
{% extends 'atlas/exams.html' %}
{% block content %}
<h2>Collection: {{collection.name}}</h2>
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
<div>
<h2 class="h4 mb-1">{{ collection.name }}</h2>
<div class="text-muted small">Learner: <strong>{{ user }}</strong></div>
</div>
<div class="card border-secondary" style="min-width: 320px;">
<div class="card-body py-2 px-3">
<div class="small text-muted">Attempt Progress</div>
<div class="fw-semibold">{{ answer_count }} / {{ collection_length }} answered</div>
<div class="progress mt-2" style="height: 8px;">
<div class="progress-bar" role="progressbar" style="width: {% widthratio answer_count collection_length 100 %}%"></div>
</div>
<div class="mt-2 small">Started: {{ cid_user_exam.start_time }}</div>
</div>
</div>
</div>
<h3>User: {{user}}</h3>
<ul>
{% for casedetail, user_answer in user_answers %}
<li class="case">
<h4>{{forloop.counter}} / Case: {{casedetail.case.title}}</h4>
Question started: {{user_answer.started_at}} - Answer submitted: {{user_answer.submitted_at}}
{% if request.user.is_superuser and user_answer %}
&nbsp;(<a href="{% url 'admin:atlas_userreportanswer_change' user_answer.pk %}" target="_blank">Edit in admin</a>)
{% endif %}
<br/>
<br/>
{% if not user_answer %}
<span class="case-not-answered">Case not answered.</span>
{% else %}
<div class="answer-block">
User answer:
<div class="user-answer">
{% if user_answer.answer %}
{{user_answer.answer}}
{% else %}
{{user_answer.json_answer}}
{% for value, user_answer, correct_answer, answer_is_correct, automark in user_answer.get_correct_json_answers %}
{% if not user_answer %}
Not answered
{% else %}
<div class="{% if answer_is_correct %}
correct
{% else %}
incorrect
{% endif %}
{% if automark %}
automark
{% endif %}
">
<h5>{{value.title}}</h5>
{{value.description}}
<div
>
Answer : {{user_answer}}
{% if not answer_is_correct %}
<br/>Correct answer: {{correct_answer}}
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}
<div class="list-group mb-4">
{% for row in history_rows %}
<div class="list-group-item {% if not row.user_answer %}bg-danger-subtle border-danger-subtle{% endif %}">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2 mb-2">
<div>
<div class="fw-semibold">
Case {{ forloop.counter }}: {{ row.casedetail.case.title|default:row.casedetail.case.pk|truncatechars:80 }}
{% if row.user_answer and row.user_answer.completed %}
<span class="badge bg-success-subtle text-success-emphasis ms-2">Completed</span>
{% elif row.user_answer %}
<span class="badge bg-warning-subtle text-warning-emphasis ms-2">Saved</span>
{% else %}
<span class="badge bg-danger-subtle text-danger-emphasis ms-2">Not answered</span>
{% endif %}
{% if row.self_reviews %}
<span class="badge bg-info-subtle text-info-emphasis ms-1">{{ row.self_reviews|length }} self review{{ row.self_reviews|length|pluralize }}</span>
{% endif %}
</div>
<div class="small text-muted">
Started: {{ row.user_answer.started_at|default:'-' }}
{% if row.user_answer %}
· Submitted: {{ row.user_answer.submitted_at|default:'-' }}
{% endif %}
</div>
</div>
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-sm btn-primary"
href="{% url 'atlas:collection_case_view' pk=collection.id case_number=forloop.counter0 %}">
Open Case
</a>
{% if row.self_reviews %}
{% for review in row.self_reviews %}
<a class="btn btn-sm btn-outline-info"
href="{% url 'atlas:self_review_detail' cid_user_exam.id row.casedetail.case.id review.id %}">
View Self Review {{ forloop.counter }}
</a>
{% endfor %}
{% endif %}
</div>
</div>
{% if not row.user_answer %}
<div class="small text-muted">This case has not been answered yet.</div>
{% else %}
<details class="mb-3" open>
<summary class="small fw-semibold">Answer / Report</summary>
<div class="small mt-2 p-3 rounded bg-body-tertiary border">
{% if row.user_answer.answer %}
{{ row.user_answer.answer|linebreaksbr }}
{% else %}
<div class="mb-2">{{ row.user_answer.json_answer }}</div>
{% for value, single_answer, correct_answer, answer_is_correct, automark in row.user_answer.get_correct_json_answers %}
<div class="mb-2 p-2 border rounded {% if answer_is_correct %}border-success-subtle{% else %}border-danger-subtle{% endif %}">
<div class="fw-semibold">
{{ value.title }}
{% if answer_is_correct %}
<span class="text-success"></span>
{% elif automark %}
<span class="text-danger"></span>
{% else %}
<span class="text-warning"></span>
{% endif %}
</div>
<div class="small text-muted">{{ value.description }}</div>
<div class="small mt-1">Answer: {{ single_answer|default:'Not answered' }}</div>
{% if not answer_is_correct and correct_answer %}
<div class="small text-muted">Correct answer: {{ correct_answer }}</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
{% if request.user.is_superuser and row.user_answer %}
<div class="mt-2 small">
{% if row.user_answer.user_id %}
<a href="{% url 'admin:atlas_userreportanswer_change' row.user_answer.pk %}" target="_blank">Edit in admin</a>
{% else %}
<a href="{% url 'admin:atlas_cidreportanswer_change' row.user_answer.pk %}" target="_blank">Edit in admin</a>
{% endif %}
</div>
{% endif %}
</div>
</details>
{% endif %}
</li>
{% if row.self_reviews %}
<details class="mb-3">
<summary class="small fw-semibold">Self Reviews</summary>
<div class="d-flex flex-column gap-2 mt-2">
{% for review in row.self_reviews %}
<div class="border rounded p-3 bg-body-tertiary">
<div class="d-flex justify-content-between align-items-start gap-2 mb-2">
<div class="small text-muted">Updated: {{ review.review_update_date|date:"Y-m-d H:i" }}</div>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-outline-info"
href="{% url 'atlas:self_review_detail' cid_user_exam.id row.casedetail.case.id review.id %}">
View
</a>
<a class="btn btn-sm btn-outline-warning"
href="{% url 'atlas:self_review_update' cid_user_exam.id row.casedetail.case.id review.id %}">
Edit
</a>
</div>
</div>
<div class="small mb-1"><strong>Findings:</strong> {{ review.findings|default:'-' }}/5</div>
<div class="small mb-2"><strong>Interpretation:</strong> {{ review.interpretation|default:'-' }}/5</div>
<div class="small text-body-secondary">{{ review.comments|default:'No comments added.'|linebreaksbr }}</div>
</div>
{% endfor %}
</div>
</details>
{% endif %}
<details class="mt-3" {% if row.case_review_stats.has_outstanding_query %}open{% endif %}>
<summary class="small fw-semibold d-flex flex-wrap align-items-center gap-2">
<span>Case conversation</span>
{% if row.case_review_stats.has_outstanding_query %}
<span class="badge bg-danger">{{ row.case_review_stats.outstanding_query_count }} new learner message{{ row.case_review_stats.outstanding_query_count|pluralize }}</span>
{% elif row.case_review_stats.has_messages %}
<span class="badge bg-secondary">{{ row.case_review_stats.message_count }} message{{ row.case_review_stats.message_count|pluralize }}</span>
{% else %}
<span class="text-muted">No messages yet</span>
{% endif %}
</summary>
<div class="mt-2"
hx-get="{{ row.thread_url }}{% if row.cid %}?cid={{ row.cid }}{% endif %}"
hx-trigger="load"
hx-target="this"
hx-swap="innerHTML">
<div class="small text-muted">Loading case conversation...</div>
</div>
</details>
</div>
{% endfor %}
</ul>
</div>
{% endblock %}
{% block css %}
{{ block.super }}
<style>
.correct {
}
.correct h5::after {
content: '✓';
color: green;
}
.incorrect h5::after {
content: '✗';
color: orange;
}
.incorrect.automark h5::after {
content: '✗';
color: red;
}
.answer-block>div {
padding-bottom: 10px;
}
.case:has(.case-not-answered) {
opacity: 0.5;
}
</style>
{% endblock %}
@@ -8,6 +8,9 @@
{% if collection.in_review_mode or cid_user_exam.completed %}
<span class="badge bg-light text-dark ms-2">Review</span>
{% endif %}
{% if total_outstanding_feedback %}
<span class="badge bg-danger ms-2">{{ total_outstanding_feedback }} outstanding feedback</span>
{% endif %}
</h2>
<div class="text-muted small">
{% if cid is not None %}
@@ -32,13 +35,23 @@
{% if cid_user_exam.completed %}
<div class="mt-1 small">End time: {{ cid_user_exam.end_time|default:"-" }}</div>
{% endif %}
<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 %}">
{% if total_outstanding_feedback %}
Review Feedback Hub
{% else %}
Open Feedback Hub
{% endif %}
</a>
</div>
</div>
</div>
</div>
<div class="list-group mb-3">
{% for question, answer, self_review in question_answer_tuples %}
<div class="list-group-item {% if not answer %}bg-danger-subtle border-danger-subtle{% endif %}">
{% for question, answer, self_review, review_stats in question_answer_tuples %}
<div class="list-group-item {% if review_stats.has_outstanding_feedback %}border-warning border-2{% elif not answer %}bg-danger-subtle border-danger-subtle{% endif %}">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2">
<div>
<div class="fw-semibold">
@@ -53,6 +66,11 @@
{% if self_review %}
<span class="badge bg-info-subtle text-info-emphasis ms-1">Self review added</span>
{% endif %}
{% 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>
{% elif review_stats.has_messages %}
<span class="badge bg-secondary ms-1">{{ review_stats.message_count }} conversation message{{ review_stats.message_count|pluralize }}</span>
{% endif %}
</div>
{% if collection.show_title_post %}
<div class="small text-muted">
@@ -80,6 +98,12 @@
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 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 }}">
View Conversation
</a>
{% endif %}
</div>
</div>
@@ -0,0 +1,99 @@
{% extends 'atlas/exams.html' %}
{% block content %}
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
<div>
<h2 class="h4 mb-1">{{ collection.name }}</h2>
<div class="text-muted small">Message hub for <strong>{{ target_label }}</strong></div>
</div>
<div class="card border-secondary" style="min-width: 320px;">
<div class="card-body py-2 px-3">
<div class="small text-muted">Outstanding messages</div>
<div class="fw-semibold {% if total_outstanding_messages %}text-danger{% endif %}">
{{ total_outstanding_messages }} awaiting acknowledgement
</div>
<div class="mt-2 d-flex flex-wrap gap-2">
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:collection_history' collection.pk %}">Back to Collection History</a>
<a class="btn btn-sm btn-outline-secondary" href="{% if target_user %}{% url 'atlas:collection_history_user' collection.pk target_user.pk %}{% else %}{% url 'atlas:collection_history_ciduser' collection.pk target_cid %}{% endif %}">
Open Full Attempt Page
</a>
</div>
<div class="mt-2">
{% if show_all %}
<a class="btn btn-sm btn-outline-primary" href="?">Show outstanding only</a>
{% else %}
<a class="btn btn-sm btn-primary" href="?show=all">Load all conversation history</a>
{% endif %}
</div>
</div>
</div>
</div>
{% if outstanding_rows %}
<div class="mb-4">
<h3 class="h5 mb-3">Outstanding Messages</h3>
<div class="d-flex flex-column gap-3">
{% for row in outstanding_rows %}
<section id="case-feedback-{{ row.casedetail.case.id }}" class="card border-danger-subtle">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<strong>Case {{ row.casedetail.sort_order|add:1 }}</strong>
{% if collection.show_title_post %}
<span class="ms-1">{{ row.casedetail.case.title|default:row.casedetail.case.pk|truncatechars:90 }}</span>
{% endif %}
</div>
<span class="badge bg-danger">{{ row.stats.outstanding_total_count }} outstanding</span>
</div>
<div class="card-body pt-0">
<div hx-get="{{ row.thread_url }}{% if row.cid %}?cid={{ row.cid }}{% endif %}"
hx-trigger="load"
hx-target="this"
hx-swap="innerHTML">
<div class="small text-muted p-3">Loading conversation...</div>
</div>
</div>
</section>
{% endfor %}
</div>
</div>
{% else %}
<div class="alert alert-success">No outstanding messages for this user.</div>
{% endif %}
{% if show_all %}
{% if history_rows %}
<div>
<h3 class="h5 mb-3">Conversation History</h3>
<div class="accordion" id="userMessageHistoryAccordion">
{% for row in history_rows %}
<div class="accordion-item" id="history-case-{{ row.casedetail.case.id }}">
<h2 class="accordion-header" id="history-heading-{{ row.casedetail.case.id }}">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#history-collapse-{{ row.casedetail.case.id }}" aria-expanded="false" aria-controls="history-collapse-{{ row.casedetail.case.id }}">
Case {{ row.casedetail.sort_order|add:1 }}
{% if collection.show_title_post %}
: {{ row.casedetail.case.title|default:row.casedetail.case.pk|truncatechars:90 }}
{% endif %}
<span class="badge bg-secondary ms-2">{{ row.stats.message_count }} messages</span>
</button>
</h2>
<div id="history-collapse-{{ row.casedetail.case.id }}" class="accordion-collapse collapse" aria-labelledby="history-heading-{{ row.casedetail.case.id }}" data-bs-parent="#userMessageHistoryAccordion">
<div class="accordion-body pt-0">
<div hx-get="{{ row.thread_url }}{% if row.cid %}?cid={{ row.cid }}{% endif %}"
hx-trigger="revealed once"
hx-target="this"
hx-swap="innerHTML">
<div class="small text-muted p-3">Open section to load history...</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% elif not outstanding_rows %}
<div class="card border-secondary">
<div class="card-body text-muted small">No conversation history for this user yet.</div>
</div>
{% endif %}
{% endif %}
{% endblock %}
@@ -0,0 +1,114 @@
<div id="{{ thread_dom_id }}" class="case-review-thread-root card border-secondary-subtle mt-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>Case Conversation</strong>
{% if unacknowledged_count %}
<span class="badge bg-danger-subtle text-danger-emphasis ms-2">{{ unacknowledged_count }} awaiting acknowledgement</span>
{% endif %}
</div>
<span class="small text-muted">Case {{ case.pk }}</span>
</div>
<div class="card-body">
{% if messages %}
<div class="d-flex flex-column gap-2 mb-3">
{% for message in messages %}
<div class="border rounded p-2 {% if message.message_type == 'feedback' %}border-warning-subtle bg-warning-subtle{% elif message.message_type == 'query' %}border-info-subtle bg-info-subtle{% else %}border-secondary-subtle bg-body-tertiary{% endif %}">
<div class="d-flex justify-content-between align-items-start gap-2 mb-1">
<div>
<span class="fw-semibold">{{ message.get_author_display }}</span>
<span class="badge text-bg-dark ms-1">{{ message.get_message_type_display }}</span>
{% if message.requires_acknowledgement %}
{% if message.is_acknowledged %}
<span class="badge bg-success-subtle text-success-emphasis ms-1">Acknowledged</span>
{% else %}
<span class="badge bg-danger-subtle text-danger-emphasis ms-1">Awaiting acknowledgement</span>
{% endif %}
{% endif %}
</div>
<div class="small text-muted">{{ message.created_at|date:"Y-m-d H:i" }}</div>
</div>
<div class="small">{{ message.message|linebreaksbr }}</div>
{% if message.requires_acknowledgement and not message.is_acknowledged %}
{% if can_acknowledge_query and message.message_type == 'query' %}
<form class="mt-2"
hx-post="{{ thread_url }}"
hx-target="#{{ thread_dom_id }}"
hx-swap="outerHTML">
{% csrf_token %}
<input type="hidden" name="action" value="acknowledge">
<input type="hidden" name="message_id" value="{{ message.id }}">
{% if cid %}<input type="hidden" name="cid" value="{{ cid }}">{% endif %}
{% if passcode %}<input type="hidden" name="passcode" value="{{ passcode }}">{% endif %}
<button type="submit" class="btn btn-sm btn-outline-success">Acknowledge learner query</button>
</form>
{% elif can_acknowledge_feedback and message.message_type != 'query' %}
<form class="mt-2"
hx-post="{{ thread_url }}"
hx-target="#{{ thread_dom_id }}"
hx-swap="outerHTML">
{% csrf_token %}
<input type="hidden" name="action" value="acknowledge">
<input type="hidden" name="message_id" value="{{ message.id }}">
{% if cid %}<input type="hidden" name="cid" value="{{ cid }}">{% endif %}
{% if passcode %}<input type="hidden" name="passcode" value="{{ passcode }}">{% endif %}
<button type="submit" class="btn btn-sm btn-outline-success">Acknowledge feedback</button>
</form>
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="text-muted small mb-3">No messages yet for this case.</div>
{% endif %}
{% if can_feedback %}
<form hx-post="{{ thread_url }}"
hx-target="#{{ thread_dom_id }}"
hx-swap="outerHTML"
class="border rounded p-3 mb-2 bg-body-tertiary">
{% csrf_token %}
<input type="hidden" name="action" value="add_message">
<div class="row g-2 align-items-end">
<div class="col-md-8">
<label class="form-label small fw-semibold">Supervisor response</label>
{{ supervisor_form.message }}
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold">Type</label>
<select name="message_type" class="form-select form-select-sm">
<option value="feedback">Feedback</option>
<option value="reply">Reply</option>
</select>
</div>
<div class="col-md-2">
<div class="form-check mt-4 pt-2">
{{ supervisor_form.requires_acknowledgement }}
<label for="{{ supervisor_form.requires_acknowledgement.id_for_label }}" class="form-check-label small">Needs acknowledgement</label>
</div>
</div>
</div>
<div class="mt-2">
<button type="submit" class="btn btn-sm btn-primary">Send feedback</button>
</div>
</form>
{% endif %}
{% if can_query %}
<form hx-post="{{ thread_url }}"
hx-target="#{{ thread_dom_id }}"
hx-swap="outerHTML"
class="border rounded p-3 bg-body-tertiary">
{% csrf_token %}
<input type="hidden" name="action" value="add_message">
{% if cid %}<input type="hidden" name="cid" value="{{ cid }}">{% endif %}
{% if passcode %}<input type="hidden" name="passcode" value="{{ passcode }}">{% endif %}
<label class="form-label small fw-semibold">Ask a question / raise a query</label>
{{ query_form.message }}
<div class="mt-2">
<button type="submit" class="btn btn-sm btn-outline-info">Send query</button>
</div>
</form>
{% endif %}
</div>
</div>