##// END OF EJS Templates
implemented user dashboards, and following system.
marcink -
r734:49eb69d7 beta
parent child Browse files
Show More
@@ -0,0 +1,84 b''
1 #!/usr/bin/env python
2 # encoding: utf-8
3 # journal controller for pylons
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
9 # of the License or (at your opinion) any later version of the license.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
20 """
21 Created on November 21, 2010
22 journal controller for pylons
23 @author: marcink
24 """
25
26 from pylons import request, response, session, tmpl_context as c, url
27 from pylons.controllers.util import abort, redirect
28 from rhodecode.lib.auth import LoginRequired
29 from rhodecode.lib.base import BaseController, render
30 from rhodecode.lib.helpers import get_token
31 from rhodecode.model.db import UserLog, UserFollowing
32 from rhodecode.model.scm import ScmModel
33 import logging
34 from paste.httpexceptions import HTTPInternalServerError, HTTPNotFound
35
36 log = logging.getLogger(__name__)
37
38 class JournalController(BaseController):
39
40
41 @LoginRequired()
42 def __before__(self):
43 super(JournalController, self).__before__()
44
45 def index(self):
46 # Return a rendered template
47
48 c.following = self.sa.query(UserFollowing)\
49 .filter(UserFollowing.user_id == c.rhodecode_user.user_id).all()
50
51
52 c.journal = self.sa.query(UserLog)\
53 .order_by(UserLog.action_date.desc())\
54 .all()
55 return render('/journal.html')
56
57
58 def toggle_following(self):
59 print c.rhodecode_user
60
61 if request.POST.get('auth_token') == get_token():
62 scm_model = ScmModel()
63
64 user_id = request.POST.get('follows_user_id')
65 if user_id:
66 try:
67 scm_model.toggle_following_user(user_id,
68 c.rhodecode_user.user_id)
69 return 'ok'
70 except:
71 raise HTTPInternalServerError()
72
73 repo_id = request.POST.get('follows_repo_id')
74 if repo_id:
75 try:
76 scm_model.toggle_following_repo(repo_id,
77 c.rhodecode_user.user_id)
78 return 'ok'
79 except:
80 raise HTTPInternalServerError()
81
82
83
84 raise HTTPInternalServerError()
@@ -0,0 +1,80 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base/base.html"/>
3 <%def name="title()">
4 ${_('Journal')} - ${c.rhodecode_name}
5 </%def>
6 <%def name="breadcrumbs()">
7 ${c.rhodecode_name}
8 </%def>
9 <%def name="page_nav()">
10 ${self.menu('home')}
11 </%def>
12 <%def name="main()">
13
14 <div class="box box-left">
15 <!-- box / title -->
16 <div class="title">
17 <h5>${_('Journal')}</h5>
18 </div>
19 <div>
20 %if c.journal:
21 %for entry in c.journal:
22 <div style="padding:10px">
23 <div class="gravatar">
24 <img alt="gravatar" src="${h.gravatar_url(entry.user.email)}"/>
25 </div>
26 <div>${entry.user.name} ${entry.user.lastname}</div>
27 <div style="padding-left: 45px;">${h.action_parser(entry)} <br/>
28 <b>
29 %if entry.repository:
30 ${h.link_to(entry.repository.repo_name,
31 h.url('summary_home',repo_name=entry.repository.repo_name))}
32 %else:
33 ${entry.repository_name}
34 %endif
35 </b> - <span title="${entry.action_date}">${h.age(entry.action_date)}</span>
36 </div>
37 </div>
38 <div style="clear:both;border-bottom:1px dashed #DDD;padding:3px 3px;margin:0px 10px 0px 10px"></div>
39 %endfor
40 %else:
41 ${_('No entries yet')}
42 %endif
43 </div>
44 </div>
45
46 <div class="box box-right">
47 <!-- box / title -->
48 <div class="title">
49 <h5>${_('Following')}</h5>
50 </div>
51 <div>
52 %if c.following:
53 %for entry in c.following:
54 <div>
55 %if entry.follows_user_id:
56 <img alt="" src="/images/icons/user.png"/>
57
58 ${entry.follows_user.username}
59 %endif
60
61 %if entry.follows_repo_id:
62
63 %if entry.follows_repository.private:
64 <img alt="" src="/images/icons/lock_closed.png"/>
65 %else:
66 <img alt="" src="/images/icons/lock_open.png"/>
67 %endif
68
69 ${h.link_to(entry.follows_repository.repo_name,h.url('summary_home',
70 repo_name=entry.follows_repository.repo_name))}
71
72 %endif
73 </div>
74 %endfor
75 %else:
76 ${_('You are not following any users or repositories')}
77 %endif
78 </div>
79 </div>
80 </%def>
@@ -0,0 +1,7 b''
1 from rhodecode.tests import *
2
3 class TestJournalController(TestController):
4
5 def test_index(self):
6 response = self.app.get(url(controller='journal', action='index'))
7 # Test response...
@@ -124,6 +124,14 b' def make_map(config):'
124 m.connect('admin_home', '', action='index')#main page
124 m.connect('admin_home', '', action='index')#main page
125 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
125 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
126 action='add_repo')
126 action='add_repo')
127
128
129 #USER JOURNAL
130 map.connect('journal', '/_admin/journal', controller='journal',)
131 map.connect('toggle_following', '/_admin/toggle_following', controller='journal',
132 action='toggle_following', conditions=dict(method=["POST"]))
133
134
127 #SEARCH
135 #SEARCH
128 map.connect('search', '/_admin/search', controller='search',)
136 map.connect('search', '/_admin/search', controller='search',)
129 map.connect('search_repo', '/_admin/search/{search_repo:.*}', controller='search')
137 map.connect('search_repo', '/_admin/search/{search_repo:.*}', controller='search')
@@ -52,8 +52,10 b' class SummaryController(BaseController):'
52 super(SummaryController, self).__before__()
52 super(SummaryController, self).__before__()
53
53
54 def index(self):
54 def index(self):
55 hg_model = ScmModel()
55 scm_model = ScmModel()
56 c.repo_info = hg_model.get_repo(c.repo_name)
56 c.repo_info = scm_model.get_repo(c.repo_name)
57 c.following = scm_model.is_following_repo(c.repo_name,
58 c.rhodecode_user.user_id)
57 def url_generator(**kw):
59 def url_generator(**kw):
58 return url('shortlog_home', repo_name=c.repo_name, **kw)
60 return url('shortlog_home', repo_name=c.repo_name, **kw)
59
61
@@ -3,6 +3,8 b''
3 Consists of functions to typically be used within templates, but also
3 Consists of functions to typically be used within templates, but also
4 available to Controllers. This module is available to both as 'h'.
4 available to Controllers. This module is available to both as 'h'.
5 """
5 """
6 import random
7 import hashlib
6 from pygments.formatters import HtmlFormatter
8 from pygments.formatters import HtmlFormatter
7 from pygments import highlight as code_highlight
9 from pygments import highlight as code_highlight
8 from pylons import url, app_globals as g
10 from pylons import url, app_globals as g
@@ -36,6 +38,24 b' def _reset(name, value=None, id=NotGiven'
36
38
37 reset = _reset
39 reset = _reset
38
40
41
42 def get_token():
43 """Return the current authentication token, creating one if one doesn't
44 already exist.
45 """
46 token_key = "_authentication_token"
47 from pylons import session
48 if not token_key in session:
49 try:
50 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
51 except AttributeError: # Python < 2.4
52 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
53 session[token_key] = token
54 if hasattr(session, 'save'):
55 session.save()
56 return session[token_key]
57
58
39 #Custom helpers here :)
59 #Custom helpers here :)
40 class _Link(object):
60 class _Link(object):
41 '''
61 '''
@@ -49,6 +49,7 b' class User(Base):'
49 user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
49 user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
50
50
51 repositories = relation('Repository')
51 repositories = relation('Repository')
52 user_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
52
53
53 @LazyProperty
54 @LazyProperty
54 def full_contact(self):
55 def full_contact(self):
@@ -101,6 +102,9 b' class Repository(Base):'
101 repo_to_perm = relation('RepoToPerm', cascade='all')
102 repo_to_perm = relation('RepoToPerm', cascade='all')
102 stats = relation('Statistics', cascade='all', uselist=False)
103 stats = relation('Statistics', cascade='all', uselist=False)
103
104
105 repo_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
106
107
104 def __repr__(self):
108 def __repr__(self):
105 return "<Repository('%s:%s')>" % (self.repo_id, self.repo_name)
109 return "<Repository('%s:%s')>" % (self.repo_id, self.repo_name)
106
110
@@ -148,6 +152,23 b' class Statistics(Base):'
148
152
149 repository = relation('Repository', single_parent=True)
153 repository = relation('Repository', single_parent=True)
150
154
155 class UserFollowing(Base):
156 __tablename__ = 'user_followings'
157 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
158 UniqueConstraint('user_id', 'follows_user_id')
159 , {'useexisting':True})
160
161 user_following_id = Column("user_following_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
162 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
163 follows_repo_id = Column("follows_repository_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None)
164 follows_user_id = Column("follows_user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None)
165
166 user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id')
167
168 follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
169 follows_repository = relation('Repository')
170
171
151 class CacheInvalidation(Base):
172 class CacheInvalidation(Base):
152 __tablename__ = 'cache_invalidation'
173 __tablename__ = 'cache_invalidation'
153 __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True})
174 __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True})
@@ -163,4 +184,4 b' class CacheInvalidation(Base):'
163 self.cache_active = False
184 self.cache_active = False
164
185
165 def __repr__(self):
186 def __repr__(self):
166 return "<CacheInvaidation('%s:%s')>" % (self.cache_id, self.cache_key)
187 return "<CacheInvalidation('%s:%s')>" % (self.cache_id, self.cache_key)
@@ -29,7 +29,8 b' from rhodecode.lib import helpers as h'
29 from rhodecode.lib.auth import HasRepoPermissionAny
29 from rhodecode.lib.auth import HasRepoPermissionAny
30 from rhodecode.lib.utils import get_repos, make_ui
30 from rhodecode.lib.utils import get_repos, make_ui
31 from rhodecode.model import meta
31 from rhodecode.model import meta
32 from rhodecode.model.db import Repository, User, RhodeCodeUi, CacheInvalidation
32 from rhodecode.model.db import Repository, User, RhodeCodeUi, CacheInvalidation, \
33 UserFollowing
33 from rhodecode.model.caching_query import FromCache
34 from rhodecode.model.caching_query import FromCache
34 from sqlalchemy.orm import joinedload
35 from sqlalchemy.orm import joinedload
35 from sqlalchemy.orm.session import make_transient
36 from sqlalchemy.orm.session import make_transient
@@ -219,7 +220,79 b' class ScmModel(object):'
219 self.sa.rollback()
220 self.sa.rollback()
220
221
221
222
223 def toggle_following_repo(self, follow_repo_id, user_id):
222
224
225 f = self.sa.query(UserFollowing)\
226 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
227 .filter(UserFollowing.user_id == user_id).scalar()
228
229 if f is not None:
230 try:
231 self.sa.delete(f)
232 self.sa.commit()
233 return
234 except:
235 log.error(traceback.format_exc())
236 self.sa.rollback()
237 raise
238
239
240 try:
241 f = UserFollowing()
242 f.user_id = user_id
243 f.follows_repo_id = follow_repo_id
244 self.sa.add(f)
245 self.sa.commit()
246 except:
247 log.error(traceback.format_exc())
248 self.sa.rollback()
249 raise
250
251 def toggle_following_user(self, follow_user_id , user_id):
252 f = self.sa.query(UserFollowing)\
253 .filter(UserFollowing.follows_user_id == follow_user_id)\
254 .filter(UserFollowing.user_id == user_id).scalar()
255
256 if f is not None:
257 try:
258 self.sa.delete(f)
259 self.sa.commit()
260 return
261 except:
262 log.error(traceback.format_exc())
263 self.sa.rollback()
264 raise
265
266 try:
267 f = UserFollowing()
268 f.user_id = user_id
269 f.follows_user_id = follow_user_id
270 self.sa.add(f)
271 self.sa.commit()
272 except:
273 log.error(traceback.format_exc())
274 self.sa.rollback()
275 raise
276
277 def is_following_repo(self, repo_name, user_id):
278 r = self.sa.query(Repository)\
279 .filter(Repository.repo_name == repo_name).scalar()
280
281 f = self.sa.query(UserFollowing)\
282 .filter(UserFollowing.follows_repository == r)\
283 .filter(UserFollowing.user_id == user_id).scalar()
284
285 return f is not None
286
287 def is_following_user(self, username, user_id):
288 u = self.sa.query(User)\
289 .filter(User.username == username).scalar()
290
291 f = self.sa.query(UserFollowing)\
292 .filter(UserFollowing.follows_user == u)\
293 .filter(UserFollowing.user_id == user_id).scalar()
294
295 return f is not None
223
296
224
297
225 def _should_invalidate(self, repo_name):
298 def _should_invalidate(self, repo_name):
@@ -1776,6 +1776,26 b' background:#556CB5;'
1776 color:#FFF;
1776 color:#FFF;
1777 }
1777 }
1778
1778
1779 .follow{
1780 background:url("../images/icons/heart_add.png") no-repeat scroll 3px;
1781 height: 16px;
1782 width: 20px;
1783 cursor: pointer;
1784 display: block;
1785 float: right;
1786 margin-top: 2px;
1787 }
1788
1789 .following{
1790 background:url("../images/icons/heart_delete.png") no-repeat scroll 3px;
1791 height: 16px;
1792 width: 20px;
1793 cursor: pointer;
1794 display: block;
1795 float: right;
1796 margin-top: 2px;
1797 }
1798
1779 .add_icon {
1799 .add_icon {
1780 background:url("../images/icons/add.png") no-repeat scroll 3px;
1800 background:url("../images/icons/add.png") no-repeat scroll 3px;
1781 height:16px;
1801 height:16px;
@@ -239,6 +239,15 b''
239 </li>
239 </li>
240
240
241 <li>
241 <li>
242 <a title="${_('Journal')}" href="${h.url('journal')}">
243 <span class="icon">
244 <img src="/images/icons/book.png" alt="${_('Journal')}" />
245 </span>
246 <span>${_('Journal')}</span>
247 </a>
248 </li>
249
250 <li>
242 <a title="${_('Search')}" href="${h.url('search')}">
251 <a title="${_('Search')}" href="${h.url('search')}">
243 <span class="icon">
252 <span class="icon">
244 <img src="/images/icons/search_16.png" alt="${_('Search')}" />
253 <img src="/images/icons/search_16.png" alt="${_('Search')}" />
@@ -286,6 +295,50 b''
286 <script type="text/javascript" src="/js/yui2a.js"></script>
295 <script type="text/javascript" src="/js/yui2a.js"></script>
287 <!--[if IE]><script language="javascript" type="text/javascript" src="/js/excanvas.min.js"></script><![endif]-->
296 <!--[if IE]><script language="javascript" type="text/javascript" src="/js/excanvas.min.js"></script><![endif]-->
288 <script type="text/javascript" src="/js/yui.flot.js"></script>
297 <script type="text/javascript" src="/js/yui.flot.js"></script>
298
299 <script type="text/javascript">
300 var base_url ='/_admin/toggle_following';
301 var YUC = YAHOO.util.Connect;
302 var YUD = YAHOO.util.Dom;
303
304
305 function onSuccess(){
306
307 var f = YUD.get('follow_toggle');
308 if(f.getAttribute('class')=='follow'){
309 f.setAttribute('class','following');
310 f.setAttribute('title',"${_('Stop following this repository')}");
311
312 }
313 else{
314 f.setAttribute('class','follow');
315 f.setAttribute('title',"${_('Start following this repository')}");
316 }
317 }
318
319 function toggleFollowingUser(fallows_user_id,token){
320 args = 'follows_user_id='+fallows_user_id;
321 args+= '&auth_token='+token;
322 YUC.asyncRequest('POST',base_url,{
323 success:function(o){
324 onSuccess();
325 }
326 },args); return false;
327 }
328
329
330 function toggleFollowingRepo(fallows_repo_id,token){
331 args = 'follows_repo_id='+fallows_repo_id;
332 args+= '&auth_token='+token;
333 YUC.asyncRequest('POST',base_url,{
334 success:function(o){
335 onSuccess();
336 }
337 },args); return false;
338 }
339 </script>
340
341
289 </%def>
342 </%def>
290
343
291 <%def name="breadcrumbs()">
344 <%def name="breadcrumbs()">
@@ -55,6 +55,16 b' E.onDOMReady(function(e){'
55 <img style="margin-bottom:2px" class="icon" title="${_('public repository')}" alt="${_('public repository')}" src="/images/icons/lock_open.png"/>
55 <img style="margin-bottom:2px" class="icon" title="${_('public repository')}" alt="${_('public repository')}" src="/images/icons/lock_open.png"/>
56 %endif
56 %endif
57 <span style="font-size: 1.6em;font-weight: bold;vertical-align: baseline;">${c.repo_info.name}</span>
57 <span style="font-size: 1.6em;font-weight: bold;vertical-align: baseline;">${c.repo_info.name}</span>
58
59 %if c.following:
60 <span id="follow_toggle" class="following" title="${_('Stop following this repository')}"
61 onclick="javascript:toggleFollowingRepo(${c.repo_info.dbrepo.repo_id},'${str(h.get_token())}')">
62 </span>
63 %else:
64 <span id="follow_toggle" class="follow" title="${_('Start following this repository')}"
65 onclick="javascript:toggleFollowingRepo(${c.repo_info.dbrepo.repo_id},'${str(h.get_token())}')">
66 </span>
67 %endif
58 <br/>
68 <br/>
59 %if c.repo_info.dbrepo.fork:
69 %if c.repo_info.dbrepo.fork:
60 <span style="margin-top:5px">
70 <span style="margin-top:5px">
@@ -73,10 +73,6 b' class TestFilesController(TestController'
73 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
73 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
74 f_path='vcs/nodes.py'))
74 f_path='vcs/nodes.py'))
75
75
76
77
78 #tests...
79
80 #test or history
76 #test or history
81 assert """<select id="diff1" name="diff1">
77 assert """<select id="diff1" name="diff1">
82 <option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776</option>
78 <option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776</option>
@@ -131,12 +127,6 b' removed extra unicode conversion in diff'
131 f_path='vcs/nodes.py'))
127 f_path='vcs/nodes.py'))
132
128
133
129
134
135
136
137
138 print response.body
139
140 assert """<option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776</option>
130 assert """<option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776</option>
141 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35</option>
131 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35</option>
142 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c</option>
132 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c</option>
General Comments 0
You need to be logged in to leave comments. Login now