Compare commits
19
Commits
bdb7b653b6
...
d2f4c21883
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2f4c21883 | ||
|
|
1f5931aab2 | ||
|
|
08c3558861 | ||
|
|
add7a1cf38 | ||
|
|
65f08cce6c | ||
|
|
3c8238d737 | ||
|
|
23f33ac7ae | ||
|
|
ab130a08ed | ||
|
|
45e01a78c1 | ||
|
|
25e5ef78ed | ||
|
|
13dd79b508 | ||
|
|
89986ba2e9 | ||
|
|
f9e69e1206 | ||
|
|
e7de6c8970 | ||
|
|
ea139456a8 | ||
|
|
035da12e96 | ||
|
|
3b9942054d | ||
|
|
5761117e68 | ||
|
|
f316dc9bcb |
@@ -21,6 +21,7 @@ from .models import (
|
|||||||
)
|
)
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.http import QueryDict
|
||||||
|
|
||||||
|
|
||||||
def get_authors(request):
|
def get_authors(request):
|
||||||
@@ -85,6 +86,22 @@ class CaseCollectionFilter(django_filters.FilterSet):
|
|||||||
user=None,
|
user=None,
|
||||||
request=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():
|
if not request.user.groups.filter(name="atlas_editor").exists():
|
||||||
queryset = queryset.prefetch_related("author").filter(
|
queryset = queryset.prefetch_related("author").filter(
|
||||||
author__id=request.user.id
|
author__id=request.user.id
|
||||||
|
|||||||
+35
-5
@@ -720,11 +720,20 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
|
|
||||||
series_qs = self.series.all()
|
series_qs = self.series.all()
|
||||||
if exclude_series_ids is not None:
|
if exclude_series_ids is not None:
|
||||||
series_qs = self.series.exclude(id__in=exclude_series_ids)
|
series_qs = series_qs.exclude(id__in=exclude_series_ids)
|
||||||
images = [
|
|
||||||
[image.image.url for image in series.images.all()]
|
# Ensure series are ordered by the through model sort_order
|
||||||
for series in series_qs.order_by('seriesdetail__sort_order').prefetch_related('images')
|
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:
|
if as_json:
|
||||||
return json.dumps(images)
|
return json.dumps(images)
|
||||||
@@ -1039,6 +1048,27 @@ class Series(SeriesBase):
|
|||||||
else:
|
else:
|
||||||
return f"{self.examination} ({self.plane})"
|
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):
|
def get_full_str(self):
|
||||||
if self.case:
|
if self.case:
|
||||||
case_id = ", ".join([str(case) for case in self.case.all()])
|
case_id = ", ".join([str(case) for case in self.case.all()])
|
||||||
|
|||||||
@@ -316,7 +316,6 @@
|
|||||||
|
|
||||||
<!-- Series Findings -->
|
<!-- Series Findings -->
|
||||||
<h5>Series Findings</h5>
|
<h5>Series Findings</h5>
|
||||||
{% if case.ordered_series %}
|
|
||||||
{% for series in case.ordered_series %}
|
{% for series in case.ordered_series %}
|
||||||
{% for finding in series.findings.all %}
|
{% for finding in series.findings.all %}
|
||||||
<div class="finding-box" id="finding-box-{{ finding.pk }}" data-series="{{ series.pk }}">
|
<div class="finding-box" id="finding-box-{{ finding.pk }}" data-series="{{ series.pk }}">
|
||||||
@@ -356,14 +355,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% empty%}
|
||||||
{% else %}
|
|
||||||
No series associated with case.
|
No series associated with case.
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
|
|
||||||
<!-- Case Display Sets -->
|
<!-- Case Display Sets -->
|
||||||
<h5 class="mt-3">Case Display Sets</h5>
|
<h5 class="mt-3">Case Display Sets</h5>
|
||||||
{% if case.display_sets %}
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{% for ds in case.display_sets.all %}
|
{% for ds in case.display_sets.all %}
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
@@ -425,11 +422,10 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">No display sets for this case.</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">No display sets for this case.</span>
|
|
||||||
{% endif %}
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -804,7 +800,7 @@
|
|||||||
dsModalBody.innerHTML = html;
|
dsModalBody.innerHTML = html;
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
try { window.mountDicomViewers(); } catch (e) { console.warn('mountDicomViewers not available', e); }
|
try { window.mountDicomViewers(); } catch (e) { console.warn('mountDicomViewers not available', e); }
|
||||||
}, 200);
|
}, 500);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Error loading display set details:", error);
|
console.error("Error loading display set details:", error);
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
<div class="case-displayset-modal">
|
<div class="case-displayset-modal">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h5>{{ ds.name }}</h5>
|
<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 %}
|
{% if ds.description %}
|
||||||
<div class="text-muted">{{ ds.description }}</div>
|
<div class="text-muted">{{ ds.description }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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 %}
|
{% if ds.viewerstate and ds.annotations %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
onclick='importAnnotations_main_viewer({{ ds.annotations|safe }}); importViewerState_main_viewer({{ ds.viewerstate|safe }});'
|
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 }});'
|
||||||
>Load into viewer</button>
|
>Reload display set</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,12 @@
|
|||||||
<h2 class="card-title mb-2">{{ collection.name }}</h2>
|
<h2 class="card-title mb-2">{{ collection.name }}</h2>
|
||||||
|
|
||||||
{% include 'exam_notes.html' %}
|
{% 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 class="mb-1">
|
||||||
</p>
|
</p>
|
||||||
@@ -158,4 +164,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
<li><a class="dropdown-item" href="{% url 'atlas:collection_authors' collection.pk %}">Authors</a></li>
|
<li><a class="dropdown-item" href="{% url 'atlas:collection_authors' collection.pk %}">Authors</a></li>
|
||||||
<li><hr class="dropdown-divider"></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><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 %}
|
{% if request.user.is_superuser %}
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'admin:atlas_casecollection_change' collection.id %}">Admin Edit</a></li>
|
<li><a class="dropdown-item" href="{% url 'admin:atlas_casecollection_change' collection.id %}">Admin Edit</a></li>
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 small text-muted">
|
<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>
|
<span class="text-warning">This case has no series / stacks attached</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ casedetail.case.series.count }} series
|
{{ casedetail.series_count }} series
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if casedetail.question_schema %}
|
{% if casedetail.question_schema %}
|
||||||
• <span class="text-success">Questions defined</span>
|
• <span class="text-success">Questions defined</span>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<div class="btn-group btn-group-sm me-2" role="group" aria-label="management-actions">
|
<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">
|
<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>
|
<i class="bi bi-display"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="btn btn-outline-secondary" href="{% url 'atlas:collection_case_details_legacy' casedetail.collection.pk casedetail.case.pk %}" title="Case details">
|
<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>
|
<i class="bi bi-info-square"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if collection.collection_type == "QUE" %}
|
{% 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">
|
<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 %}
|
{% if casedetail.question_schema %}
|
||||||
<i class="bi bi-question-square text-success"></i>
|
<i class="bi bi-question-square text-success"></i>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if casedetail.case.previous_case %}
|
{% 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">
|
<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>
|
<i class="bi bi-link-45deg"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
{# Remove button (HTMX) - only shown when the surrounding template has `can_edit` true #}
|
{# Remove button (HTMX) - only shown when the surrounding template has `can_edit` true #}
|
||||||
{% if can_edit %}
|
{% 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?">
|
<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">
|
<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>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+30
-16
@@ -545,7 +545,12 @@ def case_displaysets_detail(request, pk):
|
|||||||
def case_displaysets_modal(request, pk):
|
def case_displaysets_modal(request, pk):
|
||||||
"""Return HTML fragment for a CaseDisplaySet to be shown inside a modal."""
|
"""Return HTML fragment for a CaseDisplaySet to be shown inside a modal."""
|
||||||
displayset = get_object_or_404(CaseDisplaySet, pk=pk)
|
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):
|
def case_displaysets_delete(request, pk):
|
||||||
try:
|
try:
|
||||||
@@ -2735,7 +2740,7 @@ def case_order_dicom(request, pk):
|
|||||||
# TODO: don't try to order non stack series
|
# TODO: don't try to order non stack series
|
||||||
for series in case.series.all():
|
for series in case.series.all():
|
||||||
try:
|
try:
|
||||||
if request.user in series.get_author_objects():
|
if series.check_user_can_edit(request.user):
|
||||||
series.order_by_dicom()
|
series.order_by_dicom()
|
||||||
else:
|
else:
|
||||||
fail.append(f"{series.pk}: invalid permission")
|
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 = (
|
casedetails = (
|
||||||
CaseDetail.objects
|
CaseDetail.objects
|
||||||
.filter(collection=collection)
|
.filter(collection=collection)
|
||||||
.select_related("case")
|
.select_related("case")
|
||||||
.order_by("sort_order")
|
.order_by("sort_order")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
# Prefetch series and their images directly on the case
|
_Prefetch("case__series", queryset=series_qs),
|
||||||
"case__series",
|
_Prefetch("case__series__modality"),
|
||||||
"case__series__modality",
|
_Prefetch("case__series__examination"),
|
||||||
"case__series__examination",
|
_Prefetch("case__series__plane"),
|
||||||
"case__series__plane",
|
_Prefetch("case__series__contrast"),
|
||||||
"case__series__contrast",
|
_Prefetch("case__caseresource_set"),
|
||||||
"case__series__images",
|
_Prefetch("case__display_sets"),
|
||||||
)
|
)
|
||||||
|
.annotate(series_count=Count("case__seriesdetail"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -4322,18 +4339,15 @@ def use_dates_as_descriptions(request, pk):
|
|||||||
|
|
||||||
return HttpResponse("Done")
|
return HttpResponse("Done")
|
||||||
|
|
||||||
def collection_case_dicom_json_review(request, exam_id, case_id):
|
def collection_case_dicom_json_review(request, exam_id, case_number):
|
||||||
return collection_case_dicom_json(request, exam_id, case_id, review=True)
|
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)
|
# Support either case_number (index) or case_id (case PK)
|
||||||
try:
|
|
||||||
casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
|
||||||
except Exception:
|
|
||||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||||
try:
|
try:
|
||||||
case_obj = collection.get_case_by_index(int(case_id))
|
case_obj = collection.get_case_by_index(int(case_number))
|
||||||
casedetail = CaseDetail.objects.get(case=case_obj, collection=collection)
|
casedetail = CaseDetail.objects.get(case=case_obj, collection=collection)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise Http404("Case not found in collection")
|
raise Http404("Case not found in collection")
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ class AuthorMixin():
|
|||||||
|
|
||||||
return self.author.filter(id=user.id).exists()
|
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:
|
class UserConfigurablePaginationMixin:
|
||||||
default_per_page = 25
|
default_per_page = 25
|
||||||
|
|||||||
+1
-11
@@ -387,7 +387,7 @@ class SeriesImageBase(models.Model):
|
|||||||
super().save(*args, **kwargs) # Call the "real" save() method.
|
super().save(*args, **kwargs) # Call the "real" save() method.
|
||||||
|
|
||||||
|
|
||||||
class SeriesBase(models.Model):
|
class SeriesBase(models.Model, AuthorMixin):
|
||||||
info = models.TextField(
|
info = models.TextField(
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Description of stack, for admin organisation, will not be visible when taking",
|
help_text="Description of stack, for admin organisation, will not be visible when taking",
|
||||||
@@ -444,16 +444,6 @@ class SeriesBase(models.Model):
|
|||||||
|
|
||||||
return False
|
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):
|
def get_examination(self):
|
||||||
"""Returns a comma seperated text list of regions"""
|
"""Returns a comma seperated text list of regions"""
|
||||||
return str(self.examination)
|
return str(self.examination)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<input class="form-control" id="sbas-question-search-input" name="q" type="search"
|
<input class="form-control" id="sbas-question-search-input" name="q" type="search"
|
||||||
placeholder="Search questions by stem, answers, category or tags"
|
placeholder="Search questions by stem, answers, category or tags"
|
||||||
hx-get="{% url 'sbas:question_search_partial' %}"
|
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-target="#sbas-question-search-results"
|
||||||
hx-indicator="#sbas-question-search-indicator" />
|
hx-indicator="#sbas-question-search-indicator" />
|
||||||
<span class="input-group-text" id="sbas-question-search-indicator" aria-hidden="true">
|
<span class="input-group-text" id="sbas-question-search-indicator" aria-hidden="true">
|
||||||
+1
-1
@@ -244,7 +244,7 @@ def question_search_page(request):
|
|||||||
The actual results are returned by `question_search_partial` and swapped
|
The actual results are returned by `question_search_partial` and swapped
|
||||||
into the results container via HTMX.
|
into the results container via HTMX.
|
||||||
"""
|
"""
|
||||||
return render(request, "sbas/search.html", {})
|
return render(request, "sbas/sbas_search.html", {})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="">
|
<div class="">
|
||||||
<h2>404 error</h2>
|
<h2>404 error</h2>
|
||||||
|
{% if request.user.is_staff%}
|
||||||
{% if reason %}
|
{% if reason %}
|
||||||
{{ reason }}
|
{{ reason }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -10,5 +11,6 @@
|
|||||||
{% if resolved %}
|
{% if resolved %}
|
||||||
<h3>{{ resolved }}</h3>
|
<h3>{{ resolved }}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user