Compare commits
7
Commits
dc4cf0c3a2
...
bdb7b653b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdb7b653b6 | ||
|
|
1eeb0d89d9 | ||
|
|
f3702db5f6 | ||
|
|
a6a7d75144 | ||
|
|
14bba93c4d | ||
|
|
f29df840c6 | ||
|
|
10d30cdaa6 |
@@ -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>
|
||||
@@ -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 @@
|
||||
• <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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user