View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -78,7 +78,7 @@ def search(request):
"""Search the database for annotations matching with the given query."""
# The search results are filtered for the authenticated user
user = get_user(request)
return search_lib.search(request.params, user)
return search_lib.search(request, request.params, user)
@api_config(context=Root, name='access_token')
View
@@ -0,0 +1,7 @@
from h import hashids
def url_for_group(request, group):
"""Return the URL for the given group's page."""
hashid = hashids.encode(request, "h.groups", number=group.id)
return request.route_url('group_read', hashid=hashid, slug=group.slug)
View
@@ -0,0 +1,37 @@
def group_filter(request):
"""Return an Elasticsearch filter for the given user's groups.
This will filter out annotations that belong to groups that the given user
isn't a member of. Annotations belonging to no group (or rather, having
"group": "public" in their Elasticsearch documents) and annotations
belonging to any group that the user is a member of, will pass through the
filter.
The returned filter is suitable for inserting into an Es query dict.
For example:
query = {
"query": {
"filtered": {
"filter": group_filter(),
"query": {...}
}
}
}
:param user: The user whose group memberships should be used
for filtering. If this is ``None`` then _only_ annotations belonging to
no group / the "public" group will be allowed through the filter.
:type user: User or None
"""
# All users implicitly belong to the fake "public" group.
# FIXME: Need to somehow make sure this special group name can't clash with
# custom group names.
group_names = ["public"]
for principal in request.effective_principals:
if principal.startswith('usergroup:'):
group_names.append(principal.split(':', 1))
return {"terms": {"group": group_names}}
View
@@ -0,0 +1,21 @@
{% extends "h:templates/layouts/base.html.jinja2" %}
{% block page_title %}{{ group.name }}{% endblock page_title %}
{% block styles %}
{% assets "app_css" %}
<link rel="stylesheet" href="{{ ASSET_URL }}">
{% endassets %}
{% endblock %}
{% block content %}
<div class="content paper">
{% include "h:templates/includes/header.html.jinja2" %}
This is group {{ group.name }}.
<form method="POST">
<button type="submit">
Join this group
</button>
</form>
</div>
{% endblock content %}
View
@@ -0,0 +1,19 @@
{% extends "h:templates/layouts/base.html.jinja2" %}
{% block page_title %}{{ group.name }}{% endblock page_title %}
{% block styles %}
{% assets "app_css" %}
<link rel="stylesheet" href="{{ ASSET_URL }}">
{% endassets %}
{% endblock %}
{% block content %}
<div class="content paper">
{% include "h:templates/includes/header.html.jinja2" %}
This is group {{ group.name }}.
<a href="{{ request.route_url('login') }}" target="_blank">
Login to join this group.
</a>
</div>
{% endblock content %}
View
@@ -10,7 +10,15 @@
{% block content %}
<div class="content paper">
{% for message in request.session.pop_flash('success') %}
{{ message }}
{% endfor %}
{% include "h:templates/includes/header.html.jinja2" %}
This is group {{ group.name }}.
You're a member of this group.
You can invite other people to join this group by sending them the link
to this page: {{ group_url }}
</div>
{% endblock content %}
View
@@ -0,0 +1,31 @@
import mock
from h.groups import logic
@mock.patch('h.groups.logic.hashids')
def test_url_for_group_encodes_groupid(hashids):
"""It should encode the groupid to get the hashid.
And then pass the hashid to route_url().
"""
hashids.encode.return_value = mock.sentinel.hashid
request = mock.Mock()
group = mock.Mock()
logic.url_for_group(request, group)
assert hashids.encode.call_args[1]['number'] == group.id
assert request.route_url.call_args[1]['hashid'] == mock.sentinel.hashid
@mock.patch('h.groups.logic.hashids')
def test_url_for_group_returns_url(_):
"""It should return the URL from request.route_url()."""
request = mock.Mock()
request.route_url.return_value = mock.sentinel.group_url
url = logic.url_for_group(request, mock.Mock())
assert url == mock.sentinel.group_url
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -3,11 +3,17 @@
import deform
from pyramid import httpexceptions as exc
from pyramid.view import view_config
from pyramid import renderers
from h.groups import schemas
from h.groups import models
from h.groups import logic
from h.accounts import models as accounts_models
from h import hashids
from h import i18n
_ = i18n.TranslationString
@view_config(route_name='group_create',
@@ -48,15 +54,52 @@ def create(request):
# We need to flush the db session here so that group.id will be generated.
request.db.flush()
hashid = hashids.encode(request, "h.groups", group.id)
return exc.HTTPSeeOther(
location=request.route_url(
'group_read', hashid=hashid, slug=group.slug))
return exc.HTTPSeeOther(logic.url_for_group(request, group))
@view_config(route_name='group_read',
request_method='GET',
renderer='h:groups/templates/read.html.jinja2')
def _login_to_join(request, group):
"""Return the rendered "Login to join this group" page.
This is the page that's shown when a user who isn't logged in visits a
group's URL.
"""
template_data = {'group': group}
return renderers.render_to_response(
renderer_name='h:groups/templates/login_to_join.html.jinja2',
value=template_data, request=request)
def _read_group(request, group):
"""Return the rendered "Share this group" page.
This is the page that's shown when a user who is already a member of a
group visits the group's URL.
"""
template_data = {
'group': group, 'group_url': logic.url_for_group(request, group)}
return renderers.render_to_response(
renderer_name='h:groups/templates/read.html.jinja2',
value=template_data, request=request)
def _join(request, group):
"""Return the rendered "Join this group" page.
This is the page that's shown when a user who is not a member of a group
visits the group's URL.
"""
hashid = hashids.encode(request, 'h.groups', number=group.id)
join_url = request.route_url('group_read', hashid=hashid, slug=group.slug)
template_data = {'group': group, 'join_url': join_url}
return renderers.render_to_response(
renderer_name='h:groups/templates/join.html.jinja2',
value=template_data, request=request)
@view_config(route_name='group_read', request_method='GET')
@view_config(route_name='group_read_noslug', request_method='GET')
def read(request):
"""Render the page for a group."""
@@ -65,19 +108,52 @@ def read(request):
hashid = request.matchdict["hashid"]
slug = request.matchdict.get("slug")
group_id = hashids.decode(request, "h.groups", hashid)
group_id = hashids.decode(request, 'h.groups', hashid)
group = models.Group.get_by_id(group_id)
if group is None:
raise exc.HTTPNotFound()
if slug is None or slug != group.slug:
canonical = request.route_url('group_read',
hashid=hashid,
slug=group.slug)
return exc.HTTPMovedPermanently(location=canonical)
return exc.HTTPMovedPermanently(
location=logic.url_for_group(request, group))
if not request.authenticated_userid:
return _login_to_join(request, group)
else:
user = accounts_models.User.get_by_id(
request, request.authenticated_userid)
if group in user.groups:
return _read_group(request, group)
else:
return _join(request, group)
@view_config(route_name='group_read',
request_method='POST',
renderer='h:groups/templates/read.html.jinja2',
permission='authenticated')
def join(request):
if not request.feature('groups'):
raise exc.HTTPNotFound()
hashid = request.matchdict["hashid"]
group_id = hashids.decode(request, "h.groups", hashid)
group = models.Group.get_by_id(group_id)
if group is None:
raise exc.HTTPNotFound()
user = accounts_models.User.get_by_id(
request, request.authenticated_userid)
group.members.append(user)
request.session.flash(_(
"You've joined the {name} group.").format(name=group.name),
'success')
return {"group": group}
return exc.HTTPSeeOther(logic.url_for_group(request, group))
def includeme(config):
View
@@ -95,7 +95,6 @@ module.exports = angular.module('h', [
.directive('deepCount', require('./directive/deep-count'))
.directive('formInput', require('./directive/form-input'))
.directive('formValidate', require('./directive/form-validate'))
.directive('groupList', require('./directive/group-list'))
.directive('markdown', require('./directive/markdown'))
.directive('privacy', require('./directive/privacy'))
.directive('simpleSearch', require('./directive/simple-search'))
@@ -143,6 +142,7 @@ module.exports = angular.module('h', [
.service('viewFilter', require('./view-filter'))
.factory('serviceUrl', require('./service-url'))
.factory('group', require('./group-service'))
.value('AnnotationSync', require('./annotation-sync'))
.value('AnnotationUISync', require('./annotation-ui-sync'))
@@ -157,3 +157,5 @@ module.exports = angular.module('h', [
.run(setupCrossFrame)
.run(setupStreamer)
.run(setupHost)
require('./group-list-controller')
View
@@ -43,10 +43,10 @@ errorMessage = (reason) ->
AnnotationController = [
'$scope', '$timeout', '$q', '$rootScope', '$document',
'drafts', 'flash', 'permissions', 'tags', 'time',
'annotationUI', 'annotationMapper', 'session'
'annotationUI', 'annotationMapper', 'session', 'group'
($scope, $timeout, $q, $rootScope, $document,
drafts, flash, permissions, tags, time,
annotationUI, annotationMapper, session) ->
annotationUI, annotationMapper, session, group) ->
@annotation = {}
@action = 'view'
@@ -202,6 +202,15 @@ AnnotationController = [
switch @action
when 'create'
updateDomainModel(model, @annotation)
# FIXME: Need to use a unique id here, group names are not unique.
# FIXME: Need to prevent custom group names from clashing with the
# special "public" name.
if group.focusedGroup().name in ["All", "Public", "Only Me"]
model.group = "public"
else
model.group = group.focusedGroup().name
onFulfilled = =>
$rootScope.$emit('annotationCreated', model)
@view()
View
@@ -0,0 +1,20 @@
'use strict';
angular.module('h').controller(
'GroupListCtrl', ['session', 'group', function(session, group) {
var self = this;
self.groups = function() {
return group.groups();
};
self.focusedGroup = function() {
return group.focusedGroup();
};
self.focusGroup = function(name) {
return group.focusGroup(name);
};
}
]
);
View
@@ -0,0 +1,54 @@
/**
* @ngdoc service
* @name group
*
* @description
* Get and set the UI's currently focused group.
*/
'use strict';
// @ngInject
function group(session) {
// The currently focused group. This is the group that's shown as selected
// in the groups dropdown, the annotations displayed are filtered to only
// ones that belong to this group, and any new annotations that the user
// creates will be created in this group.
var focusedGroup;
// Some "groups" are always available, regardless of what real groups (if
// any) the user is a member of.
// FIXME: If no one is logged-in then Public and Only Me shouldn't be here.
// FIXME: This service can't just return {'name': 'All'} for when the
// focused group is All, it needs to be some id that can't clash with any
// custom group name.
var globalGroups = [{'name': 'All'}, {'name': 'Public'},
{'name': 'Only Me'}];
// Return the list of available groups.
var groups = function() {
return globalGroups.concat(session.state.groups || []);
};
return {
groups: groups,
// Return the currently focused group.
focusedGroup: function() {
return focusedGroup || groups()[0];
},
// Set the named group as the currently focused group.
// FIXME: Do this by id or something else instead - two groups may have
// the same name.
focusGroup: function(name) {
for (var i = 0; i < groups().length; i++) {
var group = groups()[i];
if (group.name === name) {
focusedGroup = group;
}
}
}
};
}
module.exports = group;
View
@@ -129,10 +129,25 @@ ol {
}
}
/* The groups dropdown list. */
.group-list {
margin-right: 0.5em;
}
$group-list-width: 225px;
.group-list .dropdown-menu {
width: $group-list-width;
}
.group-list .dropdown-menu .group-name {
max-width: $group-list-width - 45px;
overflow: hidden;
text-overflow: ellipsis;
}
.user-picker {
.avatar {
border-radius: 2px;
View
@@ -43,11 +43,7 @@
</div>
</div>
<div class="pull-right dropdown group-list"
ng-if="auth.user && feature('groups')"
group-list=""
groups="session.state.groups">
</div>
{{ include_raw("h:templates/client/group_list.html") }}
<!-- Searchbar -->
<div class="simple-search"
@@ -101,9 +97,6 @@
<script type="text/ng-template" id="annotation.html">
{{ include_raw("h:templates/client/annotation.html") }}
</script>
<script type="text/ng-template" id="group_list.html">
{{ include_raw("h:templates/client/group_list.html") }}
</script>
<script type="text/ng-template" id="markdown.html">
{{ include_raw("h:templates/client/markdown.html") }}
</script>
View
@@ -1,12 +1,20 @@
<span role="button" class="dropdown-toggle" data-toggle="dropdown">
Groups
<i class="h-icon-arrow-drop-down"></i>
</span>
<ul class="dropdown-menu pull-right" role="menu">
<li ng-repeat="group in groups">
<a ng-href="{{group.url}}" ng-bind="group.name" target="_blank"></a>
</li>
<li>
<a href="/groups/new" target="_blank"><i class="h-icon-add"></i> Create a group</a>
</li>
</ul>
<div ng-controller="GroupListCtrl as ctrl"
class="pull-right dropdown group-list">
<span role="button" class="dropdown-toggle" data-toggle="dropdown">
{{ctrl.focusedGroup().name}}
<i class="h-icon-arrow-drop-down"></i>
</span>
<ul class="dropdown-menu pull-right" role="menu">
<li ng-repeat="group in ctrl.groups()">
<a ng-bind="group.name" ng-click="ctrl.focusGroup(group.name)">
</a>
<a ng-href="{{group.url}}" target="_blank" class="h-icon-link pull-right"
title="Share this group"></a>
<div style="clear:both;"></div>
</li>
<li>
<a href="/groups/new" target="_blank"><i class="h-icon-add"></i>
New Group</a>
</li>
</ul>
</div>