/* global define */
define([
'jquery',
'underscore',
'backbone',
'localforage',
'collections/DataPackage',
'models/DataONEObject',
'models/PackageModel',
'models/metadata/ScienceMetadata',
'models/metadata/eml211/EML211',
'models/PackageModel',
'views/DataItemView',
'views/DownloadButtonView',
'text!templates/dataPackage.html',
'text!templates/dataPackageStart.html',
'text!templates/dataPackageHeader.html'],
function($, _, Backbone, LocalForage, DataPackage, 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()) * .40;
$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});
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 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);
/**
* The HTML content for the data package header.
*
* @type {string}
*/
tableRow = this.dataPackageHeaderTemplate({ id: dataPackage.id, title: title, titleTooltip: titleTooltip, disablePackageDownloads: false, downloadUrl: packageUrl });
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').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: m.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;
});