View
@@ -19,6 +19,7 @@
from h.models import _
from h.notification.models import Subscriptions
from h.resources import Application
import h.accounts.models
from . import schemas
from .events import LoginEvent, LogoutEvent
@@ -29,7 +30,8 @@ def ajax_form(request, result):
if isinstance(result, httpexceptions.HTTPRedirection):
request.response.headers.extend(result.headers)
result = {'status': 'okay'}
result = result.json
result["status"] = "okay"
elif isinstance(result, httpexceptions.HTTPError):
request.response.status_code = result.code
result = {'status': 'failure', 'reason': str(result)}
@@ -229,20 +231,52 @@ class AsyncRegisterController(RegisterController):
__view_mapper__ = AsyncFormViewMapper
def _emails_must_match_validator(form, value):
"""Raise colander.Invalid if "email" and "emailAgain" don't match."""
if value.get("email") != value.get("emailAgain"):
exc = colander.Invalid(form, "The emails must match")
exc["emailAgain"] = "The emails must match."
raise exc
class _InvalidEditProfileRequestError(Exception):
"""Raised if validating an edit user profile request fails."""
def __init__(self, errors):
super(_InvalidEditProfileRequestError, self).__init__()
self.errors = errors
def _validate_edit_profile_request(request):
"""Validate the given request using the EditProfileSchema.
:returns: if the request is valid returns a Deform "appstruct" with keys
``"username"``, ``"pwd"`` and ``"email"``
:rtype: dict
:raises _InvalidEditProfileRequestError: if the request is invalid
"""
schema = schemas.EditProfileSchema(
validator=_emails_must_match_validator).bind(request=request)
form = deform.Form(schema)
try:
return form.validate(request.POST.items())
except deform.ValidationFailure as err:
raise _InvalidEditProfileRequestError(errors=err.error.children)
@view_auth_defaults
@view_config(attr='edit_profile', route_name='edit_profile')
@view_config(attr='disable_user', route_name='disable_user')
@view_config(attr='profile', route_name='profile')
class ProfileController(horus.views.ProfileController):
def edit_profile(self):
request = self.request
schema = schemas.EditProfileSchema().bind(request=request)
form = deform.Form(schema)
try:
appstruct = form.validate(request.POST.items())
except deform.ValidationFailure as e:
return dict(errors=e.error.children)
appstruct = _validate_edit_profile_request(self.request)
except _InvalidEditProfileRequestError as err:
return dict(errors=err.errors)
username = appstruct['username']
pwd = appstruct['pwd']
@@ -252,7 +286,7 @@ def edit_profile(self):
# Update the subscriptions table
subs = json.loads(subscriptions)
if username == subs['uri']:
s = Subscriptions.get_by_id(request, subs['id'])
s = Subscriptions.get_by_id(self.request, subs['id'])
if s:
s.active = subs['active']
self.db.add(s)
@@ -270,10 +304,20 @@ def edit_profile(self):
)
# Password check
user = self.User.get_user(request, username, pwd)
user = self.User.get_user(self.request, username, pwd)
if user:
request.context = user
return super(ProfileController, self).edit_profile()
self.request.context = user
response = super(ProfileController, self).edit_profile()
# Add the user's email into the model dict that eventually gets
# returned to the browser. This is needed so that the edit profile
# forms can show the value of the user's current email.
if self.request.authenticated_userid:
user = h.accounts.models.User.get_by_id(
self.request, self.request.authenticated_userid)
response.json = {"model": {"email": user.email}}
return response
else:
return dict(errors=[{'pwd': _('Invalid password')}], code=401)
@@ -303,8 +347,10 @@ def disable_user(self):
def profile(self):
request = self.request
model = {}
userid = request.authenticated_userid
model = {}
if userid:
model["email"] = self.User.get_by_id(request, userid).email
if request.registry.feature('notification'):
model['subscriptions'] = Subscriptions.get_subscriptions_for_uri(
request,
View
@@ -100,10 +100,14 @@ config:
output: scripts/config.min.js
filters: uglifyjs
contents:
- output: scripts/config.js
- output: scripts/config.accounts.js
filters: browserify
contents:
- h:static/scripts/config/accounts.coffee
- output: scripts/config.via.js
filters: browserify
contents:
- h:static/scripts/config/via.coffee
# Application
View
@@ -4,12 +4,17 @@
class WSGIHandler(PyWSGIHandler, WebSocketWSGIHandler):
def finalize_headers(self):
# Middleware may yield from the empty upgrade response, confusing this
# method into sending "Transfer-Encoding: chunked" and, in turn, this
# confuses some strict WebSocket clients.
for name, value in self.response_headers:
if name == 'Upgrade' and value == 'websocket':
return
if self.environ.get('HTTP_UPGRADE') == 'websocket':
# Middleware, like Raven, may yield from the empty upgrade response,
# confusing this method into sending "Transfer-Encoding: chunked"
# and, in turn, this confuses some strict WebSocket clients.
if not hasattr(self.result, '__len__'):
self.result = list(self.result)
# ws4py 0.3.4 will try to pop the websocket from the environ
# even if it doesn't exist, causing a key error.
self.environ.setdefault('ws4py.websocket', None)
super(WSGIHandler, self).finalize_headers()
View
@@ -17,6 +17,7 @@ class AccountController
formModel = form.$name.slice(0, -4)
$scope[formModel] = {} # Reset form fields.
$scope.$broadcast 'formState', form.$name, 'success' # Update status btn
$scope.email = response.email
onDelete = (form, response) ->
identity.logout()
@@ -39,6 +40,7 @@ class AccountController
session.profile().$promise
.then (result) =>
$scope.subscriptions = result.subscriptions
$scope.email = result.email
# Data for each of the forms
$scope.editProfile = {}
@@ -78,6 +80,24 @@ class AccountController
promise = session.edit_profile(packet)
promise.$promise.then(successHandler, errorHandler)
$scope.changeEmailSubmit = (form) ->
formRespond(form)
return unless form.$valid
username = persona_filter auth.user
packet =
username: username
pwd: form.pwd.$modelValue
email: form.email.$modelValue
emailAgain: form.emailAgain.$modelValue
successHandler = angular.bind(null, onSuccess, form)
errorHandler = angular.bind(null, onError, form)
$scope.$broadcast 'formState', form.$name, 'loading' # Update status btn
promise = session.edit_profile(packet)
promise.$promise.then(successHandler, errorHandler)
$scope.updated = (index, form) ->
packet =
username: auth.user
View
@@ -233,3 +233,238 @@ describe 'h:AccountController', ->
assert.calledWith(fakeFlash.error,
'Sorry, we were unable to perform your request')
describe "h:AccountController", ->
before(->
try
# If this runs without error then the h module has already been defined
# by an earlier top-level describe() in this file.
angular.module("h")
catch error
# The h module hasn't been defined yet, so we need to define it
# (this happens when it.only() is used in this describe()).
angular.module("h", [])
require("../account-controller")
)
beforeEach module('h')
# Return the $controller service from Angular.
getControllerService = ->
$controller = null
inject((_$controller_) ->
$controller = _$controller_
)
return $controller
# Return the $rootScope service from Angular.
getRootScope = ->
$rootScope = null
inject((_$rootScope_) ->
$rootScope = _$rootScope_
)
return $rootScope
# Return a minimal stub version of h's session service.
getStubSession = ({profile, edit_profile}) ->
return {
profile: -> profile or {$promise: Promise.resolve({})}
edit_profile: edit_profile or -> {$promise: Promise.resolve({})}
}
# Return a minimal stub version of the object that AccountController's
# changeEmailSubmit() method receives when the user submits the changeEmailForm.
getStubChangeEmailForm = ({email, emailAgain, password}) ->
return {
$name: "changeEmailForm"
email:
$modelValue: email
$setValidity: ->
emailAgain:
$modelValue: emailAgain
$setValidity: ->
pwd:
$modelValue: password
$setValidity: ->
$valid: true
$setPristine: ->
$setValidity: ->
}
# Return an AccountController instance and stub services.
createAccountController = ({$scope, $filter, auth, flash, formRespond,
identity, session}) ->
locals = {
$scope: $scope or getRootScope().$new()
$filter: $filter or -> -> {}
auth: auth or {}
flash: flash or {}
formRespond: formRespond or ->
identity: identity or {}
session: session or getStubSession({})
}
locals["ctrl"] = getControllerService()("AccountController", locals)
return locals
###
The controller sets $scope.email to the user's current email address on
controller initialization. The templates use this for the placeholder
value of the email input fields.
###
it "adds the current email address to the scope when initialized", ->
# The controller actually calls session.profile() on init which returns
# a promise, and when that promise resolves it uses the value to set
# $scope.email. So we need to stub that promise here.
profilePromise = Promise.resolve({
email: "test_user@test_email.com"
})
{$scope} = createAccountController(
session: {profile: -> {$promise: profilePromise}})
profilePromise.then(->
assert $scope.email == "test_user@test_email.com"
)
describe "changeEmail", ->
it "calls sesson.edit_profile() with the right data on form submission", ->
new_email_addr = "new_email_address@test.com"
# Stub the session.edit_profile() function.
edit_profile = sinon.stub()
edit_profile.returns({$promise: Promise.resolve({})})
{$scope} = createAccountController(
session: getStubSession(edit_profile: edit_profile)
# Simulate a logged-in user with username "joeuser"
$filter: -> -> "joeuser")
form = getStubChangeEmailForm(
email: new_email_addr, emailAgain: new_email_addr, password: "pass")
$scope.changeEmailSubmit(form).then(->
assert edit_profile.calledWithExactly({
username: "joeuser"
pwd: "pass"
email: new_email_addr
emailAgain: new_email_addr
})
)
it "updates placeholder after successfully changing the email address", ->
new_email_addr = "new_email_address@test.com"
{$scope} = createAccountController(
# AccountController expects session.edit_profile() to respond with the
# newly saved email address.
session: getStubSession(
edit_profile: -> {
$promise: Promise.resolve({email: new_email_addr})
}
)
)
form = getStubChangeEmailForm(
email: new_email_addr, emailAgain: new_email_addr, password: "pass")
$scope.changeEmailSubmit(form).then(->
assert $scope.email == new_email_addr
)
it "shows an error if the emails don't match", ->
server_response = {
status: 400,
statusText: "Bad Request"
data:
errors:
emailAgain: "The emails must match."
}
{$scope} = createAccountController(
formRespond: require("../../form-respond")()
session: getStubSession(
edit_profile: -> {$promise: Promise.reject(server_response)}
)
)
form = getStubChangeEmailForm(
email: "my_new_email_address@yahoo.com"
emailAgain: "a_different_email_address@bluebottle.com"
pwd: "pass")
$scope.changeEmailSubmit(form).then(->
assert form.emailAgain.responseErrorMessage == "The emails must match."
)
it "broadcasts 'formState' 'changeEmailForm' 'loading' on submit", ->
{$scope} = createAccountController({})
$scope.$broadcast = sinon.stub()
form = getStubChangeEmailForm(
email: "new_email_address@test.com",
emailAgain: "new_email_address@test.com", password: "pass")
$scope.changeEmailSubmit(form)
assert $scope.$broadcast.calledWithExactly(
"formState", "changeEmailForm", "loading")
it "broadcasts 'formState' 'changeEmailForm' 'success' on success", ->
{$scope} = createAccountController({})
$scope.$broadcast = sinon.stub()
form = getStubChangeEmailForm(
email: "new_email_address@test.com",
emailAgain: "new_email_address@test.com", password: "pass")
$scope.changeEmailSubmit(form).then(->
assert $scope.$broadcast.calledWithExactly(
"formState", "changeEmailForm", "success")
)
it "broadcasts 'formState' 'changeEmailForm' '' on error", ->
{$scope} = createAccountController(
flash: {error: ->}
session: getStubSession(
edit_profile: -> {$promise: Promise.reject({data: {}})}
)
)
$scope.$broadcast = sinon.stub()
form = getStubChangeEmailForm(
email: "new_email_address@test.com",
emailAgain: "new_email_address@test.com", password: "pass")
$scope.changeEmailSubmit(form).then(->
assert $scope.$broadcast.calledWithExactly(
"formState", "changeEmailForm", "")
)
it "shows an error if the password is wrong", ->
# Mock of the server response you get when you enter the wrong password.
server_response = {
data:
errors:
pwd: "Invalid password"
status: 401
statusText: "Unauthorized"
}
{$scope} = createAccountController(
formRespond: require("../../form-respond")()
session: getStubSession(
edit_profile: -> {$promise: Promise.reject(server_response)}
)
)
form = getStubChangeEmailForm(
email: "new_email_address@test.com",
emailAgain: "new_email_address@test.com", password: "pass")
$scope.changeEmailSubmit(form).then(->
assert form.pwd.responseErrorMessage == "Invalid password"
)
View
@@ -43,8 +43,17 @@ module.exports = class AppController
applyUpdates(action, payload)
$scope.$digest()
# App dialogs
$scope.accountDialog = visible: false
$scope.viaLinkDialog = visible: false
$scope.groupDialog = visible: false
oncancel = ->
$scope.dialog.visible = false
$scope.accountDialog.visible = false
# Check to see if we are on the stream page.
if $window.top is $window
$scope.isStream = true
cleanupAnnotations = ->
# Clean up any annotations that need to be unloaded.
@@ -68,7 +77,7 @@ module.exports = class AppController
if isFirstRun and not (newVal or oldVal)
$scope.login()
else
$scope.dialog.visible = false
$scope.accountDialog.visible = false
# Update any edits in progress.
for draft in drafts.all()
@@ -85,12 +94,12 @@ module.exports = class AppController
$route.reload()
$scope.login = ->
$scope.dialog.visible = true
$scope.accountDialog.visible = true
identity.request {oncancel}
$scope.logout = ->
return unless drafts.discard()
$scope.dialog.visible = false
$scope.accountDialog.visible = false
identity.logout()
$scope.loadMore = (number) ->
@@ -100,8 +109,6 @@ module.exports = class AppController
$scope.search.query = ''
annotationUI.clearSelectedAnnotations()
$scope.dialog = visible: false
$scope.search =
query: $location.search()['q']
View
@@ -111,6 +111,9 @@ module.exports = angular.module('h', [
.directive('spinner', require('./directive/spinner'))
.directive('tabbable', require('./directive/tabbable'))
.directive('tabReveal', require('./directive/tab-reveal'))
.directive('viaLinkDialog', require('./directive/via-dialog'))
.directive('viewcontrol', require('./directive/viewcontrol'))
.directive('newgroupdialog', require('./directive/newgroup_dialog'))
.filter('converter', require('./filter/converter'))
.filter('moment', require('./filter/moment'))
View
@@ -0,0 +1,4 @@
angular = require('angular')
angular.module('h')
.value('via', url: 'https://via.hypothes.is/')
View
@@ -182,15 +182,33 @@ AnnotationController = [
# @name annotation.AnnotationController#save
# @description Saves any edits and returns to the viewer.
###
this.save = ->
$scope.published = true
this.save = (published = true) ->
unless model.user or model.deleted
return flash.info('Please sign in to save your annotations.')
unless validate(@annotation)
return flash.info('Please add text or a tag before publishing.')
if $scope.level.name != ('Public' or 'Only Me')
@annotation.tags.push { text:"group:" + $scope.level.text }
if published
$scope.published = true
keep = []
for tag in @annotation.tags
if tag.text == "group:draft"
continue
else
keep.push tag
@annotation.tags = keep
else
$scope.published = false
@annotation.tags.push { text:"group:draft"}
# Update stored tags with the new tags of this annotation
newTags = @annotation.tags.filter (tag) ->
tag.text not in (model.tags or [])
tags.store(newTags)
switch @action
@@ -279,6 +297,14 @@ AnnotationController = [
if @document.title.length > 30
@document.title = @document.title[0..29] + '…'
for tag in model.tags
str = "group:"
re = new RegExp(str, "g");
if tag == "group:draft"
$scope.published = false
else if re.test(tag)
$scope.groupAnno = tag.substring(str.length, tag.length)
# Form the tags for ngTagsInput.
@annotation.tags = ({text} for text in (model.tags or []))
@@ -293,6 +319,11 @@ AnnotationController = [
else
@showDiff = undefined
$scope.filtertags = (tag) ->
str = "group:"
re = new RegExp(str, "g");
!re.test(tag.text)
updateTimestamp = (repeat=false) =>
@timestamp = time.toFuzzyString model.updated
fuzzyUpdate = time.nextFuzzyUpdate model.updated
@@ -311,6 +342,12 @@ AnnotationController = [
updateTimestamp = angular.noop
drafts.remove model
$scope.$watch (-> $rootScope.level), (level) =>
$scope.level = level
$scope.$watch (-> $rootScope.socialview), (socialview) =>
$scope.currentView = socialview
# Watch the model.
# XXX: TODO: don't clobber the view when collaborating
$scope.$watch (-> model), (model, old) =>
View
@@ -0,0 +1,52 @@
###*
# @ngdoc directive
# @name newgroup Dialog
# @restrict A
# @description The dialog that leads to the creation of a new group.
###
module.exports = ['$timeout', '$rootScope', 'localStorage', ($timeout, $rootScope, localStorage) ->
link: (scope, elem, attrs, ctrl) ->
scope.$watch (-> scope.groupDialog.visible), (visible) ->
if visible
$timeout (-> elem.find('#nameGroup').focus().select()), 0, false
scope.$watch (-> scope.showLink), (visible) ->
if visible
$timeout (-> elem.find('#copyLink').focus().select()), 0, false
$rootScope.showGroupLink = false
scope.$watch (-> $rootScope.showGroupLink), (showGroupLink) ->
if showGroupLink[0]
scope.groupDialog.visible = true
scope.showLink = true
scope.titleText = "Share Group"
scope.groupLink = "https://hypothes.is/g/102498/" + showGroupLink[1].name
else
scope.titleText = "New Group"
scope.groupName = "groupName"
scope.showLink = false
scope.titleText = "New Group"
scope.newgroup = ->
$rootScope.socialview.selected = false
for view in $rootScope.views
if view.name == $rootScope.socialview.name
view.selected = false
$rootScope.views.push({name:scope.groupName, icon:'h-icon-group', selected:true, editable:true, link:true})
if not localStorage.getItem 'group1.name'
localStorage.setItem 'group1.name', scope.groupName
localStorage.setItem 'group1.icon', 'h-icon-group'
else if not localStorage.getItem 'group2.name'
localStorage.setItem 'group2.name', scope.groupName
localStorage.setItem 'group2.icon', 'h-icon-group'
else
localStorage.setItem 'group3.name', scope.groupName
localStorage.setItem 'group3.icon', 'h-icon-group'
$rootScope.socialview = {name:scope.groupName, icon:'h-icon-group', selected:true, editable:true, link:true}
scope.showLink = true
scope.groupLink = "https://hypothes.is/g/102498/" + scope.groupName
controller: 'AppController'
templateUrl: 'newgroup_dialog.html'
]
View
@@ -1,21 +1,24 @@
module.exports = ['localStorage', 'permissions', (localStorage, permissions) ->
module.exports = ['localStorage', 'permissions', '$rootScope', (localStorage, permissions, $rootScope) ->
VISIBILITY_KEY ='hypothesis.visibility'
VISIBILITY_PUBLIC = 'public'
VISIBILITY_PRIVATE = 'private'
VISIBILITY_PUBLIC = 'Public'
VISIBILITY_PRIVATE = 'Only Me'
viewnamelist = ['All', 'Public', 'Only Me']
levels = [
{name: VISIBILITY_PUBLIC, text: 'Public'}
{name: VISIBILITY_PRIVATE, text: 'Only Me'}
$rootScope.levels = [
{name: VISIBILITY_PUBLIC, text: 'Public', icon: 'h-icon-public'}
{name: VISIBILITY_PRIVATE, text: 'Only Me', icon: 'h-icon-lock'}
]
getLevel = (name) ->
for level in levels
for level in $rootScope.levels
if level.name == name
return level
undefined
isPublic = (level) -> level == VISIBILITY_PUBLIC
isPrivate = (level) -> level == VISIBILITY_PRIVATE
link: (scope, elem, attrs, controller) ->
return unless controller?
@@ -30,10 +33,20 @@ module.exports = ['localStorage', 'permissions', (localStorage, permissions) ->
controller.$parsers.push (privacy) ->
return unless privacy?
if isPublic(privacy.name)
newPermissions = permissions.public()
if $rootScope.socialview.name != 'All'
if $rootScope.socialview.name == 'Only Me'
newPermissions = permissions.private()
else if scope.draft
console.log "Ran."
newPermissions = permissions.private()
$rootScope.draft = false
else
newPermissions = permissions.public()
else
newPermissions = permissions.private()
if isPrivate(privacy.name)
newPermissions = permissions.private()
else
newPermissions = permissions.public()
# Cannot change the $modelValue into a new object
# Just update its properties
@@ -44,19 +57,35 @@ module.exports = ['localStorage', 'permissions', (localStorage, permissions) ->
controller.$render = ->
unless controller.$modelValue.read?.length
name = localStorage.getItem VISIBILITY_KEY
name ?= VISIBILITY_PUBLIC
if $rootScope.socialview.name == 'All'
name = localStorage.getItem VISIBILITY_KEY
name ?= VISIBILITY_PUBLIC
else if $rootScope.socialview.name == 'Public'
name = VISIBILITY_PUBLIC
else if $rootScope.socialview.name == 'Only Me'
name = VISIBILITY_PRIVATE
else
name = $rootScope.socialview.name
level = getLevel(name)
controller.$setViewValue level
$rootScope.level = controller.$viewValue
scope.level = controller.$viewValue
scope.levels = levels
scope.levels = $rootScope.levels
scope.setLevel = (level) ->
localStorage.setItem VISIBILITY_KEY, level.name
controller.$setViewValue level
controller.$render()
scope.isPublic = isPublic
scope.isPrivate = isPrivate
scope.$watch (-> $rootScope.draft), (draft) =>
scope.draft = draft
for view in $rootScope.views
if view.name not in viewnamelist
viewnamelist.push view.name
$rootScope.levels.push {name: view.name, text: view.name, icon:'h-icon-group'}
require: '?ngModel'
restrict: 'E'
View
@@ -0,0 +1,41 @@
###*
# @ngdoc directive
# @name viaLinkDialog
# @restrict A
# @description The dialog that generates a via link to the page h is currently
# loaded on.
###
module.exports = ['$timeout', 'crossframe', '$rootScope', (
$timeout, crossframe, $rootScope) ->
link: (scope, elem, attrs, ctrl) ->
scope.viaPageLink = ''
## Watch viaLinkVisible: when it changes to true, focus input and selection.
scope.$watch (-> scope.viaLinkDialog.visible), (visible) ->
if visible
$timeout (-> elem.find('#via').focus().select()), 0, false
scope.shareGroup = false
scope.nameofgroup = $rootScope.socialview.name
## Make this a via link.
scope.shareGroupLink = 'https://via.hypothes.is/' + '?groupid=34213&name=' + $rootScope.socialview.name + e?
scope.$watch (-> $rootScope.socialview.name), (socialview) ->
scope.nameofgroup = $rootScope.socialview.name
if socialview != 'All' and socialview != 'Public' and socialview != 'Only Me'
console.log socialview
scope.shareGroup = true
scope.shareGroupLink = 'https://via.hypothes.is/' + '?groupid=34213&name=' + $rootScope.socialview.name + e?
else
scope.shareGroup = false
scope.$watchCollection (-> crossframe.providers), ->
if crossframe.providers?.length
# XXX: Consider multiple providers in the future
p = crossframe.providers[0]
if p.entities?.length
e = p.entities[0]
scope.viaPageLink = 'https://via.hypothes.is/' + e
controller: 'AppController'
templateUrl: 'via_dialog.html'
]
View
@@ -0,0 +1,46 @@
###*
# @ngdoc directive
# @name View Control
# @restrict A
# @description
###
module.exports = [ '$rootScope', 'localStorage', ($rootScope, localStorage) ->
link: (scope, elem, attrs, ctrl) ->
scope.editgroups = true
scope.removeGroup = (group) ->
if confirm "Do you want to remove the " + group.name + " group?"
keep = []
for view in $rootScope.views
if view.name == group.name
continue
else
keep.push view
$rootScope.views = keep
ls1 = localStorage.getItem 'group1.name'
ls2 = localStorage.getItem 'group2.name'
ls3 = localStorage.getItem 'group3.name'
if ls1 == group.name
localStorage.removeItem 'group1.name'
if ls2 == group.name
localStorage.removeItem 'group2.name'
if ls3 == group.name
localStorage.removeItem 'group3.name'
if $rootScope.socialview.name == group.name
$rootScope.socialview == $rootScope.views[0]
alert "Unsuscribed from group."
else
return
scope.showGroupLink = (view) ->
$rootScope.showGroupLink = [true, view]
scope.select = (selectedview) ->
selectedview.selected = true
$rootScope.socialview.selected = false
$rootScope.socialview = selectedview
controller: 'WidgetController'
restrict: 'ACE'
templateUrl: 'viewcontrol.html'
]
View
@@ -117,7 +117,7 @@ describe 'AppController', ->
it 'does not show login form for logged in users', ->
createController()
assert.isFalse($scope.dialog.visible)
assert.isFalse($scope.accountDialog.visible)
describe 'applyUpdate', ->
View
@@ -3,16 +3,17 @@ angular = require('angular')
module.exports = class WidgetController
this.$inject = [
'$scope', 'annotationUI', 'crossframe', 'annotationMapper',
'streamer', 'streamFilter', 'store'
'$rootScope', '$scope', 'annotationUI', 'auth', 'crossframe', 'annotationMapper',
'localStorage', 'streamer', 'streamFilter', 'store'
]
constructor: (
$scope, annotationUI, crossframe, annotationMapper,
streamer, streamFilter, store
$rootScope, $scope, annotationUI, auth, crossframe, annotationMapper,
localStorage, streamer, streamFilter, store
) ->
# Tells the view that these annotations are embedded into the owner doc
$scope.isEmbedded = true
$scope.isStream = true
$scope.auth = auth
@chunkSize = 200
loaded = []
@@ -73,3 +74,53 @@ module.exports = class WidgetController
!!($scope.focusedAnnotations ? {})[annotation?.$$tag]
$scope.notOrphan = (container) -> !container?.message?.$orphan
$scope.filterView = (container) ->
if not container?.message?.permissions?.read?
# If an annnoation is being edited it should show up in any view.
return true
else if $rootScope.socialview.name == 'All'
# Show everything for All.
return true
else if $rootScope.socialview.name == 'Public'
# Filter out group and private annotations annotations.
str1 = "group:"
re1 = new RegExp(str1, "g");
not (re1.test(container?.message?.tags) or (container?.message?.permissions?.read?[0] != 'group:__world__'))
else if $rootScope.socialview.name == 'Only Me'
# Show private annotations.
container?.message?.permissions?.read?[0] != 'group:__world__'
else if $rootScope.socialview.name != ('All' or 'Public' or 'Only Me')
# Show group annotations.
str2 = "group:" + $rootScope.socialview.name
re2 = new RegExp(str2, "g")
# if $scope.auth.user == null
# false
# else
# re2.test(container?.message?.tags)
re2.test(container?.message?.tags)
$rootScope.views = [
{name:'All', icon:'h-icon-public', selected:false, editable:false, link:false}
{name:'Public', icon:'h-icon-public', selected:false, editable:false, link:false}
{name:'Only Me', icon:'h-icon-lock', selected:false, editable:false, link:false}
]
if localStorage.getItem 'group1.name'
groupname = localStorage.getItem 'group1.name'
groupicon = localStorage.getItem 'group1.icon'
$rootScope.views.push {name:groupname, icon:groupicon, selected:false, editable:true, link:true}
if localStorage.getItem 'group2.name'
groupname2 = localStorage.getItem 'group2.name'
groupicon2 = localStorage.getItem 'group2.icon'
$rootScope.views.push {name:groupname2, icon:groupicon2, selected:false, editable:true, link:true}
if localStorage.getItem 'group3.name'
groupname2 = localStorage.getItem 'group3.name'
groupicon2 = localStorage.getItem 'group3.icon'
$rootScope.views.push {name:groupname2, icon:groupicon2, selected:false, editable:true, link:true}
$rootScope.socialview = $rootScope.views[0]
View
@@ -88,7 +88,7 @@
}
@include icons {
font-size: 16px;
font-size: 14px;
vertical-align: -3px;
margin-right: 1px;
}
View
@@ -131,23 +131,22 @@ ol {
border-radius: 2px;
}
.dropdown-toggle {
.provider {
color: $gray-light;
display: none;
}
&:hover {
.provider {
display: inline;
}
}
}
.dropdown.open .provider {
display: inline;
}
}
.viewcontrol {
margin-right: 1em;
font-family: $sans-font-family;
&:after {
margin-left: 1.5em;
content: "/";
color: #777;
}
}
.masthead {
.masthead-heading {
margin: 0;
View
@@ -112,6 +112,27 @@ html {
}
// Share this page /////////////////////
.share-via-link {
top: 1px;
left: 8px;
position: relative;
cursor: pointer;
color: $gray-light;
float: left;
margin-right: 8px;
&:hover {
color: $gray-dark;
}
}
.share-links {
a {
font-size: 1.5em;
padding: .3em;
}
}
//FORM RELATED////////////////////////////////
.form-horizontal {
@@ -130,7 +151,6 @@ html {
.form-vertical {
select, textarea, input, button {
display: block;
// margin-top: .75em;
}
}
@@ -166,20 +186,28 @@ html {
}
}
.privacy {
.dropdown-menu {
left: -50px !important;
}
}
.dropdown-menu {
@include rotateX(90deg);
background: $white;
border: solid 1px $gray-lighter;
margin-top: .8em;
top: 100%;
float: left;
opacity: 0;
pointer-events: none;
position: absolute;
margin-top: 1px;
z-index: 2;
min-width: 130px;
width: 100%;
li:not(.ng-hide) {
text-align: left;
padding: 5px 10px;
cursor: pointer;
a {
display: block;
@@ -195,22 +223,10 @@ html {
&.selected {
color: black;
font-weight: 600;
line-height: 1;
&:before {
font-size: .7em;
}
}
&.inactive {
font-weight: 400;
color: $gray-lighter;
cursor: default;
&:hover {
color: $gray-lighter;
}
& * {
cursor: default;
}
}
& + li {
border-top: dotted 1px $gray-lighter;
@@ -221,55 +237,28 @@ html {
color: inherit;
}
// These psuedo-elements add the speech bubble tail / triangle.
&:before, &:after {
// http://www.red-team-design.com/css-diagonal-borders-still-not-rendering-properly-on-firefox
@include scale(.9999);
border-color: rgba(0, 0, 0, 0);
border-style: solid;
border-width: 0 7px 6px 7px;
content: '';
position: absolute;
height: 0;
left: 0;
width: 0;
}
&:before {
border-bottom-color: $gray-lighter;
top: -7px;
}
&:after {
border-bottom-color: $white;
top: -6px;
z-index: 3;
}
// Aligns the dropdown menu to right
&.pull-right {
right: 0;
left: auto;
text-align: right;
// Align the tail
&:before, &:after {
left: auto;
right: 0;
}
}
}
.open {
& > .dropdown-menu {
@include smallshadow;
@include rotateX(0);
opacity: 1;
pointer-events: auto;
}
}
.viewcontrol {
.dropdown-menu {
min-width: 225px;
}
}
//TABS////////////////////////////////
.nav-tabs {
margin-bottom: .7em;
View
@@ -3,13 +3,14 @@
.simple-search {
overflow: hidden;
max-width: 50%;
}
.simple-search-form {
@include clearfix;
position: relative;
padding: 0 1.5385em;
color: $gray;
color: $gray-darker;
}
.simple-search-form * {
@@ -18,19 +19,18 @@
.simple-search-icon {
position: absolute;
left: 0;
top: 0;
left: 5px;
top: -1px;
}
:not(:focus) ~ .simple-search-icon {
color: $gray-lighter;
color: $gray-light;
}
input.simple-search-input {
float: left;
outline: none;
color: $text-color;
width: 100%;
border: none;
padding: 0;
View
@@ -44,6 +44,7 @@ body {
max-width: $break-tablet;
padding: 0 4em;
position: relative;
height: 30px;
@include respond-to(wide-handhelds handhelds) {
padding-left: 0;
View
Binary file not shown.
View

Large diffs are not rendered by default.

Oops, something went wrong.
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -12,11 +12,13 @@
<div class="dropdown pull-right user-picker">
<span role="button"
class="dropdown-toggle"
data-toggle="dropdown">{% raw %}{{ auth.user|persona }}{% endraw %}<!--
--><span class="provider" ng-show="auth.user">/{% raw %}{{ auth.user|persona:'provider' }}{% endraw %}</span><!--
--><i class="h-icon-arrow-drop-down"></i></span>
title="{% raw %}{{ auth.user|persona:'provider' }}{% endraw %}"
data-toggle="dropdown">
{% raw %}{{ auth.user|persona }}{% endraw %}
<i class="h-icon-arrow-drop-down"></i>
</span>
<ul class="dropdown-menu pull-right" role="menu">
<li ng-show="auth.user"><a href="" ng-click="dialog.visible='true'">Account</a></li>
<li ng-show="auth.user"><a href="" ng-click="accountDialog.visible='true'">Account</a></li>
<li><a href="mailto:support@hypothes.is">Feedback</a></li>
<li><a href="/docs/help" target="_blank">Help</a></li>
<li ng-show="auth.user"><a href="/stream?q=user:{% raw %}{{ auth.user|persona }}{% endraw %}"
@@ -28,6 +30,18 @@
ng-click="login()"
ng-switch-when="null">Sign in</a>
<!-- Share this view -->
<span class="share-via-link">
<i class="h-icon-share"
title="Share this view"
ng-click="viaLinkDialog.visible = !viaLinkDialog.visible"></i>
</span>
<!-- / Share this view -->
<!-- View Control -->
<div class="dropdown pull-right viewcontrol"></div>
<!-- / View Control -->
<!-- Searchbar -->
<div class="simple-search"
query="search.query"
@@ -40,13 +54,13 @@
<!-- Wrapper -->
<div id="wrapper" whenscrolled="loadMore(10)">
<!-- Dialog -->
<div class="content ng-cloak" ng-if="dialog.visible">
<!-- Account Dialog -->
<div class="content ng-cloak" ng-if="accountDialog.visible">
<div id="dialog" class="sheet">
<i class="close h-icon-close"
role="button"
title="Close"
ng-click="dialog.visible = false"></i>
ng-click="accountDialog.visible = false"></i>
<div ng-if="auth.user">
{% block settings %}
<div class="tabbable"
@@ -66,7 +80,15 @@
</div>
</div>
</div>
<!-- / Dialog -->
<!-- / Account Dialog -->
<!-- Share View Dialog -->
<div via-link-dialog="" ng-show="viaLinkDialog.visible"></div>
<!-- / Share View Dialog -->
<!-- Group Dialog -->
<div newgroupdialog="" ng-show="groupDialog.visible"></div>
<!-- / Group Dialog -->
<!-- Angular view -->
<main class="content" ng-view=""></main>
@@ -84,12 +106,21 @@
<script type="text/ng-template" id="privacy.html">
{{ include_raw("h:templates/client/privacy.html") }}
</script>
<script type="text/ng-template" id="via_dialog.html">
{{ include_raw("h:templates/client/via_dialog.html") }}
</script>
<script type="text/ng-template" id="viewer.html">
{{ include_raw("h:templates/client/viewer.html") }}
</script>
<script type="text/ng-template" id="thread.html">
{{ include_raw("h:templates/client/thread.html") }}
</script>
<script type="text/ng-template" id="viewcontrol.html">
{{ include_raw("h:templates/client/viewcontrol.html") }}
</script>
<script type="text/ng-template" id="newgroup_dialog.html">
{{ include_raw("h:templates/client/newgroup_dialog.html") }}
</script>
{% endblock %}
{% block scripts %}
View
@@ -8,9 +8,25 @@
target="_blank"
ng-href="{{vm.baseURI}}u/{{vm.annotation.user}}"
>{{vm.annotation.user | persona}}</a>
<i class="h-icon-lock" ng-show="vm.isPrivate() && !vm.editing"></i>
<i class="h-icon-border-color" ng-show="vm.isHighlight() && !vm.editing"></i>
<i class="h-icon-insert-comment" ng-show="vm.isComment()"></i>
<i class="h-icon-insert-comment" title="General comment" ng-show="vm.isComment()"></i>
<i class="h-icon-lock" title="This annotation is visible only to you." ng-show="vm.isPrivate() && !vm.editing"></i>
<span class="small" ng-show="!vm.isPrivate() && !vm.editing && published">to&nbsp;<a ng-href="{{vm.baseURI}}/stream" target="_blank" class="annotation-timestamp" ng-show="!vm.isPrivate() && groupAnno && !vm.editing"><i class="h-icon-group" title="{{groupAnno}}"></i>{{groupAnno}}</a></span>
<span class="small" ng-show="!published && !vm.editing"><i class="h-icon-lock"></i> Draft</span>
<span class="small annotation-timestamp" ng-show="!vm.isPrivate() && !vm.editing && !groupAnno" style="margin-left:-3px"><i class="h-icon-public" title="Public annotation" ng-show="!vm.isPrivate() && !vm.editing && !groupAnno"></i> Public</span>
<span class="small" ng-show="vm.editing && currentView.name != 'All'">to&nbsp;<i class="{{currentView.icon}}" title="{{currentView.name}} annotation"></i> {{currentView.name}}</span>
<privacy ng-click="$event.stopPropagation()"
ng-show="currentView.name == 'All'"
ng-if="vm.editing && action != 'delete'"
ng-model="vm.annotation.permissions"
user="{{vm.annotation.user}}"
class="dropdown privacy pull-right"
name="privacy" />
<span class="annotation-citation"
ng-if="!vm.embedded"
ng-show="vm.document.title">
@@ -20,25 +36,17 @@
ng-show="vm.document.domain != vm.document.title"
>({{vm.document.domain}})</span>
</span>
<!-- Editing controls -->
<aside class="pull-right" ng-if="vm.editing">
<privacy ng-click="$event.stopPropagation()"
ng-if="vm.annotation.permissions && vm.editing && action != 'delete'"
ng-model="vm.annotation.permissions"
user="{{vm.annotation.user}}"
class="dropdown privacy pull-right"
name="privacy" />
</aside>
<!-- / Editing controls -->
</span>
<!-- Timestamp -->
<a class="annotation-timestamp small pull-right"
target="_blank"
title="{{vm.annotation.updated | moment:'LLLL'}}"
ng-if="!vm.editing && vm.annotation.updated"
ng-href="{{vm.baseURI}}a/{{vm.annotation.id}}"
>{{vm.timestamp}}</a>
<span class="small pull-right">
<a class="annotation-timestamp"
target="_blank"
title="{{vm.annotation.updated | moment:'LLLL'}}"
ng-if="!vm.editing && vm.annotation.updated"
ng-href="{{vm.baseURI}}a/{{vm.annotation.id}}"
>{{vm.timestamp}}</a>
</span>
</header>
<!-- Excerpts -->
@@ -90,26 +98,35 @@
<div class="annotation-section tags tags-read-only"
ng-if="vm.annotation.tags.length && !vm.editing">
<ul class="tag-list">
<li class="tag-item" ng-repeat="tag in vm.annotation.tags">
<li class="tag-item" ng-repeat="tag in vm.annotation.tags | filter:filtertags">
<a href="/stream?q=tag:'{{tag.text|urlencode}}'" target="_blank">{{tag.text}}</a>
</li>
</ul>
</div>
<!-- / Tags -->
<div class="annotation-section small" ng-if="vm.editing" ng-show="currentView.name == 'All'">
<p ng-show="level.text == 'Only Me'"><i class="h-icon-lock"></i> This annotation is viewable only to you.</p>
<p ng-show="(level.text != 'Public') && (level.text != 'Only Me')"><i class="h-icon-group"></i> This annotation is viewable only to people who follow the link to the group.</p>
<p ng-show="level.text == 'Public'"><i class="h-icon-public"></i> This annotation is viewable to everyone.</p>
</div>
<div class="annotation-section form-actions"
ng-if="vm.editing"
ng-switch="vm.action">
<div class="form-actions-buttons form-actions-left">
<button ng-switch-when="edit"
<button ng-switch-when="edit || currentView.name == 'Only Me'"
ng-click="vm.save()"
class="btn"><i class="h-icon-check btn-icon"></i> Save</button>
<button ng-switch-when="delete"
ng-click="vm.save()"
class="btn"><i class="h-icon-check btn-icon"></i> Delete</button>
<button ng-switch-default
ng-click="vm.save()"
class="btn"><i class="h-icon-check btn-icon"></i> Save</button>
ng-click="vm.save(true)"
class="btn"><i class="h-icon-public btn-icon"></i> Publish</button>
<button ng-switch-default
ng-click="vm.save(false)"
class="btn btn-clean"><i class="h-icon-check btn-icon"></i> Save Draft</button>
<button class="btn btn-clean"
ng-click="vm.revert()"
><i class="h-icon-cancel btn-icon"></i> Cancel</button>
@@ -119,7 +136,8 @@
<div class="annotation-section annotation-license" ng-if="vm.editing">
<a href="http://creativecommons.org/publicdomain/zero/1.0/"
title="View more information about the Creative Commons Public Domain license"
target="_blank">
target="_blank"
class="small">
<i class="h-icon-cc-logo"></i><i class="h-icon-cc-zero"></i>
Annotations can be freely reused by anyone for any purpose.
</a>
View
@@ -0,0 +1,70 @@
<div id="dialog" class="sheet content">
<i class="close h-icon-close"
role="button"
title="Close"
ng-click="groupDialog.visible = false; showLink = false;"></i>
<div class="form-vertical tabbable">
<div class="form tab-pane" data-title="{{titleText}}">
<div ng-hide="showLink">
<form name="newgroup"
ng-submit="newgroup()"
form-validate>
<div class="form-field">
<p class="form-description">Create a new group and annotate collaboratively.</p>
<input id="nameGroup"
name="groupName"
class="form-input"
type="text"
ng-minlength="3"
ng-maxlength="30"
ng-model="groupName"></input>
<ul class="form-error-list">
<li class="form-error" ng-show="newgroup.groupName.$error.required">Please enter your current password.</li>
<li class="form-error" ng-show="newgroup.groupName.$error.minlength">The group name must be at least 3 characters.</li>
<li class="form-error" ng-show="newgroup.groupName.$error.maxlength">Max group name length is 30 characters.</li>
</ul>
</div>
<div class="form-actions">
<div class="form-actions-buttons">
<button class="btn btn-primary"
type="submit"
status-button="newgroup">
Create Group</button>
</div>
</div>
</form>
</div>
<!-- Get the link -->
<div class="form-field" ng-show="showLink">
<p class="form-description">Give the link to anyone you would like to invite into the group.</p>
<input id="copyLink"
class="form-input"
type="text"
ng-value="groupLink"
readonly></input>
<div class="form-actions">
<div class="form-actions-message">
<p class="share-links">
<a href="//twitter.com/intent/tweet?url={{groupLink}}"
target="_blank"
title="Tweet link"
class="h-icon-twitter"></a>
<a href="//www.facebook.com/sharer/sharer.php?u={{groupLink}}"
target="_blank"
title="Share on Facebook"
class="h-icon-facebook"></a>
<a href="//plus.google.com/share?url={{groupLink}}"
target="_blank"
title="Post on Google Plus"
class="h-icon-google-plus"></a>
<a href="mailto:?subject=Let's%20Annotate&amp;body={{groupLink}}"
title="Share via email"
class="h-icon-mail"></a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
View
@@ -3,16 +3,14 @@
role="button"
class="dropdown-toggle"
data-toggle="dropdown">
<i class="small" ng-class="{'h-icon-public': isPublic(level.name),
'h-icon-lock': !isPublic(level.name)}"></i>
<i ng-class="level.icon"></i>
{{level.text}}
<i class="h-icon-arrow-drop-down"></i>
</span>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="level in levels" ng-click="setLevel(level)">
<a href="">
<i class="small" ng-class="{'h-icon-public': isPublic(level.name),
'h-icon-lock': !isPublic(level.name)}"></i>
<i class="small" ng-class="level.icon"></i>
{{level.text}}
</a>
</li>
View
@@ -1,4 +1,52 @@
<div class="tab-pane" title="Account">
<form class="account-form form"
name="changeEmailForm"
ng-submit="changeEmailSubmit(changeEmailForm)"
novalidate form-validate>
<h2 class="form-heading"><span>Change Your Email Address</span></h2>
<p class="form-description">Your current email address is: <strong ng-bind="email"></strong>.</p>
<div class="form-field">
<label class="form-label" for="field-email">New Email Address:</label>
<input id="field-email" class="form-input" type="email" name="email" required ng-model="changeEmail.email" />
<ul class="form-error-list">
<li class="form-error" ng-show="changeEmailForm.email.$error.required">Please enter your new email address.</li>
<li class="form-error" ng-show="changeEmailForm.email.$error.email">Please enter a valid email address.</li>
<li class="form-error" ng-show="changeEmailForm.email.$error.response">{{changeEmailForm.email.responseErrorMessage}}</li>
</ul>
</div>
<div class="form-field">
<label class="form-label" for="field-emailAgain">Enter Your New Email Address Again:</label>
<input id="field-emailAgain" class="form-input" type="email" name="emailAgain" required ng-model="changeEmail.emailAgain" />
<ul class="form-error-list">
<li class="form-error" ng-show="changeEmailForm.emailAgain.$error.required">Please enter your new email address twice.</li>
<li class="form-error" ng-show="changeEmailForm.emailAgain.$error.email">Please enter a valid email address.</li>
<li class="form-error" ng-show="changeEmailForm.emailAgain.$error.response">{{changeEmailForm.emailAgain.responseErrorMessage}}</li>
</ul>
</div>
<div class="form-field">
<label class="form-label" for="field-pwd">Password:</label>
<input id="field-pwd" class="form-input" type="password" name="pwd" required ng-model="changeEmail.pwd" />
<ul class="form-error-list">
<li class="form-error" ng-show="changeEmailForm.pwd.$error.required">Please enter your password.</li>
<li class="form-error" ng-show="changeEmailForm.pwd.$error.minlength">Your password does not match the one we have on record.</li>
<li class="form-error" ng-show="changeEmailForm.pwd.$error.response">{{changeEmailForm.pwd.responseErrorMessage}}</li>
</ul>
</div>
<div class="form-actions">
<div class="form-actions-buttons">
<button class="btn" type="submit"
status-button="changeEmailForm">Update</button>
</div>
</div>
</form>
<form class="account-form form"
name="changePasswordForm"
ng-submit="submit(changePasswordForm)"
View
@@ -0,0 +1,48 @@
<div id="dialog" class="sheet content">
<i class="close h-icon-close"
role="button"
title="Close"
ng-click="viaLinkDialog.visible = false"></i>
<div class="form-vertical tabbable">
<div class="form tab-pane" data-title="Share this page">
<p style="margin-top: 1em;" ng-hide="shareGroup">Share the link below to show anyone these annotations and invite them to contribute their own.</p>
<p ng-hide="shareGroup"><input id="via" class="form-input" type="text" ng-value="viaPageLink" readonly></input></p>
<p ng-hide="shareGroup" class="share-links">
<a href="//twitter.com/intent/tweet?url={{viaPageLink}}"
target="_blank"
title="Tweet link"
class="h-icon-twitter"></a>
<a href="//www.facebook.com/sharer/sharer.php?u={{viaPageLink}}"
target="_blank"
title="Share on Facebook"
class="h-icon-facebook"></a>
<a href="//plus.google.com/share?url={{viaPageLink}}"
target="_blank"
title="Post on Google Plus"
class="h-icon-google-plus"></a>
<a href="mailto:?subject=Let's%20Annotate&amp;body={{viaPageLink}}"
title="Share via email"
class="h-icon-mail"></a>
</p>
<p style="margin-top: 1em;" ng-show="shareGroup">Share the link below to invite others to contribute annotations to the <strong>{{nameofgroup}}</strong> group on this page:</p>
<p ng-show="shareGroup"><input id="via" class="form-input" type="text" ng-value="shareGroupLink" readonly></input></p>
<p ng-show="shareGroup" class="share-links">
<a href="//twitter.com/intent/tweet?url={{shareGroupLink}}"
target="_blank"
title="Tweet link"
class="h-icon-twitter"></a>
<a href="//www.facebook.com/sharer/sharer.php?u={{shareGroupLink}}"
target="_blank"
title="Share on Facebook"
class="h-icon-facebook"></a>
<a href="//plus.google.com/share?url={{shareGroupLink}}"
target="_blank"
title="Post on Google Plus"
class="h-icon-google-plus"></a>
<a href="mailto:?subject=Let's%20Annotate&amp;body={{shareGroupLink}}"
title="Share via email"
class="h-icon-mail"></a>
</p>
</div>
</div>
</div>
View
@@ -0,0 +1,21 @@
<span role="button"
class="dropdown-toggle"
data-toggle="dropdown">
<i ng-class="socialview.icon"></i>
{{socialview.name}}
<i class="h-icon-arrow-drop-down" ng-show="auth.user"></i>
</span>
<ul class="dropdown-menu pull-right" role="menu" ng-show="auth.user">
<li ng-repeat="view in views"
ng-class="{selected: view.selected}"
ng-click="select(view)">
<i class="viewIcons" ng-class="view.icon"></i>
{{view.name}}
<a class="h-icon-cancel pull-right" style="margin: 0 -3px;" ng-show="view.editable && editgroups" ng-click="removeGroup(view)"></a>
<a class="h-icon-link pull-right" style="margin: 0 -3px;" ng-show="view.link" ng-click="showGroupLink(view)"></a>
</li>
<li>
<a ng-click="groupDialog.visible = true" style="margin-left:-7px;"><i class="h-icon-add"></i> New Group</a>
</li>
<!-- <li><a class="" ng-click="toggleeditgroups($event)" style="margin-left:-7px;"><i class="h-icon-edit"></i> Edit Groups</a></li> -->
</ul>
View
@@ -41,8 +41,8 @@
ng-mouseenter="focus(child.message)"
ng-click="scrollTo(child.message)"
ng-mouseleave="focus()"
ng-repeat="child in threadRoot.children | filter:notOrphan | orderBy : sort.predicate"
ng-show="shouldShowThread(child) && (count('edit') || count('match') || !threadFilter.active()) || vm.isNew()">
ng-repeat="child in threadRoot.children | filter:filterView | orderBy : sort.predicate"
ng-show="shouldShowThread(child) && (count('edit') || count('match') || !threadFilter.active())">
</li>
</ul>
<!-- / Thread view -->
View
@@ -5,6 +5,8 @@
<!-- Override app body stylings for just this page -->
<style type="text/css">
body {background: #FFF !important;}
ol li {margin-top: .5em !important;}
ol {margin-top: 1em !important;}
</style>
{{ super() }}
{% if is_onboarding %}
@@ -22,42 +24,27 @@
{% block content %}
<div class="help-page">
<div class="help-page-content masthead-small">
<!-- <div class="help-page-content masthead-small">
{% include "includes/header.html" %}
</div>
</div> -->
<article class="help-page-content">
{% if is_help %}
<section class="help-page-section">
<h2 id="installation" class="help-page-heading">Installation</h2>
<p class="help-page-lede">There are a couple of installations of Hypothesis to choose from:</p>
<div class="numbered-list grid">
<div class="column-desktop-1-2"><div class="numbered-list-item">If you want to annotate and comment on documents then install our browser extension.</div></div>
<div class="column-desktop-1-2"><div class="numbered-list-item">If you wish to install Hypothesis on your own site then head over to GitHub.</div></div>
</div>
</section>
{% endif %}
{% if is_onboarding %}
<section class="help-page-section">
<h2 id="getting-started" class="help-page-heading">Getting started</h2>
<p class="help-page-lede">Now you have the extension up and running. It's time to
start annotating some documents.</p>
<div class="numbered-list grid">
<div class="column-desktop-1-2"><div class="numbered-list-item">Create an account using the sidebar on the right of the screen.</div></div>
<div class="column-desktop-1-2"><div class="numbered-list-item">Go forth and annotate! Enable the sidebar via the button in the location bar.</div></div>
</div>
</section>
{% endif %}
{% if is_index %}
<section class="help-page-section">
<h2 id="getting-started" class="help-page-heading">Getting started</h2>
<p class="help-page-lede">Great, you've got the server up and running, you're now ready to start kicking the tires.</p>
<div class="numbered-list grid">
<div class="column-desktop-1-2"><div class="numbered-list-item">Create an account using the sidebar on the right of the screen.</div></div>
<div class="column-desktop-1-2"><div class="numbered-list-item">Annotate this page or use this bookmarklet: {% include "includes/bookmarklet.html" %}</div></div>
</div>
<h2 id="getting-started" class="help-page-heading">Welcome to the Groups Prototype!</h2>
<p>You can annotate other pages with this prototype by using this bookmarklet: {% include "includes/bookmarklet.html" %}</p>
<p>Please complete the following tasks.</p>
<ol>
<li>Create an account. Note: this isn't a real hypothes.is account, but you will still need to complete the confirm email step, which you can do <strong><a href="http://dokku.hypothes.is:8025/">here</a></strong>.</li>
<li>Create a new group! Give it whatever name you choose.</li>
<li>Select some text and choose the <i class="feature-icon h-icon-insert-comment"></i> icon.</li>
<li>Write a short comment, and publish it to your new group.</li>
<li>Find the link for the group you created.</li>
<li>Make another group and then remove it from your list of groups.</li>
<li>Finally, make a public note summarizing your experience with this prototype. Did you find any of the tasks difficult? Was anything confusing? Any suggestions for improvement?</li>
</ol>
</section>
{% endif %}
<section class="help-page-section">
<!-- <section class="help-page-section">
<h2 id="key-features" class="help-page-heading">Annotation Types</h2>
<p class="help-page-lede">There are a few types of annotations that can be created
with the application:</p>
@@ -163,4 +150,4 @@ <h2 id="contributing" class="help-page-heading">Contributing</h2>
<div id="help-2"></div>
</div>
</div>
{% endblock %}
{% endblock %} -->
View
@@ -46,6 +46,7 @@ def run_tests(self):
'cryptography>=0.7',
'deform>=0.9,<1.0',
'elasticsearch>=1.1.0',
'gevent>=1.0.2,<1.1.0',
'gnsq>=0.2.0,<0.3.0',
'gunicorn>=19.2,<20',
'horus>=0.9.15',
@@ -62,7 +63,7 @@ def run_tests(self):
'python-statsd>=1.7.0,<1.8.0',
'pyramid_webassets>=0.9,<1.0',
'pyramid-jinja2>=2.3.3',
'raven>=5.1.1,<5.2.0',
'raven>=5.3.0,<5.4.0',
'requests>=2.2.1',
'ws4py>=0.3,<0.4',