Skip to content
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

File Hierarchy clean up #2496

Merged
merged 9 commits into from
Sep 9, 2024
Merged

File Hierarchy clean up #2496

merged 9 commits into from
Sep 9, 2024

Conversation

rushirajnenuji
Copy link
Member

Move bytestoSize to Utilities Ref #2484
[WIP] FH ES Lint issues #2483

Copy link
Contributor

@github-actions github-actions bot left a 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] <func-names> reported by reviewdog 🐶
Unexpected unnamed function.

this.on("change:size", function () {


🚫 [eslint] <prefer-arrow-callback> reported by reviewdog 🐶
Unexpected function expression.

this.on("change:size", function () {
var size = Utilities.bytesToSize(model.get("size"));
model.set("sizeStr", size);
});


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var size = Utilities.bytesToSize(model.get("size"));


🚫 [eslint] <vars-on-top> reported by reviewdog 🐶
All 'var' declarations must be at the top of the function scope.

var size = Utilities.bytesToSize(model.get("size"));


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var size = Utilities.bytesToSize(model.get("size"));


🚫 [eslint] <eqeqeq> reported by reviewdog 🐶
Expected '===' and instead saw '=='.

if (this.type == "DataONEObject")


🚫 [eslint] <block-scoped-var> reported by reviewdog 🐶
'totalSize' declared on line 1807 column 15 is used outside of binding context.

this.set("totalSize", totalSize);


🚫 [eslint] <block-scoped-var> reported by reviewdog 🐶
'totalSize' declared on line 1809 column 15 is used outside of binding context.

this.set("totalSize", totalSize);


🚫 [eslint] <block-scoped-var> reported by reviewdog 🐶
'totalSize' declared on line 1807 column 15 is used outside of binding context.

return totalSize;


🚫 [eslint] <block-scoped-var> reported by reviewdog 🐶
'totalSize' declared on line 1809 column 15 is used outside of binding context.

return totalSize;


🚫 [eslint] <object-shorthand> reported by reviewdog 🐶
Expected method shorthand.

parseResourceMapField: function (json) {
if (typeof json.resourceMap == "string") {
return json.resourceMap.trim();
} else if (Array.isArray(json.resourceMap)) {
let newResourceMapIds = [];
_.each(json.resourceMap, function (rMapId) {
if (typeof rMapId == "string") {
newResourceMapIds.push(rMapId.trim());
}
});
return newResourceMapIds;
}
//If nothing works so far, return an empty array
return [];
},


🚫 [eslint] <spaced-comment> reported by reviewdog 🐶
Expected exception block, space or tab after '//' in comment.

//If nothing works so far, return an empty array


🚫 [eslint] <prefer-arrow-callback> reported by reviewdog 🐶
Unexpected function expression.

], function (
$,
_,
Backbone,
LocalForage,
DataPackage,
Utilities,
DataONEObject,
PackageModel,
ScienceMetadata,
EML211,
Package,
DataItemView,
DownloadButtonView,
DataPackageTemplate,
DataPackageStartTemplate,
DataPackageHeaderTemplate,
) {
"use strict";
/**
* @class DataPackageView
* @classdesc The main view of a Data Package in MetacatUI. The view is
* a file/folder browser
* @classcategory Views
* @screenshot views/DataPackageView.png
* @extends Backbone.View
*/
var DataPackageView = Backbone.View.extend(
/** @lends DataPackageView.prototype */ {
type: "DataPackage",
tagName: "table",
className: "table table-striped table-hover",
id: "data-package-table",
events: {
"click .toggle-rows": "toggleRows", // Show/hide rows associated with event's metadata row
"click .message-row .addFiles": "handleAddFiles",
"click .expand-control": "expand",
"click .collapse-control": "collapse",
"click .d1package-expand": "expandAll",
"click .d1package-collapse": "collapseAll",
},
subviews: {},
/**
* A reference to the parent EditorView that contains this view
* @type EditorView
* @since 2.15.0
*/
parentEditorView: null,
template: _.template(DataPackageTemplate),
startMessageTemplate: _.template(DataPackageStartTemplate),
dataPackageHeaderTemplate: _.template(DataPackageHeaderTemplate),
// Models waiting for their parent folder to be rendered, hashed by parent id:
// {'parentid': [model1, model2, ...]}
delayedModels: {},
/* Flag indicating the open or closed state of the package rows */
isOpen: true,
initialize: function (options) {
if (options === undefined || !options) var options = {};
if (!options.edit) {
//The edit option will allow the user to edit the table
this.edit = options.edit || false;
this.mode = "view";
this.packageId = options.packageId || null;
this.memberId = options.memberId || null;
this.attributes = options.attributes || null;
this.dataPackage = options.dataPackage || new DataPackage();
this.dataEntities = options.dataEntities || new Array();
this.disablePackageDownloads =
options.disablePackageDownloads || false;
this.currentlyViewing = options.currentlyViewing || null;
this.parentEditorView = options.parentView || null;
this.title = options.title || "";
this.packageTitle = options.packageTitle || "";
this.nested =
typeof options.nested === "undefined" ? false : options.nested;
this.metricsModel = options.metricsModel;
// set the package model
this.packageModel = this.dataPackage.packageModel;
this.listenTo(this.packageModel, "changeAll", this.render);
} else {
//Get the options sent to this view
if (typeof options == "object") {
//The edit option will allow the user to edit the table
this.edit = options.edit || false;
this.mode = "edit";
//The data package to render
this.dataPackage = options.dataPackage || new DataPackage();
this.parentEditorView = options.parentEditorView || null;
}
//Create a new DataPackage collection if one wasn't sent
else if (!this.dataPackage) {
this.dataPackage = new DataPackage();
}
return this;
}
},
/**
* Render the DataPackage HTML
*/
render: function () {
this.$el.addClass("download-contents table-condensed");
this.$el.append(
this.template({
edit: this.edit,
dataPackageFiltering:
MetacatUI.appModel.get("dataPackageFiltering") || false,
dataPackageSorting:
MetacatUI.appModel.get("dataPackageSorting") || false,
loading: MetacatUI.appView.loadingTemplate({
msg: "Loading files table... ",
}),
id: this.dataPackage.get("id"),
title: this.title || "Files in this dataset",
classes: "download-contents table-striped table-condensed table",
}),
);
if (this.edit) {
// Listen for add events because models are being merged
this.listenTo(this.dataPackage, "add", this.addOne);
this.listenTo(this.dataPackage, "fileAdded", this.addOne);
}
// Render the current set of models in the DataPackage
this.addAll();
if (this.edit) {
//If this is a new data package, then display a message and button
if (
(this.dataPackage.length == 1 &&
this.dataPackage.models[0].isNew()) ||
!this.dataPackage.length
) {
var messageRow = this.startMessageTemplate();
this.$("tbody").append(messageRow);
this.listenTo(this.dataPackage, "add", function () {
this.$(".message-row").remove();
});
}
//Render the Share control(s)
this.renderShareControl();
} else {
// check for nessted datasets
if (this.nested) {
this.getNestedPackages();
}
}
return this;
},
/**
* Add a single DataItemView row to the DataPackageView
*/
addOne: function (item, dataPackage) {
if (!item) return false;
//Don't add duplicate rows
if (this.$(".data-package-item[data-id='" + item.id + "']").length)
return;
// Don't add data package
if (
item.get("formatType") == "RESOURCE" ||
item.get("type") == "DataPackage"
) {
return;
}
var dataItemView, scimetaParent, parentRow, delayed_models;
if (_.contains(Object.keys(this.subviews), item.id)) {
return false; // Don't double render
}
var itemPath = null,
view = this;
if (!_.isEmpty(this.atLocationObj)) {
itemPath = this.atLocationObj[item.get("id")];
if (itemPath[0] != "/") {
itemPath = "/" + itemPath;
}
}
// get the data package id
if (typeof dataPackage !== "undefined") {
var dataPackageId = dataPackage.id;
}
if (typeof dataPackageId === "undefined")
dataPackageId = this.dataPackage.id;
var insertInfoIcon = this.edit
? false
: view.dataEntities.includes(item.id);
dataItemView = new DataItemView({
model: item,
metricsModel: this.metricsModel,
itemPath: itemPath,
insertInfoIcon: insertInfoIcon,
currentlyViewing: this.currentlyViewing,
mode: this.mode,
parentEditorView: this.parentEditorView,
dataPackageId: dataPackageId,
});
this.subviews[item.id] = dataItemView; // keep track of all views
if (this.edit) {
//Get the science metadata that documents this item
scimetaParent = item.get("isDocumentedBy");
//If this item is not documented by a science metadata object,
// and there is only one science metadata doc in the package, then assume it is
// documented by that science metadata doc
if (typeof scimetaParent == "undefined" || !scimetaParent) {
//Get the science metadata models
var metadataIds = this.dataPackage.sciMetaPids;
//If there is only one science metadata model in the package, then use it
if (metadataIds.length == 1) scimetaParent = metadataIds[0];
}
//Otherwise, get the first science metadata doc that documents this object
else {
scimetaParent = scimetaParent[0];
}
if (
scimetaParent == item.get("id") ||
(!scimetaParent && item.get("type") == "Metadata")
) {
// This is a metadata folder row, append it to the table
this.$el.append(dataItemView.render().el);
// Render any delayed models if this is the parent
if (_.contains(Object.keys(this.delayedModels), dataItemView.id)) {
delayed_models = this.delayedModels[dataItemView.id];
_.each(delayed_models, this.addOne, this);
}
} else {
// Find the parent row by it's id, stored in a custom attribute
if (scimetaParent)
parentRow = this.$("[data-id='" + scimetaParent + "']");
if (typeof parentRow !== "undefined" && parentRow.length) {
// This is a data row, insert below it's metadata parent folder
parentRow.after(dataItemView.render().el);
// Remove it from the delayedModels list if necessary
if (_.contains(Object.keys(this.delayedModels), scimetaParent)) {
delayed_models = this.delayedModels[scimetaParent];
var index = _.indexOf(delayed_models, item);
delayed_models = delayed_models.splice(index, 1);
// Put the shortened array back if delayed models remains
if (delayed_models.length > 0) {
this.delayedModels[scimetaParent] = delayed_models;
} else {
this.delayedModels[scimetaParent] = undefined;
}
}
this.trigger("addOne");
} else {
console.warn(
"Couldn't render " +
item.id +
". Delayed until parent is rendered.",
);
// Postpone the data row until the parent is rendered
delayed_models = this.delayedModels[scimetaParent];
// Delay the model rendering if it isn't already delayed
if (typeof delayed_models !== "undefined") {
if (!_.contains(delayed_models, item)) {
delayed_models.push(item);
this.delayedModels[scimetaParent] = delayed_models;
}
} else {
delayed_models = [];
delayed_models.push(item);
this.delayedModels[scimetaParent] = delayed_models;
}
}
}
} else {
// This is a metadata folder row, append it to the table
this.$el.append(dataItemView.render().el);
this.trigger("addOne");
}
},
/**
* Render the Data Package View and insert it into this view
*/
renderDataPackage: function () {
var view = this;
if (MetacatUI.rootDataPackage.packageModel.isNew()) {
view.renderMember(this.model);
}
// As the root collection is updated with models, render the UI
this.listenTo(MetacatUI.rootDataPackage, "add", function (model) {
if (!model.get("synced") && model.get("id"))
this.listenTo(model, "sync", view.renderMember);
else if (model.get("synced")) view.renderMember(model);
//Listen for changes on this member
model.on("change:fileName", model.addToUploadQueue);
});
//Render the Data Package view
this.dataPackageView = new DataPackageView({
edit: true,
dataPackage: MetacatUI.rootDataPackage,
parentEditorView: this,
});
//Render the view
var $packageTableContainer = this.$("#data-package-container");
$packageTableContainer.html(this.dataPackageView.render().el);
//Make the view resizable on the bottom
var handle = $(document.createElement("div"))
.addClass("ui-resizable-handle ui-resizable-s")
.attr("title", "Drag to resize")
.append(
$(document.createElement("i")).addClass("icon icon-caret-down"),
);
$packageTableContainer.after(handle);
$packageTableContainer.resizable({
handles: { s: handle },
minHeight: 100,
maxHeight: 900,
resize: function () {
view.emlView.resizeTOC();
},
});
var tableHeight = ($(window).height() - $("#Navbar").height()) * 0.4;
$packageTableContainer.css("height", tableHeight + "px");
var table = this.dataPackageView.$el;
this.listenTo(this.dataPackageView, "addOne", function () {
if (
table.outerHeight() > $packageTableContainer.outerHeight() &&
table.outerHeight() < 220
) {
$packageTableContainer.css(
"height",
table.outerHeight() + handle.outerHeight(),
);
if (this.emlView) this.emlView.resizeTOC();
}
});
if (this.emlView) this.emlView.resizeTOC();
//Save the view as a subview
this.subviews.push(this.dataPackageView);
this.listenTo(
MetacatUI.rootDataPackage.packageModel,
"change:childPackages",
this.renderChildren,
);
},
/**
* Add all rows to the DataPackageView
*/
addAll: function () {
this.$el.find("#data-package-table-body").html(""); // clear the table first
this.dataPackage.sort();
if (!this.edit) {
var atLocationObj = this.dataPackage.getAtLocation();
this.atLocationObj = atLocationObj;
// form path to D1 object dictionary
if (
this.atLocationObj !== undefined &&
!_.isEmpty(this.atLocationObj)
) {
var filePathObj = new Object();
this.dataPackage.each(function (item) {
if (!Object.keys(this.atLocationObj).includes(item.id)) {
this.atLocationObj[item.id] = "/";
}
}, this);
for (let key of Object.keys(this.atLocationObj)) {
var path = this.atLocationObj[key];
var pathArray = path.split("/");
pathArray.pop();
var parentPath = pathArray.join("/");
if (filePathObj.hasOwnProperty(parentPath)) {
filePathObj[parentPath].push(key);
} else {
filePathObj[parentPath] = new Array();
filePathObj[parentPath].push(key);
}
}
}
// add top level data package row to the package table
var tableRow = null,
view = this,
title = this.packageTitle,
packageUrl = null;
if (title === "") {
let metadataObj = _.filter(this.dataPackage.models, function (m) {
return m.get("id") == view.currentlyViewing;
});
if (metadataObj.length > 0) {
title = metadataObj[0].get("title");
let metaId = metadataObj[0].get("id");
this.metaId = metaId;
} else {
title = this.dataPackage.get("id");
}
}
let titleTooltip = title;
title =
title.length > 150
? title.slice(0, 75) +
"..." +
title.slice(title.length - 75, title.length)
: title;
// set the package URL
if (MetacatUI.appModel.get("packageServiceUrl"))
packageUrl =
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(view.dataPackage.id);
var disablePackageDownloads = this.disablePackageDownloads;
tableRow = this.dataPackageHeaderTemplate({
id: view.dataPackage.id,
title: title,
titleTooltip: titleTooltip,
downloadUrl: packageUrl,
disablePackageDownloads: disablePackageDownloads,
disablePackageUrl: true,
});
this.$el.append(tableRow);
if (this.atLocationObj !== undefined && filePathObj !== undefined) {
// sort the filePath by length
var sortedFilePathObj = Object.keys(filePathObj)
.sort()
.reduce((obj, key) => {
obj[key] = filePathObj[key];
return obj;
}, {});
this.sortedFilePathObj = sortedFilePathObj;
this.addFilesAndFolders(sortedFilePathObj);
} else {
this.dataPackage.each(this.addOne, this, this.dataPackage);
}
} else {
this.dataPackage.each(this.addOne, this);
}
},
/**
* Add all the files and folders
*/
addFilesAndFolders: function (sortedFilePathObj) {
if (!sortedFilePathObj) return false;
var insertedPath = new Array();
let pathMap = new Object();
pathMap[""] = "";
for (let key of Object.keys(sortedFilePathObj)) {
// add folder
var pathArray = key.split("/");
//skip the first empty value
for (let i = 0; i < pathArray.length; i++) {
if (pathArray[i].length < 1) continue;
if (!(pathArray[i] in pathMap)) {
// insert path
var dataItemView, itemPath;
// root
if (i == 0) {
itemPath = "";
} else {
itemPath = pathMap[pathArray[i - 1]];
}
dataItemView = new DataItemView({
mode: this.mode,
itemName: pathArray[i],
itemPath: itemPath,
itemType: "folder",
parentEditorView: this.parentEditorView,
dataPackageId: this.dataPackage.id,
});
this.subviews[pathArray[i]] = dataItemView; // keep track of all views
this.$el.append(dataItemView.render().el);
this.trigger("addOne");
pathMap[pathArray[i]] = itemPath + "/" + pathArray[i];
}
}
// add files in the folder
var itemArray = sortedFilePathObj[key];
// Add metadata object at the top of the file table
if (
key == "" &&
this.metaId !== "undefined" &&
itemArray.includes(this.metaId)
) {
let item = this.metaId;
this.addOne(this.dataPackage.get(item));
}
for (let i = 0; i < itemArray.length; i++) {
let item = itemArray[i];
this.addOne(this.dataPackage.get(item));
}
}
},
/**
Remove the subview represented by the given model item.
@param item The model representing the sub view to be removed
*/
removeOne: function (item) {
if (_.contains(Object.keys(this.subviews), item.id)) {
// Remove the view and the its reference in the subviews list
this.subviews[item.id].remove();
delete this.subviews[item.id];
}
},
handleAddFiles: function (e) {
//Pass this on to the DataItemView for the root data package
this.$(".data-package-item.folder")
.first()
.data("view")
.handleAddFiles(e);
},
/**
* Renders a control that opens the AccessPolicyView for editing permissions on this package
* @since 2.15.0
*/
renderShareControl: function () {
if (
this.parentEditorView &&
!this.parentEditorView.isAccessPolicyEditEnabled()
) {
this.$("#data-package-table-share").remove();
}
},
/**
* Close subviews as needed
*/
onClose: function () {
// Close each subview
_.each(
Object.keys(this.subviews),
function (id) {
var subview = this.subviews[id];
subview.onClose();
},
this,
);
//Reset the subviews from the view completely (by removing it from the prototype)
this.__proto__.subviews = {};
},
/**
Show or hide the data rows associated with the event row science metadata
*/
toggleRows: function (event) {
if (this.isOpen) {
// Get the DataItemView associated with each id
_.each(
Object.keys(this.subviews),
function (id) {
var subview = this.subviews[id];
if (subview.model.get("type") === "Data" && subview.remove) {
// Remove the view from the DOM
subview.remove();
// And from the subviews list
delete this.subviews[id];
}
},
this,
);
// And then close the folder
this.$el
.find(".open")
.removeClass("open")
.addClass("closed")
.removeClass("icon-chevron-down")
.addClass("icon-chevron-right");
this.$el
.find(".icon-folder-open")
.removeClass("icon-folder-open")
.addClass("icon-folder-close");
this.isOpen = false;
} else {
// Add sub rows to the view
var dataModels = this.dataPackage.where({ type: "Data" });
_.each(
dataModels,
function (model) {
this.addOne(model);
},
this,
);
// And then open the folder
this.$el
.find(".closed")
.removeClass("closed")
.addClass("open")
.removeClass("icon-folder-close")
.addClass("icon-chevron-down");
this.$el
.find(".icon-folder-close")
.removeClass("icon-folder-close")
.addClass("icon-folder-open");
this.isOpen = true;
}
event.stopPropagation();
event.preventDefault();
},
/**
* Expand function to show hidden rows when a user clicks on an expand control.
* @param {Event} e - The event object.
* @since 2.28.0
*/
expand: function (e) {
// Don't do anything...
e.preventDefault();
var view = this;
var eventEl = $(e.target).parents("td");
var rowEl = $(e.target).parents("tr");
var parentId = rowEl.data("id");
var children = "tr[data-parent='" + parentId + "']";
this.$(children).fadeIn();
this.$(eventEl)
.children()
.children(".expand-control")
.fadeOut(function () {
view
.$(eventEl)
.children()
.children(".collapse-control")
.fadeIn("fast");
view.$(".tooltip-this").tooltip();
});
this.$(children)
.children()
.children()
.children(".collapse-control")
.fadeOut(function () {
view
.$(children)
.children()
.children()
.children(".expand-control")
.fadeIn("fast");
});
},
/**
* Collapse function to hide rows when a user clicks on a collapse control.
* @param {Event} e - The event object.
*
* @since 2.28.0
*/
collapse: function (e) {
// Don't do anything...
e.preventDefault();
var view = this;
var eventEl = $(e.target).parents("td");
var rowEl = $(e.target).parents("tr");
var parentId = rowEl.data("id");
var children = "tr[data-parent^='" + parentId + "']";
this.$(children).fadeOut();
this.$(eventEl)
.children()
.children(".collapse-control")
.fadeOut(function () {
view.$(eventEl).children().children(".expand-control").fadeIn();
view.$(".tooltip-this").tooltip();
});
},
/**
* Expand all function to show all child rows when a user clicks on an expand-all control.
* @param {Event} e - The event object.
*
* @since 2.28.0
*/
expandAll: function (e) {
// Don't do anything...
e.preventDefault();
var view = this;
var eventEl = $(e.target).parents("td");
var rowEl = $(e.target).parents("tr");
var parentId = rowEl.data("id");
var children = "tr[data-packageid='" + parentId + "']";
this.$(children).fadeIn();
this.$(eventEl)
.children(".d1package-expand")
.fadeOut(function () {
view.$(eventEl).children(".d1package-collapse").fadeIn("fast");
view.$(".tooltip-this").tooltip();
});
this.$(children)
.children()
.children()
.children(".collapse-control")
.fadeOut(function () {
view
.$(children)
.children()
.children()
.children(".expand-control")
.fadeIn("fast");
});
},
/**
* Collapse all function to hide all child rows when a user clicks on a collapse-all control.
* @param {Event} e - The event object.
*
* @since 2.28.0
*/
collapseAll: function (e) {
// Don't do anything...
e.preventDefault();
var view = this;
var eventEl = $(e.target).parents("td");
var rowEl = $(e.target).parents("tr");
var parentId = rowEl.data("id");
var children = "tr[data-packageid='" + parentId + "']";
this.$(children).each(function () {
$(this).fadeOut();
let childId = $(this).data("id");
let grandchildren = "tr[data-parent^='" + childId + "']";
$(grandchildren).fadeOut();
});
this.$(eventEl)
.children(".d1package-collapse")
.fadeOut(function () {
view.$(eventEl).children(".d1package-expand").fadeIn();
view.$(".tooltip-this").tooltip();
});
},
/**
* Check for private members and disable download buttons if necessary.
*
* @since 2.28.0
*/
checkForPrivateMembers: function () {
try {
var packageModel = this.model,
packageCollection = this.dataPackage;
if (!packageModel || !packageCollection) {
return;
}
var numMembersFromSolr = packageModel.get("members").length,
numMembersFromRDF = packageCollection.length;
if (numMembersFromRDF > numMembersFromSolr) {
var downloadButtons = this.$(".btn.download");
for (var i = 0; i < downloadButtons.length; i++) {
var btn = downloadButtons[i];
var downloadURL = $(btn).attr("href");
if (
downloadURL.indexOf(packageModel.get("id")) > -1 ||
downloadURL.indexOf(
encodeURIComponent(packageModel.get("id")),
) > -1
) {
$(btn)
.attr("disabled", "disabled")
.addClass("disabled")
.attr("href", "")
.tooltip({
trigger: "hover",
placement: "top",
delay: 500,
title:
"This dataset may contain private data, so each data file should be downloaded individually.",
});
i = downloadButtons.length;
}
}
}
} catch (e) {
console.error(e);
}
},
/**
* Retrieves and processes nested packages for the current package.
*
* @since 2.28.0
*/
getNestedPackages: function () {
var nestedPackages = new Array();
var nestedPackageIds = new Array();
this.nestedPackages = nestedPackages;
// get all the child packages for this resource map
var childPackages = this.dataPackage.filter(function (m) {
return m.get("formatType") === "RESOURCE";
});
// iterate over the list of child packages and add their members
for (var ite in childPackages) {
var childPkg = childPackages[ite];
if (!nestedPackageIds.includes(childPkg.get("id"))) {
var nestedPackage = new PackageModel();
nestedPackage.set("id", childPkg.get("id"));
nestedPackage.setURL();
nestedPackage.getMembers();
nestedPackages.push(nestedPackage);
nestedPackageIds.push(childPkg.get("id"));
this.listenToOnce(
nestedPackage,
"change:members",
this.addNestedPackages,
nestedPackage,
);
}
}
},
/**
* Adds a nested data package to the package table.
*
* @param {Object} dataPackage - The data package to be added.
* @since 2.28.0
*/
addNestedPackages: function (dataPackage) {
/**
* Generates the table row for the data package header.
* @type {null|Element}
*/
var tableRow = null,
/**
* Reference to the current view.
* @type {Object}
*/
view = this,
/**
* The title of the data package.
* @type {null|string}
*/
title = null,
/**
* The URL of the data package.
* @type {null|string}
*/
packageUrl = null,
/**
* The URL of the nested data package.
* @type {null|string}
*/
nestedPackageUrl = null;
/**
* The members of the data package.
*
* @type {Array}
*/
var members = dataPackage.get("members");
/**
* Filters out metadata objects from the members.
*
* @type {Array}
*/
let metadataObj = _.filter(members, function (m) {
return m.get("type") == "Metadata" || m.get("type") == "metadata";
});
title = metadataObj[0].get("title");
/**
* The tooltip for the title (used for long titles).
*
* @type {string}
*/
let titleTooltip = title;
title =
title.length > 150
? title.slice(0, 75) +
"..." +
title.slice(title.length - 75, title.length)
: title;
// Set the package URL
if (MetacatUI.appModel.get("packageServiceUrl"))
packageUrl =
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(dataPackage.id);
// Set the nested package URL
if (MetacatUI.root !== undefined && dataPackage.id !== undefined)
nestedPackageUrl =
MetacatUI.root + "/view/" + encodeURIComponent(dataPackage.id);
/**
* The HTML content for the data package header.
*
* @type {string}
*/
tableRow = this.dataPackageHeaderTemplate({
id: dataPackage.id,
title: title,
titleTooltip: titleTooltip,
disablePackageDownloads: false,
downloadUrl: packageUrl,
disablePackageUrl: false,
packageUrl: nestedPackageUrl,
});
this.$el.append(tableRow);
// Create an instance of DownloadButtonView to handle package downloads
this.downloadButtonView = new DownloadButtonView({
model: dataPackage,
view: "actionsView",
nested: true,
});
// Render
this.downloadButtonView.render();
// Add the downloadButtonView el to the span
this.$el
.find(".downloadAction[data-id='" + dataPackage.id + "']")
.html(this.downloadButtonView.el);
// Filter out the packages from the member list
members = _.filter(members, function (m) {
return m.type != "Package";
});
// Add each member to the package table view
var view = this;
_.each(members, function (m) {
// Update the size to bytes format
m.set({ size: Utilities.bytesToSize(m.get("size")) });
// Add each item of this nested package to the package table view
view.addOne(m, dataPackage);
});
},
/*showDownloadProgress: function(e){
e.preventDefault();
var button = $(e.target);
button.addClass("in-progress");
button.html("Downloading... <i class='icon icon-on-right icon-spinner icon-spin'></i>");
return true;
}*/
},
);
return DataPackageView;
});


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var DataPackageView = Backbone.View.extend(
/** @lends DataPackageView.prototype */ {
type: "DataPackage",
tagName: "table",
className: "table table-striped table-hover",
id: "data-package-table",
events: {
"click .toggle-rows": "toggleRows", // Show/hide rows associated with event's metadata row
"click .message-row .addFiles": "handleAddFiles",
"click .expand-control": "expand",
"click .collapse-control": "collapse",
"click .d1package-expand": "expandAll",
"click .d1package-collapse": "collapseAll",
},
subviews: {},
/**
* A reference to the parent EditorView that contains this view
* @type EditorView
* @since 2.15.0
*/
parentEditorView: null,
template: _.template(DataPackageTemplate),
startMessageTemplate: _.template(DataPackageStartTemplate),
dataPackageHeaderTemplate: _.template(DataPackageHeaderTemplate),
// Models waiting for their parent folder to be rendered, hashed by parent id:
// {'parentid': [model1, model2, ...]}
delayedModels: {},
/* Flag indicating the open or closed state of the package rows */
isOpen: true,
initialize: function (options) {
if (options === undefined || !options) var options = {};
if (!options.edit) {
//The edit option will allow the user to edit the table
this.edit = options.edit || false;
this.mode = "view";
this.packageId = options.packageId || null;
this.memberId = options.memberId || null;
this.attributes = options.attributes || null;
this.dataPackage = options.dataPackage || new DataPackage();
this.dataEntities = options.dataEntities || new Array();
this.disablePackageDownloads =
options.disablePackageDownloads || false;
this.currentlyViewing = options.currentlyViewing || null;
this.parentEditorView = options.parentView || null;
this.title = options.title || "";
this.packageTitle = options.packageTitle || "";
this.nested =
typeof options.nested === "undefined" ? false : options.nested;
this.metricsModel = options.metricsModel;
// set the package model
this.packageModel = this.dataPackage.packageModel;
this.listenTo(this.packageModel, "changeAll", this.render);
} else {
//Get the options sent to this view
if (typeof options == "object") {
//The edit option will allow the user to edit the table
this.edit = options.edit || false;
this.mode = "edit";
//The data package to render
this.dataPackage = options.dataPackage || new DataPackage();
this.parentEditorView = options.parentEditorView || null;
}
//Create a new DataPackage collection if one wasn't sent
else if (!this.dataPackage) {
this.dataPackage = new DataPackage();
}
return this;
}
},
/**
* Render the DataPackage HTML
*/
render: function () {
this.$el.addClass("download-contents table-condensed");
this.$el.append(
this.template({
edit: this.edit,
dataPackageFiltering:
MetacatUI.appModel.get("dataPackageFiltering") || false,
dataPackageSorting:
MetacatUI.appModel.get("dataPackageSorting") || false,
loading: MetacatUI.appView.loadingTemplate({
msg: "Loading files table... ",
}),
id: this.dataPackage.get("id"),
title: this.title || "Files in this dataset",
classes: "download-contents table-striped table-condensed table",
}),
);
if (this.edit) {
// Listen for add events because models are being merged
this.listenTo(this.dataPackage, "add", this.addOne);
this.listenTo(this.dataPackage, "fileAdded", this.addOne);
}
// Render the current set of models in the DataPackage
this.addAll();
if (this.edit) {
//If this is a new data package, then display a message and button
if (
(this.dataPackage.length == 1 &&
this.dataPackage.models[0].isNew()) ||
!this.dataPackage.length
) {
var messageRow = this.startMessageTemplate();
this.$("tbody").append(messageRow);
this.listenTo(this.dataPackage, "add", function () {
this.$(".message-row").remove();
});
}
//Render the Share control(s)
this.renderShareControl();
} else {
// check for nessted datasets
if (this.nested) {
this.getNestedPackages();
}
}
return this;
},
/**
* Add a single DataItemView row to the DataPackageView
*/
addOne: function (item, dataPackage) {
if (!item) return false;
//Don't add duplicate rows
if (this.$(".data-package-item[data-id='" + item.id + "']").length)
return;
// Don't add data package
if (
item.get("formatType") == "RESOURCE" ||
item.get("type") == "DataPackage"
) {
return;
}
var dataItemView, scimetaParent, parentRow, delayed_models;
if (_.contains(Object.keys(this.subviews), item.id)) {
return false; // Don't double render
}
var itemPath = null,
view = this;
if (!_.isEmpty(this.atLocationObj)) {
itemPath = this.atLocationObj[item.get("id")];
if (itemPath[0] != "/") {
itemPath = "/" + itemPath;
}
}
// get the data package id
if (typeof dataPackage !== "undefined") {
var dataPackageId = dataPackage.id;
}
if (typeof dataPackageId === "undefined")
dataPackageId = this.dataPackage.id;
var insertInfoIcon = this.edit
? false
: view.dataEntities.includes(item.id);
dataItemView = new DataItemView({
model: item,
metricsModel: this.metricsModel,
itemPath: itemPath,
insertInfoIcon: insertInfoIcon,
currentlyViewing: this.currentlyViewing,
mode: this.mode,
parentEditorView: this.parentEditorView,
dataPackageId: dataPackageId,
});
this.subviews[item.id] = dataItemView; // keep track of all views
if (this.edit) {
//Get the science metadata that documents this item
scimetaParent = item.get("isDocumentedBy");
//If this item is not documented by a science metadata object,
// and there is only one science metadata doc in the package, then assume it is
// documented by that science metadata doc
if (typeof scimetaParent == "undefined" || !scimetaParent) {
//Get the science metadata models
var metadataIds = this.dataPackage.sciMetaPids;
//If there is only one science metadata model in the package, then use it
if (metadataIds.length == 1) scimetaParent = metadataIds[0];
}
//Otherwise, get the first science metadata doc that documents this object
else {
scimetaParent = scimetaParent[0];
}
if (
scimetaParent == item.get("id") ||
(!scimetaParent && item.get("type") == "Metadata")
) {
// This is a metadata folder row, append it to the table
this.$el.append(dataItemView.render().el);
// Render any delayed models if this is the parent
if (_.contains(Object.keys(this.delayedModels), dataItemView.id)) {
delayed_models = this.delayedModels[dataItemView.id];
_.each(delayed_models, this.addOne, this);
}
} else {
// Find the parent row by it's id, stored in a custom attribute
if (scimetaParent)
parentRow = this.$("[data-id='" + scimetaParent + "']");
if (typeof parentRow !== "undefined" && parentRow.length) {
// This is a data row, insert below it's metadata parent folder
parentRow.after(dataItemView.render().el);
// Remove it from the delayedModels list if necessary
if (_.contains(Object.keys(this.delayedModels), scimetaParent)) {
delayed_models = this.delayedModels[scimetaParent];
var index = _.indexOf(delayed_models, item);
delayed_models = delayed_models.splice(index, 1);
// Put the shortened array back if delayed models remains
if (delayed_models.length > 0) {
this.delayedModels[scimetaParent] = delayed_models;
} else {
this.delayedModels[scimetaParent] = undefined;
}
}
this.trigger("addOne");
} else {
console.warn(
"Couldn't render " +
item.id +
". Delayed until parent is rendered.",
);
// Postpone the data row until the parent is rendered
delayed_models = this.delayedModels[scimetaParent];
// Delay the model rendering if it isn't already delayed
if (typeof delayed_models !== "undefined") {
if (!_.contains(delayed_models, item)) {
delayed_models.push(item);
this.delayedModels[scimetaParent] = delayed_models;
}
} else {
delayed_models = [];
delayed_models.push(item);
this.delayedModels[scimetaParent] = delayed_models;
}
}
}
} else {
// This is a metadata folder row, append it to the table
this.$el.append(dataItemView.render().el);
this.trigger("addOne");
}
},
/**
* Render the Data Package View and insert it into this view
*/
renderDataPackage: function () {
var view = this;
if (MetacatUI.rootDataPackage.packageModel.isNew()) {
view.renderMember(this.model);
}
// As the root collection is updated with models, render the UI
this.listenTo(MetacatUI.rootDataPackage, "add", function (model) {
if (!model.get("synced") && model.get("id"))
this.listenTo(model, "sync", view.renderMember);
else if (model.get("synced")) view.renderMember(model);
//Listen for changes on this member
model.on("change:fileName", model.addToUploadQueue);
});
//Render the Data Package view
this.dataPackageView = new DataPackageView({
edit: true,
dataPackage: MetacatUI.rootDataPackage,
parentEditorView: this,
});
//Render the view
var $packageTableContainer = this.$("#data-package-container");
$packageTableContainer.html(this.dataPackageView.render().el);
//Make the view resizable on the bottom
var handle = $(document.createElement("div"))
.addClass("ui-resizable-handle ui-resizable-s")
.attr("title", "Drag to resize")
.append(
$(document.createElement("i")).addClass("icon icon-caret-down"),
);
$packageTableContainer.after(handle);
$packageTableContainer.resizable({
handles: { s: handle },
minHeight: 100,
maxHeight: 900,
resize: function () {
view.emlView.resizeTOC();
},
});
var tableHeight = ($(window).height() - $("#Navbar").height()) * 0.4;
$packageTableContainer.css("height", tableHeight + "px");
var table = this.dataPackageView.$el;
this.listenTo(this.dataPackageView, "addOne", function () {
if (
table.outerHeight() > $packageTableContainer.outerHeight() &&
table.outerHeight() < 220
) {
$packageTableContainer.css(
"height",
table.outerHeight() + handle.outerHeight(),
);
if (this.emlView) this.emlView.resizeTOC();
}
});
if (this.emlView) this.emlView.resizeTOC();
//Save the view as a subview
this.subviews.push(this.dataPackageView);
this.listenTo(
MetacatUI.rootDataPackage.packageModel,
"change:childPackages",
this.renderChildren,
);
},
/**
* Add all rows to the DataPackageView
*/
addAll: function () {
this.$el.find("#data-package-table-body").html(""); // clear the table first
this.dataPackage.sort();
if (!this.edit) {
var atLocationObj = this.dataPackage.getAtLocation();
this.atLocationObj = atLocationObj;
// form path to D1 object dictionary
if (
this.atLocationObj !== undefined &&
!_.isEmpty(this.atLocationObj)
) {
var filePathObj = new Object();
this.dataPackage.each(function (item) {
if (!Object.keys(this.atLocationObj).includes(item.id)) {
this.atLocationObj[item.id] = "/";
}
}, this);
for (let key of Object.keys(this.atLocationObj)) {
var path = this.atLocationObj[key];
var pathArray = path.split("/");
pathArray.pop();
var parentPath = pathArray.join("/");
if (filePathObj.hasOwnProperty(parentPath)) {
filePathObj[parentPath].push(key);
} else {
filePathObj[parentPath] = new Array();
filePathObj[parentPath].push(key);
}
}
}
// add top level data package row to the package table
var tableRow = null,
view = this,
title = this.packageTitle,
packageUrl = null;
if (title === "") {
let metadataObj = _.filter(this.dataPackage.models, function (m) {
return m.get("id") == view.currentlyViewing;
});
if (metadataObj.length > 0) {
title = metadataObj[0].get("title");
let metaId = metadataObj[0].get("id");
this.metaId = metaId;
} else {
title = this.dataPackage.get("id");
}
}
let titleTooltip = title;
title =
title.length > 150
? title.slice(0, 75) +
"..." +
title.slice(title.length - 75, title.length)
: title;
// set the package URL
if (MetacatUI.appModel.get("packageServiceUrl"))
packageUrl =
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(view.dataPackage.id);
var disablePackageDownloads = this.disablePackageDownloads;
tableRow = this.dataPackageHeaderTemplate({
id: view.dataPackage.id,
title: title,
titleTooltip: titleTooltip,
downloadUrl: packageUrl,
disablePackageDownloads: disablePackageDownloads,
disablePackageUrl: true,
});
this.$el.append(tableRow);
if (this.atLocationObj !== undefined && filePathObj !== undefined) {
// sort the filePath by length
var sortedFilePathObj = Object.keys(filePathObj)
.sort()
.reduce((obj, key) => {
obj[key] = filePathObj[key];
return obj;
}, {});
this.sortedFilePathObj = sortedFilePathObj;
this.addFilesAndFolders(sortedFilePathObj);
} else {
this.dataPackage.each(this.addOne, this, this.dataPackage);
}
} else {
this.dataPackage.each(this.addOne, this);
}
},
/**
* Add all the files and folders
*/
addFilesAndFolders: function (sortedFilePathObj) {
if (!sortedFilePathObj) return false;
var insertedPath = new Array();
let pathMap = new Object();
pathMap[""] = "";
for (let key of Object.keys(sortedFilePathObj)) {
// add folder
var pathArray = key.split("/");
//skip the first empty value
for (let i = 0; i < pathArray.length; i++) {
if (pathArray[i].length < 1) continue;
if (!(pathArray[i] in pathMap)) {
// insert path
var dataItemView, itemPath;
// root
if (i == 0) {
itemPath = "";
} else {
itemPath = pathMap[pathArray[i - 1]];
}
dataItemView = new DataItemView({
mode: this.mode,
itemName: pathArray[i],
itemPath: itemPath,
itemType: "folder",
parentEditorView: this.parentEditorView,
dataPackageId: this.dataPackage.id,
});
this.subviews[pathArray[i]] = dataItemView; // keep track of all views
this.$el.append(dataItemView.render().el);
this.trigger("addOne");
pathMap[pathArray[i]] = itemPath + "/" + pathArray[i];
}
}
// add files in the folder
var itemArray = sortedFilePathObj[key];
// Add metadata object at the top of the file table
if (
key == "" &&
this.metaId !== "undefined" &&
itemArray.includes(this.metaId)
) {
let item = this.metaId;
this.addOne(this.dataPackage.get(item));
}
for (let i = 0; i < itemArray.length; i++) {
let item = itemArray[i];
this.addOne(this.dataPackage.get(item));
}
}
},
/**
Remove the subview represented by the given model item.
@param item The model representing the sub view to be removed
*/
removeOne: function (item) {
if (_.contains(Object.keys(this.subviews), item.id)) {
// Remove the view and the its reference in the subviews list
this.subviews[item.id].remove();
delete this.subviews[item.id];
}
},
handleAddFiles: function (e) {
//Pass this on to the DataItemView for the root data package
this.$(".data-package-item.folder")
.first()
.data("view")
.handleAddFiles(e);
},
/**
* Renders a control that opens the AccessPolicyView for editing permissions on this package
* @since 2.15.0
*/
renderShareControl: function () {
if (
this.parentEditorView &&
!this.parentEditorView.isAccessPolicyEditEnabled()
) {
this.$("#data-package-table-share").remove();
}
},
/**
* Close subviews as needed
*/
onClose: function () {
// Close each subview
_.each(
Object.keys(this.subviews),
function (id) {
var subview = this.subviews[id];
subview.onClose();
},
this,
);
//Reset the subviews from the view completely (by removing it from the prototype)
this.__proto__.subviews = {};
},
/**
Show or hide the data rows associated with the event row science metadata
*/
toggleRows: function (event) {
if (this.isOpen) {
// Get the DataItemView associated with each id
_.each(
Object.keys(this.subviews),
function (id) {
var subview = this.subviews[id];
if (subview.model.get("type") === "Data" && subview.remove) {
// Remove the view from the DOM
subview.remove();
// And from the subviews list
delete this.subviews[id];
}
},
this,
);
// And then close the folder
this.$el
.find(".open")
.removeClass("open")
.addClass("closed")
.removeClass("icon-chevron-down")
.addClass("icon-chevron-right");
this.$el
.find(".icon-folder-open")
.removeClass("icon-folder-open")
.addClass("icon-folder-close");
this.isOpen = false;
} else {
// Add sub rows to the view
var dataModels = this.dataPackage.where({ type: "Data" });
_.each(
dataModels,
function (model) {
this.addOne(model);
},
this,
);
// And then open the folder
this.$el
.find(".closed")
.removeClass("closed")
.addClass("open")
.removeClass("icon-folder-close")
.addClass("icon-chevron-down");
this.$el
.find(".icon-folder-close")
.removeClass("icon-folder-close")
.addClass("icon-folder-open");
this.isOpen = true;
}
event.stopPropagation();
event.preventDefault();
},
/**
* Expand function to show hidden rows when a user clicks on an expand control.
* @param {Event} e - The event object.
* @since 2.28.0
*/
expand: function (e) {
// Don't do anything...
e.preventDefault();
var view = this;
var eventEl = $(e.target).parents("td");
var rowEl = $(e.target).parents("tr");
var parentId = rowEl.data("id");
var children = "tr[data-parent='" + parentId + "']";
this.$(children).fadeIn();
this.$(eventEl)
.children()
.children(".expand-control")
.fadeOut(function () {
view
.$(eventEl)
.children()
.children(".collapse-control")
.fadeIn("fast");
view.$(".tooltip-this").tooltip();
});
this.$(children)
.children()
.children()
.children(".collapse-control")
.fadeOut(function () {
view
.$(children)
.children()
.children()
.children(".expand-control")
.fadeIn("fast");
});
},
/**
* Collapse function to hide rows when a user clicks on a collapse control.
* @param {Event} e - The event object.
*
* @since 2.28.0
*/
collapse: function (e) {
// Don't do anything...
e.preventDefault();
var view = this;
var eventEl = $(e.target).parents("td");
var rowEl = $(e.target).parents("tr");
var parentId = rowEl.data("id");
var children = "tr[data-parent^='" + parentId + "']";
this.$(children).fadeOut();
this.$(eventEl)
.children()
.children(".collapse-control")
.fadeOut(function () {
view.$(eventEl).children().children(".expand-control").fadeIn();
view.$(".tooltip-this").tooltip();
});
},
/**
* Expand all function to show all child rows when a user clicks on an expand-all control.
* @param {Event} e - The event object.
*
* @since 2.28.0
*/
expandAll: function (e) {
// Don't do anything...
e.preventDefault();
var view = this;
var eventEl = $(e.target).parents("td");
var rowEl = $(e.target).parents("tr");
var parentId = rowEl.data("id");
var children = "tr[data-packageid='" + parentId + "']";
this.$(children).fadeIn();
this.$(eventEl)
.children(".d1package-expand")
.fadeOut(function () {
view.$(eventEl).children(".d1package-collapse").fadeIn("fast");
view.$(".tooltip-this").tooltip();
});
this.$(children)
.children()
.children()
.children(".collapse-control")
.fadeOut(function () {
view
.$(children)
.children()
.children()
.children(".expand-control")
.fadeIn("fast");
});
},
/**
* Collapse all function to hide all child rows when a user clicks on a collapse-all control.
* @param {Event} e - The event object.
*
* @since 2.28.0
*/
collapseAll: function (e) {
// Don't do anything...
e.preventDefault();
var view = this;
var eventEl = $(e.target).parents("td");
var rowEl = $(e.target).parents("tr");
var parentId = rowEl.data("id");
var children = "tr[data-packageid='" + parentId + "']";
this.$(children).each(function () {
$(this).fadeOut();
let childId = $(this).data("id");
let grandchildren = "tr[data-parent^='" + childId + "']";
$(grandchildren).fadeOut();
});
this.$(eventEl)
.children(".d1package-collapse")
.fadeOut(function () {
view.$(eventEl).children(".d1package-expand").fadeIn();
view.$(".tooltip-this").tooltip();
});
},
/**
* Check for private members and disable download buttons if necessary.
*
* @since 2.28.0
*/
checkForPrivateMembers: function () {
try {
var packageModel = this.model,
packageCollection = this.dataPackage;
if (!packageModel || !packageCollection) {
return;
}
var numMembersFromSolr = packageModel.get("members").length,
numMembersFromRDF = packageCollection.length;
if (numMembersFromRDF > numMembersFromSolr) {
var downloadButtons = this.$(".btn.download");
for (var i = 0; i < downloadButtons.length; i++) {
var btn = downloadButtons[i];
var downloadURL = $(btn).attr("href");
if (
downloadURL.indexOf(packageModel.get("id")) > -1 ||
downloadURL.indexOf(
encodeURIComponent(packageModel.get("id")),
) > -1
) {
$(btn)
.attr("disabled", "disabled")
.addClass("disabled")
.attr("href", "")
.tooltip({
trigger: "hover",
placement: "top",
delay: 500,
title:
"This dataset may contain private data, so each data file should be downloaded individually.",
});
i = downloadButtons.length;
}
}
}
} catch (e) {
console.error(e);
}
},
/**
* Retrieves and processes nested packages for the current package.
*
* @since 2.28.0
*/
getNestedPackages: function () {
var nestedPackages = new Array();
var nestedPackageIds = new Array();
this.nestedPackages = nestedPackages;
// get all the child packages for this resource map
var childPackages = this.dataPackage.filter(function (m) {
return m.get("formatType") === "RESOURCE";
});
// iterate over the list of child packages and add their members
for (var ite in childPackages) {
var childPkg = childPackages[ite];
if (!nestedPackageIds.includes(childPkg.get("id"))) {
var nestedPackage = new PackageModel();
nestedPackage.set("id", childPkg.get("id"));
nestedPackage.setURL();
nestedPackage.getMembers();
nestedPackages.push(nestedPackage);
nestedPackageIds.push(childPkg.get("id"));
this.listenToOnce(
nestedPackage,
"change:members",
this.addNestedPackages,
nestedPackage,
);
}
}
},
/**
* Adds a nested data package to the package table.
*
* @param {Object} dataPackage - The data package to be added.
* @since 2.28.0
*/
addNestedPackages: function (dataPackage) {
/**
* Generates the table row for the data package header.
* @type {null|Element}
*/
var tableRow = null,
/**
* Reference to the current view.
* @type {Object}
*/
view = this,
/**
* The title of the data package.
* @type {null|string}
*/
title = null,
/**
* The URL of the data package.
* @type {null|string}
*/
packageUrl = null,
/**
* The URL of the nested data package.
* @type {null|string}
*/
nestedPackageUrl = null;
/**
* The members of the data package.
*
* @type {Array}
*/
var members = dataPackage.get("members");
/**
* Filters out metadata objects from the members.
*
* @type {Array}
*/
let metadataObj = _.filter(members, function (m) {
return m.get("type") == "Metadata" || m.get("type") == "metadata";
});
title = metadataObj[0].get("title");
/**
* The tooltip for the title (used for long titles).
*
* @type {string}
*/
let titleTooltip = title;
title =
title.length > 150
? title.slice(0, 75) +
"..." +
title.slice(title.length - 75, title.length)
: title;
// Set the package URL
if (MetacatUI.appModel.get("packageServiceUrl"))
packageUrl =
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(dataPackage.id);
// Set the nested package URL
if (MetacatUI.root !== undefined && dataPackage.id !== undefined)
nestedPackageUrl =
MetacatUI.root + "/view/" + encodeURIComponent(dataPackage.id);
/**
* The HTML content for the data package header.
*
* @type {string}
*/
tableRow = this.dataPackageHeaderTemplate({
id: dataPackage.id,
title: title,
titleTooltip: titleTooltip,
disablePackageDownloads: false,
downloadUrl: packageUrl,
disablePackageUrl: false,
packageUrl: nestedPackageUrl,
});
this.$el.append(tableRow);
// Create an instance of DownloadButtonView to handle package downloads
this.downloadButtonView = new DownloadButtonView({
model: dataPackage,
view: "actionsView",
nested: true,
});
// Render
this.downloadButtonView.render();
// Add the downloadButtonView el to the span
this.$el
.find(".downloadAction[data-id='" + dataPackage.id + "']")
.html(this.downloadButtonView.el);
// Filter out the packages from the member list
members = _.filter(members, function (m) {
return m.type != "Package";
});
// Add each member to the package table view
var view = this;
_.each(members, function (m) {
// Update the size to bytes format
m.set({ size: Utilities.bytesToSize(m.get("size")) });
// Add each item of this nested package to the package table view
view.addOne(m, dataPackage);
});
},
/*showDownloadProgress: function(e){
e.preventDefault();
var button = $(e.target);
button.addClass("in-progress");
button.html("Downloading... <i class='icon icon-on-right icon-spinner icon-spin'></i>");
return true;
}*/
},
);


🚫 [eslint] <object-shorthand> reported by reviewdog 🐶
Expected method shorthand.

addNestedPackages: function (dataPackage) {
/**
* Generates the table row for the data package header.
* @type {null|Element}
*/
var tableRow = null,
/**
* Reference to the current view.
* @type {Object}
*/
view = this,
/**
* The title of the data package.
* @type {null|string}
*/
title = null,
/**
* The URL of the data package.
* @type {null|string}
*/
packageUrl = null,
/**
* The URL of the nested data package.
* @type {null|string}
*/
nestedPackageUrl = null;
/**
* The members of the data package.
*
* @type {Array}
*/
var members = dataPackage.get("members");
/**
* Filters out metadata objects from the members.
*
* @type {Array}
*/
let metadataObj = _.filter(members, function (m) {
return m.get("type") == "Metadata" || m.get("type") == "metadata";
});
title = metadataObj[0].get("title");
/**
* The tooltip for the title (used for long titles).
*
* @type {string}
*/
let titleTooltip = title;
title =
title.length > 150
? title.slice(0, 75) +
"..." +
title.slice(title.length - 75, title.length)
: title;
// Set the package URL
if (MetacatUI.appModel.get("packageServiceUrl"))
packageUrl =
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(dataPackage.id);
// Set the nested package URL
if (MetacatUI.root !== undefined && dataPackage.id !== undefined)
nestedPackageUrl =
MetacatUI.root + "/view/" + encodeURIComponent(dataPackage.id);
/**
* The HTML content for the data package header.
*
* @type {string}
*/
tableRow = this.dataPackageHeaderTemplate({
id: dataPackage.id,
title: title,
titleTooltip: titleTooltip,
disablePackageDownloads: false,
downloadUrl: packageUrl,
disablePackageUrl: false,
packageUrl: nestedPackageUrl,
});
this.$el.append(tableRow);
// Create an instance of DownloadButtonView to handle package downloads
this.downloadButtonView = new DownloadButtonView({
model: dataPackage,
view: "actionsView",
nested: true,
});
// Render
this.downloadButtonView.render();
// Add the downloadButtonView el to the span
this.$el
.find(".downloadAction[data-id='" + dataPackage.id + "']")
.html(this.downloadButtonView.el);
// Filter out the packages from the member list
members = _.filter(members, function (m) {
return m.type != "Package";
});
// Add each member to the package table view
var view = this;
_.each(members, function (m) {
// Update the size to bytes format
m.set({ size: Utilities.bytesToSize(m.get("size")) });
// Add each item of this nested package to the package table view
view.addOne(m, dataPackage);
});
},


🚫 [eslint] <vars-on-top> reported by reviewdog 🐶
All 'var' declarations must be at the top of the function scope.

var view = this;


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var view = this;


🚫 [eslint] <no-redeclare> reported by reviewdog 🐶
'view' is already defined.

var view = this;


⚠️ [eslint] <func-names> reported by reviewdog 🐶
Unexpected unnamed function.

_.each(members, function (m) {


🚫 [eslint] <prefer-arrow-callback> reported by reviewdog 🐶
Unexpected function expression.

_.each(members, function (m) {
// Update the size to bytes format
m.set({ size: Utilities.bytesToSize(m.get("size")) });
// Add each item of this nested package to the package table view
view.addOne(m, dataPackage);
});


🚫 [eslint] <prefer-arrow-callback> reported by reviewdog 🐶
Unexpected function expression.

], function (
$,
_,
Backbone,
gmaps,
Utilities,
SolrResult,
DownloadButtonView,
LoadingTemplate,
alertTemplate,
AttributeTemplate,
DataDisplayTemplate,
) {
var MetadataIndexView = Backbone.View.extend({
type: "MetadataIndex",
id: "Metadata",
className: "metadata-index container form-horizontal",
tagName: "article",
template: null,
loadingTemplate: _.template(LoadingTemplate),
attributeTemplate: _.template(AttributeTemplate),
alertTemplate: _.template(alertTemplate),
dataDisplayTemplate: _.template(DataDisplayTemplate),
semanticFields: null,
events: {},
initialize: function (options) {
this.pid = options.pid || null;
//this.el.id = this.id + "-" + this.pid; //Give this element a specific ID in case multiple MetadataIndex views are on one page
this.parentView = options.parentView || null;
// use these to tailor the annotation ui widget
this.semanticFields = {
attribute: "sem_annotation",
attributeName: "sem_annotation",
attributeLabel: "sem_annotation",
attributeDescription: "sem_annotation",
attributeUnit: "sem_annotation",
origin: "orcid_sm",
investigator: "orcid_sm",
};
},
render: function () {
if (!this.pid) return false;
var view = this;
//Get all the fields from the Solr index
var query =
'q=(id:"' +
encodeURIComponent(this.pid) +
'"+OR+seriesId:"' +
encodeURIComponent(this.pid) +
'")&rows=1&start=0&fl=*&wt=json';
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
try {
if (!data?.response?.numFound) {
if (view.parentView && view.parentView.model) {
//Show a "not indexed" message if there is system metadata but nothing in
// the index
if (view.parentView.model.get("systemMetadata")) {
view.showNotIndexed();
}
//Show a "not found" message if there is no system metadata and no results in the index
else {
view.parentView.model.set("notFound", true);
view.parentView.showNotFound();
}
}
view.flagComplete();
} else {
view.docs = data.response.docs;
_.each(data.response.docs, function (doc, i, list) {
//If this is a data object and there is a science metadata doc that describes it, then navigate to that Metadata View.
if (
doc.formatType == "DATA" &&
doc.isDocumentedBy &&
doc.isDocumentedBy.length
) {
view.onClose();
MetacatUI.uiRouter.navigate(
"view/" + doc.isDocumentedBy[0],
true,
);
return;
}
var metadataEl = $(document.createElement("section")).attr(
"id",
"metadata-index-details",
),
id = doc.id,
creator = doc.origin,
title = doc.title,
pubDate = doc.pubDate,
dateUploaded = doc.dateUploaded,
keys = Object.keys(doc),
docModel = new SolrResult(doc);
//Extract General Info details that we want to list first
var generalInfoKeys = [
"title",
"id",
"abstract",
"pubDate",
"keywords",
];
keys = _.difference(keys, generalInfoKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
generalInfoKeys,
"General",
),
);
//Extract Spatial details
var spatialKeys = [
"site",
"southBoundCoord",
"northBoundCoord",
"westBoundCoord",
"eastBoundCoord",
];
keys = _.difference(keys, spatialKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
spatialKeys,
"Geographic Region",
),
);
//Extract Temporal Coverage details
var temporalKeys = ["beginDate", "endDate"];
keys = _.difference(keys, temporalKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
temporalKeys,
"Temporal Coverage",
),
);
//Extract Taxonomic Coverage details
var taxonKeys = [
"order",
"phylum",
"family",
"genus",
"species",
"scientificName",
];
keys = _.difference(keys, taxonKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
taxonKeys,
"Taxonomic Coverage",
),
);
//Extract People details
var peopleKeys = [
"origin",
"investigator",
"contactOrganization",
"project",
];
keys = _.difference(keys, peopleKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
peopleKeys,
"People and Associated Parties",
),
);
//Extract Access Control details
var accessKeys = [
"isPublic",
"submitter",
"rightsHolder",
"writePermission",
"readPermission",
"changePermission",
"authoritativeMN",
];
keys = _.difference(keys, accessKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
accessKeys,
"Access Control",
),
);
//Add the rest of the metadata
$(metadataEl).append(
view.formatAttributeSection(docModel, keys, "Other"),
);
view.$el.html(metadataEl);
view.flagComplete();
});
}
} catch (e) {
console.log("Error parsing Solr response: " + e);
console.log("Solr response: " + data);
view.parentView.showNotFound();
}
},
error: function () {
var msg = "<h4>Sorry, no dataset was found.</h4>";
view.$el.html(
view.alertTemplate({ msg: msg, classes: "alert-danger" }),
);
},
};
$.ajax(
_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()),
);
//Send a request for the EML doc itself to extract certain info
if (this.parentView && this.parentView.model) {
var formatId = this.parentView.model.get("formatId");
if (formatId.indexOf("eml://") >= 0) {
var url =
MetacatUI.appModel.get("baseUrl") +
MetacatUI.appModel.get("context") +
MetacatUI.appModel.get("d1Service") +
"/object/" +
encodeURIComponent(this.parentView.model.get("id"));
var requestSettings = {
url: url,
success: function (data, textStatus, xhr) {
if (!data || !$(data).length) return;
//Find the distribution information
var emlDoc = $(data)
.find("distribution")
.each(function (i, dist) {
var onlineDist = $(dist).children("online");
if (onlineDist.length) {
var linkText = $(onlineDist).text();
if (linkText.indexOf("ecogrid") >= 0) {
//Clean up the link text
var start = linkText.lastIndexOf("/");
var ecogridPid = linkText.substr(start + 1).trim(),
dataObjects = [];
//Iterate over each id in the package and try to fuzzily match the ecogrid link to the id
if (view.parentView.packageModels) {
//Get all the data objects in this metadata's packages
_.each(view.parentView.packageModels, function (pckg) {
dataObjects.push(pckg.get("members"));
});
dataObjects = _.flatten(dataObjects);
}
for (var i = 0; i < dataObjects.length; i++) {
//If we find a match, replace the ecogrid links with a DataONE API link to the object
if (dataObjects[i].get("id").indexOf(ecogridPid) > -1) {
var linkText = dataObjects[i].get("url");
//We can stop looking now
i = dataObjects.length;
}
}
}
var link = $(document.createElement("a"))
.attr("href", linkText)
.text(linkText),
fullHTML = view.formatAttribute(
"Online Distribution Info",
link,
);
//Find the "General" section of this page
if (view.$(".General").length)
view.$(".General").after(fullHTML);
else view.$el.append(fullHTML);
}
});
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
}
}
return this;
},
formatAttributeSection: function (doc, keys, title, className) {
if (keys.length == 0) return "";
if (typeof title === "string") {
var titleHTML = $(document.createElement("h4")).text(title);
var titleText = title;
} else if (typeof title === "undefined") {
var titleHTML = $(document.createElement("h4"));
var titleText = "";
} else {
var titleHTML = title;
var titleText = titleHTML.text();
}
var html = "",
sectionClass =
typeof className === "undefined"
? titleText.replace(/ /g, "")
: className,
view = this,
populated = false;
_.each(keys, function (key, keyNum, list) {
if (typeof key === "object" && doc.get(key.field)) {
html += view.formatAttribute(key.display, doc.get(key.field));
populated = true;
} else if (doc.get(key)) {
html += view.formatAttribute(key, doc.get(key));
populated = true;
}
});
if (populated) {
var section = $(document.createElement("section"))
.addClass(sectionClass)
.append(titleHTML)
.append(html);
return section;
} else return null;
},
formatAttribute: function (attribute, value) {
var html = "",
view = this,
embeddedAttributes = "",
type = "sem_annotation";
// see if there is special handling for this field
if (this.semanticFields[attribute]) {
type = this.semanticFields[attribute];
}
//If this is a multi-valued field from Solr, the attribute value is actually multiple embedded attribute templates
var numAttributes =
Array.isArray(value) && value.length > 1 ? value.length : 0;
for (var i = 0; i < numAttributes; i++) {
embeddedAttributes += view.attributeTemplate({
attribute: "",
formattedAttribute: view.transformCamelCase(attribute),
value: value[i].toString(),
id: attribute + "_" + (i + 1),
type: type,
resource: "#xpointer(//" + attribute + "[" + (i + 1) + "])",
});
}
if (!embeddedAttributes && value instanceof $) {
value = value[0].outerHTML;
}
html += view.attributeTemplate({
attribute: attribute,
formattedAttribute: view.transformCamelCase(attribute),
value: embeddedAttributes || value.toString(),
id: attribute,
type: type,
resource: "#xpointer(//" + attribute + ")",
});
return html;
},
transformCamelCase: function (string) {
var result = string
.replace(/([A-Z]+)/g, " $1")
.replace(/([A-Z][a-z])/g, " $1");
var finalResult = result.charAt(0).toUpperCase() + result.slice(1);
return finalResult;
},
insertDataDetails: function () {
var view = this;
//Get the Package Model - it is attached with the parent Metadata View
var pkg = this.parentView.packageModel;
if (!pkg) return;
if (pkg.get("members").length <= 1) return;
//Start some html
var html = $(document.createElement("section"));
_.each(pkg.get("members"), function (solrResult, i) {
if (solrResult.get("formatType") != "DATA") return;
solrResult.set(
"formattedSize",
Utilities.bytesToSize(solrResult.get("size")),
);
//Add a section for the data details, just like the other attribute sections
var keys = [
"id",
{ field: "formattedSize", display: "size" },
"views",
"pubDate",
"dataSource",
"formatId",
];
//Determine the icon type based on format id
var type = solrResult.getType(),
icon = "";
if (type == "program") icon = "icon-code";
else if (type == "metadata") icon = "icon-file-text";
else if (type == "image") icon = "icon-picture";
else if (type == "pdf") icon = "icon-file pdf";
else icon = "icon-table";
var icon = $(document.createElement("i")).addClass(icon),
title = $(document.createElement("span"))
.text(solrResult.get("id"))
.addClass("title"),
downloadBtn = new DownloadButtonView({ model: solrResult }),
anchor = $(document.createElement("a")).attr(
"name",
encodeURIComponent(solrResult.get("id")),
),
header = $(document.createElement("h4"))
.append(anchor)
.append(icon)
.append(title)
.append(downloadBtn.render().el);
//Create the section
var entityDetailsSection = view
.formatAttributeSection(solrResult, keys, header, "entitydetails")
.attr("data-id", solrResult.get("id"));
//Create an image thumbnail, if this is an image
if (type == "image") {
//var thumbnail = view.parentView.createThumbnail(solrResult.get("id"));
//$(entityDetailsSection).prepend(thumbnail);
}
//Mark this section with an anchor tag with the doc id
$(entityDetailsSection).prepend(
$(document.createElement("a")).attr(
"id",
solrResult.get("id").replace(/[^A-Za-z0-9]/g, "-"),
),
);
$(html).append(entityDetailsSection);
});
//Glue together the header and attribute info section
var header = $(document.createElement("h4")).text(
"Data Table, Image, and Other Data Details",
);
var section = $(html).prepend(header);
//Insert into the DOM right after the "general" information
this.$(".General").after(section);
},
//Shows a message to the user that indicates this object has not been indexed
showNotIndexed: function () {
var message = this.alertTemplate({
classes: "alert-warning",
msg:
"<h4>There is limited information about this content.</h4>" +
"<p>This data or science metadata is available to download, but " +
"there seems to be an issue with displaying details on this webpage. " +
"If this content was recently submitted, it may still be in the processing queue.</p>",
includeEmail: true,
});
this.$el.append(message);
//If this metadata doc is not indexed, we need to search the system metadata
//to see if it is publicly accessible.
if (this.parentView && this.parentView.model) {
//Get the system metadata string
var sysMeta = this.parentView.model.get("systemMetadata");
if (sysMeta) {
//Parse it into XML nodes
sysMeta = $.parseXML(sysMeta);
//Find the allow permission for the public
var publicPermission = $(sysMeta).find(
"allow subject:contains('public')",
);
if (publicPermission.length) {
//Remove the "private" icon
$("#metadata-controls-container .private").remove();
}
}
//If there is no system metadata, default to hiding the private icon
else {
$("#metadata-controls-container .private").remove();
}
}
},
flagComplete: function () {
this.complete = true;
this.trigger("complete");
},
onClose: function () {
this.$el.html(this.loadingTemplate());
this.pid = null;
//Detach this view from its parent view
this.parentView.subviews = _.without(this.parentView.subviews, this);
this.parentView = null;
//Remove listeners
this.stopListening();
},
});
return MetadataIndexView;
});


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var MetadataIndexView = Backbone.View.extend({
type: "MetadataIndex",
id: "Metadata",
className: "metadata-index container form-horizontal",
tagName: "article",
template: null,
loadingTemplate: _.template(LoadingTemplate),
attributeTemplate: _.template(AttributeTemplate),
alertTemplate: _.template(alertTemplate),
dataDisplayTemplate: _.template(DataDisplayTemplate),
semanticFields: null,
events: {},
initialize: function (options) {
this.pid = options.pid || null;
//this.el.id = this.id + "-" + this.pid; //Give this element a specific ID in case multiple MetadataIndex views are on one page
this.parentView = options.parentView || null;
// use these to tailor the annotation ui widget
this.semanticFields = {
attribute: "sem_annotation",
attributeName: "sem_annotation",
attributeLabel: "sem_annotation",
attributeDescription: "sem_annotation",
attributeUnit: "sem_annotation",
origin: "orcid_sm",
investigator: "orcid_sm",
};
},
render: function () {
if (!this.pid) return false;
var view = this;
//Get all the fields from the Solr index
var query =
'q=(id:"' +
encodeURIComponent(this.pid) +
'"+OR+seriesId:"' +
encodeURIComponent(this.pid) +
'")&rows=1&start=0&fl=*&wt=json';
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
try {
if (!data?.response?.numFound) {
if (view.parentView && view.parentView.model) {
//Show a "not indexed" message if there is system metadata but nothing in
// the index
if (view.parentView.model.get("systemMetadata")) {
view.showNotIndexed();
}
//Show a "not found" message if there is no system metadata and no results in the index
else {
view.parentView.model.set("notFound", true);
view.parentView.showNotFound();
}
}
view.flagComplete();
} else {
view.docs = data.response.docs;
_.each(data.response.docs, function (doc, i, list) {
//If this is a data object and there is a science metadata doc that describes it, then navigate to that Metadata View.
if (
doc.formatType == "DATA" &&
doc.isDocumentedBy &&
doc.isDocumentedBy.length
) {
view.onClose();
MetacatUI.uiRouter.navigate(
"view/" + doc.isDocumentedBy[0],
true,
);
return;
}
var metadataEl = $(document.createElement("section")).attr(
"id",
"metadata-index-details",
),
id = doc.id,
creator = doc.origin,
title = doc.title,
pubDate = doc.pubDate,
dateUploaded = doc.dateUploaded,
keys = Object.keys(doc),
docModel = new SolrResult(doc);
//Extract General Info details that we want to list first
var generalInfoKeys = [
"title",
"id",
"abstract",
"pubDate",
"keywords",
];
keys = _.difference(keys, generalInfoKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
generalInfoKeys,
"General",
),
);
//Extract Spatial details
var spatialKeys = [
"site",
"southBoundCoord",
"northBoundCoord",
"westBoundCoord",
"eastBoundCoord",
];
keys = _.difference(keys, spatialKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
spatialKeys,
"Geographic Region",
),
);
//Extract Temporal Coverage details
var temporalKeys = ["beginDate", "endDate"];
keys = _.difference(keys, temporalKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
temporalKeys,
"Temporal Coverage",
),
);
//Extract Taxonomic Coverage details
var taxonKeys = [
"order",
"phylum",
"family",
"genus",
"species",
"scientificName",
];
keys = _.difference(keys, taxonKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
taxonKeys,
"Taxonomic Coverage",
),
);
//Extract People details
var peopleKeys = [
"origin",
"investigator",
"contactOrganization",
"project",
];
keys = _.difference(keys, peopleKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
peopleKeys,
"People and Associated Parties",
),
);
//Extract Access Control details
var accessKeys = [
"isPublic",
"submitter",
"rightsHolder",
"writePermission",
"readPermission",
"changePermission",
"authoritativeMN",
];
keys = _.difference(keys, accessKeys);
$(metadataEl).append(
view.formatAttributeSection(
docModel,
accessKeys,
"Access Control",
),
);
//Add the rest of the metadata
$(metadataEl).append(
view.formatAttributeSection(docModel, keys, "Other"),
);
view.$el.html(metadataEl);
view.flagComplete();
});
}
} catch (e) {
console.log("Error parsing Solr response: " + e);
console.log("Solr response: " + data);
view.parentView.showNotFound();
}
},
error: function () {
var msg = "<h4>Sorry, no dataset was found.</h4>";
view.$el.html(
view.alertTemplate({ msg: msg, classes: "alert-danger" }),
);
},
};
$.ajax(
_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()),
);
//Send a request for the EML doc itself to extract certain info
if (this.parentView && this.parentView.model) {
var formatId = this.parentView.model.get("formatId");
if (formatId.indexOf("eml://") >= 0) {
var url =
MetacatUI.appModel.get("baseUrl") +
MetacatUI.appModel.get("context") +
MetacatUI.appModel.get("d1Service") +
"/object/" +
encodeURIComponent(this.parentView.model.get("id"));
var requestSettings = {
url: url,
success: function (data, textStatus, xhr) {
if (!data || !$(data).length) return;
//Find the distribution information
var emlDoc = $(data)
.find("distribution")
.each(function (i, dist) {
var onlineDist = $(dist).children("online");
if (onlineDist.length) {
var linkText = $(onlineDist).text();
if (linkText.indexOf("ecogrid") >= 0) {
//Clean up the link text
var start = linkText.lastIndexOf("/");
var ecogridPid = linkText.substr(start + 1).trim(),
dataObjects = [];
//Iterate over each id in the package and try to fuzzily match the ecogrid link to the id
if (view.parentView.packageModels) {
//Get all the data objects in this metadata's packages
_.each(view.parentView.packageModels, function (pckg) {
dataObjects.push(pckg.get("members"));
});
dataObjects = _.flatten(dataObjects);
}
for (var i = 0; i < dataObjects.length; i++) {
//If we find a match, replace the ecogrid links with a DataONE API link to the object
if (dataObjects[i].get("id").indexOf(ecogridPid) > -1) {
var linkText = dataObjects[i].get("url");
//We can stop looking now
i = dataObjects.length;
}
}
}
var link = $(document.createElement("a"))
.attr("href", linkText)
.text(linkText),
fullHTML = view.formatAttribute(
"Online Distribution Info",
link,
);
//Find the "General" section of this page
if (view.$(".General").length)
view.$(".General").after(fullHTML);
else view.$el.append(fullHTML);
}
});
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
}
}
return this;
},
formatAttributeSection: function (doc, keys, title, className) {
if (keys.length == 0) return "";
if (typeof title === "string") {
var titleHTML = $(document.createElement("h4")).text(title);
var titleText = title;
} else if (typeof title === "undefined") {
var titleHTML = $(document.createElement("h4"));
var titleText = "";
} else {
var titleHTML = title;
var titleText = titleHTML.text();
}
var html = "",
sectionClass =
typeof className === "undefined"
? titleText.replace(/ /g, "")
: className,
view = this,
populated = false;
_.each(keys, function (key, keyNum, list) {
if (typeof key === "object" && doc.get(key.field)) {
html += view.formatAttribute(key.display, doc.get(key.field));
populated = true;
} else if (doc.get(key)) {
html += view.formatAttribute(key, doc.get(key));
populated = true;
}
});
if (populated) {
var section = $(document.createElement("section"))
.addClass(sectionClass)
.append(titleHTML)
.append(html);
return section;
} else return null;
},
formatAttribute: function (attribute, value) {
var html = "",
view = this,
embeddedAttributes = "",
type = "sem_annotation";
// see if there is special handling for this field
if (this.semanticFields[attribute]) {
type = this.semanticFields[attribute];
}
//If this is a multi-valued field from Solr, the attribute value is actually multiple embedded attribute templates
var numAttributes =
Array.isArray(value) && value.length > 1 ? value.length : 0;
for (var i = 0; i < numAttributes; i++) {
embeddedAttributes += view.attributeTemplate({
attribute: "",
formattedAttribute: view.transformCamelCase(attribute),
value: value[i].toString(),
id: attribute + "_" + (i + 1),
type: type,
resource: "#xpointer(//" + attribute + "[" + (i + 1) + "])",
});
}
if (!embeddedAttributes && value instanceof $) {
value = value[0].outerHTML;
}
html += view.attributeTemplate({
attribute: attribute,
formattedAttribute: view.transformCamelCase(attribute),
value: embeddedAttributes || value.toString(),
id: attribute,
type: type,
resource: "#xpointer(//" + attribute + ")",
});
return html;
},
transformCamelCase: function (string) {
var result = string
.replace(/([A-Z]+)/g, " $1")
.replace(/([A-Z][a-z])/g, " $1");
var finalResult = result.charAt(0).toUpperCase() + result.slice(1);
return finalResult;
},
insertDataDetails: function () {
var view = this;
//Get the Package Model - it is attached with the parent Metadata View
var pkg = this.parentView.packageModel;
if (!pkg) return;
if (pkg.get("members").length <= 1) return;
//Start some html
var html = $(document.createElement("section"));
_.each(pkg.get("members"), function (solrResult, i) {
if (solrResult.get("formatType") != "DATA") return;
solrResult.set(
"formattedSize",
Utilities.bytesToSize(solrResult.get("size")),
);
//Add a section for the data details, just like the other attribute sections
var keys = [
"id",
{ field: "formattedSize", display: "size" },
"views",
"pubDate",
"dataSource",
"formatId",
];
//Determine the icon type based on format id
var type = solrResult.getType(),
icon = "";
if (type == "program") icon = "icon-code";
else if (type == "metadata") icon = "icon-file-text";
else if (type == "image") icon = "icon-picture";
else if (type == "pdf") icon = "icon-file pdf";
else icon = "icon-table";
var icon = $(document.createElement("i")).addClass(icon),
title = $(document.createElement("span"))
.text(solrResult.get("id"))
.addClass("title"),
downloadBtn = new DownloadButtonView({ model: solrResult }),
anchor = $(document.createElement("a")).attr(
"name",
encodeURIComponent(solrResult.get("id")),
),
header = $(document.createElement("h4"))
.append(anchor)
.append(icon)
.append(title)
.append(downloadBtn.render().el);
//Create the section
var entityDetailsSection = view
.formatAttributeSection(solrResult, keys, header, "entitydetails")
.attr("data-id", solrResult.get("id"));
//Create an image thumbnail, if this is an image
if (type == "image") {
//var thumbnail = view.parentView.createThumbnail(solrResult.get("id"));
//$(entityDetailsSection).prepend(thumbnail);
}
//Mark this section with an anchor tag with the doc id
$(entityDetailsSection).prepend(
$(document.createElement("a")).attr(
"id",
solrResult.get("id").replace(/[^A-Za-z0-9]/g, "-"),
),
);
$(html).append(entityDetailsSection);
});
//Glue together the header and attribute info section
var header = $(document.createElement("h4")).text(
"Data Table, Image, and Other Data Details",
);
var section = $(html).prepend(header);
//Insert into the DOM right after the "general" information
this.$(".General").after(section);
},
//Shows a message to the user that indicates this object has not been indexed
showNotIndexed: function () {
var message = this.alertTemplate({
classes: "alert-warning",
msg:
"<h4>There is limited information about this content.</h4>" +
"<p>This data or science metadata is available to download, but " +
"there seems to be an issue with displaying details on this webpage. " +
"If this content was recently submitted, it may still be in the processing queue.</p>",
includeEmail: true,
});
this.$el.append(message);
//If this metadata doc is not indexed, we need to search the system metadata
//to see if it is publicly accessible.
if (this.parentView && this.parentView.model) {
//Get the system metadata string
var sysMeta = this.parentView.model.get("systemMetadata");
if (sysMeta) {
//Parse it into XML nodes
sysMeta = $.parseXML(sysMeta);
//Find the allow permission for the public
var publicPermission = $(sysMeta).find(
"allow subject:contains('public')",
);
if (publicPermission.length) {
//Remove the "private" icon
$("#metadata-controls-container .private").remove();
}
}
//If there is no system metadata, default to hiding the private icon
else {
$("#metadata-controls-container .private").remove();
}
}
},
flagComplete: function () {
this.complete = true;
this.trigger("complete");
},
onClose: function () {
this.$el.html(this.loadingTemplate());
this.pid = null;
//Detach this view from its parent view
this.parentView.subviews = _.without(this.parentView.subviews, this);
this.parentView = null;
//Remove listeners
this.stopListening();
},
});


🚫 [eslint] <object-shorthand> reported by reviewdog 🐶
Expected method shorthand.

insertDataDetails: function () {
var view = this;
//Get the Package Model - it is attached with the parent Metadata View
var pkg = this.parentView.packageModel;
if (!pkg) return;
if (pkg.get("members").length <= 1) return;
//Start some html
var html = $(document.createElement("section"));
_.each(pkg.get("members"), function (solrResult, i) {
if (solrResult.get("formatType") != "DATA") return;
solrResult.set(
"formattedSize",
Utilities.bytesToSize(solrResult.get("size")),
);
//Add a section for the data details, just like the other attribute sections
var keys = [
"id",
{ field: "formattedSize", display: "size" },
"views",
"pubDate",
"dataSource",
"formatId",
];
//Determine the icon type based on format id
var type = solrResult.getType(),
icon = "";
if (type == "program") icon = "icon-code";
else if (type == "metadata") icon = "icon-file-text";
else if (type == "image") icon = "icon-picture";
else if (type == "pdf") icon = "icon-file pdf";
else icon = "icon-table";
var icon = $(document.createElement("i")).addClass(icon),
title = $(document.createElement("span"))
.text(solrResult.get("id"))
.addClass("title"),
downloadBtn = new DownloadButtonView({ model: solrResult }),
anchor = $(document.createElement("a")).attr(
"name",
encodeURIComponent(solrResult.get("id")),
),
header = $(document.createElement("h4"))
.append(anchor)
.append(icon)
.append(title)
.append(downloadBtn.render().el);
//Create the section
var entityDetailsSection = view
.formatAttributeSection(solrResult, keys, header, "entitydetails")
.attr("data-id", solrResult.get("id"));
//Create an image thumbnail, if this is an image
if (type == "image") {
//var thumbnail = view.parentView.createThumbnail(solrResult.get("id"));
//$(entityDetailsSection).prepend(thumbnail);
}
//Mark this section with an anchor tag with the doc id
$(entityDetailsSection).prepend(
$(document.createElement("a")).attr(
"id",
solrResult.get("id").replace(/[^A-Za-z0-9]/g, "-"),
),
);
$(html).append(entityDetailsSection);
});
//Glue together the header and attribute info section
var header = $(document.createElement("h4")).text(
"Data Table, Image, and Other Data Details",
);
var section = $(html).prepend(header);
//Insert into the DOM right after the "general" information
this.$(".General").after(section);
},


⚠️ [eslint] <func-names> reported by reviewdog 🐶
Unexpected unnamed function.

_.each(pkg.get("members"), function (solrResult, i) {


🚫 [eslint] <prefer-arrow-callback> reported by reviewdog 🐶
Unexpected function expression.

_.each(pkg.get("members"), function (solrResult, i) {
if (solrResult.get("formatType") != "DATA") return;
solrResult.set(
"formattedSize",
Utilities.bytesToSize(solrResult.get("size")),
);
//Add a section for the data details, just like the other attribute sections
var keys = [
"id",
{ field: "formattedSize", display: "size" },
"views",
"pubDate",
"dataSource",
"formatId",
];
//Determine the icon type based on format id
var type = solrResult.getType(),
icon = "";
if (type == "program") icon = "icon-code";
else if (type == "metadata") icon = "icon-file-text";
else if (type == "image") icon = "icon-picture";
else if (type == "pdf") icon = "icon-file pdf";
else icon = "icon-table";
var icon = $(document.createElement("i")).addClass(icon),
title = $(document.createElement("span"))
.text(solrResult.get("id"))
.addClass("title"),
downloadBtn = new DownloadButtonView({ model: solrResult }),
anchor = $(document.createElement("a")).attr(
"name",
encodeURIComponent(solrResult.get("id")),
),
header = $(document.createElement("h4"))
.append(anchor)
.append(icon)
.append(title)
.append(downloadBtn.render().el);
//Create the section
var entityDetailsSection = view
.formatAttributeSection(solrResult, keys, header, "entitydetails")
.attr("data-id", solrResult.get("id"));
//Create an image thumbnail, if this is an image
if (type == "image") {
//var thumbnail = view.parentView.createThumbnail(solrResult.get("id"));
//$(entityDetailsSection).prepend(thumbnail);
}
//Mark this section with an anchor tag with the doc id
$(entityDetailsSection).prepend(
$(document.createElement("a")).attr(
"id",
solrResult.get("id").replace(/[^A-Za-z0-9]/g, "-"),
),
);
$(html).append(entityDetailsSection);
});


🚫 [eslint] <no-unused-vars> reported by reviewdog 🐶
'i' is defined but never used. Allowed unused args must match /^_/u.

_.each(pkg.get("members"), function (solrResult, i) {


🚫 [eslint] <eqeqeq> reported by reviewdog 🐶
Expected '!==' and instead saw '!='.

if (solrResult.get("formatType") != "DATA") return;


🚫 [eslint] <spaced-comment> reported by reviewdog 🐶
Expected exception block, space or tab after '//' in comment.

//Add a section for the data details, just like the other attribute sections


🚫 [eslint] <vars-on-top> reported by reviewdog 🐶
All 'var' declarations must be at the top of the function scope.

var keys = [
"id",
{ field: "formattedSize", display: "size" },
"views",
"pubDate",
"dataSource",
"formatId",
];


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var keys = [
"id",
{ field: "formattedSize", display: "size" },
"views",
"pubDate",
"dataSource",
"formatId",
];


⚠️ [eslint] <func-names> reported by reviewdog 🐶
Unexpected unnamed function.

], function ($, _, Backbone, Utilities, Package, DownloadButtonView, Template) {


🚫 [eslint] <prefer-arrow-callback> reported by reviewdog 🐶
Unexpected function expression.

], function ($, _, Backbone, Utilities, Package, DownloadButtonView, Template) {
"use strict";
var PackageTable = Backbone.View.extend({
template: _.template(Template),
type: "PackageTable",
tagName: "div",
className: "download-contents",
events: {
"click .expand-control": "expand",
"click .collapse-control": "collapse",
},
initialize: function (options) {
if (options === undefined || !options) var options = {};
this.packageId = options.packageId || null;
this.memberId = options.memberId || null;
this.attributes = options.attributes || null;
this.className += options.className || "";
this.currentlyViewing = options.currentlyViewing || null;
this.numVisible = options.numVisible || 4;
this.parentView = options.parentView || null;
this.title = options.title || "";
this.nested =
typeof options.nested === "undefined" ? false : options.nested;
//Set up the Package model
if (typeof options.model === "undefined" || !options.model) {
this.model = new Package();
this.model.set("memberId", this.memberId);
this.model.set("packageId", this.packageId);
}
if (!(typeof options.metricsModel == "undefined")) {
this.metricsModel = options.metricsModel;
}
//Get the members
if (this.packageId) this.model.getMembers();
else if (this.memberId) this.model.getMembersByMemberID(this.memberId);
this.onMetadataView =
this.parentView && this.parentView.type == "Metadata";
this.hasEntityDetails =
this.onMetadataView &&
this.model.get("members") &&
this.model.get("members").length < 150
? this.parentView.hasEntityDetails()
: false;
this.listenTo(this.model, "changeAll", this.render);
},
/*
* Creates a table of package/download contents that this metadata doc is a part of
*/
render: function () {
var view = this,
members = this.model.get("members");
//If the model isn't complete, we may be still waiting on a response from the index so don't render anything yet
if (!this.model.complete) return false;
//Start the HTML for the rows
var tbody = $(document.createElement("tbody"));
//Filter out the packages from the member list
members = _.filter(members, function (m) {
return m.type != "Package";
});
//Filter the members in order of preferred appearance
members = this.sort(members);
this.sortedMembers = members;
var metadata = this.model.getMetadata();
//Count the number of rows in this table
var numRows = members.length;
//Cut down the members list to only those that will be visible
members = members.slice(0, this.numVisible);
this.rowsComplete = false;
//Create the HTML for each row
_.each(members, function (solrResult) {
//Append the row element
$(tbody).append(view.getMemberRow(solrResult));
});
var bodyRows = $(tbody).find("tr");
this.numHidden = numRows - this.numVisible;
//Draw the footer which will have an expandable/collapsable control
if (this.numHidden > 0) {
var tfoot = $(document.createElement("tfoot")),
tfootRow = $(document.createElement("tr")),
tfootCell = $(document.createElement("th")).attr("colspan", "100%"),
item = this.numHidden == 1 ? "item" : "items",
expandLink = $(document.createElement("a"))
.addClass("expand-control control")
.text(
"Show " + this.numHidden + " more " + item + " in this data set",
),
expandIcon = $(document.createElement("i")).addClass(
"icon icon-caret-right icon-on-left",
),
collapseLink = $(document.createElement("a"))
.addClass("collapse-control control")
.text("Show less")
.css("display", "none"),
collapseIcon = $(document.createElement("i")).addClass(
"icon icon-caret-up icon-on-left",
);
$(tfoot).append(tfootRow);
$(tfootRow).append(tfootCell);
$(tfootCell).append(expandLink, collapseLink);
$(expandLink).prepend(expandIcon);
$(collapseLink).prepend(collapseIcon);
}
if (bodyRows.length == 0) {
tbody.html(
"<tr><td colspan='100%'>This is an empty dataset.</td></tr>",
);
}
if (!this.title && metadata) {
this.title =
'<a href="<%= MetacatUI.root %>/view/' +
encodeURIComponent(metadata.get("id")) +
'">Files in this dataset';
if (this.model.get("id"))
this.title +=
'<span class="subtle"> Package: ' +
this.model.get("id") +
"</span>";
this.title += "</a>";
} else if (!this.title && !metadata) {
this.title = "Files in this dataset";
}
this.$el.html(
this.template({
title: this.title || "Files in this dataset",
metadata: this.nested ? metadata : null,
colspan: bodyRows.first().find("td").length,
packageId: this.model.get("id"),
nested: this.nested,
}),
);
//Insert the Download All button
if (this.model.getURL() && this.model.get("id")) {
var downloadBtn = new DownloadButtonView({ model: this.model });
downloadBtn.render();
this.$(".download-container").append(downloadBtn.el);
}
//Add the table body and footer
this.$("thead").after(tbody);
if (typeof tfoot !== "undefined") this.$(tbody).after(tfoot);
return this;
},
sort: function (models) {
//Default to the package model members as the models to sort
if (!models) {
var models = this.model.get("members");
//If this model doesn't have members, return an empty array or a falsey value
if (!models) return models;
}
// One == already sorted!
if (models.length == 1) return models;
//If there are too many models to sort (takes too much time) then just get the metadata to display first
else if (models.length > 150) {
var view = this;
//Find the metadata doc we are currently viewing
var currentMetadata = _.find(models, function (m) {
return m.get("id") == view.currentlyViewing;
});
//Add it to the front
if (currentMetadata) {
models = _.without(models, currentMetadata);
models.unshift(currentMetadata);
}
//Return the newly sorted array
return models;
}
var view = this,
metadataView = this.onMetadataView ? this.parentView : null;
//** If this is not a nested package AND the parent view is the metadata view, then sort by order of appearance in the metadata **/
if (
!this.nested &&
metadataView &&
!_.findWhere(metadataView.subviews, { type: "MetadataIndex" })
) {
if (metadataView.hasEntityDetails()) {
//If we are currently viewing a metadata document, find it
if (this.currentlyViewing)
var currentMetadata = _.find(models, function (m) {
return m.get("id") == view.currentlyViewing;
});
//For each model, find its position on the Metadata View page
var numNotFound = 0;
_.each(models, function (model) {
if (currentMetadata == model) return;
var container = view.parentView.findEntityDetailsContainer(model);
if (container) model.offsetTop = $(container)[0].offsetTop;
else {
model.offsetTop = window.outerHeight;
numNotFound++;
}
});
//Continue only if we found the entity details section for at least one model, if not, sort by the default method later
if (numNotFound < models.length - 1) {
//Minus 1 since we don't count the metadata
//Sort the models by this position
models = _.sortBy(models, "offsetTop");
//Move the metadata model that we are currently viewing in the Metadata view to the top
if (currentMetadata) {
models = _.without(models, currentMetadata);
models.unshift(currentMetadata);
}
//Flatten the array in case we have nesting
models = _.flatten(models);
//Return the sorted array
return models;
}
}
}
//** For tables with no accompanying metadata (nested or not on the Metadata View), default to sorting by group then alpha by name**/
//Split the members of this package into groups based on their format type (metaata, data, image, code, etc)
var groupedModels = _.groupBy(models, function (m) {
if (!m.get("type") || typeof m.get("type") == "undefined")
return "data";
return m.get("type");
}),
sortedModels = [];
var rowOrder = [
"metadata",
"image",
"PDF",
"program",
"data",
"annotation",
];
for (var i = 0; i < rowOrder.length; i++) {
//Sort the members/rows alphabetically within each group
/*models = _.sortBy(models, function(m){
if(m.get("type") == "metadata") return "!"; //Always display metadata first since it will have the title in the table
return m.get("type");
}); */
var group = groupedModels[rowOrder[i]];
group = _.sortBy(group, function (m) {
return m.get("fileName") || m.get("id");
});
sortedModels.push(group);
}
models = _.flatten(sortedModels);
return models;
},
getMemberRow: function (memberModel, options) {
var formatType = memberModel.get("formatType"),
type = memberModel.type == "Package" ? "data" : memberModel.getType(),
id = memberModel.get("id"),
entityName = memberModel.get("fileName"),
url = memberModel.get("url"),
hidden = typeof options === "undefined" ? false : options.hidden,
collapsable = hidden
? true
: typeof options === "undefined"
? false
: options.collapsable;
if (!url) {
memberModel.setURL();
url = memberModel.get("url");
}
//Use the metadata title instead of the ID
if (!entityName && formatType == "METADATA")
entityName = memberModel.get("title");
if (formatType == "METADATA" && entityName)
entityName = "Metadata: " + entityName;
else if (formatType == "METADATA" && !entityName) entityName = "Metadata";
//Display the id in the table if not name is present
if (typeof entityName === "undefined" || !entityName) entityName = id;
//Create a row for this member of the data package
var tr = $(document.createElement("tr"));
//Icon cell (based on formatType)
var iconCell = $(document.createElement("td")).addClass("format-type"),
formatTypeIcon = document.createElement("i"),
icon = "icon-table";
//Determine the icon type based on format type
if (type == "program") icon = "icon-code";
else if (type == "data") icon = "icon-table";
else if (type == "metadata") icon = "icon-file-text";
else if (type == "image") icon = "icon-picture";
else if (type == "pdf") icon = "icon-file pdf";
$(formatTypeIcon)
.addClass(icon)
.tooltip({
placement: "top",
trigger: "hover focus",
title: type.charAt(0).toUpperCase() + type.slice(1),
});
$(iconCell).html(formatTypeIcon);
$(tr).append(iconCell);
//Name cell
var nameCell = $(document.createElement("td")).addClass(
"name wrap-contents",
);
var nameEl = $(document.createElement("span")).text(entityName);
$(nameCell).html(nameEl);
$(tr).append(nameCell);
if (entityName == id)
$(nameCell).addClass("entity-name-placeholder").attr("data-id", id);
//"More info" cell
var moreInfoCell = $(document.createElement("td")).addClass("more-info");
//If we are on the metadata view and there is no entity details section, then append a blank cell
var entityDetails = this.hasEntityDetails
? this.parentView.findEntityDetailsContainer(memberModel)
: false,
currentlyViewing = id == this.currentlyViewing;
if (
(this.onMetadataView && !this.hasEntityDetails) ||
(this.onMetadataView && !entityDetails) ||
currentlyViewing ||
this.nested
) {
$(tr).append(moreInfoCell);
} else {
let metadataId =
this.onMetadataView && this.currentlyViewing
? this.currentlyViewing
: memberModel.get("isDocumentedBy")[0];
var moreInfo = $(document.createElement("a"))
.attr(
"href",
MetacatUI.root +
"/view/" +
encodeURIComponent(metadataId) +
"#" +
encodeURIComponent(id),
)
.addClass("preview")
.attr("data-id", id)
.text("More info");
$(moreInfoCell).append(moreInfo);
}
$(tr).append(moreInfoCell);
//Format id cell
var fileTypeCell = $(document.createElement("td")).addClass(
"formatId wrap-contents",
);
$(fileTypeCell).html(memberModel.getFormat());
$(tr).append(fileTypeCell);
//File size cell
var sizeCell = $(document.createElement("td")).addClass("size");
var size = Utilities.bytesToSize(memberModel.get("size"));
memberModel.set("sizeStr", size);
$(sizeCell).text(size);
$(tr).append(sizeCell);
if (MetacatUI.appModel.get("displayDatasetMetrics")) {
// Retreiving the Package Metrics Counts from the Metrics Model
// Adding a Metric Cell for the corresponding DataONE object in the table
var readsCell = $(document.createElement("td"))
.addClass("metrics-count downloads")
.attr("data-id", id);
$(tr).append(readsCell);
if (!memberModel.hideMetrics()) {
// If the model has already been fethced.
if (this.metricsModel.get("views") !== null) {
readsCell.append(this.getMemberRowMetrics(id, formatType));
} else {
// Update the metrics later on
// If the fetch() is still in progress.
this.listenTo(this.metricsModel, "sync", function () {
var readsCell = this.$(
'.metrics-count.downloads[data-id="' + id + '"]',
);
readsCell.text(this.getMemberRowMetrics(id, formatType));
});
}
}
}
//Download button cell
var downloadBtnCell = $(document.createElement("td")).addClass(
"download-btn btn-container",
);
var downloadButton = new DownloadButtonView({ model: memberModel });
downloadButton.render();
$(downloadBtnCell).append(downloadButton.el);
$(tr).append(downloadBtnCell);
if (collapsable) tr.addClass("collapse");
if (hidden) tr.css("display", "none");
return tr;
},
// Member row metrics for the package table
// Retrieving information from the Metrics Model's result details
getMemberRowMetrics: function (id, formatType) {
if (typeof this.metricsModel !== "undefined") {
var metricsResultDetails = this.metricsModel.get("resultDetails");
if (
typeof metricsResultDetails !== "undefined" &&
metricsResultDetails
) {
var metricsPackageDetails =
metricsResultDetails["metrics_package_counts"];
var objectLevelMetrics = metricsPackageDetails[id];
if (typeof objectLevelMetrics !== "undefined") {
if (formatType == "METADATA") {
var reads = objectLevelMetrics["viewCount"];
} else {
var reads = objectLevelMetrics["downloadCount"];
}
} else {
var reads = 0;
}
} else {
var reads = 0;
}
}
if (typeof reads !== "undefined" && reads) {
// giving labels
if (formatType == "METADATA" && reads == 1) reads += " view";
else if (formatType == "METADATA") reads += " views";
else if (reads == 1) reads += " download";
else reads += " downloads";
} else {
// returning an empty string if the metrics are 0
reads = "";
}
return reads;
},
expand: function (e) {
//Don't do anything...
e.preventDefault();
var view = this;
//If this is a nested dataset, we need to actually draw the remaining rows
if (!this.rowsComplete) {
var tbody = this.$("tbody");
//Create the HTML for each row
var members = this.sortedMembers.slice(this.numVisible);
_.each(members, function (solrResult) {
//Append the row element
$(tbody).append(view.getMemberRow(solrResult, { collapsable: true }));
});
//Make the view as complete so we don't do this again
this.rowsComplete = true;
}
this.$("tr.collapse").fadeIn();
this.$(".expand-control").fadeOut(function () {
view.$(".collapse-control").fadeIn("fast");
view.$(".tooltip-this").tooltip();
});
},
collapse: function (e) {
//Don't do anything...
e.preventDefault();
var view = this;
this.$("tr.collapse").fadeOut();
this.$(".collapse-control").fadeOut(function () {
view.$(".expand-control").fadeIn();
});
},
checkForPrivateMembers: function () {
try {
var packageModel = this.model,
packageCollection = this.dataPackageCollection;
if (!packageModel || !packageCollection) {
return;
}
//Get the number of package members found in Solr and parsed from the RDF XML
var numMembersFromSolr = packageModel.get("members").length,
numMembersFromRDF = packageCollection.length;
//If there are more package members in the RDF XML tthan found in SOlr, we
// can assume that those objects are private.
if (numMembersFromRDF > numMembersFromSolr) {
var downloadButtons = this.$(".btn.download");
for (var i = 0; i < downloadButtons.length; i++) {
var btn = downloadButtons[i];
//Find the Download All button for the package
var downloadURL = $(btn).attr("href");
if (
downloadURL.indexOf(packageModel.get("id")) > -1 ||
downloadURL.indexOf(encodeURIComponent(packageModel.get("id"))) >
-1
) {
//Disable this download button
$(btn)
.attr("disabled", "disabled")
.addClass("disabled")
.attr("href", "")
.tooltip({
trigger: "hover",
placement: "top",
delay: 500,
title:
"This dataset may contain private data, so each data file should be downloaded individually.",
});
i = downloadButtons.length;
}
}
}
} catch (e) {
console.error(e);
}
},
/*showDownloadProgress: function(e){
e.preventDefault();
var button = $(e.target);
button.addClass("in-progress");
button.html("Downloading... <i class='icon icon-on-right icon-spinner icon-spin'></i>");
return true;
}*/
});
return PackageTable;
});


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var PackageTable = Backbone.View.extend({
template: _.template(Template),
type: "PackageTable",
tagName: "div",
className: "download-contents",
events: {
"click .expand-control": "expand",
"click .collapse-control": "collapse",
},
initialize: function (options) {
if (options === undefined || !options) var options = {};
this.packageId = options.packageId || null;
this.memberId = options.memberId || null;
this.attributes = options.attributes || null;
this.className += options.className || "";
this.currentlyViewing = options.currentlyViewing || null;
this.numVisible = options.numVisible || 4;
this.parentView = options.parentView || null;
this.title = options.title || "";
this.nested =
typeof options.nested === "undefined" ? false : options.nested;
//Set up the Package model
if (typeof options.model === "undefined" || !options.model) {
this.model = new Package();
this.model.set("memberId", this.memberId);
this.model.set("packageId", this.packageId);
}
if (!(typeof options.metricsModel == "undefined")) {
this.metricsModel = options.metricsModel;
}
//Get the members
if (this.packageId) this.model.getMembers();
else if (this.memberId) this.model.getMembersByMemberID(this.memberId);
this.onMetadataView =
this.parentView && this.parentView.type == "Metadata";
this.hasEntityDetails =
this.onMetadataView &&
this.model.get("members") &&
this.model.get("members").length < 150
? this.parentView.hasEntityDetails()
: false;
this.listenTo(this.model, "changeAll", this.render);
},
/*
* Creates a table of package/download contents that this metadata doc is a part of
*/
render: function () {
var view = this,
members = this.model.get("members");
//If the model isn't complete, we may be still waiting on a response from the index so don't render anything yet
if (!this.model.complete) return false;
//Start the HTML for the rows
var tbody = $(document.createElement("tbody"));
//Filter out the packages from the member list
members = _.filter(members, function (m) {
return m.type != "Package";
});
//Filter the members in order of preferred appearance
members = this.sort(members);
this.sortedMembers = members;
var metadata = this.model.getMetadata();
//Count the number of rows in this table
var numRows = members.length;
//Cut down the members list to only those that will be visible
members = members.slice(0, this.numVisible);
this.rowsComplete = false;
//Create the HTML for each row
_.each(members, function (solrResult) {
//Append the row element
$(tbody).append(view.getMemberRow(solrResult));
});
var bodyRows = $(tbody).find("tr");
this.numHidden = numRows - this.numVisible;
//Draw the footer which will have an expandable/collapsable control
if (this.numHidden > 0) {
var tfoot = $(document.createElement("tfoot")),
tfootRow = $(document.createElement("tr")),
tfootCell = $(document.createElement("th")).attr("colspan", "100%"),
item = this.numHidden == 1 ? "item" : "items",
expandLink = $(document.createElement("a"))
.addClass("expand-control control")
.text(
"Show " + this.numHidden + " more " + item + " in this data set",
),
expandIcon = $(document.createElement("i")).addClass(
"icon icon-caret-right icon-on-left",
),
collapseLink = $(document.createElement("a"))
.addClass("collapse-control control")
.text("Show less")
.css("display", "none"),
collapseIcon = $(document.createElement("i")).addClass(
"icon icon-caret-up icon-on-left",
);
$(tfoot).append(tfootRow);
$(tfootRow).append(tfootCell);
$(tfootCell).append(expandLink, collapseLink);
$(expandLink).prepend(expandIcon);
$(collapseLink).prepend(collapseIcon);
}
if (bodyRows.length == 0) {
tbody.html(
"<tr><td colspan='100%'>This is an empty dataset.</td></tr>",
);
}
if (!this.title && metadata) {
this.title =
'<a href="<%= MetacatUI.root %>/view/' +
encodeURIComponent(metadata.get("id")) +
'">Files in this dataset';
if (this.model.get("id"))
this.title +=
'<span class="subtle"> Package: ' +
this.model.get("id") +
"</span>";
this.title += "</a>";
} else if (!this.title && !metadata) {
this.title = "Files in this dataset";
}
this.$el.html(
this.template({
title: this.title || "Files in this dataset",
metadata: this.nested ? metadata : null,
colspan: bodyRows.first().find("td").length,
packageId: this.model.get("id"),
nested: this.nested,
}),
);
//Insert the Download All button
if (this.model.getURL() && this.model.get("id")) {
var downloadBtn = new DownloadButtonView({ model: this.model });
downloadBtn.render();
this.$(".download-container").append(downloadBtn.el);
}
//Add the table body and footer
this.$("thead").after(tbody);
if (typeof tfoot !== "undefined") this.$(tbody).after(tfoot);
return this;
},
sort: function (models) {
//Default to the package model members as the models to sort
if (!models) {
var models = this.model.get("members");
//If this model doesn't have members, return an empty array or a falsey value
if (!models) return models;
}
// One == already sorted!
if (models.length == 1) return models;
//If there are too many models to sort (takes too much time) then just get the metadata to display first
else if (models.length > 150) {
var view = this;
//Find the metadata doc we are currently viewing
var currentMetadata = _.find(models, function (m) {
return m.get("id") == view.currentlyViewing;
});
//Add it to the front
if (currentMetadata) {
models = _.without(models, currentMetadata);
models.unshift(currentMetadata);
}
//Return the newly sorted array
return models;
}
var view = this,
metadataView = this.onMetadataView ? this.parentView : null;
//** If this is not a nested package AND the parent view is the metadata view, then sort by order of appearance in the metadata **/
if (
!this.nested &&
metadataView &&
!_.findWhere(metadataView.subviews, { type: "MetadataIndex" })
) {
if (metadataView.hasEntityDetails()) {
//If we are currently viewing a metadata document, find it
if (this.currentlyViewing)
var currentMetadata = _.find(models, function (m) {
return m.get("id") == view.currentlyViewing;
});
//For each model, find its position on the Metadata View page
var numNotFound = 0;
_.each(models, function (model) {
if (currentMetadata == model) return;
var container = view.parentView.findEntityDetailsContainer(model);
if (container) model.offsetTop = $(container)[0].offsetTop;
else {
model.offsetTop = window.outerHeight;
numNotFound++;
}
});
//Continue only if we found the entity details section for at least one model, if not, sort by the default method later
if (numNotFound < models.length - 1) {
//Minus 1 since we don't count the metadata
//Sort the models by this position
models = _.sortBy(models, "offsetTop");
//Move the metadata model that we are currently viewing in the Metadata view to the top
if (currentMetadata) {
models = _.without(models, currentMetadata);
models.unshift(currentMetadata);
}
//Flatten the array in case we have nesting
models = _.flatten(models);
//Return the sorted array
return models;
}
}
}
//** For tables with no accompanying metadata (nested or not on the Metadata View), default to sorting by group then alpha by name**/
//Split the members of this package into groups based on their format type (metaata, data, image, code, etc)
var groupedModels = _.groupBy(models, function (m) {
if (!m.get("type") || typeof m.get("type") == "undefined")
return "data";
return m.get("type");
}),
sortedModels = [];
var rowOrder = [
"metadata",
"image",
"PDF",
"program",
"data",
"annotation",
];
for (var i = 0; i < rowOrder.length; i++) {
//Sort the members/rows alphabetically within each group
/*models = _.sortBy(models, function(m){
if(m.get("type") == "metadata") return "!"; //Always display metadata first since it will have the title in the table
return m.get("type");
}); */
var group = groupedModels[rowOrder[i]];
group = _.sortBy(group, function (m) {
return m.get("fileName") || m.get("id");
});
sortedModels.push(group);
}
models = _.flatten(sortedModels);
return models;
},
getMemberRow: function (memberModel, options) {
var formatType = memberModel.get("formatType"),
type = memberModel.type == "Package" ? "data" : memberModel.getType(),
id = memberModel.get("id"),
entityName = memberModel.get("fileName"),
url = memberModel.get("url"),
hidden = typeof options === "undefined" ? false : options.hidden,
collapsable = hidden
? true
: typeof options === "undefined"
? false
: options.collapsable;
if (!url) {
memberModel.setURL();
url = memberModel.get("url");
}
//Use the metadata title instead of the ID
if (!entityName && formatType == "METADATA")
entityName = memberModel.get("title");
if (formatType == "METADATA" && entityName)
entityName = "Metadata: " + entityName;
else if (formatType == "METADATA" && !entityName) entityName = "Metadata";
//Display the id in the table if not name is present
if (typeof entityName === "undefined" || !entityName) entityName = id;
//Create a row for this member of the data package
var tr = $(document.createElement("tr"));
//Icon cell (based on formatType)
var iconCell = $(document.createElement("td")).addClass("format-type"),
formatTypeIcon = document.createElement("i"),
icon = "icon-table";
//Determine the icon type based on format type
if (type == "program") icon = "icon-code";
else if (type == "data") icon = "icon-table";
else if (type == "metadata") icon = "icon-file-text";
else if (type == "image") icon = "icon-picture";
else if (type == "pdf") icon = "icon-file pdf";
$(formatTypeIcon)
.addClass(icon)
.tooltip({
placement: "top",
trigger: "hover focus",
title: type.charAt(0).toUpperCase() + type.slice(1),
});
$(iconCell).html(formatTypeIcon);
$(tr).append(iconCell);
//Name cell
var nameCell = $(document.createElement("td")).addClass(
"name wrap-contents",
);
var nameEl = $(document.createElement("span")).text(entityName);
$(nameCell).html(nameEl);
$(tr).append(nameCell);
if (entityName == id)
$(nameCell).addClass("entity-name-placeholder").attr("data-id", id);
//"More info" cell
var moreInfoCell = $(document.createElement("td")).addClass("more-info");
//If we are on the metadata view and there is no entity details section, then append a blank cell
var entityDetails = this.hasEntityDetails
? this.parentView.findEntityDetailsContainer(memberModel)
: false,
currentlyViewing = id == this.currentlyViewing;
if (
(this.onMetadataView && !this.hasEntityDetails) ||
(this.onMetadataView && !entityDetails) ||
currentlyViewing ||
this.nested
) {
$(tr).append(moreInfoCell);
} else {
let metadataId =
this.onMetadataView && this.currentlyViewing
? this.currentlyViewing
: memberModel.get("isDocumentedBy")[0];
var moreInfo = $(document.createElement("a"))
.attr(
"href",
MetacatUI.root +
"/view/" +
encodeURIComponent(metadataId) +
"#" +
encodeURIComponent(id),
)
.addClass("preview")
.attr("data-id", id)
.text("More info");
$(moreInfoCell).append(moreInfo);
}
$(tr).append(moreInfoCell);
//Format id cell
var fileTypeCell = $(document.createElement("td")).addClass(
"formatId wrap-contents",
);
$(fileTypeCell).html(memberModel.getFormat());
$(tr).append(fileTypeCell);
//File size cell
var sizeCell = $(document.createElement("td")).addClass("size");
var size = Utilities.bytesToSize(memberModel.get("size"));
memberModel.set("sizeStr", size);
$(sizeCell).text(size);
$(tr).append(sizeCell);
if (MetacatUI.appModel.get("displayDatasetMetrics")) {
// Retreiving the Package Metrics Counts from the Metrics Model
// Adding a Metric Cell for the corresponding DataONE object in the table
var readsCell = $(document.createElement("td"))
.addClass("metrics-count downloads")
.attr("data-id", id);
$(tr).append(readsCell);
if (!memberModel.hideMetrics()) {
// If the model has already been fethced.
if (this.metricsModel.get("views") !== null) {
readsCell.append(this.getMemberRowMetrics(id, formatType));
} else {
// Update the metrics later on
// If the fetch() is still in progress.
this.listenTo(this.metricsModel, "sync", function () {
var readsCell = this.$(
'.metrics-count.downloads[data-id="' + id + '"]',
);
readsCell.text(this.getMemberRowMetrics(id, formatType));
});
}
}
}
//Download button cell
var downloadBtnCell = $(document.createElement("td")).addClass(
"download-btn btn-container",
);
var downloadButton = new DownloadButtonView({ model: memberModel });
downloadButton.render();
$(downloadBtnCell).append(downloadButton.el);
$(tr).append(downloadBtnCell);
if (collapsable) tr.addClass("collapse");
if (hidden) tr.css("display", "none");
return tr;
},
// Member row metrics for the package table
// Retrieving information from the Metrics Model's result details
getMemberRowMetrics: function (id, formatType) {
if (typeof this.metricsModel !== "undefined") {
var metricsResultDetails = this.metricsModel.get("resultDetails");
if (
typeof metricsResultDetails !== "undefined" &&
metricsResultDetails
) {
var metricsPackageDetails =
metricsResultDetails["metrics_package_counts"];
var objectLevelMetrics = metricsPackageDetails[id];
if (typeof objectLevelMetrics !== "undefined") {
if (formatType == "METADATA") {
var reads = objectLevelMetrics["viewCount"];
} else {
var reads = objectLevelMetrics["downloadCount"];
}
} else {
var reads = 0;
}
} else {
var reads = 0;
}
}
if (typeof reads !== "undefined" && reads) {
// giving labels
if (formatType == "METADATA" && reads == 1) reads += " view";
else if (formatType == "METADATA") reads += " views";
else if (reads == 1) reads += " download";
else reads += " downloads";
} else {
// returning an empty string if the metrics are 0
reads = "";
}
return reads;
},
expand: function (e) {
//Don't do anything...
e.preventDefault();
var view = this;
//If this is a nested dataset, we need to actually draw the remaining rows
if (!this.rowsComplete) {
var tbody = this.$("tbody");
//Create the HTML for each row
var members = this.sortedMembers.slice(this.numVisible);
_.each(members, function (solrResult) {
//Append the row element
$(tbody).append(view.getMemberRow(solrResult, { collapsable: true }));
});
//Make the view as complete so we don't do this again
this.rowsComplete = true;
}
this.$("tr.collapse").fadeIn();
this.$(".expand-control").fadeOut(function () {
view.$(".collapse-control").fadeIn("fast");
view.$(".tooltip-this").tooltip();
});
},
collapse: function (e) {
//Don't do anything...
e.preventDefault();
var view = this;
this.$("tr.collapse").fadeOut();
this.$(".collapse-control").fadeOut(function () {
view.$(".expand-control").fadeIn();
});
},
checkForPrivateMembers: function () {
try {
var packageModel = this.model,
packageCollection = this.dataPackageCollection;
if (!packageModel || !packageCollection) {
return;
}
//Get the number of package members found in Solr and parsed from the RDF XML
var numMembersFromSolr = packageModel.get("members").length,
numMembersFromRDF = packageCollection.length;
//If there are more package members in the RDF XML tthan found in SOlr, we
// can assume that those objects are private.
if (numMembersFromRDF > numMembersFromSolr) {
var downloadButtons = this.$(".btn.download");
for (var i = 0; i < downloadButtons.length; i++) {
var btn = downloadButtons[i];
//Find the Download All button for the package
var downloadURL = $(btn).attr("href");
if (
downloadURL.indexOf(packageModel.get("id")) > -1 ||
downloadURL.indexOf(encodeURIComponent(packageModel.get("id"))) >
-1
) {
//Disable this download button
$(btn)
.attr("disabled", "disabled")
.addClass("disabled")
.attr("href", "")
.tooltip({
trigger: "hover",
placement: "top",
delay: 500,
title:
"This dataset may contain private data, so each data file should be downloaded individually.",
});
i = downloadButtons.length;
}
}
}
} catch (e) {
console.error(e);
}
},
/*showDownloadProgress: function(e){
e.preventDefault();
var button = $(e.target);
button.addClass("in-progress");
button.html("Downloading... <i class='icon icon-on-right icon-spinner icon-spin'></i>");
return true;
}*/
});


🚫 [eslint] <object-shorthand> reported by reviewdog 🐶
Expected method shorthand.

getMemberRow: function (memberModel, options) {
var formatType = memberModel.get("formatType"),
type = memberModel.type == "Package" ? "data" : memberModel.getType(),
id = memberModel.get("id"),
entityName = memberModel.get("fileName"),
url = memberModel.get("url"),
hidden = typeof options === "undefined" ? false : options.hidden,
collapsable = hidden
? true
: typeof options === "undefined"
? false
: options.collapsable;
if (!url) {
memberModel.setURL();
url = memberModel.get("url");
}
//Use the metadata title instead of the ID
if (!entityName && formatType == "METADATA")
entityName = memberModel.get("title");
if (formatType == "METADATA" && entityName)
entityName = "Metadata: " + entityName;
else if (formatType == "METADATA" && !entityName) entityName = "Metadata";
//Display the id in the table if not name is present
if (typeof entityName === "undefined" || !entityName) entityName = id;
//Create a row for this member of the data package
var tr = $(document.createElement("tr"));
//Icon cell (based on formatType)
var iconCell = $(document.createElement("td")).addClass("format-type"),
formatTypeIcon = document.createElement("i"),
icon = "icon-table";
//Determine the icon type based on format type
if (type == "program") icon = "icon-code";
else if (type == "data") icon = "icon-table";
else if (type == "metadata") icon = "icon-file-text";
else if (type == "image") icon = "icon-picture";
else if (type == "pdf") icon = "icon-file pdf";
$(formatTypeIcon)
.addClass(icon)
.tooltip({
placement: "top",
trigger: "hover focus",
title: type.charAt(0).toUpperCase() + type.slice(1),
});
$(iconCell).html(formatTypeIcon);
$(tr).append(iconCell);
//Name cell
var nameCell = $(document.createElement("td")).addClass(
"name wrap-contents",
);
var nameEl = $(document.createElement("span")).text(entityName);
$(nameCell).html(nameEl);
$(tr).append(nameCell);
if (entityName == id)
$(nameCell).addClass("entity-name-placeholder").attr("data-id", id);
//"More info" cell
var moreInfoCell = $(document.createElement("td")).addClass("more-info");
//If we are on the metadata view and there is no entity details section, then append a blank cell
var entityDetails = this.hasEntityDetails
? this.parentView.findEntityDetailsContainer(memberModel)
: false,
currentlyViewing = id == this.currentlyViewing;
if (
(this.onMetadataView && !this.hasEntityDetails) ||
(this.onMetadataView && !entityDetails) ||
currentlyViewing ||
this.nested
) {
$(tr).append(moreInfoCell);
} else {
let metadataId =
this.onMetadataView && this.currentlyViewing
? this.currentlyViewing
: memberModel.get("isDocumentedBy")[0];
var moreInfo = $(document.createElement("a"))
.attr(
"href",
MetacatUI.root +
"/view/" +
encodeURIComponent(metadataId) +
"#" +
encodeURIComponent(id),
)
.addClass("preview")
.attr("data-id", id)
.text("More info");
$(moreInfoCell).append(moreInfo);
}
$(tr).append(moreInfoCell);
//Format id cell
var fileTypeCell = $(document.createElement("td")).addClass(
"formatId wrap-contents",
);
$(fileTypeCell).html(memberModel.getFormat());
$(tr).append(fileTypeCell);
//File size cell
var sizeCell = $(document.createElement("td")).addClass("size");
var size = Utilities.bytesToSize(memberModel.get("size"));
memberModel.set("sizeStr", size);
$(sizeCell).text(size);
$(tr).append(sizeCell);
if (MetacatUI.appModel.get("displayDatasetMetrics")) {
// Retreiving the Package Metrics Counts from the Metrics Model
// Adding a Metric Cell for the corresponding DataONE object in the table
var readsCell = $(document.createElement("td"))
.addClass("metrics-count downloads")
.attr("data-id", id);
$(tr).append(readsCell);
if (!memberModel.hideMetrics()) {
// If the model has already been fethced.
if (this.metricsModel.get("views") !== null) {
readsCell.append(this.getMemberRowMetrics(id, formatType));
} else {
// Update the metrics later on
// If the fetch() is still in progress.
this.listenTo(this.metricsModel, "sync", function () {
var readsCell = this.$(
'.metrics-count.downloads[data-id="' + id + '"]',
);
readsCell.text(this.getMemberRowMetrics(id, formatType));
});
}
}
}
//Download button cell
var downloadBtnCell = $(document.createElement("td")).addClass(
"download-btn btn-container",
);
var downloadButton = new DownloadButtonView({ model: memberModel });
downloadButton.render();
$(downloadBtnCell).append(downloadButton.el);
$(tr).append(downloadBtnCell);
if (collapsable) tr.addClass("collapse");
if (hidden) tr.css("display", "none");
return tr;
},


🚫 [eslint] <spaced-comment> reported by reviewdog 🐶
Expected exception block, space or tab after '//' in comment.


🚫 [eslint] <vars-on-top> reported by reviewdog 🐶
All 'var' declarations must be at the top of the function scope.

var sizeCell = $(document.createElement("td")).addClass("size");


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var sizeCell = $(document.createElement("td")).addClass("size");


🚫 [eslint] <vars-on-top> reported by reviewdog 🐶
All 'var' declarations must be at the top of the function scope.

var size = Utilities.bytesToSize(memberModel.get("size"));


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var size = Utilities.bytesToSize(memberModel.get("size"));


🚫 [eslint] <prefer-arrow-callback> reported by reviewdog 🐶
Unexpected function expression.

], function (
$,
_,
Backbone,
d3,
LineChart,
BarChart,
DonutChart,
CircleBadge,
Citations,
Utilities,
MetricsModel,
StatsModel,
MetricsChart,
CitationList,
MetricModalTemplate,
profileTemplate,
AlertTemplate,
LoadingTemplate,
MetricsLoadingTemplate,
) {
"use strict";
var StatsView = Backbone.View.extend(
/** @lends StatsView.prototype */ {
el: "#Content",
model: null,
hideUpdatesChart: false,
/*
* Flag to indicate whether the statsview is a node summary view
* @type {boolean}
*/
nodeSummaryView: false,
/**
* Whether or not to show the graph that indicated the assessment score for all metadata in the query.
* @type {boolean}
*/
hideMetadataAssessment: false,
subviews: [],
template: _.template(profileTemplate),
metricTemplate: _.template(MetricModalTemplate),
alertTemplate: _.template(AlertTemplate),
loadingTemplate: _.template(LoadingTemplate),
metricsLoadingTemplate: _.template(MetricsLoadingTemplate),
initialize: function (options) {
if (!options) options = {};
this.title =
typeof options.title === "undefined"
? "Summary of Holdings"
: options.title;
this.description =
typeof options.description === "undefined"
? "A summary of all datasets in our catalog."
: options.description;
this.metricsModel =
typeof options.metricsModel === undefined
? undefined
: options.metricsModel;
this.userType =
typeof options.userType === undefined ? undefined : options.userType;
this.userId =
typeof options.userId === undefined ? undefined : options.userId;
this.userLabel =
typeof options.userLabel === undefined
? undefined
: options.userLabel;
if (typeof options.el === "undefined") this.el = options.el;
this.hideUpdatesChart =
options.hideUpdatesChart === true ? true : false;
this.hideMetadataAssessment =
typeof options.hideMetadataAssessment === "undefined"
? true
: options.hideMetadataAssessment;
this.hideCitationsChart =
typeof options.hideCitationsChart === "undefined"
? true
: options.hideCitationsChart;
this.hideDownloadsChart =
typeof options.hideDownloadsChart === "undefined"
? true
: options.hideDownloadsChart;
this.hideViewsChart =
typeof options.hideViewsChart === "undefined"
? true
: options.hideViewsChart;
this.model = options.model || null;
},
render: function (options) {
//The Node info needs to be fetched first since a lot of this code requires info about MNs
if (
!MetacatUI.nodeModel.get("checked") &&
!MetacatUI.nodeModel.get("error")
) {
this.listenToOnce(
MetacatUI.nodeModel,
"change:checked error",
function () {
//Remove listeners and render the view, even if there was an error fetching the NodeModel
this.stopListening(MetacatUI.nodeModel);
this.render(options);
},
);
this.$el.html(this.loadingTemplate);
return;
}
if (!options) options = {};
var view = this,
userIsCN = false,
nodeId,
isHostedRepo = false;
// Check if the node is a coordinating node
this.userIsCN = userIsCN;
if (this.userType !== undefined && this.userLabel !== undefined) {
if (this.userType === "repository") {
userIsCN = MetacatUI.nodeModel.isCN(this.userId);
if (userIsCN && typeof isCN !== "undefined") this.userIsCN = true;
}
}
if (options.nodeSummaryView) {
this.nodeSummaryView = true;
nodeId = MetacatUI.appModel.get("nodeId");
userIsCN = MetacatUI.nodeModel.isCN(nodeId);
//Save whether this profile is for a CN
if (userIsCN && typeof userIsCN !== "undefined") {
this.userIsCN = true;
}
//Figure out if this profile is for a hosted repo
else if (nodeId) {
isHostedRepo = _.contains(
MetacatUI.appModel.get("dataoneHostedRepos"),
nodeId,
);
}
// Disable the metrics if the nodeId is not available or if it is not a DataONE Hosted Repo
if (
!this.userIsCN &&
(nodeId === "undefined" || nodeId === null || !isHostedRepo)
) {
this.hideCitationsChart = true;
this.hideDownloadsChart = true;
this.hideViewsChart = true;
this.hideMetadataAssessment = true;
} else {
// Overwrite the metrics display flags as set in the AppModel
this.hideMetadataAssessment = MetacatUI.appModel.get(
"hideSummaryMetadataAssessment",
);
this.hideCitationsChart = MetacatUI.appModel.get(
"hideSummaryCitationsChart",
);
this.hideDownloadsChart = MetacatUI.appModel.get(
"hideSummaryDownloadsChart",
);
this.hideViewsChart = MetacatUI.appModel.get(
"hideSummaryViewsChart",
);
}
}
if (
!this.hideCitationsChart ||
!this.hideDownloadsChart ||
!this.hideViewsChart
) {
if (typeof this.metricsModel === "undefined") {
// Create a list with the repository ID
var pid_list = new Array();
pid_list.push(nodeId);
// Create a new object of the metrics model
var metricsModel = new MetricsModel({
pid_list: pid_list,
type: this.userType,
});
metricsModel.fetch();
this.metricsModel = metricsModel;
}
}
if (!this.model) {
this.model = new StatsModel({
hideMetadataAssessment: this.hideMetadataAssessment,
mdqImageId: nodeId,
});
}
//Clear the page
this.$el.html("");
//Only trigger the functions that draw SVG charts if d3 loaded correctly
if (d3) {
//Draw a chart that shows the temporal coverage of all datasets in this collection
this.listenTo(
this.model,
"change:temporalCoverage",
this.drawCoverageChart,
);
//Draw charts that plot the latest updates of metadata and data files
this.listenTo(
this.model,
"change:dataUpdateDates",
this.drawDataUpdatesChart,
);
this.listenTo(
this.model,
"change:metadataUpdateDates",
this.drawMetadataUpdatesChart,
);
//Render the total file size of all contents in this collection
this.listenTo(this.model, "change:totalSize", this.displayTotalSize);
//Render the total number of datasets in this collection
this.listenTo(
this.model,
"change:metadataCount",
this.displayTotalCount,
);
// Display replicas only for member nodes
if (this.userType === "repository" && !this.userIsCN)
this.listenTo(
this.model,
"change:totalReplicas",
this.displayTotalReplicas,
);
//Draw charts that show the breakdown of format IDs for metadata and data files
this.listenTo(
this.model,
"change:dataFormatIDs",
this.drawDataCountChart,
);
this.listenTo(
this.model,
"change:metadataFormatIDs",
this.drawMetadataCountChart,
);
}
//When the last coverage endDate is found, draw a title for the temporal coverage chart
this.listenTo(
this.model,
"change:lastEndDate",
this.drawCoverageChartTitle,
);
//When the total count is updated, check if there if the count is 0, so we can show there is no "activity" for this collection
this.listenTo(this.model, "change:totalCount", this.showNoActivity);
// set the header type
MetacatUI.appModel.set("headerType", "default");
// Loading template for the FAIR chart
var fairLoadingHtml = this.metricsLoadingTemplate({
message: "Running an assessment report...",
character: "none",
type: "FAIR",
});
// Loading template for the citations section
var citationsLoadingHtml = this.metricsLoadingTemplate({
message:
"Scouring our records for publications that cited these datasets...",
character: "none",
type: "citations",
});
// Loading template for the downloads bar chart
var downloadsLoadingHtml = this.metricsLoadingTemplate({
message: "Crunching some numbers...",
character: "developer",
type: "barchart",
});
// Loading template for the views bar chart
var viewsLoadingHtml = this.metricsLoadingTemplate({
message: "Just doing a few more calculations...",
character: "statistician",
type: "barchart",
});
//Insert the template
this.$el.html(
this.template({
query: this.model.get("query"),
title: this.title,
description: this.description,
userType: this.userType,
userIsCN: this.userIsCN,
fairLoadingHtml: fairLoadingHtml,
citationsLoadingHtml: citationsLoadingHtml,
downloadsLoadingHtml: downloadsLoadingHtml,
viewsLoadingHtml: viewsLoadingHtml,
hideUpdatesChart: this.hideUpdatesChart,
hideCitationsChart: this.hideCitationsChart,
hideDownloadsChart: this.hideDownloadsChart,
hideViewsChart: this.hideViewsChart,
hideMetadataAssessment: this.hideMetadataAssessment,
}),
);
// Insert the metadata assessment chart
var view = this;
if (this.hideMetadataAssessment !== true) {
this.listenTo(
this.model,
"change:mdqScoresImage",
this.drawMetadataAssessment,
);
this.listenTo(this.model, "change:mdqScoresError", function () {
view.renderMetadataAssessmentError();
});
}
//Insert the loading template into the space where the charts will go
if (d3) {
this.$(".chart").html(this.loadingTemplate);
this.$(".show-loading").html(this.loadingTemplate);
}
//If SVG isn't supported, insert an info warning
else {
this.$el.prepend(
this.alertTemplate({
classes: "alert-info",
msg: "Please upgrade your browser or use a different browser to view graphs of these statistics.",
email: false,
}),
);
}
this.$el.data("view", this);
if (this.userType == "portal" || this.userType === "repository") {
if (
!this.hideCitationsChart ||
!this.hideDownloadsChart ||
!this.hideViewsChart
) {
if (this.metricsModel.get("totalViews") !== null) {
this.renderMetrics();
} else {
// render metrics on fetch success.
this.listenTo(view.metricsModel, "sync", this.renderMetrics);
// in case when there is an error for the fetch call.
this.listenTo(
view.metricsModel,
"error",
this.renderUsageMetricsError,
);
var view = this;
setTimeout(function () {
if (
view
.$(".views-metrics, .downloads-metrics, #user-citations")
.find(".metric-chart-loading").length
) {
view.renderUsageMetricsError();
view.stopListening(
view.metricsModel,
"error",
view.renderUsageMetricsError,
);
}
}, 6000);
}
}
}
//Start retrieving data from Solr
this.model.getAll();
// Only gather replication stats if the view is a repository view
if (this.userType === "repository") {
if (this.userLabel !== undefined) {
var identifier = MetacatUI.appSearchModel.escapeSpecialChar(
encodeURIComponent(this.userId),
);
this.model.getTotalReplicas(identifier);
} else if (this.nodeSummaryView) {
var nodeId = MetacatUI.appModel.get("nodeId");
var identifier = MetacatUI.appSearchModel.escapeSpecialChar(
encodeURIComponent(nodeId),
);
this.model.getTotalReplicas(identifier);
}
}
return this;
},
/**
* drawMetadataAssessment - Insert the metadata assessment image into the view
*/
drawMetadataAssessment: function () {
try {
var scoresImage = this.model.get("mdqScoresImage");
if (scoresImage) {
// Replace the preloader figure with the assessment chart
this.$("#metadata-assessment-graphic").html(scoresImage);
}
// If there was no image received from the MDQ scores service,
// then show a warning message
else {
this.renderMetadataAssessmentError();
}
} catch (e) {
// If there's an error inserting the image, log an error message
console.log(
"Error displaying the metadata assessment figure. Error message: " +
e,
);
this.renderMetadataAssessmentError();
}
},
renderMetrics: function () {
if (!this.hideCitationsChart) this.renderCitationMetric();
if (!this.hideDownloadsChart) this.renderDownloadMetric();
if (!this.hideViewsChart) this.renderViewMetric();
},
renderCitationMetric: function () {
var citationSectionEl = this.$("#user-citations");
var citationEl = this.$(".citations-metrics-list");
var citationCountEl = this.$(".citation-count");
var metricName = "Citations";
var metricCount = this.metricsModel.get("totalCitations");
citationCountEl.text(
MetacatUI.appView.numberAbbreviator(metricCount, 1),
);
// Displaying Citations
var resultDetails = this.metricsModel.get("resultDetails");
// Creating a new collection object
// Parsing result-details with citation dictionary format
var resultDetailsCitationCollection = new Array();
for (var key in resultDetails["citations"]) {
resultDetailsCitationCollection.push(resultDetails["citations"][key]);
}
var citationCollection = new Citations(
resultDetailsCitationCollection,
{ parse: true },
);
this.citationCollection = citationCollection;
// Checking if there are any citations available for the List display.
if (this.metricsModel.get("totalCitations") == 0) {
var citationList = new CitationList();
// reattaching the citations at the bottom when the counts are 0.
var detachCitationEl = this.$(citationSectionEl).detach();
this.$(".charts-container").append(detachCitationEl);
} else {
var citationList = new CitationList({
citations: this.citationCollection,
});
}
this.citationList = citationList;
citationEl.html(this.citationList.render().$el.html());
},
renderDownloadMetric: function () {
var downloadEl = this.$(".downloads-metrics > .metric-chart");
var metricName = "Downloads";
var metricCount = this.metricsModel.get("totalDownloads");
var downloadCountEl = this.$(".download-count");
downloadCountEl.text(
MetacatUI.appView.numberAbbreviator(metricCount, 1),
);
var metricChartView = this.createMetricsChart(metricName);
downloadEl.html(metricChartView.el);
metricChartView.render();
},
renderViewMetric: function () {
var viewEl = this.$(".views-metrics > .metric-chart");
var metricName = "Views";
var metricCount = this.metricsModel.get("totalViews");
var viewCountEl = this.$(".view-count");
viewCountEl.text(MetacatUI.appView.numberAbbreviator(metricCount, 1));
var metricChartView = this.createMetricsChart(metricName);
viewEl.html(metricChartView.el);
metricChartView.render();
},
// Currently only being used for portals and profile views
createMetricsChart: function (metricName) {
var metricNameLemma = metricName.toLowerCase();
var metricMonths = this.metricsModel.get("months");
var metricCount = this.metricsModel.get(metricNameLemma);
var chartEl = document.getElementById(
"user-" + metricNameLemma + "-chart",
);
var viewType = this.userType;
// Draw a metric chart
var modalMetricChart = new MetricsChart({
id: metricNameLemma + "-chart",
metricCount: metricCount,
metricMonths: metricMonths,
type: viewType,
metricName: metricName,
});
this.subviews.push(modalMetricChart);
return modalMetricChart;
},
drawDataCountChart: function () {
var dataCount = this.model.get("dataCount");
var data = this.model.get("dataFormatIDs");
if (dataCount) {
var svgClass = "data";
} else if (
!this.model.get("dataCount") &&
this.model.get("metadataCount")
) {
//Are we drawing a blank chart (i.e. 0 data objects found)?
var svgClass = "data default";
} else if (
!this.model.get("metadataCount") &&
!this.model.get("dataCount")
)
var svgClass = "data no-activity";
//If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
if (!d3) {
this.$(".format-charts-data").html(
"<h2 class='" +
svgClass +
" fallback'>" +
MetacatUI.appView.commaSeparateNumber(dataCount) +
" data files</h2>",
);
return;
}
//Draw a donut chart
var donut = new DonutChart({
id: "data-chart",
data: data,
total: this.model.get("dataCount"),
titleText: "data files",
titleCount: dataCount,
svgClass: svgClass,
countClass: "data",
height: 300,
width: 380,
formatLabel: function (name) {
//If this is the application/vnd.ms-excel formatID - let's just display "MS Excel"
if (name !== undefined && name.indexOf("ms-excel") > -1)
name = "MS Excel";
else if (
name != undefined &&
name ==
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
name = "MS Excel OpenXML";
else if (
name != undefined &&
name ==
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
name = "MS Word OpenXML";
//Application/octet-stream - shorten it
else if (name !== undefined && name == "application/octet-stream")
name = "Application file";
if (name === undefined) name = "";
return name;
},
});
this.$(".format-charts-data").html(donut.render().el);
},
drawMetadataCountChart: function () {
var metadataCount = this.model.get("metadataCount");
var data = this.model.get("metadataFormatIDs");
if (metadataCount) {
var svgClass = "metadata";
} else if (
!this.model.get("metadataCount") &&
this.model.get("dataCount")
) {
//Are we drawing a blank chart (i.e. 0 data objects found)?
var svgClass = "metadata default";
} else if (
!this.model.get("metadataCount") &&
!this.model.get("dataCount")
)
var svgClass = "metadata no-activity";
//If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
if (!d3) {
this.$(".format-charts-metadata").html(
"<h2 class='" +
svgClass +
" fallback'>" +
MetacatUI.appView.commaSeparateNumber(metadataCount) +
" metadata files</h2>",
);
return;
}
//Draw a donut chart
var donut = new DonutChart({
id: "metadata-chart",
data: data,
total: this.model.get("metadataCount"),
titleText: "metadata files",
titleCount: metadataCount,
svgClass: svgClass,
countClass: "metadata",
height: 300,
width: 380,
formatLabel: function (name) {
if (
name !== undefined &&
(name.indexOf("//ecoinformatics.org") > -1 ||
name.indexOf("//eml.ecoinformatics.org") > -1)
) {
//EML - extract the version only
if (
name.substring(0, 4) == "eml:" ||
name.substring(0, 6) == "https:"
)
name = name
.substr(name.lastIndexOf("/") + 1)
.toUpperCase()
.replace("-", " ");
//EML modules
if (
name.indexOf("-//ecoinformatics.org//eml-") > -1 ||
name.indexOf("-//eml.ecoinformatics.org//eml-") > -1
)
name =
"EML " +
name.substring(
name.indexOf("//eml-") + 6,
name.lastIndexOf("-"),
) +
" " +
name.substr(name.lastIndexOf("-") + 1, 5);
}
//Dryad - shorten it
else if (
name !== undefined &&
name == "http://datadryad.org/profile/v3.1"
)
name = "Dryad 3.1";
//FGDC - just display "FGDC {year}"
else if (name !== undefined && name.indexOf("FGDC") > -1)
name = "FGDC " + name.substring(name.length - 4);
//Onedcx v1.0
else if (
name !== undefined &&
name == "http://ns.dataone.org/metadata/schema/onedcx/v1.0"
)
name = "Onedcx v1.0";
//GMD-NOAA
else if (
name !== undefined &&
name == "http://www.isotc211.org/2005/gmd-noaa"
)
name = "GMD-NOAA";
//GMD-PANGAEA
else if (
name !== undefined &&
name == "http://www.isotc211.org/2005/gmd-pangaea"
)
name = "GMD-PANGAEA";
if (name === undefined) name = "";
return name;
},
});
this.$(".format-charts-metadata").html(donut.render().el);
},
//drawUploadChart will get the upload stats from the stats model and draw a time series cumulative chart
drawUploadChart: function () {
//Get the width of the chart by using the parent container width
var parentEl = this.$(".upload-chart");
var width = parentEl.width() || null;
//If there was no first upload, draw a blank chart and exit
if (
(!this.model.get("metadataUploads") ||
!this.model.get("metadataUploads").length) &&
(!this.model.get("dataUploads") ||
!this.model.get("dataUploads").length)
) {
var lineChartView = new LineChart({
id: "upload-chart",
yLabel: "files uploaded",
frequency: 0,
width: width,
});
this.$(".upload-chart").html(lineChartView.render().el);
return;
}
//Set the frequency of our points
var frequency = 12;
//Check which line we should draw first since the scale will be based off the first line
if (this.model.get("metadataUploads") > this.model.get("dataUploads")) {
//If there isn't a lot of point to graph, draw points more frequently on the line
if (this.model.get("metadataUploadDates").length < 40) frequency = 1;
//Create the line chart and draw the metadata line
var lineChartView = new LineChart({
data: this.model.get("metadataUploadDates"),
formatFromSolrFacets: true,
cumulative: true,
id: "upload-chart",
className: "metadata",
yLabel: "files uploaded",
labelValue: "Metadata: ",
width: width,
labelDate: "M-y",
});
this.$(".upload-chart").html(lineChartView.render().el);
//Only draw the data file line if there was at least one uploaded
if (this.model.get("dataUploads")) {
//Add a line to our chart for data uploads
lineChartView.className = "data";
lineChartView.labelValue = "Data: ";
lineChartView.addLine(this.model.get("dataUploadDates"));
}
} else {
var lineChartView = new LineChart({
data: this.model.get("dataUploadDates"),
formatFromSolrFacets: true,
cumulative: true,
id: "upload-chart",
className: "data",
yLabel: "files uploaded",
labelValue: "Data: ",
width: width,
labelDate: "M-y",
});
this.$(".upload-chart").html(lineChartView.render().el);
//If no metadata files were uploaded, we don't want to draw the data file line
if (this.model.get("metadataUploads")) {
//Add a line to our chart for metadata uploads
lineChartView.className = "metadata";
lineChartView.labelValue = "Metadata: ";
lineChartView.addLine(this.model.get("metadataUploadDates"));
}
}
},
//drawUploadTitle will draw a circle badge title for the uploads time series chart
drawUploadTitle: function () {
//If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
if (!d3) {
this.$("#uploads-title").html(
"<h2 class='packages fallback'>" +
MetacatUI.appView.commaSeparateNumber(
this.model.get("totalCount"),
) +
"</h2>",
);
return;
}
if (
!this.model.get("dataUploads") &&
!this.model.get("metadataUploads")
) {
//Draw the upload chart title
var uploadChartTitle = new CircleBadge({
id: "upload-chart-title",
className: "no-activity",
globalR: 60,
data: [{ count: 0, label: "uploads" }],
});
this.$("#uploads-title").prepend(uploadChartTitle.render().el);
return;
}
//Get information for our upload chart title
var titleChartData = [],
metadataUploads = this.model.get("metadataUploads"),
dataUploads = this.model.get("dataUploads"),
metadataClass = "metadata",
dataClass = "data";
if (metadataUploads == 0) metadataClass = "default";
if (dataUploads == 0) dataClass = "default";
var titleChartData = [
{
count: this.model.get("metadataUploads"),
label: "metadata",
className: metadataClass,
},
{
count: this.model.get("dataUploads"),
label: "data",
className: dataClass,
},
];
//Draw the upload chart title
var uploadChartTitle = new CircleBadge({
id: "upload-chart-title",
data: titleChartData,
className: "chart-title",
useGlobalR: true,
globalR: 60,
});
this.$("#uploads-title").prepend(uploadChartTitle.render().el);
},
/*
* displayTotalCount - renders a simple count of total metadata files/datasets
*/
displayTotalCount: function () {
var className = "quick-stats-count";
if (!this.model.get("metadataCount") && !this.model.get("dataCount"))
className += " no-activity";
var countEl = $(document.createElement("p"))
.addClass(className)
.text(
MetacatUI.appView.commaSeparateNumber(
this.model.get("metadataCount"),
),
);
var titleEl = $(document.createElement("p"))
.addClass("chart-title")
.text("datasets");
this.$("#total-datasets").html(countEl);
this.$("#total-datasets").append(titleEl);
},
/*
* displayTotalSize renders a count of the total file size of
* all current metadata and data files
*/
displayTotalSize: function () {
var className = "quick-stats-count";
var count = "";
var view = this;
if (!this.model.get("totalSize")) {
count = "0 bytes";
className += " no-activity";
} else {
count = Utilities.bytesToSize(view.model.get("totalSize"));
}
var countEl = $(document.createElement("p"))
.addClass(className)
.text(count);
var titleEl = $(document.createElement("p"))
.addClass("chart-title")
.text("of content");
this.$("#total-size").html(countEl);
this.$("#total-size").append(titleEl);
},
/**
* Draws both the metadata and data update date charts.
* Note that this function may be deprecated in the future.
* Views should directly call drawMetadataUpdatesChart() or drawDataUpdatesChart() directly,
* since metadata and data dates are fetched via separate AJAX calls.
*/
drawUpdatesChart: function () {
//Draw the metadata and data updates charts
this.drawMetadataUpdatesChart();
this.drawDataUpdatesChart();
},
/**
* Draws a line chart representing the latest metadata updates over time
*/
drawMetadataUpdatesChart: function () {
//Set some configurations for the LineChart
var chartClasses = "data",
data;
//If the number of metadata objects in this data collection is 0, then set the data for the LineChart to null.
// And add a "no-activity" class to the chart.
if (
!this.model.get("metadataUpdateDates") ||
!this.model.get("metadataUpdateDates").length
) {
data = null;
chartClasses += " no-activity";
} else {
//Use the metadata update dates for the LineChart
data = this.model.get("metadataUpdateDates");
}
//Create the line chart for metadata updates
var metadataLineChart = new LineChart({
data: data,
formatFromSolrFacets: true,
cumulative: false,
id: "updates-chart",
className: chartClasses,
yLabel: "metadata files updated",
width: this.$(".metadata-updates-chart").width(),
labelDate: "M-y",
});
//Render the LineChart and insert it into the container element
this.$(".metadata-updates-chart").html(metadataLineChart.render().el);
},
/**
* Draws a line chart representing the latest metadata updates over time
*/
drawDataUpdatesChart: function () {
//Set some configurations for the LineChart
var chartClasses = "data",
view = this,
data;
//Use the data update dates for the LineChart
if (this.model.get("dataCount")) {
data = this.model.get("dataUpdateDates");
} else {
//If the number of data objects in this data collection is 0, then set the data for the LineChart to null.
// And add a "no-activity" class to the chart.
data = null;
chartClasses += " no-activity";
}
//Create the line chart for data updates
var dataLineChart = new LineChart({
data: data,
formatFromSolrFacets: true,
cumulative: false,
id: "updates-chart",
className: chartClasses,
yLabel: "data files updated",
width: this.$(".data-updates-chart").width(),
labelDate: "M-y",
});
//Render the LineChart and insert it into the container element
this.$(".data-updates-chart").html(dataLineChart.render().el);
// redraw the charts to avoid overlap at different widths
$(window).on("resize", function () {
if (!view.hideUpdatesChart) view.drawUpdatesChart();
});
},
//Draw a bar chart for the temporal coverage
drawCoverageChart: function (e, data) {
//Get the width of the chart by using the parent container width
var parentEl = this.$(".temporal-coverage-chart");
if (this.userType == "repository") {
parentEl.addClass("repository-portal-view");
}
var width = parentEl.width() || null;
// If results were found but none have temporal coverage, draw a default chart
if (!this.model.get("temporalCoverage")) {
parentEl.html(
"<p class='subtle center'>There are no metadata documents that describe temporal coverage.</p>",
);
return;
}
var options = {
data: data,
formatFromSolrFacets: true,
id: "temporal-coverage-chart",
yLabel: "data packages",
yFormat: d3.format(",d"),
barClass: "packages",
roundedRect: true,
roundedRadius: 3,
barLabelClass: "packages",
width: width,
};
var barChart = new BarChart(options);
parentEl.html(barChart.render().el);
},
drawCoverageChartTitle: function () {
if (
!this.model.get("firstBeginDate") ||
!this.model.get("lastEndDate") ||
!this.model.get("temporalCoverage")
)
return;
//Create the range query
var yearRange =
this.model.get("firstBeginDate").getUTCFullYear() +
" - " +
this.model.get("lastEndDate").getUTCFullYear();
//Find the year range element
this.$("#data-coverage-year-range").text(yearRange);
},
/*
* Shows that this person/group/node has no activity
*/
showNoActivity: function () {
if (
this.model.get("metadataCount") === 0 &&
this.model.get("dataCount") === 0
) {
this.$(".show-loading .loading").remove();
this.$(".stripe").addClass("no-activity");
this.$(".metric-chart-loading svg animate").remove();
$.each($(".metric-chart-loading .message"), function (i, messageEl) {
$(messageEl).html("No metrics to show");
});
}
},
renderUsageMetricsError: function () {
var message =
"<p class='check-back-message'><strong>This might take some time. Check back in 24 hours to see these results.</strong></p>";
$.each(
$(".views-metrics, .downloads-metrics, #user-citations"),
function (i, metricEl) {
$(metricEl).find(".check-back-message").remove();
$(metricEl).find(".message").append(message);
},
);
},
/**
* renderMetadataAssessmentError - update the metadata assessment
* pre-loading figure to indicate to the user that the assessment is not
* available at the moment.
*/
renderMetadataAssessmentError: function () {
try {
$("#metadata-assessment-graphic .message").append(
"<br><strong>This might take some time. Check back in 24 hours to see these results.</strong>",
);
} catch (e) {
console.log(
"Error showing the metadata assessment error message in the metrics. " +
e,
);
}
},
/*
* getReplicas gets the number of replicas in this member node
*/
displayTotalReplicas: function () {
var view = this;
var className = "quick-stats-count";
var count;
if (this.model.get("totalReplicas") > 0) {
count = MetacatUI.appView.commaSeparateNumber(
view.model.get("totalReplicas"),
);
var countEl = $(document.createElement("p"))
.addClass(className)
.text(count);
var titleEl = $(document.createElement("p"))
.addClass("chart-title")
.text("replicas");
// display the totals
this.$("#total-replicas").html(countEl);
this.$("#total-replicas").append(titleEl);
} else {
// hide the replicas container if the replica count is 0.
this.$("#replicas-container").hide();
}
},
onClose: function () {
//Clear the template
this.$el.html("");
//Stop listening to changes in the model
this.stopListening(this.model);
//Stop listening to resize
$(window).off("resize");
//Reset the stats model
this.model = null;
},
},
);
return StatsView;
});


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var StatsView = Backbone.View.extend(
/** @lends StatsView.prototype */ {
el: "#Content",
model: null,
hideUpdatesChart: false,
/*
* Flag to indicate whether the statsview is a node summary view
* @type {boolean}
*/
nodeSummaryView: false,
/**
* Whether or not to show the graph that indicated the assessment score for all metadata in the query.
* @type {boolean}
*/
hideMetadataAssessment: false,
subviews: [],
template: _.template(profileTemplate),
metricTemplate: _.template(MetricModalTemplate),
alertTemplate: _.template(AlertTemplate),
loadingTemplate: _.template(LoadingTemplate),
metricsLoadingTemplate: _.template(MetricsLoadingTemplate),
initialize: function (options) {
if (!options) options = {};
this.title =
typeof options.title === "undefined"
? "Summary of Holdings"
: options.title;
this.description =
typeof options.description === "undefined"
? "A summary of all datasets in our catalog."
: options.description;
this.metricsModel =
typeof options.metricsModel === undefined
? undefined
: options.metricsModel;
this.userType =
typeof options.userType === undefined ? undefined : options.userType;
this.userId =
typeof options.userId === undefined ? undefined : options.userId;
this.userLabel =
typeof options.userLabel === undefined
? undefined
: options.userLabel;
if (typeof options.el === "undefined") this.el = options.el;
this.hideUpdatesChart =
options.hideUpdatesChart === true ? true : false;
this.hideMetadataAssessment =
typeof options.hideMetadataAssessment === "undefined"
? true
: options.hideMetadataAssessment;
this.hideCitationsChart =
typeof options.hideCitationsChart === "undefined"
? true
: options.hideCitationsChart;
this.hideDownloadsChart =
typeof options.hideDownloadsChart === "undefined"
? true
: options.hideDownloadsChart;
this.hideViewsChart =
typeof options.hideViewsChart === "undefined"
? true
: options.hideViewsChart;
this.model = options.model || null;
},
render: function (options) {
//The Node info needs to be fetched first since a lot of this code requires info about MNs
if (
!MetacatUI.nodeModel.get("checked") &&
!MetacatUI.nodeModel.get("error")
) {
this.listenToOnce(
MetacatUI.nodeModel,
"change:checked error",
function () {
//Remove listeners and render the view, even if there was an error fetching the NodeModel
this.stopListening(MetacatUI.nodeModel);
this.render(options);
},
);
this.$el.html(this.loadingTemplate);
return;
}
if (!options) options = {};
var view = this,
userIsCN = false,
nodeId,
isHostedRepo = false;
// Check if the node is a coordinating node
this.userIsCN = userIsCN;
if (this.userType !== undefined && this.userLabel !== undefined) {
if (this.userType === "repository") {
userIsCN = MetacatUI.nodeModel.isCN(this.userId);
if (userIsCN && typeof isCN !== "undefined") this.userIsCN = true;
}
}
if (options.nodeSummaryView) {
this.nodeSummaryView = true;
nodeId = MetacatUI.appModel.get("nodeId");
userIsCN = MetacatUI.nodeModel.isCN(nodeId);
//Save whether this profile is for a CN
if (userIsCN && typeof userIsCN !== "undefined") {
this.userIsCN = true;
}
//Figure out if this profile is for a hosted repo
else if (nodeId) {
isHostedRepo = _.contains(
MetacatUI.appModel.get("dataoneHostedRepos"),
nodeId,
);
}
// Disable the metrics if the nodeId is not available or if it is not a DataONE Hosted Repo
if (
!this.userIsCN &&
(nodeId === "undefined" || nodeId === null || !isHostedRepo)
) {
this.hideCitationsChart = true;
this.hideDownloadsChart = true;
this.hideViewsChart = true;
this.hideMetadataAssessment = true;
} else {
// Overwrite the metrics display flags as set in the AppModel
this.hideMetadataAssessment = MetacatUI.appModel.get(
"hideSummaryMetadataAssessment",
);
this.hideCitationsChart = MetacatUI.appModel.get(
"hideSummaryCitationsChart",
);
this.hideDownloadsChart = MetacatUI.appModel.get(
"hideSummaryDownloadsChart",
);
this.hideViewsChart = MetacatUI.appModel.get(
"hideSummaryViewsChart",
);
}
}
if (
!this.hideCitationsChart ||
!this.hideDownloadsChart ||
!this.hideViewsChart
) {
if (typeof this.metricsModel === "undefined") {
// Create a list with the repository ID
var pid_list = new Array();
pid_list.push(nodeId);
// Create a new object of the metrics model
var metricsModel = new MetricsModel({
pid_list: pid_list,
type: this.userType,
});
metricsModel.fetch();
this.metricsModel = metricsModel;
}
}
if (!this.model) {
this.model = new StatsModel({
hideMetadataAssessment: this.hideMetadataAssessment,
mdqImageId: nodeId,
});
}
//Clear the page
this.$el.html("");
//Only trigger the functions that draw SVG charts if d3 loaded correctly
if (d3) {
//Draw a chart that shows the temporal coverage of all datasets in this collection
this.listenTo(
this.model,
"change:temporalCoverage",
this.drawCoverageChart,
);
//Draw charts that plot the latest updates of metadata and data files
this.listenTo(
this.model,
"change:dataUpdateDates",
this.drawDataUpdatesChart,
);
this.listenTo(
this.model,
"change:metadataUpdateDates",
this.drawMetadataUpdatesChart,
);
//Render the total file size of all contents in this collection
this.listenTo(this.model, "change:totalSize", this.displayTotalSize);
//Render the total number of datasets in this collection
this.listenTo(
this.model,
"change:metadataCount",
this.displayTotalCount,
);
// Display replicas only for member nodes
if (this.userType === "repository" && !this.userIsCN)
this.listenTo(
this.model,
"change:totalReplicas",
this.displayTotalReplicas,
);
//Draw charts that show the breakdown of format IDs for metadata and data files
this.listenTo(
this.model,
"change:dataFormatIDs",
this.drawDataCountChart,
);
this.listenTo(
this.model,
"change:metadataFormatIDs",
this.drawMetadataCountChart,
);
}
//When the last coverage endDate is found, draw a title for the temporal coverage chart
this.listenTo(
this.model,
"change:lastEndDate",
this.drawCoverageChartTitle,
);
//When the total count is updated, check if there if the count is 0, so we can show there is no "activity" for this collection
this.listenTo(this.model, "change:totalCount", this.showNoActivity);
// set the header type
MetacatUI.appModel.set("headerType", "default");
// Loading template for the FAIR chart
var fairLoadingHtml = this.metricsLoadingTemplate({
message: "Running an assessment report...",
character: "none",
type: "FAIR",
});
// Loading template for the citations section
var citationsLoadingHtml = this.metricsLoadingTemplate({
message:
"Scouring our records for publications that cited these datasets...",
character: "none",
type: "citations",
});
// Loading template for the downloads bar chart
var downloadsLoadingHtml = this.metricsLoadingTemplate({
message: "Crunching some numbers...",
character: "developer",
type: "barchart",
});
// Loading template for the views bar chart
var viewsLoadingHtml = this.metricsLoadingTemplate({
message: "Just doing a few more calculations...",
character: "statistician",
type: "barchart",
});
//Insert the template
this.$el.html(
this.template({
query: this.model.get("query"),
title: this.title,
description: this.description,
userType: this.userType,
userIsCN: this.userIsCN,
fairLoadingHtml: fairLoadingHtml,
citationsLoadingHtml: citationsLoadingHtml,
downloadsLoadingHtml: downloadsLoadingHtml,
viewsLoadingHtml: viewsLoadingHtml,
hideUpdatesChart: this.hideUpdatesChart,
hideCitationsChart: this.hideCitationsChart,
hideDownloadsChart: this.hideDownloadsChart,
hideViewsChart: this.hideViewsChart,
hideMetadataAssessment: this.hideMetadataAssessment,
}),
);
// Insert the metadata assessment chart
var view = this;
if (this.hideMetadataAssessment !== true) {
this.listenTo(
this.model,
"change:mdqScoresImage",
this.drawMetadataAssessment,
);
this.listenTo(this.model, "change:mdqScoresError", function () {
view.renderMetadataAssessmentError();
});
}
//Insert the loading template into the space where the charts will go
if (d3) {
this.$(".chart").html(this.loadingTemplate);
this.$(".show-loading").html(this.loadingTemplate);
}
//If SVG isn't supported, insert an info warning
else {
this.$el.prepend(
this.alertTemplate({
classes: "alert-info",
msg: "Please upgrade your browser or use a different browser to view graphs of these statistics.",
email: false,
}),
);
}
this.$el.data("view", this);
if (this.userType == "portal" || this.userType === "repository") {
if (
!this.hideCitationsChart ||
!this.hideDownloadsChart ||
!this.hideViewsChart
) {
if (this.metricsModel.get("totalViews") !== null) {
this.renderMetrics();
} else {
// render metrics on fetch success.
this.listenTo(view.metricsModel, "sync", this.renderMetrics);
// in case when there is an error for the fetch call.
this.listenTo(
view.metricsModel,
"error",
this.renderUsageMetricsError,
);
var view = this;
setTimeout(function () {
if (
view
.$(".views-metrics, .downloads-metrics, #user-citations")
.find(".metric-chart-loading").length
) {
view.renderUsageMetricsError();
view.stopListening(
view.metricsModel,
"error",
view.renderUsageMetricsError,
);
}
}, 6000);
}
}
}
//Start retrieving data from Solr
this.model.getAll();
// Only gather replication stats if the view is a repository view
if (this.userType === "repository") {
if (this.userLabel !== undefined) {
var identifier = MetacatUI.appSearchModel.escapeSpecialChar(
encodeURIComponent(this.userId),
);
this.model.getTotalReplicas(identifier);
} else if (this.nodeSummaryView) {
var nodeId = MetacatUI.appModel.get("nodeId");
var identifier = MetacatUI.appSearchModel.escapeSpecialChar(
encodeURIComponent(nodeId),
);
this.model.getTotalReplicas(identifier);
}
}
return this;
},
/**
* drawMetadataAssessment - Insert the metadata assessment image into the view
*/
drawMetadataAssessment: function () {
try {
var scoresImage = this.model.get("mdqScoresImage");
if (scoresImage) {
// Replace the preloader figure with the assessment chart
this.$("#metadata-assessment-graphic").html(scoresImage);
}
// If there was no image received from the MDQ scores service,
// then show a warning message
else {
this.renderMetadataAssessmentError();
}
} catch (e) {
// If there's an error inserting the image, log an error message
console.log(
"Error displaying the metadata assessment figure. Error message: " +
e,
);
this.renderMetadataAssessmentError();
}
},
renderMetrics: function () {
if (!this.hideCitationsChart) this.renderCitationMetric();
if (!this.hideDownloadsChart) this.renderDownloadMetric();
if (!this.hideViewsChart) this.renderViewMetric();
},
renderCitationMetric: function () {
var citationSectionEl = this.$("#user-citations");
var citationEl = this.$(".citations-metrics-list");
var citationCountEl = this.$(".citation-count");
var metricName = "Citations";
var metricCount = this.metricsModel.get("totalCitations");
citationCountEl.text(
MetacatUI.appView.numberAbbreviator(metricCount, 1),
);
// Displaying Citations
var resultDetails = this.metricsModel.get("resultDetails");
// Creating a new collection object
// Parsing result-details with citation dictionary format
var resultDetailsCitationCollection = new Array();
for (var key in resultDetails["citations"]) {
resultDetailsCitationCollection.push(resultDetails["citations"][key]);
}
var citationCollection = new Citations(
resultDetailsCitationCollection,
{ parse: true },
);
this.citationCollection = citationCollection;
// Checking if there are any citations available for the List display.
if (this.metricsModel.get("totalCitations") == 0) {
var citationList = new CitationList();
// reattaching the citations at the bottom when the counts are 0.
var detachCitationEl = this.$(citationSectionEl).detach();
this.$(".charts-container").append(detachCitationEl);
} else {
var citationList = new CitationList({
citations: this.citationCollection,
});
}
this.citationList = citationList;
citationEl.html(this.citationList.render().$el.html());
},
renderDownloadMetric: function () {
var downloadEl = this.$(".downloads-metrics > .metric-chart");
var metricName = "Downloads";
var metricCount = this.metricsModel.get("totalDownloads");
var downloadCountEl = this.$(".download-count");
downloadCountEl.text(
MetacatUI.appView.numberAbbreviator(metricCount, 1),
);
var metricChartView = this.createMetricsChart(metricName);
downloadEl.html(metricChartView.el);
metricChartView.render();
},
renderViewMetric: function () {
var viewEl = this.$(".views-metrics > .metric-chart");
var metricName = "Views";
var metricCount = this.metricsModel.get("totalViews");
var viewCountEl = this.$(".view-count");
viewCountEl.text(MetacatUI.appView.numberAbbreviator(metricCount, 1));
var metricChartView = this.createMetricsChart(metricName);
viewEl.html(metricChartView.el);
metricChartView.render();
},
// Currently only being used for portals and profile views
createMetricsChart: function (metricName) {
var metricNameLemma = metricName.toLowerCase();
var metricMonths = this.metricsModel.get("months");
var metricCount = this.metricsModel.get(metricNameLemma);
var chartEl = document.getElementById(
"user-" + metricNameLemma + "-chart",
);
var viewType = this.userType;
// Draw a metric chart
var modalMetricChart = new MetricsChart({
id: metricNameLemma + "-chart",
metricCount: metricCount,
metricMonths: metricMonths,
type: viewType,
metricName: metricName,
});
this.subviews.push(modalMetricChart);
return modalMetricChart;
},
drawDataCountChart: function () {
var dataCount = this.model.get("dataCount");
var data = this.model.get("dataFormatIDs");
if (dataCount) {
var svgClass = "data";
} else if (
!this.model.get("dataCount") &&
this.model.get("metadataCount")
) {
//Are we drawing a blank chart (i.e. 0 data objects found)?
var svgClass = "data default";
} else if (
!this.model.get("metadataCount") &&
!this.model.get("dataCount")
)
var svgClass = "data no-activity";
//If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
if (!d3) {
this.$(".format-charts-data").html(
"<h2 class='" +
svgClass +
" fallback'>" +
MetacatUI.appView.commaSeparateNumber(dataCount) +
" data files</h2>",
);
return;
}
//Draw a donut chart
var donut = new DonutChart({
id: "data-chart",
data: data,
total: this.model.get("dataCount"),
titleText: "data files",
titleCount: dataCount,
svgClass: svgClass,
countClass: "data",
height: 300,
width: 380,
formatLabel: function (name) {
//If this is the application/vnd.ms-excel formatID - let's just display "MS Excel"
if (name !== undefined && name.indexOf("ms-excel") > -1)
name = "MS Excel";
else if (
name != undefined &&
name ==
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
name = "MS Excel OpenXML";
else if (
name != undefined &&
name ==
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
name = "MS Word OpenXML";
//Application/octet-stream - shorten it
else if (name !== undefined && name == "application/octet-stream")
name = "Application file";
if (name === undefined) name = "";
return name;
},
});
this.$(".format-charts-data").html(donut.render().el);
},
drawMetadataCountChart: function () {
var metadataCount = this.model.get("metadataCount");
var data = this.model.get("metadataFormatIDs");
if (metadataCount) {
var svgClass = "metadata";
} else if (
!this.model.get("metadataCount") &&
this.model.get("dataCount")
) {
//Are we drawing a blank chart (i.e. 0 data objects found)?
var svgClass = "metadata default";
} else if (
!this.model.get("metadataCount") &&
!this.model.get("dataCount")
)
var svgClass = "metadata no-activity";
//If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
if (!d3) {
this.$(".format-charts-metadata").html(
"<h2 class='" +
svgClass +
" fallback'>" +
MetacatUI.appView.commaSeparateNumber(metadataCount) +
" metadata files</h2>",
);
return;
}
//Draw a donut chart
var donut = new DonutChart({
id: "metadata-chart",
data: data,
total: this.model.get("metadataCount"),
titleText: "metadata files",
titleCount: metadataCount,
svgClass: svgClass,
countClass: "metadata",
height: 300,
width: 380,
formatLabel: function (name) {
if (
name !== undefined &&
(name.indexOf("//ecoinformatics.org") > -1 ||
name.indexOf("//eml.ecoinformatics.org") > -1)
) {
//EML - extract the version only
if (
name.substring(0, 4) == "eml:" ||
name.substring(0, 6) == "https:"
)
name = name
.substr(name.lastIndexOf("/") + 1)
.toUpperCase()
.replace("-", " ");
//EML modules
if (
name.indexOf("-//ecoinformatics.org//eml-") > -1 ||
name.indexOf("-//eml.ecoinformatics.org//eml-") > -1
)
name =
"EML " +
name.substring(
name.indexOf("//eml-") + 6,
name.lastIndexOf("-"),
) +
" " +
name.substr(name.lastIndexOf("-") + 1, 5);
}
//Dryad - shorten it
else if (
name !== undefined &&
name == "http://datadryad.org/profile/v3.1"
)
name = "Dryad 3.1";
//FGDC - just display "FGDC {year}"
else if (name !== undefined && name.indexOf("FGDC") > -1)
name = "FGDC " + name.substring(name.length - 4);
//Onedcx v1.0
else if (
name !== undefined &&
name == "http://ns.dataone.org/metadata/schema/onedcx/v1.0"
)
name = "Onedcx v1.0";
//GMD-NOAA
else if (
name !== undefined &&
name == "http://www.isotc211.org/2005/gmd-noaa"
)
name = "GMD-NOAA";
//GMD-PANGAEA
else if (
name !== undefined &&
name == "http://www.isotc211.org/2005/gmd-pangaea"
)
name = "GMD-PANGAEA";
if (name === undefined) name = "";
return name;
},
});
this.$(".format-charts-metadata").html(donut.render().el);
},
//drawUploadChart will get the upload stats from the stats model and draw a time series cumulative chart
drawUploadChart: function () {
//Get the width of the chart by using the parent container width
var parentEl = this.$(".upload-chart");
var width = parentEl.width() || null;
//If there was no first upload, draw a blank chart and exit
if (
(!this.model.get("metadataUploads") ||
!this.model.get("metadataUploads").length) &&
(!this.model.get("dataUploads") ||
!this.model.get("dataUploads").length)
) {
var lineChartView = new LineChart({
id: "upload-chart",
yLabel: "files uploaded",
frequency: 0,
width: width,
});
this.$(".upload-chart").html(lineChartView.render().el);
return;
}
//Set the frequency of our points
var frequency = 12;
//Check which line we should draw first since the scale will be based off the first line
if (this.model.get("metadataUploads") > this.model.get("dataUploads")) {
//If there isn't a lot of point to graph, draw points more frequently on the line
if (this.model.get("metadataUploadDates").length < 40) frequency = 1;
//Create the line chart and draw the metadata line
var lineChartView = new LineChart({
data: this.model.get("metadataUploadDates"),
formatFromSolrFacets: true,
cumulative: true,
id: "upload-chart",
className: "metadata",
yLabel: "files uploaded",
labelValue: "Metadata: ",
width: width,
labelDate: "M-y",
});
this.$(".upload-chart").html(lineChartView.render().el);
//Only draw the data file line if there was at least one uploaded
if (this.model.get("dataUploads")) {
//Add a line to our chart for data uploads
lineChartView.className = "data";
lineChartView.labelValue = "Data: ";
lineChartView.addLine(this.model.get("dataUploadDates"));
}
} else {
var lineChartView = new LineChart({
data: this.model.get("dataUploadDates"),
formatFromSolrFacets: true,
cumulative: true,
id: "upload-chart",
className: "data",
yLabel: "files uploaded",
labelValue: "Data: ",
width: width,
labelDate: "M-y",
});
this.$(".upload-chart").html(lineChartView.render().el);
//If no metadata files were uploaded, we don't want to draw the data file line
if (this.model.get("metadataUploads")) {
//Add a line to our chart for metadata uploads
lineChartView.className = "metadata";
lineChartView.labelValue = "Metadata: ";
lineChartView.addLine(this.model.get("metadataUploadDates"));
}
}
},
//drawUploadTitle will draw a circle badge title for the uploads time series chart
drawUploadTitle: function () {
//If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
if (!d3) {
this.$("#uploads-title").html(
"<h2 class='packages fallback'>" +
MetacatUI.appView.commaSeparateNumber(
this.model.get("totalCount"),
) +
"</h2>",
);
return;
}
if (
!this.model.get("dataUploads") &&
!this.model.get("metadataUploads")
) {
//Draw the upload chart title
var uploadChartTitle = new CircleBadge({
id: "upload-chart-title",
className: "no-activity",
globalR: 60,
data: [{ count: 0, label: "uploads" }],
});
this.$("#uploads-title").prepend(uploadChartTitle.render().el);
return;
}
//Get information for our upload chart title
var titleChartData = [],
metadataUploads = this.model.get("metadataUploads"),
dataUploads = this.model.get("dataUploads"),
metadataClass = "metadata",
dataClass = "data";
if (metadataUploads == 0) metadataClass = "default";
if (dataUploads == 0) dataClass = "default";
var titleChartData = [
{
count: this.model.get("metadataUploads"),
label: "metadata",
className: metadataClass,
},
{
count: this.model.get("dataUploads"),
label: "data",
className: dataClass,
},
];
//Draw the upload chart title
var uploadChartTitle = new CircleBadge({
id: "upload-chart-title",
data: titleChartData,
className: "chart-title",
useGlobalR: true,
globalR: 60,
});
this.$("#uploads-title").prepend(uploadChartTitle.render().el);
},
/*
* displayTotalCount - renders a simple count of total metadata files/datasets
*/
displayTotalCount: function () {
var className = "quick-stats-count";
if (!this.model.get("metadataCount") && !this.model.get("dataCount"))
className += " no-activity";
var countEl = $(document.createElement("p"))
.addClass(className)
.text(
MetacatUI.appView.commaSeparateNumber(
this.model.get("metadataCount"),
),
);
var titleEl = $(document.createElement("p"))
.addClass("chart-title")
.text("datasets");
this.$("#total-datasets").html(countEl);
this.$("#total-datasets").append(titleEl);
},
/*
* displayTotalSize renders a count of the total file size of
* all current metadata and data files
*/
displayTotalSize: function () {
var className = "quick-stats-count";
var count = "";
var view = this;
if (!this.model.get("totalSize")) {
count = "0 bytes";
className += " no-activity";
} else {
count = Utilities.bytesToSize(view.model.get("totalSize"));
}
var countEl = $(document.createElement("p"))
.addClass(className)
.text(count);
var titleEl = $(document.createElement("p"))
.addClass("chart-title")
.text("of content");
this.$("#total-size").html(countEl);
this.$("#total-size").append(titleEl);
},
/**
* Draws both the metadata and data update date charts.
* Note that this function may be deprecated in the future.
* Views should directly call drawMetadataUpdatesChart() or drawDataUpdatesChart() directly,
* since metadata and data dates are fetched via separate AJAX calls.
*/
drawUpdatesChart: function () {
//Draw the metadata and data updates charts
this.drawMetadataUpdatesChart();
this.drawDataUpdatesChart();
},
/**
* Draws a line chart representing the latest metadata updates over time
*/
drawMetadataUpdatesChart: function () {
//Set some configurations for the LineChart
var chartClasses = "data",
data;
//If the number of metadata objects in this data collection is 0, then set the data for the LineChart to null.
// And add a "no-activity" class to the chart.
if (
!this.model.get("metadataUpdateDates") ||
!this.model.get("metadataUpdateDates").length
) {
data = null;
chartClasses += " no-activity";
} else {
//Use the metadata update dates for the LineChart
data = this.model.get("metadataUpdateDates");
}
//Create the line chart for metadata updates
var metadataLineChart = new LineChart({
data: data,
formatFromSolrFacets: true,
cumulative: false,
id: "updates-chart",
className: chartClasses,
yLabel: "metadata files updated",
width: this.$(".metadata-updates-chart").width(),
labelDate: "M-y",
});
//Render the LineChart and insert it into the container element
this.$(".metadata-updates-chart").html(metadataLineChart.render().el);
},
/**
* Draws a line chart representing the latest metadata updates over time
*/
drawDataUpdatesChart: function () {
//Set some configurations for the LineChart
var chartClasses = "data",
view = this,
data;
//Use the data update dates for the LineChart
if (this.model.get("dataCount")) {
data = this.model.get("dataUpdateDates");
} else {
//If the number of data objects in this data collection is 0, then set the data for the LineChart to null.
// And add a "no-activity" class to the chart.
data = null;
chartClasses += " no-activity";
}
//Create the line chart for data updates
var dataLineChart = new LineChart({
data: data,
formatFromSolrFacets: true,
cumulative: false,
id: "updates-chart",
className: chartClasses,
yLabel: "data files updated",
width: this.$(".data-updates-chart").width(),
labelDate: "M-y",
});
//Render the LineChart and insert it into the container element
this.$(".data-updates-chart").html(dataLineChart.render().el);
// redraw the charts to avoid overlap at different widths
$(window).on("resize", function () {
if (!view.hideUpdatesChart) view.drawUpdatesChart();
});
},
//Draw a bar chart for the temporal coverage
drawCoverageChart: function (e, data) {
//Get the width of the chart by using the parent container width
var parentEl = this.$(".temporal-coverage-chart");
if (this.userType == "repository") {
parentEl.addClass("repository-portal-view");
}
var width = parentEl.width() || null;
// If results were found but none have temporal coverage, draw a default chart
if (!this.model.get("temporalCoverage")) {
parentEl.html(
"<p class='subtle center'>There are no metadata documents that describe temporal coverage.</p>",
);
return;
}
var options = {
data: data,
formatFromSolrFacets: true,
id: "temporal-coverage-chart",
yLabel: "data packages",
yFormat: d3.format(",d"),
barClass: "packages",
roundedRect: true,
roundedRadius: 3,
barLabelClass: "packages",
width: width,
};
var barChart = new BarChart(options);
parentEl.html(barChart.render().el);
},
drawCoverageChartTitle: function () {
if (
!this.model.get("firstBeginDate") ||
!this.model.get("lastEndDate") ||
!this.model.get("temporalCoverage")
)
return;
//Create the range query
var yearRange =
this.model.get("firstBeginDate").getUTCFullYear() +
" - " +
this.model.get("lastEndDate").getUTCFullYear();
//Find the year range element
this.$("#data-coverage-year-range").text(yearRange);
},
/*
* Shows that this person/group/node has no activity
*/
showNoActivity: function () {
if (
this.model.get("metadataCount") === 0 &&
this.model.get("dataCount") === 0
) {
this.$(".show-loading .loading").remove();
this.$(".stripe").addClass("no-activity");
this.$(".metric-chart-loading svg animate").remove();
$.each($(".metric-chart-loading .message"), function (i, messageEl) {
$(messageEl).html("No metrics to show");
});
}
},
renderUsageMetricsError: function () {
var message =
"<p class='check-back-message'><strong>This might take some time. Check back in 24 hours to see these results.</strong></p>";
$.each(
$(".views-metrics, .downloads-metrics, #user-citations"),
function (i, metricEl) {
$(metricEl).find(".check-back-message").remove();
$(metricEl).find(".message").append(message);
},
);
},
/**
* renderMetadataAssessmentError - update the metadata assessment
* pre-loading figure to indicate to the user that the assessment is not
* available at the moment.
*/
renderMetadataAssessmentError: function () {
try {
$("#metadata-assessment-graphic .message").append(
"<br><strong>This might take some time. Check back in 24 hours to see these results.</strong>",
);
} catch (e) {
console.log(
"Error showing the metadata assessment error message in the metrics. " +
e,
);
}
},
/*
* getReplicas gets the number of replicas in this member node
*/
displayTotalReplicas: function () {
var view = this;
var className = "quick-stats-count";
var count;
if (this.model.get("totalReplicas") > 0) {
count = MetacatUI.appView.commaSeparateNumber(
view.model.get("totalReplicas"),
);
var countEl = $(document.createElement("p"))
.addClass(className)
.text(count);
var titleEl = $(document.createElement("p"))
.addClass("chart-title")
.text("replicas");
// display the totals
this.$("#total-replicas").html(countEl);
this.$("#total-replicas").append(titleEl);
} else {
// hide the replicas container if the replica count is 0.
this.$("#replicas-container").hide();
}
},
onClose: function () {
//Clear the template
this.$el.html("");
//Stop listening to changes in the model
this.stopListening(this.model);
//Stop listening to resize
$(window).off("resize");
//Reset the stats model
this.model = null;
},
},
);


⚠️ [eslint] <func-names> reported by reviewdog 🐶
Unexpected unnamed method 'displayTotalSize'.

displayTotalSize: function () {


🚫 [eslint] <object-shorthand> reported by reviewdog 🐶
Expected method shorthand.

displayTotalSize: function () {
var className = "quick-stats-count";
var count = "";
var view = this;
if (!this.model.get("totalSize")) {
count = "0 bytes";
className += " no-activity";
} else {
count = Utilities.bytesToSize(view.model.get("totalSize"));
}
var countEl = $(document.createElement("p"))
.addClass(className)
.text(count);
var titleEl = $(document.createElement("p"))
.addClass("chart-title")
.text("of content");
this.$("#total-size").html(countEl);
this.$("#total-size").append(titleEl);
},


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var className = "quick-stats-count";


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var count = "";


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var view = this;


🚫 [eslint] <vars-on-top> reported by reviewdog 🐶
All 'var' declarations must be at the top of the function scope.

var countEl = $(document.createElement("p"))
.addClass(className)
.text(count);


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var countEl = $(document.createElement("p"))
.addClass(className)
.text(count);


🚫 [eslint] <object-shorthand> reported by reviewdog 🐶
Expected method shorthand.

showNoActivity: function () {
if (
this.model.get("metadataCount") === 0 &&
this.model.get("dataCount") === 0
) {
this.$(".show-loading .loading").remove();
this.$(".stripe").addClass("no-activity");
this.$(".metric-chart-loading svg animate").remove();
$.each($(".metric-chart-loading .message"), function (i, messageEl) {
$(messageEl).html("No metrics to show");
});
}
},


⚠️ [eslint] <func-names> reported by reviewdog 🐶
Unexpected unnamed method 'renderUsageMetricsError'.

renderUsageMetricsError: function () {


🚫 [eslint] <object-shorthand> reported by reviewdog 🐶
Expected method shorthand.

renderUsageMetricsError: function () {
var message =
"<p class='check-back-message'><strong>This might take some time. Check back in 24 hours to see these results.</strong></p>";
$.each(
$(".views-metrics, .downloads-metrics, #user-citations"),
function (i, metricEl) {
$(metricEl).find(".check-back-message").remove();
$(metricEl).find(".message").append(message);
},
);
},


🚫 [eslint] <no-var> reported by reviewdog 🐶
Unexpected var, use let or const instead.

var message =
"<p class='check-back-message'><strong>This might take some time. Check back in 24 hours to see these results.</strong></p>";

Comment on lines 165 to 169
/**
* Convert number of bytes into human readable format
*
* @param integer bytes Number of bytes to convert
* @param integer precision Number of digits after the decimal separator
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <jsdoc/require-param> reported by reviewdog 🐶
Missing JSDoc @param "bytes" declaration.

Suggested change
/**
* Convert number of bytes into human readable format
*
* @param integer bytes Number of bytes to convert
* @param integer precision Number of digits after the decimal separator
/**
* Convert number of bytes into human readable format
*
* @param integer bytes Number of bytes to convert
* @param integer precision Number of digits after the decimal separator
* @param bytes
* @param precision

Comment on lines 165 to 169
/**
* Convert number of bytes into human readable format
*
* @param integer bytes Number of bytes to convert
* @param integer precision Number of digits after the decimal separator
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <jsdoc/require-param> reported by reviewdog 🐶
Missing JSDoc @param "precision" declaration.

Suggested change
/**
* Convert number of bytes into human readable format
*
* @param integer bytes Number of bytes to convert
* @param integer precision Number of digits after the decimal separator
/**
* Convert number of bytes into human readable format
*
* @param integer bytes Number of bytes to convert
* @param integer precision Number of digits after the decimal separator
* @param bytes
* @param precision
* @param bytes
* @param precision

Comment on lines 165 to 171
/**
* Convert number of bytes into human readable format
*
* @param integer bytes Number of bytes to convert
* @param integer precision Number of digits after the decimal separator
* @return string
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <jsdoc/require-returns> reported by reviewdog 🐶
Missing JSDoc @returns declaration.

Comment on lines 165 to 167
/**
* Convert number of bytes into human readable format
*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <jsdoc/tag-lines> reported by reviewdog 🐶
Expected only 0 line after block description

Suggested change
/**
* Convert number of bytes into human readable format
*
/**
* Convert number of bytes into human readable format

/**
* Convert number of bytes into human readable format
*
* @param integer bytes Number of bytes to convert
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <jsdoc/require-param-type> reported by reviewdog 🐶
Missing JSDoc @param "integer" type.

precision = 2;
return (bytes / TEBIBYTE).toFixed(precision) + " TiB";
} else {
return bytes + " B";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prefer-template> reported by reviewdog 🐶
Unexpected string concatenation.

Suggested change
return bytes + " B";
return `${bytes } B`;

"md5",
], function ($, _, Backbone, uuid, he, AccessPolicy, ObjectFormats, md5) {
], function (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <func-names> reported by reviewdog 🐶
Unexpected unnamed function.

ObjectFormats,
Utilities,
md5,
) {
/**
* @class DataONEObject
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <jsdoc/check-alignment> reported by reviewdog 🐶
Expected JSDoc block to be aligned.

reviewdog suggestion errorGitHub comment range and suggestion line range must be same. L23-L23 v.s. L22-L32

@@ -124,8 +135,15 @@ define([

this.set("accessPolicy", this.createAccessPolicy());

this.on("change:size", this.bytesToSize);
if (attrs.size) this.bytesToSize();
var model = this;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <vars-on-top> reported by reviewdog 🐶
All 'var' declarations must be at the top of the function scope.

@@ -124,8 +135,15 @@ define([

this.set("accessPolicy", this.createAccessPolicy());

this.on("change:size", this.bytesToSize);
if (attrs.size) this.bytesToSize();
var model = this;
Copy link
Contributor

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.

Suggested change
var model = this;
let model = this;

Copy link
Contributor

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.

], function (
$,
_,
Backbone,
uuid,
he,
AccessPolicy,
ObjectFormats,
Utilities,
md5,
) {
/**
* @class DataONEObject
* @classdesc A DataONEObject represents a DataONE object, such as a data file,
a science metadata object, or a resource map. It stores the system
metadata attributes for the object, performs updates to the system metadata,
and other basic DataONE API functions. This model can be extended to provide
specific functionality for different object types, such as the {@link ScienceMetadata}
model and the {@link EML211} model.
* @classcategory Models
* @augments Backbone.Model
*/
var DataONEObject = Backbone.Model.extend(
/** @lends DataONEObject.prototype */ {
type: "DataONEObject",
selectedInEditor: false, // Has this package member been selected and displayed in the provenance editor?
PROV: "http://www.w3.org/ns/prov#",
PROVONE: "http://purl.dataone.org/provone/2015/01/15/ontology#",
defaults: function () {
return {
// System Metadata attributes
serialVersion: null,
identifier: null,
formatId: null,
size: null,
checksum: null,
originalChecksum: null,
checksumAlgorithm: "MD5",
submitter: null,
rightsHolder: null,
accessPolicy: [], //An array of accessPolicy literal JS objects
replicationAllowed: null,
replicationPolicy: [],
obsoletes: null,
obsoletedBy: null,
archived: null,
dateUploaded: null,
dateSysMetadataModified: null,
originMemberNode: null,
authoritativeMemberNode: null,
replica: [],
seriesId: null, // uuid.v4(), (decide if we want to auto-set this)
mediaType: null,
fileName: null,
// Non-system metadata attributes:
isNew: null,
datasource: null,
insert_count_i: null,
read_count_i: null,
changePermission: null,
writePermission: null,
readPermission: null,
isPublic: null,
dateModified: null,
id: "urn:uuid:" + uuid.v4(),
sizeStr: null,
type: "", // Data, Metadata, or DataPackage
formatType: "",
metadataEntity: null, // A model that represents the metadata for this file, e.g. an EMLEntity model
latestVersion: null,
isDocumentedBy: null,
documents: [],
members: [],
resourceMap: [],
nodeLevel: 0, // Indicates hierarchy level in the view for indentation
sortOrder: 2, // Metadata: 1, Data: 2, DataPackage: 3
synced: false, // True if the full model has been synced
uploadStatus: null, //c=complete, p=in progress, q=queued, e=error, w=warning, no upload status=not in queue
uploadProgress: null,
sysMetaUploadStatus: null, //c=complete, p=in progress, q=queued, e=error, l=loading, no upload status=not in queue
percentLoaded: 0, // Percent the file is read before caclculating the md5 sum
uploadFile: null, // The file reference to be uploaded (JS object: File)
errorMessage: null,
sysMetaErrorCode: null, // The status code given when there is an error updating the system metadata
numSaveAttempts: 0,
notFound: false, //Whether or not this object was found in the system
originalAttrs: [], // An array of original attributes in a DataONEObject
changed: false, // If any attributes have been changed, including attrs in nested objects
hasContentChanges: false, // If attributes outside of originalAttrs have been changed
sysMetaXML: null, // A cached original version of the fetched system metadata document
objectXML: null, // A cached version of the object fetched from the server
isAuthorized: null, // If the stated permission is authorized by the user
isAuthorized_read: null, //If the user has permission to read
isAuthorized_write: null, //If the user has permission to write
isAuthorized_changePermission: null, //If the user has permission to changePermission
createSeriesId: false, //If true, a seriesId will be created when this object is saved.
collections: [], //References to collections that this model is in
possibleAuthMNs: [], //A list of possible authoritative MNs of this object
useAltRepo: false,
isLoadingFiles: false, //Only relevant to Resource Map objects. Is true if there is at least one file still loading into the package.
numLoadingFiles: 0, //Only relevant to Resource Map objects. The number of files still loading into the package.
provSources: [],
provDerivations: [],
prov_generated: [],
prov_generatedByExecution: [],
prov_generatedByProgram: [],
prov_generatedByUser: [],
prov_hasDerivations: [],
prov_hasSources: [],
prov_instanceOfClass: [],
prov_used: [],
prov_usedByExecution: [],
prov_usedByProgram: [],
prov_usedByUser: [],
prov_wasDerivedFrom: [],
prov_wasExecutedByExecution: [],
prov_wasExecutedByUser: [],
prov_wasInformedBy: [],
};
},
initialize: function (attrs, options) {
if (typeof attrs == "undefined") var attrs = {};
this.set("accessPolicy", this.createAccessPolicy());
var model = this;
this.on("change:size", function () {
var size = Utilities.bytesToSize(model.get("size"));
model.set("sizeStr", size);
});
if (attrs.size) {
var size = Utilities.bytesToSize(model.get("size"));
model.set("sizeStr", size);
}
// Cache an array of original attribute names to help in handleChange()
if (this.type == "DataONEObject")
this.set("originalAttrs", Object.keys(this.attributes));
else
this.set(
"originalAttrs",
Object.keys(DataONEObject.prototype.defaults()),
);
this.on("successSaving", this.updateRelationships);
//Save a reference to this DataONEObject model in the metadataEntity model
//whenever the metadataEntity is set
this.on("change:metadataEntity", function () {
var entityMetadataModel = this.get("metadataEntity");
if (entityMetadataModel)
entityMetadataModel.set("dataONEObject", this);
});
this.on("sync", function () {
this.set("synced", true);
});
//Find Member Node object that might be the authoritative MN
//This is helpful when MetacatUI may be displaying content from multiple MNs
this.setPossibleAuthMNs();
},
/**
* Maps the lower-case sys meta node names (valid in HTML DOM) to the
* camel-cased sys meta node names (valid in DataONE).
* Used during parse() and serialize()
*/
nodeNameMap: function () {
return {
accesspolicy: "accessPolicy",
accessrule: "accessRule",
authoritativemembernode: "authoritativeMemberNode",
checksumalgorithm: "checksumAlgorithm",
dateuploaded: "dateUploaded",
datesysmetadatamodified: "dateSysMetadataModified",
formatid: "formatId",
filename: "fileName",
nodereference: "nodeReference",
numberreplicas: "numberReplicas",
obsoletedby: "obsoletedBy",
originmembernode: "originMemberNode",
replicamembernode: "replicaMemberNode",
replicationallowed: "replicationAllowed",
replicationpolicy: "replicationPolicy",
replicationstatus: "replicationStatus",
replicaverified: "replicaVerified",
rightsholder: "rightsHolder",
serialversion: "serialVersion",
seriesid: "seriesId",
};
},
/**
* Returns the URL string where this DataONEObject can be fetched from or saved to
* @returns {string}
*/
url: function () {
// With no id, we can't do anything
if (!this.get("id") && !this.get("seriesid")) return "";
//Get the active alternative repository, if one is configured
var activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
//Start the base URL string
var baseUrl = "";
// Determine if we're updating a new/existing object,
// or just its system metadata
// New uploads use the object service URL
if (this.isNew()) {
//Use the object service URL from the alt repo
if (this.get("useAltRepo") && activeAltRepo) {
baseUrl = activeAltRepo.objectServiceUrl;
}
//If this MetacatUI deployment is pointing to a MN, use the object service URL from the AppModel
else {
baseUrl = MetacatUI.appModel.get("objectServiceUrl");
}
//Return the full URL
return baseUrl;
} else {
if (this.hasUpdates()) {
if (this.get("hasContentChanges")) {
//Use the object service URL from the alt repo
if (this.get("useAltRepo") && activeAltRepo) {
baseUrl = activeAltRepo.objectServiceUrl;
} else {
baseUrl = MetacatUI.appModel.get("objectServiceUrl");
}
// Exists on the server, use MN.update()
return baseUrl + encodeURIComponent(this.get("oldPid"));
} else {
//Use the meta service URL from the alt repo
if (this.get("useAltRepo") && activeAltRepo) {
baseUrl = activeAltRepo.metaServiceUrl;
} else {
baseUrl = MetacatUI.appModel.get("metaServiceUrl");
}
// Exists on the server, use MN.updateSystemMetadata()
return baseUrl + encodeURIComponent(this.get("id"));
}
} else {
//Use the meta service URL from the alt repo
if (this.get("useAltRepo") && activeAltRepo) {
baseUrl = activeAltRepo.metaServiceUrl;
} else {
baseUrl = MetacatUI.appModel.get("metaServiceUrl");
}
// Use MN.getSystemMetadata()
return (
baseUrl +
(encodeURIComponent(this.get("id")) ||
encodeURIComponent(this.get("seriesid")))
);
}
}
},
/**
* Create the URL string that is used to download this package
* @returns PackageURL string for this DataONE Object
* @since 2.28.0
*/
getPackageURL: function () {
var url = null;
// With no id, we can't do anything
if (!this.get("id") && !this.get("seriesid")) return url;
//If we haven't set a packageServiceURL upon app initialization and we are querying a CN, then the packageServiceURL is dependent on the MN this package is from
if (
MetacatUI.appModel.get("d1Service").toLowerCase().indexOf("cn/") >
-1 &&
MetacatUI.nodeModel.get("members").length
) {
var source = this.get("datasource"),
node = _.find(MetacatUI.nodeModel.get("members"), {
identifier: source,
});
//If this node has MNRead v2 services...
if (node && node.readv2)
url =
node.baseURL +
"/v2/packages/application%2Fbagit-097/" +
encodeURIComponent(this.get("id"));
} else if (MetacatUI.appModel.get("packageServiceUrl"))
url =
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(this.get("id"));
return url;
},
/**
* Overload Backbone.Model.fetch, so that we can set custom options for each fetch() request
*/
fetch: function (options) {
if (!options) var options = {};
else var options = _.clone(options);
options.url = this.url();
//If we are using the Solr service to retrieve info about this object, then construct a query
if (typeof options != "undefined" && options.solrService) {
//Get basic information
var query = "";
//Do not search for seriesId when it is not configured in this model/app
if (typeof this.get("seriesid") === "undefined")
query += 'id:"' + encodeURIComponent(this.get("id")) + '"';
//If there is no seriesid set, then search for pid or sid
else if (!this.get("seriesid"))
query +=
'(id:"' +
encodeURIComponent(this.get("id")) +
'" OR seriesId:"' +
encodeURIComponent(this.get("id")) +
'")';
//If a seriesId is specified, then search for that
else if (this.get("seriesid") && this.get("id").length > 0)
query +=
'(seriesId:"' +
encodeURIComponent(this.get("seriesid")) +
'" AND id:"' +
encodeURIComponent(this.get("id")) +
'")';
//If only a seriesId is specified, then just search for the most recent version
else if (this.get("seriesid") && !this.get("id"))
query +=
'seriesId:"' +
encodeURIComponent(this.get("id")) +
'" -obsoletedBy:*';
//The fields to return
var fl = "formatId,formatType,documents,isDocumentedBy,id,seriesId";
//Use the Solr query URL
var solrOptions = {
url:
MetacatUI.appModel.get("queryServiceUrl") +
"q=" +
query +
"&fl=" +
fl +
"&wt=json",
};
//Merge with the options passed to this function
var fetchOptions = _.extend(options, solrOptions);
} else if (typeof options != "undefined") {
//Use custom options for retreiving XML
//Merge with the options passed to this function
var fetchOptions = _.extend(
{
dataType: "text",
},
options,
);
} else {
//Use custom options for retreiving XML
var fetchOptions = _.extend({
dataType: "text",
});
}
//Add the authorization options
fetchOptions = _.extend(
fetchOptions,
MetacatUI.appUserModel.createAjaxSettings(),
);
//Call Backbone.Model.fetch to retrieve the info
return Backbone.Model.prototype.fetch.call(this, fetchOptions);
},
/**
* This function is called by Backbone.Model.fetch.
* It deserializes the incoming XML from the /meta REST endpoint and converts it into JSON.
*/
parse: function (response) {
// If the response is XML
if (typeof response == "string" && response.indexOf("<") == 0) {
var responseDoc = $.parseHTML(response),
systemMetadata;
//Save the raw XML in case it needs to be used later
this.set("sysMetaXML", response);
//Find the XML node for the system metadata
for (var i = 0; i < responseDoc.length; i++) {
if (
responseDoc[i].nodeType == 1 &&
responseDoc[i].localName.indexOf("systemmetadata") > -1
) {
systemMetadata = responseDoc[i];
break;
}
}
//Parse the XML to JSON
var sysMetaValues = this.toJson(systemMetadata);
//Convert the JSON to a camel-cased version, which matches Solr and is easier to work with in code
_.each(
Object.keys(sysMetaValues),
function (key) {
var camelCasedKey = this.nodeNameMap()[key];
if (camelCasedKey) {
sysMetaValues[camelCasedKey] = sysMetaValues[key];
delete sysMetaValues[key];
}
},
this,
);
//Save the checksum from the system metadata in a separate attribute on the model
sysMetaValues.originalChecksum = sysMetaValues.checksum;
sysMetaValues.checksum = this.defaults().checksum;
//Save the identifier as the id attribute
sysMetaValues.id = sysMetaValues.identifier;
//Parse the Access Policy
if (
this.get("accessPolicy") &&
AccessPolicy.prototype.isPrototypeOf(this.get("accessPolicy"))
) {
this.get("accessPolicy").parse(
$(systemMetadata).find("accesspolicy"),
);
sysMetaValues.accessPolicy = this.get("accessPolicy");
} else {
//Create a new AccessPolicy collection, if there isn't one already.
sysMetaValues.accessPolicy = this.createAccessPolicy(
$(systemMetadata).find("accesspolicy"),
);
}
return sysMetaValues;
// If the response is a list of Solr docs
} else if (
typeof response === "object" &&
response.response &&
response.response.docs
) {
//If no objects were found in the index, mark as notFound and exit
if (!response.response.docs.length) {
this.set("notFound", true);
this.trigger("notFound");
return;
}
//Get the Solr document (there should be only one)
var doc = response.response.docs[0];
//Take out any empty values
_.each(Object.keys(doc), function (field) {
if (!doc[field] && doc[field] !== 0) delete doc[field];
});
//Remove any erroneous white space from fields
this.removeWhiteSpaceFromSolrFields(doc);
return doc;
}
// Default to returning the raw response
else return response;
},
/** A utility function for converting XML to JSON */
toJson: function (xml) {
// Create the return object
var obj = {};
// do children
if (xml.hasChildNodes()) {
for (var i = 0; i < xml.childNodes.length; i++) {
var item = xml.childNodes.item(i);
//If it's an empty text node, skip it
if (item.nodeType == 3 && !item.nodeValue.trim()) continue;
//Get the node name
var nodeName = item.localName;
//If it's a new container node, convert it to JSON and add as a new object attribute
if (typeof obj[nodeName] == "undefined" && item.nodeType == 1) {
obj[nodeName] = this.toJson(item);
}
//If it's a new text node, just store the text value and add as a new object attribute
else if (
typeof obj[nodeName] == "undefined" &&
item.nodeType == 3
) {
obj =
item.nodeValue == "false"
? false
: item.nodeValue == "true"
? true
: item.nodeValue;
}
//If this node name is already stored as an object attribute...
else if (typeof obj[nodeName] != "undefined") {
//Cache what we have now
var old = obj[nodeName];
if (!Array.isArray(old)) old = [old];
//Create a new object to store this node info
var newNode = {};
//Add the new node info to the existing array we have now
if (item.nodeType == 1) {
newNode = this.toJson(item);
var newArray = old.concat(newNode);
} else if (item.nodeType == 3) {
newNode = item.nodeValue;
var newArray = old.concat(newNode);
}
//Store the attributes for this node
_.each(item.attributes, function (attr) {
newNode[attr.localName] = attr.nodeValue;
});
//Replace the old array with the updated one
obj[nodeName] = newArray;
//Exit
continue;
}
//Store the attributes for this node
/*_.each(item.attributes, function(attr){
obj[nodeName][attr.localName] = attr.nodeValue;
});*/
}
}
return obj;
},
/**
Serialize the DataONE object JSON to XML
@param {object} json - the JSON object to convert to XML
@param {Element} containerNode - an HTML element to insertt the resulting XML into
@returns {Element} The updated HTML Element
*/
toXML: function (json, containerNode) {
if (typeof json == "string") {
containerNode.textContent = json;
return containerNode;
}
for (var i = 0; i < Object.keys(json).length; i++) {
var key = Object.keys(json)[i],
contents = json[key] || json[key];
var node = document.createElement(key);
//Skip this attribute if it is not populated
if (!contents || (Array.isArray(contents) && !contents.length))
continue;
//If it's a simple text node
if (typeof contents == "string") {
containerNode.textContent = contents;
return containerNode;
} else if (Array.isArray(contents)) {
var allNewNodes = [];
for (var ii = 0; ii < contents.length; ii++) {
allNewNodes.push(this.toXML(contents[ii], $(node).clone()[0]));
}
if (allNewNodes.length) node = allNewNodes;
} else if (typeof contents == "object") {
$(node).append(this.toXML(contents, node));
var attributeNames = _.without(Object.keys(json[key]), "content");
}
$(containerNode).append(node);
}
return containerNode;
},
/**
* Saves the DataONEObject System Metadata to the server
*/
save: function (attributes, options) {
// Set missing file names before saving
if (!this.get("fileName")) {
this.setMissingFileName();
} else {
//Replace all non-alphanumeric characters with underscores
var fileNameWithoutExt = this.get("fileName").substring(
0,
this.get("fileName").lastIndexOf("."),
),
extension = this.get("fileName").substring(
this.get("fileName").lastIndexOf("."),
this.get("fileName").length,
);
this.set(
"fileName",
fileNameWithoutExt.replace(/[^a-zA-Z0-9]/g, "_") + extension,
);
}
if (!this.hasUpdates()) {
this.set("uploadStatus", null);
return;
}
//Set the upload transfer as in progress
this.set("uploadProgress", 2);
this.set("uploadStatus", "p");
//Check if the checksum has been calculated yet.
if (!this.get("checksum")) {
//When it is calculated, restart this function
this.on("checksumCalculated", this.save);
//Calculate the checksum for this file
this.calculateChecksum();
//Exit this function until the checksum is done
return;
}
//Create a FormData object to send data with our XHR
var formData = new FormData();
//If this is not a new object, update the id. New DataONEObjects will have an id
// created during initialize.
if (!this.isNew()) {
this.updateID();
formData.append("pid", this.get("oldPid"));
formData.append("newPid", this.get("id"));
} else {
//Create an ID if there isn't one
if (!this.get("id")) {
this.set("id", "urn:uuid:" + uuid.v4());
}
//Add the identifier to the XHR data
formData.append("pid", this.get("id"));
}
//Create the system metadata XML
var sysMetaXML = this.serializeSysMeta();
//Send the system metadata as a Blob
var xmlBlob = new Blob([sysMetaXML], { type: "application/xml" });
//Add the system metadata XML to the XHR data
formData.append("sysmeta", xmlBlob, "sysmeta.xml");
// Create the new object (MN.create())
formData.append("object", this.get("uploadFile"), this.get("fileName"));
var model = this;
// On create(), add to the package and the metadata
// Note: This should be added to the parent collection
// but for now we are using the root collection
_.each(
this.get("collections"),
function (collection) {
if (collection.type == "DataPackage") {
this.off("successSaving", collection.addNewModel);
this.once("successSaving", collection.addNewModel, collection);
}
},
this,
);
//Put together the AJAX and Backbone.save() options
var requestSettings = {
url: this.url(),
cache: false,
contentType: false,
dataType: "text",
processData: false,
data: formData,
parse: false,
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: this.onSuccessfulSave,
error: function (model, response, xhr) {
//Reset the identifier changes
model.resetID();
//Reset the checksum, if this is a model that needs to be serialized with each save.
if (model.serialize) {
model.set("checksum", model.defaults().checksum);
}
model.set("numSaveAttempts", model.get("numSaveAttempts") + 1);
var numSaveAttempts = model.get("numSaveAttempts");
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);
var parsedResponse = $(response.responseText)
.not("style, title")
.text();
//When there is no network connection (status == 0), there will be no response text
if (!parsedResponse)
parsedResponse =
"There was a network issue that prevented this file from uploading. " +
"Make sure you are connected to a reliable internet connection.";
model.set("errorMessage", parsedResponse);
//Set the model status as e for error
model.set("uploadStatus", "e");
//Trigger a custom event for the model save error
model.trigger("errorSaving", parsedResponse);
// Track this error in our analytics
MetacatUI.analytics?.trackException(
`DataONEObject save error: ${parsedResponse}`,
model.get("id"),
true,
);
}
},
};
//Add the user settings
requestSettings = _.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
);
//Send the Save request
Backbone.Model.prototype.save.call(this, null, requestSettings);
},
/**
* This function is executed when the XHR that saves this DataONEObject has
* successfully completed. It can be called directly if a DataONEObject is saved
* without directly using the DataONEObject.save() function.
* @param {DataONEObject} [model] A reference to this DataONEObject model
* @param {XMLHttpRequest.response} [response] The XHR response object
* @param {XMLHttpRequest} [xhr] The XHR that was just completed successfully
*/
onSuccessfulSave: function (model, response, xhr) {
if (typeof model == "undefined") {
var model = this;
}
model.set("numSaveAttempts", 0);
model.set("uploadStatus", "c");
model.set("isNew", false);
model.trigger("successSaving", model);
// Get the newest sysmeta set by the MN
model.fetch({
merge: true,
systemMetadataOnly: true,
});
// Reset the content changes status
model.set("hasContentChanges", false);
//Reset the model isNew attribute
model.set("isNew", false);
// Reset oldPid so we can replace again
model.set("oldPid", null);
//Set the last-calculated checksum as the original checksum
model.set("originalChecksum", model.get("checksum"));
model.set("checksum", model.defaults().checksum);
},
/**
* Updates the DataONEObject System Metadata to the server
*/
updateSysMeta: function () {
//Update the upload status to "p" for "in progress"
this.set("uploadStatus", "p");
//Update the system metadata upload status to "p" as well, so the app
// knows that the system metadata, specifically, is being updated.
this.set("sysMetaUploadStatus", "p");
var formData = new FormData();
//Add the identifier to the XHR data
formData.append("pid", this.get("id"));
var sysMetaXML = this.serializeSysMeta();
//Send the system metadata as a Blob
var xmlBlob = new Blob([sysMetaXML], { type: "application/xml" });
//Add the system metadata XML to the XHR data
formData.append("sysmeta", xmlBlob, "sysmeta.xml");
var model = this;
var baseUrl = "",
activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
//Use the meta service URL from the alt repo
if (activeAltRepo) {
baseUrl = activeAltRepo.metaServiceUrl;
}
//If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel
else {
baseUrl = MetacatUI.appModel.get("metaServiceUrl");
}
var requestSettings = {
url: baseUrl + encodeURIComponent(this.get("id")),
cache: false,
contentType: false,
dataType: "text",
type: "PUT",
processData: false,
data: formData,
parse: false,
success: function () {
model.set("numSaveAttempts", 0);
//Fetch the system metadata from the server so we have a fresh copy of the newest sys meta.
model.fetch({ systemMetadataOnly: true });
model.set("sysMetaErrorCode", null);
//Update the upload status to "c" for "complete"
model.set("uploadStatus", "c");
model.set("sysMetaUploadStatus", "c");
//Trigger a custom event that the sys meta was updated
model.trigger("sysMetaUpdated");
},
error: function (xhr, status, statusCode) {
model.set("numSaveAttempts", model.get("numSaveAttempts") + 1);
var numSaveAttempts = model.get("numSaveAttempts");
if (numSaveAttempts < 3 && (statusCode == 408 || statusCode == 0)) {
//Try saving again in 10, 40, and 90 seconds
setTimeout(
function () {
model.updateSysMeta.call(model);
},
numSaveAttempts * numSaveAttempts * 10000,
);
} else {
model.set("numSaveAttempts", 0);
var parsedResponse = $(xhr.responseText)
.not("style, title")
.text();
//When there is no network connection (status == 0), there will be no response text
if (!parsedResponse)
parsedResponse =
"There was a network issue that prevented this file from updating. " +
"Make sure you are connected to a reliable internet connection.";
model.set("errorMessage", parsedResponse);
model.set("sysMetaErrorCode", statusCode);
model.set("uploadStatus", "e");
model.set("sysMetaUploadStatus", "e");
// Trigger a custom event for the sysmeta update that
// errored
model.trigger("sysMetaUpdateError");
// Track this error in our analytics
MetacatUI.analytics?.trackException(
`DataONEObject update system metadata ` +
`error: ${parsedResponse}`,
model.get("id"),
true,
);
}
},
};
//Add the user settings
requestSettings = _.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
);
//Send the XHR
$.ajax(requestSettings);
},
/**
* Check if the current user is authorized to perform an action on this object. This function doesn't return
* the result of the check, but it sends an XHR, updates this model, and triggers a change event.
* @param {string} [action=changePermission] - The action (read, write, or changePermission) to check
* if the current user has authorization to perform. By default checks for the highest level of permission.
* @param {object} [options] Additional options for this function. See the properties below.
* @property {function} options.onSuccess - A function to execute when the checkAuthority API is successfully completed
* @property {function} options.onError - A function to execute when the checkAuthority API returns an error, or when no PID or SID can be found for this object.
* @return {boolean}
*/
checkAuthority: function (action = "changePermission", options) {
try {
// return false - if neither PID nor SID is present to check the authority
if (this.get("id") == null && this.get("seriesId") == null) {
return false;
}
if (typeof options == "undefined") {
var options = {};
}
// If onError or onSuccess options were provided by the user,
// check that they are functions first, so we don't try to use
// some other type of variable as a function later on.
["onError", "onSuccess"].forEach(function (userFunction) {
if (typeof options[userFunction] !== "function") {
options[userFunction] = null;
}
});
// If PID is not present - check authority with seriesId
var identifier = this.get("id");
if (identifier == null) {
identifier = this.get("seriesId");
}
//If there are alt repositories configured, find the possible authoritative
// Member Node for this DataONEObject.
if (MetacatUI.appModel.get("alternateRepositories").length) {
//Get the array of possible authoritative MNs
var possibleAuthMNs = this.get("possibleAuthMNs");
//If there are no possible authoritative MNs, use the auth service URL from the AppModel
if (!possibleAuthMNs.length) {
baseUrl = MetacatUI.appModel.get("authServiceUrl");
} else {
//Use the auth service URL from the top possible auth MN
baseUrl = possibleAuthMNs[0].authServiceUrl;
}
} else {
//Get the auth service URL from the AppModel
baseUrl = MetacatUI.appModel.get("authServiceUrl");
}
if (!baseUrl) {
return false;
}
var onSuccess =
options.onSuccess ||
function (data, textStatus, xhr) {
model.set("isAuthorized_" + action, true);
model.set("isAuthorized", true);
model.trigger("change:isAuthorized");
},
onError =
options.onError ||
function (xhr, textStatus, errorThrown) {
if (errorThrown == 404) {
var possibleAuthMNs = model.get("possibleAuthMNs");
if (possibleAuthMNs.length) {
//Remove the first MN from the array, since it didn't contain the object, so it's not the auth MN
possibleAuthMNs.shift();
}
//If there are no other possible auth MNs to check, trigger this model as Not Found.
if (possibleAuthMNs.length == 0 || !possibleAuthMNs) {
model.set("notFound", true);
model.trigger("notFound");
}
//If there's more MNs to check, try again
else {
model.checkAuthority(action, options);
}
} else {
model.set("isAuthorized_" + action, false);
model.set("isAuthorized", false);
}
};
var model = this;
var requestSettings = {
url: baseUrl + encodeURIComponent(identifier) + "?action=" + action,
type: "GET",
success: onSuccess,
error: onError,
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
} catch (e) {
//Log an error to the console
console.error("Couldn't check the authority for this user: ", e);
// Track this error in our analytics
const name = MetacatUI.appModel.get("username");
MetacatUI.analytics?.trackException(
`Couldn't check the authority for the user ${name}: ${e}`,
this.get("id"),
true,
);
//Set the user as unauthorized
model.set("isAuthorized_" + action, false);
model.set("isAuthorized", false);
return false;
}
},
/**
* Using the attributes set on this DataONEObject model, serializes the system metadata XML
* @returns {string}
*/
serializeSysMeta: function () {
//Get the system metadata XML that currently exists in the system
var sysMetaXML = this.get("sysMetaXML"), // sysmeta as string
xml, // sysmeta as DOM object
accessPolicyXML, // The generated access policy XML
previousSiblingNode, // A DOM node indicating any previous sibling
rightsHolderNode, // A DOM node for the rights holder field
accessPolicyNode, // A DOM node for the access policy
replicationPolicyNode, // A DOM node for the replication policy
obsoletesNode, // A DOM node for the obsoletes field
obsoletedByNode, // A DOM node for the obsoletedBy field
fileNameNode, // A DOM node for the file name
xmlString, // The system metadata document as a string
nodeNameMap, // The map of camelCase to lowercase attributes
extension; // the file name extension for this object
if (typeof sysMetaXML === "undefined" || sysMetaXML === null) {
xml = this.createSysMeta();
} else {
xml = $($.parseHTML(sysMetaXML));
}
//Update the system metadata values
xml.find("serialversion").text(this.get("serialVersion") || "0");
xml.find("identifier").text(this.get("newPid") || this.get("id"));
xml
.find("submitter")
.text(
this.get("submitter") || MetacatUI.appUserModel.get("username"),
);
xml.find("formatid").text(this.get("formatId") || this.getFormatId());
//If there is a seriesId, add it
if (this.get("seriesId")) {
//Get the seriesId XML node
var seriesIdNode = xml.find("seriesId");
//If it doesn't exist, create one
if (!seriesIdNode.length) {
seriesIdNode = $(document.createElement("seriesid"));
xml.find("identifier").before(seriesIdNode);
}
//Add the seriesId string to the XML node
seriesIdNode.text(this.get("seriesId"));
}
//If there is no size, get it
if (!this.get("size") && this.get("uploadFile")) {
this.set("size", this.get("uploadFile").size);
}
//Get the size of the file, if there is one
if (this.get("uploadFile")) {
xml.find("size").text(this.get("uploadFile").size);
}
//Otherwise, use the last known size
else {
xml.find("size").text(this.get("size"));
}
//Save the original checksum
if (!this.get("checksum") && this.get("originalChecksum")) {
xml.find("checksum").text(this.get("originalChecksum"));
}
//Update the checksum and checksum algorithm
else {
xml.find("checksum").text(this.get("checksum"));
xml.find("checksum").attr("algorithm", this.get("checksumAlgorithm"));
}
//Update the rightsholder
xml
.find("rightsholder")
.text(
this.get("rightsHolder") || MetacatUI.appUserModel.get("username"),
);
//Write the access policy
accessPolicyXML = this.get("accessPolicy").serialize();
// Get the access policy node, if it exists
accessPolicyNode = xml.find("accesspolicy");
previousSiblingNode = xml.find("rightsholder");
// Create an access policy node if needed
if (!accessPolicyNode.length && accessPolicyXML) {
accessPolicyNode = $(document.createElement("accesspolicy"));
previousSiblingNode.after(accessPolicyNode);
}
//Replace the old access policy with the new one if it exists
if (accessPolicyXML) {
accessPolicyNode.replaceWith(accessPolicyXML);
} else {
// Remove the node if it is empty
accessPolicyNode.remove();
}
// Set the obsoletes node after replPolicy or accessPolicy, or rightsHolder
replicationPolicyNode = xml.find("replicationpolicy");
accessPolicyNode = xml.find("accesspolicy");
rightsHolderNode = xml.find("rightsholder");
if (replicationPolicyNode.length) {
previousSiblingNode = replicationPolicyNode;
} else if (accessPolicyNode.length) {
previousSiblingNode = accessPolicyNode;
} else {
previousSiblingNode = rightsHolderNode;
}
obsoletesNode = xml.find("obsoletes");
if (this.get("obsoletes")) {
if (obsoletesNode.length) {
obsoletesNode.text(this.get("obsoletes"));
} else {
obsoletesNode = $(document.createElement("obsoletes")).text(
this.get("obsoletes"),
);
previousSiblingNode.after(obsoletesNode);
}
} else {
if (obsoletesNode) {
obsoletesNode.remove();
}
}
if (obsoletesNode) {
previousSiblingNode = obsoletesNode;
}
obsoletedByNode = xml.find("obsoletedby");
//remove the obsoletedBy node if it exists
// TODO: Verify this is what we want to do
if (obsoletedByNode) {
obsoletedByNode.remove();
}
xml.find("archived").text(this.get("archived") || "false");
xml
.find("dateuploaded")
.text(this.get("dateUploaded") || new Date().toISOString());
//Get the filename node
fileNameNode = xml.find("filename");
//If the filename node doesn't exist, then create one
if (!fileNameNode.length) {
fileNameNode = $(document.createElement("filename"));
xml.find("dateuploaded").after(fileNameNode);
}
//Set the object file name
$(fileNameNode).text(this.get("fileName"));
xmlString = $(document.createElement("div")).append(xml.clone()).html();
//Now camel case the nodes
nodeNameMap = this.nodeNameMap();
_.each(
Object.keys(nodeNameMap),
function (name, i) {
var originalXMLString = xmlString;
//Camel case node names
var regEx = new RegExp("<" + name, "g");
xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name]);
var regEx = new RegExp(name + ">", "g");
xmlString = xmlString.replace(regEx, nodeNameMap[name] + ">");
//If node names haven't been changed, then find an attribute
if (xmlString == originalXMLString) {
var regEx = new RegExp(" " + name + "=", "g");
xmlString = xmlString.replace(
regEx,
" " + nodeNameMap[name] + "=",
);
}
},
this,
);
xmlString = xmlString.replace(/systemmetadata/g, "systemMetadata");
return xmlString;
},
/**
* Get the object format identifier for this object
*/
getFormatId: function () {
var formatId = "application/octet-stream", // default to untyped data
objectFormats = {
mediaTypes: [], // The list of potential formatIds based on mediaType matches
extensions: [], // The list of possible formatIds based onextension matches
},
fileName = this.get("fileName"), // the fileName for this object
ext; // The extension of the filename for this object
objectFormats["mediaTypes"] = MetacatUI.objectFormats.where({
formatId: this.get("mediaType"),
});
if (
typeof fileName !== "undefined" &&
fileName !== null &&
fileName.length > 1
) {
ext = fileName.substring(
fileName.lastIndexOf(".") + 1,
fileName.length,
);
objectFormats["extensions"] = MetacatUI.objectFormats.where({
extension: ext,
});
}
if (
objectFormats["mediaTypes"].length > 0 &&
objectFormats["extensions"].length > 0
) {
var firstMediaType = objectFormats["mediaTypes"][0].get("formatId");
var firstExtension = objectFormats["extensions"][0].get("formatId");
// Check if they're equal
if (firstMediaType === firstExtension) {
formatId = firstMediaType;
return formatId;
}
// Handle mismatched mediaType and extension cases - additional cases can be added below
if (
firstMediaType === "application/vnd.ms-excel" &&
firstExtension === "text/csv"
) {
formatId = firstExtension;
return formatId;
}
}
if (objectFormats["mediaTypes"].length > 0) {
formatId = objectFormats["mediaTypes"][0].get("formatId");
console.log("returning default mediaType");
console.log(formatId);
return formatId;
}
if (objectFormats["extensions"].length > 0) {
//If this is a "nc" file, assume it is a netCDF-3 file.
if (ext == "nc") {
formatId = "netCDF-3";
} else {
formatId = objectFormats["extensions"][0].get("formatId");
}
return formatId;
}
return formatId;
},
/**
* Looks up human readable format of the DataONE Object
* @returns format String
* @since 2.28.0
*/
getFormat: function () {
var formatMap = {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
"Microsoft Excel OpenXML",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
"Microsoft Word OpenXML",
"application/vnd.ms-excel.sheet.binary.macroEnabled.12":
"Microsoft Office Excel 2007 binary workbooks",
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
"Microsoft Office OpenXML Presentation",
"application/vnd.ms-excel": "Microsoft Excel",
"application/msword": "Microsoft Word",
"application/vnd.ms-powerpoint": "Microsoft Powerpoint",
"text/html": "HTML",
"text/plain": "plain text (.txt)",
"video/avi": "Microsoft AVI file",
"video/x-ms-wmv": "Windows Media Video (.wmv)",
"audio/x-ms-wma": "Windows Media Audio (.wma)",
"application/vnd.google-earth.kml xml":
"Google Earth Keyhole Markup Language (KML)",
"http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html":
"annotation",
"application/mathematica": "Mathematica Notebook",
"application/postscript": "Postscript",
"application/rtf": "Rich Text Format (RTF)",
"application/xml": "XML Application",
"text/xml": "XML",
"application/x-fasta": "FASTA sequence file",
"nexus/1997": "NEXUS File Format for Systematic Information",
"anvl/erc-v02":
"Kernel Metadata and Electronic Resource Citations (ERCs), 2010.05.13",
"http://purl.org/dryad/terms/":
"Dryad Metadata Application Profile Version 3.0",
"http://datadryad.org/profile/v3.1":
"Dryad Metadata Application Profile Version 3.1",
"application/pdf": "PDF",
"application/zip": "ZIP file",
"http://www.w3.org/TR/rdf-syntax-grammar": "RDF/XML",
"http://www.w3.org/TR/rdfa-syntax": "RDFa",
"application/rdf xml": "RDF",
"text/turtle": "TURTLE",
"text/n3": "N3",
"application/x-gzip": "GZIP Format",
"application/x-python": "Python script",
"http://www.w3.org/2005/Atom": "ATOM-1.0",
"application/octet-stream": "octet stream (application file)",
"http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd":
"Darwin Core, v2.0",
"http://rs.tdwg.org/dwc/xsd/simpledarwincore/": "Simple Darwin Core",
"eml://ecoinformatics.org/eml-2.1.0": "EML v2.1.0",
"eml://ecoinformatics.org/eml-2.1.1": "EML v2.1.1",
"eml://ecoinformatics.org/eml-2.0.1": "EML v2.0.1",
"eml://ecoinformatics.org/eml-2.0.0": "EML v2.0.0",
"https://eml.ecoinformatics.org/eml-2.2.0": "EML v2.2.0",
};
return formatMap[this.get("formatId")] || this.get("formatId");
},
/**
* Build a fresh system metadata document for this object when it is new
* Return it as a DOM object
*/
createSysMeta: function () {
var sysmetaDOM, // The DOM
sysmetaXML = []; // The document as a string array
sysmetaXML.push(
//'<?xml version="1.0" encoding="UTF-8"?>',
"<d1_v2.0:systemmetadata",
' xmlns:d1_v2.0="http://ns.dataone.org/service/types/v2.0"',
' xmlns:d1="http://ns.dataone.org/service/types/v1">',
" <serialversion />",
" <identifier />",
" <formatid />",
" <size />",
" <checksum />",
" <submitter />",
" <rightsholder />",
" <filename />",
"</d1_v2.0:systemmetadata>",
);
sysmetaDOM = $($.parseHTML(sysmetaXML.join("")));
return sysmetaDOM;
},
/**
* Create an access policy for this DataONEObject using the default access
* policy set in the AppModel.
*
* @param {Element} [accessPolicyXML] - An <accessPolicy> XML node
* that contains a list of access rules.
* @return {AccessPolicy} - an AccessPolicy collection that represents the
* given XML or the default policy set in the AppModel.
*/
createAccessPolicy: function (accessPolicyXML) {
//Create a new AccessPolicy collection
var accessPolicy = new AccessPolicy();
accessPolicy.dataONEObject = this;
//If there is no access policy XML sent,
if (this.isNew() && !accessPolicyXML) {
try {
//If the app is configured to inherit the access policy from the parent metadata,
// then get the parent metadata and copy it's AccessPolicy
let scienceMetadata = this.get("isDocumentedByModels");
if (
MetacatUI.appModel.get("inheritAccessPolicy") &&
scienceMetadata &&
scienceMetadata.length
) {
let sciMetaAccessPolicy = scienceMetadata[0].get("accessPolicy");
if (sciMetaAccessPolicy) {
accessPolicy.copyAccessPolicy(sciMetaAccessPolicy);
} else {
accessPolicy.createDefaultPolicy();
}
}
//Otherwise, set the default access policy using the AppModel configuration
else {
accessPolicy.createDefaultPolicy();
}
} catch (e) {
console.error(
"Could create access policy, so defaulting to default",
e,
);
accessPolicy.createDefaultPolicy();
}
} else {
//Parse the access policy XML to create AccessRule models from the XML
accessPolicy.parse(accessPolicyXML);
}
//Listen to changes on the collection and trigger a change on this model
var self = this;
this.listenTo(accessPolicy, "change update", function () {
self.trigger("change");
this.addToUploadQueue();
});
return accessPolicy;
},
/**
* Update identifiers for this object
*
* @param {string} id - Optional identifier to update with. Generated
* automatically when not given.
*
* Note that this method caches the objects attributes prior to
* updating so this.resetID() can be called in case of a failure
* state.
*
* Also note that this method won't run if theh oldPid attribute is
* set. This enables knowing before this.save is called what the next
* PID will be such as the case where we want to update a matching
* EML entity when replacing files.
*/
updateID: function (id) {
// Only run once until oldPid is reset
if (this.get("oldPid")) {
return;
}
//Save the attributes so we can reset the ID later
this.attributeCache = this.toJSON();
//Set the old identifier
var oldPid = this.get("id"),
selfDocuments,
selfDocumentedBy,
documentedModels,
documentedModel,
index;
//Save the current id as the old pid
this.set("oldPid", oldPid);
//Create a new seriesId, if there isn't one, and if this model specifies that one is required
if (!this.get("seriesId") && this.get("createSeriesId")) {
this.set("seriesId", "urn:uuid:" + uuid.v4());
}
// Check to see if the old pid documents or is documented by itself
selfDocuments = _.contains(this.get("documents"), oldPid);
selfDocumentedBy = _.contains(this.get("isDocumentedBy"), oldPid);
//Set the new identifier
if (id) {
this.set("id", id);
} else {
if (this.get("type") == "DataPackage") {
this.set("id", "resource_map_urn:uuid:" + uuid.v4());
} else {
this.set("id", "urn:uuid:" + uuid.v4());
}
}
// Remove the old pid from the documents list if present
if (selfDocuments) {
index = this.get("documents").indexOf(oldPid);
if (index > -1) {
this.get("documents").splice(index, 1);
}
// And add the new pid in
this.get("documents").push(this.get("id"));
}
// Remove the old pid from the isDocumentedBy list if present
if (selfDocumentedBy) {
index = this.get("isDocumentedBy").indexOf(oldPid);
if (index > -1) {
this.get("isDocumentedBy").splice(index, 1);
}
// And add the new pid in
this.get("isDocumentedBy").push(this.get("id"));
}
// Update all models documented by this pid with the new id
_.each(
this.get("documents"),
function (id) {
(documentedModels = MetacatUI.rootDataPackage.where({ id: id })),
documentedModel;
if (documentedModels.length > 0) {
documentedModel = documentedModels[0];
}
if (typeof documentedModel !== "undefined") {
// Find the oldPid in the array
if (Array.isArray(documentedModel.get("isDocumentedBy"))) {
index = documentedModel.get("isDocumentedBy").indexOf("oldPid");
if (index > -1) {
// Remove it
documentedModel.get("isDocumentedBy").splice(index, 1);
}
// And add the new pid in
documentedModel.get("isDocumentedBy").push(this.get("id"));
}
}
},
this,
);
this.trigger("change:id");
//Update the obsoletes and obsoletedBy
this.set("obsoletes", oldPid);
this.set("obsoletedBy", null);
// Update the latest version of this object
this.set("latestVersion", this.get("id"));
//Set the archived option to false
this.set("archived", false);
},
/**
* Resets the identifier for this model. This undos all of the changes made in {DataONEObject#updateID}
*/
resetID: function () {
if (!this.attributeCache) return false;
this.set("oldPid", this.attributeCache.oldPid, { silent: true });
this.set("id", this.attributeCache.id, { silent: true });
this.set("obsoletes", this.attributeCache.obsoletes, { silent: true });
this.set("obsoletedBy", this.attributeCache.obsoletedBy, {
silent: true,
});
this.set("archived", this.attributeCache.archived, { silent: true });
this.set("latestVersion", this.attributeCache.latestVersion, {
silent: true,
});
//Reset the attribute cache
this.attributeCache = {};
},
/**
* Checks if this system metadata XML has updates that need to be synced with the server.
* @returns {boolean}
*/
hasUpdates: function () {
if (this.isNew()) return true;
// Compare the new system metadata XML to the old system metadata XML
//Check if there is system metadata first
if (!this.get("sysMetaXML")) {
return false;
}
var D1ObjectClone = this.clone(),
// Make sure we are using the parse function in the DataONEObject model.
// Sometimes hasUpdates is called from extensions of the D1Object model,
// (e.g. from the portal model), and the parse function is overwritten
oldSysMetaAttrs = new DataONEObject().parse(
D1ObjectClone.get("sysMetaXML"),
);
D1ObjectClone.set(oldSysMetaAttrs);
var oldSysMeta = D1ObjectClone.serializeSysMeta();
var newSysMeta = this.serializeSysMeta();
if (oldSysMeta === "") return false;
return !(newSysMeta == oldSysMeta);
},
/**
Set the changed flag on any system metadata or content attribute changes,
and set the hasContentChanges flag on content changes only
@param {DataONEObject} [model]
@param {object} options Furhter options for this function
@property {boolean} options.force If true, a change will be handled regardless if the attribute actually changed
*/
handleChange: function (model, options) {
if (!model) var model = this;
var sysMetaAttrs = [
"serialVersion",
"identifier",
"formatId",
"formatType",
"size",
"checksum",
"checksumAlgorithm",
"submitter",
"rightsHolder",
"accessPolicy",
"replicationAllowed",
"replicationPolicy",
"obsoletes",
"obsoletedBy",
"archived",
"dateUploaded",
"dateSysMetadataModified",
"originMemberNode",
"authoritativeMemberNode",
"replica",
"seriesId",
"mediaType",
"fileName",
],
nonSysMetaNonContentAttrs = _.difference(
model.get("originalAttrs"),
sysMetaAttrs,
),
allChangedAttrs = Object.keys(model.changedAttributes()),
changedSysMetaOrContentAttrs = [], //sysmeta or content attributes that have changed
changedContentAttrs = []; // attributes from sub classes like ScienceMetadata or EML211 ...
// Get a list of all changed sysmeta and content attributes
changedSysMetaOrContentAttrs = _.difference(
allChangedAttrs,
nonSysMetaNonContentAttrs,
);
if (changedSysMetaOrContentAttrs.length > 0) {
// For any sysmeta or content change, set the package dirty flag
if (
MetacatUI.rootDataPackage &&
MetacatUI.rootDataPackage.packageModel &&
!MetacatUI.rootDataPackage.packageModel.get("changed") &&
model.get("synced")
) {
MetacatUI.rootDataPackage.packageModel.set("changed", true);
}
}
// And get a list of all changed content attributes
changedContentAttrs = _.difference(
changedSysMetaOrContentAttrs,
sysMetaAttrs,
);
if (
(changedContentAttrs.length > 0 &&
!this.get("hasContentChanges") &&
model.get("synced")) ||
(options && options.force)
) {
this.set("hasContentChanges", true);
this.addToUploadQueue();
}
},
/**
* Returns true if this DataONE object is new. A DataONE object is new
* if there is no upload date and it's been synced (i.e. been fetched)
* @return {boolean}
*/
isNew: function () {
//If the model is explicitly marked as not new, return false
if (this.get("isNew") === false) {
return false;
}
//If the model is explicitly marked as new, return true
else if (this.get("isNew") === true) {
return true;
}
//Check if there is an upload date that was retrieved from the server
return (
this.get("dateUploaded") === this.defaults().dateUploaded &&
this.get("synced")
);
},
/**
* Updates the upload status attribute on this model and marks the collection as changed
*/
addToUploadQueue: function () {
if (!this.get("synced")) {
return;
}
//Add this item to the queue
if (
this.get("uploadStatus") == "c" ||
this.get("uploadStatus") == "e" ||
!this.get("uploadStatus")
) {
this.set("uploadStatus", "q");
//Mark each DataPackage collection this model is in as changed
_.each(
this.get("collections"),
function (collection) {
if (collection.packageModel)
collection.packageModel.set("changed", true);
},
this,
);
}
},
/**
* Updates the progress percentage when the model is getting uploaded
* @param {ProgressEvent} e - The ProgressEvent when this file is being uploaded
*/
updateProgress: function (e) {
if (e.lengthComputable) {
var max = e.total;
var current = e.loaded;
var Percentage = (current * 100) / max;
if (Percentage >= 100) {
// process completed
}
}
},
/**
* Updates the relationships with other models when this model has been updated
*/
updateRelationships: function () {
_.each(
this.get("collections"),
function (collection) {
//Get the old id for this model
var oldId = this.get("oldPid");
if (!oldId) return;
//Find references to the old id in the documents relationship
var outdatedModels = collection.filter(function (m) {
return _.contains(m.get("documents"), oldId);
});
//Update the documents array in each model
_.each(
outdatedModels,
function (model) {
var updatedDocuments = _.without(model.get("documents"), oldId);
updatedDocuments.push(this.get("id"));
model.set("documents", updatedDocuments);
},
this,
);
},
this,
);
},
/**
* Finds the latest version of this object by travesing the obsolescence chain
* @param {string} [latestVersion] - The identifier of the latest known object in the version chain.
If not supplied, this model's `id` will be used.
* @param {string} [possiblyNewer] - The identifier of the object that obsoletes the latestVersion. It's "possibly" newer, because it may be private/inaccessible
*/
findLatestVersion: function (latestVersion, possiblyNewer) {
var baseUrl = "",
activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
//Use the meta service URL from the alt repo
if (activeAltRepo) {
baseUrl = activeAltRepo.metaServiceUrl;
}
//If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel
else {
baseUrl = MetacatUI.appModel.get("metaServiceUrl");
}
if (!baseUrl) {
return;
}
//If there is no system metadata, then retrieve it first
if (!this.get("sysMetaXML")) {
this.once("sync", this.findLatestVersion);
this.once("systemMetadataSync", this.findLatestVersion);
this.fetch({
url: baseUrl + encodeURIComponent(this.get("id")),
dataType: "text",
systemMetadataOnly: true,
});
return;
}
//If no pid was supplied, use this model's id
if (!latestVersion || typeof latestVersion != "string") {
var latestVersion = this.get("id");
var possiblyNewer = this.get("obsoletedBy");
}
//If this isn't obsoleted by anything, then there is no newer version
if (!possiblyNewer || typeof latestVersion != "string") {
this.set("latestVersion", latestVersion);
//Trigger an event that will fire whether or not the latestVersion
// attribute was actually changed
this.trigger("latestVersionFound", this);
//Remove the listeners now that we found the latest version
this.stopListening("sync", this.findLatestVersion);
this.stopListening("systemMetadataSync", this.findLatestVersion);
return;
}
var model = this;
//Get the system metadata for the possibly newer version
var requestSettings = {
url: baseUrl + encodeURIComponent(possiblyNewer),
type: "GET",
success: function (data) {
// the response may have an obsoletedBy element
var obsoletedBy = $(data).find("obsoletedBy").text();
//If there is an even newer version, then get it and rerun this function
if (obsoletedBy) {
model.findLatestVersion(possiblyNewer, obsoletedBy);
}
//If there isn't a newer version, then this is it
else {
model.set("latestVersion", possiblyNewer);
model.trigger("latestVersionFound", model);
//Remove the listeners now that we found the latest version
model.stopListening("sync", model.findLatestVersion);
model.stopListening(
"systemMetadataSync",
model.findLatestVersion,
);
}
},
error: function (xhr) {
//If this newer version isn't accessible, link to the latest version that is
if (xhr.status == "401") {
model.set("latestVersion", latestVersion);
model.trigger("latestVersionFound", model);
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/**
* A utility function that will format an XML string or XML nodes by camel-casing the node names, as necessary
* @param {string|Element} xml - The XML to format
* @returns {string} The formatted XML string
*/
formatXML: function (xml) {
var nodeNameMap = this.nodeNameMap(),
xmlString = "";
//XML must be provided for this function
if (!xml) return "";
//Support XML strings
else if (typeof xml == "string") xmlString = xml;
//Support DOMs
else if (typeof xml == "object" && xml.nodeType) {
//XML comments should be formatted with start and end carets
if (xml.nodeType == 8) xmlString = "<" + xml.nodeValue + ">";
//XML nodes have the entire XML string available in the outerHTML attribute
else if (xml.nodeType == 1) xmlString = xml.outerHTML;
//Text node types are left as-is
else if (xml.nodeType == 3) return xml.nodeValue;
}
//Return empty strings if something went wrong
if (!xmlString) return "";
_.each(
Object.keys(nodeNameMap),
function (name, i) {
var originalXMLString = xmlString;
//Check for this node name whe it's an opening XML node, e.g. `<name>`
var regEx = new RegExp("<" + name + ">", "g");
xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + ">");
//Check for this node name when it's an opening XML node, e.g. `<name `
regEx = new RegExp("<" + name + " ", "g");
xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + " ");
//Check for this node name when it's preceeded by a namespace, e.g. `:name `
regEx = new RegExp(":" + name + " ", "g");
xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + " ");
//Check for this node name when it's a closing tag preceeded by a namespace, e.g. `:name>`
regEx = new RegExp(":" + name + ">", "g");
xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + ">");
//Check for this node name when it's a closing XML tag, e.g. `</name>`
regEx = new RegExp("</" + name + ">", "g");
xmlString = xmlString.replace(
regEx,
"</" + nodeNameMap[name] + ">",
);
//If node names haven't been changed, then find an attribute, e.g. ` name=`
if (xmlString == originalXMLString) {
regEx = new RegExp(" " + name + "=", "g");
xmlString = xmlString.replace(
regEx,
" " + nodeNameMap[name] + "=",
);
}
},
this,
);
//Take each XML node text value and decode any XML entities
var regEx = new RegExp("&[0-9a-zA-Z]+;", "g");
xmlString = xmlString.replace(regEx, function (match) {
return he.encode(he.decode(match));
});
return xmlString;
},
/**
* This method will download this object while
* sending the user's auth token in the request.
* @returns None
* @since: 2.28.0
*/
downloadWithCredentials: function () {
//if(this.get("isPublic")) return;
//Get info about this object
var url = this.get("url"),
model = this;
//Create an XHR
var xhr = new XMLHttpRequest();
//Open and send the request with the user's auth token
xhr.open("GET", url);
if (MetacatUI.appUserModel.get("loggedIn")) xhr.withCredentials = true;
//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
xhr.onload = function () {
if (this.status == 404) {
this.onerror.call(this);
return;
}
//Get the file name to save this file as
var filename = xhr.getResponseHeader("Content-Disposition");
if (!filename) {
filename =
model.get("fileName") ||
model.get("title") ||
model.get("id") ||
"download";
} else
filename = filename
.substring(filename.indexOf("filename=") + 9)
.replace(/"/g, "");
//Replace any whitespaces
filename = filename.trim().replace(/ /g, "_");
//For IE, we need to use the navigator API
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(xhr.response, filename);
}
//Other browsers can download it via a link
else {
var a = document.createElement("a");
a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
// Set the file name.
a.download = filename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}
model.trigger("downloadComplete");
// Track this event
MetacatUI.analytics?.trackEvent(
"download",
"Download DataONEObject",
model.get("id"),
);
};
xhr.onerror = function (e) {
model.trigger("downloadError");
// Track the error
MetacatUI.analytics?.trackException(
`Download DataONEObject error: ${e || ""}`,
model.get("id"),
true,
);
};
xhr.onprogress = function (e) {
if (e.lengthComputable) {
var percent = (e.loaded / e.total) * 100;
model.set("downloadPercent", percent);
}
};
xhr.responseType = "blob";
if (MetacatUI.appUserModel.get("loggedIn"))
xhr.setRequestHeader(
"Authorization",
"Bearer " + MetacatUI.appUserModel.get("token"),
);
xhr.send();
},
/**
* Creates a file name for this DataONEObject and updates the `fileName` attribute
*/
setMissingFileName: function () {
var objectFormats, filename, extension;
objectFormats = MetacatUI.objectFormats.where({
formatId: this.get("formatId"),
});
if (objectFormats.length > 0) {
extension = objectFormats[0].get("extension");
}
//Science metadata file names will use the title
if (this.get("type") == "Metadata") {
filename =
Array.isArray(this.get("title")) && this.get("title").length
? this.get("title")[0]
: this.get("id");
}
//Resource maps will use a "resource_map_" prefix
else if (this.get("type") == "DataPackage") {
filename = "resource_map_" + this.get("id");
extension = ".rdf.xml";
}
//All other object types will just use the id
else {
filename = this.get("id");
}
//Replace all non-alphanumeric characters with underscores
filename = filename.replace(/[^a-zA-Z0-9]/g, "_");
if (typeof extension !== "undefined") {
filename = filename + "." + extension;
}
this.set("fileName", filename);
},
/**
* Creates a URL for viewing more information about this object
* @return {string}
*/
createViewURL: function () {
return (
MetacatUI.root +
"/view/" +
encodeURIComponent(this.get("seriesId") || this.get("id"))
);
},
/**
* Check if the seriesID or PID matches a DOI regex, and if so, return
* a canonical IRI for the DOI.
* @return {string|null} - The canonical IRI for the DOI, or null if
* neither the seriesId nor the PID match a DOI regex.
* @since 2.26.0
*/
getCanonicalDOIIRI: function () {
const id = this.get("id");
const seriesId = this.get("seriesId");
let DOI = null;
if (this.isDOI(seriesId)) DOI = seriesId;
else if (this.isDOI(id)) DOI = id;
return MetacatUI.appModel.DOItoURL(DOI);
},
/**
* Converts the identifier string to a string safe to use in an XML id attribute
* @param {string} [id] - The ID string
* @return {string} - The XML-safe string
*/
getXMLSafeID: function (id) {
if (typeof id == "undefined") {
var id = this.get("id");
}
//Replace XML id attribute invalid characters and patterns in the identifier
id = id
.replace(/</g, "-")
.replace(/:/g, "-")
.replace(/&[a-zA-Z0-9]+;/g);
return id;
},
/**** Provenance-related functions ****/
/**
* Returns true if this provenance field points to a source of this data or metadata object
* @param {string} field
* @returns {boolean}
*/
isSourceField: function (field) {
if (typeof field == "undefined" || !field) return false;
// Is the field we are checking a prov field?
if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field))
return false;
if (
field == "prov_generatedByExecution" ||
field == "prov_generatedByProgram" ||
field == "prov_used" ||
field == "prov_wasDerivedFrom" ||
field == "prov_wasInformedBy"
)
return true;
else return false;
},
/**
* Returns true if this provenance field points to a derivation of this data or metadata object
* @param {string} field
* @returns {boolean}
*/
isDerivationField: function (field) {
if (typeof field == "undefined" || !field) return false;
if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field))
return false;
if (
field == "prov_usedByExecution" ||
field == "prov_usedByProgram" ||
field == "prov_hasDerivations" ||
field == "prov_generated"
)
return true;
else return false;
},
/**
* Returns a plain-english version of the general format - either image, program, metadata, PDF, annotation or data
*/
getType: function () {
//The list of formatIds that are images
//The list of formatIds that are images
var pdfIds = ["application/pdf"];
var annotationIds = [
"http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html",
];
// Type has already been set, use that.
if (this.get("type").toLowerCase() == "metadata") return "metadata";
//Determine the type via provONE
var instanceOfClass = this.get("prov_instanceOfClass");
if (
typeof instanceOfClass !== "undefined" &&
Array.isArray(instanceOfClass) &&
instanceOfClass.length
) {
var programClass = _.filter(instanceOfClass, function (className) {
return className.indexOf("#Program") > -1;
});
if (typeof programClass !== "undefined" && programClass.length)
return "program";
} else {
if (this.get("prov_generated").length || this.get("prov_used").length)
return "program";
}
//Determine the type via file format
if (this.isSoftware()) return "program";
if (this.isData()) return "data";
if (this.get("type").toLowerCase() == "metadata") return "metadata";
if (this.isImage()) return "image";
if (_.contains(pdfIds, this.get("formatId"))) return "PDF";
if (_.contains(annotationIds, this.get("formatId")))
return "annotation";
else return "data";
},
/**
* Checks the formatId of this model and determines if it is an image.
* @returns {boolean} true if this data object is an image, false if it is other
*/
isImage: function () {
//The list of formatIds that are images
var imageIds = ["image/gif", "image/jp2", "image/jpeg", "image/png"];
//Does this data object match one of these IDs?
if (_.indexOf(imageIds, this.get("formatId")) == -1) return false;
else return true;
},
/**
* Checks the formatId of this model and determines if it is a data file.
* This determination is mostly used for display and the provenance editor. In the
* DataONE API, many formatIds are considered `DATA` formatTypes, but they are categorized
* as images {@link DataONEObject#isImage} or software {@link DataONEObject#isSoftware}.
* @returns {boolean} true if this data object is a data file, false if it is other
*/
isData: function () {
var dataIds = [
"application/atom+xml",
"application/mathematica",
"application/msword",
"application/netcdf",
"application/octet-stream",
"application/pdf",
"application/postscript",
"application/rdf+xml",
"application/rtf",
"application/vnd.google-earth.kml+xml",
"application/vnd.ms-excel",
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/x-bzip2",
"application/x-fasta",
"application/x-gzip",
"application/x-rar-compressed",
"application/x-tar",
"application/xhtml+xml",
"application/xml",
"application/zip",
"audio/mpeg",
"audio/x-ms-wma",
"audio/x-wav",
"image/svg xml",
"image/svg+xml",
"image/bmp",
"image/tiff",
"text/anvl",
"text/csv",
"text/html",
"text/n3",
"text/plain",
"text/tab-separated-values",
"text/turtle",
"text/xml",
"video/avi",
"video/mp4",
"video/mpeg",
"video/quicktime",
"video/x-ms-wmv",
];
//Does this data object match one of these IDs?
if (_.indexOf(dataIds, this.get("formatId")) == -1) return false;
else return true;
},
/**
* Checks the formatId of this model and determines if it is a software file.
* This determination is mostly used for display and the provenance editor. In the
* DataONE API, many formatIds are considered `DATA` formatTypes, but they are categorized
* as images {@link DataONEObject#isImage} for display purposes.
* @returns {boolean} true if this data object is a software file, false if it is other
*/
isSoftware: function () {
//The list of formatIds that are programs
var softwareIds = [
"text/x-python",
"text/x-rsrc",
"text/x-matlab",
"text/x-sas",
"application/R",
"application/x-ipynb+json",
];
//Does this data object match one of these IDs?
if (_.indexOf(softwareIds, this.get("formatId")) == -1) return false;
else return true;
},
/**
* Checks the formatId of this model and determines if it a PDF.
* @returns {boolean} true if this data object is a pdf, false if it is other
*/
isPDF: function () {
//The list of formatIds that are images
var ids = ["application/pdf"];
//Does this data object match one of these IDs?
if (_.indexOf(ids, this.get("formatId")) == -1) return false;
else return true;
},
/**
* Set the DataONE ProvONE provenance class
* param className - the shortened form of the actual classname value. The
* shortname will be appened to the ProvONE namespace, for example,
* the className "program" will result in the final class name
* "http://purl.dataone.org/provone/2015/01/15/ontology#Program"
* see https://github.com/DataONEorg/sem-prov-ontologies/blob/master/provenance/ProvONE/v1/provone.html
* @param {string} className
*/
setProvClass: function (className) {
className = className.toLowerCase();
className = className.charAt(0).toUpperCase() + className.slice(1);
/* This function is intended to be used for the ProvONE classes that are
* typically represented in DataONEObjects: "Data", "Program", and hopefully
* someday "Execution", as we don't allow the user to set the namespace
* e.g. to "PROV", so therefor we check for the currently known ProvONE classes.
*/
if (
_.contains(
[
"Program",
"Data",
"Visualization",
"Document",
"Execution",
"User",
],
className,
)
) {
this.set("prov_instanceOfClass", [this.PROVONE + className]);
} else if (
_.contains(
["Entity", "Usage", "Generation", "Association"],
className,
)
) {
this.set("prov_instanceOfClass", [this.PROV + className]);
} else {
message =
"The given class name: " +
className +
" is not in the known ProvONE or PROV classes.";
throw new Error(message);
}
},
/**
* Calculate a checksum for the object
* @param {string} [algorithm] The algorithm to use, defaults to MD5
* @return {string} A checksum plain JS object with value and algorithm attributes
*/
calculateChecksum: function (algorithm) {
var algorithm = algorithm || "MD5";
var checksum = { algorithm: undefined, value: undefined };
var hash; // The checksum hash
var file; // The file to be read by slicing
var reader; // The FileReader used to read each slice
var offset = 0; // Byte offset for reading slices
var sliceSize = Math.pow(2, 20); // 1MB slices
var model = this;
// Do we have a file?
if (this.get("uploadFile") instanceof Blob) {
file = this.get("uploadFile");
reader = new FileReader();
/* Handle load errors */
reader.onerror = function (event) {
console.log("Error reading: " + event);
};
/* Show progress */
reader.onprogress = function (event) {};
/* Handle load finish */
reader.onloadend = function (event) {
if (event.target.readyState == FileReader.DONE) {
hash.update(event.target.result);
}
offset += sliceSize;
if (_seek()) {
model.set("checksum", hash.hex());
model.set("checksumAlgorithm", checksum.algorithm);
model.trigger("checksumCalculated", model.attributes);
}
};
} else {
message = "The given object is not a blob or a file object.";
throw new Error(message);
}
switch (algorithm) {
case "MD5":
checksum.algorithm = algorithm;
hash = md5.create();
_seek();
break;
case "SHA-1":
// TODO: Support SHA-1
// break;
default:
message =
"The given algorithm: " + algorithm + " is not supported.";
throw new Error(message);
}
/*
* A helper function internal to calculateChecksum() used to slice
* the file at the next offset by slice size
*/
function _seek() {
var calculated = false;
var slice;
// Digest the checksum when we're done calculating
if (offset >= file.size) {
hash.digest();
calculated = true;
return calculated;
}
// slice the file and read the slice
slice = file.slice(offset, offset + sliceSize);
reader.readAsArrayBuffer(slice);
return calculated;
}
},
/**
* Checks if the pid or sid or given string is a DOI
*
* @param {string} customString - Optional. An identifier string to check instead of the id and seriesId attributes on the model
* @returns {boolean} True if it is a DOI
*/
isDOI: function (customString) {
return (
isDOI(customString) ||
isDOI(this.get("id")) ||
isDOI(this.get("seriesId"))
);
},
/**
* Creates an array of objects that represent Member Nodes that could possibly be this
* object's authoritative MN. This function updates the `possibleAuthMNs` attribute on this model.
*/
setPossibleAuthMNs: function () {
//Only do this for Coordinating Node MetacatUIs.
if (MetacatUI.appModel.get("alternateRepositories").length) {
//Set the possibleAuthMNs attribute
var possibleAuthMNs = [];
//If a datasource is already found for this Portal, move that to the top of the list of auth MNs
var datasource = this.get("datasource") || "";
if (datasource) {
//Find the MN object that matches the datasource node ID
var datasourceMN = _.findWhere(
MetacatUI.appModel.get("alternateRepositories"),
{ identifier: datasource },
);
if (datasourceMN) {
//Clone the MN object and add it to the array
var clonedDatasourceMN = Object.assign({}, datasourceMN);
possibleAuthMNs.push(clonedDatasourceMN);
}
}
//If there is an active alternate repo, move that to the top of the list of auth MNs
var activeAltRepo =
MetacatUI.appModel.get("activeAlternateRepositoryId") || "";
if (activeAltRepo) {
var activeAltRepoMN = _.findWhere(
MetacatUI.appModel.get("alternateRepositories"),
{ identifier: activeAltRepo },
);
if (activeAltRepoMN) {
//Clone the MN object and add it to the array
var clonedActiveAltRepoMN = Object.assign({}, activeAltRepoMN);
possibleAuthMNs.push(clonedActiveAltRepoMN);
}
}
//Add all the other alternate repositories to the list of auth MNs
var otherPossibleAuthMNs = _.reject(
MetacatUI.appModel.get("alternateRepositories"),
function (mn) {
return (
mn.identifier == datasource || mn.identifier == activeAltRepo
);
},
);
//Clone each MN object and add to the array
_.each(otherPossibleAuthMNs, function (mn) {
var clonedMN = Object.assign({}, mn);
possibleAuthMNs.push(clonedMN);
});
//Update this model
this.set("possibleAuthMNs", possibleAuthMNs);
}
},
/**
* Removes white space from string values returned by Solr when the white space causes issues.
* For now this only effects the `resourceMap` field, which will index new line characters and spaces
* when the RDF XML has those in the `identifier` XML element content. This was causing bugs where DataONEObject
* models were created with `id`s with new line and white space characters (e.g. `\n urn:uuid:1234...`)
* @param {object} json - The Solr document as a JS Object, which will be directly altered
*/
removeWhiteSpaceFromSolrFields: function (json) {
if (typeof json.resourceMap == "string") {
json.resourceMap = json.resourceMap.trim();
} else if (Array.isArray(json.resourceMap)) {
let newResourceMapIds = [];
_.each(json.resourceMap, function (rMapId) {
if (typeof rMapId == "string") {
newResourceMapIds.push(rMapId.trim());
}
});
json.resourceMap = newResourceMapIds;
}
},
},
/** @lends DataONEObject.prototype */
{
/**
* Generate a unique identifier to be used as an XML id attribute
* @returns {string} The identifier string that was generated
*/
generateId: function () {
var idStr = ""; // the id to return
var length = 30; // the length of the generated string
var chars =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz".split(
"",
);
for (var i = 0; i < length; i++) {
idStr += chars[Math.floor(Math.random() * chars.length)];
}
return idStr;
},
},
);
return DataONEObject;
});

Copy link
Contributor

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.

var DataONEObject = Backbone.Model.extend(
/** @lends DataONEObject.prototype */ {
type: "DataONEObject",
selectedInEditor: false, // Has this package member been selected and displayed in the provenance editor?
PROV: "http://www.w3.org/ns/prov#",
PROVONE: "http://purl.dataone.org/provone/2015/01/15/ontology#",
defaults: function () {
return {
// System Metadata attributes
serialVersion: null,
identifier: null,
formatId: null,
size: null,
checksum: null,
originalChecksum: null,
checksumAlgorithm: "MD5",
submitter: null,
rightsHolder: null,
accessPolicy: [], //An array of accessPolicy literal JS objects
replicationAllowed: null,
replicationPolicy: [],
obsoletes: null,
obsoletedBy: null,
archived: null,
dateUploaded: null,
dateSysMetadataModified: null,
originMemberNode: null,
authoritativeMemberNode: null,
replica: [],
seriesId: null, // uuid.v4(), (decide if we want to auto-set this)
mediaType: null,
fileName: null,
// Non-system metadata attributes:
isNew: null,
datasource: null,
insert_count_i: null,
read_count_i: null,
changePermission: null,
writePermission: null,
readPermission: null,
isPublic: null,
dateModified: null,
id: "urn:uuid:" + uuid.v4(),
sizeStr: null,
type: "", // Data, Metadata, or DataPackage
formatType: "",
metadataEntity: null, // A model that represents the metadata for this file, e.g. an EMLEntity model
latestVersion: null,
isDocumentedBy: null,
documents: [],
members: [],
resourceMap: [],
nodeLevel: 0, // Indicates hierarchy level in the view for indentation
sortOrder: 2, // Metadata: 1, Data: 2, DataPackage: 3
synced: false, // True if the full model has been synced
uploadStatus: null, //c=complete, p=in progress, q=queued, e=error, w=warning, no upload status=not in queue
uploadProgress: null,
sysMetaUploadStatus: null, //c=complete, p=in progress, q=queued, e=error, l=loading, no upload status=not in queue
percentLoaded: 0, // Percent the file is read before caclculating the md5 sum
uploadFile: null, // The file reference to be uploaded (JS object: File)
errorMessage: null,
sysMetaErrorCode: null, // The status code given when there is an error updating the system metadata
numSaveAttempts: 0,
notFound: false, //Whether or not this object was found in the system
originalAttrs: [], // An array of original attributes in a DataONEObject
changed: false, // If any attributes have been changed, including attrs in nested objects
hasContentChanges: false, // If attributes outside of originalAttrs have been changed
sysMetaXML: null, // A cached original version of the fetched system metadata document
objectXML: null, // A cached version of the object fetched from the server
isAuthorized: null, // If the stated permission is authorized by the user
isAuthorized_read: null, //If the user has permission to read
isAuthorized_write: null, //If the user has permission to write
isAuthorized_changePermission: null, //If the user has permission to changePermission
createSeriesId: false, //If true, a seriesId will be created when this object is saved.
collections: [], //References to collections that this model is in
possibleAuthMNs: [], //A list of possible authoritative MNs of this object
useAltRepo: false,
isLoadingFiles: false, //Only relevant to Resource Map objects. Is true if there is at least one file still loading into the package.
numLoadingFiles: 0, //Only relevant to Resource Map objects. The number of files still loading into the package.
provSources: [],
provDerivations: [],
prov_generated: [],
prov_generatedByExecution: [],
prov_generatedByProgram: [],
prov_generatedByUser: [],
prov_hasDerivations: [],
prov_hasSources: [],
prov_instanceOfClass: [],
prov_used: [],
prov_usedByExecution: [],
prov_usedByProgram: [],
prov_usedByUser: [],
prov_wasDerivedFrom: [],
prov_wasExecutedByExecution: [],
prov_wasExecutedByUser: [],
prov_wasInformedBy: [],
};
},
initialize: function (attrs, options) {
if (typeof attrs == "undefined") var attrs = {};
this.set("accessPolicy", this.createAccessPolicy());
var model = this;
this.on("change:size", function () {
var size = Utilities.bytesToSize(model.get("size"));
model.set("sizeStr", size);
});
if (attrs.size) {
var size = Utilities.bytesToSize(model.get("size"));
model.set("sizeStr", size);
}
// Cache an array of original attribute names to help in handleChange()
if (this.type == "DataONEObject")
this.set("originalAttrs", Object.keys(this.attributes));
else
this.set(
"originalAttrs",
Object.keys(DataONEObject.prototype.defaults()),
);
this.on("successSaving", this.updateRelationships);
//Save a reference to this DataONEObject model in the metadataEntity model
//whenever the metadataEntity is set
this.on("change:metadataEntity", function () {
var entityMetadataModel = this.get("metadataEntity");
if (entityMetadataModel)
entityMetadataModel.set("dataONEObject", this);
});
this.on("sync", function () {
this.set("synced", true);
});
//Find Member Node object that might be the authoritative MN
//This is helpful when MetacatUI may be displaying content from multiple MNs
this.setPossibleAuthMNs();
},
/**
* Maps the lower-case sys meta node names (valid in HTML DOM) to the
* camel-cased sys meta node names (valid in DataONE).
* Used during parse() and serialize()
*/
nodeNameMap: function () {
return {
accesspolicy: "accessPolicy",
accessrule: "accessRule",
authoritativemembernode: "authoritativeMemberNode",
checksumalgorithm: "checksumAlgorithm",
dateuploaded: "dateUploaded",
datesysmetadatamodified: "dateSysMetadataModified",
formatid: "formatId",
filename: "fileName",
nodereference: "nodeReference",
numberreplicas: "numberReplicas",
obsoletedby: "obsoletedBy",
originmembernode: "originMemberNode",
replicamembernode: "replicaMemberNode",
replicationallowed: "replicationAllowed",
replicationpolicy: "replicationPolicy",
replicationstatus: "replicationStatus",
replicaverified: "replicaVerified",
rightsholder: "rightsHolder",
serialversion: "serialVersion",
seriesid: "seriesId",
};
},
/**
* Returns the URL string where this DataONEObject can be fetched from or saved to
* @returns {string}
*/
url: function () {
// With no id, we can't do anything
if (!this.get("id") && !this.get("seriesid")) return "";
//Get the active alternative repository, if one is configured
var activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
//Start the base URL string
var baseUrl = "";
// Determine if we're updating a new/existing object,
// or just its system metadata
// New uploads use the object service URL
if (this.isNew()) {
//Use the object service URL from the alt repo
if (this.get("useAltRepo") && activeAltRepo) {
baseUrl = activeAltRepo.objectServiceUrl;
}
//If this MetacatUI deployment is pointing to a MN, use the object service URL from the AppModel
else {
baseUrl = MetacatUI.appModel.get("objectServiceUrl");
}
//Return the full URL
return baseUrl;
} else {
if (this.hasUpdates()) {
if (this.get("hasContentChanges")) {
//Use the object service URL from the alt repo
if (this.get("useAltRepo") && activeAltRepo) {
baseUrl = activeAltRepo.objectServiceUrl;
} else {
baseUrl = MetacatUI.appModel.get("objectServiceUrl");
}
// Exists on the server, use MN.update()
return baseUrl + encodeURIComponent(this.get("oldPid"));
} else {
//Use the meta service URL from the alt repo
if (this.get("useAltRepo") && activeAltRepo) {
baseUrl = activeAltRepo.metaServiceUrl;
} else {
baseUrl = MetacatUI.appModel.get("metaServiceUrl");
}
// Exists on the server, use MN.updateSystemMetadata()
return baseUrl + encodeURIComponent(this.get("id"));
}
} else {
//Use the meta service URL from the alt repo
if (this.get("useAltRepo") && activeAltRepo) {
baseUrl = activeAltRepo.metaServiceUrl;
} else {
baseUrl = MetacatUI.appModel.get("metaServiceUrl");
}
// Use MN.getSystemMetadata()
return (
baseUrl +
(encodeURIComponent(this.get("id")) ||
encodeURIComponent(this.get("seriesid")))
);
}
}
},
/**
* Create the URL string that is used to download this package
* @returns PackageURL string for this DataONE Object
* @since 2.28.0
*/
getPackageURL: function () {
var url = null;
// With no id, we can't do anything
if (!this.get("id") && !this.get("seriesid")) return url;
//If we haven't set a packageServiceURL upon app initialization and we are querying a CN, then the packageServiceURL is dependent on the MN this package is from
if (
MetacatUI.appModel.get("d1Service").toLowerCase().indexOf("cn/") >
-1 &&
MetacatUI.nodeModel.get("members").length
) {
var source = this.get("datasource"),
node = _.find(MetacatUI.nodeModel.get("members"), {
identifier: source,
});
//If this node has MNRead v2 services...
if (node && node.readv2)
url =
node.baseURL +
"/v2/packages/application%2Fbagit-097/" +
encodeURIComponent(this.get("id"));
} else if (MetacatUI.appModel.get("packageServiceUrl"))
url =
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(this.get("id"));
return url;
},
/**
* Overload Backbone.Model.fetch, so that we can set custom options for each fetch() request
*/
fetch: function (options) {
if (!options) var options = {};
else var options = _.clone(options);
options.url = this.url();
//If we are using the Solr service to retrieve info about this object, then construct a query
if (typeof options != "undefined" && options.solrService) {
//Get basic information
var query = "";
//Do not search for seriesId when it is not configured in this model/app
if (typeof this.get("seriesid") === "undefined")
query += 'id:"' + encodeURIComponent(this.get("id")) + '"';
//If there is no seriesid set, then search for pid or sid
else if (!this.get("seriesid"))
query +=
'(id:"' +
encodeURIComponent(this.get("id")) +
'" OR seriesId:"' +
encodeURIComponent(this.get("id")) +
'")';
//If a seriesId is specified, then search for that
else if (this.get("seriesid") && this.get("id").length > 0)
query +=
'(seriesId:"' +
encodeURIComponent(this.get("seriesid")) +
'" AND id:"' +
encodeURIComponent(this.get("id")) +
'")';
//If only a seriesId is specified, then just search for the most recent version
else if (this.get("seriesid") && !this.get("id"))
query +=
'seriesId:"' +
encodeURIComponent(this.get("id")) +
'" -obsoletedBy:*';
//The fields to return
var fl = "formatId,formatType,documents,isDocumentedBy,id,seriesId";
//Use the Solr query URL
var solrOptions = {
url:
MetacatUI.appModel.get("queryServiceUrl") +
"q=" +
query +
"&fl=" +
fl +
"&wt=json",
};
//Merge with the options passed to this function
var fetchOptions = _.extend(options, solrOptions);
} else if (typeof options != "undefined") {
//Use custom options for retreiving XML
//Merge with the options passed to this function
var fetchOptions = _.extend(
{
dataType: "text",
},
options,
);
} else {
//Use custom options for retreiving XML
var fetchOptions = _.extend({
dataType: "text",
});
}
//Add the authorization options
fetchOptions = _.extend(
fetchOptions,
MetacatUI.appUserModel.createAjaxSettings(),
);
//Call Backbone.Model.fetch to retrieve the info
return Backbone.Model.prototype.fetch.call(this, fetchOptions);
},
/**
* This function is called by Backbone.Model.fetch.
* It deserializes the incoming XML from the /meta REST endpoint and converts it into JSON.
*/
parse: function (response) {
// If the response is XML
if (typeof response == "string" && response.indexOf("<") == 0) {
var responseDoc = $.parseHTML(response),
systemMetadata;
//Save the raw XML in case it needs to be used later
this.set("sysMetaXML", response);
//Find the XML node for the system metadata
for (var i = 0; i < responseDoc.length; i++) {
if (
responseDoc[i].nodeType == 1 &&
responseDoc[i].localName.indexOf("systemmetadata") > -1
) {
systemMetadata = responseDoc[i];
break;
}
}
//Parse the XML to JSON
var sysMetaValues = this.toJson(systemMetadata);
//Convert the JSON to a camel-cased version, which matches Solr and is easier to work with in code
_.each(
Object.keys(sysMetaValues),
function (key) {
var camelCasedKey = this.nodeNameMap()[key];
if (camelCasedKey) {
sysMetaValues[camelCasedKey] = sysMetaValues[key];
delete sysMetaValues[key];
}
},
this,
);
//Save the checksum from the system metadata in a separate attribute on the model
sysMetaValues.originalChecksum = sysMetaValues.checksum;
sysMetaValues.checksum = this.defaults().checksum;
//Save the identifier as the id attribute
sysMetaValues.id = sysMetaValues.identifier;
//Parse the Access Policy
if (
this.get("accessPolicy") &&
AccessPolicy.prototype.isPrototypeOf(this.get("accessPolicy"))
) {
this.get("accessPolicy").parse(
$(systemMetadata).find("accesspolicy"),
);
sysMetaValues.accessPolicy = this.get("accessPolicy");
} else {
//Create a new AccessPolicy collection, if there isn't one already.
sysMetaValues.accessPolicy = this.createAccessPolicy(
$(systemMetadata).find("accesspolicy"),
);
}
return sysMetaValues;
// If the response is a list of Solr docs
} else if (
typeof response === "object" &&
response.response &&
response.response.docs
) {
//If no objects were found in the index, mark as notFound and exit
if (!response.response.docs.length) {
this.set("notFound", true);
this.trigger("notFound");
return;
}
//Get the Solr document (there should be only one)
var doc = response.response.docs[0];
//Take out any empty values
_.each(Object.keys(doc), function (field) {
if (!doc[field] && doc[field] !== 0) delete doc[field];
});
//Remove any erroneous white space from fields
this.removeWhiteSpaceFromSolrFields(doc);
return doc;
}
// Default to returning the raw response
else return response;
},
/** A utility function for converting XML to JSON */
toJson: function (xml) {
// Create the return object
var obj = {};
// do children
if (xml.hasChildNodes()) {
for (var i = 0; i < xml.childNodes.length; i++) {
var item = xml.childNodes.item(i);
//If it's an empty text node, skip it
if (item.nodeType == 3 && !item.nodeValue.trim()) continue;
//Get the node name
var nodeName = item.localName;
//If it's a new container node, convert it to JSON and add as a new object attribute
if (typeof obj[nodeName] == "undefined" && item.nodeType == 1) {
obj[nodeName] = this.toJson(item);
}
//If it's a new text node, just store the text value and add as a new object attribute
else if (
typeof obj[nodeName] == "undefined" &&
item.nodeType == 3
) {
obj =
item.nodeValue == "false"
? false
: item.nodeValue == "true"
? true
: item.nodeValue;
}
//If this node name is already stored as an object attribute...
else if (typeof obj[nodeName] != "undefined") {
//Cache what we have now
var old = obj[nodeName];
if (!Array.isArray(old)) old = [old];
//Create a new object to store this node info
var newNode = {};
//Add the new node info to the existing array we have now
if (item.nodeType == 1) {
newNode = this.toJson(item);
var newArray = old.concat(newNode);
} else if (item.nodeType == 3) {
newNode = item.nodeValue;
var newArray = old.concat(newNode);
}
//Store the attributes for this node
_.each(item.attributes, function (attr) {
newNode[attr.localName] = attr.nodeValue;
});
//Replace the old array with the updated one
obj[nodeName] = newArray;
//Exit
continue;
}
//Store the attributes for this node
/*_.each(item.attributes, function(attr){
obj[nodeName][attr.localName] = attr.nodeValue;
});*/
}
}
return obj;
},
/**
Serialize the DataONE object JSON to XML
@param {object} json - the JSON object to convert to XML
@param {Element} containerNode - an HTML element to insertt the resulting XML into
@returns {Element} The updated HTML Element
*/
toXML: function (json, containerNode) {
if (typeof json == "string") {
containerNode.textContent = json;
return containerNode;
}
for (var i = 0; i < Object.keys(json).length; i++) {
var key = Object.keys(json)[i],
contents = json[key] || json[key];
var node = document.createElement(key);
//Skip this attribute if it is not populated
if (!contents || (Array.isArray(contents) && !contents.length))
continue;
//If it's a simple text node
if (typeof contents == "string") {
containerNode.textContent = contents;
return containerNode;
} else if (Array.isArray(contents)) {
var allNewNodes = [];
for (var ii = 0; ii < contents.length; ii++) {
allNewNodes.push(this.toXML(contents[ii], $(node).clone()[0]));
}
if (allNewNodes.length) node = allNewNodes;
} else if (typeof contents == "object") {
$(node).append(this.toXML(contents, node));
var attributeNames = _.without(Object.keys(json[key]), "content");
}
$(containerNode).append(node);
}
return containerNode;
},
/**
* Saves the DataONEObject System Metadata to the server
*/
save: function (attributes, options) {
// Set missing file names before saving
if (!this.get("fileName")) {
this.setMissingFileName();
} else {
//Replace all non-alphanumeric characters with underscores
var fileNameWithoutExt = this.get("fileName").substring(
0,
this.get("fileName").lastIndexOf("."),
),
extension = this.get("fileName").substring(
this.get("fileName").lastIndexOf("."),
this.get("fileName").length,
);
this.set(
"fileName",
fileNameWithoutExt.replace(/[^a-zA-Z0-9]/g, "_") + extension,
);
}
if (!this.hasUpdates()) {
this.set("uploadStatus", null);
return;
}
//Set the upload transfer as in progress
this.set("uploadProgress", 2);
this.set("uploadStatus", "p");
//Check if the checksum has been calculated yet.
if (!this.get("checksum")) {
//When it is calculated, restart this function
this.on("checksumCalculated", this.save);
//Calculate the checksum for this file
this.calculateChecksum();
//Exit this function until the checksum is done
return;
}
//Create a FormData object to send data with our XHR
var formData = new FormData();
//If this is not a new object, update the id. New DataONEObjects will have an id
// created during initialize.
if (!this.isNew()) {
this.updateID();
formData.append("pid", this.get("oldPid"));
formData.append("newPid", this.get("id"));
} else {
//Create an ID if there isn't one
if (!this.get("id")) {
this.set("id", "urn:uuid:" + uuid.v4());
}
//Add the identifier to the XHR data
formData.append("pid", this.get("id"));
}
//Create the system metadata XML
var sysMetaXML = this.serializeSysMeta();
//Send the system metadata as a Blob
var xmlBlob = new Blob([sysMetaXML], { type: "application/xml" });
//Add the system metadata XML to the XHR data
formData.append("sysmeta", xmlBlob, "sysmeta.xml");
// Create the new object (MN.create())
formData.append("object", this.get("uploadFile"), this.get("fileName"));
var model = this;
// On create(), add to the package and the metadata
// Note: This should be added to the parent collection
// but for now we are using the root collection
_.each(
this.get("collections"),
function (collection) {
if (collection.type == "DataPackage") {
this.off("successSaving", collection.addNewModel);
this.once("successSaving", collection.addNewModel, collection);
}
},
this,
);
//Put together the AJAX and Backbone.save() options
var requestSettings = {
url: this.url(),
cache: false,
contentType: false,
dataType: "text",
processData: false,
data: formData,
parse: false,
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: this.onSuccessfulSave,
error: function (model, response, xhr) {
//Reset the identifier changes
model.resetID();
//Reset the checksum, if this is a model that needs to be serialized with each save.
if (model.serialize) {
model.set("checksum", model.defaults().checksum);
}
model.set("numSaveAttempts", model.get("numSaveAttempts") + 1);
var numSaveAttempts = model.get("numSaveAttempts");
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);
var parsedResponse = $(response.responseText)
.not("style, title")
.text();
//When there is no network connection (status == 0), there will be no response text
if (!parsedResponse)
parsedResponse =
"There was a network issue that prevented this file from uploading. " +
"Make sure you are connected to a reliable internet connection.";
model.set("errorMessage", parsedResponse);
//Set the model status as e for error
model.set("uploadStatus", "e");
//Trigger a custom event for the model save error
model.trigger("errorSaving", parsedResponse);
// Track this error in our analytics
MetacatUI.analytics?.trackException(
`DataONEObject save error: ${parsedResponse}`,
model.get("id"),
true,
);
}
},
};
//Add the user settings
requestSettings = _.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
);
//Send the Save request
Backbone.Model.prototype.save.call(this, null, requestSettings);
},
/**
* This function is executed when the XHR that saves this DataONEObject has
* successfully completed. It can be called directly if a DataONEObject is saved
* without directly using the DataONEObject.save() function.
* @param {DataONEObject} [model] A reference to this DataONEObject model
* @param {XMLHttpRequest.response} [response] The XHR response object
* @param {XMLHttpRequest} [xhr] The XHR that was just completed successfully
*/
onSuccessfulSave: function (model, response, xhr) {
if (typeof model == "undefined") {
var model = this;
}
model.set("numSaveAttempts", 0);
model.set("uploadStatus", "c");
model.set("isNew", false);
model.trigger("successSaving", model);
// Get the newest sysmeta set by the MN
model.fetch({
merge: true,
systemMetadataOnly: true,
});
// Reset the content changes status
model.set("hasContentChanges", false);
//Reset the model isNew attribute
model.set("isNew", false);
// Reset oldPid so we can replace again
model.set("oldPid", null);
//Set the last-calculated checksum as the original checksum
model.set("originalChecksum", model.get("checksum"));
model.set("checksum", model.defaults().checksum);
},
/**
* Updates the DataONEObject System Metadata to the server
*/
updateSysMeta: function () {
//Update the upload status to "p" for "in progress"
this.set("uploadStatus", "p");
//Update the system metadata upload status to "p" as well, so the app
// knows that the system metadata, specifically, is being updated.
this.set("sysMetaUploadStatus", "p");
var formData = new FormData();
//Add the identifier to the XHR data
formData.append("pid", this.get("id"));
var sysMetaXML = this.serializeSysMeta();
//Send the system metadata as a Blob
var xmlBlob = new Blob([sysMetaXML], { type: "application/xml" });
//Add the system metadata XML to the XHR data
formData.append("sysmeta", xmlBlob, "sysmeta.xml");
var model = this;
var baseUrl = "",
activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
//Use the meta service URL from the alt repo
if (activeAltRepo) {
baseUrl = activeAltRepo.metaServiceUrl;
}
//If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel
else {
baseUrl = MetacatUI.appModel.get("metaServiceUrl");
}
var requestSettings = {
url: baseUrl + encodeURIComponent(this.get("id")),
cache: false,
contentType: false,
dataType: "text",
type: "PUT",
processData: false,
data: formData,
parse: false,
success: function () {
model.set("numSaveAttempts", 0);
//Fetch the system metadata from the server so we have a fresh copy of the newest sys meta.
model.fetch({ systemMetadataOnly: true });
model.set("sysMetaErrorCode", null);
//Update the upload status to "c" for "complete"
model.set("uploadStatus", "c");
model.set("sysMetaUploadStatus", "c");
//Trigger a custom event that the sys meta was updated
model.trigger("sysMetaUpdated");
},
error: function (xhr, status, statusCode) {
model.set("numSaveAttempts", model.get("numSaveAttempts") + 1);
var numSaveAttempts = model.get("numSaveAttempts");
if (numSaveAttempts < 3 && (statusCode == 408 || statusCode == 0)) {
//Try saving again in 10, 40, and 90 seconds
setTimeout(
function () {
model.updateSysMeta.call(model);
},
numSaveAttempts * numSaveAttempts * 10000,
);
} else {
model.set("numSaveAttempts", 0);
var parsedResponse = $(xhr.responseText)
.not("style, title")
.text();
//When there is no network connection (status == 0), there will be no response text
if (!parsedResponse)
parsedResponse =
"There was a network issue that prevented this file from updating. " +
"Make sure you are connected to a reliable internet connection.";
model.set("errorMessage", parsedResponse);
model.set("sysMetaErrorCode", statusCode);
model.set("uploadStatus", "e");
model.set("sysMetaUploadStatus", "e");
// Trigger a custom event for the sysmeta update that
// errored
model.trigger("sysMetaUpdateError");
// Track this error in our analytics
MetacatUI.analytics?.trackException(
`DataONEObject update system metadata ` +
`error: ${parsedResponse}`,
model.get("id"),
true,
);
}
},
};
//Add the user settings
requestSettings = _.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
);
//Send the XHR
$.ajax(requestSettings);
},
/**
* Check if the current user is authorized to perform an action on this object. This function doesn't return
* the result of the check, but it sends an XHR, updates this model, and triggers a change event.
* @param {string} [action=changePermission] - The action (read, write, or changePermission) to check
* if the current user has authorization to perform. By default checks for the highest level of permission.
* @param {object} [options] Additional options for this function. See the properties below.
* @property {function} options.onSuccess - A function to execute when the checkAuthority API is successfully completed
* @property {function} options.onError - A function to execute when the checkAuthority API returns an error, or when no PID or SID can be found for this object.
* @return {boolean}
*/
checkAuthority: function (action = "changePermission", options) {
try {
// return false - if neither PID nor SID is present to check the authority
if (this.get("id") == null && this.get("seriesId") == null) {
return false;
}
if (typeof options == "undefined") {
var options = {};
}
// If onError or onSuccess options were provided by the user,
// check that they are functions first, so we don't try to use
// some other type of variable as a function later on.
["onError", "onSuccess"].forEach(function (userFunction) {
if (typeof options[userFunction] !== "function") {
options[userFunction] = null;
}
});
// If PID is not present - check authority with seriesId
var identifier = this.get("id");
if (identifier == null) {
identifier = this.get("seriesId");
}
//If there are alt repositories configured, find the possible authoritative
// Member Node for this DataONEObject.
if (MetacatUI.appModel.get("alternateRepositories").length) {
//Get the array of possible authoritative MNs
var possibleAuthMNs = this.get("possibleAuthMNs");
//If there are no possible authoritative MNs, use the auth service URL from the AppModel
if (!possibleAuthMNs.length) {
baseUrl = MetacatUI.appModel.get("authServiceUrl");
} else {
//Use the auth service URL from the top possible auth MN
baseUrl = possibleAuthMNs[0].authServiceUrl;
}
} else {
//Get the auth service URL from the AppModel
baseUrl = MetacatUI.appModel.get("authServiceUrl");
}
if (!baseUrl) {
return false;
}
var onSuccess =
options.onSuccess ||
function (data, textStatus, xhr) {
model.set("isAuthorized_" + action, true);
model.set("isAuthorized", true);
model.trigger("change:isAuthorized");
},
onError =
options.onError ||
function (xhr, textStatus, errorThrown) {
if (errorThrown == 404) {
var possibleAuthMNs = model.get("possibleAuthMNs");
if (possibleAuthMNs.length) {
//Remove the first MN from the array, since it didn't contain the object, so it's not the auth MN
possibleAuthMNs.shift();
}
//If there are no other possible auth MNs to check, trigger this model as Not Found.
if (possibleAuthMNs.length == 0 || !possibleAuthMNs) {
model.set("notFound", true);
model.trigger("notFound");
}
//If there's more MNs to check, try again
else {
model.checkAuthority(action, options);
}
} else {
model.set("isAuthorized_" + action, false);
model.set("isAuthorized", false);
}
};
var model = this;
var requestSettings = {
url: baseUrl + encodeURIComponent(identifier) + "?action=" + action,
type: "GET",
success: onSuccess,
error: onError,
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
} catch (e) {
//Log an error to the console
console.error("Couldn't check the authority for this user: ", e);
// Track this error in our analytics
const name = MetacatUI.appModel.get("username");
MetacatUI.analytics?.trackException(
`Couldn't check the authority for the user ${name}: ${e}`,
this.get("id"),
true,
);
//Set the user as unauthorized
model.set("isAuthorized_" + action, false);
model.set("isAuthorized", false);
return false;
}
},
/**
* Using the attributes set on this DataONEObject model, serializes the system metadata XML
* @returns {string}
*/
serializeSysMeta: function () {
//Get the system metadata XML that currently exists in the system
var sysMetaXML = this.get("sysMetaXML"), // sysmeta as string
xml, // sysmeta as DOM object
accessPolicyXML, // The generated access policy XML
previousSiblingNode, // A DOM node indicating any previous sibling
rightsHolderNode, // A DOM node for the rights holder field
accessPolicyNode, // A DOM node for the access policy
replicationPolicyNode, // A DOM node for the replication policy
obsoletesNode, // A DOM node for the obsoletes field
obsoletedByNode, // A DOM node for the obsoletedBy field
fileNameNode, // A DOM node for the file name
xmlString, // The system metadata document as a string
nodeNameMap, // The map of camelCase to lowercase attributes
extension; // the file name extension for this object
if (typeof sysMetaXML === "undefined" || sysMetaXML === null) {
xml = this.createSysMeta();
} else {
xml = $($.parseHTML(sysMetaXML));
}
//Update the system metadata values
xml.find("serialversion").text(this.get("serialVersion") || "0");
xml.find("identifier").text(this.get("newPid") || this.get("id"));
xml
.find("submitter")
.text(
this.get("submitter") || MetacatUI.appUserModel.get("username"),
);
xml.find("formatid").text(this.get("formatId") || this.getFormatId());
//If there is a seriesId, add it
if (this.get("seriesId")) {
//Get the seriesId XML node
var seriesIdNode = xml.find("seriesId");
//If it doesn't exist, create one
if (!seriesIdNode.length) {
seriesIdNode = $(document.createElement("seriesid"));
xml.find("identifier").before(seriesIdNode);
}
//Add the seriesId string to the XML node
seriesIdNode.text(this.get("seriesId"));
}
//If there is no size, get it
if (!this.get("size") && this.get("uploadFile")) {
this.set("size", this.get("uploadFile").size);
}
//Get the size of the file, if there is one
if (this.get("uploadFile")) {
xml.find("size").text(this.get("uploadFile").size);
}
//Otherwise, use the last known size
else {
xml.find("size").text(this.get("size"));
}
//Save the original checksum
if (!this.get("checksum") && this.get("originalChecksum")) {
xml.find("checksum").text(this.get("originalChecksum"));
}
//Update the checksum and checksum algorithm
else {
xml.find("checksum").text(this.get("checksum"));
xml.find("checksum").attr("algorithm", this.get("checksumAlgorithm"));
}
//Update the rightsholder
xml
.find("rightsholder")
.text(
this.get("rightsHolder") || MetacatUI.appUserModel.get("username"),
);
//Write the access policy
accessPolicyXML = this.get("accessPolicy").serialize();
// Get the access policy node, if it exists
accessPolicyNode = xml.find("accesspolicy");
previousSiblingNode = xml.find("rightsholder");
// Create an access policy node if needed
if (!accessPolicyNode.length && accessPolicyXML) {
accessPolicyNode = $(document.createElement("accesspolicy"));
previousSiblingNode.after(accessPolicyNode);
}
//Replace the old access policy with the new one if it exists
if (accessPolicyXML) {
accessPolicyNode.replaceWith(accessPolicyXML);
} else {
// Remove the node if it is empty
accessPolicyNode.remove();
}
// Set the obsoletes node after replPolicy or accessPolicy, or rightsHolder
replicationPolicyNode = xml.find("replicationpolicy");
accessPolicyNode = xml.find("accesspolicy");
rightsHolderNode = xml.find("rightsholder");
if (replicationPolicyNode.length) {
previousSiblingNode = replicationPolicyNode;
} else if (accessPolicyNode.length) {
previousSiblingNode = accessPolicyNode;
} else {
previousSiblingNode = rightsHolderNode;
}
obsoletesNode = xml.find("obsoletes");
if (this.get("obsoletes")) {
if (obsoletesNode.length) {
obsoletesNode.text(this.get("obsoletes"));
} else {
obsoletesNode = $(document.createElement("obsoletes")).text(
this.get("obsoletes"),
);
previousSiblingNode.after(obsoletesNode);
}
} else {
if (obsoletesNode) {
obsoletesNode.remove();
}
}
if (obsoletesNode) {
previousSiblingNode = obsoletesNode;
}
obsoletedByNode = xml.find("obsoletedby");
//remove the obsoletedBy node if it exists
// TODO: Verify this is what we want to do
if (obsoletedByNode) {
obsoletedByNode.remove();
}
xml.find("archived").text(this.get("archived") || "false");
xml
.find("dateuploaded")
.text(this.get("dateUploaded") || new Date().toISOString());
//Get the filename node
fileNameNode = xml.find("filename");
//If the filename node doesn't exist, then create one
if (!fileNameNode.length) {
fileNameNode = $(document.createElement("filename"));
xml.find("dateuploaded").after(fileNameNode);
}
//Set the object file name
$(fileNameNode).text(this.get("fileName"));
xmlString = $(document.createElement("div")).append(xml.clone()).html();
//Now camel case the nodes
nodeNameMap = this.nodeNameMap();
_.each(
Object.keys(nodeNameMap),
function (name, i) {
var originalXMLString = xmlString;
//Camel case node names
var regEx = new RegExp("<" + name, "g");
xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name]);
var regEx = new RegExp(name + ">", "g");
xmlString = xmlString.replace(regEx, nodeNameMap[name] + ">");
//If node names haven't been changed, then find an attribute
if (xmlString == originalXMLString) {
var regEx = new RegExp(" " + name + "=", "g");
xmlString = xmlString.replace(
regEx,
" " + nodeNameMap[name] + "=",
);
}
},
this,
);
xmlString = xmlString.replace(/systemmetadata/g, "systemMetadata");
return xmlString;
},
/**
* Get the object format identifier for this object
*/
getFormatId: function () {
var formatId = "application/octet-stream", // default to untyped data
objectFormats = {
mediaTypes: [], // The list of potential formatIds based on mediaType matches
extensions: [], // The list of possible formatIds based onextension matches
},
fileName = this.get("fileName"), // the fileName for this object
ext; // The extension of the filename for this object
objectFormats["mediaTypes"] = MetacatUI.objectFormats.where({
formatId: this.get("mediaType"),
});
if (
typeof fileName !== "undefined" &&
fileName !== null &&
fileName.length > 1
) {
ext = fileName.substring(
fileName.lastIndexOf(".") + 1,
fileName.length,
);
objectFormats["extensions"] = MetacatUI.objectFormats.where({
extension: ext,
});
}
if (
objectFormats["mediaTypes"].length > 0 &&
objectFormats["extensions"].length > 0
) {
var firstMediaType = objectFormats["mediaTypes"][0].get("formatId");
var firstExtension = objectFormats["extensions"][0].get("formatId");
// Check if they're equal
if (firstMediaType === firstExtension) {
formatId = firstMediaType;
return formatId;
}
// Handle mismatched mediaType and extension cases - additional cases can be added below
if (
firstMediaType === "application/vnd.ms-excel" &&
firstExtension === "text/csv"
) {
formatId = firstExtension;
return formatId;
}
}
if (objectFormats["mediaTypes"].length > 0) {
formatId = objectFormats["mediaTypes"][0].get("formatId");
console.log("returning default mediaType");
console.log(formatId);
return formatId;
}
if (objectFormats["extensions"].length > 0) {
//If this is a "nc" file, assume it is a netCDF-3 file.
if (ext == "nc") {
formatId = "netCDF-3";
} else {
formatId = objectFormats["extensions"][0].get("formatId");
}
return formatId;
}
return formatId;
},
/**
* Looks up human readable format of the DataONE Object
* @returns format String
* @since 2.28.0
*/
getFormat: function () {
var formatMap = {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
"Microsoft Excel OpenXML",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
"Microsoft Word OpenXML",
"application/vnd.ms-excel.sheet.binary.macroEnabled.12":
"Microsoft Office Excel 2007 binary workbooks",
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
"Microsoft Office OpenXML Presentation",
"application/vnd.ms-excel": "Microsoft Excel",
"application/msword": "Microsoft Word",
"application/vnd.ms-powerpoint": "Microsoft Powerpoint",
"text/html": "HTML",
"text/plain": "plain text (.txt)",
"video/avi": "Microsoft AVI file",
"video/x-ms-wmv": "Windows Media Video (.wmv)",
"audio/x-ms-wma": "Windows Media Audio (.wma)",
"application/vnd.google-earth.kml xml":
"Google Earth Keyhole Markup Language (KML)",
"http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html":
"annotation",
"application/mathematica": "Mathematica Notebook",
"application/postscript": "Postscript",
"application/rtf": "Rich Text Format (RTF)",
"application/xml": "XML Application",
"text/xml": "XML",
"application/x-fasta": "FASTA sequence file",
"nexus/1997": "NEXUS File Format for Systematic Information",
"anvl/erc-v02":
"Kernel Metadata and Electronic Resource Citations (ERCs), 2010.05.13",
"http://purl.org/dryad/terms/":
"Dryad Metadata Application Profile Version 3.0",
"http://datadryad.org/profile/v3.1":
"Dryad Metadata Application Profile Version 3.1",
"application/pdf": "PDF",
"application/zip": "ZIP file",
"http://www.w3.org/TR/rdf-syntax-grammar": "RDF/XML",
"http://www.w3.org/TR/rdfa-syntax": "RDFa",
"application/rdf xml": "RDF",
"text/turtle": "TURTLE",
"text/n3": "N3",
"application/x-gzip": "GZIP Format",
"application/x-python": "Python script",
"http://www.w3.org/2005/Atom": "ATOM-1.0",
"application/octet-stream": "octet stream (application file)",
"http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd":
"Darwin Core, v2.0",
"http://rs.tdwg.org/dwc/xsd/simpledarwincore/": "Simple Darwin Core",
"eml://ecoinformatics.org/eml-2.1.0": "EML v2.1.0",
"eml://ecoinformatics.org/eml-2.1.1": "EML v2.1.1",
"eml://ecoinformatics.org/eml-2.0.1": "EML v2.0.1",
"eml://ecoinformatics.org/eml-2.0.0": "EML v2.0.0",
"https://eml.ecoinformatics.org/eml-2.2.0": "EML v2.2.0",
};
return formatMap[this.get("formatId")] || this.get("formatId");
},
/**
* Build a fresh system metadata document for this object when it is new
* Return it as a DOM object
*/
createSysMeta: function () {
var sysmetaDOM, // The DOM
sysmetaXML = []; // The document as a string array
sysmetaXML.push(
//'<?xml version="1.0" encoding="UTF-8"?>',
"<d1_v2.0:systemmetadata",
' xmlns:d1_v2.0="http://ns.dataone.org/service/types/v2.0"',
' xmlns:d1="http://ns.dataone.org/service/types/v1">',
" <serialversion />",
" <identifier />",
" <formatid />",
" <size />",
" <checksum />",
" <submitter />",
" <rightsholder />",
" <filename />",
"</d1_v2.0:systemmetadata>",
);
sysmetaDOM = $($.parseHTML(sysmetaXML.join("")));
return sysmetaDOM;
},
/**
* Create an access policy for this DataONEObject using the default access
* policy set in the AppModel.
*
* @param {Element} [accessPolicyXML] - An <accessPolicy> XML node
* that contains a list of access rules.
* @return {AccessPolicy} - an AccessPolicy collection that represents the
* given XML or the default policy set in the AppModel.
*/
createAccessPolicy: function (accessPolicyXML) {
//Create a new AccessPolicy collection
var accessPolicy = new AccessPolicy();
accessPolicy.dataONEObject = this;
//If there is no access policy XML sent,
if (this.isNew() && !accessPolicyXML) {
try {
//If the app is configured to inherit the access policy from the parent metadata,
// then get the parent metadata and copy it's AccessPolicy
let scienceMetadata = this.get("isDocumentedByModels");
if (
MetacatUI.appModel.get("inheritAccessPolicy") &&
scienceMetadata &&
scienceMetadata.length
) {
let sciMetaAccessPolicy = scienceMetadata[0].get("accessPolicy");
if (sciMetaAccessPolicy) {
accessPolicy.copyAccessPolicy(sciMetaAccessPolicy);
} else {
accessPolicy.createDefaultPolicy();
}
}
//Otherwise, set the default access policy using the AppModel configuration
else {
accessPolicy.createDefaultPolicy();
}
} catch (e) {
console.error(
"Could create access policy, so defaulting to default",
e,
);
accessPolicy.createDefaultPolicy();
}
} else {
//Parse the access policy XML to create AccessRule models from the XML
accessPolicy.parse(accessPolicyXML);
}
//Listen to changes on the collection and trigger a change on this model
var self = this;
this.listenTo(accessPolicy, "change update", function () {
self.trigger("change");
this.addToUploadQueue();
});
return accessPolicy;
},
/**
* Update identifiers for this object
*
* @param {string} id - Optional identifier to update with. Generated
* automatically when not given.
*
* Note that this method caches the objects attributes prior to
* updating so this.resetID() can be called in case of a failure
* state.
*
* Also note that this method won't run if theh oldPid attribute is
* set. This enables knowing before this.save is called what the next
* PID will be such as the case where we want to update a matching
* EML entity when replacing files.
*/
updateID: function (id) {
// Only run once until oldPid is reset
if (this.get("oldPid")) {
return;
}
//Save the attributes so we can reset the ID later
this.attributeCache = this.toJSON();
//Set the old identifier
var oldPid = this.get("id"),
selfDocuments,
selfDocumentedBy,
documentedModels,
documentedModel,
index;
//Save the current id as the old pid
this.set("oldPid", oldPid);
//Create a new seriesId, if there isn't one, and if this model specifies that one is required
if (!this.get("seriesId") && this.get("createSeriesId")) {
this.set("seriesId", "urn:uuid:" + uuid.v4());
}
// Check to see if the old pid documents or is documented by itself
selfDocuments = _.contains(this.get("documents"), oldPid);
selfDocumentedBy = _.contains(this.get("isDocumentedBy"), oldPid);
//Set the new identifier
if (id) {
this.set("id", id);
} else {
if (this.get("type") == "DataPackage") {
this.set("id", "resource_map_urn:uuid:" + uuid.v4());
} else {
this.set("id", "urn:uuid:" + uuid.v4());
}
}
// Remove the old pid from the documents list if present
if (selfDocuments) {
index = this.get("documents").indexOf(oldPid);
if (index > -1) {
this.get("documents").splice(index, 1);
}
// And add the new pid in
this.get("documents").push(this.get("id"));
}
// Remove the old pid from the isDocumentedBy list if present
if (selfDocumentedBy) {
index = this.get("isDocumentedBy").indexOf(oldPid);
if (index > -1) {
this.get("isDocumentedBy").splice(index, 1);
}
// And add the new pid in
this.get("isDocumentedBy").push(this.get("id"));
}
// Update all models documented by this pid with the new id
_.each(
this.get("documents"),
function (id) {
(documentedModels = MetacatUI.rootDataPackage.where({ id: id })),
documentedModel;
if (documentedModels.length > 0) {
documentedModel = documentedModels[0];
}
if (typeof documentedModel !== "undefined") {
// Find the oldPid in the array
if (Array.isArray(documentedModel.get("isDocumentedBy"))) {
index = documentedModel.get("isDocumentedBy").indexOf("oldPid");
if (index > -1) {
// Remove it
documentedModel.get("isDocumentedBy").splice(index, 1);
}
// And add the new pid in
documentedModel.get("isDocumentedBy").push(this.get("id"));
}
}
},
this,
);
this.trigger("change:id");
//Update the obsoletes and obsoletedBy
this.set("obsoletes", oldPid);
this.set("obsoletedBy", null);
// Update the latest version of this object
this.set("latestVersion", this.get("id"));
//Set the archived option to false
this.set("archived", false);
},
/**
* Resets the identifier for this model. This undos all of the changes made in {DataONEObject#updateID}
*/
resetID: function () {
if (!this.attributeCache) return false;
this.set("oldPid", this.attributeCache.oldPid, { silent: true });
this.set("id", this.attributeCache.id, { silent: true });
this.set("obsoletes", this.attributeCache.obsoletes, { silent: true });
this.set("obsoletedBy", this.attributeCache.obsoletedBy, {
silent: true,
});
this.set("archived", this.attributeCache.archived, { silent: true });
this.set("latestVersion", this.attributeCache.latestVersion, {
silent: true,
});
//Reset the attribute cache
this.attributeCache = {};
},
/**
* Checks if this system metadata XML has updates that need to be synced with the server.
* @returns {boolean}
*/
hasUpdates: function () {
if (this.isNew()) return true;
// Compare the new system metadata XML to the old system metadata XML
//Check if there is system metadata first
if (!this.get("sysMetaXML")) {
return false;
}
var D1ObjectClone = this.clone(),
// Make sure we are using the parse function in the DataONEObject model.
// Sometimes hasUpdates is called from extensions of the D1Object model,
// (e.g. from the portal model), and the parse function is overwritten
oldSysMetaAttrs = new DataONEObject().parse(
D1ObjectClone.get("sysMetaXML"),
);
D1ObjectClone.set(oldSysMetaAttrs);
var oldSysMeta = D1ObjectClone.serializeSysMeta();
var newSysMeta = this.serializeSysMeta();
if (oldSysMeta === "") return false;
return !(newSysMeta == oldSysMeta);
},
/**
Set the changed flag on any system metadata or content attribute changes,
and set the hasContentChanges flag on content changes only
@param {DataONEObject} [model]
@param {object} options Furhter options for this function
@property {boolean} options.force If true, a change will be handled regardless if the attribute actually changed
*/
handleChange: function (model, options) {
if (!model) var model = this;
var sysMetaAttrs = [
"serialVersion",
"identifier",
"formatId",
"formatType",
"size",
"checksum",
"checksumAlgorithm",
"submitter",
"rightsHolder",
"accessPolicy",
"replicationAllowed",
"replicationPolicy",
"obsoletes",
"obsoletedBy",
"archived",
"dateUploaded",
"dateSysMetadataModified",
"originMemberNode",
"authoritativeMemberNode",
"replica",
"seriesId",
"mediaType",
"fileName",
],
nonSysMetaNonContentAttrs = _.difference(
model.get("originalAttrs"),
sysMetaAttrs,
),
allChangedAttrs = Object.keys(model.changedAttributes()),
changedSysMetaOrContentAttrs = [], //sysmeta or content attributes that have changed
changedContentAttrs = []; // attributes from sub classes like ScienceMetadata or EML211 ...
// Get a list of all changed sysmeta and content attributes
changedSysMetaOrContentAttrs = _.difference(
allChangedAttrs,
nonSysMetaNonContentAttrs,
);
if (changedSysMetaOrContentAttrs.length > 0) {
// For any sysmeta or content change, set the package dirty flag
if (
MetacatUI.rootDataPackage &&
MetacatUI.rootDataPackage.packageModel &&
!MetacatUI.rootDataPackage.packageModel.get("changed") &&
model.get("synced")
) {
MetacatUI.rootDataPackage.packageModel.set("changed", true);
}
}
// And get a list of all changed content attributes
changedContentAttrs = _.difference(
changedSysMetaOrContentAttrs,
sysMetaAttrs,
);
if (
(changedContentAttrs.length > 0 &&
!this.get("hasContentChanges") &&
model.get("synced")) ||
(options && options.force)
) {
this.set("hasContentChanges", true);
this.addToUploadQueue();
}
},
/**
* Returns true if this DataONE object is new. A DataONE object is new
* if there is no upload date and it's been synced (i.e. been fetched)
* @return {boolean}
*/
isNew: function () {
//If the model is explicitly marked as not new, return false
if (this.get("isNew") === false) {
return false;
}
//If the model is explicitly marked as new, return true
else if (this.get("isNew") === true) {
return true;
}
//Check if there is an upload date that was retrieved from the server
return (
this.get("dateUploaded") === this.defaults().dateUploaded &&
this.get("synced")
);
},
/**
* Updates the upload status attribute on this model and marks the collection as changed
*/
addToUploadQueue: function () {
if (!this.get("synced")) {
return;
}
//Add this item to the queue
if (
this.get("uploadStatus") == "c" ||
this.get("uploadStatus") == "e" ||
!this.get("uploadStatus")
) {
this.set("uploadStatus", "q");
//Mark each DataPackage collection this model is in as changed
_.each(
this.get("collections"),
function (collection) {
if (collection.packageModel)
collection.packageModel.set("changed", true);
},
this,
);
}
},
/**
* Updates the progress percentage when the model is getting uploaded
* @param {ProgressEvent} e - The ProgressEvent when this file is being uploaded
*/
updateProgress: function (e) {
if (e.lengthComputable) {
var max = e.total;
var current = e.loaded;
var Percentage = (current * 100) / max;
if (Percentage >= 100) {
// process completed
}
}
},
/**
* Updates the relationships with other models when this model has been updated
*/
updateRelationships: function () {
_.each(
this.get("collections"),
function (collection) {
//Get the old id for this model
var oldId = this.get("oldPid");
if (!oldId) return;
//Find references to the old id in the documents relationship
var outdatedModels = collection.filter(function (m) {
return _.contains(m.get("documents"), oldId);
});
//Update the documents array in each model
_.each(
outdatedModels,
function (model) {
var updatedDocuments = _.without(model.get("documents"), oldId);
updatedDocuments.push(this.get("id"));
model.set("documents", updatedDocuments);
},
this,
);
},
this,
);
},
/**
* Finds the latest version of this object by travesing the obsolescence chain
* @param {string} [latestVersion] - The identifier of the latest known object in the version chain.
If not supplied, this model's `id` will be used.
* @param {string} [possiblyNewer] - The identifier of the object that obsoletes the latestVersion. It's "possibly" newer, because it may be private/inaccessible
*/
findLatestVersion: function (latestVersion, possiblyNewer) {
var baseUrl = "",
activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
//Use the meta service URL from the alt repo
if (activeAltRepo) {
baseUrl = activeAltRepo.metaServiceUrl;
}
//If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel
else {
baseUrl = MetacatUI.appModel.get("metaServiceUrl");
}
if (!baseUrl) {
return;
}
//If there is no system metadata, then retrieve it first
if (!this.get("sysMetaXML")) {
this.once("sync", this.findLatestVersion);
this.once("systemMetadataSync", this.findLatestVersion);
this.fetch({
url: baseUrl + encodeURIComponent(this.get("id")),
dataType: "text",
systemMetadataOnly: true,
});
return;
}
//If no pid was supplied, use this model's id
if (!latestVersion || typeof latestVersion != "string") {
var latestVersion = this.get("id");
var possiblyNewer = this.get("obsoletedBy");
}
//If this isn't obsoleted by anything, then there is no newer version
if (!possiblyNewer || typeof latestVersion != "string") {
this.set("latestVersion", latestVersion);
//Trigger an event that will fire whether or not the latestVersion
// attribute was actually changed
this.trigger("latestVersionFound", this);
//Remove the listeners now that we found the latest version
this.stopListening("sync", this.findLatestVersion);
this.stopListening("systemMetadataSync", this.findLatestVersion);
return;
}
var model = this;
//Get the system metadata for the possibly newer version
var requestSettings = {
url: baseUrl + encodeURIComponent(possiblyNewer),
type: "GET",
success: function (data) {
// the response may have an obsoletedBy element
var obsoletedBy = $(data).find("obsoletedBy").text();
//If there is an even newer version, then get it and rerun this function
if (obsoletedBy) {
model.findLatestVersion(possiblyNewer, obsoletedBy);
}
//If there isn't a newer version, then this is it
else {
model.set("latestVersion", possiblyNewer);
model.trigger("latestVersionFound", model);
//Remove the listeners now that we found the latest version
model.stopListening("sync", model.findLatestVersion);
model.stopListening(
"systemMetadataSync",
model.findLatestVersion,
);
}
},
error: function (xhr) {
//If this newer version isn't accessible, link to the latest version that is
if (xhr.status == "401") {
model.set("latestVersion", latestVersion);
model.trigger("latestVersionFound", model);
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/**
* A utility function that will format an XML string or XML nodes by camel-casing the node names, as necessary
* @param {string|Element} xml - The XML to format
* @returns {string} The formatted XML string
*/
formatXML: function (xml) {
var nodeNameMap = this.nodeNameMap(),
xmlString = "";
//XML must be provided for this function
if (!xml) return "";
//Support XML strings
else if (typeof xml == "string") xmlString = xml;
//Support DOMs
else if (typeof xml == "object" && xml.nodeType) {
//XML comments should be formatted with start and end carets
if (xml.nodeType == 8) xmlString = "<" + xml.nodeValue + ">";
//XML nodes have the entire XML string available in the outerHTML attribute
else if (xml.nodeType == 1) xmlString = xml.outerHTML;
//Text node types are left as-is
else if (xml.nodeType == 3) return xml.nodeValue;
}
//Return empty strings if something went wrong
if (!xmlString) return "";
_.each(
Object.keys(nodeNameMap),
function (name, i) {
var originalXMLString = xmlString;
//Check for this node name whe it's an opening XML node, e.g. `<name>`
var regEx = new RegExp("<" + name + ">", "g");
xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + ">");
//Check for this node name when it's an opening XML node, e.g. `<name `
regEx = new RegExp("<" + name + " ", "g");
xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + " ");
//Check for this node name when it's preceeded by a namespace, e.g. `:name `
regEx = new RegExp(":" + name + " ", "g");
xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + " ");
//Check for this node name when it's a closing tag preceeded by a namespace, e.g. `:name>`
regEx = new RegExp(":" + name + ">", "g");
xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + ">");
//Check for this node name when it's a closing XML tag, e.g. `</name>`
regEx = new RegExp("</" + name + ">", "g");
xmlString = xmlString.replace(
regEx,
"</" + nodeNameMap[name] + ">",
);
//If node names haven't been changed, then find an attribute, e.g. ` name=`
if (xmlString == originalXMLString) {
regEx = new RegExp(" " + name + "=", "g");
xmlString = xmlString.replace(
regEx,
" " + nodeNameMap[name] + "=",
);
}
},
this,
);
//Take each XML node text value and decode any XML entities
var regEx = new RegExp("&[0-9a-zA-Z]+;", "g");
xmlString = xmlString.replace(regEx, function (match) {
return he.encode(he.decode(match));
});
return xmlString;
},
/**
* This method will download this object while
* sending the user's auth token in the request.
* @returns None
* @since: 2.28.0
*/
downloadWithCredentials: function () {
//if(this.get("isPublic")) return;
//Get info about this object
var url = this.get("url"),
model = this;
//Create an XHR
var xhr = new XMLHttpRequest();
//Open and send the request with the user's auth token
xhr.open("GET", url);
if (MetacatUI.appUserModel.get("loggedIn")) xhr.withCredentials = true;
//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
xhr.onload = function () {
if (this.status == 404) {
this.onerror.call(this);
return;
}
//Get the file name to save this file as
var filename = xhr.getResponseHeader("Content-Disposition");
if (!filename) {
filename =
model.get("fileName") ||
model.get("title") ||
model.get("id") ||
"download";
} else
filename = filename
.substring(filename.indexOf("filename=") + 9)
.replace(/"/g, "");
//Replace any whitespaces
filename = filename.trim().replace(/ /g, "_");
//For IE, we need to use the navigator API
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(xhr.response, filename);
}
//Other browsers can download it via a link
else {
var a = document.createElement("a");
a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
// Set the file name.
a.download = filename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}
model.trigger("downloadComplete");
// Track this event
MetacatUI.analytics?.trackEvent(
"download",
"Download DataONEObject",
model.get("id"),
);
};
xhr.onerror = function (e) {
model.trigger("downloadError");
// Track the error
MetacatUI.analytics?.trackException(
`Download DataONEObject error: ${e || ""}`,
model.get("id"),
true,
);
};
xhr.onprogress = function (e) {
if (e.lengthComputable) {
var percent = (e.loaded / e.total) * 100;
model.set("downloadPercent", percent);
}
};
xhr.responseType = "blob";
if (MetacatUI.appUserModel.get("loggedIn"))
xhr.setRequestHeader(
"Authorization",
"Bearer " + MetacatUI.appUserModel.get("token"),
);
xhr.send();
},
/**
* Creates a file name for this DataONEObject and updates the `fileName` attribute
*/
setMissingFileName: function () {
var objectFormats, filename, extension;
objectFormats = MetacatUI.objectFormats.where({
formatId: this.get("formatId"),
});
if (objectFormats.length > 0) {
extension = objectFormats[0].get("extension");
}
//Science metadata file names will use the title
if (this.get("type") == "Metadata") {
filename =
Array.isArray(this.get("title")) && this.get("title").length
? this.get("title")[0]
: this.get("id");
}
//Resource maps will use a "resource_map_" prefix
else if (this.get("type") == "DataPackage") {
filename = "resource_map_" + this.get("id");
extension = ".rdf.xml";
}
//All other object types will just use the id
else {
filename = this.get("id");
}
//Replace all non-alphanumeric characters with underscores
filename = filename.replace(/[^a-zA-Z0-9]/g, "_");
if (typeof extension !== "undefined") {
filename = filename + "." + extension;
}
this.set("fileName", filename);
},
/**
* Creates a URL for viewing more information about this object
* @return {string}
*/
createViewURL: function () {
return (
MetacatUI.root +
"/view/" +
encodeURIComponent(this.get("seriesId") || this.get("id"))
);
},
/**
* Check if the seriesID or PID matches a DOI regex, and if so, return
* a canonical IRI for the DOI.
* @return {string|null} - The canonical IRI for the DOI, or null if
* neither the seriesId nor the PID match a DOI regex.
* @since 2.26.0
*/
getCanonicalDOIIRI: function () {
const id = this.get("id");
const seriesId = this.get("seriesId");
let DOI = null;
if (this.isDOI(seriesId)) DOI = seriesId;
else if (this.isDOI(id)) DOI = id;
return MetacatUI.appModel.DOItoURL(DOI);
},
/**
* Converts the identifier string to a string safe to use in an XML id attribute
* @param {string} [id] - The ID string
* @return {string} - The XML-safe string
*/
getXMLSafeID: function (id) {
if (typeof id == "undefined") {
var id = this.get("id");
}
//Replace XML id attribute invalid characters and patterns in the identifier
id = id
.replace(/</g, "-")
.replace(/:/g, "-")
.replace(/&[a-zA-Z0-9]+;/g);
return id;
},
/**** Provenance-related functions ****/
/**
* Returns true if this provenance field points to a source of this data or metadata object
* @param {string} field
* @returns {boolean}
*/
isSourceField: function (field) {
if (typeof field == "undefined" || !field) return false;
// Is the field we are checking a prov field?
if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field))
return false;
if (
field == "prov_generatedByExecution" ||
field == "prov_generatedByProgram" ||
field == "prov_used" ||
field == "prov_wasDerivedFrom" ||
field == "prov_wasInformedBy"
)
return true;
else return false;
},
/**
* Returns true if this provenance field points to a derivation of this data or metadata object
* @param {string} field
* @returns {boolean}
*/
isDerivationField: function (field) {
if (typeof field == "undefined" || !field) return false;
if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field))
return false;
if (
field == "prov_usedByExecution" ||
field == "prov_usedByProgram" ||
field == "prov_hasDerivations" ||
field == "prov_generated"
)
return true;
else return false;
},
/**
* Returns a plain-english version of the general format - either image, program, metadata, PDF, annotation or data
*/
getType: function () {
//The list of formatIds that are images
//The list of formatIds that are images
var pdfIds = ["application/pdf"];
var annotationIds = [
"http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html",
];
// Type has already been set, use that.
if (this.get("type").toLowerCase() == "metadata") return "metadata";
//Determine the type via provONE
var instanceOfClass = this.get("prov_instanceOfClass");
if (
typeof instanceOfClass !== "undefined" &&
Array.isArray(instanceOfClass) &&
instanceOfClass.length
) {
var programClass = _.filter(instanceOfClass, function (className) {
return className.indexOf("#Program") > -1;
});
if (typeof programClass !== "undefined" && programClass.length)
return "program";
} else {
if (this.get("prov_generated").length || this.get("prov_used").length)
return "program";
}
//Determine the type via file format
if (this.isSoftware()) return "program";
if (this.isData()) return "data";
if (this.get("type").toLowerCase() == "metadata") return "metadata";
if (this.isImage()) return "image";
if (_.contains(pdfIds, this.get("formatId"))) return "PDF";
if (_.contains(annotationIds, this.get("formatId")))
return "annotation";
else return "data";
},
/**
* Checks the formatId of this model and determines if it is an image.
* @returns {boolean} true if this data object is an image, false if it is other
*/
isImage: function () {
//The list of formatIds that are images
var imageIds = ["image/gif", "image/jp2", "image/jpeg", "image/png"];
//Does this data object match one of these IDs?
if (_.indexOf(imageIds, this.get("formatId")) == -1) return false;
else return true;
},
/**
* Checks the formatId of this model and determines if it is a data file.
* This determination is mostly used for display and the provenance editor. In the
* DataONE API, many formatIds are considered `DATA` formatTypes, but they are categorized
* as images {@link DataONEObject#isImage} or software {@link DataONEObject#isSoftware}.
* @returns {boolean} true if this data object is a data file, false if it is other
*/
isData: function () {
var dataIds = [
"application/atom+xml",
"application/mathematica",
"application/msword",
"application/netcdf",
"application/octet-stream",
"application/pdf",
"application/postscript",
"application/rdf+xml",
"application/rtf",
"application/vnd.google-earth.kml+xml",
"application/vnd.ms-excel",
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/x-bzip2",
"application/x-fasta",
"application/x-gzip",
"application/x-rar-compressed",
"application/x-tar",
"application/xhtml+xml",
"application/xml",
"application/zip",
"audio/mpeg",
"audio/x-ms-wma",
"audio/x-wav",
"image/svg xml",
"image/svg+xml",
"image/bmp",
"image/tiff",
"text/anvl",
"text/csv",
"text/html",
"text/n3",
"text/plain",
"text/tab-separated-values",
"text/turtle",
"text/xml",
"video/avi",
"video/mp4",
"video/mpeg",
"video/quicktime",
"video/x-ms-wmv",
];
//Does this data object match one of these IDs?
if (_.indexOf(dataIds, this.get("formatId")) == -1) return false;
else return true;
},
/**
* Checks the formatId of this model and determines if it is a software file.
* This determination is mostly used for display and the provenance editor. In the
* DataONE API, many formatIds are considered `DATA` formatTypes, but they are categorized
* as images {@link DataONEObject#isImage} for display purposes.
* @returns {boolean} true if this data object is a software file, false if it is other
*/
isSoftware: function () {
//The list of formatIds that are programs
var softwareIds = [
"text/x-python",
"text/x-rsrc",
"text/x-matlab",
"text/x-sas",
"application/R",
"application/x-ipynb+json",
];
//Does this data object match one of these IDs?
if (_.indexOf(softwareIds, this.get("formatId")) == -1) return false;
else return true;
},
/**
* Checks the formatId of this model and determines if it a PDF.
* @returns {boolean} true if this data object is a pdf, false if it is other
*/
isPDF: function () {
//The list of formatIds that are images
var ids = ["application/pdf"];
//Does this data object match one of these IDs?
if (_.indexOf(ids, this.get("formatId")) == -1) return false;
else return true;
},
/**
* Set the DataONE ProvONE provenance class
* param className - the shortened form of the actual classname value. The
* shortname will be appened to the ProvONE namespace, for example,
* the className "program" will result in the final class name
* "http://purl.dataone.org/provone/2015/01/15/ontology#Program"
* see https://github.com/DataONEorg/sem-prov-ontologies/blob/master/provenance/ProvONE/v1/provone.html
* @param {string} className
*/
setProvClass: function (className) {
className = className.toLowerCase();
className = className.charAt(0).toUpperCase() + className.slice(1);
/* This function is intended to be used for the ProvONE classes that are
* typically represented in DataONEObjects: "Data", "Program", and hopefully
* someday "Execution", as we don't allow the user to set the namespace
* e.g. to "PROV", so therefor we check for the currently known ProvONE classes.
*/
if (
_.contains(
[
"Program",
"Data",
"Visualization",
"Document",
"Execution",
"User",
],
className,
)
) {
this.set("prov_instanceOfClass", [this.PROVONE + className]);
} else if (
_.contains(
["Entity", "Usage", "Generation", "Association"],
className,
)
) {
this.set("prov_instanceOfClass", [this.PROV + className]);
} else {
message =
"The given class name: " +
className +
" is not in the known ProvONE or PROV classes.";
throw new Error(message);
}
},
/**
* Calculate a checksum for the object
* @param {string} [algorithm] The algorithm to use, defaults to MD5
* @return {string} A checksum plain JS object with value and algorithm attributes
*/
calculateChecksum: function (algorithm) {
var algorithm = algorithm || "MD5";
var checksum = { algorithm: undefined, value: undefined };
var hash; // The checksum hash
var file; // The file to be read by slicing
var reader; // The FileReader used to read each slice
var offset = 0; // Byte offset for reading slices
var sliceSize = Math.pow(2, 20); // 1MB slices
var model = this;
// Do we have a file?
if (this.get("uploadFile") instanceof Blob) {
file = this.get("uploadFile");
reader = new FileReader();
/* Handle load errors */
reader.onerror = function (event) {
console.log("Error reading: " + event);
};
/* Show progress */
reader.onprogress = function (event) {};
/* Handle load finish */
reader.onloadend = function (event) {
if (event.target.readyState == FileReader.DONE) {
hash.update(event.target.result);
}
offset += sliceSize;
if (_seek()) {
model.set("checksum", hash.hex());
model.set("checksumAlgorithm", checksum.algorithm);
model.trigger("checksumCalculated", model.attributes);
}
};
} else {
message = "The given object is not a blob or a file object.";
throw new Error(message);
}
switch (algorithm) {
case "MD5":
checksum.algorithm = algorithm;
hash = md5.create();
_seek();
break;
case "SHA-1":
// TODO: Support SHA-1
// break;
default:
message =
"The given algorithm: " + algorithm + " is not supported.";
throw new Error(message);
}
/*
* A helper function internal to calculateChecksum() used to slice
* the file at the next offset by slice size
*/
function _seek() {
var calculated = false;
var slice;
// Digest the checksum when we're done calculating
if (offset >= file.size) {
hash.digest();
calculated = true;
return calculated;
}
// slice the file and read the slice
slice = file.slice(offset, offset + sliceSize);
reader.readAsArrayBuffer(slice);
return calculated;
}
},
/**
* Checks if the pid or sid or given string is a DOI
*
* @param {string} customString - Optional. An identifier string to check instead of the id and seriesId attributes on the model
* @returns {boolean} True if it is a DOI
*/
isDOI: function (customString) {
return (
isDOI(customString) ||
isDOI(this.get("id")) ||
isDOI(this.get("seriesId"))
);
},
/**
* Creates an array of objects that represent Member Nodes that could possibly be this
* object's authoritative MN. This function updates the `possibleAuthMNs` attribute on this model.
*/
setPossibleAuthMNs: function () {
//Only do this for Coordinating Node MetacatUIs.
if (MetacatUI.appModel.get("alternateRepositories").length) {
//Set the possibleAuthMNs attribute
var possibleAuthMNs = [];
//If a datasource is already found for this Portal, move that to the top of the list of auth MNs
var datasource = this.get("datasource") || "";
if (datasource) {
//Find the MN object that matches the datasource node ID
var datasourceMN = _.findWhere(
MetacatUI.appModel.get("alternateRepositories"),
{ identifier: datasource },
);
if (datasourceMN) {
//Clone the MN object and add it to the array
var clonedDatasourceMN = Object.assign({}, datasourceMN);
possibleAuthMNs.push(clonedDatasourceMN);
}
}
//If there is an active alternate repo, move that to the top of the list of auth MNs
var activeAltRepo =
MetacatUI.appModel.get("activeAlternateRepositoryId") || "";
if (activeAltRepo) {
var activeAltRepoMN = _.findWhere(
MetacatUI.appModel.get("alternateRepositories"),
{ identifier: activeAltRepo },
);
if (activeAltRepoMN) {
//Clone the MN object and add it to the array
var clonedActiveAltRepoMN = Object.assign({}, activeAltRepoMN);
possibleAuthMNs.push(clonedActiveAltRepoMN);
}
}
//Add all the other alternate repositories to the list of auth MNs
var otherPossibleAuthMNs = _.reject(
MetacatUI.appModel.get("alternateRepositories"),
function (mn) {
return (
mn.identifier == datasource || mn.identifier == activeAltRepo
);
},
);
//Clone each MN object and add to the array
_.each(otherPossibleAuthMNs, function (mn) {
var clonedMN = Object.assign({}, mn);
possibleAuthMNs.push(clonedMN);
});
//Update this model
this.set("possibleAuthMNs", possibleAuthMNs);
}
},
/**
* Removes white space from string values returned by Solr when the white space causes issues.
* For now this only effects the `resourceMap` field, which will index new line characters and spaces
* when the RDF XML has those in the `identifier` XML element content. This was causing bugs where DataONEObject
* models were created with `id`s with new line and white space characters (e.g. `\n urn:uuid:1234...`)
* @param {object} json - The Solr document as a JS Object, which will be directly altered
*/
removeWhiteSpaceFromSolrFields: function (json) {
if (typeof json.resourceMap == "string") {
json.resourceMap = json.resourceMap.trim();
} else if (Array.isArray(json.resourceMap)) {
let newResourceMapIds = [];
_.each(json.resourceMap, function (rMapId) {
if (typeof rMapId == "string") {
newResourceMapIds.push(rMapId.trim());
}
});
json.resourceMap = newResourceMapIds;
}
},
},
/** @lends DataONEObject.prototype */
{
/**
* Generate a unique identifier to be used as an XML id attribute
* @returns {string} The identifier string that was generated
*/
generateId: function () {
var idStr = ""; // the id to return
var length = 30; // the length of the generated string
var chars =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz".split(
"",
);
for (var i = 0; i < length; i++) {
idStr += chars[Math.floor(Math.random() * chars.length)];
}
return idStr;
},
},
);

Copy link
Contributor

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.

initialize: function (attrs, options) {
if (typeof attrs == "undefined") var attrs = {};
this.set("accessPolicy", this.createAccessPolicy());
var model = this;
this.on("change:size", function () {
var size = Utilities.bytesToSize(model.get("size"));
model.set("sizeStr", size);
});
if (attrs.size) {
var size = Utilities.bytesToSize(model.get("size"));
model.set("sizeStr", size);
}
// Cache an array of original attribute names to help in handleChange()
if (this.type == "DataONEObject")
this.set("originalAttrs", Object.keys(this.attributes));
else
this.set(
"originalAttrs",
Object.keys(DataONEObject.prototype.defaults()),
);
this.on("successSaving", this.updateRelationships);
//Save a reference to this DataONEObject model in the metadataEntity model
//whenever the metadataEntity is set
this.on("change:metadataEntity", function () {
var entityMetadataModel = this.get("metadataEntity");
if (entityMetadataModel)
entityMetadataModel.set("dataONEObject", this);
});
this.on("sync", function () {
this.set("synced", true);
});
//Find Member Node object that might be the authoritative MN
//This is helpful when MetacatUI may be displaying content from multiple MNs
this.setPossibleAuthMNs();
},

Copy link
Contributor

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.

formatXML: function (xml) {
var nodeNameMap = this.nodeNameMap(),
xmlString = "";
//XML must be provided for this function
if (!xml) return "";
//Support XML strings
else if (typeof xml == "string") xmlString = xml;
//Support DOMs
else if (typeof xml == "object" && xml.nodeType) {
//XML comments should be formatted with start and end carets
if (xml.nodeType == 8) xmlString = "<" + xml.nodeValue + ">";
//XML nodes have the entire XML string available in the outerHTML attribute
else if (xml.nodeType == 1) xmlString = xml.outerHTML;
//Text node types are left as-is
else if (xml.nodeType == 3) return xml.nodeValue;
}
//Return empty strings if something went wrong
if (!xmlString) return "";
_.each(
Object.keys(nodeNameMap),
function (name, i) {
var originalXMLString = xmlString;
//Check for this node name whe it's an opening XML node, e.g. `<name>`
var regEx = new RegExp("<" + name + ">", "g");
xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + ">");
//Check for this node name when it's an opening XML node, e.g. `<name `
regEx = new RegExp("<" + name + " ", "g");
xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + " ");
//Check for this node name when it's preceeded by a namespace, e.g. `:name `
regEx = new RegExp(":" + name + " ", "g");
xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + " ");
//Check for this node name when it's a closing tag preceeded by a namespace, e.g. `:name>`
regEx = new RegExp(":" + name + ">", "g");
xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + ">");
//Check for this node name when it's a closing XML tag, e.g. `</name>`
regEx = new RegExp("</" + name + ">", "g");
xmlString = xmlString.replace(
regEx,
"</" + nodeNameMap[name] + ">",
);
//If node names haven't been changed, then find an attribute, e.g. ` name=`
if (xmlString == originalXMLString) {
regEx = new RegExp(" " + name + "=", "g");
xmlString = xmlString.replace(
regEx,
" " + nodeNameMap[name] + "=",
);
}
},
this,
);
//Take each XML node text value and decode any XML entities
var regEx = new RegExp("&[0-9a-zA-Z]+;", "g");
xmlString = xmlString.replace(regEx, function (match) {
return he.encode(he.decode(match));
});
return xmlString;
},

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <jsdoc/require-returns-check> reported by reviewdog 🐶
JSDoc @returns declaration present but return expression not available in function.

/**
* This method will download this object while
* sending the user's auth token in the request.
* @returns None
* @since: 2.28.0
*/

Copy link
Contributor

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.

], function ($, _, Backbone, uuid, md5, rdf, SolrResult) {
// Package Model
// ------------------
var PackageModel = Backbone.Model.extend(
/** @lends PackageModel.prototype */ {
// This model contains information about a package/resource map
defaults: function () {
return {
id: null, //The id of the resource map/package itself
url: null, //the URL to retrieve this package
memberId: null, //An id of a member of the data package
indexDoc: null, //A SolrResult object representation of the resource map
size: 0, //The number of items aggregated in this package
totalSize: null,
formattedSize: "",
formatId: null,
obsoletedBy: null,
obsoletes: null,
read_count_i: null,
isPublic: true,
members: [],
memberIds: [],
sources: [],
derivations: [],
provenanceFlag: null,
sourcePackages: [],
derivationPackages: [],
sourceDocs: [],
derivationDocs: [],
relatedModels: [], //A condensed list of all SolrResult models related to this package in some way
parentPackageMetadata: null,
//If true, when the member objects are retrieved, archived content will be included
getArchivedMembers: false,
};
},
//Define the namespaces
namespaces: {
RDF: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
FOAF: "http://xmlns.com/foaf/0.1/",
OWL: "http://www.w3.org/2002/07/owl#",
DC: "http://purl.org/dc/elements/1.1/",
ORE: "http://www.openarchives.org/ore/terms/",
DCTERMS: "http://purl.org/dc/terms/",
CITO: "http://purl.org/spar/cito/",
XML: "http://www.w3.org/2001/XMLSchema#",
},
sysMetaNodeMap: {
accesspolicy: "accessPolicy",
accessrule: "accessRule",
authoritativemembernode: "authoritativeMemberNode",
dateuploaded: "dateUploaded",
datesysmetadatamodified: "dateSysMetadataModified",
dateuploaded: "dateUploaded",
formatid: "formatId",
nodereference: "nodeReference",
obsoletedby: "obsoletedBy",
originmembernode: "originMemberNode",
replicamembernode: "replicaMemberNode",
replicapolicy: "replicaPolicy",
replicationstatus: "replicationStatus",
replicaverified: "replicaVerified",
rightsholder: "rightsHolder",
serialversion: "serialVersion",
},
complete: false,
pending: false,
type: "Package",
// The RDF graph representing this data package
dataPackageGraph: null,
initialize: function (options) {
this.setURL();
// Create an initial RDF graph
this.dataPackageGraph = rdf.graph();
},
setURL: function () {
if (MetacatUI.appModel.get("packageServiceUrl"))
this.set(
"url",
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(this.get("id")),
);
},
/*
* Set the URL for fetch
*/
url: function () {
return (
MetacatUI.appModel.get("objectServiceUrl") +
encodeURIComponent(this.get("id"))
);
},
/* Retrieve the id of the resource map/package that this id belongs to */
getMembersByMemberID: function (id) {
this.pending = true;
if (typeof id === "undefined" || !id) var id = this.memberId;
var model = this;
//Get the id of the resource map for this member
var provFlList =
MetacatUI.appSearchModel.getProvFlList() + "prov_instanceOfClass,";
var query =
"fl=resourceMap,fileName,read:read_count_i,obsoletedBy,size,formatType,formatId,id,datasource,title,origin,pubDate,dateUploaded,isPublic,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription," +
provFlList +
"&rows=1" +
"&q=id:%22" +
encodeURIComponent(id) +
"%22" +
"&wt=json";
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
//There should be only one response since we searched by id
if (typeof data.response.docs !== "undefined") {
var doc = data.response.docs[0];
//Is this document a resource map itself?
if (doc.formatId == "http://www.openarchives.org/ore/terms") {
model.set("id", doc.id); //this is the package model ID
model.set("members", new Array()); //Reset the member list
model.getMembers();
}
//If there is no resource map, then this is the only document to in this package
else if (
typeof doc.resourceMap === "undefined" ||
!doc.resourceMap
) {
model.set("id", null);
model.set("memberIds", new Array(doc.id));
model.set("members", [new SolrResult(doc)]);
model.trigger("change:members");
model.flagComplete();
} else {
model.set("id", doc.resourceMap[0]);
model.getMembers();
}
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/* Get all the members of a resource map/package based on the id attribute of this model.
* Create a SolrResult model for each member and save it in the members[] attribute of this model. */
getMembers: function (options) {
this.pending = true;
var model = this,
members = [],
pids = []; //Keep track of each object pid
//*** Find all the files that are a part of this resource map and the resource map itself
var provFlList = MetacatUI.appSearchModel.getProvFlList();
var query =
"fl=resourceMap,fileName,obsoletes,obsoletedBy,size,formatType,formatId,id,datasource," +
"rightsHolder,dateUploaded,archived,title,origin,prov_instanceOfClass,isDocumentedBy,isPublic" +
"&rows=1000" +
"&q=%28resourceMap:%22" +
encodeURIComponent(this.id) +
"%22%20OR%20id:%22" +
encodeURIComponent(this.id) +
"%22%29" +
"&wt=json";
if (this.get("getArchivedMembers")) {
query += "&archived=archived:*";
}
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
//Separate the resource maps from the data/metadata objects
_.each(data.response.docs, function (doc) {
if (doc.id == model.get("id")) {
model.set("indexDoc", doc);
model.set(doc);
if (
model.get("resourceMap") &&
options &&
options.getParentMetadata
)
model.getParentMetadata();
} else {
pids.push(doc.id);
if (doc.formatType == "RESOURCE") {
var newPckg = new PackageModel(doc);
newPckg.set("parentPackage", model);
members.push(newPckg);
} else members.push(new SolrResult(doc));
}
});
model.set("memberIds", _.uniq(pids));
model.set("members", members);
if (model.getNestedPackages().length > 0)
model.createNestedPackages();
else model.flagComplete();
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
return this;
},
/*
* Send custom options to the Backbone.Model.fetch() function
*/
fetch: function (options) {
if (!options) var options = {};
var fetchOptions = _.extend({ dataType: "text" }, options);
//Add the authorization options
fetchOptions = _.extend(
fetchOptions,
MetacatUI.appUserModel.createAjaxSettings(),
);
return Backbone.Model.prototype.fetch.call(this, fetchOptions);
},
/*
* Deserialize a Package from OAI-ORE RDF XML
*/
parse: function (response, options) {
//Save the raw XML in case it needs to be used later
this.set("objectXML", $.parseHTML(response));
//Define the namespaces
var RDF = rdf.Namespace(this.namespaces.RDF),
FOAF = rdf.Namespace(this.namespaces.FOAF),
OWL = rdf.Namespace(this.namespaces.OWL),
DC = rdf.Namespace(this.namespaces.DC),
ORE = rdf.Namespace(this.namespaces.ORE),
DCTERMS = rdf.Namespace(this.namespaces.DCTERMS),
CITO = rdf.Namespace(this.namespaces.CITO);
var memberStatements = [],
memberURIParts,
memberPIDStr,
memberPID,
memberModel,
models = []; // the models returned by parse()
try {
rdf.parse(
response,
this.dataPackageGraph,
MetacatUI.appModel.get("objectServiceUrl") +
(encodeURIComponent(this.id) ||
encodeURIComponent(this.seriesid)),
"application/rdf+xml",
);
// List the package members
memberStatements = this.dataPackageGraph.statementsMatching(
undefined,
ORE("aggregates"),
undefined,
undefined,
);
var memberPIDs = [],
members = [],
currentMembers = this.get("members"),
model = this;
// Get system metadata for each member to eval the formatId
_.each(
memberStatements,
function (memberStatement) {
memberURIParts = memberStatement.object.value.split("/");
memberPIDStr = _.last(memberURIParts);
memberPID = decodeURIComponent(memberPIDStr);
if (memberPID) {
memberPIDs.push(memberPID);
//Get the current model from the member list, if it exists
var existingModel = _.find(currentMembers, function (m) {
return m.get("id") == decodeURIComponent(memberPID);
});
//Add the existing model to the new member list
if (existingModel) {
members.push(existingModel);
}
//Or create a new SolrResult model
else {
members.push(
new SolrResult({
id: decodeURIComponent(memberPID),
}),
);
}
}
},
this,
);
//Get the documents relationships
var documentedByStatements = this.dataPackageGraph.statementsMatching(
undefined,
CITO("isDocumentedBy"),
undefined,
undefined,
),
metadataPids = [];
_.each(
documentedByStatements,
function (statement) {
//Get the data object that is documentedBy metadata
var dataPid = decodeURIComponent(
_.last(statement.subject.value.split("/")),
),
dataObj = _.find(members, function (m) {
return m.get("id") == dataPid;
}),
metadataPid = _.last(statement.object.value.split("/"));
//Save this as a metadata model
metadataPids.push(metadataPid);
//Set the isDocumentedBy field
var isDocBy = dataObj.get("isDocumentedBy");
if (isDocBy && Array.isArray(isDocBy)) isDocBy.push(metadataPid);
else if (isDocBy && !Array.isArray(isDocBy))
isDocBy = [isDocBy, metadataPid];
else isDocBy = [metadataPid];
dataObj.set("isDocumentedBy", isDocBy);
},
this,
);
//Get the metadata models and mark them as metadata
var metadataModels = _.filter(members, function (m) {
return _.contains(metadataPids, m.get("id"));
});
_.invoke(metadataModels, "set", "formatType", "METADATA");
//Keep the pids in the collection for easy access later
this.set("memberIds", memberPIDs);
this.set("members", members);
} catch (error) {
console.log(error);
}
return models;
},
/*
* Overwrite the Backbone.Model.save() function to set custom options
*/
save: function (attrs, options) {
if (!options) var options = {};
//Get the system metadata first
if (!this.get("hasSystemMetadata")) {
var model = this;
var requestSettings = {
url:
MetacatUI.appModel.get("metaServiceUrl") +
encodeURIComponent(this.get("id")),
success: function (response) {
model.parseSysMeta(response);
model.set("hasSystemMetadata", true);
model.save.call(model, null, options);
},
dataType: "text",
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
return;
}
//Create a new pid if we are updating the object
if (!options.sysMetaOnly) {
//Set a new id
var oldPid = this.get("id");
this.set("oldPid", oldPid);
this.set("id", "urn:uuid:" + uuid.v4());
this.set("obsoletes", oldPid);
this.set("obsoletedBy", null);
this.set("archived", false);
}
//Create the system metadata
var sysMetaXML = this.serializeSysMeta();
//Send the new pid, old pid, and system metadata
var xmlBlob = new Blob([sysMetaXML], { type: "application/xml" });
var formData = new FormData();
formData.append("sysmeta", xmlBlob, "sysmeta");
//Let's try updating the system metadata for now
if (options.sysMetaOnly) {
formData.append("pid", this.get("id"));
var requestSettings = {
url: MetacatUI.appModel.get("metaServiceUrl"),
type: "PUT",
cache: false,
contentType: false,
processData: false,
data: formData,
success: function (response) {},
error: function (data) {
console.log("error updating system metadata");
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
} else {
//Add the ids to the form data
formData.append("newPid", this.get("id"));
formData.append("pid", oldPid);
//Create the resource map XML
var mapXML = this.serialize();
var mapBlob = new Blob([mapXML], { type: "application/xml" });
formData.append("object", mapBlob);
//Get the size of the new resource map
this.set("size", mapBlob.size);
//Get the new checksum of the resource map
var checksum = md5(mapXML);
this.set("checksum", checksum);
var requestSettings = {
url: MetacatUI.appModel.get("objectServiceUrl"),
type: "PUT",
cache: false,
contentType: false,
processData: false,
data: formData,
success: function (response) {},
error: function (data) {
console.log("error udpating object");
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
}
},
parseSysMeta: function (response) {
this.set("sysMetaXML", $.parseHTML(response));
var responseDoc = $.parseHTML(response),
systemMetadata,
prependXML = "",
appendXML = "";
for (var i = 0; i < responseDoc.length; i++) {
if (
responseDoc[i].nodeType == 1 &&
responseDoc[i].localName.indexOf("systemmetadata") > -1
)
systemMetadata = responseDoc[i];
}
//Parse the XML to JSON
var sysMetaValues = this.toJson(systemMetadata),
camelCasedValues = {};
//Convert the JSON to a camel-cased version, which matches Solr and is easier to work with in code
_.each(
Object.keys(sysMetaValues),
function (key) {
camelCasedValues[this.sysMetaNodeMap[key]] = sysMetaValues[key];
},
this,
);
//Set the values on the model
this.set(camelCasedValues);
},
serialize: function () {
//Create an RDF serializer
var serializer = rdf.Serializer();
serializer.store = this.dataPackageGraph;
//Define the namespaces
var ORE = rdf.Namespace(this.namespaces.ORE),
CITO = rdf.Namespace(this.namespaces.CITO);
//Get the pid of this package - depends on whether we are updating or creating a resource map
var pid = this.get("id"),
oldPid = this.get("oldPid"),
updating = oldPid ? true : false;
//Update the pids in the RDF graph only if we are updating the resource map with a new pid
if (updating) {
//Find the identifier statement in the resource map
var idNode = rdf.lit(oldPid),
idStatement = this.dataPackageGraph.statementsMatching(
undefined,
undefined,
idNode,
);
//Get the CN Resolve Service base URL from the resource map (mostly important in dev environments where it will not always be cn.dataone.org)
var cnResolveUrl = idStatement[0].subject.value.substring(
0,
idStatement[0].subject.value.indexOf(oldPid),
);
this.dataPackageGraph.cnResolveUrl = cnResolveUrl;
//Create variations of the resource map ID using the resolve URL so we can always find it in the RDF graph
var oldPidVariations = [
oldPid,
encodeURIComponent(oldPid),
cnResolveUrl + encodeURIComponent(oldPid),
];
//Get all the isAggregatedBy statements
var aggregationNode = rdf.sym(
cnResolveUrl + encodeURIComponent(oldPid) + "#aggregation",
),
aggByStatements = this.dataPackageGraph.statementsMatching(
undefined,
ORE("isAggregatedBy"),
);
//Using the isAggregatedBy statements, find all the DataONE object ids in the RDF graph
var idsFromXML = [];
_.each(
aggByStatements,
function (statement) {
//Check if the resource map ID is the old existing id, so we don't collect ids that are not about this resource map
if (
_.find(oldPidVariations, function (oldPidV) {
return oldPidV + "#aggregation" == statement.object.value;
})
) {
var statementID = statement.subject.value;
idsFromXML.push(statementID);
//Add variations of the ID so we make sure we account for all the ways they exist in the RDF XML
if (statementID.indexOf(cnResolveUrl) > -1)
idsFromXML.push(
statementID.substring(statementID.lastIndexOf("/") + 1),
);
else
idsFromXML.push(
cnResolveUrl + encodeURIComponent(statementID),
);
}
},
this,
);
//Get all the ids from this model
var idsFromModel = _.invoke(this.get("members"), "get", "id");
//Find the difference between the model IDs and the XML IDs to get a list of added members
var addedIds = _.without(
_.difference(idsFromModel, idsFromXML),
oldPidVariations,
);
//Create variations of all these ids too
var allMemberIds = idsFromModel;
_.each(idsFromModel, function (id) {
allMemberIds.push(cnResolveUrl + encodeURIComponent(id));
});
//Remove any other isAggregatedBy statements that are not listed as members of this model
_.each(
aggByStatements,
function (statement) {
if (!_.contains(allMemberIds, statement.subject.value))
this.removeFromAggregation(statement.subject.value);
else if (
_.find(oldPidVariations, function (oldPidV) {
return oldPidV + "#aggregation" == statement.object.value;
})
)
statement.object.value =
cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
},
this,
);
//Change all the statements in the RDF where the aggregation is the subject, to reflect the new resource map ID
var aggregationSubjStatements =
this.dataPackageGraph.statementsMatching(aggregationNode);
_.each(aggregationSubjStatements, function (statement) {
statement.subject.value =
cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
});
//Change all the statements in the RDF where the aggregation is the object, to reflect the new resource map ID
var aggregationObjStatements =
this.dataPackageGraph.statementsMatching(
undefined,
undefined,
aggregationNode,
);
_.each(aggregationObjStatements, function (statement) {
statement.object.value =
cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
});
//Change all the resource map subject nodes in the RDF graph
var rMapNode = rdf.sym(cnResolveUrl + encodeURIComponent(oldPid));
var rMapStatements =
this.dataPackageGraph.statementsMatching(rMapNode);
_.each(rMapStatements, function (statement) {
statement.subject.value = cnResolveUrl + encodeURIComponent(pid);
});
//Change the idDescribedBy statement
var isDescribedByStatements =
this.dataPackageGraph.statementsMatching(
undefined,
ORE("isDescribedBy"),
rdf.sym(oldPid),
);
if (isDescribedByStatements[0])
isDescribedByStatements[0].object.value = pid;
//Add nodes for new package members
_.each(
addedIds,
function (id) {
this.addToAggregation(id);
},
this,
);
//Change all the resource map identifier literal node in the RDF graph
if (idStatement[0]) idStatement[0].object.value = pid;
}
//Now serialize the RDF XML
var serializer = rdf.Serializer();
serializer.store = this.dataPackageGraph;
var xmlString = serializer.statementsToXML(
this.dataPackageGraph.statements,
);
return xmlString;
},
serializeSysMeta: function () {
//Get the system metadata XML that currently exists in the system
var xml = $(this.get("sysMetaXML"));
//Update the system metadata values
xml.find("serialversion").text(this.get("serialVersion") || "0");
xml.find("identifier").text(this.get("newPid") || this.get("id"));
xml.find("formatid").text(this.get("formatId"));
xml.find("size").text(this.get("size"));
xml.find("checksum").text(this.get("checksum"));
xml
.find("submitter")
.text(
this.get("submitter") || MetacatUI.appUserModel.get("username"),
);
xml
.find("rightsholder")
.text(
this.get("rightsHolder") || MetacatUI.appUserModel.get("username"),
);
xml.find("archived").text(this.get("archived"));
xml
.find("dateuploaded")
.text(this.get("dateUploaded") || new Date().toISOString());
xml
.find("datesysmetadatamodified")
.text(
this.get("dateSysMetadataModified") || new Date().toISOString(),
);
xml
.find("originmembernode")
.text(
this.get("originMemberNode") ||
MetacatUI.nodeModel.get("currentMemberNode"),
);
xml
.find("authoritativemembernode")
.text(
this.get("authoritativeMemberNode") ||
MetacatUI.nodeModel.get("currentMemberNode"),
);
if (this.get("obsoletes"))
xml.find("obsoletes").text(this.get("obsoletes"));
else xml.find("obsoletes").remove();
if (this.get("obsoletedBy"))
xml.find("obsoletedby").text(this.get("obsoletedBy"));
else xml.find("obsoletedby").remove();
//Write the access policy
var accessPolicyXML = "<accessPolicy>\n";
_.each(this.get("accesspolicy"), function (policy, policyType, all) {
var fullPolicy = all[policyType];
_.each(fullPolicy, function (policyPart) {
accessPolicyXML += "\t<" + policyType + ">\n";
accessPolicyXML +=
"\t\t<subject>" + policyPart.subject + "</subject>\n";
var permissions = Array.isArray(policyPart.permission)
? policyPart.permission
: [policyPart.permission];
_.each(permissions, function (perm) {
accessPolicyXML += "\t\t<permission>" + perm + "</permission>\n";
});
accessPolicyXML += "\t</" + policyType + ">\n";
});
});
accessPolicyXML += "</accessPolicy>";
//Replace the old access policy with the new one
xml.find("accesspolicy").replaceWith(accessPolicyXML);
var xmlString = $(document.createElement("div"))
.append(xml.clone())
.html();
//Now camel case the nodes
_.each(
Object.keys(this.sysMetaNodeMap),
function (name, i, allNodeNames) {
var regEx = new RegExp("<" + name, "g");
xmlString = xmlString.replace(
regEx,
"<" + this.sysMetaNodeMap[name],
);
var regEx = new RegExp(name + ">", "g");
xmlString = xmlString.replace(
regEx,
this.sysMetaNodeMap[name] + ">",
);
},
this,
);
xmlString = xmlString.replace(/systemmetadata/g, "systemMetadata");
return xmlString;
},
//Adds a new object to the resource map RDF graph
addToAggregation: function (id) {
if (id.indexOf(this.dataPackageGraph.cnResolveUrl) < 0)
var fullID =
this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id);
else {
var fullID = id;
id = id.substring(
this.dataPackageGraph.cnResolveUrl.lastIndexOf("/") + 1,
);
}
//Initialize the namespaces
var ORE = rdf.Namespace(this.namespaces.ORE),
DCTERMS = rdf.Namespace(this.namespaces.DCTERMS),
XML = rdf.Namespace(this.namespaces.XML),
CITO = rdf.Namespace(this.namespaces.CITO);
//Create a node for this object, the identifier, the resource map, and the aggregation
var objectNode = rdf.sym(fullID),
mapNode = rdf.sym(
this.dataPackageGraph.cnResolveUrl +
encodeURIComponent(this.get("id")),
),
aggNode = rdf.sym(
this.dataPackageGraph.cnResolveUrl +
encodeURIComponent(this.get("id")) +
"#aggregation",
),
idNode = rdf.literal(id, undefined, XML("string"));
//Add the statement: this object isAggregatedBy the resource map aggregation
this.dataPackageGraph.addStatement(
rdf.st(objectNode, ORE("isAggregatedBy"), aggNode),
);
//Add the statement: The resource map aggregation aggregates this object
this.dataPackageGraph.addStatement(
rdf.st(aggNode, ORE("aggregates"), objectNode),
);
//Add the statement: This object has the identifier {id}
this.dataPackageGraph.addStatement(
rdf.st(objectNode, DCTERMS("identifier"), idNode),
);
//Find the metadata doc that describes this object
var model = _.find(this.get("members"), function (m) {
return m.get("id") == id;
}),
isDocBy = model.get("isDocumentedBy");
//If this object is documented by any metadata...
if (isDocBy) {
//Get the ids of all the metadata objects in this package
var metadataInPackage = _.compact(
_.map(this.get("members"), function (m) {
if (m.get("formatType") == "METADATA") return m.get("id");
}),
);
//Find the metadata IDs that are in this package that also documents this data object
var metadataIds = Array.isArray(isDocBy)
? _.intersection(metadataInPackage, isDocBy)
: _.intersection(metadataInPackage, [isDocBy]);
//For each metadata that documents this object, add a CITO:isDocumentedBy and CITO:documents statement
_.each(
metadataIds,
function (metaId) {
//Create the named nodes and statements
var memberNode = rdf.sym(
this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id),
),
metadataNode = rdf.sym(
this.dataPackageGraph.cnResolveUrl +
encodeURIComponent(metaId),
),
isDocByStatement = rdf.st(
memberNode,
CITO("isDocumentedBy"),
metadataNode,
),
documentsStatement = rdf.st(
metadataNode,
CITO("documents"),
memberNode,
);
//Add the statements
this.dataPackageGraph.addStatement(isDocByStatement);
this.dataPackageGraph.addStatement(documentsStatement);
},
this,
);
}
},
removeFromAggregation: function (id) {
if (!id.indexOf(this.dataPackageGraph.cnResolveUrl))
id = this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id);
var removedObjNode = rdf.sym(id),
statements = _.union(
this.dataPackageGraph.statementsMatching(
undefined,
undefined,
removedObjNode,
),
this.dataPackageGraph.statementsMatching(removedObjNode),
);
this.dataPackageGraph.removeStatements(statements);
},
getParentMetadata: function () {
var rMapIds = this.get("resourceMap");
//Create a query that searches for any resourceMap with an id matching one of the parents OR an id that matches one of the parents.
//This will return all members of the parent resource maps AND the parent resource maps themselves
var rMapQuery = "",
idQuery = "";
if (Array.isArray(rMapIds) && rMapIds.length > 1) {
_.each(rMapIds, function (id, i, ids) {
//At the begininng of the list of ids
if (rMapQuery.length == 0) {
rMapQuery += "resourceMap:(";
idQuery += "id:(";
}
//The id
rMapQuery += "%22" + encodeURIComponent(id) + "%22";
idQuery += "%22" + encodeURIComponent(id) + "%22";
//At the end of the list of ids
if (i + 1 == ids.length) {
rMapQuery += ")";
idQuery += ")";
}
//In-between each id
else {
rMapQuery += " OR ";
idQuery += " OR ";
}
});
} else {
//When there is just one parent, the query is simple
var rMapId = Array.isArray(rMapIds) ? rMapIds[0] : rMapIds;
rMapQuery += "resourceMap:%22" + encodeURIComponent(rMapId) + "%22";
idQuery += "id:%22" + encodeURIComponent(rMapId) + "%22";
}
var query =
"fl=title,id,obsoletedBy,resourceMap" +
"&wt=json" +
"&group=true&group.field=formatType&group.limit=-1" +
"&q=((formatType:METADATA AND " +
rMapQuery +
") OR " +
idQuery +
")";
var model = this;
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
var results = data.grouped.formatType.groups,
resourceMapGroup = _.where(results, {
groupValue: "RESOURCE",
})[0],
rMapList = resourceMapGroup ? resourceMapGroup.doclist : null,
rMaps = rMapList ? rMapList.docs : [],
rMapIds = _.pluck(rMaps, "id"),
parents = [],
parentIds = [];
//As long as this map isn't obsoleted by another map in our results list, we will show it
_.each(rMaps, function (map) {
if (!(map.obsoletedBy && _.contains(rMapIds, map.obsoletedBy))) {
parents.push(map);
parentIds.push(map.id);
}
});
var metadataList = _.where(results, { groupValue: "METADATA" })[0],
metadata =
metadataList && metadataList.doclist
? metadataList.doclist.docs
: [],
metadataModels = [];
//As long as this map isn't obsoleted by another map in our results list, we will show it
_.each(metadata, function (m) {
//Find the metadata doc that obsoletes this one
var isObsoletedBy = _.findWhere(metadata, { id: m.obsoletedBy });
//If one isn't found, then this metadata doc is the most recent
if (typeof isObsoletedBy == "undefined") {
//If this metadata doc is in one of the filtered parent resource maps
if (_.intersection(parentIds, m.resourceMap).length) {
//Create a SolrResult model and add to an array
metadataModels.push(new SolrResult(m));
}
}
});
model.set("parentPackageMetadata", metadataModels);
model.trigger("change:parentPackageMetadata");
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
//Create the URL string that is used to download this package
getURL: function () {
var url = null;
//If we haven't set a packageServiceURL upon app initialization and we are querying a CN, then the packageServiceURL is dependent on the MN this package is from
if (
MetacatUI.appModel.get("d1Service").toLowerCase().indexOf("cn/") >
-1 &&
MetacatUI.nodeModel.get("members").length
) {
var source = this.get("datasource"),
node = _.find(MetacatUI.nodeModel.get("members"), {
identifier: source,
});
//If this node has MNRead v2 services...
if (node && node.readv2)
url =
node.baseURL +
"/v2/packages/application%2Fbagit-097/" +
encodeURIComponent(this.get("id"));
} else if (MetacatUI.appModel.get("packageServiceUrl"))
url =
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(this.get("id"));
this.set("url", url);
return url;
},
createNestedPackages: function () {
var parentPackage = this,
nestedPackages = this.getNestedPackages(),
numNestedPackages = nestedPackages.length,
numComplete = 0;
_.each(nestedPackages, function (nestedPackage, i, nestedPackages) {
//Flag the parent model as complete when all the nested package info is ready
nestedPackage.on("complete", function () {
numComplete++;
//This is the last package in this package - finish up details and flag as complete
if (numNestedPackages == numComplete) {
var sorted = _.sortBy(parentPackage.get("members"), function (p) {
return p.get("id");
});
parentPackage.set("members", sorted);
parentPackage.flagComplete();
}
});
//Only look one-level deep at all times to avoid going down a rabbit hole
if (
nestedPackage.get("parentPackage") &&
nestedPackage.get("parentPackage").get("parentPackage")
) {
nestedPackage.flagComplete();
return;
} else {
//Get the members of this nested package
nestedPackage.getMembers();
}
});
},
getNestedPackages: function () {
return _.where(this.get("members"), { type: "Package" });
},
getMemberNames: function () {
var metadata = this.getMetadata();
if (!metadata) return false;
//Load the rendered metadata from the view service
var viewService =
MetacatUI.appModel.get("viewServiceUrl") +
encodeURIComponent(metadata.get("id"));
var requestSettings = {
url: viewService,
success: function (data, response, xhr) {
if (solrResult.get("formatType") == "METADATA")
entityName = solrResult.get("title");
else {
var container = viewRef.findEntityDetailsContainer(
solrResult.get("id"),
);
if (container && container.length > 0) {
var entityName = $(container)
.find(".entityName")
.attr("data-entity-name");
if (typeof entityName === "undefined" || !entityName) {
entityName = $(container)
.find(
".control-label:contains('Entity Name') + .controls-well",
)
.text();
if (typeof entityName === "undefined" || !entityName)
entityName = null;
}
} else entityName = null;
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/*
* Will query for the derivations of this package, and sort all entities in the prov trace
* into sources and derivations.
*/
getProvTrace: function () {
var model = this;
//See if there are any prov fields in our index before continuing
if (!MetacatUI.appSearchModel.getProvFields()) return this;
//Start keeping track of the sources and derivations
var sources = new Array(),
derivations = new Array();
//Search for derivations of this package
var derivationsQuery =
MetacatUI.appSearchModel.getGroupedQuery(
"prov_wasDerivedFrom",
_.map(this.get("members"), function (m) {
return m.get("id");
}),
"OR",
) + "%20-obsoletedBy:*";
var requestSettings = {
url:
MetacatUI.appModel.get("queryServiceUrl") +
"&q=" +
derivationsQuery +
"&wt=json&rows=1000" +
"&fl=id,resourceMap,documents,isDocumentedBy,prov_wasDerivedFrom",
success: function (data) {
_.each(data.response.docs, function (result) {
derivations.push(result.id);
});
//Make arrays of unique IDs of objects that are sources or derivations of this package.
_.each(model.get("members"), function (member, i) {
if (member.type == "Package") return;
if (member.hasProvTrace()) {
sources = _.union(sources, member.getSources());
derivations = _.union(derivations, member.getDerivations());
}
});
//Save the arrays of sources and derivations
model.set("sources", sources);
model.set("derivations", derivations);
//Now get metadata about all the entities in the prov trace not in this package
model.getExternalProvTrace();
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
getExternalProvTrace: function () {
var model = this;
//Compact our list of ids that are in the prov trace by combining the sources and derivations and removing ids of members of this package
var externalProvEntities = _.difference(
_.union(this.get("sources"), this.get("derivations")),
this.get("memberIds"),
);
//If there are no sources or derivations, then we do not need to find resource map ids for anything
if (!externalProvEntities.length) {
//Save this prov trace on a package-member/document/object level.
if (this.get("sources").length || this.get("derivations").length)
this.setMemberProvTrace();
//Flag that the provenance trace is complete
this.set("provenanceFlag", "complete");
return this;
} else {
//Create a query where we retrieve the ID of the resource map of each source and derivation
var idQuery = MetacatUI.appSearchModel.getGroupedQuery(
"id",
externalProvEntities,
"OR",
);
//Create a query where we retrieve the metadata for each source and derivation
var metadataQuery = MetacatUI.appSearchModel.getGroupedQuery(
"documents",
externalProvEntities,
"OR",
);
}
//TODO: Find the products of programs/executions
//Make a comma-separated list of the provenance field names
var provFieldList = "";
_.each(
MetacatUI.appSearchModel.getProvFields(),
function (fieldName, i, list) {
provFieldList += fieldName;
if (i < list.length - 1) provFieldList += ",";
},
);
//Combine the two queries with an OR operator
if (idQuery.length && metadataQuery.length)
var combinedQuery = idQuery + "%20OR%20" + metadataQuery;
else return this;
//the full and final query in Solr syntax
var query =
"q=" +
combinedQuery +
"&fl=id,resourceMap,documents,isDocumentedBy,formatType,formatId,dateUploaded,rightsHolder,datasource,prov_instanceOfClass," +
provFieldList +
"&rows=100&wt=json";
//Send the query to the query service
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
//Do any of our docs have multiple resource maps?
var hasMultipleMaps = _.filter(data.response.docs, function (doc) {
return (
typeof doc.resourceMap !== "undefined" &&
doc.resourceMap.length > 1
);
});
//If so, we want to find the latest version of each resource map and only represent that one in the Prov Chart
if (typeof hasMultipleMaps !== "undefined") {
var allMapIDs = _.uniq(
_.flatten(_.pluck(hasMultipleMaps, "resourceMap")),
);
if (allMapIDs.length) {
var query =
"q=+-obsoletedBy:*+" +
MetacatUI.appSearchModel.getGroupedQuery(
"id",
allMapIDs,
"OR",
) +
"&fl=obsoletes,id" +
"&wt=json";
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (mapData, textStatus, xhr) {
//Create a list of resource maps that are not obsoleted by any other resource map retrieved
var resourceMaps = mapData.response.docs;
model.obsoletedResourceMaps = _.pluck(
resourceMaps,
"obsoletes",
);
model.latestResourceMaps = _.difference(
resourceMaps,
model.obsoletedResourceMaps,
);
model.sortProvTrace(data.response.docs);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
} else model.sortProvTrace(data.response.docs);
} else model.sortProvTrace(data.response.docs);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
return this;
},
sortProvTrace: function (docs) {
var model = this;
//Start an array to hold the packages in the prov trace
var sourcePackages = new Array(),
derPackages = new Array(),
sourceDocs = new Array(),
derDocs = new Array(),
sourceIDs = this.get("sources"),
derivationIDs = this.get("derivations");
//Separate the results into derivations and sources and group by their resource map.
_.each(docs, function (doc, i) {
var docModel = new SolrResult(doc),
mapIds = docModel.get("resourceMap");
if (
(typeof mapIds === "undefined" || !mapIds) &&
docModel.get("formatType") == "DATA" &&
(typeof docModel.get("isDocumentedBy") === "undefined" ||
!docModel.get("isDocumentedBy"))
) {
//If this object is not in a resource map and does not have metadata, it is a "naked" data doc, so save it by itself
if (_.contains(sourceIDs, doc.id)) sourceDocs.push(docModel);
if (_.contains(derivationIDs, doc.id)) derDocs.push(docModel);
} else if (
(typeof mapIds === "undefined" || !mapIds) &&
docModel.get("formatType") == "DATA" &&
docModel.get("isDocumentedBy")
) {
//If this data doc does not have a resource map but has a metadata doc that documents it, create a blank package model and save it
var p = new PackageModel({
members: new Array(docModel),
});
//Add this package model to the sources and/or derivations packages list
if (_.contains(sourceIDs, docModel.get("id")))
sourcePackages[docModel.get("id")] = p;
if (_.contains(derivationIDs, docModel.get("id")))
derPackages[docModel.get("id")] = p;
} else if (mapIds.length) {
//If this doc has a resource map, create a package model and SolrResult model and store it
var id = docModel.get("id");
//Some of these objects may have multiple resource maps
_.each(mapIds, function (mapId, i, list) {
if (!_.contains(model.obsoletedResourceMaps, mapId)) {
var documentsSource, documentsDerivation;
if (docModel.get("formatType") == "METADATA") {
if (
_.intersection(docModel.get("documents"), sourceIDs).length
)
documentsSource = true;
if (
_.intersection(docModel.get("documents"), derivationIDs)
.length
)
documentsDerivation = true;
}
//Is this a source object or a metadata doc of a source object?
if (_.contains(sourceIDs, id) || documentsSource) {
//Have we encountered this source package yet?
if (!sourcePackages[mapId] && mapId != model.get("id")) {
//Now make a new package model for it
var p = new PackageModel({
id: mapId,
members: new Array(docModel),
});
//Add to the array of source packages
sourcePackages[mapId] = p;
}
//If so, add this member to its package model
else if (mapId != model.get("id")) {
var memberList = sourcePackages[mapId].get("members");
memberList.push(docModel);
sourcePackages[mapId].set("members", memberList);
}
}
//Is this a derivation object or a metadata doc of a derivation object?
if (_.contains(derivationIDs, id) || documentsDerivation) {
//Have we encountered this derivation package yet?
if (!derPackages[mapId] && mapId != model.get("id")) {
//Now make a new package model for it
var p = new PackageModel({
id: mapId,
members: new Array(docModel),
});
//Add to the array of source packages
derPackages[mapId] = p;
}
//If so, add this member to its package model
else if (mapId != model.get("id")) {
var memberList = derPackages[mapId].get("members");
memberList.push(docModel);
derPackages[mapId].set("members", memberList);
}
}
}
});
}
});
//Transform our associative array (Object) of packages into an array
var newArrays = new Array();
_.each(
new Array(sourcePackages, derPackages, sourceDocs, derDocs),
function (provObject) {
var newArray = new Array(),
key;
for (key in provObject) {
newArray.push(provObject[key]);
}
newArrays.push(newArray);
},
);
//We now have an array of source packages and an array of derivation packages.
model.set("sourcePackages", newArrays[0]);
model.set("derivationPackages", newArrays[1]);
model.set("sourceDocs", newArrays[2]);
model.set("derivationDocs", newArrays[3]);
//Save this prov trace on a package-member/document/object level.
model.setMemberProvTrace();
//Flag that the provenance trace is complete
model.set("provenanceFlag", "complete");
},
setMemberProvTrace: function () {
var model = this,
relatedModels = this.get("relatedModels"),
relatedModelIDs = new Array();
//Now for each doc, we want to find which member it is related to
_.each(this.get("members"), function (member, i, members) {
if (member.type == "Package") return;
//Get the sources and derivations of this member
var memberSourceIDs = member.getSources();
var memberDerIDs = member.getDerivations();
//Look through each source package, derivation package, source doc, and derivation doc.
_.each(model.get("sourcePackages"), function (pkg, i) {
_.each(pkg.get("members"), function (sourcePkgMember, i) {
//Is this package member a direct source of this package member?
if (_.contains(memberSourceIDs, sourcePkgMember.get("id")))
//Save this source package member as a source of this member
member.set(
"provSources",
_.union(member.get("provSources"), [sourcePkgMember]),
);
//Save this in the list of related models
if (!_.contains(relatedModelIDs, sourcePkgMember.get("id"))) {
relatedModels.push(sourcePkgMember);
relatedModelIDs.push(sourcePkgMember.get("id"));
}
});
});
_.each(model.get("derivationPackages"), function (pkg, i) {
_.each(pkg.get("members"), function (derPkgMember, i) {
//Is this package member a direct source of this package member?
if (_.contains(memberDerIDs, derPkgMember.get("id")))
//Save this derivation package member as a derivation of this member
member.set(
"provDerivations",
_.union(member.get("provDerivations"), [derPkgMember]),
);
//Save this in the list of related models
if (!_.contains(relatedModelIDs, derPkgMember.get("id"))) {
relatedModels.push(derPkgMember);
relatedModelIDs.push(derPkgMember.get("id"));
}
});
});
_.each(model.get("sourceDocs"), function (doc, i) {
//Is this package member a direct source of this package member?
if (_.contains(memberSourceIDs, doc.get("id")))
//Save this source package member as a source of this member
member.set(
"provSources",
_.union(member.get("provSources"), [doc]),
);
//Save this in the list of related models
if (!_.contains(relatedModelIDs, doc.get("id"))) {
relatedModels.push(doc);
relatedModelIDs.push(doc.get("id"));
}
});
_.each(model.get("derivationDocs"), function (doc, i) {
//Is this package member a direct derivation of this package member?
if (_.contains(memberDerIDs, doc.get("id")))
//Save this derivation package member as a derivation of this member
member.set(
"provDerivations",
_.union(member.get("provDerivations"), [doc]),
);
//Save this in the list of related models
if (!_.contains(relatedModelIDs, doc.get("id"))) {
relatedModels.push(doc);
relatedModelIDs.push(doc.get("id"));
}
});
_.each(members, function (otherMember, i) {
//Is this other package member a direct derivation of this package member?
if (_.contains(memberDerIDs, otherMember.get("id")))
//Save this other derivation package member as a derivation of this member
member.set(
"provDerivations",
_.union(member.get("provDerivations"), [otherMember]),
);
//Is this other package member a direct source of this package member?
if (_.contains(memberSourceIDs, otherMember.get("id")))
//Save this other source package member as a source of this member
member.set(
"provSources",
_.union(member.get("provSources"), [otherMember]),
);
//Is this other package member an indirect source or derivation?
if (
otherMember.get("type") == "program" &&
_.contains(
member.get("prov_generatedByProgram"),
otherMember.get("id"),
)
) {
var indirectSources = _.filter(members, function (m) {
return _.contains(otherMember.getInputs(), m.get("id"));
});
indirectSourcesIds = _.each(indirectSources, function (m) {
return m.get("id");
});
member.set(
"prov_wasDerivedFrom",
_.union(member.get("prov_wasDerivedFrom"), indirectSourcesIds),
);
//otherMember.set("prov_hasDerivations", _.union(otherMember.get("prov_hasDerivations"), [member.get("id")]));
member.set(
"provSources",
_.union(member.get("provSources"), indirectSources),
);
}
if (
otherMember.get("type") == "program" &&
_.contains(
member.get("prov_usedByProgram"),
otherMember.get("id"),
)
) {
var indirectDerivations = _.filter(members, function (m) {
return _.contains(otherMember.getOutputs(), m.get("id"));
});
indirectDerivationsIds = _.each(
indirectDerivations,
function (m) {
return m.get("id");
},
);
member.set(
"prov_hasDerivations",
_.union(
member.get("prov_hasDerivations"),
indirectDerivationsIds,
),
);
//otherMember.set("prov_wasDerivedFrom", _.union(otherMember.get("prov_wasDerivedFrom"), [member.get("id")]));
member.set(
"provDerivations",
_.union(member.get("provDerivations"), indirectDerivationsIds),
);
}
});
//Add this member to the list of related models
if (!_.contains(relatedModelIDs, member.get("id"))) {
relatedModels.push(member);
relatedModelIDs.push(member.get("id"));
}
//Clear out any duplicates
member.set("provSources", _.uniq(member.get("provSources")));
member.set("provDerivations", _.uniq(member.get("provDerivations")));
});
//Update the list of related models
this.set("relatedModels", relatedModels);
},
downloadWithCredentials: function () {
//Get info about this object
var url = this.get("url"),
model = this;
//Create an XHR
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
xhr.onload = function () {
//Get the file name from the Content-Disposition header
var filename = xhr.getResponseHeader("Content-Disposition");
//As a backup, use the system metadata file name or the id
if (!filename) {
filename = model.get("filename") || model.get("id");
}
//Add a ".zip" extension if it doesn't exist
if (
filename.indexOf(".zip") < 0 ||
filename.indexOf(".zip") != filename.length - 4
) {
filename += ".zip";
}
//For IE, we need to use the navigator API
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(xhr.response, filename);
} else {
var a = document.createElement("a");
a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
a.download = filename; // Set the file name.
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}
model.trigger("downloadComplete");
// Track this event
MetacatUI.analytics?.trackEvent(
"download",
"Download Package",
model.get("id"),
);
};
xhr.onprogress = function (e) {
if (e.lengthComputable) {
var percent = (e.loaded / e.total) * 100;
model.set("downloadPercent", percent);
}
};
xhr.onerror = function (e) {
model.trigger("downloadError");
// Track this event
MetacatUI.analytics?.trackEvent(
"download",
"Download Package",
model.get("id"),
);
};
//Open and send the request with the user's auth token
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.setRequestHeader(
"Authorization",
"Bearer " + MetacatUI.appUserModel.get("token"),
);
xhr.send();
},
/* Returns the SolrResult that represents the metadata doc */
getMetadata: function () {
var members = this.get("members");
for (var i = 0; i < members.length; i++) {
if (members[i].get("formatType") == "METADATA") return members[i];
}
//If there are no metadata objects in this package, make sure we have searched for them already
if (!this.complete && !this.pending) this.getMembers();
return false;
},
//Check authority of the Metadata SolrResult model instead
checkAuthority: function () {
//Call the auth service
var authServiceUrl = MetacatUI.appModel.get("authServiceUrl");
if (!authServiceUrl) return false;
var model = this;
var requestSettings = {
url:
authServiceUrl +
encodeURIComponent(this.get("id")) +
"?action=write",
type: "GET",
success: function (data, textStatus, xhr) {
model.set("isAuthorized", true);
model.trigger("change:isAuthorized");
},
error: function (xhr, textStatus, errorThrown) {
model.set("isAuthorized", false);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
flagComplete: function () {
this.complete = true;
this.pending = false;
this.trigger("complete", this);
},
/*
* function xmlToJson - A utility function for converting XML to JSON
*
* @param xml {DOM Element} - An XML or HTML DOM element to convert to json
* @returns {object} - A literal JS object that represents the given XML
*/
toJson: function (xml) {
// Create the return object
var obj = {};
// do children
if (xml.hasChildNodes()) {
for (var i = 0; i < xml.childNodes.length; i++) {
var item = xml.childNodes.item(i);
//If it's an empty text node, skip it
if (item.nodeType == 3 && !item.nodeValue.trim()) continue;
//Get the node name
var nodeName = item.localName;
//If it's a new container node, convert it to JSON and add as a new object attribute
if (typeof obj[nodeName] == "undefined" && item.nodeType == 1) {
obj[nodeName] = this.toJson(item);
}
//If it's a new text node, just store the text value and add as a new object attribute
else if (
typeof obj[nodeName] == "undefined" &&
item.nodeType == 3
) {
obj = item.nodeValue;
}
//If this node name is already stored as an object attribute...
else if (typeof obj[nodeName] != "undefined") {
//Cache what we have now
var old = obj[nodeName];
if (!Array.isArray(old)) old = [old];
//Create a new object to store this node info
var newNode = {};
//Add the new node info to the existing array we have now
if (item.nodeType == 1) {
newNode = this.toJson(item);
var newArray = old.concat(newNode);
} else if (item.nodeType == 3) {
newNode = item.nodeValue;
var newArray = old.concat(newNode);
}
//Store the attributes for this node
_.each(item.attributes, function (attr) {
newNode[attr.localName] = attr.nodeValue;
});
//Replace the old array with the updated one
obj[nodeName] = newArray;
//Exit
continue;
}
//Store the attributes for this node
/*_.each(item.attributes, function(attr){
obj[nodeName][attr.localName] = attr.nodeValue;
});*/
}
}
return obj;
},
//Sums up the byte size of each member
getTotalSize: function () {
if (this.get("totalSize")) return this.get("totalSize");
if (this.get("members").length == 1) {
var totalSize = this.get("members")[0].get("size");
} else {
var totalSize = _.reduce(this.get("members"), function (sum, member) {
if (typeof sum == "object") sum = sum.get("size");
return sum + member.get("size");
});
}
this.set("totalSize", totalSize);
return totalSize;
},
},
);
return PackageModel;
});

Copy link
Contributor

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.

var PackageModel = Backbone.Model.extend(
/** @lends PackageModel.prototype */ {
// This model contains information about a package/resource map
defaults: function () {
return {
id: null, //The id of the resource map/package itself
url: null, //the URL to retrieve this package
memberId: null, //An id of a member of the data package
indexDoc: null, //A SolrResult object representation of the resource map
size: 0, //The number of items aggregated in this package
totalSize: null,
formattedSize: "",
formatId: null,
obsoletedBy: null,
obsoletes: null,
read_count_i: null,
isPublic: true,
members: [],
memberIds: [],
sources: [],
derivations: [],
provenanceFlag: null,
sourcePackages: [],
derivationPackages: [],
sourceDocs: [],
derivationDocs: [],
relatedModels: [], //A condensed list of all SolrResult models related to this package in some way
parentPackageMetadata: null,
//If true, when the member objects are retrieved, archived content will be included
getArchivedMembers: false,
};
},
//Define the namespaces
namespaces: {
RDF: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
FOAF: "http://xmlns.com/foaf/0.1/",
OWL: "http://www.w3.org/2002/07/owl#",
DC: "http://purl.org/dc/elements/1.1/",
ORE: "http://www.openarchives.org/ore/terms/",
DCTERMS: "http://purl.org/dc/terms/",
CITO: "http://purl.org/spar/cito/",
XML: "http://www.w3.org/2001/XMLSchema#",
},
sysMetaNodeMap: {
accesspolicy: "accessPolicy",
accessrule: "accessRule",
authoritativemembernode: "authoritativeMemberNode",
dateuploaded: "dateUploaded",
datesysmetadatamodified: "dateSysMetadataModified",
dateuploaded: "dateUploaded",
formatid: "formatId",
nodereference: "nodeReference",
obsoletedby: "obsoletedBy",
originmembernode: "originMemberNode",
replicamembernode: "replicaMemberNode",
replicapolicy: "replicaPolicy",
replicationstatus: "replicationStatus",
replicaverified: "replicaVerified",
rightsholder: "rightsHolder",
serialversion: "serialVersion",
},
complete: false,
pending: false,
type: "Package",
// The RDF graph representing this data package
dataPackageGraph: null,
initialize: function (options) {
this.setURL();
// Create an initial RDF graph
this.dataPackageGraph = rdf.graph();
},
setURL: function () {
if (MetacatUI.appModel.get("packageServiceUrl"))
this.set(
"url",
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(this.get("id")),
);
},
/*
* Set the URL for fetch
*/
url: function () {
return (
MetacatUI.appModel.get("objectServiceUrl") +
encodeURIComponent(this.get("id"))
);
},
/* Retrieve the id of the resource map/package that this id belongs to */
getMembersByMemberID: function (id) {
this.pending = true;
if (typeof id === "undefined" || !id) var id = this.memberId;
var model = this;
//Get the id of the resource map for this member
var provFlList =
MetacatUI.appSearchModel.getProvFlList() + "prov_instanceOfClass,";
var query =
"fl=resourceMap,fileName,read:read_count_i,obsoletedBy,size,formatType,formatId,id,datasource,title,origin,pubDate,dateUploaded,isPublic,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription," +
provFlList +
"&rows=1" +
"&q=id:%22" +
encodeURIComponent(id) +
"%22" +
"&wt=json";
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
//There should be only one response since we searched by id
if (typeof data.response.docs !== "undefined") {
var doc = data.response.docs[0];
//Is this document a resource map itself?
if (doc.formatId == "http://www.openarchives.org/ore/terms") {
model.set("id", doc.id); //this is the package model ID
model.set("members", new Array()); //Reset the member list
model.getMembers();
}
//If there is no resource map, then this is the only document to in this package
else if (
typeof doc.resourceMap === "undefined" ||
!doc.resourceMap
) {
model.set("id", null);
model.set("memberIds", new Array(doc.id));
model.set("members", [new SolrResult(doc)]);
model.trigger("change:members");
model.flagComplete();
} else {
model.set("id", doc.resourceMap[0]);
model.getMembers();
}
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/* Get all the members of a resource map/package based on the id attribute of this model.
* Create a SolrResult model for each member and save it in the members[] attribute of this model. */
getMembers: function (options) {
this.pending = true;
var model = this,
members = [],
pids = []; //Keep track of each object pid
//*** Find all the files that are a part of this resource map and the resource map itself
var provFlList = MetacatUI.appSearchModel.getProvFlList();
var query =
"fl=resourceMap,fileName,obsoletes,obsoletedBy,size,formatType,formatId,id,datasource," +
"rightsHolder,dateUploaded,archived,title,origin,prov_instanceOfClass,isDocumentedBy,isPublic" +
"&rows=1000" +
"&q=%28resourceMap:%22" +
encodeURIComponent(this.id) +
"%22%20OR%20id:%22" +
encodeURIComponent(this.id) +
"%22%29" +
"&wt=json";
if (this.get("getArchivedMembers")) {
query += "&archived=archived:*";
}
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
//Separate the resource maps from the data/metadata objects
_.each(data.response.docs, function (doc) {
if (doc.id == model.get("id")) {
model.set("indexDoc", doc);
model.set(doc);
if (
model.get("resourceMap") &&
options &&
options.getParentMetadata
)
model.getParentMetadata();
} else {
pids.push(doc.id);
if (doc.formatType == "RESOURCE") {
var newPckg = new PackageModel(doc);
newPckg.set("parentPackage", model);
members.push(newPckg);
} else members.push(new SolrResult(doc));
}
});
model.set("memberIds", _.uniq(pids));
model.set("members", members);
if (model.getNestedPackages().length > 0)
model.createNestedPackages();
else model.flagComplete();
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
return this;
},
/*
* Send custom options to the Backbone.Model.fetch() function
*/
fetch: function (options) {
if (!options) var options = {};
var fetchOptions = _.extend({ dataType: "text" }, options);
//Add the authorization options
fetchOptions = _.extend(
fetchOptions,
MetacatUI.appUserModel.createAjaxSettings(),
);
return Backbone.Model.prototype.fetch.call(this, fetchOptions);
},
/*
* Deserialize a Package from OAI-ORE RDF XML
*/
parse: function (response, options) {
//Save the raw XML in case it needs to be used later
this.set("objectXML", $.parseHTML(response));
//Define the namespaces
var RDF = rdf.Namespace(this.namespaces.RDF),
FOAF = rdf.Namespace(this.namespaces.FOAF),
OWL = rdf.Namespace(this.namespaces.OWL),
DC = rdf.Namespace(this.namespaces.DC),
ORE = rdf.Namespace(this.namespaces.ORE),
DCTERMS = rdf.Namespace(this.namespaces.DCTERMS),
CITO = rdf.Namespace(this.namespaces.CITO);
var memberStatements = [],
memberURIParts,
memberPIDStr,
memberPID,
memberModel,
models = []; // the models returned by parse()
try {
rdf.parse(
response,
this.dataPackageGraph,
MetacatUI.appModel.get("objectServiceUrl") +
(encodeURIComponent(this.id) ||
encodeURIComponent(this.seriesid)),
"application/rdf+xml",
);
// List the package members
memberStatements = this.dataPackageGraph.statementsMatching(
undefined,
ORE("aggregates"),
undefined,
undefined,
);
var memberPIDs = [],
members = [],
currentMembers = this.get("members"),
model = this;
// Get system metadata for each member to eval the formatId
_.each(
memberStatements,
function (memberStatement) {
memberURIParts = memberStatement.object.value.split("/");
memberPIDStr = _.last(memberURIParts);
memberPID = decodeURIComponent(memberPIDStr);
if (memberPID) {
memberPIDs.push(memberPID);
//Get the current model from the member list, if it exists
var existingModel = _.find(currentMembers, function (m) {
return m.get("id") == decodeURIComponent(memberPID);
});
//Add the existing model to the new member list
if (existingModel) {
members.push(existingModel);
}
//Or create a new SolrResult model
else {
members.push(
new SolrResult({
id: decodeURIComponent(memberPID),
}),
);
}
}
},
this,
);
//Get the documents relationships
var documentedByStatements = this.dataPackageGraph.statementsMatching(
undefined,
CITO("isDocumentedBy"),
undefined,
undefined,
),
metadataPids = [];
_.each(
documentedByStatements,
function (statement) {
//Get the data object that is documentedBy metadata
var dataPid = decodeURIComponent(
_.last(statement.subject.value.split("/")),
),
dataObj = _.find(members, function (m) {
return m.get("id") == dataPid;
}),
metadataPid = _.last(statement.object.value.split("/"));
//Save this as a metadata model
metadataPids.push(metadataPid);
//Set the isDocumentedBy field
var isDocBy = dataObj.get("isDocumentedBy");
if (isDocBy && Array.isArray(isDocBy)) isDocBy.push(metadataPid);
else if (isDocBy && !Array.isArray(isDocBy))
isDocBy = [isDocBy, metadataPid];
else isDocBy = [metadataPid];
dataObj.set("isDocumentedBy", isDocBy);
},
this,
);
//Get the metadata models and mark them as metadata
var metadataModels = _.filter(members, function (m) {
return _.contains(metadataPids, m.get("id"));
});
_.invoke(metadataModels, "set", "formatType", "METADATA");
//Keep the pids in the collection for easy access later
this.set("memberIds", memberPIDs);
this.set("members", members);
} catch (error) {
console.log(error);
}
return models;
},
/*
* Overwrite the Backbone.Model.save() function to set custom options
*/
save: function (attrs, options) {
if (!options) var options = {};
//Get the system metadata first
if (!this.get("hasSystemMetadata")) {
var model = this;
var requestSettings = {
url:
MetacatUI.appModel.get("metaServiceUrl") +
encodeURIComponent(this.get("id")),
success: function (response) {
model.parseSysMeta(response);
model.set("hasSystemMetadata", true);
model.save.call(model, null, options);
},
dataType: "text",
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
return;
}
//Create a new pid if we are updating the object
if (!options.sysMetaOnly) {
//Set a new id
var oldPid = this.get("id");
this.set("oldPid", oldPid);
this.set("id", "urn:uuid:" + uuid.v4());
this.set("obsoletes", oldPid);
this.set("obsoletedBy", null);
this.set("archived", false);
}
//Create the system metadata
var sysMetaXML = this.serializeSysMeta();
//Send the new pid, old pid, and system metadata
var xmlBlob = new Blob([sysMetaXML], { type: "application/xml" });
var formData = new FormData();
formData.append("sysmeta", xmlBlob, "sysmeta");
//Let's try updating the system metadata for now
if (options.sysMetaOnly) {
formData.append("pid", this.get("id"));
var requestSettings = {
url: MetacatUI.appModel.get("metaServiceUrl"),
type: "PUT",
cache: false,
contentType: false,
processData: false,
data: formData,
success: function (response) {},
error: function (data) {
console.log("error updating system metadata");
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
} else {
//Add the ids to the form data
formData.append("newPid", this.get("id"));
formData.append("pid", oldPid);
//Create the resource map XML
var mapXML = this.serialize();
var mapBlob = new Blob([mapXML], { type: "application/xml" });
formData.append("object", mapBlob);
//Get the size of the new resource map
this.set("size", mapBlob.size);
//Get the new checksum of the resource map
var checksum = md5(mapXML);
this.set("checksum", checksum);
var requestSettings = {
url: MetacatUI.appModel.get("objectServiceUrl"),
type: "PUT",
cache: false,
contentType: false,
processData: false,
data: formData,
success: function (response) {},
error: function (data) {
console.log("error udpating object");
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
}
},
parseSysMeta: function (response) {
this.set("sysMetaXML", $.parseHTML(response));
var responseDoc = $.parseHTML(response),
systemMetadata,
prependXML = "",
appendXML = "";
for (var i = 0; i < responseDoc.length; i++) {
if (
responseDoc[i].nodeType == 1 &&
responseDoc[i].localName.indexOf("systemmetadata") > -1
)
systemMetadata = responseDoc[i];
}
//Parse the XML to JSON
var sysMetaValues = this.toJson(systemMetadata),
camelCasedValues = {};
//Convert the JSON to a camel-cased version, which matches Solr and is easier to work with in code
_.each(
Object.keys(sysMetaValues),
function (key) {
camelCasedValues[this.sysMetaNodeMap[key]] = sysMetaValues[key];
},
this,
);
//Set the values on the model
this.set(camelCasedValues);
},
serialize: function () {
//Create an RDF serializer
var serializer = rdf.Serializer();
serializer.store = this.dataPackageGraph;
//Define the namespaces
var ORE = rdf.Namespace(this.namespaces.ORE),
CITO = rdf.Namespace(this.namespaces.CITO);
//Get the pid of this package - depends on whether we are updating or creating a resource map
var pid = this.get("id"),
oldPid = this.get("oldPid"),
updating = oldPid ? true : false;
//Update the pids in the RDF graph only if we are updating the resource map with a new pid
if (updating) {
//Find the identifier statement in the resource map
var idNode = rdf.lit(oldPid),
idStatement = this.dataPackageGraph.statementsMatching(
undefined,
undefined,
idNode,
);
//Get the CN Resolve Service base URL from the resource map (mostly important in dev environments where it will not always be cn.dataone.org)
var cnResolveUrl = idStatement[0].subject.value.substring(
0,
idStatement[0].subject.value.indexOf(oldPid),
);
this.dataPackageGraph.cnResolveUrl = cnResolveUrl;
//Create variations of the resource map ID using the resolve URL so we can always find it in the RDF graph
var oldPidVariations = [
oldPid,
encodeURIComponent(oldPid),
cnResolveUrl + encodeURIComponent(oldPid),
];
//Get all the isAggregatedBy statements
var aggregationNode = rdf.sym(
cnResolveUrl + encodeURIComponent(oldPid) + "#aggregation",
),
aggByStatements = this.dataPackageGraph.statementsMatching(
undefined,
ORE("isAggregatedBy"),
);
//Using the isAggregatedBy statements, find all the DataONE object ids in the RDF graph
var idsFromXML = [];
_.each(
aggByStatements,
function (statement) {
//Check if the resource map ID is the old existing id, so we don't collect ids that are not about this resource map
if (
_.find(oldPidVariations, function (oldPidV) {
return oldPidV + "#aggregation" == statement.object.value;
})
) {
var statementID = statement.subject.value;
idsFromXML.push(statementID);
//Add variations of the ID so we make sure we account for all the ways they exist in the RDF XML
if (statementID.indexOf(cnResolveUrl) > -1)
idsFromXML.push(
statementID.substring(statementID.lastIndexOf("/") + 1),
);
else
idsFromXML.push(
cnResolveUrl + encodeURIComponent(statementID),
);
}
},
this,
);
//Get all the ids from this model
var idsFromModel = _.invoke(this.get("members"), "get", "id");
//Find the difference between the model IDs and the XML IDs to get a list of added members
var addedIds = _.without(
_.difference(idsFromModel, idsFromXML),
oldPidVariations,
);
//Create variations of all these ids too
var allMemberIds = idsFromModel;
_.each(idsFromModel, function (id) {
allMemberIds.push(cnResolveUrl + encodeURIComponent(id));
});
//Remove any other isAggregatedBy statements that are not listed as members of this model
_.each(
aggByStatements,
function (statement) {
if (!_.contains(allMemberIds, statement.subject.value))
this.removeFromAggregation(statement.subject.value);
else if (
_.find(oldPidVariations, function (oldPidV) {
return oldPidV + "#aggregation" == statement.object.value;
})
)
statement.object.value =
cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
},
this,
);
//Change all the statements in the RDF where the aggregation is the subject, to reflect the new resource map ID
var aggregationSubjStatements =
this.dataPackageGraph.statementsMatching(aggregationNode);
_.each(aggregationSubjStatements, function (statement) {
statement.subject.value =
cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
});
//Change all the statements in the RDF where the aggregation is the object, to reflect the new resource map ID
var aggregationObjStatements =
this.dataPackageGraph.statementsMatching(
undefined,
undefined,
aggregationNode,
);
_.each(aggregationObjStatements, function (statement) {
statement.object.value =
cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
});
//Change all the resource map subject nodes in the RDF graph
var rMapNode = rdf.sym(cnResolveUrl + encodeURIComponent(oldPid));
var rMapStatements =
this.dataPackageGraph.statementsMatching(rMapNode);
_.each(rMapStatements, function (statement) {
statement.subject.value = cnResolveUrl + encodeURIComponent(pid);
});
//Change the idDescribedBy statement
var isDescribedByStatements =
this.dataPackageGraph.statementsMatching(
undefined,
ORE("isDescribedBy"),
rdf.sym(oldPid),
);
if (isDescribedByStatements[0])
isDescribedByStatements[0].object.value = pid;
//Add nodes for new package members
_.each(
addedIds,
function (id) {
this.addToAggregation(id);
},
this,
);
//Change all the resource map identifier literal node in the RDF graph
if (idStatement[0]) idStatement[0].object.value = pid;
}
//Now serialize the RDF XML
var serializer = rdf.Serializer();
serializer.store = this.dataPackageGraph;
var xmlString = serializer.statementsToXML(
this.dataPackageGraph.statements,
);
return xmlString;
},
serializeSysMeta: function () {
//Get the system metadata XML that currently exists in the system
var xml = $(this.get("sysMetaXML"));
//Update the system metadata values
xml.find("serialversion").text(this.get("serialVersion") || "0");
xml.find("identifier").text(this.get("newPid") || this.get("id"));
xml.find("formatid").text(this.get("formatId"));
xml.find("size").text(this.get("size"));
xml.find("checksum").text(this.get("checksum"));
xml
.find("submitter")
.text(
this.get("submitter") || MetacatUI.appUserModel.get("username"),
);
xml
.find("rightsholder")
.text(
this.get("rightsHolder") || MetacatUI.appUserModel.get("username"),
);
xml.find("archived").text(this.get("archived"));
xml
.find("dateuploaded")
.text(this.get("dateUploaded") || new Date().toISOString());
xml
.find("datesysmetadatamodified")
.text(
this.get("dateSysMetadataModified") || new Date().toISOString(),
);
xml
.find("originmembernode")
.text(
this.get("originMemberNode") ||
MetacatUI.nodeModel.get("currentMemberNode"),
);
xml
.find("authoritativemembernode")
.text(
this.get("authoritativeMemberNode") ||
MetacatUI.nodeModel.get("currentMemberNode"),
);
if (this.get("obsoletes"))
xml.find("obsoletes").text(this.get("obsoletes"));
else xml.find("obsoletes").remove();
if (this.get("obsoletedBy"))
xml.find("obsoletedby").text(this.get("obsoletedBy"));
else xml.find("obsoletedby").remove();
//Write the access policy
var accessPolicyXML = "<accessPolicy>\n";
_.each(this.get("accesspolicy"), function (policy, policyType, all) {
var fullPolicy = all[policyType];
_.each(fullPolicy, function (policyPart) {
accessPolicyXML += "\t<" + policyType + ">\n";
accessPolicyXML +=
"\t\t<subject>" + policyPart.subject + "</subject>\n";
var permissions = Array.isArray(policyPart.permission)
? policyPart.permission
: [policyPart.permission];
_.each(permissions, function (perm) {
accessPolicyXML += "\t\t<permission>" + perm + "</permission>\n";
});
accessPolicyXML += "\t</" + policyType + ">\n";
});
});
accessPolicyXML += "</accessPolicy>";
//Replace the old access policy with the new one
xml.find("accesspolicy").replaceWith(accessPolicyXML);
var xmlString = $(document.createElement("div"))
.append(xml.clone())
.html();
//Now camel case the nodes
_.each(
Object.keys(this.sysMetaNodeMap),
function (name, i, allNodeNames) {
var regEx = new RegExp("<" + name, "g");
xmlString = xmlString.replace(
regEx,
"<" + this.sysMetaNodeMap[name],
);
var regEx = new RegExp(name + ">", "g");
xmlString = xmlString.replace(
regEx,
this.sysMetaNodeMap[name] + ">",
);
},
this,
);
xmlString = xmlString.replace(/systemmetadata/g, "systemMetadata");
return xmlString;
},
//Adds a new object to the resource map RDF graph
addToAggregation: function (id) {
if (id.indexOf(this.dataPackageGraph.cnResolveUrl) < 0)
var fullID =
this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id);
else {
var fullID = id;
id = id.substring(
this.dataPackageGraph.cnResolveUrl.lastIndexOf("/") + 1,
);
}
//Initialize the namespaces
var ORE = rdf.Namespace(this.namespaces.ORE),
DCTERMS = rdf.Namespace(this.namespaces.DCTERMS),
XML = rdf.Namespace(this.namespaces.XML),
CITO = rdf.Namespace(this.namespaces.CITO);
//Create a node for this object, the identifier, the resource map, and the aggregation
var objectNode = rdf.sym(fullID),
mapNode = rdf.sym(
this.dataPackageGraph.cnResolveUrl +
encodeURIComponent(this.get("id")),
),
aggNode = rdf.sym(
this.dataPackageGraph.cnResolveUrl +
encodeURIComponent(this.get("id")) +
"#aggregation",
),
idNode = rdf.literal(id, undefined, XML("string"));
//Add the statement: this object isAggregatedBy the resource map aggregation
this.dataPackageGraph.addStatement(
rdf.st(objectNode, ORE("isAggregatedBy"), aggNode),
);
//Add the statement: The resource map aggregation aggregates this object
this.dataPackageGraph.addStatement(
rdf.st(aggNode, ORE("aggregates"), objectNode),
);
//Add the statement: This object has the identifier {id}
this.dataPackageGraph.addStatement(
rdf.st(objectNode, DCTERMS("identifier"), idNode),
);
//Find the metadata doc that describes this object
var model = _.find(this.get("members"), function (m) {
return m.get("id") == id;
}),
isDocBy = model.get("isDocumentedBy");
//If this object is documented by any metadata...
if (isDocBy) {
//Get the ids of all the metadata objects in this package
var metadataInPackage = _.compact(
_.map(this.get("members"), function (m) {
if (m.get("formatType") == "METADATA") return m.get("id");
}),
);
//Find the metadata IDs that are in this package that also documents this data object
var metadataIds = Array.isArray(isDocBy)
? _.intersection(metadataInPackage, isDocBy)
: _.intersection(metadataInPackage, [isDocBy]);
//For each metadata that documents this object, add a CITO:isDocumentedBy and CITO:documents statement
_.each(
metadataIds,
function (metaId) {
//Create the named nodes and statements
var memberNode = rdf.sym(
this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id),
),
metadataNode = rdf.sym(
this.dataPackageGraph.cnResolveUrl +
encodeURIComponent(metaId),
),
isDocByStatement = rdf.st(
memberNode,
CITO("isDocumentedBy"),
metadataNode,
),
documentsStatement = rdf.st(
metadataNode,
CITO("documents"),
memberNode,
);
//Add the statements
this.dataPackageGraph.addStatement(isDocByStatement);
this.dataPackageGraph.addStatement(documentsStatement);
},
this,
);
}
},
removeFromAggregation: function (id) {
if (!id.indexOf(this.dataPackageGraph.cnResolveUrl))
id = this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id);
var removedObjNode = rdf.sym(id),
statements = _.union(
this.dataPackageGraph.statementsMatching(
undefined,
undefined,
removedObjNode,
),
this.dataPackageGraph.statementsMatching(removedObjNode),
);
this.dataPackageGraph.removeStatements(statements);
},
getParentMetadata: function () {
var rMapIds = this.get("resourceMap");
//Create a query that searches for any resourceMap with an id matching one of the parents OR an id that matches one of the parents.
//This will return all members of the parent resource maps AND the parent resource maps themselves
var rMapQuery = "",
idQuery = "";
if (Array.isArray(rMapIds) && rMapIds.length > 1) {
_.each(rMapIds, function (id, i, ids) {
//At the begininng of the list of ids
if (rMapQuery.length == 0) {
rMapQuery += "resourceMap:(";
idQuery += "id:(";
}
//The id
rMapQuery += "%22" + encodeURIComponent(id) + "%22";
idQuery += "%22" + encodeURIComponent(id) + "%22";
//At the end of the list of ids
if (i + 1 == ids.length) {
rMapQuery += ")";
idQuery += ")";
}
//In-between each id
else {
rMapQuery += " OR ";
idQuery += " OR ";
}
});
} else {
//When there is just one parent, the query is simple
var rMapId = Array.isArray(rMapIds) ? rMapIds[0] : rMapIds;
rMapQuery += "resourceMap:%22" + encodeURIComponent(rMapId) + "%22";
idQuery += "id:%22" + encodeURIComponent(rMapId) + "%22";
}
var query =
"fl=title,id,obsoletedBy,resourceMap" +
"&wt=json" +
"&group=true&group.field=formatType&group.limit=-1" +
"&q=((formatType:METADATA AND " +
rMapQuery +
") OR " +
idQuery +
")";
var model = this;
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
var results = data.grouped.formatType.groups,
resourceMapGroup = _.where(results, {
groupValue: "RESOURCE",
})[0],
rMapList = resourceMapGroup ? resourceMapGroup.doclist : null,
rMaps = rMapList ? rMapList.docs : [],
rMapIds = _.pluck(rMaps, "id"),
parents = [],
parentIds = [];
//As long as this map isn't obsoleted by another map in our results list, we will show it
_.each(rMaps, function (map) {
if (!(map.obsoletedBy && _.contains(rMapIds, map.obsoletedBy))) {
parents.push(map);
parentIds.push(map.id);
}
});
var metadataList = _.where(results, { groupValue: "METADATA" })[0],
metadata =
metadataList && metadataList.doclist
? metadataList.doclist.docs
: [],
metadataModels = [];
//As long as this map isn't obsoleted by another map in our results list, we will show it
_.each(metadata, function (m) {
//Find the metadata doc that obsoletes this one
var isObsoletedBy = _.findWhere(metadata, { id: m.obsoletedBy });
//If one isn't found, then this metadata doc is the most recent
if (typeof isObsoletedBy == "undefined") {
//If this metadata doc is in one of the filtered parent resource maps
if (_.intersection(parentIds, m.resourceMap).length) {
//Create a SolrResult model and add to an array
metadataModels.push(new SolrResult(m));
}
}
});
model.set("parentPackageMetadata", metadataModels);
model.trigger("change:parentPackageMetadata");
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
//Create the URL string that is used to download this package
getURL: function () {
var url = null;
//If we haven't set a packageServiceURL upon app initialization and we are querying a CN, then the packageServiceURL is dependent on the MN this package is from
if (
MetacatUI.appModel.get("d1Service").toLowerCase().indexOf("cn/") >
-1 &&
MetacatUI.nodeModel.get("members").length
) {
var source = this.get("datasource"),
node = _.find(MetacatUI.nodeModel.get("members"), {
identifier: source,
});
//If this node has MNRead v2 services...
if (node && node.readv2)
url =
node.baseURL +
"/v2/packages/application%2Fbagit-097/" +
encodeURIComponent(this.get("id"));
} else if (MetacatUI.appModel.get("packageServiceUrl"))
url =
MetacatUI.appModel.get("packageServiceUrl") +
encodeURIComponent(this.get("id"));
this.set("url", url);
return url;
},
createNestedPackages: function () {
var parentPackage = this,
nestedPackages = this.getNestedPackages(),
numNestedPackages = nestedPackages.length,
numComplete = 0;
_.each(nestedPackages, function (nestedPackage, i, nestedPackages) {
//Flag the parent model as complete when all the nested package info is ready
nestedPackage.on("complete", function () {
numComplete++;
//This is the last package in this package - finish up details and flag as complete
if (numNestedPackages == numComplete) {
var sorted = _.sortBy(parentPackage.get("members"), function (p) {
return p.get("id");
});
parentPackage.set("members", sorted);
parentPackage.flagComplete();
}
});
//Only look one-level deep at all times to avoid going down a rabbit hole
if (
nestedPackage.get("parentPackage") &&
nestedPackage.get("parentPackage").get("parentPackage")
) {
nestedPackage.flagComplete();
return;
} else {
//Get the members of this nested package
nestedPackage.getMembers();
}
});
},
getNestedPackages: function () {
return _.where(this.get("members"), { type: "Package" });
},
getMemberNames: function () {
var metadata = this.getMetadata();
if (!metadata) return false;
//Load the rendered metadata from the view service
var viewService =
MetacatUI.appModel.get("viewServiceUrl") +
encodeURIComponent(metadata.get("id"));
var requestSettings = {
url: viewService,
success: function (data, response, xhr) {
if (solrResult.get("formatType") == "METADATA")
entityName = solrResult.get("title");
else {
var container = viewRef.findEntityDetailsContainer(
solrResult.get("id"),
);
if (container && container.length > 0) {
var entityName = $(container)
.find(".entityName")
.attr("data-entity-name");
if (typeof entityName === "undefined" || !entityName) {
entityName = $(container)
.find(
".control-label:contains('Entity Name') + .controls-well",
)
.text();
if (typeof entityName === "undefined" || !entityName)
entityName = null;
}
} else entityName = null;
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/*
* Will query for the derivations of this package, and sort all entities in the prov trace
* into sources and derivations.
*/
getProvTrace: function () {
var model = this;
//See if there are any prov fields in our index before continuing
if (!MetacatUI.appSearchModel.getProvFields()) return this;
//Start keeping track of the sources and derivations
var sources = new Array(),
derivations = new Array();
//Search for derivations of this package
var derivationsQuery =
MetacatUI.appSearchModel.getGroupedQuery(
"prov_wasDerivedFrom",
_.map(this.get("members"), function (m) {
return m.get("id");
}),
"OR",
) + "%20-obsoletedBy:*";
var requestSettings = {
url:
MetacatUI.appModel.get("queryServiceUrl") +
"&q=" +
derivationsQuery +
"&wt=json&rows=1000" +
"&fl=id,resourceMap,documents,isDocumentedBy,prov_wasDerivedFrom",
success: function (data) {
_.each(data.response.docs, function (result) {
derivations.push(result.id);
});
//Make arrays of unique IDs of objects that are sources or derivations of this package.
_.each(model.get("members"), function (member, i) {
if (member.type == "Package") return;
if (member.hasProvTrace()) {
sources = _.union(sources, member.getSources());
derivations = _.union(derivations, member.getDerivations());
}
});
//Save the arrays of sources and derivations
model.set("sources", sources);
model.set("derivations", derivations);
//Now get metadata about all the entities in the prov trace not in this package
model.getExternalProvTrace();
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
getExternalProvTrace: function () {
var model = this;
//Compact our list of ids that are in the prov trace by combining the sources and derivations and removing ids of members of this package
var externalProvEntities = _.difference(
_.union(this.get("sources"), this.get("derivations")),
this.get("memberIds"),
);
//If there are no sources or derivations, then we do not need to find resource map ids for anything
if (!externalProvEntities.length) {
//Save this prov trace on a package-member/document/object level.
if (this.get("sources").length || this.get("derivations").length)
this.setMemberProvTrace();
//Flag that the provenance trace is complete
this.set("provenanceFlag", "complete");
return this;
} else {
//Create a query where we retrieve the ID of the resource map of each source and derivation
var idQuery = MetacatUI.appSearchModel.getGroupedQuery(
"id",
externalProvEntities,
"OR",
);
//Create a query where we retrieve the metadata for each source and derivation
var metadataQuery = MetacatUI.appSearchModel.getGroupedQuery(
"documents",
externalProvEntities,
"OR",
);
}
//TODO: Find the products of programs/executions
//Make a comma-separated list of the provenance field names
var provFieldList = "";
_.each(
MetacatUI.appSearchModel.getProvFields(),
function (fieldName, i, list) {
provFieldList += fieldName;
if (i < list.length - 1) provFieldList += ",";
},
);
//Combine the two queries with an OR operator
if (idQuery.length && metadataQuery.length)
var combinedQuery = idQuery + "%20OR%20" + metadataQuery;
else return this;
//the full and final query in Solr syntax
var query =
"q=" +
combinedQuery +
"&fl=id,resourceMap,documents,isDocumentedBy,formatType,formatId,dateUploaded,rightsHolder,datasource,prov_instanceOfClass," +
provFieldList +
"&rows=100&wt=json";
//Send the query to the query service
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
//Do any of our docs have multiple resource maps?
var hasMultipleMaps = _.filter(data.response.docs, function (doc) {
return (
typeof doc.resourceMap !== "undefined" &&
doc.resourceMap.length > 1
);
});
//If so, we want to find the latest version of each resource map and only represent that one in the Prov Chart
if (typeof hasMultipleMaps !== "undefined") {
var allMapIDs = _.uniq(
_.flatten(_.pluck(hasMultipleMaps, "resourceMap")),
);
if (allMapIDs.length) {
var query =
"q=+-obsoletedBy:*+" +
MetacatUI.appSearchModel.getGroupedQuery(
"id",
allMapIDs,
"OR",
) +
"&fl=obsoletes,id" +
"&wt=json";
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (mapData, textStatus, xhr) {
//Create a list of resource maps that are not obsoleted by any other resource map retrieved
var resourceMaps = mapData.response.docs;
model.obsoletedResourceMaps = _.pluck(
resourceMaps,
"obsoletes",
);
model.latestResourceMaps = _.difference(
resourceMaps,
model.obsoletedResourceMaps,
);
model.sortProvTrace(data.response.docs);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
} else model.sortProvTrace(data.response.docs);
} else model.sortProvTrace(data.response.docs);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
return this;
},
sortProvTrace: function (docs) {
var model = this;
//Start an array to hold the packages in the prov trace
var sourcePackages = new Array(),
derPackages = new Array(),
sourceDocs = new Array(),
derDocs = new Array(),
sourceIDs = this.get("sources"),
derivationIDs = this.get("derivations");
//Separate the results into derivations and sources and group by their resource map.
_.each(docs, function (doc, i) {
var docModel = new SolrResult(doc),
mapIds = docModel.get("resourceMap");
if (
(typeof mapIds === "undefined" || !mapIds) &&
docModel.get("formatType") == "DATA" &&
(typeof docModel.get("isDocumentedBy") === "undefined" ||
!docModel.get("isDocumentedBy"))
) {
//If this object is not in a resource map and does not have metadata, it is a "naked" data doc, so save it by itself
if (_.contains(sourceIDs, doc.id)) sourceDocs.push(docModel);
if (_.contains(derivationIDs, doc.id)) derDocs.push(docModel);
} else if (
(typeof mapIds === "undefined" || !mapIds) &&
docModel.get("formatType") == "DATA" &&
docModel.get("isDocumentedBy")
) {
//If this data doc does not have a resource map but has a metadata doc that documents it, create a blank package model and save it
var p = new PackageModel({
members: new Array(docModel),
});
//Add this package model to the sources and/or derivations packages list
if (_.contains(sourceIDs, docModel.get("id")))
sourcePackages[docModel.get("id")] = p;
if (_.contains(derivationIDs, docModel.get("id")))
derPackages[docModel.get("id")] = p;
} else if (mapIds.length) {
//If this doc has a resource map, create a package model and SolrResult model and store it
var id = docModel.get("id");
//Some of these objects may have multiple resource maps
_.each(mapIds, function (mapId, i, list) {
if (!_.contains(model.obsoletedResourceMaps, mapId)) {
var documentsSource, documentsDerivation;
if (docModel.get("formatType") == "METADATA") {
if (
_.intersection(docModel.get("documents"), sourceIDs).length
)
documentsSource = true;
if (
_.intersection(docModel.get("documents"), derivationIDs)
.length
)
documentsDerivation = true;
}
//Is this a source object or a metadata doc of a source object?
if (_.contains(sourceIDs, id) || documentsSource) {
//Have we encountered this source package yet?
if (!sourcePackages[mapId] && mapId != model.get("id")) {
//Now make a new package model for it
var p = new PackageModel({
id: mapId,
members: new Array(docModel),
});
//Add to the array of source packages
sourcePackages[mapId] = p;
}
//If so, add this member to its package model
else if (mapId != model.get("id")) {
var memberList = sourcePackages[mapId].get("members");
memberList.push(docModel);
sourcePackages[mapId].set("members", memberList);
}
}
//Is this a derivation object or a metadata doc of a derivation object?
if (_.contains(derivationIDs, id) || documentsDerivation) {
//Have we encountered this derivation package yet?
if (!derPackages[mapId] && mapId != model.get("id")) {
//Now make a new package model for it
var p = new PackageModel({
id: mapId,
members: new Array(docModel),
});
//Add to the array of source packages
derPackages[mapId] = p;
}
//If so, add this member to its package model
else if (mapId != model.get("id")) {
var memberList = derPackages[mapId].get("members");
memberList.push(docModel);
derPackages[mapId].set("members", memberList);
}
}
}
});
}
});
//Transform our associative array (Object) of packages into an array
var newArrays = new Array();
_.each(
new Array(sourcePackages, derPackages, sourceDocs, derDocs),
function (provObject) {
var newArray = new Array(),
key;
for (key in provObject) {
newArray.push(provObject[key]);
}
newArrays.push(newArray);
},
);
//We now have an array of source packages and an array of derivation packages.
model.set("sourcePackages", newArrays[0]);
model.set("derivationPackages", newArrays[1]);
model.set("sourceDocs", newArrays[2]);
model.set("derivationDocs", newArrays[3]);
//Save this prov trace on a package-member/document/object level.
model.setMemberProvTrace();
//Flag that the provenance trace is complete
model.set("provenanceFlag", "complete");
},
setMemberProvTrace: function () {
var model = this,
relatedModels = this.get("relatedModels"),
relatedModelIDs = new Array();
//Now for each doc, we want to find which member it is related to
_.each(this.get("members"), function (member, i, members) {
if (member.type == "Package") return;
//Get the sources and derivations of this member
var memberSourceIDs = member.getSources();
var memberDerIDs = member.getDerivations();
//Look through each source package, derivation package, source doc, and derivation doc.
_.each(model.get("sourcePackages"), function (pkg, i) {
_.each(pkg.get("members"), function (sourcePkgMember, i) {
//Is this package member a direct source of this package member?
if (_.contains(memberSourceIDs, sourcePkgMember.get("id")))
//Save this source package member as a source of this member
member.set(
"provSources",
_.union(member.get("provSources"), [sourcePkgMember]),
);
//Save this in the list of related models
if (!_.contains(relatedModelIDs, sourcePkgMember.get("id"))) {
relatedModels.push(sourcePkgMember);
relatedModelIDs.push(sourcePkgMember.get("id"));
}
});
});
_.each(model.get("derivationPackages"), function (pkg, i) {
_.each(pkg.get("members"), function (derPkgMember, i) {
//Is this package member a direct source of this package member?
if (_.contains(memberDerIDs, derPkgMember.get("id")))
//Save this derivation package member as a derivation of this member
member.set(
"provDerivations",
_.union(member.get("provDerivations"), [derPkgMember]),
);
//Save this in the list of related models
if (!_.contains(relatedModelIDs, derPkgMember.get("id"))) {
relatedModels.push(derPkgMember);
relatedModelIDs.push(derPkgMember.get("id"));
}
});
});
_.each(model.get("sourceDocs"), function (doc, i) {
//Is this package member a direct source of this package member?
if (_.contains(memberSourceIDs, doc.get("id")))
//Save this source package member as a source of this member
member.set(
"provSources",
_.union(member.get("provSources"), [doc]),
);
//Save this in the list of related models
if (!_.contains(relatedModelIDs, doc.get("id"))) {
relatedModels.push(doc);
relatedModelIDs.push(doc.get("id"));
}
});
_.each(model.get("derivationDocs"), function (doc, i) {
//Is this package member a direct derivation of this package member?
if (_.contains(memberDerIDs, doc.get("id")))
//Save this derivation package member as a derivation of this member
member.set(
"provDerivations",
_.union(member.get("provDerivations"), [doc]),
);
//Save this in the list of related models
if (!_.contains(relatedModelIDs, doc.get("id"))) {
relatedModels.push(doc);
relatedModelIDs.push(doc.get("id"));
}
});
_.each(members, function (otherMember, i) {
//Is this other package member a direct derivation of this package member?
if (_.contains(memberDerIDs, otherMember.get("id")))
//Save this other derivation package member as a derivation of this member
member.set(
"provDerivations",
_.union(member.get("provDerivations"), [otherMember]),
);
//Is this other package member a direct source of this package member?
if (_.contains(memberSourceIDs, otherMember.get("id")))
//Save this other source package member as a source of this member
member.set(
"provSources",
_.union(member.get("provSources"), [otherMember]),
);
//Is this other package member an indirect source or derivation?
if (
otherMember.get("type") == "program" &&
_.contains(
member.get("prov_generatedByProgram"),
otherMember.get("id"),
)
) {
var indirectSources = _.filter(members, function (m) {
return _.contains(otherMember.getInputs(), m.get("id"));
});
indirectSourcesIds = _.each(indirectSources, function (m) {
return m.get("id");
});
member.set(
"prov_wasDerivedFrom",
_.union(member.get("prov_wasDerivedFrom"), indirectSourcesIds),
);
//otherMember.set("prov_hasDerivations", _.union(otherMember.get("prov_hasDerivations"), [member.get("id")]));
member.set(
"provSources",
_.union(member.get("provSources"), indirectSources),
);
}
if (
otherMember.get("type") == "program" &&
_.contains(
member.get("prov_usedByProgram"),
otherMember.get("id"),
)
) {
var indirectDerivations = _.filter(members, function (m) {
return _.contains(otherMember.getOutputs(), m.get("id"));
});
indirectDerivationsIds = _.each(
indirectDerivations,
function (m) {
return m.get("id");
},
);
member.set(
"prov_hasDerivations",
_.union(
member.get("prov_hasDerivations"),
indirectDerivationsIds,
),
);
//otherMember.set("prov_wasDerivedFrom", _.union(otherMember.get("prov_wasDerivedFrom"), [member.get("id")]));
member.set(
"provDerivations",
_.union(member.get("provDerivations"), indirectDerivationsIds),
);
}
});
//Add this member to the list of related models
if (!_.contains(relatedModelIDs, member.get("id"))) {
relatedModels.push(member);
relatedModelIDs.push(member.get("id"));
}
//Clear out any duplicates
member.set("provSources", _.uniq(member.get("provSources")));
member.set("provDerivations", _.uniq(member.get("provDerivations")));
});
//Update the list of related models
this.set("relatedModels", relatedModels);
},
downloadWithCredentials: function () {
//Get info about this object
var url = this.get("url"),
model = this;
//Create an XHR
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
xhr.onload = function () {
//Get the file name from the Content-Disposition header
var filename = xhr.getResponseHeader("Content-Disposition");
//As a backup, use the system metadata file name or the id
if (!filename) {
filename = model.get("filename") || model.get("id");
}
//Add a ".zip" extension if it doesn't exist
if (
filename.indexOf(".zip") < 0 ||
filename.indexOf(".zip") != filename.length - 4
) {
filename += ".zip";
}
//For IE, we need to use the navigator API
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(xhr.response, filename);
} else {
var a = document.createElement("a");
a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
a.download = filename; // Set the file name.
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}
model.trigger("downloadComplete");
// Track this event
MetacatUI.analytics?.trackEvent(
"download",
"Download Package",
model.get("id"),
);
};
xhr.onprogress = function (e) {
if (e.lengthComputable) {
var percent = (e.loaded / e.total) * 100;
model.set("downloadPercent", percent);
}
};
xhr.onerror = function (e) {
model.trigger("downloadError");
// Track this event
MetacatUI.analytics?.trackEvent(
"download",
"Download Package",
model.get("id"),
);
};
//Open and send the request with the user's auth token
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.setRequestHeader(
"Authorization",
"Bearer " + MetacatUI.appUserModel.get("token"),
);
xhr.send();
},
/* Returns the SolrResult that represents the metadata doc */
getMetadata: function () {
var members = this.get("members");
for (var i = 0; i < members.length; i++) {
if (members[i].get("formatType") == "METADATA") return members[i];
}
//If there are no metadata objects in this package, make sure we have searched for them already
if (!this.complete && !this.pending) this.getMembers();
return false;
},
//Check authority of the Metadata SolrResult model instead
checkAuthority: function () {
//Call the auth service
var authServiceUrl = MetacatUI.appModel.get("authServiceUrl");
if (!authServiceUrl) return false;
var model = this;
var requestSettings = {
url:
authServiceUrl +
encodeURIComponent(this.get("id")) +
"?action=write",
type: "GET",
success: function (data, textStatus, xhr) {
model.set("isAuthorized", true);
model.trigger("change:isAuthorized");
},
error: function (xhr, textStatus, errorThrown) {
model.set("isAuthorized", false);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
flagComplete: function () {
this.complete = true;
this.pending = false;
this.trigger("complete", this);
},
/*
* function xmlToJson - A utility function for converting XML to JSON
*
* @param xml {DOM Element} - An XML or HTML DOM element to convert to json
* @returns {object} - A literal JS object that represents the given XML
*/
toJson: function (xml) {
// Create the return object
var obj = {};
// do children
if (xml.hasChildNodes()) {
for (var i = 0; i < xml.childNodes.length; i++) {
var item = xml.childNodes.item(i);
//If it's an empty text node, skip it
if (item.nodeType == 3 && !item.nodeValue.trim()) continue;
//Get the node name
var nodeName = item.localName;
//If it's a new container node, convert it to JSON and add as a new object attribute
if (typeof obj[nodeName] == "undefined" && item.nodeType == 1) {
obj[nodeName] = this.toJson(item);
}
//If it's a new text node, just store the text value and add as a new object attribute
else if (
typeof obj[nodeName] == "undefined" &&
item.nodeType == 3
) {
obj = item.nodeValue;
}
//If this node name is already stored as an object attribute...
else if (typeof obj[nodeName] != "undefined") {
//Cache what we have now
var old = obj[nodeName];
if (!Array.isArray(old)) old = [old];
//Create a new object to store this node info
var newNode = {};
//Add the new node info to the existing array we have now
if (item.nodeType == 1) {
newNode = this.toJson(item);
var newArray = old.concat(newNode);
} else if (item.nodeType == 3) {
newNode = item.nodeValue;
var newArray = old.concat(newNode);
}
//Store the attributes for this node
_.each(item.attributes, function (attr) {
newNode[attr.localName] = attr.nodeValue;
});
//Replace the old array with the updated one
obj[nodeName] = newArray;
//Exit
continue;
}
//Store the attributes for this node
/*_.each(item.attributes, function(attr){
obj[nodeName][attr.localName] = attr.nodeValue;
});*/
}
}
return obj;
},
//Sums up the byte size of each member
getTotalSize: function () {
if (this.get("totalSize")) return this.get("totalSize");
if (this.get("members").length == 1) {
var totalSize = this.get("members")[0].get("size");
} else {
var totalSize = _.reduce(this.get("members"), function (sum, member) {
if (typeof sum == "object") sum = sum.get("size");
return sum + member.get("size");
});
}
this.set("totalSize", totalSize);
return totalSize;
},
},
);

Copy link
Contributor

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.

getTotalSize: function () {
if (this.get("totalSize")) return this.get("totalSize");
if (this.get("members").length == 1) {
var totalSize = this.get("members")[0].get("size");
} else {
var totalSize = _.reduce(this.get("members"), function (sum, member) {
if (typeof sum == "object") sum = sum.get("size");
return sum + member.get("size");
});
}
this.set("totalSize", totalSize);
return totalSize;
},

Copy link
Contributor

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.

define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
/**
* @class SolrResult
* @classdesc A single result from the Solr search service
* @classcategory Models
* @extends Backbone.Model
*/
var SolrResult = Backbone.Model.extend(
/** @lends SolrResult.prototype */ {
// This model contains all of the attributes found in the SOLR 'docs' field inside of the SOLR response element
defaults: {
abstract: null,
entityName: null,
indexed: true,
archived: false,
origin: "",
keywords: "",
title: "",
pubDate: "",
eastBoundCoord: "",
westBoundCoord: "",
northBoundCoord: "",
southBoundCoord: "",
attributeName: "",
beginDate: "",
endDate: "",
pubDate: "",
id: "",
seriesId: null,
resourceMap: null,
downloads: null,
citations: 0,
selected: false,
formatId: null,
formatType: null,
fileName: null,
datasource: null,
rightsHolder: null,
size: 0,
type: "",
url: null,
obsoletedBy: null,
geohash_9: null,
read_count_i: 0,
reads: 0,
isDocumentedBy: null,
isPublic: null,
isService: false,
serviceDescription: null,
serviceTitle: null,
serviceEndpoint: null,
serviceOutput: null,
notFound: false,
newestVersion: null,
//@type {string} - The system metadata XML as a string
systemMetadata: null,
provSources: [],
provDerivations: [],
//Provenance index fields
prov_generated: null,
prov_generatedByDataONEDN: null,
prov_generatedByExecution: null,
prov_generatedByFoafName: null,
prov_generatedByOrcid: null,
prov_generatedByProgram: null,
prov_generatedByUser: null,
prov_hasDerivations: null,
prov_hasSources: null,
prov_instanceOfClass: null,
prov_used: null,
prov_usedByDataONEDN: null,
prov_usedByExecution: null,
prov_usedByFoafName: null,
prov_usedByOrcid: null,
prov_usedByProgram: null,
prov_usedByUser: null,
prov_wasDerivedFrom: null,
prov_wasExecutedByExecution: null,
prov_wasExecutedByUser: null,
prov_wasGeneratedBy: null,
prov_wasInformedBy: null,
},
initialize: function () {
this.setURL();
this.on("change:id", this.setURL);
this.set("type", this.getType());
this.on("change:read_count_i", function () {
this.set("reads", this.get("read_count_i"));
});
},
type: "SolrResult",
// Toggle the `selected` state of the result
toggle: function () {
this.selected = !this.get("selected");
},
/**
* Returns a plain-english version of the general format - either image, program, metadata, PDF, annotation or data
* @return {string}
*/
getType: function () {
//The list of formatIds that are images
var imageIds = [
"image/gif",
"image/jp2",
"image/jpeg",
"image/png",
"image/svg xml",
"image/svg+xml",
"image/bmp",
];
//The list of formatIds that are images
var pdfIds = ["application/pdf"];
var annotationIds = [
"http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html",
];
var collectionIds = [
"https://purl.dataone.org/collections-1.0.0",
"https://purl.dataone.org/collections-1.1.0",
];
var portalIds = [
"https://purl.dataone.org/portals-1.0.0",
"https://purl.dataone.org/portals-1.1.0",
];
//Determine the type via provONE
var instanceOfClass = this.get("prov_instanceOfClass");
if (typeof instanceOfClass !== "undefined") {
var programClass = _.filter(instanceOfClass, function (className) {
return className.indexOf("#Program") > -1;
});
if (typeof programClass !== "undefined" && programClass.length)
return "program";
} else {
if (this.get("prov_generated") || this.get("prov_used"))
return "program";
}
//Determine the type via file format
if (_.contains(collectionIds, this.get("formatId")))
return "collection";
if (_.contains(portalIds, this.get("formatId"))) return "portal";
if (this.get("formatType") == "METADATA") return "metadata";
if (_.contains(imageIds, this.get("formatId"))) return "image";
if (_.contains(pdfIds, this.get("formatId"))) return "PDF";
if (_.contains(annotationIds, this.get("formatId")))
return "annotation";
else return "data";
},
//Returns a plain-english version of the specific format ID (for selected ids)
getFormat: function () {
var formatMap = {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
"Microsoft Excel OpenXML",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
"Microsoft Word OpenXML",
"application/vnd.ms-excel.sheet.binary.macroEnabled.12":
"Microsoft Office Excel 2007 binary workbooks",
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
"Microsoft Office OpenXML Presentation",
"application/vnd.ms-excel": "Microsoft Excel",
"application/msword": "Microsoft Word",
"application/vnd.ms-powerpoint": "Microsoft Powerpoint",
"text/html": "HTML",
"text/plain": "plain text (.txt)",
"video/avi": "Microsoft AVI file",
"video/x-ms-wmv": "Windows Media Video (.wmv)",
"audio/x-ms-wma": "Windows Media Audio (.wma)",
"application/vnd.google-earth.kml xml":
"Google Earth Keyhole Markup Language (KML)",
"http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html":
"annotation",
"application/mathematica": "Mathematica Notebook",
"application/postscript": "Postscript",
"application/rtf": "Rich Text Format (RTF)",
"application/xml": "XML Application",
"text/xml": "XML",
"application/x-fasta": "FASTA sequence file",
"nexus/1997": "NEXUS File Format for Systematic Information",
"anvl/erc-v02":
"Kernel Metadata and Electronic Resource Citations (ERCs), 2010.05.13",
"http://purl.org/dryad/terms/":
"Dryad Metadata Application Profile Version 3.0",
"http://datadryad.org/profile/v3.1":
"Dryad Metadata Application Profile Version 3.1",
"application/pdf": "PDF",
"application/zip": "ZIP file",
"http://www.w3.org/TR/rdf-syntax-grammar": "RDF/XML",
"http://www.w3.org/TR/rdfa-syntax": "RDFa",
"application/rdf xml": "RDF",
"text/turtle": "TURTLE",
"text/n3": "N3",
"application/x-gzip": "GZIP Format",
"application/x-python": "Python script",
"http://www.w3.org/2005/Atom": "ATOM-1.0",
"application/octet-stream": "octet stream (application file)",
"http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd":
"Darwin Core, v2.0",
"http://rs.tdwg.org/dwc/xsd/simpledarwincore/": "Simple Darwin Core",
"eml://ecoinformatics.org/eml-2.1.0": "EML v2.1.0",
"eml://ecoinformatics.org/eml-2.1.1": "EML v2.1.1",
"eml://ecoinformatics.org/eml-2.0.1": "EML v2.0.1",
"eml://ecoinformatics.org/eml-2.0.0": "EML v2.0.0",
"https://eml.ecoinformatics.org/eml-2.2.0": "EML v2.2.0",
};
return formatMap[this.get("formatId")] || this.get("formatId");
},
setURL: function () {
if (MetacatUI.appModel.get("objectServiceUrl"))
this.set(
"url",
MetacatUI.appModel.get("objectServiceUrl") +
encodeURIComponent(this.get("id")),
);
else if (MetacatUI.appModel.get("resolveServiceUrl"))
this.set(
"url",
MetacatUI.appModel.get("resolveServiceUrl") +
encodeURIComponent(this.get("id")),
);
},
/**
* Checks if the pid or sid or given string is a DOI
*
* @param {string} customString - Optional. An identifier string to check instead of the id and seriesId attributes on the model
* @returns {boolean} True if it is a DOI
*/
isDOI: function (customString) {
return (
MetacatUI.appModel.isDOI(customString) ||
MetacatUI.appModel.isDOI(this.get("id")) ||
MetacatUI.appModel.isDOI(this.get("seriesId"))
);
},
/*
* Checks if the currently-logged-in user is authorized to change
* permissions (or other action if set as parameter) on this doc
* @param {string} [action=changePermission] - The action (read, write, or changePermission) to check
* if the current user has authorization to perform. By default checks for the highest level of permission.
*/
checkAuthority: function (action = "changePermission") {
var authServiceUrl = MetacatUI.appModel.get("authServiceUrl");
if (!authServiceUrl) return false;
var model = this;
var requestSettings = {
url:
authServiceUrl +
encodeURIComponent(this.get("id")) +
"?action=" +
action,
type: "GET",
success: function (data, textStatus, xhr) {
model.set("isAuthorized_" + action, true);
model.set("isAuthorized", true);
model.trigger("change:isAuthorized");
},
error: function (xhr, textStatus, errorThrown) {
model.set("isAuthorized_" + action, false);
model.set("isAuthorized", false);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/*
* This method will download this object while sending the user's auth token in the request.
*/
downloadWithCredentials: function () {
//if(this.get("isPublic")) return;
//Get info about this object
var url = this.get("url"),
model = this;
//Create an XHR
var xhr = new XMLHttpRequest();
//Open and send the request with the user's auth token
xhr.open("GET", url);
if (MetacatUI.appUserModel.get("loggedIn")) xhr.withCredentials = true;
//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
xhr.onload = function () {
if (this.status == 404) {
this.onerror.call(this);
return;
}
//Get the file name to save this file as
var filename = xhr.getResponseHeader("Content-Disposition");
if (!filename) {
filename =
model.get("fileName") ||
model.get("title") ||
model.get("id") ||
"download";
} else
filename = filename
.substring(filename.indexOf("filename=") + 9)
.replace(/"/g, "");
//Replace any whitespaces
filename = filename.trim().replace(/ /g, "_");
//For IE, we need to use the navigator API
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(xhr.response, filename);
}
//Other browsers can download it via a link
else {
var a = document.createElement("a");
a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
// Set the file name.
a.download = filename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}
model.trigger("downloadComplete");
// Track this event
MetacatUI.analytics?.trackEvent(
"download",
"Download DataONEObject",
model.get("id"),
);
};
xhr.onerror = function (e) {
model.trigger("downloadError");
// Track the error
MetacatUI.analytics?.trackException(
`Download DataONEObject error: ${e || ""}`,
model.get("id"),
true,
);
};
xhr.onprogress = function (e) {
if (e.lengthComputable) {
var percent = (e.loaded / e.total) * 100;
model.set("downloadPercent", percent);
}
};
xhr.responseType = "blob";
if (MetacatUI.appUserModel.get("loggedIn"))
xhr.setRequestHeader(
"Authorization",
"Bearer " + MetacatUI.appUserModel.get("token"),
);
xhr.send();
},
getInfo: function (fields) {
var model = this;
if (!fields)
var fields =
"abstract,id,seriesId,fileName,resourceMap,formatType,formatId,obsoletedBy,isDocumentedBy,documents,title,origin,keywords,attributeName,pubDate,eastBoundCoord,westBoundCoord,northBoundCoord,southBoundCoord,beginDate,endDate,dateUploaded,archived,datasource,replicaMN,isAuthorized,isPublic,size,read_count_i,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription,serviceType,project,dateModified";
var escapeSpecialChar = MetacatUI.appSearchModel.escapeSpecialChar;
var query = "q=";
//If there is no seriesId set, then search for pid or sid
if (!this.get("seriesId"))
query +=
'(id:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'" OR seriesId:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'")';
//If a seriesId is specified, then search for that
else if (this.get("seriesId") && this.get("id").length > 0)
query +=
'(seriesId:"' +
escapeSpecialChar(encodeURIComponent(this.get("seriesId"))) +
'" AND id:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'")';
//If only a seriesId is specified, then just search for the most recent version
else if (this.get("seriesId") && !this.get("id"))
query +=
'seriesId:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'" -obsoletedBy:*';
query +=
"&fl=" +
fields + //Specify the fields to return
"&wt=json&rows=1000" + //Get the results in JSON format and get 1000 rows
"&archived=archived:*"; //Get archived or unarchived content
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
type: "GET",
success: function (data, response, xhr) {
//If the Solr response was not as expected, trigger and error and exit
if (!data || typeof data.response == "undefined") {
model.set("indexed", false);
model.trigger("getInfoError");
return;
}
var docs = data.response.docs;
if (docs.length == 1) {
docs[0].resourceMap = model.parseResourceMapField(docs[0]);
model.set(docs[0]);
model.trigger("sync");
}
//If we searched by seriesId, then let's find the most recent version in the series
else if (docs.length > 1) {
//Filter out docs that are obsoleted
var mostRecent = _.reject(docs, function (doc) {
return typeof doc.obsoletedBy !== "undefined";
});
//If there is only one doc that is not obsoleted (the most recent), then
// set this doc's values on this model
if (mostRecent.length == 1) {
mostRecent[0].resourceMap = model.parseResourceMapField(
mostRecent[0],
);
model.set(mostRecent[0]);
model.trigger("sync");
} else {
//If there are multiple docs without an obsoletedBy statement, then
// retreive the head of the series via the system metadata
var sysMetaRequestSettings = {
url:
MetacatUI.appModel.get("metaServiceUrl") +
encodeURIComponent(docs[0].seriesId),
type: "GET",
success: function (sysMetaData) {
//Get the identifier node from the system metadata
var seriesHeadID = $(sysMetaData).find("identifier").text();
//Get the doc from the Solr results with that identifier
var seriesHead = _.findWhere(docs, { id: seriesHeadID });
//If there is a doc in the Solr results list that matches the series head id
if (seriesHead) {
seriesHead.resourceMap =
model.parseResourceMapField(seriesHead);
//Set those values on this model
model.set(seriesHead);
}
//Otherwise, just fall back on the first doc in the list
else if (mostRecent.length) {
mostRecent[0].resourceMap = model.parseResourceMapField(
mostRecent[0],
);
model.set(mostRecent[0]);
} else {
docs[0].resourceMap = model.parseResourceMapField(
docs[0],
);
model.set(docs[0]);
}
model.trigger("sync");
},
error: function (xhr, textStatus, errorThrown) {
// Fall back on the first doc in the list
if (mostRecent.length) {
model.set(mostRecent[0]);
} else {
model.set(docs[0]);
}
model.trigger("sync");
},
};
$.ajax(
_.extend(
sysMetaRequestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
}
} else {
model.set("indexed", false);
//Try getting the system metadata as a backup
model.getSysMeta();
}
},
error: function (xhr, textStatus, errorThrown) {
model.set("indexed", false);
model.trigger("getInfoError");
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
getCitationInfo: function () {
this.getInfo(
"id,seriesId,origin,pubDate,dateUploaded,title,datasource,project",
);
},
/*
* Get the system metadata for this object
*/
getSysMeta: function () {
var url =
MetacatUI.appModel.get("metaServiceUrl") +
encodeURIComponent(this.get("id")),
model = this;
var requestSettings = {
url: url,
type: "GET",
dataType: "text",
success: function (data, response, xhr) {
if (data && data.length) {
model.set("systemMetadata", data);
}
//Check if this is archvied
var archived = $(data).find("archived").text() == "true";
model.set("archived", archived);
//Get the file size
model.set("size", $(data).find("size").text() || "");
//Get the entity name
model.set("filename", $(data).find("filename").text() || "");
//Check if this is a metadata doc
var formatId = $(data).find("formatid").text() || "",
formatType;
model.set("formatId", formatId);
if (
formatId.indexOf("ecoinformatics.org") > -1 ||
formatId.indexOf("FGDC") > -1 ||
formatId.indexOf("INCITS") > -1 ||
formatId.indexOf("namespaces/netcdf") > -1 ||
formatId.indexOf("waterML") > -1 ||
formatId.indexOf("darwin") > -1 ||
formatId.indexOf("dryad") > -1 ||
formatId.indexOf("http://www.loc.gov/METS") > -1 ||
formatId.indexOf("ddi:codebook:2_5") > -1 ||
formatId.indexOf("http://www.icpsr.umich.edu/DDI") > -1 ||
formatId.indexOf(
"http://purl.org/ornl/schema/mercury/terms/v1.0",
) > -1 ||
formatId.indexOf("datacite") > -1 ||
formatId.indexOf("isotc211") > -1 ||
formatId.indexOf("metadata") > -1
)
model.set("formatType", "METADATA");
//Trigger the sync event so the app knows we found the model info
model.trigger("sync");
},
error: function (response) {
//When the user is unauthorized to access this object, trigger a 401 error
if (response.status == 401) {
model.set("notFound", true);
model.trigger("401");
}
//When the object doesn't exist, trigger a 404 error
else if (response.status == 404) {
model.set("notFound", true);
model.trigger("404");
}
//Other error codes trigger a generic error
else {
model.trigger("error");
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
//Transgresses the obsolence chain until it finds the newest version that this user is authorized to read
findLatestVersion: function (newestVersion, possiblyNewer) {
// Make sure we have the /meta service configured
if (!MetacatUI.appModel.get("metaServiceUrl")) return;
//If no pid was supplied, use this model's id
if (!newestVersion) {
var newestVersion = this.get("id");
var possiblyNewer = this.get("obsoletedBy");
}
//If this isn't obsoleted by anything, then there is no newer version
if (!possiblyNewer) {
this.set("newestVersion", newestVersion);
return;
}
var model = this;
//Get the system metadata for the possibly newer version
var requestSettings = {
url:
MetacatUI.appModel.get("metaServiceUrl") +
encodeURIComponent(possiblyNewer),
type: "GET",
success: function (data) {
// the response may have an obsoletedBy element
var obsoletedBy = $(data).find("obsoletedBy").text();
//If there is an even newer version, then get it and rerun this function
if (obsoletedBy)
model.findLatestVersion(possiblyNewer, obsoletedBy);
//If there isn't a newer version, then this is it
else model.set("newestVersion", possiblyNewer);
},
error: function (xhr) {
//If this newer version isn't found or accessible, then save the last
// accessible id as the newest version
if (
xhr.status == 401 ||
xhr.status == 404 ||
xhr.status == "401" ||
xhr.status == "404"
) {
model.set("newestVersion", newestVersion);
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/**** Provenance-related functions ****/
/*
* Returns true if this provenance field points to a source of this data or metadata object
*/
isSourceField: function (field) {
if (typeof field == "undefined" || !field) return false;
if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field))
return false;
if (
field == "prov_generatedByExecution" ||
field == "prov_generatedByProgram" ||
field == "prov_used" ||
field == "prov_wasDerivedFrom" ||
field == "prov_wasInformedBy"
)
return true;
else return false;
},
/*
* Returns true if this provenance field points to a derivation of this data or metadata object
*/
isDerivationField: function (field) {
if (typeof field == "undefined" || !field) return false;
if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field))
return false;
if (
field == "prov_usedByExecution" ||
field == "prov_usedByProgram" ||
field == "prov_hasDerivations" ||
field == "prov_generated"
)
return true;
else return false;
},
/*
* Returns true if this SolrResult has a provenance trace (i.e. has either sources or derivations)
*/
hasProvTrace: function () {
if (this.get("formatType") == "METADATA") {
if (this.get("prov_hasSources") || this.get("prov_hasDerivations"))
return true;
}
var fieldNames = MetacatUI.appSearchModel.getProvFields(),
currentField = "";
for (var i = 0; i < fieldNames.length; i++) {
currentField = fieldNames[i];
if (this.has(currentField)) return true;
}
return false;
},
/*
* Returns an array of all the IDs of objects that are sources of this object
*/
getSources: function () {
var sources = new Array(),
model = this,
//Get the prov fields but leave out references to executions which are not used in the UI yet
fields = _.reject(
MetacatUI.appSearchModel.getProvFields(),
function (f) {
return f.indexOf("xecution") > -1;
},
); //Leave out the first e in execution so we don't have to worry about case sensitivity
_.each(fields, function (provField, i) {
if (model.isSourceField(provField) && model.has(provField))
sources.push(model.get(provField));
});
return _.uniq(_.flatten(sources));
},
/*
* Returns an array of all the IDs of objects that are derivations of this object
*/
getDerivations: function () {
var derivations = new Array(),
model = this,
//Get the prov fields but leave out references to executions which are not used in the UI yet
fields = _.reject(
MetacatUI.appSearchModel.getProvFields(),
function (f) {
return f.indexOf("xecution") > -1;
},
); //Leave out the first e in execution so we don't have to worry about case sensitivity
_.each(fields, function (provField, i) {
if (model.isDerivationField(provField) && model.has(provField))
derivations.push(model.get(provField));
});
return _.uniq(_.flatten(derivations));
},
getInputs: function () {
return this.get("prov_used");
},
getOutputs: function () {
return this.get("prov_generated");
},
/*
* Uses the app configuration to check if this model's metrics should be hidden in the display
*
* @return {boolean}
*/
hideMetrics: function () {
//If the AppModel is configured with cases of where to hide metrics,
if (
typeof MetacatUI.appModel.get("hideMetricsWhen") == "object" &&
MetacatUI.appModel.get("hideMetricsWhen")
) {
//Check for at least one match
return _.some(
MetacatUI.appModel.get("hideMetricsWhen"),
function (value, modelProperty) {
//Get the value of this property from this model
var modelValue = this.get(modelProperty);
//Check for the presence of this model's value in the AppModel value
if (Array.isArray(value) && typeof modelValue == "string") {
return _.contains(value, modelValue);
}
//Check for the presence of the AppModel's value in this model's value
else if (typeof value == "string" && Array.isArray(modelValue)) {
return _.contains(modelValue, value);
}
//Check for overlap of two arrays
else if (Array.isArray(value) && Array.isArray(modelValue)) {
return _.intersection(value, modelValue).length > 0;
}
//If the AppModel value is a function, execute it
else if (typeof value == "function") {
return value(modelValue);
}
//Otherwise, just check for equality
else {
return value === modelValue;
}
},
this,
);
} else {
return false;
}
},
/**
* Creates a URL for viewing more information about this metadata
* @return {string}
*/
createViewURL: function () {
return this.getType() == "portal" || this.getType() == "collection"
? MetacatUI.root +
"/" +
MetacatUI.appModel.get("portalTermPlural") +
"/" +
encodeURIComponent(
this.get("label") || this.get("seriesId") || this.get("id"),
)
: MetacatUI.root +
"/view/" +
encodeURIComponent(this.get("seriesId") || this.get("id"));
},
parseResourceMapField: function (json) {
if (typeof json.resourceMap == "string") {
return json.resourceMap.trim();
} else if (Array.isArray(json.resourceMap)) {
let newResourceMapIds = [];
_.each(json.resourceMap, function (rMapId) {
if (typeof rMapId == "string") {
newResourceMapIds.push(rMapId.trim());
}
});
return newResourceMapIds;
}
//If nothing works so far, return an empty array
return [];
},
},
);
return SolrResult;
});

Copy link
Contributor

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.

var SolrResult = Backbone.Model.extend(
/** @lends SolrResult.prototype */ {
// This model contains all of the attributes found in the SOLR 'docs' field inside of the SOLR response element
defaults: {
abstract: null,
entityName: null,
indexed: true,
archived: false,
origin: "",
keywords: "",
title: "",
pubDate: "",
eastBoundCoord: "",
westBoundCoord: "",
northBoundCoord: "",
southBoundCoord: "",
attributeName: "",
beginDate: "",
endDate: "",
pubDate: "",
id: "",
seriesId: null,
resourceMap: null,
downloads: null,
citations: 0,
selected: false,
formatId: null,
formatType: null,
fileName: null,
datasource: null,
rightsHolder: null,
size: 0,
type: "",
url: null,
obsoletedBy: null,
geohash_9: null,
read_count_i: 0,
reads: 0,
isDocumentedBy: null,
isPublic: null,
isService: false,
serviceDescription: null,
serviceTitle: null,
serviceEndpoint: null,
serviceOutput: null,
notFound: false,
newestVersion: null,
//@type {string} - The system metadata XML as a string
systemMetadata: null,
provSources: [],
provDerivations: [],
//Provenance index fields
prov_generated: null,
prov_generatedByDataONEDN: null,
prov_generatedByExecution: null,
prov_generatedByFoafName: null,
prov_generatedByOrcid: null,
prov_generatedByProgram: null,
prov_generatedByUser: null,
prov_hasDerivations: null,
prov_hasSources: null,
prov_instanceOfClass: null,
prov_used: null,
prov_usedByDataONEDN: null,
prov_usedByExecution: null,
prov_usedByFoafName: null,
prov_usedByOrcid: null,
prov_usedByProgram: null,
prov_usedByUser: null,
prov_wasDerivedFrom: null,
prov_wasExecutedByExecution: null,
prov_wasExecutedByUser: null,
prov_wasGeneratedBy: null,
prov_wasInformedBy: null,
},
initialize: function () {
this.setURL();
this.on("change:id", this.setURL);
this.set("type", this.getType());
this.on("change:read_count_i", function () {
this.set("reads", this.get("read_count_i"));
});
},
type: "SolrResult",
// Toggle the `selected` state of the result
toggle: function () {
this.selected = !this.get("selected");
},
/**
* Returns a plain-english version of the general format - either image, program, metadata, PDF, annotation or data
* @return {string}
*/
getType: function () {
//The list of formatIds that are images
var imageIds = [
"image/gif",
"image/jp2",
"image/jpeg",
"image/png",
"image/svg xml",
"image/svg+xml",
"image/bmp",
];
//The list of formatIds that are images
var pdfIds = ["application/pdf"];
var annotationIds = [
"http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html",
];
var collectionIds = [
"https://purl.dataone.org/collections-1.0.0",
"https://purl.dataone.org/collections-1.1.0",
];
var portalIds = [
"https://purl.dataone.org/portals-1.0.0",
"https://purl.dataone.org/portals-1.1.0",
];
//Determine the type via provONE
var instanceOfClass = this.get("prov_instanceOfClass");
if (typeof instanceOfClass !== "undefined") {
var programClass = _.filter(instanceOfClass, function (className) {
return className.indexOf("#Program") > -1;
});
if (typeof programClass !== "undefined" && programClass.length)
return "program";
} else {
if (this.get("prov_generated") || this.get("prov_used"))
return "program";
}
//Determine the type via file format
if (_.contains(collectionIds, this.get("formatId")))
return "collection";
if (_.contains(portalIds, this.get("formatId"))) return "portal";
if (this.get("formatType") == "METADATA") return "metadata";
if (_.contains(imageIds, this.get("formatId"))) return "image";
if (_.contains(pdfIds, this.get("formatId"))) return "PDF";
if (_.contains(annotationIds, this.get("formatId")))
return "annotation";
else return "data";
},
//Returns a plain-english version of the specific format ID (for selected ids)
getFormat: function () {
var formatMap = {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
"Microsoft Excel OpenXML",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
"Microsoft Word OpenXML",
"application/vnd.ms-excel.sheet.binary.macroEnabled.12":
"Microsoft Office Excel 2007 binary workbooks",
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
"Microsoft Office OpenXML Presentation",
"application/vnd.ms-excel": "Microsoft Excel",
"application/msword": "Microsoft Word",
"application/vnd.ms-powerpoint": "Microsoft Powerpoint",
"text/html": "HTML",
"text/plain": "plain text (.txt)",
"video/avi": "Microsoft AVI file",
"video/x-ms-wmv": "Windows Media Video (.wmv)",
"audio/x-ms-wma": "Windows Media Audio (.wma)",
"application/vnd.google-earth.kml xml":
"Google Earth Keyhole Markup Language (KML)",
"http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html":
"annotation",
"application/mathematica": "Mathematica Notebook",
"application/postscript": "Postscript",
"application/rtf": "Rich Text Format (RTF)",
"application/xml": "XML Application",
"text/xml": "XML",
"application/x-fasta": "FASTA sequence file",
"nexus/1997": "NEXUS File Format for Systematic Information",
"anvl/erc-v02":
"Kernel Metadata and Electronic Resource Citations (ERCs), 2010.05.13",
"http://purl.org/dryad/terms/":
"Dryad Metadata Application Profile Version 3.0",
"http://datadryad.org/profile/v3.1":
"Dryad Metadata Application Profile Version 3.1",
"application/pdf": "PDF",
"application/zip": "ZIP file",
"http://www.w3.org/TR/rdf-syntax-grammar": "RDF/XML",
"http://www.w3.org/TR/rdfa-syntax": "RDFa",
"application/rdf xml": "RDF",
"text/turtle": "TURTLE",
"text/n3": "N3",
"application/x-gzip": "GZIP Format",
"application/x-python": "Python script",
"http://www.w3.org/2005/Atom": "ATOM-1.0",
"application/octet-stream": "octet stream (application file)",
"http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd":
"Darwin Core, v2.0",
"http://rs.tdwg.org/dwc/xsd/simpledarwincore/": "Simple Darwin Core",
"eml://ecoinformatics.org/eml-2.1.0": "EML v2.1.0",
"eml://ecoinformatics.org/eml-2.1.1": "EML v2.1.1",
"eml://ecoinformatics.org/eml-2.0.1": "EML v2.0.1",
"eml://ecoinformatics.org/eml-2.0.0": "EML v2.0.0",
"https://eml.ecoinformatics.org/eml-2.2.0": "EML v2.2.0",
};
return formatMap[this.get("formatId")] || this.get("formatId");
},
setURL: function () {
if (MetacatUI.appModel.get("objectServiceUrl"))
this.set(
"url",
MetacatUI.appModel.get("objectServiceUrl") +
encodeURIComponent(this.get("id")),
);
else if (MetacatUI.appModel.get("resolveServiceUrl"))
this.set(
"url",
MetacatUI.appModel.get("resolveServiceUrl") +
encodeURIComponent(this.get("id")),
);
},
/**
* Checks if the pid or sid or given string is a DOI
*
* @param {string} customString - Optional. An identifier string to check instead of the id and seriesId attributes on the model
* @returns {boolean} True if it is a DOI
*/
isDOI: function (customString) {
return (
MetacatUI.appModel.isDOI(customString) ||
MetacatUI.appModel.isDOI(this.get("id")) ||
MetacatUI.appModel.isDOI(this.get("seriesId"))
);
},
/*
* Checks if the currently-logged-in user is authorized to change
* permissions (or other action if set as parameter) on this doc
* @param {string} [action=changePermission] - The action (read, write, or changePermission) to check
* if the current user has authorization to perform. By default checks for the highest level of permission.
*/
checkAuthority: function (action = "changePermission") {
var authServiceUrl = MetacatUI.appModel.get("authServiceUrl");
if (!authServiceUrl) return false;
var model = this;
var requestSettings = {
url:
authServiceUrl +
encodeURIComponent(this.get("id")) +
"?action=" +
action,
type: "GET",
success: function (data, textStatus, xhr) {
model.set("isAuthorized_" + action, true);
model.set("isAuthorized", true);
model.trigger("change:isAuthorized");
},
error: function (xhr, textStatus, errorThrown) {
model.set("isAuthorized_" + action, false);
model.set("isAuthorized", false);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/*
* This method will download this object while sending the user's auth token in the request.
*/
downloadWithCredentials: function () {
//if(this.get("isPublic")) return;
//Get info about this object
var url = this.get("url"),
model = this;
//Create an XHR
var xhr = new XMLHttpRequest();
//Open and send the request with the user's auth token
xhr.open("GET", url);
if (MetacatUI.appUserModel.get("loggedIn")) xhr.withCredentials = true;
//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
xhr.onload = function () {
if (this.status == 404) {
this.onerror.call(this);
return;
}
//Get the file name to save this file as
var filename = xhr.getResponseHeader("Content-Disposition");
if (!filename) {
filename =
model.get("fileName") ||
model.get("title") ||
model.get("id") ||
"download";
} else
filename = filename
.substring(filename.indexOf("filename=") + 9)
.replace(/"/g, "");
//Replace any whitespaces
filename = filename.trim().replace(/ /g, "_");
//For IE, we need to use the navigator API
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(xhr.response, filename);
}
//Other browsers can download it via a link
else {
var a = document.createElement("a");
a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
// Set the file name.
a.download = filename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}
model.trigger("downloadComplete");
// Track this event
MetacatUI.analytics?.trackEvent(
"download",
"Download DataONEObject",
model.get("id"),
);
};
xhr.onerror = function (e) {
model.trigger("downloadError");
// Track the error
MetacatUI.analytics?.trackException(
`Download DataONEObject error: ${e || ""}`,
model.get("id"),
true,
);
};
xhr.onprogress = function (e) {
if (e.lengthComputable) {
var percent = (e.loaded / e.total) * 100;
model.set("downloadPercent", percent);
}
};
xhr.responseType = "blob";
if (MetacatUI.appUserModel.get("loggedIn"))
xhr.setRequestHeader(
"Authorization",
"Bearer " + MetacatUI.appUserModel.get("token"),
);
xhr.send();
},
getInfo: function (fields) {
var model = this;
if (!fields)
var fields =
"abstract,id,seriesId,fileName,resourceMap,formatType,formatId,obsoletedBy,isDocumentedBy,documents,title,origin,keywords,attributeName,pubDate,eastBoundCoord,westBoundCoord,northBoundCoord,southBoundCoord,beginDate,endDate,dateUploaded,archived,datasource,replicaMN,isAuthorized,isPublic,size,read_count_i,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription,serviceType,project,dateModified";
var escapeSpecialChar = MetacatUI.appSearchModel.escapeSpecialChar;
var query = "q=";
//If there is no seriesId set, then search for pid or sid
if (!this.get("seriesId"))
query +=
'(id:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'" OR seriesId:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'")';
//If a seriesId is specified, then search for that
else if (this.get("seriesId") && this.get("id").length > 0)
query +=
'(seriesId:"' +
escapeSpecialChar(encodeURIComponent(this.get("seriesId"))) +
'" AND id:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'")';
//If only a seriesId is specified, then just search for the most recent version
else if (this.get("seriesId") && !this.get("id"))
query +=
'seriesId:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'" -obsoletedBy:*';
query +=
"&fl=" +
fields + //Specify the fields to return
"&wt=json&rows=1000" + //Get the results in JSON format and get 1000 rows
"&archived=archived:*"; //Get archived or unarchived content
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
type: "GET",
success: function (data, response, xhr) {
//If the Solr response was not as expected, trigger and error and exit
if (!data || typeof data.response == "undefined") {
model.set("indexed", false);
model.trigger("getInfoError");
return;
}
var docs = data.response.docs;
if (docs.length == 1) {
docs[0].resourceMap = model.parseResourceMapField(docs[0]);
model.set(docs[0]);
model.trigger("sync");
}
//If we searched by seriesId, then let's find the most recent version in the series
else if (docs.length > 1) {
//Filter out docs that are obsoleted
var mostRecent = _.reject(docs, function (doc) {
return typeof doc.obsoletedBy !== "undefined";
});
//If there is only one doc that is not obsoleted (the most recent), then
// set this doc's values on this model
if (mostRecent.length == 1) {
mostRecent[0].resourceMap = model.parseResourceMapField(
mostRecent[0],
);
model.set(mostRecent[0]);
model.trigger("sync");
} else {
//If there are multiple docs without an obsoletedBy statement, then
// retreive the head of the series via the system metadata
var sysMetaRequestSettings = {
url:
MetacatUI.appModel.get("metaServiceUrl") +
encodeURIComponent(docs[0].seriesId),
type: "GET",
success: function (sysMetaData) {
//Get the identifier node from the system metadata
var seriesHeadID = $(sysMetaData).find("identifier").text();
//Get the doc from the Solr results with that identifier
var seriesHead = _.findWhere(docs, { id: seriesHeadID });
//If there is a doc in the Solr results list that matches the series head id
if (seriesHead) {
seriesHead.resourceMap =
model.parseResourceMapField(seriesHead);
//Set those values on this model
model.set(seriesHead);
}
//Otherwise, just fall back on the first doc in the list
else if (mostRecent.length) {
mostRecent[0].resourceMap = model.parseResourceMapField(
mostRecent[0],
);
model.set(mostRecent[0]);
} else {
docs[0].resourceMap = model.parseResourceMapField(
docs[0],
);
model.set(docs[0]);
}
model.trigger("sync");
},
error: function (xhr, textStatus, errorThrown) {
// Fall back on the first doc in the list
if (mostRecent.length) {
model.set(mostRecent[0]);
} else {
model.set(docs[0]);
}
model.trigger("sync");
},
};
$.ajax(
_.extend(
sysMetaRequestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
}
} else {
model.set("indexed", false);
//Try getting the system metadata as a backup
model.getSysMeta();
}
},
error: function (xhr, textStatus, errorThrown) {
model.set("indexed", false);
model.trigger("getInfoError");
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
getCitationInfo: function () {
this.getInfo(
"id,seriesId,origin,pubDate,dateUploaded,title,datasource,project",
);
},
/*
* Get the system metadata for this object
*/
getSysMeta: function () {
var url =
MetacatUI.appModel.get("metaServiceUrl") +
encodeURIComponent(this.get("id")),
model = this;
var requestSettings = {
url: url,
type: "GET",
dataType: "text",
success: function (data, response, xhr) {
if (data && data.length) {
model.set("systemMetadata", data);
}
//Check if this is archvied
var archived = $(data).find("archived").text() == "true";
model.set("archived", archived);
//Get the file size
model.set("size", $(data).find("size").text() || "");
//Get the entity name
model.set("filename", $(data).find("filename").text() || "");
//Check if this is a metadata doc
var formatId = $(data).find("formatid").text() || "",
formatType;
model.set("formatId", formatId);
if (
formatId.indexOf("ecoinformatics.org") > -1 ||
formatId.indexOf("FGDC") > -1 ||
formatId.indexOf("INCITS") > -1 ||
formatId.indexOf("namespaces/netcdf") > -1 ||
formatId.indexOf("waterML") > -1 ||
formatId.indexOf("darwin") > -1 ||
formatId.indexOf("dryad") > -1 ||
formatId.indexOf("http://www.loc.gov/METS") > -1 ||
formatId.indexOf("ddi:codebook:2_5") > -1 ||
formatId.indexOf("http://www.icpsr.umich.edu/DDI") > -1 ||
formatId.indexOf(
"http://purl.org/ornl/schema/mercury/terms/v1.0",
) > -1 ||
formatId.indexOf("datacite") > -1 ||
formatId.indexOf("isotc211") > -1 ||
formatId.indexOf("metadata") > -1
)
model.set("formatType", "METADATA");
//Trigger the sync event so the app knows we found the model info
model.trigger("sync");
},
error: function (response) {
//When the user is unauthorized to access this object, trigger a 401 error
if (response.status == 401) {
model.set("notFound", true);
model.trigger("401");
}
//When the object doesn't exist, trigger a 404 error
else if (response.status == 404) {
model.set("notFound", true);
model.trigger("404");
}
//Other error codes trigger a generic error
else {
model.trigger("error");
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
//Transgresses the obsolence chain until it finds the newest version that this user is authorized to read
findLatestVersion: function (newestVersion, possiblyNewer) {
// Make sure we have the /meta service configured
if (!MetacatUI.appModel.get("metaServiceUrl")) return;
//If no pid was supplied, use this model's id
if (!newestVersion) {
var newestVersion = this.get("id");
var possiblyNewer = this.get("obsoletedBy");
}
//If this isn't obsoleted by anything, then there is no newer version
if (!possiblyNewer) {
this.set("newestVersion", newestVersion);
return;
}
var model = this;
//Get the system metadata for the possibly newer version
var requestSettings = {
url:
MetacatUI.appModel.get("metaServiceUrl") +
encodeURIComponent(possiblyNewer),
type: "GET",
success: function (data) {
// the response may have an obsoletedBy element
var obsoletedBy = $(data).find("obsoletedBy").text();
//If there is an even newer version, then get it and rerun this function
if (obsoletedBy)
model.findLatestVersion(possiblyNewer, obsoletedBy);
//If there isn't a newer version, then this is it
else model.set("newestVersion", possiblyNewer);
},
error: function (xhr) {
//If this newer version isn't found or accessible, then save the last
// accessible id as the newest version
if (
xhr.status == 401 ||
xhr.status == 404 ||
xhr.status == "401" ||
xhr.status == "404"
) {
model.set("newestVersion", newestVersion);
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/**** Provenance-related functions ****/
/*
* Returns true if this provenance field points to a source of this data or metadata object
*/
isSourceField: function (field) {
if (typeof field == "undefined" || !field) return false;
if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field))
return false;
if (
field == "prov_generatedByExecution" ||
field == "prov_generatedByProgram" ||
field == "prov_used" ||
field == "prov_wasDerivedFrom" ||
field == "prov_wasInformedBy"
)
return true;
else return false;
},
/*
* Returns true if this provenance field points to a derivation of this data or metadata object
*/
isDerivationField: function (field) {
if (typeof field == "undefined" || !field) return false;
if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field))
return false;
if (
field == "prov_usedByExecution" ||
field == "prov_usedByProgram" ||
field == "prov_hasDerivations" ||
field == "prov_generated"
)
return true;
else return false;
},
/*
* Returns true if this SolrResult has a provenance trace (i.e. has either sources or derivations)
*/
hasProvTrace: function () {
if (this.get("formatType") == "METADATA") {
if (this.get("prov_hasSources") || this.get("prov_hasDerivations"))
return true;
}
var fieldNames = MetacatUI.appSearchModel.getProvFields(),
currentField = "";
for (var i = 0; i < fieldNames.length; i++) {
currentField = fieldNames[i];
if (this.has(currentField)) return true;
}
return false;
},
/*
* Returns an array of all the IDs of objects that are sources of this object
*/
getSources: function () {
var sources = new Array(),
model = this,
//Get the prov fields but leave out references to executions which are not used in the UI yet
fields = _.reject(
MetacatUI.appSearchModel.getProvFields(),
function (f) {
return f.indexOf("xecution") > -1;
},
); //Leave out the first e in execution so we don't have to worry about case sensitivity
_.each(fields, function (provField, i) {
if (model.isSourceField(provField) && model.has(provField))
sources.push(model.get(provField));
});
return _.uniq(_.flatten(sources));
},
/*
* Returns an array of all the IDs of objects that are derivations of this object
*/
getDerivations: function () {
var derivations = new Array(),
model = this,
//Get the prov fields but leave out references to executions which are not used in the UI yet
fields = _.reject(
MetacatUI.appSearchModel.getProvFields(),
function (f) {
return f.indexOf("xecution") > -1;
},
); //Leave out the first e in execution so we don't have to worry about case sensitivity
_.each(fields, function (provField, i) {
if (model.isDerivationField(provField) && model.has(provField))
derivations.push(model.get(provField));
});
return _.uniq(_.flatten(derivations));
},
getInputs: function () {
return this.get("prov_used");
},
getOutputs: function () {
return this.get("prov_generated");
},
/*
* Uses the app configuration to check if this model's metrics should be hidden in the display
*
* @return {boolean}
*/
hideMetrics: function () {
//If the AppModel is configured with cases of where to hide metrics,
if (
typeof MetacatUI.appModel.get("hideMetricsWhen") == "object" &&
MetacatUI.appModel.get("hideMetricsWhen")
) {
//Check for at least one match
return _.some(
MetacatUI.appModel.get("hideMetricsWhen"),
function (value, modelProperty) {
//Get the value of this property from this model
var modelValue = this.get(modelProperty);
//Check for the presence of this model's value in the AppModel value
if (Array.isArray(value) && typeof modelValue == "string") {
return _.contains(value, modelValue);
}
//Check for the presence of the AppModel's value in this model's value
else if (typeof value == "string" && Array.isArray(modelValue)) {
return _.contains(modelValue, value);
}
//Check for overlap of two arrays
else if (Array.isArray(value) && Array.isArray(modelValue)) {
return _.intersection(value, modelValue).length > 0;
}
//If the AppModel value is a function, execute it
else if (typeof value == "function") {
return value(modelValue);
}
//Otherwise, just check for equality
else {
return value === modelValue;
}
},
this,
);
} else {
return false;
}
},
/**
* Creates a URL for viewing more information about this metadata
* @return {string}
*/
createViewURL: function () {
return this.getType() == "portal" || this.getType() == "collection"
? MetacatUI.root +
"/" +
MetacatUI.appModel.get("portalTermPlural") +
"/" +
encodeURIComponent(
this.get("label") || this.get("seriesId") || this.get("id"),
)
: MetacatUI.root +
"/view/" +
encodeURIComponent(this.get("seriesId") || this.get("id"));
},
parseResourceMapField: function (json) {
if (typeof json.resourceMap == "string") {
return json.resourceMap.trim();
} else if (Array.isArray(json.resourceMap)) {
let newResourceMapIds = [];
_.each(json.resourceMap, function (rMapId) {
if (typeof rMapId == "string") {
newResourceMapIds.push(rMapId.trim());
}
});
return newResourceMapIds;
}
//If nothing works so far, return an empty array
return [];
},
},
);

Fix ES Linting errors for DataONEObject & Package model
Ref: #2483
Add ES Lint fixes for the DataPackage collection
Copy link
Contributor

@github-actions github-actions bot left a 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

prettier

[prettier] reported by reviewdog 🐶

success (response) {


[prettier] reported by reviewdog 🐶


[prettier] reported by reviewdog 🐶

getMember (context, args) {


[prettier] reported by reviewdog 🐶

triggerComplete (model) {


[prettier] reported by reviewdog 🐶

const notSynced = this.reject((m) => m.get("synced") || m.get("id") == model.get("id"));


[prettier] reported by reviewdog 🐶

recordProvEdit (operation, subject, predicate, object) {


[prettier] reported by reviewdog 🐶

const editFound = _.find(this.provEdits, (edit) => (


[prettier] reported by reviewdog 🐶

edit[3] == object
));


[prettier] reported by reviewdog 🐶

provEditsPending () {


[prettier] reported by reviewdog 🐶

saveProv () {
const {rdf} = this;


[prettier] reported by reviewdog 🐶

const {provEdits} = this;


[prettier] reported by reviewdog 🐶

const PROV = rdf.Namespace(this.namespaces.PROV);
const PROVONE = rdf.Namespace(this.namespaces.PROVONE);
const DCTERMS = rdf.Namespace(this.namespaces.DCTERMS);
const CITO = rdf.Namespace(this.namespaces.CITO);
const XSD = rdf.Namespace(this.namespaces.XSD);


[prettier] reported by reviewdog 🐶

let operation; let subject; let predicate; let object;


[prettier] reported by reviewdog 🐶

let executionId; let executionURI; let executionNode;
let programId; let programURI; let programNode;
let dataId; let dataURI; let dataNode;
let derivedDataURI; let derivedDataNode;


[prettier] reported by reviewdog 🐶

const objectNode = rdf.sym(this.getURIFromRDF(object));


[prettier] reported by reviewdog 🐶

addToGraph (subject, predicate, object) {


[prettier] reported by reviewdog 🐶

removeIfLastProvRef (subjectNode, predicateNode, objectNode) {


[prettier] reported by reviewdog 🐶

removeOrphanedBlankNodes () {


[prettier] reported by reviewdog 🐶

getExecutionId (programId) {
const {rdf} = this;


[prettier] reported by reviewdog 🐶

const DCTERMS = rdf.Namespace(this.namespaces.DCTERMS);
const PROV = rdf.Namespace(this.namespaces.PROV);
const PROVONE = rdf.Namespace(this.namespaces.PROVONE);


[prettier] reported by reviewdog 🐶

getExecutionNode (executionId) {
const {rdf} = this;


[prettier] reported by reviewdog 🐶

}
return testNode;
}
// The executionNode was found in the RDF graph as a urn
var executionNode = stmts[0].subject;
return executionNode;


[prettier] reported by reviewdog 🐶

addProgramToGraph (programId) {
const {rdf} = this;


[prettier] reported by reviewdog 🐶

const DCTERMS = rdf.Namespace(this.namespaces.DCTERMS);
const PROV = rdf.Namespace(this.namespaces.PROV);
const PROVONE = rdf.Namespace(this.namespaces.PROVONE);
const XSD = rdf.Namespace(this.namespaces.XSD);


[prettier] reported by reviewdog 🐶

executionId = `urn:uuid:${ uuid.v4()}`;


[prettier] reported by reviewdog 🐶

removeProgramFromGraph (programId) {


[prettier] reported by reviewdog 🐶

const {rdf} = this;


[prettier] reported by reviewdog 🐶

const DCTERMS = rdf.Namespace(this.namespaces.DCTERMS);
const PROV = rdf.Namespace(this.namespaces.PROV);
const PROVONE = rdf.Namespace(this.namespaces.PROVONE);
const XSD = rdf.Namespace(this.namespaces.XSD);


[prettier] reported by reviewdog 🐶


[prettier] reported by reviewdog 🐶

let oldPidVariations;
let modifiedDate;
let subjectClone;
let predicateClone;
let objectClone;


[prettier] reported by reviewdog 🐶

const CITO = this.rdf.Namespace(this.namespaces.CITO);
const DC = this.rdf.Namespace(this.namespaces.DC);
const DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS);
const FOAF = this.rdf.Namespace(this.namespaces.FOAF);
const RDF = this.rdf.Namespace(this.namespaces.RDF);
const XSD = this.rdf.Namespace(this.namespaces.XSD);


[prettier] reported by reviewdog 🐶

const oldPid = this.packageModel.get("oldPid");
let cnResolveUrl = this.getCnURI();


[prettier] reported by reviewdog 🐶

const rMapIdNode = this.rdf.lit(pid);


[prettier] reported by reviewdog 🐶

aggregationNode = this.rdf.sym(`${oldPid }#aggregation`);


[prettier] reported by reviewdog 🐶

`${oldPid }#aggregation`,


[prettier] reported by reviewdog 🐶

subjectClone.value =
`${this.getURIFromRDF(pid) }#aggregation`;


[prettier] reported by reviewdog 🐶

objectClone.value =
`${this.getURIFromRDF(pid) }#aggregation`;


[prettier] reported by reviewdog 🐶

this.rdf.sym(`${this.getURIFromRDF(pid) }#aggregation`),


[prettier] reported by reviewdog 🐶

this.rdf.sym(`${this.getURIFromRDF(pid) }#aggregation`),


[prettier] reported by reviewdog 🐶

`${this.getURIFromRDF(this.packageModel.id) }#aggregation`,


[prettier] reported by reviewdog 🐶

`${MetacatUI.appUserModel.get("firstName") || ""
} ${
MetacatUI.appUserModel.get("lastName") || ""}`,


[prettier] reported by reviewdog 🐶

const idLiteral = this.rdf.lit(this.packageModel.id, "", XSD("string"));


[prettier] reported by reviewdog 🐶

cloneNode (nodeToClone) {


[prettier] reported by reviewdog 🐶

}
return this.rdf.literal(nodeToClone.value);


[prettier] reported by reviewdog 🐶

`ERROR: unknown node type to clone: ${ nodeToClone.termType}`,


[prettier] reported by reviewdog 🐶

addToAggregation (id) {


[prettier] reported by reviewdog 🐶

const DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS);
const XSD = this.rdf.Namespace(this.namespaces.XSD);
const CITO = this.rdf.Namespace(this.namespaces.CITO);


[prettier] reported by reviewdog 🐶

const rMapURI = this.getURIFromRDF(this.packageModel.get("id"));
const mapNode = this.rdf.sym(rMapURI);
const aggNode = this.rdf.sym(`${rMapURI }#aggregation`);
const idNode = this.rdf.literal(id, undefined, XSD("string"));
let idStatements = [];
let aggStatements = [];
let aggByStatements = [];
let documentsStatements = [];
let isDocumentedByStatements = [];


[prettier] reported by reviewdog 🐶

const isDocBy = model.get("isDocumentedBy");
const documents = model.get("documents");


[prettier] reported by reviewdog 🐶

_.map(this.models, (m) => {
if (m.get("formatType") == "METADATA") return m;
}),
);
const metadataInPackageIDs = _.each(metadataInPackage, (m) => m.get("id"));


[prettier] reported by reviewdog 🐶

removeFromAggregation (id) {


[prettier] reported by reviewdog 🐶

getURIFromRDF (id) {


[prettier] reported by reviewdog 🐶

const DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS);
const idNode = this.rdf.literal(id, undefined, XSD("string"));
// Find the identifier statements for the given id
const idStatements = this.dataPackageGraph.statementsMatching(
undefined,
DCTERMS("identifier"),
idNode,
);


[prettier] reported by reviewdog 🐶

}
return this.getCnURI() + encodeURIComponent(id);


[prettier] reported by reviewdog 🐶


[prettier] reported by reviewdog 🐶

} if (this.packageModel.get("oldPid")) {


[prettier] reported by reviewdog 🐶

const idStatements = this.dataPackageGraph.statementsMatching(
undefined,
undefined,
idNode,
);
const idStatement = idStatements.length ? idStatements[0] : null;


[prettier] reported by reviewdog 🐶


[prettier] reported by reviewdog 🐶


[prettier] reported by reviewdog 🐶

const id = this.models[i].get("id");
let origIsDocBy = this.originalIsDocBy[id];


[prettier] reported by reviewdog 🐶

getQueue () {
return this.filter((m) => m.get("uploadStatus") == "q" || m.get("uploadStatus") == "p");


[prettier] reported by reviewdog 🐶

addNewModel (model) {


[prettier] reported by reviewdog 🐶

handleAdd (dataONEObject) {


[prettier] reported by reviewdog 🐶

} else if (!_.contains(metadataModel.get("documents"), dataONEObject.id))
metadataModel.get("documents").push(dataONEObject.id);


[prettier] reported by reviewdog 🐶

fetchFromIndex () {


[prettier] reported by reviewdog 🐶

mergeModels (otherModels, fieldsToMerge) {


[prettier] reported by reviewdog 🐶

// Get a JSON object of all the attributes on this model
const otherModelAttr = otherModel.toJSON();
// Start an array of attributes to omit during the merge
const omitKeys = [];


[prettier] reported by reviewdog 🐶

updateRelationships () {


[prettier] reported by reviewdog 🐶

saveReference (model) {


[prettier] reported by reviewdog 🐶

broadcastAccessPolicy (accessPolicy) {


[prettier] reported by reviewdog 🐶

setLoadingFiles (dataONEObject) {


[prettier] reported by reviewdog 🐶

getAtLocation () {

@rushirajnenuji rushirajnenuji marked this pull request as ready for review September 5, 2024 17:02
@robyngit robyngit self-assigned this Sep 5, 2024
Copy link
Member

@robyngit robyngit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good to have the bytesToSize consolidated into one location! Everything seems to still work & I see that your ES-lint changes are a WIP so we can ignore the remaining errors for this PR.

Approve conditional on a run of npm run format (DataPackage has some prettier errors). Feel free to merge after that! 🎉

@robyngit robyngit linked an issue Sep 5, 2024 that may be closed by this pull request
2 tasks
@robyngit
Copy link
Member

robyngit commented Sep 9, 2024

@rushirajnenuji, I pushed a little fix for the formatting issue with DataPackage hoping to get the branch merged. There remain two small conflicts with develop in the MetadataView that need to be resolved before it can be merged.

@rushirajnenuji
Copy link
Member Author

Thank you for adding the reviewing and testing this PR, and for adding the formatting fix @robyngit , I've got this branch up to speed with develop, and is ready for the merge.

@robyngit robyngit merged commit b057aab into develop Sep 9, 2024
1 check failed
@robyngit robyngit deleted the enhancement-2483-2484-FH-2 branch September 9, 2024 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Move repeated method to Utilities module
2 participants