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()) * 0.4;
$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;
});