Compare commits

...
7 Commits
10 changed files with 227 additions and 7 deletions
+7
View File
@@ -1627,6 +1627,13 @@ class CaseDetail(models.Model):
return self.case.history or "No history provided"
return ""
def get_history(self):
if self.redact_history:
return "[Redacted]"
if self.override_history and self.override_history != "":
return self.override_history
return self.case.history or "No history provided"
def get_case_series_nested(self, include_priors=True):
case_series_images = self.case.get_series_images_nested(as_json=False)
@@ -373,6 +373,7 @@
{% endif %}
<span>
<a href="{% url 'atlas:case_displaysets_detail' ds.pk %}">View Display Set</a>
<button type="button" class="btn btn-secondary btn-sm view-displayset-modal ms-2" data-ds-id="{{ ds.pk }}" data-url="{% url 'atlas:case_displaysets_modal' ds.pk %}">View in Modal</button>
</span>
<div>
<strong>Findings:</strong>
@@ -493,6 +494,22 @@
{% include 'question_notes.html' %}
</div>
<div class="modal fade" id="displaysetModal" tabindex="-1" aria-labelledby="displaysetModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="displaysetModalLabel">Display Set Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="displaysetModalBody">
<p>Loading...</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<p><b>Checked by:</b> {% for verified in case.verified.all %} <a
href="{% url 'atlas:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
@@ -773,6 +790,30 @@
});
});
// Display Set modal handler
const dsModal = new bootstrap.Modal(document.getElementById("displaysetModal"));
const dsModalBody = document.getElementById("displaysetModalBody");
document.querySelectorAll(".view-displayset-modal").forEach(button => {
button.addEventListener("click", function () {
const url = this.getAttribute("data-url");
dsModalBody.innerHTML = "<p>Loading...</p>";
fetch(url)
.then(response => response.text())
.then(html => {
dsModalBody.innerHTML = html;
setTimeout(function () {
try { window.mountDicomViewers(); } catch (e) { console.warn('mountDicomViewers not available', e); }
}, 200);
})
.catch(error => {
console.error("Error loading display set details:", error);
dsModalBody.innerHTML = "<p>Failed to load display set.</p>";
});
dsModal.show();
});
});
});
@@ -0,0 +1,71 @@
<div class="case-displayset-modal">
<div class="mb-3">
<h5>{{ ds.name }}</h5>
{% if ds.description %}
<div class="text-muted">{{ ds.description }}</div>
{% endif %}
</div>
{% if ds.viewerstate and ds.annotations %}
<div class="mb-3">
<button
type="button"
class="btn btn-primary"
onclick='importAnnotations_main_viewer({{ ds.annotations|safe }}); importViewerState_main_viewer({{ ds.viewerstate|safe }});'
>Load into viewer</button>
</div>
{% endif %}
<div class="mt-2">
<strong>Findings:</strong>
{% if ds.findings.all %}
<ul>
{% for finding in ds.findings.all %}
<li>{{ finding }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
<div>
<strong>Structures:</strong>
{% if ds.structures.all %}
<ul>
{% for structure in ds.structures.all %}
<li>{{ structure }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
<div>
<strong>Conditions:</strong>
{% if ds.conditions.all %}
<ul>
{% for condition in ds.conditions.all %}
<li>{{ condition }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
<div class="mt-3">
<details>
<summary class="small text-muted" style="cursor:pointer;">Show advanced (viewer state & annotations)</summary>
<div>
<strong>Viewer State:</strong>
<pre class="small">{{ ds.viewerstate|default:"{}" }}</pre>
</div>
<div>
<strong>Annotations:</strong>
<pre class="small">{{ ds.annotations|default:"{}" }}</pre>
</div>
</details>
</div>
</div>
+11 -6
View File
@@ -21,23 +21,22 @@
{% include 'exam_notes.html' %}
<p class="mb-1">
<span class="badge bg-info text-dark me-2">{{ collection.exam_mode }}</span>
<small class="text-muted">(open access: {{ collection.exam_open_access }})</small>
</p>
<p class="mb-2">
{% include "generic/partials/exams/exam_status.html#publish-results" %}
{% include "generic/partials/exams/exam_status.html#exam-active" %}
{% include "generic/partials/exams/exam_status.html#publish-results" %} {% include "generic/partials/exams/exam_status.html#exam-active" %}
</p>
<p class="mb-1">
<span class="badge bg-secondary me-2">{{ collection.get_collection_type_display }}</span>
<span class="badge bg-light text-dark me-2">Self review: {{ collection.self_review }}</span>
Collection type: <span class="badge bg-secondary me-2">{{ collection.get_collection_type_display }}</span>
Self review: <span class="badge bg-light text-dark me-2">{{ collection.self_review }}</span>
Open access:
{% if collection.open_access %}
<span class="badge bg-success">Open</span>
{% else %}
<span class="badge bg-warning text-dark">Restricted</span>
{% endif %}
Exam mode: <span class="badge bg-info text-dark me-2">{{ collection.exam_mode }}</span>
</p>
{% if collection.prerequisites.exists %}
@@ -151,6 +150,12 @@
{% endif %}
{% include 'exam_overview_js.html' %}
<!-- Modal for case fields (history / discussion / report) -->
<div id="case-field-modal" class="modal fade" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content"></div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,14 @@
<div class="modal-header">
<h5 class="modal-title">{{ field|capfirst }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% if content %}
<div class="pre-whitespace">{{ content|linebreaks }}</div>
{% else %}
<div class="text-muted">No {{ field }} available for this case.</div>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
@@ -22,10 +22,80 @@
&nbsp;&nbsp;<span class="text-success">Questions defined</span>
{% endif %}
</div>
<div class="btn-group btn-group-sm ms-2" role="group" aria-label="case-field-links">
<!-- History button: opens modal populated from hidden content on page -->
{% if casedetail.case.history %}
<a class="btn btn-outline-secondary" href="#"
onclick="document.querySelector('#case-field-modal .modal-content').innerHTML = document.getElementById('case-field-{{ casedetail.case.pk }}-history').innerHTML;"
data-bs-toggle="modal" data-bs-target="#case-field-modal" title="View history">
<i class="bi bi-clock-history text-primary"></i>
</a>
{% else %}
<a class="btn btn-outline-secondary disabled" aria-disabled="true" href="javascript:void(0);" title="No history">
<i class="bi bi-clock-history text-muted"></i>
</a>
{% endif %}
<!-- Discussion button -->
{% if casedetail.case.discussion %}
<a class="btn btn-outline-secondary" href="#"
onclick="document.querySelector('#case-field-modal .modal-content').innerHTML = document.getElementById('case-field-{{ casedetail.case.pk }}-discussion').innerHTML;"
data-bs-toggle="modal" data-bs-target="#case-field-modal" title="View discussion">
<i class="bi bi-chat-left-text text-primary"></i>
</a>
{% else %}
<a class="btn btn-outline-secondary disabled" aria-disabled="true" href="javascript:void(0);" title="No discussion">
<i class="bi bi-chat-left-text text-muted"></i>
</a>
{% endif %}
<!-- Report button -->
{% if casedetail.case.report %}
<a class="btn btn-outline-secondary" href="#"
onclick="document.querySelector('#case-field-modal .modal-content').innerHTML = document.getElementById('case-field-{{ casedetail.case.pk }}-report').innerHTML;"
data-bs-toggle="modal" data-bs-target="#case-field-modal" title="View report">
<i class="bi bi-file-earmark-text text-primary"></i>
</a>
{% else %}
<a class="btn btn-outline-secondary disabled" aria-disabled="true" href="javascript:void(0);" title="No report">
<i class="bi bi-file-earmark-text text-muted"></i>
</a>
{% endif %}
</div>
</div>
<div class="d-flex align-items-center ms-3">
{% include 'atlas/partials/casedetails_management_links.html' %}
</div>
{# Pre-render hidden modal content for this case to avoid HTMX calls #}
<div class="d-none">
<div id="case-field-{{ casedetail.case.pk }}-history">
<div class="modal-header">
<h5 class="modal-title">History</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<br/>
{% if casedetail.get_history %}
<div class="pre-whitespace">{{ casedetail.get_history|linebreaks }}</div>
{% else %}
<div class="text-muted">No history available for this case.</div>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
<div id="case-field-{{ casedetail.case.pk }}-discussion">
{% include 'atlas/partials/case_field_modal.html' with field='discussion' content=casedetail.case.discussion %}
</div>
<div id="case-field-{{ casedetail.case.pk }}-report">
{% include 'atlas/partials/case_field_modal.html' with field='report' content=casedetail.case.report %}
</div>
</div>
</li>
@@ -24,6 +24,8 @@
{% endif %}
</div>
<div class="btn-group btn-group-sm" role="group" aria-label="status-indicator">
{% if casedetail.default_viewerstate %}
<button type="button" class="btn btn-sm btn-outline-success" aria-disabled="true" title="This case has a default viewerstate defined">
+3
View File
@@ -227,6 +227,8 @@ urlpatterns = [
views.collection_case_dicom_json_review,
name="collection_case_dicom_json_review",
),
# Partial endpoint to return a single case field (history/discussion/report) for modal display
path(
"collection/<int:exam_id>/case/id/<int:case_id>/dicom_json/review",
views.redirect_collection_case_dicom_json_review_by_id,
@@ -378,6 +380,7 @@ urlpatterns = [
path("case/<int:pk>/display_sets/add", views.case_displaysets_add, name="case_displaysets_add"),
path("<int:pk>/display_sets/edit", views.case_displaysets_edit, name="case_displaysets_edit"),
path("<int:pk>/display_sets/detail", views.case_displaysets_detail, name="case_displaysets_detail"),
path("<int:pk>/display_sets/modal", views.case_displaysets_modal, name="case_displaysets_modal"),
path("<int:pk>/display_sets/delete", views.case_displaysets_delete, name="case_displaysets_delete"),
path(
"case/<int:pk>/dicom_json",
+7
View File
@@ -540,6 +540,13 @@ def case_displaysets_detail(request, pk):
# Redirect to the case_displaysets page with ?displayset=<pk>
return redirect(f"{reverse('atlas:case_displaysets', args=[case.pk])}?displayset={pk}")
@login_required
def case_displaysets_modal(request, pk):
"""Return HTML fragment for a CaseDisplaySet to be shown inside a modal."""
displayset = get_object_or_404(CaseDisplaySet, pk=pk)
return render(request, 'atlas/case_displayset_modal.html', {'ds': displayset})
def case_displaysets_delete(request, pk):
try:
displayset = CaseDisplaySet.objects.get(pk=pk)
+1 -1
View File
@@ -8,7 +8,7 @@ services:
# Development: run Django's autoreloading development server instead of
# the production entrypoint/gunicorn. Mount the repository into the
# container so code edits on the host trigger Django's autoreload.
entrypoint: ["python", "manage.py", "runserver", "0.0.0.0:8000", "--nothreading"]
entrypoint: ["sh", "-c", "python manage.py collectstatic --noinput && python manage.py runserver 0.0.0.0:8000 --nothreading"]
# Clear any command set by the prod compose file (which would be passed
# as an extra argument to manage.py). Setting an empty command prevents
# the image `command` from being appended to the entrypoint.