This commit is contained in:
j3d1 2026-08-17 18:28:01 +02:00
parent 25cef95711
commit 7d7730354e
8 changed files with 381 additions and 39 deletions

View file

@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from authentication.models import ToolshedUser, KnownIdentity
from authentication.signature_auth import SignatureAuthentication
from authentication.signature_auth import SignatureAuthentication, split_userhandle_or_throw
from files.models import File
from toolshed.models import InventoryItem, StorageLocation, WorkflowInstance
from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer, WorkflowInstanceSerializer
@ -76,6 +76,37 @@ def search_inventory_items(request):
return Response({'error': 'No query provided.'}, status=400)
@api_view(['GET'])
@authentication_classes([SignatureAuthentication])
@permission_classes([IsAuthenticated])
def get_shared_item(request, handle, id):
"""Fetch a single item by its owner's handle (username@domain) and local id, e.g. for the
/i/<handle>/<id> item URL (see docs/design-in-progress/items-labels.md) or the
/inventory/shared/<handle>/<id> in-app view. Unlike InventoryItemViewSet, which only ever
returns the requester's own items, this looks the item up by owner instead of by requester,
so it's the only endpoint that can serve a friend's item - subject to the same
friends-or-self and availability_policy checks getUserProfile/_accessible_files already
use elsewhere."""
try:
username, domain = split_userhandle_or_throw(handle)
except ValueError:
return Response(status=400)
try:
owner = ToolshedUser.objects.get(username=username, domain=domain)
except ToolshedUser.DoesNotExist:
return Response(status=404)
if owner not in request.user.friends_or_self():
return Response(status=403)
try:
item = owner.inventory_items.get(id=id)
except InventoryItem.DoesNotExist:
return Response(status=404)
is_owner = request.user.user.filter(pk=owner.pk).exists()
if item.availability_policy == 'private' and not is_owner:
return Response(status=403)
return Response(InventoryItemSerializer(item).data)
class StorageLocationViewSet(viewsets.ModelViewSet):
serializer_class = StorageLocationSerializer
authentication_classes = [SignatureAuthentication]
@ -136,4 +167,5 @@ router.register(r'workflows', WorkflowInstanceViewSet, basename='workflows')
urlpatterns = router.urls + [
path('search/', search_inventory_items, name='search_inventory_items'),
path('inventory_items/<str:handle>/<int:id>/', get_shared_item, name='shared_inventory_item'),
]

View file

@ -213,6 +213,47 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 0)
def test_get_shared_item_as_friend(self):
reply = client.get('/api/inventory_items/testuser1@example.com/' + str(self.f['item1'].id) + '/',
self.f['local_user2'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(reply.json()['name'], 'test1')
self.assertEqual(reply.json()['owner'], 'testuser1@example.com')
def test_get_shared_item_as_owner(self):
reply = client.get('/api/inventory_items/testuser1@example.com/' + str(self.f['item1'].id) + '/',
self.f['local_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(reply.json()['name'], 'test1')
def test_get_shared_item_not_friend(self):
reply = client.get('/api/inventory_items/testuser1@example.com/' + str(self.f['item1'].id) + '/',
self.f['ext_user1'])
self.assertEqual(reply.status_code, 403)
def test_get_shared_item_private(self):
private_item = InventoryItem.objects.create(
owner=self.f['local_user1'], owned_quantity=1, name='secret', availability_policy='private')
reply = client.get('/api/inventory_items/testuser1@example.com/' + str(private_item.id) + '/',
self.f['local_user2'])
self.assertEqual(reply.status_code, 403)
reply = client.get('/api/inventory_items/testuser1@example.com/' + str(private_item.id) + '/',
self.f['local_user1'])
self.assertEqual(reply.status_code, 200)
def test_get_shared_item_unknown_handle(self):
reply = client.get('/api/inventory_items/nobody@example.com/' + str(self.f['item1'].id) + '/',
self.f['local_user2'])
self.assertEqual(reply.status_code, 404)
def test_get_shared_item_unknown_id(self):
reply = client.get('/api/inventory_items/testuser1@example.com/99999/', self.f['local_user2'])
self.assertEqual(reply.status_code, 404)
def test_get_shared_item_bad_handle(self):
reply = client.get('/api/inventory_items/testuser1/' + str(self.f['item1'].id) + '/', self.f['local_user2'])
self.assertEqual(reply.status_code, 400)
class TestInventoryItemWithFileApiTestCase(UserTestMixin, FilesTestMixin, InventoryTestMixin, ToolshedTestCase):
def setUp(self):