/* global define */ define(['underscore', 'jquery', 'backbone', 'collections/DataPackage', 'models/metadata/eml211/EML211', 'models/metadata/eml211/EMLOtherEntity', 'models/metadata/ScienceMetadata', 'views/CitationView', 'views/DataPackageView', 'views/metadata/EML211View', 'views/metadata/EMLEntityView', 'views/SignInView', 'text!templates/editor.html', 'collections/ObjectFormats', 'text!templates/editorSubmitMessage.html'], function(_, $, Backbone, DataPackage, EML, EMLOtherEntity, ScienceMetadata, CitationView, DataPackageView, EMLView, EMLEntityView, SignInView, EditorTemplate, ObjectFormats, EditorSubmitMessageTemplate){ var EditorView = Backbone.View.extend({ type: "Editor", el: "#Content", /* The initial editor layout */ template: _.template(EditorTemplate), editorSubmitMessageTemplate: _.template(EditorSubmitMessageTemplate), /* Events that apply to the entire editor */ events: { "click #save-editor" : "save", "click .data-package-item .edit" : "showEntity" }, /* The identifier of the root package EML being rendered */ pid: null, /* A list of the subviews of the editor */ subviews: [], /* The data package view */ dataPackageView: null, /* Initialize a new EditorView - 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() { MetacatUI.appModel.set('headerType', 'default'); //Style the body as an Editor $("body").addClass("Editor rendering"); this.$el.empty(); //Inert the basic template on the page this.$el.html(this.template({ loading: MetacatUI.appView.loadingTemplate({ msg: "Loading editor..."}) })); //If we don't have a model at this point, create one if(!this.model) this.createModel(); //When the basic Solr metadata are retrieved, get the associated package this.listenToOnce(this.model, "sync", this.getDataPackage); //If no object is found with this ID, then tell the user this.listenToOnce(this.model, "change:notFound", this.showNotFound); //If we checked for authentication already if(MetacatUI.appUserModel.get("checked")){ this.fetchModel(); } //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", this.fetchModel); } window.onbeforeunload = this.confirmClose; // 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; }, fetchModel: function(){ //If we checked for authentication and the user is not logged in if(!MetacatUI.appUserModel.get("loggedIn")){ this.showSignIn(); } else{ //If the user hasn't provided an id, then don't check the authority and mark as synced already if(!this.pid){ this.model.set("isAuthorized", true); this.model.trigger("sync"); } else { //Get the data package when we find out the user is authorized to edit it this.listenToOnce(this.model, "change:isAuthorized", this.getDataPackage); //Let a user know when they are not authorized to edit this data set this.listenToOnce(this.model, "change:isAuthorized", this.notAuthorized); //Fetch the model this.model.fetch(); //Check the authority of this user this.model.checkAuthority(); } } }, /* Get the data package associated with the EML */ getDataPackage: function(scimetaModel) { if(!this.model.get("synced") || !this.model.get("isAuthorized")) return; 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"); if ( typeof resourceMapIds === "undefined" || resourceMapIds === null || resourceMapIds.length <= 0 ) { console.log("Resource map ids could not be found for " + scimetaModel.id); //Check if the rootDataPackage contains the metadata document the user is trying to edit if( MetacatUI.rootDataPackage && MetacatUI.rootDataPackage.pluck && _.contains(MetacatUI.rootDataPackage.pluck("id"), this.model.get("id")) ){ //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 MetacatUI.rootDataPackage = new DataPackage([this.model], { id: model.get("latestVersion") }); //Handle the add of the metadata model MetacatUI.rootDataPackage.saveReference(this.model); //Fetch the data package MetacatUI.rootDataPackage.fetch(); //Render the Data Package table this.renderDataPackage(); }); //Remove the cached system metadata XML so we retrieve it again MetacatUI.rootDataPackage.packageModel.set("sysMetaXML", null); //Find the latest version of the resource map MetacatUI.rootDataPackage.packageModel.findLatestVersion(); } else{ //Create a new DataPackage collection for this view this.createDataPackage(); // Set the listeners this.setListeners(); //Render the data package this.renderDataPackage(); //Render the metadata this.renderMetadata(); } } else { // Create a new data package with this id MetacatUI.rootDataPackage = new DataPackage([this.model], {id: resourceMapIds[0]}); //Handle the add of the metadata model MetacatUI.rootDataPackage.saveReference(this.model); // If there is more than one resource map, we need to make sure we fetch the most recent one if ( resourceMapIds.length > 1 ) { //Now, find the latest version this.listenToOnce(MetacatUI.rootDataPackage.packageModel, "change:latestVersion", function(model) { //Create a new data package for the latest version package MetacatUI.rootDataPackage = new DataPackage([this.model], { id: model.get("latestVersion") }); //Handle the add of the metadata model MetacatUI.rootDataPackage.saveReference(this.model); //Fetch the data package MetacatUI.rootDataPackage.fetch(); //Render the Data Package table this.renderDataPackage(); }); MetacatUI.rootDataPackage.packageModel.findLatestVersion(); return; } //Fetch the data package MetacatUI.rootDataPackage.fetch(); //Render the Data Package table this.renderDataPackage(); } }, /* * Creates a DataPackage collection for this EditorView and sets it on the MetacatUI * global object (as `rootDataPackage`) */ createDataPackage: function(){ // Create a new Data packages MetacatUI.rootDataPackage = new DataPackage([this.model]); MetacatUI.rootDataPackage.packageModel.set("synced", true); //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()); }, renderChildren: function(model, options) { }, renderDataPackage: function(){ var view = this; // 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.updateUploadStatus); }); //Render the Data Package view this.dataPackageView = new DataPackageView({ edit: true, dataPackage: MetacatUI.rootDataPackage }); //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 EditorView */ 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" ) { console.log("Not EML. TODO: Render generic ScienceMetadata."); return; } else { //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(); // this.renderDataPackageItem(model, collection, options); // this.off("change", this.renderMember, model); // avoid double renderings // Create a citation view and render it var citationView = new CitationView({ model: model, title: "Untitled dataset" }); if( model.isNew() ){ citationView.createLink = false; citationView.createTitleLink = false; } else{ citationView.createLink = false; citationView.createTitleLink = true; } 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 EditorView */ 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 }); 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); // 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 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); }, /* * Saves all edits in the collection */ 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 */ 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/" + this.model.get("id"), { trigger: false, replace: true }); this.toggleControls(); // Construct the save message var message = this.editorSubmitMessageTemplate({ viewURL: MetacatUI.root + "/view/" + this.model.get("id") }); 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 */ 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(); }, /* * Called when there is no object found with this ID */ showNotFound: function(){ //If we haven't checked the logged-in status of the user yet, wait a bit until we show a 404 msg, in case this content is their private content if(!MetacatUI.appUserModel.get("checked")){ this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.showNotFound); return; } //If the user is not logged in else if(!MetacatUI.appUserModel.get("loggedIn")){ this.showSignIn(); return; } if(!this.model.get("notFound")) return; var msg = "