Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1cc01c4
Add venv to gitignore
tijmenbaarda Mar 26, 2026
af7c6f7
Change icon of annotation button to pen
tijmenbaarda Mar 26, 2026
37a762c
Prefill original text for new correcting annotations
tijmenbaarda Mar 26, 2026
63057a0
Do not use nested table
tijmenbaarda Mar 26, 2026
23bd7c4
Allow submitting annotation with Enter
tijmenbaarda Mar 27, 2026
98da03b
Allow OA.editing as a motivation in backend
tijmenbaarda Mar 27, 2026
ecc1628
Correctly show edit annotations
tijmenbaarda Mar 27, 2026
35eaa6a
Merge branch 'develop' into feature/corrections
tijmenbaarda Mar 27, 2026
b0a5a36
Separate motivations for additions and corrections
tijmenbaarda Apr 2, 2026
6a3c5e6
Show corrected values in table
tijmenbaarda Apr 9, 2026
292a90b
Update table when annotations are deleted
tijmenbaarda Apr 9, 2026
2543014
Give modal focus on opening to make Esc key work
tijmenbaarda Apr 9, 2026
33b326f
Show annotations to empty fields in table
tijmenbaarda Apr 9, 2026
2b4eecc
Close annotation editor on escape
tijmenbaarda Apr 26, 2026
1e22a2a
Allow dismissing the modal with escape after closing annotation editor
tijmenbaarda Apr 26, 2026
5a6b981
Add clarification about textarea focus timeout
tijmenbaarda May 21, 2026
5a147b1
Remove redundant line
tijmenbaarda May 21, 2026
bec3dbd
Fix incomplete comment
tijmenbaarda May 21, 2026
6af110a
Simplify code
tijmenbaarda May 21, 2026
cde9d6f
Use has method of Backbone.js model
tijmenbaarda May 21, 2026
bddd079
Change confusing template data key name
tijmenbaarda May 21, 2026
4a1552f
Remove code duplication
tijmenbaarda May 21, 2026
84e7017
Update changed attribute name in other area
tijmenbaarda May 28, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Virtual environment
ENV/
.env/
venv/

# node
node_modules
Expand Down
6 changes: 3 additions & 3 deletions backend/annotations/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,14 @@ def post(self, request, **kwargs):
if s3 != target:
raise ValidationError('Source must be a property of the target')
motivation = request_graph.value(s1, OA.motivatedBy, None, OA.commenting)
if motivation == OA.commenting:
if motivation in (OA.commenting, OA.editing, OA.describing):
if not isinstance(body, Literal):
raise ValidationError('Comment must be a literal')
raise ValidationError('Body must be a literal when commenting or editing')
elif motivation == OA.tagging:
if not isinstance(body, URIRef):
raise ValidationError('Tag must be a URI')
else:
raise ValidationError('Only commenting or tagging is supported at this time')
raise ValidationError('Only commenting, tagging, editing and describing are supported')

# Step 2: normalize the data. We modify the request_graph in place
# because this is convenient.
Expand Down
38 changes: 34 additions & 4 deletions frontend/vre/annotation/annotation.edit.view.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import confirmDeletionTemplate from './annotation.confirm.deletion.mustache';
import {glossary} from "../utils/glossary";

export var AnnotationEditView = View.extend({
tagName: 'tr',
tagName: 'div',
Comment thread
tijmenbaarda marked this conversation as resolved.
className: 'form-inline',
template: annotationEditTemplate,
glossaryTemplate: annotationTagEditTemplate,
events: {
'submit': 'submit',
'reset': 'reset',
'keydown textarea': 'handleTextareaKeydown',
},
initialize: function(options) {
_.assign(this, _.pick(options, ['existing']));
_.assign(this, _.pick(options, ['existing', 'defaultText']));
this.render().$el.popover({
container: 'body',
content: confirmDeletionTemplate(this),
Expand All @@ -38,6 +39,13 @@ export var AnnotationEditView = View.extend({
if (this.model.get('motivation') === 'oa:tagging') {
glossary.on('update', this.render);
}
setTimeout(() => {
var el = this.$('textarea').get(0);
if (el) {
el.focus();
el.select();
}
}, 0);
Comment thread
tijmenbaarda marked this conversation as resolved.
},
render: function() {
if (this.model.get('motivation') === 'oa:tagging') {
Expand All @@ -53,21 +61,31 @@ export var AnnotationEditView = View.extend({
if (tag) this.$('select').val(tag);
this.$('select').trigger('change');
} else {
var text = this.model.get('oa:hasBody') || this.defaultText;
this.$el.html(this.template({
currentText: this.model.get('oa:hasBody'),
currentText: text,
cid: this.cid,
}));
}
return this;
},
remove: function() {
var parentElement = this.el.parentElement;
this.$el.popover('dispose');
this.trashConfirmer.off();
this.trashCanceller.off();
if (this.model.get('motivation') === 'oa:tagging') {
this.$('select').select2('destroy');
}
return View.prototype.remove.call(this);
var result = View.prototype.remove.call(this);
if (parentElement) {
// Give focus to parent element to make escape key work
if (!parentElement.hasAttribute('tabindex')) {
parentElement.setAttribute('tabindex', '-1');
}
parentElement.focus();
}
return result;
},
submit: function(event) {
event.preventDefault();
Expand All @@ -91,4 +109,16 @@ export var AnnotationEditView = View.extend({
this.$('button[aria-label="Delete"]').popover('hide');
this.trigger('trash', this);
},
handleTextareaKeydown: function(event) {
/* We want to use a textarea to give the user more space, but
most of the time they will write only one line. */
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
this.submit(event);
} else if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
this.reset(event);
}
}
});
4 changes: 2 additions & 2 deletions frontend/vre/annotation/annotation.edit.view.mustache
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<td><textarea
<div><textarea
type=text
name=value
form=form-{{cid}}
Expand All @@ -11,4 +11,4 @@
<button type=button class="btn btn-danger" aria-label=Delete>
<span class="fa fa-trash" aria-hidden=true></span>
</button>
</form></td>
</form></div>
2 changes: 1 addition & 1 deletion frontend/vre/annotation/annotation.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export var Annotation = JsonLdModel.extend({
urlRoot: '/api/annotation/',

getDisplayText: function() {
if (this.get('motivation') === 'oa:commenting') {
if (['oa:commenting', 'oa:editing', 'oa:describing'].includes(this.get('motivation'))) {
return this.get('oa:hasBody');
} else if (this.get('motivation') === 'oa:tagging') {
var id = this.get('tagURL');
Expand Down
3 changes: 2 additions & 1 deletion frontend/vre/annotation/comment.view.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export var CommentView = View.extend({
var updatedDate = this.model.getUpdatedDate();
var author = this.model.getAuthor();
Object.assign(templateData, {
isFieldAnnotation: this.fieldAnnotation,
isFieldComment: this.fieldAnnotation && this.model.get('motivation') === 'oa:commenting',
originalText: this.model.get('motivation') === 'oa:editing' && this.model.get('edpopcol:originalText'),
isTag: this.model.get('motivation') === 'oa:tagging',
displayText: this.model.getDisplayText(),
author: (author ? author.getUsername() : null),
Expand Down
2 changes: 1 addition & 1 deletion frontend/vre/annotation/comment.view.mustache
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<td title="{{publishedDate}}{{#updatedDate}} (updated: {{updatedDate}}){{/updatedDate}}"><span {{#isFieldAnnotation}}class="field-annotation"{{/isFieldAnnotation}}>{{#isTag}}<b>Tag: </b>{{/isTag}}{{displayText}} </span>{{#author}}<span class="authored-by">@{{author}}</span>{{/author}} </td>
<td title="{{#originalText}}Text before correction: {{originalText}}. {{/originalText}}{{publishedDate}}{{#updatedDate}} (updated: {{updatedDate}}){{/updatedDate}}"><span {{#isFieldComment}}class="field-annotation"{{/isFieldComment}}>{{#isTag}}<b>Tag: </b>{{/isTag}}{{displayText}} </span>{{#author}}<span class="authored-by">@{{author}}</span>{{/author}} </td>
7 changes: 5 additions & 2 deletions frontend/vre/field/annotatable.view.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export var AnnotatableView = AggregateView.extend({
this.listenTo(this.collection, 'edit', this.edit);
},

edit: function(model) {
edit: function(model, defaultText) {
var project = vreChannel.request('projects:current').id,
editTarget = model.clone().set('context', project),
preExisting = this.collection.get(editTarget),
Expand All @@ -32,7 +32,10 @@ export var AnnotatableView = AggregateView.extend({
this.items.splice(index, 1, newRow);
oldRow.remove();
} else {
newRow = new AnnotationEditView({model: editTarget});
newRow = new AnnotationEditView({
model: editTarget,
defaultText: defaultText,
});
this.items.push(newRow);
}
this.placeItems();
Expand Down
57 changes: 49 additions & 8 deletions frontend/vre/field/field.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,46 @@ import {
} from "../utils/record-ontology";
import {getStringLiteral} from "../utils/jsonld.model";

/**
* Get correction for a field value based on
* @param {Object} value - the specific value to get the display of
* @param {Field} field - the whole field object
* @param {Annotations} [annotations] - annotations for the record
* @returns {string|undefined}
*/
function getCorrectedTextForFieldValue(value, field, annotations) {
if (!annotations) return undefined;
var annotation;
if (!value) {
// Field missing in the source data: edpopcol:originalText should not be present
annotation = annotations.find(x => {
return x.get('edpopcol:field') === field.id && !x.get('edpopcol:originalText');
});
} else {
// Field present in the source data: check if edpopcol:originalText matches
annotation = annotations.find(x => {
return x.get('edpopcol:field') === field.id && x.get('edpopcol:originalText') === value['edpoprec:originalText']
});
}
Comment thread
tijmenbaarda marked this conversation as resolved.
Outdated
if (annotation) {
return annotation.get('oa:hasBody');
}
return undefined;
Comment thread
tijmenbaarda marked this conversation as resolved.
Outdated
}

/**
* Get a default main display string of the `value` attribute of a
* field flattened using {@link FlatterFields}. Currently, this is
* the normalized "summary text" if available and otherwise the
* original text from the source database.
* @param {object} value
* original text from the source database. If
Comment thread
tijmenbaarda marked this conversation as resolved.
Outdated
* @param {Object} value - the specific value to get the display of
* @param {Field} field - the whole field object
* @param {Annotations} [annotations] - annotations for the field
* @return {string}
*/
function getMainDisplayOfFieldValue(value) {
function getMainDisplayOfFieldValue(value, field, annotations = null) {
var correctedText = getCorrectedTextForFieldValue(value, field, annotations);
if (correctedText) return correctedText;
return value['edpoprec:summaryText'] || value['edpoprec:originalText'];
}

Expand All @@ -28,16 +59,26 @@ export var Field = Backbone.Model.extend({
idAttribute: 'key',
/**
* Get the default rendering of the field
*
* @param {Annotations} annotations - Optional annotations for the field
* @return {string}
*/
getMainDisplay() {
getMainDisplay(annotations = null) {
// Currently, only normalizedText is supported.
const value = this.get('value');
if (!value) return value;
if (_.isArray(value)) {
return _.map(value, getMainDisplayOfFieldValue).join(' ; ');
if (!value) {
// Field is missing in the source data: return annotation if present
var correctedText = getCorrectedTextForFieldValue(null, this, annotations);
if (correctedText)
return correctedText;
else
return '';
Comment thread
tijmenbaarda marked this conversation as resolved.
Outdated
} else if (_.isArray(value)) {
// Field is repeated: concatenate all values
return _.map(value, (value) => getMainDisplayOfFieldValue(value, this, annotations)).join(' ; ');
} else {
return getMainDisplayOfFieldValue(value, this, annotations);
}
Comment on lines +70 to 75
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't really need the elses here because every branch returns immediately. I won't insist that you need to remove them, though.

return getMainDisplayOfFieldValue(value);
},
getFieldInfo() {
const property = properties.get(this.id);
Expand Down
19 changes: 14 additions & 5 deletions frontend/vre/field/field.view.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export var FieldView = AnnotatableView.extend({

initialize: function(options) {
this.render().listenTo(this.model, 'change:value', this.render);
this.listenTo(this.collection, 'update', this.render);
parent(this).initialize.call(this, options);
},

Expand All @@ -23,12 +24,18 @@ export var FieldView = AnnotatableView.extend({
},

events: {
'click a.comment': 'addComment',
'click a.comment': 'addEdit',
},

hasEdit: function() {
return this.collection.length > 0; // TODO check for just edits
},

renderContainer: function() {
const templateData = {
field: this.model.get('key'),
hasEdit: !this.hasEdit(),
Comment thread
tijmenbaarda marked this conversation as resolved.
Outdated
isEmpty: typeof this.model.get('value') === 'undefined',
Comment thread
tijmenbaarda marked this conversation as resolved.
Outdated
};
// Check if model is of Field model before using these methods, because
// there are some tests relating to old-style annotations that assign
Expand All @@ -47,18 +54,20 @@ export var FieldView = AnnotatableView.extend({
return this;
},

addComment: function(event) {
addEdit: function(event) {
event.preventDefault();
var fieldId = this.model.get('key');
var fieldContents = this.model.get('value');
var fieldContents = this.model.get('value'); // If undefined, this field did not exist in the original record
var attributes = {
"oa:hasSource": this.collection.underlying.target,
"edpopcol:field": fieldId,
"motivation": "oa:commenting",
"motivation": (fieldContents ? "oa:editing" : "oa:describing"),
};
if (fieldContents) {
attributes['edpopcol:originalText'] = fieldContents['edpoprec:originalText'];
this.edit(new Annotation(attributes), fieldContents['edpoprec:originalText']);
} else {
this.edit(new Annotation(attributes));
}
this.edit(new Annotation(attributes));
},
});
9 changes: 6 additions & 3 deletions frontend/vre/field/field.view.mustache
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<td><u title="{{fieldInfo.description}}">{{fieldInfo.name}}</u></td>
<td><div>
<td>{{#hasEdit}}<div>
{{#linkedRecordUri}}<a href="/record/{{linkedRecordUri}}" target="_blank" title="Go to linked record">{{displayText}}</a>{{/linkedRecordUri}}
{{^linkedRecordUri}}{{displayText}}{{/linkedRecordUri}}
</div><div class="annotations"></div></td>
<td><a class="fa-regular fa-message comment" role="button"></a></td>
</div>{{/hasEdit}}<div class="annotations"></div></td>
<td>{{#hasEdit}}
{{#isEmpty}}<a class="fa-solid fa-plus comment text-decoration-none" role="button" title="Make addition"></a>{{/isEmpty}}
{{^isEmpty}}<a class="fa-solid fa-pen comment text-decoration-none" role="button" title="Add correction"></a>{{/isEmpty}}
{{/hasEdit}}</td>
3 changes: 3 additions & 0 deletions frontend/vre/record/record.detail.view.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export var RecordDetailView = CompositeView.extend({
attributes: {
'role': 'dialog',
'data-bs-focus': 'false',
'data-bs-keyboard': 'true',
'tabindex': '-1',
},

subviews: [{
Expand Down Expand Up @@ -131,6 +133,7 @@ export var RecordDetailView = CompositeView.extend({

display: function() {
this.$el.modal('show');
this.$el.focus();
return this;
},

Expand Down
8 changes: 5 additions & 3 deletions frontend/vre/record/record.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Annotations } from '../annotation/annotation.model';
import { JsonLdModel, JsonLdNestedCollection} from "../utils/jsonld.model";
import { FlatFields } from "../field/field.model";
import {vreChannel} from "../radio";
import { FilteredCollection } from "../utils/filtered.collection";

/**
* Get the URL to fetch the record on its own from the backend.
Expand Down Expand Up @@ -56,7 +55,7 @@ export var Record = JsonLdModel.extend({
id: this.id, // id is used for identification by Tabular by default
};
fields.forEach((field) => {
data[field.id] = field.getMainDisplay();
data[field.id] = field.getMainDisplay(this.annotations);
});
return data;
},
Expand All @@ -66,7 +65,10 @@ export var Record = JsonLdModel.extend({
if (!this.isNew()) {
this.annotations.fetch();
}
this.annotations.on('sync', () => this.trigger('annotations:loaded', this));
/* Trigger annotations:loaded to rerender the table row with annotations
on sync (which happens after fetching, creating and editing an annotation
and remove (which happens after deleting an annotation) */
this.annotations.on('sync remove', () => this.trigger('annotations:loaded', this));
Comment thread
jgonggrijp marked this conversation as resolved.
}
return this.annotations;
},
Expand Down
Loading