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:
-
- - diagnosis
- - age_approx
- - sex
- - benign_malignant
- - diagnosis_confirm_type
- - personal_hx_mm
- - family_hx_mm
- - clin_size_long_diam_mm
- - melanocytic
- - acquisition_day
- - nevus_type
- - image_type
- - dermoscopic_type
- - anatom_site_general
- - mel_class
- - mel_mitotic_index
- - mel_thick_mm
- - mel_type
- - mel_ulcer
-
- """, # 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:
+
+ - diagnosis
+ - age_approx
+ - sex
+ - benign_malignant
+ - diagnosis_confirm_type
+ - personal_hx_mm
+ - family_hx_mm
+ - clin_size_long_diam_mm
+ - melanocytic
+ - acquisition_day
+ - nevus_type
+ - image_type
+ - dermoscopic_type
+ - anatom_site_general
+ - mel_class
+ - mel_mitotic_index
+ - mel_thick_mm
+ - mel_type
+ - mel_ulcer
+
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)