Merge branch 'open1062' into open-master

Merge changes for WTD-1062
This commit is contained in:
Victor Woeltjen 2015-04-08 12:48:49 -07:00
commit 79f6e8c082
13 changed files with 389 additions and 8 deletions

View File

@ -108,7 +108,8 @@
"templateUrl": "templates/items/items.html",
"uses": [ "composition" ],
"gestures": [ "drop" ],
"type": "folder"
"type": "folder",
"editable": false
}
],
"components": [

View File

@ -41,7 +41,7 @@
},
{
"key": "properties",
"category": "contextual",
"category": ["contextual", "view-control"],
"implementation": "actions/PropertiesAction.js",
"glyph": "p",
"name": "Edit Properties...",
@ -75,6 +75,16 @@
"depends": [ "$location" ]
}
],
"policies": [
{
"category": "action",
"implementation": "policies/EditActionPolicy.js"
},
{
"category": "view",
"implementation": "policies/EditableViewPolicy.js"
}
],
"templates": [
{
"key": "edit-library",

View File

@ -0,0 +1,61 @@
/*global define*/
define(
[],
function () {
"use strict";
/**
* Policy controlling when the `edit` and/or `properties` actions
* can appear as applicable actions of the `view-control` category
* (shown as buttons in the top-right of browse mode.)
* @constructor
*/
function EditActionPolicy() {
// Get a count of views which are not flagged as non-editable.
function countEditableViews(context) {
var domainObject = (context || {}).domainObject,
views = domainObject && domainObject.useCapability('view'),
count = 0;
// A view is editable unless explicitly flagged as not
(views || []).forEach(function (view) {
count += (view.editable !== false) ? 1 : 0;
});
return count;
}
return {
/**
* Check whether or not a given action is allowed by this
* policy.
* @param {Action} action the action
* @param context the context
* @returns {boolean} true if not disallowed
*/
allow: function (action, context) {
var key = action.getMetadata().key,
category = (context || {}).category;
// Only worry about actions in the view-control category
if (category === 'view-control') {
// Restrict 'edit' to cases where there are editable
// views (similarly, restrict 'properties' to when
// the converse is true)
if (key === 'edit') {
return countEditableViews(context) > 0;
} else if (key === 'properties') {
return countEditableViews(context) < 1;
}
}
// Like all policies, allow by default.
return true;
}
};
}
return EditActionPolicy;
}
);

View File

@ -0,0 +1,36 @@
/*global define*/
define(
[],
function () {
"use strict";
/**
* Policy controlling which views should be visible in Edit mode.
* @constructor
*/
function EditableViewPolicy() {
return {
/**
* Check whether or not a given action is allowed by this
* policy.
* @param {Action} action the action
* @param domainObject the domain object which will be viewed
* @returns {boolean} true if not disallowed
*/
allow: function (view, domainObject) {
// If a view is flagged as non-editable, only allow it
// while we're not in Edit mode.
if ((view || {}).editable === false) {
return !domainObject.hasCapability('editor');
}
// Like all policies, allow by default.
return true;
}
};
}
return EditableViewPolicy;
}
);

View File

@ -0,0 +1,78 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
["../../src/policies/EditActionPolicy"],
function (EditActionPolicy) {
"use strict";
describe("The Edit action policy", function () {
var editableView,
nonEditableView,
undefinedView,
testViews,
testContext,
mockDomainObject,
mockEditAction,
mockPropertiesAction,
policy;
beforeEach(function () {
mockDomainObject = jasmine.createSpyObj(
'domainObject',
[ 'useCapability' ]
);
mockEditAction = jasmine.createSpyObj('edit', ['getMetadata']);
mockPropertiesAction = jasmine.createSpyObj('edit', ['getMetadata']);
editableView = { editable: true };
nonEditableView = { editable: false };
undefinedView = { someKey: "some value" };
testViews = [];
mockDomainObject.useCapability.andCallFake(function (c) {
// Provide test views, only for the view capability
return c === 'view' && testViews;
});
mockEditAction.getMetadata.andReturn({ key: 'edit' });
mockPropertiesAction.getMetadata.andReturn({ key: 'properties' });
testContext = {
domainObject: mockDomainObject,
category: 'view-control'
};
policy = new EditActionPolicy();
});
it("allows the edit action when there are editable views", function () {
testViews = [ editableView ];
expect(policy.allow(mockEditAction, testContext)).toBeTruthy();
// No edit flag defined; should be treated as editable
testViews = [ undefinedView, undefinedView ];
expect(policy.allow(mockEditAction, testContext)).toBeTruthy();
});
it("allows the edit properties action when there are no editable views", function () {
testViews = [ nonEditableView, nonEditableView ];
expect(policy.allow(mockPropertiesAction, testContext)).toBeTruthy();
});
it("disallows the edit action when there are no editable views", function () {
testViews = [ nonEditableView, nonEditableView ];
expect(policy.allow(mockEditAction, testContext)).toBeFalsy();
});
it("disallows the edit properties action when there are editable views", function () {
testViews = [ editableView ];
expect(policy.allow(mockPropertiesAction, testContext)).toBeFalsy();
});
it("allows the edit properties outside of the 'view-control' category", function () {
testViews = [ nonEditableView ];
testContext.category = "something-else";
expect(policy.allow(mockPropertiesAction, testContext)).toBeTruthy();
});
});
}
);

View File

@ -0,0 +1,56 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
["../../src/policies/EditableViewPolicy"],
function (EditableViewPolicy) {
"use strict";
describe("The editable view policy", function () {
var testView,
mockDomainObject,
testMode,
policy;
beforeEach(function () {
testMode = true; // Act as if we're in Edit mode by default
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['hasCapability']
);
mockDomainObject.hasCapability.andCallFake(function (c) {
return (c === 'editor') && testMode;
});
policy = new EditableViewPolicy();
});
it("disallows views in edit mode that are flagged as non-editable", function () {
expect(policy.allow({ editable: false }, mockDomainObject))
.toBeFalsy();
});
it("allows views in edit mode that are flagged as editable", function () {
expect(policy.allow({ editable: true }, mockDomainObject))
.toBeTruthy();
});
it("allows any view outside of edit mode", function () {
var testViews = [
{ editable: false },
{ editable: true },
{ someKey: "some value" }
];
testMode = false; // Act as if we're not in Edit mode
testViews.forEach(function (testView) {
expect(policy.allow(testView, mockDomainObject)).toBeTruthy();
});
});
it("treats views with no defined 'editable' property as editable", function () {
expect(policy.allow({ someKey: "some value" }, mockDomainObject))
.toBeTruthy();
});
});
}
);

View File

@ -18,6 +18,8 @@
"objects/EditableDomainObject",
"objects/EditableDomainObjectCache",
"objects/EditableModelCache",
"policies/EditableViewPolicy",
"policies/EditActionPolicy",
"representers/EditRepresenter",
"representers/EditToolbar",
"representers/EditToolbarRepresenter",

View File

@ -84,11 +84,21 @@ define(
// Build up look-up tables
actions.forEach(function (Action) {
if (Action.category) {
actionsByCategory[Action.category] =
actionsByCategory[Action.category] || [];
actionsByCategory[Action.category].push(Action);
}
// Get an action's category or categories
var categories = Action.category || [];
// Convert to an array if necessary
categories = Array.isArray(categories) ?
categories : [categories];
// Store action under all relevant categories
categories.forEach(function (category) {
actionsByCategory[category] =
actionsByCategory[category] || [];
actionsByCategory[category].push(Action);
});
// Store action by ekey as well
if (Action.key) {
actionsByKey[Action.key] =
actionsByKey[Action.key] || [];

View File

@ -10,6 +10,12 @@
"implementation": "PolicyActionDecorator.js",
"depends": [ "policyService" ]
},
{
"type": "decorator",
"provides": "viewService",
"implementation": "PolicyViewDecorator.js",
"depends": [ "policyService" ]
},
{
"type": "provider",
"provides": "policyService",

View File

@ -15,7 +15,7 @@ define(
return {
/**
* Get actions which are applicable in this context.
* These will be filters to remove any actions which
* These will be filtered to remove any actions which
* are deemed inapplicable by policy.
* @param context the context in which the action will occur
* @returns {Action[]} applicable actions

View File

@ -0,0 +1,37 @@
/*global define*/
define(
[],
function () {
"use strict";
/**
* Filters out views based on policy.
* @param {PolicyService} policyService the service which provides
* policy decisions
* @param {ViewService} viewService the service to decorate
*/
function PolicyActionDecorator(policyService, viewService) {
return {
/**
* Get views which are applicable to this domain object.
* These will be filtered to remove any views which
* are deemed inapplicable by policy.
* @param {DomainObject} the domain object to view
* @returns {View[]} applicable views
*/
getViews: function (domainObject) {
// Check if an action is allowed by policy.
function allow(view) {
return policyService.allow('view', view, domainObject);
}
// Look up actions, filter out the disallowed ones.
return viewService.getViews(domainObject).filter(allow);
}
};
}
return PolicyActionDecorator;
}
);

View File

@ -0,0 +1,83 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/PolicyViewDecorator"],
function (PolicyViewDecorator) {
"use strict";
describe("The policy view decorator", function () {
var mockPolicyService,
mockViewService,
mockDomainObject,
testViews,
decorator;
beforeEach(function () {
mockPolicyService = jasmine.createSpyObj(
'policyService',
['allow']
);
mockViewService = jasmine.createSpyObj(
'viewService',
['getViews']
);
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['getId']
);
// Content of actions should be irrelevant to this
// decorator, so just give it some objects to pass
// around.
testViews = [
{ someKey: "a" },
{ someKey: "b" },
{ someKey: "c" }
];
mockDomainObject.getId.andReturn('xyz');
mockViewService.getViews.andReturn(testViews);
mockPolicyService.allow.andReturn(true);
decorator = new PolicyViewDecorator(
mockPolicyService,
mockViewService
);
});
it("delegates to its decorated view service", function () {
decorator.getViews(mockDomainObject);
expect(mockViewService.getViews)
.toHaveBeenCalledWith(mockDomainObject);
});
it("provides views from its decorated view service", function () {
// Mock policy service allows everything by default,
// so everything should be returned
expect(decorator.getViews(mockDomainObject))
.toEqual(testViews);
});
it("consults the policy service for each candidate view", function () {
decorator.getViews(mockDomainObject);
testViews.forEach(function (testView) {
expect(mockPolicyService.allow).toHaveBeenCalledWith(
'view',
testView,
mockDomainObject
);
});
});
it("filters out policy-disallowed views", function () {
// Disallow the second action
mockPolicyService.allow.andCallFake(function (cat, candidate, ctxt) {
return candidate.someKey !== 'b';
});
expect(decorator.getViews(mockDomainObject))
.toEqual([ testViews[0], testViews[2] ]);
});
});
}
);

View File

@ -1,4 +1,5 @@
[
"PolicyActionDecorator",
"PolicyViewDecorator",
"PolicyProvider"
]