diff --git a/isic/core/api/__init__.py b/isic/core/api/__init__.py index f251a45c..bfe862d5 100644 --- a/isic/core/api/__init__.py +++ b/isic/core/api/__init__.py @@ -1,7 +1,5 @@ -from .image import ImageViewSet from .user import user_me __all__ = [ - "ImageViewSet", "user_me", ] diff --git a/isic/core/api/image.py b/isic/core/api/image.py index 5dcf040a..26f3267e 100644 --- a/isic/core/api/image.py +++ b/isic/core/api/image.py @@ -1,109 +1,118 @@ -from django.utils.decorators import method_decorator -from drf_yasg.utils import swagger_auto_schema -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.viewsets import ReadOnlyModelViewSet - -from isic.core.models.collection import Collection -from isic.core.models.image import Image -from isic.core.permissions import IsicObjectPermissionsFilter, get_visible_objects +from django.conf import settings +from django.http.request import HttpRequest +from django.shortcuts import get_object_or_404 +from django.template.loader import render_to_string +from isic_metadata import FIELD_REGISTRY +from ninja import Field, ModelSchema, Query, Router, Schema +from ninja.pagination import paginate + +from isic.core.models import Collection, Image +from isic.core.pagination import CursorPagination +from isic.core.permissions import get_visible_objects from isic.core.search import build_elasticsearch_query, facets -from isic.core.serializers import ImageSerializer, SearchQuerySerializer +from isic.core.serializers import SearchQueryIn +router = Router() -@method_decorator( - name="list", decorator=swagger_auto_schema(operation_summary="Return a list of images.") +# TODO this originally had "distinct()" on it; I don't think that's needed though +default_qs = Image.objects.select_related("accession__cohort").defer( + "accession__unstructured_metadata" ) -@method_decorator( - name="retrieve", - decorator=swagger_auto_schema(operation_summary="Retrieve a single image by ISIC ID."), + + +class FileOut(Schema): + url: str + size: int + + +class ImageFilesOut(Schema): + full: FileOut + thumbnail_256: FileOut + + +class ImageOut(ModelSchema): + class Config: + model = Image + model_fields = ["public"] + + isic_id: str = Field(alias="isic_id") + copyright_license: str = Field(alias="accession.copyright_license") + attribution: str = Field(alias="accession.cohort.attribution") + files: ImageFilesOut + metadata: dict + + @staticmethod + def resolve_files(image: Image) -> dict: + if settings.ISIC_PLACEHOLDER_IMAGES: + full_url = f"https://picsum.photos/seed/{image.id}/1000" + thumbnail_url = f"https://picsum.photos/seed/{image.id}/256" + else: + full_url = image.accession.blob.url + thumbnail_url = image.accession.thumbnail_256.url + + full_size = image.accession.blob_size + thumbnail_size = image.accession.thumbnail_256_size + + return ImageFilesOut( + full=FileOut(url=full_url, size=full_size), + thumbnail_256=FileOut(url=thumbnail_url, size=thumbnail_size), + ) + + @staticmethod + def resolve_metadata(image: Image) -> dict: + metadata = { + "acquisition": {"pixels_x": image.accession.width, "pixels_y": image.accession.height}, + "clinical": {}, + } + + for key, value in image.accession.redacted_metadata.items(): + # this is the only field that we expose that isn't in the FIELD_REGISTRY + # since it's a derived field. + if key == "age_approx": + metadata["clinical"][key] = value + else: + metadata[FIELD_REGISTRY[key]["type"]][key] = value + + return metadata + + +@router.get("/", response=list[ImageOut], summary="Return a list of images.") +@paginate(CursorPagination) +def list_images(request: HttpRequest): + return get_visible_objects(request.user, "core.view_image", default_qs) + + +@router.get( + "/search/", + response=list[ImageOut], + summary="Search images with a key:value query string.", + description=render_to_string("core/swagger_image_search_description.html"), ) -class ImageViewSet(ReadOnlyModelViewSet): - serializer_class = ImageSerializer - queryset = ( - Image.objects.select_related("accession__cohort") - .defer("accession__unstructured_metadata") - .distinct() +@paginate(CursorPagination) +def search_images(request: HttpRequest, search: SearchQueryIn = Query(...)): # noqa: B008 + return search.to_queryset(user=request.user, qs=default_qs) + + +@router.get("/facets/", response=dict, include_in_schema=False) +def get_facets(request: HttpRequest, search: SearchQueryIn = Query(...)): # noqa: B008 + query = build_elasticsearch_query( + search.query or "", + request.user, + search.collections, ) - filter_backends = [IsicObjectPermissionsFilter] - lookup_field = "isic_id" - - @swagger_auto_schema(auto_schema=None) - @action(detail=False, methods=["get"], pagination_class=None) - def facets(self, request): - serializer = SearchQuerySerializer(data=request.query_params) - serializer.is_valid(raise_exception=True) - query = build_elasticsearch_query( - serializer.validated_data.get("query", ""), + # Manually pass the list of visible collection PKs through so buckets with + # counts of 0 aren't included in the facets output for non-visible collections. + collection_pks = list( + get_visible_objects( request.user, - serializer.validated_data.get("collections"), + "core.view_collection", + Collection.objects.values_list("pk", flat=True), ) - # Manually pass the list of visible collection PKs through so buckets with - # counts of 0 aren't included in the facets output for non-visible collections. - collection_pks = list( - get_visible_objects( - request.user, - "core.view_collection", - Collection.objects.values_list("pk", flat=True), - ) - ) - response = facets(query, collection_pks) - - return Response(response) - - @swagger_auto_schema( - operation_summary="Search images with a key:value query string.", - operation_description=""" - The search query uses a simple DSL syntax. - - Some example queries are: -
-            # Display images diagnosed as melanoma from patients that are approximately 50 years old.
-            age_approx:50 AND diagnosis:melanoma
-        
-
-            # Display images from male patients that are approximately 20 to 40 years old.
-            age_approx:[20 TO 40] AND sex:male
-        
-
-            # Display images from the anterior, posterior, or lateral torso anatomical site where the diagnosis was confirmed by single image expert consensus.
-            anatom_site_general:*torso AND diagnosis_confirm_type:"single image expert consensus"
-        
- - The following fields are exposed to the query parameter: - - """, # noqa: E501 - query_serializer=SearchQuerySerializer, ) - @action(detail=False, methods=["get"]) - def search(self, request): - serializer = SearchQuerySerializer( - data=request.query_params, context={"user": request.user} - ) - serializer.is_valid(raise_exception=True) - qs = serializer.to_queryset(self.get_queryset()) - page = self.paginate_queryset(qs) - serializer = self.get_serializer(page, many=True) - paginated_response = self.get_paginated_response(serializer.data) + return facets(query, collection_pks) + - return paginated_response +@router.get("/{isic_id}/", response=ImageOut, summary="Retrieve a single image by ISIC ID.") +def retrieve_image(request: HttpRequest, isic_id: str): + qs = get_visible_objects(request.user, "core.view_image", default_qs) + return get_object_or_404(qs, isic_id=isic_id) diff --git a/isic/core/pagination.py b/isic/core/pagination.py index 3dfabd11..057d7c9c 100644 --- a/isic/core/pagination.py +++ b/isic/core/pagination.py @@ -6,7 +6,7 @@ from django.db.models.query import QuerySet from django.http.request import HttpRequest -from ninja import Schema +from ninja import Field, Schema from ninja.pagination import PaginationBase from rest_framework.pagination import CursorPagination from rest_framework.response import Response @@ -71,14 +71,14 @@ def _replace_query_param(url: str, key: str, val: str): class CursorPagination(PaginationBase): class Input(Schema): - limit: int | None = None - cursor: str | None = None + limit: int | None = Field(None, description="Number of results to return per page.") + cursor: str | None = Field(None, description="The pagination cursor value.") class Output(Schema): - results: list[Any] - count: int - next: str | None - previous: str | None + results: list[Any] = Field(description="The page of objects.") + count: int = Field(description="The total number of results across all pages.") + next: str | None = Field(description="URL of next page of results if there is one.") + previous: str | None = Field(description="URL of previous page of results if there is one.") items_attribute = "results" default_ordering = ("-created",) diff --git a/isic/core/serializers.py b/isic/core/serializers.py index 5ea3116a..0df2adcc 100644 --- a/isic/core/serializers.py +++ b/isic/core/serializers.py @@ -1,11 +1,9 @@ import re from typing import Optional -from django.conf import settings from django.contrib.auth.models import AnonymousUser, User from django.db.models.query import QuerySet from django.shortcuts import get_object_or_404 -from isic_metadata import FIELD_REGISTRY from ninja import Schema from pydantic import validator from pyparsing.exceptions import ParseException @@ -65,7 +63,7 @@ def to_queryset(self, qs: Optional[QuerySet[Image]] = None) -> QuerySet[Image]: # TODO: https://github.com/vitalik/django-ninja/issues/526#issuecomment-1283984292 # Update this to use context for the user once django-ninja supports it class SearchQueryIn(Schema): - query: str | None + query: str | None = None collections: list[int] | None = None @validator("query") @@ -111,7 +109,7 @@ def from_token_representation(cls, token): # TODO return cls(data=token, context={"user": user}) - def to_queryset(self, user, qs: Optional[QuerySet[Image]] = None) -> QuerySet[Image]: + def to_queryset(self, user: User, qs: Optional[QuerySet[Image]] = None) -> QuerySet[Image]: qs = qs if qs is not None else Image._default_manager.all() if self.query: @@ -184,80 +182,3 @@ def to_queryset(self, qs: Optional[QuerySet[Image]] = None) -> QuerySet[Image]: ) return get_visible_objects(self.context["user"], "core.view_image", qs).distinct() - - -class ImageFileSerializer(serializers.Serializer): - full = serializers.SerializerMethodField() - thumbnail_256 = serializers.SerializerMethodField() - - def get_full(self, obj: Image) -> dict: - if settings.ISIC_PLACEHOLDER_IMAGES: - url = f"https://picsum.photos/seed/{ obj.id }/1000" - else: - url = obj.accession.blob.url - - return { - "url": url, - "size": obj.accession.blob_size, - } - - def get_thumbnail_256(self, obj: Image) -> dict: - if settings.ISIC_PLACEHOLDER_IMAGES: - url = f"https://picsum.photos/seed/{ obj.id }/256" - else: - url = obj.accession.thumbnail_256.url - - return { - "url": url, - "size": obj.accession.thumbnail_256_size, - } - - -class ImageSerializer(serializers.ModelSerializer): - class Meta: - model = Image - fields = [ - "isic_id", - "public", - "copyright_license", - "attribution", - "metadata", - "files", - ] - - copyright_license = serializers.CharField(source="accession.copyright_license", read_only=True) - attribution = serializers.CharField(source="accession.cohort.attribution", read_only=True) - metadata = serializers.SerializerMethodField(read_only=True) - files = ImageFileSerializer(source="*", read_only=True) - - def get_metadata(self, image: Image) -> dict: - metadata = { - "acquisition": {"pixels_x": image.accession.width, "pixels_y": image.accession.height}, - "clinical": {}, - } - - for key, value in image.accession.redacted_metadata.items(): - # this is the only field that we expose that isn't in the FIELD_REGISTRY - # since it's a derived field. - if key == "age_approx": - metadata["clinical"][key] = value - else: - metadata[FIELD_REGISTRY[key]["type"]][key] = value - - return metadata - - -class CollectionSerializer(serializers.ModelSerializer): - class Meta: - model = Collection - fields = [ - "id", - "name", - "description", - "public", - "pinned", - "locked", - "doi", - ] - - doi = serializers.URLField(source="doi_url") diff --git a/isic/core/templates/core/swagger_image_search_description.html b/isic/core/templates/core/swagger_image_search_description.html new file mode 100644 index 00000000..1287f714 --- /dev/null +++ b/isic/core/templates/core/swagger_image_search_description.html @@ -0,0 +1,38 @@ +The search query uses a simple DSL syntax. + +Some example queries are: +
+    # Display images diagnosed as melanoma from patients that are approximately 50 years old.
+    age_approx:50 AND diagnosis:melanoma
+
+
+    # Display images from male patients that are approximately 20 to 40 years old.
+    age_approx:[20 TO 40] AND sex:male
+
+
+    # Display images from the anterior, posterior, or lateral torso anatomical site where the diagnosis was confirmed by single image expert consensus.
+    anatom_site_general:*torso AND diagnosis_confirm_type:"single image expert consensus"
+
+ +The following fields are exposed to the query parameter: + diff --git a/isic/core/tests/test_api_image.py b/isic/core/tests/test_api_image.py index ae253839..3208ef5c 100644 --- a/isic/core/tests/test_api_image.py +++ b/isic/core/tests/test_api_image.py @@ -15,40 +15,40 @@ def searchable_image(search_index, image_factory): @pytest.mark.django_db def test_core_api_image_ages_are_always_rounded( - authenticated_api_client, staff_api_client, searchable_image + authenticated_client, staff_client, searchable_image ): - for client in [authenticated_api_client, staff_api_client]: - r = client.get("/api/v2/images/") - assert r.status_code == 200, r.data - assert r.data["count"] == 1 - assert r.data["results"][0]["metadata"]["clinical"]["age_approx"] == 50 + for client_ in [authenticated_client, staff_client]: + r = client_.get("/api/v2/images/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1 + assert r.json()["results"][0]["metadata"]["clinical"]["age_approx"] == 50 - r = client.get(f"/api/v2/images/{searchable_image.isic_id}/") - assert r.status_code == 200, r.data - assert r.data["metadata"]["clinical"]["age_approx"] == 50 + r = client_.get(f"/api/v2/images/{searchable_image.isic_id}/") + assert r.status_code == 200, r.json() + assert r.json()["metadata"]["clinical"]["age_approx"] == 50 # test search isn't leaking ages - r = client.get("/api/v2/images/search/", {"query": "age_approx:50"}) - assert r.status_code == 200, r.data - assert r.data["count"] == 1 - assert r.data["results"][0]["metadata"]["clinical"]["age_approx"] == 50 + r = client_.get("/api/v2/images/search/", {"query": "age_approx:50"}) + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1 + assert r.json()["results"][0]["metadata"]["clinical"]["age_approx"] == 50 - r = client.get("/api/v2/images/search/", {"query": "age_approx:52"}) - assert r.status_code == 200, r.data - assert r.data["count"] == 0 + r = client_.get("/api/v2/images/search/", {"query": "age_approx:52"}) + assert r.status_code == 200, r.json() + assert r.json()["count"] == 0 @pytest.mark.django_db @pytest.mark.parametrize("image_file", ["full", "thumbnail_256"]) -def test_api_image_urls_thumbnail_256(api_client, image_factory, image_file): +def test_api_image_urls_thumbnail_256(client, image_factory, image_file): image = image_factory(public=True) - api_resp = api_client.get(f"/api/v2/images/{image.isic_id}/") + api_resp = client.get(f"/api/v2/images/{image.isic_id}/") - assert isinstance(api_resp.data.get("files"), dict) - assert isinstance(api_resp.data["files"].get(image_file), dict) - assert isinstance(api_resp.data["files"][image_file]["url"], str) - image_url = api_resp.data["files"][image_file]["url"] + assert isinstance(api_resp.json().get("files"), dict) + assert isinstance(api_resp.json()["files"].get(image_file), dict) + assert isinstance(api_resp.json()["files"][image_file]["url"], str) + image_url = api_resp.json()["files"][image_file]["url"] assert image_url # "stream=True", as there's no need to download the actual response body diff --git a/isic/core/tests/test_api_permissions.py b/isic/core/tests/test_api_permissions.py index e61f745a..2e144a57 100644 --- a/isic/core/tests/test_api_permissions.py +++ b/isic/core/tests/test_api_permissions.py @@ -10,55 +10,55 @@ def images(image_factory): @pytest.mark.django_db -def test_core_api_image_list(images, api_client, authenticated_api_client, staff_api_client): - for client in [api_client, authenticated_api_client]: - r = client.get("/api/v2/images/") - assert r.status_code == 200, r.data - assert r.data["count"] == 1 - assert {x["public"] for x in r.data["results"]} == {True} +def test_core_api_image_list(images, client, authenticated_client, staff_client): + for client_ in [client, authenticated_client]: + r = client_.get("/api/v2/images/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1 + assert {x["public"] for x in r.json()["results"]} == {True} - r = staff_api_client.get("/api/v2/images/") - assert r.status_code == 200, r.data - assert r.data["count"] == 2 + r = staff_client.get("/api/v2/images/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 2 @pytest.mark.django_db -def test_core_api_image_list_private(private_image, authenticated_api_client): - r = authenticated_api_client.get("/api/v2/images/") - assert r.status_code == 200, r.data - assert r.data["count"] == 0 +def test_core_api_image_list_private(private_image, authenticated_client): + r = authenticated_client.get("/api/v2/images/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 0 @pytest.mark.django_db -def test_core_api_image_list_contributed(private_image, authenticated_api_client, user): +def test_core_api_image_list_contributed(private_image, authenticated_client, user): private_image.accession.cohort.contributor.owners.add(user) - r = authenticated_api_client.get("/api/v2/images/") - assert r.status_code == 200, r.data - assert r.data["count"] == 1 + r = authenticated_client.get("/api/v2/images/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1 @pytest.mark.django_db -def test_core_api_image_list_shares(private_image, authenticated_api_client, user, staff_user): +def test_core_api_image_list_shares(private_image, authenticated_client, user, staff_user): private_image.shares.add(user, through_defaults={"creator": staff_user}) private_image.save() - r = authenticated_api_client.get("/api/v2/images/") - assert r.status_code == 200, r.data - assert r.data["count"] == 1 + r = authenticated_client.get("/api/v2/images/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1 @pytest.mark.django_db -def test_core_api_image_detail(images, authenticated_api_client, staff_api_client): +def test_core_api_image_detail(images, authenticated_client, staff_client): public_image_id = images[0].isic_id private_image_id = images[1].isic_id - r = authenticated_api_client.get(f"/api/v2/images/{public_image_id}/") - assert r.status_code == 200, r.data - r = authenticated_api_client.get(f"/api/v2/images/{private_image_id}/") - assert r.status_code == 404, r.data + r = authenticated_client.get(f"/api/v2/images/{public_image_id}/") + assert r.status_code == 200, r.json() + r = authenticated_client.get(f"/api/v2/images/{private_image_id}/") + assert r.status_code == 404, r.json() - r = staff_api_client.get(f"/api/v2/images/{public_image_id}/") - assert r.status_code == 200, r.data - r = staff_api_client.get(f"/api/v2/images/{private_image_id}/") - assert r.status_code == 200, r.data + r = staff_client.get(f"/api/v2/images/{public_image_id}/") + assert r.status_code == 200, r.json() + r = staff_client.get(f"/api/v2/images/{private_image_id}/") + assert r.status_code == 200, r.json() diff --git a/isic/core/tests/test_search.py b/isic/core/tests/test_search.py index 96497598..99283356 100644 --- a/isic/core/tests/test_search.py +++ b/isic/core/tests/test_search.py @@ -73,69 +73,67 @@ def collection_with_image(search_index, image_factory, collection_factory): @pytest.mark.django_db -def test_core_api_image_search(searchable_images, staff_api_client): - r = staff_api_client.get("/api/v2/images/search/") - assert r.status_code == 200, r.data - assert r.data["count"] == 2, r.data +def test_core_api_image_search(searchable_images, staff_client): + r = staff_client.get("/api/v2/images/search/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 2, r.json() - r = staff_api_client.get("/api/v2/images/search/", {"query": "diagnosis:nevus"}) - assert r.status_code == 200, r.data - assert r.data["count"] == 1, r.data + r = staff_client.get("/api/v2/images/search/", {"query": "diagnosis:nevus"}) + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1, r.json() @pytest.mark.django_db -def test_core_api_image_search_private_image(private_searchable_image, authenticated_api_client): - r = authenticated_api_client.get("/api/v2/images/search/") - assert r.status_code == 200, r.data - assert r.data["count"] == 0, r.data +def test_core_api_image_search_private_image(private_searchable_image, authenticated_client): + r = authenticated_client.get("/api/v2/images/search/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 0, r.json() @pytest.mark.django_db -def test_core_api_image_search_private_image_as_guest(private_searchable_image, api_client): - r = api_client.get("/api/v2/images/search/") - assert r.status_code == 200, r.data - assert r.data["count"] == 0, r.data +def test_core_api_image_search_private_image_as_guest(private_searchable_image, client): + r = client.get("/api/v2/images/search/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 0, r.json() @pytest.mark.django_db -def test_core_api_image_search_images_as_guest(searchable_images, api_client): - r = api_client.get("/api/v2/images/search/") - assert r.status_code == 200, r.data - assert r.data["count"] == 1, r.data +def test_core_api_image_search_images_as_guest(searchable_images, client): + r = client.get("/api/v2/images/search/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1, r.json() @pytest.mark.django_db -def test_core_api_image_search_contributed( - private_searchable_image, authenticated_api_client, user -): +def test_core_api_image_search_contributed(private_searchable_image, authenticated_client, user): private_searchable_image.accession.cohort.contributor.owners.add(user) add_to_search_index(private_searchable_image) get_elasticsearch_client().indices.refresh(index="_all") - r = authenticated_api_client.get("/api/v2/images/search/") - assert r.status_code == 200, r.data - assert r.data["count"] == 1, r.data + r = authenticated_client.get("/api/v2/images/search/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1, r.json() @pytest.mark.django_db def test_core_api_image_search_shares( - private_searchable_image, authenticated_api_client, user, staff_user + private_searchable_image, authenticated_client, user, staff_user ): private_searchable_image.shares.add(user, through_defaults={"creator": staff_user}) private_searchable_image.save() add_to_search_index(private_searchable_image) get_elasticsearch_client().indices.refresh(index="_all") - r = authenticated_api_client.get("/api/v2/images/search/") - assert r.status_code == 200, r.data - assert r.data["count"] == 1, r.data + r = authenticated_client.get("/api/v2/images/search/") + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1, r.json() @pytest.mark.django_db @pytest.mark.parametrize("route", ["images/search/", "images/facets/"]) -def test_core_api_image_search_invalid_query(route, searchable_images, authenticated_api_client): - r = authenticated_api_client.get(f"/api/v2/{route}", {"query": "age_approx:[[[[]]]]"}) - assert r.status_code == 400, r.data +def test_core_api_image_search_invalid_query(route, searchable_images, authenticated_client): + r = authenticated_client.get(f"/api/v2/{route}", {"query": "age_approx:[[[[]]]]"}) + assert r.status_code == 400, r.json() @pytest.mark.django_db @@ -144,25 +142,23 @@ def test_core_api_image_search_invalid_query(route, searchable_images, authentic itertools.product(RESTRICTED_METADATA_FIELDS, ["/api/v2/images/", "/api/v2/images/search/"]), ) def test_core_api_image_hides_fields( - authenticated_api_client, searchable_image_with_private_field, restricted_field, route + authenticated_client, searchable_image_with_private_field, restricted_field, route ): - r = authenticated_api_client.get(route) - assert r.status_code == 200, r.data - assert r.data["count"] == 1, r.data - for image in r.data["results"]: + r = authenticated_client.get(route) + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1, r.json() + for image in r.json()["results"]: assert restricted_field not in image["metadata"] @pytest.mark.django_db -def test_core_api_image_search_collection_and_query( - collection_with_image, authenticated_api_client -): - r = authenticated_api_client.get( +def test_core_api_image_search_collection_and_query(collection_with_image, authenticated_client): + r = authenticated_client.get( "/api/v2/images/search/", {"collections": f"{collection_with_image.pk}", "query": "age_approx:50"}, ) - assert r.status_code == 200, r.data - assert r.data["count"] == 1, r.data + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1, r.json() @pytest.mark.django_db @@ -177,7 +173,7 @@ def test_core_api_image_search_collection_and_query( ids=["all-public", "private-coll-public-image", "all-private"], ) def test_core_api_image_search_collection( - authenticated_api_client, + authenticated_client, image_factory, collection_factory, search_index, @@ -191,33 +187,33 @@ def test_core_api_image_search_collection( add_to_search_index(image) get_elasticsearch_client().indices.refresh(index="_all") - r = authenticated_api_client.get("/api/v2/images/search/", {"collections": str(collection.pk)}) - assert r.status_code == 200, r.data + r = authenticated_client.get("/api/v2/images/search/", {"collections": str(collection.pk)}) + assert r.status_code == 200, r.json() if can_see: - assert r.data["count"] == 1, r.data + assert r.json()["count"] == 1, r.json() else: - assert r.data["count"] == 0, r.data + assert r.json()["count"] == 0, r.json() @pytest.mark.django_db def test_core_api_image_search_collection_parsing( - private_and_public_images_collections, authenticated_api_client + private_and_public_images_collections, authenticated_client ): public_coll, private_coll = private_and_public_images_collections - r = authenticated_api_client.get( + r = authenticated_client.get( "/api/v2/images/search/", {"collections": f"{public_coll.pk},{private_coll.pk}"} ) - assert r.status_code == 200, r.data - assert r.data["count"] == 1, r.data + assert r.status_code == 200, r.json() + assert r.json()["count"] == 1, r.json() @pytest.mark.parametrize( "client_", [ - lazy_fixture("api_client"), - lazy_fixture("authenticated_api_client"), + lazy_fixture("client"), + lazy_fixture("authenticated_client"), ], ) @pytest.mark.django_db @@ -227,8 +223,8 @@ def test_core_api_image_faceting_collections(private_and_public_images_collectio r = client_.get( "/api/v2/images/facets/", {"collections": f"{public_coll.pk},{private_coll.pk}"} ) - assert r.status_code == 200, r.data - buckets = r.data["collections"]["buckets"] + assert r.status_code == 200, r.json() + buckets = r.json()["collections"]["buckets"] assert len(buckets) == 1 assert buckets[0] == {"key": public_coll.pk, "doc_count": 1} @@ -236,8 +232,8 @@ def test_core_api_image_faceting_collections(private_and_public_images_collectio @pytest.mark.parametrize( "client_", [ - lazy_fixture("api_client"), - lazy_fixture("authenticated_api_client"), + lazy_fixture("client"), + lazy_fixture("authenticated_client"), ], ) @pytest.mark.django_db @@ -247,8 +243,8 @@ def test_core_api_image_faceting(private_and_public_images_collections, client_) r = client_.get( "/api/v2/images/facets/", ) - assert r.status_code == 200, r.data - buckets = r.data["collections"]["buckets"] + assert r.status_code == 200, r.json() + buckets = r.json()["collections"]["buckets"] assert len(buckets) == 1, buckets assert buckets[0] == {"key": public_coll.pk, "doc_count": 1}, buckets @@ -256,8 +252,8 @@ def test_core_api_image_faceting(private_and_public_images_collections, client_) @pytest.mark.parametrize( "client_", [ - lazy_fixture("api_client"), - lazy_fixture("authenticated_api_client"), + lazy_fixture("client"), + lazy_fixture("authenticated_client"), ], ) @pytest.mark.django_db @@ -265,7 +261,7 @@ def test_core_api_image_faceting_query(private_and_public_images_collections, cl public_coll, private_coll = private_and_public_images_collections r = client_.get("/api/v2/images/facets/", {"query": "age_approx:10"}) - assert r.status_code == 200, r.data - buckets = r.data["collections"]["buckets"] + assert r.status_code == 200, r.json() + buckets = r.json()["collections"]["buckets"] assert len(buckets) == 1, buckets assert buckets[0] == {"key": public_coll.pk, "doc_count": 1}, buckets diff --git a/isic/urls.py b/isic/urls.py index 287087a1..85a82fb0 100644 --- a/isic/urls.py +++ b/isic/urls.py @@ -12,8 +12,8 @@ from ninja.openapi.views import openapi_view from rest_framework import permissions, routers -from isic.core.api import ImageViewSet from isic.core.api.collection import router as collection_router +from isic.core.api.image import router as image_router from isic.core.api.user import router as user_router from isic.find.api import router as quickfind_router from isic.ingest.api import CohortViewSet, ContributorViewSet, MetadataFileViewSet @@ -28,8 +28,9 @@ ) swagger_view = partial(openapi_view, api=api) -api.add_router("/quickfind/", quickfind_router, tags=["quickfind"]) api.add_router("/collections/", collection_router, tags=["collections"]) +api.add_router("/images/", image_router, tags=["images"]) +api.add_router("/quickfind/", quickfind_router, tags=["quickfind"]) api.add_router("/users", user_router, tags=["users"]) @@ -37,7 +38,6 @@ router.register("annotations", AnnotationViewSet) router.register("cohorts", CohortViewSet) router.register("contributors", ContributorViewSet) -router.register("images", ImageViewSet) router.register("metadata-files", MetadataFileViewSet) router.register("studies", StudyViewSet) router.register("study-tasks", StudyTaskViewSet)