Add series images endpoint and template for HTMX integration

This commit is contained in:
Ross
2025-12-01 14:50:11 +00:00
parent e9a04e823c
commit 590e0b76ed
5 changed files with 378 additions and 309 deletions
@@ -0,0 +1,31 @@
{% load static %}
<div class="series-images-list">
{% for image in images %}
<div class="d-flex align-items-center mb-2">
{% if image.image %}
<a href="{% url 'atlas:series_image_dicom' pk=image.pk %}" target="_blank" class="me-2">
<img src="{{ image.image.url }}" alt="image-{{ image.pk }}" style="width:96px;height:auto;object-fit:cover;border-radius:.25rem;" class="img-thumbnail">
</a>
{% else %}
<div class="me-2 bg-secondary" style="width:96px;height:72px;border-radius:.25rem;"></div>
{% endif %}
<div class="flex-grow-1">
<div><strong>{{ image.image.name|default:"(no file)" }}</strong></div>
{% if image.image %}
<div class="small text-muted">{{ image.image.size|filesizeformat }}</div>
{% endif %}
{% if image.image_blake3_hash %}
<div class="small text-muted">hash: {{ image.image_blake3_hash|truncatechars:12 }}</div>
{% endif %}
</div>
<div class="ms-auto text-end">
<a href="{% url 'atlas:series_image_dicom' pk=image.pk %}" class="btn btn-sm btn-outline-secondary">View DICOM</a>
</div>
</div>
{% empty %}
<p class="text-muted">No images found for this series.</p>
{% endfor %}
</div>
+35 -11
View File
@@ -1,7 +1,7 @@
{% load crispy_forms_tags %}
{% with image_url_array_and_count=series.get_image_url_array_and_count %}
<div class="container my-4">
<div class="container my-4">
<div class="row">
<div class="col-12 mb-3">
<div class="card shadow-sm">
@@ -49,7 +49,6 @@
<div class="ms-auto text-muted small">{{ series.get_image_count }} images</div>
</div>
</div>
<div class="mt-3">
<!-- Finding form moved below the viewport -->
<div class="card mt-3">
@@ -114,6 +113,31 @@
</div>
</details>
</div>
<!-- Images (loaded on demand via HTMX) -->
<div class="card mt-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Images</span>
<div>
<button id="load-images-button" class="btn btn-sm btn-outline-primary"
hx-get="{% url 'atlas:series_images' series.pk %}"
hx-target="#series-images-container"
hx-swap="innerHTML"
hx-trigger="click"
hx-on="htmx:beforeRequest: document.getElementById('images-loading-indicator').style.display='inline-block'; htmx:afterSwap: document.getElementById('images-loading-indicator').style.display='none'; document.getElementById('load-images-button').disabled = true">
Load images
</button>
<a href="{% url 'atlas:series_download' series.pk %}" class="btn btn-sm btn-outline-success ms-2" target="_blank" rel="noopener">Download</a>
<span id="images-loading-indicator" class="spinner-border spinner-border-sm ms-2" role="status" aria-hidden="true" style="display:none"></span>
</div>
</div>
<div class="card-body">
<div id="series-images-container">
<p class="text-muted small">Click 'Load images' to fetch.</p>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
@@ -190,17 +214,17 @@
{% endif %}
</div>
</div>
</div>
</div>
<div id="clone-findings-modal" class="modal modal-blur fade" tabindex="-1">
<div id="clone-findings-modal" class="modal modal-blur fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered related-finding-modal" style="max-width: 700px;">
<div class="modal-content"></div>
</div>
</div>
</div>
<script>
<script>
$(document).ready(function () {
const apiKey = "root"
@@ -331,7 +355,7 @@
}
});
});
</script>
</script>
{% endwith %}
@@ -402,14 +426,14 @@
transition: background 0.2s, font-size 0.2s;
}
details.styled-detail:not([open]) {
details.styled-detail:not([open]) {
padding-bottom: 0;
min-height: 0;
}
}
details.styled-detail:not([open]) > *:not(summary) {
details.styled-detail:not([open]) > *:not(summary) {
display: none;
}
}
details.styled-detail:not([open]) > summary {
padding: 0.7em 1.2em 0.3em 1.2em;
+1
View File
@@ -457,6 +457,7 @@ urlpatterns = [
# TODO: case context series viewing (so that we can view series in the context of a case)
path("series/<int:pk>", views.series_detail, name="series_detail"),
path("series/<int:pk>/viewer", views.series_viewer, name="series_viewer"),
path("series/<int:pk>/images/", views.series_images_partial, name="series_images"),
path("series/<int:pk>/authors", views.SeriesAuthorUpdate.as_view(), name="series_authors"),
path("series/<int:series_id>/finding/related", views.series_finding_related, name="series_finding_related"),
path("series/", views.SeriesView.as_view(), name="series_view"),
+11
View File
@@ -796,6 +796,17 @@ def series_detail(request, pk, finding_pk=None):
)
@login_required
@user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access
def series_images_partial(request, pk):
"""Return a small fragment that lists images for a series. Intended for HTMX."""
series = get_object_or_404(Series, pk=pk)
logger.debug(series)
images = series.images.filter(removed=False).order_by("pk")
logger.debug(images)
return render(request, "atlas/partials/series_images.html", {"series": series, "images": images})
@login_required
# @user_is_atlas_editor
def question_schema_detail(request, pk: int):
+2
View File
@@ -118,3 +118,5 @@ CSRF_TRUSTED_ORIGINS = [
"http://127.0.0.1:8000",
"http://localhost:8000",
]
REMOTE_URL = "http://127.0.0.1:8000"