Enhance case display sets management with HTMX integration and improved UI elements
This commit is contained in:
@@ -1,21 +1,49 @@
|
||||
{# Partial: Case Display Sets (displaysets) #}
|
||||
{# Buttons adapt at runtime: if an in-page viewer is present (importAnnotations_main_viewer), #}
|
||||
{# "View" loads the display set into it; otherwise falls back to the modal viewer. #}
|
||||
{# An Edit button is shown separately when can_edit is truthy. #}
|
||||
<div>
|
||||
<h5 class="mt-3">Case Display Sets</h5>
|
||||
<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>
|
||||
<li class="list-group-item" id="displayset-row-{{ ds.pk }}">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div>
|
||||
<b>{{ ds.name }}</b>
|
||||
{% if ds.description %}
|
||||
<span class="text-muted ms-1">({{ ds.description }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-shrink-0">
|
||||
{% if ds.viewerstate or ds.annotations %}
|
||||
<button type="button"
|
||||
class="btn btn-primary btn-sm ds-view-btn"
|
||||
data-ds-id="{{ ds.pk }}"
|
||||
data-modal-url="{% url 'atlas:case_displaysets_modal' ds.pk %}"
|
||||
data-annotations='{{ ds.annotations|safe }}'
|
||||
data-viewerstate='{{ ds.viewerstate|safe }}'>
|
||||
<i class="bi bi-display"></i> View Display Set
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button"
|
||||
class="btn btn-secondary btn-sm view-displayset-modal"
|
||||
data-ds-id="{{ ds.pk }}"
|
||||
data-url="{% url 'atlas:case_displaysets_modal' ds.pk %}">
|
||||
<i class="bi bi-eye"></i> View in Modal
|
||||
</button>
|
||||
{% if can_edit %}
|
||||
<a href="{% url 'atlas:case_displaysets_detail' ds.pk %}?edit=1"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<strong>Findings:</strong>
|
||||
{% if ds.findings.all %}
|
||||
<ul>
|
||||
<ul class="mb-1">
|
||||
{% for finding in ds.findings.all %}
|
||||
<li>{{ finding.get_link }}</li>
|
||||
{% endfor %}
|
||||
@@ -27,7 +55,7 @@
|
||||
<div>
|
||||
<strong>Structures:</strong>
|
||||
{% if ds.structures.all %}
|
||||
<ul>
|
||||
<ul class="mb-1">
|
||||
{% for structure in ds.structures.all %}
|
||||
<li>{{ structure.get_link }}</li>
|
||||
{% endfor %}
|
||||
@@ -39,7 +67,7 @@
|
||||
<div>
|
||||
<strong>Conditions:</strong>
|
||||
{% if ds.conditions.all %}
|
||||
<ul>
|
||||
<ul class="mb-1">
|
||||
{% for condition in ds.conditions.all %}
|
||||
<li>{{ condition.get_link }}</li>
|
||||
{% endfor %}
|
||||
@@ -48,25 +76,64 @@
|
||||
<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>
|
||||
{% include 'atlas/partials/json_pretty.html' with json_value=ds.viewerstate title='Viewer State' %}
|
||||
</div>
|
||||
<div>
|
||||
{% include 'atlas/partials/json_pretty.html' with json_value=ds.annotations title='Annotations' %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
{% empty %}
|
||||
<div class="displayset-empty-state">
|
||||
<span class="text-muted">No display sets for this case.</span>
|
||||
<li class="list-group-item text-muted">
|
||||
No display sets for this case.
|
||||
{% if can_edit %}
|
||||
<a class="displayset-empty-link" href="{% url 'atlas:case_displaysets' case.pk %}">Add one now</a>
|
||||
<a href="{% url 'atlas:case_displaysets' case.pk %}">Add one now</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
/**
|
||||
* For each "View Display Set" button:
|
||||
* - If the page has an in-page viewer (importAnnotations_main_viewer available),
|
||||
* load the display set's state into it and scroll the viewer into view.
|
||||
* - Otherwise, fall back to the existing modal loader (loadDisplaysetModal).
|
||||
*/
|
||||
document.querySelectorAll('.ds-view-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var dsId = btn.dataset.dsId;
|
||||
var modalUrl = btn.dataset.modalUrl;
|
||||
var annotations = btn.dataset.annotations;
|
||||
var viewerstate = btn.dataset.viewerstate;
|
||||
|
||||
var hasInPageViewer = (
|
||||
typeof window.importAnnotations_main_viewer === 'function' &&
|
||||
typeof window.importViewerState_main_viewer === 'function'
|
||||
);
|
||||
|
||||
if (hasInPageViewer) {
|
||||
// Ensure viewer UI is visible before applying display set state.
|
||||
var viewerDetails = document.getElementById('dicom-viewer-details');
|
||||
if (viewerDetails && viewerDetails.tagName === 'DETAILS') {
|
||||
viewerDetails.open = true;
|
||||
}
|
||||
|
||||
try { window.importAnnotations_main_viewer(JSON.parse(annotations || '{}')); } catch (e) { console.warn('displayset annotations import error', e); }
|
||||
try { window.importViewerState_main_viewer(JSON.parse(viewerstate || '{}')); } catch (e) { console.warn('displayset viewerstate import error', e); }
|
||||
|
||||
try { window.dispatchEvent(new Event('resize')); } catch (e) {}
|
||||
if (typeof window.resizeDicomViewers === 'function') {
|
||||
try { window.resizeDicomViewers(); } catch (e) { console.warn('displayset viewer resize error', e); }
|
||||
}
|
||||
|
||||
// Scroll the viewer into view
|
||||
var viewer = document.getElementById('main_viewer') || document.querySelector('.dicom-viewer-root');
|
||||
if (viewer) { viewer.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
|
||||
} else if (typeof window.loadDisplaysetModal === 'function') {
|
||||
window.loadDisplaysetModal(modalUrl, dsId);
|
||||
} else {
|
||||
// Ultimate fallback: navigate to the detail page
|
||||
window.location.href = modalUrl;
|
||||
}
|
||||
});
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
|
||||
+43
-8
@@ -484,11 +484,19 @@ class NormalCaseList(LoginRequiredMixin, FilterView):
|
||||
@login_required
|
||||
@user_has_case_view_access
|
||||
def case_displaysets(request, pk):
|
||||
case = get_object_or_404(Case, pk=pk)
|
||||
"""Render the dedicated case display set workspace.
|
||||
|
||||
This is the full-page management/viewer screen used to review all display sets
|
||||
for a case and, when permitted, add/edit/delete them inline via HTMX.
|
||||
|
||||
Query params:
|
||||
- ``displayset``: highlight/select a specific display set row.
|
||||
- ``edit``: when truthy and ``displayset`` is provided, auto-open that row's
|
||||
inline edit form on page load.
|
||||
"""
|
||||
case = get_object_or_404(Case, pk=pk)
|
||||
can_edit = case.check_user_can_edit(request.user)
|
||||
|
||||
# Get displayset id from GET parameter
|
||||
displayset_id = request.GET.get("displayset")
|
||||
edit = request.GET.get("edit", False)
|
||||
selected_displayset = None
|
||||
@@ -657,6 +665,11 @@ def api_tokens_page(request):
|
||||
@user_has_case_view_access
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def case_displaysets_add(request, pk):
|
||||
"""Create a display set for a case and return HTMX fragments.
|
||||
|
||||
GET returns a blank display set form fragment.
|
||||
POST validates and creates the display set, then returns the rendered list row.
|
||||
"""
|
||||
case = get_object_or_404(Case, pk=pk)
|
||||
form = CaseDisplaySetForm(request.POST or None)
|
||||
if request.method == "POST":
|
||||
@@ -683,11 +696,16 @@ def case_displaysets_add(request, pk):
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def case_displaysets_edit(request, pk):
|
||||
"""Edit a single display set via HTMX.
|
||||
|
||||
This endpoint is fragment-only and is intended to be called from the dedicated
|
||||
``case_displaysets`` page inline editor, not linked as a standalone page.
|
||||
"""
|
||||
ds = get_object_or_404(CaseDisplaySet, pk=pk)
|
||||
|
||||
case = ds.case
|
||||
if not case.check_user_can_edit(request.user):
|
||||
return HttpResponse("You do not have permission to delete this display set.")
|
||||
return HttpResponse("You do not have permission to edit this display set.")
|
||||
form = CaseDisplaySetForm(request.POST or None, instance=ds)
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
@@ -706,17 +724,33 @@ def case_displaysets_edit(request, pk):
|
||||
html = render_to_string("atlas/partials/displayset_form.html", {"form": form, "case": case}, request=request)
|
||||
return HttpResponse(html)
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def case_displaysets_detail(request, pk):
|
||||
"""Redirect to the case display set workspace focused on one display set.
|
||||
|
||||
Used as the canonical navigation target from non-management pages.
|
||||
It redirects to ``case_displaysets`` with ``displayset=<pk>`` and forwards
|
||||
optional ``edit`` intent (for example ``?edit=1``) so the target page can
|
||||
auto-open inline editing for permitted users.
|
||||
"""
|
||||
displayset = get_object_or_404(CaseDisplaySet, pk=pk)
|
||||
case = displayset.case
|
||||
# Redirect to the case_displaysets page with ?displayset=<pk>
|
||||
return redirect(f"{reverse('atlas:case_displaysets', args=[case.pk])}?displayset={pk}")
|
||||
edit = request.GET.get("edit")
|
||||
|
||||
redirect_url = f"{reverse('atlas:case_displaysets', args=[case.pk])}?displayset={pk}"
|
||||
if edit:
|
||||
redirect_url = f"{redirect_url}&edit={edit}"
|
||||
return redirect(redirect_url)
|
||||
|
||||
|
||||
@login_required
|
||||
def case_displaysets_modal(request, pk):
|
||||
"""Return HTML fragment for a CaseDisplaySet to be shown inside a modal."""
|
||||
"""Return a display set detail fragment for modal/overlay viewers.
|
||||
|
||||
This supports pages that do not have the full ``case_displaysets`` workspace
|
||||
open but still need to inspect a display set in-place.
|
||||
"""
|
||||
displayset = get_object_or_404(CaseDisplaySet, pk=pk)
|
||||
can_edit = False
|
||||
try:
|
||||
@@ -727,11 +761,12 @@ def case_displaysets_modal(request, pk):
|
||||
|
||||
|
||||
def displayset_preview(request, pk):
|
||||
"""Return a compact HTML fragment summarising a CaseDisplaySet for popups/modals."""
|
||||
"""Return a compact display set preview fragment for lightweight popups."""
|
||||
displayset = get_object_or_404(CaseDisplaySet, pk=pk)
|
||||
return render(request, 'atlas/partials/_displayset_preview.html', {'ds': displayset})
|
||||
|
||||
def case_displaysets_delete(request, pk):
|
||||
"""Delete a display set and return a replacement row fragment/message for HTMX."""
|
||||
try:
|
||||
displayset = CaseDisplaySet.objects.get(pk=pk)
|
||||
except CaseDisplaySet.DoesNotExist:
|
||||
|
||||
Reference in New Issue
Block a user