Skip to content

Commit

Permalink
Add additional item list options
Browse files Browse the repository at this point in the history
  • Loading branch information
manthey committed Oct 4, 2024
1 parent d0c49f5 commit e6931aa
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 27 deletions.
6 changes: 6 additions & 0 deletions docs/girder_config_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ This is used to specify how items appear in item lists. There are two settings,
itemList:
# layout does not need to be specified.
layout:
# The default list (with flatten: false) shows only the items in the
# current folder; flattening the list shows items in the current folder
# and all subfolders. This can also be "only", in which case the
# flatten option will start enabled and, when flattened, the folder
# list will be hidden.
flatten: true
# The default layout is a list. This can optionally be "grid"
mode: grid
# max-width is only used in grid mode. It is the maximum width in
Expand Down
155 changes: 137 additions & 18 deletions girder/girder_large_image/rest/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import collections
import json

from girder import logger
Expand All @@ -9,7 +10,7 @@
from girder.models.item import Item


def addSystemEndpoints(apiRoot):
def addSystemEndpoints(apiRoot): # noqa
"""
This adds endpoints to routes that already exist in Girder.
Expand All @@ -29,6 +30,9 @@ def altItemFind(self, folderId, text, name, limit, offset, sort, filters=None):
if text and text.startswith('_recurse_:'):
recurse = True
text = text.split('_recurse_:', 1)[1]
group = None
if text and text.startswith('_group_:') and len(text.split(':', 2)) >= 3:
_, group, text = text.split(':', 2)

Check warning on line 35 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L35

Added line #L35 was not covered by tests
if filters is None and text and text.startswith('_filter_:'):
try:
filters = json.loads(text.split('_filter_:', 1)[1].strip())
Expand All @@ -40,9 +44,10 @@ def altItemFind(self, folderId, text, name, limit, offset, sort, filters=None):
logger.debug('Item find filters: %s', json.dumps(filters))
except Exception:
pass
if recurse:
if recurse or group:
return _itemFindRecursive(
self, origItemFind, folderId, text, name, limit, offset, sort, filters)
self, origItemFind, folderId, text, name, limit, offset, sort,
filters, recurse, group)
return origItemFind(folderId, text, name, limit, offset, sort, filters)

@boundHandler(apiRoot.item)
Expand All @@ -58,7 +63,55 @@ def altFolderFind(self, parentType, parentId, text, name, limit, offset, sort, f
altFolderFind._origFunc = origFolderFind


def _itemFindRecursive(self, origItemFind, folderId, text, name, limit, offset, sort, filters):
def _groupingPipeline(initialPipeline, cbase, grouping, sort=None):
"""
Modify the recursive pipeline to add grouping and counts.
:param initialPipeline: a pipeline to extend.
:param cbase: a unique value for each grouping set.
:param grouping: a dictionary where 'keys' is a list of data to group by
and, optionally, 'counts' is a dictionary of data to count as keys and
names where to add the results. For instance, this could be
{'keys': ['meta.dicom.PatientID'], 'counts': {
'meta.dicom.StudyInstanceUID': 'meta._count.studycount',
'meta.dicom.SeriesInstanceUID': 'meta._count.seriescount'}}
:param sort: an optional lost of (key, direction) tuples
"""
for gidx, gr in enumerate(grouping['keys']):
grsort = [(gr, 1)] + (sort or [])
initialPipeline.extend([{

Check warning on line 82 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L81-L82

Added lines #L81 - L82 were not covered by tests
'$match': {gr: {'$exists': True}},
}, {
'$sort': collections.OrderedDict(grsort),
}, {
'$group': {
'_id': f'${gr}',
'firstOrder': {'$first': '$$ROOT'},
},
}])
groupStep = initialPipeline[-1]['$group']

Check warning on line 92 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L92

Added line #L92 was not covered by tests
if not gidx and grouping['counts']:
for cidx, (ckey, cval) in enumerate(grouping['counts'].items()):
groupStep[f'count_{cbase}_{cidx}'] = {'$addToSet': f'${ckey}'}
cparts = cval.split('.')
centry = {cparts[-1]: {'$size': f'$count_{cbase}_{cidx}'}}

Check warning on line 97 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L95-L97

Added lines #L95 - L97 were not covered by tests
for cidx in range(len(cparts) - 2, -1, -1):
centry = {

Check warning on line 99 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L99

Added line #L99 was not covered by tests
cparts[cidx]: {
'$mergeObjects': [
'$firstOrder.' + '.'.join(cparts[:cidx + 1]),
centry,
],
},
}
initialPipeline.append({'$set': {'firstOrder': {

Check warning on line 107 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L107

Added line #L107 was not covered by tests
'$mergeObjects': ['$firstOrder', centry]}}})
initialPipeline.append({'$replaceRoot': {'newRoot': '$firstOrder'}})

Check warning on line 109 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L109

Added line #L109 was not covered by tests


def _itemFindRecursive( # noqa
self, origItemFind, folderId, text, name, limit, offset, sort, filters,
recurse=True, group=None):
"""
If a recursive search within a folderId is specified, use an aggregation to
find all folders that are descendants of the specified folder. If there
Expand All @@ -73,20 +126,23 @@ def _itemFindRecursive(self, origItemFind, folderId, text, name, limit, offset,
from bson.objectid import ObjectId

if folderId:
pipeline = [
{'$match': {'_id': ObjectId(folderId)}},
{'$graphLookup': {
'from': 'folder',
'connectFromField': '_id',
'connectToField': 'parentId',
'depthField': '_depth',
'as': '_folder',
'startWith': '$_id',
}},
{'$group': {'_id': '$_folder._id'}},
]
children = [ObjectId(folderId)] + next(Folder().collection.aggregate(pipeline))['_id']
if len(children) > 1:
if recurse:
pipeline = [
{'$match': {'_id': ObjectId(folderId)}},
{'$graphLookup': {
'from': 'folder',
'connectFromField': '_id',
'connectToField': 'parentId',
'depthField': '_depth',
'as': '_folder',
'startWith': '$_id',
}},
{'$group': {'_id': '$_folder._id'}},
]
children = [ObjectId(folderId)] + next(Folder().collection.aggregate(pipeline))['_id']
else:
children = [ObjectId(folderId)]

Check warning on line 144 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L144

Added line #L144 was not covered by tests
if len(children) > 1 or group:
filters = (filters.copy() if filters else {})
if text:
filters['$text'] = {
Expand All @@ -98,6 +154,69 @@ def _itemFindRecursive(self, origItemFind, folderId, text, name, limit, offset,
user = self.getCurrentUser()
if isinstance(sort, list):
sort.append(('parentId', 1))

# This is taken from girder.utility.acl_mixin.findWithPermissions,
# except it adds a grouping stage
initialPipeline = [
{'$match': filters},
{'$lookup': {
'from': 'folder',
'localField': Item().resourceParent,
'foreignField': '_id',
'as': '__parent',
}},
{'$match': Item().permissionClauses(user, AccessType.READ, '__parent.')},
{'$project': {'__parent': False}},
]
if group is not None:
if not isinstance(group, list):
group = [gr for gr in group.split(',') if gr]
groups = []
idx = 0

Check warning on line 175 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L174-L175

Added lines #L174 - L175 were not covered by tests
while idx < len(group):
if group[idx] != '_count_':
if not len(groups) or groups[-1]['counts']:
groups.append({'keys': [], 'counts': {}})
groups[-1]['keys'].append(group[idx])
idx += 1

Check warning on line 181 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L179-L181

Added lines #L179 - L181 were not covered by tests
else:
if idx + 3 <= len(group):
groups[-1]['counts'][group[idx + 1]] = group[idx + 2]
idx += 3

Check warning on line 185 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L184-L185

Added lines #L184 - L185 were not covered by tests
for gidx, grouping in enumerate(groups):
_groupingPipeline(initialPipeline, gidx, grouping, sort)

Check warning on line 187 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L187

Added line #L187 was not covered by tests
fullPipeline = initialPipeline
countPipeline = initialPipeline + [
{'$count': 'count'},
]
if sort is not None:
fullPipeline.append({'$sort': collections.OrderedDict(sort)})
if limit:
fullPipeline.append({'$limit': limit + (offset or 0)})
if offset:
fullPipeline.append({'$skip': offset})

Check warning on line 197 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L197

Added line #L197 was not covered by tests

logger.debug('Find item pipeline %r', fullPipeline)

options = {
'allowDiskUse': True,
'cursor': {'batchSize': 0},
}
result = Item().collection.aggregate(fullPipeline, **options)

def count():
try:
return next(iter(
Item().collection.aggregate(countPipeline, **options)))['count']
except StopIteration:

Check warning on line 211 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L211

Added line #L211 was not covered by tests
# If there are no values, this won't return the count, in
# which case it is zero.
return 0

Check warning on line 214 in girder/girder_large_image/rest/__init__.py

View check run for this annotation

Codecov / codecov/patch

girder/girder_large_image/rest/__init__.py#L214

Added line #L214 was not covered by tests

result.count = count
result.fromAggregate = True
return result

return Item().findWithPermissions(filters, offset, limit, sort=sort, user=user)
return origItemFind(folderId, text, name, limit, offset, sort, filters)

Expand Down
4 changes: 2 additions & 2 deletions girder/girder_large_image/web_client/templates/itemList.pug
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ ul.g-item-list.li-item-list(layout_mode=(itemList.layout || {}).mode || '')
skip = true;
}
});
#{divtype}.li-item-list-cell(class=classes.join(' '), g-item-cid=item.cid, href=`#item/${item.id}`, title=colNames[colidx])
#{divtype}.li-item-list-cell(class=classes.join(' '), g-item-cid=item.cid, href=item._href ? item._href : `#item/${item.id}`, title=colNames[colidx])
if !skip && column.label
span.g-item-list-label
= column.label
Expand Down Expand Up @@ -92,7 +92,7 @@ ul.g-item-list.li-item-list(layout_mode=(itemList.layout || {}).mode || '')
!= String(value).replace(/&/g, '&amp;').replace(/</, '&lt;').replace(/>/, '&gt;').replace(/"/, '&quot').replace(/'/, '&#39;').replace(/\./g, '.&shy;').replace(/_/g, '_&shy;')
else
= value
if value
if value && column.format !== 'count'
span.li-item-list-cell-filter(title="Only show items that match this metadata value exactly", filter-value=value, column-value=column.value)
i.icon-filter
if (hasMore && !paginated)
Expand Down
Loading

0 comments on commit e6931aa

Please sign in to comment.