diff --git a/swh/deposit/api/collection.py b/swh/deposit/api/collection.py --- a/swh/deposit/api/collection.py +++ b/swh/deposit/api/collection.py @@ -7,7 +7,7 @@ from rest_framework import status -from ..config import EDIT_SE_IRI +from ..config import EDIT_IRI from ..parsers import ( SWHAtomEntryParser, SWHFileUploadTarParser, @@ -99,4 +99,4 @@ req, headers, collection_name, check_slug_is_present=True ) - return status.HTTP_201_CREATED, EDIT_SE_IRI, data + return status.HTTP_201_CREATED, EDIT_IRI, data diff --git a/swh/deposit/api/common.py b/swh/deposit/api/common.py --- a/swh/deposit/api/common.py +++ b/swh/deposit/api/common.py @@ -41,11 +41,12 @@ DEPOSIT_STATUS_DEPOSITED, DEPOSIT_STATUS_LOAD_SUCCESS, DEPOSIT_STATUS_PARTIAL, - EDIT_SE_IRI, + EDIT_IRI, EM_IRI, METADATA_KEY, METADATA_TYPE, RAW_METADATA_KEY, + SE_IRI, STATE_IRI, APIConfig, ) @@ -885,7 +886,7 @@ args = [collection_name, deposit_id] return { iri: request.build_absolute_uri(reverse(iri, args=args)) - for iri in [EM_IRI, EDIT_SE_IRI, CONT_FILE_IRI, STATE_IRI] + for iri in [EM_IRI, EDIT_IRI, CONT_FILE_IRI, SE_IRI, STATE_IRI] } def additional_checks( @@ -1102,7 +1103,7 @@ Returns Tuple of: - response status code (200, 201, etc...) - - key iri (EM_IRI, EDIT_SE_IRI, etc...) + - key iri (EM_IRI, EDIT_IRI, etc...) - dictionary of the processing result """ diff --git a/swh/deposit/api/edit.py b/swh/deposit/api/edit.py --- a/swh/deposit/api/edit.py +++ b/swh/deposit/api/edit.py @@ -3,30 +3,25 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict -from rest_framework import status from rest_framework.request import Request from swh.deposit.models import Deposit from swh.model.identifiers import parse_swhid -from ..config import ( - DEPOSIT_STATUS_LOAD_SUCCESS, - EDIT_SE_IRI, - EM_IRI, -) +from ..config import DEPOSIT_STATUS_LOAD_SUCCESS from ..errors import BAD_REQUEST, BadRequestError, ParserError, make_error_dict from ..parsers import SWHAtomEntryParser, SWHMultiPartParser -from .common import APIDelete, APIPost, APIPut +from .common import APIDelete, APIPut -class EditAPI(APIPost, APIPut, APIDelete): +class EditAPI(APIPut, APIDelete): """Deposit request class defining api endpoints for sword deposit. - What's known as 'Edit-IRI' and 'SE-IRI' in the sword specification. + What's known as 'Edit-IRI' in the sword specification. - HTTP verbs supported: POST (SE IRI), PUT (Edit IRI), DELETE + HTTP verbs supported: PUT, DELETE """ @@ -144,55 +139,6 @@ "archive": None, } - def process_post( - self, - request, - headers: Dict, - collection_name: str, - deposit_id: Optional[int] = None, - ) -> Tuple[int, str, Dict]: - """Add new metadata/archive to existing deposit. - - This allows the following scenarios to occur: - - - multipart: Add new metadata and archive to a deposit in status partial with - the provided ones. - - - empty atom: Allows to finalize a deposit in status partial (transition to - deposited). - - source: - - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_addingcontent_metadata - - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_addingcontent_multipart - - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#continueddeposit_complete - - Returns: - In optimal case for a multipart and atom-entry update, a - 201 Created response. The body response will hold a - deposit. And the response headers will contain an entry - 'Location' with the EM-IRI. - - For the empty post case, this returns a 200. - - """ # noqa - assert deposit_id is not None - if request.content_type.startswith("multipart/"): - data = self._multipart_upload( - request, headers, collection_name, deposit_id=deposit_id - ) - return (status.HTTP_201_CREATED, EM_IRI, data) - - content_length = headers["content-length"] or 0 - if content_length == 0 and headers["in-progress"] is False: - # check for final empty post - data = self._empty_post(request, headers, collection_name, deposit_id) - return (status.HTTP_200_OK, EDIT_SE_IRI, data) - - data = self._atom_entry( - request, headers, collection_name, deposit_id=deposit_id - ) - return (status.HTTP_201_CREATED, EM_IRI, data) - def process_delete(self, req, collection_name: str, deposit_id: int) -> Dict: """Delete the container (deposit). diff --git a/swh/deposit/api/sword_edit.py b/swh/deposit/api/sword_edit.py new file mode 100644 --- /dev/null +++ b/swh/deposit/api/sword_edit.py @@ -0,0 +1,82 @@ +# Copyright (C) 2017-2020 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +from typing import Dict, Optional, Tuple + +from rest_framework import status + +from swh.storage import get_storage +from swh.storage.interface import StorageInterface + +from ..config import EDIT_IRI, EM_IRI +from ..parsers import SWHAtomEntryParser, SWHMultiPartParser +from .common import APIPost + + +class SwordEditAPI(APIPost): + """Deposit request class defining api endpoints for sword deposit. + + What's known as 'SE-IRI' in the sword specification. + + HTTP verbs supported: POST + + """ + + parser_classes = (SWHMultiPartParser, SWHAtomEntryParser) + + def __init__(self): + super().__init__() + self.storage_metadata: StorageInterface = get_storage( + **self.config["storage_metadata"] + ) + + def process_post( + self, + request, + headers: Dict, + collection_name: str, + deposit_id: Optional[int] = None, + ) -> Tuple[int, str, Dict]: + """Add new metadata/archive to existing deposit. + + This allows the following scenarios to occur: + + - multipart: Add new metadata and archive to a deposit in status partial with + the provided ones. + + - empty atom: Allows to finalize a deposit in status partial (transition to + deposited). + + source: + - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_addingcontent_metadata + - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_addingcontent_multipart + - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#continueddeposit_complete + + Returns: + In optimal case for a multipart and atom-entry update, a + 201 Created response. The body response will hold a + deposit. And the response headers will contain an entry + 'Location' with the EM-IRI. + + For the empty post case, this returns a 200. + + """ # noqa + assert deposit_id is not None + if request.content_type.startswith("multipart/"): + data = self._multipart_upload( + request, headers, collection_name, deposit_id=deposit_id + ) + return (status.HTTP_201_CREATED, EM_IRI, data) + + content_length = headers["content-length"] or 0 + if content_length == 0 and headers["in-progress"] is False: + # check for final empty post + data = self._empty_post(request, headers, collection_name, deposit_id) + return (status.HTTP_200_OK, EDIT_IRI, data) + + data = self._atom_entry( + request, headers, collection_name, deposit_id=deposit_id + ) + return (status.HTTP_201_CREATED, EM_IRI, data) diff --git a/swh/deposit/api/urls.py b/swh/deposit/api/urls.py --- a/swh/deposit/api/urls.py +++ b/swh/deposit/api/urls.py @@ -10,13 +10,14 @@ from django.conf.urls import url from django.shortcuts import render -from ..config import COL_IRI, CONT_FILE_IRI, EDIT_SE_IRI, EM_IRI, SD_IRI, STATE_IRI +from ..config import COL_IRI, CONT_FILE_IRI, EDIT_IRI, EM_IRI, SD_IRI, SE_IRI, STATE_IRI from .collection import CollectionAPI from .content import ContentAPI from .edit import EditAPI from .edit_media import EditMediaAPI from .service_document import ServiceDocumentAPI from .state import StateAPI +from .sword_edit import SwordEditAPI def api_view(req): @@ -42,13 +43,19 @@ name=EM_IRI, ), # Edit IRI - Atom Entry Edit IRI (update metadata IRI) - # SE IRI - Sword Edit IRI ;; possibly same as Edit IRI # -> PUT (update in place) + # -> DELETE (delete container) + url( + r"^(?P[^/]+)/(?P[^/]+)/atom/$", + EditAPI.as_view(), + name=EDIT_IRI, + ), + # SE IRI - Sword Edit IRI ;; possibly same as Edit IRI # -> POST (add new metadata) url( r"^(?P[^/]+)/(?P[^/]+)/metadata/$", - EditAPI.as_view(), - name=EDIT_SE_IRI, + SwordEditAPI.as_view(), + name=SE_IRI, ), # State IRI # -> GET diff --git a/swh/deposit/config.py b/swh/deposit/config.py --- a/swh/deposit/config.py +++ b/swh/deposit/config.py @@ -14,7 +14,8 @@ from swh.storage.interface import StorageInterface # IRIs (Internationalized Resource identifier) sword 2.0 specified -EDIT_SE_IRI = "edit_se_iri" +EDIT_IRI = "edit_iri" +SE_IRI = "se_iri" EM_IRI = "em_iri" CONT_FILE_IRI = "cont_file_iri" SD_IRI = "servicedocument" diff --git a/swh/deposit/templates/deposit/deposit_receipt.xml b/swh/deposit/templates/deposit/deposit_receipt.xml --- a/swh/deposit/templates/deposit/deposit_receipt.xml +++ b/swh/deposit/templates/deposit/deposit_receipt.xml @@ -7,11 +7,11 @@ {{ status }} - + - + diff --git a/swh/deposit/tests/api/test_collection.py b/swh/deposit/tests/api/test_collection.py --- a/swh/deposit/tests/api/test_collection.py +++ b/swh/deposit/tests/api/test_collection.py @@ -15,7 +15,7 @@ DEPOSIT_STATUS_LOAD_SUCCESS, DEPOSIT_STATUS_PARTIAL, DEPOSIT_STATUS_REJECTED, - EDIT_SE_IRI, + SE_IRI, ) from swh.deposit.models import Deposit from swh.deposit.parsers import parse_xml @@ -89,7 +89,7 @@ deposit = rejected_deposit response = authenticated_client.post( - reverse(EDIT_SE_IRI, args=[deposit.collection.name, deposit.id]), + reverse(SE_IRI, args=[deposit.collection.name, deposit.id]), content_type="application/atom+xml;type=entry", data=atom_dataset["entry-data1"], HTTP_SLUG=deposit.external_id, diff --git a/swh/deposit/tests/api/test_collection_post_binary.py b/swh/deposit/tests/api/test_collection_post_binary.py --- a/swh/deposit/tests/api/test_collection_post_binary.py +++ b/swh/deposit/tests/api/test_collection_post_binary.py @@ -123,11 +123,11 @@ assert int(response_content["deposit_id"]) == deposit.id assert response_content["deposit_status"] == deposit.status - edit_se_iri = reverse("edit_se_iri", args=[deposit_collection.name, deposit.id]) + edit_iri = reverse("edit_iri", args=[deposit_collection.name, deposit.id]) assert response._headers["location"] == ( "Location", - "http://testserver" + edit_se_iri, + "http://testserver" + edit_iri, ) @@ -465,7 +465,8 @@ # updating/adding is forbidden # uri to update the content - edit_se_iri = reverse("edit_se_iri", args=[deposit_collection.name, deposit_id]) + edit_iri = reverse("edit_iri", args=[deposit_collection.name, deposit_id]) + se_iri = reverse("se_iri", args=[deposit_collection.name, deposit_id]) em_iri = reverse("em_iri", args=[deposit_collection.name, deposit_id]) # Testing all update/add endpoint should fail @@ -512,7 +513,7 @@ # replacing metadata is no longer possible since the deposit's # status is ready r = authenticated_client.put( - edit_se_iri, + edit_iri, content_type="application/atom+xml;type=entry", data=atom_dataset["entry-data-deposit-binary"], CONTENT_LENGTH=len(atom_dataset["entry-data-deposit-binary"]), @@ -525,7 +526,7 @@ # adding new metadata is no longer possible since the # deposit's status is ready r = authenticated_client.post( - edit_se_iri, + se_iri, content_type="application/atom+xml;type=entry", data=atom_dataset["entry-data-deposit-binary"], CONTENT_LENGTH=len(atom_dataset["entry-data-deposit-binary"]), @@ -557,7 +558,7 @@ # replacing multipart metadata is no longer possible since the # deposit's status is ready r = authenticated_client.put( - edit_se_iri, + edit_iri, format="multipart", data={"archive": archive, "atom_entry": atom_entry,}, ) @@ -568,7 +569,7 @@ # adding new metadata is no longer possible since the # deposit's status is ready r = authenticated_client.post( - edit_se_iri, + se_iri, format="multipart", data={"archive": archive, "atom_entry": atom_entry,}, ) diff --git a/swh/deposit/tests/api/test_delete.py b/swh/deposit/tests/api/test_delete.py --- a/swh/deposit/tests/api/test_delete.py +++ b/swh/deposit/tests/api/test_delete.py @@ -12,7 +12,7 @@ from swh.deposit.config import ( ARCHIVE_KEY, DEPOSIT_STATUS_DEPOSITED, - EDIT_SE_IRI, + EDIT_IRI, EM_IRI, METADATA_KEY, ) @@ -99,7 +99,7 @@ deposit = partial_deposit # when - url = reverse(EDIT_SE_IRI, args=[deposit_collection.name, deposit.id]) + url = reverse(EDIT_IRI, args=[deposit_collection.name, deposit.id]) response = authenticated_client.delete(url) # then assert response.status_code == status.HTTP_204_NO_CONTENT @@ -109,7 +109,7 @@ assert deposits == [] -def test_delete_on_edit_se_iri_cannot_delete_non_partial_deposit( +def test_delete_on_edit_iri_cannot_delete_non_partial_deposit( authenticated_client, deposit_collection, complete_deposit ): """Delete !partial deposit should return a 400 response @@ -119,7 +119,7 @@ deposit = complete_deposit # when - url = reverse(EDIT_SE_IRI, args=[deposit_collection.name, deposit.id]) + url = reverse(EDIT_IRI, args=[deposit_collection.name, deposit.id]) response = authenticated_client.delete(url) # then assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/swh/deposit/tests/api/test_deposit_private_read_metadata.py b/swh/deposit/tests/api/test_deposit_private_read_metadata.py --- a/swh/deposit/tests/api/test_deposit_private_read_metadata.py +++ b/swh/deposit/tests/api/test_deposit_private_read_metadata.py @@ -7,7 +7,7 @@ from rest_framework import status from swh.deposit import __version__, utils -from swh.deposit.config import EDIT_SE_IRI, PRIVATE_GET_DEPOSIT_METADATA, SWH_PERSON +from swh.deposit.config import PRIVATE_GET_DEPOSIT_METADATA, SE_IRI, SWH_PERSON from swh.deposit.models import Deposit from swh.deposit.parsers import parse_xml @@ -26,7 +26,7 @@ def update_deposit_with_metadata(authenticated_client, collection, deposit, metadata): # update deposit's metadata response = authenticated_client.post( - reverse(EDIT_SE_IRI, args=[collection.name, deposit.id]), + reverse(SE_IRI, args=[collection.name, deposit.id]), content_type="application/atom+xml;type=entry", data=metadata, HTTP_SLUG=deposit.external_id, diff --git a/swh/deposit/tests/api/test_deposit_update.py b/swh/deposit/tests/api/test_deposit_update.py --- a/swh/deposit/tests/api/test_deposit_update.py +++ b/swh/deposit/tests/api/test_deposit_update.py @@ -14,8 +14,9 @@ from swh.deposit.config import ( DEPOSIT_STATUS_DEPOSITED, DEPOSIT_STATUS_PARTIAL, - EDIT_SE_IRI, + EDIT_IRI, EM_IRI, + SE_IRI, APIConfig, ) from swh.deposit.models import Deposit, DepositCollection, DepositRequest @@ -57,7 +58,7 @@ assert len(requests) == 0 response = authenticated_client.post( - reverse(EDIT_SE_IRI, args=[deposit_collection.name, deposit.id]), + reverse(SE_IRI, args=[deposit_collection.name, deposit.id]), content_type="application/atom+xml;type=entry", data=atom_dataset["entry-data1"], HTTP_SLUG=deposit.external_id, @@ -120,7 +121,7 @@ requests_archive0 = DepositRequest.objects.filter(deposit=deposit, type="archive") assert len(requests_archive0) == 1 - update_uri = reverse(EDIT_SE_IRI, args=[deposit_collection.name, deposit.id]) + update_uri = reverse(EDIT_IRI, args=[deposit_collection.name, deposit.id]) response = authenticated_client.put( update_uri, @@ -221,7 +222,7 @@ requests_archive0 = DepositRequest.objects.filter(deposit=deposit, type="archive") assert len(requests_archive0) == 1 - update_uri = reverse(EDIT_SE_IRI, args=[deposit_collection.name, deposit.id]) + update_uri = reverse(SE_IRI, args=[deposit_collection.name, deposit.id]) atom_entry = atom_dataset["entry-data1"] response = authenticated_client.post( @@ -267,7 +268,7 @@ requests_archive0 = DepositRequest.objects.filter(deposit=deposit, type="archive") assert len(requests_archive0) == 1 - update_uri = reverse(EDIT_SE_IRI, args=[deposit_collection.name, deposit.id]) + update_uri = reverse(EDIT_IRI, args=[deposit_collection.name, deposit.id]) archive = InMemoryUploadedFile( BytesIO(sample_archive["data"]), field_name=sample_archive["name"], @@ -287,7 +288,7 @@ charset="utf-8", ) - update_uri = reverse(EDIT_SE_IRI, args=[deposit_collection.name, deposit.id]) + update_uri = reverse(SE_IRI, args=[deposit_collection.name, deposit.id]) response = authenticated_client.post( update_uri, format="multipart", @@ -327,7 +328,7 @@ deposit = partial_deposit_with_metadata assert deposit.status == DEPOSIT_STATUS_PARTIAL - update_uri = reverse(EDIT_SE_IRI, args=[deposit_collection.name, deposit.id]) + update_uri = reverse(SE_IRI, args=[deposit_collection.name, deposit.id]) response = authenticated_client.post( update_uri, content_type="application/atom+xml;type=entry", @@ -353,7 +354,7 @@ except Deposit.DoesNotExist: assert True - url = reverse(EDIT_SE_IRI, args=[deposit_collection, unknown_deposit_id]) + url = reverse(SE_IRI, args=[deposit_collection, unknown_deposit_id]) response = authenticated_client.post( url, content_type="application/atom+xml;type=entry", @@ -377,7 +378,7 @@ except DepositCollection.DoesNotExist: assert True - url = reverse(EDIT_SE_IRI, args=[unknown_collection_name, deposit.id]) + url = reverse(SE_IRI, args=[unknown_collection_name, deposit.id]) response = authenticated_client.post( url, content_type="application/atom+xml;type=entry", @@ -399,7 +400,7 @@ Deposit.objects.get(pk=unknown_deposit_id) except Deposit.DoesNotExist: assert True - url = reverse(EDIT_SE_IRI, args=[deposit_collection.name, unknown_deposit_id]) + url = reverse(EDIT_IRI, args=[deposit_collection.name, unknown_deposit_id]) response = authenticated_client.put( url, content_type="application/atom+xml;type=entry", @@ -546,7 +547,7 @@ charset="utf-8", ) - update_uri = reverse(EDIT_SE_IRI, args=[deposit_collection.name, deposit.id]) + update_uri = reverse(EDIT_IRI, args=[deposit_collection.name, deposit.id]) response = authenticated_client.put( update_uri, format="multipart", @@ -612,9 +613,7 @@ ) nb_metadata = len(actual_existing_requests_metadata) - update_uri = reverse( - EDIT_SE_IRI, args=[deposit_collection.name, complete_deposit.id] - ) + update_uri = reverse(EDIT_IRI, args=[deposit_collection.name, complete_deposit.id]) response = authenticated_client.put( update_uri, content_type="application/atom+xml;type=entry", @@ -700,9 +699,7 @@ incorrect_swhid = "swh:1:dir:ef04a768181417fbc5eef4243e2507915f24deea" assert complete_deposit.swhid != incorrect_swhid - update_uri = reverse( - EDIT_SE_IRI, args=[deposit_collection.name, complete_deposit.id] - ) + update_uri = reverse(EDIT_IRI, args=[deposit_collection.name, complete_deposit.id]) response = authenticated_client.put( update_uri, content_type="application/atom+xml;type=entry", @@ -727,9 +724,7 @@ Response: 400 """ - update_uri = reverse( - EDIT_SE_IRI, args=[deposit_collection.name, complete_deposit.id] - ) + update_uri = reverse(EDIT_IRI, args=[deposit_collection.name, complete_deposit.id]) response = authenticated_client.put( update_uri, content_type="application/atom+xml;type=entry", @@ -754,9 +749,7 @@ Response: 400 """ - update_uri = reverse( - EDIT_SE_IRI, args=[deposit_collection.name, complete_deposit.id] - ) + update_uri = reverse(EDIT_IRI, args=[deposit_collection.name, complete_deposit.id]) for atom_key in ["entry-data-empty-body", "entry-data-empty-body-no-namespace"]: atom_content = atom_dataset[atom_key] @@ -784,9 +777,7 @@ Response: 400 """ - update_uri = reverse( - EDIT_SE_IRI, args=[deposit_collection.name, complete_deposit.id] - ) + update_uri = reverse(EDIT_IRI, args=[deposit_collection.name, complete_deposit.id]) response = authenticated_client.put( update_uri, diff --git a/swh/deposit/tests/conftest.py b/swh/deposit/tests/conftest.py --- a/swh/deposit/tests/conftest.py +++ b/swh/deposit/tests/conftest.py @@ -28,7 +28,7 @@ DEPOSIT_STATUS_PARTIAL, DEPOSIT_STATUS_REJECTED, DEPOSIT_STATUS_VERIFIED, - EDIT_SE_IRI, + SE_IRI, setup_django_for, ) from swh.deposit.parsers import parse_xml @@ -315,7 +315,7 @@ ) response = authenticated_client.post( - reverse(EDIT_SE_IRI, args=[collection_name, deposit.id]), + reverse(SE_IRI, args=[collection_name, deposit.id]), content_type="application/atom+xml;type=entry", data=atom_dataset["entry-data0"] % deposit.external_id.encode("utf-8"), HTTP_SLUG=deposit.external_id,