From 4bec329aff11cc159d2b109b1c75c56929c164a5 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 15 Dec 2025 09:53:19 +0000 Subject: [PATCH] Implement resource search functionality and enhance resource management in case details --- atlas/templates/atlas/case_display_block.html | 48 +++- .../atlas/partials/case_resources_list.html | 19 ++ .../atlas/partials/resource_media.html | 238 +++++++++--------- .../resource_search_widget_results.html | 37 +++ atlas/templates/atlas/resource_detail.html | 2 +- atlas/urls.py | 6 + atlas/views.py | 101 +++++++- 7 files changed, 321 insertions(+), 130 deletions(-) create mode 100644 atlas/templates/atlas/partials/case_resources_list.html create mode 100644 atlas/templates/atlas/partials/resource_search_widget_results.html diff --git a/atlas/templates/atlas/case_display_block.html b/atlas/templates/atlas/case_display_block.html index 11943278..b10bb4da 100755 --- a/atlas/templates/atlas/case_display_block.html +++ b/atlas/templates/atlas/case_display_block.html @@ -201,15 +201,45 @@ -

Resource
{% for caseresource in case.caseresource_set.all %} - {{caseresource.resource.name}} - {% if caseresource.pre_review %} - (pre) - {% else %} - (post) - {% endif %} - {% endfor %} -

+
+
Resources
+ +
+
+ +
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+ +
+ {% include 'atlas/partials/case_resources_list.html' with case=case %} +
+
Series:
diff --git a/atlas/templates/atlas/partials/case_resources_list.html b/atlas/templates/atlas/partials/case_resources_list.html new file mode 100644 index 00000000..8b82fca7 --- /dev/null +++ b/atlas/templates/atlas/partials/case_resources_list.html @@ -0,0 +1,19 @@ +
+ {% if case.caseresource_set.exists %} +
    + {% for cr in case.caseresource_set.all %} +
  • +
    + {{ cr.resource.name }} + {% if cr.pre_review %}pre{% endif %} +
    +
    + +
    +
  • + {% endfor %} +
+ {% else %} +
No resources attached to this case.
+ {% endif %} +
diff --git a/atlas/templates/atlas/partials/resource_media.html b/atlas/templates/atlas/partials/resource_media.html index d64a2843..1c63469a 100644 --- a/atlas/templates/atlas/partials/resource_media.html +++ b/atlas/templates/atlas/partials/resource_media.html @@ -5,142 +5,142 @@
diff --git a/atlas/templates/atlas/partials/resource_search_widget_results.html b/atlas/templates/atlas/partials/resource_search_widget_results.html new file mode 100644 index 00000000..9c19cd24 --- /dev/null +++ b/atlas/templates/atlas/partials/resource_search_widget_results.html @@ -0,0 +1,37 @@ +{% if results %} + +
+ {{ results|length }} results +
+{% else %} +
No resources found
+{% endif %} diff --git a/atlas/templates/atlas/resource_detail.html b/atlas/templates/atlas/resource_detail.html index 8f604856..a52dd419 100755 --- a/atlas/templates/atlas/resource_detail.html +++ b/atlas/templates/atlas/resource_detail.html @@ -45,7 +45,7 @@
Download {% if resource.file.name %} - ({{ resource.file.name|truncatechars:40 }}) + ({{ resource.file.name|truncatechars:40 }}) {% endif %}
{% endif %} diff --git a/atlas/urls.py b/atlas/urls.py index 2097dc8d..54fa261c 100755 --- a/atlas/urls.py +++ b/atlas/urls.py @@ -49,6 +49,8 @@ urlpatterns = [ path("resource//view", views.resource_view, name="resource_view"), path("resource//update", views.ResourceUpdate.as_view(), name="resource_update"), path("resource//delete", views.ResourceDelete.as_view(), name="resource_delete"), + path("resource-search-widget/", views.resource_search_widget, name="resource_search_widget"), + path( "resource/create", views.ResourceCreate.as_view(), @@ -177,6 +179,10 @@ urlpatterns = [ views.collection_case_details, name="collection_case_details", ), + + # HTMX endpoints for attaching/detaching resources to a case + path("case//attach-resource/", views.case_attach_resource, name="case_attach_resource"), + path("case//detach-resource/", views.case_detach_resource, name="case_detach_resource"), # Backwards-compatible redirect from legacy case_id-style URLs path( "collection//case/id//details", diff --git a/atlas/views.py b/atlas/views.py index 18b269a6..cf3f9681 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -46,7 +46,7 @@ from django.http import Http404, JsonResponse from django.http import HttpResponseRedirect, HttpResponse 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 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_case_author": is_case_author, "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()) +@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): series = get_object_or_404(Series, pk=pk)