256 lines
11 KiB
Python
256 lines
11 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 resolve_group_by_handle(handle):
|
|
"""handle is "name@domain" (no leading '#') - the same format/parser group.py's GroupDetail
|
|
uses, so a group reference parses identically everywhere it appears (URL path, ?group= query
|
|
param, or an owner_group payload field), rather than some spots taking a handle and others a
|
|
bare pk."""
|
|
try:
|
|
name, domain = split_userhandle_or_throw(handle)
|
|
except ValueError:
|
|
return None
|
|
try:
|
|
return Group.objects.get(name=name, domain=domain)
|
|
except Group.DoesNotExist:
|
|
return None
|
|
|
|
|
|
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. See
|
|
# docs/implementation.md#inventory-detail-routes-use-owner-scoped-ids.
|
|
lookup_field = 'id'
|
|
lookup_url_kwarg = 'pk'
|
|
|
|
def get_queryset(self):
|
|
# A pure group-member KnownIdentity may have no local ToolshedUser account; only
|
|
# personal items require .user.exists(). See
|
|
# docs/implementation.md#group-member-identities-without-local-accounts.
|
|
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: any item the caller may act on, own or group. See
|
|
# docs/implementation.md#inventory-queryset-scope-by-action.
|
|
if identity.user.exists():
|
|
return InventoryItem.objects.filter(owner=identity.user.get()) | group_items
|
|
return group_items
|
|
group_handle = self.request.query_params.get('group')
|
|
if group_handle:
|
|
group = resolve_group_by_handle(group_handle)
|
|
if not group or 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_handle = self.request.data.get('owner_group')
|
|
with transaction.atomic():
|
|
if group_handle:
|
|
group = resolve_group_by_handle(group_handle)
|
|
if not group:
|
|
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 and local id, for /i/<handle>/<id> or
|
|
/inventory/shared/<handle>/<id>. See docs/implementation.md#get-shared-item-looks-up-by-owner."""
|
|
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]
|
|
# Detail routes address a location by its owner-scoped id, not the internal row id. See
|
|
# docs/implementation.md#inventory-detail-routes-use-owner-scoped-ids.
|
|
lookup_field = 'id'
|
|
lookup_url_kwarg = 'pk'
|
|
|
|
def get_queryset(self):
|
|
# Mirrors InventoryItemViewSet.get_queryset() - see its own comments for why the
|
|
# list/detail scopes differ and why group membership alone (no linked ToolshedUser
|
|
# required) is enough for the group branch.
|
|
if type(self.request.user) != KnownIdentity:
|
|
return StorageLocation.objects.none()
|
|
identity = self.request.user
|
|
group_locations = StorageLocation.objects.filter(owner_group__in=identity.member_of_groups.all())
|
|
if self.action != 'list':
|
|
if identity.user.exists():
|
|
return StorageLocation.objects.filter(owner=identity.user.get()) | group_locations
|
|
return group_locations
|
|
group_handle = self.request.query_params.get('group')
|
|
if group_handle:
|
|
group = resolve_group_by_handle(group_handle)
|
|
if not group or not group.is_member(identity):
|
|
return StorageLocation.objects.none()
|
|
return StorageLocation.objects.filter(owner_group=group)
|
|
if identity.user.exists():
|
|
return StorageLocation.objects.filter(owner=identity.user.get())
|
|
return StorageLocation.objects.none()
|
|
|
|
def perform_create(self, serializer):
|
|
group_handle = self.request.data.get('owner_group')
|
|
with transaction.atomic():
|
|
if group_handle:
|
|
group = resolve_group_by_handle(group_handle)
|
|
if not group:
|
|
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()
|
|
|
|
|
|
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'),
|
|
]
|