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