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

Bugfixes related to inverse property labels #488

Merged
merged 9 commits into from
Jul 1, 2021
21 changes: 20 additions & 1 deletion frontend/src/common-adapters/flat-item-model-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/common-adapters/flat-item-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getCssClassName,
isBlank,
isRdfsClass,
isRdfProperty,
} from '../utilities/linked-data-utilities';

/**
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -238,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);
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/semantic-search/chain-view-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand All @@ -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();
});
});
3 changes: 3 additions & 0 deletions frontend/src/semantic-search/dropdown-view-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand All @@ -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();
});
});
9 changes: 6 additions & 3 deletions frontend/src/semantic-search/dropdown-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/semantic-search/multibranch-view-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
36 changes: 31 additions & 5 deletions frontend/src/utilities/linked-data-utilities-test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
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,
getCssClassName,
isRdfsClass,
isRdfProperty,
isOntologyClass,
isBlank,
transitiveClosure,
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());
Expand Down Expand Up @@ -174,6 +177,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();
Expand Down
24 changes: 19 additions & 5 deletions frontend/src/utilities/linked-data-utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -38,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 {
Expand All @@ -58,16 +61,27 @@ 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 {
return node.has(rdfs.subClassOf) || node.has('@type', owl.Class) || node.has('@type', rdfs.Class);
}

/**
* Check if a node is an annotation category used in the class picker when editing annotations.
* 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
*/
export function isAnnotationCategory(node: Node): boolean {
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/utilities/relation-utilities-test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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';

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';

Expand Down Expand Up @@ -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');
});
});
});
3 changes: 2 additions & 1 deletion frontend/src/utilities/relation-utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
});
Expand Down