From 0ac3cce784f7d52712e4e87987095e7816ec002c Mon Sep 17 00:00:00 2001 From: robyngit Date: Tue, 15 Oct 2024 10:13:36 -0400 Subject: [PATCH 1/9] Fix linting errors in EMLAnnotations Issue #2542 --- .../metadata/eml/EMLAnnotations.js | 89 ++++++++----------- 1 file changed, 37 insertions(+), 52 deletions(-) diff --git a/src/js/collections/metadata/eml/EMLAnnotations.js b/src/js/collections/metadata/eml/EMLAnnotations.js index b29c17cfc..015f414e8 100644 --- a/src/js/collections/metadata/eml/EMLAnnotations.js +++ b/src/js/collections/metadata/eml/EMLAnnotations.js @@ -1,75 +1,60 @@ "use strict"; -define([ - "jquery", - "underscore", - "backbone", - "models/metadata/eml211/EMLAnnotation", -], function ($, _, Backbone, EMLAnnotation) { +define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], ( + _, + Backbone, + EMLAnnotation, +) => { /** * @class EMLAnnotations * @classdesc A collection of EMLAnnotations. * @classcategory Collections/Metadata/EML * @since 2.19.0 - * @extends Backbone.Collection + * @augments Backbone.Collection */ - var EMLAnnotations = Backbone.Collection.extend( + const EMLAnnotations = Backbone.Collection.extend( /** @lends EMLAnnotations.prototype */ { - /** - * The reference to the model class that this collection is made of. - * @type EMLAnnotation - * @since 2.19.0 - */ + /** @inheritdoc */ model: EMLAnnotation, /** - * Checks if this collection already has an annotation for the same property URI. - * @param {EMLAnnotation} annotation The EMLAnnotation to compare against the annotations already in this collection. - * @returns {Boolean} Returns true is this collection already has an annotation for this property. - * @since 2.19.0 + * Checks if this collection already has an annotation for the same + * property URI. + * @param {EMLAnnotation} annotation The EMLAnnotation to compare against + * the annotations already in this collection. + * @returns {boolean} Returns true is this collection already has an + * annotation for this property. */ - hasDuplicateOf: function (annotation) { - try { - //If there is at least one model in this collection and there is a propertyURI set on the given model, - if (this.length && annotation.get("propertyURI")) { - //Return whether or not there is a duplicate - let properties = this.pluck("propertyURI"); - return properties.includes(annotation.get("propertyURI")); - } - //If this collection is empty or the propertyURI is falsey, return false - else { - return false; - } - } catch (e) { - console.error("Could not check for a duplicate annotation: ", e); - return false; + hasDuplicateOf(annotation) { + // If there is at least one model in this collection and there is a + // propertyURI set on the given model, + if (this.length && annotation.get("propertyURI")) { + // Return whether or not there is a duplicate + const properties = this.pluck("propertyURI"); + return properties.includes(annotation.get("propertyURI")); } + // If this collection is empty or the propertyURI is falsey, return + // false + return false; }, /** - * Removes the EMLAnnotation from this collection that has the same propertyURI as the given annotation. - * Then adds the given annotation to the collection. If no duplicate is found, the given annotation is still added - * to the collection. - * @param {EMLAnnotation} annotation - * @since 2.19.0 + * Removes the EMLAnnotation from this collection that has the same + * propertyURI as the given annotation. Then adds the given annotation to + * the collection. If no duplicate is found, the given annotation is still + * added to the collection. + * @param {EMLAnnotation} annotation The EMLAnnotation to replace + * duplicates with. */ - replaceDuplicateWith: function (annotation) { - try { - if (this.length && annotation.get("propertyURI")) { - let duplicates = this.findWhere({ - propertyURI: annotation.get("propertyURI"), - }); - this.remove(duplicates); - } - - this.add(annotation); - } catch (e) { - console.error( - "Could not replace the EMLAnnotation in the collection: ", - e, - ); + replaceDuplicateWith(annotation) { + if (this.length && annotation.get("propertyURI")) { + const duplicates = this.findWhere({ + propertyURI: annotation.get("propertyURI"), + }); + this.remove(duplicates); } + this.add(annotation); }, }, ); From 6189421e9aa55c89e3103f45ee76c02cfb7cfb0d Mon Sep 17 00:00:00 2001 From: robyngit Date: Tue, 15 Oct 2024 10:20:53 -0400 Subject: [PATCH 2/9] Add unit tests for the EMLAnnotations collection Issue #2542 --- test/config/tests.json | 1 + .../metadata/eml/EMLAnnotations.spec.js | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js diff --git a/test/config/tests.json b/test/config/tests.json index 6eef2b901..299a08059 100644 --- a/test/config/tests.json +++ b/test/config/tests.json @@ -33,6 +33,7 @@ "./js/specs/unit/models/metadata/eml211/EMLParty.spec.js", "./js/specs/unit/models/metadata/eml211/EMLTemporalCoverage.spec.js", "./js/specs/unit/collections/metadata/eml/EMLMissingValueCodes.spec.js", + "./js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js", "./js/specs/unit/models/metadata/eml211/EMLMissingValueCode.spec.js", "./js/specs/unit/models/metadata/eml211/EMLDistribution.spec.js", "./js/specs/unit/models/metadata/eml211/EMLGeoCoverage.spec.js", diff --git a/test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js b/test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js new file mode 100644 index 000000000..0c1aafa08 --- /dev/null +++ b/test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js @@ -0,0 +1,49 @@ +"use strict"; + +define([ + "/test/js/specs/shared/clean-state.js", + "collections/metadata/eml/EMLAnnotations", +], (cleanState, EMLAnnotations) => { + const should = chai.should(); + const expect = chai.expect; + + describe("Accordion Test Suite", () => { + const state = cleanState(() => { + const annotations = new EMLAnnotations({ + propertyLabel: "Property Label", + propertyURI: "http://example.com/property", + valueLabel: "Value Label", + valueURI: "http://example.com/value", + }); + return { annotations }; + }, beforeEach); + + it("creates an EMLAnnotations collection", () => { + state.annotations.should.be.instanceof(EMLAnnotations); + }); + + it("checks for duplicates", () => { + state.annotations.hasDuplicateOf(state.annotations.at(0)).should.be.true; + }); + + it("replaces duplicates", () => { + state.annotations.replaceDuplicateWith(state.annotations.at(0)); + state.annotations.length.should.equal(1); + }); + + it("adds annotations", () => { + state.annotations.add({ + propertyLabel: "Property Label2", + propertyURI: "http://example.com/property2", + valueLabel: "Value Label2", + valueURI: "http://example.com/value2", + }); + state.annotations.length.should.equal(2); + }); + + it("removes annotations", () => { + state.annotations.remove(state.annotations.at(0)); + state.annotations.length.should.equal(0); + }); + }); +}); From bea2d7b9a33eaacc86c8d77bdb712832bf3cd7a1 Mon Sep 17 00:00:00 2001 From: robyngit Date: Tue, 15 Oct 2024 17:09:45 -0400 Subject: [PATCH 3/9] Add EMLAnnotation methods for canonical datasets - Add methods to add, remove, update, and find sameAs and derivedFrom annotations representing canonical datasets Issue #2542 --- .../metadata/eml/EMLAnnotations.js | 131 ++++++++++++++++++ .../models/metadata/eml211/EMLAnnotation.js | 5 +- .../metadata/eml/EMLAnnotations.spec.js | 68 ++++++++- 3 files changed, 201 insertions(+), 3 deletions(-) diff --git a/src/js/collections/metadata/eml/EMLAnnotations.js b/src/js/collections/metadata/eml/EMLAnnotations.js index 015f414e8..fa1f26b3e 100644 --- a/src/js/collections/metadata/eml/EMLAnnotations.js +++ b/src/js/collections/metadata/eml/EMLAnnotations.js @@ -5,6 +5,8 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], ( Backbone, EMLAnnotation, ) => { + const SCHEMA_ORG_SAME_AS = "http://www.w3.org/2002/07/owl#sameAs"; + const PROV_WAS_DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom"; /** * @class EMLAnnotations * @classdesc A collection of EMLAnnotations. @@ -56,6 +58,135 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], ( } this.add(annotation); }, + + /** + * Find all annotations with the given propertyURI. + * @param {string} propertyURI The propertyURI to search for. + * @returns {EMLAnnotation[]} An array of EMLAnnotations with the given + * propertyURI. + * @since 0.0.0 + */ + findByProperty(propertyURI) { + return this.where({ propertyURI }); + }, + + /** + * Adds canonical dataset annotations to this collection. A canonical + * dataset is the one that is considered the authoritative version; the + * current EML doc being essentially a duplicate version. + * @param {string} sourceId The DOI or URL of the canonical dataset. + * @returns {void} + * @since 0.0.0 + */ + addCanonicalDatasetAnnotation(sourceId) { + if (!sourceId) return null; + // TODO: Check that sourceId is a valid DOI or URL + + // TODO: Check that there is not already a canonical dataset annotation + // before adding a new one, since there should only be one. + return this.add([ + { + propertyLabel: "derivedFrom", + propertyURI: PROV_WAS_DERIVED_FROM, + valueLabel: sourceId, + valueURI: sourceId, + }, + { + propertyLabel: "sameAs", + propertyURI: SCHEMA_ORG_SAME_AS, + valueLabel: sourceId, + valueURI: sourceId, + }, + ]); + }, + + /** + * Find the annotations that make up the canonical dataset annotation. A + * canonical dataset is identified by having both a "derivedFrom" and a + * "sameAs" annotation with the same DOI or URL for the valueURI. + * @returns {object} An object with the derivedFrom and sameAs + * annotations. + * @since 0.0.0 + */ + findCanonicalDatasetAnnotation() { + // There must be at least one derivedFrom and one sameAs annotation + // for this to have a canonical dataset annotation + if (!this.length) return null; + const derivedFrom = this.findByProperty(PROV_WAS_DERIVED_FROM); + if (!derivedFrom?.length) return null; + const sameAs = this.findByProperty(SCHEMA_ORG_SAME_AS); + if (!sameAs?.length) return null; + + // Find all pairs that have matching valueURIs + const pairs = []; + derivedFrom.forEach((derived) => { + sameAs.forEach((same) => { + if (derived.get("valueURI") === same.get("valueURI")) { + // TODO? Check that the URI is a valid DOI or URL + pairs.push({ derived, same, uri: derived.get("valueURI") }); + } + }); + }); + + // If there are multiple pairs, we cannot determine which is the + // canonical dataset. + if (pairs.length > 1 || !pairs.length) return null; + + // There is only one pair, so return it + return pairs[0]; + }, + + /** + * Updates the canonical dataset annotations to have the given ID. If + * there is no canonical dataset annotation, one is added. If the ID is a + * falsy value, the canonical dataset annotation is removed. + * @param {string} newSourceId The DOI or URL of the canonical dataset. + * @returns {object} An object with the derivedFrom and sameAs annotations + * if the canonical dataset annotations were updated. + * @since 0.0.0 + */ + updateCanonicalDataset(newSourceId) { + if (!newSourceId) { + this.removeCanonicalDatasetAnnotation(); + return null; + } + const canonical = this.findCanonicalDatasetAnnotation(); + if (!canonical) { + return this.addCanonicalDatasetAnnotation(newSourceId); + } + + const { derived, same, uri } = canonical; + if (uri === newSourceId) return null; + + derived.set("valueURI", newSourceId); + derived.set("valueLabel", newSourceId); + same.set("valueURI", newSourceId); + same.set("valueLabel", newSourceId); + + return [derived, same]; + }, + + /** + * Removes the canonical dataset annotations from this collection. + * @returns {EMLAnnotation[]} The canonical dataset annotations that were + * removed. + * @since 0.0.0 + */ + removeCanonicalDatasetAnnotation() { + const canonical = this.findCanonicalDatasetAnnotation(); + if (!canonical) return null; + return this.remove([canonical.derived, canonical.same]); + }, + + /** + * Returns the URI of the canonical dataset. + * @returns {string} The URI of the canonical dataset. + * @since 0.0.0 + */ + getCanonicalURI() { + const canonical = this.findCanonicalDatasetAnnotation(); + return canonical?.uri; + }, }, ); diff --git a/src/js/models/metadata/eml211/EMLAnnotation.js b/src/js/models/metadata/eml211/EMLAnnotation.js index 066af4687..eb8e9751b 100644 --- a/src/js/models/metadata/eml211/EMLAnnotation.js +++ b/src/js/models/metadata/eml211/EMLAnnotation.js @@ -23,7 +23,8 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) { }, initialize: function (attributes, opions) { - this.on("change", this.trickleUpChange); + this.stopListening(this, "change", this.trickleUpChange); + this.listenTo(this, "change", this.trickleUpChange); }, parse: function (attributes, options) { @@ -175,7 +176,7 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) { /* Let the top level package know of attribute changes from this object */ trickleUpChange: function () { - MetacatUI.rootDataPackage.packageModel.set("changed", true); + MetacatUI.rootDataPackage.packageModel?.set("changed", true); }, }, ); diff --git a/test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js b/test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js index 0c1aafa08..1f92cd742 100644 --- a/test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js +++ b/test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js @@ -7,7 +7,10 @@ define([ const should = chai.should(); const expect = chai.expect; - describe("Accordion Test Suite", () => { + const SCHEMA_ORG_SAME_AS = "http://www.w3.org/2002/07/owl#sameAs"; + const PROV_WAS_DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom"; + + describe("EML Annotations Test Suite", () => { const state = cleanState(() => { const annotations = new EMLAnnotations({ propertyLabel: "Property Label", @@ -45,5 +48,68 @@ define([ state.annotations.remove(state.annotations.at(0)); state.annotations.length.should.equal(0); }); + + it("finds annotations by property", () => { + state.annotations + .findByProperty("http://example.com/property") + .length.should.equal(1); + }); + + it("adds canonical dataset annotations", () => { + const annotations = + state.annotations.addCanonicalDatasetAnnotation("http://example.com"); + state.annotations.length.should.equal(3); + annotations[0].get("propertyURI").should.equal(PROV_WAS_DERIVED_FROM); + annotations[0].get("valueURI").should.equal("http://example.com"); + annotations[1].get("propertyURI").should.equal(SCHEMA_ORG_SAME_AS); + annotations[1].get("valueURI").should.equal("http://example.com"); + }); + + it("finds canonical dataset annotations", () => { + state.annotations.addCanonicalDatasetAnnotation("http://example.com"); + const annotations = state.annotations.findCanonicalDatasetAnnotation(); + annotations.should.have.property("derived"); + annotations.should.have.property("same"); + annotations.derived.get("valueURI").should.equal("http://example.com"); + annotations.same.get("valueURI").should.equal("http://example.com"); + annotations.derived + .get("propertyURI") + .should.equal(PROV_WAS_DERIVED_FROM); + annotations.same.get("propertyURI").should.equal(SCHEMA_ORG_SAME_AS); + }); + + it("updates canonical dataset annotations", () => { + state.annotations.addCanonicalDatasetAnnotation("http://example.com"); + state.annotations.updateCanonicalDataset("http://newexample.com"); + state.annotations.getCanonicalURI().should.equal("http://newexample.com"); + }); + + it("removes canonical dataset annotations", () => { + state.annotations.addCanonicalDatasetAnnotation("http://example.com"); + state.annotations.removeCanonicalDatasetAnnotation(); + state.annotations.length.should.equal(1); + }); + + it("gets the URI of the canonical dataset", () => { + state.annotations.addCanonicalDatasetAnnotation("http://example.com"); + state.annotations.getCanonicalURI().should.equal("http://example.com"); + }); + + it("adds annotations if they didn't exist when updating", () => { + state.annotations.updateCanonicalDataset("http://example.com"); + state.annotations.length.should.equal(3); + }); + + it("removes canonical dataset annotations if the ID is falsy", () => { + state.annotations.addCanonicalDatasetAnnotation("http://example.com"); + state.annotations.updateCanonicalDataset(""); + state.annotations.length.should.equal(1); + }); + + it("does not update canonical dataset annotations if the ID is the same", () => { + state.annotations.addCanonicalDatasetAnnotation("http://example.com"); + state.annotations.updateCanonicalDataset("http://example.com"); + state.annotations.length.should.equal(3); + }); }); }); From 4df698de30e00e1755cd3c2cfa6767f9ed1fed63 Mon Sep 17 00:00:00 2001 From: robyngit Date: Tue, 15 Oct 2024 17:13:23 -0400 Subject: [PATCH 4/9] Add input for canoncial dataset in EML editor - Add logic to identify the canonical dataset while parsing EML - Update the Annotations collection with the value in the canonical dataset input changes Issue #2542 --- src/js/models/metadata/eml211/EML211.js | 29 +++++++++++++++++++ .../templates/metadata/metadataOverview.html | 8 +++++ src/js/views/metadata/EML211View.js | 26 +++++++++++++---- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/js/models/metadata/eml211/EML211.js b/src/js/models/metadata/eml211/EML211.js index 65c2a69ff..5cebdb0aa 100644 --- a/src/js/models/metadata/eml211/EML211.js +++ b/src/js/models/metadata/eml211/EML211.js @@ -86,6 +86,7 @@ define([ methods: new EMLMethods(), // An EMLMethods objects project: null, // An EMLProject object, annotations: null, // Dataset-level annotations + canonicalDataset: null, dataSensitivityPropertyURI: "http://purl.dataone.org/odo/SENSO_00000005", nodeOrder: [ @@ -143,6 +144,13 @@ define([ this.set("synced", true); }); + this.stopListening(this, "change:canonicalDataset"); + this.listenTo( + this, + "change:canonicalDataset", + this.updateCanonicalDataset, + ); + //Create a Unit collection if (!this.units.length) this.createUnits(); }, @@ -160,6 +168,17 @@ define([ ); }, + updateCanonicalDataset() { + let uri = this.get("canonicalDataset"); + uri = uri?.length ? uri[0] : null; + let annotations = this.get("annotations"); + if (!annotations) { + annotations = new EMLAnnotations(); + this.set("annotations", annotations); + } + annotations.updateCanonicalDataset(uri); + }, + /* * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML). * Used during parse() and serialize() @@ -734,6 +753,16 @@ define([ } } + // Once all the nodes have been parsed, check if any of the annotations + // make up a canonical dataset reference + const annotations = modelJSON["annotations"]; + if (annotations) { + const canonicalDataset = annotations.getCanonicalURI(); + if (canonicalDataset) { + modelJSON["canonicalDataset"] = canonicalDataset; + } + } + return modelJSON; }, diff --git a/src/js/templates/metadata/metadataOverview.html b/src/js/templates/metadata/metadataOverview.html index a8f117c8b..9a8c46ace 100644 --- a/src/js/templates/metadata/metadataOverview.html +++ b/src/js/templates/metadata/metadataOverview.html @@ -94,3 +94,11 @@
Alternate Identifiers