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
* @extends Backbone.View
* @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.
* @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),
/**
* 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 .access-rule .remove" : "handleRemove"
},
/**
* Creates a new AccessPolicyView
* @param {Object} options - A literal object with options to pass to the view
*/
initialize: function(options){
},
/**
* 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 = "science metadata";
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
}));
//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
if( MetacatUI.appModel.get("showPortalPublicToggle") !== false ){
var enabledSubjects = MetacatUI.appModel.get("showPortalPublicToggleForSubjects");
if( Array.isArray(enabledSubjects) && enabledSubjects.length ){
var usersGroups = _.pluck(MetacatUI.appUserModel.get("isMemberOf"), "groupId");
if( _.contains(enabledSubjects, MetacatUI.appUserModel.get("username")) ||
_.intersection(enabledSubjects, usersGroups).length){
this.renderPublicToggle();
}
}
else{
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(){
var view = this;
//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: function(){
if( view.collection.isPublic() ){
return "Your " + view.resourceType + " is public. Anyone can see this content."
}
else{
return "Your " + view.resourceType + " is private. Only people you approve can see this content."
}
},
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());
},
/**
* 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;
}
//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");
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"),
saveBtn = this.$(".save.btn");
//Disable the Save button and change the text to say, "Saving..."
saveBtn.text("Saved").prepend(icon).removeAttr("disabled");
setTimeout(function(){ saveBtn.empty().text("Save") }, 2000);
}
//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);
}
});
return AccessPolicyView;
});