From bc12f88fa83db486ee4ebd7df7952c3a54c4a822 Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Thu, 1 Jul 2021 12:31:07 +0200 Subject: [PATCH 1/9] Expose a missing feature in the FlatItem tests --- .../common-adapters/flat-item-model-test.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/src/common-adapters/flat-item-model-test.ts b/frontend/src/common-adapters/flat-item-model-test.ts index c801cea4..7ac686f5 100644 --- a/frontend/src/common-adapters/flat-item-model-test.ts +++ b/frontend/src/common-adapters/flat-item-model-test.ts @@ -5,8 +5,13 @@ import { import { Events } from 'backbone'; import { event, timeout, startStore, endStore } from '../test-util'; -import { contentClass, readerClass } from '../mock-data/mock-ontology'; +import { + contentClass, + readerClass, + descriptionOfProperty, +} from '../mock-data/mock-ontology'; import mockItems from '../mock-data/mock-items'; + import { skos, dcterms, oa, readit, item } from '../common-rdf/ns'; import { asNative } from '../common-rdf/conversion'; import Node from '../common-rdf/node'; @@ -240,6 +245,20 @@ describe('FlatItem', function() { ))); }); + it('can flatten a bare property', async function() { + const ontologyProp = new Node(descriptionOfProperty); + const flatProp = new FlatItem(ontologyProp); + await completion(flatProp); + expect(flatProp.attributes).toEqual({ + id: ontologyProp.id, + class: ontologyProp, + classLabel: 'description of', + cssClass: 'is-readit-descriptionof', + creator: expectedFlatAttributes.creator, + created: expectedFlatAttributes.created, + }); + }); + it('can flatten a bare target', async function() { const items = getFullItems(); const flatTarget = new FlatItem(items.target); From 1dfd744f98ffa4ad6497882cbab5aea3db055d7f Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Thu, 1 Jul 2021 13:18:42 +0200 Subject: [PATCH 2/9] Add isRdfProperty utility function --- .../utilities/linked-data-utilities-test.ts | 24 +++++++++++++++++++ .../src/utilities/linked-data-utilities.ts | 14 ++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/frontend/src/utilities/linked-data-utilities-test.ts b/frontend/src/utilities/linked-data-utilities-test.ts index f26bdd48..d8a026fc 100644 --- a/frontend/src/utilities/linked-data-utilities-test.ts +++ b/frontend/src/utilities/linked-data-utilities-test.ts @@ -4,6 +4,7 @@ import { getLabelFromId, getCssClassName, isRdfsClass, + isRdfProperty, isOntologyClass, isBlank, transitiveClosure, @@ -174,6 +175,29 @@ describe('utilities', function () { }); }); + describe('isRdfProperty', function() { + it('recognizes straight-on properties', function() { + const yes = new Node({ '@type': rdf.Property }), no = new Node(); + expect(isRdfProperty(yes)).toBeTruthy(); + expect(isRdfProperty(no)).toBeFalsy(); + }); + + it('recognizes OWL object properties', function() { + const owlProp = new Node({ '@type': owl.ObjectProperty }); + expect(isRdfProperty(owlProp)).toBeTruthy(); + }); + + it('recognizes subproperties', function() { + const subProp = new Node({ [rdfs.subPropertyOf]: rdfs.range }); + expect(isRdfProperty(subProp)).toBeTruthy(); + }); + + it('recognizes inverse properties', function() { + const inverseProp = new Node({ [owl.inverseOf]: rdfs.range }); + expect(isRdfProperty(inverseProp)).toBeTruthy(); + }); + }); + describe('isOntologyClass', function() { it('is robust against nodes without an id', function() { const node = new Node(); diff --git a/frontend/src/utilities/linked-data-utilities.ts b/frontend/src/utilities/linked-data-utilities.ts index 447230b4..77458eaf 100644 --- a/frontend/src/utilities/linked-data-utilities.ts +++ b/frontend/src/utilities/linked-data-utilities.ts @@ -4,7 +4,9 @@ import ldChannel from '../common-rdf/radio'; import { Identifier, isIdentifier } from '../common-rdf/json'; import Node, { isNode, NodeLike } from '../common-rdf/node'; import Graph, { ReadOnlyGraph } from '../common-rdf//graph'; -import { nlp, skos, rdfs, readit, dcterms, owl, schema } from '../common-rdf/ns'; +import { + nlp, skos, rdf, rdfs, readit, dcterms, owl, schema, +} from '../common-rdf/ns'; export const labelKeys = [skos.prefLabel, rdfs.label, skos.altLabel, readit('name'), dcterms.title]; @@ -66,6 +68,16 @@ export function isRdfsClass(node: Node): boolean { return node.has(rdfs.subClassOf) || node.has('@type', owl.Class) || node.has('@type', rdfs.Class); } +/** + * Check if a node is a rdf:Property, i.e., has rdf:Property or + * owl:ObjectProperty as (one of its) type(s) or has a non-empty + * rdfs:subPropertyOf or owl:inverseOf property. + * @param node The node to evaluate + */ +export function isRdfProperty(node: Node): boolean { + return node.has(rdfs.subPropertyOf) || node.has(owl.inverseOf) || node.has('@type', rdf.Property) || node.has('@type', owl.ObjectProperty); +} + /** * Check if a node is an annotation category used in the class picker when editing annotations. * @param node The node to evaluate From 05197a2258a3e8a5e078012eb22e47654eea6bb1 Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Thu, 1 Jul 2021 13:19:09 +0200 Subject: [PATCH 3/9] Organize imports and rewrap comments while we're at it --- frontend/src/utilities/linked-data-utilities-test.ts | 12 +++++++----- frontend/src/utilities/linked-data-utilities.ts | 7 ++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/src/utilities/linked-data-utilities-test.ts b/frontend/src/utilities/linked-data-utilities-test.ts index d8a026fc..bb72a794 100644 --- a/frontend/src/utilities/linked-data-utilities-test.ts +++ b/frontend/src/utilities/linked-data-utilities-test.ts @@ -1,4 +1,10 @@ -import { rdf, rdfs, skos, item } from '../common-rdf/ns'; +import { startStore, endStore } from '../test-util'; + +import { rdf, rdfs, owl, skos, item } from '../common-rdf/ns'; +import { FlatLdObject, FlatLdGraph } from '../common-rdf/json'; +import Node from '../common-rdf/node'; +import Graph from '../common-rdf/graph'; + import { getLabel, getLabelFromId, @@ -11,10 +17,6 @@ import { getRdfSuperClasses, getRdfSubClasses, } from './linked-data-utilities'; -import { FlatLdObject, FlatLdGraph } from '../common-rdf/json'; -import Node from '../common-rdf/node'; -import Graph from '../common-rdf/graph'; -import { startStore, endStore } from '../test-util'; function getDefaultNode(): Node { return new Node(getDefaultAttributes()); diff --git a/frontend/src/utilities/linked-data-utilities.ts b/frontend/src/utilities/linked-data-utilities.ts index 77458eaf..28b88cf3 100644 --- a/frontend/src/utilities/linked-data-utilities.ts +++ b/frontend/src/utilities/linked-data-utilities.ts @@ -60,8 +60,8 @@ export function asURI(source: Node | string): string { } /** - * Check if a node is a rdfs:Class, i.e. has rdfs:Class or owl:Class as (one of its) type(s) or - * has a non-empty rdfs:subClassOf property. + * Check if a node is a rdfs:Class, i.e., has rdfs:Class or owl:Class as (one of + * its) type(s) or has a non-empty rdfs:subClassOf property. * @param node The node to evaluate */ export function isRdfsClass(node: Node): boolean { @@ -79,7 +79,8 @@ export function isRdfProperty(node: Node): boolean { } /** - * Check if a node is an annotation category used in the class picker when editing annotations. + * Check if a node is an annotation category used in the class picker when + * editing annotations. * @param node The node to evaluate */ export function isAnnotationCategory(node: Node): boolean { From 645425c9abe1b0855c858c99515a374792c2b298 Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Thu, 1 Jul 2021 13:24:11 +0200 Subject: [PATCH 4/9] Flatten properties in the same way as classes --- frontend/src/common-adapters/flat-item-model.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/common-adapters/flat-item-model.ts b/frontend/src/common-adapters/flat-item-model.ts index fa4c547a..a3a2907b 100644 --- a/frontend/src/common-adapters/flat-item-model.ts +++ b/frontend/src/common-adapters/flat-item-model.ts @@ -9,6 +9,7 @@ import { getCssClassName, isBlank, isRdfsClass, + isRdfProperty, } from '../utilities/linked-data-utilities'; /** @@ -174,7 +175,7 @@ export default class FlatItem extends Model { } else if (node.has('@type', oa.TextQuoteSelector)) { this.set('quoteSelector', node); this._setCompletionFlag(F_COMPLETE ^ F_TEXT); - } else if (isRdfsClass(node)) { + } else if (isRdfsClass(node) || isRdfProperty(node)) { this.set('class', node); this._setCompletionFlag(F_COMPLETE ^ F_CLASS); } else { From e573e260910db1e4bc349e1cc6f966a233d6597b Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Thu, 1 Jul 2021 13:40:33 +0200 Subject: [PATCH 5/9] Add a test that exposes the missing inverse property label bug --- frontend/src/utilities/relation-utilities-test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/utilities/relation-utilities-test.ts b/frontend/src/utilities/relation-utilities-test.ts index 1a6e1d4a..d070fa5c 100644 --- a/frontend/src/utilities/relation-utilities-test.ts +++ b/frontend/src/utilities/relation-utilities-test.ts @@ -1,6 +1,6 @@ import { constant } from 'lodash'; -import { startStore, endStore } from '../test-util'; +import { startStore, endStore, event } from '../test-util'; import mockOntology from '../mock-data/mock-ontology'; import { anno2ReaderInstance } from '../mock-data/mock-items'; @@ -8,6 +8,7 @@ import ldChannel from '../common-rdf/radio'; import { readit, skos, rdfs } from '../common-rdf/ns'; import Node from '../common-rdf/node'; import Graph from '../common-rdf/graph'; +import FlatItem from '../common-adapters/flat-item-model'; import { applicablePredicates } from './relation-utilities'; @@ -40,5 +41,12 @@ describe('relation utilities', function() { expect(predicates.at(0).get(skos.prefLabel)[0]).toBe('description of'); expect(predicates.at(1).get(rdfs.label)[0]).toBe('inverse of description of'); }); + + it('generates inverse properties that can be flattened', async function() { + const inverse = applicablePredicates(readit('Reader')).at(1); + const flat = new FlatItem(inverse); + await event(flat, 'complete'); + expect(flat.get('classLabel')).toBe('inverse of description of'); + }); }); }); From 041172c00517c541a11a7efbadf8f6a801b4550c Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Thu, 1 Jul 2021 13:43:13 +0200 Subject: [PATCH 6/9] Theoretically fix the bug (actually run into an exception) --- frontend/src/utilities/relation-utilities.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/utilities/relation-utilities.ts b/frontend/src/utilities/relation-utilities.ts index 2a224d0b..b878a7cf 100644 --- a/frontend/src/utilities/relation-utilities.ts +++ b/frontend/src/utilities/relation-utilities.ts @@ -2,7 +2,7 @@ import { forEach, some, keys, ListIterator, isString } from 'lodash'; import Collection from '../core/collection'; import ldChannel from '../common-rdf/radio'; -import { rdfs, owl, item } from '../common-rdf/ns'; +import { rdf, rdfs, owl, item } from '../common-rdf/ns'; import Graph from '../common-rdf/graph'; import Node from '../common-rdf/node'; import ItemGraph from '../common-adapters/item-graph'; @@ -48,6 +48,7 @@ export function applicablePredicates(model: Node | string): Graph { ontology.filter(matchRelatee(rdfs.range, allTypes)).forEach(direct => { let inverse = getInverse(direct, ontology); if (!inverse) inverse = new Node({ + '@type': rdf.Property, [rdfs.label]: `inverse of ${getLabel(direct)}`, [owl.inverseOf]: direct, }); From 08a18edb865462960228c4f68c659cc6c7884753 Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Thu, 1 Jul 2021 13:49:05 +0200 Subject: [PATCH 7/9] Fix unguarded node.id accesses (finally really fix the bug) --- frontend/src/common-adapters/flat-item-model.ts | 2 +- frontend/src/utilities/linked-data-utilities.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/common-adapters/flat-item-model.ts b/frontend/src/common-adapters/flat-item-model.ts index a3a2907b..f89095a2 100644 --- a/frontend/src/common-adapters/flat-item-model.ts +++ b/frontend/src/common-adapters/flat-item-model.ts @@ -239,7 +239,7 @@ export default class FlatItem extends Model { processBody(body: Node) { if (isBlank(body)) return this.set('item', body); const id = body.id; - if (id.startsWith(item())) return this.set('item', body); + if (id && id.startsWith(item())) return this.set('item', body); // We can add another line like the above to add support for // preannotations. return this.set('class', body); diff --git a/frontend/src/utilities/linked-data-utilities.ts b/frontend/src/utilities/linked-data-utilities.ts index 28b88cf3..e08b68f9 100644 --- a/frontend/src/utilities/linked-data-utilities.ts +++ b/frontend/src/utilities/linked-data-utilities.ts @@ -40,7 +40,8 @@ export function getCssClassName(node: Node): string { if (label) { label = label.replace(new RegExp(' ', 'g'), '').replace(new RegExp('[\(\)\/]', 'g'), '').toLowerCase(); - if (node.id.startsWith(nlp())) { + const id = node.id; + if (id && id.startsWith(nlp())) { return `is-nlp-${label}` } else { From 8d0a2c720977befa60ec02c747050736f728c40e Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Thu, 1 Jul 2021 14:08:24 +0200 Subject: [PATCH 8/9] Clean up test views at the end of each spec A leftover select2 widget was overlaid on the Jasmine browser interface because I didn't do this originally. --- frontend/src/semantic-search/chain-view-test.ts | 3 +++ frontend/src/semantic-search/dropdown-view-test.ts | 3 +++ frontend/src/semantic-search/multibranch-view-test.ts | 1 + 3 files changed, 7 insertions(+) diff --git a/frontend/src/semantic-search/chain-view-test.ts b/frontend/src/semantic-search/chain-view-test.ts index eff059e7..42c3a520 100644 --- a/frontend/src/semantic-search/chain-view-test.ts +++ b/frontend/src/semantic-search/chain-view-test.ts @@ -36,6 +36,7 @@ describe('semantic search ChainView', function() { await event(view.items[0], 'ready'); expect(view.items[0]['typeGroup']).toBeDefined(); expect(view.items[0]['typeGroup'].collection.length).toBe(4); + view.remove(); }); it('can be constructed with a preselection', async function() { @@ -46,6 +47,7 @@ describe('semantic search ChainView', function() { await event(view.items[0], 'ready'); expect(view.items[0]['predicateGroup']).toBeDefined(); expect(view.items[0]['predicateGroup'].collection.length).toBe(2); + view.remove(); }); it('appends dropdowns as choices are made', async function() { @@ -58,5 +60,6 @@ describe('semantic search ChainView', function() { await event(dropdown2, 'ready'); expect(dropdown2.predicateGroup).toBeDefined(); expect(dropdown2.predicateGroup.collection.length).toBe(2); + view.remove(); }); }); diff --git a/frontend/src/semantic-search/dropdown-view-test.ts b/frontend/src/semantic-search/dropdown-view-test.ts index a74264d7..2e34945b 100644 --- a/frontend/src/semantic-search/dropdown-view-test.ts +++ b/frontend/src/semantic-search/dropdown-view-test.ts @@ -40,6 +40,7 @@ describe('semantic search DropdownView', function() { expect(view.$('optgroup:nth-child(3) option').length).toBe(4); expect(view.$('optgroup:nth-child(3)').text()).toContain('Reader'); expect(view.$('optgroup:nth-child(3)').text()).toContain('Person'); + view.remove(); }); it('can be constructed with a property', async function() { @@ -56,6 +57,7 @@ describe('semantic search DropdownView', function() { expect(view.$('optgroup:nth-child(3) option').length).toBe(2); expect(view.$('optgroup:nth-child(3)').text()).toContain('Reader'); expect(view.$('optgroup:nth-child(3)').text()).toContain('Person'); + view.remove(); }); it(`can be constructed with a single class`, async function() { @@ -75,5 +77,6 @@ describe('semantic search DropdownView', function() { expect(view.$('optgroup:nth-child(4)').prop('label')).toBe('traverse predicate'); expect(view.$('optgroup:nth-child(4) option').length).toBe(2); expect(view.$('optgroup:nth-child(4)').text()).toContain('description of'); + view.remove(); }); }); diff --git a/frontend/src/semantic-search/multibranch-view-test.ts b/frontend/src/semantic-search/multibranch-view-test.ts index 443e0921..394b340e 100644 --- a/frontend/src/semantic-search/multibranch-view-test.ts +++ b/frontend/src/semantic-search/multibranch-view-test.ts @@ -4,5 +4,6 @@ describe('semantic search MultibranchView', function() { it('can be constructed in isolation', function() { const view = new Multibranch(); expect(view.$el.children().length).toBe(1); + view.remove(); }); }); From 858ba8a411a037308c582fbf3cc4f92ce30bc0b6 Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Thu, 1 Jul 2021 14:56:11 +0200 Subject: [PATCH 9/9] Bind semSearch Dropdown automatic open on view instead of collection This prevents a race condition where the select2 plugin might attempt to open a dropdown that no longer exists. --- frontend/src/semantic-search/dropdown-view.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/semantic-search/dropdown-view.ts b/frontend/src/semantic-search/dropdown-view.ts index 8d4bec62..f3e7fd0d 100644 --- a/frontend/src/semantic-search/dropdown-view.ts +++ b/frontend/src/semantic-search/dropdown-view.ts @@ -147,9 +147,12 @@ export default class Dropdown extends CompositeView { this.render(); // Conditionally open the dropdown on creation. This helps the user to // type her way through the form, saving keystrokes. - if (this.model.has('precedent') && !this.model.has('selection')) ( - this.typeGroup || this.predicateGroup - ).collection.once('complete:all', this.open, this); + if (this.model.has('precedent') && !this.model.has('selection')) { + this.listenToOnce( + (this.typeGroup || this.predicateGroup).collection, + 'complete:all', this.open + ); + } } subviews(): SubViewDescription[] {