diff --git a/documents/api/docs.py b/documents/api/docs.py index 85c462d..707921a 100644 --- a/documents/api/docs.py +++ b/documents/api/docs.py @@ -630,6 +630,37 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: }, examples=[example_document, example_error], ), + "batch_list": extend_schema( + summary="Fetch multiple documents by IDs", + description="This endpoint allows a service to fetch multiple documents by their IDs", + # TODO: Uncomment when organization features are implemented + # Replace the previous line with the following + # "* Authenticated users are allowed access to the document if they are the owner of the document " + # "or the document is owned by an organization and the user has permission to act on behalf " + # "of that organization.", + request=serializers.JSONField(), + responses={ + (status.HTTP_200_OK, "application/json"): OpenApiResponse( + response=DocumentSerializer, + description="The document/s was found and contents are returned as JSON.", + ), + (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), + status.HTTP_401_UNAUTHORIZED: _base_401_response(), + status.HTTP_403_FORBIDDEN: OpenApiResponse( + description="The authenticated user lacks the proper permissions to access the document. " + "Depending on the requested document, " + "either the user does not belong to the admin group of the service which owns the document, " + "the user does not own the document." + # TODO: Uncomment when organization features are implemented + # " or the user does not have permission to act on behalf " + # "of the organization which owns the document." + ), + status.HTTP_404_NOT_FOUND: OpenApiResponse( + description="No document was found with `documentId`.", + ), + status.HTTP_500_INTERNAL_SERVER_ERROR: _base_500_response(), + }, + ), "create": extend_schema( summary="Store a new document and its attachments", description="Store a new document and its attachments.\n\n" diff --git a/documents/api/viewsets.py b/documents/api/viewsets.py index 49d4e1f..1cca5cf 100644 --- a/documents/api/viewsets.py +++ b/documents/api/viewsets.py @@ -7,13 +7,14 @@ from drf_spectacular.utils import extend_schema, extend_schema_view from helusers.utils import uuid_to_username from rest_framework import filters, status +from rest_framework.decorators import action from rest_framework.exceptions import ( MethodNotAllowed, NotAuthenticated, NotFound, PermissionDenied, ) -from rest_framework.parsers import FileUploadParser, MultiPartParser +from rest_framework.parsers import FileUploadParser, JSONParser, MultiPartParser from rest_framework.response import Response from rest_framework.utils import json from rest_framework_extensions.mixins import NestedViewSetMixin @@ -426,7 +427,7 @@ def update(self, request, *args, **kwargs): @extend_schema_view(**document_viewset_docs) class DocumentViewSet(AuditLoggingModelViewSet): - parser_classes = [MultiPartParser] + parser_classes = [MultiPartParser, JSONParser] serializer_class = DocumentSerializer # Filtering/sorting filter_backends = [ @@ -446,6 +447,23 @@ def get_queryset(self): service_api_key = get_service_api_key_from_request(self.request) return get_document_queryset(user, service, service_api_key) + @action(detail=False, methods=["POST"], url_path="batch-list") + def batch_list(self, request, *args, **kwargs): + data = request.data + if not isinstance(data, dict): + raise InvalidFieldException(detail="Data is not a json object.") + document_ids = data.get("document_ids") + if not document_ids: + raise InvalidFieldException(detail="Field 'document_ids' is required") + if not isinstance(document_ids, list): + raise InvalidFieldException( + detail="Field 'document_ids' should be a list of UUIDs" + ) + queryset = self.get_queryset().filter(id__in=document_ids) + with self.record_action(): + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + def retrieve(self, request, *args, **kwargs): return super().retrieve(request, *args, **kwargs) diff --git a/documents/tests/snapshots/snap_test_api_patch_document.py b/documents/tests/snapshots/snap_test_api_patch_document.py index 8caeb23..b2a1a14 100644 --- a/documents/tests/snapshots/snap_test_api_patch_document.py +++ b/documents/tests/snapshots/snap_test_api_patch_document.py @@ -25,7 +25,7 @@ "id": "2d2b7a36-a306-4e35-990f-13aea04263ff", "locked_after": None, "metadata": {"created_by": "alex", "testing": True}, - "service": "service 155", + "service": "service 160", "status": { "timestamp": "2021-06-30T12:00:00+03:00", "value": "handled", @@ -59,7 +59,7 @@ "id": "2d2b7a36-a306-4e35-990f-13aea04263ff", "locked_after": None, "metadata": {"created_by": "alex", "testing": True}, - "service": "service 161", + "service": "service 166", "status": { "timestamp": "2021-06-30T12:00:00+03:00", "value": "handled", @@ -94,7 +94,7 @@ "id": "2d2b7a36-a306-4e35-990f-13aea04263ff", "locked_after": None, "metadata": {"created_by": "alex", "testing": True}, - "service": "service 163", + "service": "service 168", "status": { "timestamp": "2021-06-30T12:00:00+03:00", "value": "handled", diff --git a/documents/tests/snapshots/snap_test_api_retrieve_document.py b/documents/tests/snapshots/snap_test_api_retrieve_document.py index 7def21b..8cccb79 100644 --- a/documents/tests/snapshots/snap_test_api_retrieve_document.py +++ b/documents/tests/snapshots/snap_test_api_retrieve_document.py @@ -19,7 +19,7 @@ "id": "485af718-d9d1-46b9-ad7b-33ea054126e3", "locked_after": None, "metadata": {}, - "service": "service 207", + "service": "service 212", "status": { "timestamp": "2020-06-01T03:00:00+03:00", "value": "testing", @@ -47,7 +47,7 @@ "id": "485af718-d9d1-46b9-ad7b-33ea054126e3", "locked_after": None, "metadata": {}, - "service": "service 210", + "service": "service 215", "status": { "timestamp": "2020-06-01T03:00:00+03:00", "value": "testing", @@ -75,7 +75,7 @@ "id": "485af718-d9d1-46b9-ad7b-33ea054126e3", "locked_after": None, "metadata": {}, - "service": "service 209", + "service": "service 214", "status": { "timestamp": "2020-06-01T03:00:00+03:00", "value": "testing", diff --git a/documents/tests/test_api_list_documents.py b/documents/tests/test_api_list_documents.py index 809ea76..10828b3 100644 --- a/documents/tests/test_api_list_documents.py +++ b/documents/tests/test_api_list_documents.py @@ -178,6 +178,52 @@ def test_gdpr_api_list_user(user, service): assert response.status_code == status.HTTP_403_FORBIDDEN +def test_document_batch_list_service_api_key(service_api_client, user): + data = {**VALID_DOCUMENT_DATA, "user_id": user.uuid} + other_service = ServiceFactory() + document_ids = [] + for _i in range(0, 2): + response = service_api_client.post( + reverse("documents-list"), data, format="multipart" + ) + assert response.status_code == status.HTTP_201_CREATED + document_ids.append(response.json()["id"]) + + other_document_id = DocumentFactory(service=other_service, user=user).id + + assert Document.objects.count() == 3 + response = service_api_client.post( + reverse("documents-batch-list"), + data={"document_ids": document_ids}, + format="json", + ) + assert response.status_code == status.HTTP_200_OK + response_ids = [x["id"] for x in response.json()] + assert len(response_ids) == 2 + assert str(other_document_id) not in response_ids + + +def test_document_batch_list_user(user, service): + api_client = get_user_service_client(user, service) + other_service = ServiceFactory() + other_user = UserFactory() + d1 = DocumentFactory(service=service, user=user, deletable=True) + d2 = DocumentFactory(service=other_service, user=user, deletable=False) + d3 = DocumentFactory(service=other_service, user=other_user, deletable=False) + + assert Document.objects.count() == 3 + + response = api_client.post( + reverse("documents-batch-list"), + data={"document_ids": [d1.id, d2.id, d3.id]}, + format="json", + ) + assert response.status_code == status.HTTP_200_OK + response_ids = [x["id"] for x in response.json()] + assert len(response_ids) == 1 + assert [str(d1.id)] == response_ids + + @pytest.mark.parametrize( "param,value", [