Skip to content

Commit

Permalink
Add models, views, collections for Missing Values
Browse files Browse the repository at this point in the history
- Enables entering missing value codes in the Attribute editor
- Works but lacks styling & unit tests

Issue #612
  • Loading branch information
robyngit committed Jul 20, 2023
1 parent c9aae3b commit 9d1ff5d
Show file tree
Hide file tree
Showing 7 changed files with 668 additions and 6 deletions.
83 changes: 83 additions & 0 deletions src/js/collections/metadata/eml/EMLMissingValueCodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use strict";

define(["backbone", "models/metadata/eml211/EMLMissingValueCode"], function (
Backbone,
EMLMissingValueCode
) {
/**
* @class EMLMissingValueCodes
* @classdesc A collection of EMLMissingValueCodes.
* @classcategory Collections/Metadata/EML
* @since x.x.x
*/
var EMLMissingValueCodes = Backbone.Collection.extend(
/** @lends EMLMissingValueCodes.prototype */
{
/**
* The reference to the model class that this collection is made of.
* @type {Backbone.Model}
*/
model: EMLMissingValueCode,

/**
* Parse the incoming XML nodes
* @param {jQuery|Element} objectDOM - The XML DOM element that represents
*/
parse: function (objectDOM) {
const collection = this;

if (!objectDOM) return;
const $objectDOM = $(objectDOM);

// Loop through each missingValueCode node
const opts = { parse: true };
for (var i = 0; i < $objectDOM.length; i++) {
const missingValueCodeNode = $objectDOM[i];
// Create a new missingValueCode model & add it to the collection
const attrs = { objectDOM: missingValueCodeNode };
const missingValueCode = new EMLMissingValueCode(attrs, opts);
collection.add(missingValueCode);
}

return collection;
},

/**
* Update the DOM with the current model state for each model in the
* collection, then return the set of updated DOMs. Warning: this will
* remove any empty models from the collection.
* @returns {Element[]} An array of updated DOM elements
*/
updateDOM: function () {
this.removeEmptyModels();
const objectDOMs = this.map((model) => model.updateDOM());
return objectDOMs;
},

/**
* Remove any empty models from the collection
*/
removeEmptyModels: function () {
this.remove(this.filter((model) => model.isEmpty()));
},

/**
* Validate the collection of missing value codes. This will remove any
* empty models from the collection.
* @returns {Array} An array of error messages
*/
validate: function () {
this.removeEmptyModels();
const errors = [];
this.forEach((model) => {
if (!model.isValid()) {
errors.push(model.validationError);
}
});
return errors.length ? errors : null;
},
}
);

return EMLMissingValueCodes;
});
53 changes: 47 additions & 6 deletions src/js/models/metadata/eml211/EMLAttribute.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
define(["jquery", "underscore", "backbone", "uuid",
"models/metadata/eml211/EMLMeasurementScale", "models/metadata/eml211/EMLAnnotation",
"collections/metadata/eml/EMLMissingValueCodes",
"models/DataONEObject"],
function($, _, Backbone, uuid, EMLMeasurementScale, EMLAnnotation,
function ($, _, Backbone, uuid, EMLMeasurementScale, EMLAnnotation,
EMLMissingValueCodes,
DataONEObject) {

/**
Expand All @@ -25,7 +27,7 @@ define(["jquery", "underscore", "backbone", "uuid",
storageType: [], // Zero or more storage types
typeSystem: [], // Zero or more system types for storage type
measurementScale: null, // An EML{Non}NumericDomain or EMLDateTimeDomain object
missingValueCode: [], // Zero or more {code: value, definition: value} objects
missingValueCodes: new EMLMissingValueCodes(), // An EMLMissingValueCodes collection
accuracy: null, // An EMLAccuracy object
coverage: null, // an EMLCoverage object
methods: [], // Zero or more EMLMethods objects
Expand Down Expand Up @@ -72,14 +74,33 @@ define(["jquery", "underscore", "backbone", "uuid",

/* Initialize an EMLAttribute object */
initialize: function(attributes, options) {

if (!attributes) {
var attributes = {};
}

// If initialized with missingValueCode as an array, convert it to a collection
if (
attributes.missingValueCodes &&
attributes.missingValueCodes instanceof Array
) {
this.missingValueCodes =
new EMLMissingValueCodes(attributes.missingValueCode);
}

this.stopListening(this.get("missingValueCodes"));
this.listenTo(
this.get("missingValueCodes"),
"update",
this.trickleUpChange
)
this.on(
"change:attributeName " +
"change:attributeLabel " +
"change:attributeDefinition " +
"change:storageType " +
"change:measurementScale " +
"change:missingValueCode " +
"change:missingValueCodes " +
"change:accuracy " +
"change:coverage " +
"change:methods " +
Expand Down Expand Up @@ -130,7 +151,6 @@ define(["jquery", "underscore", "backbone", "uuid",
attributes.typeSystem.push(type || null);
});


var measurementScale = $objectDOM.find("measurementscale")[0];
if ( measurementScale ) {
attributes.measurementScale =
Expand All @@ -151,6 +171,12 @@ define(["jquery", "underscore", "backbone", "uuid",
attributes.annotation.push(annotation);
}, this);

// Add the missingValueCodes as a collection
attributes.missingValueCodes = new EMLMissingValueCodes();
attributes.missingValueCodes.parse(
$objectDOM.children("missingvaluecode")
);

attributes.objectDOM = $objectDOM[0];

return attributes;
Expand Down Expand Up @@ -187,7 +213,6 @@ define(["jquery", "underscore", "backbone", "uuid",
// This is new, create it
} else {
objectDOM = document.createElement(type);

}

// update the id attribute
Expand Down Expand Up @@ -291,7 +316,7 @@ define(["jquery", "underscore", "backbone", "uuid",
}
}
}
//If there is no attirbute definition, then return an empty String
// If there is no attribute definition, then return an empty String
// because it is invalid
else{
return "";
Expand Down Expand Up @@ -390,6 +415,22 @@ define(["jquery", "underscore", "backbone", "uuid",
$(after).after(anno.updateDOM());
}, this);

// Update the missingValueCodes
nodeToInsertAfter = undefined;
var missingValueCodes = this.get("missingValueCodes");
$(objectDOM).children("missingvaluecode").remove();
if (missingValueCodes) {
var missingValueCodeNodes = missingValueCodes.updateDOM();
if (missingValueCodeNodes) {
nodeToInsertAfter = this.getEMLPosition(objectDOM, "missingValueCode");
if (typeof nodeToInsertAfter === "undefined") {
$(objectDOM).append(missingValueCodeNodes);
} else {
$(nodeToInsertAfter).after(missingValueCodeNodes);
}
}
}

return objectDOM;
},

Expand Down
140 changes: 140 additions & 0 deletions src/js/models/metadata/eml211/EMLMissingValueCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
define(["backbone"], function (Backbone) {
/**
* @class EMLMissingValueCode
* @classdesc A missing value code is a code that is used to indicate
* that a value is missing from the data.
* @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html
* @classcategory Models/Metadata/EML211
* @since x.x.x
*/
var EMLMissingValueCode = Backbone.Model.extend(
/** @lends EMLMissingValueCode.prototype */ {
/**
* The default attributes for an EMLMissingValueCode model.
* @returns {object} The default attributes
* @property {string} type - The element type in the DOM
* @property {string} code - The missing value code
* @property {string} codeExplanation - The explanation for the missing value code
* @property {string[]} nodeOrder - The order of the EML nodes in this object
*/
defaults: function () {
return {
type: "missingValueCode",
code: "",
codeExplanation: "",
nodeOrder: ["code", "codeExplanation"],
};
},

/*
* Parse the incoming attribute's XML elements.
*/
parse: function (attributes, options) {
if (!attributes) return {};
const objectDOM = attributes.objectDOM || attributes.objectXML;
if (!objectDOM) return {};
const $objectDOM = $(objectDOM);

this.defaults().nodeOrder.forEach((node) => {
attributes[node] = $objectDOM.children(node.toLowerCase()).text();
});

attributes.objectDOM = $objectDOM[0];
return attributes;
},

/**
* Create an XML string from the model's attributes.
* @return {string} The XML string
*/
serialize: function () {
const xml = this.updateDOM().outerHTML;
const nodes = this.get("nodeOrder");
// replace lowercase node names with camelCase
nodes.forEach((node) => {
xml.replace(`<${node.toLowerCase()}>`, `<${node}>`);
xml.replace(`</${node.toLowerCase()}>`, `</${node}>`);
});
return xml;
},

/**
* Copy the original XML and update fields in a DOM object
* @param {object} objectDOM - The DOM object to update
*/
updateDOM: function (objectDOM) {
const type = this.get("type") || "missingValueCode";
if (!objectDOM) {
objectDOM = this.get("objectDOM") || this.get("objectXML");
}
if (!objectDOM) {
objectDOM = document.createElement(type);
}
const $objectDOM = $(objectDOM);

this.get("nodeOrder").forEach((nodeName) => {
// Remove any existing nodes
$objectDOM.children(nodeName.toLowerCase()).remove();

const newValue = this.get(nodeName)?.trim();

// Add the new node
if (newValue) {
const node = document.createElement(nodeName);
$(node).text(newValue);
$objectDOM.append(node);
}
});

return $objectDOM[0];
},

/**
* Return true if all of the model's attributes are empty
* @return {boolean}
*/
isEmpty: function () {
return !this.get("code") && !this.get("codeExplanation");
},

/**
* Validate the model attributes
* @return {object} The validation errors, if any
*/
validate: function () {
if (this.isEmpty()) return undefined;

const errors = [];

// Need a code and an explanation. Both must be non-empty strings.
let code = this.get("code");
let codeExplanation = this.get("codeExplanation");
if (
!code ||
!codeExplanation ||
typeof code !== "string" ||
typeof codeExplanation !== "string"
) {
errors.missingValueCode =
"Missing value code and explanation are required.";
return errors;
}
code = code.trim();
codeExplanation = codeExplanation.trim();
this.set("code", code);
this.set("codeExplanation", codeExplanation);

// Code must be a non-empty string
if (!code || !codeExplanation) {
errors.missingValueCode =
"Missing value code and explanation are required.";
return errors;
}

return errors.length > 0 ? errors : undefined;
},
}
);

return EMLMissingValueCode;
});
3 changes: 3 additions & 0 deletions src/js/templates/metadata/eml-attribute.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

<h4>Attribute</h4>
<p class="subtle">
Describe the attributes (i.e., fields or variables) of this file.
Expand Down Expand Up @@ -57,4 +58,6 @@ <h5>Definition <span class="required-icon"></span></h5>

<div class="measurement-scale-container"></div>

<div class="missing-values-container"></div>

</div>
Loading

0 comments on commit 9d1ff5d

Please sign in to comment.