update viva cases view
This commit is contained in:
@@ -1,140 +1,169 @@
|
||||
{% extends 'atlas/exams.html' %}
|
||||
|
||||
{% block content %}
|
||||
<details class="help-text">
|
||||
<summary><i class="bi bi-info-circle"></i> Help</summary>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>It is also possible to open a single series by clicking on the 'Load' button next to the individual series.</p>
|
||||
<p>Once opened the case details will be show on the right hand side of the page.</p>
|
||||
|
||||
<div class="mb-2">
|
||||
<button id="toggle-edit-mode" class="btn btn-warning ms-2">Enable Edit Mode</button>
|
||||
<div class="container py-3 viva-local-page">
|
||||
<div class="card border-secondary-subtle shadow-sm mb-3">
|
||||
<div class="card-body d-flex flex-wrap gap-2 justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="h4 mb-1">Viva Cases (Local Viewer)</h1>
|
||||
<p class="text-muted mb-0">Load full cases, single series, resources, or display sets into a linked local DV3D window.</p>
|
||||
</div>
|
||||
<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>
|
||||
</details>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<h3>Cases</h3>
|
||||
<button id="open-viewer-local" >Open Viewer Local</button>
|
||||
<button id="view-series" class="text-dark border-dark" >Toggle series</button>
|
||||
<ol id="" class="sortable">
|
||||
|
||||
<details class="help-text card border-secondary-subtle shadow-sm mb-3">
|
||||
<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>
|
||||
</details>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-7">
|
||||
<h2 class="h5 mb-2">Cases</h2>
|
||||
<ol class="sortable list-unstyled d-flex flex-column gap-3">
|
||||
{% for casedetail in casedetails %}
|
||||
{% with case=casedetail.case %}
|
||||
<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}}>
|
||||
Case {{forloop.counter}}: {{case.title}}
|
||||
<button class="open-case-local no-padding" data-target=case.pk data-type="case">Load</button>
|
||||
<!-- Edit mode link for case -->
|
||||
<button class="view-link-case btn btn-outline-primary btn-sm ms-2" style="display:none;">
|
||||
<a href="{% url 'atlas:case_detail' case.pk %}" style="color: inherit; text-decoration: none;">View Case</a>
|
||||
</button>
|
||||
<button class="edit-link-case btn btn-outline-warning btn-sm ms-2" style="display:none;">
|
||||
<a href="{% url 'atlas:case_update' case.pk %}" style="color: inherit; text-decoration: none;">Edit Case</a>
|
||||
</button>
|
||||
<br />
|
||||
<div class="pre-whitespace multi-image-block">
|
||||
<details class="series-detail"><summary>Series:</summary>
|
||||
{% for series in case.series.all %}
|
||||
<span class="series-block">
|
||||
<span>
|
||||
<span class="series-block-series-number">Series {{ forloop.counter }}:</span><br>
|
||||
test
|
||||
{{series.get_block}}
|
||||
<br />
|
||||
<button class="open-series-local no-padding text-secondary border-secondary"
|
||||
data-type="series"
|
||||
data-series="{{ forloop.counter }}"
|
||||
data-case="{{ case.pk }}"
|
||||
data-target="{{ series.pk }}">
|
||||
Load
|
||||
</button>
|
||||
<!-- Edit mode link for series -->
|
||||
<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>
|
||||
</button>
|
||||
<br>
|
||||
</span>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</details>
|
||||
</div>
|
||||
{# Hidden per-case question snippet to inject when case is loaded #}
|
||||
<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>
|
||||
<span class="me-2">{{ res.name }}</span>
|
||||
<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>
|
||||
<li data-question_pk="{{ case.pk }}">
|
||||
<article class="card border-secondary-subtle shadow-sm case-item"
|
||||
data-title="{{ case.title }}"
|
||||
data-case="{{ case.pk }}"
|
||||
data-casejson="{{ case.viva_details_json|escapejs }}"
|
||||
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>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="card-body d-flex flex-column gap-3">
|
||||
<details class="series-detail">
|
||||
<summary class="fw-semibold">Series</summary>
|
||||
<div class="mt-2 d-flex flex-column gap-2">
|
||||
{% for series in case.prefetched_series %}
|
||||
<div class="border rounded p-2 bg-body-tertiary">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div class="small">
|
||||
<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-series="{{ forloop.counter }}"
|
||||
data-case="{{ case.pk }}"
|
||||
data-target="{{ series.pk }}">
|
||||
Load Series
|
||||
</button>
|
||||
<button class="edit-link-series btn btn-outline-warning btn-sm" style="display:none;">
|
||||
<a href="{% url 'atlas:series_update' series.pk %}" style="color: inherit; text-decoration: none;">Edit Series</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-muted small">No series linked.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if case.display_sets.all %}
|
||||
<details class="displayset-detail"><summary>Display Sets:</summary>
|
||||
{% for ds in case.display_sets.all %}
|
||||
<div class="displayset-block">
|
||||
<span>
|
||||
<b>{{ ds.name }}</b>
|
||||
{% if ds.description %}
|
||||
<span class="text-muted">({{ ds.description }})</span>
|
||||
{% endif %}
|
||||
<button class="open-displayset no-padding text-secondary border-secondary"
|
||||
data-type="displayset"
|
||||
data-displayset="{{ ds.pk }}"
|
||||
data-case="{{ case.pk }}"
|
||||
data-title="{{ ds.name }}"
|
||||
data-casejson='{{case.get_viva_details_json}}'
|
||||
data-images='{{case.get_series_images_nested}}'
|
||||
data-viewerstate={{ds.viewerstate_string}}
|
||||
data-annotations='{{ds.annotations_string}}'
|
||||
>
|
||||
Load Display Set
|
||||
</button>
|
||||
<!-- Edit mode link for display set -->
|
||||
<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>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{% empty %}
|
||||
<span class="text-muted">No display sets for this case.</span>
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if case.prefetched_case_resources %}
|
||||
<div>
|
||||
<div class="fw-semibold mb-1">Resources</div>
|
||||
<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>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if case.prefetched_display_sets %}
|
||||
<details class="displayset-detail">
|
||||
<summary class="fw-semibold">Display Sets</summary>
|
||||
<div class="mt-2 d-flex flex-column gap-2">
|
||||
{% for ds in case.prefetched_display_sets %}
|
||||
<div class="border rounded p-2 bg-body-tertiary d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div class="small">
|
||||
<div class="fw-semibold">{{ ds.name }}</div>
|
||||
{% 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-displayset="{{ ds.pk }}"
|
||||
data-case="{{ case.pk }}"
|
||||
data-title="{{ ds.name }}"
|
||||
data-casejson="{{ case.viva_details_json|escapejs }}"
|
||||
data-images="{{ case.series_images_nested_json|escapejs }}"
|
||||
data-viewerstate="{{ ds.viewerstate_string|escapejs }}"
|
||||
data-annotations="{{ ds.annotations_string|escapejs }}">
|
||||
Load Display Set
|
||||
</button>
|
||||
<button class="edit-link-displayset btn btn-outline-warning btn-sm" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<div class="question-block-snippet" style="display:none;">
|
||||
{% include 'atlas/partials/collection_question_block.html' with casedetail=casedetail %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
|
||||
<div class="sticky-top">
|
||||
<h3>Current Case</h3>
|
||||
<div id="case-details">
|
||||
<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 id="current-case-title">None</div>
|
||||
<div id="current-case-series"></div>
|
||||
<div id="current-case-history"></div>
|
||||
<div id="current-case-discussion"></div>
|
||||
<div id="current-case-report"></div>
|
||||
<div id="current-case-questions" class="mt-3"></div>
|
||||
<div class="col-lg-5">
|
||||
<div class="sticky-top pt-2">
|
||||
<div class="card border-secondary-subtle shadow-sm">
|
||||
<div class="card-header"><h2 class="h5 mb-0">Current Case</h2></div>
|
||||
<div class="card-body" id="case-details">
|
||||
<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-title" class="mb-2">None</div>
|
||||
<div id="current-case-series" class="mb-2"></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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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">
|
||||
|
||||
+38
-11
@@ -5769,28 +5769,55 @@ def collection_viva_local(request, pk):
|
||||
|
||||
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)
|
||||
series_images_qs = (
|
||||
SeriesImage.objects
|
||||
.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
|
||||
.filter(collection=collection)
|
||||
.select_related("case")
|
||||
.order_by("sort_order")
|
||||
.prefetch_related(
|
||||
_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"),
|
||||
_Prefetch("case__series", queryset=series_qs, to_attr="prefetched_series"),
|
||||
_Prefetch("case__caseresource_set", queryset=CaseResource.objects.select_related("resource"), to_attr="prefetched_case_resources"),
|
||||
_Prefetch("case__display_sets", queryset=CaseDisplaySet.objects.all(), to_attr="prefetched_display_sets"),
|
||||
)
|
||||
.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(
|
||||
request,
|
||||
"atlas/collection_viva_local.html",
|
||||
|
||||
+61
-61
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user