Commit 6fe1e357 authored by Sean Hammond's avatar Sean Hammond

Set group and visibility of reply to that of parent

Set the group and visibility of replies to that of their parents, when
replies are first created.

Also prevent the visibility setting of a reply from overwriting the
cached-in-local-storage visibility level. This prevents the following
problem:

- I'm annotating publicly (my cached-in-local-storage visibility
  setting is public)
- I reply to a private annotation
- The visibility of my reply is set to that of its parent, private
- This visibility is then cached in local storage
- I make a new top-level annotation, and now it's defaulting to private
  instead of public, even though the user never did anything to change
  the visibility from public to private, it was done automatically
  because they replied to a private annotation.

Obviously changing the mode from private to public when replying to a
public annotation was also possible.

This means that if the user _does_ change the visibility of a reply, we
_don't_ cache that either:

- I'm annotating publicly (my cached-in-local-storage visibility
  setting is public)
- I reply to a public annotation
- The visibility of my reply is set to that of its parent, public
- This visibility is not cached in local storage because it's a reply
  (and the value cached in the local storage is already the same anyway)
- I then deliberately change the visibility of my reply from public to
  private
- Again this visibility is not cached in local storage because it's a reply
- The next time I make a new top-level annotation, it will still be
  defaulting to public, will not have changed to private mode
parent 17954cf0
### global -validate ### ### global -validate ###
STORAGE_KEY = 'hypothesis.privacy'
# Validate an annotation. # Validate an annotation.
# Annotations must be attributed to a user or marked as deleted. # Annotations must be attributed to a user or marked as deleted.
# A public annotation is valid only if they have a body. # A public annotation is valid only if they have a body.
...@@ -44,10 +46,10 @@ errorMessage = (reason) -> ...@@ -44,10 +46,10 @@ errorMessage = (reason) ->
AnnotationController = [ AnnotationController = [
'$scope', '$timeout', '$q', '$rootScope', '$document', '$scope', '$timeout', '$q', '$rootScope', '$document',
'drafts', 'flash', 'permissions', 'tags', 'time', 'drafts', 'flash', 'permissions', 'tags', 'time',
'annotationUI', 'annotationMapper', 'session', 'groups' 'annotationUI', 'annotationMapper', 'session', 'groups', 'localStorage'
($scope, $timeout, $q, $rootScope, $document, ($scope, $timeout, $q, $rootScope, $document,
drafts, flash, permissions, tags, time, drafts, flash, permissions, tags, time,
annotationUI, annotationMapper, session, groups) -> annotationUI, annotationMapper, session, groups, localStorage) ->
@annotation = {} @annotation = {}
@action = 'view' @action = 'view'
...@@ -60,9 +62,21 @@ AnnotationController = [ ...@@ -60,9 +62,21 @@ AnnotationController = [
@timestamp = null @timestamp = null
model = $scope.annotationGet() model = $scope.annotationGet()
# Set the group of new annotations.
if not model.group if not model.group
model.group = groups.focused().id model.group = groups.focused().id
# Set the permissions of new annotations.
if not model.permissions
defaultLevel = localStorage.getItem(STORAGE_KEY);
if (defaultLevel != 'private') and (defaultLevel != 'shared')
defaultLevel = 'shared';
if defaultLevel == 'private'
model.permissions = permissions.private()
else
model.permissions = permissions.public(model.group)
highlight = model.$highlight highlight = model.$highlight
original = null original = null
vm = this vm = this
...@@ -122,6 +136,14 @@ AnnotationController = [ ...@@ -122,6 +136,14 @@ AnnotationController = [
# The changes take effect when the annotation is saved # The changes take effect when the annotation is saved
### ###
this.setPrivacy = (privacy) -> this.setPrivacy = (privacy) ->
# When the user changes the privacy level of an annotation they're
# creating or editing, we cache that and use the same privacy level the
# next time they create an annotation.
# But _don't_ cache it when they change the privacy level of a reply.
if not model.references # If the annotation is not a reply.
localStorage.setItem(STORAGE_KEY, privacy);
if privacy == 'private' if privacy == 'private'
@annotation.permissions = permissions.private() @annotation.permissions = permissions.private()
else if privacy == 'shared' else if privacy == 'shared'
...@@ -276,9 +298,11 @@ AnnotationController = [ ...@@ -276,9 +298,11 @@ AnnotationController = [
reply = annotationMapper.createAnnotation({references, uri}) reply = annotationMapper.createAnnotation({references, uri})
reply.group = model.group
if session.state.userid if session.state.userid
if permissions.isPublic model.permissions if permissions.isPublic(model.permissions, model.group)
reply.permissions = permissions.public() reply.permissions = permissions.public(reply.group)
else else
reply.permissions = permissions.private() reply.permissions = permissions.private()
......
'use strict'; 'use strict';
var STORAGE_KEY = 'hypothesis.privacy';
// @ngInject // @ngInject
function PublishAnnotationBtnController($scope, localStorage) { function PublishAnnotationBtnController($scope) {
var vm = this; var vm = this;
vm.group = { vm.group = {
...@@ -24,7 +22,6 @@ function PublishAnnotationBtnController($scope, localStorage) { ...@@ -24,7 +22,6 @@ function PublishAnnotationBtnController($scope, localStorage) {
} }
this.setPrivacy = function (level) { this.setPrivacy = function (level) {
localStorage.setItem(STORAGE_KEY, level);
$scope.onSetPrivacy({level: level}); $scope.onSetPrivacy({level: level});
} }
...@@ -32,18 +29,6 @@ function PublishAnnotationBtnController($scope, localStorage) { ...@@ -32,18 +29,6 @@ function PublishAnnotationBtnController($scope, localStorage) {
updatePublishActionLabel(); updatePublishActionLabel();
}); });
if ($scope.isNew) {
// set the privacy level for new annotations.
// FIXME - This should be done at the time the annotation is created,
// not by this control
var defaultLevel = localStorage.getItem(STORAGE_KEY);
if (defaultLevel !== 'private' &&
defaultLevel !== 'shared') {
defaultLevel = 'shared';
}
this.setPrivacy(defaultLevel);
}
function updatePublishActionLabel() { function updatePublishActionLabel() {
if ($scope.isShared) { if ($scope.isShared) {
vm.publishDestination = vm.group.name; vm.publishDestination = vm.group.name;
......
...@@ -26,6 +26,7 @@ describe 'annotation', -> ...@@ -26,6 +26,7 @@ describe 'annotation', ->
fakeTags = null fakeTags = null
fakeTime = null fakeTime = null
fakeUrlEncodeFilter = null fakeUrlEncodeFilter = null
fakeLocalStorage = null
sandbox = null sandbox = null
createDirective = -> createDirective = ->
...@@ -87,6 +88,10 @@ describe 'annotation', -> ...@@ -87,6 +88,10 @@ describe 'annotation', ->
nextFuzzyUpdate: sandbox.stub().returns(30) nextFuzzyUpdate: sandbox.stub().returns(30)
} }
fakeUrlEncodeFilter = (v) -> encodeURIComponent(v) fakeUrlEncodeFilter = (v) -> encodeURIComponent(v)
fakeLocalStorage = {
setItem: sandbox.stub()
getItem: sandbox.stub()
}
fakeGroups = { fakeGroups = {
focused: -> {} focused: -> {}
...@@ -108,6 +113,7 @@ describe 'annotation', -> ...@@ -108,6 +113,7 @@ describe 'annotation', ->
$provide.value 'tags', fakeTags $provide.value 'tags', fakeTags
$provide.value 'time', fakeTime $provide.value 'time', fakeTime
$provide.value 'urlencodeFilter', fakeUrlEncodeFilter $provide.value 'urlencodeFilter', fakeUrlEncodeFilter
$provide.value 'localStorage', fakeLocalStorage
$provide.value 'groups', fakeGroups $provide.value 'groups', fakeGroups
return return
...@@ -177,6 +183,20 @@ describe 'annotation', -> ...@@ -177,6 +183,20 @@ describe 'annotation', ->
controller.reply() controller.reply()
assert.deepEqual(reply.permissions, {read: ['everybody']}) assert.deepEqual(reply.permissions, {read: ['everybody']})
it 'makes the annotation shared if the parent is shared', ->
$scope.annotation.group = "my group"
$scope.annotation.permissions = {read: ["my group"]}
reply = {}
fakeAnnotationMapper.createAnnotation.returns(reply)
fakePermissions.isPublic = (permissions, group) ->
return group in permissions.read
fakePermissions.public = (group) ->
return {read: [group]}
controller.reply()
assert "my group" in reply.permissions.read
it 'does not add the world readable principal if the parent is private', -> it 'does not add the world readable principal if the parent is private', ->
reply = {} reply = {}
fakeAnnotationMapper.createAnnotation.returns(reply) fakeAnnotationMapper.createAnnotation.returns(reply)
...@@ -184,6 +204,13 @@ describe 'annotation', -> ...@@ -184,6 +204,13 @@ describe 'annotation', ->
controller.reply() controller.reply()
assert.deepEqual(reply.permissions, {read: ['justme']}) assert.deepEqual(reply.permissions, {read: ['justme']})
it "sets the reply's group to be the same as its parent's", ->
$scope.annotation.group = "my group"
reply = {}
fakeAnnotationMapper.createAnnotation.returns(reply)
controller.reply()
assert.equal(reply.group, $scope.annotation.group)
describe '#setPrivacy', -> describe '#setPrivacy', ->
beforeEach -> beforeEach ->
createDirective() createDirective()
...@@ -192,9 +219,6 @@ describe 'annotation', -> ...@@ -192,9 +219,6 @@ describe 'annotation', ->
annotation.$update = sinon.stub().returns(Promise.resolve()) annotation.$update = sinon.stub().returns(Promise.resolve())
controller.edit(); controller.edit();
controller.setPrivacy('private') controller.setPrivacy('private')
# verify that permissions are not updated until the annotation
# is saved
assert.deepEqual(annotation.permissions, {})
controller.save().then(-> controller.save().then(->
# verify that the permissions are updated once the annotation # verify that the permissions are updated once the annotation
# is saved # is saved
...@@ -205,11 +229,37 @@ describe 'annotation', -> ...@@ -205,11 +229,37 @@ describe 'annotation', ->
annotation.$update = sinon.stub().returns(Promise.resolve()) annotation.$update = sinon.stub().returns(Promise.resolve())
controller.edit(); controller.edit();
controller.setPrivacy('shared') controller.setPrivacy('shared')
assert.deepEqual(annotation.permissions, {})
controller.save().then(-> controller.save().then(->
assert.deepEqual(annotation.permissions, {read: ['everybody']}) assert.deepEqual(annotation.permissions, {read: ['everybody']})
) )
it 'saves the "shared" visibility level to localStorage', ->
annotation.$update = sinon.stub().returns(Promise.resolve())
controller.edit();
controller.setPrivacy('shared')
controller.save().then(->
assert(fakeLocalStorage.setItem.calledWithExactly(
'hypothesis.privacy', 'shared'))
)
it 'saves the "private" visibility level to localStorage', ->
annotation.$update = sinon.stub().returns(Promise.resolve())
controller.edit();
controller.setPrivacy('private')
controller.save().then(->
assert(fakeLocalStorage.setItem.calledWithExactly(
'hypothesis.privacy', 'private'))
)
it "doesn't save the visibility if the annotation is a reply", ->
annotation.$update = sinon.stub().returns(Promise.resolve())
annotation.references = ["parent id"]
controller.edit();
controller.setPrivacy('private')
controller.save().then(->
assert(not fakeLocalStorage.setItem.called)
)
describe '#hasContent', -> describe '#hasContent', ->
beforeEach -> beforeEach ->
createDirective() createDirective()
...@@ -508,7 +558,7 @@ describe 'annotation', -> ...@@ -508,7 +558,7 @@ describe 'annotation', ->
done() done()
$timeout.flush() $timeout.flush()
describe "creating a new annotation", -> describe "saving a new annotation", ->
beforeEach -> beforeEach ->
createDirective() createDirective()
fakeFlash.error = sandbox.stub() fakeFlash.error = sandbox.stub()
...@@ -548,7 +598,7 @@ describe 'annotation', -> ...@@ -548,7 +598,7 @@ describe 'annotation', ->
controller.save() controller.save()
assert fakeFlash.error.notCalled assert fakeFlash.error.notCalled
describe "editing an annotation", -> describe "saving an edited an annotation", ->
beforeEach -> beforeEach ->
createDirective() createDirective()
...@@ -617,8 +667,9 @@ describe("AnnotationController", -> ...@@ -617,8 +667,9 @@ describe("AnnotationController", ->
createAnnotationDirective = ({annotation, personaFilter, momentFilter, createAnnotationDirective = ({annotation, personaFilter, momentFilter,
urlencodeFilter, drafts, features, flash, urlencodeFilter, drafts, features, flash,
permissions, session, tags, time, annotationUI, permissions, session, tags, time, annotationUI,
annotationMapper, groups, annotationMapper, groups, documentTitleFilter,
documentTitleFilter, documentDomainFilter}) -> documentDomainFilter, localStorage}) ->
session = session or {state: {userid: "acct:fred@hypothes.is"}}
locals = { locals = {
personaFilter: personaFilter or -> personaFilter: personaFilter or ->
momentFilter: momentFilter or {} momentFilter: momentFilter or {}
...@@ -635,11 +686,15 @@ describe("AnnotationController", -> ...@@ -635,11 +686,15 @@ describe("AnnotationController", ->
error: -> error: ->
} }
permissions: permissions or { permissions: permissions or {
isPublic: -> false isPublic: (permissions, group) ->
isPrivate: -> false return group in permissions.read
isPrivate: (permissions, user) ->
return user in permissions.read
permits: -> true permits: -> true
public: (group) -> {"read": [group]}
private: -> {"read": [session.state.userid]}
} }
session: session or {state: {}} session: session
tags: tags or {store: ->} tags: tags or {store: ->}
time: time or { time: time or {
toFuzzyString: -> toFuzzyString: ->
...@@ -653,6 +708,10 @@ describe("AnnotationController", -> ...@@ -653,6 +708,10 @@ describe("AnnotationController", ->
} }
documentTitleFilter: documentTitleFilter or -> '' documentTitleFilter: documentTitleFilter or -> ''
documentDomainFilter: documentDomainFilter or -> '' documentDomainFilter: documentDomainFilter or -> ''
localStorage: localStorage or {
setItem: ->
getItem: ->
}
} }
module(($provide) -> module(($provide) ->
$provide.value("personaFilter", locals.personaFilter) $provide.value("personaFilter", locals.personaFilter)
...@@ -670,6 +729,7 @@ describe("AnnotationController", -> ...@@ -670,6 +729,7 @@ describe("AnnotationController", ->
$provide.value("groups", locals.groups) $provide.value("groups", locals.groups)
$provide.value("documentTitleFilter", locals.documentTitleFilter) $provide.value("documentTitleFilter", locals.documentTitleFilter)
$provide.value("documentDomainFilter", locals.documentDomainFilter) $provide.value("documentDomainFilter", locals.documentDomainFilter)
$provide.value("localStorage", locals.localStorage)
return return
) )
...@@ -691,6 +751,44 @@ describe("AnnotationController", -> ...@@ -691,6 +751,44 @@ describe("AnnotationController", ->
it("creates the directive without crashing", -> it("creates the directive without crashing", ->
createAnnotationDirective({}) createAnnotationDirective({})
) )
it("sets the permissions of new annotations from local storage", ->
{controller} = createAnnotationDirective({
localStorage: {
setItem: ->
getItem: (key) ->
if key == 'hypothesis.privacy'
return 'shared'
else
assert(false, "Wrong key requested from localStorage")
}
})
assert(controller.isShared())
)
it("sets the permissions of new annotations from local storage to private", ->
{controller} = createAnnotationDirective({
localStorage: {
setItem: ->
getItem: (key) ->
if key == 'hypothesis.privacy'
return 'private'
else
assert(false, "Wrong key requested from localStorage")
}
})
assert(controller.isPrivate())
)
it("defaults to shared if no locally cached visibility", ->
{controller} = createAnnotationDirective({
localStorage: {
setItem: ->
getItem: ->
}
})
assert(controller.isShared())
)
) )
describe("save", -> describe("save", ->
......
...@@ -63,16 +63,6 @@ describe('publishAnnotationBtn', function () { ...@@ -63,16 +63,6 @@ describe('publishAnnotationBtn', function () {
assert.equal(buttons[0].innerHTML, 'Post to Research Lab'); assert.equal(buttons[0].innerHTML, 'Post to Research Lab');
}); });
it('should default to "shared" as the privacy level for new annotations', function () {
fakeStorage = {};
element.link({
isNew: true
});
assert.deepEqual(fakeStorage, {
'hypothesis.privacy': 'shared'
});
});
it('should save when "Post..." is clicked', function () { it('should save when "Post..." is clicked', function () {
var savedSpy = sinon.spy(); var savedSpy = sinon.spy();
element.link({ element.link({
......
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