Skip to content

Commit

Permalink
Merge pull request #1029 from girder/flatten-item-lists
Browse files Browse the repository at this point in the history
Add an option to flatten girder item lists.
  • Loading branch information
manthey authored Jan 17, 2023
2 parents 9e38897 + dfc8e38 commit b47fc9d
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 3 deletions.
51 changes: 51 additions & 0 deletions girder/girder_large_image/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,19 @@ def addSystemEndpoints(apiRoot):
def altItemFind(self, folderId, text, name, limit, offset, sort, filters=None):
if sort and sort[0][0][0] == '[':
sort = json.loads(sort[0][0])
recurse = False
if text and text.startswith('_recurse_:'):
recurse = True
text = text.split('_recurse_:', 1)[1]
if filters is None and text and text.startswith('_filter_:'):
try:
filters = json.loads(text.split('_filter_:', 1)[1].strip())
text = None
except Exception as exc:
logger.warning('Failed to parse _filter_ from text field: %r', exc)
if recurse:
return _itemFindRecursive(
self, origItemFind, folderId, text, name, limit, offset, sort, filters)
return origItemFind(folderId, text, name, limit, offset, sort, filters)

@boundHandler(apiRoot.item)
Expand All @@ -52,6 +59,50 @@ 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):
"""
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
are any, then perform a search that matches any of those folders rather
than just the parent.
:param self: A reference to the Item() resource record.
:param origItemFind: the original _find method, used as a fallback.
For the remaining parameters, see girder/api/v1/item._find
"""
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:
filters = (filters.copy() if filters else {})
if text:
filters['$text'] = {
'$search': text
}
if name:
filters['name'] = name
filters['folderId'] = {'$in': children}
user = self.getCurrentUser()
if isinstance(sort, list):
sort.append(('parentId', 1))
return Item().findWithPermissions(filters, offset, limit, sort=sort, user=user)
return origItemFind(folderId, text, name, limit, offset, sort, filters)


def _mergeDictionaries(a, b):
"""
Merge two dictionaries recursively. If the second dictionary (or any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,11 @@ ul.g-item-list
.modal-dialog.li-item-list-dialog
width 70%
max-width 1000px

.li-flatten-item-list
font-size 14px
display inline-block
margin-left 20px
label
padding-left 5px
font-weight normal
54 changes: 51 additions & 3 deletions girder/girder_large_image/web_client/views/itemList.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { getApiRoot, restRequest } from '@girder/core/rest';
import { getCurrentUser } from '@girder/core/auth';
import { AccessType } from '@girder/core/constants';
import { formatSize, parseQueryString, splitRoute } from '@girder/core/misc';
import HierarchyWidget from '@girder/core/views/widgets/HierarchyWidget';
import FolderListWidget from '@girder/core/views/widgets/FolderListWidget';
import ItemListWidget from '@girder/core/views/widgets/ItemListWidget';

import largeImageConfig from './configView';
Expand All @@ -15,6 +17,34 @@ import { addToRoute } from '../routes';
import '../stylesheets/itemList.styl';
import ItemListTemplate from '../templates/itemList.pug';

wrap(HierarchyWidget, 'render', function (render) {
render.call(this);
if (!this.$('#flattenitemlist').length && this.$('.g-item-list-container').length && this.itemListView && this.itemListView.setFlatten) {
$('button.g-checked-actions-button').parent().after(
'<div class="li-flatten-item-list" title="Check to show items in all subfolders in this list"><input type="checkbox" id="flattenitemlist"></input><label for="flattenitemlist">Flatten</label></div>'
);
if ((this.itemListView || {})._recurse) {
this.$('#flattenitemlist').prop('checked', true);
}
this.events['click #flattenitemlist'] = (evt) => {
this.itemListView.setFlatten(this.$('#flattenitemlist').is(':checked'));
};
this.delegateEvents();
}
if (this.$('#flattenitemlist').length && this.parentModel.get('_modelType') !== 'folder') {
this.$('.li-flatten-item-list').addClass('hidden');
} else {
this.$('.li-flatten-item-list').removeClass('hidden');
}
});

wrap(FolderListWidget, 'checkAll', function (checkAll, checked) {
if (checked && (this.parentView.itemListView || {})._recurse) {
return;
}
return checkAll.call(this, checked);
});

wrap(ItemListWidget, 'initialize', function (initialize, settings) {
let result = initialize.call(this, settings);
delete this._hasAnyLargeImage;
Expand All @@ -26,7 +56,7 @@ wrap(ItemListWidget, 'initialize', function (initialize, settings) {
url: `folder/${settings.folderId}/yaml_config/.large_image_config.yaml`
}).done((val) => {
val = val || {};
if (_.isEqual(val, this._liconfig)) {
if (_.isEqual(val, this._liconfig) && !this._recurse) {
return;
}
delete this._lastSort;
Expand All @@ -46,7 +76,7 @@ wrap(ItemListWidget, 'initialize', function (initialize, settings) {
});
update = true;
}
if (query.filter) {
if (query.filter || this._recurse) {
this._generalFilter = query.filter;
this._setFilter();
update = true;
Expand All @@ -58,6 +88,13 @@ wrap(ItemListWidget, 'initialize', function (initialize, settings) {
});
this.events['click .li-item-list-header.sortable'] = (evt) => sortColumn.call(this, evt);
this.delegateEvents();
this.setFlatten = (flatten) => {
if (!!flatten !== !!this._recurse) {
this._recurse = !!flatten;
this._setFilter();
this.render();
}
};
return result;
});

Expand Down Expand Up @@ -167,8 +204,10 @@ wrap(ItemListWidget, 'render', function (render) {
this._setSort();
}
if (oldPages !== pages) {
this.trigger('g:paginated');
this.collection.trigger('g:changed');
}
this.bindOnChanged();
});
} else {
this._needsFetch = true;
Expand Down Expand Up @@ -237,7 +276,10 @@ wrap(ItemListWidget, 'render', function (render) {
filter = '_filter_:' + JSON.stringify(filter);
}
}
if (filter !== this._filter) {
if (this._recurse) {
filter = '_recurse_:' + (filter || '');
}
if (filter !== this._filter || filter !== (this.collection.params || {}).text) {
this._filter = filter;
this.collection.params = this.collection.params || {};
this.collection.params.text = this._filter;
Expand Down Expand Up @@ -320,6 +362,12 @@ wrap(ItemListWidget, 'render', function (render) {
if (this._confList()) {
return itemListRender.apply(this, _.rest(arguments));
}

if (this._recurse && !((this.collection || {}).params || {}).text) {
this._setFilter();
this.render();
return;
}
render.call(this);
if (settings['large_image.show_thumbnails'] === false ||
this.$('.large_image_container').length > 0) {
Expand Down
58 changes: 58 additions & 0 deletions girder/test_girder/test_large_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,3 +646,61 @@ def testMetadataSearch(server, admin, fsAssetstore):
params={'q': 'key:key2 value', 'mode': 'li_metadata', 'types': '["item","folder"]'})
assert utilities.respStatus(resp) == 200
assert len(resp.json['item']) == 0


@pytest.mark.usefixtures('unbindLargeImage')
@pytest.mark.plugin('large_image')
def testFlattenItemLists(server, admin, user, fsAssetstore):
collection = Collection().createCollection(
'collection A', admin)
colFolderA = Folder().createFolder(
collection, 'folder A', parentType='collection',
creator=admin)
colFolderB = Folder().createFolder(
colFolderA, 'folder B', creator=admin)
utilities.uploadText(
json.dumps({'keyA': 'value1'}),
admin, fsAssetstore, colFolderA, 'sample1.json')
utilities.uploadText(
json.dumps({'keyB': 'value2'}),
admin, fsAssetstore, colFolderB, 'sample2.json')
resp = server.request(
path='/item', user=admin,
params={'folderId': str(colFolderA['_id'])})
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 1
resp = server.request(
path='/item', user=admin,
params={'folderId': str(colFolderB['_id'])})
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 1
resp = server.request(
path='/item', user=admin,
params={'folderId': str(colFolderA['_id']), 'text': '_recurse_:'})
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 2
resp = server.request(
path='/item', user=admin,
params={'folderId': str(colFolderB['_id']), 'text': '_recurse_:'})
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 1
resp = server.request(
path='/item', user=admin,
params={'folderId': str(colFolderA['_id']), 'text': '_recurse_:sample1'})
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 1
resp = server.request(
path='/item', user=admin,
params={'folderId': str(colFolderB['_id']), 'text': '_recurse_:sample1'})
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 0
resp = server.request(
path='/item', user=admin,
params={'folderId': str(colFolderA['_id']), 'text': '_recurse_:', 'name': 'sample1.json'})
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 1
resp = server.request(
path='/item', user=admin,
params={'folderId': str(colFolderB['_id']), 'text': '_recurse_:', 'name': 'sample1.json'})
assert utilities.respStatus(resp) == 200
assert len(resp.json) == 0
16 changes: 16 additions & 0 deletions girder/test_girder/web_client_specs/imageViewerSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,22 @@ $(function () {
expect(!$('.g-item-list-entry').length);
});
});
it('flatten the item list', function () {
runs(function () {
$('.li-flatten-item-list #flattenitemlist').click();
});
girderTest.waitForLoad();
runs(function () {
expect($('.li-flatten-item-list #flattenitemlist:checked').length);
});
runs(function () {
$('.li-flatten-item-list #flattenitemlist').click();
});
girderTest.waitForLoad();
runs(function () {
expect(!$('.li-flatten-item-list #flattenitemlist:checked').length);
});
});
it('navigate back to image', function () {
waitsFor(function () {
return $('span.g-item-list-link').filter(function () { return $(this).text() !== '.large_image_config.yaml'; }).length > 0;
Expand Down

0 comments on commit b47fc9d

Please sign in to comment.