class DeepLinkSelector(APIView): @never_cache def get(self, request: Request, project_id, alias) -> Response: """Get a deep-links available to the client. No specific permissions are needed here, because this just rewrites a URL and the client can handle the potential permission error in a more user-friendly manner. --- serializer: DeepLinkSerializer """ params = [f'pid={project_id}', f'link={alias}'] url = f'{reverse("catmaid:home")}?{"&".join(params)}' return HttpResponseRedirect(url) @method_decorator(requires_user_role_for_any_project([UserRole.Browse])) @never_cache def head(self, request: Request, project_id, alias) -> Response: """Get a deep-links available to the client. --- serializer: DeepLinkSerializer """ try: deep_link = DeepLink.objects.get(project_id=project_id, alias=alias) if not deep_link.is_public and request.user != deep_link.user: raise PermissionError('Can not find or access link') return Response() except DeepLink.DoesNotExist: return Response('Link not found', status=status.HTTP_404_NOT_FOUND) @method_decorator(requires_user_role_for_any_project([UserRole.Annotate])) @never_cache def delete(self, request: Request, project_id, alias) -> Response: """Delete a deep-links available to the client. --- serializer: DeepLinkSerializer """ try: deep_link = DeepLink.objects.get(project_id=project_id, alias=alias) can_edit_or_fail(request.user, deep_link.id, 'catmaid_deep_link') deep_link_id = deep_link.id deep_link.delete() return Response({'deleted_id': deep_link_id}) except DeepLink.DoesNotExist: return Response('Link not found', status=status.HTTP_404_NOT_FOUND)
class ClientDatastoreList(APIView): @method_decorator( requires_user_role_for_any_project( [UserRole.Browse, UserRole.Annotate])) @never_cache def get(self, request: Request, format=None) -> Response: """List key-value store datastores used by the client. --- serializer: ClientDatastoreSerializer """ datastores = ClientDatastore.objects.all() serializer = ClientDatastoreSerializer(datastores, many=True) return Response(serializer.data) @method_decorator( requires_user_role_for_any_project( [UserRole.Browse, UserRole.Annotate])) def post(self, request: Request, format=None) -> Response: """Create a key-value store datastore for the client. The request user must not be anonymous and must have browse, annotate or administer permissions for at least one project. --- parameters: - name: name description: | String key for the datastore. This will be used in URLs so may only contain alphanumeric characters and hyphens. required: true type: string paramType: form serializer: ClientDatastoreSerializer """ if request.user == get_anonymous_user( ) or not request.user.is_authenticated: raise PermissionDenied('Unauthenticated or anonymous users ' \ 'can not create datastores.') name = request.POST['name'] if not name: raise ValidationError('A name for the datastore must be provided.') datastore = ClientDatastore(name=name) datastore.full_clean() datastore.save() serializer = ClientDatastoreSerializer(datastore) return Response(serializer.data)
class DeepLinkByIdSelector(APIView): @method_decorator(requires_user_role_for_any_project([UserRole.Annotate])) @never_cache def delete(self, request: Request, project_id, link_id) -> Response: """Delete a deep-links available to the client. --- serializer: DeepLinkSerializer """ try: deep_link = DeepLink.objects.get(project_id=project_id, id=link_id) can_edit_or_fail(request.user, deep_link.id, 'catmaid_deep_link') deep_link.delete() return Response({'deleted_id': link_id}) except DeepLink.DoesNotExist: return Response('Link not found', status=status.HTTP_404_NOT_FOUND)
class DeepLinkDetails(APIView): @method_decorator(requires_user_role_for_any_project([UserRole.Browse])) @never_cache def get(self, request: Request, project_id, alias) -> Response: """Get details on a deep-link. --- serializer: DeepLinkSerializer """ try: deep_link = DeepLink.objects.get(project_id=project_id, alias=alias) if not deep_link.is_public and request.user != deep_link.user: raise PermissionError('Can not find or access link') serializer = DeepLinkSerializer(deep_link) return Response(serializer.data) except DeepLink.DoesNotExist: return Response('Link not found', status=status.HTTP_404_NOT_FOUND)
class ClientDataList(APIView): @method_decorator( requires_user_role_for_any_project( [UserRole.Browse, UserRole.Annotate])) @never_cache def get(self, request: Request, name=None, format=None) -> Response: """List key-value data in a datastore for the client. Returns key-values belong to the request user or no user, optionally filtering for those pairs belong to a specific project or no project. --- parameters: - name: name description: | String key for the **datastore** with which this key-value entry is associated. required: true type: string paramType: path - name: project_id description: | ID of a project to associate this data with, if any. required: false type: integer paramType: query serializer: ClientDataSerializer """ datastore = get_object_or_404(ClientDatastore, name=name) data = ClientData.objects.filter(datastore=datastore, user_id=request.user.id) | \ ClientData.objects.filter(datastore=datastore, user_id=None) project_id = request.GET.get('project_id', None) if project_id: project_id = int(project_id) project = get_object_or_404(Project, pk=project_id) if not check_user_role(request.user, project, [UserRole.Browse, UserRole.Annotate]): raise PermissionDenied('User lacks the appropriate ' \ 'permissions for this project.') data = data.filter(project_id=project_id) | data.filter( project_id=None) else: data = data.filter(project_id=None) serializer = ClientDataSerializer(data, many=True) return Response(serializer.data) @method_decorator( requires_user_role_for_any_project( [UserRole.Browse, UserRole.Annotate])) def put(self, request: Request, name: Optional[int] = None, format=None) -> Response: """Create or replace a key-value data entry for the client. Each entry is associated with a datastore, an optional project, an optional user, and a key. Creating a request that duplicates this quadruple will replace rather than create the value in the key-value pair. Entries associated with neither a project nor user are considered global; those associated with a project but no user are project- default; those associated with a user but no project are user-default; and those associated with both a project and a user are user-project specific. When listing key-value data, all four of these values, if existing, will be returned. --- parameters: - name: name description: | String key for the **datastore** with which this key-value entry is associated. required: true type: string paramType: path - name: project_id description: | ID of a project to associate this data with, if any. required: false type: integer paramType: form - name: ignore_user description: | Whether to associate this key-value entry with the instance rather than the request user. Only project administrators can do this for project-associated instance data, and only super users can do this for global data (instance data not associated with any project). required: false type: boolean default: false paramType: form - name: key description: A key for this entry. required: true type: string paramType: form - name: value description: A value for this entry. Must be valid JSON. required: true type: string paramType: form - name: format description: This function parameter is ignored required: false type: Any default: None response_serializer: ClientDataSerializer """ if request.user == get_anonymous_user( ) or not request.user.is_authenticated: raise PermissionDenied('Unauthenticated or anonymous users ' \ 'can not create data.') datastore = get_object_or_404(ClientDatastore, name=name) key = request.data.get('key', None) if not key: raise ValidationError('A key for the data must be provided.') value = request.data.get('value', None) if not value: raise ValidationError('A value for the data must be provided.') # Validate JSON by reserializing. try: value = json.loads(value) except ValueError as exc: raise ValidationError('Data value is invalid JSON: ' + str(exc)) project_id = request.data.get('project_id', None) project = None if project_id: project_id = int(project_id) project = get_object_or_404(Project, pk=project_id) if not check_user_role(request.user, project, [UserRole.Browse, UserRole.Annotate]): raise PermissionDenied('User lacks the appropriate ' \ 'permissions for this project.') ignore_user = get_request_bool(request.data, 'ignore_user', False) if ignore_user and not project_id: if not request.user.is_superuser: raise PermissionDenied('Only super users can create instance ' \ 'data.') if ignore_user: if not check_user_role(request.user, project, [UserRole.Admin]): raise PermissionDenied('Only administrators can create ' \ 'project default data.') user = None if ignore_user else request.user try: data = ClientData.objects.get(datastore=datastore, key=key, project=project, user=user) data.value = value data.full_clean() data.save() return Response(status=status.HTTP_204_NO_CONTENT) except ClientData.DoesNotExist: data = ClientData(datastore=datastore, key=key, value=value, project=project, user=user) data.full_clean() data.save() serializer = ClientDataSerializer(data) return Response(serializer.data)
class DeepLinkList(APIView): @method_decorator(requires_user_role_for_any_project([UserRole.Browse])) @never_cache def get(self, request: Request, project_id) -> Response: """List deep-links available to the client. --- serializer: SimpleDeepLinkSerializer """ only_own = get_request_bool(request.GET, 'only_own', False) only_private = get_request_bool(request.GET, 'only_private', False) filter_term = (Q(is_public=True) | Q(user_id=request.user.id)) & Q(project_id=project_id) if only_own: filter_term = filter_term & Q(user_id=request.user.id) if only_private: filter_term = filter_term & Q(is_public=False) deep_links = DeepLink.objects.filter(filter_term) serializer = SimpleDeepLinkSerializer(deep_links, many=True) return Response(serializer.data) @method_decorator(requires_user_role_for_any_project([UserRole.Annotate])) def post(self, request: Request, project_id) -> Response: """Create a deep-link. The request user must not be anonymous and must have annotate permissions. --- serializer: DeepLinkSerializer """ if request.user == get_anonymous_user( ) or not request.user.is_authenticated: raise PermissionError('Unauthenticated or anonymous users ' \ 'can not create persistent deep links.') project_id = int(project_id) alias = request.POST.get('alias') if alias: if not re.match(r'^[a-zA-Z0-9-_\.]+$', alias): raise ValueError( "Only alphanumeric characters, '-', '_' and '.' allowed") else: n_links = DeepLink.objects.filter(project_id=project_id).count() alias = make_unique_id() params = { 'project_id': project_id, 'user': request.user, 'alias': alias, } if 'is_public' in request.POST: params['is_public'] = get_request_bool(request.POST, 'is_public') if 'location_x' in request.POST: params['location_x'] = float(request.POST['location_x']) if 'location_y' in request.POST: params['location_y'] = float(request.POST['location_y']) if 'location_z' in request.POST: params['location_z'] = float(request.POST['location_z']) if 'active_treenode_id' in request.POST: params['active_treenode_id'] = int( request.POST['active_treenode_id']) if 'active_connector_id' in request.POST: params['active_connector_id'] = int( request.POST['active_connector_id']) if 'active_skeleton_id' in request.POST: params['active_skeleton_id'] = int( request.POST['active_skeleton_id']) if 'closest_node_to_location' in request.POST: params['closest_node_to_location'] = get_request_bool( request.POST, 'closest_node_to_location') if 'follow_id_history' in request.POST: params['follow_id_history'] = get_request_bool( request.POST, 'follow_id_history') if 'layered_stacks' in request.POST: params['layered_stacks'] = get_request_bool( request.POST, 'layered_stacks') if 'layout' in request.POST: params['layout'] = request.POST['layout'] if 'tool' in request.POST: params['tool'] = request.POST['tool'] if 'show_help' in request.POST: params['show_help'] = get_request_bool(request.POST, 'show_help') if 'password' in request.POST: params['password'] = make_password(request.POST('password')) if 'message' in request.POST: params['message'] = request.POST['message'] # TBA: data_view deeplink = DeepLink(**params) deeplink.save() serializer = DeepLinkSerializer(deeplink) # Stacks stacks = get_request_list(request.POST, 'stacks', map_fn=float) if stacks: # Nested lists of 2-tuples: [[stack_id, scale_level]] for s in stacks: stack_link = DeepLinkStack( **{ 'project_id': project_id, 'user_id': request.user.id, 'deep_link': deeplink, 'stack_id': int(s[0]), 'zoom_level': s[1], }) stack_link.save() # Stack groups if 'stack_group' in request.POST: sg_id = int(request.POST['stack_group']) sg_zoom_levels = get_request_list(request.POST, 'stack_group_scale_levels', map_fn=float) sg_link = DeepLinkStackGroup( **{ 'project_id': project_id, 'user_id': request.user.id, 'deeplink': deeplink, 'stack_group_id': sg_id, 'zoom_levels': sg_zoom_levels, }) sg_link.save() return Response(serializer.data)