diff --git a/src/js/collections/metadata/eml/EMLDistributions.js b/src/js/collections/metadata/eml/EMLDistributions.js new file mode 100644 index 000000000..8ba696865 --- /dev/null +++ b/src/js/collections/metadata/eml/EMLDistributions.js @@ -0,0 +1,112 @@ +"use strict"; + +define(["backbone", "models/metadata/eml211/EMLDistribution"], function ( + Backbone, + EMLDistribution +) { + /** + * @class EMLDistributions + * @classdesc A collection of EMLDistributions. + * @classcategory Collections/Metadata/EML + * @since x.x.x + */ + var EMLDistributions = Backbone.Collection.extend( + /** @lends EMLDistributions.prototype */ + { + /** + * The reference to the model class that this collection is made of. + * @type EMLDistribution + */ + model: EMLDistribution, + + /** + * Find the distribution that has all of the matching attributes. This + * will return true if the distribution has all of the attributes, even if + * it has more attributes than the ones passed in. Only the first matching + * distribution will be returned. + * @param {object} attributes - The attributes to match + * @param {boolean} partialMatch - If true, then the attribute values in + * the distribution models only need to partially match the attribute + * values given. If false, then the attributes must match exactly. + * @return {EMLDistribution|undefined} The matching distribution, or + * undefined if there is no match. + */ + findByAttributes: function (attributes, partialMatch = false) { + return this.find((d) => { + return Object.keys(attributes).every((key) => { + const val = d.get(key); + if (partialMatch) { + return val.includes(attributes[key]); + } + return val === attributes[key]; + }); + }); + }, + + /** + * Remove the distribution that has all of the matching attributes. This + * will remove the first distribution that has all of the attributes, even + * if it has more attributes than the ones passed in. + * @param {object} attributes - The attributes to match + * @param {boolean} partialMatch - If true, then the attribute values in + * the distribution models only need to partially match the attribute + * values given. If false, then the attributes must match exactly. + * @return {EMLDistribution|undefined} The matching distribution, or + * undefined if there is no match. + */ + removeByAttributes: function (attributes, partialMatch = false) { + const dist = this.findByAttributes(attributes, partialMatch); + if (dist) { + return this.remove(dist); + } + }, + + /** + * Make sure that the EML dataset element has a distribution node with the + * location where the data package can be viewed. This will be either the + * view URL for the member node being used or the DOI.org URL if the + * dataset has one. This method will look for the old distribution URL and + * update it if it exists, or add a new distribution node if it doesn't. + * @param {string} url - The URL to add to the dataset distribution + * @param {string[]} oldIDs - The old PIDs, seriesIds, or current PID to + * remove from the dataset distribution + * @return {EMLDistribution} The distribution that was added or updated + */ + addDatasetDistributionURL: function (url, oldIDs = []) { + if (!url) { + console.warn("No URL given to addDatasetDistributionURL"); + return; + } + + // Reference to this collection + const dists = this; + // The URL function used for dataset distribution URLs + const func = "information"; + + // Remove any distribution models with the old PID, seriesId, or current + // PID in the URL (only if the URL function is "information") + if (dists.length && oldIDs.length) { + oldIDs.forEach((url) => { + dists.removeByAttributes({ url: id, urlFunction: func }, true); + }); + } + + // Add a new distribution with the view URL + return dists.add({ url: url, urlFunction: urlFunction }); + }, + + /** + * Update the DOM for each distribution in this collection with the + * current model state. + * @return {object[]} An array of jQuery DOM objects for each distribution + * in this collection. + */ + updateDOMs: function (doms) { + const objectDOMs = this.map((model) => model.updateDOM()); + return objectDOMs; + }, + } + ); + + return EMLDistributions; +}); diff --git a/src/js/models/metadata/eml211/EML211.js b/src/js/models/metadata/eml211/EML211.js index 35dab46df..c83c08055 100644 --- a/src/js/models/metadata/eml211/EML211.js +++ b/src/js/models/metadata/eml211/EML211.js @@ -7,7 +7,7 @@ define(['jquery', 'underscore', 'backbone', 'uuid', 'models/metadata/eml211/EMLKeywordSet', 'models/metadata/eml211/EMLTaxonCoverage', 'models/metadata/eml211/EMLTemporalCoverage', - 'models/metadata/eml211/EMLDistribution', + 'collections/metadata/eml/EMLDistributions', 'models/metadata/eml211/EMLEntity', 'models/metadata/eml211/EMLDataTable', 'models/metadata/eml211/EMLOtherEntity', @@ -19,7 +19,7 @@ define(['jquery', 'underscore', 'backbone', 'uuid', 'models/metadata/eml211/EMLAnnotation'], function($, _, Backbone, uuid, Units, ScienceMetadata, DataONEObject, EMLGeoCoverage, EMLKeywordSet, EMLTaxonCoverage, EMLTemporalCoverage, - EMLDistribution, EMLEntity, EMLDataTable, EMLOtherEntity, EMLParty, + EMLDistributions, EMLEntity, EMLDataTable, EMLOtherEntity, EMLParty, EMLProject, EMLText, EMLMethods, EMLAnnotations, EMLAnnotation) { /** @@ -55,7 +55,7 @@ define(['jquery', 'underscore', 'backbone', 'uuid', keywordSets: [], //array of EMLKeywordSet objects additionalInfo: [], intellectualRights: "This work is dedicated to the public domain under the Creative Commons Universal 1.0 Public Domain Dedication. To view a copy of this dedication, visit https://creativecommons.org/publicdomain/zero/1.0/.", - distribution: [], // array of EMLDistribution objects + distributions: new EMLDistributions(), // EMLDistribution collection geoCoverage : [], //an array for EMLGeoCoverages temporalCoverage : [], //an array of EMLTempCoverage models taxonCoverage : [], //an array of EMLTaxonCoverages @@ -513,14 +513,18 @@ define(['jquery', 'underscore', 'backbone', 'uuid', type: attributeName })); } - //EML Distribution modules are stored in EMLDistribution models - else if(_.contains(emlDistribution, thisNode.localName)) { - if(typeof modelJSON[thisNode.localName] == "undefined") modelJSON[thisNode.localName] = []; - - modelJSON[thisNode.localName].push(new EMLDistribution({ - objectDOM: thisNode, - parentModel: model - }, { parse: true })); + //EML Distribution info is stored in an EMLDistribution collection + else if (_.contains(emlDistribution, thisNode.localName)) { + // Create the collection if it doesn't exist + const distName = thisNode.localName + let distCollection = modelJSON[distName] + if (!distCollection) { + modelJSON[distName] = distCollection = new EMLDistributions(); + } + // Add the distribution to the collection + const distAttrs = { objectDOM: thisNode, parentModel: model } + const distOpts = { parse: true } + distCollection.add(distAttrs, distOpts); } //The EML Project is stored in the EMLProject model else if(thisNode.localName == "project"){ @@ -1029,21 +1033,15 @@ define(['jquery', 'underscore', 'backbone', 'uuid', } // Serialize the distribution - const distributions = this.get('distribution'); - if (distributions && distributions.length > 0) { - // Remove existing nodes - datasetNode.children('distribution').remove(); - // Get the updated DOMs - const distributionDOMs = distributions.map(d => d.updateDOM()); - // Insert the updated DOMs in their correct positions - distributionDOMs.forEach((dom, i) => { - const insertAfter = this.getEMLPosition(eml, 'distribution'); - if (insertAfter) { - insertAfter.after(dom); - } else { - datasetNode.append(dom); - } - }); + datasetNode.children('distribution').remove(); + const distributionDOMs = this.getDistributions().updateDOMs(); + if (distributionDOMs.length) { + const insertAfter = this.getEMLPosition(eml, 'distribution'); + if (insertAfter) { + insertAfter.after(distributionDOMs); + } else { + datasetNode.append(distributionDOMs); + } } //Detach the project elements from the DOM @@ -1438,44 +1436,24 @@ define(['jquery', 'underscore', 'backbone', 'uuid', }, /** - * Adds a new EMLDistribution model to the distribution array - * @param {object} attributes - The attributes to set on the new - * EMLDistribution model - * @param {object} options - Options to pass to the new EMLDistribution - * model + * Get the distribution model collection on this EML model. If there is no + * distribution collection, then one is created, set on the EML model, and + * returned. + * @return {EMLDistributions} The distribution collection on this EML model */ - addDistribution: function (attributes, options) { + getDistributions: function () { try { - const distributions = this.get('distribution') || []; - const newDistribution = new EMLDistribution(attributes, options); - distributions.push(newDistribution); - this.set('distribution', distributions); + let distributions = this.get('distribution'); + if (!distributions) { + distributions = new EMLDistributions(); + this.set('distribution', distributions); + } + return distributions; } catch (e) { - console.log("Couldn't add a distribution to the EML model", e); + console.log("Couldn't get the distributions from the EML model", e); } }, - /** - * Find the distribution that has all of the matching attributes. This will - * return true if the distribution has all of the attributes, even if it - * has more attributes than the ones passed in. - * @param {object} attributes - The attributes to match - * @param {boolean} partialMatch - If true, then the attributes only need - * to partially match. If false, then the attributes must match exactly. - */ - findDistribution: function (attributes, partialMatch = false) { - const distributions = this.get('distribution') || []; - return distributions.find(d => { - return Object.keys(attributes).every(key => { - const val = d.get(key); - if (partialMatch) { - return val.includes(attributes[key]); - } - return val === attributes[key]; - }); - }); - }, - /** * Make sure that the EML dataset element has a distribution node with the * location where the data package can be viewed. This will be either the @@ -1484,38 +1462,16 @@ define(['jquery', 'underscore', 'backbone', 'uuid', * if it exists, or add a new distribution node if it doesn't. */ addDatasetDistributionURL: function () { - const model = this; - const oldPid = this.get('oldPid'); - const newPid = this.get('id'); - const seriesId = this.get('seriesId'); - const IDs = [oldPid, newPid, seriesId] - - // Remove any distribution models with the old PID, seriesId, or current - // PID in the URL (only if the URL function is "information") - const distributions = this.get('distribution') || []; - IDs.forEach(id => { - const distributions = model.get('distribution'); - if(!distributions || !distributions.length) return; - const dist = this.findDistribution( - { url: id, urlFunction: 'information' }, true); - if (dist) { - // Remove the distribution model from the array - distributions.splice(distributions.indexOf(dist), 1); - } - }); - - - // Add a new distribution node with the view URL - const viewURL = this.getCanonicalDOIIRI() || this.createViewURL(); - if (viewURL) { - this.addDistribution({ - url: viewURL, - urlFunction: 'information' - }); - } else { - console.log('Could not add a distribution node with the view URL'); + try { + // Old distribution URLs could exist for any of the old or current + const IDs = [ this.get('oldPid'), this.get('id'), this.get('seriesId')] + // The new distribution URL will be the view URL or DOI URL + const viewURL = this.getCanonicalDOIIRI() || this.createViewURL(); + // Add the new distribution URL to the collection + this.getDistributions().addDatasetDistributionURL(viewURL, IDs); + } catch (e) { + console.log("Couldn't add the distribution URL to the EML model", e); } - }, diff --git a/src/js/models/metadata/eml211/EMLDistribution.js b/src/js/models/metadata/eml211/EMLDistribution.js index 2f49165c0..1cc1ff6ed 100644 --- a/src/js/models/metadata/eml211/EMLDistribution.js +++ b/src/js/models/metadata/eml211/EMLDistribution.js @@ -228,6 +228,7 @@ define(["jquery", "underscore", "backbone", "models/DataONEObject"], function ( /* * Makes a copy of the original XML DOM and updates it with the new values * from the model. + * @return {Element} The updated XML DOM */ updateDOM: function () { const objectDOM = @@ -357,10 +358,18 @@ define(["jquery", "underscore", "backbone", "models/DataONEObject"], function ( else return false; }, + /* + * Trigger a change event on the parent EML model + */ trickleUpChange: function () { MetacatUI.rootDataPackage?.packageModel?.set("changed", true); }, + /* + * Formats the given XML string to be human-readable + * @param {string} xmlString - The XML string to format + * @return {string} - The formatted XML string + */ formatXML: function (xmlString) { return DataONEObject.prototype.formatXML.call(this, xmlString); }, diff --git a/test/config/tests.json b/test/config/tests.json index a558e0dec..5585a8f20 100644 --- a/test/config/tests.json +++ b/test/config/tests.json @@ -21,6 +21,7 @@ "./js/specs/unit/collections/metadata/eml/EMLMissingValueCodes.spec.js", "./js/specs/unit/models/metadata/eml211/EMLMissingValueCode.spec.js", "./js/specs/unit/models/metadata/eml211/EMLDistribution.spec.js", + "./js/specs/unit/collections/metadata/eml/EMLDistributions.spec.js", "./js/specs/unit/models/maps/assets/CesiumImagery.spec.js", "./js/specs/unit/collections/maps/Geohashes.spec.js", "./js/specs/unit/models/connectors/Filters-Map.spec.js", diff --git a/test/js/specs/unit/collections/metadata/eml/EMLDistributions.spec.js b/test/js/specs/unit/collections/metadata/eml/EMLDistributions.spec.js new file mode 100644 index 000000000..8ff730f08 --- /dev/null +++ b/test/js/specs/unit/collections/metadata/eml/EMLDistributions.spec.js @@ -0,0 +1,115 @@ +define([ + "../../../../../../../../src/js/collections/metadata/eml/EMLDistributions", +], function (EMLDistributions) { + // Configure the Chai assertion library + var should = chai.should(); + var expect = chai.expect; + + describe("EMLDistributions Test Suite", function () { + /* Set up */ + beforeEach(function () { + this.EMLDistributions = new EMLDistributions(); + }); + + /* Tear down */ + afterEach(function () { + delete this.EMLDistributions; + }); + + describe("Initialization", function () { + it("should create a EMLDistributions instance", function () { + new EMLDistributions().should.be.instanceof(EMLDistributions); + }); + }); + + describe("Finding distributions", function () { + it("should find a distribution by attributes", function () { + let dist = this.EMLDistributions.add({ url: "http://example.com" }); + this.EMLDistributions.findByAttributes({ url: "http://example.com" }).should.equal(dist); + }); + + it("should find a distribution by partial attributes", function () { + let dist = this.EMLDistributions.add({ url: "http://example.com" }); + this.EMLDistributions.findByAttributes({ url: "example.com" }, true).should.equal(dist); + }); + + it("should not find a distribution by attributes", function () { + let dist = this.EMLDistributions.add({ url: "http://example.com" }); + expect(this.EMLDistributions.findByAttributes({ url: "http://example.org" })).to.be.undefined; + }); + + it("should not find a distribution by partial attributes", function () { + let dist = this.EMLDistributions.add({ url: "http://example.com" }); + expect(this.EMLDistributions.findByAttributes({ url: "example.org" }, true)).to.be.undefined; + }); + + it("should find a distribution by attributes with multiple matches", function () { + let dist1 = this.EMLDistributions.add({ url: "http://example.com" }); + let dist2 = this.EMLDistributions.add({ url: "http://example.com" }); + this.EMLDistributions.findByAttributes({ url: "http://example.com" }).should.equal(dist1); + }); + + it("should find a distribution by partial attributes with multiple matches", function () { + let dist1 = this.EMLDistributions.add({ url: "http://example.com" }); + let dist2 = this.EMLDistributions.add({ url: "http://example.com" }); + this.EMLDistributions.findByAttributes({ url: "example.com" }, true).should.equal(dist1); + }); + + }); + + describe("Adding and removing distributions", function () { + it("should add a distribution", function () { + let dist = this.EMLDistributions.add({ url: "http://example.com" }); + this.EMLDistributions.length.should.equal(1); + this.EMLDistributions.at(0).should.equal(dist); + }); + + it("should parse a distribution that is added with a DOM", function () { + let dom = jQuery("http://example.com"); + let dist = this.EMLDistributions.add({ objectDOM: dom }, { parse: true }); + this.EMLDistributions.length.should.equal(1); + this.EMLDistributions.at(0).should.equal(dist); + this.EMLDistributions.at(0).get("url").should.equal("http://example.com"); + }); + + it("should remove a distribution", function () { + let dist = this.EMLDistributions.add({ url: "http://example.com" }); + this.EMLDistributions.remove(dist); + this.EMLDistributions.length.should.equal(0); + }); + + it("should remove a distribution by attributes", function () { + let dist = this.EMLDistributions.add({ url: "http://example.com" }); + this.EMLDistributions.removeByAttributes({ url: "http://example.com" }); + this.EMLDistributions.length.should.equal(0); + }); + + it("should remove a distribution by partial attributes", function () { + let dist = this.EMLDistributions.add({ url: "http://example.com" }); + this.EMLDistributions.removeByAttributes({ url: "example.com" }, true); + this.EMLDistributions.length.should.equal(0); + }); + + }); + + describe("Updating DOMs", function () { + it("should update DOMs", function () { + let dist = this.EMLDistributions.add({ url: "http://example.com", urlFunction: "information" }); + let doms = this.EMLDistributions.updateDOMs(); + doms.length.should.equal(1); + doms[0].tagName.should.equal("DISTRIBUTION"); + // Immediate child should be + doms[0].children[0].tagName.should.equal("ONLINE"); + // should have 1 child: + doms[0].children[0].children.length.should.equal(1); + // should have the correct text + doms[0].children[0].children[0].textContent.should.equal("http://example.com"); + // should have the correct attribute: function="information" + doms[0].children[0].children[0].getAttribute("function").should.equal("information"); + }); + }); + + + + }); +});