Skip to content

Commit

Permalink
Add annotation validation to Editor
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
robyngit committed Oct 24, 2024
1 parent 4df698d commit 99cf131
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 63 deletions.
75 changes: 74 additions & 1 deletion src/js/collections/metadata/eml/EMLAnnotations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
]);
},
Expand Down Expand Up @@ -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];
},

Expand Down Expand Up @@ -187,6 +228,38 @@ 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);

// For testing, add a nonCanonicalDataset annotation
flatErrors.push({
attr: "propertyURI",
message: "This is a test error",
isCanonicalDataset: false,
});

if (!filteredErrors.length) return null;

return flatErrors;
},
},
);

Expand Down
39 changes: 28 additions & 11 deletions src/js/models/metadata/eml211/EML211.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 49 additions & 51 deletions src/js/models/metadata/eml211/EMLAnnotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 99cf131

Please sign in to comment.