Source: src/js/views/metadata/EML211EditorView.js

/* global define */
define(['underscore',
  'jquery',
  'backbone',
  'localforage',
  'collections/DataPackage',
  'models/metadata/eml211/EML211',
  'models/metadata/eml211/EMLOtherEntity',
  'models/metadata/ScienceMetadata',
  'views/EditorView',
  'views/CitationView',
  'views/DataPackageView',
  'views/metadata/EML211View',
  'views/metadata/EMLEntityView',
  'views/SignInView',
  'text!templates/editor.html',
  'collections/ObjectFormats',
  'text!templates/editorSubmitMessage.html'],
  function (_, $, Backbone, LocalForage,
    DataPackage, EML, EMLOtherEntity, ScienceMetadata,
    EditorView, CitationView, DataPackageView, EMLView, EMLEntityView, SignInView,
    EditorTemplate, ObjectFormats, EditorSubmitMessageTemplate) {

    /**
    * @class EML211EditorView
    * @classdesc A view of a form for creating and editing EML 2.1.1 documents
    * @classcategory Views/Metadata
    * @name EML211EditorView
    * @extends EditorView
    * @constructs
    */
    var EML211EditorView = EditorView.extend(
      /** @lends EML211EditorView.prototype */{

        type: "EML211Editor",

        /* The initial editor layout */
        template: _.template(EditorTemplate),
        editorSubmitMessageTemplate: _.template(EditorSubmitMessageTemplate),

        /**
        * The text to use in the editor submit button
        * @type {string}
        */
        submitButtonText: MetacatUI.appModel.get("editorSaveButtonText"),

        /**
        * The events this view will listen to and the associated function to call.
        * This view will inherit events from the parent class, EditorView.
        * @type {Object}
        */
        events: _.extend(EditorView.prototype.events, {
          "change": "saveDraft",
          "click .data-package-item .edit": "showEntity"
        }),

        /**
        The identifier of the root package EML being rendered
        * @type {string}
        */
        pid: null,

        /**
        * A list of the subviews of the editor
        * @type {Backbone.Views[]}
        */
        subviews: [],

        /**
        * The data package view
        * @type {DataPackageView}
        */
        dataPackageView: null,

        /**
        * Initialize a new EML211EditorView - called post constructor
        */
        initialize: function (options) {

          // Ensure the object formats are cached for the editor's use
          if (typeof MetacatUI.objectFormats === "undefined") {
            MetacatUI.objectFormats = new ObjectFormats();
            MetacatUI.objectFormats.fetch();

          }
          return this;
        },

        /**
        * Create a new EML model for this view
        */
        createModel: function () {

          //If no pid is given, create a new EML model
          if (!this.pid)
            var model = new EML({ 'synced': true });
          //Otherwise create a generic metadata model until we find out the formatId
          else
            var model = new ScienceMetadata({ id: this.pid });

          // Once the ScienceMetadata is populated, populate the associated package
          this.model = model;

          //Listen for the replace event on this model
          var view = this;
          this.listenTo(this.model, "replace", function (newModel) {
            if (view.model.get("id") == newModel.get("id")) {
              view.model = newModel;
              view.setListeners();
            }
          });

          this.setListeners();
        },

        /**
        * Render the view
        */
        render: function () {

          var view = this;

          //Execute the superclass render() function, which will add some basic Editor functionality
          EditorView.prototype.render.call(this);

          MetacatUI.appModel.set('headerType', 'default');

          //Empty the view element first
          this.$el.empty();

          //Inert the basic template on the page
          this.$el.html(this.template({
            loading: MetacatUI.appView.loadingTemplate({ msg: "Starting the editor..." }),
            submitButtonText: this.submitButtonText
          }));

          //If we don't have a model at this point, create one
          if (!this.model) this.createModel();

          // Before rendering the editor, we must:
          // 1. Make sure the user is signed in
          // 2. Fetch the metadata
          // 3. Use the metadata to identify and then fetch the resource map
          // 4. Make sure the user has write permission on the metadata
          // 5. Make sure the user has write permission on the resource map

          // As soon as we have all of the metadata information (STEP 2 complete)...
          this.listenToOnce(this.model, "sync", function () {

            // Skip the remaining steps the metadata doesn't exist.
            if (this.model.get("notFound") == true) {
              this.showNotFound();
              return
            }

            // STEP 3
            // Listen for a trigger from the getDataPackage function that indicates
            // The data package (resource map) has been retrieved.
            this.listenToOnce(this, "dataPackageFound", function () {

              var resourceMap = MetacatUI.rootDataPackage.packageModel;

              // STEP 5
              // Once we have the resource map, then check that the user is authorized to edit this package.
              this.listenToOnce(resourceMap, "change:isAuthorized_write", function (model, authorization) {
                // Render if authorized (will show not authorized if not)
                this.renderEditorComponents();
              });
              // No need to check authorization for a new resource map
              if (resourceMap.isNew()) {
                resourceMap.set("isAuthorized_write", true);
              } else {
                resourceMap.checkAuthority("write");
                this.updateLoadingText("Loading metadata...");
              }

            });

            this.getDataPackage();

            // STEP 4
            // Check the authority of this user to edit the metadata
            this.listenToOnce(this.model, "change:isAuthorized_write", function (model, authorization) {
              // Render if authorized (will show not authorized if not)
              this.renderEditorComponents();
            });
            // If the model is new, no need to check for authorization.
            if (this.model.isNew()) {
              this.model.set("isAuthorized_write", true);
            } else {
              this.model.checkAuthority("write");
              this.updateLoadingText("Checking authorization...");
            }
          });

          // STEP 1
          // Check that the user is signed in
          var afterAccountChecked = function () {
            if (MetacatUI.appUserModel.get("loggedIn") == false) {
              // If they are not signed in, then show the sign-in view
              view.showSignIn();
            } else {
              // STEP 2
              // If signed in, then fetch model
              view.fetchModel();
            }
          }
          // If we've already checked the user account
          if (MetacatUI.appUserModel.get("checked")) {
            afterAccountChecked();
          }
          // If we haven't checked for authentication yet,
          // wait until the user info is loaded before we request the Metadata
          else {
            this.listenToOnce(MetacatUI.appUserModel, "change:checked", function () {
              afterAccountChecked();
            });
          }

          // When the user mistakenly drops a file into an area in the window
          // that isn't a proper drop-target, prevent navigating away from the
          // page. Without this, the user will lose their progress in the
          // editor.
          window.addEventListener("dragover", function (e) {
            e = e || event;
            e.preventDefault();
          }, false);

          window.addEventListener("drop", function (e) {
            e = e || event;
            e.preventDefault();
          }, false);

          return this;
        },

        /**
         * Render the editor components (data package view and metadata view),
         * or, if not authorized, render the not authorized message.
         */
        renderEditorComponents: function () {

          if (!MetacatUI.rootDataPackage.packageModel) {
            return
          }
          var resMapPermission = MetacatUI.rootDataPackage.packageModel.get("isAuthorized_write"),
            metadataPermission = this.model.get("isAuthorized_write");

          if (resMapPermission === true && metadataPermission === true) {
            var view = this;
            // Render the Data Package table.
            // This function will also render metadata.
            view.renderDataPackage();
          } else if (resMapPermission === false || metadataPermission === false) {
            this.notAuthorized();
          }

        },

        /**
        * Fetch the metadata model
        */
        fetchModel: function () {

          //If the user hasn't provided an id, then don't check the authority and mark as synced already
          if (!this.pid) {
            this.model.trigger("sync");
          }
          else {
            //Fetch the model
            this.model.fetch();
          }
        },

        /**
        * @inheritdoc
        */
        isAccessPolicyEditEnabled: function(){

          if( !MetacatUI.appModel.get("allowAccessPolicyChanges") ){
            return false;
          }

          if( !MetacatUI.appModel.get("allowAccessPolicyChangesDatasets") ){
            return false;
          }

          let limitedTo = MetacatUI.appModel.get("allowAccessPolicyChangesDatasetsForSubjects");
          if( Array.isArray(limitedTo) && limitedTo.length ){

            return _.intersection(limitedTo, MetacatUI.appUserModel.get("allIdentitiesAndGroups")).length > 0;

          }
          else{
            return true;
          }

        },

        /**
         * Update the text that is shown below the spinner while the editor is loading
         *
         * @param {string} message - The message to display
         */
        updateLoadingText: function (message) {
          try {
            if (!message || typeof message != "string") {
              console.log("Was not able to update the loading message, left it as-is. A message must be provided to the updateLoadingText function");
              return
            }
            var loadingPara = this.$el.find(".loading > p");
            if (loadingPara) {
              loadingPara.text(message)
            }
          } catch (error) {
            console.log("Was not able to update the loading message, left it as-is. Error details: " + error);
          }
        },

        /**
        * Get the data package (resource map) associated with the EML. Save it to MetacatUI.rootDataPackage.
        * The metadata model must already be synced, and the user must be authorized to edit the EML before this function
        * can run.
        * @param {Model} scimetaModel - The science metadata model for which to find the associated data package
        */
        getDataPackage: function (scimetaModel) {

          if (!scimetaModel)
            var scimetaModel = this.model;

          // Check if this package is obsoleted
          if (this.model.get("obsoletedBy")) {
            this.showLatestVersion();
            return;
          }

          var resourceMapIds = scimetaModel.get("resourceMap");

          // Case 1: No resource map PID found in the metadata
          if (typeof resourceMapIds === "undefined" || resourceMapIds === null || resourceMapIds.length <= 0) {

            // 1A: Check if the rootDataPackage contains the metadata document the user is trying to edit.
            // Ensure the resource map is not new. If it's a previously unsaved map, then getLatestVersion
            // will result in a 404.
            if (
              MetacatUI.rootDataPackage &&
              MetacatUI.rootDataPackage.pluck &&
              !MetacatUI.rootDataPackage.packageModel.isNew() &&
              _.contains(MetacatUI.rootDataPackage.pluck("id"), this.model.get("id"))
            ) {

              // Remove the cached system metadata XML so we retrieve it again
              MetacatUI.rootDataPackage.packageModel.set("sysMetaXML", null);
              this.getLatestResourceMap();

            }

            // 1B. If the root data package does not contain the metadata the user is trying to edit,
            // then create a new data package.
            else {

              console.log("Resource map ids could not be found for " + scimetaModel.id + ", creating a new resource map.");

              // Create a new DataPackage collection for this view
              this.createDataPackage();
              this.trigger("dataPackageFound");
              // Set the listeners
              this.setListeners();
            }

            // Case 2: A resource map PID was found in the metadata
          } else {

            // Create a new data package with this id
            this.createRootDataPackage([this.model], { id: resourceMapIds[0] });

            //Handle the add of the metadata model
            MetacatUI.rootDataPackage.saveReference(this.model);

            // 2A. If there is more than one resource map, we need to make sure we fetch the most recent one
            if (resourceMapIds.length > 1) {
              this.getLatestResourceMap();

              // 2B. Just one resource map found
            } else {

              this.listenToOnce(MetacatUI.rootDataPackage, "sync", function () {
                this.trigger("dataPackageFound");
              })
              // Fetch the data package
              MetacatUI.rootDataPackage.fetch();
            }

          }

        },


        /**
         * Get the latest version of the resource map model stored in MetacatUI.rootDataPackage.packageModel.
         * When the newest resource map is synced, the "dataPackageFound" event will be triggered.
         */
        getLatestResourceMap: function () {

          try {

            if (!MetacatUI.rootDataPackage || !MetacatUI.rootDataPackage.packageModel) {
              console.log("Could not get the latest verion of the resource map because no resource map is saved.");
              return
            }
            // Make sure we have the latest version of the resource map before we allow editing
            this.listenToOnce(MetacatUI.rootDataPackage.packageModel, "latestVersionFound", function (model) {
              //Create a new data package for the latest version package
              this.createRootDataPackage([this.model], { id: model.get("latestVersion") });
              //Handle the add of the metadata model
              MetacatUI.rootDataPackage.saveReference(this.model);
              this.listenToOnce(MetacatUI.rootDataPackage, "sync", function () {
                this.trigger("dataPackageFound");
              })
              // Fetch the data package
              MetacatUI.rootDataPackage.fetch();
            });

            //Find the latest version of the resource map
            MetacatUI.rootDataPackage.packageModel.findLatestVersion();
          } catch (error) {
            console.log("Error attempting to find the latest version of the resource map. Error details: " + error);
          }
        },

        /**
        * Creates a DataPackage collection for this EML211EditorView and sets it on the MetacatUI
        * global object (as `rootDataPackage`)
        */
        createDataPackage: function () {
          // Create a new Data packages
          this.createRootDataPackage([this.model], { packageModelAttrs: { synced: true }})

          try{
            //Inherit the access policy of the metadata document, if the metadata document is not `new`
            if(!this.model.isNew()){
              let metadataAccPolicy = this.model.get("accessPolicy");
              let accPolicy = MetacatUI.rootDataPackage.packageModel.get("accessPolicy")

              //If there is no access policy, it hasn't been fetched yet, so wait
              if( !metadataAccPolicy.length ){
                //If the model is of ScienceMetadata class, we need to wait for the "replace" function,
                // which happens when the model is fetched and an EML211 model is created to replace it.
                if( this.model.type == "ScienceMetadata" ){
                   this.listenTo(this.model, "replace", function(){
                     this.listenToOnce(this.model, "sysMetaUpdated", function(){
                       accPolicy.copyAccessPolicy(this.model.get("accessPolicy"))
                       MetacatUI.rootDataPackage.packageModel.set("rightsHolder", this.model.get("rightsHolder"));
                     });
                   });
                }
              }
              else{
                accPolicy.copyAccessPolicy(this.model.get("accessPolicy"))
              }
            }
          }
          catch(e){
            console.error("Could not copy the access policy from the metadata to the resource map: ", e);
          }

          //Handle the add of the metadata model
          MetacatUI.rootDataPackage.handleAdd(this.model);

          // Associate the science metadata with the resource map
          if (this.model.get && Array.isArray(this.model.get("resourceMap"))) {
            this.model.get("resourceMap").push(MetacatUI.rootDataPackage.packageModel.id);

          } else {
            this.model.set("resourceMap", MetacatUI.rootDataPackage.packageModel.id);

          }

          // Set the sysMetaXML for the packageModel
          MetacatUI.rootDataPackage.packageModel.set("sysMetaXML",
            MetacatUI.rootDataPackage.packageModel.serializeSysMeta());
        },

        /**
        * Creates a {@link DataPackage} collection for this Editor view, and saves it as the Root Data Package of the app.
        * This centralizes the DataPackage creation so listeners and other functionality is always performed
        * @param {(DataONEObject[]|ScienceMetadata[]|EML211[])} models - An array of models to add to the collection
        * @param {object} [attributes] A literal object of attributes to pass to the DataPackage.initialize() function
        * @since 2.17.1
        */
        createRootDataPackage: function(models, attributes){
          MetacatUI.rootDataPackage = new DataPackage(models, attributes);

          this.listenTo(MetacatUI.rootDataPackage.packageModel, "change:numLoadingFiles", this.toggleEnableControls);
        },

        renderChildren: function (model, options) {


        },

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


        /**
         * Calls the appropriate render method depending on the model type
         */
        renderMember: function (model, collection, options) {

          // Render metadata or package information, based on the type
          if (typeof model.attributes === "undefined") {
            return;

          } else {
            switch (model.get("type")) {
              case "DataPackage":
                // Do recursive rendering here for sub packages
                break;

              case "Metadata":

                // this.renderDataPackageItem(model, collection, options);
                this.renderMetadata(model, collection, options);
                break;

              case "Data":
                //this.renderDataPackageItem(model, collection, options);
                break;

              default:
                console.log("model.type is not set correctly");

            }
          }
        },


        /**
         * Renders the metadata section of the EML211EditorView
         */
        renderMetadata: function (model, collection, options) {

          if (!model && this.model) var model = this.model;
          if (!model) return;

          var emlView, dataPackageView;

          // render metadata as the collection is updated, but only EML passed from the event
          if (typeof model.get === "undefined" ||
            !(
              model.get("formatId") === "eml://ecoinformatics.org/eml-2.1.1" ||
              model.get("formatId") === "https://eml.ecoinformatics.org/eml-2.2.0"
            )) {
            console.log("Not EML. TODO: Render generic ScienceMetadata.");
            return;

          }

          //Create an EML model
          if (model.type != "EML") {
              //Create a new EML model from the ScienceMetadata model
              var EMLmodel = new EML(model.toJSON());
              //Replace the old ScienceMetadata model in the collection
              MetacatUI.rootDataPackage.remove(model);
              MetacatUI.rootDataPackage.add(EMLmodel, { silent: true });
              MetacatUI.rootDataPackage.handleAdd(EMLmodel);
              model.trigger("replace", EMLmodel);

              //Fetch the EML and render it
              this.listenToOnce(EMLmodel, "sync", this.renderMetadata);
              EMLmodel.fetch();

              return;
            }

          //Create an EML211 View and render it
          emlView = new EMLView({
            model: model,
            edit: true
          });
          this.subviews.push(emlView);
          this.emlView = emlView;
          emlView.render();

          //Show the required fields for this editor
          this.renderRequiredIcons(this.getRequiredFields());
          this.listenTo(emlView, "editorInputsAdded", function(){
            this.trigger("editorInputsAdded")
          });

          // Create a citation view and render it
          var citationView = new CitationView({
            model: model,
            defaultTitle: "Untitled dataset",
            createLink: false,
            createTitleLink: !model.isNew()
          });

          this.subviews.push(citationView);
          $("#citation-container").html(citationView.render().$el);

          //Remove the rendering class from the body element
          $("body").removeClass("rendering");

          // Focus the folder name field once loaded but only if this is a new
          // document
          if (!this.pid) {
            $("#data-package-table-body td.name").focus();
          }

        },


        /**
         * Renders the data package section of the EML211EditorView
         */
        renderDataPackageItem: function (model, collection, options) {

          var hasPackageSubView =
            _.find(this.subviews, function (subview) {
              return subview.id === "data-package-table";
            }, model);

          // Only create the package table if it hasn't been created
          if (!hasPackageSubView) {
            this.dataPackageView = new DataPackageView({
              dataPackage: MetacatUI.rootDataPackage,
              edit: true,
              parentEditorView: this
            });
            this.subviews.push(this.dataPackageView);
            dataPackageView.render();

          }
        },

        /**
         * Set listeners on the view's model for various reasons.
         * This function centralizes all the listeners so that when/if the view's model is replaced, the listeners would be reset.
         */
        setListeners: function () {

          this.listenTo(this.model, "change:uploadStatus", this.showControls);

          // Register a listener for any attribute change
          this.model.on("change", this.model.handleChange, this.model);

          // Register a listener to save drafts on change
          this.model.on("change", this.model.saveDraft, this.model);

          // If any attributes have changed (including nested objects), show the controls
          if (typeof MetacatUI.rootDataPackage.packageModel !== "undefined") {
            this.stopListening(MetacatUI.rootDataPackage.packageModel, "change:changed");
            this.listenTo(MetacatUI.rootDataPackage.packageModel, "change:changed", this.toggleControls);
            this.listenTo(MetacatUI.rootDataPackage.packageModel, "change:changed", function (event) {
              if (MetacatUI.rootDataPackage.packageModel.get("changed")) {
                // Put this metadata model in the queue when the package has been changed
                // Don't put it in the queue if it's in the process of saving already
                if (this.model.get("uploadStatus") != "p")
                  this.model.set("uploadStatus", "q");
              }
            });

          }

          if (MetacatUI.rootDataPackage && DataPackage.prototype.isPrototypeOf(MetacatUI.rootDataPackage)) {
            // If the Data Package failed saving, display an error message
            this.listenTo(MetacatUI.rootDataPackage, "errorSaving", this.saveError);

            // Listen for when the package has been successfully saved
            this.listenTo(MetacatUI.rootDataPackage, "successSaving", this.saveSuccess);

            //When the Data Package cancels saving, hide the saving styling
            this.listenTo(MetacatUI.rootDataPackage, "cancelSave", this.hideSaving);
            this.listenTo(MetacatUI.rootDataPackage, "cancelSave", this.handleSaveCancel);
          }

          //When the model is invalid, show the required fields
          this.listenTo(this.model, "invalid", this.showValidation);
          this.listenTo(this.model, "valid", this.showValidation);

          // When a data package member fails to load, remove it and warn the user
          this.listenTo(MetacatUI.eventDispatcher, "fileLoadError", this.handleFileLoadError);

          // When a data package member fails to be read, remove it and warn the user
          this.listenTo(MetacatUI.eventDispatcher, "fileReadError", this.handleFileReadError);

          //Set a beforeunload event only if there isn't one already
          if (!this.beforeunloadCallback) {
            var view = this;
            //When the Window is about to be closed, show a confirmation message
            this.beforeunloadCallback = function (e) {
              if (!view.canClose()) {
                //Browsers don't support custom confirmation messages anymore,
                // so preventDefault() needs to be called or the return value has to be set
                e.preventDefault();
                e.returnValue = "";
              }
              return;
            }
            window.addEventListener("beforeunload", this.beforeunloadCallback);
          }

        },

        /**
         * Saves all edits in the collection
         * @param {Event} e - The DOM Event that triggerd this function
         */
        save: function (e) {
          var btn = (e && e.target) ? $(e.target) : this.$("#save-editor");

          //If the save button is disabled, then we don't want to save right now
          if (btn.is(".btn-disabled")) return;

          this.showSaving();

          //Save the package!
          MetacatUI.rootDataPackage.save();
        },

        /**
         * When the data package collection saves successfully, tell the user
         * @param {DataPackage|DataONEObject} savedObject - The model or collection that was just saved
         */
        saveSuccess: function (savedObject) {

          //We only want to perform these actions after the package saves
          if (savedObject.type != "DataPackage") return;

          //Change the URL to the new id
          MetacatUI.uiRouter.navigate("submit/" + encodeURIComponent(this.model.get("id")), { trigger: false, replace: true });

          this.toggleControls();

          // Construct the save message
          var message = this.editorSubmitMessageTemplate({
            messageText: "Your changes have been submitted.",
            viewURL: MetacatUI.root + "/view/" + encodeURIComponent(this.model.get("id")),
            buttonText: "View your dataset"
          });

          MetacatUI.appView.showAlert(message, "alert-success", this.$el, null, { remove: true });

          //Rerender the CitationView
          var citationView = _.where(this.subviews, { type: "Citation" });
          if (citationView.length) {
            citationView[0].createTitleLink = true;
            citationView[0].render();
          }

          // Reset the state to clean
          MetacatUI.rootDataPackage.packageModel.set("changed", false);
          this.model.set("hasContentChanges", false);

          this.setListeners();
        },

        /**
         * When the data package collection fails to save, tell the user
         * @param {string} errorMsg - The error message from the failed save() function
         */
        saveError: function (errorMsg) {

          var errorId = "error" + Math.round(Math.random() * 100),
            messageContainer = $(document.createElement("div")).append(document.createElement("p")),
            messageParagraph = messageContainer.find("p"),
            messageClasses = "alert-error";

          //Get all the models that have an error
          var failedModels = MetacatUI.rootDataPackage.where({ uploadStatus: "e" });

          //If every failed model is a DataONEObject data file that failed
          // because of a slow network, construct a specific error message that
          // is more informative than the usual message
          if (failedModels.length &&
            _.every(failedModels, function (m) {
              return m.get("type") == "Data" &&
                m.get("errorMessage").indexOf("network issue") > -1
            })) {

            //Create a list of file names for the files that failed to upload
            var failedFileList = $(document.createElement("ul"));

            _.each(failedModels, function (failedModel) {

              failedFileList.append($(document.createElement("li")).text(failedModel.get("fileName")));

            }, this);

            //Make the error message
            messageParagraph.text("The following files could not be uploaded due to a network issue. Make sure you are connected to a reliable internet connection. ");
            messageParagraph.after(failedFileList);
          }
          //If one of the failed models is this package's metadata model or the
          // resource map model and it failed to upload due to a network issue,
          // show a more specific error message
          else if (_.find(failedModels, function (m) {
            var errorMsg = m.get("errorMessage") || "";
            return (m == this.model && errorMsg.indexOf("network issue") > -1)
          }, this) ||
            (MetacatUI.rootDataPackage.packageModel.get("uploadStatus") == "e" &&
              MetacatUI.rootDataPackage.packageModel.get("errorMessage").indexOf("network issue") > -1)) {

            messageParagraph.text("Your changes could not be submitted due to a network issue. Make sure you are connected to a reliable internet connection. ");

          }
          else {

            if (this.model.get("draftSaved") && MetacatUI.appModel.get("editorSaveErrorMsgWithDraft")) {
              messageParagraph.text(MetacatUI.appModel.get("editorSaveErrorMsgWithDraft"));
              messageClasses = "alert-warning"
            }
            else if (MetacatUI.appModel.get("editorSaveErrorMsg")) {
              messageParagraph.text(MetacatUI.appModel.get("editorSaveErrorMsg"));
              messageClasses = "alert-error";
            }
            else {
              messageParagraph.text("Not all of your changes could be submitted.");
              messageClasses = "alert-error";
            }

            messageParagraph.after($(document.createElement("p")).append($(document.createElement("a"))
              .text("See technical details")
              .attr("data-toggle", "collapse")
              .attr("data-target", "#" + errorId)
              .addClass("pointer")),
              $(document.createElement("div"))
                .addClass("collapse")
                .attr("id", errorId)
                .append($(document.createElement("pre")).text(errorMsg)));
          }

          MetacatUI.appView.showAlert(messageContainer, messageClasses, this.$el, null, {
            emailBody: "Error message: Data Package save error: " + errorMsg,
            remove: true
          });

          //Reset the Saving styling
          this.hideSaving();
        },


        /**
         * Find the most recently updated version of the metadata
         */
        showLatestVersion: function () {
          var view = this;

          //When the latest version is found,
          this.listenToOnce(this.model, "change:latestVersion", function () {
            //Make sure it has a newer version, and if so,
            if (view.model.get("latestVersion") != view.model.get("id")) {
              //Get the obsoleted id
              var oldID = view.model.get("id");

              //Reset the current model
              view.pid = view.model.get("latestVersion");
              view.model = null;

              //Update the URL
              MetacatUI.uiRouter.navigate("submit/" + encodeURIComponent(view.pid), { trigger: false, replace: true });

              //Render the new model
              view.render();

              //Show a warning that the user was trying to edit old content
              MetacatUI.appView.showAlert("You've been forwarded to the newest version of your dataset for editing.",
                "alert-warning", this.$el, 12000, { remove: true });
            }
            else {
              view.getDataPackage();
            }

          });

          //Find the latest version of this metadata object
          this.model.findLatestVersion();
        },

        /**
         * Show the entity editor
         * @param {Event} e - The DOM Event that triggerd this function
         */
        showEntity: function (e) {
          if (!e || !e.target)
            return;

          //For EML metadata docs
          if (this.model.type == "EML") {
            //Get the Entity View
            var row = $(e.target).parents(".data-package-item"),
              entityView = row.data("entityView"),
              dataONEObject = row.data("model");

            if (dataONEObject.get("uploadStatus") == "p" || dataONEObject.get("uploadStatus") == "l" || dataONEObject.get("uploadStatus") == "e")
              return;

            //If there isn't a view yet, create one
            if (!entityView) {

              //Get the entity model for this data package item
              var entityModel = this.model.getEntity(row.data("model"));

              //Create a new EMLOtherEntity if it doesn't exist
              if (!entityModel) {
                entityModel = new EMLOtherEntity({
                  entityName: dataONEObject.get("fileName"),
                  entityType: dataONEObject.get("formatId") || dataONEObject.get("mediaType"),
                  parentModel: this.model,
                  xmlID: dataONEObject.getXMLSafeID()
                });

                if (!dataONEObject.get("fileName")) {
                  //Listen to changes to required fields on the otherEntity models
                  this.listenTo(entityModel, "change:entityName", function () {
                    if (!entityModel.isValid()) return;

                    //Get the position this entity will be in
                    var position = $(".data-package-item.data").index(row);

                    this.model.addEntity(entityModel, position);
                  });
                }
                else {
                  //Get the position this entity will be in
                  var position = $(".data-package-item.data").index(row);

                  this.model.addEntity(entityModel, position);
                }
              }
              else {
                entityView = new EMLEntityView({
                  model: entityModel,
                  DataONEObject: dataONEObject,
                  edit: true
                });
              }

              //Attach the view to the edit button so we can access it again
              row.data("entityView", entityView);

              //Render the view
              entityView.render();
            }

            //Show the modal window editor for this entity
            if (entityView)
              entityView.show();
          }

        },

        /**
         * Shows a message if the user is not authorized to edit this package
         */
        notAuthorized: function () {

          // Don't show the not authorized message if the user is authorized to edit the EML and the resource map
          if (MetacatUI.rootDataPackage && MetacatUI.rootDataPackage.packageModel) {
            if (
              MetacatUI.rootDataPackage.packageModel.get("isAuthorized_changePermission") &&
              this.model.get("isAuthorized")
            ) {
              return
            }
          } else {
            if (this.model.get("isAuthorized")) {
              return
            }
          }

          this.$("#editor-body").empty();
          MetacatUI.appView.showAlert("You are not authorized to edit this data set.",
            "alert-error", "#editor-body");

          //Stop listening to any further events
          this.stopListening();
          this.model.off();
        },


        /**
         * Toggle the editor footer controls (Save bar)
         */
        toggleControls: function () {
          if (MetacatUI.rootDataPackage &&
            MetacatUI.rootDataPackage.packageModel &&
            MetacatUI.rootDataPackage.packageModel.get("changed")) {
            this.showControls();

          } else {
            this.hideControls();

          }
        },

        /**
        * Toggles whether the Save controls for the Editor are enabled or disabled based on various attributes of the DataPackage and its models.
        * @since 2.17.1
        */
        toggleEnableControls: function(){

          if( MetacatUI.rootDataPackage.packageModel.get("isLoadingFiles") ){
            let noun = MetacatUI.rootDataPackage.packageModel.get("numLoadingFiles") > 1? " files" : " file";
            this.disableControls("Waiting for " + MetacatUI.rootDataPackage.packageModel.get("numLoadingFiles") + noun + " to upload...");
          }
          else{
            this.enableControls();
          }

        },

        /**
         * Show any errors that occured when trying to save changes
         */
        showValidation: function () {

          //First clear all the error messaging
          this.$(".notification.error").empty();
          this.$(".side-nav-item .icon").hide();
          this.$("#metadata-container .error").removeClass("error");
          $(".alert-container:not(:has(.temporary-message))").remove();


          var errors = this.model.validationError;

          _.each(errors, function (errorMsg, category) {

            var categoryEls = this.$("[data-category='" + category + "']"),
                dataItemRow = categoryEls.parents(".data-package-item");

            //If this field is in a DataItemView, then delegate to that view
            if (dataItemRow.length && dataItemRow.data("view")) {
              dataItemRow.data("view").showValidation(category, errorMsg);
              return;
            }
            else {
              var elsWithViews = _.filter(categoryEls, function (el) {
                return ($(el).data("view") &&
                  $(el).data("view").showValidation &&
                  !$(el).data("view").isNew);
              });

              if (elsWithViews.length) {
                _.each(elsWithViews, function (el) {
                  $(el).data("view").showValidation();
                });
              }
              else if(categoryEls.length) {
                //Show the error message
                categoryEls.filter(".notification").addClass("error").text(errorMsg);

                //Add the error message to inputs
                categoryEls.filter("textarea, input").addClass("error");
              }
            }

            //Get the link in the table of contents navigation
            var navigationLink = this.$(".side-nav-item[data-category='" + category + "']");

            if (!navigationLink.length) {
              var section = categoryEls.parents("[data-section]");
              navigationLink = this.$(".side-nav-item." + $(section).attr("data-section"));
            }

            //Show the error icon in the table of contents
            navigationLink.addClass("error")
              .find(".icon")
              .addClass("error")
              .show();

            this.model.off("change:" + category, this.model.checkValidity);
            this.model.once("change:" + category, this.model.checkValidity);

          }, this);

          if (errors) {

            //Create a list of errors to display in the error message shown to the user
            let errorList = "<ul>" +
                            this.getErrorListItem(errors) +
                            "</ul>";


            MetacatUI.appView.showAlert("Fix the errors flagged below before submitting: " + errorList,
              "alert-error",
              this.$el,
              null,
              {
                remove: true
              });
          }

        },

        /**
        * @inheritdoc
        */
        hasUnsavedChanges: function () {
          //If the form hasn't been edited, we can close this view without confirmation
          if (typeof MetacatUI.rootDataPackage.getQueue != "function" || MetacatUI.rootDataPackage.getQueue().length)
            return true;
          else
            return false;
        },

        /**
        *  @inheritdoc
        */
        onClose: function () {

          //Execute the parent class onClose() function
          //EditorView.prototype.onClose.call(this);

          //Remove the listener on the Window
          if (this.beforeunloadCallback) {
            window.removeEventListener("beforeunload", this.beforeunloadCallback);
            delete this.beforeunloadCallback;
          }

          //Stop listening to the "add" event so that new package members aren't rendered.
          //Check first if the DataPackage has been intialized. An easy check is to see is
          // the 'models' attribute is undefined. If the DataPackage collection has been intialized,
          // then it would be an empty array.
          if (typeof MetacatUI.rootDataPackage.models !== "undefined") {
            this.stopListening(MetacatUI.rootDataPackage, "add");
          }

          //Remove all the other events
          this.off();    // remove callbacks, prevent zombies
          this.model.off();

          $(".Editor").removeClass("Editor");
          this.$el.empty();

          this.model = null;

          // Close each subview
          _.each(this.subviews, function (subview) {
            if (subview.onClose)
              subview.onClose();
          });

          this.subviews = [];

          this.undelegateEvents();

        },

        /**
         *  Handle "fileLoadError" events by alerting the user
         * and removing the row from the data package table.
         * @param  {DataONEObject} item The model item passed by the fileLoadError event
         */
        handleFileLoadError: function (item) {
          var message;
          var fileName;
          /* Remove the data package table row */
          this.dataPackageView.removeOne(item);
          /* Then inform the user */
          if (item && item.get &&
            (item.get("fileName") !== "undefined" || item.get("fileName") !== null)) {
            fileName = item.get("fileName");
            message = "The file " + fileName +
              " is already included in this dataset. The duplicate file has not been added.";
          } else {
            message = "The chosen file is already included in this dataset. " +
              "The duplicate file has not been added.";
          }
          MetacatUI.appView.showAlert(message, "alert-info", this.el, 10000, { remove: true });
        },

        /**
         * Handle "fileReadError" events by alerting the user
         * and removing the row from the data package table.
         * @param  {DataONEObject} item The model item passed by the fileReadError event
         */
        handleFileReadError: function (item) {
          var message;
          var fileName;
          /* Remove the data package table row */
          this.dataPackageView.removeOne(item);
          /* Then inform the user */
          if (item && item.get &&
            (item.get("fileName") !== "undefined" || item.get("fileName") !== null)) {
            fileName = item.get("fileName");
            message = "The file " + fileName +
              " could not be read. You may not have permission to read the file," +
              " or the file was too large for your browser to upload. " +
              "The file has not been added.";
          } else {
            message = "The chosen file " +
              " could not be read. You may not have permission to read the file," +
              " or the file was too large for your browser to upload. " +
              "The file has not been added.";
          }
          MetacatUI.appView.showAlert(message, "alert-info", this.el, 10000, { remove: true });
        },

        /**
         * Save a draft of the parent EML model
         */
        saveDraft: function () {
          var view = this;

          try {
            var title = this.model.get("title") || "No title";
            // Create a clone of the model that we will use for serialization.
            // Don't serialize the model that is currently being edited,
            // since serialize may make changes to the model that should not
            // happen until the user is ready to save
            // (e.g. - create a contact if there is not one)
            var draftModel = this.model.clone();

            LocalForage.setItem(this.model.get("id"), {
              id: this.model.get("id"),
              datetime: (new Date()).toISOString(),
              title: Array.isArray(title) ? title[0] : title,
              draft: draftModel.serialize()
            }).then(function () {
              view.clearOldDrafts();
            });
          } catch (ex) {
            console.log("Error saving draft:", ex);
          }
        },

        /**
         * Clear older drafts by iterating over the sorted list of drafts
         * stored by LocalForage and removing any beyond a hardcoded limit.
         */
        clearOldDrafts: function () {
          var drafts = [];

          try {
            LocalForage.iterate(function (value, key, iterationNumber) {
              // Extract each draft
              drafts.push({
                key: key,
                value: value
              });
            }).then(function () {
              // Sort by datetime
              drafts = _.sortBy(drafts, function (draft) {
                return draft.value.datetime.toString();
              }).reverse();
            }).then(function () {
              _.each(drafts, function (draft, i) {
                var age = (new Date()) - new Date(draft.value.datetime);
                var isOld = (age / 2678400000) > 1; // ~31days

                // Delete this draft is not in the most recent 100 or
                // if older than 31 days
                var shouldDelete = i > 100 || isOld;

                if (!shouldDelete) {
                  return;
                }

                LocalForage.removeItem(draft.key).then(function () {
                  // Item should be removed
                });
              });
            });
          }
          catch (ex) {
            console.log("Failed to clear old drafts: ", ex);
          }
        },

        /**
         * Show the AccessPolicy view in a modal dialog
         *
         * This method calls the superclass method, feeding it the identifier
         * associated with the row in the package table that was clicked. The
         * reason for this is so the AccessPolicyView can be used for single
         * objects (like in the Portal editor) or an entire Collection of
         * objects, like in the EML editor: The superclass impelements the
         * generic behavior and the subclass tweaks it.
         *
         * @param {EventHandler} e: The click event
         */
        showAccessPolicyModal: function(e) {
          var id = null;

          try {
            id = $(e.target).parents("tr").data("id");
          } catch (e) {
            console.log("Error determining the identifier to show an AccessPolicyView for:", e);
          }

          var model = MetacatUI.rootDataPackage.find(function(model) {
            return model.get("id") === id;
          });

          EditorView.prototype.showAccessPolicyModal.call(this, e, model);
        },

        /**
        * Gets the EML required fields, as configured in the {@link AppConfig#emlEditorRequiredFields}, and adds
        * possible other special fields that may be configured elsewhere. (e.g. the {@link AppConfig#customEMLMethods})
        * @extends EditorView.getRequiredFields
        */
        getRequiredFields: function(){
          let requiredFields = _.clone(MetacatUI.appModel.get("emlEditorRequiredFields"));

          //Add required fields for Custom Methods, which are configured in a different property of the AppConfig
          let customMethodOptions =  MetacatUI.appModel.get("customEMLMethods");
          if(customMethodOptions){
            customMethodOptions.forEach(options => {
              if( options.required && !requiredFields[options.id] ){
                requiredFields[options.id] = true;
              }
            })
          }

          return requiredFields;

        }
      });
    return EML211EditorView;
  });