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
+
+
+
Canonical Dataset
+
If this dataset is essentially a duplicate of a version
+ stored elsewhere, provide the ID of the original dataset here. This must be a
+ DOI or URL
+
+
diff --git a/src/js/views/metadata/EML211View.js b/src/js/views/metadata/EML211View.js
index 05eba027a..0a08961ac 100644
--- a/src/js/views/metadata/EML211View.js
+++ b/src/js/views/metadata/EML211View.js
@@ -302,6 +302,13 @@ define([
);
$(overviewEl).find(".altids").append(altIdsEls);
+ // Canonical Identifier
+ const canonicalIdEl = this.createBasicTextFields(
+ "canonicalDataset",
+ "Add a new canonical identifier",
+ );
+ $(overviewEl).find(".canonical-id").append(canonicalIdEl);
+
//Usage
//Find the model value that matches a radio button and check it
// Note the replace() call removing newlines and replacing them with a single space
@@ -1909,7 +1916,7 @@ define([
.addClass("basic-text");
textRow.append(input.clone().val(value));
- if (category != "title")
+ if (category !== "title" && category !== "canonicalDataset")
textRow.append(
this.createRemoveButton(
null,
@@ -1922,7 +1929,11 @@ define([
textContainer.append(textRow);
//At the end, append an empty input for the user to add a new one
- if (i + 1 == allModelValues.length && category != "title") {
+ if (
+ i + 1 == allModelValues.length &&
+ category !== "title" &&
+ category !== "canonicalDataset"
+ ) {
var newRow = $(
$(document.createElement("div")).addClass("basic-text-row"),
);
@@ -2006,7 +2017,12 @@ define([
}
//Add another blank text input
- if ($(e.target).is(".new") && value != "" && category != "title") {
+ if (
+ $(e.target).is(".new") &&
+ value != "" &&
+ category != "title" &&
+ category !== "canonicalDataset"
+ ) {
$(e.target).removeClass("new");
this.addBasicText(e);
}
@@ -2036,12 +2052,12 @@ define([
allBasicTexts = $(
".basic-text.new[data-category='" + category + "']",
);
-
//Only show one new row at a time
if (allBasicTexts.length == 1 && !allBasicTexts.val()) return;
else if (allBasicTexts.length > 1) return;
//We are only supporting one title right now
- else if (category == "title") return;
+ else if (category === "title" || category === "canonicalDataset")
+ return;
//Add another blank text input
var newRow = $(document.createElement("div")).addClass(
From 877beac1ad0de3c1beafd7befa54d547684eef90 Mon Sep 17 00:00:00 2001
From: robyngit
Date: Thu, 24 Oct 2024 16:52:39 -0400
Subject: [PATCH 5/9] Add annotation validation to Editor
- Handle canonical dataset annotation errors specially so that they can be displayed in the new input field.
- Improve annotations validation to remove empty annotations and duplicates
Issue #2542
---
.../metadata/eml/EMLAnnotations.js | 68 +++++++++++-
src/js/models/metadata/eml211/EML211.js | 39 +++++--
.../models/metadata/eml211/EMLAnnotation.js | 100 +++++++++---------
3 files changed, 144 insertions(+), 63 deletions(-)
diff --git a/src/js/collections/metadata/eml/EMLAnnotations.js b/src/js/collections/metadata/eml/EMLAnnotations.js
index fa1f26b3e..4568703d6 100644
--- a/src/js/collections/metadata/eml/EMLAnnotations.js
+++ b/src/js/collections/metadata/eml/EMLAnnotations.js
@@ -41,6 +41,38 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], (
return false;
},
+ /**
+ * Find all annotations that have the same property & value URIs & labels.
+ * Only returns the models that are duplicates, not the original. The original
+ * is the first instance found in the collection.
+ * @returns {EMLAnnotation[]} An array of EMLAnnotations that are duplicates.
+ * @since 0.0.0
+ */
+ getDuplicates() {
+ const duplicates = [];
+ this.forEach((annotation) => {
+ const propertyURI = annotation.get("propertyURI");
+ const valueURI = annotation.get("valueURI");
+ const propertyLabel = annotation.get("propertyLabel");
+ const valueLabel = annotation.get("valueLabel");
+
+ const found = this.filter(
+ (a) =>
+ a.get("propertyURI") === propertyURI &&
+ a.get("valueURI") === valueURI &&
+ a.get("propertyLabel") === propertyLabel &&
+ a.get("valueLabel") === valueLabel &&
+ a.id !== annotation.id,
+ );
+
+ if (found.length) {
+ duplicates.push(...found);
+ }
+ });
+
+ return duplicates;
+ },
+
/**
* Removes the EMLAnnotation from this collection that has the same
* propertyURI as the given annotation. Then adds the given annotation to
@@ -90,12 +122,14 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], (
propertyURI: PROV_WAS_DERIVED_FROM,
valueLabel: sourceId,
valueURI: sourceId,
+ isCanonicalDataset: true,
},
{
propertyLabel: "sameAs",
propertyURI: SCHEMA_ORG_SAME_AS,
valueLabel: sourceId,
valueURI: sourceId,
+ isCanonicalDataset: true,
},
]);
},
@@ -132,7 +166,14 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], (
// canonical dataset.
if (pairs.length > 1 || !pairs.length) return null;
- // There is only one pair, so return it
+ // There is only one pair left in this case
+ const canonAnnos = pairs[0];
+
+ // Make sure each annotation has the isCanonicalDataset flag set,
+ // we will use it later, e.g. in validation
+ canonAnnos.derived.set("isCanonicalDataset", true);
+ canonAnnos.same.set("isCanonicalDataset", true);
+
return pairs[0];
},
@@ -187,6 +228,31 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], (
const canonical = this.findCanonicalDatasetAnnotation();
return canonical?.uri;
},
+
+ /** @inheritdoc */
+ validate() {
+ // Remove any totally empty annotations
+ this.remove(this.filter((annotation) => annotation.isEmpty()));
+
+ // Remove annotations with the same value URI & property URI
+ const duplicates = this.getDuplicates();
+ if (duplicates.length) {
+ this.remove(duplicates);
+ }
+
+ // Validate each annotation
+ const errors = this.map((annotation) => annotation.validate());
+
+ // Remove any empty errors
+ const filteredErrors = errors.filter((error) => error);
+
+ // Each annotation validation is an array, flatten them to one array
+ const flatErrors = [].concat(...filteredErrors);
+
+ if (!filteredErrors.length) return null;
+
+ return flatErrors;
+ },
},
);
diff --git a/src/js/models/metadata/eml211/EML211.js b/src/js/models/metadata/eml211/EML211.js
index 5cebdb0aa..c23c8df44 100644
--- a/src/js/models/metadata/eml211/EML211.js
+++ b/src/js/models/metadata/eml211/EML211.js
@@ -1705,19 +1705,36 @@ define([
}
}
- // Validate each EMLAnnotation model
- if (this.get("annotations")) {
- this.get("annotations").each(function (model) {
- if (model.isValid()) {
- return;
- }
-
- if (!errors.annotations) {
- errors.annotations = [];
+ // Validate the EMLAnnotation models
+ const annotations = this.get("annotations");
+ const annotationErrors = annotations.validate();
+ if (annotationErrors) {
+ // Put canonicalDataset annotation errors in their own category
+ // so they can be displayed in the special canonicalDataset field.
+ const canonicalErrors = [];
+ const errorsToRemove = [];
+ // Check for a canonicalDataset annotation error
+ annotationErrors.forEach((annotationError, i) => {
+ if (annotationError.isCanonicalDataset) {
+ canonicalErrors.push(annotationError);
+ errorsToRemove.push(i);
}
+ });
+ // Remove canonicalDataset errors from the annotation errors
+ // backwards so we don't mess up the indexes.
+ errorsToRemove.reverse().forEach((i) => {
+ annotationErrors.splice(i, 1);
+ });
- errors.annotations.push(model.validationError);
- }, this);
+ if (canonicalErrors.length) {
+ // The two canonicalDataset errors are the same, so just show one.
+ errors.canonicalDataset = canonicalErrors[0].message;
+ }
+ }
+ // Add the rest of the annotation errors if there are any
+ // non-canonical left
+ if (annotationErrors.length) {
+ errors.annotations = annotationErrors;
}
//Check the required fields for this MetacatUI configuration
diff --git a/src/js/models/metadata/eml211/EMLAnnotation.js b/src/js/models/metadata/eml211/EMLAnnotation.js
index eb8e9751b..ffb537bd0 100644
--- a/src/js/models/metadata/eml211/EMLAnnotation.js
+++ b/src/js/models/metadata/eml211/EMLAnnotation.js
@@ -55,65 +55,63 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
return attributes;
},
- validate: function () {
- var errors = [];
+ validate() {
+ const errors = [];
if (this.isEmpty()) {
this.trigger("valid");
-
- return;
- }
-
- var propertyURI = this.get("propertyURI");
-
- if (!propertyURI || propertyURI.length <= 0) {
- errors.push({
- category: "propertyURI",
- message: "Property URI must be set.",
- });
- } else if (propertyURI.match(/http[s]?:\/\/.+/) === null) {
- errors.push({
- category: "propertyURI",
- message: "Property URI should be an HTTP(S) URI.",
- });
+ return null;
}
- var propertyLabel = this.get("propertyLabel");
-
- if (!propertyLabel || propertyLabel.length <= 0) {
- errors.push({
- category: "propertyLabel",
- message: "Property Label must be set.",
- });
- }
-
- var valueURI = this.get("valueURI");
-
- if (!valueURI || valueURI.length <= 0) {
- errors.push({
- category: "valueURI",
- message: "Value URI must be set.",
- });
- } else if (valueURI.match(/http[s]?:\/\/.+/) === null) {
- errors.push({
- category: "valueURI",
- message: "Value URI should be an HTTP(S) URI.",
- });
- }
-
- var valueLabel = this.get("valueLabel");
-
- if (!valueLabel || valueLabel.length <= 0) {
- errors.push({
- category: "valueLabel",
- message: "Value Label must be set.",
- });
- }
+ const isCanonicalDataset = this.get("isCanonicalDataset");
+
+ const emptyErrorMsg = (label) => `${label} must be set.`;
+ const uriErrorMsg = (label) =>
+ `${label} should be an HTTP(S) URI, for example: http://example.com`;
+
+ const isValidURI = (uri) => uri.match(/http[s]?:\/\/.+/) !== null;
+
+ // Both URIs must be set and must be valid URIs
+ const uriAttrs = [
+ { attr: "propertyURI", label: "Property URI" },
+ { attr: "valueURI", label: "Value URI" },
+ ];
+ uriAttrs.forEach(({ attr, label }) => {
+ const uri = this.get(attr);
+ if (!uri || uri.length <= 0) {
+ errors.push({
+ attr,
+ message: emptyErrorMsg(label),
+ isCanonicalDataset,
+ });
+ } else if (!isValidURI(uri)) {
+ errors.push({
+ attr,
+ message: uriErrorMsg(label),
+ isCanonicalDataset,
+ });
+ }
+ });
+
+ // Both labels must be set to a string
+ const labelAttrs = [
+ { attr: "propertyLabel", label: "Property Label" },
+ { attr: "valueLabel", label: "Value Label" },
+ ];
+ labelAttrs.forEach(({ attr, label }) => {
+ const value = this.get(attr);
+ if (!value || value.length <= 0) {
+ errors.push({
+ attr,
+ message: emptyErrorMsg(label),
+ isCanonicalDataset,
+ });
+ }
+ });
if (errors.length === 0) {
this.trigger("valid");
-
- return;
+ return null;
}
return errors;
From 6ea1434562b5dd384f4d08769ccf5f51e5500bb6 Mon Sep 17 00:00:00 2001
From: robyngit
Date: Mon, 4 Nov 2024 11:40:17 -0500
Subject: [PATCH 6/9] Show canonical dataset validation on value change
(in Editor)
Issue #2542
---
.../metadata/eml/EMLAnnotations.js | 26 ++++++++
src/js/models/metadata/eml211/EML211.js | 60 +++++++++++--------
src/js/views/metadata/EML211View.js | 24 ++++++++
3 files changed, 86 insertions(+), 24 deletions(-)
diff --git a/src/js/collections/metadata/eml/EMLAnnotations.js b/src/js/collections/metadata/eml/EMLAnnotations.js
index 4568703d6..7aa92950e 100644
--- a/src/js/collections/metadata/eml/EMLAnnotations.js
+++ b/src/js/collections/metadata/eml/EMLAnnotations.js
@@ -251,6 +251,32 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], (
if (!filteredErrors.length) return null;
+ // Put canonicalDataset annotation errors in their own category
+ // so they can be displayed in the special canonicalDataset field.
+ const canonicalErrors = [];
+ const errorsToRemove = [];
+ // Check for a canonicalDataset annotation error
+ flatErrors.forEach((annotationError, i) => {
+ if (annotationError.isCanonicalDataset) {
+ canonicalErrors.push(annotationError);
+ errorsToRemove.push(i);
+ }
+ });
+
+ // Remove canonicalDataset errors from the annotation errors
+ // backwards so we don't mess up the indexes.
+ errorsToRemove.reverse().forEach((i) => {
+ flatErrors.splice(i, 1);
+ });
+
+ if (canonicalErrors.length) {
+ // The two canonicalDataset errors are the same, so just show one.
+ flatErrors.push({
+ attr: "canonicalDataset",
+ message: canonicalErrors[0].message,
+ });
+ }
+
return flatErrors;
},
},
diff --git a/src/js/models/metadata/eml211/EML211.js b/src/js/models/metadata/eml211/EML211.js
index c23c8df44..87aeb3bd5 100644
--- a/src/js/models/metadata/eml211/EML211.js
+++ b/src/js/models/metadata/eml211/EML211.js
@@ -168,6 +168,10 @@ define([
);
},
+ /**
+ * Update the canonoical dataset URI in the annotations collection to
+ * match the canonicalDataset value on this model.
+ */
updateCanonicalDataset() {
let uri = this.get("canonicalDataset");
uri = uri?.length ? uri[0] : null;
@@ -1708,33 +1712,41 @@ define([
// Validate the EMLAnnotation models
const annotations = this.get("annotations");
const annotationErrors = annotations.validate();
- if (annotationErrors) {
- // Put canonicalDataset annotation errors in their own category
- // so they can be displayed in the special canonicalDataset field.
- const canonicalErrors = [];
- const errorsToRemove = [];
- // Check for a canonicalDataset annotation error
- annotationErrors.forEach((annotationError, i) => {
- if (annotationError.isCanonicalDataset) {
- canonicalErrors.push(annotationError);
- errorsToRemove.push(i);
- }
- });
- // Remove canonicalDataset errors from the annotation errors
- // backwards so we don't mess up the indexes.
- errorsToRemove.reverse().forEach((i) => {
- annotationErrors.splice(i, 1);
- });
-
- if (canonicalErrors.length) {
- // The two canonicalDataset errors are the same, so just show one.
- errors.canonicalDataset = canonicalErrors[0].message;
- }
- }
+ // if (annotationErrors) {
+ // // Put canonicalDataset annotation errors in their own category
+ // // so they can be displayed in the special canonicalDataset field.
+ // const canonicalErrors = [];
+ // const errorsToRemove = [];
+ // // Check for a canonicalDataset annotation error
+ // annotationErrors.forEach((annotationError, i) => {
+ // if (annotationError.isCanonicalDataset) {
+ // canonicalErrors.push(annotationError);
+ // errorsToRemove.push(i);
+ // }
+ // });
+ // // Remove canonicalDataset errors from the annotation errors
+ // // backwards so we don't mess up the indexes.
+ // errorsToRemove.reverse().forEach((i) => {
+ // annotationErrors.splice(i, 1);
+ // });
+
+ // if (canonicalErrors.length) {
+ // // The two canonicalDataset errors are the same, so just show one.
+ // errors.canonicalDataset = canonicalErrors[0].message;
+ // }
+ // }
// Add the rest of the annotation errors if there are any
// non-canonical left
if (annotationErrors.length) {
- errors.annotations = annotationErrors;
+ errors.annotations = annotationErrors.filter(
+ (e) => e.attr !== "canonicalDataset",
+ );
+ const canonicalError = annotationErrors.find(
+ (e) => e.attr === "canonicalDataset",
+ );
+ if (canonicalError) {
+ errors.canonicalDataset = canonicalError.message;
+ }
}
//Check the required fields for this MetacatUI configuration
diff --git a/src/js/views/metadata/EML211View.js b/src/js/views/metadata/EML211View.js
index 0a08961ac..b84784225 100644
--- a/src/js/views/metadata/EML211View.js
+++ b/src/js/views/metadata/EML211View.js
@@ -309,6 +309,30 @@ define([
);
$(overviewEl).find(".canonical-id").append(canonicalIdEl);
+ // Show canonical dataset error message on change
+ this.stopListening(this.model, "change:canonicalDataset");
+ this.listenTo(this.model, "change:canonicalDataset", () => {
+ const annotations = this.model.get("annotations");
+ const annoErrors = annotations.validate();
+ const canonicalError = annoErrors?.filter(
+ (e) => e.attr === "canonicalDataset",
+ );
+
+ if (canonicalError) {
+ const container = canonicalIdEl.parent();
+ const input = canonicalIdEl.find("input");
+ const notification = container.find(".notification");
+ notification.addClass("error").text(canonicalError[0].message);
+ input.addClass("error");
+
+ // When the user starts typing, remove the error message
+ input.one("keyup", () => {
+ notification.removeClass("error").text("");
+ input.removeClass("error");
+ });
+ }
+ });
+
//Usage
//Find the model value that matches a radio button and check it
// Note the replace() call removing newlines and replacing them with a single space
From 8368b09261b445d4ee9257939150cb25ed958c88 Mon Sep 17 00:00:00 2001
From: robyngit
Date: Mon, 4 Nov 2024 13:56:36 -0500
Subject: [PATCH 7/9] Add EMLAnnotations validation unit tests + fix
- Fix the findDuplicates method to return the correct result
Issue #2542
---
.../metadata/eml/EMLAnnotations.js | 39 ++++++++++---------
.../metadata/eml/EMLAnnotations.spec.js | 33 ++++++++++++++++
2 files changed, 54 insertions(+), 18 deletions(-)
diff --git a/src/js/collections/metadata/eml/EMLAnnotations.js b/src/js/collections/metadata/eml/EMLAnnotations.js
index 7aa92950e..92d30c0d1 100644
--- a/src/js/collections/metadata/eml/EMLAnnotations.js
+++ b/src/js/collections/metadata/eml/EMLAnnotations.js
@@ -49,24 +49,27 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], (
* @since 0.0.0
*/
getDuplicates() {
- const duplicates = [];
- this.forEach((annotation) => {
- const propertyURI = annotation.get("propertyURI");
- const valueURI = annotation.get("valueURI");
- const propertyLabel = annotation.get("propertyLabel");
- const valueLabel = annotation.get("valueLabel");
-
- const found = this.filter(
- (a) =>
- a.get("propertyURI") === propertyURI &&
- a.get("valueURI") === valueURI &&
- a.get("propertyLabel") === propertyLabel &&
- a.get("valueLabel") === valueLabel &&
- a.id !== annotation.id,
- );
-
- if (found.length) {
- duplicates.push(...found);
+ const groups = {};
+ let duplicates = [];
+
+ // Group models by their serialized attributes
+ this.each((model) => {
+ // Serialize the model's attributes to create a unique key
+ const key = JSON.stringify(model.attributes);
+
+ // Group models by the serialized key
+ if (groups[key]) {
+ groups[key].push(model);
+ } else {
+ groups[key] = [model];
+ }
+ });
+
+ // Identify duplicates in each group
+ Object.values(groups).forEach((group) => {
+ if (group.length > 1) {
+ // Add all but one model from each group to the duplicates array
+ duplicates = duplicates.concat(group.slice(1));
}
});
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 1f92cd742..f5dc1d4a0 100644
--- a/test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js
+++ b/test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js
@@ -111,5 +111,38 @@ define([
state.annotations.updateCanonicalDataset("http://example.com");
state.annotations.length.should.equal(3);
});
+
+ it("returns null if there are no validation errors", () => {
+ const errors = state.annotations.validate();
+ expect(errors).to.be.null;
+ });
+
+ it("shows validation errors if canonical dataset is not a valid URI", () => {
+ state.annotations.addCanonicalDatasetAnnotation("not-a-valid-uri");
+ const errors = state.annotations.validate();
+ expect(errors).to.be.an("array");
+ expect(errors.length).to.equal(1);
+ expect(errors[0].attr).to.equal("canonicalDataset");
+ });
+
+ it("removes duplicates during validation", () => {
+ state.annotations.add([
+ {
+ propertyLabel: "Property Label",
+ propertyURI: "http://example.com/property",
+ valueLabel: "Value Label",
+ valueURI: "http://example.com/value",
+ },
+ {
+ propertyLabel: "Property Label",
+ propertyURI: "http://example.com/property",
+ valueLabel: "Value Label",
+ valueURI: "http://example.com/value",
+ },
+ ]);
+ const errors = state.annotations.validate();
+ expect(errors).to.be.null;
+ expect(state.annotations.length).to.equal(1);
+ });
});
});
From 4fd8ec0a793539c6e30894590391cdd2036d1770 Mon Sep 17 00:00:00 2001
From: robyngit
Date: Mon, 4 Nov 2024 14:05:24 -0500
Subject: [PATCH 8/9] Remove dev code
Issue #2542
---
src/js/collections/metadata/eml/EMLAnnotations.js | 5 -----
1 file changed, 5 deletions(-)
diff --git a/src/js/collections/metadata/eml/EMLAnnotations.js b/src/js/collections/metadata/eml/EMLAnnotations.js
index 92d30c0d1..24461e0ca 100644
--- a/src/js/collections/metadata/eml/EMLAnnotations.js
+++ b/src/js/collections/metadata/eml/EMLAnnotations.js
@@ -115,10 +115,6 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], (
*/
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",
@@ -159,7 +155,6 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], (
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") });
}
});
From 979d2a5cdfcae2434c1a8430ed2178e528e33939 Mon Sep 17 00:00:00 2001
From: robyngit
Date: Mon, 4 Nov 2024 14:27:54 -0500
Subject: [PATCH 9/9] Fix small bug in validation annotations in EML
And remove dev code
Issue #2542
---
src/js/models/metadata/eml211/EML211.js | 28 ++-----------------------
1 file changed, 2 insertions(+), 26 deletions(-)
diff --git a/src/js/models/metadata/eml211/EML211.js b/src/js/models/metadata/eml211/EML211.js
index 87aeb3bd5..d4dfe724c 100644
--- a/src/js/models/metadata/eml211/EML211.js
+++ b/src/js/models/metadata/eml211/EML211.js
@@ -1712,32 +1712,8 @@ define([
// Validate the EMLAnnotation models
const annotations = this.get("annotations");
const annotationErrors = annotations.validate();
- // if (annotationErrors) {
- // // Put canonicalDataset annotation errors in their own category
- // // so they can be displayed in the special canonicalDataset field.
- // const canonicalErrors = [];
- // const errorsToRemove = [];
- // // Check for a canonicalDataset annotation error
- // annotationErrors.forEach((annotationError, i) => {
- // if (annotationError.isCanonicalDataset) {
- // canonicalErrors.push(annotationError);
- // errorsToRemove.push(i);
- // }
- // });
- // // Remove canonicalDataset errors from the annotation errors
- // // backwards so we don't mess up the indexes.
- // errorsToRemove.reverse().forEach((i) => {
- // annotationErrors.splice(i, 1);
- // });
-
- // if (canonicalErrors.length) {
- // // The two canonicalDataset errors are the same, so just show one.
- // errors.canonicalDataset = canonicalErrors[0].message;
- // }
- // }
- // Add the rest of the annotation errors if there are any
- // non-canonical left
- if (annotationErrors.length) {
+
+ if (annotationErrors?.length) {
errors.annotations = annotationErrors.filter(
(e) => e.attr !== "canonicalDataset",
);