diff --git a/isic/core/templates/core/collection_table.html b/isic/core/templates/core/collection_table.html new file mode 100644 index 00000000..e5d1103c --- /dev/null +++ b/isic/core/templates/core/collection_table.html @@ -0,0 +1,132 @@ +{% extends 'core/base.html' %} +{% load humanize %} +{% load partials %} + +{% partialdef sortable-header %} + + {% if current_sort == field %} + {% if current_order == "asc" %} + + {{ label }} + + {% else %} + + {{ label }} + + {% endif %} + {% else %} + + {{ label }} + + {% endif %} + +{% endpartialdef %} + +{% block content %} +
+
+
Collections ({{ page.paginator.count|intcomma }})
+
+
+ +
+ + + +
+ + + +
+
+
+
+ + + + {% with field="name" label="Name" %}{% partial sortable-header %}{% endwith %} + {% with field="public" label="Public" %}{% partial sortable-header %}{% endwith %} + + + + + {% with field="created" label="Created" %}{% partial sortable-header %}{% endwith %} + + + + + {% for collection in page %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
DOIImagesLesionsPatientsDescription
+ + {% if collection.pinned %}{% endif %} + {{ collection.name }} + + + {% if collection.public %}{% else %}{% endif %} + + {% if collection.doi %} + {{ collection.doi.id }} + {% else %} + - + {% endif %} + + {% if collection.counts.image_count is not None %}{{ collection.counts.image_count|intcomma }}{% else %}-{% endif %} + + {% if collection.counts.lesion_count is not None %}{{ collection.counts.lesion_count|intcomma }}{% else %}-{% endif %} + + {% if collection.counts.patient_count is not None %}{{ collection.counts.patient_count|intcomma }}{% else %}-{% endif %} + + {{ collection.created|date:"Y-m-d" }} + + {{ collection.description|truncatechars:50 }} +
+ No collections found. +
+
+
+
+
+ + {% include 'studies/partials/pagination.html' with page_obj=page %} +{% endblock %} diff --git a/isic/core/tests/test_collection.py b/isic/core/tests/test_collection.py index bea9cd47..c2f49a9e 100644 --- a/isic/core/tests/test_collection.py +++ b/isic/core/tests/test_collection.py @@ -162,3 +162,20 @@ def test_collection_move_images_already_exist_in_collection(collection_factory, assert collection_src.images.count() == 0 assert collection_dest.images.count() == 1 + + +@pytest.mark.django_db +def test_collection_table_staff_only( + client, authenticated_client, staff_client, collection_factory +): + collection_factory(name="Test Collection") + + r = client.get(reverse("core/collection-table")) + assert r.status_code == 302 + + r = authenticated_client.get(reverse("core/collection-table")) + assert r.status_code == 302 + + r = staff_client.get(reverse("core/collection-table") + "?exclude_empty=0") + assert r.status_code == 200 + assert b"Test Collection" in r.content diff --git a/isic/core/urls.py b/isic/core/urls.py index 431ab81c..c2fc8706 100644 --- a/isic/core/urls.py +++ b/isic/core/urls.py @@ -8,6 +8,7 @@ collection_download_metadata, collection_edit, collection_list, + collection_table, ) from isic.core.views.doi import doi_detail, draft_doi_edit from isic.core.views.embargoed import embargoed_dashboard @@ -37,6 +38,11 @@ collection_list, name="core/collection-list", ), + path( + "collections/table/", + collection_table, + name="core/collection-table", + ), path( "collections//", collection_detail, diff --git a/isic/core/views/collections.py b/isic/core/views/collections.py index e3f41261..9679f07e 100644 --- a/isic/core/views/collections.py +++ b/isic/core/views/collections.py @@ -2,12 +2,13 @@ from datetime import UTC, datetime from django.contrib import messages +from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required from django.core.exceptions import ValidationError from django.core.paginator import Paginator from django.db.models import Count from django.db.models.query_utils import Q -from django.http import StreamingHttpResponse +from django.http import HttpRequest, HttpResponse, StreamingHttpResponse from django.http.response import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.template.defaultfilters import slugify @@ -208,3 +209,45 @@ def collection_detail(request, pk): "show_shares": request.user.is_staff or request.user == collection.creator, }, ) + + +@staff_member_required +def collection_table(request: HttpRequest) -> HttpResponse: + collections = Collection.objects.select_related("cohort", "cached_counts", "doi").all() + + exclude_magic = request.GET.get("exclude_magic", "1") == "1" + exclude_empty = request.GET.get("exclude_empty", "1") == "1" + exclude_pinned = request.GET.get("exclude_pinned", "0") == "1" + + if exclude_magic: + collections = collections.regular() + + if exclude_empty: + collections = collections.filter(cached_counts__image_count__gt=0) + + if exclude_pinned: + collections = collections.filter(pinned=False) + + sort = request.GET.get("sort", "name") + order = request.GET.get("order", "asc") + valid_sorts: set[str] = {"name", "created", "public"} + + if sort in valid_sorts: + order_field = f"-{sort}" if order == "desc" else sort + collections = collections.order_by(order_field) + + paginator = Paginator(collections, 50) + page = paginator.get_page(request.GET.get("page")) + + return render( + request, + "core/collection_table.html", + { + "page": page, + "current_sort": sort, + "current_order": order, + "exclude_magic": exclude_magic, + "exclude_empty": exclude_empty, + "exclude_pinned": exclude_pinned, + }, + )