diff --git a/atlas/templates/atlas/base.html b/atlas/templates/atlas/base.html
index 385a2fd5..edaf8de2 100755
--- a/atlas/templates/atlas/base.html
+++ b/atlas/templates/atlas/base.html
@@ -49,6 +49,12 @@
Normals
+ {% if request.user.is_staff %}
+
+
+ Large cases
+
+ {% endif %}
diff --git a/atlas/urls.py b/atlas/urls.py
index d21b80ba..2ff9b81e 100755
--- a/atlas/urls.py
+++ b/atlas/urls.py
@@ -634,4 +634,5 @@ urlpatterns = [
views.linked_cases_overview,
name="linked_cases_overview",
),
+ path("cases/by_size", views.cases_by_size, name="cases_by_size"),
]
diff --git a/atlas/views.py b/atlas/views.py
index 15580884..f3dd8d13 100755
--- a/atlas/views.py
+++ b/atlas/views.py
@@ -30,6 +30,8 @@ from django.views.generic.detail import DetailView
import pydicom
from pydicom.uid import generate_uid
from generic.mixins import SuperuserRequiredMixin, UserConfigurablePaginationMixin
+from django.contrib.admin.views.decorators import staff_member_required
+from django.core.paginator import Paginator
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
from django.views.generic import ListView
@@ -2304,6 +2306,66 @@ class CaseView(LoginRequiredMixin, UserConfigurablePaginationMixin, SingleTableM
filterset_class = CaseFilter
+@staff_member_required
+def cases_by_size(request):
+ """Admin view: list cases ordered by total images size to find large cases.
+
+ Shows case id, title, number of series, number of images and total size (bytes).
+ Optional GET parameter `min_size` (bytes) to filter cases larger than that.
+ """
+ min_size = request.GET.get("min_size")
+ try:
+ min_size = int(min_size) if min_size else 0
+ except Exception:
+ min_size = 0
+
+ cases_qs = Case.objects.prefetch_related("series__images")
+
+ def human(n):
+ # simple human readable bytes
+ for unit in ("B", "KB", "MB", "GB", "TB"):
+ if n < 1024.0:
+ return f"{n:.1f}{unit}"
+ n /= 1024.0
+ return f"{n:.1f}PB"
+
+ rows = []
+ for c in cases_qs:
+ try:
+ total = c.get_total_series_images_size()
+ except Exception:
+ total = 0
+ series_count = c.series.count()
+ image_count = sum(s.images.filter(removed=False).count() for s in c.series.all())
+ if total >= min_size:
+ rows.append({
+ "case": c,
+ "total": total,
+ "series_count": series_count,
+ "image_count": image_count,
+ "human_total": human(total),
+ })
+
+ rows.sort(key=lambda r: r["total"], reverse=True)
+
+ rows.sort(key=lambda r: r["total"], reverse=True)
+
+ paginator = Paginator(rows, 50)
+ page_num = request.GET.get("page")
+ page = paginator.get_page(page_num)
+
+ def human(n):
+ # simple human readable bytes
+ for unit in ("B", "KB", "MB", "GB", "TB"):
+ if n < 1024.0:
+ return f"{n:.1f}{unit}"
+ n /= 1024.0
+ return f"{n:.1f}PB"
+
+ context = {"page": page, "human": human, "min_size": min_size}
+ return render(request, "atlas/cases_by_size.html", context)
+
+
class SeriesView(LoginRequiredMixin, UserConfigurablePaginationMixin, SingleTableMixin, FilterView):
model = Series
table_class = SeriesTable
diff --git a/templates/atlas/cases_by_size.html b/templates/atlas/cases_by_size.html
new file mode 100644
index 00000000..86dd2d75
--- /dev/null
+++ b/templates/atlas/cases_by_size.html
@@ -0,0 +1,43 @@
+{% extends 'base.html' %}
+{% block content %}
+Cases by size
+
+
+
+
+ | Case |
+ Title |
+ Series |
+ Images |
+ Total Size |
+
+
+
+ {% for row in page.object_list %}
+
+ | {{ row.case.pk }} |
+ {{ row.case.title }} |
+ {{ row.series_count }} |
+ {{ row.image_count }} |
+ {{ row.human_total }} |
+
+ {% empty %}
+ | No cases |
+ {% endfor %}
+
+
+
+{% endblock %}