diff --git a/girder_annotation/girder_large_image_annotation/rest/annotation.py b/girder_annotation/girder_large_image_annotation/rest/annotation.py index e4ca0e453..1e61eeaa4 100644 --- a/girder_annotation/girder_large_image_annotation/rest/annotation.py +++ b/girder_annotation/girder_large_image_annotation/rest/annotation.py @@ -20,6 +20,7 @@ import cherrypy import orjson +from bson.objectid import ObjectId from girder import logger from girder.api import access @@ -58,6 +59,9 @@ def __init__(self): self.route('GET', ('item', ':id'), self.getItemAnnotations) self.route('POST', ('item', ':id'), self.createItemAnnotations) self.route('DELETE', ('item', ':id'), self.deleteItemAnnotations) + self.route('GET', ('folder', ':id'), self.returnFolderAnnotations) + self.route('GET', ('folder', ':id', 'present'), self.existFolderAnnotations) + self.route('PUT', ('folder', ':id', 'access'), self.setFolderAnnotationAccess) self.route('GET', ('old',), self.getOldAnnotations) self.route('DELETE', ('old',), self.deleteOldAnnotations) self.route('GET', ('counts',), self.getItemListAnnotationCounts) @@ -558,6 +562,147 @@ def deleteItemAnnotations(self, item): count += 1 return count + def getFolderAnnotations(self, id, recurse, user, limit=False, offset=False, sort=False, + sortDir=False, count=False): + recursivePipeline = [ + {'$graphLookup': { + 'from': 'folder', + 'startWith': '$_id', + 'connectFromField': '_id', + 'connectToField': 'parentId', + 'as': '__children' + }}, + {'$unwind': {'path': '$__children'}}, + {'$replaceRoot': {'newRoot': '$__children'}}, + {'$unionWith': { + 'coll': 'folder', + 'pipeline': [{'$match': {'_id': ObjectId(id)}}] + }}] if recurse else [] + accessPipeline = [ + {'$match': { + '$or': [ + {'access.users': + {'$elemMatch': { + 'id': user['_id'], + 'level': {'$gte': 2} + }}}, + {'access.groups': + {'$elemMatch': { + 'id': {'$in': user['groups']}, + 'level': {'$gte': 2} + }}} + ] + }} + ] if not user['admin'] else [] + pipeline = [ + {'$match': {'_id': 'none'}}, + {'$unionWith': { + 'coll': 'folder', + 'pipeline': [{'$match': {'_id': ObjectId(id)}}] + + recursivePipeline + + [{'$lookup': { + 'from': 'item', + 'localField': '_id', + 'foreignField': 'folderId', + 'as': '__items' + }}, {'$lookup': { + 'from': 'annotation', + 'localField': '__items._id', + 'foreignField': 'itemId', + 'as': '__annotations' + }}, {'$unwind': '$__annotations'}, + {'$replaceRoot': {'newRoot': '$__annotations'}}, + {'$match': {'_active': {'$ne': False}}} + ] + accessPipeline + }}, + ] + if count: + pipeline += [{'$count': 'count'}] + else: + pipeline = pipeline + [{'$sort': {sort: sortDir}}] if sort else pipeline + pipeline = pipeline + [{'$skip': offset}] if offset else pipeline + pipeline = pipeline + [{'$limit': limit}] if limit else pipeline + + return Annotation().collection.aggregate(pipeline) + + @autoDescribeRoute( + Description('Check if the user owns any annotations for the items in a folder') + .param('id', 'The ID of the folder', required=True, paramType='path') + .param('recurse', 'Whether or not to recursively check ' + 'subfolders for annotations', required=False, default=True, dataType='boolean') + .errorResponse() + ) + @access.public + def existFolderAnnotations(self, id, recurse): + user = self.getCurrentUser() + if not user: + return [] + annotations = self.getFolderAnnotations(id, recurse, user, 1) + try: + next(annotations) + return True + except StopIteration: + return False + + @autoDescribeRoute( + Description('Get the user-owned annotations from the items in a folder') + .param('id', 'The ID of the folder', required=True, paramType='path') + .param('recurse', 'Whether or not to retrieve all ' + 'annotations from subfolders', required=False, default=False, dataType='boolean') + .pagingParams(defaultSort='created', defaultSortDir=-1) + .errorResponse() + ) + @access.public + def returnFolderAnnotations(self, id, recurse, limit, offset, sort): + user = self.getCurrentUser() + if not user: + return [] + annotations = self.getFolderAnnotations(id, recurse, user, limit, offset, + sort[0][0], sort[0][1]) + + def count(): + try: + return next(self.getFolderAnnotations(id, recurse, self.getCurrentUser(), + count=True))['count'] + except StopIteration: + # If there are no values to iterate over, the count is 0 and should be returned + return 0 + + annotations.count = count + return annotations + + @autoDescribeRoute( + Description('Set the access for all the user-owned annotations from the items in a folder') + .param('id', 'The ID of the folder', required=True, paramType='path') + .param('access', 'The JSON-encoded access control list.') + .param('public', 'Whether the annotation should be publicly visible.', + dataType='boolean', required=False) + .param('recurse', 'Whether or not to retrieve all ' + 'annotations from subfolders', required=False, default=False, dataType='boolean') + .errorResponse('ID was invalid.') + ) + @access.user + def setFolderAnnotationAccess(self, id, params): + setResponseTimeLimit(86400) + user = self.getCurrentUser() + if not user: + return [] + access = json.loads(params['access']) + public = self.boolParam('public', params, False) + count = 0 + for annotation in self.getFolderAnnotations(id, params['recurse'], user): + annot = Annotation().load(annotation['_id'], user=user, getElements=False) + annot = Annotation().setPublic(annot, public) + annot = Annotation().setAccessList( + annot, access, user=user) + Annotation().update({'_id': annot['_id']}, {'$set': { + key: annot[key] for key in ('access', 'public', 'publicFlags') + if key in annot + }}) + count += 1 + + return {'updated': count} + @autoDescribeRoute( Description('Report on old annotations.') .param('age', 'The minimum age in days.', required=False, diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/hierarchyWidget.js b/girder_annotation/girder_large_image_annotation/web_client/views/hierarchyWidget.js new file mode 100644 index 000000000..e28f8cb56 --- /dev/null +++ b/girder_annotation/girder_large_image_annotation/web_client/views/hierarchyWidget.js @@ -0,0 +1,96 @@ +import $ from 'jquery'; + +import { wrap } from '@girder/core/utilities/PluginUtils'; +import { restRequest } from '@girder/core/rest'; +import AccessWidget from '@girder/core/views/widgets/AccessWidget'; +import HierarchyWidget from '@girder/core/views/widgets/HierarchyWidget'; + +import AnnotationModel from '../models/AnnotationModel'; + +wrap(HierarchyWidget, 'render', function (render) { + render.call(this); + + if (this.parentModel.get('_modelType') === 'folder') { + ownAnnotation(this, this.parentModel.id); + } +}); + +function ownAnnotation(root, folderId) { + restRequest({ + type: 'GET', + url: 'annotation/folder/' + folderId + '/present', + data: { + recurse: true + } + }).done((ownsAnnot) => { + if (ownsAnnot) { + addAccessControl(root); + } + }); +} + +function addAccessControl(root) { + if (root.$('.g-edit-annotation-access').length === 0) { + if (root.$('.g-folder-actions-menu > .divider').length > 0) { + root.$('.g-folder-actions-menu > .divider').before( + '