Compare commits

..
19 Commits
Author SHA1 Message Date
Ross d2f4c21883 Add "View as viva" option to collection actions dropdown 2026-02-09 13:03:17 +00:00
Ross 1f5931aab2 Refactor question search template: rename and move to sbas_search.html 2026-02-09 12:50:31 +00:00
Ross 08c3558861 Add dismissible performance warning for large collections with persistence 2026-02-09 12:45:46 +00:00
Ross add7a1cf38 Add dismissible performance warning for large collections in collection_detail template 2026-02-09 12:44:19 +00:00
Ross 65f08cce6c Add performance warning for large collections in collection_detail template 2026-02-09 12:42:03 +00:00
Ross 3c8238d737 Add user permission checks for series editing and enhance author display method 2026-02-09 12:27:20 +00:00
Ross 23f33ac7ae Optimize collection_viva by aggressively prefetching and annotating related objects to prevent N+1 queries 2026-02-09 12:15:36 +00:00
Ross ab130a08ed Optimize collection_viva by annotating and prefetching related objects to prevent N+1 queries 2026-02-09 12:13:00 +00:00
Ross 45e01a78c1 Refactor case management links and button structure for improved readability and consistency 2026-02-09 11:24:43 +00:00
Ross 25e5ef78ed Update case series count and management links to use collection.pk for consistency 2026-02-09 11:24:33 +00:00
Ross 13dd79b508 Rename parameter 'case_id' to 'case_number' in collection_case_dicom_json_review for consistency 2026-02-09 11:15:57 +00:00
Ross 89986ba2e9 Refactor collection_case_dicom_json to use case_number instead of case_id for improved clarity and error handling 2026-02-09 11:14:53 +00:00
Ross f9e69e1206 Rename parameter 'case_id' to 'case_number' in collection_case_dicom_json function for clarity 2026-02-09 11:13:44 +00:00
Ross e7de6c8970 Set default archive filter to False in CaseCollectionFilter 2026-02-09 11:06:39 +00:00
Ross ea139456a8 Fix URL for editing display set in modal 2026-02-09 10:56:37 +00:00
Ross 035da12e96 Add edit permission check for display set modal and update template 2026-02-09 10:49:28 +00:00
Ross 3b9942054d Increase timeout for loading display set details and remove redundant script from modal 2026-02-09 10:34:20 +00:00
Ross 5761117e68 Enhance case display with modal functionality for findings and display sets 2026-02-09 10:33:06 +00:00
Ross f316dc9bcb Filter out removed images in Case model's image retrieval 2026-02-09 09:46:16 +00:00
14 changed files with 306 additions and 217 deletions
+17
View File
@@ -21,6 +21,7 @@ from .models import (
)
from django.contrib.auth.models import User
from django.db.models import Q
from django.http import QueryDict
def get_authors(request):
@@ -85,6 +86,22 @@ class CaseCollectionFilter(django_filters.FilterSet):
user=None,
request=None,
):
# Default the archive filter to False when not provided so collections
# are not archived by default in listings.
if data is None:
data = QueryDict('', mutable=True)
data['archive'] = 'false'
else:
# QueryDict instances need to be copied to be mutable
try:
if 'archive' not in data:
data = data.copy()
data['archive'] = 'false'
except Exception:
# Fallback for non-QueryDict mappings
if isinstance(data, dict) and 'archive' not in data:
data = dict(data)
data['archive'] = 'false'
if not request.user.groups.filter(name="atlas_editor").exists():
queryset = queryset.prefetch_related("author").filter(
author__id=request.user.id
+35 -5
View File
@@ -720,11 +720,20 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
series_qs = self.series.all()
if exclude_series_ids is not None:
series_qs = self.series.exclude(id__in=exclude_series_ids)
images = [
[image.image.url for image in series.images.all()]
for series in series_qs.order_by('seriesdetail__sort_order').prefetch_related('images')
]
series_qs = series_qs.exclude(id__in=exclude_series_ids)
# Ensure series are ordered by the through model sort_order
series_list = list(series_qs.order_by('seriesdetail__sort_order').prefetch_related('images'))
images = []
for series in series_list:
# If images were prefetched, use the cached list and filter in Python
prefetched = getattr(series, '_prefetched_objects_cache', {}).get('images')
if prefetched is not None:
imgs = [img.image.url for img in prefetched if not getattr(img, 'removed', False)]
else:
imgs = [image.image.url for image in series.images.filter(removed=False)]
images.append(imgs)
if as_json:
return json.dumps(images)
@@ -1038,6 +1047,27 @@ class Series(SeriesBase):
return self.description
else:
return f"{self.examination} ({self.plane})"
def user_can_edit(self, user, allow_if_can_edit_cases=True) -> bool:
"""Check if the user can edit the series.
Args:
user (User): The user to check.
Returns:
bool: True if the user can edit the series, False otherwise.
"""
if user.is_superuser:
return True
if self.author.filter(id=user.id).exists():
return True
if allow_if_can_edit_cases:
# Check if user can edit any of the cases this series is in
for case in self.case.all():
if case.check_user_can_edit(user):
return True
return False
def get_full_str(self):
if self.case:
+101 -105
View File
@@ -316,120 +316,116 @@
<!-- Series Findings -->
<h5>Series Findings</h5>
{% if case.ordered_series %}
{% for series in case.ordered_series %}
{% for finding in series.findings.all %}
<div class="finding-box" id="finding-box-{{ finding.pk }}" data-series="{{ series.pk }}">
{% for series in case.ordered_series %}
{% for finding in series.findings.all %}
<div class="finding-box" id="finding-box-{{ finding.pk }}" data-series="{{ series.pk }}">
<span>
<a href="{{series.get_absolute_url}}?show_finding={{finding.pk}}">
<button class="btn btn-primary btn-sm">View</button>
</a>
<button class="btn btn-secondary btn-sm view-finding-modal" data-finding-id="{{ finding.pk }}" data-series-id="{{ series.pk }}">
View in Modal
</button>
</span>
{% if finding.findings %}
<span>
<a href="{{series.get_absolute_url}}?show_finding={{finding.pk}}">
<button class="btn btn-primary btn-sm">View</button>
</a>
<button class="btn btn-secondary btn-sm view-finding-modal" data-finding-id="{{ finding.pk }}" data-series-id="{{ series.pk }}">
View in Modal
</button>
Findings: {% for f in finding.findings.all %}
{{f.get_link}},&nbsp;
{% endfor %}
</span>
{% if finding.findings %}
<span>
Findings: {% for f in finding.findings.all %}
{{f.get_link}},&nbsp;
{% endfor %}
</span>
{% endif %}
{% if finding.conditions %}
<span>
Conditions: {% for c in finding.conditions.all %}
{{c.get_link}},&nbsp;
{% endfor %}
</span>
{% endif %}
{% if finding.structures %}
<span>
Structure: {% for s in finding.structures.all %}
{{s.get_link}},&nbsp;
{% endfor %}
</span>
{% endif %}
{% if finding.description %}
<span>
Description: {{finding.description}}
</span>
{% endif %}
</div>
{% endfor %}
{% endif %}
{% if finding.conditions %}
<span>
Conditions: {% for c in finding.conditions.all %}
{{c.get_link}},&nbsp;
{% endfor %}
</span>
{% endif %}
{% if finding.structures %}
<span>
Structure: {% for s in finding.structures.all %}
{{s.get_link}},&nbsp;
{% endfor %}
</span>
{% endif %}
{% if finding.description %}
<span>
Description: {{finding.description}}
</span>
{% endif %}
</div>
{% endfor %}
{% else %}
{% empty%}
No series associated with case.
{% endif %}
{% endfor %}
<!-- Case Display Sets -->
<h5 class="mt-3">Case Display Sets</h5>
{% if case.display_sets %}
<ul class="list-group">
{% for ds in case.display_sets.all %}
<li class="list-group-item">
<b>Name: {{ ds.name }}</b>
{% if ds.description %}
<span class="text-muted">({{ ds.description }})</span>
<ul class="list-group">
{% for ds in case.display_sets.all %}
<li class="list-group-item">
<b>Name: {{ ds.name }}</b>
{% if ds.description %}
<span class="text-muted">({{ ds.description }})</span>
{% endif %}
<span>
<a href="{% url 'atlas:case_displaysets_detail' ds.pk %}">View Display Set</a>
<button type="button" class="btn btn-secondary btn-sm view-displayset-modal ms-2" data-ds-id="{{ ds.pk }}" data-url="{% url 'atlas:case_displaysets_modal' ds.pk %}">View in Modal</button>
</span>
<div>
<strong>Findings:</strong>
{% if ds.findings.all %}
<ul>
{% for finding in ds.findings.all %}
<li>{{ finding }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
<span>
<a href="{% url 'atlas:case_displaysets_detail' ds.pk %}">View Display Set</a>
<button type="button" class="btn btn-secondary btn-sm view-displayset-modal ms-2" data-ds-id="{{ ds.pk }}" data-url="{% url 'atlas:case_displaysets_modal' ds.pk %}">View in Modal</button>
</span>
<div>
<strong>Findings:</strong>
{% if ds.findings.all %}
<ul>
{% for finding in ds.findings.all %}
<li>{{ finding }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
<div>
<strong>Structures:</strong>
{% if ds.structures.all %}
<ul>
{% for structure in ds.structures.all %}
<li>{{ structure }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
<div>
<strong>Conditions:</strong>
{% if ds.conditions.all %}
<ul>
{% for condition in ds.conditions.all %}
<li>{{ condition }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
<div>
<details>
<summary class="small text-muted" style="cursor:pointer;">Show advanced (viewer state & annotations)</summary>
<div>
<strong>Viewer State:</strong>
</div>
<div>
<strong>Structures:</strong>
{% if ds.structures.all %}
<ul>
{% for structure in ds.structures.all %}
<li>{{ structure }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
<div>
<strong>Conditions:</strong>
{% if ds.conditions.all %}
<ul>
{% for condition in ds.conditions.all %}
<li>{{ condition }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
<div>
<details>
<summary class="small text-muted" style="cursor:pointer;">Show advanced (viewer state & annotations)</summary>
<div>
<strong>Viewer State:</strong>
<pre class="small">{{ ds.viewerstate|default:"{}" }}</pre>
</div>
<div>
<strong>Annotations:</strong>
</div>
<div>
<strong>Annotations:</strong>
<pre class="small">{{ ds.annotations|default:"{}" }}</pre>
</div>
</details>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">No display sets for this case.</span>
{% endif %}
</div>
</details>
</div>
</li>
{% empty %}
<span class="text-muted">No display sets for this case.</span>
{% endfor %}
</ul>
</details>
</div>
<div>
@@ -804,7 +800,7 @@
dsModalBody.innerHTML = html;
setTimeout(function () {
try { window.mountDicomViewers(); } catch (e) { console.warn('mountDicomViewers not available', e); }
}, 200);
}, 500);
})
.catch(error => {
console.error("Error loading display set details:", error);
@@ -1,18 +1,26 @@
<div class="case-displayset-modal">
<div class="mb-3">
<h5>{{ ds.name }}</h5>
{% if can_edit %}
<a href="{% url 'atlas:case_displaysets' ds.case.pk %}?displayset={{ ds.pk }}&edit=true" class="btn btn-sm btn-outline-secondary ms-2">Edit display set</a>
{% endif %}
{% if ds.description %}
<div class="text-muted">{{ ds.description }}</div>
{% endif %}
</div>
{# Viewer area for the modal - mounted by rad/static/dv3d bundle #}
<div class="mb-3">
<div id="modal_viewer_{{ ds.pk }}" class="dicom-viewer-root" style="height:420px;" data-auto-cache-stack="false" data-annotationjson="{{ ds.annotations }}" data-viewerstate="{{ ds.viewerstate }}"></div>
</div>
{% if ds.viewerstate and ds.annotations %}
<div class="mb-3">
<button
type="button"
class="btn btn-primary"
onclick='importAnnotations_main_viewer({{ ds.annotations|safe }}); importViewerState_main_viewer({{ ds.viewerstate|safe }});'
>Load into viewer</button>
onclick='(window.importAnnotations_modal_viewer_{{ ds.pk }}||window.importAnnotations_modal_viewer||window.importAnnotations)({{ ds.annotations|safe }}); (window.importViewerState_modal_viewer_{{ ds.pk }}||window.importViewerState_modal_viewer||window.importViewerState)({{ ds.viewerstate|safe }});'
>Reload display set</button>
</div>
{% endif %}
@@ -19,6 +19,12 @@
<h2 class="card-title mb-2">{{ collection.name }}</h2>
{% include 'exam_notes.html' %}
{% if casesdetails|length > 20 %}
<div id="collection-warning-{{ collection.pk }}" class="alert alert-warning alert-dismissible fade show" role="alert">
<strong>Performance warning:</strong> This collection contains {{ casesdetails|length }} cases — in some situations this may adversely affect performance, all features will still work but page loading may be slower. Consider splitting into smaller collections if performance is an issue.
<button id="collection-warning-close-{{ collection.pk }}" type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<p class="mb-1">
</p>
@@ -158,4 +164,25 @@
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
try {
var pk = '{{ collection.pk }}';
var key = 'collection_warning_dismissed_' + pk;
var alertEl = document.getElementById('collection-warning-' + pk);
if (!alertEl) return;
if (localStorage.getItem(key) === '1') {
alertEl.remove();
return;
}
var btn = document.getElementById('collection-warning-close-' + pk);
if (btn) {
btn.addEventListener('click', function() {
try { localStorage.setItem(key, '1'); } catch (e) { /* ignore */ }
});
}
} catch (e) { console.warn('collection warning persistence error', e); }
});
</script>
{% endblock %}
@@ -28,6 +28,8 @@
<li><a class="dropdown-item" href="{% url 'atlas:collection_authors' collection.pk %}">Authors</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="{% url 'atlas:exam_deleted' collection.id %}">Delete</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'atlas:collection_viva' collection.pk %}">View as viva</a></li>
{% if request.user.is_superuser %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'admin:atlas_casecollection_change' collection.id %}">Admin Edit</a></li>
@@ -13,10 +13,10 @@
</div>
<div class="mt-2 small text-muted">
{% if casedetail.case.series.count == 0 %}
{% if casedetail.series_count|default:0 == 0 %}
<span class="text-warning">This case has no series / stacks attached</span>
{% else %}
{{ casedetail.case.series.count }} series
{{ casedetail.series_count }} series
{% endif %}
{% if casedetail.question_schema %}
&nbsp;&nbsp;<span class="text-success">Questions defined</span>
@@ -24,46 +24,46 @@
</div>
<div class="btn-group btn-group-sm ms-2" role="group" aria-label="case-field-links">
<div class="btn-group btn-group-sm ms-2" role="group" aria-label="case-field-links">
<!-- History button: opens modal populated from hidden content on page -->
{% if casedetail.case.history %}
<a class="btn btn-outline-secondary" href="#"
onclick="document.querySelector('#case-field-modal .modal-content').innerHTML = document.getElementById('case-field-{{ casedetail.case.pk }}-history').innerHTML;"
data-bs-toggle="modal" data-bs-target="#case-field-modal" title="View history">
<i class="bi bi-clock-history text-primary"></i>
</a>
{% else %}
<a class="btn btn-outline-secondary disabled" aria-disabled="true" href="javascript:void(0);" title="No history">
<i class="bi bi-clock-history text-muted"></i>
</a>
{% endif %}
{% if casedetail.case.history %}
<a class="btn btn-outline-secondary" href="#"
onclick="document.querySelector('#case-field-modal .modal-content').innerHTML = document.getElementById('case-field-{{ casedetail.case.pk }}-history').innerHTML;"
data-bs-toggle="modal" data-bs-target="#case-field-modal" title="View history">
<i class="bi bi-clock-history text-primary"></i>
</a>
{% else %}
<a class="btn btn-outline-secondary disabled" aria-disabled="true" href="javascript:void(0);" title="No history">
<i class="bi bi-clock-history text-muted"></i>
</a>
{% endif %}
<!-- Discussion button -->
{% if casedetail.case.discussion %}
<a class="btn btn-outline-secondary" href="#"
onclick="document.querySelector('#case-field-modal .modal-content').innerHTML = document.getElementById('case-field-{{ casedetail.case.pk }}-discussion').innerHTML;"
data-bs-toggle="modal" data-bs-target="#case-field-modal" title="View discussion">
<i class="bi bi-chat-left-text text-primary"></i>
</a>
{% else %}
<a class="btn btn-outline-secondary disabled" aria-disabled="true" href="javascript:void(0);" title="No discussion">
<i class="bi bi-chat-left-text text-muted"></i>
</a>
{% endif %}
{% if casedetail.case.discussion %}
<a class="btn btn-outline-secondary" href="#"
onclick="document.querySelector('#case-field-modal .modal-content').innerHTML = document.getElementById('case-field-{{ casedetail.case.pk }}-discussion').innerHTML;"
data-bs-toggle="modal" data-bs-target="#case-field-modal" title="View discussion">
<i class="bi bi-chat-left-text text-primary"></i>
</a>
{% else %}
<a class="btn btn-outline-secondary disabled" aria-disabled="true" href="javascript:void(0);" title="No discussion">
<i class="bi bi-chat-left-text text-muted"></i>
</a>
{% endif %}
<!-- Report button -->
{% if casedetail.case.report %}
<a class="btn btn-outline-secondary" href="#"
onclick="document.querySelector('#case-field-modal .modal-content').innerHTML = document.getElementById('case-field-{{ casedetail.case.pk }}-report').innerHTML;"
data-bs-toggle="modal" data-bs-target="#case-field-modal" title="View report">
<i class="bi bi-file-earmark-text text-primary"></i>
</a>
{% else %}
<a class="btn btn-outline-secondary disabled" aria-disabled="true" href="javascript:void(0);" title="No report">
<i class="bi bi-file-earmark-text text-muted"></i>
</a>
{% endif %}
</div>
{% if casedetail.case.report %}
<a class="btn btn-outline-secondary" href="#"
onclick="document.querySelector('#case-field-modal .modal-content').innerHTML = document.getElementById('case-field-{{ casedetail.case.pk }}-report').innerHTML;"
data-bs-toggle="modal" data-bs-target="#case-field-modal" title="View report">
<i class="bi bi-file-earmark-text text-primary"></i>
</a>
{% else %}
<a class="btn btn-outline-secondary disabled" aria-disabled="true" href="javascript:void(0);" title="No report">
<i class="bi bi-file-earmark-text text-muted"></i>
</a>
{% endif %}
</div>
</div>
@@ -1,44 +1,44 @@
<div class="btn-group btn-group-sm me-2" role="group" aria-label="management-actions">
<a class="btn btn-outline-secondary" href="{% url 'atlas:collection_case_displaysetup_legacy' casedetail.collection.pk casedetail.case.pk %}" title="Setup default display">
<i class="bi bi-display"></i>
<div class="btn-group btn-group-sm me-2" role="group" aria-label="management-actions">
<a class="btn btn-outline-secondary" href="{% url 'atlas:collection_case_displaysetup_legacy' collection.pk casedetail.case.pk %}" title="Setup default display">
<i class="bi bi-display"></i>
</a>
<a class="btn btn-outline-secondary" href="{% url 'atlas:collection_case_details_legacy' collection.pk casedetail.case.pk %}" title="Case details">
<i class="bi bi-info-square"></i>
</a>
{% if collection.collection_type == "QUE" %}
<a class="btn btn-outline-secondary" href='{% url "atlas:collection_case_questions_legacy" collection.pk casedetail.case.pk %}' title="Manage questions">
{% if casedetail.question_schema %}
<i class="bi bi-question-square text-success"></i>
{% else %}
<i class="bi bi-question-square text-danger"></i>
{% endif %}
</a>
{% endif %}
<a class="btn btn-outline-secondary" href="{% url 'atlas:collection_case_details_legacy' casedetail.collection.pk casedetail.case.pk %}" title="Case details">
<i class="bi bi-info-square"></i>
{% if casedetail.case.previous_case %}
<a class="btn btn-outline-secondary" href='{% url "atlas:collection_case_priors_legacy" collection.pk casedetail.case.pk %}' title="Manage priors">
<i class="bi bi-link-45deg"></i>
</a>
{% endif %}
</div>
{% if collection.collection_type == "QUE" %}
<a class="btn btn-outline-secondary" href='{% url "atlas:collection_case_questions_legacy" casedetail.collection.pk casedetail.case.pk %}' title="Manage questions">
{% if casedetail.question_schema %}
<i class="bi bi-question-square text-success"></i>
{% else %}
<i class="bi bi-question-square text-danger"></i>
{% endif %}
</a>
{% endif %}
{% if casedetail.case.previous_case %}
<a class="btn btn-outline-secondary" href='{% url "atlas:collection_case_priors_legacy" casedetail.collection.pk casedetail.case.pk %}' title="Manage priors">
<i class="bi bi-link-45deg"></i>
</a>
{% endif %}
</div>
<div class="btn-group btn-group-sm" role="group" aria-label="status-indicator">
{% if casedetail.default_viewerstate %}
<button type="button" class="btn btn-sm btn-outline-success" aria-disabled="true" title="This case has a default viewerstate defined">
<i class="bi bi-check"></i>
</button>
{% endif %}
</div>
<div class="btn-group btn-group-sm" role="group" aria-label="status-indicator">
{% if casedetail.default_viewerstate %}
<button type="button" class="btn btn-sm btn-outline-success" aria-disabled="true" title="This case has a default viewerstate defined">
<i class="bi bi-check"></i>
</button>
{% endif %}
</div>
{# Remove button (HTMX) - only shown when the surrounding template has `can_edit` true #}
{% if can_edit %}
<form class="d-inline ms-2 m-0" hx-post="{% url 'atlas:remove_case_from_collection' casedetail.case.pk casedetail.collection.pk %}" hx-target="closest li" hx-swap="outerHTML" hx-confirm="Remove this case from the collection?">
<button type="submit" class="btn btn-outline-danger btn-sm case-remove" title="Remove this case from the collection">
<i class="bi bi-trash"></i>
</button>
</form>
{% endif %}
{% if can_edit %}
<form class="d-inline ms-2 m-0" hx-post="{% url 'atlas:remove_case_from_collection' casedetail.case.pk collection.pk %}" hx-target="closest li" hx-swap="outerHTML" hx-confirm="Remove this case from the collection?">
<button type="submit" class="btn btn-outline-danger btn-sm case-remove" title="Remove this case from the collection">
<i class="bi bi-trash"></i>
</button>
</form>
{% endif %}
+33 -19
View File
@@ -545,7 +545,12 @@ def case_displaysets_detail(request, pk):
def case_displaysets_modal(request, pk):
"""Return HTML fragment for a CaseDisplaySet to be shown inside a modal."""
displayset = get_object_or_404(CaseDisplaySet, pk=pk)
return render(request, 'atlas/case_displayset_modal.html', {'ds': displayset})
can_edit = False
try:
can_edit = displayset.case.check_user_can_edit(request.user)
except Exception:
can_edit = False
return render(request, 'atlas/case_displayset_modal.html', {'ds': displayset, 'can_edit': can_edit})
def case_displaysets_delete(request, pk):
try:
@@ -2735,7 +2740,7 @@ def case_order_dicom(request, pk):
# TODO: don't try to order non stack series
for series in case.series.all():
try:
if request.user in series.get_author_objects():
if series.check_user_can_edit(request.user):
series.order_by_dicom()
else:
fail.append(f"{series.pk}: invalid permission")
@@ -3106,20 +3111,32 @@ def collection_viva(request, pk):
# )
#)
# Prefetch and annotate aggressively to avoid N+1 queries in the viva view.
# - Prefetch `case__series` using the ordering from SeriesDetail
# - Prefetch only non-removed images for those series (ordered by position)
# - Prefetch case resources and display sets
from django.db.models import Prefetch as _Prefetch
series_images_qs = SeriesImage.objects.filter(removed=False).order_by("position")
series_qs = Series.objects.order_by("seriesdetail__sort_order").prefetch_related(
_Prefetch("images", queryset=series_images_qs)
)
casedetails = (
CaseDetail.objects
.filter(collection=collection)
.select_related("case")
.order_by("sort_order")
.prefetch_related(
# Prefetch series and their images directly on the case
"case__series",
"case__series__modality",
"case__series__examination",
"case__series__plane",
"case__series__contrast",
"case__series__images",
_Prefetch("case__series", queryset=series_qs),
_Prefetch("case__series__modality"),
_Prefetch("case__series__examination"),
_Prefetch("case__series__plane"),
_Prefetch("case__series__contrast"),
_Prefetch("case__caseresource_set"),
_Prefetch("case__display_sets"),
)
.annotate(series_count=Count("case__seriesdetail"))
)
@@ -4322,21 +4339,18 @@ def use_dates_as_descriptions(request, pk):
return HttpResponse("Done")
def collection_case_dicom_json_review(request, exam_id, case_id):
return collection_case_dicom_json(request, exam_id, case_id, review=True)
def collection_case_dicom_json_review(request, exam_id, case_number):
return collection_case_dicom_json(request, exam_id, case_number, review=True)
def collection_case_dicom_json(request, exam_id, case_id, review=False):
def collection_case_dicom_json(request, exam_id, case_number, review=False):
# Support either case_number (index) or case_id (case PK)
collection = get_object_or_404(CaseCollection, pk=exam_id)
try:
casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id)
case_obj = collection.get_case_by_index(int(case_number))
casedetail = CaseDetail.objects.get(case=case_obj, collection=collection)
except Exception:
collection = get_object_or_404(CaseCollection, pk=exam_id)
try:
case_obj = collection.get_case_by_index(int(case_id))
casedetail = CaseDetail.objects.get(case=case_obj, collection=collection)
except Exception:
raise Http404("Case not found in collection")
raise Http404("Case not found in collection")
if review:
priors = casedetail.caseprior_set.exclude(prior_visibility="NO")
+3
View File
@@ -70,6 +70,9 @@ class AuthorMixin():
return self.author.filter(id=user.id).exists()
def get_author_display(self):
return ", ".join([i.username for i in self.get_author_objects()])
class UserConfigurablePaginationMixin:
default_per_page = 25
+1 -11
View File
@@ -387,7 +387,7 @@ class SeriesImageBase(models.Model):
super().save(*args, **kwargs) # Call the "real" save() method.
class SeriesBase(models.Model):
class SeriesBase(models.Model, AuthorMixin):
info = models.TextField(
blank=True,
help_text="Description of stack, for admin organisation, will not be visible when taking",
@@ -444,16 +444,6 @@ class SeriesBase(models.Model):
return False
def get_author_objects(self):
"""Returns a list of authors"""
if self.author:
return self.author.all()
else:
return ["None"]
def get_author_display(self):
return ", ".join([i.username for i in self.get_author_objects()])
def get_examination(self):
"""Returns a comma seperated text list of regions"""
return str(self.examination)
@@ -17,7 +17,7 @@
<input class="form-control" id="sbas-question-search-input" name="q" type="search"
placeholder="Search questions by stem, answers, category or tags"
hx-get="{% url 'sbas:question_search_partial' %}"
hx-trigger="input changed delay:400ms, keyup[key=='Enter']"
hx-trigger="input delay:400ms, keyup[key=='Enter']"
hx-target="#sbas-question-search-results"
hx-indicator="#sbas-question-search-indicator" />
<span class="input-group-text" id="sbas-question-search-indicator" aria-hidden="true">
+1 -1
View File
@@ -244,7 +244,7 @@ def question_search_page(request):
The actual results are returned by `question_search_partial` and swapped
into the results container via HTMX.
"""
return render(request, "sbas/search.html", {})
return render(request, "sbas/sbas_search.html", {})
@login_required
+2
View File
@@ -3,6 +3,7 @@
{% block content %}
<div class="">
<h2>404 error</h2>
{% if request.user.is_staff%}
{% if reason %}
{{ reason }}
{% endif %}
@@ -10,5 +11,6 @@
{% if resolved %}
<h3>{{ resolved }}</h3>
{% endif %}
{% endif %}
</div>
{% endblock %}