Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F7163624
D5765.id.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Subscribers
None
D5765.id.diff
View Options
diff --git a/swh/deposit/api/collection_list.py b/swh/deposit/api/collection_list.py
new file mode 100644
--- /dev/null
+++ b/swh/deposit/api/collection_list.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2021 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 django.shortcuts import render
+from rest_framework import status
+from rest_framework.generics import ListAPIView
+
+from swh.deposit.api.common import APIBase
+from swh.deposit.api.utils import DefaultPagination, DepositSerializer
+from swh.deposit.models import Deposit
+
+
+class CollectionListAPI(ListAPIView, APIBase):
+ """Deposit request class to list the user deposits.
+
+ HTTP verbs supported: ``GET``
+
+ """
+
+ serializer_class = DepositSerializer
+ pagination_class = DefaultPagination
+
+ def get(self, request, *args, **kwargs):
+ """List the user's collection if the user has access to said collection.
+
+ """
+ self.checks(request, kwargs["collection_name"])
+ paginated_result = super().get(request, *args, **kwargs)
+ data = paginated_result.data
+ # Build pagination link headers
+ links = []
+ for link_name in ["next", "previous"]:
+ link = data.get(link_name)
+ if link is None:
+ continue
+ links.append(f'<{link}>; rel="{link_name}"')
+ response = render(
+ request,
+ "deposit/collection_list.xml",
+ context={
+ "count": data["count"],
+ "results": [dict(d) for d in data["results"]],
+ },
+ content_type="application/xml",
+ status=status.HTTP_200_OK,
+ )
+ response._headers["Link"] = ",".join(links)
+ return response
+
+ def get_queryset(self):
+ """List the deposits for the authenticated user (pagination is handled by the
+ `pagination_class` class attribute).
+
+ """
+ return Deposit.objects.filter(client=self.request.user.id).order_by("id")
diff --git a/swh/deposit/api/private/deposit_list.py b/swh/deposit/api/private/deposit_list.py
--- a/swh/deposit/api/private/deposit_list.py
+++ b/swh/deposit/api/private/deposit_list.py
@@ -1,41 +1,15 @@
-# Copyright (C) 2018-2020 The Software Heritage developers
+# Copyright (C) 2018-2021 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 rest_framework import serializers
-from rest_framework.fields import _UnvalidatedField
from rest_framework.generics import ListAPIView
-from rest_framework.pagination import PageNumberPagination
+
+from swh.deposit.api.utils import DefaultPagination, DepositSerializer
from . import APIPrivateView
from ...models import Deposit
-from ..converters import convert_status_detail
-
-
-class DefaultPagination(PageNumberPagination):
- page_size = 100
- page_size_query_param = "page_size"
-
-
-class StatusDetailField(_UnvalidatedField):
- """status_detail field is a dict, we want a simple message instead.
- So, we reuse the convert_status_detail from deposit_status
- endpoint to that effect.
-
- """
-
- def to_representation(self, value):
- return convert_status_detail(value)
-
-
-class DepositSerializer(serializers.ModelSerializer):
- status_detail = StatusDetailField()
-
- class Meta:
- model = Deposit
- fields = "__all__"
class APIList(ListAPIView, APIPrivateView):
@@ -55,7 +29,6 @@
# sql injection: A priori, nothing to worry about, django does it for
# queryset
# https://docs.djangoproject.com/en/3.0/topics/security/#sql-injection-protection # noqa
- # https://docs.djangoproject.com/en/2.2/topics/security/#sql-injection-protection # noqa
deposits = (
Deposit.objects.all()
.exclude(external_id__startswith=exclude_like)
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
@@ -1,4 +1,4 @@
-# Copyright (C) 2017-2020 The Software Heritage developers
+# Copyright (C) 2017-2021 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
@@ -10,8 +10,18 @@
from django.conf.urls import url
from django.shortcuts import render
-from ..config import COL_IRI, CONT_FILE_IRI, EDIT_IRI, EM_IRI, SD_IRI, SE_IRI, STATE_IRI
+from ..config import (
+ COL_IRI,
+ COLLECTION_LIST,
+ CONT_FILE_IRI,
+ EDIT_IRI,
+ EM_IRI,
+ SD_IRI,
+ SE_IRI,
+ STATE_IRI,
+)
from .collection import CollectionAPI
+from .collection_list import CollectionListAPI
from .content import ContentAPI
from .edit import EditAPI
from .edit_media import EditMediaAPI
@@ -73,4 +83,9 @@
), # specification is not clear about
# File-IRI, we assume it's the same as
# the Cont-IRI one
+ url(
+ r"^(?P<collection_name>[^/]+)/list/$",
+ CollectionListAPI.as_view(),
+ name=COLLECTION_LIST,
+ ),
]
diff --git a/swh/deposit/api/utils.py b/swh/deposit/api/utils.py
new file mode 100644
--- /dev/null
+++ b/swh/deposit/api/utils.py
@@ -0,0 +1,35 @@
+# Copyright (C) 2018-2021 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 rest_framework import serializers
+from rest_framework.fields import _UnvalidatedField
+from rest_framework.pagination import PageNumberPagination
+
+from swh.deposit.api.converters import convert_status_detail
+from swh.deposit.models import Deposit
+
+
+class DefaultPagination(PageNumberPagination):
+ page_size = 100
+ page_size_query_param = "page_size"
+
+
+class StatusDetailField(_UnvalidatedField):
+ """status_detail field is a dict, we want a simple message instead.
+ So, we reuse the convert_status_detail from deposit_status
+ endpoint to that effect.
+
+ """
+
+ def to_representation(self, value):
+ return convert_status_detail(value)
+
+
+class DepositSerializer(serializers.ModelSerializer):
+ status_detail = StatusDetailField()
+
+ class Meta:
+ model = Deposit
+ fields = "__all__"
diff --git a/swh/deposit/config.py b/swh/deposit/config.py
--- a/swh/deposit/config.py
+++ b/swh/deposit/config.py
@@ -22,6 +22,7 @@
SD_IRI = "servicedocument"
COL_IRI = "upload"
STATE_IRI = "state_iri"
+COLLECTION_LIST = "collection-list"
PRIVATE_GET_RAW_CONTENT = "private-download"
PRIVATE_CHECK_DEPOSIT = "check-deposit"
PRIVATE_PUT_DEPOSIT = "private-update"
diff --git a/swh/deposit/templates/deposit/collection_list.xml b/swh/deposit/templates/deposit/collection_list.xml
new file mode 100644
--- /dev/null
+++ b/swh/deposit/templates/deposit/collection_list.xml
@@ -0,0 +1,18 @@
+<entry xmlns="http://www.w3.org/2005/Atom"
+ xmlns:sword="http://purl.org/net/sword/terms/"
+ xmlns:dcterms="http://purl.org/dc/terms/"
+ xmlns:sd="https://www.softwareheritage.org/schema/2018/deposit"
+ >
+ <sd:count>{{ count }}</sd:count>
+ <sd:deposits>
+ {% for deposit in results %}
+ <sd:deposit>
+ {% for key, value in deposit.items %}
+ {% if value is not None %}
+ <sd:{{ key }}>{{ value }}</sd:{{ key }}>
+ {% endif %}
+ {% endfor %}
+ </sd:deposit>
+ {% endfor %}
+ </sd:deposits>
+</entry>
diff --git a/swh/deposit/tests/api/test_collection_list.py b/swh/deposit/tests/api/test_collection_list.py
new file mode 100644
--- /dev/null
+++ b/swh/deposit/tests/api/test_collection_list.py
@@ -0,0 +1,117 @@
+# Copyright (C) 2017-2021 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 io import BytesIO
+
+from django.urls import reverse_lazy as reverse
+from requests.utils import parse_header_links
+from rest_framework import status
+
+from swh.deposit.config import (
+ COLLECTION_LIST,
+ DEPOSIT_STATUS_DEPOSITED,
+ DEPOSIT_STATUS_PARTIAL,
+)
+from swh.deposit.models import DepositCollection
+from swh.deposit.parsers import parse_xml
+
+
+def test_deposit_collection_list_is_auth_protected(anonymous_client):
+ """Deposit list should require authentication
+
+ """
+ url = reverse(COLLECTION_LIST, args=("test",))
+ response = anonymous_client.get(url)
+ assert response.status_code == status.HTTP_401_UNAUTHORIZED
+ assert b"protected by basic authentication" in response.content
+
+
+def test_deposit_collection_list_collection_access_restricted_to_user_coll(
+ deposit_another_collection, deposit_user, authenticated_client
+):
+ """Deposit list api should restrict access to user's collection
+
+ """
+ collection_id = authenticated_client.deposit_client.collections[0]
+ coll = DepositCollection.objects.get(pk=collection_id)
+ # authenticated_client has access to the "coll" collection
+ coll2 = deposit_another_collection
+ assert coll.name != coll2.name
+ # but does not have access to that coll2 collection
+ url = reverse(COLLECTION_LIST, args=(coll2.name,))
+ response = authenticated_client.get(url)
+ # so it gets rejected access to the listing of that coll2 collection
+ assert response.status_code == status.HTTP_403_FORBIDDEN
+ msg = f"{deposit_user.username} cannot access collection {coll2.name}"
+ assert msg in response.content.decode("utf-8")
+
+
+def test_deposit_collection_list_nominal(
+ partial_deposit, deposited_deposit, authenticated_client
+):
+ """Deposit list api should return the user deposits in a paginated way
+
+ """
+ client_id = authenticated_client.deposit_client.id
+ assert partial_deposit.client.id == client_id
+ assert deposited_deposit.client.id == client_id
+ # Both deposit were deposited by the authenticated client
+ # so requesting the listing of the deposits, both should be listed
+
+ deposit_id = str(partial_deposit.id)
+ deposit_id2 = str(deposited_deposit.id)
+ coll = partial_deposit.collection
+ # requesting the listing of the deposit for the user's collection
+ url = reverse(COLLECTION_LIST, args=(coll.name,))
+ response = authenticated_client.get(f"{url}?page_size=1")
+ assert response.status_code == status.HTTP_200_OK
+
+ data = parse_xml(BytesIO(response.content))
+ assert (
+ data["swh:count"] == "2"
+ ) # total result of 2 deposits if consuming all results
+ header_link = parse_header_links(response._headers["Link"])
+ assert len(header_link) == 1 # only 1 next link
+ expected_next = f"{url}?page=2&page_size=1"
+ assert header_link[0]["url"].endswith(expected_next)
+ assert header_link[0]["rel"] == "next"
+
+ # only one deposit in the response
+ deposit = data["swh:deposits"]["swh:deposit"] # dict as only 1 value (a-la js)
+ assert isinstance(deposit, dict)
+ assert deposit["swh:id"] == deposit_id
+ assert deposit["swh:status"] == DEPOSIT_STATUS_PARTIAL
+
+ # then 2nd page
+ response2 = authenticated_client.get(expected_next)
+
+ assert response2.status_code == status.HTTP_200_OK
+ data2 = parse_xml(BytesIO(response2.content))
+ assert data2["swh:count"] == "2" # still total of 2 deposits across all results
+
+ expected_previous = f"{url}?page_size=1"
+ header_link2 = parse_header_links(response2._headers["Link"])
+ assert len(header_link2) == 1 # only 1 previous link
+ assert header_link2[0]["url"].endswith(expected_previous)
+ assert header_link2[0]["rel"] == "previous"
+
+ # only 1 deposit in the response
+ deposit2 = data2["swh:deposits"]["swh:deposit"] # dict as only 1 value (a-la js)
+ assert isinstance(deposit2, dict)
+ assert deposit2["swh:id"] == deposit_id2
+ assert deposit2["swh:status"] == DEPOSIT_STATUS_DEPOSITED
+
+ # Retrieve every deposit in one query (no page_size parameter)
+ response3 = authenticated_client.get(url)
+ assert response3.status_code == status.HTTP_200_OK
+ data3 = parse_xml(BytesIO(response3.content))
+ assert data3["swh:count"] == "2" # total result of 2 deposits across all results
+ deposits3 = data3["swh:deposits"]["swh:deposit"] # list here
+ assert isinstance(deposits3, list)
+ assert len(deposits3) == 2
+ header_link3 = parse_header_links(response3._headers["Link"])
+ assert header_link3 == [] # no pagination as all results received in one round
+ assert deposit in deposits3
+ assert deposit2 in deposits3
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Jan 30, 11:23 AM (1 w, 20 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3216285
Attached To
D5765: Open a paginated list user deposits endpoint
Event Timeline
Log In to Comment