From e391f4f9434e4431db1f45e341fdc9d92bb1f808 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 16 Mar 2026 11:03:37 +0000 Subject: [PATCH] Add download functionality for case DICOMs and metadata as ZIP --- atlas/templates/atlas/case_display_block.html | 5 ++ atlas/urls.py | 5 ++ atlas/views.py | 69 +++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/atlas/templates/atlas/case_display_block.html b/atlas/templates/atlas/case_display_block.html index 7939e0d4..689ec95a 100755 --- a/atlas/templates/atlas/case_display_block.html +++ b/atlas/templates/atlas/case_display_block.html @@ -223,6 +223,11 @@
  • Open in new tab
  • + {% if can_edit %} +
  • + Download DICOMs + metadata (ZIP) +
  • + {% endif %}
    diff --git a/atlas/urls.py b/atlas/urls.py index fbcce3d5..a16564fb 100755 --- a/atlas/urls.py +++ b/atlas/urls.py @@ -525,6 +525,11 @@ urlpatterns = [ views.SeriesImagesZipView.as_view(), name="series_download", ), + path( + "case//download", + views.case_download, + name="case_download", + ), path("api_tokens/", views.api_tokens_page, name="api_tokens"), path( "series//order_dicom_instance", diff --git a/atlas/views.py b/atlas/views.py index 05fce740..79b58926 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -48,6 +48,10 @@ from django.urls.exceptions import NoReverseMatch from django.http import Http404, JsonResponse from django.http import HttpResponseRedirect, HttpResponse from django.contrib.contenttypes.models import ContentType +import zipfile +import os +from django.core.files.base import ContentFile +from django.core.exceptions import PermissionDenied from generic.models import CidUser, CidUserExam, CimarCase, Site from atlas.helpers import get_cases_available_to_user @@ -3373,6 +3377,71 @@ class SeriesImagesZipView(SeriesImagesZipViewBase, SuperuserRequiredMixin): series_object = Series +@login_required +def case_download(request, pk): + """Allow a case author (or superuser) to download all linked series DICOMs + and a metadata export as a zip file. + """ + case = get_object_or_404(Case, pk=pk) + + # Only allow superusers or case authors + if not (request.user.is_superuser or case.author.filter(id=request.user.id).exists()): + raise PermissionDenied() + + temp_file = ContentFile(b"", name=f"case_{case.pk}.zip") + with zipfile.ZipFile(temp_file, mode="w", compression=zipfile.ZIP_DEFLATED) as zip_file: + metadata = { + "case": { + "id": case.pk, + "title": case.title, + "authors": [str(a) for a in case.author.all()], + }, + "series": [], + } + + # Iterate ordered series if available, otherwise fall back to related series + try: + ordered_series = case.get_ordered_series() + except Exception: + ordered_series = list(case.series.all()) + + for series in ordered_series: + series_entry = {"pk": series.pk, "description": str(series)} + # Prefer series-provided dicom JSON if available + try: + series_entry["dicom_json"] = series.get_series_dicom_json() + except Exception: + series_entry["dicom_json"] = None + + images = series.images.filter(removed=False).order_by("position") + series_entry["images"] = [] + for img in images: + try: + file_obj = img.image.file + fname = os.path.split(file_obj.name)[-1] + arcname = f"series_{series.pk}/{fname}" + zip_file.writestr(arcname, file_obj.read()) + series_entry["images"].append({"filename": fname, "position": img.position}) + except Exception: + # skip missing files + continue + + metadata["series"].append(series_entry) + + # Write metadata.json + zip_file.writestr("metadata.json", json.dumps(metadata, default=str, indent=2)) + + file_size = temp_file.tell() + temp_file.seek(0) + + response = HttpResponse(temp_file, content_type="application/zip") + response["Content-Disposition"] = ( + "attachment; filename=%s" % f"case_{case.pk}.zip" + ) + response["Content-Length"] = file_size + return response + + # At the moment only the series owner / author can delete findings but anyone can create def create_series_findings(request): # posts = Post.objects.all()