define([
"underscore",
"jquery",
"backbone",
"models/AccessRule",
"collections/AccessPolicy",
"views/AccessRuleView",
"text!templates/accessPolicy.html",
"text!templates/filters/toggleFilter.html",
], function (
_,
$,
Backbone,
AccessRule,
AccessPolicy,
AccessRuleView,
Template,
ToggleTemplate,
) {
/**
* @class AccessPolicyView
* @classdesc A view of an Access Policy of a DataONEObject
* @classcategory Views
* @extends Backbone.View
* @screenshot views/AccessPolicyView.png
* @constructor
*/
var AccessPolicyView = Backbone.View.extend(
/** @lends AccessPolicyView.prototype */
{
/**
* The type of View this is
* @type {string}
*/
type: "AccessPolicy",
/**
* The type of object/resource that this AccessPolicy is for. This is used for display purposes only.
* @example "dataset", "portal", "data file"
* @type {string}
*/
resourceType: "resource",
/**
* The HTML classes to use for this view's element
* @type {string}
*/
className: "access-policy-view",
/**
* The AccessPolicy collection that is displayed in this View
* @type {AccessPolicy}
*/
collection: undefined,
/**
* References to templates for this view. HTML files are converted to Underscore.js templates
* @type {Underscore.Template}
*/
template: _.template(Template),
toggleTemplate: _.template(ToggleTemplate),
/**
* Used to track the collection of models set on the view in order to handle
* undoing all changes made when we either hit Cancel or click otherwise
* hide the modal (such as clicking outside of it).
* @type {AccessRule[]}
* @since 2.15.0
*/
cachedModels: null,
/**
* Whether or not changes to the accessPolicy managed by this view will be
* broadcasted to the accessPolicy of the editor's rootDataPackage's
* packageModle.
*
* This implementation is very likely to change in the future as we iron out
* how to handle bulk accessPolicy (and other) changes.
* @type {boolean}
* @since 2.15.0
*/
broadcast: false,
/**
* A selector for the element in this view that contains the public/private toggle section
* @type {string}
* @since 2.15.0
*/
publicToggleSection: "#public-toggle-section",
/**
* The events this view will listen to and the associated function to call.
* @type {Object}
*/
events: {
"change .public-toggle-container input": "togglePrivacy",
"click .save": "save",
"click .cancel": "reset",
"click .access-rule .remove": "handleRemove",
},
/**
* Creates a new AccessPolicyView
* @param {Object} options - A literal object with options to pass to the view
*/
initialize: function (options) {
this.cachedModels = _.clone(this.collection.models);
},
/**
* Renders this view
*/
render: function () {
try {
//If there is no AccessPolicy collection, then exit now
if (!this.collection) {
return;
}
var dataONEObject = this.collection.dataONEObject;
if (dataONEObject && dataONEObject.type) {
switch (dataONEObject.type) {
case "Portal":
this.resourceType =
MetacatUI.appModel.get("portalTermSingular");
break;
case "DataPackage":
this.resourceType = "dataset";
break;
case "EML" || "ScienceMetadata":
this.resourceType = "metadata record";
break;
case "DataONEObject":
this.resourceType = "data file";
break;
case "Collection":
this.resourceType = "collection";
break;
default:
this.resourceType = "resource";
break;
}
} else {
this.resourceType = "resource";
}
//Insert the template into this view
this.$el.html(
this.template({
resourceType: this.resourceType,
fileName: dataONEObject.get("fileName"),
}),
);
//If the user is not authorized to change the permissions of this object,
// then skip rendering the rest of the AccessPolicy.
if (dataONEObject.get("isAuthorized_changePermission") === false) {
this.showUnauthorized();
return;
}
//Show the rightsHolder as an AccessRuleView
this.showRightsholder();
var modelsToRemove = [];
//Iterate over each AccessRule in the AccessPolicy and render a AccessRuleView
this.collection.each(function (accessRule) {
//Don't display access rules for the public since these are controlled via the public/private toggle
if (accessRule.get("subject") == "public") {
return;
}
//If this AccessRule is a duplicate of the rightsHolder, remove it from the policy and don't display it
if (
accessRule.get("subject") == dataONEObject.get("rightsHolder")
) {
modelsToRemove.push(accessRule);
return;
}
//Create an AccessRuleView
var accessRuleView = new AccessRuleView();
accessRuleView.model = accessRule;
accessRuleView.accessPolicyView = this;
//Add the AccessRuleView to this view
this.$(".access-rules-container").append(accessRuleView.el);
//Render the view
accessRuleView.render();
//Listen to changes on the access rule, to check that there is at least one owner
this.listenTo(
accessRule,
"change:read change:write change:changePermission",
this.checkForOwners,
);
}, this);
//Remove each AccessRule from the AccessPolicy that should be removed.
// We don't remove these during the collection.each() function because it
// messes up the .each() iteration.
this.collection.remove(modelsToRemove);
//Get the subject info for each subject in the AccessPolicy, so we can display names
this.collection.getSubjectInfo();
//Show a blank row at the bottom of the table for adding a new Access Rule.
this.addEmptyRow();
//Render various help text for this view
this.renderHelpText();
//Render the public/private toggle, if it's enabled in the app config
this.renderPublicToggle();
} catch (e) {
MetacatUI.appView.showAlert(
"Something went wrong while trying to display the " +
MetacatUI.appModel.get("accessPolicyName") +
". <p>Technical details: " +
e.message +
"</p>",
"alert-error",
this.$el,
null,
);
console.error(e);
}
},
/**
* Renders a public/private toggle that toggles the public readability of the given resource.
*/
renderPublicToggle: function () {
//Check if the public/private toggle is enabled. Default to enabling it.
var isEnabled = true,
enabledSubjects = [];
//Get the DataONEObject that this AccessPlicy is about
var dataONEObject = this.collection.dataONEObject;
//If there is a DataONEObject model found, and it has a type
if (dataONEObject && dataONEObject.type) {
//Get the Portal configs from the AppConfig
if (dataONEObject.type == "Portal") {
isEnabled = MetacatUI.appModel.get("showPortalPublicToggle");
enabledSubjects = MetacatUI.appModel.get(
"showPortalPublicToggleForSubjects",
);
}
//Get the Dataset configs from the AppConfig
else {
isEnabled = MetacatUI.appModel.get("showDatasetPublicToggle");
enabledSubjects = MetacatUI.appModel.get(
"showDatasetPublicToggleForSubjects",
);
}
}
//Get the public/private help text
let helpText = this.getPublicToggleHelpText();
// Or if the public toggle is limited to a set of users and/or groups, and the current user is
// not in that list, then display a message instead of the toggle
if (
!isEnabled ||
(Array.isArray(enabledSubjects) &&
enabledSubjects.length &&
!_.intersection(
enabledSubjects,
MetacatUI.appUserModel.get("allIdentitiesAndGroups"),
).length)
) {
let isPublicClass = this.collection.isPublic() ? "public" : "private";
this.$(".public-toggle-container").html(
$(document.createElement("p"))
.addClass("public-toggle-disabled-text " + isPublicClass)
.text(helpText),
);
this.$(this.publicToggleSection).find("p.help").remove();
return;
}
//Render the private/public toggle
this.$(".public-toggle-container")
.html(
this.toggleTemplate({
label: "",
id: this.collection.id,
trueLabel: "Public",
falseLabel: "Private",
}),
)
.tooltip({
placement: "top",
trigger: "hover",
title: helpText,
container: this.$(".public-toggle-container"),
delay: {
show: 800,
},
});
//If the dataset is public, check the checkbox
this.$(".public-toggle-container input").prop(
"checked",
this.collection.isPublic(),
);
},
/**
* Constructs and returns a message that explains if this resource is public or private. This message is displayed
* in the tooltip for the public/private toggle or in place of the toggle when the toggle is disabled. Override this
* function to create a custom message.
* @returns {string}
* @since 2.15.0
*/
getPublicToggleHelpText: function () {
if (this.collection.isPublic()) {
return (
"Your " +
this.resourceType +
" is public. Anyone can see this " +
this.resourceType +
" in searches or by a direct link."
);
} else {
return (
"Your " +
this.resourceType +
" is private. Only people you approve can see this " +
this.resourceType +
"."
);
}
},
/**
* Render a row with input elements for adding a new AccessRule
*/
addEmptyRow: function () {
try {
//Create a new AccessRule model and add to the collection
var accessRule = new AccessRule({
read: true,
dataONEObject: this.collection.dataONEObject,
});
//Create a new AccessRuleView
var accessRuleView = new AccessRuleView();
accessRuleView.model = accessRule;
accessRuleView.isNew = true;
this.listenTo(accessRule, "change", this.addAccessRule);
//Add the new row to the table
this.$(".access-rules-container").append(accessRuleView.el);
//Render the AccessRuleView
accessRuleView.render();
} catch (e) {
console.error(
"Something went wrong while adding the empty access policy row ",
e,
);
}
},
/**
* Adds the given AccessRule model to the AccessPolicy collection associated with this view
* @param {AccessRule} accessRule - The AccessRule to add
*/
addAccessRule: function (accessRule) {
//If this AccessPolicy already contains this AccessRule, then exit
if (this.collection.contains(accessRule)) {
return;
}
//If there is no subject set on this AccessRule, exit
if (!accessRule.get("subject")) {
return;
}
//Add the AccessRule to the AccessPolicy
this.collection.push(accessRule);
//Get the name for this new person or group
accessRule.getSubjectInfo();
//Render a new empty row
this.addEmptyRow();
},
/**
* Adds an AccessRuleView that represents the rightsHolder of the object.
* The rightsHolder needs to be handled specially because it's not a regular access rule in the system metadata.
*/
showRightsholder: function () {
//If the app is configured to hide the rightsHolder, then exit now
if (!MetacatUI.appModel.get("displayRightsHolderInAccessPolicy")) {
return;
}
//Get the DataONEObject associated with this access policy
var dataONEObject = this.collection.dataONEObject;
//If there is no DataONEObject associated with this access policy, then exit
if (!dataONEObject || !dataONEObject.get("rightsHolder")) {
return;
}
//Create an AccessRule model that represents the rightsHolder
var accessRuleModel = new AccessRule({
subject: dataONEObject.get("rightsHolder"),
read: true,
write: true,
changePermission: true,
dataONEObject: dataONEObject,
});
//Create an AccessRuleView
var accessRuleView = new AccessRuleView();
accessRuleView.accessPolicyView = this;
accessRuleView.model = accessRuleModel;
accessRuleView.allowChanges = MetacatUI.appModel.get(
"allowChangeRightsHolder",
);
//Add the AccessRuleView to this view
if (this.$(".access-rules-container .new").length) {
this.$(".access-rules-container .new").before(accessRuleView.el);
} else {
this.$(".access-rules-container").append(accessRuleView.el);
}
//Render the view
accessRuleView.render();
//Get the name for this subject
accessRuleModel.getSubjectInfo();
//When the access type is changed, check that there is still at least one owner.
this.listenTo(
accessRuleModel,
"change:read change:write change:changePermission",
this.checkForOwners,
);
},
/**
* Checks that there is at least one owner of this resource, and displays a warning message if not.
* @param {AccessRule} accessRuleModel
*/
checkForOwners: function (accessRuleModel) {
try {
if (!accessRuleModel) {
return;
}
//If changing the rightsHolder is disabled, we don't need to check for owners,
// since the rightsHolder will always be the owner.
if (
!MetacatUI.appModel.get("allowChangeRightsHolder") ||
!MetacatUI.appModel.get("displayRightsHolderInAccessPolicy")
) {
return;
}
//Get the rightsHolder for this resource
var rightsHolder;
if (
this.collection.dataONEObject &&
this.collection.dataONEObject.get("rightsHolder")
) {
rightsHolder = this.collection.dataONEObject.get("rightsHolder");
}
//Check if any priveleges have been removed
if (
!accessRuleModel.get("read") ||
!accessRuleModel.get("write") ||
!accessRuleModel.get("changePermission")
) {
//If there is no owner of this resource
if (!this.collection.hasOwner()) {
//If there is no rightsHolder either, then make this person the rightsHolder
// or if this is the rightsHolder, keep them the rightsHolder
if (
!rightsHolder ||
rightsHolder == accessRuleModel.get("subject")
) {
//Change this access rule back to an ownership level, since there needs to be at least one owner per object
accessRuleModel.set({
read: true,
write: true,
changePermission: true,
});
this.showOwnerWarning();
if (!rightsHolder) {
this.collection.dataONEObject.set(
"rightsHolder",
accessRuleModel.get("subject"),
);
this.collection.remove(accessRuleModel);
}
}
//If there is a rightsHolder, we don't need to do anything
else {
return;
}
}
//If the AccessRule model that was just changed was the rightsHolder,
// demote that subject as the rightsHolder, and replace with another subject
else if (rightsHolder == accessRuleModel.get("subject")) {
//Replace the rightsHolder with a different subject with ownership permissions
this.collection.replaceRightsHolder();
//Add the old rightsHolder AccessRule to the AccessPolicy
this.collection.add(accessRuleModel);
}
}
} catch (e) {
console.error(
"Could not check that there are owners in this access policy: ",
e,
);
}
},
/**
* Checks that there is at least one owner of this resource, and displays a warning message if not.
* @param {Event} e
*/
handleRemove: function (e) {
var accessRuleModel = $(e.target).parents(".access-rule").data("model");
//Get the rightsHolder for this resource
var rightsHolder;
if (
this.collection.dataONEObject &&
this.collection.dataONEObject.get("rightsHolder")
) {
rightsHolder = this.collection.dataONEObject.get("rightsHolder");
}
//If the rightsHolder was just removed,
if (rightsHolder == accessRuleModel.get("subject")) {
//If changing the rightsHolder is disabled, we don't need to check for owners,
// since the rightsHolder will always be the owner.
if (
!MetacatUI.appModel.get("allowChangeRightsHolder") ||
!MetacatUI.appModel.get("displayRightsHolderInAccessPolicy")
) {
return;
}
//If there is another owner of this resource
if (this.collection.hasOwner()) {
//Replace the rightsHolder with a different subject with ownership permissions
this.collection.replaceRightsHolder();
var accessRuleView = $(e.target)
.parents(".access-rule")
.data("view");
if (accessRuleView) {
accessRuleView.remove();
}
}
//If there are no other owners of this dataset, keep this person as the rightsHolder
else {
this.showOwnerWarning();
}
} else {
//Remove the AccessRule from the AccessPolicy
this.collection.remove(accessRuleModel);
}
},
/**
* Displays a warning message in this view that the object needs at least one owner.
*/
showOwnerWarning: function () {
//Show warning message
var msgContainer = this.$(".modal-body").length
? this.$(".modal-body")
: this.$el;
MetacatUI.appView.showAlert(
"At least one person or group needs to be an owner of this " +
this.resourceType +
".",
"alert-warning",
msgContainer,
2000,
{ remove: true },
);
},
/**
* Renders help text for the form in this view
*/
renderHelpText: function () {
try {
//Create HTML that shows the access policy help text
var accessExplanationEl = $(document.createElement("div")),
listEl = $(document.createElement("ul")).addClass("unstyled");
accessExplanationEl.append(listEl);
//Get the AccessRule options names
var accessRuleOptionNames = MetacatUI.appModel.get(
"accessRuleOptionNames",
);
if (
typeof accessRuleOptionNames !== "object" ||
!Object.keys(accessRuleOptionNames).length
) {
accessRuleOptionNames = {};
}
//Create HTML that shows an explanation of each enabled access rule option
_.mapObject(
MetacatUI.appModel.get("accessRuleOptions"),
function (isEnabled, accessType) {
//If this access type is disabled, exit
if (!isEnabled) {
return;
}
var accessTypeExplanation = "",
accessTypeName = accessRuleOptionNames[accessType];
//Get explanation text for the given access type
switch (accessType) {
case "read":
accessTypeExplanation =
" - can view this content, even when it's private.";
break;
case "write":
accessTypeExplanation =
" - can view and edit this content, even when it's private.";
break;
case "changePermission":
accessTypeExplanation =
" - can view and edit this content, even when it's private. In addition, can add and remove other people from these " +
MetacatUI.appModel.get("accessPolicyName") +
".";
break;
}
//Add this to the list
listEl.append(
$(document.createElement("li")).append(
$(document.createElement("h5")).text(accessTypeName),
$(document.createElement("span")).text(accessTypeExplanation),
),
);
},
);
//Add a popover to the Access column header to give more help text about the access types
this.$(".access-icon.popover-this").popover({
title: 'What does "Access" mean?',
delay: {
show: 800,
},
placement: "top",
trigger: "hover focus click",
container: this.$el,
html: true,
content: accessExplanationEl,
});
} catch (e) {
console.error("Could not render help text", e);
}
},
/**
* Toggles the public-read AccessRule for this resource
*/
togglePrivacy: function () {
//If this AccessPolicy is public already, make it private
if (this.collection.isPublic()) {
this.collection.makePrivate();
}
//Otherwise, make it public
else {
this.collection.makePublic();
}
},
/**
* Saves the AccessPolicy associated with this view
*/
save: function () {
//Remove any alerts that are currently displayed
this.$(".alert-container").remove();
//Get the DataONE Object that this Access Policy is for
var dataONEObject = this.collection.dataONEObject;
if (!dataONEObject) {
return;
}
// Broadcast the change across the package if appropriate
if (this.broadcast) {
MetacatUI.rootDataPackage.broadcastAccessPolicy(this.collection);
}
// Don't trigger a save if the item is new and just close the modal
if (dataONEObject.isNew()) {
$(this.$el).modal("hide");
return;
}
//Show the save progress as it is in progress, complete, in error, etc.
this.listenTo(
dataONEObject,
"change:uploadStatus",
this.showSaveProgress,
);
//Update the SystemMetadata for this object
dataONEObject.updateSysMeta();
},
/**
* Show visual cues in this view to show the user the status of the system metadata update.
* @param {DataONEObject} dataONEObject - The object being updated
*/
showSaveProgress: function (dataONEObject) {
if (!dataONEObject) {
return;
}
var status = dataONEObject.get("uploadStatus");
//When the status is "in progress"
if (status == "p") {
//Disable the Save button and change the text to say, "Saving..."
this.$(".save.btn").text("Saving...").attr("disabled", "disabled");
this.$(".cancel.btn").attr("disabled", "disabled");
return;
}
//When the status is "complete"
else if (status == "c") {
//Create a checkmark icon
var icon = $(document.createElement("i")).addClass(
"icon icon-ok icon-on-left",
),
cancelBtn = this.$(".cancel.btn");
saveBtn = this.$(".save.btn");
//Disable the Save button and change the text to say, "Saving..."
cancelBtn.text("Saved").removeAttr("disabled");
saveBtn.text("Saved").prepend(icon).removeAttr("disabled");
setTimeout(function () {
saveBtn.empty().text("Save");
}, 2000);
this.cachedModels = _.clone(this.collection.models);
// Hide the modal only on a successful save
$(this.$el).modal("hide");
}
//When the status is "error"
else if (status == "e") {
var msgContainer = this.$(".modal-body").length
? this.$(".modal-body")
: this.$el;
MetacatUI.appView.showAlert(
"Your changes could not be saved.",
"alert-error",
msgContainer,
0,
{ remove: true },
);
//Reset the save button
this.$(".save.btn").text("Save").removeAttr("disabled");
}
//Remove the listener for this function
this.stopListening(
dataONEObject,
"change:uploadStatus",
this.showSaveProgress,
);
},
/**
* Resets the state of the models stored in the view's collection to the
* latest cached copy. Triggered either when the Cancel button is hit or
* the modal containing this view is hidden.
* @since 2.15.0
*/
reset: function () {
if (!this.collection || !this.cachedModels) {
return;
}
this.collection.set(this.cachedModels);
},
/**
* Adds messaging to this view to tell the user they are unauthorized to change the AccessPolicy
* of this object(s)
*/
showUnauthorized: function () {
//Get the container element for the message
var msgContainer = this.$(".modal-body").length
? this.$(".modal-body")
: this.$el;
//Empty the container element
msgContainer.empty();
//Show the info message
MetacatUI.appView.showAlert(
"The person who owns this " +
this.resourceType +
" has not given you permission to change the " +
MetacatUI.appModel.get("accessPolicyName") +
". Contact the owner to be added " +
" as another owner of this " +
this.resourceType +
".",
"alert-info subtle",
msgContainer,
null,
{ remove: false },
);
//Add an unauthorized class to this view for further styling options
this.$el.addClass("unauthorized");
},
},
);
return AccessPolicyView;
});