Commit a4ed4a72 authored by Robert Knight's avatar Robert Knight

Hide the "Public" group from the client under certain conditions

Implement a business requirement that the "Public" group should be
hidden from the groups list if all these conditions are met:

 - The user is logged-out
 - The current document has groups associated with it
 - The user has not visited a direct-link to an annotation in "Public"

---

This requirement introduces a complexity in that deciding which group to
focus and fetch annotations for after initially fetching groups now
depends on the current profile. However the profile and groups are
fetched concurrently to reduce the number of API round-trips before we
can show annotations in the client.

Fortunately we don't need to know the exact userid to know which group
to focus, only whether the list of groups was fetched as a logged-in
user or not. This can be determined from the access token that was sent
with the `/api/groups` request.
parent a21d3c23
...@@ -38,17 +38,65 @@ function groups($rootScope, store, api, isSidebar, localStorage, serviceUrl, ses ...@@ -38,17 +38,65 @@ function groups($rootScope, store, api, isSidebar, localStorage, serviceUrl, ses
return awaitStateChange(store, mainUri); return awaitStateChange(store, mainUri);
} }
/**
* Filter the returned list of groups from the API.
*
* `filterGroups` performs client-side filtering to hide the "Public" group
* for logged-out users under certain conditions.
*
* @param {Group[]} groups
* @param {boolean} isLoggedIn
* @param {string|null} directLinkedAnnotationId
* @return {Promise<Group[]>}
*/
function filterGroups(groups, isLoggedIn, directLinkedAnnotationId) {
// Logged-in users always see the "Public" group.
if (isLoggedIn) {
return Promise.resolve(groups);
}
// If the main document URL has no groups associated with it, always show
// the "Public" group.
var pageHasAssociatedGroups = groups.some(g => g.id !== '__world__');
if (!pageHasAssociatedGroups) {
return Promise.resolve(groups);
}
// Hide the "Public" group, unless the user specifically visited a direct-
// link to an annotation in that group.
var nonWorldGroups = groups.filter(g => g.id !== '__world__');
if (!directLinkedAnnotationId) {
return Promise.resolve(nonWorldGroups);
}
return api.annotation.get({ id: directLinkedAnnotationId }).then(ann => {
if (ann.group === '__world__') {
return groups;
} else {
return nonWorldGroups;
}
}).catch(() => {
// Annotation does not exist or we do not have permission to read it.
// Assume it is not in "Public".
return nonWorldGroups;
});
}
// The document URI passed to the most recent `GET /api/groups` call in order // The document URI passed to the most recent `GET /api/groups` call in order
// to include groups associated with this page. This is retained to determine // to include groups associated with this page. This is retained to determine
// whether we need to re-fetch groups if the URLs of frames connected to the // whether we need to re-fetch groups if the URLs of frames connected to the
// sidebar app changes. // sidebar app changes.
var documentUri; var documentUri;
/** /**
* Fetch the list of applicable groups from the API. * Fetch the list of applicable groups from the API.
* *
* The list of applicable groups depends on the current userid and the URI of * The list of applicable groups depends on the current userid and the URI of
* the attached frames. * the attached frames.
*
* @return {Promise<Group[]>}
*/ */
function load() { function load() {
var uri = Promise.resolve(null); var uri = Promise.resolve(null);
...@@ -66,7 +114,13 @@ function groups($rootScope, store, api, isSidebar, localStorage, serviceUrl, ses ...@@ -66,7 +114,13 @@ function groups($rootScope, store, api, isSidebar, localStorage, serviceUrl, ses
params.document_uri = uri; params.document_uri = uri;
} }
documentUri = uri; documentUri = uri;
return api.groups.list(params);
// Fetch groups from the API.
return api.groups.list(params, null, { includeMetadata: true });
}).then(({ data, token }) => {
var isLoggedIn = token !== null;
var directLinkedAnnotation = settings.annotations;
return filterGroups(data, isLoggedIn, directLinkedAnnotation);
}).then(groups => { }).then(groups => {
var isFirstLoad = store.allGroups().length === 0; var isFirstLoad = store.allGroups().length === 0;
var prevFocusedGroup = localStorage.getItem(STORAGE_KEY); var prevFocusedGroup = localStorage.getItem(STORAGE_KEY);
...@@ -76,7 +130,7 @@ function groups($rootScope, store, api, isSidebar, localStorage, serviceUrl, ses ...@@ -76,7 +130,7 @@ function groups($rootScope, store, api, isSidebar, localStorage, serviceUrl, ses
store.focusGroup(prevFocusedGroup); store.focusGroup(prevFocusedGroup);
} }
return store.allGroups(); return groups;
}); });
} }
......
...@@ -4,6 +4,22 @@ var events = require('../../events'); ...@@ -4,6 +4,22 @@ var events = require('../../events');
var fakeReduxStore = require('../../test/fake-redux-store'); var fakeReduxStore = require('../../test/fake-redux-store');
var groups = require('../groups'); var groups = require('../groups');
/**
* Generate a truth table containing every possible combination of a set of
* boolean inputs.
*
* @param {number} columns
* @return {Array<boolean[]>}
*/
function truthTable(columns) {
if (columns === 1) {
return [[true], [false]];
}
var subTable = truthTable(columns - 1);
return [...subTable.map(row => [true, ...row]),
...subTable.map(row => [false, ...row])];
}
// Return a mock session service containing three groups. // Return a mock session service containing three groups.
var sessionWithThreeGroups = function() { var sessionWithThreeGroups = function() {
return { return {
...@@ -75,13 +91,20 @@ describe('groups', function() { ...@@ -75,13 +91,20 @@ describe('groups', function() {
$broadcast: sandbox.stub(), $broadcast: sandbox.stub(),
}; };
fakeApi = { fakeApi = {
annotation: {
get: sinon.stub(),
},
group: { group: {
member: { member: {
delete: sandbox.stub().returns(Promise.resolve()), delete: sandbox.stub().returns(Promise.resolve()),
}, },
}, },
groups: { groups: {
list: sandbox.stub().returns(Promise.resolve(dummyGroups)), list: sandbox.stub().returns(Promise.resolve({
data: dummyGroups,
token: '1234',
})),
}, },
}; };
fakeServiceUrl = sandbox.stub(); fakeServiceUrl = sandbox.stub();
...@@ -181,6 +204,40 @@ describe('groups', function() { ...@@ -181,6 +204,40 @@ describe('groups', function() {
assert.calledWith(fakeApi.groups.list, sinon.match({ authority: 'publisher.org' })); assert.calledWith(fakeApi.groups.list, sinon.match({ authority: 'publisher.org' }));
}); });
}); });
truthTable(3).forEach(([ loggedIn, pageHasAssociatedGroups, directLinkToPublicAnnotation ]) => {
it('excludes the "Public" group if user logged out and page has associated groups', () => {
var svc = service();
var shouldShowPublicGroup = loggedIn || !pageHasAssociatedGroups || directLinkToPublicAnnotation;
// Setup the direct-linked annotation.
if (directLinkToPublicAnnotation) {
fakeApi.annotation.get.returns(Promise.resolve({
id: 'direct-linked-ann',
group: '__world__',
}));
fakeSettings.annotations = 'direct-linked-ann';
} else {
fakeSettings.annotations = null;
}
// Create groups response from server.
var groups = [{ name: 'Public', id: '__world__' }];
if (pageHasAssociatedGroups) {
groups.push({ name: 'BioPub', id: 'biopub' });
}
fakeApi.groups.list.returns(Promise.resolve({
token: loggedIn ? '1234' : null,
data: groups,
}));
return svc.load().then(groups => {
var publicGroupShown = groups.some(g => g.id === '__world__');
assert.equal(publicGroupShown, shouldShowPublicGroup);
});
});
});
}); });
describe('#get', function() { describe('#get', function() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment