define([
"underscore",
"jquery",
"backbone",
"models/DataONEObject",
"models/metadata/eml211/EML211",
"models/metadata/eml211/EMLOtherEntity",
"views/DownloadButtonView",
"text!templates/dataItem.html",
"text!templates/dataItemHierarchy.html",
], function (
_,
$,
Backbone,
DataONEObject,
EML,
EMLOtherEntity,
DownloadButtonView,
DataItemTemplate,
DataItemHierarchy,
) {
/**
* @class DataItemView
* @classdesc A DataItemView represents a single data item in a data package as a single row of
a nested table. An item may represent a metadata object (as a folder), or a data
object described by the metadata (as a file). Every metadata DataItemView has a
resource map associated with it that describes the relationships between the
aggregated metadata and data objects.
* @classcategory Views
* @constructor
* @screenshot views/DataItemView.png
*/
var DataItemView = Backbone.View.extend(
/** @lends DataItemView.prototype */ {
tagName: "tr",
className: "data-package-item",
id: null,
/** The HTML template for a data item */
template: _.template(DataItemTemplate),
/** The HTML template for a data item */
dataItemHierarchyTemplate: _.template(DataItemHierarchy),
//Templates
metricTemplate: _.template(
"<span class='packageTable-resultItem badge '>" +
"<i class='catalog-metric-icon <%= metricIcon %>'>" +
"</i> <%= memberRowMetrics %> " +
"</span>",
),
/**
* The DataONEObject model to display in this view
* @type {DataONEObject}
*/
model: null,
/**
* A reference to the parent EditorView that contains this DataItemView
* @type EditorView
* @since 2.15.0
*/
parentEditorView: null,
/** Events this view listens to */
events: {
"focusout .name.canRename": "updateName",
"click .name.canRename": "emptyName",
"click .duplicate": "duplicate", // Edit dropdown, duplicate scimeta/rdf
"click .addFolder": "handleAddFolder", // Edit dropdown, add nested scimeta/rdf
"click .addFiles": "handleAddFiles", // Edit dropdown, open file picker dialog
"change .file-upload": "addFiles", // Adds the files into the collection
"change .file-replace": "replaceFile", // Replace a file in the collection
dragover: "showDropzone", // Drag & drop, show the dropzone for this row
dragend: "hideDropzone", // Drag & drop, hide the dropzone for this row
dragleave: "hideDropzone", // Drag & drop, hide the dropzone for this row
drop: "addFiles", // Drag & drop, adds the files into the collection
"click .replaceFile": "handleReplace", // Replace dropdown, data in collection
"click .removeFiles": "handleRemove", // Edit dropdown, remove sci{data,meta} from collection
"click .cancel": "handleCancel", // Cancel a file load
"change: percentLoaded": "updateLoadProgress", // Update the file read progress bar
"mouseover .remove": "previewRemove",
"mouseout .remove": "previewRemove",
"change .public": "changeAccessPolicy",
"click .downloadAction": "downloadFile",
},
/** Initialize the object - post constructor */
initialize: function (options) {
if (typeof options == "undefined") var options = {};
this.model = options.model || new DataONEObject();
this.currentlyViewing = options.currentlyViewing || null;
this.mode = options.mode || "edit";
this.itemName = options.itemName || null;
this.itemPath = options.itemPath || null;
this.itemType = options.itemType || "file";
this.insertInfoIcon = options.insertInfoIcon || false;
this.id = this.model.get("id");
this.canWrite = false; // Default. Updated in render()
this.canShare = false; // Default. Updated in render()
this.parentEditorView = options.parentEditorView || null;
this.dataPackageId = options.dataPackageId || null;
if (!(typeof options.metricsModel == "undefined")) {
this.metricsModel = options.metricsModel;
}
},
/** Renders a DataItemView for the given DataONEObject
* @param {DataONEObject} model
*/
render: function (model) {
//Prevent duplicate listeners
this.stopListening();
if (this.itemType === "folder") {
// Set the data-id for identifying events to model ids
this.$el.attr(
"data-id",
(this.itemPath ? this.itemPath : "") + "/" + this.itemName,
);
this.$el.attr("data-parent", this.itemPath ? this.itemPath : "");
this.$el.attr("data-category", "entities-" + this.itemName);
var attributes = new Object();
attributes.fileType = undefined;
attributes.isFolder = true;
attributes.icon = "icon-folder-open";
attributes.id = this.itemName;
attributes.size = undefined;
attributes.insertInfoIcon = false;
attributes.memberRowMetrics = undefined;
attributes.isMetadata = false;
attributes.downloadUrl = undefined;
attributes.moreInfoLink = undefined;
// attributes.isMetadata = false;
attributes.viewType = this.mode;
attributes.objectTitle = this.itemName;
var itemPathParts = new Array();
if (this.itemPath) {
itemPathParts = this.itemPath.split("/");
attributes.nodeLevel = itemPathParts.length;
if (this.itemPath.startsWith("/")) {
attributes.nodeLevel -= 1;
}
if (this.itemPath.endsWith("/")) {
attributes.nodeLevel -= 1;
}
if (itemPathParts[-1] == attributes.objectTitle) {
attributes.nodeLevel -= 1;
}
} else {
attributes.nodeLevel = 0;
this.itemPath = "/";
this.$el.attr("data-packageId", this.dataPackageId);
}
this.$el.html(this.dataItemHierarchyTemplate(attributes));
} else {
// Set the data-id for identifying events to model ids
this.$el.attr("data-id", this.model.get("id"));
this.$el.attr("data-category", "entities-" + this.model.get("id"));
//Destroy the old tooltip
this.$(".status .icon, .status .progress")
.tooltip("hide")
.tooltip("destroy");
var attributes = this.model.toJSON();
// check if this data item is a metadata object
attributes.isMetadata = false;
if (
this.model.get("type") == "Metadata" ||
this.model.get("formatType") == "METADATA"
) {
attributes.isMetadata = true;
}
//Format the title
if (Array.isArray(attributes.title)) {
attributes.title = attributes.title[0];
}
//Set some defaults
attributes.numAttributes = 0;
attributes.entityIsValid = true;
attributes.hasInvalidAttribute = false;
attributes.viewType = this.mode;
if (this.mode === "edit") {
// Restrict item replacement and renaming depending on access policy
//
// Note: .canWrite is set here (at render) instead of at init
// because render will get called a few times during page load
// as the app updates what it knows about the object
let accessPolicy = this.model.get("accessPolicy");
if (accessPolicy) {
attributes.canWrite = accessPolicy.isAuthorized("write");
this.canWrite = attributes.canWrite;
attributes.canRename = accessPolicy.isAuthorizedUpdateSysMeta();
} else {
attributes.canWrite = false;
this.canWrite = false;
attributes.canRename = false;
}
// Restrict item sharing depending on access
this.canShare = this.canShareItem();
attributes.canShare = this.canShare;
//Get the number of attributes for this item
if (this.model.type != "EML") {
//Get the parent EML model
if (this.parentEML) {
var parentEML = this.parentEML;
} else {
var parentEML = MetacatUI.rootDataPackage.where({
id: Array.isArray(this.model.get("isDocumentedBy"))
? this.model.get("isDocumentedBy")[0]
: null,
});
}
if (Array.isArray(parentEML)) parentEML = parentEML[0];
//If we found a parent EML model
if (parentEML && parentEML.type == "EML") {
this.parentEML = parentEML;
//Find the EMLEntity model for this data item
var entity =
this.model.get("metadataEntity") ||
parentEML.getEntity(this.model);
//If we found an EMLEntity model
if (entity) {
this.entity = entity;
//Get the file name from the metadata if it is not in the model
if (!this.model.get("fileName")) {
var fileName = "";
if (entity.get("physicalObjectName"))
fileName = entity.get("physicalObjectName");
else if (entity.get("entityName"))
fileName = entity.get("entityName");
if (fileName) attributes.fileName = fileName;
this.model.set("fileName", fileName);
}
//Get the number of attributes for this entity
attributes.numAttributes = entity.get("attributeList").length;
//Determine if the entity model is valid
attributes.entityIsValid = entity.isValid();
//Listen to changes to certain attributes of this EMLEntity model
// to re-render this view
this.stopListening(entity);
this.listenTo(
entity,
"change:entityType, change:entityName",
this.render,
);
//Check if there are any invalid attribute models
//Also listen to each attribute model
_.each(
entity.get("attributeList"),
function (attr) {
var isValid = attr.isValid();
//Mark that this entity has at least one invalid attribute
if (!attributes.hasInvalidAttribute && !isValid)
attributes.hasInvalidAttribute = true;
this.stopListening(attr);
//Listen to when the validation status changes and rerender
if (isValid) this.listenTo(attr, "invalid", this.render);
else this.listenTo(attr, "valid", this.render);
},
this,
);
//If there are no attributes now, rerender when one is added
this.listenTo(entity, "change:attributeList", this.render);
} else {
//Rerender when an entity is added
this.listenTo(this.model, "change:entities", this.render);
}
} else {
//When the package is complete, rerender
this.listenTo(
MetacatUI.rootDataPackage,
"add:EML",
this.render,
);
}
}
this.$el.html(this.template(attributes));
//Initialize dropdowns
this.$el.find(".dropdown-toggle").dropdown();
//Render the Share button
this.renderShareControl();
if (this.model.get("type") == "Metadata") {
//Add the title data-attribute attribute to the name cell
this.$el.find(".name").attr("data-attribute", "title");
this.$el.addClass("folder");
} else {
this.$el.addClass("data");
}
// Add tooltip to a disabled Replace link
$(this.$el)
.find(".replace.disabled")
.tooltip({
title:
"You don't have sufficient privileges to replace this item.",
placement: "left",
trigger: "hover",
delay: { show: 400 },
container: "body",
});
//Check if the data package is in progress of being uploaded
this.toggleSaving();
//Create tooltips based on the upload status
var uploadStatus = this.model.get("uploadStatus"),
errorMessage = this.model.get("errorMessage");
// Use a friendlier message for 401 errors (the one returned is a little hard to understand)
if (this.model.get("sysMetaErrorCode") == 401) {
// If the user at least has write permission, they cannot update the system metadata only, so show this message
/** @todo Do an object update when someone has write permission but not changePermission and is trying to change the system metadata (but not the access policy) */
if (accessPolicy && accessPolicy.isAuthorized("write")) {
errorMessage =
"The owner of this data file has not given you permission to rename it or change the " +
MetacatUI.appModel.get("accessPolicyName") +
".";
// Otherwise, assume they only have read access
} else {
errorMessage =
"The owner of this data file has not given you permission to edit this data file or change the " +
MetacatUI.appModel.get("accessPolicyName") +
".";
}
}
// When there's an error or a warninig
if (uploadStatus == "e" && errorMessage) {
var tooltipClass = uploadStatus == "e" ? "error" : "";
this.$(".status .icon").tooltip({
placement: "top",
trigger: "hover",
html: true,
title:
"<div class='status-tooltip " +
tooltipClass +
"'><h6>Issue saving:</h6><div>" +
errorMessage +
"</div></div>",
container: "body",
});
this.$el.removeClass("loading");
} else if (
(!uploadStatus || uploadStatus == "c" || uploadStatus == "q") &&
attributes.numAttributes == 0
) {
this.$(".status .icon").tooltip({
placement: "top",
trigger: "hover",
html: true,
title:
"<div class='status-tooltip'>This file needs to be described - Click 'Describe'</div>",
container: "body",
});
this.$el.removeClass("loading");
} else if (
attributes.hasInvalidAttribute ||
!attributes.entityIsValid
) {
this.$(".status .icon").tooltip({
placement: "top",
trigger: "hover",
html: true,
title:
"<div class='status-tooltip'>There is missing information about this file. Click 'Describe'</div>",
container: "body",
});
this.$el.removeClass("loading");
} else if (uploadStatus == "c") {
this.$(".status .icon").tooltip({
placement: "top",
trigger: "hover",
html: true,
title: "<div class='status-tooltip'>Complete</div>",
container: "body",
});
this.$el.removeClass("loading");
} else if (uploadStatus == "l") {
this.$(".status .icon").tooltip({
placement: "top",
trigger: "hover",
html: true,
title: "<div class='status-tooltip'>Reading file...</div>",
container: "body",
});
this.$el.addClass("loading");
} else if (uploadStatus == "p") {
var model = this.model;
this.$(".status .progress").tooltip({
placement: "top",
trigger: "hover",
html: true,
title: function () {
if (model.get("numSaveAttempts") > 0) {
return (
"<div class='status-tooltip'>Something went wrong during upload. <br/> Trying again... (attempt " +
(model.get("numSaveAttempts") + 1) +
" of 3)</div>"
);
} else if (model.get("uploadProgress")) {
var percentDone = model.get("uploadProgress").toString();
if (percentDone.indexOf(".") > -1)
percentDone = percentDone.substring(
0,
percentDone.indexOf("."),
);
} else var percentDone = "0";
return (
"<div class='status-tooltip'>Uploading: " +
percentDone +
"%</div>"
);
},
container: "body",
});
this.$el.addClass("loading");
} else {
this.$el.removeClass("loading");
}
//Listen to changes to the upload progress of this object
this.listenTo(
this.model,
"change:uploadProgress",
this.showUploadProgress,
);
//Listen to changes to the upload status of the entire package
this.listenTo(
MetacatUI.rootDataPackage.packageModel,
"change:uploadStatus",
this.toggleSaving,
);
//listen for changes to rerender the view
this.listenTo(
this.model,
"change:fileName change:title change:id change:formatType " +
"change:formatId change:type change:resourceMap change:documents change:isDocumentedBy " +
"change:size change:nodeLevel change:uploadStatus",
this.render,
); // render changes to the item
var view = this;
this.listenTo(this.model, "replace", function (newModel) {
view.model = newModel;
view.render();
});
} else {
this.isMetadata = false;
// format metadata object title
if (
attributes.isMetadata ||
this.model.getFormat() == "metadata" ||
this.model.get("id") == this.currentlyViewing
) {
attributes.title = "Metadata: " + this.model.get("fileName");
attributes.icon = "icon-file-text";
attributes.metricIcon = "icon-eye-open";
this.isMetadata = true;
this.$el.attr("data-packageId", this.dataPackageId);
}
var objectTitleTooltip =
attributes.title || attributes.fileName || attributes.id;
attributes.objectTitle =
objectTitleTooltip.length > 150
? objectTitleTooltip.slice(0, 75) +
"..." +
objectTitleTooltip.slice(
objectTitleTooltip.length - 75,
objectTitleTooltip.length,
)
: objectTitleTooltip;
attributes.fileType = this.model.getFormat();
attributes.isFolder = false;
//Determine the icon type based on format type
if (this.model.getFormat() == "program")
attributes.icon = "icon-code";
else if (this.model.getFormat() == "data")
attributes.icon = "icon-table";
else if (this.model.getFormat() == "image/jpeg")
attributes.icon = "icon-picture";
else if (this.model.getFormat() == "PDF")
attributes.icon = "icon-file";
else attributes.icon = "icon-table";
attributes.id = this.model.get("id");
attributes.memberRowMetrics = null;
var metricToolTip = null,
view = this;
// Insert metrics for this item,
// if the model has already been fethced.
if (this.metricsModel.get("views") !== null) {
metricToolTip = this.getMemberRowMetrics(view.id);
attributes.memberRowMetrics = metricToolTip.split(" ")[0];
} else {
// Update the metrics later on
// If the fetch() is still in progress.
this.listenTo(this.metricsModel, "sync", function () {
metricToolTip = this.getMemberRowMetrics(view.id);
let readsCell = this.$(
'.metrics-count.downloads[data-id="' + view.id + '"]',
);
metricToolTip = view.getMemberRowMetrics(view.id);
if (typeof metricToolTip !== "undefined" && metricToolTip)
readsCell.html(
this.metricTemplate({
metricIcon: attributes.metricIcon,
memberRowMetrics: metricToolTip.split(" ")[0],
}),
);
});
}
// add nodeLevel for displaying indented filename
attributes.nodeLevel = 1;
if (
!(
attributes.isMetadata ||
this.model.getFormat() == "metadata" ||
this.model.get("id") == this.currentlyViewing
)
) {
attributes.metricIcon = "icon-cloud-download";
this.$el.addClass();
if (
this.itemPath &&
typeof this.itemPath !== undefined &&
this.itemPath != "/"
) {
itemPathParts = this.itemPath.split("/");
attributes.nodeLevel = itemPathParts.length;
if (this.itemPath.startsWith("/")) {
attributes.nodeLevel -= 1;
}
if (this.itemPath.endsWith("/")) {
attributes.nodeLevel -= 1;
}
// var parent = itemPathParts[itemPathParts.length - 2];
var parentPath = itemPathParts.slice(0, -1).join("/");
if (parentPath !== undefined) {
this.$el.attr("data-parent", parentPath);
}
} else {
attributes.nodeLevel = 1;
this.$el.attr("data-packageId", this.dataPackageId);
}
}
if (attributes.nodeLevel == 1) {
this.$el.attr("data-packageId", this.dataPackageId);
}
//Download button
attributes.downloadUrl = undefined;
if (
this.model.get("dataUrl") !== undefined ||
this.model.get("url") !== undefined ||
this.model.url() !== undefined
) {
if (this.model.get("dataUrl") !== undefined) {
attributes.downloadUrl = this.model.get("dataUrl");
} else if (this.model.get("url") !== undefined) {
attributes.downloadUrl = this.model.get("url");
} else if (this.model.url() !== undefined) {
var downloadUrl = this.model.url();
attributes.downloadUrl = downloadUrl.replace(
"/meta/",
"/object/",
);
}
}
this.downloadButtonView = new DownloadButtonView({
model: this.model,
view: "actionsView",
});
this.downloadButtonView.render();
let id = this.model.get("id");
let infoLink =
MetacatUI.root +
"/view/" +
encodeURIComponent(this.currentlyViewing) +
"#" +
encodeURIComponent(id);
attributes.moreInfoLink = infoLink;
attributes.insertInfoIcon = this.insertInfoIcon;
this.$el.html(this.dataItemHierarchyTemplate(attributes));
this.$(".downloadAction").html(this.downloadButtonView.el);
// add tooltip for metrics in package table
this.$(".packageTable-resultItem").tooltip({
placement: "top",
trigger: "hover",
delay: 300,
title: metricToolTip,
});
this.$(".fileTitle")
.addClass("tooltip-this")
.attr("data-placement", "top")
.attr("data-trigger", "hover")
.attr("data-delay", "300")
.attr("data-title", objectTitleTooltip);
}
}
this.$el.data({
view: this,
model: this.model,
});
return this;
},
/**
* Renders a button that opens the AccessPolicyView for editing permissions on this data item
* @since 2.15.0
*/
renderShareControl: function () {
//Get the Share button element
var shareButton = this.$(".sharing button");
if (
this.parentEditorView &&
this.parentEditorView.isAccessPolicyEditEnabled()
) {
//Start a title for the button tooltip
var sharebuttonTitle;
// If the user is not authorized to change the permissions of
// this object, then disable the share button
if (this.canShare) {
shareButton.removeClass("disabled");
sharebuttonTitle = "Share this item with others";
} else {
shareButton.addClass("disabled");
sharebuttonTitle = "You are not authorized to share this item.";
}
// Set up tooltips for share button
shareButton.tooltip({
title: sharebuttonTitle,
placement: "top",
container: this.el,
trigger: "hover",
delay: { show: 400 },
});
} else {
shareButton.remove();
}
},
/** Close the view and remove it from the DOM */
onClose: function () {
this.remove(); // remove for the DOM, stop listening
this.off(); // remove callbacks, prevent zombies
},
/**
Generate a unique id for each data item in the table
TODO: This could be replaced with the DataONE identifier
*/
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;
},
/**
* Update the folder name based on the scimeta title
*
* @param e The event triggering this method
*/
updateName: function (e) {
var enteredText = this.cleanInput($(e.target).text().trim());
// Set the title if this item is metadata or set the file name
// if its not
if (this.model.get("type") == "Metadata") {
var title = this.model.get("title");
// Get the current title which is either an array of titles
// or a single string. When it's an array of strings, we
// use the first as the canonical title
var currentTitle = Array.isArray(title) ? title[0] : title;
// Don't set the title if it hasn't changed or is empty
if (
enteredText !== "" &&
currentTitle !== enteredText &&
enteredText !== "Untitled dataset"
) {
// Set the new title, upgrading any title attributes
// that aren't Arrays into Arrays
if (
(Array.isArray(title) && title.length < 2) ||
typeof title == "string"
) {
this.model.set("title", [enteredText]);
} else {
title[0] = enteredText;
}
}
} else {
this.model.set("fileName", enteredText);
// Reset sysMetaUploadStatus only if this item doesn't
// have content changes. This is here because replaceFile
// sets sysMetaUploadStatus to "c" to prevent the editor
// from updating sysmeta after the update call
if (!this.model.get("hasContentChanges")) {
this.model.set("sysMetaUploadStatus", null);
}
}
},
/**
Handle the add file event, showing the file picker dialog
Multiple files are allowed using the shift and or option/alt key
@param {Event} event
*/
handleAddFiles: function (event) {
event.stopPropagation();
var fileUploadElement = this.$(".file-upload");
fileUploadElement.val("");
if (fileUploadElement) {
fileUploadElement.click();
}
event.preventDefault();
},
/**
With a file list from the file picker or drag and drop,
add the files to the collection
@param {Event} event
*/
addFiles: function (event) {
var fileList, // The list of chosen files
parentDataPackage, // The id of the first resource of this row's scimeta
self = this; // A reference to this view
event.stopPropagation();
event.preventDefault();
// handle drag and drop files
if (typeof event.originalEvent.dataTransfer !== "undefined") {
fileList = event.originalEvent.dataTransfer.files;
// handle file picker files
} else {
if (event.target) {
fileList = event.target.files;
}
}
this.$el.removeClass("droppable");
// Find the correct collection to add to. Use JQuery's delegateTarget
// attribute corresponding to the element where the event handler was attached
if (typeof event.delegateTarget.dataset.id !== "undefined") {
this.parentSciMeta = this.getParentScienceMetadata(event);
this.collection = this.getParentDataPackage(event);
// Read each file, and make a DataONEObject
_.each(
fileList,
function (file) {
var uploadStatus = "l",
errorMessage = "";
if (file.size == 0) {
uploadStatus = "e";
errorMessage =
"This is an empty file. It won't be included in the dataset.";
}
var dataONEObject = new DataONEObject({
synced: true,
type: "Data",
fileName: file.name,
size: file.size,
mediaType: file.type,
uploadFile: file,
uploadStatus: uploadStatus,
errorMessage: errorMessage,
isDocumentedBy: [this.parentSciMeta.id],
isDocumentedByModels: [this.parentSciMeta],
resourceMap: [this.collection.packageModel.id],
});
// Add it to the parent collection
this.collection.add(dataONEObject);
// Asychronously calculate the checksum
if (
dataONEObject.get("uploadFile") &&
!dataONEObject.get("checksum")
) {
dataONEObject.stopListening(
dataONEObject,
"checksumCalculated",
);
dataONEObject.listenToOnce(
dataONEObject,
"checksumCalculated",
dataONEObject.save,
);
try {
dataONEObject.calculateChecksum();
} catch (exception) {
// TODO: Fail gracefully here for the user
}
}
},
this,
);
}
},
/** Show the drop zone for this row in the table */
showDropzone: function () {
if (this.model.get("type") !== "Metadata") return;
this.$el.addClass("droppable");
},
/** Hide the drop zone for this row in the table */
hideDropzone: function (event) {
if (this.model.get("type") !== "Metadata") return;
this.$el.removeClass("droppable");
},
/**
* Handle the user's click of the Replace item in the DataItemView
* dropdown. Triggers replaceFile after some basic validation.
*
* Called indirectly via the "click" event on elements with the
* class .replaceFile. See this View's events map.
*
* @param {MouseEvent} event Browser Click event
*/
handleReplace: function (event) {
event.stopPropagation();
// Stop immediately if we know the user doesn't have privs
if (!this.canWrite) {
event.preventDefault();
return;
}
var fileReplaceElement = $(event.target)
.parents(".dropdown-menu")
.children(".file-replace");
if (!fileReplaceElement) {
console.log("Unable to find Replace file picker.");
return;
}
fileReplaceElement.val("");
fileReplaceElement.trigger("click");
event.preventDefault();
},
/**
* Replace a file (DataONEObject) in the collection with another one
* from a file picker. Maintains attributes on the original
* DataONEObject and maintains the entity information in the parent
* collection's metadata record (i.e., keeps your attributes, etc.).
*
* Called indirectly via the "change" event on elements with the
* class .file-upload. See this View's events map.
*
* The bulk of the work is done in a try-catch block to catch
* mistakes that would cause the editor to get into a broken state.
* On error, we attempt to return the editor back to its pre-replace
* state.
*
* @param {Event} event
*/
replaceFile: function (event) {
event.stopPropagation();
event.preventDefault();
if (!this.canWrite) {
return;
}
var fileList = event.target.files;
// Pre-check fileList value to make sure we can work with it
if (fileList.length != 1) {
// TODO: Show error, find out how to do this
return;
}
if (typeof event.delegateTarget.dataset.id === "undefined") {
// TODO: Show error, find out how to do this
return;
}
// Save uploadStatus for reverting if need to
var oldUploadStatus = this.model.get("uploadStatus");
var file = fileList[0],
uploadStatus = "q",
errorMessage = "";
if (file.size == 0) {
uploadStatus = "e";
errorMessage =
"This is an empty file. It won't be included in the dataset.";
}
if (!this.model) {
console.log(
"Couldn't find model we're supposed to be replacing. Stopping.",
);
return;
}
// Copy model attributes aside for reverting on error
var newAttributes = {
synced: false,
fileName: file.name,
size: file.size,
mediaType: file.type,
uploadFile: file,
hasContentChanges: true,
checksum: null,
uploadStatus: uploadStatus,
sysMetaUploadStatus: "c", // I set this so DataPackage::save
// wouldn't try to update the sysmeta after the update
errorMessage: errorMessage,
};
// Save a copy of the attributes we're changing so we can revert
// later if we encounter an exception
var oldAttributes = {};
_.each(
Object.keys(newAttributes),
function (k) {
oldAttributes[k] = _.clone(this.model.get(k));
},
this,
);
oldAttributes["uploadStatus"] = oldUploadStatus;
try {
this.model.set(newAttributes);
// Attempt the formatId. Defaults to app/octet-stream
this.model.set("formatId", this.model.getFormatId());
// Grab a reference to the entity in the EML for the object
// we're replacing
this.parentSciMeta = this.getParentScienceMetadata(event);
var entity = null;
if (this.parentSciMeta) {
entity = this.parentSciMeta.getEntity(this.model);
}
// Eagerly update the PID for this object so we can update
// the matching EML entity
this.model.updateID();
// Update the EML entity with the new id
if (entity) {
entity.set("xmlID", this.model.getXMLSafeID());
}
this.render();
if (this.model.get("uploadFile") && !this.model.get("checksum")) {
try {
this.model.calculateChecksum();
} catch (exception) {
// TODO: Fail gracefully here for the user
}
}
MetacatUI.rootDataPackage.packageModel.set("changed", true);
// Last, provided a visual indication the replace was completed
var describeButton = this.$el
.children(".controls")
.children(".btn-group")
.children("button.edit")
.first();
if (describeButton.length != 1) {
return;
}
var oldText = describeButton.html();
describeButton.html('<i class="icon icon-ok success" /> Replaced');
var previousBtnClasses = describeButton.attr("class");
describeButton.removeClass("warning error").addClass("message");
window.setTimeout(function () {
describeButton.html(oldText);
describeButton.addClass(previousBtnClasses).removeClass("message");
}, 3000);
} catch (error) {
console.log("Error replacing: ", error);
// Revert changes to the attributes
this.model.set(oldAttributes);
this.model.set("formatId", this.model.getFormatId());
this.model.set("sysMetaUploadStatus", "c"); // Prevents a sysmeta update
this.model.resetID();
this.render();
}
return;
},
/**
Handle remove events for this row in the data package table
@param {Event} event
*/
handleRemove: function (event) {
var eventId, // The id of the row of this event
removalIds = [], // The list of target ids to remove
dataONEObject, // The model represented by this row
documents; // The list of ids documented by this row (if meta)
event.stopPropagation();
event.preventDefault();
// Get the row id, add it to the remove list
if (typeof event.delegateTarget.dataset.id !== "undefined") {
eventId = event.delegateTarget.dataset.id;
removalIds.push(eventId);
}
this.parentSciMeta = this.getParentScienceMetadata(event);
if (!this.parentSciMeta) {
this.$(".status .icon, .status .progress")
.tooltip("hide")
.tooltip("destroy");
// Remove the row
this.remove();
return;
}
this.collection = this.getParentDataPackage(event);
// Get the corresponding model
if (typeof eventId !== "undefined") {
dataONEObject = this.collection.get(eventId);
}
// Is it nested science metadata?
if (dataONEObject && dataONEObject.get("type") == "Metadata") {
// We also remove the data documented by these metadata
documents = dataONEObject.get("documents");
if (documents.length > 0) {
_.each(documents, removalIds.push());
}
}
//Data objects may need to be removed from the EML model entities list
else if (dataONEObject && this.parentSciMeta.type == "EML") {
var matchingEntity = this.parentSciMeta.getEntity(dataONEObject);
if (matchingEntity) this.parentSciMeta.removeEntity(matchingEntity);
}
// Remove the id from the documents array in the science metadata
_.each(
removalIds,
function (id) {
var documents = this.parentSciMeta.get("documents");
var index = documents.indexOf(id);
if (index > -1) {
this.parentSciMeta.get("documents").splice(index, 1);
}
},
this,
);
// Remove each object from the collection
this.collection.remove(removalIds);
this.$(".status .icon, .status .progress")
.tooltip("hide")
.tooltip("destroy");
// Remove the row
this.remove();
MetacatUI.rootDataPackage.packageModel.set("changed", true);
},
/**
* Return the parent science metadata model associated with the
* data or metadata row of the UI event
* @param {Event} event
*/
getParentScienceMetadata: function (event) {
var parentMetadata, // The parent metadata array in the collection
eventModels, // The models associated with the event's table row
eventModel, // The model associated with the event's table row
parentSciMeta; // The parent science metadata for the event model
if (typeof event.delegateTarget.dataset.id !== "undefined") {
eventModels = MetacatUI.rootDataPackage.where({
id: event.delegateTarget.dataset.id,
});
if (eventModels.length > 0) {
eventModel = eventModels[0];
} else {
return;
}
// Is this a Data or Metadata model?
if (eventModel.get && eventModel.get("type") === "Metadata") {
return eventModel;
} else {
// It's data, get the parent scimeta
parentMetadata = MetacatUI.rootDataPackage.where({
id: Array.isArray(eventModel.get("isDocumentedBy"))
? eventModel.get("isDocumentedBy")[0]
: null,
});
if (parentMetadata.length > 0) {
parentSciMeta = parentMetadata[0];
return parentSciMeta;
} else {
//If there is only one metadata model in the root data package, then use that metadata model
var metadataModels = MetacatUI.rootDataPackage.where({
type: "Metadata",
});
if (metadataModels.length == 1) return metadataModels[0];
}
}
}
},
/**
* Return the parent data package collection associated with the
* data or metadata row of the UI event
* @param {Event} event
*/
getParentDataPackage: function (event) {
var parentSciMeta, parenResourceMaps, parentResourceMapId;
if (typeof event.delegateTarget.dataset.id !== "undefined") {
parentSciMeta = this.getParentScienceMetadata(event);
if (
parentSciMeta.get &&
parentSciMeta.get("resourceMap").length > 0
) {
parentResourceMaps = parentSciMeta.get("resourceMap");
if (!MetacatUI.rootDataPackage.packageModel.get("latestVersion")) {
// Decide how to handle this by calling model.findLatestVersion()
// and listen for the result, setting getParentDataPackage() as the callback?
} else {
parentResourceMapId =
MetacatUI.rootDataPackage.packageModel.get("latestVersion");
}
} else {
console.log(
"There is no resource map associated with the science metadata.",
);
}
// Is this the root package or a nested package?
if (
MetacatUI.rootDataPackage.packageModel.id === parentResourceMapId
) {
return MetacatUI.rootDataPackage;
// A nested package
} else {
return MetacatUI.rootDataPackage.where({
id: parentResourceMapId,
})[0];
}
}
},
/**
* Removes invalid characters and formatting from the given input string
* @param {string} input The string to clean
* @return {string}
*/
cleanInput: function (input) {
// 1. remove line breaks / Mso classes
var stringStripper = /(\n|\r| class=(")?Mso[a-zA-Z]+(")?)/g;
var output = input.replace(stringStripper, " ");
// 2. strip Word generated HTML comments
var commentSripper = new RegExp("<!--(.*?)-->", "g");
output = output.replace(commentSripper, "");
var tagStripper = new RegExp(
"<(/)*(meta|link|span|\\?xml:|st1:|o:|font)(.*?)>",
"gi",
);
// 3. remove tags leave content if any
output = output.replace(tagStripper, "");
// 4. Remove everything in between and including tags '<style(.)style(.)>'
var badTags = [
"style",
"script",
"applet",
"embed",
"noframes",
"noscript",
];
for (var i = 0; i < badTags.length; i++) {
tagStripper = new RegExp(
"<" + badTags[i] + ".*?" + badTags[i] + "(.*?)>",
"gi",
);
output = output.replace(tagStripper, "");
}
// 5. remove attributes ' style="..."'
var badAttributes = ["style", "start"];
for (var i = 0; i < badAttributes.length; i++) {
var attributeStripper = new RegExp(
" " + badAttributes[i] + '="(.*?)"',
"gi",
);
output = output.replace(attributeStripper, "");
}
output = EML.prototype.cleanXMLText(output);
return output;
},
/**
* Style this table row to indicate it will be removed
*/
previewRemove: function () {
this.$el.toggleClass("remove-preview");
},
/**
* Clears the text in the cell if the text was the default. We add
* an 'empty' class, and remove it when the user focuses back out.
* @param {Event} e
*/
emptyName: function (e) {
var editableCell = this.$(".canRename [contenteditable]");
editableCell.tooltip("hide");
if (editableCell.text().indexOf("Untitled") > -1) {
editableCell
.attr("data-original-text", editableCell.text().trim())
.text("")
.addClass("empty")
.on("focusout", function () {
if (!editableCell.text())
editableCell
.text(editableCell.attr("data-original-text"))
.removeClass("empty");
});
}
},
/**
* Changes the access policy of a data object based on user input.
*
* @param {Event} e - The event that triggered this function as a callback
*/
changeAccessPolicy: function (e) {
if (typeof e === "undefined" || !e) return;
var accessPolicy = this.model.get("accessPolicy");
var makePublic = $(e.target).prop("checked");
//If the user has chosen to make this object private
if (!makePublic) {
if (accessPolicy) {
//Make the access policy private
accessPolicy.makePrivate();
} else {
//Create an access policy from the default settings
this.model.createAccessPolicy();
//Make the access policy private
this.model.get("accessPolicy").makePrivate();
}
} else {
if (accessPolicy) {
//Make the access policy public
accessPolicy.makePublic();
} else {
//Create an access policy from the default settings
this.model.createAccessPolicy();
//Make the access policy public
this.model.get("accessPolicy").makePublic();
}
}
},
/**
* Shows form validation for this data item
* @param {string} attr The modal attribute that has been validated
* @param {string} errorMsg The validation error message to display
*/
showValidation: function (attr, errorMsg) {
//Find the element that is required
var requiredEl = this.$("[data-category='" + attr + "']").addClass(
"error",
);
//When it is updated, remove the error styling
this.listenToOnce(this.model, "change:" + attr, this.hideRequired);
},
/**
* Hides the 'required' styling from this view
*/
hideRequired: function () {
//Remove the error styling
this.$("[contenteditable].error").removeClass("error");
},
/**
* Show the data item as saving
*/
showSaving: function () {
this.$(".controls button").prop("disabled", true);
if (this.model.get("type") != "Metadata")
this.$(".controls").prepend(
$(document.createElement("div")).addClass("disable-layer"),
);
this.$(".canRename > div").prop("contenteditable", false);
},
/**
* Hides the styles applied in {@link DataItemView#showSaving}
*/
hideSaving: function () {
this.$(".controls button").prop("disabled", false);
this.$(".disable-layer").remove();
//Make the name cell editable again
this.$(".canRename > div").prop("contenteditable", true);
this.$el.removeClass("error-saving");
},
toggleSaving: function () {
if (
this.model.get("uploadStatus") == "p" ||
this.model.get("uploadStatus") == "l" ||
(this.model.get("uploadStatus") == "e" &&
this.model.get("type") != "Metadata") ||
MetacatUI.rootDataPackage.packageModel.get("uploadStatus") == "p"
)
this.showSaving();
else this.hideSaving();
if (this.model.get("uploadStatus") == "e")
this.$el.addClass("error-saving");
},
/**
* Shows the current progress of the file upload
*/
showUploadProgress: function () {
if (this.model.get("numSaveAttempts") > 0) {
this.$(".progress .bar").css("width", "100%");
} else {
this.$(".progress .bar").css(
"width",
this.model.get("uploadProgress") + "%",
);
}
},
/**
* Determine whether this item can be shared
*
* Used to control whether the Share button in the template
* is enabled or not.
*
* Has special behavior depending on whether the item is metadata or
* not. If metadata (ie type is EML), also checks that the resource
* map can be shared. Otherwise, only checks if the data item can be
* shared.
*
* @return {boolean} Whether the item can be shared
* @since 2.15.0
*/
canShareItem: function () {
if (this.parentEditorView) {
if (this.parentEditorView.isAccessPolicyEditEnabled()) {
if (this.model.type === "EML") {
// Check whether we can share the resource map
var pkgModel = MetacatUI.rootDataPackage.packageModel,
pkgAccessPolicy = pkgModel.get("accessPolicy");
var canShareResourceMap =
pkgModel.isNew() ||
(pkgAccessPolicy &&
pkgAccessPolicy.isAuthorized("changePermission"));
// Check whether we can share the metadata
var modelAccessPolicy = this.model.get("accessPolicy");
var canShareMetadata =
this.model.isNew() ||
(modelAccessPolicy &&
modelAccessPolicy.isAuthorized("changePermission"));
// Only return true if we can share both
return canShareMetadata && canShareResourceMap;
} else {
return (
this.model.get("accessPolicy") &&
this.model.get("accessPolicy").isAuthorized("changePermission")
);
}
}
}
},
downloadFile: function (e) {
this.downloadButtonView.download(e);
},
// Member row metrics for the package table
// Retrieving information from the Metrics Model's result details
getMemberRowMetrics: function (id) {
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 (this.isMetadata) {
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 (this.isMetadata && reads == 1) reads += " view";
else if (this.isMetadata) reads += " views";
else if (reads == 1) reads += " download";
else reads += " downloads";
} else {
// returning an empty string if the metrics are 0
reads = "";
}
return reads;
},
},
);
return DataItemView;
});