-
-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow adding, removing, and changing the "canonical dataset" in the Editor #2551
base: develop
Are you sure you want to change the base?
Conversation
Left to do:
|
43c3062
to
4df698d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit
eslint
🚫 [eslint] <object-shorthand> reported by reviewdog 🐶
Expected method shorthand.
metacatui/src/js/views/metadata/EML211View.js
Lines 2050 to 2085 in 4df698d
addBasicText: function (e) { | |
var category = $(e.target).attr("data-category"), | |
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" || category === "canonicalDataset") | |
return; | |
//Add another blank text input | |
var newRow = $(document.createElement("div")).addClass( | |
"basic-text-row", | |
); | |
newRow.append( | |
$(document.createElement("input")) | |
.attr("type", "text") | |
.attr("data-category", category) | |
.attr("placeholder", $(e.target).attr("placeholder")) | |
.addClass("new basic-text"), | |
); | |
$(e.target).parent().after(newRow); | |
$(e.target).after( | |
this.createRemoveButton( | |
null, | |
category, | |
".basic-text-row", | |
"div.text-container", | |
), | |
); | |
}, |
🚫 [eslint] <one-var> reported by reviewdog 🐶
Split 'var' declarations into multiple statements.
metacatui/src/js/views/metadata/EML211View.js
Lines 2051 to 2054 in 4df698d
var category = $(e.target).attr("data-category"), | |
allBasicTexts = $( | |
".basic-text.new[data-category='" + category + "']", | |
); |
🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.
metacatui/src/js/views/metadata/EML211View.js
Lines 2051 to 2054 in 4df698d
var category = $(e.target).attr("data-category"), | |
allBasicTexts = $( | |
".basic-text.new[data-category='" + category + "']", | |
); |
🚫 [eslint] <vars-on-top> reported by reviewdog 🐶
All 'var' declarations must be at the top of the function scope.
metacatui/src/js/views/metadata/EML211View.js
Lines 2063 to 2065 in 4df698d
var newRow = $(document.createElement("div")).addClass( | |
"basic-text-row", | |
); |
🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.
metacatui/src/js/views/metadata/EML211View.js
Lines 2063 to 2065 in 4df698d
var newRow = $(document.createElement("div")).addClass( | |
"basic-text-row", | |
); |
- 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
99cf131
to
877beac
Compare
(in Editor) Issue #2542
- Fix the findDuplicates method to return the correct result Issue #2542
Issue #2542
And remove dev code Issue #2542
); | ||
if (canonicalError) { | ||
errors.canonicalDataset = canonicalError.message; | ||
} | ||
} | ||
|
||
//Check the required fields for this MetacatUI configuration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 [eslint] <spaced-comment> reported by reviewdog 🐶
Expected exception block, space or tab after '//' in comment.
//Check the required fields for this MetacatUI configuration | |
// Check the required fields for this MetacatUI configuration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 [eslint] <prefer-arrow-callback> reported by reviewdog 🐶
Unexpected function expression.
metacatui/src/js/models/metadata/eml211/EML211.js
Lines 23 to 2671 in 979d2a5
], function ( | |
$, | |
_, | |
Backbone, | |
uuid, | |
Units, | |
ScienceMetadata, | |
DataONEObject, | |
EMLGeoCoverage, | |
EMLKeywordSet, | |
EMLTaxonCoverage, | |
EMLTemporalCoverage, | |
EMLDistribution, | |
EMLEntity, | |
EMLDataTable, | |
EMLOtherEntity, | |
EMLParty, | |
EMLProject, | |
EMLText, | |
EMLMethods, | |
EMLAnnotations, | |
EMLAnnotation, | |
) { | |
/** | |
* @class EML211 | |
* @classdesc An EML211 object represents an Ecological Metadata Language | |
* document, version 2.1.1 | |
* @classcategory Models/Metadata/EML211 | |
* @extends ScienceMetadata | |
*/ | |
var EML211 = ScienceMetadata.extend( | |
/** @lends EML211.prototype */ { | |
type: "EML", | |
defaults: function () { | |
return _.extend(ScienceMetadata.prototype.defaults(), { | |
id: "urn:uuid:" + uuid.v4(), | |
formatId: "https://eml.ecoinformatics.org/eml-2.2.0", | |
objectXML: null, | |
isEditable: false, | |
alternateIdentifier: [], | |
shortName: null, | |
title: [], | |
creator: [], // array of EMLParty objects | |
metadataProvider: [], // array of EMLParty objects | |
associatedParty: [], // array of EMLParty objects | |
contact: [], // array of EMLParty objects | |
publisher: [], // array of EMLParty objects | |
pubDate: null, | |
language: null, | |
series: null, | |
abstract: [], //array of EMLText objects | |
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 | |
geoCoverage: [], //an array for EMLGeoCoverages | |
temporalCoverage: [], //an array of EMLTempCoverage models | |
taxonCoverage: [], //an array of EMLTaxonCoverages | |
purpose: [], | |
entities: [], //An array of EMLEntities | |
pubplace: null, | |
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: [ | |
"alternateidentifier", | |
"shortname", | |
"title", | |
"creator", | |
"metadataprovider", | |
"associatedparty", | |
"pubdate", | |
"language", | |
"series", | |
"abstract", | |
"keywordset", | |
"additionalinfo", | |
"intellectualrights", | |
"licensed", | |
"distribution", | |
"coverage", | |
"annotation", | |
"purpose", | |
"introduction", | |
"gettingstarted", | |
"acknowledgements", | |
"maintenance", | |
"contact", | |
"publisher", | |
"pubplace", | |
"methods", | |
"project", | |
"datatable", | |
"spatialraster", | |
"spatialvector", | |
"storedprocedure", | |
"view", | |
"otherentity", | |
"referencepublications", | |
"usagecitations", | |
"literaturecited", | |
], | |
}); | |
}, | |
units: new Units(), | |
initialize: function (attributes) { | |
// Call initialize for the super class | |
ScienceMetadata.prototype.initialize.call(this, attributes); | |
// EML211-specific init goes here | |
// this.set("objectXML", this.createXML()); | |
this.parse(this.createXML()); | |
this.on("sync", function () { | |
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(); | |
}, | |
url: function (options) { | |
var identifier; | |
if (options && options.update) { | |
identifier = this.get("oldPid") || this.get("seriesid"); | |
} else { | |
identifier = this.get("id") || this.get("seriesid"); | |
} | |
return ( | |
MetacatUI.appModel.get("objectServiceUrl") + | |
encodeURIComponent(identifier) | |
); | |
}, | |
/** | |
* 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; | |
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() | |
*/ | |
nodeNameMap: function () { | |
return _.extend( | |
this.constructor.__super__.nodeNameMap(), | |
EMLDistribution.prototype.nodeNameMap(), | |
EMLGeoCoverage.prototype.nodeNameMap(), | |
EMLKeywordSet.prototype.nodeNameMap(), | |
EMLParty.prototype.nodeNameMap(), | |
EMLProject.prototype.nodeNameMap(), | |
EMLTaxonCoverage.prototype.nodeNameMap(), | |
EMLTemporalCoverage.prototype.nodeNameMap(), | |
EMLMethods.prototype.nodeNameMap(), | |
{ | |
accuracyreport: "accuracyReport", | |
actionlist: "actionList", | |
additionalclassifications: "additionalClassifications", | |
additionalinfo: "additionalInfo", | |
additionallinks: "additionalLinks", | |
additionalmetadata: "additionalMetadata", | |
allowfirst: "allowFirst", | |
alternateidentifier: "alternateIdentifier", | |
altitudedatumname: "altitudeDatumName", | |
altitudedistanceunits: "altitudeDistanceUnits", | |
altituderesolution: "altitudeResolution", | |
altitudeencodingmethod: "altitudeEncodingMethod", | |
altitudesysdef: "altitudeSysDef", | |
asneeded: "asNeeded", | |
associatedparty: "associatedParty", | |
attributeaccuracyexplanation: "attributeAccuracyExplanation", | |
attributeaccuracyreport: "attributeAccuracyReport", | |
attributeaccuracyvalue: "attributeAccuracyValue", | |
attributedefinition: "attributeDefinition", | |
attributelabel: "attributeLabel", | |
attributelist: "attributeList", | |
attributename: "attributeName", | |
attributeorientation: "attributeOrientation", | |
attributereference: "attributeReference", | |
awardnumber: "awardNumber", | |
awardurl: "awardUrl", | |
audiovisual: "audioVisual", | |
authsystem: "authSystem", | |
banddescription: "bandDescription", | |
bilinearfit: "bilinearFit", | |
binaryrasterformat: "binaryRasterFormat", | |
blockedmembernode: "blockedMemberNode", | |
booktitle: "bookTitle", | |
cameracalibrationinformationavailability: | |
"cameraCalibrationInformationAvailability", | |
casesensitive: "caseSensitive", | |
cellgeometry: "cellGeometry", | |
cellsizexdirection: "cellSizeXDirection", | |
cellsizeydirection: "cellSizeYDirection", | |
changehistory: "changeHistory", | |
changedate: "changeDate", | |
changescope: "changeScope", | |
chapternumber: "chapterNumber", | |
characterencoding: "characterEncoding", | |
checkcondition: "checkCondition", | |
checkconstraint: "checkConstraint", | |
childoccurences: "childOccurences", | |
citableclassificationsystem: "citableClassificationSystem", | |
cloudcoverpercentage: "cloudCoverPercentage", | |
codedefinition: "codeDefinition", | |
codeexplanation: "codeExplanation", | |
codesetname: "codesetName", | |
codeseturl: "codesetURL", | |
collapsedelimiters: "collapseDelimiters", | |
communicationtype: "communicationType", | |
compressiongenerationquality: "compressionGenerationQuality", | |
compressionmethod: "compressionMethod", | |
conferencedate: "conferenceDate", | |
conferencelocation: "conferenceLocation", | |
conferencename: "conferenceName", | |
conferenceproceedings: "conferenceProceedings", | |
constraintdescription: "constraintDescription", | |
constraintname: "constraintName", | |
constanttosi: "constantToSI", | |
controlpoint: "controlPoint", | |
cornerpoint: "cornerPoint", | |
customunit: "customUnit", | |
dataformat: "dataFormat", | |
datasetgpolygon: "datasetGPolygon", | |
datasetgpolygonoutergring: "datasetGPolygonOuterGRing", | |
datasetgpolygonexclusiongring: "datasetGPolygonExclusionGRing", | |
datatable: "dataTable", | |
datatype: "dataType", | |
datetime: "dateTime", | |
datetimedomain: "dateTimeDomain", | |
datetimeprecision: "dateTimePrecision", | |
defaultvalue: "defaultValue", | |
definitionattributereference: "definitionAttributeReference", | |
denomflatratio: "denomFlatRatio", | |
depthsysdef: "depthSysDef", | |
depthdatumname: "depthDatumName", | |
depthdistanceunits: "depthDistanceUnits", | |
depthencodingmethod: "depthEncodingMethod", | |
depthresolution: "depthResolution", | |
descriptorvalue: "descriptorValue", | |
dictref: "dictRef", | |
diskusage: "diskUsage", | |
domainDescription: "domainDescription", | |
editedbook: "editedBook", | |
encodingmethod: "encodingMethod", | |
endcondition: "endCondition", | |
entitycodelist: "entityCodeList", | |
entitydescription: "entityDescription", | |
entityname: "entityName", | |
entityreference: "entityReference", | |
entitytype: "entityType", | |
enumerateddomain: "enumeratedDomain", | |
errorbasis: "errorBasis", | |
errorvalues: "errorValues", | |
externalcodeset: "externalCodeSet", | |
externallydefinedformat: "externallyDefinedFormat", | |
fielddelimiter: "fieldDelimiter", | |
fieldstartcolumn: "fieldStartColumn", | |
fieldwidth: "fieldWidth", | |
filmdistortioninformationavailability: | |
"filmDistortionInformationAvailability", | |
foreignkey: "foreignKey", | |
formatname: "formatName", | |
formatstring: "formatString", | |
formatversion: "formatVersion", | |
fractiondigits: "fractionDigits", | |
fundername: "funderName", | |
funderidentifier: "funderIdentifier", | |
gettingstarted: "gettingStarted", | |
gring: "gRing", | |
gringpoint: "gRingPoint", | |
gringlatitude: "gRingLatitude", | |
gringlongitude: "gRingLongitude", | |
geogcoordsys: "geogCoordSys", | |
geometricobjectcount: "geometricObjectCount", | |
georeferenceinfo: "georeferenceInfo", | |
highwavelength: "highWavelength", | |
horizontalaccuracy: "horizontalAccuracy", | |
horizcoordsysdef: "horizCoordSysDef", | |
horizcoordsysname: "horizCoordSysName", | |
identifiername: "identifierName", | |
illuminationazimuthangle: "illuminationAzimuthAngle", | |
illuminationelevationangle: "illuminationElevationAngle", | |
imagingcondition: "imagingCondition", | |
imagequalitycode: "imageQualityCode", | |
imageorientationangle: "imageOrientationAngle", | |
intellectualrights: "intellectualRights", | |
imagedescription: "imageDescription", | |
isbn: "ISBN", | |
issn: "ISSN", | |
joincondition: "joinCondition", | |
keywordtype: "keywordType", | |
languagevalue: "LanguageValue", | |
languagecodestandard: "LanguageCodeStandard", | |
lensdistortioninformationavailability: | |
"lensDistortionInformationAvailability", | |
licensename: "licenseName", | |
licenseurl: "licenseURL", | |
linenumber: "lineNumber", | |
literalcharacter: "literalCharacter", | |
literallayout: "literalLayout", | |
literaturecited: "literatureCited", | |
lowwavelength: "lowWaveLength", | |
machineprocessor: "machineProcessor", | |
maintenanceupdatefrequency: "maintenanceUpdateFrequency", | |
matrixtype: "matrixType", | |
maxexclusive: "maxExclusive", | |
maxinclusive: "maxInclusive", | |
maxlength: "maxLength", | |
maxrecordlength: "maxRecordLength", | |
maxvalues: "maxValues", | |
measurementscale: "measurementScale", | |
metadatalist: "metadataList", | |
methodstep: "methodStep", | |
minexclusive: "minExclusive", | |
mininclusive: "minInclusive", | |
minlength: "minLength", | |
minvalues: "minValues", | |
missingvaluecode: "missingValueCode", | |
moduledocs: "moduleDocs", | |
modulename: "moduleName", | |
moduledescription: "moduleDescription", | |
multiband: "multiBand", | |
multipliertosi: "multiplierToSI", | |
nonnumericdomain: "nonNumericDomain", | |
notnullconstraint: "notNullConstraint", | |
notplanned: "notPlanned", | |
numberofbands: "numberOfBands", | |
numbertype: "numberType", | |
numericdomain: "numericDomain", | |
numfooterlines: "numFooterLines", | |
numheaderlines: "numHeaderLines", | |
numberofrecords: "numberOfRecords", | |
numberofvolumes: "numberOfVolumes", | |
numphysicallinesperrecord: "numPhysicalLinesPerRecord", | |
objectname: "objectName", | |
oldvalue: "oldValue", | |
operatingsystem: "operatingSystem", | |
orderattributereference: "orderAttributeReference", | |
originalpublication: "originalPublication", | |
otherentity: "otherEntity", | |
othermaintenanceperiod: "otherMaintenancePeriod", | |
parameterdefinition: "parameterDefinition", | |
packageid: "packageId", | |
pagerange: "pageRange", | |
parentoccurences: "parentOccurences", | |
parentsi: "parentSI", | |
peakresponse: "peakResponse", | |
personalcommunication: "personalCommunication", | |
physicallinedelimiter: "physicalLineDelimiter", | |
pointinpixel: "pointInPixel", | |
preferredmembernode: "preferredMemberNode", | |
preprocessingtypecode: "preProcessingTypeCode", | |
primarykey: "primaryKey", | |
primemeridian: "primeMeridian", | |
proceduralstep: "proceduralStep", | |
programminglanguage: "programmingLanguage", | |
projcoordsys: "projCoordSys", | |
projectionlist: "projectionList", | |
propertyuri: "propertyURI", | |
pubdate: "pubDate", | |
pubplace: "pubPlace", | |
publicationplace: "publicationPlace", | |
quantitativeaccuracyreport: "quantitativeAccuracyReport", | |
quantitativeaccuracyvalue: "quantitativeAccuracyValue", | |
quantitativeaccuracymethod: "quantitativeAccuracyMethod", | |
quantitativeattributeaccuracyassessment: | |
"quantitativeAttributeAccuracyAssessment", | |
querystatement: "queryStatement", | |
quotecharacter: "quoteCharacter", | |
radiometricdataavailability: "radiometricDataAvailability", | |
rasterorigin: "rasterOrigin", | |
recommendedunits: "recommendedUnits", | |
recommendedusage: "recommendedUsage", | |
referencedkey: "referencedKey", | |
referencetype: "referenceType", | |
relatedentry: "relatedEntry", | |
relationshiptype: "relationshipType", | |
reportnumber: "reportNumber", | |
reprintedition: "reprintEdition", | |
researchproject: "researchProject", | |
researchtopic: "researchTopic", | |
recorddelimiter: "recordDelimiter", | |
referencepublication: "referencePublication", | |
revieweditem: "reviewedItem", | |
rowcolumnorientation: "rowColumnOrientation", | |
runtimememoryusage: "runtimeMemoryUsage", | |
samplingdescription: "samplingDescription", | |
scalefactor: "scaleFactor", | |
sequenceidentifier: "sequenceIdentifier", | |
semiaxismajor: "semiAxisMajor", | |
shortname: "shortName", | |
simpledelimited: "simpleDelimited", | |
spatialraster: "spatialRaster", | |
spatialreference: "spatialReference", | |
spatialvector: "spatialVector", | |
standalone: "standAlone", | |
standardunit: "standardUnit", | |
startcondition: "startCondition", | |
studyareadescription: "studyAreaDescription", | |
storagetype: "storageType", | |
studyextent: "studyExtent", | |
studytype: "studyType", | |
textdelimited: "textDelimited", | |
textdomain: "textDomain", | |
textfixed: "textFixed", | |
textformat: "textFormat", | |
topologylevel: "topologyLevel", | |
tonegradation: "toneGradation", | |
totaldigits: "totalDigits", | |
totalfigures: "totalFigures", | |
totalpages: "totalPages", | |
totaltables: "totalTables", | |
triangulationindicator: "triangulationIndicator", | |
typesystem: "typeSystem", | |
uniquekey: "uniqueKey", | |
unittype: "unitType", | |
unitlist: "unitList", | |
usagecitation: "usageCitation", | |
valueuri: "valueURI", | |
valueattributereference: "valueAttributeReference", | |
verticalaccuracy: "verticalAccuracy", | |
vertcoordsys: "vertCoordSys", | |
virtualmachine: "virtualMachine", | |
wavelengthunits: "waveLengthUnits", | |
whitespace: "whiteSpace", | |
xintercept: "xIntercept", | |
xcoordinate: "xCoordinate", | |
"xsi:schemalocation": "xsi:schemaLocation", | |
xslope: "xSlope", | |
ycoordinate: "yCoordinate", | |
yintercept: "yIntercept", | |
yslope: "ySlope", | |
}, | |
); | |
}, | |
/** | |
* Fetch the EML from the MN object service | |
* @param {object} [options] - A set of options for this fetch() | |
* @property {boolean} [options.systemMetadataOnly=false] - If true, only the system metadata will be fetched. | |
* If false, the system metadata AND EML document will be fetched. | |
*/ | |
fetch: function (options) { | |
if (!options) var options = {}; | |
//Add the authorization header and other AJAX settings | |
_.extend(options, MetacatUI.appUserModel.createAjaxSettings(), { | |
dataType: "text", | |
}); | |
// Merge the system metadata into the object first | |
_.extend(options, { merge: true }); | |
this.fetchSystemMetadata(options); | |
//If we are retrieving system metadata only, then exit now | |
if (options.systemMetadataOnly) return; | |
//Call Backbone.Model.fetch to retrieve the info | |
return Backbone.Model.prototype.fetch.call(this, options); | |
}, | |
/* | |
Deserialize an EML 2.1.1 XML document | |
*/ | |
parse: function (response) { | |
// Save a reference to this model for use in setting the | |
// parentModel inside anonymous functions | |
var model = this; | |
//If the response is XML | |
if (typeof response == "string" && response.indexOf("<") == 0) { | |
//Look for a system metadata tag and call DataONEObject parse instead | |
if (response.indexOf("systemMetadata>") > -1) | |
return DataONEObject.prototype.parse.call(this, response); | |
response = this.cleanUpXML(response); | |
response = this.dereference(response); | |
this.set("objectXML", response); | |
var emlElement = $($.parseHTML(response)).filter("eml\\:eml"); | |
} | |
var datasetEl; | |
if (emlElement[0]) datasetEl = $(emlElement[0]).find("dataset"); | |
if (!datasetEl || !datasetEl.length) return {}; | |
var emlParties = [ | |
"metadataprovider", | |
"associatedparty", | |
"creator", | |
"contact", | |
"publisher", | |
], | |
emlDistribution = ["distribution"], | |
emlEntities = [ | |
"datatable", | |
"otherentity", | |
"spatialvector", | |
"spatialraster", | |
"storedprocedure", | |
"view", | |
], | |
emlText = ["abstract", "additionalinfo"], | |
emlMethods = ["methods"]; | |
var nodes = datasetEl.children(), | |
modelJSON = {}; | |
for (var i = 0; i < nodes.length; i++) { | |
var thisNode = nodes[i]; | |
var convertedName = | |
this.nodeNameMap()[thisNode.localName] || thisNode.localName; | |
//EML Party modules are stored in EMLParty models | |
if (_.contains(emlParties, thisNode.localName)) { | |
if (thisNode.localName == "metadataprovider") | |
var attributeName = "metadataProvider"; | |
else if (thisNode.localName == "associatedparty") | |
var attributeName = "associatedParty"; | |
else var attributeName = thisNode.localName; | |
if (typeof modelJSON[attributeName] == "undefined") | |
modelJSON[attributeName] = []; | |
modelJSON[attributeName].push( | |
new EMLParty({ | |
objectDOM: thisNode, | |
parentModel: model, | |
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 }, | |
), | |
); | |
} | |
//The EML Project is stored in the EMLProject model | |
else if (thisNode.localName == "project") { | |
modelJSON.project = new EMLProject({ | |
objectDOM: thisNode, | |
parentModel: model, | |
}); | |
} | |
//EML Temporal, Taxonomic, and Geographic Coverage modules are stored in their own models | |
else if (thisNode.localName == "coverage") { | |
var temporal = $(thisNode).children("temporalcoverage"), | |
geo = $(thisNode).children("geographiccoverage"), | |
taxon = $(thisNode).children("taxonomiccoverage"); | |
if (temporal.length) { | |
modelJSON.temporalCoverage = []; | |
_.each(temporal, function (t) { | |
modelJSON.temporalCoverage.push( | |
new EMLTemporalCoverage({ | |
objectDOM: t, | |
parentModel: model, | |
}), | |
); | |
}); | |
} | |
if (geo.length) { | |
modelJSON.geoCoverage = []; | |
_.each(geo, function (g) { | |
modelJSON.geoCoverage.push( | |
new EMLGeoCoverage({ | |
objectDOM: g, | |
parentModel: model, | |
}), | |
); | |
}); | |
} | |
if (taxon.length) { | |
modelJSON.taxonCoverage = []; | |
_.each(taxon, function (t) { | |
modelJSON.taxonCoverage.push( | |
new EMLTaxonCoverage({ | |
objectDOM: t, | |
parentModel: model, | |
}), | |
); | |
}); | |
} | |
} | |
//Parse EMLText modules | |
else if (_.contains(emlText, thisNode.localName)) { | |
if (typeof modelJSON[convertedName] == "undefined") | |
modelJSON[convertedName] = []; | |
modelJSON[convertedName].push( | |
new EMLText({ | |
objectDOM: thisNode, | |
parentModel: model, | |
}), | |
); | |
} else if (_.contains(emlMethods, thisNode.localName)) { | |
if (typeof modelJSON[thisNode.localName] === "undefined") | |
modelJSON[thisNode.localName] = []; | |
modelJSON[thisNode.localName] = new EMLMethods({ | |
objectDOM: thisNode, | |
parentModel: model, | |
}); | |
} | |
//Parse keywords | |
else if (thisNode.localName == "keywordset") { | |
//Start an array of keyword sets | |
if (typeof modelJSON["keywordSets"] == "undefined") | |
modelJSON["keywordSets"] = []; | |
modelJSON["keywordSets"].push( | |
new EMLKeywordSet({ | |
objectDOM: thisNode, | |
parentModel: model, | |
}), | |
); | |
} | |
//Parse intellectual rights | |
else if (thisNode.localName == "intellectualrights") { | |
var value = ""; | |
if ($(thisNode).children("para").length == 1) | |
value = $(thisNode).children("para").first().text().trim(); | |
else $(thisNode).text().trim(); | |
//If the value is one of our pre-defined options, then add it to the model | |
//if(_.contains(this.get("intellRightsOptions"), value)) | |
modelJSON["intellectualRights"] = value; | |
} | |
//Parse Entities | |
else if (_.contains(emlEntities, thisNode.localName)) { | |
//Start an array of Entities | |
if (typeof modelJSON["entities"] == "undefined") | |
modelJSON["entities"] = []; | |
//Create the model | |
var entityModel; | |
if (thisNode.localName == "otherentity") { | |
entityModel = new EMLOtherEntity( | |
{ | |
objectDOM: thisNode, | |
parentModel: model, | |
}, | |
{ | |
parse: true, | |
}, | |
); | |
} else if (thisNode.localName == "datatable") { | |
entityModel = new EMLDataTable( | |
{ | |
objectDOM: thisNode, | |
parentModel: model, | |
}, | |
{ | |
parse: true, | |
}, | |
); | |
} else { | |
entityModel = new EMLEntity( | |
{ | |
objectDOM: thisNode, | |
parentModel: model, | |
entityType: "application/octet-stream", | |
type: thisNode.localName, | |
}, | |
{ | |
parse: true, | |
}, | |
); | |
} | |
modelJSON["entities"].push(entityModel); | |
} | |
//Parse dataset-level annotations | |
else if (thisNode.localName === "annotation") { | |
if (!modelJSON["annotations"]) { | |
modelJSON["annotations"] = new EMLAnnotations(); | |
} | |
var annotationModel = new EMLAnnotation( | |
{ | |
objectDOM: thisNode, | |
}, | |
{ parse: true }, | |
); | |
modelJSON["annotations"].add(annotationModel); | |
} else { | |
//Is this a multi-valued field in EML? | |
if (Array.isArray(this.get(convertedName))) { | |
//If we already have a value for this field, then add this value to the array | |
if (Array.isArray(modelJSON[convertedName])) | |
modelJSON[convertedName].push(this.toJson(thisNode)); | |
//If it's the first value for this field, then create a new array | |
else modelJSON[convertedName] = [this.toJson(thisNode)]; | |
} else modelJSON[convertedName] = this.toJson(thisNode); | |
} | |
} | |
// 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; | |
}, | |
/* | |
* Retireves the model attributes and serializes into EML XML, to produce the new or modified EML document. | |
* Returns the EML XML as a string. | |
*/ | |
serialize: function () { | |
//Get the EML document | |
var xmlString = this.get("objectXML"), | |
html = $.parseHTML(xmlString), | |
eml = $(html).filter("eml\\:eml"), | |
datasetNode = $(eml).find("dataset"); | |
//Update the packageId on the eml node with the EML id | |
$(eml).attr("packageId", this.get("id")); | |
// Set id attribute on dataset node if needed | |
if (this.get("xmlID")) { | |
$(datasetNode).attr("id", this.get("xmlID")); | |
} | |
// Set schema version | |
$(eml).attr( | |
"xmlns:eml", | |
MetacatUI.appModel.get("editorSerializationFormat") || | |
"https://eml.ecoinformatics.org/eml-2.2.0", | |
); | |
// Set formatID | |
this.set( | |
"formatId", | |
MetacatUI.appModel.get("editorSerializationFormat") || | |
"https://eml.ecoinformatics.org/eml-2.2.0", | |
); | |
// Ensure xsi:schemaLocation has a value for the current format | |
eml = this.setSchemaLocation(eml); | |
var nodeNameMap = this.nodeNameMap(); | |
//Serialize the basic text fields | |
var basicText = ["alternateIdentifier", "title"]; | |
_.each( | |
basicText, | |
function (fieldName) { | |
var basicTextValues = this.get(fieldName); | |
if (!Array.isArray(basicTextValues)) | |
basicTextValues = [basicTextValues]; | |
// Remove existing nodes | |
datasetNode.children(fieldName.toLowerCase()).remove(); | |
// Create new nodes | |
var nodes = _.map(basicTextValues, function (value) { | |
if (value) { | |
var node = document.createElement(fieldName.toLowerCase()); | |
$(node).text(value); | |
return node; | |
} else { | |
return ""; | |
} | |
}); | |
var insertAfter = this.getEMLPosition(eml, fieldName.toLowerCase()); | |
if (insertAfter) { | |
insertAfter.after(nodes); | |
} else { | |
datasetNode.prepend(nodes); | |
} | |
}, | |
this, | |
); | |
// Serialize pubDate | |
// This one is special because it has a default behavior, unlike | |
// the others: When no pubDate is set, it should be set to | |
// the current year | |
var pubDate = this.get("pubDate"); | |
datasetNode.find("pubdate").remove(); | |
if (pubDate != null && pubDate.length > 0) { | |
var pubDateEl = document.createElement("pubdate"); | |
$(pubDateEl).text(pubDate); | |
this.getEMLPosition(eml, "pubdate").after(pubDateEl); | |
} | |
// Serialize the parts of EML that are eml-text modules | |
var textFields = ["abstract", "additionalInfo"]; | |
_.each( | |
textFields, | |
function (field) { | |
var fieldName = this.nodeNameMap()[field] || field; | |
// Get the EMLText model | |
var emlTextModels = Array.isArray(this.get(field)) | |
? this.get(field) | |
: [this.get(field)]; | |
if (!emlTextModels.length) return; | |
// Get the node from the EML doc | |
var nodes = datasetNode.find(fieldName); | |
// Update the DOMs for each model | |
_.each( | |
emlTextModels, | |
function (thisTextModel, i) { | |
//Don't serialize falsey values | |
if (!thisTextModel) return; | |
var node; | |
//Get the existing node or create a new one | |
if (nodes.length < i + 1) { | |
node = document.createElement(fieldName); | |
this.getEMLPosition(eml, fieldName).after(node); | |
} else { | |
node = nodes[i]; | |
} | |
$(node).html($(thisTextModel.updateDOM()).html()); | |
}, | |
this, | |
); | |
// Remove the extra nodes | |
this.removeExtraNodes(nodes, emlTextModels); | |
}, | |
this, | |
); | |
//Create a <coverage> XML node if there isn't one | |
if (datasetNode.children("coverage").length === 0) { | |
var coverageNode = $(document.createElement("coverage")), | |
coveragePosition = this.getEMLPosition(eml, "coverage"); | |
if (coveragePosition) coveragePosition.after(coverageNode); | |
else datasetNode.append(coverageNode); | |
} else { | |
var coverageNode = datasetNode.children("coverage").first(); | |
} | |
//Serialize the geographic coverage | |
if ( | |
typeof this.get("geoCoverage") !== "undefined" && | |
this.get("geoCoverage").length > 0 | |
) { | |
// Don't serialize if geoCoverage is invalid | |
var validCoverages = _.filter( | |
this.get("geoCoverage"), | |
function (cov) { | |
return cov.isValid(); | |
}, | |
); | |
//Get the existing geo coverage nodes from the EML | |
var existingGeoCov = datasetNode.find("geographiccoverage"); | |
//Update the DOM of each model | |
_.each( | |
validCoverages, | |
function (cov, position) { | |
//Update the existing node if it exists | |
if (existingGeoCov.length - 1 >= position) { | |
$(existingGeoCov[position]).replaceWith(cov.updateDOM()); | |
} | |
//Or, append new nodes | |
else { | |
var insertAfter = existingGeoCov.length | |
? datasetNode.find("geographiccoverage").last() | |
: null; | |
if (insertAfter) insertAfter.after(cov.updateDOM()); | |
else coverageNode.append(cov.updateDOM()); | |
} | |
}, | |
this, | |
); | |
//Remove existing taxon coverage nodes that don't have an accompanying model | |
this.removeExtraNodes( | |
datasetNode.find("geographiccoverage"), | |
validCoverages, | |
); | |
} else { | |
//If there are no geographic coverages, remove the nodes | |
coverageNode.children("geographiccoverage").remove(); | |
} | |
//Serialize the taxonomic coverage | |
if ( | |
typeof this.get("taxonCoverage") !== "undefined" && | |
this.get("taxonCoverage").length > 0 | |
) { | |
// Group the taxonomic coverage models into empty and non-empty | |
var sortedTaxonModels = _.groupBy( | |
this.get("taxonCoverage"), | |
function (t) { | |
if (_.flatten(t.get("taxonomicClassification")).length > 0) { | |
return "notEmpty"; | |
} else { | |
return "empty"; | |
} | |
}, | |
); | |
//Get the existing taxon coverage nodes from the EML | |
var existingTaxonCov = coverageNode.children("taxonomiccoverage"); | |
//Iterate over each taxon coverage and update it's DOM | |
if ( | |
sortedTaxonModels["notEmpty"] && | |
sortedTaxonModels["notEmpty"].length > 0 | |
) { | |
//Update the DOM of each model | |
_.each( | |
sortedTaxonModels["notEmpty"], | |
function (taxonCoverage, position) { | |
//Update the existing taxonCoverage node if it exists | |
if (existingTaxonCov.length - 1 >= position) { | |
$(existingTaxonCov[position]).replaceWith( | |
taxonCoverage.updateDOM(), | |
); | |
} | |
//Or, append new nodes | |
else { | |
coverageNode.append(taxonCoverage.updateDOM()); | |
} | |
}, | |
); | |
//Remove existing taxon coverage nodes that don't have an accompanying model | |
this.removeExtraNodes(existingTaxonCov, this.get("taxonCoverage")); | |
} | |
//If all the taxon coverages are empty, remove the parent taxonomicCoverage node | |
else if ( | |
!sortedTaxonModels["notEmpty"] || | |
sortedTaxonModels["notEmpty"].length == 0 | |
) { | |
existingTaxonCov.remove(); | |
} | |
} | |
//Serialize the temporal coverage | |
var existingTemporalCoverages = datasetNode.find("temporalcoverage"); | |
//Update the DOM of each model | |
_.each( | |
this.get("temporalCoverage"), | |
function (temporalCoverage, position) { | |
//Update the existing temporalCoverage node if it exists | |
if (existingTemporalCoverages.length - 1 >= position) { | |
$(existingTemporalCoverages[position]).replaceWith( | |
temporalCoverage.updateDOM(), | |
); | |
} | |
//Or, append new nodes | |
else { | |
coverageNode.append(temporalCoverage.updateDOM()); | |
} | |
}, | |
); | |
//Remove existing taxon coverage nodes that don't have an accompanying model | |
this.removeExtraNodes( | |
existingTemporalCoverages, | |
this.get("temporalCoverage"), | |
); | |
//Remove the temporal coverage if it is empty | |
if (!coverageNode.children("temporalcoverage").children().length) { | |
coverageNode.children("temporalcoverage").remove(); | |
} | |
//Remove the <coverage> node if it's empty | |
if (coverageNode.children().length == 0) { | |
coverageNode.remove(); | |
} | |
// Dataset-level annotations | |
datasetNode.children("annotation").remove(); | |
if (this.get("annotations")) { | |
this.get("annotations").each(function (annotation) { | |
if (annotation.isEmpty()) { | |
return; | |
} | |
var after = this.getEMLPosition(eml, "annotation"); | |
$(after).after(annotation.updateDOM()); | |
}, this); | |
//Since there is at least one annotation, the dataset node needs to have an id attribute. | |
datasetNode.attr("id", this.getUniqueEntityId(this)); | |
} | |
//If there is no creator, create one from the user | |
if (!this.get("creator").length) { | |
var party = new EMLParty({ parentModel: this, type: "creator" }); | |
party.createFromUser(); | |
this.set("creator", [party]); | |
} | |
//Serialize the creators | |
this.serializeParties(eml, "creator"); | |
//Serialize the metadata providers | |
this.serializeParties(eml, "metadataProvider"); | |
//Serialize the associated parties | |
this.serializeParties(eml, "associatedParty"); | |
//Serialize the contacts | |
this.serializeParties(eml, "contact"); | |
//Serialize the publishers | |
this.serializeParties(eml, "publisher"); | |
// Serialize methods | |
if (this.get("methods")) { | |
//If the methods model is empty, remove it from the EML | |
if (this.get("methods").isEmpty()) | |
datasetNode.find("methods").remove(); | |
else { | |
//Serialize the methods model | |
var methodsEl = this.get("methods").updateDOM(); | |
//If the methodsEl is an empty string or other falsey value, then remove the methods node | |
if (!methodsEl || !$(methodsEl).children().length) { | |
datasetNode.find("methods").remove(); | |
} else { | |
//Add the <methods> node to the EML | |
datasetNode.find("methods").detach(); | |
var insertAfter = this.getEMLPosition(eml, "methods"); | |
if (insertAfter) insertAfter.after(methodsEl); | |
else datasetNode.append(methodsEl); | |
} | |
} | |
} | |
//If there are no methods, then remove the methods nodes | |
else { | |
if (datasetNode.find("methods").length > 0) { | |
datasetNode.find("methods").remove(); | |
} | |
} | |
//Serialize the keywords | |
this.serializeKeywords(eml, "keywordSets"); | |
//Serialize the intellectual rights | |
if (this.get("intellectualRights")) { | |
if (datasetNode.find("intellectualRights").length) | |
datasetNode | |
.find("intellectualRights") | |
.html("<para>" + this.get("intellectualRights") + "</para>"); | |
else { | |
this.getEMLPosition(eml, "intellectualrights").after( | |
$(document.createElement("intellectualRights")).html( | |
"<para>" + this.get("intellectualRights") + "</para>", | |
), | |
); | |
} | |
} | |
// 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); | |
} | |
}); | |
} | |
//Detach the project elements from the DOM | |
if (datasetNode.find("project").length) { | |
datasetNode.find("project").detach(); | |
} | |
//If there is an EMLProject, update its DOM | |
if (this.get("project")) { | |
this.getEMLPosition(eml, "project").after( | |
this.get("project").updateDOM(), | |
); | |
} | |
//Get the existing taxon coverage nodes from the EML | |
var existingEntities = datasetNode.find( | |
"otherEntity, dataTable, spatialRaster, spatialVector, storedProcedure, view", | |
); | |
//Serialize the entities | |
_.each( | |
this.get("entities"), | |
function (entity, position) { | |
//Update the existing node if it exists | |
if (existingEntities.length - 1 >= position) { | |
//Remove the entity from the EML | |
$(existingEntities[position]).detach(); | |
//Insert it into the correct position | |
this.getEMLPosition(eml, entity.get("type").toLowerCase()).after( | |
entity.updateDOM(), | |
); | |
} | |
//Or, append new nodes | |
else { | |
//Inser the entity into the correct position | |
this.getEMLPosition(eml, entity.get("type").toLowerCase()).after( | |
entity.updateDOM(), | |
); | |
} | |
}, | |
this, | |
); | |
//Remove extra entities that have been removed | |
var numExtraEntities = | |
existingEntities.length - this.get("entities").length; | |
for ( | |
var i = existingEntities.length - numExtraEntities; | |
i < existingEntities.length; | |
i++ | |
) { | |
$(existingEntities)[i].remove(); | |
} | |
//Do a final check to make sure there are no duplicate ids in the EML | |
var elementsWithIDs = $(eml).find("[id]"), | |
//Get an array of all the ids in this EML doc | |
allIDs = _.map(elementsWithIDs, function (el) { | |
return $(el).attr("id"); | |
}); | |
//If there is at least one id in the EML... | |
if (allIDs && allIDs.length) { | |
//Boil the array down to just the unique values | |
var uniqueIDs = _.uniq(allIDs); | |
//If the unique array is shorter than the array of all ids, | |
// then there is a duplicate somewhere | |
if (uniqueIDs.length < allIDs.length) { | |
//For each element in the EML that has an id, | |
_.each(elementsWithIDs, function (el) { | |
//Get the id for this element | |
var id = $(el).attr("id"); | |
//If there is more than one element in the EML with this id, | |
if ($(eml).find("[id='" + id + "']").length > 1) { | |
//And if it is not a unit node, which we don't want to change, | |
if (!$(el).is("unit")) | |
//Then change the id attribute to a random uuid | |
$(el).attr("id", "urn-uuid-" + uuid.v4()); | |
} | |
}); | |
} | |
} | |
//Camel-case the XML | |
var emlString = ""; | |
_.each( | |
html, | |
function (rootEMLNode) { | |
emlString += this.formatXML(rootEMLNode); | |
}, | |
this, | |
); | |
return emlString; | |
}, | |
/* | |
* Given an EML DOM and party type, this function updated and/or adds the EMLParties to the EML | |
*/ | |
serializeParties: function (eml, type) { | |
//Remove the nodes from the EML for this party type | |
$(eml).children("dataset").children(type.toLowerCase()).remove(); | |
//Serialize each party of this type | |
_.each( | |
this.get(type), | |
function (party, i) { | |
//Get the last node of this type to insert after | |
var insertAfter = $(eml) | |
.children("dataset") | |
.children(type.toLowerCase()) | |
.last(); | |
//If there isn't a node found, find the EML position to insert after | |
if (!insertAfter.length) { | |
insertAfter = this.getEMLPosition(eml, type); | |
} | |
//Update the DOM of the EMLParty | |
var emlPartyDOM = party.updateDOM(); | |
//Make sure we don't insert empty EMLParty nodes into the EML | |
if ($(emlPartyDOM).children().length) { | |
//Insert the party DOM at the insert position | |
if (insertAfter && insertAfter.length) | |
insertAfter.after(emlPartyDOM); | |
//If an insert position still hasn't been found, then just append to the dataset node | |
else $(eml).find("dataset").append(emlPartyDOM); | |
} | |
}, | |
this, | |
); | |
//Create a certain parties from the current app user if none is given | |
if (type == "contact" && !this.get("contact").length) { | |
//Get the creators | |
var creators = this.get("creator"), | |
contacts = []; | |
_.each( | |
creators, | |
function (creator) { | |
//Clone the creator model and add it to the contacts array | |
var newModel = new EMLParty({ parentModel: this }); | |
newModel.set(creator.toJSON()); | |
newModel.set("type", type); | |
contacts.push(newModel); | |
}, | |
this, | |
); | |
this.set(type, contacts); | |
//Call this function again to serialize the new models | |
this.serializeParties(eml, type); | |
} | |
}, | |
serializeKeywords: function (eml) { | |
// Remove all existing keywordSets before appending | |
$(eml).find("dataset").find("keywordset").remove(); | |
if (this.get("keywordSets").length == 0) return; | |
// Create the new keywordSets nodes | |
var nodes = _.map(this.get("keywordSets"), function (kwd) { | |
return kwd.updateDOM(); | |
}); | |
this.getEMLPosition(eml, "keywordset").after(nodes); | |
}, | |
/* | |
* Remoes nodes from the EML that do not have an accompanying model | |
* (Were probably removed from the EML by the user during editing) | |
*/ | |
removeExtraNodes: function (nodes, models) { | |
// Remove the extra nodes | |
var extraNodes = nodes.length - models.length; | |
if (extraNodes > 0) { | |
for (var i = models.length; i < nodes.length; i++) { | |
$(nodes[i]).remove(); | |
} | |
} | |
}, | |
/* | |
* Saves the EML document to the server using the DataONE API | |
*/ | |
save: function (attributes, options) { | |
//Validate before we try anything else | |
if (!this.isValid()) { | |
this.trigger("invalid"); | |
this.trigger("cancelSave"); | |
return false; | |
} else { | |
this.trigger("valid"); | |
} | |
this.setFileName(); | |
//Set the upload transfer as in progress | |
this.set("uploadStatus", "p"); | |
//Reset the draftSaved attribute | |
this.set("draftSaved", false); | |
//Create the creator from the current user if none is provided | |
if (!this.get("creator").length) { | |
var party = new EMLParty({ parentModel: this, type: "creator" }); | |
party.createFromUser(); | |
this.set("creator", [party]); | |
} | |
//Create the contact from the current user if none is provided | |
if (!this.get("contact").length) { | |
var party = new EMLParty({ parentModel: this, type: "contact" }); | |
party.createFromUser(); | |
this.set("contact", [party]); | |
} | |
//If this is an existing object and there is no system metadata, retrieve it | |
if (!this.isNew() && !this.get("sysMetaXML")) { | |
var model = this; | |
//When the system metadata is fetched, try saving again | |
var fetchOptions = { | |
success: function (response) { | |
model.set(DataONEObject.prototype.parse.call(model, response)); | |
model.save(attributes, options); | |
}, | |
}; | |
//Fetch the system metadata now | |
this.fetchSystemMetadata(fetchOptions); | |
return; | |
} | |
//Create a FormData object to send data with our XHR | |
var formData = new FormData(); | |
try { | |
//Add the identifier to the XHR data | |
if (this.isNew()) { | |
formData.append("pid", this.get("id")); | |
} else { | |
//Create a new ID | |
this.updateID(); | |
//Add the ids to the form data | |
formData.append("newPid", this.get("id")); | |
formData.append("pid", this.get("oldPid")); | |
} | |
//Serialize the EML XML | |
var xml = this.serialize(); | |
var xmlBlob = new Blob([xml], { type: "application/xml" }); | |
//Get the size of the new EML XML | |
this.set("size", xmlBlob.size); | |
//Get the new checksum of the EML XML | |
var checksum = md5(xml); | |
this.set("checksum", checksum); | |
this.set("checksumAlgorithm", "MD5"); | |
//Create the system metadata XML | |
var sysMetaXML = this.serializeSysMeta(); | |
//Send the system metadata as a Blob | |
var sysMetaXMLBlob = new Blob([sysMetaXML], { | |
type: "application/xml", | |
}); | |
//Add the object XML and System Metadata XML to the form data | |
//Append the system metadata first, so we can take advantage of Metacat's streaming multipart handler | |
formData.append("sysmeta", sysMetaXMLBlob, "sysmeta"); | |
formData.append("object", xmlBlob); | |
} catch (error) { | |
//Reset the identifier since we didn't actually update the object | |
this.resetID(); | |
this.set("uploadStatus", "e"); | |
this.trigger("error"); | |
this.trigger("cancelSave"); | |
return false; | |
} | |
var model = this; | |
var saveOptions = options || {}; | |
_.extend( | |
saveOptions, | |
{ | |
data: formData, | |
cache: false, | |
contentType: false, | |
dataType: "text", | |
processData: false, | |
parse: false, | |
//Use the URL function to determine the URL | |
url: this.isNew() ? this.url() : this.url({ update: true }), | |
xhr: function () { | |
var xhr = new window.XMLHttpRequest(); | |
//Upload progress | |
xhr.upload.addEventListener( | |
"progress", | |
function (evt) { | |
if (evt.lengthComputable) { | |
var percentComplete = (evt.loaded / evt.total) * 100; | |
model.set("uploadProgress", percentComplete); | |
} | |
}, | |
false, | |
); | |
return xhr; | |
}, | |
success: function (model, response, xhr) { | |
model.set("numSaveAttempts", 0); | |
model.set("uploadStatus", "c"); | |
model.set("sysMetaXML", model.serializeSysMeta()); | |
model.set("oldPid", null); | |
model.fetch({ merge: true, systemMetadataOnly: true }); | |
model.trigger("successSaving", model); | |
}, | |
error: function (model, response, xhr) { | |
model.set("numSaveAttempts", model.get("numSaveAttempts") + 1); | |
var numSaveAttempts = model.get("numSaveAttempts"); | |
//Reset the identifier changes | |
model.resetID(); | |
if ( | |
numSaveAttempts < 3 && | |
(response.status == 408 || response.status == 0) | |
) { | |
//Try saving again in 10, 40, and 90 seconds | |
setTimeout( | |
function () { | |
model.save.call(model); | |
}, | |
numSaveAttempts * numSaveAttempts * 10000, | |
); | |
} else { | |
model.set("numSaveAttempts", 0); | |
//Get the error error information | |
var errorDOM = $($.parseHTML(response.responseText)), | |
errorContainer = errorDOM.filter("error"), | |
msgContainer = errorContainer.length | |
? errorContainer.find("description") | |
: errorDOM.not("style, title"), | |
errorMsg = msgContainer.length | |
? msgContainer.text() | |
: errorDOM; | |
//When there is no network connection (status == 0), there will be no response text | |
if (!errorMsg || response.status == 408 || response.status == 0) | |
errorMsg = | |
"There was a network issue that prevented your metadata from uploading. " + | |
"Make sure you are connected to a reliable internet connection."; | |
//Save the error message in the model | |
model.set("errorMessage", errorMsg); | |
//Set the model status as e for error | |
model.set("uploadStatus", "e"); | |
//Save the EML as a plain text file, until drafts are a supported feature | |
var copy = model.createTextCopy(); | |
//If the EML copy successfully saved, let the user know that there is a copy saved behind the scenes | |
model.listenToOnce(copy, "successSaving", function () { | |
model.set("draftSaved", true); | |
//Trigger the errorSaving event so other parts of the app know that the model failed to save | |
//And send the error message with it | |
model.trigger("errorSaving", errorMsg); | |
}); | |
//If the EML copy fails to save too, then just display the usual error message | |
model.listenToOnce(copy, "errorSaving", function () { | |
//Trigger the errorSaving event so other parts of the app know that the model failed to save | |
//And send the error message with it | |
model.trigger("errorSaving", errorMsg); | |
}); | |
//Save the EML plain text copy | |
copy.save(); | |
// Track the error | |
MetacatUI.analytics?.trackException( | |
`EML save error: ${errorMsg}, EML draft: ${copy.get("id")}`, | |
model.get("id"), | |
true, | |
); | |
} | |
}, | |
}, | |
MetacatUI.appUserModel.createAjaxSettings(), | |
); | |
return Backbone.Model.prototype.save.call( | |
this, | |
attributes, | |
saveOptions, | |
); | |
}, | |
/* | |
* Checks if this EML model has all the required values necessary to save to the server | |
*/ | |
validate: function () { | |
let errors = {}; | |
//A title is always required by EML | |
if (!this.get("title").length || !this.get("title")[0]) { | |
errors.title = "A title is required"; | |
} | |
// Validate the publication date | |
if (this.get("pubDate") != null) { | |
if (!this.isValidYearDate(this.get("pubDate"))) { | |
errors["pubDate"] = [ | |
"The value entered for publication date, '" + | |
this.get("pubDate") + | |
"' is not a valid value for this field. Enter with a year (e.g. 2017) or a date in the format YYYY-MM-DD.", | |
]; | |
} | |
} | |
// Validate the temporal coverage | |
errors.temporalCoverage = []; | |
//If temporal coverage is required and there aren't any, return an error | |
if ( | |
MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && | |
!this.get("temporalCoverage").length | |
) { | |
errors.temporalCoverage = [{ beginDate: "Provide a begin date." }]; | |
} | |
//If temporal coverage is required and they are all empty, return an error | |
else if ( | |
MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && | |
_.every(this.get("temporalCoverage"), function (tc) { | |
return tc.isEmpty(); | |
}) | |
) { | |
errors.temporalCoverage = [{ beginDate: "Provide a begin date." }]; | |
} | |
//If temporal coverage is not required, validate each one | |
else if ( | |
this.get("temporalCoverage").length || | |
(MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && | |
_.every(this.get("temporalCoverage"), function (tc) { | |
return tc.isEmpty(); | |
})) | |
) { | |
//Iterate over each temporal coverage and add it's validation errors | |
_.each(this.get("temporalCoverage"), function (temporalCoverage) { | |
if (!temporalCoverage.isValid() && !temporalCoverage.isEmpty()) { | |
errors.temporalCoverage.push(temporalCoverage.validationError); | |
} | |
}); | |
} | |
//Remove the temporalCoverage attribute if no errors were found | |
if (errors.temporalCoverage.length == 0) { | |
delete errors.temporalCoverage; | |
} | |
//Validate the EMLParty models | |
var partyTypes = [ | |
"associatedParty", | |
"contact", | |
"creator", | |
"metadataProvider", | |
"publisher", | |
]; | |
_.each( | |
partyTypes, | |
function (type) { | |
var people = this.get(type); | |
_.each( | |
people, | |
function (person, i) { | |
if (!person.isValid()) { | |
if (!errors[type]) errors[type] = [person.validationError]; | |
else errors[type].push(person.validationError); | |
} | |
}, | |
this, | |
); | |
}, | |
this, | |
); | |
//Validate the EMLGeoCoverage models | |
_.each( | |
this.get("geoCoverage"), | |
function (geoCoverageModel, i) { | |
if (!geoCoverageModel.isValid()) { | |
if (!errors.geoCoverage) | |
errors.geoCoverage = [geoCoverageModel.validationError]; | |
else errors.geoCoverage.push(geoCoverageModel.validationError); | |
} | |
}, | |
this, | |
); | |
//Validate the EMLTaxonCoverage model | |
var taxonModel = this.get("taxonCoverage")[0]; | |
if (!taxonModel.isEmpty() && !taxonModel.isValid()) { | |
errors = _.extend(errors, taxonModel.validationError); | |
} else if ( | |
taxonModel.isEmpty() && | |
this.get("taxonCoverage").length == 1 && | |
MetacatUI.appModel.get("emlEditorRequiredFields").taxonCoverage | |
) { | |
taxonModel.isValid(); | |
errors = _.extend(errors, taxonModel.validationError); | |
} | |
//Validate each EMLEntity model | |
_.each(this.get("entities"), function (entityModel) { | |
if (!entityModel.isValid()) { | |
if (!errors.entities) | |
errors.entities = [entityModel.validationError]; | |
else errors.entities.push(entityModel.validationError); | |
} | |
}); | |
//Validate the EML Methods | |
let emlMethods = this.get("methods"); | |
if (emlMethods) { | |
if (!emlMethods.isValid()) { | |
errors.methods = emlMethods.validationError; | |
} | |
} | |
// Validate the EMLAnnotation models | |
const annotations = this.get("annotations"); | |
const annotationErrors = annotations.validate(); | |
if (annotationErrors?.length) { | |
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 | |
for ([field, isRequired] of Object.entries( | |
MetacatUI.appModel.get("emlEditorRequiredFields"), | |
)) { | |
//If it's not required, then go to the next field | |
if (!isRequired) continue; | |
if (field == "alternateIdentifier") { | |
if ( | |
!this.get("alternateIdentifier").length || | |
_.every(this.get("alternateIdentifier"), function (altId) { | |
return altId.trim() == ""; | |
}) | |
) | |
errors.alternateIdentifier = | |
"At least one alternate identifier is required."; | |
} else if (field == "generalTaxonomicCoverage") { | |
if ( | |
!this.get("taxonCoverage").length || | |
!this.get("taxonCoverage")[0].get("generalTaxonomicCoverage") | |
) | |
errors.generalTaxonomicCoverage = | |
"Provide a description of the general taxonomic coverage of this data set."; | |
} else if (field == "geoCoverage") { | |
if (!this.get("geoCoverage").length) | |
errors.geoCoverage = "At least one location is required."; | |
} else if (field == "intellectualRights") { | |
if (!this.get("intellectualRights")) | |
errors.intellectualRights = | |
"Select usage rights for this data set."; | |
} else if (field == "studyExtentDescription") { | |
if ( | |
!this.get("methods") || | |
!this.get("methods").get("studyExtentDescription") | |
) | |
errors.studyExtentDescription = | |
"Provide a study extent description."; | |
} else if (field == "samplingDescription") { | |
if ( | |
!this.get("methods") || | |
!this.get("methods").get("samplingDescription") | |
) | |
errors.samplingDescription = "Provide a sampling description."; | |
} else if (field == "temporalCoverage") { | |
if (!this.get("temporalCoverage").length) | |
errors.temporalCoverage = | |
"Provide the date(s) for this data set."; | |
} else if (field == "taxonCoverage") { | |
if (!this.get("taxonCoverage").length) | |
errors.taxonCoverage = | |
"At least one taxa rank and value is required."; | |
} else if (field == "keywordSets") { | |
if (!this.get("keywordSets").length) | |
errors.keywordSets = "Provide at least one keyword."; | |
} | |
//The EMLMethods model will validate itself for required fields, but | |
// this is a rudimentary check to make sure the EMLMethods model was created | |
// in the first place | |
else if (field == "methods") { | |
if (!this.get("methods")) | |
errors.methods = "At least one method step is required."; | |
} else if (field == "funding") { | |
// Note: Checks for either the funding or award element. award | |
// element is checked by the project's objectDOM for now until | |
// EMLProject fully supports the award element | |
if ( | |
!this.get("project") || | |
!( | |
this.get("project").get("funding").length || | |
(this.get("project").get("objectDOM") && | |
this.get("project").get("objectDOM").querySelectorAll && | |
this.get("project").get("objectDOM").querySelectorAll("award") | |
.length > 0) | |
) | |
) | |
errors.funding = | |
"Provide at least one project funding number or name."; | |
} else if (field == "abstract") { | |
if (!this.get("abstract").length) | |
errors["abstract"] = "Provide an abstract."; | |
} else if (field == "dataSensitivity") { | |
if (!this.getDataSensitivity()) { | |
errors["dataSensitivity"] = | |
"Pick the category that best describes the level of sensitivity or restriction of the data."; | |
} | |
} | |
//If this is an EMLParty type, check that there is a party of this type in the model | |
else if ( | |
EMLParty.prototype.partyTypes | |
.map((t) => t.dataCategory) | |
.includes(field) | |
) { | |
//If this is an associatedParty role | |
if (EMLParty.prototype.defaults().roleOptions?.includes(field)) { | |
if ( | |
!this.get("associatedParty") | |
?.map((p) => p.get("roles")) | |
.flat() | |
.includes(field) | |
) { | |
errors[field] = | |
"Provide information about the people or organization(s) in the role: " + | |
EMLParty.prototype.partyTypes.find( | |
(t) => t.dataCategory == field, | |
)?.label; | |
} | |
} else if (!this.get(field)?.length) { | |
errors[field] = | |
"Provide information about the people or organization(s) in the role: " + | |
EMLParty.prototype.partyTypes.find( | |
(t) => t.dataCategory == field, | |
)?.label; | |
} | |
} else if (!this.get(field) || !this.get(field)?.length) { | |
errors[field] = "Provide a " + field + "."; | |
} | |
} | |
if (Object.keys(errors).length) return errors; | |
else { | |
return; | |
} | |
}, | |
/* Returns a boolean for whether the argument 'value' is a valid | |
value for EML's yearDate type which is used in a few places. | |
Note that this method considers a zero-length String to be valid | |
because the EML211.serialize() method will properly handle a null | |
or zero-length String by serializing out the current year. */ | |
isValidYearDate: function (value) { | |
return ( | |
value === "" || | |
/^\d{4}$/.test(value) || | |
/^\d{4}-\d{2}-\d{2}$/.test(value) | |
); | |
}, | |
/* | |
* Sends an AJAX request to fetch the system metadata for this EML object. | |
* Will not trigger a sync event since it does not use Backbone.Model.fetch | |
*/ | |
fetchSystemMetadata: function (options) { | |
if (!options) var options = {}; | |
else options = _.clone(options); | |
var model = this, | |
fetchOptions = _.extend( | |
{ | |
url: | |
MetacatUI.appModel.get("metaServiceUrl") + | |
encodeURIComponent(this.get("id")), | |
dataType: "text", | |
success: function (response) { | |
model.set(DataONEObject.prototype.parse.call(model, response)); | |
//Trigger a custom event that the sys meta was updated | |
model.trigger("sysMetaUpdated"); | |
}, | |
error: function () { | |
model.trigger("error"); | |
}, | |
}, | |
options, | |
); | |
//Add the authorization header and other AJAX settings | |
_.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings()); | |
$.ajax(fetchOptions); | |
}, | |
/* | |
* Returns the nofde in the given EML document that the given node type | |
* should be inserted after | |
* | |
* Returns false if either the node is not found in the and this should | |
* be handled by the caller. | |
*/ | |
getEMLPosition: function (eml, nodeName) { | |
var nodeOrder = this.get("nodeOrder"); | |
var position = _.indexOf(nodeOrder, nodeName.toLowerCase()); | |
if (position == -1) { | |
return false; | |
} | |
// Go through each node in the node list and find the position where this | |
// node will be inserted after | |
for (var i = position - 1; i >= 0; i--) { | |
if ($(eml).find("dataset").children(nodeOrder[i]).length) { | |
return $(eml).find("dataset").children(nodeOrder[i]).last(); | |
} | |
} | |
return false; | |
}, | |
/* | |
* Checks if this model has updates that need to be synced with the server. | |
*/ | |
hasUpdates: function () { | |
if (this.constructor.__super__.hasUpdates.call(this)) return true; | |
//If nothing else has been changed, then this object hasn't had any updates | |
return false; | |
}, | |
/* | |
Add an entity into the EML 2.1.1 object | |
*/ | |
addEntity: function (emlEntity, position) { | |
//Get the current list of entities | |
var currentEntities = this.get("entities"); | |
if (typeof position == "undefined" || position == -1) | |
currentEntities.push(emlEntity); | |
//Add the entity model to the entity array | |
else currentEntities.splice(position, 0, emlEntity); | |
this.trigger("change:entities"); | |
this.trickleUpChange(); | |
return this; | |
}, | |
/* | |
Remove an entity from the EML 2.1.1 object | |
*/ | |
removeEntity: function (emlEntity) { | |
if (!emlEntity || typeof emlEntity != "object") return; | |
//Get the current list of entities | |
var entities = this.get("entities"); | |
entities = _.without(entities, emlEntity); | |
this.set("entities", entities); | |
}, | |
/* | |
* Find the entity model for a given DataONEObject | |
*/ | |
getEntity: function (dataONEObj) { | |
//If an EMLEntity model has been found for this object before, then return it | |
if (dataONEObj.get("metadataEntity")) { | |
dataONEObj.get("metadataEntity").set("dataONEObject", dataONEObj); | |
return dataONEObj.get("metadataEntity"); | |
} | |
var entity = _.find( | |
this.get("entities"), | |
function (e) { | |
//Matches of the checksum or identifier are definite matches | |
if (e.get("xmlID") == dataONEObj.getXMLSafeID()) return true; | |
else if ( | |
e.get("physicalMD5Checksum") && | |
e.get("physicalMD5Checksum") == dataONEObj.get("checksum") && | |
dataONEObj.get("checksumAlgorithm").toUpperCase() == "MD5" | |
) | |
return true; | |
else if ( | |
e.get("downloadID") && | |
e.get("downloadID") == dataONEObj.get("id") | |
) | |
return true; | |
// Get the file name from the EML for this entity | |
var fileNameFromEML = | |
e.get("physicalObjectName") || e.get("entityName"); | |
// If the EML file name matches the DataONEObject file name | |
if ( | |
fileNameFromEML && | |
dataONEObj.get("fileName") && | |
(fileNameFromEML.toLowerCase() == | |
dataONEObj.get("fileName").toLowerCase() || | |
fileNameFromEML.replace(/ /g, "_").toLowerCase() == | |
dataONEObj.get("fileName").toLowerCase()) | |
) { | |
//Get an array of all the other entities in this EML | |
var otherEntities = _.without(this.get("entities"), e); | |
// If this entity name matches the dataone object file name, AND no other dataone object file name | |
// matches, then we can assume this is the entity element for this file. | |
var otherMatchingEntity = _.find( | |
otherEntities, | |
function (otherE) { | |
// Get the file name from the EML for the other entities | |
var otherFileNameFromEML = | |
otherE.get("physicalObjectName") || | |
otherE.get("entityName"); | |
// If the file names match, return true | |
if ( | |
otherFileNameFromEML == dataONEObj.get("fileName") || | |
otherFileNameFromEML.replace(/ /g, "_") == | |
dataONEObj.get("fileName") | |
) | |
return true; | |
}, | |
); | |
// If this entity's file name didn't match any other file names in the EML, | |
// then this entity is a match for the given dataONEObject | |
if (!otherMatchingEntity) return true; | |
} | |
}, | |
this, | |
); | |
//If we found an entity, give it an ID and return it | |
if (entity) { | |
//If this entity has been matched to another DataONEObject already, then don't match it again | |
if (entity.get("dataONEObject") == dataONEObj) { | |
return entity; | |
} | |
//If this entity has been matched to a different DataONEObject already, then don't match it again. | |
//i.e. We will not override existing entity<->DataONEObject pairings | |
else if (entity.get("dataONEObject")) { | |
return; | |
} else { | |
entity.set("dataONEObject", dataONEObj); | |
} | |
//Create an XML-safe ID and set it on the Entity model | |
var entityID = this.getUniqueEntityId(dataONEObj); | |
entity.set("xmlID", entityID); | |
//Save a reference to this entity so we don't have to refind it later | |
dataONEObj.set("metadataEntity", entity); | |
return entity; | |
} | |
//See if one data object is of this type in the package | |
var matchingTypes = _.filter(this.get("entities"), function (e) { | |
return ( | |
e.get("formatName") == | |
(dataONEObj.get("formatId") || dataONEObj.get("mediaType")) | |
); | |
}); | |
if (matchingTypes.length == 1) { | |
//Create an XML-safe ID and set it on the Entity model | |
matchingTypes[0].set("xmlID", dataONEObj.getXMLSafeID()); | |
return matchingTypes[0]; | |
} | |
//If this EML is in a DataPackage with only one other DataONEObject, | |
// and there is only one entity in the EML, then we can assume they are the same entity | |
if (this.get("entities").length == 1) { | |
if ( | |
this.get("collections")[0] && | |
this.get("collections")[0].type == "DataPackage" && | |
this.get("collections")[0].length == 2 && | |
_.contains(this.get("collections")[0].models, dataONEObj) | |
) { | |
return this.get("entities")[0]; | |
} | |
} | |
return false; | |
}, | |
createEntity: function (dataONEObject) { | |
// Add or append an entity to the parent's entity list | |
var entityModel = new EMLOtherEntity({ | |
entityName: dataONEObject.get("fileName"), | |
entityType: | |
dataONEObject.get("formatId") || | |
dataONEObject.get("mediaType") || | |
"application/octet-stream", | |
dataONEObject: dataONEObject, | |
parentModel: this, | |
xmlID: dataONEObject.getXMLSafeID(), | |
}); | |
this.addEntity(entityModel); | |
//If this DataONEObject fails to upload, remove the EML entity | |
this.listenTo(dataONEObject, "errorSaving", function () { | |
this.removeEntity(dataONEObject.get("metadataEntity")); | |
//Listen for a successful save so the entity can be added back | |
this.listenToOnce(dataONEObject, "successSaving", function () { | |
this.addEntity(dataONEObject.get("metadataEntity")); | |
}); | |
}); | |
}, | |
/* | |
* Creates an XML-safe identifier that is unique to this EML document, | |
* based on the given DataONEObject model. It is intended for EML entity nodes in particular. | |
* | |
* @param {DataONEObject} - a DataONEObject model that this EML documents | |
* @return {string} - an identifier string unique to this EML document | |
*/ | |
getUniqueEntityId: function (dataONEObject) { | |
var uniqueId = ""; | |
uniqueId = dataONEObject.getXMLSafeID(); | |
//Get the EML string, if there is one, to check if this id already exists | |
var emlString = this.get("objectXML"); | |
//If this id already exists in the EML... | |
if (emlString && emlString.indexOf(' id="' + uniqueId + '"')) { | |
//Create a random uuid to use instead | |
uniqueId = "urn-uuid-" + uuid.v4(); | |
} | |
return uniqueId; | |
}, | |
/* | |
* removeParty - removes the given EMLParty model from this EML211 model's attributes | |
*/ | |
removeParty: function (partyModel) { | |
//The list of attributes this EMLParty might be stored in | |
var possibleAttr = [ | |
"creator", | |
"contact", | |
"metadataProvider", | |
"publisher", | |
"associatedParty", | |
]; | |
// Iterate over each possible attribute | |
_.each( | |
possibleAttr, | |
function (attr) { | |
if (_.contains(this.get(attr), partyModel)) { | |
this.set(attr, _.without(this.get(attr), partyModel)); | |
} | |
}, | |
this, | |
); | |
}, | |
/** | |
* Attempt to move a party one index forward within its sibling models | |
* | |
* @param {EMLParty} partyModel: The EMLParty model we're moving | |
*/ | |
movePartyUp: function (partyModel) { | |
var possibleAttr = [ | |
"creator", | |
"contact", | |
"metadataProvider", | |
"publisher", | |
"associatedParty", | |
]; | |
// Iterate over each possible attribute | |
_.each( | |
possibleAttr, | |
function (attr) { | |
if (!_.contains(this.get(attr), partyModel)) { | |
return; | |
} | |
// Make a clone because we're going to use splice | |
var models = _.clone(this.get(attr)); | |
// Find the index of the model we're moving | |
var index = _.findIndex(models, function (m) { | |
return m === partyModel; | |
}); | |
if (index === 0) { | |
// Already first | |
return; | |
} | |
if (index === -1) { | |
// Couldn't find the model | |
return; | |
} | |
// Do the move using splice and update the model | |
models.splice(index - 1, 0, models.splice(index, 1)[0]); | |
this.set(attr, models); | |
this.trigger("change:" + attr); | |
}, | |
this, | |
); | |
}, | |
/** | |
* Attempt to move a party one index forward within its sibling models | |
* | |
* @param {EMLParty} partyModel: The EMLParty model we're moving | |
*/ | |
movePartyDown: function (partyModel) { | |
var possibleAttr = [ | |
"creator", | |
"contact", | |
"metadataProvider", | |
"publisher", | |
"associatedParty", | |
]; | |
// Iterate over each possible attribute | |
_.each( | |
possibleAttr, | |
function (attr) { | |
if (!_.contains(this.get(attr), partyModel)) { | |
return; | |
} | |
// Make a clone because we're going to use splice | |
var models = _.clone(this.get(attr)); | |
// Find the index of the model we're moving | |
var index = _.findIndex(models, function (m) { | |
return m === partyModel; | |
}); | |
if (index === -1) { | |
// Couldn't find the model | |
return; | |
} | |
// Figure out where to put the new model | |
// Leave it in the same place if the next index doesn't exist | |
// Move one forward if it does | |
var newIndex = models.length <= index + 1 ? index : index + 1; | |
// Do the move using splice and update the model | |
models.splice(newIndex, 0, models.splice(index, 1)[0]); | |
this.set(attr, models); | |
this.trigger("change:" + attr); | |
}, | |
this, | |
); | |
}, | |
/* | |
* Adds the given EMLParty model to this EML211 model in the | |
* appropriate role array in the given position | |
* | |
* @param {EMLParty} - The EMLParty model to add | |
* @param {number} - The position in the role array in which to insert this EMLParty | |
* @return {boolean} - Returns true if the EMLParty was successfully added, false if it was cancelled | |
*/ | |
addParty: function (partyModel, position) { | |
//If the EMLParty model is empty, don't add it to the EML211 model | |
if (partyModel.isEmpty()) return false; | |
//Get the role of this EMLParty | |
var role = partyModel.get("type") || "associatedParty"; | |
//If this model already contains this EMLParty, then exit | |
if (_.contains(this.get(role), partyModel)) return false; | |
if (typeof position == "undefined") { | |
this.get(role).push(partyModel); | |
} else { | |
this.get(role).splice(position, 0, partyModel); | |
} | |
this.trigger("change:" + role); | |
return true; | |
}, | |
/** | |
* getPartiesByType - Gets an array of EMLParty members that have a particular party type or role. | |
* @param {string} partyType - A string that represents either the role or the party type. For example, "contact", "creator", "principalInvestigator", etc. | |
* @since 2.15.0 | |
*/ | |
getPartiesByType: function (partyType) { | |
try { | |
if (!partyType) { | |
return false; | |
} | |
var associatedPartyTypes = new EMLParty().get("roleOptions"), | |
isAssociatedParty = associatedPartyTypes.includes(partyType), | |
parties = []; | |
// For "contact", "creator", "metadataProvider", "publisher", each party type has it's own | |
// array in the EML model | |
if (!isAssociatedParty) { | |
parties = this.get(partyType); | |
// For "custodianSteward", "principalInvestigator", "collaboratingPrincipalInvestigator", etc., | |
// party members are listed in the EML model's associated parties array. Each associated party's | |
// party type is indicated in the role attribute. | |
} else { | |
parties = _.filter( | |
this.get("associatedParty"), | |
function (associatedParty) { | |
return associatedParty.get("roles").includes(partyType); | |
}, | |
); | |
} | |
return parties; | |
} catch (error) { | |
console.log( | |
"Error trying to find a list of party members in an EML model by type. Error details: " + | |
error, | |
); | |
} | |
}, | |
createUnits: function () { | |
this.units.fetch(); | |
}, | |
/* Initialize the object XML for brand spankin' new EML objects */ | |
createXML: function () { | |
let emlSystem = MetacatUI.appModel.get("emlSystem"); | |
emlSystem = | |
!emlSystem || typeof emlSystem != "string" ? "knb" : emlSystem; | |
var xml = | |
'<eml:eml xmlns:eml="https://eml.ecoinformatics.org/eml-2.2.0"></eml:eml>', | |
eml = $($.parseHTML(xml)); | |
// Set base attributes | |
eml.attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); | |
eml.attr("xmlns:stmml", "http://www.xml-cml.org/schema/stmml-1.1"); | |
eml.attr( | |
"xsi:schemaLocation", | |
"https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd", | |
); | |
eml.attr("packageId", this.get("id")); | |
eml.attr("system", emlSystem); | |
// Add the dataset | |
eml.append(document.createElement("dataset")); | |
eml.find("dataset").append(document.createElement("title")); | |
var emlString = $(document.createElement("div")) | |
.append(eml.clone()) | |
.html(); | |
return emlString; | |
}, | |
/* | |
Replace elements named "source" with "sourced" due to limitations | |
with using $.parseHTML() rather than $.parseXML() | |
@param xmlString The XML string to make the replacement in | |
*/ | |
cleanUpXML: function (xmlString) { | |
xmlString.replace("<source>", "<sourced>"); | |
xmlString.replace("</source>", "</sourced>"); | |
return xmlString; | |
}, | |
createTextCopy: function () { | |
var emlDraftText = | |
"EML draft for " + | |
this.get("id") + | |
"(" + | |
this.get("title") + | |
") by " + | |
MetacatUI.appUserModel.get("firstName") + | |
" " + | |
MetacatUI.appUserModel.get("lastName"); | |
if (this.get("uploadStatus") == "e" && this.get("errorMessage")) { | |
emlDraftText += | |
". This EML had the following save error: `" + | |
this.get("errorMessage") + | |
"` "; | |
} else { | |
emlDraftText += ": "; | |
} | |
emlDraftText += this.serialize(); | |
var plainTextEML = new DataONEObject({ | |
formatId: "text/plain", | |
fileName: | |
"eml_draft_" + | |
(MetacatUI.appUserModel.get("lastName") || "") + | |
".txt", | |
uploadFile: new Blob([emlDraftText], { type: "plain/text" }), | |
synced: true, | |
}); | |
return plainTextEML; | |
}, | |
/* | |
* Cleans up the given text so that it is XML-valid by escaping reserved characters, trimming white space, etc. | |
* | |
* @param {string} textString - The string to clean up | |
* @return {string} - The cleaned up string | |
*/ | |
cleanXMLText: function (textString) { | |
if (typeof textString != "string") return; | |
textString = textString.trim(); | |
//Check for XML/HTML elements | |
_.each(textString.match(/<\s*[^>]*>/g), function (xmlNode) { | |
//Encode <, >, and </ substrings | |
var tagName = xmlNode.replace(/>/g, ">"); | |
tagName = tagName.replace(/</g, "<"); | |
//Replace the xmlNode in the full text string | |
textString = textString.replace(xmlNode, tagName); | |
}); | |
//Remove Unicode characters that are not valid XML characters | |
//Create a regular expression that matches any character that is not a valid XML character | |
// (see https://www.w3.org/TR/xml/#charsets) | |
var invalidCharsRegEx = | |
/[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD]/g; | |
textString = textString.replace(invalidCharsRegEx, ""); | |
return textString; | |
}, | |
/* | |
Dereference "reference" elements and replace them with a cloned copy | |
of the referenced content | |
@param xmlString The XML string with reference elements to transform | |
*/ | |
dereference: function (xmlString) { | |
var referencesList; // the array of references elements in the document | |
var referencedID; // The id of the referenced element | |
var referencesParentEl; // The parent of the given references element | |
var referencedEl; // The referenced DOM to be copied | |
var xmlDOM = $.parseXML(xmlString); | |
referencesList = xmlDOM.getElementsByTagName("references"); | |
if (referencesList.length) { | |
// Process each references elements | |
_.each( | |
referencesList, | |
function (referencesEl, index, referencesList) { | |
// Can't rely on the passed referencesEl since the list length changes | |
// because of the remove() below. Reuse referencesList[0] for every item: | |
// referencedID = $(referencesEl).text(); // doesn't work | |
referencesEl = referencesList[0]; | |
referencedID = $(referencesEl).text(); | |
referencesParentEl = $(referencesEl).parent()[0]; | |
if (typeof referencedID !== "undefined" && referencedID != "") { | |
referencedEl = xmlDOM.getElementById(referencedID); | |
if (typeof referencedEl != "undefined") { | |
// Clone the referenced element and replace the references element | |
var referencedClone = $(referencedEl).clone()[0]; | |
$(referencesParentEl) | |
.children(referencesEl.localName) | |
.replaceWith($(referencedClone).children()); | |
//$(referencesParentEl).append($(referencedClone).children()); | |
$(referencesParentEl).attr("id", DataONEObject.generateId()); | |
} | |
} | |
}, | |
xmlDOM, | |
); | |
} | |
return new XMLSerializer().serializeToString(xmlDOM); | |
}, | |
/* | |
* Uses the EML `title` to set the `fileName` attribute on this model. | |
*/ | |
setFileName: function () { | |
var title = ""; | |
// Get the title from the metadata | |
if (Array.isArray(this.get("title"))) { | |
title = this.get("title")[0]; | |
} else if (typeof this.get("title") == "string") { | |
title = this.get("title"); | |
} | |
//Max title length | |
var maxLength = 50; | |
//trim the string to the maximum length | |
var trimmedTitle = title.trim().substr(0, maxLength); | |
//re-trim if we are in the middle of a word | |
if (trimmedTitle.indexOf(" ") > -1) { | |
trimmedTitle = trimmedTitle.substr( | |
0, | |
Math.min(trimmedTitle.length, trimmedTitle.lastIndexOf(" ")), | |
); | |
} | |
//Replace all non alphanumeric characters with underscores | |
// and make sure there isn't more than one underscore in a row | |
trimmedTitle = trimmedTitle | |
.replace(/[^a-zA-Z0-9]/g, "_") | |
.replace(/_{2,}/g, "_"); | |
//Set the fileName on the model | |
this.set("fileName", trimmedTitle + ".xml"); | |
}, | |
trickleUpChange: function () { | |
if ( | |
!MetacatUI.rootDataPackage || | |
!MetacatUI.rootDataPackage.packageModel | |
) | |
return; | |
//Mark the package as changed | |
MetacatUI.rootDataPackage.packageModel.set("changed", true); | |
}, | |
/** | |
* Sets the xsi:schemaLocation attribute on the passed-in Element | |
* depending on the application configuration. | |
* | |
* @param {Element} eml: The root eml:eml element to modify | |
* @return {Element} The element, possibly modified | |
*/ | |
setSchemaLocation: function (eml) { | |
if (!MetacatUI || !MetacatUI.appModel) { | |
return eml; | |
} | |
var current = $(eml).attr("xsi:schemaLocation"), | |
format = MetacatUI.appModel.get("editorSerializationFormat"), | |
location = MetacatUI.appModel.get("editorSchemaLocation"); | |
// Return now if we can't do anything anyway | |
if (!format || !location) { | |
return eml; | |
} | |
// Simply add if the attribute isn't present to begin with | |
if (!current || typeof current !== "string") { | |
$(eml).attr("xsi:schemaLocation", format + " " + location); | |
return eml; | |
} | |
// Don't append if it's already present | |
if (current.indexOf(format) >= 0) { | |
return eml; | |
} | |
$(eml).attr("xsi:schemaLocation", current + " " + location); | |
return eml; | |
}, | |
createID: function () { | |
this.set("xmlID", uuid.v4()); | |
}, | |
/** | |
* Creates and adds an {@link EMLAnnotation} to this EML211 model with the given annotation data in JSON form. | |
* @param {object} annotationData The attribute data to set on the new {@link EMLAnnotation}. See {@link EMLAnnotation#defaults} for | |
* details on what attributes can be passed to the EMLAnnotation. In addition, there is an `elementName` property. | |
* @property {string} [annotationData.elementName] The name of the EML Element that this | |
annotation should be applied to. e.g. dataset, entity, attribute. Defaults to `dataset`. NOTE: Right now only dataset annotations are supported until | |
more annotation editing is added to the EML Editor. | |
* @property {Boolean} [annotationData.allowDuplicates] If false, this annotation will replace all annotations already set with the same propertyURI. | |
* By default, more than one annotation with a given propertyURI can be added (defaults to true) | |
*/ | |
addAnnotation: function (annotationData) { | |
try { | |
if (!annotationData || typeof annotationData != "object") { | |
return; | |
} | |
//If no element name is provided, default to the dataset element. | |
let elementName = ""; | |
if (!annotationData.elementName) { | |
elementName = "dataset"; | |
} else { | |
elementName = annotationData.elementName; | |
} | |
//Remove the elementName property so it isn't set on the EMLAnnotation model later. | |
delete annotationData.elementName; | |
//Check if duplicates are allowed | |
let allowDuplicates = annotationData.allowDuplicates; | |
delete annotationData.allowDuplicates; | |
//Create a new EMLAnnotation model | |
let annotation = new EMLAnnotation(annotationData); | |
//Update annotations set on the dataset element | |
if (elementName == "dataset") { | |
let annotations = this.get("annotations"); | |
//If the current annotations set on the EML model are not in Array form, change it to an array | |
if (!annotations) { | |
annotations = new EMLAnnotations(); | |
} | |
if (allowDuplicates === false) { | |
//Add the EMLAnnotation to the collection, making sure to remove duplicates first | |
annotations.replaceDuplicateWith(annotation); | |
} else { | |
annotations.add(annotation); | |
} | |
//Set the annotations and force the change to be recognized by the model | |
this.set("annotations", annotations, { silent: true }); | |
this.handleChange(this, { force: true }); | |
} else { | |
/** @todo Add annotation support for other EML Elements */ | |
} | |
} catch (e) { | |
console.error("Could not add Annotation to the EML: ", e); | |
} | |
}, | |
/** | |
* Finds annotations that are of the `data sensitivity` property from the NCEAS SENSO ontology. | |
* Returns undefined if none are found. This function returns EMLAnnotation models because the data | |
* sensitivity is stored in the EML Model as EMLAnnotations and added to EML as semantic annotations. | |
* @returns {EMLAnnotation[]|undefined} | |
*/ | |
getDataSensitivity: function () { | |
try { | |
let annotations = this.get("annotations"); | |
if (annotations) { | |
let found = annotations.where({ | |
propertyURI: this.get("dataSensitivityPropertyURI"), | |
}); | |
if (!found || !found.length) { | |
return; | |
} else { | |
return found; | |
} | |
} else { | |
return; | |
} | |
} catch (e) { | |
console.error("Failed to get Data Sensitivity from EML model: ", e); | |
return; | |
} | |
}, | |
}, | |
); | |
return EML211; | |
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.
metacatui/src/js/models/metadata/eml211/EML211.js
Lines 53 to 2668 in 979d2a5
var EML211 = ScienceMetadata.extend( | |
/** @lends EML211.prototype */ { | |
type: "EML", | |
defaults: function () { | |
return _.extend(ScienceMetadata.prototype.defaults(), { | |
id: "urn:uuid:" + uuid.v4(), | |
formatId: "https://eml.ecoinformatics.org/eml-2.2.0", | |
objectXML: null, | |
isEditable: false, | |
alternateIdentifier: [], | |
shortName: null, | |
title: [], | |
creator: [], // array of EMLParty objects | |
metadataProvider: [], // array of EMLParty objects | |
associatedParty: [], // array of EMLParty objects | |
contact: [], // array of EMLParty objects | |
publisher: [], // array of EMLParty objects | |
pubDate: null, | |
language: null, | |
series: null, | |
abstract: [], //array of EMLText objects | |
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 | |
geoCoverage: [], //an array for EMLGeoCoverages | |
temporalCoverage: [], //an array of EMLTempCoverage models | |
taxonCoverage: [], //an array of EMLTaxonCoverages | |
purpose: [], | |
entities: [], //An array of EMLEntities | |
pubplace: null, | |
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: [ | |
"alternateidentifier", | |
"shortname", | |
"title", | |
"creator", | |
"metadataprovider", | |
"associatedparty", | |
"pubdate", | |
"language", | |
"series", | |
"abstract", | |
"keywordset", | |
"additionalinfo", | |
"intellectualrights", | |
"licensed", | |
"distribution", | |
"coverage", | |
"annotation", | |
"purpose", | |
"introduction", | |
"gettingstarted", | |
"acknowledgements", | |
"maintenance", | |
"contact", | |
"publisher", | |
"pubplace", | |
"methods", | |
"project", | |
"datatable", | |
"spatialraster", | |
"spatialvector", | |
"storedprocedure", | |
"view", | |
"otherentity", | |
"referencepublications", | |
"usagecitations", | |
"literaturecited", | |
], | |
}); | |
}, | |
units: new Units(), | |
initialize: function (attributes) { | |
// Call initialize for the super class | |
ScienceMetadata.prototype.initialize.call(this, attributes); | |
// EML211-specific init goes here | |
// this.set("objectXML", this.createXML()); | |
this.parse(this.createXML()); | |
this.on("sync", function () { | |
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(); | |
}, | |
url: function (options) { | |
var identifier; | |
if (options && options.update) { | |
identifier = this.get("oldPid") || this.get("seriesid"); | |
} else { | |
identifier = this.get("id") || this.get("seriesid"); | |
} | |
return ( | |
MetacatUI.appModel.get("objectServiceUrl") + | |
encodeURIComponent(identifier) | |
); | |
}, | |
/** | |
* 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; | |
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() | |
*/ | |
nodeNameMap: function () { | |
return _.extend( | |
this.constructor.__super__.nodeNameMap(), | |
EMLDistribution.prototype.nodeNameMap(), | |
EMLGeoCoverage.prototype.nodeNameMap(), | |
EMLKeywordSet.prototype.nodeNameMap(), | |
EMLParty.prototype.nodeNameMap(), | |
EMLProject.prototype.nodeNameMap(), | |
EMLTaxonCoverage.prototype.nodeNameMap(), | |
EMLTemporalCoverage.prototype.nodeNameMap(), | |
EMLMethods.prototype.nodeNameMap(), | |
{ | |
accuracyreport: "accuracyReport", | |
actionlist: "actionList", | |
additionalclassifications: "additionalClassifications", | |
additionalinfo: "additionalInfo", | |
additionallinks: "additionalLinks", | |
additionalmetadata: "additionalMetadata", | |
allowfirst: "allowFirst", | |
alternateidentifier: "alternateIdentifier", | |
altitudedatumname: "altitudeDatumName", | |
altitudedistanceunits: "altitudeDistanceUnits", | |
altituderesolution: "altitudeResolution", | |
altitudeencodingmethod: "altitudeEncodingMethod", | |
altitudesysdef: "altitudeSysDef", | |
asneeded: "asNeeded", | |
associatedparty: "associatedParty", | |
attributeaccuracyexplanation: "attributeAccuracyExplanation", | |
attributeaccuracyreport: "attributeAccuracyReport", | |
attributeaccuracyvalue: "attributeAccuracyValue", | |
attributedefinition: "attributeDefinition", | |
attributelabel: "attributeLabel", | |
attributelist: "attributeList", | |
attributename: "attributeName", | |
attributeorientation: "attributeOrientation", | |
attributereference: "attributeReference", | |
awardnumber: "awardNumber", | |
awardurl: "awardUrl", | |
audiovisual: "audioVisual", | |
authsystem: "authSystem", | |
banddescription: "bandDescription", | |
bilinearfit: "bilinearFit", | |
binaryrasterformat: "binaryRasterFormat", | |
blockedmembernode: "blockedMemberNode", | |
booktitle: "bookTitle", | |
cameracalibrationinformationavailability: | |
"cameraCalibrationInformationAvailability", | |
casesensitive: "caseSensitive", | |
cellgeometry: "cellGeometry", | |
cellsizexdirection: "cellSizeXDirection", | |
cellsizeydirection: "cellSizeYDirection", | |
changehistory: "changeHistory", | |
changedate: "changeDate", | |
changescope: "changeScope", | |
chapternumber: "chapterNumber", | |
characterencoding: "characterEncoding", | |
checkcondition: "checkCondition", | |
checkconstraint: "checkConstraint", | |
childoccurences: "childOccurences", | |
citableclassificationsystem: "citableClassificationSystem", | |
cloudcoverpercentage: "cloudCoverPercentage", | |
codedefinition: "codeDefinition", | |
codeexplanation: "codeExplanation", | |
codesetname: "codesetName", | |
codeseturl: "codesetURL", | |
collapsedelimiters: "collapseDelimiters", | |
communicationtype: "communicationType", | |
compressiongenerationquality: "compressionGenerationQuality", | |
compressionmethod: "compressionMethod", | |
conferencedate: "conferenceDate", | |
conferencelocation: "conferenceLocation", | |
conferencename: "conferenceName", | |
conferenceproceedings: "conferenceProceedings", | |
constraintdescription: "constraintDescription", | |
constraintname: "constraintName", | |
constanttosi: "constantToSI", | |
controlpoint: "controlPoint", | |
cornerpoint: "cornerPoint", | |
customunit: "customUnit", | |
dataformat: "dataFormat", | |
datasetgpolygon: "datasetGPolygon", | |
datasetgpolygonoutergring: "datasetGPolygonOuterGRing", | |
datasetgpolygonexclusiongring: "datasetGPolygonExclusionGRing", | |
datatable: "dataTable", | |
datatype: "dataType", | |
datetime: "dateTime", | |
datetimedomain: "dateTimeDomain", | |
datetimeprecision: "dateTimePrecision", | |
defaultvalue: "defaultValue", | |
definitionattributereference: "definitionAttributeReference", | |
denomflatratio: "denomFlatRatio", | |
depthsysdef: "depthSysDef", | |
depthdatumname: "depthDatumName", | |
depthdistanceunits: "depthDistanceUnits", | |
depthencodingmethod: "depthEncodingMethod", | |
depthresolution: "depthResolution", | |
descriptorvalue: "descriptorValue", | |
dictref: "dictRef", | |
diskusage: "diskUsage", | |
domainDescription: "domainDescription", | |
editedbook: "editedBook", | |
encodingmethod: "encodingMethod", | |
endcondition: "endCondition", | |
entitycodelist: "entityCodeList", | |
entitydescription: "entityDescription", | |
entityname: "entityName", | |
entityreference: "entityReference", | |
entitytype: "entityType", | |
enumerateddomain: "enumeratedDomain", | |
errorbasis: "errorBasis", | |
errorvalues: "errorValues", | |
externalcodeset: "externalCodeSet", | |
externallydefinedformat: "externallyDefinedFormat", | |
fielddelimiter: "fieldDelimiter", | |
fieldstartcolumn: "fieldStartColumn", | |
fieldwidth: "fieldWidth", | |
filmdistortioninformationavailability: | |
"filmDistortionInformationAvailability", | |
foreignkey: "foreignKey", | |
formatname: "formatName", | |
formatstring: "formatString", | |
formatversion: "formatVersion", | |
fractiondigits: "fractionDigits", | |
fundername: "funderName", | |
funderidentifier: "funderIdentifier", | |
gettingstarted: "gettingStarted", | |
gring: "gRing", | |
gringpoint: "gRingPoint", | |
gringlatitude: "gRingLatitude", | |
gringlongitude: "gRingLongitude", | |
geogcoordsys: "geogCoordSys", | |
geometricobjectcount: "geometricObjectCount", | |
georeferenceinfo: "georeferenceInfo", | |
highwavelength: "highWavelength", | |
horizontalaccuracy: "horizontalAccuracy", | |
horizcoordsysdef: "horizCoordSysDef", | |
horizcoordsysname: "horizCoordSysName", | |
identifiername: "identifierName", | |
illuminationazimuthangle: "illuminationAzimuthAngle", | |
illuminationelevationangle: "illuminationElevationAngle", | |
imagingcondition: "imagingCondition", | |
imagequalitycode: "imageQualityCode", | |
imageorientationangle: "imageOrientationAngle", | |
intellectualrights: "intellectualRights", | |
imagedescription: "imageDescription", | |
isbn: "ISBN", | |
issn: "ISSN", | |
joincondition: "joinCondition", | |
keywordtype: "keywordType", | |
languagevalue: "LanguageValue", | |
languagecodestandard: "LanguageCodeStandard", | |
lensdistortioninformationavailability: | |
"lensDistortionInformationAvailability", | |
licensename: "licenseName", | |
licenseurl: "licenseURL", | |
linenumber: "lineNumber", | |
literalcharacter: "literalCharacter", | |
literallayout: "literalLayout", | |
literaturecited: "literatureCited", | |
lowwavelength: "lowWaveLength", | |
machineprocessor: "machineProcessor", | |
maintenanceupdatefrequency: "maintenanceUpdateFrequency", | |
matrixtype: "matrixType", | |
maxexclusive: "maxExclusive", | |
maxinclusive: "maxInclusive", | |
maxlength: "maxLength", | |
maxrecordlength: "maxRecordLength", | |
maxvalues: "maxValues", | |
measurementscale: "measurementScale", | |
metadatalist: "metadataList", | |
methodstep: "methodStep", | |
minexclusive: "minExclusive", | |
mininclusive: "minInclusive", | |
minlength: "minLength", | |
minvalues: "minValues", | |
missingvaluecode: "missingValueCode", | |
moduledocs: "moduleDocs", | |
modulename: "moduleName", | |
moduledescription: "moduleDescription", | |
multiband: "multiBand", | |
multipliertosi: "multiplierToSI", | |
nonnumericdomain: "nonNumericDomain", | |
notnullconstraint: "notNullConstraint", | |
notplanned: "notPlanned", | |
numberofbands: "numberOfBands", | |
numbertype: "numberType", | |
numericdomain: "numericDomain", | |
numfooterlines: "numFooterLines", | |
numheaderlines: "numHeaderLines", | |
numberofrecords: "numberOfRecords", | |
numberofvolumes: "numberOfVolumes", | |
numphysicallinesperrecord: "numPhysicalLinesPerRecord", | |
objectname: "objectName", | |
oldvalue: "oldValue", | |
operatingsystem: "operatingSystem", | |
orderattributereference: "orderAttributeReference", | |
originalpublication: "originalPublication", | |
otherentity: "otherEntity", | |
othermaintenanceperiod: "otherMaintenancePeriod", | |
parameterdefinition: "parameterDefinition", | |
packageid: "packageId", | |
pagerange: "pageRange", | |
parentoccurences: "parentOccurences", | |
parentsi: "parentSI", | |
peakresponse: "peakResponse", | |
personalcommunication: "personalCommunication", | |
physicallinedelimiter: "physicalLineDelimiter", | |
pointinpixel: "pointInPixel", | |
preferredmembernode: "preferredMemberNode", | |
preprocessingtypecode: "preProcessingTypeCode", | |
primarykey: "primaryKey", | |
primemeridian: "primeMeridian", | |
proceduralstep: "proceduralStep", | |
programminglanguage: "programmingLanguage", | |
projcoordsys: "projCoordSys", | |
projectionlist: "projectionList", | |
propertyuri: "propertyURI", | |
pubdate: "pubDate", | |
pubplace: "pubPlace", | |
publicationplace: "publicationPlace", | |
quantitativeaccuracyreport: "quantitativeAccuracyReport", | |
quantitativeaccuracyvalue: "quantitativeAccuracyValue", | |
quantitativeaccuracymethod: "quantitativeAccuracyMethod", | |
quantitativeattributeaccuracyassessment: | |
"quantitativeAttributeAccuracyAssessment", | |
querystatement: "queryStatement", | |
quotecharacter: "quoteCharacter", | |
radiometricdataavailability: "radiometricDataAvailability", | |
rasterorigin: "rasterOrigin", | |
recommendedunits: "recommendedUnits", | |
recommendedusage: "recommendedUsage", | |
referencedkey: "referencedKey", | |
referencetype: "referenceType", | |
relatedentry: "relatedEntry", | |
relationshiptype: "relationshipType", | |
reportnumber: "reportNumber", | |
reprintedition: "reprintEdition", | |
researchproject: "researchProject", | |
researchtopic: "researchTopic", | |
recorddelimiter: "recordDelimiter", | |
referencepublication: "referencePublication", | |
revieweditem: "reviewedItem", | |
rowcolumnorientation: "rowColumnOrientation", | |
runtimememoryusage: "runtimeMemoryUsage", | |
samplingdescription: "samplingDescription", | |
scalefactor: "scaleFactor", | |
sequenceidentifier: "sequenceIdentifier", | |
semiaxismajor: "semiAxisMajor", | |
shortname: "shortName", | |
simpledelimited: "simpleDelimited", | |
spatialraster: "spatialRaster", | |
spatialreference: "spatialReference", | |
spatialvector: "spatialVector", | |
standalone: "standAlone", | |
standardunit: "standardUnit", | |
startcondition: "startCondition", | |
studyareadescription: "studyAreaDescription", | |
storagetype: "storageType", | |
studyextent: "studyExtent", | |
studytype: "studyType", | |
textdelimited: "textDelimited", | |
textdomain: "textDomain", | |
textfixed: "textFixed", | |
textformat: "textFormat", | |
topologylevel: "topologyLevel", | |
tonegradation: "toneGradation", | |
totaldigits: "totalDigits", | |
totalfigures: "totalFigures", | |
totalpages: "totalPages", | |
totaltables: "totalTables", | |
triangulationindicator: "triangulationIndicator", | |
typesystem: "typeSystem", | |
uniquekey: "uniqueKey", | |
unittype: "unitType", | |
unitlist: "unitList", | |
usagecitation: "usageCitation", | |
valueuri: "valueURI", | |
valueattributereference: "valueAttributeReference", | |
verticalaccuracy: "verticalAccuracy", | |
vertcoordsys: "vertCoordSys", | |
virtualmachine: "virtualMachine", | |
wavelengthunits: "waveLengthUnits", | |
whitespace: "whiteSpace", | |
xintercept: "xIntercept", | |
xcoordinate: "xCoordinate", | |
"xsi:schemalocation": "xsi:schemaLocation", | |
xslope: "xSlope", | |
ycoordinate: "yCoordinate", | |
yintercept: "yIntercept", | |
yslope: "ySlope", | |
}, | |
); | |
}, | |
/** | |
* Fetch the EML from the MN object service | |
* @param {object} [options] - A set of options for this fetch() | |
* @property {boolean} [options.systemMetadataOnly=false] - If true, only the system metadata will be fetched. | |
* If false, the system metadata AND EML document will be fetched. | |
*/ | |
fetch: function (options) { | |
if (!options) var options = {}; | |
//Add the authorization header and other AJAX settings | |
_.extend(options, MetacatUI.appUserModel.createAjaxSettings(), { | |
dataType: "text", | |
}); | |
// Merge the system metadata into the object first | |
_.extend(options, { merge: true }); | |
this.fetchSystemMetadata(options); | |
//If we are retrieving system metadata only, then exit now | |
if (options.systemMetadataOnly) return; | |
//Call Backbone.Model.fetch to retrieve the info | |
return Backbone.Model.prototype.fetch.call(this, options); | |
}, | |
/* | |
Deserialize an EML 2.1.1 XML document | |
*/ | |
parse: function (response) { | |
// Save a reference to this model for use in setting the | |
// parentModel inside anonymous functions | |
var model = this; | |
//If the response is XML | |
if (typeof response == "string" && response.indexOf("<") == 0) { | |
//Look for a system metadata tag and call DataONEObject parse instead | |
if (response.indexOf("systemMetadata>") > -1) | |
return DataONEObject.prototype.parse.call(this, response); | |
response = this.cleanUpXML(response); | |
response = this.dereference(response); | |
this.set("objectXML", response); | |
var emlElement = $($.parseHTML(response)).filter("eml\\:eml"); | |
} | |
var datasetEl; | |
if (emlElement[0]) datasetEl = $(emlElement[0]).find("dataset"); | |
if (!datasetEl || !datasetEl.length) return {}; | |
var emlParties = [ | |
"metadataprovider", | |
"associatedparty", | |
"creator", | |
"contact", | |
"publisher", | |
], | |
emlDistribution = ["distribution"], | |
emlEntities = [ | |
"datatable", | |
"otherentity", | |
"spatialvector", | |
"spatialraster", | |
"storedprocedure", | |
"view", | |
], | |
emlText = ["abstract", "additionalinfo"], | |
emlMethods = ["methods"]; | |
var nodes = datasetEl.children(), | |
modelJSON = {}; | |
for (var i = 0; i < nodes.length; i++) { | |
var thisNode = nodes[i]; | |
var convertedName = | |
this.nodeNameMap()[thisNode.localName] || thisNode.localName; | |
//EML Party modules are stored in EMLParty models | |
if (_.contains(emlParties, thisNode.localName)) { | |
if (thisNode.localName == "metadataprovider") | |
var attributeName = "metadataProvider"; | |
else if (thisNode.localName == "associatedparty") | |
var attributeName = "associatedParty"; | |
else var attributeName = thisNode.localName; | |
if (typeof modelJSON[attributeName] == "undefined") | |
modelJSON[attributeName] = []; | |
modelJSON[attributeName].push( | |
new EMLParty({ | |
objectDOM: thisNode, | |
parentModel: model, | |
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 }, | |
), | |
); | |
} | |
//The EML Project is stored in the EMLProject model | |
else if (thisNode.localName == "project") { | |
modelJSON.project = new EMLProject({ | |
objectDOM: thisNode, | |
parentModel: model, | |
}); | |
} | |
//EML Temporal, Taxonomic, and Geographic Coverage modules are stored in their own models | |
else if (thisNode.localName == "coverage") { | |
var temporal = $(thisNode).children("temporalcoverage"), | |
geo = $(thisNode).children("geographiccoverage"), | |
taxon = $(thisNode).children("taxonomiccoverage"); | |
if (temporal.length) { | |
modelJSON.temporalCoverage = []; | |
_.each(temporal, function (t) { | |
modelJSON.temporalCoverage.push( | |
new EMLTemporalCoverage({ | |
objectDOM: t, | |
parentModel: model, | |
}), | |
); | |
}); | |
} | |
if (geo.length) { | |
modelJSON.geoCoverage = []; | |
_.each(geo, function (g) { | |
modelJSON.geoCoverage.push( | |
new EMLGeoCoverage({ | |
objectDOM: g, | |
parentModel: model, | |
}), | |
); | |
}); | |
} | |
if (taxon.length) { | |
modelJSON.taxonCoverage = []; | |
_.each(taxon, function (t) { | |
modelJSON.taxonCoverage.push( | |
new EMLTaxonCoverage({ | |
objectDOM: t, | |
parentModel: model, | |
}), | |
); | |
}); | |
} | |
} | |
//Parse EMLText modules | |
else if (_.contains(emlText, thisNode.localName)) { | |
if (typeof modelJSON[convertedName] == "undefined") | |
modelJSON[convertedName] = []; | |
modelJSON[convertedName].push( | |
new EMLText({ | |
objectDOM: thisNode, | |
parentModel: model, | |
}), | |
); | |
} else if (_.contains(emlMethods, thisNode.localName)) { | |
if (typeof modelJSON[thisNode.localName] === "undefined") | |
modelJSON[thisNode.localName] = []; | |
modelJSON[thisNode.localName] = new EMLMethods({ | |
objectDOM: thisNode, | |
parentModel: model, | |
}); | |
} | |
//Parse keywords | |
else if (thisNode.localName == "keywordset") { | |
//Start an array of keyword sets | |
if (typeof modelJSON["keywordSets"] == "undefined") | |
modelJSON["keywordSets"] = []; | |
modelJSON["keywordSets"].push( | |
new EMLKeywordSet({ | |
objectDOM: thisNode, | |
parentModel: model, | |
}), | |
); | |
} | |
//Parse intellectual rights | |
else if (thisNode.localName == "intellectualrights") { | |
var value = ""; | |
if ($(thisNode).children("para").length == 1) | |
value = $(thisNode).children("para").first().text().trim(); | |
else $(thisNode).text().trim(); | |
//If the value is one of our pre-defined options, then add it to the model | |
//if(_.contains(this.get("intellRightsOptions"), value)) | |
modelJSON["intellectualRights"] = value; | |
} | |
//Parse Entities | |
else if (_.contains(emlEntities, thisNode.localName)) { | |
//Start an array of Entities | |
if (typeof modelJSON["entities"] == "undefined") | |
modelJSON["entities"] = []; | |
//Create the model | |
var entityModel; | |
if (thisNode.localName == "otherentity") { | |
entityModel = new EMLOtherEntity( | |
{ | |
objectDOM: thisNode, | |
parentModel: model, | |
}, | |
{ | |
parse: true, | |
}, | |
); | |
} else if (thisNode.localName == "datatable") { | |
entityModel = new EMLDataTable( | |
{ | |
objectDOM: thisNode, | |
parentModel: model, | |
}, | |
{ | |
parse: true, | |
}, | |
); | |
} else { | |
entityModel = new EMLEntity( | |
{ | |
objectDOM: thisNode, | |
parentModel: model, | |
entityType: "application/octet-stream", | |
type: thisNode.localName, | |
}, | |
{ | |
parse: true, | |
}, | |
); | |
} | |
modelJSON["entities"].push(entityModel); | |
} | |
//Parse dataset-level annotations | |
else if (thisNode.localName === "annotation") { | |
if (!modelJSON["annotations"]) { | |
modelJSON["annotations"] = new EMLAnnotations(); | |
} | |
var annotationModel = new EMLAnnotation( | |
{ | |
objectDOM: thisNode, | |
}, | |
{ parse: true }, | |
); | |
modelJSON["annotations"].add(annotationModel); | |
} else { | |
//Is this a multi-valued field in EML? | |
if (Array.isArray(this.get(convertedName))) { | |
//If we already have a value for this field, then add this value to the array | |
if (Array.isArray(modelJSON[convertedName])) | |
modelJSON[convertedName].push(this.toJson(thisNode)); | |
//If it's the first value for this field, then create a new array | |
else modelJSON[convertedName] = [this.toJson(thisNode)]; | |
} else modelJSON[convertedName] = this.toJson(thisNode); | |
} | |
} | |
// 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; | |
}, | |
/* | |
* Retireves the model attributes and serializes into EML XML, to produce the new or modified EML document. | |
* Returns the EML XML as a string. | |
*/ | |
serialize: function () { | |
//Get the EML document | |
var xmlString = this.get("objectXML"), | |
html = $.parseHTML(xmlString), | |
eml = $(html).filter("eml\\:eml"), | |
datasetNode = $(eml).find("dataset"); | |
//Update the packageId on the eml node with the EML id | |
$(eml).attr("packageId", this.get("id")); | |
// Set id attribute on dataset node if needed | |
if (this.get("xmlID")) { | |
$(datasetNode).attr("id", this.get("xmlID")); | |
} | |
// Set schema version | |
$(eml).attr( | |
"xmlns:eml", | |
MetacatUI.appModel.get("editorSerializationFormat") || | |
"https://eml.ecoinformatics.org/eml-2.2.0", | |
); | |
// Set formatID | |
this.set( | |
"formatId", | |
MetacatUI.appModel.get("editorSerializationFormat") || | |
"https://eml.ecoinformatics.org/eml-2.2.0", | |
); | |
// Ensure xsi:schemaLocation has a value for the current format | |
eml = this.setSchemaLocation(eml); | |
var nodeNameMap = this.nodeNameMap(); | |
//Serialize the basic text fields | |
var basicText = ["alternateIdentifier", "title"]; | |
_.each( | |
basicText, | |
function (fieldName) { | |
var basicTextValues = this.get(fieldName); | |
if (!Array.isArray(basicTextValues)) | |
basicTextValues = [basicTextValues]; | |
// Remove existing nodes | |
datasetNode.children(fieldName.toLowerCase()).remove(); | |
// Create new nodes | |
var nodes = _.map(basicTextValues, function (value) { | |
if (value) { | |
var node = document.createElement(fieldName.toLowerCase()); | |
$(node).text(value); | |
return node; | |
} else { | |
return ""; | |
} | |
}); | |
var insertAfter = this.getEMLPosition(eml, fieldName.toLowerCase()); | |
if (insertAfter) { | |
insertAfter.after(nodes); | |
} else { | |
datasetNode.prepend(nodes); | |
} | |
}, | |
this, | |
); | |
// Serialize pubDate | |
// This one is special because it has a default behavior, unlike | |
// the others: When no pubDate is set, it should be set to | |
// the current year | |
var pubDate = this.get("pubDate"); | |
datasetNode.find("pubdate").remove(); | |
if (pubDate != null && pubDate.length > 0) { | |
var pubDateEl = document.createElement("pubdate"); | |
$(pubDateEl).text(pubDate); | |
this.getEMLPosition(eml, "pubdate").after(pubDateEl); | |
} | |
// Serialize the parts of EML that are eml-text modules | |
var textFields = ["abstract", "additionalInfo"]; | |
_.each( | |
textFields, | |
function (field) { | |
var fieldName = this.nodeNameMap()[field] || field; | |
// Get the EMLText model | |
var emlTextModels = Array.isArray(this.get(field)) | |
? this.get(field) | |
: [this.get(field)]; | |
if (!emlTextModels.length) return; | |
// Get the node from the EML doc | |
var nodes = datasetNode.find(fieldName); | |
// Update the DOMs for each model | |
_.each( | |
emlTextModels, | |
function (thisTextModel, i) { | |
//Don't serialize falsey values | |
if (!thisTextModel) return; | |
var node; | |
//Get the existing node or create a new one | |
if (nodes.length < i + 1) { | |
node = document.createElement(fieldName); | |
this.getEMLPosition(eml, fieldName).after(node); | |
} else { | |
node = nodes[i]; | |
} | |
$(node).html($(thisTextModel.updateDOM()).html()); | |
}, | |
this, | |
); | |
// Remove the extra nodes | |
this.removeExtraNodes(nodes, emlTextModels); | |
}, | |
this, | |
); | |
//Create a <coverage> XML node if there isn't one | |
if (datasetNode.children("coverage").length === 0) { | |
var coverageNode = $(document.createElement("coverage")), | |
coveragePosition = this.getEMLPosition(eml, "coverage"); | |
if (coveragePosition) coveragePosition.after(coverageNode); | |
else datasetNode.append(coverageNode); | |
} else { | |
var coverageNode = datasetNode.children("coverage").first(); | |
} | |
//Serialize the geographic coverage | |
if ( | |
typeof this.get("geoCoverage") !== "undefined" && | |
this.get("geoCoverage").length > 0 | |
) { | |
// Don't serialize if geoCoverage is invalid | |
var validCoverages = _.filter( | |
this.get("geoCoverage"), | |
function (cov) { | |
return cov.isValid(); | |
}, | |
); | |
//Get the existing geo coverage nodes from the EML | |
var existingGeoCov = datasetNode.find("geographiccoverage"); | |
//Update the DOM of each model | |
_.each( | |
validCoverages, | |
function (cov, position) { | |
//Update the existing node if it exists | |
if (existingGeoCov.length - 1 >= position) { | |
$(existingGeoCov[position]).replaceWith(cov.updateDOM()); | |
} | |
//Or, append new nodes | |
else { | |
var insertAfter = existingGeoCov.length | |
? datasetNode.find("geographiccoverage").last() | |
: null; | |
if (insertAfter) insertAfter.after(cov.updateDOM()); | |
else coverageNode.append(cov.updateDOM()); | |
} | |
}, | |
this, | |
); | |
//Remove existing taxon coverage nodes that don't have an accompanying model | |
this.removeExtraNodes( | |
datasetNode.find("geographiccoverage"), | |
validCoverages, | |
); | |
} else { | |
//If there are no geographic coverages, remove the nodes | |
coverageNode.children("geographiccoverage").remove(); | |
} | |
//Serialize the taxonomic coverage | |
if ( | |
typeof this.get("taxonCoverage") !== "undefined" && | |
this.get("taxonCoverage").length > 0 | |
) { | |
// Group the taxonomic coverage models into empty and non-empty | |
var sortedTaxonModels = _.groupBy( | |
this.get("taxonCoverage"), | |
function (t) { | |
if (_.flatten(t.get("taxonomicClassification")).length > 0) { | |
return "notEmpty"; | |
} else { | |
return "empty"; | |
} | |
}, | |
); | |
//Get the existing taxon coverage nodes from the EML | |
var existingTaxonCov = coverageNode.children("taxonomiccoverage"); | |
//Iterate over each taxon coverage and update it's DOM | |
if ( | |
sortedTaxonModels["notEmpty"] && | |
sortedTaxonModels["notEmpty"].length > 0 | |
) { | |
//Update the DOM of each model | |
_.each( | |
sortedTaxonModels["notEmpty"], | |
function (taxonCoverage, position) { | |
//Update the existing taxonCoverage node if it exists | |
if (existingTaxonCov.length - 1 >= position) { | |
$(existingTaxonCov[position]).replaceWith( | |
taxonCoverage.updateDOM(), | |
); | |
} | |
//Or, append new nodes | |
else { | |
coverageNode.append(taxonCoverage.updateDOM()); | |
} | |
}, | |
); | |
//Remove existing taxon coverage nodes that don't have an accompanying model | |
this.removeExtraNodes(existingTaxonCov, this.get("taxonCoverage")); | |
} | |
//If all the taxon coverages are empty, remove the parent taxonomicCoverage node | |
else if ( | |
!sortedTaxonModels["notEmpty"] || | |
sortedTaxonModels["notEmpty"].length == 0 | |
) { | |
existingTaxonCov.remove(); | |
} | |
} | |
//Serialize the temporal coverage | |
var existingTemporalCoverages = datasetNode.find("temporalcoverage"); | |
//Update the DOM of each model | |
_.each( | |
this.get("temporalCoverage"), | |
function (temporalCoverage, position) { | |
//Update the existing temporalCoverage node if it exists | |
if (existingTemporalCoverages.length - 1 >= position) { | |
$(existingTemporalCoverages[position]).replaceWith( | |
temporalCoverage.updateDOM(), | |
); | |
} | |
//Or, append new nodes | |
else { | |
coverageNode.append(temporalCoverage.updateDOM()); | |
} | |
}, | |
); | |
//Remove existing taxon coverage nodes that don't have an accompanying model | |
this.removeExtraNodes( | |
existingTemporalCoverages, | |
this.get("temporalCoverage"), | |
); | |
//Remove the temporal coverage if it is empty | |
if (!coverageNode.children("temporalcoverage").children().length) { | |
coverageNode.children("temporalcoverage").remove(); | |
} | |
//Remove the <coverage> node if it's empty | |
if (coverageNode.children().length == 0) { | |
coverageNode.remove(); | |
} | |
// Dataset-level annotations | |
datasetNode.children("annotation").remove(); | |
if (this.get("annotations")) { | |
this.get("annotations").each(function (annotation) { | |
if (annotation.isEmpty()) { | |
return; | |
} | |
var after = this.getEMLPosition(eml, "annotation"); | |
$(after).after(annotation.updateDOM()); | |
}, this); | |
//Since there is at least one annotation, the dataset node needs to have an id attribute. | |
datasetNode.attr("id", this.getUniqueEntityId(this)); | |
} | |
//If there is no creator, create one from the user | |
if (!this.get("creator").length) { | |
var party = new EMLParty({ parentModel: this, type: "creator" }); | |
party.createFromUser(); | |
this.set("creator", [party]); | |
} | |
//Serialize the creators | |
this.serializeParties(eml, "creator"); | |
//Serialize the metadata providers | |
this.serializeParties(eml, "metadataProvider"); | |
//Serialize the associated parties | |
this.serializeParties(eml, "associatedParty"); | |
//Serialize the contacts | |
this.serializeParties(eml, "contact"); | |
//Serialize the publishers | |
this.serializeParties(eml, "publisher"); | |
// Serialize methods | |
if (this.get("methods")) { | |
//If the methods model is empty, remove it from the EML | |
if (this.get("methods").isEmpty()) | |
datasetNode.find("methods").remove(); | |
else { | |
//Serialize the methods model | |
var methodsEl = this.get("methods").updateDOM(); | |
//If the methodsEl is an empty string or other falsey value, then remove the methods node | |
if (!methodsEl || !$(methodsEl).children().length) { | |
datasetNode.find("methods").remove(); | |
} else { | |
//Add the <methods> node to the EML | |
datasetNode.find("methods").detach(); | |
var insertAfter = this.getEMLPosition(eml, "methods"); | |
if (insertAfter) insertAfter.after(methodsEl); | |
else datasetNode.append(methodsEl); | |
} | |
} | |
} | |
//If there are no methods, then remove the methods nodes | |
else { | |
if (datasetNode.find("methods").length > 0) { | |
datasetNode.find("methods").remove(); | |
} | |
} | |
//Serialize the keywords | |
this.serializeKeywords(eml, "keywordSets"); | |
//Serialize the intellectual rights | |
if (this.get("intellectualRights")) { | |
if (datasetNode.find("intellectualRights").length) | |
datasetNode | |
.find("intellectualRights") | |
.html("<para>" + this.get("intellectualRights") + "</para>"); | |
else { | |
this.getEMLPosition(eml, "intellectualrights").after( | |
$(document.createElement("intellectualRights")).html( | |
"<para>" + this.get("intellectualRights") + "</para>", | |
), | |
); | |
} | |
} | |
// 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); | |
} | |
}); | |
} | |
//Detach the project elements from the DOM | |
if (datasetNode.find("project").length) { | |
datasetNode.find("project").detach(); | |
} | |
//If there is an EMLProject, update its DOM | |
if (this.get("project")) { | |
this.getEMLPosition(eml, "project").after( | |
this.get("project").updateDOM(), | |
); | |
} | |
//Get the existing taxon coverage nodes from the EML | |
var existingEntities = datasetNode.find( | |
"otherEntity, dataTable, spatialRaster, spatialVector, storedProcedure, view", | |
); | |
//Serialize the entities | |
_.each( | |
this.get("entities"), | |
function (entity, position) { | |
//Update the existing node if it exists | |
if (existingEntities.length - 1 >= position) { | |
//Remove the entity from the EML | |
$(existingEntities[position]).detach(); | |
//Insert it into the correct position | |
this.getEMLPosition(eml, entity.get("type").toLowerCase()).after( | |
entity.updateDOM(), | |
); | |
} | |
//Or, append new nodes | |
else { | |
//Inser the entity into the correct position | |
this.getEMLPosition(eml, entity.get("type").toLowerCase()).after( | |
entity.updateDOM(), | |
); | |
} | |
}, | |
this, | |
); | |
//Remove extra entities that have been removed | |
var numExtraEntities = | |
existingEntities.length - this.get("entities").length; | |
for ( | |
var i = existingEntities.length - numExtraEntities; | |
i < existingEntities.length; | |
i++ | |
) { | |
$(existingEntities)[i].remove(); | |
} | |
//Do a final check to make sure there are no duplicate ids in the EML | |
var elementsWithIDs = $(eml).find("[id]"), | |
//Get an array of all the ids in this EML doc | |
allIDs = _.map(elementsWithIDs, function (el) { | |
return $(el).attr("id"); | |
}); | |
//If there is at least one id in the EML... | |
if (allIDs && allIDs.length) { | |
//Boil the array down to just the unique values | |
var uniqueIDs = _.uniq(allIDs); | |
//If the unique array is shorter than the array of all ids, | |
// then there is a duplicate somewhere | |
if (uniqueIDs.length < allIDs.length) { | |
//For each element in the EML that has an id, | |
_.each(elementsWithIDs, function (el) { | |
//Get the id for this element | |
var id = $(el).attr("id"); | |
//If there is more than one element in the EML with this id, | |
if ($(eml).find("[id='" + id + "']").length > 1) { | |
//And if it is not a unit node, which we don't want to change, | |
if (!$(el).is("unit")) | |
//Then change the id attribute to a random uuid | |
$(el).attr("id", "urn-uuid-" + uuid.v4()); | |
} | |
}); | |
} | |
} | |
//Camel-case the XML | |
var emlString = ""; | |
_.each( | |
html, | |
function (rootEMLNode) { | |
emlString += this.formatXML(rootEMLNode); | |
}, | |
this, | |
); | |
return emlString; | |
}, | |
/* | |
* Given an EML DOM and party type, this function updated and/or adds the EMLParties to the EML | |
*/ | |
serializeParties: function (eml, type) { | |
//Remove the nodes from the EML for this party type | |
$(eml).children("dataset").children(type.toLowerCase()).remove(); | |
//Serialize each party of this type | |
_.each( | |
this.get(type), | |
function (party, i) { | |
//Get the last node of this type to insert after | |
var insertAfter = $(eml) | |
.children("dataset") | |
.children(type.toLowerCase()) | |
.last(); | |
//If there isn't a node found, find the EML position to insert after | |
if (!insertAfter.length) { | |
insertAfter = this.getEMLPosition(eml, type); | |
} | |
//Update the DOM of the EMLParty | |
var emlPartyDOM = party.updateDOM(); | |
//Make sure we don't insert empty EMLParty nodes into the EML | |
if ($(emlPartyDOM).children().length) { | |
//Insert the party DOM at the insert position | |
if (insertAfter && insertAfter.length) | |
insertAfter.after(emlPartyDOM); | |
//If an insert position still hasn't been found, then just append to the dataset node | |
else $(eml).find("dataset").append(emlPartyDOM); | |
} | |
}, | |
this, | |
); | |
//Create a certain parties from the current app user if none is given | |
if (type == "contact" && !this.get("contact").length) { | |
//Get the creators | |
var creators = this.get("creator"), | |
contacts = []; | |
_.each( | |
creators, | |
function (creator) { | |
//Clone the creator model and add it to the contacts array | |
var newModel = new EMLParty({ parentModel: this }); | |
newModel.set(creator.toJSON()); | |
newModel.set("type", type); | |
contacts.push(newModel); | |
}, | |
this, | |
); | |
this.set(type, contacts); | |
//Call this function again to serialize the new models | |
this.serializeParties(eml, type); | |
} | |
}, | |
serializeKeywords: function (eml) { | |
// Remove all existing keywordSets before appending | |
$(eml).find("dataset").find("keywordset").remove(); | |
if (this.get("keywordSets").length == 0) return; | |
// Create the new keywordSets nodes | |
var nodes = _.map(this.get("keywordSets"), function (kwd) { | |
return kwd.updateDOM(); | |
}); | |
this.getEMLPosition(eml, "keywordset").after(nodes); | |
}, | |
/* | |
* Remoes nodes from the EML that do not have an accompanying model | |
* (Were probably removed from the EML by the user during editing) | |
*/ | |
removeExtraNodes: function (nodes, models) { | |
// Remove the extra nodes | |
var extraNodes = nodes.length - models.length; | |
if (extraNodes > 0) { | |
for (var i = models.length; i < nodes.length; i++) { | |
$(nodes[i]).remove(); | |
} | |
} | |
}, | |
/* | |
* Saves the EML document to the server using the DataONE API | |
*/ | |
save: function (attributes, options) { | |
//Validate before we try anything else | |
if (!this.isValid()) { | |
this.trigger("invalid"); | |
this.trigger("cancelSave"); | |
return false; | |
} else { | |
this.trigger("valid"); | |
} | |
this.setFileName(); | |
//Set the upload transfer as in progress | |
this.set("uploadStatus", "p"); | |
//Reset the draftSaved attribute | |
this.set("draftSaved", false); | |
//Create the creator from the current user if none is provided | |
if (!this.get("creator").length) { | |
var party = new EMLParty({ parentModel: this, type: "creator" }); | |
party.createFromUser(); | |
this.set("creator", [party]); | |
} | |
//Create the contact from the current user if none is provided | |
if (!this.get("contact").length) { | |
var party = new EMLParty({ parentModel: this, type: "contact" }); | |
party.createFromUser(); | |
this.set("contact", [party]); | |
} | |
//If this is an existing object and there is no system metadata, retrieve it | |
if (!this.isNew() && !this.get("sysMetaXML")) { | |
var model = this; | |
//When the system metadata is fetched, try saving again | |
var fetchOptions = { | |
success: function (response) { | |
model.set(DataONEObject.prototype.parse.call(model, response)); | |
model.save(attributes, options); | |
}, | |
}; | |
//Fetch the system metadata now | |
this.fetchSystemMetadata(fetchOptions); | |
return; | |
} | |
//Create a FormData object to send data with our XHR | |
var formData = new FormData(); | |
try { | |
//Add the identifier to the XHR data | |
if (this.isNew()) { | |
formData.append("pid", this.get("id")); | |
} else { | |
//Create a new ID | |
this.updateID(); | |
//Add the ids to the form data | |
formData.append("newPid", this.get("id")); | |
formData.append("pid", this.get("oldPid")); | |
} | |
//Serialize the EML XML | |
var xml = this.serialize(); | |
var xmlBlob = new Blob([xml], { type: "application/xml" }); | |
//Get the size of the new EML XML | |
this.set("size", xmlBlob.size); | |
//Get the new checksum of the EML XML | |
var checksum = md5(xml); | |
this.set("checksum", checksum); | |
this.set("checksumAlgorithm", "MD5"); | |
//Create the system metadata XML | |
var sysMetaXML = this.serializeSysMeta(); | |
//Send the system metadata as a Blob | |
var sysMetaXMLBlob = new Blob([sysMetaXML], { | |
type: "application/xml", | |
}); | |
//Add the object XML and System Metadata XML to the form data | |
//Append the system metadata first, so we can take advantage of Metacat's streaming multipart handler | |
formData.append("sysmeta", sysMetaXMLBlob, "sysmeta"); | |
formData.append("object", xmlBlob); | |
} catch (error) { | |
//Reset the identifier since we didn't actually update the object | |
this.resetID(); | |
this.set("uploadStatus", "e"); | |
this.trigger("error"); | |
this.trigger("cancelSave"); | |
return false; | |
} | |
var model = this; | |
var saveOptions = options || {}; | |
_.extend( | |
saveOptions, | |
{ | |
data: formData, | |
cache: false, | |
contentType: false, | |
dataType: "text", | |
processData: false, | |
parse: false, | |
//Use the URL function to determine the URL | |
url: this.isNew() ? this.url() : this.url({ update: true }), | |
xhr: function () { | |
var xhr = new window.XMLHttpRequest(); | |
//Upload progress | |
xhr.upload.addEventListener( | |
"progress", | |
function (evt) { | |
if (evt.lengthComputable) { | |
var percentComplete = (evt.loaded / evt.total) * 100; | |
model.set("uploadProgress", percentComplete); | |
} | |
}, | |
false, | |
); | |
return xhr; | |
}, | |
success: function (model, response, xhr) { | |
model.set("numSaveAttempts", 0); | |
model.set("uploadStatus", "c"); | |
model.set("sysMetaXML", model.serializeSysMeta()); | |
model.set("oldPid", null); | |
model.fetch({ merge: true, systemMetadataOnly: true }); | |
model.trigger("successSaving", model); | |
}, | |
error: function (model, response, xhr) { | |
model.set("numSaveAttempts", model.get("numSaveAttempts") + 1); | |
var numSaveAttempts = model.get("numSaveAttempts"); | |
//Reset the identifier changes | |
model.resetID(); | |
if ( | |
numSaveAttempts < 3 && | |
(response.status == 408 || response.status == 0) | |
) { | |
//Try saving again in 10, 40, and 90 seconds | |
setTimeout( | |
function () { | |
model.save.call(model); | |
}, | |
numSaveAttempts * numSaveAttempts * 10000, | |
); | |
} else { | |
model.set("numSaveAttempts", 0); | |
//Get the error error information | |
var errorDOM = $($.parseHTML(response.responseText)), | |
errorContainer = errorDOM.filter("error"), | |
msgContainer = errorContainer.length | |
? errorContainer.find("description") | |
: errorDOM.not("style, title"), | |
errorMsg = msgContainer.length | |
? msgContainer.text() | |
: errorDOM; | |
//When there is no network connection (status == 0), there will be no response text | |
if (!errorMsg || response.status == 408 || response.status == 0) | |
errorMsg = | |
"There was a network issue that prevented your metadata from uploading. " + | |
"Make sure you are connected to a reliable internet connection."; | |
//Save the error message in the model | |
model.set("errorMessage", errorMsg); | |
//Set the model status as e for error | |
model.set("uploadStatus", "e"); | |
//Save the EML as a plain text file, until drafts are a supported feature | |
var copy = model.createTextCopy(); | |
//If the EML copy successfully saved, let the user know that there is a copy saved behind the scenes | |
model.listenToOnce(copy, "successSaving", function () { | |
model.set("draftSaved", true); | |
//Trigger the errorSaving event so other parts of the app know that the model failed to save | |
//And send the error message with it | |
model.trigger("errorSaving", errorMsg); | |
}); | |
//If the EML copy fails to save too, then just display the usual error message | |
model.listenToOnce(copy, "errorSaving", function () { | |
//Trigger the errorSaving event so other parts of the app know that the model failed to save | |
//And send the error message with it | |
model.trigger("errorSaving", errorMsg); | |
}); | |
//Save the EML plain text copy | |
copy.save(); | |
// Track the error | |
MetacatUI.analytics?.trackException( | |
`EML save error: ${errorMsg}, EML draft: ${copy.get("id")}`, | |
model.get("id"), | |
true, | |
); | |
} | |
}, | |
}, | |
MetacatUI.appUserModel.createAjaxSettings(), | |
); | |
return Backbone.Model.prototype.save.call( | |
this, | |
attributes, | |
saveOptions, | |
); | |
}, | |
/* | |
* Checks if this EML model has all the required values necessary to save to the server | |
*/ | |
validate: function () { | |
let errors = {}; | |
//A title is always required by EML | |
if (!this.get("title").length || !this.get("title")[0]) { | |
errors.title = "A title is required"; | |
} | |
// Validate the publication date | |
if (this.get("pubDate") != null) { | |
if (!this.isValidYearDate(this.get("pubDate"))) { | |
errors["pubDate"] = [ | |
"The value entered for publication date, '" + | |
this.get("pubDate") + | |
"' is not a valid value for this field. Enter with a year (e.g. 2017) or a date in the format YYYY-MM-DD.", | |
]; | |
} | |
} | |
// Validate the temporal coverage | |
errors.temporalCoverage = []; | |
//If temporal coverage is required and there aren't any, return an error | |
if ( | |
MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && | |
!this.get("temporalCoverage").length | |
) { | |
errors.temporalCoverage = [{ beginDate: "Provide a begin date." }]; | |
} | |
//If temporal coverage is required and they are all empty, return an error | |
else if ( | |
MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && | |
_.every(this.get("temporalCoverage"), function (tc) { | |
return tc.isEmpty(); | |
}) | |
) { | |
errors.temporalCoverage = [{ beginDate: "Provide a begin date." }]; | |
} | |
//If temporal coverage is not required, validate each one | |
else if ( | |
this.get("temporalCoverage").length || | |
(MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && | |
_.every(this.get("temporalCoverage"), function (tc) { | |
return tc.isEmpty(); | |
})) | |
) { | |
//Iterate over each temporal coverage and add it's validation errors | |
_.each(this.get("temporalCoverage"), function (temporalCoverage) { | |
if (!temporalCoverage.isValid() && !temporalCoverage.isEmpty()) { | |
errors.temporalCoverage.push(temporalCoverage.validationError); | |
} | |
}); | |
} | |
//Remove the temporalCoverage attribute if no errors were found | |
if (errors.temporalCoverage.length == 0) { | |
delete errors.temporalCoverage; | |
} | |
//Validate the EMLParty models | |
var partyTypes = [ | |
"associatedParty", | |
"contact", | |
"creator", | |
"metadataProvider", | |
"publisher", | |
]; | |
_.each( | |
partyTypes, | |
function (type) { | |
var people = this.get(type); | |
_.each( | |
people, | |
function (person, i) { | |
if (!person.isValid()) { | |
if (!errors[type]) errors[type] = [person.validationError]; | |
else errors[type].push(person.validationError); | |
} | |
}, | |
this, | |
); | |
}, | |
this, | |
); | |
//Validate the EMLGeoCoverage models | |
_.each( | |
this.get("geoCoverage"), | |
function (geoCoverageModel, i) { | |
if (!geoCoverageModel.isValid()) { | |
if (!errors.geoCoverage) | |
errors.geoCoverage = [geoCoverageModel.validationError]; | |
else errors.geoCoverage.push(geoCoverageModel.validationError); | |
} | |
}, | |
this, | |
); | |
//Validate the EMLTaxonCoverage model | |
var taxonModel = this.get("taxonCoverage")[0]; | |
if (!taxonModel.isEmpty() && !taxonModel.isValid()) { | |
errors = _.extend(errors, taxonModel.validationError); | |
} else if ( | |
taxonModel.isEmpty() && | |
this.get("taxonCoverage").length == 1 && | |
MetacatUI.appModel.get("emlEditorRequiredFields").taxonCoverage | |
) { | |
taxonModel.isValid(); | |
errors = _.extend(errors, taxonModel.validationError); | |
} | |
//Validate each EMLEntity model | |
_.each(this.get("entities"), function (entityModel) { | |
if (!entityModel.isValid()) { | |
if (!errors.entities) | |
errors.entities = [entityModel.validationError]; | |
else errors.entities.push(entityModel.validationError); | |
} | |
}); | |
//Validate the EML Methods | |
let emlMethods = this.get("methods"); | |
if (emlMethods) { | |
if (!emlMethods.isValid()) { | |
errors.methods = emlMethods.validationError; | |
} | |
} | |
// Validate the EMLAnnotation models | |
const annotations = this.get("annotations"); | |
const annotationErrors = annotations.validate(); | |
if (annotationErrors?.length) { | |
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 | |
for ([field, isRequired] of Object.entries( | |
MetacatUI.appModel.get("emlEditorRequiredFields"), | |
)) { | |
//If it's not required, then go to the next field | |
if (!isRequired) continue; | |
if (field == "alternateIdentifier") { | |
if ( | |
!this.get("alternateIdentifier").length || | |
_.every(this.get("alternateIdentifier"), function (altId) { | |
return altId.trim() == ""; | |
}) | |
) | |
errors.alternateIdentifier = | |
"At least one alternate identifier is required."; | |
} else if (field == "generalTaxonomicCoverage") { | |
if ( | |
!this.get("taxonCoverage").length || | |
!this.get("taxonCoverage")[0].get("generalTaxonomicCoverage") | |
) | |
errors.generalTaxonomicCoverage = | |
"Provide a description of the general taxonomic coverage of this data set."; | |
} else if (field == "geoCoverage") { | |
if (!this.get("geoCoverage").length) | |
errors.geoCoverage = "At least one location is required."; | |
} else if (field == "intellectualRights") { | |
if (!this.get("intellectualRights")) | |
errors.intellectualRights = | |
"Select usage rights for this data set."; | |
} else if (field == "studyExtentDescription") { | |
if ( | |
!this.get("methods") || | |
!this.get("methods").get("studyExtentDescription") | |
) | |
errors.studyExtentDescription = | |
"Provide a study extent description."; | |
} else if (field == "samplingDescription") { | |
if ( | |
!this.get("methods") || | |
!this.get("methods").get("samplingDescription") | |
) | |
errors.samplingDescription = "Provide a sampling description."; | |
} else if (field == "temporalCoverage") { | |
if (!this.get("temporalCoverage").length) | |
errors.temporalCoverage = | |
"Provide the date(s) for this data set."; | |
} else if (field == "taxonCoverage") { | |
if (!this.get("taxonCoverage").length) | |
errors.taxonCoverage = | |
"At least one taxa rank and value is required."; | |
} else if (field == "keywordSets") { | |
if (!this.get("keywordSets").length) | |
errors.keywordSets = "Provide at least one keyword."; | |
} | |
//The EMLMethods model will validate itself for required fields, but | |
// this is a rudimentary check to make sure the EMLMethods model was created | |
// in the first place | |
else if (field == "methods") { | |
if (!this.get("methods")) | |
errors.methods = "At least one method step is required."; | |
} else if (field == "funding") { | |
// Note: Checks for either the funding or award element. award | |
// element is checked by the project's objectDOM for now until | |
// EMLProject fully supports the award element | |
if ( | |
!this.get("project") || | |
!( | |
this.get("project").get("funding").length || | |
(this.get("project").get("objectDOM") && | |
this.get("project").get("objectDOM").querySelectorAll && | |
this.get("project").get("objectDOM").querySelectorAll("award") | |
.length > 0) | |
) | |
) | |
errors.funding = | |
"Provide at least one project funding number or name."; | |
} else if (field == "abstract") { | |
if (!this.get("abstract").length) | |
errors["abstract"] = "Provide an abstract."; | |
} else if (field == "dataSensitivity") { | |
if (!this.getDataSensitivity()) { | |
errors["dataSensitivity"] = | |
"Pick the category that best describes the level of sensitivity or restriction of the data."; | |
} | |
} | |
//If this is an EMLParty type, check that there is a party of this type in the model | |
else if ( | |
EMLParty.prototype.partyTypes | |
.map((t) => t.dataCategory) | |
.includes(field) | |
) { | |
//If this is an associatedParty role | |
if (EMLParty.prototype.defaults().roleOptions?.includes(field)) { | |
if ( | |
!this.get("associatedParty") | |
?.map((p) => p.get("roles")) | |
.flat() | |
.includes(field) | |
) { | |
errors[field] = | |
"Provide information about the people or organization(s) in the role: " + | |
EMLParty.prototype.partyTypes.find( | |
(t) => t.dataCategory == field, | |
)?.label; | |
} | |
} else if (!this.get(field)?.length) { | |
errors[field] = | |
"Provide information about the people or organization(s) in the role: " + | |
EMLParty.prototype.partyTypes.find( | |
(t) => t.dataCategory == field, | |
)?.label; | |
} | |
} else if (!this.get(field) || !this.get(field)?.length) { | |
errors[field] = "Provide a " + field + "."; | |
} | |
} | |
if (Object.keys(errors).length) return errors; | |
else { | |
return; | |
} | |
}, | |
/* Returns a boolean for whether the argument 'value' is a valid | |
value for EML's yearDate type which is used in a few places. | |
Note that this method considers a zero-length String to be valid | |
because the EML211.serialize() method will properly handle a null | |
or zero-length String by serializing out the current year. */ | |
isValidYearDate: function (value) { | |
return ( | |
value === "" || | |
/^\d{4}$/.test(value) || | |
/^\d{4}-\d{2}-\d{2}$/.test(value) | |
); | |
}, | |
/* | |
* Sends an AJAX request to fetch the system metadata for this EML object. | |
* Will not trigger a sync event since it does not use Backbone.Model.fetch | |
*/ | |
fetchSystemMetadata: function (options) { | |
if (!options) var options = {}; | |
else options = _.clone(options); | |
var model = this, | |
fetchOptions = _.extend( | |
{ | |
url: | |
MetacatUI.appModel.get("metaServiceUrl") + | |
encodeURIComponent(this.get("id")), | |
dataType: "text", | |
success: function (response) { | |
model.set(DataONEObject.prototype.parse.call(model, response)); | |
//Trigger a custom event that the sys meta was updated | |
model.trigger("sysMetaUpdated"); | |
}, | |
error: function () { | |
model.trigger("error"); | |
}, | |
}, | |
options, | |
); | |
//Add the authorization header and other AJAX settings | |
_.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings()); | |
$.ajax(fetchOptions); | |
}, | |
/* | |
* Returns the nofde in the given EML document that the given node type | |
* should be inserted after | |
* | |
* Returns false if either the node is not found in the and this should | |
* be handled by the caller. | |
*/ | |
getEMLPosition: function (eml, nodeName) { | |
var nodeOrder = this.get("nodeOrder"); | |
var position = _.indexOf(nodeOrder, nodeName.toLowerCase()); | |
if (position == -1) { | |
return false; | |
} | |
// Go through each node in the node list and find the position where this | |
// node will be inserted after | |
for (var i = position - 1; i >= 0; i--) { | |
if ($(eml).find("dataset").children(nodeOrder[i]).length) { | |
return $(eml).find("dataset").children(nodeOrder[i]).last(); | |
} | |
} | |
return false; | |
}, | |
/* | |
* Checks if this model has updates that need to be synced with the server. | |
*/ | |
hasUpdates: function () { | |
if (this.constructor.__super__.hasUpdates.call(this)) return true; | |
//If nothing else has been changed, then this object hasn't had any updates | |
return false; | |
}, | |
/* | |
Add an entity into the EML 2.1.1 object | |
*/ | |
addEntity: function (emlEntity, position) { | |
//Get the current list of entities | |
var currentEntities = this.get("entities"); | |
if (typeof position == "undefined" || position == -1) | |
currentEntities.push(emlEntity); | |
//Add the entity model to the entity array | |
else currentEntities.splice(position, 0, emlEntity); | |
this.trigger("change:entities"); | |
this.trickleUpChange(); | |
return this; | |
}, | |
/* | |
Remove an entity from the EML 2.1.1 object | |
*/ | |
removeEntity: function (emlEntity) { | |
if (!emlEntity || typeof emlEntity != "object") return; | |
//Get the current list of entities | |
var entities = this.get("entities"); | |
entities = _.without(entities, emlEntity); | |
this.set("entities", entities); | |
}, | |
/* | |
* Find the entity model for a given DataONEObject | |
*/ | |
getEntity: function (dataONEObj) { | |
//If an EMLEntity model has been found for this object before, then return it | |
if (dataONEObj.get("metadataEntity")) { | |
dataONEObj.get("metadataEntity").set("dataONEObject", dataONEObj); | |
return dataONEObj.get("metadataEntity"); | |
} | |
var entity = _.find( | |
this.get("entities"), | |
function (e) { | |
//Matches of the checksum or identifier are definite matches | |
if (e.get("xmlID") == dataONEObj.getXMLSafeID()) return true; | |
else if ( | |
e.get("physicalMD5Checksum") && | |
e.get("physicalMD5Checksum") == dataONEObj.get("checksum") && | |
dataONEObj.get("checksumAlgorithm").toUpperCase() == "MD5" | |
) | |
return true; | |
else if ( | |
e.get("downloadID") && | |
e.get("downloadID") == dataONEObj.get("id") | |
) | |
return true; | |
// Get the file name from the EML for this entity | |
var fileNameFromEML = | |
e.get("physicalObjectName") || e.get("entityName"); | |
// If the EML file name matches the DataONEObject file name | |
if ( | |
fileNameFromEML && | |
dataONEObj.get("fileName") && | |
(fileNameFromEML.toLowerCase() == | |
dataONEObj.get("fileName").toLowerCase() || | |
fileNameFromEML.replace(/ /g, "_").toLowerCase() == | |
dataONEObj.get("fileName").toLowerCase()) | |
) { | |
//Get an array of all the other entities in this EML | |
var otherEntities = _.without(this.get("entities"), e); | |
// If this entity name matches the dataone object file name, AND no other dataone object file name | |
// matches, then we can assume this is the entity element for this file. | |
var otherMatchingEntity = _.find( | |
otherEntities, | |
function (otherE) { | |
// Get the file name from the EML for the other entities | |
var otherFileNameFromEML = | |
otherE.get("physicalObjectName") || | |
otherE.get("entityName"); | |
// If the file names match, return true | |
if ( | |
otherFileNameFromEML == dataONEObj.get("fileName") || | |
otherFileNameFromEML.replace(/ /g, "_") == | |
dataONEObj.get("fileName") | |
) | |
return true; | |
}, | |
); | |
// If this entity's file name didn't match any other file names in the EML, | |
// then this entity is a match for the given dataONEObject | |
if (!otherMatchingEntity) return true; | |
} | |
}, | |
this, | |
); | |
//If we found an entity, give it an ID and return it | |
if (entity) { | |
//If this entity has been matched to another DataONEObject already, then don't match it again | |
if (entity.get("dataONEObject") == dataONEObj) { | |
return entity; | |
} | |
//If this entity has been matched to a different DataONEObject already, then don't match it again. | |
//i.e. We will not override existing entity<->DataONEObject pairings | |
else if (entity.get("dataONEObject")) { | |
return; | |
} else { | |
entity.set("dataONEObject", dataONEObj); | |
} | |
//Create an XML-safe ID and set it on the Entity model | |
var entityID = this.getUniqueEntityId(dataONEObj); | |
entity.set("xmlID", entityID); | |
//Save a reference to this entity so we don't have to refind it later | |
dataONEObj.set("metadataEntity", entity); | |
return entity; | |
} | |
//See if one data object is of this type in the package | |
var matchingTypes = _.filter(this.get("entities"), function (e) { | |
return ( | |
e.get("formatName") == | |
(dataONEObj.get("formatId") || dataONEObj.get("mediaType")) | |
); | |
}); | |
if (matchingTypes.length == 1) { | |
//Create an XML-safe ID and set it on the Entity model | |
matchingTypes[0].set("xmlID", dataONEObj.getXMLSafeID()); | |
return matchingTypes[0]; | |
} | |
//If this EML is in a DataPackage with only one other DataONEObject, | |
// and there is only one entity in the EML, then we can assume they are the same entity | |
if (this.get("entities").length == 1) { | |
if ( | |
this.get("collections")[0] && | |
this.get("collections")[0].type == "DataPackage" && | |
this.get("collections")[0].length == 2 && | |
_.contains(this.get("collections")[0].models, dataONEObj) | |
) { | |
return this.get("entities")[0]; | |
} | |
} | |
return false; | |
}, | |
createEntity: function (dataONEObject) { | |
// Add or append an entity to the parent's entity list | |
var entityModel = new EMLOtherEntity({ | |
entityName: dataONEObject.get("fileName"), | |
entityType: | |
dataONEObject.get("formatId") || | |
dataONEObject.get("mediaType") || | |
"application/octet-stream", | |
dataONEObject: dataONEObject, | |
parentModel: this, | |
xmlID: dataONEObject.getXMLSafeID(), | |
}); | |
this.addEntity(entityModel); | |
//If this DataONEObject fails to upload, remove the EML entity | |
this.listenTo(dataONEObject, "errorSaving", function () { | |
this.removeEntity(dataONEObject.get("metadataEntity")); | |
//Listen for a successful save so the entity can be added back | |
this.listenToOnce(dataONEObject, "successSaving", function () { | |
this.addEntity(dataONEObject.get("metadataEntity")); | |
}); | |
}); | |
}, | |
/* | |
* Creates an XML-safe identifier that is unique to this EML document, | |
* based on the given DataONEObject model. It is intended for EML entity nodes in particular. | |
* | |
* @param {DataONEObject} - a DataONEObject model that this EML documents | |
* @return {string} - an identifier string unique to this EML document | |
*/ | |
getUniqueEntityId: function (dataONEObject) { | |
var uniqueId = ""; | |
uniqueId = dataONEObject.getXMLSafeID(); | |
//Get the EML string, if there is one, to check if this id already exists | |
var emlString = this.get("objectXML"); | |
//If this id already exists in the EML... | |
if (emlString && emlString.indexOf(' id="' + uniqueId + '"')) { | |
//Create a random uuid to use instead | |
uniqueId = "urn-uuid-" + uuid.v4(); | |
} | |
return uniqueId; | |
}, | |
/* | |
* removeParty - removes the given EMLParty model from this EML211 model's attributes | |
*/ | |
removeParty: function (partyModel) { | |
//The list of attributes this EMLParty might be stored in | |
var possibleAttr = [ | |
"creator", | |
"contact", | |
"metadataProvider", | |
"publisher", | |
"associatedParty", | |
]; | |
// Iterate over each possible attribute | |
_.each( | |
possibleAttr, | |
function (attr) { | |
if (_.contains(this.get(attr), partyModel)) { | |
this.set(attr, _.without(this.get(attr), partyModel)); | |
} | |
}, | |
this, | |
); | |
}, | |
/** | |
* Attempt to move a party one index forward within its sibling models | |
* | |
* @param {EMLParty} partyModel: The EMLParty model we're moving | |
*/ | |
movePartyUp: function (partyModel) { | |
var possibleAttr = [ | |
"creator", | |
"contact", | |
"metadataProvider", | |
"publisher", | |
"associatedParty", | |
]; | |
// Iterate over each possible attribute | |
_.each( | |
possibleAttr, | |
function (attr) { | |
if (!_.contains(this.get(attr), partyModel)) { | |
return; | |
} | |
// Make a clone because we're going to use splice | |
var models = _.clone(this.get(attr)); | |
// Find the index of the model we're moving | |
var index = _.findIndex(models, function (m) { | |
return m === partyModel; | |
}); | |
if (index === 0) { | |
// Already first | |
return; | |
} | |
if (index === -1) { | |
// Couldn't find the model | |
return; | |
} | |
// Do the move using splice and update the model | |
models.splice(index - 1, 0, models.splice(index, 1)[0]); | |
this.set(attr, models); | |
this.trigger("change:" + attr); | |
}, | |
this, | |
); | |
}, | |
/** | |
* Attempt to move a party one index forward within its sibling models | |
* | |
* @param {EMLParty} partyModel: The EMLParty model we're moving | |
*/ | |
movePartyDown: function (partyModel) { | |
var possibleAttr = [ | |
"creator", | |
"contact", | |
"metadataProvider", | |
"publisher", | |
"associatedParty", | |
]; | |
// Iterate over each possible attribute | |
_.each( | |
possibleAttr, | |
function (attr) { | |
if (!_.contains(this.get(attr), partyModel)) { | |
return; | |
} | |
// Make a clone because we're going to use splice | |
var models = _.clone(this.get(attr)); | |
// Find the index of the model we're moving | |
var index = _.findIndex(models, function (m) { | |
return m === partyModel; | |
}); | |
if (index === -1) { | |
// Couldn't find the model | |
return; | |
} | |
// Figure out where to put the new model | |
// Leave it in the same place if the next index doesn't exist | |
// Move one forward if it does | |
var newIndex = models.length <= index + 1 ? index : index + 1; | |
// Do the move using splice and update the model | |
models.splice(newIndex, 0, models.splice(index, 1)[0]); | |
this.set(attr, models); | |
this.trigger("change:" + attr); | |
}, | |
this, | |
); | |
}, | |
/* | |
* Adds the given EMLParty model to this EML211 model in the | |
* appropriate role array in the given position | |
* | |
* @param {EMLParty} - The EMLParty model to add | |
* @param {number} - The position in the role array in which to insert this EMLParty | |
* @return {boolean} - Returns true if the EMLParty was successfully added, false if it was cancelled | |
*/ | |
addParty: function (partyModel, position) { | |
//If the EMLParty model is empty, don't add it to the EML211 model | |
if (partyModel.isEmpty()) return false; | |
//Get the role of this EMLParty | |
var role = partyModel.get("type") || "associatedParty"; | |
//If this model already contains this EMLParty, then exit | |
if (_.contains(this.get(role), partyModel)) return false; | |
if (typeof position == "undefined") { | |
this.get(role).push(partyModel); | |
} else { | |
this.get(role).splice(position, 0, partyModel); | |
} | |
this.trigger("change:" + role); | |
return true; | |
}, | |
/** | |
* getPartiesByType - Gets an array of EMLParty members that have a particular party type or role. | |
* @param {string} partyType - A string that represents either the role or the party type. For example, "contact", "creator", "principalInvestigator", etc. | |
* @since 2.15.0 | |
*/ | |
getPartiesByType: function (partyType) { | |
try { | |
if (!partyType) { | |
return false; | |
} | |
var associatedPartyTypes = new EMLParty().get("roleOptions"), | |
isAssociatedParty = associatedPartyTypes.includes(partyType), | |
parties = []; | |
// For "contact", "creator", "metadataProvider", "publisher", each party type has it's own | |
// array in the EML model | |
if (!isAssociatedParty) { | |
parties = this.get(partyType); | |
// For "custodianSteward", "principalInvestigator", "collaboratingPrincipalInvestigator", etc., | |
// party members are listed in the EML model's associated parties array. Each associated party's | |
// party type is indicated in the role attribute. | |
} else { | |
parties = _.filter( | |
this.get("associatedParty"), | |
function (associatedParty) { | |
return associatedParty.get("roles").includes(partyType); | |
}, | |
); | |
} | |
return parties; | |
} catch (error) { | |
console.log( | |
"Error trying to find a list of party members in an EML model by type. Error details: " + | |
error, | |
); | |
} | |
}, | |
createUnits: function () { | |
this.units.fetch(); | |
}, | |
/* Initialize the object XML for brand spankin' new EML objects */ | |
createXML: function () { | |
let emlSystem = MetacatUI.appModel.get("emlSystem"); | |
emlSystem = | |
!emlSystem || typeof emlSystem != "string" ? "knb" : emlSystem; | |
var xml = | |
'<eml:eml xmlns:eml="https://eml.ecoinformatics.org/eml-2.2.0"></eml:eml>', | |
eml = $($.parseHTML(xml)); | |
// Set base attributes | |
eml.attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); | |
eml.attr("xmlns:stmml", "http://www.xml-cml.org/schema/stmml-1.1"); | |
eml.attr( | |
"xsi:schemaLocation", | |
"https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd", | |
); | |
eml.attr("packageId", this.get("id")); | |
eml.attr("system", emlSystem); | |
// Add the dataset | |
eml.append(document.createElement("dataset")); | |
eml.find("dataset").append(document.createElement("title")); | |
var emlString = $(document.createElement("div")) | |
.append(eml.clone()) | |
.html(); | |
return emlString; | |
}, | |
/* | |
Replace elements named "source" with "sourced" due to limitations | |
with using $.parseHTML() rather than $.parseXML() | |
@param xmlString The XML string to make the replacement in | |
*/ | |
cleanUpXML: function (xmlString) { | |
xmlString.replace("<source>", "<sourced>"); | |
xmlString.replace("</source>", "</sourced>"); | |
return xmlString; | |
}, | |
createTextCopy: function () { | |
var emlDraftText = | |
"EML draft for " + | |
this.get("id") + | |
"(" + | |
this.get("title") + | |
") by " + | |
MetacatUI.appUserModel.get("firstName") + | |
" " + | |
MetacatUI.appUserModel.get("lastName"); | |
if (this.get("uploadStatus") == "e" && this.get("errorMessage")) { | |
emlDraftText += | |
". This EML had the following save error: `" + | |
this.get("errorMessage") + | |
"` "; | |
} else { | |
emlDraftText += ": "; | |
} | |
emlDraftText += this.serialize(); | |
var plainTextEML = new DataONEObject({ | |
formatId: "text/plain", | |
fileName: | |
"eml_draft_" + | |
(MetacatUI.appUserModel.get("lastName") || "") + | |
".txt", | |
uploadFile: new Blob([emlDraftText], { type: "plain/text" }), | |
synced: true, | |
}); | |
return plainTextEML; | |
}, | |
/* | |
* Cleans up the given text so that it is XML-valid by escaping reserved characters, trimming white space, etc. | |
* | |
* @param {string} textString - The string to clean up | |
* @return {string} - The cleaned up string | |
*/ | |
cleanXMLText: function (textString) { | |
if (typeof textString != "string") return; | |
textString = textString.trim(); | |
//Check for XML/HTML elements | |
_.each(textString.match(/<\s*[^>]*>/g), function (xmlNode) { | |
//Encode <, >, and </ substrings | |
var tagName = xmlNode.replace(/>/g, ">"); | |
tagName = tagName.replace(/</g, "<"); | |
//Replace the xmlNode in the full text string | |
textString = textString.replace(xmlNode, tagName); | |
}); | |
//Remove Unicode characters that are not valid XML characters | |
//Create a regular expression that matches any character that is not a valid XML character | |
// (see https://www.w3.org/TR/xml/#charsets) | |
var invalidCharsRegEx = | |
/[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD]/g; | |
textString = textString.replace(invalidCharsRegEx, ""); | |
return textString; | |
}, | |
/* | |
Dereference "reference" elements and replace them with a cloned copy | |
of the referenced content | |
@param xmlString The XML string with reference elements to transform | |
*/ | |
dereference: function (xmlString) { | |
var referencesList; // the array of references elements in the document | |
var referencedID; // The id of the referenced element | |
var referencesParentEl; // The parent of the given references element | |
var referencedEl; // The referenced DOM to be copied | |
var xmlDOM = $.parseXML(xmlString); | |
referencesList = xmlDOM.getElementsByTagName("references"); | |
if (referencesList.length) { | |
// Process each references elements | |
_.each( | |
referencesList, | |
function (referencesEl, index, referencesList) { | |
// Can't rely on the passed referencesEl since the list length changes | |
// because of the remove() below. Reuse referencesList[0] for every item: | |
// referencedID = $(referencesEl).text(); // doesn't work | |
referencesEl = referencesList[0]; | |
referencedID = $(referencesEl).text(); | |
referencesParentEl = $(referencesEl).parent()[0]; | |
if (typeof referencedID !== "undefined" && referencedID != "") { | |
referencedEl = xmlDOM.getElementById(referencedID); | |
if (typeof referencedEl != "undefined") { | |
// Clone the referenced element and replace the references element | |
var referencedClone = $(referencedEl).clone()[0]; | |
$(referencesParentEl) | |
.children(referencesEl.localName) | |
.replaceWith($(referencedClone).children()); | |
//$(referencesParentEl).append($(referencedClone).children()); | |
$(referencesParentEl).attr("id", DataONEObject.generateId()); | |
} | |
} | |
}, | |
xmlDOM, | |
); | |
} | |
return new XMLSerializer().serializeToString(xmlDOM); | |
}, | |
/* | |
* Uses the EML `title` to set the `fileName` attribute on this model. | |
*/ | |
setFileName: function () { | |
var title = ""; | |
// Get the title from the metadata | |
if (Array.isArray(this.get("title"))) { | |
title = this.get("title")[0]; | |
} else if (typeof this.get("title") == "string") { | |
title = this.get("title"); | |
} | |
//Max title length | |
var maxLength = 50; | |
//trim the string to the maximum length | |
var trimmedTitle = title.trim().substr(0, maxLength); | |
//re-trim if we are in the middle of a word | |
if (trimmedTitle.indexOf(" ") > -1) { | |
trimmedTitle = trimmedTitle.substr( | |
0, | |
Math.min(trimmedTitle.length, trimmedTitle.lastIndexOf(" ")), | |
); | |
} | |
//Replace all non alphanumeric characters with underscores | |
// and make sure there isn't more than one underscore in a row | |
trimmedTitle = trimmedTitle | |
.replace(/[^a-zA-Z0-9]/g, "_") | |
.replace(/_{2,}/g, "_"); | |
//Set the fileName on the model | |
this.set("fileName", trimmedTitle + ".xml"); | |
}, | |
trickleUpChange: function () { | |
if ( | |
!MetacatUI.rootDataPackage || | |
!MetacatUI.rootDataPackage.packageModel | |
) | |
return; | |
//Mark the package as changed | |
MetacatUI.rootDataPackage.packageModel.set("changed", true); | |
}, | |
/** | |
* Sets the xsi:schemaLocation attribute on the passed-in Element | |
* depending on the application configuration. | |
* | |
* @param {Element} eml: The root eml:eml element to modify | |
* @return {Element} The element, possibly modified | |
*/ | |
setSchemaLocation: function (eml) { | |
if (!MetacatUI || !MetacatUI.appModel) { | |
return eml; | |
} | |
var current = $(eml).attr("xsi:schemaLocation"), | |
format = MetacatUI.appModel.get("editorSerializationFormat"), | |
location = MetacatUI.appModel.get("editorSchemaLocation"); | |
// Return now if we can't do anything anyway | |
if (!format || !location) { | |
return eml; | |
} | |
// Simply add if the attribute isn't present to begin with | |
if (!current || typeof current !== "string") { | |
$(eml).attr("xsi:schemaLocation", format + " " + location); | |
return eml; | |
} | |
// Don't append if it's already present | |
if (current.indexOf(format) >= 0) { | |
return eml; | |
} | |
$(eml).attr("xsi:schemaLocation", current + " " + location); | |
return eml; | |
}, | |
createID: function () { | |
this.set("xmlID", uuid.v4()); | |
}, | |
/** | |
* Creates and adds an {@link EMLAnnotation} to this EML211 model with the given annotation data in JSON form. | |
* @param {object} annotationData The attribute data to set on the new {@link EMLAnnotation}. See {@link EMLAnnotation#defaults} for | |
* details on what attributes can be passed to the EMLAnnotation. In addition, there is an `elementName` property. | |
* @property {string} [annotationData.elementName] The name of the EML Element that this | |
annotation should be applied to. e.g. dataset, entity, attribute. Defaults to `dataset`. NOTE: Right now only dataset annotations are supported until | |
more annotation editing is added to the EML Editor. | |
* @property {Boolean} [annotationData.allowDuplicates] If false, this annotation will replace all annotations already set with the same propertyURI. | |
* By default, more than one annotation with a given propertyURI can be added (defaults to true) | |
*/ | |
addAnnotation: function (annotationData) { | |
try { | |
if (!annotationData || typeof annotationData != "object") { | |
return; | |
} | |
//If no element name is provided, default to the dataset element. | |
let elementName = ""; | |
if (!annotationData.elementName) { | |
elementName = "dataset"; | |
} else { | |
elementName = annotationData.elementName; | |
} | |
//Remove the elementName property so it isn't set on the EMLAnnotation model later. | |
delete annotationData.elementName; | |
//Check if duplicates are allowed | |
let allowDuplicates = annotationData.allowDuplicates; | |
delete annotationData.allowDuplicates; | |
//Create a new EMLAnnotation model | |
let annotation = new EMLAnnotation(annotationData); | |
//Update annotations set on the dataset element | |
if (elementName == "dataset") { | |
let annotations = this.get("annotations"); | |
//If the current annotations set on the EML model are not in Array form, change it to an array | |
if (!annotations) { | |
annotations = new EMLAnnotations(); | |
} | |
if (allowDuplicates === false) { | |
//Add the EMLAnnotation to the collection, making sure to remove duplicates first | |
annotations.replaceDuplicateWith(annotation); | |
} else { | |
annotations.add(annotation); | |
} | |
//Set the annotations and force the change to be recognized by the model | |
this.set("annotations", annotations, { silent: true }); | |
this.handleChange(this, { force: true }); | |
} else { | |
/** @todo Add annotation support for other EML Elements */ | |
} | |
} catch (e) { | |
console.error("Could not add Annotation to the EML: ", e); | |
} | |
}, | |
/** | |
* Finds annotations that are of the `data sensitivity` property from the NCEAS SENSO ontology. | |
* Returns undefined if none are found. This function returns EMLAnnotation models because the data | |
* sensitivity is stored in the EML Model as EMLAnnotations and added to EML as semantic annotations. | |
* @returns {EMLAnnotation[]|undefined} | |
*/ | |
getDataSensitivity: function () { | |
try { | |
let annotations = this.get("annotations"); | |
if (annotations) { | |
let found = annotations.where({ | |
propertyURI: this.get("dataSensitivityPropertyURI"), | |
}); | |
if (!found || !found.length) { | |
return; | |
} else { | |
return found; | |
} | |
} else { | |
return; | |
} | |
} catch (e) { | |
console.error("Failed to get Data Sensitivity from EML model: ", e); | |
return; | |
} | |
}, | |
}, | |
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 [eslint] <object-shorthand> reported by reviewdog 🐶
Expected method shorthand.
metacatui/src/js/models/metadata/eml211/EML211.js
Lines 1583 to 1850 in 979d2a5
validate: function () { | |
let errors = {}; | |
//A title is always required by EML | |
if (!this.get("title").length || !this.get("title")[0]) { | |
errors.title = "A title is required"; | |
} | |
// Validate the publication date | |
if (this.get("pubDate") != null) { | |
if (!this.isValidYearDate(this.get("pubDate"))) { | |
errors["pubDate"] = [ | |
"The value entered for publication date, '" + | |
this.get("pubDate") + | |
"' is not a valid value for this field. Enter with a year (e.g. 2017) or a date in the format YYYY-MM-DD.", | |
]; | |
} | |
} | |
// Validate the temporal coverage | |
errors.temporalCoverage = []; | |
//If temporal coverage is required and there aren't any, return an error | |
if ( | |
MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && | |
!this.get("temporalCoverage").length | |
) { | |
errors.temporalCoverage = [{ beginDate: "Provide a begin date." }]; | |
} | |
//If temporal coverage is required and they are all empty, return an error | |
else if ( | |
MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && | |
_.every(this.get("temporalCoverage"), function (tc) { | |
return tc.isEmpty(); | |
}) | |
) { | |
errors.temporalCoverage = [{ beginDate: "Provide a begin date." }]; | |
} | |
//If temporal coverage is not required, validate each one | |
else if ( | |
this.get("temporalCoverage").length || | |
(MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && | |
_.every(this.get("temporalCoverage"), function (tc) { | |
return tc.isEmpty(); | |
})) | |
) { | |
//Iterate over each temporal coverage and add it's validation errors | |
_.each(this.get("temporalCoverage"), function (temporalCoverage) { | |
if (!temporalCoverage.isValid() && !temporalCoverage.isEmpty()) { | |
errors.temporalCoverage.push(temporalCoverage.validationError); | |
} | |
}); | |
} | |
//Remove the temporalCoverage attribute if no errors were found | |
if (errors.temporalCoverage.length == 0) { | |
delete errors.temporalCoverage; | |
} | |
//Validate the EMLParty models | |
var partyTypes = [ | |
"associatedParty", | |
"contact", | |
"creator", | |
"metadataProvider", | |
"publisher", | |
]; | |
_.each( | |
partyTypes, | |
function (type) { | |
var people = this.get(type); | |
_.each( | |
people, | |
function (person, i) { | |
if (!person.isValid()) { | |
if (!errors[type]) errors[type] = [person.validationError]; | |
else errors[type].push(person.validationError); | |
} | |
}, | |
this, | |
); | |
}, | |
this, | |
); | |
//Validate the EMLGeoCoverage models | |
_.each( | |
this.get("geoCoverage"), | |
function (geoCoverageModel, i) { | |
if (!geoCoverageModel.isValid()) { | |
if (!errors.geoCoverage) | |
errors.geoCoverage = [geoCoverageModel.validationError]; | |
else errors.geoCoverage.push(geoCoverageModel.validationError); | |
} | |
}, | |
this, | |
); | |
//Validate the EMLTaxonCoverage model | |
var taxonModel = this.get("taxonCoverage")[0]; | |
if (!taxonModel.isEmpty() && !taxonModel.isValid()) { | |
errors = _.extend(errors, taxonModel.validationError); | |
} else if ( | |
taxonModel.isEmpty() && | |
this.get("taxonCoverage").length == 1 && | |
MetacatUI.appModel.get("emlEditorRequiredFields").taxonCoverage | |
) { | |
taxonModel.isValid(); | |
errors = _.extend(errors, taxonModel.validationError); | |
} | |
//Validate each EMLEntity model | |
_.each(this.get("entities"), function (entityModel) { | |
if (!entityModel.isValid()) { | |
if (!errors.entities) | |
errors.entities = [entityModel.validationError]; | |
else errors.entities.push(entityModel.validationError); | |
} | |
}); | |
//Validate the EML Methods | |
let emlMethods = this.get("methods"); | |
if (emlMethods) { | |
if (!emlMethods.isValid()) { | |
errors.methods = emlMethods.validationError; | |
} | |
} | |
// Validate the EMLAnnotation models | |
const annotations = this.get("annotations"); | |
const annotationErrors = annotations.validate(); | |
if (annotationErrors?.length) { | |
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 | |
for ([field, isRequired] of Object.entries( | |
MetacatUI.appModel.get("emlEditorRequiredFields"), | |
)) { | |
//If it's not required, then go to the next field | |
if (!isRequired) continue; | |
if (field == "alternateIdentifier") { | |
if ( | |
!this.get("alternateIdentifier").length || | |
_.every(this.get("alternateIdentifier"), function (altId) { | |
return altId.trim() == ""; | |
}) | |
) | |
errors.alternateIdentifier = | |
"At least one alternate identifier is required."; | |
} else if (field == "generalTaxonomicCoverage") { | |
if ( | |
!this.get("taxonCoverage").length || | |
!this.get("taxonCoverage")[0].get("generalTaxonomicCoverage") | |
) | |
errors.generalTaxonomicCoverage = | |
"Provide a description of the general taxonomic coverage of this data set."; | |
} else if (field == "geoCoverage") { | |
if (!this.get("geoCoverage").length) | |
errors.geoCoverage = "At least one location is required."; | |
} else if (field == "intellectualRights") { | |
if (!this.get("intellectualRights")) | |
errors.intellectualRights = | |
"Select usage rights for this data set."; | |
} else if (field == "studyExtentDescription") { | |
if ( | |
!this.get("methods") || | |
!this.get("methods").get("studyExtentDescription") | |
) | |
errors.studyExtentDescription = | |
"Provide a study extent description."; | |
} else if (field == "samplingDescription") { | |
if ( | |
!this.get("methods") || | |
!this.get("methods").get("samplingDescription") | |
) | |
errors.samplingDescription = "Provide a sampling description."; | |
} else if (field == "temporalCoverage") { | |
if (!this.get("temporalCoverage").length) | |
errors.temporalCoverage = | |
"Provide the date(s) for this data set."; | |
} else if (field == "taxonCoverage") { | |
if (!this.get("taxonCoverage").length) | |
errors.taxonCoverage = | |
"At least one taxa rank and value is required."; | |
} else if (field == "keywordSets") { | |
if (!this.get("keywordSets").length) | |
errors.keywordSets = "Provide at least one keyword."; | |
} | |
//The EMLMethods model will validate itself for required fields, but | |
// this is a rudimentary check to make sure the EMLMethods model was created | |
// in the first place | |
else if (field == "methods") { | |
if (!this.get("methods")) | |
errors.methods = "At least one method step is required."; | |
} else if (field == "funding") { | |
// Note: Checks for either the funding or award element. award | |
// element is checked by the project's objectDOM for now until | |
// EMLProject fully supports the award element | |
if ( | |
!this.get("project") || | |
!( | |
this.get("project").get("funding").length || | |
(this.get("project").get("objectDOM") && | |
this.get("project").get("objectDOM").querySelectorAll && | |
this.get("project").get("objectDOM").querySelectorAll("award") | |
.length > 0) | |
) | |
) | |
errors.funding = | |
"Provide at least one project funding number or name."; | |
} else if (field == "abstract") { | |
if (!this.get("abstract").length) | |
errors["abstract"] = "Provide an abstract."; | |
} else if (field == "dataSensitivity") { | |
if (!this.getDataSensitivity()) { | |
errors["dataSensitivity"] = | |
"Pick the category that best describes the level of sensitivity or restriction of the data."; | |
} | |
} | |
//If this is an EMLParty type, check that there is a party of this type in the model | |
else if ( | |
EMLParty.prototype.partyTypes | |
.map((t) => t.dataCategory) | |
.includes(field) | |
) { | |
//If this is an associatedParty role | |
if (EMLParty.prototype.defaults().roleOptions?.includes(field)) { | |
if ( | |
!this.get("associatedParty") | |
?.map((p) => p.get("roles")) | |
.flat() | |
.includes(field) | |
) { | |
errors[field] = | |
"Provide information about the people or organization(s) in the role: " + | |
EMLParty.prototype.partyTypes.find( | |
(t) => t.dataCategory == field, | |
)?.label; | |
} | |
} else if (!this.get(field)?.length) { | |
errors[field] = | |
"Provide information about the people or organization(s) in the role: " + | |
EMLParty.prototype.partyTypes.find( | |
(t) => t.dataCategory == field, | |
)?.label; | |
} | |
} else if (!this.get(field) || !this.get(field)?.length) { | |
errors[field] = "Provide a " + field + "."; | |
} | |
} | |
if (Object.keys(errors).length) return errors; | |
else { | |
return; | |
} | |
}, |
Add an input in the Overview page of the EML Editor View that allows users to identify the authoritative version of the dataset: