Source: src/js/views/DataItemView.js

/* global define */
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;
    });