stash
This commit is contained in:
parent
d606de8773
commit
2218cc3543
12 changed files with 393 additions and 2204 deletions
|
|
@ -55,8 +55,8 @@ def post_item_file(request, item_id):
|
|||
if not request.user.user.exists():
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
try:
|
||||
file = File.objects.get(hash=request.data['file_hash'],
|
||||
staged_by_workflows__owner=request.user.user.get())
|
||||
file = File.objects.filter(hash=request.data['file_hash'],
|
||||
staged_by_workflows__owner=request.user.user.get()).distinct().get()
|
||||
except File.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
item.files.add(file)
|
||||
|
|
@ -101,6 +101,31 @@ def item_files(request, item_id, format=None):
|
|||
return post_item_file(request, item_id)
|
||||
|
||||
|
||||
@api_view(['DELETE'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
@authentication_classes([SignatureAuthenticationLocal])
|
||||
def delete_file(request, file_id, format=None):
|
||||
try:
|
||||
file = File.objects.get(id=file_id)
|
||||
except File.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
# Only detach from items this identity is actually authorized to act on - a file is
|
||||
# content-addressed and can be shared by other users'/groups' items via the same hash,
|
||||
# so it must never be removed from connections this request has no authority over.
|
||||
authorized_items = file.connected_items.filter(
|
||||
Q(owner=request.user) | Q(owner_group__in=request.user.public_identity.member_of_groups.all())
|
||||
)
|
||||
if not authorized_items.exists():
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
for item in authorized_items:
|
||||
item.files.remove(file)
|
||||
if file.connected_items.count() == 0 and file.profile_picture_users.count() == 0 \
|
||||
and file.staged_by_workflows.count() == 0:
|
||||
file.file.delete(save=False)
|
||||
file.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@api_view(['DELETE'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
@authentication_classes([SignatureAuthentication])
|
||||
|
|
@ -151,6 +176,7 @@ def delete_staged_file(request, workflow_id, file_hash, format=None):
|
|||
|
||||
urlpatterns = [
|
||||
path('files/', list_all_files),
|
||||
path('files/<int:file_id>/', delete_file),
|
||||
path('item_files/<int:item_id>/', item_files),
|
||||
path('item_files/<int:item_id>/<int:file_id>/', delete_item_file),
|
||||
path('staged_files/<int:workflow_id>/', staged_files),
|
||||
|
|
|
|||
|
|
@ -49,15 +49,21 @@ class InventoryItemViewSet(viewsets.ModelViewSet):
|
|||
if owner_group:
|
||||
if not owner_group.is_member(identity):
|
||||
return InventoryItem.objects.none()
|
||||
return InventoryItem.objects.filter(owner_group=owner_group)
|
||||
if owner_user:
|
||||
queryset = InventoryItem.objects.filter(owner_group=owner_group)
|
||||
elif owner_user:
|
||||
if owner_user not in identity.friends_or_self():
|
||||
return InventoryItem.objects.none()
|
||||
queryset = InventoryItem.objects.filter(owner=owner_user)
|
||||
if not identity.user.filter(pk=owner_user.pk).exists():
|
||||
queryset = queryset.exclude(availability_policy='private')
|
||||
return queryset
|
||||
return InventoryItem.objects.none()
|
||||
else:
|
||||
return InventoryItem.objects.none()
|
||||
# InventoryItemSerializer touches owner/owner_group/category/storage_location (FKs) and
|
||||
# tags/files/itemproperty_set (M2M/reverse-FK) for every row - without this, listing N
|
||||
# items costs ~5N extra queries (each a full network round trip once every other API
|
||||
# call also goes through the federation ServerSet layer, not just local Django).
|
||||
return queryset.select_related('owner', 'owner_group', 'category', 'storage_location').prefetch_related(
|
||||
'tags', 'files', 'itemproperty_set__property')
|
||||
|
||||
def perform_create(self, serializer):
|
||||
try:
|
||||
|
|
@ -209,6 +215,18 @@ class WorkflowInstanceViewSet(viewsets.ModelViewSet):
|
|||
file.file.delete(save=False)
|
||||
file.delete()
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def update_step(self, request, pk=None):
|
||||
# get_queryset already scopes to the request user's own workflows, so get_object 404s for anyone else's.
|
||||
instance = self.get_object()
|
||||
with transaction.atomic():
|
||||
if 'current_step' in request.data:
|
||||
instance.current_step = request.data['current_step']
|
||||
if 'payload' in request.data:
|
||||
instance.payload = request.data['payload']
|
||||
instance.save()
|
||||
return Response(self.get_serializer(instance).data)
|
||||
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r'inventory_items/(?P<handle>[^/]+)', InventoryItemViewSet, basename='inventory_items')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from django.test import Client
|
||||
from authentication.models import Group
|
||||
from authentication.models import Group, ToolshedUser
|
||||
from authentication.tests import SignatureAuthClient, UserTestMixin, GroupTestMixin, ToolshedTestCase
|
||||
from files.tests import FilesTestMixin
|
||||
from toolshed.models import File, InventoryItem
|
||||
|
|
@ -140,6 +140,49 @@ class FileApiTestCase(UserTestMixin, FilesTestMixin, InventoryTestMixin, Toolshe
|
|||
self.assertEqual(File.objects.count(), 3)
|
||||
self.assertEqual(self.f['item1'].files.count(), 2)
|
||||
|
||||
def test_delete_file_top_level(self):
|
||||
response = client.delete(f"/api/v1/files/{self.f['test_file2'].id}/", self.f['local_user1'])
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertEqual(File.objects.count(), 2)
|
||||
self.assertEqual(self.f['item1'].files.count(), 1)
|
||||
|
||||
def test_delete_file_top_level_removes_from_all_owned_items(self):
|
||||
response = client.delete(f"/api/v1/files/{self.f['test_file1'].id}/", self.f['local_user1'])
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertEqual(File.objects.count(), 2)
|
||||
self.assertEqual(self.f['item1'].files.count(), 1)
|
||||
self.assertEqual(self.f['item2'].files.count(), 0)
|
||||
|
||||
def test_delete_file_top_level_not_found(self):
|
||||
response = client.delete(f"/api/v1/files/99999/", self.f['local_user1'])
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(File.objects.count(), 3)
|
||||
|
||||
def test_delete_file_top_level_not_owner(self):
|
||||
response = client.delete(f"/api/v1/files/{self.f['test_file1'].id}/", self.f['local_user2'])
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(File.objects.count(), 3)
|
||||
self.assertEqual(self.f['item1'].files.count(), 2)
|
||||
|
||||
def test_delete_file_top_level_anonymous(self):
|
||||
response = anonymous_client.delete(f"/api/v1/files/{self.f['test_file1'].id}/")
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(File.objects.count(), 3)
|
||||
|
||||
def test_delete_file_top_level_only_detaches_own_items_when_shared_by_hash(self):
|
||||
other_item = InventoryItem.create_for_owner(
|
||||
owner=self.f['local_user2'], owned_quantity=1, name='other-user-item',
|
||||
availability_policy='private')
|
||||
other_item.files.add(self.f['test_file1'])
|
||||
|
||||
response = client.delete(f"/api/v1/files/{self.f['test_file1'].id}/", self.f['local_user1'])
|
||||
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertEqual(self.f['item1'].files.count(), 1)
|
||||
self.assertEqual(self.f['item2'].files.count(), 0)
|
||||
self.assertEqual(other_item.files.count(), 1)
|
||||
self.assertEqual(File.objects.filter(id=self.f['test_file1'].id).count(), 1)
|
||||
|
||||
def test_get_inventory(self):
|
||||
reply = client.get('/api/v1/inventory_items/{}/'.format(self.f['local_user1']), self.f['local_user1'])
|
||||
self.assertEqual(reply.status_code, 200)
|
||||
|
|
@ -215,6 +258,18 @@ class GroupOwnedFileApiTestCase(UserTestMixin, GroupTestMixin, FilesTestMixin, T
|
|||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(self.f['group_item'].files.count(), 1)
|
||||
|
||||
def test_other_member_can_delete_file_top_level(self):
|
||||
response = client.delete(f"/api/v1/files/{self.f['test_file1'].id}/", self.f['local_user2'])
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertEqual(self.f['group_item'].files.count(), 0)
|
||||
|
||||
def test_non_member_cannot_delete_file_top_level(self):
|
||||
outsider = ToolshedUser.objects.create_user('testuser3', 'test3@abc.de', 'testpassword4',
|
||||
domain=self.f['example_com'].name)
|
||||
response = client.delete(f"/api/v1/files/{self.f['test_file1'].id}/", outsider)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(self.f['group_item'].files.count(), 1)
|
||||
|
||||
def test_item_files_when_id_collides_across_two_groups(self):
|
||||
group2 = Group.objects.create(name='group2', domain=self.f['example_com'].name)
|
||||
group2.members.add(self.f['local_user2'].public_identity)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue