allura
Revision | c4109f62d8436ef507061d932ad94a41fac5637e (tree) |
---|---|
Zeit | 2012-06-28 07:33:48 |
Autor | Cory Johns <johnsca@geek...> |
Commiter | Cory Johns |
[#4388] Added UI for watching / following projects / users
Signed-off-by: Cory Johns <johnsca@geek.net>
@@ -122,6 +122,8 @@ class Globals(object): | ||
122 | 122 | user=Icon('U', 'ico-user'), |
123 | 123 | secure=Icon('(', 'ico-lock'), |
124 | 124 | unsecure=Icon(')', 'ico-unlock'), |
125 | + star=Icon('S', 'ico-star'), | |
126 | + watch=Icon('E', 'ico-watch'), | |
125 | 127 | # Permissions |
126 | 128 | perm_read=Icon('E', 'ico-focus'), |
127 | 129 | perm_update=Icon('0', 'ico-sync'), |
@@ -0,0 +1,5 @@ | ||
1 | +import pkg_resources | |
2 | + | |
3 | +def register_ew_resources(manager): | |
4 | + manager.register_directory( | |
5 | + 'activity_js', pkg_resources.resource_filename('forgeactivity', 'widgets/resources/js')) |
@@ -8,13 +8,21 @@ from pylons import c, response | ||
8 | 8 | from tg import expose, validate, config, redirect |
9 | 9 | from tg.decorators import with_trailing_slash |
10 | 10 | from paste.deploy.converters import asbool |
11 | +from formencode import validators as fev | |
12 | +from webob import exc | |
11 | 13 | |
12 | 14 | from allura.app import Application |
13 | 15 | from allura import version |
14 | 16 | from allura.controllers import BaseController |
17 | +from allura.lib.security import require_authenticated | |
18 | + | |
19 | +from activitystream import director | |
20 | + | |
21 | +from .widgets.follow import FollowToggle | |
15 | 22 | |
16 | 23 | log = logging.getLogger(__name__) |
17 | 24 | |
25 | + | |
18 | 26 | class ForgeActivityApp(Application): |
19 | 27 | """Project Activity page for projects.""" |
20 | 28 | __version__ = version.__version__ |
@@ -43,6 +51,8 @@ class ForgeActivityApp(Application): | ||
43 | 51 | def uninstall(self, project): |
44 | 52 | pass # pragma no cover |
45 | 53 | |
54 | +class W: | |
55 | + follow_toggle = FollowToggle() | |
46 | 56 | |
47 | 57 | class ForgeActivityController(BaseController): |
48 | 58 | @expose('jinja:forgeactivity:templates/index.html') |
@@ -50,6 +60,36 @@ class ForgeActivityController(BaseController): | ||
50 | 60 | def index(self, **kw): |
51 | 61 | activity_enabled = asbool(config.get('activity_stream.enabled', False)) |
52 | 62 | if not activity_enabled: |
53 | - response.status = 404 | |
54 | - return dict() | |
55 | - return dict() | |
63 | + raise HTTPNotFound() | |
64 | + | |
65 | + c.follow_toggle = W.follow_toggle | |
66 | + followee = c.project | |
67 | + if c.project.is_user_project: | |
68 | + followee = c.project.user_project_of | |
69 | + following = director().is_connected(c.user, followee) | |
70 | + return dict(following=following) | |
71 | + | |
72 | + @expose('json:') | |
73 | + @validate(W.follow_toggle) | |
74 | + def follow(self, follow, **kw): | |
75 | + activity_enabled = asbool(config.get('activity_stream.enabled', False)) | |
76 | + if not activity_enabled: | |
77 | + raise HTTPNotFound() | |
78 | + | |
79 | + require_authenticated() | |
80 | + followee = c.project | |
81 | + if c.project.is_user_project: | |
82 | + followee = c.project.user_project_of | |
83 | + try: | |
84 | + if follow: | |
85 | + director().connect(c.user, followee) | |
86 | + else: | |
87 | + director().disconnect(c.user, followee) | |
88 | + except Exception as e: | |
89 | + return dict( | |
90 | + success=False, | |
91 | + message='Unexpected error: %s' % e) | |
92 | + return dict( | |
93 | + success=True, | |
94 | + message=W.follow_toggle.success_message(follow), | |
95 | + following=follow) |
@@ -3,7 +3,20 @@ | ||
3 | 3 | |
4 | 4 | {% block title %}{{c.project.name}} Activity{% endblock %} |
5 | 5 | |
6 | -{% block header %}{{c.project.name}} Activity{% endblock %} | |
6 | +{% block header %} | |
7 | + Activity for | |
8 | + {% if c.project.is_user_project %} | |
9 | + {{c.project.user_project_of.display_name}} | |
10 | + {% else %} | |
11 | + {{c.project.name}} | |
12 | + {% endif %} | |
13 | +{% endblock %} | |
14 | + | |
15 | +{% block actions %} | |
16 | + {% if c.user and c.user != c.user.anonymous() %} | |
17 | + {{c.follow_toggle.display(following=following)}} | |
18 | + {% endif %} | |
19 | +{% endblock %} | |
7 | 20 | |
8 | 21 | {% block content %} |
9 | 22 | <div class="grid-14"> |
@@ -0,0 +1,5 @@ | ||
1 | +<a href="{{action}}?follow={{not following}}" | |
2 | + class="artifact_follow{{ ' active' if following }}" | |
3 | + title="{{'Stop %sing' % action_label if following else action_label|capitalize}} {{thing}}"><b | |
4 | + data-icon="{{g.icons[icon].char}}" class="ico {{g.icons[icon].css}}" | |
5 | + title="{{'Stop %sing' % action_label if following else action_label|capitalize}} {{thing}}"></b></a> |
@@ -0,0 +1,42 @@ | ||
1 | +from pylons import c | |
2 | +from formencode import validators as fev | |
3 | +import ew as ew_core | |
4 | +import ew.jinja2_ew as ew | |
5 | + | |
6 | + | |
7 | +class FollowToggle(ew.SimpleForm): | |
8 | + template='jinja:forgeactivity:templates/widgets/follow.html' | |
9 | + defaults=dict( | |
10 | + ew.SimpleForm.defaults, | |
11 | + thing='project', | |
12 | + action='follow', | |
13 | + action_label='watch', | |
14 | + icon='watch', | |
15 | + following=False) | |
16 | + | |
17 | + class fields(ew_core.NameList): | |
18 | + follow = ew.HiddenField(validator=fev.StringBool()) | |
19 | + | |
20 | + def resources(self): | |
21 | + yield ew.JSLink('activity_js/follow.js') | |
22 | + | |
23 | + def prepare_context(self, context): | |
24 | + default_context = super(FollowToggle, self).prepare_context({}) | |
25 | + if c.project.is_user_project: | |
26 | + default_context.update( | |
27 | + thing=c.project.user_project_of.display_name, | |
28 | + action_label='follow', | |
29 | + icon='star', | |
30 | + ) | |
31 | + else: | |
32 | + default_context.update(thing=c.project.name) | |
33 | + default_context.update(context) | |
34 | + return default_context | |
35 | + | |
36 | + def success_message(self, following): | |
37 | + context = self.prepare_context({}) | |
38 | + return u'You are {state} {action}ing {thing}.'.format( | |
39 | + state='now' if following else 'no longer', | |
40 | + action=context['action_label'], | |
41 | + thing=context['thing'], | |
42 | + ) |
@@ -0,0 +1,34 @@ | ||
1 | +$(document).ready(function() { | |
2 | + function title_stop_following($elem) { | |
3 | + $elem.attr('title', $elem.attr('title').replace(/^([A-Z])(\w+)/, function(p,c,w) { | |
4 | + return 'Stop ' + c.toLowerCase() + w + 'ing'; | |
5 | + })); | |
6 | + } | |
7 | + | |
8 | + function title_start_following($elem) { | |
9 | + $elem.attr('title', $elem.attr('title').replace(/^Stop ([a-z])(\w+)ing/, function(p,c,w) { | |
10 | + return c.toUpperCase() + w; | |
11 | + })); | |
12 | + } | |
13 | + | |
14 | + $('.artifact_follow').click(function(e) { | |
15 | + e.preventDefault(); | |
16 | + var $link = $(this); | |
17 | + $.get(this.href, function(result) { | |
18 | + flash(result.message, result.success ? 'success' : 'error'); | |
19 | + console.log(result.following); | |
20 | + if (result.following && !$link.hasClass('active')) { | |
21 | + $link.attr('href', $link.attr('href').replace(/True$/i, 'False')); | |
22 | + $link.addClass('active'); | |
23 | + title_stop_following($link); | |
24 | + title_stop_following($link.find('b')); | |
25 | + } else if (!result.following && $link.hasClass('active')) { | |
26 | + $link.attr('href', $link.attr('href').replace(/False$/i, 'True')); | |
27 | + $link.removeClass('active'); | |
28 | + title_start_following($link); | |
29 | + title_start_following($link.find('b')); | |
30 | + } | |
31 | + }); | |
32 | + return false; | |
33 | + }); | |
34 | +}); |
@@ -22,5 +22,8 @@ setup(name='ForgeActivity', | ||
22 | 22 | # -*- Entry points: -*- |
23 | 23 | [allura] |
24 | 24 | activity=forgeactivity.main:ForgeActivityApp |
25 | + | |
26 | + [easy_widgets.resources] | |
27 | + ew_resources=forgeactivity.config.resources:register_ew_resources | |
25 | 28 | """, |
26 | 29 | ) |