Add admin feature to search uploads by pixel hash and corresponding template
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
{% extends 'atlas/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Search uploaded DICOMs by pixel-data hash</h2>
|
||||
<p class="text-muted">Admin-only tool for searching uncategorised uploads by <code>image_blake3_hash</code>.</p>
|
||||
|
||||
<form method="get" class="mb-3">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="hash"
|
||||
value="{{ hash_query }}"
|
||||
placeholder="Enter full or partial pixel-data hash"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Search is case-insensitive and supports partial matches.</small>
|
||||
</form>
|
||||
|
||||
{% if hash_query %}
|
||||
<p>Found <strong>{{ result_count }}</strong> upload{{ result_count|pluralize }} for <code>{{ hash_query }}</code>.</p>
|
||||
|
||||
{% if page_obj.object_list %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hash</th>
|
||||
<th>Uploaded</th>
|
||||
<th>User</th>
|
||||
<th>Series UID</th>
|
||||
<th>Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for dicom in page_obj.object_list %}
|
||||
<tr>
|
||||
<td><code>{{ dicom.image_blake3_hash }}</code></td>
|
||||
<td>{{ dicom.created_date }}</td>
|
||||
<td>{{ dicom.user }}</td>
|
||||
<td><code>{{ dicom.series_instance_uid|default:"-" }}</code></td>
|
||||
<td>
|
||||
{% if dicom.series_instance_uid %}
|
||||
<a href="{% url 'atlas:user_uploads_series' dicom.series_instance_uid %}">View series uploads</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<nav aria-label="Hash search pagination">
|
||||
<ul class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?hash={{ hash_query|urlencode }}&page={{ page_obj.previous_page_number }}">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">Previous</span></li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?hash={{ hash_query|urlencode }}&page={{ page_obj.next_page_number }}">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">Next</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">No uploads matched that hash.</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">Enter a hash value to search uploads.</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -5,6 +5,9 @@
|
||||
<h2>Uploaded dicoms</h2>
|
||||
User: {{user}}<br/>
|
||||
<a href="{% url 'atlas:new_uploads' %}">Upload more dicoms</a><br/>
|
||||
{% if request.user.is_staff %}
|
||||
<a href="{% url 'atlas:uploads_hash_search' %}">Admin: search uploads by pixel hash</a><br/>
|
||||
{% endif %}
|
||||
{% if not case %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
Series imported from here will be orphaned (not associated with a case). <br/>It is usually better to <a href="{% url 'atlas:case_create' %}">create a case</a> first and then import directly into the case.
|
||||
|
||||
@@ -41,6 +41,7 @@ urlpatterns = [
|
||||
path("uploads", views.user_uploads, name="user_uploads"),
|
||||
path("uploads/<int:user_pk>/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/<int:case_id>", views.user_uploads, name="user_uploads_case"),
|
||||
path("uploads/<int:user_pk>/user/case/<int:case_id>", views.other_user_uploads, name="other_user_uploads_case"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user