diff --git a/atlas/templates/atlas/uploads_hash_search.html b/atlas/templates/atlas/uploads_hash_search.html
new file mode 100644
index 00000000..bbdbd0dc
--- /dev/null
+++ b/atlas/templates/atlas/uploads_hash_search.html
@@ -0,0 +1,89 @@
+{% extends 'atlas/base.html' %}
+
+{% block content %}
+
Search uploaded DICOMs by pixel-data hash
+ Admin-only tool for searching uncategorised uploads by image_blake3_hash.
+
+
+
+ {% if hash_query %}
+ Found {{ result_count }} upload{{ result_count|pluralize }} for {{ hash_query }}.
+
+ {% if page_obj.object_list %}
+
+
+
+
+ | Hash |
+ Uploaded |
+ User |
+ Series UID |
+ Links |
+
+
+
+ {% for dicom in page_obj.object_list %}
+
+ {{ dicom.image_blake3_hash }} |
+ {{ dicom.created_date }} |
+ {{ dicom.user }} |
+ {{ dicom.series_instance_uid|default:"-" }} |
+
+ {% if dicom.series_instance_uid %}
+ View series uploads
+ {% else %}
+ -
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+
+
+ {% if page_obj.paginator.num_pages > 1 %}
+
+ {% endif %}
+ {% else %}
+ No uploads matched that hash.
+ {% endif %}
+ {% else %}
+ Enter a hash value to search uploads.
+ {% endif %}
+{% endblock %}
diff --git a/atlas/templates/atlas/user_uploads.html b/atlas/templates/atlas/user_uploads.html
index bdabe51c..73d7470f 100644
--- a/atlas/templates/atlas/user_uploads.html
+++ b/atlas/templates/atlas/user_uploads.html
@@ -5,6 +5,9 @@
Uploaded dicoms
User: {{user}}
Upload more dicoms
+ {% if request.user.is_staff %}
+ Admin: search uploads by pixel hash
+ {% endif %}
{% if not case %}
Series imported from here will be orphaned (not associated with a case).
It is usually better to
create a case first and then import directly into the case.
diff --git a/atlas/urls.py b/atlas/urls.py
index 27758edb..a9fae930 100755
--- a/atlas/urls.py
+++ b/atlas/urls.py
@@ -41,6 +41,7 @@ urlpatterns = [
path("uploads", views.user_uploads, name="user_uploads"),
path("uploads/
/user", views.other_user_uploads, name="other_user_uploads"),
path("uploads/all", views.all_uploads, name="all_uploads"),
+ path("uploads/hash-search", views.uploads_hash_search, name="uploads_hash_search"),
path("uploads/new", views.new_uploads, name="new_uploads"),
path("uploads/case/", views.user_uploads, name="user_uploads_case"),
path("uploads//user/case/", views.other_user_uploads, name="other_user_uploads_case"),
diff --git a/atlas/views.py b/atlas/views.py
index 45d724a8..fd58ffc4 100755
--- a/atlas/views.py
+++ b/atlas/views.py
@@ -2012,6 +2012,31 @@ def all_uploads(request, case_id: int | None = None):
return user_uploads(request, case_id, user_only=False)
+@staff_member_required
+def uploads_hash_search(request):
+ query = (request.GET.get("hash") or "").strip().lower()
+ dicoms = UncategorisedDicom.objects.none()
+
+ if query:
+ dicoms = UncategorisedDicom.objects.filter(
+ image_blake3_hash__icontains=query
+ ).select_related("user", "series").order_by("-created_date")
+
+ paginator = Paginator(dicoms, 50)
+ page_number = request.GET.get("page")
+ page_obj = paginator.get_page(page_number)
+
+ return render(
+ request,
+ "atlas/uploads_hash_search.html",
+ {
+ "hash_query": query,
+ "page_obj": page_obj,
+ "result_count": paginator.count,
+ },
+ )
+
+
@user_passes_test(lambda u: u.is_superuser)
def other_user_uploads(request, user_pk: int, case_id: int | None = None):
return user_uploads(request, case_id, user_only=False, user_pk=user_pk)