/* global define */ define(['jquery', 'underscore', 'backbone', 'models/DataONEObject'], function($, _, Backbone, DataONEObject) { var EMLParty = Backbone.Model.extend({ defaults: function(){ return { objectXML: null, objectDOM: null, individualName: null, organizationName: null, positionName: null, address: [], phone: [], fax: [], email: [], onlineUrl: [], roles: [], references: null, userId: [], xmlID: null, type: null, typeOptions: ["associatedParty", "contact", "creator", "metadataProvider", "publisher"], roleOptions: ["custodianSteward", "principalInvestigator", "collaboratingPrincipalInvestigator", "coPrincipalInvestigator", "user"], parentModel: null, removed: false //Indicates whether this model has been removed from the parent model } }, initialize: function(options){ if(options && options.objectDOM) this.set(this.parse(options.objectDOM)); if(!this.get("xmlID")) this.createID(); this.on("change:roles", this.setType); }, /* * 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 { "administrativearea" : "administrativeArea", "associatedparty" : "associatedParty", "deliverypoint" : "deliveryPoint", "electronicmailaddress" : "electronicMailAddress", "givenname" : "givenName", "individualname" : "individualName", "metadataprovider" : "metadataProvider", "onlineurl" : "onlineUrl", "organizationname" : "organizationName", "positionname" : "positionName", "postalcode" : "postalCode", "surname" : "surName", "userid" : "userId" } }, /* Parse the object DOM to create the model @param objectDOM the XML DOM to parse @return modelJSON the resulting model attributes object */ parse: function(objectDOM){ if(!objectDOM) var objectDOM = this.get("objectDOM"); var model = this, modelJSON = {}; //Set the name var person = $(objectDOM).children("individualname, individualName"); if(person.length) modelJSON.individualName = this.parsePerson(person); //Set the phone and fax numbers var phones = $(objectDOM).children("phone"), phoneNums = [], faxNums = []; phones.each(function(i, phone){ if($(phone).attr("phonetype") == "voice") phoneNums.push($(phone).text()); else if($(phone).attr("phonetype") == "facsimile") faxNums.push($(phone).text()); }); modelJSON.phone = phoneNums; modelJSON.fax = faxNums; //Set the address var addresses = $(objectDOM).children("address") || [], addressesJSON = []; addresses.each(function(i, address){ addressesJSON.push(model.parseAddress(address)); }); modelJSON.address = addressesJSON; //Set the text fields modelJSON.organizationName = $(objectDOM).children("organizationname, organizationName").text() || null; modelJSON.positionName = $(objectDOM).children("positionname, positionName").text() || null; // roles modelJSON.roles = []; $(objectDOM).find("role").each(function(i,role){ modelJSON.roles.push($(role).text()); }); //Set the id attribute modelJSON.xmlID = $(objectDOM).attr("id"); //Email - only set it on the JSON if it exists (we want to avoid an empty string value in the array) if( $(objectDOM).children("electronicmailaddress, electronicMailAddress").length ){ modelJSON.email = _.map($(objectDOM).children("electronicmailaddress, electronicMailAddress"), function(email){ return $(email).text(); }); } //Online URL - only set it on the JSON if it exists (we want to avoid an empty string value in the array) if( $(objectDOM).find("onlineurl, onlineUrl").length ){ // modelJSON.onlineUrl = [$(objectDOM).find("onlineurl, onlineUrl").first().text()]; modelJSON.onlineUrl = $(objectDOM).find("onlineurl, onlineUrl").map(function(i,v) { return $(v).text(); }).get(); } //User ID - only set it on the JSON if it exists (we want to avoid an empty string value in the array) if( $(objectDOM).find("userid, userId").length ){ modelJSON.userId = [$(objectDOM).find("userid, userId").first().text()]; } return modelJSON; }, parseNode: function(node){ if(!node || (Array.isArray(node) && !node.length)) return; this.set($(node)[0].localName, $(node).text()); }, parsePerson: function(personXML){ var person = { givenName: [], surName: "", salutation: [] }, givenName = $(personXML).find("givenname, givenName"), surName = $(personXML).find("surname, surName"), salutations = $(personXML).find("salutation"); //Concatenate all the given names into one, for now //TODO: Support multiple given names givenName.each(function(i, name){ if(i==0) person.givenName[0] = ""; person.givenName[0] += $(name).text() + " "; if(i==givenName.length-1) person.givenName[0] = person.givenName[0].trim(); }); person.surName = surName.text(); salutations.each(function(i, name){ person.salutation.push($(name).text()); }); return person; }, parseAddress: function(addressXML){ var address = {}, delPoint = $(addressXML).find("deliverypoint, deliveryPoint"), city = $(addressXML).find("city"), adminArea = $(addressXML).find("administrativearea, administrativeArea"), postalCode = $(addressXML).find("postalcode, postalCode"), country = $(addressXML).find("country"); address.city = city.length? city.text() : ""; address.administrativeArea = adminArea.length? adminArea.text() : ""; address.postalCode = postalCode.length? postalCode.text() : ""; address.country = country.length? country.text() : ""; //Get an array of all the address line (or delivery point) values var addressLines = []; _.each(delPoint, function(addressLine, i){ addressLines.push($(addressLine).text()); }, this); address.deliveryPoint = addressLines; return address; }, serialize: function(){ var objectDOM = this.updateDOM(), xmlString = objectDOM.outerHTML; //Camel-case the XML xmlString = this.formatXML(xmlString); return xmlString; }, /* * Updates the attributes on this model based on the application user (the app UserModel) */ createFromUser: function(){ //Create the name from the user var name = this.get("individualName") || {}; name.givenName = [MetacatUI.appUserModel.get("firstName")]; name.surName = MetacatUI.appUserModel.get("lastName"); this.set("individualName", name); //Get the email and username if(MetacatUI.appUserModel.get("email")) this.set("email", [MetacatUI.appUserModel.get("email")]); this.set("userId", [MetacatUI.appUserModel.get("username")]); }, /* * Makes a copy of the original XML DOM and updates it with the new values from the model. */ updateDOM: function(){ var type = this.get("type") || "associatedParty", objectDOM = this.get("objectDOM"); // If there is already an XML node for this model and it is the wrong type, // then replace the XML node contents if(objectDOM && objectDOM.nodeName != type.toUpperCase()){ objectDOM = $(document.createElement(type)).html( objectDOM.innerHTML ); } // If there is already an XML node for this model and it is the correct type, // then simply clone the XML node else if(objectDOM){ objectDOM = objectDOM.cloneNode(true); } // Otherwise, create a new XML node else{ objectDOM = document.createElement(type); } //There needs to be at least one individual name, organization name, or position name if( this.nameIsEmpty() && !this.get("organizationName") && !this.get("positionName")) return ""; var name = this.get("individualName"); if(name){ //Get the individualName node var nameNode = $(objectDOM).find("individualname"); if(!nameNode.length){ nameNode = document.createElement("individualname"); $(objectDOM).prepend(nameNode); } //Empty the individualName node $(nameNode).empty(); // salutation[s] if(!Array.isArray(name.salutation) && name.salutation) name.salutation = [name.salutation]; _.each(name.salutation, function(salutation) { $(nameNode).prepend("" + salutation + ""); }); //Given name if(!Array.isArray(name.givenName) && name.givenName) name.givenName = [name.givenName]; _.each(name.givenName, function(givenName) { //If there is a given name string, create a givenName node if(typeof givenName == "string" && givenName){ $(nameNode).append("" + givenName + ""); } }); // surname if(name.surName) $(nameNode).append("" + name.surName + ""); } //If there is no name set on the model, remove it from the DOM else{ $(objectDOM).find("individualname").remove(); } // organizationName if(this.get("organizationName")){ //Get the organization name node if($(objectDOM).find("organizationname").length) var orgNameNode = $(objectDOM).find("organizationname").detach(); else var orgNameNode = document.createElement("organizationname"); //Insert the text $(orgNameNode).text(this.get("organizationName")); //If the DOM is empty, append it if( !$(objectDOM).children().length ) $(objectDOM).append(orgNameNode); else{ var insertAfter = this.getEMLPosition(objectDOM, "organizationname"); if(insertAfter && insertAfter.length) insertAfter.after(orgNameNode); else $(objectDOM).prepend(orgNameNode); } } //Remove the organization name node if there is no organization name else{ var orgNameNode = $(objectDOM).find("organizationname").remove(); } // positionName if(this.get("positionName")){ //Get the name node if($(objectDOM).find("positionname").length) var posNameNode = $(objectDOM).find("positionname").detach(); else var posNameNode = document.createElement("positionname"); //Insert the text $(posNameNode).text(this.get("positionName")); //If the DOM is empty, append it if( !$(objectDOM).children().length ) $(objectDOM).append(posNameNode); else this.getEMLPosition(objectDOM, "positionname").after(posNameNode); } //Remove the position name node if there is no position name else{ $(objectDOM).find("positionname").remove(); } // address _.each(this.get("address"), function(address, i) { var addressNode = $(objectDOM).find("address")[i]; if(!addressNode){ addressNode = document.createElement("address"); this.getEMLPosition(objectDOM, "address").after(addressNode); } //Remove all the delivery points since they'll be reserialized $(addressNode).find("deliverypoint").remove(); _.each(address.deliveryPoint, function(deliveryPoint, ii){ if(!deliveryPoint) return; var delPointNode = $(addressNode).find("deliverypoint")[ii]; if(!delPointNode){ delPointNode = document.createElement("deliverypoint"); //Add the deliveryPoint node to the address node //Insert after the last deliveryPoint node var appendAfter = $(addressNode).find("deliverypoint")[ii-1]; if(appendAfter) $(appendAfter).after(delPointNode); //Or just prepend to the beginning else $(addressNode).prepend(delPointNode); } $(delPointNode).text(deliveryPoint); }); if(address.city){ var cityNode = $(addressNode).find("city"); if(!cityNode.length){ cityNode = document.createElement("city"); if(this.getEMLPosition(addressNode, "city")){ this.getEMLPosition(addressNode, "city").after(cityNode); } else{ $(addressNode).append(cityNode); } } $(cityNode).text(address.city); } else{ $(addressNode).find("city").remove(); } if(address.administrativeArea){ var adminAreaNode = $(addressNode).find("administrativearea"); if(!adminAreaNode.length){ adminAreaNode = document.createElement("administrativearea"); if(this.getEMLPosition(addressNode, "administrativearea")){ this.getEMLPosition(addressNode, "administrativearea").after(adminAreaNode); } else{ $(addressNode).append(adminAreaNode); } } $(adminAreaNode).text(address.administrativeArea); } else{ $(addressNode).find("administrativearea").remove(); } if(address.postalCode){ var postalcodeNode = $(addressNode).find("postalcode"); if(!postalcodeNode.length){ postalcodeNode = document.createElement("postalcode"); if(this.getEMLPosition(addressNode, "postalcode")){ this.getEMLPosition(addressNode, "postalcode").after(postalcodeNode); } else{ $(addressNode).append(postalcodeNode); } } $(postalcodeNode).text(address.postalCode); } else{ $(addressNode).find("postalcode").remove(); } if(address.country){ var countryNode = $(addressNode).find("country"); if(!countryNode.length){ countryNode = document.createElement("country"); if(this.getEMLPosition(addressNode, "country")){ this.getEMLPosition(addressNode, "country").after(countryNode); } else{ $(addressNode).append(countryNode); } } $(countryNode).text(address.country); } else{ $(addressNode).find("country").remove(); } }, this); if( this.get("address").length == 0 ){ $(objectDOM).find("address").remove(); } // phone[s] $(objectDOM).find("phone[phonetype='voice']").remove(); _.each(this.get("phone"), function(phone) { var phoneNode = $(document.createElement("phone")).attr("phonetype", "voice").text(phone); this.getEMLPosition(objectDOM, "phone").after(phoneNode); }, this); // fax[es] $(objectDOM).find("phone[phonetype='facsimile']").remove(); _.each(this.get("fax"), function(fax) { var faxNode = $(document.createElement("phone")).attr("phonetype", "facsimile").text(fax); this.getEMLPosition(objectDOM, "phone").after(faxNode); }, this); // electronicMailAddress[es] $(objectDOM).find("electronicmailaddress").remove(); _.each(this.get("email"), function(email) { var emailNode = document.createElement("electronicmailaddress"); this.getEMLPosition(objectDOM, "electronicmailaddress").after(emailNode); $(emailNode).text(email); }, this); // online URL[es] $(objectDOM).find("onlineurl").remove(); _.each(this.get("onlineUrl"), function(onlineUrl, i) { var urlNode = document.createElement("onlineurl"); this.getEMLPosition(objectDOM, "onlineurl").after(urlNode); $(urlNode).text(onlineUrl); }, this); //user ID var userId = Array.isArray(this.get("userId")) ? this.get("userId") : [this.get("userId")]; _.each(userId, function(id) { if(!id) return; var idNode = $(objectDOM).find("userid"); //Create the userid node if(!idNode.length){ idNode = $(document.createElement("userid")); this.getEMLPosition(objectDOM, "userid").after(idNode); } //If this is an orcid identifier, format it correctly if(this.isOrcid(id)){ // Add the directory attribute idNode.attr("directory", "https://orcid.org"); //If this ORCID does not start with "http" if( id.indexOf("http") == -1 ){ //If this is an ORCID with just the 16-digit numbers and hyphens, then add // the https://orcid.org/ prefix to it if( id.length == 19){ id = "https://orcid.org/" + id; } //If it starts with "orcid.org", then add the "https://" prefix else if( id.indexOf("orcid.org") == 0 ){ id = "https://" + id; } //If it starts with "www.orcid.org", then add the "https" prefix and remove the "www" else if( id.indexOf("www.orcid.org") == 0 ){ id = "https://" + id.replace("www.orcid.org", "orcid.org"); } } //If there is a "www", remove it if( id.indexOf("www.orcid.org") > -1 ){ id = id.replace("www.orcid.org", "orcid.org"); } //If it has the http:// prefix, add the 's' for secure protocol if( id.indexOf("http://") == 0){ id = id.replace("http", "https"); } } else{ idNode.attr("directory", "unknown"); } $(idNode).text(id); }, this); //Remove all the user id's if there aren't any in the model if( userId.length == 0 ){ $(objectDOM).find("userid").remove(); } // role //If this party type is not an associated party, then remove the role element if( type != "associatedParty" && type != "personnel" ){ $(objectDOM).find("role").remove(); } //Otherwise, change the value of the role element else { // If for some reason there is no role, create a default role if( !this.get("roles").length ){ var roles = ["Associated Party"]; } else { var roles = this.get("roles"); } _.each(roles, function(role, i){ var roleSerialized = $(objectDOM).find("role"); if(roleSerialized.length){ $(roleSerialized[i]).text(role) } else { roleSerialized = $(document.createElement("role")).text(role); this.getEMLPosition(objectDOM, "role").after( roleSerialized ); } }, this); } //XML id attribute this.createID(); //if(this.get("xmlID")) $(objectDOM).attr("id", this.get("xmlID")); //else // $(objectDOM).removeAttr("id"); // Remove empty (zero-length or whitespace-only) nodes $(objectDOM).find("*").filter(function() { return $.trim(this.innerHTML) === ""; } ).remove(); return objectDOM; }, /* * Adds this EMLParty model to it's parent EML211 model in the appropriate role array * * @return {boolean} - Returns true if the merge was successful, false if the merge was cancelled */ mergeIntoParent: function(){ //Get the type of EML Party, in relation to the parent model if(this.get("type") && this.get("type") != "associatedParty") var type = this.get("type"); else var type = "associatedParty"; //Update the list of EMLParty models in the parent model var parentEML = this.getParentEML(); if(parentEML.type != "EML") return false; //Add this model to the EML model var successfulAdd = parentEML.addParty(this); //Validate the model this.isValid(); return successfulAdd; }, isEmpty: function(){ // If we add any new fields, be sure to add the attribute here var attributes = ["userId", "fax", "phone", "onlineUrl", "email", "positionName", "organizationName"]; //Check each value in the model that gets serialized to see if there is a value for(var i in attributes) { //Get the value from the model for this attribute var modelValue = this.get(attributes[i]); //If this is an array, then we want to check if there are any values in it if( Array.isArray(modelValue) ){ if( modelValue.length > 0 ) return false; } //Otherwise, check if this value differs from the default value else if(this.get(attributes[i]) !== this.defaults()[attributes[i]]){ return false; } } //Check for a first and last name if( this.get("individualName") && (this.get("individualName").givenName || this.get("individualName").surName) ) return false; //Check for addresses var isAddress = false; if( this.get("address") ){ //Checks if there are any values anywhere in the address _.each(this.get("address"), function(address){ //Delivery point is an array so we need to check the first and second //values of that array if( address.administrativeArea || address.city || address.country || address.postalCode || (address.deliveryPoint && address.deliveryPoint.length && (address.deliveryPoint[0] || address.deliveryPoint[1]) ) ){ isAddress = true; } }); } //If we found an address value anywhere, then it is not empty if(isAddress) return false; //If we never found a value, then return true because this model is empty return true; }, /* * Returns the node in the given EML snippet that the given node type should be inserted after */ getEMLPosition: function(objectDOM, nodeName){ var nodeOrder = [ "individualname", "organizationname", "positionname", "address", "phone", "electronicmailaddress", "onlineurl", "userid", "role"]; var addressOrder = ["deliverypoint", "city", "administrativearea", "postalcode", "country"]; //If this is an address node, find the position within the address if( _.contains(addressOrder, nodeName) ){ nodeOrder = addressOrder; } var position = _.indexOf(nodeOrder, nodeName); if(position == -1) return $(objectDOM).children().last(); //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($(objectDOM).find(nodeOrder[i]).length) return $(objectDOM).find(nodeOrder[i]).last(); } return false; }, createID: function(){ this.set("xmlID", Math.ceil(Math.random() * (9999999999999999 - 1000000000000000) + 1000000000000000)); }, setType: function(){ if(this.get("roles")){ if(this.get("roles").length && !this.get("type")){ this.set("type", "associatedParty"); } } }, trickleUpChange: function(){ if ( this.get("parentModel") ) { MetacatUI.rootDataPackage.packageModel.set("changed", true); } }, removeFromParent: function(){ if( !this.get("parentModel") ) return; else if( typeof this.get("parentModel").removeParty != "function" ) return; this.get("parentModel").removeParty(this); this.set("removed", true); }, /* * Checks the values of the model to determine if it is EML-valid */ validate: function(){ var individualName = this.get("individualName") || {}, givenName = individualName.givenName || [], surName = individualName.surName || null; //If there are no values in this model that would be serialized, then the model is valid if( !this.get("organizationName") && !this.get("positionName") && !givenName.length && !surName && !this.get("address").length && !this.get("phone").length && !this.get("fax").length && !this.get("email").length && !this.get("onlineUrl").length && !this.get("userId").length){ return; } //The EMLParty must have either an organization name, position name, or surname. // It must ALSO have a type or role. if ( !this.get("organizationName") && !this.get("positionName") && (!this.get("individualName") || !surName ) ){ return { surName: "Either a last name, position name, or organization name is required.", positionName: "", organizationName: "" } } //If there is a first name and no last name, then this is not a valid individualName else if( (givenName.length && !surName) && this.get("organizationName") && this.get("positionName") ){ return { surName: "Provide a last name." } } }, isOrcid: function(username){ if(!username) return false; //If the ORCID URL is anywhere in this username string, it is an ORCID if(username.indexOf("orcid.org") > -1){ return true; } /* The ORCID checksum algorithm to determine is a character string is an ORCiD * http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier */ var total = 0, baseDigits = username.replace(/-/g, "").substr(0, 15); for(var i=0; i