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;