Source: src/js/models/metadata/eml211/EMLParty.js

  1. define(["jquery", "underscore", "backbone", "models/DataONEObject"], (
  2. $,
  3. _,
  4. Backbone,
  5. DataONEObject,
  6. ) => {
  7. /**
  8. * @class EMLParty
  9. * @classcategory Models/Metadata/EML211
  10. * @classdesc EMLParty represents a single Party from the EML 2.1.1 and 2.2.0
  11. * metadata schema. This can be a person or organization.
  12. * @see https://eml.ecoinformatics.org/schema/eml-party_xsd.html#ResponsibleParty
  13. * @extends Backbone.Model
  14. * @constructor
  15. */
  16. const EMLParty = Backbone.Model.extend(
  17. /** @lends EMLParty.prototype */ {
  18. defaults: function () {
  19. return {
  20. objectXML: null,
  21. objectDOM: null,
  22. individualName: null,
  23. organizationName: null,
  24. positionName: null,
  25. address: [],
  26. phone: [],
  27. fax: [],
  28. email: [],
  29. onlineUrl: [],
  30. roles: [],
  31. references: null,
  32. userId: [],
  33. xmlID: null,
  34. type: null,
  35. typeOptions: [
  36. "associatedParty",
  37. "contact",
  38. "creator",
  39. "metadataProvider",
  40. "publisher",
  41. ],
  42. roleOptions: [
  43. "custodianSteward",
  44. "principalInvestigator",
  45. "collaboratingPrincipalInvestigator",
  46. "coPrincipalInvestigator",
  47. "user",
  48. ],
  49. parentModel: null,
  50. removed: false, //Indicates whether this model has been removed from the parent model
  51. };
  52. },
  53. initialize: function (options) {
  54. if (options && options.objectDOM)
  55. this.set(this.parse(options.objectDOM));
  56. if (!this.get("xmlID")) this.createID();
  57. this.on("change:roles", this.setType);
  58. },
  59. /*
  60. * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
  61. * Used during parse() and serialize()
  62. */
  63. nodeNameMap: function () {
  64. return {
  65. administrativearea: "administrativeArea",
  66. associatedparty: "associatedParty",
  67. deliverypoint: "deliveryPoint",
  68. electronicmailaddress: "electronicMailAddress",
  69. givenname: "givenName",
  70. individualname: "individualName",
  71. metadataprovider: "metadataProvider",
  72. onlineurl: "onlineUrl",
  73. organizationname: "organizationName",
  74. positionname: "positionName",
  75. postalcode: "postalCode",
  76. surname: "surName",
  77. userid: "userId",
  78. };
  79. },
  80. /*
  81. Parse the object DOM to create the model
  82. @param objectDOM the XML DOM to parse
  83. @return modelJSON the resulting model attributes object
  84. */
  85. parse: function (objectDOM) {
  86. if (!objectDOM) var objectDOM = this.get("objectDOM");
  87. var model = this,
  88. modelJSON = {};
  89. //Set the name
  90. var person = $(objectDOM).children("individualname, individualName");
  91. if (person.length) modelJSON.individualName = this.parsePerson(person);
  92. //Set the phone and fax numbers
  93. var phones = $(objectDOM).children("phone"),
  94. phoneNums = [],
  95. faxNums = [];
  96. phones.each(function (i, phone) {
  97. if ($(phone).attr("phonetype") == "voice")
  98. phoneNums.push($(phone).text());
  99. else if ($(phone).attr("phonetype") == "facsimile")
  100. faxNums.push($(phone).text());
  101. });
  102. modelJSON.phone = phoneNums;
  103. modelJSON.fax = faxNums;
  104. //Set the address
  105. var addresses = $(objectDOM).children("address") || [],
  106. addressesJSON = [];
  107. addresses.each(function (i, address) {
  108. addressesJSON.push(model.parseAddress(address));
  109. });
  110. modelJSON.address = addressesJSON;
  111. //Set the text fields
  112. modelJSON.organizationName =
  113. $(objectDOM).children("organizationname, organizationName").text() ||
  114. null;
  115. modelJSON.positionName =
  116. $(objectDOM).children("positionname, positionName").text() || null;
  117. // roles
  118. modelJSON.roles = [];
  119. $(objectDOM)
  120. .find("role")
  121. .each(function (i, role) {
  122. modelJSON.roles.push($(role).text());
  123. });
  124. //Set the id attribute
  125. modelJSON.xmlID = $(objectDOM).attr("id");
  126. //Email - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
  127. if (
  128. $(objectDOM).children("electronicmailaddress, electronicMailAddress")
  129. .length
  130. ) {
  131. modelJSON.email = _.map(
  132. $(objectDOM).children(
  133. "electronicmailaddress, electronicMailAddress",
  134. ),
  135. function (email) {
  136. return $(email).text();
  137. },
  138. );
  139. }
  140. //Online URL - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
  141. if ($(objectDOM).find("onlineurl, onlineUrl").length) {
  142. // modelJSON.onlineUrl = [$(objectDOM).find("onlineurl, onlineUrl").first().text()];
  143. modelJSON.onlineUrl = $(objectDOM)
  144. .find("onlineurl, onlineUrl")
  145. .map(function (i, v) {
  146. return $(v).text();
  147. })
  148. .get();
  149. }
  150. //User ID - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
  151. if ($(objectDOM).find("userid, userId").length) {
  152. modelJSON.userId = [
  153. $(objectDOM).find("userid, userId").first().text(),
  154. ];
  155. }
  156. return modelJSON;
  157. },
  158. parseNode: function (node) {
  159. if (!node || (Array.isArray(node) && !node.length)) return;
  160. this.set($(node)[0].localName, $(node).text());
  161. },
  162. parsePerson: function (personXML) {
  163. var person = {
  164. givenName: [],
  165. surName: "",
  166. salutation: [],
  167. },
  168. givenName = $(personXML).find("givenname, givenName"),
  169. surName = $(personXML).find("surname, surName"),
  170. salutations = $(personXML).find("salutation");
  171. //Concatenate all the given names into one, for now
  172. //TODO: Support multiple given names
  173. givenName.each(function (i, name) {
  174. if (i == 0) person.givenName[0] = "";
  175. person.givenName[0] += $(name).text() + " ";
  176. if (i == givenName.length - 1)
  177. person.givenName[0] = person.givenName[0].trim();
  178. });
  179. person.surName = surName.text();
  180. salutations.each(function (i, name) {
  181. person.salutation.push($(name).text());
  182. });
  183. return person;
  184. },
  185. parseAddress: function (addressXML) {
  186. var address = {},
  187. delPoint = $(addressXML).find("deliverypoint, deliveryPoint"),
  188. city = $(addressXML).find("city"),
  189. adminArea = $(addressXML).find(
  190. "administrativearea, administrativeArea",
  191. ),
  192. postalCode = $(addressXML).find("postalcode, postalCode"),
  193. country = $(addressXML).find("country");
  194. address.city = city.length ? city.text() : "";
  195. address.administrativeArea = adminArea.length ? adminArea.text() : "";
  196. address.postalCode = postalCode.length ? postalCode.text() : "";
  197. address.country = country.length ? country.text() : "";
  198. //Get an array of all the address line (or delivery point) values
  199. var addressLines = [];
  200. _.each(
  201. delPoint,
  202. function (addressLine, i) {
  203. addressLines.push($(addressLine).text());
  204. },
  205. this,
  206. );
  207. address.deliveryPoint = addressLines;
  208. return address;
  209. },
  210. serialize: function () {
  211. var objectDOM = this.updateDOM(),
  212. xmlString = objectDOM.outerHTML;
  213. //Camel-case the XML
  214. xmlString = this.formatXML(xmlString);
  215. return xmlString;
  216. },
  217. /*
  218. * Updates the attributes on this model based on the application user (the app UserModel)
  219. */
  220. createFromUser: function () {
  221. //Create the name from the user
  222. var name = this.get("individualName") || {};
  223. name.givenName = [MetacatUI.appUserModel.get("firstName")];
  224. name.surName = MetacatUI.appUserModel.get("lastName");
  225. this.set("individualName", name);
  226. //Get the email and username
  227. if (MetacatUI.appUserModel.get("email"))
  228. this.set("email", [MetacatUI.appUserModel.get("email")]);
  229. this.set("userId", [MetacatUI.appUserModel.get("username")]);
  230. },
  231. /*
  232. * Makes a copy of the original XML DOM and updates it with the new values from the model.
  233. */
  234. updateDOM: function () {
  235. var type = this.get("type") || "associatedParty",
  236. objectDOM = this.get("objectDOM");
  237. // If there is already an XML node for this model and it is the wrong type,
  238. // then replace the XML node contents
  239. if (objectDOM && objectDOM.nodeName != type.toUpperCase()) {
  240. objectDOM = $(document.createElement(type)).html(objectDOM.innerHTML);
  241. }
  242. // If there is already an XML node for this model and it is the correct type,
  243. // then simply clone the XML node
  244. else if (objectDOM) {
  245. objectDOM = objectDOM.cloneNode(true);
  246. }
  247. // Otherwise, create a new XML node
  248. else {
  249. objectDOM = document.createElement(type);
  250. }
  251. //There needs to be at least one individual name, organization name, or position name
  252. if (
  253. this.nameIsEmpty() &&
  254. !this.get("organizationName") &&
  255. !this.get("positionName")
  256. )
  257. return "";
  258. var name = this.get("individualName");
  259. if (name) {
  260. //Get the individualName node
  261. var nameNode = $(objectDOM).find("individualname");
  262. if (!nameNode.length) {
  263. nameNode = document.createElement("individualname");
  264. $(objectDOM).prepend(nameNode);
  265. }
  266. //Empty the individualName node
  267. $(nameNode).empty();
  268. // salutation[s]
  269. if (!Array.isArray(name.salutation) && name.salutation)
  270. name.salutation = [name.salutation];
  271. _.each(name.salutation, function (salutation) {
  272. $(nameNode).prepend("<salutation>" + salutation + "</salutation>");
  273. });
  274. //Given name
  275. if (!Array.isArray(name.givenName) && name.givenName)
  276. name.givenName = [name.givenName];
  277. _.each(name.givenName, function (givenName) {
  278. //If there is a given name string, create a givenName node
  279. if (typeof givenName == "string" && givenName) {
  280. $(nameNode).append("<givenname>" + givenName + "</givenname>");
  281. }
  282. });
  283. // surname
  284. if (name.surName)
  285. $(nameNode).append("<surname>" + name.surName + "</surname>");
  286. }
  287. //If there is no name set on the model, remove it from the DOM
  288. else {
  289. $(objectDOM).find("individualname").remove();
  290. }
  291. // organizationName
  292. if (this.get("organizationName")) {
  293. //Get the organization name node
  294. if ($(objectDOM).find("organizationname").length)
  295. var orgNameNode = $(objectDOM).find("organizationname").detach();
  296. else var orgNameNode = document.createElement("organizationname");
  297. //Insert the text
  298. $(orgNameNode).text(this.get("organizationName"));
  299. //If the DOM is empty, append it
  300. if (!$(objectDOM).children().length) $(objectDOM).append(orgNameNode);
  301. else {
  302. var insertAfter = this.getEMLPosition(
  303. objectDOM,
  304. "organizationname",
  305. );
  306. if (insertAfter && insertAfter.length)
  307. insertAfter.after(orgNameNode);
  308. else $(objectDOM).prepend(orgNameNode);
  309. }
  310. }
  311. //Remove the organization name node if there is no organization name
  312. else {
  313. var orgNameNode = $(objectDOM).find("organizationname").remove();
  314. }
  315. // positionName
  316. if (this.get("positionName")) {
  317. //Get the name node
  318. if ($(objectDOM).find("positionname").length)
  319. var posNameNode = $(objectDOM).find("positionname").detach();
  320. else var posNameNode = document.createElement("positionname");
  321. //Insert the text
  322. $(posNameNode).text(this.get("positionName"));
  323. //If the DOM is empty, append it
  324. if (!$(objectDOM).children().length) $(objectDOM).append(posNameNode);
  325. else {
  326. let insertAfter = this.getEMLPosition(objectDOM, "positionname");
  327. if (insertAfter) insertAfter.after(posNameNode);
  328. else $(objectDOM).prepend(posNameNode);
  329. }
  330. }
  331. //Remove the position name node if there is no position name
  332. else {
  333. $(objectDOM).find("positionname").remove();
  334. }
  335. // address
  336. _.each(
  337. this.get("address"),
  338. function (address, i) {
  339. var addressNode = $(objectDOM).find("address")[i];
  340. if (!addressNode) {
  341. addressNode = document.createElement("address");
  342. this.getEMLPosition(objectDOM, "address").after(addressNode);
  343. }
  344. //Remove all the delivery points since they'll be reserialized
  345. $(addressNode).find("deliverypoint").remove();
  346. _.each(address.deliveryPoint, function (deliveryPoint, ii) {
  347. if (!deliveryPoint) return;
  348. var delPointNode = $(addressNode).find("deliverypoint")[ii];
  349. if (!delPointNode) {
  350. delPointNode = document.createElement("deliverypoint");
  351. //Add the deliveryPoint node to the address node
  352. //Insert after the last deliveryPoint node
  353. var appendAfter = $(addressNode).find("deliverypoint")[ii - 1];
  354. if (appendAfter) $(appendAfter).after(delPointNode);
  355. //Or just prepend to the beginning
  356. else $(addressNode).prepend(delPointNode);
  357. }
  358. $(delPointNode).text(deliveryPoint);
  359. });
  360. if (address.city) {
  361. var cityNode = $(addressNode).find("city");
  362. if (!cityNode.length) {
  363. cityNode = document.createElement("city");
  364. if (this.getEMLPosition(addressNode, "city")) {
  365. this.getEMLPosition(addressNode, "city").after(cityNode);
  366. } else {
  367. $(addressNode).append(cityNode);
  368. }
  369. }
  370. $(cityNode).text(address.city);
  371. } else {
  372. $(addressNode).find("city").remove();
  373. }
  374. if (address.administrativeArea) {
  375. var adminAreaNode = $(addressNode).find("administrativearea");
  376. if (!adminAreaNode.length) {
  377. adminAreaNode = document.createElement("administrativearea");
  378. if (this.getEMLPosition(addressNode, "administrativearea")) {
  379. this.getEMLPosition(addressNode, "administrativearea").after(
  380. adminAreaNode,
  381. );
  382. } else {
  383. $(addressNode).append(adminAreaNode);
  384. }
  385. }
  386. $(adminAreaNode).text(address.administrativeArea);
  387. } else {
  388. $(addressNode).find("administrativearea").remove();
  389. }
  390. if (address.postalCode) {
  391. var postalcodeNode = $(addressNode).find("postalcode");
  392. if (!postalcodeNode.length) {
  393. postalcodeNode = document.createElement("postalcode");
  394. if (this.getEMLPosition(addressNode, "postalcode")) {
  395. this.getEMLPosition(addressNode, "postalcode").after(
  396. postalcodeNode,
  397. );
  398. } else {
  399. $(addressNode).append(postalcodeNode);
  400. }
  401. }
  402. $(postalcodeNode).text(address.postalCode);
  403. } else {
  404. $(addressNode).find("postalcode").remove();
  405. }
  406. if (address.country) {
  407. var countryNode = $(addressNode).find("country");
  408. if (!countryNode.length) {
  409. countryNode = document.createElement("country");
  410. if (this.getEMLPosition(addressNode, "country")) {
  411. this.getEMLPosition(addressNode, "country").after(
  412. countryNode,
  413. );
  414. } else {
  415. $(addressNode).append(countryNode);
  416. }
  417. }
  418. $(countryNode).text(address.country);
  419. } else {
  420. $(addressNode).find("country").remove();
  421. }
  422. },
  423. this,
  424. );
  425. if (this.get("address").length == 0) {
  426. $(objectDOM).find("address").remove();
  427. }
  428. // phone[s]
  429. $(objectDOM).find("phone[phonetype='voice']").remove();
  430. _.each(
  431. this.get("phone"),
  432. function (phone) {
  433. var phoneNode = $(document.createElement("phone"))
  434. .attr("phonetype", "voice")
  435. .text(phone);
  436. this.getEMLPosition(objectDOM, "phone").after(phoneNode);
  437. },
  438. this,
  439. );
  440. // fax[es]
  441. $(objectDOM).find("phone[phonetype='facsimile']").remove();
  442. _.each(
  443. this.get("fax"),
  444. function (fax) {
  445. var faxNode = $(document.createElement("phone"))
  446. .attr("phonetype", "facsimile")
  447. .text(fax);
  448. this.getEMLPosition(objectDOM, "phone").after(faxNode);
  449. },
  450. this,
  451. );
  452. // electronicMailAddress[es]
  453. $(objectDOM).find("electronicmailaddress").remove();
  454. _.each(
  455. this.get("email"),
  456. function (email) {
  457. var emailNode = document.createElement("electronicmailaddress");
  458. this.getEMLPosition(objectDOM, "electronicmailaddress").after(
  459. emailNode,
  460. );
  461. $(emailNode).text(email);
  462. },
  463. this,
  464. );
  465. // online URL[es]
  466. $(objectDOM).find("onlineurl").remove();
  467. _.each(
  468. this.get("onlineUrl"),
  469. function (onlineUrl, i) {
  470. var urlNode = document.createElement("onlineurl");
  471. this.getEMLPosition(objectDOM, "onlineurl").after(urlNode);
  472. $(urlNode).text(onlineUrl);
  473. },
  474. this,
  475. );
  476. //user ID
  477. var userId = this.getUserIdArray();
  478. _.each(
  479. userId,
  480. function (id) {
  481. if (!id) return;
  482. var idNode = $(objectDOM).find("userid");
  483. //Create the userid node
  484. if (!idNode.length) {
  485. idNode = $(document.createElement("userid"));
  486. this.getEMLPosition(objectDOM, "userid").after(idNode);
  487. }
  488. // If this is an orcid identifier, format it correctly
  489. const validOrcid = this.validateOrcid(id, true);
  490. // validOrcid will be false if the ORCID is invalid, and a correctly
  491. // formatted ORCID if it is valid
  492. if (validOrcid) {
  493. // Add the directory attribute
  494. idNode.attr("directory", "https://orcid.org");
  495. // Check the orcid ID and standardize it if possible
  496. id = validOrcid;
  497. } else {
  498. idNode.attr("directory", "unknown");
  499. }
  500. $(idNode).text(id);
  501. },
  502. this,
  503. );
  504. //Remove all the user id's if there aren't any in the model
  505. if (userId.length == 0) {
  506. $(objectDOM).find("userid").remove();
  507. }
  508. // role
  509. //If this party type is not an associated party, then remove the role element
  510. if (type != "associatedParty" && type != "personnel") {
  511. $(objectDOM).find("role").remove();
  512. }
  513. //Otherwise, change the value of the role element
  514. else {
  515. // If for some reason there is no role, create a default role
  516. if (!this.get("roles").length) {
  517. var roles = ["Associated Party"];
  518. } else {
  519. var roles = this.get("roles");
  520. }
  521. _.each(
  522. roles,
  523. function (role, i) {
  524. var roleSerialized = $(objectDOM).find("role");
  525. if (roleSerialized.length) {
  526. $(roleSerialized[i]).text(role);
  527. } else {
  528. roleSerialized = $(document.createElement("role")).text(role);
  529. this.getEMLPosition(objectDOM, "role").after(roleSerialized);
  530. }
  531. },
  532. this,
  533. );
  534. }
  535. //XML id attribute
  536. this.createID();
  537. //if(this.get("xmlID"))
  538. $(objectDOM).attr("id", this.get("xmlID"));
  539. //else
  540. // $(objectDOM).removeAttr("id");
  541. // Remove empty (zero-length or whitespace-only) nodes
  542. $(objectDOM)
  543. .find("*")
  544. .filter(function () {
  545. return $.trim(this.innerHTML) === "";
  546. })
  547. .remove();
  548. return objectDOM;
  549. },
  550. /*
  551. * Adds this EMLParty model to it's parent EML211 model in the appropriate role array
  552. *
  553. * @return {boolean} - Returns true if the merge was successful, false if the merge was cancelled
  554. */
  555. mergeIntoParent: function () {
  556. //Get the type of EML Party, in relation to the parent model
  557. if (this.get("type") && this.get("type") != "associatedParty")
  558. var type = this.get("type");
  559. else var type = "associatedParty";
  560. //Update the list of EMLParty models in the parent model
  561. var parentEML = this.getParentEML();
  562. if (parentEML.type != "EML") return false;
  563. //Add this model to the EML model
  564. var successfulAdd = parentEML.addParty(this);
  565. //Validate the model
  566. this.isValid();
  567. return successfulAdd;
  568. },
  569. isEmpty: function () {
  570. // If we add any new fields, be sure to add the attribute here
  571. var attributes = [
  572. "userId",
  573. "fax",
  574. "phone",
  575. "onlineUrl",
  576. "email",
  577. "positionName",
  578. "organizationName",
  579. ];
  580. //Check each value in the model that gets serialized to see if there is a value
  581. for (var i in attributes) {
  582. //Get the value from the model for this attribute
  583. var modelValue = this.get(attributes[i]);
  584. //If this is an array, then we want to check if there are any values in it
  585. if (Array.isArray(modelValue)) {
  586. if (modelValue.length > 0) return false;
  587. }
  588. //Otherwise, check if this value differs from the default value
  589. else if (this.get(attributes[i]) !== this.defaults()[attributes[i]]) {
  590. return false;
  591. }
  592. }
  593. //Check for a first and last name
  594. if (
  595. this.get("individualName") &&
  596. (this.get("individualName").givenName ||
  597. this.get("individualName").surName)
  598. )
  599. return false;
  600. //Check for addresses
  601. var isAddress = false;
  602. if (this.get("address")) {
  603. //Checks if there are any values anywhere in the address
  604. _.each(this.get("address"), function (address) {
  605. //Delivery point is an array so we need to check the first and second
  606. //values of that array
  607. if (
  608. address.administrativeArea ||
  609. address.city ||
  610. address.country ||
  611. address.postalCode ||
  612. (address.deliveryPoint &&
  613. address.deliveryPoint.length &&
  614. (address.deliveryPoint[0] || address.deliveryPoint[1]))
  615. ) {
  616. isAddress = true;
  617. }
  618. });
  619. }
  620. //If we found an address value anywhere, then it is not empty
  621. if (isAddress) return false;
  622. //If we never found a value, then return true because this model is empty
  623. return true;
  624. },
  625. /*
  626. * Returns the node in the given EML snippet that the given node type should be inserted after
  627. */
  628. getEMLPosition: function (objectDOM, nodeName) {
  629. var nodeOrder = [
  630. "individualname",
  631. "organizationname",
  632. "positionname",
  633. "address",
  634. "phone",
  635. "electronicmailaddress",
  636. "onlineurl",
  637. "userid",
  638. "role",
  639. ];
  640. var addressOrder = [
  641. "deliverypoint",
  642. "city",
  643. "administrativearea",
  644. "postalcode",
  645. "country",
  646. ];
  647. //If this is an address node, find the position within the address
  648. if (_.contains(addressOrder, nodeName)) {
  649. nodeOrder = addressOrder;
  650. }
  651. var position = _.indexOf(nodeOrder, nodeName);
  652. if (position == -1) return $(objectDOM).children().last();
  653. //Go through each node in the node list and find the position where this node will be inserted after
  654. for (var i = position - 1; i >= 0; i--) {
  655. if ($(objectDOM).find(nodeOrder[i]).length)
  656. return $(objectDOM).find(nodeOrder[i]).last();
  657. }
  658. return false;
  659. },
  660. createID: function () {
  661. this.set(
  662. "xmlID",
  663. Math.ceil(
  664. Math.random() * (9999999999999999 - 1000000000000000) +
  665. 1000000000000000,
  666. ),
  667. );
  668. },
  669. setType: function () {
  670. if (this.get("roles")) {
  671. if (this.get("roles").length && !this.get("type")) {
  672. this.set("type", "associatedParty");
  673. }
  674. }
  675. },
  676. trickleUpChange: function () {
  677. if (this.get("parentModel")) {
  678. MetacatUI.rootDataPackage.packageModel.set("changed", true);
  679. }
  680. },
  681. removeFromParent: function () {
  682. if (!this.get("parentModel")) return;
  683. else if (typeof this.get("parentModel").removeParty != "function")
  684. return;
  685. this.get("parentModel").removeParty(this);
  686. this.set("removed", true);
  687. },
  688. /*
  689. * Checks the values of the model to determine if it is EML-valid
  690. */
  691. validate: function () {
  692. var individualName = this.get("individualName") || {},
  693. givenName = individualName.givenName || [],
  694. surName = individualName.surName || null,
  695. errors = {};
  696. //If there are no values in this model that would be serialized, then the model is valid
  697. if (
  698. !this.get("organizationName") &&
  699. !this.get("positionName") &&
  700. !givenName[0]?.length &&
  701. !surName &&
  702. !this.get("address").length &&
  703. !this.get("phone").length &&
  704. !this.get("fax").length &&
  705. !this.get("email").length &&
  706. !this.get("onlineUrl").length &&
  707. !this.get("userId").length
  708. ) {
  709. return;
  710. }
  711. //The EMLParty must have either an organization name, position name, or surname.
  712. // It must ALSO have a type or role.
  713. if (
  714. !this.get("organizationName") &&
  715. !this.get("positionName") &&
  716. (!this.get("individualName") || !surName)
  717. ) {
  718. errors = {
  719. surName:
  720. "Either a last name, position name, or organization name is required.",
  721. positionName: "",
  722. organizationName: "",
  723. };
  724. }
  725. //If there is a first name and no last name, then this is not a valid individualName
  726. else if (
  727. givenName[0]?.length &&
  728. !surName &&
  729. this.get("organizationName") &&
  730. this.get("positionName")
  731. ) {
  732. errors = { surName: "Provide a last name." };
  733. }
  734. //Check that each required field has a value. Required fields are configured in the {@link AppConfig}
  735. let roles =
  736. this.get("type") == "associatedParty"
  737. ? this.get("roles")
  738. : [this.get("type")];
  739. for (let role of roles) {
  740. let requiredFields = MetacatUI.appModel.get(
  741. "emlEditorRequiredFields_EMLParty",
  742. )[role];
  743. requiredFields?.forEach((field) => {
  744. let currentVal = this.get(field);
  745. if (!currentVal || !currentVal?.length) {
  746. errors[field] =
  747. `Provide a${["a", "e", "i", "o", "u"].includes(field.charAt(0)) ? "n " : " "} ${field}. `;
  748. }
  749. });
  750. }
  751. // If there is an email address, validate it
  752. const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  753. const email = this.get("email");
  754. if (email?.length) {
  755. email.forEach((emailAddress) => {
  756. if (!emailAddress.match(emailRegex)) {
  757. errors.email = "Provide a valid email address.";
  758. }
  759. });
  760. }
  761. // If there is an ORCID, ensure it is valid
  762. const userId = this.getUserIdArray();
  763. userId.forEach((id) => {
  764. if (this.isOrcid(id) && !this.validateOrcid(id)) {
  765. errors.userId =
  766. "Provide a valid ORCID in the format https://orcid.org/0000-0000-0000-0000.";
  767. }
  768. });
  769. return Object.keys(errors)?.length ? errors : false;
  770. },
  771. /**
  772. * Get the userId attribute and ensure it is an array
  773. * @returns {string[]} - An array of userIds
  774. * @since 2.32.0
  775. */
  776. getUserIdArray() {
  777. const userId = this.get("userId");
  778. if (!userId) return [];
  779. if (Array.isArray(userId)) return userId;
  780. return [userId];
  781. },
  782. /**
  783. * Validate an ORCID string. The following formats are valid according to
  784. * {@link https://support.orcid.org/hc/en-us/articles/17697515256855-I-entered-my-ORCID-iD-in-a-form-and-it-said-it-s-invalid}.
  785. * - Full URL (http): http://orcid.org/0000-0000-0000-0000
  786. * - Full URL (https): https://orcid.org/0000-0000-0000-0000
  787. * - Numbers only, with hyphens: 0000-0000-0000-0000.
  788. *
  789. * The last character in the ORCID iD is a checksum. This checksum must be
  790. * the digits 0-9 or the letter X, which represents the value 10.
  791. * @param {string} orcid - The ORCID iD to validate
  792. * @param {boolean} standardize - If true, the ORCID iD will be
  793. * standardized to the https://orcid.org/0000-0000-0000-0000 format.
  794. * @returns {string|boolean} - Returns false if the ORCID iD is invalid, or
  795. * the string if it is valid. If standardize is true, the returned orcid
  796. * will be the standardized URL.
  797. * @since 2.32.0
  798. */
  799. validateOrcid(orcid, standardize = false) {
  800. // isOrcid doesn't allow for the id without orcid.org
  801. if (!this.isOrcid(orcid) && !/^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$/) {
  802. return false;
  803. }
  804. // Find the 0000-0000-0000-0000 part of the string
  805. const id = orcid.match(/\d.*[\dX]$/)?.[0];
  806. // The ORCID must follow the 0000-0000-0000-0000 format exactly
  807. if (!id?.match(/^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$/)) return false;
  808. // Only the digits + hypen pattern is valid
  809. if (id === orcid && !standardize) return orcid;
  810. // Assuming the ORCID is valid at this point, we can standardize it
  811. if (standardize) return `https://orcid.org/${id}`;
  812. // Both the HTTP and HTTPS URL formats are valid
  813. if (
  814. orcid.match(/^https?:\/\/orcid.org\/\d{4}-\d{4}-\d{4}-\d{3}[\dX]$/)
  815. ) {
  816. return orcid;
  817. }
  818. // Remaining options are that the ORCID includes orcid.org but not the
  819. // entire https or http URL, which makes it invalid.
  820. return orcid;
  821. },
  822. isOrcid: function (username) {
  823. if (!username) return false;
  824. //If the ORCID URL is anywhere in this username string, it is an ORCID
  825. if (username.indexOf("orcid.org") > -1) {
  826. return true;
  827. }
  828. /* The ORCID checksum algorithm to determine is a character string is an ORCiD
  829. * http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
  830. */
  831. var total = 0,
  832. baseDigits = username.replace(/-/g, "").substr(0, 15);
  833. for (var i = 0; i < baseDigits.length; i++) {
  834. var digit = parseInt(baseDigits.charAt(i));
  835. total = (total + digit) * 2;
  836. }
  837. var remainder = total % 11,
  838. result = (12 - remainder) % 11,
  839. checkDigit = result == 10 ? "X" : result.toString(),
  840. isOrcid = checkDigit == username.charAt(username.length - 1);
  841. return isOrcid;
  842. },
  843. /*
  844. * Clones all the values of this array into a new JS Object.
  845. * Special care is needed for nested objects and arrays
  846. * This is helpful when copying this EMLParty to another role in the EML
  847. */
  848. copyValues: function () {
  849. //Get a JSON object of all the model copyValues
  850. var modelValues = this.toJSON();
  851. //Go through each model value and properly clone the arrays
  852. _.each(Object.keys(modelValues), function (key, i) {
  853. //Clone the array via slice()
  854. if (Array.isArray(modelValues[key]))
  855. modelValues[key] = modelValues[key].slice(0);
  856. });
  857. //Individual Names are objects, so properly clone them
  858. if (modelValues.individualName) {
  859. modelValues.individualName = Object.assign(
  860. {},
  861. modelValues.individualName,
  862. );
  863. }
  864. //Addresses are objects, so properly clone them
  865. if (modelValues.address.length) {
  866. _.each(modelValues.address, function (address, i) {
  867. modelValues.address[i] = Object.assign({}, address);
  868. //The delivery point is an array of strings, so properly clone the array
  869. if (Array.isArray(modelValues.address[i].deliveryPoint))
  870. modelValues.address[i].deliveryPoint =
  871. modelValues.address[i].deliveryPoint.slice(0);
  872. });
  873. }
  874. return modelValues;
  875. },
  876. /**
  877. * getName - For an individual, returns the first and last name as a string. Otherwise,
  878. * returns the organization or position name.
  879. *
  880. * @return {string} Returns the name of the party or an empty string if one cannot be found
  881. *
  882. * @since 2.15.0
  883. */
  884. getName: function () {
  885. return this.get("individualName")
  886. ? this.get("individualName").givenName +
  887. " " +
  888. this.get("individualName").surName
  889. : this.get("organizationName") || this.get("positionName") || "";
  890. },
  891. /**
  892. * Return the EML Party as a CSL JSON object. See
  893. * {@link https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#names}.
  894. * @return {object} The CSL JSON object
  895. * @since 2.23.0
  896. */
  897. toCSLJSON: function () {
  898. const name = this.get("individualName");
  899. const csl = {
  900. family: name?.surName || null,
  901. given: name?.givenName || null,
  902. literal:
  903. this.get("organizationName") || this.get("positionName") || "",
  904. "dropping-particle": name?.salutation || null,
  905. };
  906. // If any of the fields are arrays, join them with a space
  907. for (const key in csl) {
  908. if (Array.isArray(csl[key])) {
  909. csl[key] = csl[key].join(" ");
  910. }
  911. }
  912. return csl;
  913. },
  914. /*
  915. * function nameIsEmpty - Returns true if the individualName set on this
  916. * model contains only empty values. Otherwise, returns false. This is just a
  917. * shortcut for manually checking each name field individually.
  918. *
  919. * @return {boolean}
  920. */
  921. nameIsEmpty: function () {
  922. var name = this.get("individualName");
  923. if (!name || typeof name != "object") return true;
  924. //Check if there are given names
  925. var givenName = name.givenName,
  926. givenNameEmpty = false;
  927. if (
  928. !givenName ||
  929. (Array.isArray(givenName) && givenName.length == 0) ||
  930. (typeof givenName == "string" && givenName.trim().length == 0)
  931. )
  932. givenNameEmpty = true;
  933. //Check if there are no sur names
  934. var surName = name.surName,
  935. surNameEmpty = false;
  936. if (
  937. !surName ||
  938. (Array.isArray(surName) && surName.length == 0) ||
  939. (typeof surName == "string" && surName.trim().length == 0)
  940. )
  941. surNameEmpty = true;
  942. //Check if there are no salutations
  943. var salutation = name.salutation,
  944. salutationEmpty = false;
  945. if (
  946. !salutation ||
  947. (Array.isArray(salutation) && salutation.length == 0) ||
  948. (typeof salutation == "string" && salutation.trim().length == 0)
  949. )
  950. salutationEmpty = true;
  951. if (givenNameEmpty && surNameEmpty && salutationEmpty) return true;
  952. else return false;
  953. },
  954. /*
  955. * Climbs up the model heirarchy until it finds the EML model
  956. *
  957. * @return {EML211 or false} - Returns the EML 211 Model or false if not found
  958. */
  959. getParentEML: function () {
  960. var emlModel = this.get("parentModel"),
  961. tries = 0;
  962. while (emlModel.type !== "EML" && tries < 6) {
  963. emlModel = emlModel.get("parentModel");
  964. tries++;
  965. }
  966. if (emlModel && emlModel.type == "EML") return emlModel;
  967. else return false;
  968. },
  969. /**
  970. * @type {object[]}
  971. * @property {string} label - The name of the party category to display to the user
  972. * @property {string} dataCategory - The string that is used to represent this party. This value
  973. * should exactly match one of the strings listed in EMLParty typeOptions or EMLParty roleOptions.
  974. * @property {string} description - An optional description to display below the label to help the user
  975. * with this category.
  976. * @property {boolean} createFromUser - If set to true, the information from the logged-in user will be
  977. * used to create an EML party for this category if none exist already when the view loads.
  978. * @property {number} limit - If the number of parties allowed for this category is not unlimited,
  979. * then limit should be set to the maximum allowable number.
  980. * @since 2.21.0
  981. */
  982. partyTypes: [
  983. {
  984. label: "Dataset Creators (Authors/Owners/Originators)",
  985. dataCategory: "creator",
  986. description:
  987. "Each person or organization listed as a Creator will be listed in the data" +
  988. " citation. At least one person, organization, or position with a 'Creator'" +
  989. " role is required.",
  990. createFromUser: true,
  991. },
  992. {
  993. label: "Contact",
  994. dataCategory: "contact",
  995. createFromUser: true,
  996. },
  997. {
  998. label: "Principal Investigators",
  999. dataCategory: "principalInvestigator",
  1000. },
  1001. {
  1002. label: "Co-Principal Investigators",
  1003. dataCategory: "coPrincipalInvestigator",
  1004. },
  1005. {
  1006. label: "Collaborating-Principal Investigators",
  1007. dataCategory: "collaboratingPrincipalInvestigator",
  1008. },
  1009. {
  1010. label: "Metadata Provider",
  1011. dataCategory: "metadataProvider",
  1012. },
  1013. {
  1014. label: "Custodians/Stewards",
  1015. dataCategory: "custodianSteward",
  1016. },
  1017. {
  1018. label: "Publisher",
  1019. dataCategory: "publisher",
  1020. description: "Only one publisher can be specified.",
  1021. limit: 1,
  1022. },
  1023. {
  1024. label: "Users",
  1025. dataCategory: "user",
  1026. },
  1027. ],
  1028. formatXML: function (xmlString) {
  1029. return DataONEObject.prototype.formatXML.call(this, xmlString);
  1030. },
  1031. },
  1032. );
  1033. return EMLParty;
  1034. });