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 self.case.history or "No history provided"
|
||||||
return ""
|
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):
|
def get_case_series_nested(self, include_priors=True):
|
||||||
case_series_images = self.case.get_series_images_nested(as_json=False)
|
case_series_images = self.case.get_series_images_nested(as_json=False)
|
||||||
|
|
||||||
|
|||||||
@@ -373,6 +373,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<span>
|
<span>
|
||||||
<a href="{% url 'atlas:case_displaysets_detail' ds.pk %}">View Display Set</a>
|
<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>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<strong>Findings:</strong>
|
<strong>Findings:</strong>
|
||||||
@@ -493,6 +494,22 @@
|
|||||||
{% include 'question_notes.html' %}
|
{% include 'question_notes.html' %}
|
||||||
|
|
||||||
</div>
|
</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
|
<p><b>Checked by:</b> {% for verified in case.verified.all %} <a
|
||||||
href="{% url 'atlas:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
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' %}
|
{% include 'exam_notes.html' %}
|
||||||
|
|
||||||
<p class="mb-1">
|
<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>
|
||||||
|
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
{% include "generic/partials/exams/exam_status.html#publish-results" %}
|
{% 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#exam-active" %}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="mb-1">
|
<p class="mb-1">
|
||||||
<span class="badge bg-secondary me-2">{{ collection.get_collection_type_display }}</span>
|
Collection type: <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>
|
Self review: <span class="badge bg-light text-dark me-2">{{ collection.self_review }}</span>
|
||||||
|
Open access:
|
||||||
{% if collection.open_access %}
|
{% if collection.open_access %}
|
||||||
<span class="badge bg-success">Open</span>
|
<span class="badge bg-success">Open</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-warning text-dark">Restricted</span>
|
<span class="badge bg-warning text-dark">Restricted</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
Exam mode: <span class="badge bg-info text-dark me-2">{{ collection.exam_mode }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if collection.prerequisites.exists %}
|
{% if collection.prerequisites.exists %}
|
||||||
@@ -151,6 +150,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include 'exam_overview_js.html' %}
|
{% 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>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% 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>
|
• <span class="text-success">Questions defined</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="d-flex align-items-center ms-3">
|
<div class="d-flex align-items-center ms-3">
|
||||||
{% include 'atlas/partials/casedetails_management_links.html' %}
|
{% include 'atlas/partials/casedetails_management_links.html' %}
|
||||||
</div>
|
</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>
|
</li>
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="btn-group btn-group-sm" role="group" aria-label="status-indicator">
|
<div class="btn-group btn-group-sm" role="group" aria-label="status-indicator">
|
||||||
{% if casedetail.default_viewerstate %}
|
{% 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">
|
<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,
|
views.collection_case_dicom_json_review,
|
||||||
name="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(
|
path(
|
||||||
"collection/<int:exam_id>/case/id/<int:case_id>/dicom_json/review",
|
"collection/<int:exam_id>/case/id/<int:case_id>/dicom_json/review",
|
||||||
views.redirect_collection_case_dicom_json_review_by_id,
|
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("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/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/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("<int:pk>/display_sets/delete", views.case_displaysets_delete, name="case_displaysets_delete"),
|
||||||
path(
|
path(
|
||||||
"case/<int:pk>/dicom_json",
|
"case/<int:pk>/dicom_json",
|
||||||
|
|||||||
@@ -540,6 +540,13 @@ def case_displaysets_detail(request, pk):
|
|||||||
# Redirect to the case_displaysets page with ?displayset=<pk>
|
# Redirect to the case_displaysets page with ?displayset=<pk>
|
||||||
return redirect(f"{reverse('atlas:case_displaysets', args=[case.pk])}?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):
|
def case_displaysets_delete(request, pk):
|
||||||
try:
|
try:
|
||||||
displayset = CaseDisplaySet.objects.get(pk=pk)
|
displayset = CaseDisplaySet.objects.get(pk=pk)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ services:
|
|||||||
# Development: run Django's autoreloading development server instead of
|
# Development: run Django's autoreloading development server instead of
|
||||||
# the production entrypoint/gunicorn. Mount the repository into the
|
# the production entrypoint/gunicorn. Mount the repository into the
|
||||||
# container so code edits on the host trigger Django's autoreload.
|
# 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
|
# 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
|
# as an extra argument to manage.py). Setting an empty command prevents
|
||||||
# the image `command` from being appended to the entrypoint.
|
# the image `command` from being appended to the entrypoint.
|
||||||
|
|||||||
Reference in New Issue
Block a user