218 lines
9.4 KiB
Python
218 lines
9.4 KiB
Python
from django.db import transaction
|
|
from django.urls import path
|
|
from rest_framework import routers, viewsets, status
|
|
from rest_framework.decorators import authentication_classes, api_view, permission_classes, action
|
|
from rest_framework.exceptions import NotFound, PermissionDenied
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
|
|
from authentication.models import ToolshedUser, KnownIdentity, Group
|
|
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
|
|
|
|
router = routers.SimpleRouter()
|
|
|
|
|
|
def inventory_items(identity):
|
|
try:
|
|
user = identity.user.get()
|
|
if user:
|
|
for item in user.inventory_items.all():
|
|
yield item
|
|
except ToolshedUser.DoesNotExist:
|
|
pass
|
|
for friend in identity.friends.all():
|
|
friend_user = friend.user.first()
|
|
if friend_user:
|
|
for item in friend_user.inventory_items.all():
|
|
if item.availability_policy != 'private':
|
|
yield item
|
|
|
|
|
|
class InventoryItemViewSet(viewsets.ModelViewSet):
|
|
serializer_class = InventoryItemSerializer
|
|
authentication_classes = [SignatureAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
# Detail routes address an item by its owner-scoped id, not the internal row id - the
|
|
# router still names the URL capture group 'pk', so keep that as lookup_url_kwarg and just
|
|
# change which model field it's matched against. get_queryset() below is always already
|
|
# scoped to the requester's own items/groups, so this can't cross into another owner's ids.
|
|
lookup_field = 'id'
|
|
lookup_url_kwarg = 'pk'
|
|
|
|
def get_queryset(self):
|
|
# A KnownIdentity acting purely as a group member (e.g. a remote member on a group
|
|
# hosted on this backend) never has a local ToolshedUser account here - group-owned
|
|
# items must stay reachable for such an identity, only personal ("owner=...") items
|
|
# require .user.exists().
|
|
if type(self.request.user) != KnownIdentity:
|
|
return InventoryItem.objects.none()
|
|
identity = self.request.user
|
|
group_items = InventoryItem.objects.filter(owner_group__in=identity.member_of_groups.all())
|
|
if self.action != 'list':
|
|
# retrieve/update/destroy: anything the caller may act on - their own items, or any
|
|
# group they're currently a member of. The narrower per-group listing below is only
|
|
# for the list action, so the main Inventory page stays scoped to personal items.
|
|
if identity.user.exists():
|
|
return InventoryItem.objects.filter(owner=identity.user.get()) | group_items
|
|
return group_items
|
|
group_id = self.request.query_params.get('group')
|
|
if group_id:
|
|
try:
|
|
group = Group.objects.get(pk=group_id)
|
|
except Group.DoesNotExist:
|
|
return InventoryItem.objects.none()
|
|
if not group.is_member(identity):
|
|
return InventoryItem.objects.none()
|
|
return InventoryItem.objects.filter(owner_group=group)
|
|
if identity.user.exists():
|
|
return InventoryItem.objects.filter(owner=identity.user.get())
|
|
return InventoryItem.objects.none()
|
|
|
|
def perform_create(self, serializer):
|
|
group_id = self.request.data.get('owner_group')
|
|
with transaction.atomic():
|
|
if group_id:
|
|
try:
|
|
group = Group.objects.get(pk=group_id)
|
|
except Group.DoesNotExist:
|
|
raise NotFound('No such group')
|
|
if not group.is_member(self.request.user):
|
|
raise PermissionDenied('Not a member of this group')
|
|
serializer.save(owner=None, owner_group=group).clean()
|
|
else:
|
|
serializer.save(owner=self.request.user.user.get()).clean()
|
|
|
|
@staticmethod
|
|
def _is_authorized(request, instance):
|
|
if instance.owner_id:
|
|
return request.user.user.filter(pk=instance.owner_id).exists()
|
|
return instance.owner_group.is_member(request.user)
|
|
|
|
def perform_update(self, serializer):
|
|
with transaction.atomic():
|
|
if self._is_authorized(self.request, serializer.instance):
|
|
serializer.save().clean()
|
|
|
|
def perform_destroy(self, instance):
|
|
if self._is_authorized(self.request, instance):
|
|
instance.delete()
|
|
|
|
|
|
def matches_query(item, query):
|
|
query = query.lower()
|
|
if query in item.name.lower():
|
|
return True
|
|
if item.description and query in item.description.lower():
|
|
return True
|
|
if any(query in tag.name.lower() for tag in item.tags.all()):
|
|
return True
|
|
return False
|
|
|
|
|
|
@api_view(['GET'])
|
|
@authentication_classes([SignatureAuthentication])
|
|
@permission_classes([IsAuthenticated])
|
|
def search_inventory_items(request):
|
|
query = request.query_params.get('query')
|
|
if query:
|
|
matching_items = [item for item in inventory_items(request.user) if matches_query(item, query)]
|
|
return Response(InventoryItemSerializer(matching_items, many=True).data)
|
|
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]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
if type(self.request.user) == KnownIdentity and self.request.user.user.exists():
|
|
return StorageLocation.objects.filter(owner=self.request.user.user.get())
|
|
return StorageLocation.objects.none()
|
|
|
|
def perform_create(self, serializer):
|
|
with transaction.atomic():
|
|
serializer.save(owner=self.request.user.user.get())
|
|
|
|
def perform_update(self, serializer):
|
|
with transaction.atomic():
|
|
if serializer.instance.owner == self.request.user.user.get():
|
|
serializer.save()
|
|
|
|
def perform_destroy(self, instance):
|
|
if instance.owner == self.request.user.user.get():
|
|
instance.delete()
|
|
|
|
|
|
class WorkflowInstanceViewSet(viewsets.ModelViewSet):
|
|
serializer_class = WorkflowInstanceSerializer
|
|
authentication_classes = [SignatureAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
if type(self.request.user) == KnownIdentity and self.request.user.user.exists():
|
|
return WorkflowInstance.objects.filter(owner=self.request.user.user.get())
|
|
return WorkflowInstance.objects.none()
|
|
|
|
def perform_create(self, serializer):
|
|
with transaction.atomic():
|
|
serializer.save(owner=self.request.user.user.get())
|
|
|
|
def perform_update(self, serializer):
|
|
with transaction.atomic():
|
|
if serializer.instance.owner == self.request.user.user.get():
|
|
serializer.save()
|
|
|
|
def perform_destroy(self, instance):
|
|
if instance.owner == self.request.user.user.get():
|
|
staged_file_ids = list(instance.staged_files.values_list('id', flat=True))
|
|
instance.delete()
|
|
for file in File.objects.filter(id__in=staged_file_ids):
|
|
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()
|
|
|
|
|
|
router.register(r'inventory_items', InventoryItemViewSet, basename='inventory_items')
|
|
router.register(r'storage_locations', StorageLocationViewSet, basename='storage_locations')
|
|
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'),
|
|
]
|