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 the ability to show annotation metadata in item annotation lists #977

Merged
merged 1 commit into from
Oct 5, 2022
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Show and edit yaml and json files using codemirror ([#969](../../pull/969), [#971](../../pull/971))
- Show configured item lists even if there are no large images ([#972](../../pull/972))
- Add metadata and annotation metadata search modes to Girder ([#974](../../pull/974))
- Add the ability to show annotation metadata in item annotation lists ([#977](../../pull/977))

## 1.17.0

Expand Down
54 changes: 54 additions & 0 deletions docs/girder_annotation_config_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,57 @@ Store annotation history
~~~~~~~~~~~~~~~~~~~~~~~~

If ``Record annotation history`` is selected, whenever annotations are saved, previous versions are kept in the database. This can greatly increase the size of the database. The old versions of the annotations allow the API to be used to revent to previous versions or to audit changes over time.

.large_image_config.yaml
~~~~~~~~~~~~~~~~~~~~~~~~

This can be used to specify how annotations are listed on the item page.

::

---
# If present, show a table with column headers in annotation lists
annotationList:
# show these columns in order from left to right. Each column has a
# "type" and "value". It optionally has a "title" used for the column
# header, and a "format" used for searching and filtering. There are
# always control columns at the left and right.
columns:
-
# The "record" type is from the default annotation record. The value
# is one of "name", "creator", "created", "updatedId", "updated",
type: record
value: name
-
type: record
value: creator
# A format of user will print the user name instead of the id
format: user
-
type: record
value: created
# A format of date will use the browser's default date format
format: date
-
# The "metadata" type is taken from the annotations's
# "annotation.attributes" contents. It can be a nested key by using
# dots in its name.
type: metadata
value: Stain
# "format" can be "text", "number", "category". Other values may be
# specified later.
format: text
defaultSort:
# The default lists a sort order for sortable columns. This must have
# type, value, and dir for each entry, where dir is either "up" or
# "down".
-
type: metadata
value: Stain
dir: up
-
type: record
value: name
dir: down

These values can be combined with values from the base large_image plugin.
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ def __init__(self):
@filtermodel(model='annotation', plugin='large_image')
def find(self, params):
limit, offset, sort = self.getPagingParameters(params, 'lowerName')
if sort and sort[0][0][0] == '[':
sort = json.loads(sort[0][0])
query = {'_active': {'$ne': False}}
if 'itemId' in params:
item = Item().load(params.get('itemId'), force=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
font-weight bold

.g-annotation-list
&.table
width initial
max-width initial
min-width 100%
table-layout fixed

td
white-space nowrap
text-overflow ellipsis
overflow hidden

.g-annotation-toggle
.g-annotation-toggle, .g-annotation-select
width 30px

.g-annotation-actions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
i.icon-pencil
| Annotations
.btn-group.pull-right
if annotations.length
a.g-annotation-download(href=`${apiRoot}/annotation/item/${item.id}`, title='Download annotations', download=`${item.get('name')}_annotations.json`)
i.icon-download
if creationAccess
a.g-annotation-upload(title='Upload annotation')
i.icon-upload
if annotations.length
a.g-annotation-download(href=`${apiRoot}/annotation/item/${item.id}`, title='Download annotations', download=`${item.get('name')}_annotations.json`)
i.icon-download
if accessLevel >= AccessType.ADMIN && annotations.length
a.g-annotation-permissions(title='Adjust permissions')
i.icon-lock
Expand All @@ -17,29 +17,65 @@
if annotations.length
table.g-annotation-list.table.table-hover.table-condensed
thead
//
th.g-annotation-select
input.g-select-all(type='checkbox')
th.g-annotation-toggle
th.g-annotation-name Name
th.g-annotation-user Creator
th.g-annotation-date Date
a.g-annotation-toggle-all(class=canDraw ? 'disabled' : '', title='Hide or show all annotations')
- let anyDrawn = annotations.models.some((annotation) => drawn.has(annotation.id))
if anyDrawn
i.icon-eye
else
i.icon-eye-off
for column in confList.columns || []
if column.type !== 'record' || column.value !== 'controls'
th.g-annotation-column
if column.title !== undefined
= column.title
else
= `${column.value.substr(0, 1).toUpperCase()}${column.value.substr(1)}`
th.g-annotation-actions
tbody
for annotation in annotations.models
- var name = annotation.get('annotation').name;
- var creatorModel = users.get(annotation.get('creatorId'));
- var creator = creatorModel ? creatorModel.get('login') : annotation.get('creatorId');
-
var name = annotation.get('annotation').name;
var creatorModel = users.get(annotation.get('creatorId'));
var creator = creatorModel ? creatorModel.get('login') : annotation.get('creatorId');
var updatedModel = users.get(annotation.get('updatedId'));
var updater = updatedModel ? updatedModel.get('login') : annotation.get('updatedId');
tr.g-annotation-row(data-annotation-id=annotation.id)
//
td.g-annotation-select
input(type='checkbox', title='Select annotation for bulk actions')
td.g-annotation-toggle
input(type='checkbox', disabled=!canDraw, checked=drawn.has(annotation.id), title='Show annotation')
td.g-annotation-name(title=name)
= name

td.g-annotation-user
a(href=`#user/${annotation.get('creatorId')}`)
= creator

td.g-annotation-date
= (new Date(annotation.get('created'))).toLocaleString()
a.g-annotation-toggle-select(class=canDraw ? 'disabled' : '', title='Show annotation')
if drawn.has(annotation.id)
i.icon-eye
else
i.icon-eye-off
for column in confList.columns || []
if column.type !== 'record' || column.value !== 'controls'
-
var value = (column.type === 'record' ? annotation.get(column.value) || annotation.get('annotation')[column.value] : (column.type === 'metadata' ? ((annotation.get('annotation').attributes || {})[column.value] || '') : '')) || '';
if (column.type === 'record' && column.value === 'creator') {
value = creator;
}
if (column.type === 'record' && column.value === 'updatedId') {
value = updater;
}
td.g-annotation-entry(title=value)
if column.format === 'user'
a(href=`#user/${annotation.get(column.value) || annotation.get(column.value + 'Id')}`)
= value
else if column.format === 'date'
= (new Date(value)).toLocaleString()
else
= value
td.g-annotation-actions
//
if annotation.get('_accessLevel') >= AccessType.WRITE
a.g-annotation-edit(title='Edit annotation')
i.icon-cog
a.g-annotation-download(href=`${apiRoot}/annotation/${annotation.id}`, title='Download', download=`${name}.json`)
i.icon-download
if annotation.get('_accessLevel') >= AccessType.ADMIN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ import '../stylesheets/annotationListWidget.styl';

const AnnotationListWidget = View.extend({
events: {
'change .g-annotation-toggle': '_displayAnnotation',
'click .g-annotation-toggle-select': '_displayAnnotation',
'click .g-annotation-toggle-all': '_displayAllAnnotations',
'click .g-annotation-delete': '_deleteAnnotation',
'click .g-annotation-upload': '_uploadAnnotation',
'click .g-annotation-permissions': '_changePermissions',
'click .g-annotation-metadata': '_annotationMetadata',
'click .g-annotation-row'(evt) {
var $el = $(evt.currentTarget);
$el.find('.g-annotation-toggle > input').click();
$el.find('.g-annotation-toggle-select').click();
},
'click .g-annotation-row a,input'(evt) {
'click .g-annotation-row a,.g-annotation-toggle-select'(evt) {
evt.stopPropagation();
}
},
Expand All @@ -49,32 +51,76 @@ const AnnotationListWidget = View.extend({
this.listenTo(eventStream, 'g:event.large_image_annotation.create', () => this.collection.fetch(null, true));
this.listenTo(eventStream, 'g:event.large_image_annotation.remove', () => this.collection.fetch(null, true));

this.collection.fetch({
itemId: this.model.id,
sort: 'created',
sortdir: -1
}).done(() => {
this._fetchUsers();
});
},

render() {
restRequest({
type: 'GET',
url: 'annotation/folder/' + this.model.get('folderId') + '/create'
}).done((createResp) => {
this.$el.html(annotationList({
item: this.model,
accessLevel: this.model.getAccessLevel(),
creationAccess: createResp,
annotations: this.collection,
users: this.users,
canDraw: this._viewer && this._viewer.annotationAPI(),
drawn: this._drawn,
apiRoot: getApiRoot(),
AccessType
}));
this.createResp = createResp;
restRequest({
url: `folder/${this.model.get('folderId')}/yaml_config/.large_image_config.yaml`
}).done((val) => {
this._liconfig = val || {};
this._confList = this._liconfig.annotationList || {
columns: [{
type: 'record',
value: 'name'
}, {
type: 'record',
value: 'creator',
format: 'user'
}, {
type: 'record',
value: 'created',
format: 'date'
}]
};
this.collection.comparator = _.constant(0);
this._lastSort = this._confList.defaultSort || [{
type: 'record',
value: 'updated',
dir: 'up'
}, {
type: 'record',
value: 'updated',
dir: 'down'
}];
this.collection.sortField = JSON.stringify(this._lastSort.reduce((result, e) => {
result.push([
(e.type === 'metadata' ? 'annotation.attributes.' : '') + e.value,
e.dir === 'down' ? 1 : -1
]);
if (e.type === 'record') {
result.push([
`annotation.${e.value}`,
e.dir === 'down' ? 1 : -1
]);
}
return result;
}, []));
this.collection.fetch({
itemId: this.model.id,
sort: this.collection.sortField || 'created',
sortdir: -1
}).done(() => {
this._fetchUsers();
});
});
});
},

render() {
this.$el.html(annotationList({
item: this.model,
accessLevel: this.model.getAccessLevel(),
creationAccess: this.createResp,
annotations: this.collection,
users: this.users,
canDraw: this._viewer && this._viewer.annotationAPI(),
drawn: this._drawn,
apiRoot: getApiRoot(),
confList: this._confList,
AccessType
}));
return this;
},

Expand All @@ -85,10 +131,14 @@ const AnnotationListWidget = View.extend({
},

_displayAnnotation(evt) {
const $el = $(evt.currentTarget);
const id = $el.parent().data('annotationId');
if (!this._viewer || !this._viewer.annotationAPI()) {
return;
}
const $el = $(evt.currentTarget).closest('.g-annotation-row');
const id = $el.data('annotationId');
const annotation = this.collection.get(id);
if ($el.find('input').prop('checked')) {
const startedOn = $el.find('.g-annotation-toggle-select i.icon-eye').length;
if (!startedOn) {
this._drawn.add(id);
annotation.fetch().then(() => {
if (this._drawn.has(id)) {
Expand All @@ -100,6 +150,36 @@ const AnnotationListWidget = View.extend({
this._drawn.delete(id);
this._viewer.removeAnnotation(annotation);
}
$el.find('.g-annotation-toggle-select i').toggleClass('icon-eye', !startedOn).toggleClass('icon-eye-off', !!startedOn);
const anyOn = this.collection.some((annotation) => this._drawn.has(annotation.id));
this.$el.find('th.g-annotation-toggle i').toggleClass('icon-eye', !!anyOn).toggleClass('icon-eye-off', !anyOn);
},

_displayAllAnnotations(evt) {
if (!this._viewer || !this._viewer.annotationAPI()) {
return;
}
const anyOn = this.collection.some((annotation) => this._drawn.has(annotation.id));
this.collection.forEach((annotation) => {
const id = annotation.id;
let isDrawn = this._drawn.has(annotation.id);
if (anyOn && isDrawn) {
this._drawn.delete(id);
this._viewer.removeAnnotation(annotation);
isDrawn = false;
} else if (!anyOn && !isDrawn) {
this._drawn.add(id);
annotation.fetch().then(() => {
if (this._drawn.has(id)) {
this._viewer.drawAnnotation(annotation);
}
return null;
});
isDrawn = true;
}
this.$el.find(`.g-annotation-row[data-annotation-id="${id}"] .g-annotation-toggle-select i`).toggleClass('icon-eye', !!isDrawn).toggleClass('icon-eye-off', !isDrawn);
});
this.$el.find('th.g-annotation-toggle i').toggleClass('icon-eye', !anyOn).toggleClass('icon-eye-off', !!anyOn);
},

_deleteAnnotation(evt) {
Expand Down Expand Up @@ -201,6 +281,7 @@ const AnnotationListWidget = View.extend({
_fetchUsers() {
this.collection.each((model) => {
this.users.add({'_id': model.get('creatorId')});
this.users.add({'_id': model.get('updatedId')});
});
$.when.apply($, this.users.map((model) => {
return model.fetch();
Expand Down
Loading