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