update viva cases view

This commit is contained in:
Ross
2026-06-01 11:54:50 +01:00
parent 0412aa82e4
commit 74f1ad4b2c
4 changed files with 250 additions and 189 deletions
+114 -85
View File
@@ -1,140 +1,169 @@
{% extends 'atlas/exams.html' %} {% extends 'atlas/exams.html' %}
{% block content %} {% block content %}
<details class="help-text"> <div class="container py-3 viva-local-page">
<summary><i class="bi bi-info-circle"></i> Help</summary> <div class="card border-secondary-subtle shadow-sm mb-3">
<p>This set up is designed to allow you to project/share cases in a seperate window whilst controlling/tracking their display from this page.</p> <div class="card-body d-flex flex-wrap gap-2 justify-content-between align-items-center">
<p>First you will need to open the linked viewer window by clicking the 'Open Viewer Local' button. This will open a new window/tab with the viewer in it. You will probably need to detach this from your current window.</p> <div>
<p>If you are sharing the window you can maximise the screen area by hiding the tab/bookmark bar and the address bar. In Microsoft Edge this can be done by installing as an app, in Chrome you can use an extension such as <a href='https://chromewebstore.google.com/detail/ncppfjladdkdaemaghochfikpmghbcpc'>Open-as-Popup</a>.</p> <h1 class="h4 mb-1">Viva Cases (Local Viewer)</h1>
<p>Click on the 'Load' button to open the case in a the linked window. Loading subsequent cases/series will replace the first case in the window.</p> <p class="text-muted mb-0">Load full cases, single series, resources, or display sets into a linked local DV3D window.</p>
<p>It is also possible to open a single series by clicking on the 'Load' button next to the individual series.</p> </div>
<p>Once opened the case details will be show on the right hand side of the page.</p> <div class="d-flex flex-wrap gap-2">
<button id="open-viewer-local" class="btn btn-primary">Open Viewer Local</button>
<button id="view-series" class="btn btn-outline-secondary">Toggle series</button>
<button id="toggle-edit-mode" class="btn btn-outline-warning">Enable Edit Mode</button>
</div>
</div>
</div>
<div class="mb-2"> <details class="help-text card border-secondary-subtle shadow-sm mb-3">
<button id="toggle-edit-mode" class="btn btn-warning ms-2">Enable Edit Mode</button> <summary class="card-header bg-body-secondary d-flex align-items-center gap-2"><i class="bi bi-info-circle"></i> Quick help</summary>
<div class="card-body">
<p class="mb-2">Open the linked viewer first, then use the Load controls on this page. Loading a new case/series replaces the current viewer content.</p>
<p class="mb-0">Use Edit Mode to reveal direct links for case, series, and display-set editing without leaving the viva workflow.</p>
</div> </div>
</details> </details>
<div class="container">
<div class="row"> <div class="row g-3">
<div class="col-sm"> <div class="col-lg-7">
<h3>Cases</h3> <h2 class="h5 mb-2">Cases</h2>
<button id="open-viewer-local" >Open Viewer Local</button> <ol class="sortable list-unstyled d-flex flex-column gap-3">
<button id="view-series" class="text-dark border-dark" >Toggle series</button>
<ol id="" class="sortable">
{% for casedetail in casedetails %} {% for casedetail in casedetails %}
{% with case=casedetail.case %} {% with case=casedetail.case %}
<li data-question_pk={{case.pk}}> <li data-question_pk="{{ case.pk }}">
<div class="case-item" data-title="{{case.title}}" data-case={{case.pk}} data-casejson='{{case.get_viva_details_json}}' data-images='{{case.get_series_images_nested}}' data-viewerstate={{casedetail.default_viewerstate_string}}> <article class="card border-secondary-subtle shadow-sm case-item"
Case {{forloop.counter}}: {{case.title}} &nbsp; data-title="{{ case.title }}"
<button class="open-case-local no-padding" data-target=case.pk data-type="case">Load</button> data-case="{{ case.pk }}"
<!-- Edit mode link for case --> data-casejson="{{ case.viva_details_json|escapejs }}"
<button class="view-link-case btn btn-outline-primary btn-sm ms-2" style="display:none;"> data-images="{{ case.series_images_nested_json|escapejs }}"
data-viewerstate="{{ casedetail.default_viewerstate_string|escapejs }}">
<header class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<span class="badge bg-primary me-2">Case {{ forloop.counter }}</span>
<span class="fw-semibold">{{ case.title }}</span>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<button class="open-case-local btn btn-sm btn-success" data-target="{{ case.pk }}" data-type="case">Load Case</button>
<button class="view-link-case btn btn-outline-primary btn-sm" style="display:none;">
<a href="{% url 'atlas:case_detail' case.pk %}" style="color: inherit; text-decoration: none;">View Case</a> <a href="{% url 'atlas:case_detail' case.pk %}" style="color: inherit; text-decoration: none;">View Case</a>
</button> </button>
<button class="edit-link-case btn btn-outline-warning btn-sm ms-2" style="display:none;"> <button class="edit-link-case btn btn-outline-warning btn-sm" style="display:none;">
<a href="{% url 'atlas:case_update' case.pk %}" style="color: inherit; text-decoration: none;">Edit Case</a> <a href="{% url 'atlas:case_update' case.pk %}" style="color: inherit; text-decoration: none;">Edit Case</a>
</button> </button>
<br /> </div>
<div class="pre-whitespace multi-image-block"> </header>
<details class="series-detail"><summary>Series:</summary> <div class="card-body d-flex flex-column gap-3">
{% for series in case.series.all %} <details class="series-detail">
<span class="series-block"> <summary class="fw-semibold">Series</summary>
<span> <div class="mt-2 d-flex flex-column gap-2">
<span class="series-block-series-number">Series {{ forloop.counter }}:</span><br> {% for series in case.prefetched_series %}
test <div class="border rounded p-2 bg-body-tertiary">
{{series.get_block}} <div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
<br /> <div class="small">
<button class="open-series-local no-padding text-secondary border-secondary" <div class="fw-semibold">Series {{ forloop.counter }}</div>
<div class="text-muted">
{% if series.modality %}{{ series.modality }}{% endif %}
{% if series.examination %} · {{ series.examination }}{% endif %}
{% if series.plane %} · {{ series.plane }}{% endif %}
</div>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<button class="open-series-local btn btn-sm btn-outline-success"
data-type="series" data-type="series"
data-series="{{ forloop.counter }}" data-series="{{ forloop.counter }}"
data-case="{{ case.pk }}" data-case="{{ case.pk }}"
data-target="{{ series.pk }}"> data-target="{{ series.pk }}">
Load Load Series
</button> </button>
<!-- Edit mode link for series --> <button class="edit-link-series btn btn-outline-warning btn-sm" style="display:none;">
<button class="edit-link-series btn btn-outline-warning btn-sm ms-2" style="display:none;">
<a href="{% url 'atlas:series_update' series.pk %}" style="color: inherit; text-decoration: none;">Edit Series</a> <a href="{% url 'atlas:series_update' series.pk %}" style="color: inherit; text-decoration: none;">Edit Series</a>
</button> </button>
<br> </div>
</span> </div>
</span> </div>
{% empty %}
<div class="text-muted small">No series linked.</div>
{% endfor %} {% endfor %}
</div>
</details> </details>
</div>
{# Hidden per-case question snippet to inject when case is loaded #} {% if case.prefetched_case_resources %}
<div class="question-block-snippet" style="display:none;">
{% include 'atlas/partials/collection_question_block.html' with casedetail=casedetail %}
</div>
{# Attached resources for this case: allow loading into the shared viewer #}
{% if case.caseresource_set.exists %}
<div class="mt-2">
<strong>Resources:</strong>
<div class="d-flex flex-column gap-1">
{% for cr in case.caseresource_set.all %}
{% with res=cr.resource %}
<div> <div>
<span class="me-2">{{ res.name }}</span> <div class="fw-semibold mb-1">Resources</div>
<button class="open-resource btn btn-sm btn-outline-primary ms-1" data-src="{% if res.file %}{{ res.file.url }}{% else %}{{ res.url }}{% endif %}" data-title="{{ res.name|escapejs }}">Load</button> <div class="d-flex flex-column gap-1">
{% for cr in case.prefetched_case_resources %}
{% with res=cr.resource %}
<div class="d-flex flex-wrap align-items-center gap-2 small">
<span>{{ res.name }}</span>
<button class="open-resource btn btn-sm btn-outline-primary" data-src="{% if res.file %}{{ res.file.url }}{% else %}{{ res.url }}{% endif %}" data-title="{{ res.name|escapejs }}">Load</button>
</div> </div>
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if case.display_sets.all %}
<details class="displayset-detail"><summary>Display Sets:</summary> {% if case.prefetched_display_sets %}
{% for ds in case.display_sets.all %} <details class="displayset-detail">
<div class="displayset-block"> <summary class="fw-semibold">Display Sets</summary>
<span> <div class="mt-2 d-flex flex-column gap-2">
<b>{{ ds.name }}</b> {% for ds in case.prefetched_display_sets %}
{% if ds.description %} <div class="border rounded p-2 bg-body-tertiary d-flex flex-wrap justify-content-between align-items-center gap-2">
<span class="text-muted">({{ ds.description }})</span> <div class="small">
{% endif %} <div class="fw-semibold">{{ ds.name }}</div>
<button class="open-displayset no-padding text-secondary border-secondary" {% if ds.description %}<div class="text-muted">{{ ds.description }}</div>{% endif %}
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<button class="open-displayset btn btn-sm btn-outline-info"
data-type="displayset" data-type="displayset"
data-displayset="{{ ds.pk }}" data-displayset="{{ ds.pk }}"
data-case="{{ case.pk }}" data-case="{{ case.pk }}"
data-title="{{ ds.name }}" data-title="{{ ds.name }}"
data-casejson='{{case.get_viva_details_json}}' data-casejson="{{ case.viva_details_json|escapejs }}"
data-images='{{case.get_series_images_nested}}' data-images="{{ case.series_images_nested_json|escapejs }}"
data-viewerstate={{ds.viewerstate_string}} data-viewerstate="{{ ds.viewerstate_string|escapejs }}"
data-annotations='{{ds.annotations_string}}' data-annotations="{{ ds.annotations_string|escapejs }}">
>
Load Display Set Load Display Set
</button> </button>
<!-- Edit mode link for display set --> <button class="edit-link-displayset btn btn-outline-warning btn-sm" style="display:none;">
<button class="edit-link-displayset btn btn-outline-warning btn-sm ms-2" style="display:none;">
<a href="{% url 'atlas:case_displaysets' case.pk %}?displayset={{ ds.pk }}&edit=true" style="color: inherit; text-decoration: none;">Edit Display Set</a> <a href="{% url 'atlas:case_displaysets' case.pk %}?displayset={{ ds.pk }}&edit=true" style="color: inherit; text-decoration: none;">Edit Display Set</a>
</button> </button>
</span>
</div> </div>
{% empty %} </div>
<span class="text-muted">No display sets for this case.</span>
{% endfor %} {% endfor %}
</div>
</details> </details>
{% endif %} {% endif %}
<div class="question-block-snippet" style="display:none;">
{% include 'atlas/partials/collection_question_block.html' with casedetail=casedetail %}
</div> </div>
</div>
</article>
</li> </li>
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}
</ol> </ol>
</div> </div>
<div class="col-sm">
<div class="sticky-top"> <div class="col-lg-5">
<h3>Current Case</h3> <div class="sticky-top pt-2">
<div id="case-details"> <div class="card border-secondary-subtle shadow-sm">
<div id="loading-case">Waiting for case to load in linked window.<br/> If you are seeing this message you probably need to open the viewer window and reload the case.</div> <div class="card-header"><h2 class="h5 mb-0">Current Case</h2></div>
<div id="current-case-title">None</div> <div class="card-body" id="case-details">
<div id="current-case-series"></div> <div id="loading-case" class="alert alert-warning py-2" style="display:none;">Waiting for case to load in linked window.</div>
<div id="current-case-history"></div> <div id="current-case-title" class="mb-2">None</div>
<div id="current-case-discussion"></div> <div id="current-case-series" class="mb-2"></div>
<div id="current-case-report"></div> <div id="current-case-history" class="mb-2"></div>
<div id="current-case-discussion" class="mb-2"></div>
<div id="current-case-report" class="mb-2"></div>
<div id="current-case-questions" class="mt-3"></div> <div id="current-case-questions" class="mt-3"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
{% endblock %} {% endblock %}
+5
View File
@@ -38,6 +38,11 @@
<h5>Viva</h5> <h5>Viva</h5>
<p>A Viva collection is intended for oral examination practice. Cases in this collection can be used to simulate viva-style assessments. These are designed to be presented by the "examiner" and features a number of features to make this as easy as possible.</p> <p>A Viva collection is intended for oral examination practice. Cases in this collection can be used to simulate viva-style assessments. These are designed to be presented by the "examiner" and features a number of features to make this as easy as possible.</p>
<h5>Viva (Local Viewer)</h5>
<p>The local viva view opens a linked DV3D viewer in a separate window and lets you load complete cases, individual series, resources, and display sets without leaving the control page.</p>
<p>Use "Open Viewer Local" first, then use the Load buttons in the cases panel. The right-hand current-case panel tracks the active case details and injected question block for discussion.</p>
<p>Use "Enable Edit Mode" in the local viva page when you need quick access to case, series, or display-set edit links during preparation.</p>
<h2>Uploading/creating cases</h2> <h2>Uploading/creating cases</h2>
<p>If set up, the easiest way to upload cases is to use the uploader tool. This can export, anonnyimize and upload cases from Insignia Insight, however it may need setting up manually. If this is not possible you can upload files directly to the site.</p> <p>If set up, the easiest way to upload cases is to use the uploader tool. This can export, anonnyimize and upload cases from Insignia Insight, however it may need setting up manually. If this is not possible you can upload files directly to the site.</p>
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
+38 -11
View File
@@ -5769,28 +5769,55 @@ def collection_viva_local(request, pk):
from django.db.models import Prefetch as _Prefetch from django.db.models import Prefetch as _Prefetch
series_images_qs = SeriesImage.objects.filter(removed=False).order_by("position") series_images_qs = (
series_qs = Series.objects.order_by("seriesdetail__sort_order").prefetch_related( SeriesImage.objects
_Prefetch("images", queryset=series_images_qs) .filter(removed=False)
.only("id", "series_id", "image", "removed", "position")
.order_by("position")
)
series_qs = (
Series.objects
.select_related("modality", "examination", "plane", "contrast")
.order_by("seriesdetail__sort_order")
.prefetch_related(_Prefetch("images", queryset=series_images_qs, to_attr="prefetched_images"))
) )
casedetails = ( casedetails = list(
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("case__series", queryset=series_qs), _Prefetch("case__series", queryset=series_qs, to_attr="prefetched_series"),
_Prefetch("case__series__modality"), _Prefetch("case__caseresource_set", queryset=CaseResource.objects.select_related("resource"), to_attr="prefetched_case_resources"),
_Prefetch("case__series__examination"), _Prefetch("case__display_sets", queryset=CaseDisplaySet.objects.all(), to_attr="prefetched_display_sets"),
_Prefetch("case__series__plane"),
_Prefetch("case__series__contrast"),
_Prefetch("case__caseresource_set"),
_Prefetch("case__display_sets"),
) )
.annotate(series_count=Count("case__seriesdetail")) .annotate(series_count=Count("case__seriesdetail"))
) )
for casedetail in casedetails:
case = casedetail.case
prefetched_series = getattr(case, "prefetched_series", [])
images_nested = []
for series in prefetched_series:
prefetched_images = getattr(series, "prefetched_images", [])
images_nested.append([
img.image.url
for img in prefetched_images
if not getattr(img, "removed", False) and getattr(img, "image", None)
])
case.viva_details_json = json.dumps(
{
"title": case.title,
"description": case.description,
"history": case.history,
"discussion": case.discussion,
"report": case.report,
}
)
case.series_images_nested_json = json.dumps(images_nested)
return render( return render(
request, request,
"atlas/collection_viva_local.html", "atlas/collection_viva_local.html",
File diff suppressed because one or more lines are too long