Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional item list options #1659

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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 @@
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 @@
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 @@
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