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:
@@ -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 %}
|
||||
(<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>
|
||||
Reference in New Issue
Block a user