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) #}
|
{# 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>
|
<div>
|
||||||
<h5 class="mt-3">Case Display Sets</h5>
|
<h5 class="mt-3">Case Display Sets</h5>
|
||||||
<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" id="displayset-row-{{ ds.pk }}">
|
||||||
<b>Name: {{ ds.name }}</b>
|
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||||
{% if ds.description %}
|
<div>
|
||||||
<span class="text-muted">({{ ds.description }})</span>
|
<b>{{ ds.name }}</b>
|
||||||
{% endif %}
|
{% if ds.description %}
|
||||||
<span>
|
<span class="text-muted ms-1">({{ ds.description }})</span>
|
||||||
<a href="{% url 'atlas:case_displaysets_detail' ds.pk %}">View Display Set</a>
|
{% endif %}
|
||||||
<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>
|
</div>
|
||||||
</span>
|
<div class="d-flex gap-2 flex-shrink-0">
|
||||||
<div>
|
{% 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>
|
<strong>Findings:</strong>
|
||||||
{% if ds.findings.all %}
|
{% if ds.findings.all %}
|
||||||
<ul>
|
<ul class="mb-1">
|
||||||
{% for finding in ds.findings.all %}
|
{% for finding in ds.findings.all %}
|
||||||
<li>{{ finding.get_link }}</li>
|
<li>{{ finding.get_link }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -27,7 +55,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<strong>Structures:</strong>
|
<strong>Structures:</strong>
|
||||||
{% if ds.structures.all %}
|
{% if ds.structures.all %}
|
||||||
<ul>
|
<ul class="mb-1">
|
||||||
{% for structure in ds.structures.all %}
|
{% for structure in ds.structures.all %}
|
||||||
<li>{{ structure.get_link }}</li>
|
<li>{{ structure.get_link }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -39,7 +67,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<strong>Conditions:</strong>
|
<strong>Conditions:</strong>
|
||||||
{% if ds.conditions.all %}
|
{% if ds.conditions.all %}
|
||||||
<ul>
|
<ul class="mb-1">
|
||||||
{% for condition in ds.conditions.all %}
|
{% for condition in ds.conditions.all %}
|
||||||
<li>{{ condition.get_link }}</li>
|
<li>{{ condition.get_link }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -48,25 +76,64 @@
|
|||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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>
|
</li>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="displayset-empty-state">
|
<li class="list-group-item text-muted">
|
||||||
<span class="text-muted">No display sets for this case.</span>
|
No display sets for this case.
|
||||||
{% if can_edit %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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
|
@login_required
|
||||||
@user_has_case_view_access
|
@user_has_case_view_access
|
||||||
def case_displaysets(request, pk):
|
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)
|
can_edit = case.check_user_can_edit(request.user)
|
||||||
|
|
||||||
# Get displayset id from GET parameter
|
|
||||||
displayset_id = request.GET.get("displayset")
|
displayset_id = request.GET.get("displayset")
|
||||||
edit = request.GET.get("edit", False)
|
edit = request.GET.get("edit", False)
|
||||||
selected_displayset = None
|
selected_displayset = None
|
||||||
@@ -657,6 +665,11 @@ def api_tokens_page(request):
|
|||||||
@user_has_case_view_access
|
@user_has_case_view_access
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def case_displaysets_add(request, pk):
|
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)
|
case = get_object_or_404(Case, pk=pk)
|
||||||
form = CaseDisplaySetForm(request.POST or None)
|
form = CaseDisplaySetForm(request.POST or None)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -683,11 +696,16 @@ def case_displaysets_add(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def case_displaysets_edit(request, pk):
|
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)
|
ds = get_object_or_404(CaseDisplaySet, pk=pk)
|
||||||
|
|
||||||
case = ds.case
|
case = ds.case
|
||||||
if not case.check_user_can_edit(request.user):
|
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)
|
form = CaseDisplaySetForm(request.POST or None, instance=ds)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if form.is_valid():
|
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)
|
html = render_to_string("atlas/partials/displayset_form.html", {"form": form, "case": case}, request=request)
|
||||||
return HttpResponse(html)
|
return HttpResponse(html)
|
||||||
|
|
||||||
@require_http_methods(["GET", "POST"])
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
def case_displaysets_detail(request, pk):
|
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)
|
displayset = get_object_or_404(CaseDisplaySet, pk=pk)
|
||||||
case = displayset.case
|
case = displayset.case
|
||||||
# Redirect to the case_displaysets page with ?displayset=<pk>
|
edit = request.GET.get("edit")
|
||||||
return redirect(f"{reverse('atlas:case_displaysets', args=[case.pk])}?displayset={pk}")
|
|
||||||
|
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
|
@login_required
|
||||||
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 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)
|
displayset = get_object_or_404(CaseDisplaySet, pk=pk)
|
||||||
can_edit = False
|
can_edit = False
|
||||||
try:
|
try:
|
||||||
@@ -727,11 +761,12 @@ def case_displaysets_modal(request, pk):
|
|||||||
|
|
||||||
|
|
||||||
def displayset_preview(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)
|
displayset = get_object_or_404(CaseDisplaySet, pk=pk)
|
||||||
return render(request, 'atlas/partials/_displayset_preview.html', {'ds': displayset})
|
return render(request, 'atlas/partials/_displayset_preview.html', {'ds': displayset})
|
||||||
|
|
||||||
def case_displaysets_delete(request, pk):
|
def case_displaysets_delete(request, pk):
|
||||||
|
"""Delete a display set and return a replacement row fragment/message for HTMX."""
|
||||||
try:
|
try:
|
||||||
displayset = CaseDisplaySet.objects.get(pk=pk)
|
displayset = CaseDisplaySet.objects.get(pk=pk)
|
||||||
except CaseDisplaySet.DoesNotExist:
|
except CaseDisplaySet.DoesNotExist:
|
||||||
|
|||||||
Reference in New Issue
Block a user