Add caching and group detection for series images in series viewer
This commit is contained in:
+66
-5
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user