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") + ".

Technical details: " + e.message + "

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