Implement resource search functionality and enhance resource management in case details
This commit is contained in:
@@ -201,15 +201,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="pre-whitespace"><b>Resource</b><br/>{% for caseresource in case.caseresource_set.all %}
|
<div class="mb-3">
|
||||||
<a href='{% url "atlas:resource_detail" caseresource.resource.pk %}'>{{caseresource.resource.name}}</a>
|
<h5>Resources</h5>
|
||||||
{% if caseresource.pre_review %}
|
|
||||||
<span title="This resource will be avalible to review when or taking a case">(pre)</span>
|
<div class="mb-2">
|
||||||
{% else %}
|
<form id="resource-search-form" class="row g-2" hx-get="{% url 'atlas:resource_search_widget' %}" hx-target="#resource-search-results" hx-swap="innerHTML" hx-trigger="keyup changed delay:500ms from:#resource-search-q">
|
||||||
<span title="This resource will only be available when reviewing a completed case">(post)</span>
|
<input type="hidden" name="case_id" value="{{ case.pk }}" />
|
||||||
{% endif %}
|
<div class="col-auto" style="flex: 1 1 300px;">
|
||||||
{% endfor %}
|
<input type="search" id="resource-search-q" name="q" class="form-control form-control-sm" placeholder="Search resources by name, description or URL" />
|
||||||
</p>
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select name="site" class="form-select form-select-sm">
|
||||||
|
<option value="">All sites</option>
|
||||||
|
{% for s in site_queryset %}
|
||||||
|
<option value="{{ s.pk }}">{{ s.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select name="subspecialty" class="form-select form-select-sm">
|
||||||
|
<option value="">All subspecialties</option>
|
||||||
|
{% for sp in subspecialty_queryset %}
|
||||||
|
<option value="{{ sp.pk }}">{{ sp.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" id="resource-open-access-{{ case.pk }}" name="open_access" value="1">
|
||||||
|
<label class="form-check-label small" for="resource-open-access-{{ case.pk }}">Open</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="resource-search-results" class="mt-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% include 'atlas/partials/case_resources_list.html' with case=case %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pre-whitespace multi-image-block"><b>Series:</b>
|
<div class="pre-whitespace multi-image-block"><b>Series:</b>
|
||||||
<form>
|
<form>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<div id="case-resources-list">
|
||||||
|
{% if case.caseresource_set.exists %}
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
{% for cr in case.caseresource_set.all %}
|
||||||
|
<li class="py-1 d-flex justify-content-between align-items-center" data-caseresource-id="{{ cr.id }}">
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'atlas:resource_detail' cr.resource.pk %}">{{ cr.resource.name }}</a>
|
||||||
|
{% if cr.pre_review %}<span class="badge bg-info text-dark ms-2 small">pre</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" hx-post="{% url 'atlas:case_detach_resource' pk=case.pk %}" hx-vals='{"resource_id": "{{ cr.resource.pk }}"}' hx-swap="outerHTML" hx-target="#case-resources-list">Remove</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted">No resources attached to this case.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -5,142 +5,142 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
var wrapperId = 'resource_media_wrapper_{{ resource.pk }}';
|
var wrapperId = 'resource_media_wrapper_{{ resource.pk }}';
|
||||||
if (document.getElementById(wrapperId).dataset._inited) return;
|
if (document.getElementById(wrapperId).dataset._inited) return;
|
||||||
var wrap = document.getElementById(wrapperId);
|
var wrap = document.getElementById(wrapperId);
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
wrap.dataset._inited = '1';
|
wrap.dataset._inited = '1';
|
||||||
var src = wrap.getAttribute('data-src');
|
var src = wrap.getAttribute('data-src');
|
||||||
if (!src) return;
|
if (!src) return;
|
||||||
var lower = src.toLowerCase();
|
var lower = src.toLowerCase();
|
||||||
|
|
||||||
function createImg(src) {
|
function createImg(src) {
|
||||||
var img = document.createElement('img');
|
var img = document.createElement('img');
|
||||||
img.src = src;
|
img.src = src;
|
||||||
img.className = 'img-fluid resource-media-element rounded';
|
img.className = 'img-fluid resource-media-element rounded';
|
||||||
img.style.maxHeight = '60vh';
|
img.style.maxHeight = '60vh';
|
||||||
img.style.objectFit = 'contain';
|
img.style.objectFit = 'contain';
|
||||||
img.style.cursor = 'zoom-in';
|
img.style.cursor = 'zoom-in';
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
function createVideo(src) {
|
function createVideo(src) {
|
||||||
var video = document.createElement('video');
|
var video = document.createElement('video');
|
||||||
video.className = 'resource-media-element rounded';
|
video.className = 'resource-media-element rounded';
|
||||||
video.controls = true;
|
video.controls = true;
|
||||||
video.preload = 'metadata';
|
video.preload = 'metadata';
|
||||||
video.style.maxWidth = '100%';
|
video.style.maxWidth = '100%';
|
||||||
video.style.maxHeight = '60vh';
|
video.style.maxHeight = '60vh';
|
||||||
video.style.objectFit = 'contain';
|
video.style.objectFit = 'contain';
|
||||||
var srcEl = document.createElement('source');
|
var srcEl = document.createElement('source');
|
||||||
srcEl.src = src;
|
srcEl.src = src;
|
||||||
video.appendChild(srcEl);
|
video.appendChild(srcEl);
|
||||||
return video;
|
return video;
|
||||||
}
|
}
|
||||||
function createIframe(src, embedUrl) {
|
function createIframe(src, embedUrl) {
|
||||||
var iframe = document.createElement('iframe');
|
var iframe = document.createElement('iframe');
|
||||||
iframe.src = embedUrl || src;
|
iframe.src = embedUrl || src;
|
||||||
iframe.setAttribute('frameborder','0');
|
iframe.setAttribute('frameborder','0');
|
||||||
iframe.setAttribute('allowfullscreen','');
|
iframe.setAttribute('allowfullscreen','');
|
||||||
iframe.className = 'resource-media-element rounded';
|
iframe.className = 'resource-media-element rounded';
|
||||||
iframe.style.width = '100%';
|
iframe.style.width = '100%';
|
||||||
iframe.style.height = '60vh';
|
iframe.style.height = '60vh';
|
||||||
return iframe;
|
return iframe;
|
||||||
}
|
}
|
||||||
|
|
||||||
var el = null;
|
var el = null;
|
||||||
// YouTube
|
// YouTube
|
||||||
if (lower.indexOf('youtube.com') !== -1 || lower.indexOf('youtu.be') !== -1) {
|
if (lower.indexOf('youtube.com') !== -1 || lower.indexOf('youtu.be') !== -1) {
|
||||||
// build embed url
|
// build embed url
|
||||||
var videoId = null;
|
var videoId = null;
|
||||||
try {
|
try {
|
||||||
var m = src.match(/(?:v=|v\/|embed\/|youtu\.be\/)([A-Za-z0-9_-]{6,})/);
|
var m = src.match(/(?:v=|v\/|embed\/|youtu\.be\/)([A-Za-z0-9_-]{6,})/);
|
||||||
if (m && m[1]) videoId = m[1];
|
if (m && m[1]) videoId = m[1];
|
||||||
} catch(e){}
|
} catch(e){}
|
||||||
var embed = videoId ? ('https://www.youtube.com/embed/' + videoId) : src;
|
var embed = videoId ? ('https://www.youtube.com/embed/' + videoId) : src;
|
||||||
el = createIframe(src, embed);
|
el = createIframe(src, embed);
|
||||||
} else if (lower.indexOf('vimeo.com') !== -1) {
|
} else if (lower.indexOf('vimeo.com') !== -1) {
|
||||||
var m2 = src.match(/vimeo\.com\/(\d+)/);
|
var m2 = src.match(/vimeo\.com\/(\d+)/);
|
||||||
var embed2 = m2 && m2[1] ? ('https://player.vimeo.com/video/' + m2[1]) : src;
|
var embed2 = m2 && m2[1] ? ('https://player.vimeo.com/video/' + m2[1]) : src;
|
||||||
el = createIframe(src, embed2);
|
el = createIframe(src, embed2);
|
||||||
} else if (lower.match(/\.(mp4|webm|ogg)(?:$|\?|#)/)) {
|
} else if (lower.match(/\.(mp4|webm|ogg)(?:$|\?|#)/)) {
|
||||||
el = createVideo(src);
|
el = createVideo(src);
|
||||||
} else if (lower.match(/\.(jpe?g|png|gif|webp|svg)(?:$|\?|#)/)) {
|
} else if (lower.match(/\.(jpe?g|png|gif|webp|svg)(?:$|\?|#)/)) {
|
||||||
el = createImg(src);
|
el = createImg(src);
|
||||||
} else {
|
} else {
|
||||||
// unknown: try image first, fallback to link
|
// unknown: try image first, fallback to link
|
||||||
el = createImg(src);
|
el = createImg(src);
|
||||||
// if image fails to load, replace with link
|
// if image fails to load, replace with link
|
||||||
el.addEventListener('error', function(){
|
el.addEventListener('error', function(){
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
a.href = src; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Open resource';
|
a.href = src; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Open resource';
|
||||||
wrap.innerHTML = ''; wrap.appendChild(a);
|
wrap.innerHTML = ''; wrap.appendChild(a);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// append preview element
|
// append preview element
|
||||||
if (el) {
|
if (el) {
|
||||||
wrap.appendChild(el);
|
wrap.appendChild(el);
|
||||||
|
|
||||||
// click -> open modal/full-size
|
// click -> open modal/full-size
|
||||||
el.addEventListener('click', function(e) {
|
el.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// create modal container if not present
|
// create modal container if not present
|
||||||
var modalId = 'resource_media_modal_{{ resource.pk }}';
|
var modalId = 'resource_media_modal_{{ resource.pk }}';
|
||||||
var existing = document.getElementById(modalId);
|
var existing = document.getElementById(modalId);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
var modalDiv = document.createElement('div');
|
var modalDiv = document.createElement('div');
|
||||||
modalDiv.className = 'modal fade resource-media-modal';
|
modalDiv.className = 'modal fade resource-media-modal';
|
||||||
modalDiv.id = modalId;
|
modalDiv.id = modalId;
|
||||||
modalDiv.tabIndex = -1;
|
modalDiv.tabIndex = -1;
|
||||||
modalDiv.innerHTML = '<div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content bg-dark"><div class="modal-body p-0 text-center"></div></div></div>';
|
modalDiv.innerHTML = '<div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content bg-dark"><div class="modal-body p-0 text-center"></div></div></div>';
|
||||||
document.body.appendChild(modalDiv);
|
document.body.appendChild(modalDiv);
|
||||||
}
|
}
|
||||||
var modalEl = document.getElementById(modalId);
|
var modalEl = document.getElementById(modalId);
|
||||||
var body = modalEl.querySelector('.modal-body');
|
var body = modalEl.querySelector('.modal-body');
|
||||||
body.innerHTML = '';
|
body.innerHTML = '';
|
||||||
|
|
||||||
// create full-size node depending on type
|
// create full-size node depending on type
|
||||||
if (el.tagName.toLowerCase() === 'img') {
|
if (el.tagName.toLowerCase() === 'img') {
|
||||||
var big = document.createElement('img');
|
var big = document.createElement('img');
|
||||||
big.src = src;
|
big.src = src;
|
||||||
big.style.maxWidth = '95vw';
|
big.style.maxWidth = '95vw';
|
||||||
big.style.maxHeight = '95vh';
|
big.style.maxHeight = '95vh';
|
||||||
big.style.objectFit = 'contain';
|
big.style.objectFit = 'contain';
|
||||||
big.className = 'img-fluid';
|
big.className = 'img-fluid';
|
||||||
body.appendChild(big);
|
body.appendChild(big);
|
||||||
} else if (el.tagName.toLowerCase() === 'video') {
|
} else if (el.tagName.toLowerCase() === 'video') {
|
||||||
var bigv = document.createElement('video');
|
var bigv = document.createElement('video');
|
||||||
bigv.controls = true; bigv.autoplay = false; bigv.style.maxWidth = '95vw'; bigv.style.maxHeight = '95vh'; bigv.style.objectFit = 'contain'; bigv.className = 'w-100';
|
bigv.controls = true; bigv.autoplay = false; bigv.style.maxWidth = '95vw'; bigv.style.maxHeight = '95vh'; bigv.style.objectFit = 'contain'; bigv.className = 'w-100';
|
||||||
var s = document.createElement('source'); s.src = src; bigv.appendChild(s);
|
var s = document.createElement('source'); s.src = src; bigv.appendChild(s);
|
||||||
body.appendChild(bigv);
|
body.appendChild(bigv);
|
||||||
} else if (el.tagName.toLowerCase() === 'iframe') {
|
} else if (el.tagName.toLowerCase() === 'iframe') {
|
||||||
var frame = document.createElement('iframe');
|
var frame = document.createElement('iframe');
|
||||||
frame.src = el.src;
|
frame.src = el.src;
|
||||||
frame.setAttribute('frameborder','0');
|
frame.setAttribute('frameborder','0');
|
||||||
frame.setAttribute('allowfullscreen','');
|
frame.setAttribute('allowfullscreen','');
|
||||||
frame.style.width = '95vw'; frame.style.height = '80vh';
|
frame.style.width = '95vw'; frame.style.height = '80vh';
|
||||||
body.appendChild(frame);
|
body.appendChild(frame);
|
||||||
} else {
|
} else {
|
||||||
var a = document.createElement('a'); a.href = src; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Open resource'; body.appendChild(a);
|
var a = document.createElement('a'); a.href = src; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Open resource'; body.appendChild(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
// show modal using Bootstrap if available
|
// show modal using Bootstrap if available
|
||||||
try {
|
try {
|
||||||
var bsModal = new bootstrap.Modal(modalEl);
|
var bsModal = new bootstrap.Modal(modalEl);
|
||||||
bsModal.show();
|
bsModal.show();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// fallback: open in new tab
|
// fallback: open in new tab
|
||||||
window.open(src, '_blank', 'noopener');
|
window.open(src, '_blank', 'noopener');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.resource-media-wrapper .resource-media-element { display: inline-block; }
|
.resource-media-wrapper .resource-media-element { display: inline-block; }
|
||||||
.resource-media-wrapper img.resource-media-element { max-width: 100%; height: auto; }
|
.resource-media-wrapper img.resource-media-element { max-width: 100%; height: auto; }
|
||||||
.resource-media-wrapper video.resource-media-element { max-width: 100%; height: auto; }
|
.resource-media-wrapper video.resource-media-element { max-width: 100%; height: auto; }
|
||||||
.resource-media-modal .modal-content { background: rgba(0,0,0,0.95); }
|
.resource-media-modal .modal-content { background: rgba(0,0,0,0.95); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{% if results %}
|
||||||
|
<ul class="list-unstyled mb-0" id="resource_search_results_list_{{ field }}">
|
||||||
|
{% for item in results %}
|
||||||
|
<li class="py-1 d-flex justify-content-between align-items-center" data-resource-id="{{ item.id }}">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<a href="{% url 'atlas:resource_detail' item.pk %}" target="_blank">{{ item.name|escape }}</a>
|
||||||
|
<div class="small text-muted">{{ item.get_display|safe }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-success resource-attach-btn"
|
||||||
|
hx-post="{% url 'atlas:case_attach_resource' pk=case_id %}"
|
||||||
|
hx-vals='{"resource_id": "{{ item.id }}", "pre_review": "0"}'
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target="#case-resources-list">
|
||||||
|
Attach
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-success resource-attach-pre-btn"
|
||||||
|
hx-post="{% url 'atlas:case_attach_resource' pk=case_id %}"
|
||||||
|
hx-vals='{"resource_id": "{{ item.id }}", "pre_review": "1"}'
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target="#case-resources-list">
|
||||||
|
Attach (pre)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<div class="mt-2 text-end">
|
||||||
|
<small class="text-muted">{{ results|length }} results</small>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div><em>No resources found</em></div>
|
||||||
|
{% endif %}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<dd class="col-sm-9">
|
<dd class="col-sm-9">
|
||||||
<a href="{{ resource.file.url }}" target="_blank" rel="noopener">Download</a>
|
<a href="{{ resource.file.url }}" target="_blank" rel="noopener">Download</a>
|
||||||
{% if resource.file.name %}
|
{% if resource.file.name %}
|
||||||
<small class="text-muted ms-2">({{ resource.file.name|truncatechars:40 }})</small>
|
<small class="text-muted ms-2">({{ resource.file.name|truncatechars:40 }})</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ urlpatterns = [
|
|||||||
path("resource/<int:pk>/view", views.resource_view, name="resource_view"),
|
path("resource/<int:pk>/view", views.resource_view, name="resource_view"),
|
||||||
path("resource/<int:pk>/update", views.ResourceUpdate.as_view(), name="resource_update"),
|
path("resource/<int:pk>/update", views.ResourceUpdate.as_view(), name="resource_update"),
|
||||||
path("resource/<int:pk>/delete", views.ResourceDelete.as_view(), name="resource_delete"),
|
path("resource/<int:pk>/delete", views.ResourceDelete.as_view(), name="resource_delete"),
|
||||||
|
path("resource-search-widget/", views.resource_search_widget, name="resource_search_widget"),
|
||||||
|
|
||||||
path(
|
path(
|
||||||
"resource/create",
|
"resource/create",
|
||||||
views.ResourceCreate.as_view(),
|
views.ResourceCreate.as_view(),
|
||||||
@@ -177,6 +179,10 @@ urlpatterns = [
|
|||||||
views.collection_case_details,
|
views.collection_case_details,
|
||||||
name="collection_case_details",
|
name="collection_case_details",
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# HTMX endpoints for attaching/detaching resources to a case
|
||||||
|
path("case/<int:pk>/attach-resource/", views.case_attach_resource, name="case_attach_resource"),
|
||||||
|
path("case/<int:pk>/detach-resource/", views.case_detach_resource, name="case_detach_resource"),
|
||||||
# Backwards-compatible redirect from legacy case_id-style URLs
|
# Backwards-compatible redirect from legacy case_id-style URLs
|
||||||
path(
|
path(
|
||||||
"collection/<int:exam_id>/case/id/<int:case_id>/details",
|
"collection/<int:exam_id>/case/id/<int:case_id>/details",
|
||||||
|
|||||||
+100
-1
@@ -46,7 +46,7 @@ from django.http import Http404, JsonResponse
|
|||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from generic.models import CidUser, CidUserExam, CimarCase
|
from generic.models import CidUser, CidUserExam, CimarCase, Site
|
||||||
from atlas.helpers import get_cases_available_to_user
|
from atlas.helpers import get_cases_available_to_user
|
||||||
from rad.settings import REMOTE_URL, CIMAR_USERNAME, CIMAR_PASSWORD
|
from rad.settings import REMOTE_URL, CIMAR_USERNAME, CIMAR_PASSWORD
|
||||||
|
|
||||||
@@ -478,6 +478,9 @@ def case_detail(request, pk):
|
|||||||
"is_atlas_editor": is_atlas_editor,
|
"is_atlas_editor": is_atlas_editor,
|
||||||
"is_case_author": is_case_author,
|
"is_case_author": is_case_author,
|
||||||
"can_mark_normal": can_mark_normal,
|
"can_mark_normal": can_mark_normal,
|
||||||
|
# Provide helper lists for resource search filters
|
||||||
|
"site_queryset": Site.objects.all(),
|
||||||
|
"subspecialty_queryset": Subspecialty.objects.all(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4007,6 +4010,102 @@ def case_dicom_json(request, pk):
|
|||||||
return JsonResponse(case.get_case_dicom_json())
|
return JsonResponse(case.get_case_dicom_json())
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def resource_search_widget(request):
|
||||||
|
"""HTMX endpoint: search existing `Resource` objects with simple filters.
|
||||||
|
|
||||||
|
GET params:
|
||||||
|
- q: query string (name/description)
|
||||||
|
- site: site id (optional)
|
||||||
|
- subspecialty: subspecialty id (optional)
|
||||||
|
- open_access: '1' or '0' (optional)
|
||||||
|
|
||||||
|
Returns rendered partial `atlas/partials/resource_search_widget_results.html`.
|
||||||
|
"""
|
||||||
|
from .models import Resource
|
||||||
|
|
||||||
|
q = request.GET.get("q", "").strip()
|
||||||
|
site = request.GET.get("site")
|
||||||
|
subspecialty = request.GET.get("subspecialty")
|
||||||
|
open_access = request.GET.get("open_access")
|
||||||
|
|
||||||
|
qs = Resource.objects.all()
|
||||||
|
if q:
|
||||||
|
qs = qs.filter(Q(name__icontains=q) | Q(description__icontains=q) | Q(url__icontains=q))
|
||||||
|
if site:
|
||||||
|
try:
|
||||||
|
qs = qs.filter(sites__id=int(site))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if subspecialty:
|
||||||
|
try:
|
||||||
|
qs = qs.filter(subspecialty__id=int(subspecialty))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if open_access in ["0", "1"]:
|
||||||
|
qs = qs.filter(open_access=(open_access == "1"))
|
||||||
|
|
||||||
|
# Restrict to what the user may see: open_access or authored (unless editor)
|
||||||
|
if not (request.user.is_superuser or request.user.groups.filter(name="atlas_editor").exists()):
|
||||||
|
qs = qs.filter(Q(open_access=True) | Q(author__id=request.user.id)).distinct()
|
||||||
|
|
||||||
|
results = qs.order_by("name")[:50]
|
||||||
|
|
||||||
|
field = request.GET.get("field", "resource_search")
|
||||||
|
case_id = request.GET.get("case_id")
|
||||||
|
return render(request, "atlas/partials/resource_search_widget_results.html", {"results": results, "field": field, "case_id": case_id})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def case_attach_resource(request, pk):
|
||||||
|
"""Attach an existing resource to the case (HTMX POST).
|
||||||
|
|
||||||
|
POST params:
|
||||||
|
- resource_id: integer
|
||||||
|
- pre_review: '1' or '0' (optional)
|
||||||
|
|
||||||
|
Returns the updated `atlas/partials/case_resources_list.html` fragment.
|
||||||
|
"""
|
||||||
|
case = get_case_for_case_detail(pk)
|
||||||
|
if not case.check_user_can_edit(request.user):
|
||||||
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
|
resource_id = request.POST.get("resource_id")
|
||||||
|
pre_review = request.POST.get("pre_review", "0") == "1"
|
||||||
|
from .models import Resource, CaseResource
|
||||||
|
try:
|
||||||
|
resource = Resource.objects.get(pk=int(resource_id))
|
||||||
|
except Exception:
|
||||||
|
return HttpResponse(status=400)
|
||||||
|
|
||||||
|
# create if not exists
|
||||||
|
cr, created = CaseResource.objects.get_or_create(case=case, resource=resource, defaults={"pre_review": pre_review})
|
||||||
|
if not created:
|
||||||
|
# update flag
|
||||||
|
cr.pre_review = pre_review
|
||||||
|
cr.save()
|
||||||
|
|
||||||
|
# Render updated list
|
||||||
|
return render(request, "atlas/partials/case_resources_list.html", {"case": case})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def case_detach_resource(request, pk):
|
||||||
|
case = get_case_for_case_detail(pk)
|
||||||
|
if not case.check_user_can_edit(request.user):
|
||||||
|
return HttpResponse(status=403)
|
||||||
|
resource_id = request.POST.get("resource_id")
|
||||||
|
from .models import CaseResource
|
||||||
|
try:
|
||||||
|
cr = CaseResource.objects.filter(case=case, resource__id=int(resource_id))
|
||||||
|
cr.delete()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return render(request, "atlas/partials/case_resources_list.html", {"case": case})
|
||||||
|
|
||||||
|
|
||||||
def series_dicom_json(request, pk):
|
def series_dicom_json(request, pk):
|
||||||
series = get_object_or_404(Series, pk=pk)
|
series = get_object_or_404(Series, pk=pk)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user