Add caching and group detection for series images in series viewer

This commit is contained in:
Ross
2026-05-21 22:24:44 +01:00
parent f85aefe5c7
commit 734460777b
3 changed files with 103 additions and 10 deletions
+66 -5
View File
@@ -10,6 +10,8 @@ from rad.settings import REMOTE_URL
from django.db.models.fields.files import ImageField
from django.db.models.fields.related import ForeignKey
from django.db import models
from django.db.models.signals import pre_save, post_save, post_delete
from django.dispatch import receiver
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.core.cache import cache
@@ -129,11 +131,34 @@ def _normalise_group_rule_value(value):
return text
def _series_declared_groups_cache_key(series_id):
return f"atlas:series_declared_groups:{series_id}"
def invalidate_series_declared_groups_cache(series_id):
if not series_id:
return
cache.delete(_series_declared_groups_cache_key(series_id))
def _build_declared_series_groups(series, series_images_with_urls):
cache_key = f"atlas:series_declared_groups:{series.pk}:{int(series.modified_date.timestamp())}:{len(series_images_with_urls)}"
cache_key = _series_declared_groups_cache_key(series.pk)
signature = {
"series_modified": int(series.modified_date.timestamp()) if getattr(series, "modified_date", None) else 0,
"image_count": len(series_images_with_urls),
"images": [
{
"pk": getattr(image_obj, "pk", None),
"position": getattr(image_obj, "position", None),
"url": image_url,
}
for image_obj, image_url in series_images_with_urls
],
}
cached = cache.get(cache_key)
if cached is not None:
return cached
if isinstance(cached, dict):
if cached.get("signature") == signature:
return cached.get("groups", [])
rules = [
"dwiBValue",
@@ -232,7 +257,7 @@ def _build_declared_series_groups(series, series_images_with_urls):
"groupValues": {},
}
]
cache.set(cache_key, result, timeout=60 * 60)
cache.set(cache_key, {"signature": signature, "groups": result}, timeout=60 * 60)
return result
grouped = {}
@@ -288,7 +313,7 @@ def _build_declared_series_groups(series, series_images_with_urls):
}
)
cache.set(cache_key, ordered_groups, timeout=60 * 60)
cache.set(cache_key, {"signature": signature, "groups": ordered_groups}, timeout=60 * 60)
return ordered_groups
@@ -1274,6 +1299,42 @@ class SeriesImage(SeriesImageBase):
# }}
@receiver(pre_save, sender=SeriesImage)
def _capture_previous_series_for_series_image(sender, instance, **kwargs):
if not getattr(instance, "pk", None):
instance._previous_series_id_for_cache = None
return
previous_series_id = (
sender.objects.filter(pk=instance.pk).values_list("series_id", flat=True).first()
)
instance._previous_series_id_for_cache = previous_series_id
def _invalidate_and_touch_series(series_id):
if not series_id:
return
invalidate_series_declared_groups_cache(series_id)
Series.objects.filter(pk=series_id).update(modified_date=timezone.now())
@receiver(post_save, sender=SeriesImage)
def _invalidate_declared_groups_on_series_image_save(sender, instance, **kwargs):
affected_series_ids = set()
if instance.series_id:
affected_series_ids.add(instance.series_id)
previous_series_id = getattr(instance, "_previous_series_id_for_cache", None)
if previous_series_id:
affected_series_ids.add(previous_series_id)
for series_id in affected_series_ids:
_invalidate_and_touch_series(series_id)
@receiver(post_delete, sender=SeriesImage)
def _invalidate_declared_groups_on_series_image_delete(sender, instance, **kwargs):
_invalidate_and_touch_series(getattr(instance, "series_id", None))
class SeriesFinding(FindingBase):
series = models.ForeignKey(
"Series", related_name="findings", on_delete=models.SET_NULL, null=True
+30 -5
View File
@@ -32,10 +32,10 @@
<div class="card">
<div class="card-body p-2">
{% with image_url_array_and_count=series.get_image_url_array_and_count %}
<div id="root"
class="dicom-viewer-root w-100 viewer-frame-standard series-viewer-resizable"
style="height: clamp(640px, 78vh, 1200px); min-height: 640px; resize: vertical; overflow: auto;"
data-images="{{ image_url_array_and_count.0 }}"
<div id="root"
class="dicom-viewer-root w-100 viewer-frame-standard series-viewer-resizable"
style="height: clamp(640px, 78vh, 1200px); min-height: 640px; resize: vertical; overflow: auto;"
data-images="{{ image_url_array_and_count.0 }}"
data-auto-cache-stack=false>
</div>
{% endwith %}
@@ -187,6 +187,31 @@
</div>
</div>
<div class="card mb-3">
<div class="card-header">Detected Groups</div>
<div class="card-body">
{% if detected_series_groups %}
<div class="d-flex flex-column gap-2">
{% for group in detected_series_groups %}
<div class="border rounded p-2">
<div class="fw-semibold small">{{ group.label|default:"Series group" }}</div>
<div class="small text-muted">{{ group.imageIds|length }} images</div>
{% if group.groupValues %}
<div class="small mt-1">
{% for key, value in group.groupValues.items %}
<span class="badge bg-secondary-subtle text-body border me-1 mb-1">{{ key }}={{ value }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted small mb-0">No detected grouping values for this series.</p>
{% endif %}
</div>
</div>
<div class="card mb-3">
<div class="card-header">Actions</div>
<div class="card-body d-flex flex-column gap-2">
@@ -268,7 +293,7 @@
<div class="card-body p-2 d-flex flex-column gap-2">
<div id="truncate_viewer" class="dicom-viewer-root w-100"
style="height: 460px; min-height: 360px; resize: vertical; overflow: auto; background: #222;"
data-images="{{ image_url_array_and_count.0 }}"
data-images="{{ image_url_array_and_count.0 }}"
data-auto-cache-stack="false">
</div>
<div class="small text-muted">Downsample compare preview loads into this viewer as stack options.</div>
+7
View File
@@ -136,6 +136,7 @@ from .models import (
UserReportAnswer,
PrerequisiteRequired
, APIToken,
_build_declared_series_groups,
)
from .models import Procedure
from .models import NormalCase
@@ -2270,6 +2271,11 @@ def series_viewer(request, pk, finding_pk=None):
@user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access
def series_detail(request, pk, finding_pk=None):
series = get_object_or_404(Series, pk=pk)
series_images_with_urls = [
(image, f"{REMOTE_URL}{image.image.url}")
for image in series.get_images()
]
detected_series_groups = _build_declared_series_groups(series, series_images_with_urls)
can_edit = series.check_user_can_edit(request.user)
@@ -2286,6 +2292,7 @@ def series_detail(request, pk, finding_pk=None):
"series_finding_form": series_finding_form,
"editing_finding": editing_finding,
"can_edit": can_edit,
"detected_series_groups": detected_series_groups,
}
if can_edit: