##// END OF EJS Templates
final implementation of #210 journal filtering.
marcink -
r3070:cc7eedb5 beta
parent child Browse files
Show More
@@ -1,142 +1,149 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.admin.admin
3 rhodecode.controllers.admin.admin
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Controller for Admin panel of Rhodecode
6 Controller for Admin panel of Rhodecode
7
7
8 :created_on: Apr 7, 2010
8 :created_on: Apr 7, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import logging
26 import logging
27
27
28 from pylons import request, tmpl_context as c, url
28 from pylons import request, tmpl_context as c, url
29 from sqlalchemy.orm import joinedload
29 from sqlalchemy.orm import joinedload
30 from webhelpers.paginate import Page
30 from webhelpers.paginate import Page
31 from whoosh.qparser.default import QueryParser
31 from whoosh.qparser.default import QueryParser
32 from whoosh import query
32 from whoosh import query
33 from sqlalchemy.sql.expression import or_
33 from sqlalchemy.sql.expression import or_, and_
34
34
35 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
35 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
36 from rhodecode.lib.base import BaseController, render
36 from rhodecode.lib.base import BaseController, render
37 from rhodecode.model.db import UserLog, User
37 from rhodecode.model.db import UserLog, User
38 from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix
38 from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix
39 from rhodecode.lib.indexers import JOURNAL_SCHEMA
39 from rhodecode.lib.indexers import JOURNAL_SCHEMA
40 from whoosh.qparser.dateparse import DateParserPlugin
40
41
41
42
42 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
43
44
44
45
45 def _filter(user_log, search_term):
46 def _journal_filter(user_log, search_term):
46 """
47 """
47 Filters sqlalchemy user_log based on search_term with whoosh Query language
48 Filters sqlalchemy user_log based on search_term with whoosh Query language
48 http://packages.python.org/Whoosh/querylang.html
49 http://packages.python.org/Whoosh/querylang.html
49
50
50 :param user_log:
51 :param user_log:
51 :param search_term:
52 :param search_term:
52 """
53 """
53 log.debug('Initial search term: %r' % search_term)
54 log.debug('Initial search term: %r' % search_term)
54 qry = None
55 qry = None
55 if search_term:
56 if search_term:
56 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
57 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
58 qp.add_plugin(DateParserPlugin())
57 qry = qp.parse(unicode(search_term))
59 qry = qp.parse(unicode(search_term))
58 log.debug('Filtering using parsed query %r' % qry)
60 log.debug('Filtering using parsed query %r' % qry)
59
61
60 def wildcard_handler(col, wc_term):
62 def wildcard_handler(col, wc_term):
61 if wc_term.startswith('*') and not wc_term.endswith('*'):
63 if wc_term.startswith('*') and not wc_term.endswith('*'):
62 #postfix == endswith
64 #postfix == endswith
63 wc_term = remove_prefix(wc_term, prefix='*')
65 wc_term = remove_prefix(wc_term, prefix='*')
64 return getattr(col, 'endswith')(wc_term)
66 return getattr(col, 'endswith')(wc_term)
65 elif wc_term.startswith('*') and wc_term.endswith('*'):
67 elif wc_term.startswith('*') and wc_term.endswith('*'):
66 #wildcard == ilike
68 #wildcard == ilike
67 wc_term = remove_prefix(wc_term, prefix='*')
69 wc_term = remove_prefix(wc_term, prefix='*')
68 wc_term = remove_suffix(wc_term, suffix='*')
70 wc_term = remove_suffix(wc_term, suffix='*')
69 return getattr(col, 'contains')(wc_term)
71 return getattr(col, 'contains')(wc_term)
70
72
71 def get_filterion(field, val, term):
73 def get_filterion(field, val, term):
72
74
73 if field == 'repository':
75 if field == 'repository':
74 field = getattr(UserLog, 'repository_name')
76 field = getattr(UserLog, 'repository_name')
75 elif field == 'ip':
77 elif field == 'ip':
76 field = getattr(UserLog, 'user_ip')
78 field = getattr(UserLog, 'user_ip')
77 elif field == 'date':
79 elif field == 'date':
78 field = getattr(UserLog, 'action_date')
80 field = getattr(UserLog, 'action_date')
79 elif field == 'username':
81 elif field == 'username':
80 field = getattr(UserLog, 'username')
82 field = getattr(UserLog, 'username')
81 else:
83 else:
82 field = getattr(UserLog, field)
84 field = getattr(UserLog, field)
83 log.debug('filter field: %s val=>%s' % (field, val))
85 log.debug('filter field: %s val=>%s' % (field, val))
84
86
85 #sql filtering
87 #sql filtering
86 if isinstance(term, query.Wildcard):
88 if isinstance(term, query.Wildcard):
87 return wildcard_handler(field, val)
89 return wildcard_handler(field, val)
88 elif isinstance(term, query.Prefix):
90 elif isinstance(term, query.Prefix):
89 return field.startswith(val)
91 return field.startswith(val)
92 elif isinstance(term, query.DateRange):
93 return and_(field >= val[0], field <= val[1])
90 return field == val
94 return field == val
91
95
92 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard)):
96 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
97 query.DateRange)):
93 if not isinstance(qry, query.And):
98 if not isinstance(qry, query.And):
94 qry = [qry]
99 qry = [qry]
95 for term in qry:
100 for term in qry:
96 field = term.fieldname
101 field = term.fieldname
97 val = term.text
102 val = (term.text if not isinstance(term, query.DateRange)
103 else [term.startdate, term.enddate])
98 user_log = user_log.filter(get_filterion(field, val, term))
104 user_log = user_log.filter(get_filterion(field, val, term))
99 elif isinstance(qry, query.Or):
105 elif isinstance(qry, query.Or):
100 filters = []
106 filters = []
101 for term in qry:
107 for term in qry:
102 field = term.fieldname
108 field = term.fieldname
103 val = term.text
109 val = (term.text if not isinstance(term, query.DateRange)
110 else [term.startdate, term.enddate])
104 filters.append(get_filterion(field, val, term))
111 filters.append(get_filterion(field, val, term))
105 user_log = user_log.filter(or_(*filters))
112 user_log = user_log.filter(or_(*filters))
106
113
107 return user_log
114 return user_log
108
115
109
116
110 class AdminController(BaseController):
117 class AdminController(BaseController):
111
118
112 @LoginRequired()
119 @LoginRequired()
113 def __before__(self):
120 def __before__(self):
114 super(AdminController, self).__before__()
121 super(AdminController, self).__before__()
115
122
116 @HasPermissionAllDecorator('hg.admin')
123 @HasPermissionAllDecorator('hg.admin')
117 def index(self):
124 def index(self):
118 users_log = UserLog.query()\
125 users_log = UserLog.query()\
119 .options(joinedload(UserLog.user))\
126 .options(joinedload(UserLog.user))\
120 .options(joinedload(UserLog.repository))
127 .options(joinedload(UserLog.repository))
121
128
122 #FILTERING
129 #FILTERING
123 c.search_term = request.GET.get('filter')
130 c.search_term = request.GET.get('filter')
124 try:
131 try:
125 users_log = _filter(users_log, c.search_term)
132 users_log = _journal_filter(users_log, c.search_term)
126 except:
133 except:
127 # we want this to crash for now
134 # we want this to crash for now
128 raise
135 raise
129
136
130 users_log = users_log.order_by(UserLog.action_date.desc())
137 users_log = users_log.order_by(UserLog.action_date.desc())
131
138
132 p = safe_int(request.params.get('page', 1), 1)
139 p = safe_int(request.params.get('page', 1), 1)
133
140
134 def url_generator(**kw):
141 def url_generator(**kw):
135 return url.current(filter=c.search_term, **kw)
142 return url.current(filter=c.search_term, **kw)
136
143
137 c.users_log = Page(users_log, page=p, items_per_page=10, url=url_generator)
144 c.users_log = Page(users_log, page=p, items_per_page=10, url=url_generator)
138 c.log_data = render('admin/admin_log.html')
145 c.log_data = render('admin/admin_log.html')
139
146
140 if request.environ.get('HTTP_X_PARTIAL_XHR'):
147 if request.environ.get('HTTP_X_PARTIAL_XHR'):
141 return c.log_data
148 return c.log_data
142 return render('admin/admin.html')
149 return render('admin/admin.html')
@@ -1,299 +1,311 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.journal
3 rhodecode.controllers.journal
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Journal controller for pylons
6 Journal controller for pylons
7
7
8 :created_on: Nov 21, 2010
8 :created_on: Nov 21, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import logging
25 import logging
26 from itertools import groupby
26 from itertools import groupby
27
27
28 from sqlalchemy import or_
28 from sqlalchemy import or_
29 from sqlalchemy.orm import joinedload
29 from sqlalchemy.orm import joinedload
30 from webhelpers.paginate import Page
30 from webhelpers.paginate import Page
31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32
32
33 from webob.exc import HTTPBadRequest
33 from webob.exc import HTTPBadRequest
34 from pylons import request, tmpl_context as c, response, url
34 from pylons import request, tmpl_context as c, response, url
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36
36
37 import rhodecode.lib.helpers as h
37 import rhodecode.lib.helpers as h
38 from rhodecode.lib.auth import LoginRequired, NotAnonymous
38 from rhodecode.lib.auth import LoginRequired, NotAnonymous
39 from rhodecode.lib.base import BaseController, render
39 from rhodecode.lib.base import BaseController, render
40 from rhodecode.model.db import UserLog, UserFollowing, Repository, User
40 from rhodecode.model.db import UserLog, UserFollowing, Repository, User
41 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
42 from sqlalchemy.sql.expression import func
42 from sqlalchemy.sql.expression import func
43 from rhodecode.model.scm import ScmModel
43 from rhodecode.model.scm import ScmModel
44 from rhodecode.lib.utils2 import safe_int
44 from rhodecode.lib.utils2 import safe_int
45 from rhodecode.controllers.admin.admin import _journal_filter
45
46
46 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
47
48
48
49
49 class JournalController(BaseController):
50 class JournalController(BaseController):
50
51
51 def __before__(self):
52 def __before__(self):
52 super(JournalController, self).__before__()
53 super(JournalController, self).__before__()
53 self.language = 'en-us'
54 self.language = 'en-us'
54 self.ttl = "5"
55 self.ttl = "5"
55 self.feed_nr = 20
56 self.feed_nr = 20
56
57
57 @LoginRequired()
58 @LoginRequired()
58 @NotAnonymous()
59 @NotAnonymous()
59 def index(self):
60 def index(self):
60 # Return a rendered template
61 # Return a rendered template
61 p = safe_int(request.params.get('page', 1), 1)
62 p = safe_int(request.params.get('page', 1), 1)
62 c.user = User.get(self.rhodecode_user.user_id)
63 c.user = User.get(self.rhodecode_user.user_id)
63 c.following = self.sa.query(UserFollowing)\
64 c.following = self.sa.query(UserFollowing)\
64 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
65 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
65 .options(joinedload(UserFollowing.follows_repository))\
66 .options(joinedload(UserFollowing.follows_repository))\
66 .all()
67 .all()
67
68
69 #FILTERING
70 c.search_term = request.GET.get('filter')
68 journal = self._get_journal_data(c.following)
71 journal = self._get_journal_data(c.following)
69
72
70 c.journal_pager = Page(journal, page=p, items_per_page=20)
73 def url_generator(**kw):
74 return url.current(filter=c.search_term, **kw)
75
76 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
71 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
77 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
72
78
73 c.journal_data = render('journal/journal_data.html')
79 c.journal_data = render('journal/journal_data.html')
74 if request.environ.get('HTTP_X_PARTIAL_XHR'):
80 if request.environ.get('HTTP_X_PARTIAL_XHR'):
75 return c.journal_data
81 return c.journal_data
76 return render('journal/journal.html')
82 return render('journal/journal.html')
77
83
78 @LoginRequired()
84 @LoginRequired()
79 @NotAnonymous()
85 @NotAnonymous()
80 def index_my_repos(self):
86 def index_my_repos(self):
81 c.user = User.get(self.rhodecode_user.user_id)
87 c.user = User.get(self.rhodecode_user.user_id)
82 if request.environ.get('HTTP_X_PARTIAL_XHR'):
88 if request.environ.get('HTTP_X_PARTIAL_XHR'):
83 all_repos = self.sa.query(Repository)\
89 all_repos = self.sa.query(Repository)\
84 .filter(Repository.user_id == c.user.user_id)\
90 .filter(Repository.user_id == c.user.user_id)\
85 .order_by(func.lower(Repository.repo_name)).all()
91 .order_by(func.lower(Repository.repo_name)).all()
86 c.user_repos = ScmModel().get_repos(all_repos)
92 c.user_repos = ScmModel().get_repos(all_repos)
87 return render('journal/journal_page_repos.html')
93 return render('journal/journal_page_repos.html')
88
94
89 @LoginRequired(api_access=True)
95 @LoginRequired(api_access=True)
90 @NotAnonymous()
96 @NotAnonymous()
91 def journal_atom(self):
97 def journal_atom(self):
92 """
98 """
93 Produce an atom-1.0 feed via feedgenerator module
99 Produce an atom-1.0 feed via feedgenerator module
94 """
100 """
95 following = self.sa.query(UserFollowing)\
101 following = self.sa.query(UserFollowing)\
96 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
102 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
97 .options(joinedload(UserFollowing.follows_repository))\
103 .options(joinedload(UserFollowing.follows_repository))\
98 .all()
104 .all()
99 return self._atom_feed(following, public=False)
105 return self._atom_feed(following, public=False)
100
106
101 @LoginRequired(api_access=True)
107 @LoginRequired(api_access=True)
102 @NotAnonymous()
108 @NotAnonymous()
103 def journal_rss(self):
109 def journal_rss(self):
104 """
110 """
105 Produce an rss feed via feedgenerator module
111 Produce an rss feed via feedgenerator module
106 """
112 """
107 following = self.sa.query(UserFollowing)\
113 following = self.sa.query(UserFollowing)\
108 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
114 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
109 .options(joinedload(UserFollowing.follows_repository))\
115 .options(joinedload(UserFollowing.follows_repository))\
110 .all()
116 .all()
111 return self._rss_feed(following, public=False)
117 return self._rss_feed(following, public=False)
112
118
113 def _get_daily_aggregate(self, journal):
119 def _get_daily_aggregate(self, journal):
114 groups = []
120 groups = []
115 for k, g in groupby(journal, lambda x: x.action_as_day):
121 for k, g in groupby(journal, lambda x: x.action_as_day):
116 user_group = []
122 user_group = []
117 #groupby username if it's a present value, else fallback to journal username
123 #groupby username if it's a present value, else fallback to journal username
118 for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
124 for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
119 l = list(g2)
125 l = list(g2)
120 user_group.append((l[0].user, l))
126 user_group.append((l[0].user, l))
121
127
122 groups.append((k, user_group,))
128 groups.append((k, user_group,))
123
129
124 return groups
130 return groups
125
131
126 def _get_journal_data(self, following_repos):
132 def _get_journal_data(self, following_repos):
127 repo_ids = [x.follows_repository.repo_id for x in following_repos
133 repo_ids = [x.follows_repository.repo_id for x in following_repos
128 if x.follows_repository is not None]
134 if x.follows_repository is not None]
129 user_ids = [x.follows_user.user_id for x in following_repos
135 user_ids = [x.follows_user.user_id for x in following_repos
130 if x.follows_user is not None]
136 if x.follows_user is not None]
131
137
132 filtering_criterion = None
138 filtering_criterion = None
133
139
134 if repo_ids and user_ids:
140 if repo_ids and user_ids:
135 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
141 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
136 UserLog.user_id.in_(user_ids))
142 UserLog.user_id.in_(user_ids))
137 if repo_ids and not user_ids:
143 if repo_ids and not user_ids:
138 filtering_criterion = UserLog.repository_id.in_(repo_ids)
144 filtering_criterion = UserLog.repository_id.in_(repo_ids)
139 if not repo_ids and user_ids:
145 if not repo_ids and user_ids:
140 filtering_criterion = UserLog.user_id.in_(user_ids)
146 filtering_criterion = UserLog.user_id.in_(user_ids)
141 if filtering_criterion is not None:
147 if filtering_criterion is not None:
142 journal = self.sa.query(UserLog)\
148 journal = self.sa.query(UserLog)\
143 .options(joinedload(UserLog.user))\
149 .options(joinedload(UserLog.user))\
144 .options(joinedload(UserLog.repository))\
150 .options(joinedload(UserLog.repository))
145 .filter(filtering_criterion)\
151 #filter
146 .order_by(UserLog.action_date.desc())
152 try:
153 journal = _journal_filter(journal, c.search_term)
154 except:
155 # we want this to crash for now
156 raise
157 journal = journal.filter(filtering_criterion)\
158 .order_by(UserLog.action_date.desc())
147 else:
159 else:
148 journal = []
160 journal = []
149
161
150 return journal
162 return journal
151
163
152 @LoginRequired()
164 @LoginRequired()
153 @NotAnonymous()
165 @NotAnonymous()
154 def toggle_following(self):
166 def toggle_following(self):
155 cur_token = request.POST.get('auth_token')
167 cur_token = request.POST.get('auth_token')
156 token = h.get_token()
168 token = h.get_token()
157 if cur_token == token:
169 if cur_token == token:
158
170
159 user_id = request.POST.get('follows_user_id')
171 user_id = request.POST.get('follows_user_id')
160 if user_id:
172 if user_id:
161 try:
173 try:
162 self.scm_model.toggle_following_user(user_id,
174 self.scm_model.toggle_following_user(user_id,
163 self.rhodecode_user.user_id)
175 self.rhodecode_user.user_id)
164 Session.commit()
176 Session.commit()
165 return 'ok'
177 return 'ok'
166 except:
178 except:
167 raise HTTPBadRequest()
179 raise HTTPBadRequest()
168
180
169 repo_id = request.POST.get('follows_repo_id')
181 repo_id = request.POST.get('follows_repo_id')
170 if repo_id:
182 if repo_id:
171 try:
183 try:
172 self.scm_model.toggle_following_repo(repo_id,
184 self.scm_model.toggle_following_repo(repo_id,
173 self.rhodecode_user.user_id)
185 self.rhodecode_user.user_id)
174 Session.commit()
186 Session.commit()
175 return 'ok'
187 return 'ok'
176 except:
188 except:
177 raise HTTPBadRequest()
189 raise HTTPBadRequest()
178
190
179 log.debug('token mismatch %s vs %s' % (cur_token, token))
191 log.debug('token mismatch %s vs %s' % (cur_token, token))
180 raise HTTPBadRequest()
192 raise HTTPBadRequest()
181
193
182 @LoginRequired()
194 @LoginRequired()
183 def public_journal(self):
195 def public_journal(self):
184 # Return a rendered template
196 # Return a rendered template
185 p = safe_int(request.params.get('page', 1), 1)
197 p = safe_int(request.params.get('page', 1), 1)
186
198
187 c.following = self.sa.query(UserFollowing)\
199 c.following = self.sa.query(UserFollowing)\
188 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
200 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
189 .options(joinedload(UserFollowing.follows_repository))\
201 .options(joinedload(UserFollowing.follows_repository))\
190 .all()
202 .all()
191
203
192 journal = self._get_journal_data(c.following)
204 journal = self._get_journal_data(c.following)
193
205
194 c.journal_pager = Page(journal, page=p, items_per_page=20)
206 c.journal_pager = Page(journal, page=p, items_per_page=20)
195
207
196 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
208 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
197
209
198 c.journal_data = render('journal/journal_data.html')
210 c.journal_data = render('journal/journal_data.html')
199 if request.environ.get('HTTP_X_PARTIAL_XHR'):
211 if request.environ.get('HTTP_X_PARTIAL_XHR'):
200 return c.journal_data
212 return c.journal_data
201 return render('journal/public_journal.html')
213 return render('journal/public_journal.html')
202
214
203 def _atom_feed(self, repos, public=True):
215 def _atom_feed(self, repos, public=True):
204 journal = self._get_journal_data(repos)
216 journal = self._get_journal_data(repos)
205 if public:
217 if public:
206 _link = url('public_journal_atom', qualified=True)
218 _link = url('public_journal_atom', qualified=True)
207 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
219 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
208 'atom feed')
220 'atom feed')
209 else:
221 else:
210 _link = url('journal_atom', qualified=True)
222 _link = url('journal_atom', qualified=True)
211 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
223 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
212
224
213 feed = Atom1Feed(title=_desc,
225 feed = Atom1Feed(title=_desc,
214 link=_link,
226 link=_link,
215 description=_desc,
227 description=_desc,
216 language=self.language,
228 language=self.language,
217 ttl=self.ttl)
229 ttl=self.ttl)
218
230
219 for entry in journal[:self.feed_nr]:
231 for entry in journal[:self.feed_nr]:
220 action, action_extra, ico = h.action_parser(entry, feed=True)
232 action, action_extra, ico = h.action_parser(entry, feed=True)
221 title = "%s - %s %s" % (entry.user.short_contact, action(),
233 title = "%s - %s %s" % (entry.user.short_contact, action(),
222 entry.repository.repo_name)
234 entry.repository.repo_name)
223 desc = action_extra()
235 desc = action_extra()
224 _url = None
236 _url = None
225 if entry.repository is not None:
237 if entry.repository is not None:
226 _url = url('changelog_home',
238 _url = url('changelog_home',
227 repo_name=entry.repository.repo_name,
239 repo_name=entry.repository.repo_name,
228 qualified=True)
240 qualified=True)
229
241
230 feed.add_item(title=title,
242 feed.add_item(title=title,
231 pubdate=entry.action_date,
243 pubdate=entry.action_date,
232 link=_url or url('', qualified=True),
244 link=_url or url('', qualified=True),
233 author_email=entry.user.email,
245 author_email=entry.user.email,
234 author_name=entry.user.full_contact,
246 author_name=entry.user.full_contact,
235 description=desc)
247 description=desc)
236
248
237 response.content_type = feed.mime_type
249 response.content_type = feed.mime_type
238 return feed.writeString('utf-8')
250 return feed.writeString('utf-8')
239
251
240 def _rss_feed(self, repos, public=True):
252 def _rss_feed(self, repos, public=True):
241 journal = self._get_journal_data(repos)
253 journal = self._get_journal_data(repos)
242 if public:
254 if public:
243 _link = url('public_journal_atom', qualified=True)
255 _link = url('public_journal_atom', qualified=True)
244 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
256 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
245 'rss feed')
257 'rss feed')
246 else:
258 else:
247 _link = url('journal_atom', qualified=True)
259 _link = url('journal_atom', qualified=True)
248 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
260 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
249
261
250 feed = Rss201rev2Feed(title=_desc,
262 feed = Rss201rev2Feed(title=_desc,
251 link=_link,
263 link=_link,
252 description=_desc,
264 description=_desc,
253 language=self.language,
265 language=self.language,
254 ttl=self.ttl)
266 ttl=self.ttl)
255
267
256 for entry in journal[:self.feed_nr]:
268 for entry in journal[:self.feed_nr]:
257 action, action_extra, ico = h.action_parser(entry, feed=True)
269 action, action_extra, ico = h.action_parser(entry, feed=True)
258 title = "%s - %s %s" % (entry.user.short_contact, action(),
270 title = "%s - %s %s" % (entry.user.short_contact, action(),
259 entry.repository.repo_name)
271 entry.repository.repo_name)
260 desc = action_extra()
272 desc = action_extra()
261 _url = None
273 _url = None
262 if entry.repository is not None:
274 if entry.repository is not None:
263 _url = url('changelog_home',
275 _url = url('changelog_home',
264 repo_name=entry.repository.repo_name,
276 repo_name=entry.repository.repo_name,
265 qualified=True)
277 qualified=True)
266
278
267 feed.add_item(title=title,
279 feed.add_item(title=title,
268 pubdate=entry.action_date,
280 pubdate=entry.action_date,
269 link=_url or url('', qualified=True),
281 link=_url or url('', qualified=True),
270 author_email=entry.user.email,
282 author_email=entry.user.email,
271 author_name=entry.user.full_contact,
283 author_name=entry.user.full_contact,
272 description=desc)
284 description=desc)
273
285
274 response.content_type = feed.mime_type
286 response.content_type = feed.mime_type
275 return feed.writeString('utf-8')
287 return feed.writeString('utf-8')
276
288
277 @LoginRequired(api_access=True)
289 @LoginRequired(api_access=True)
278 def public_journal_atom(self):
290 def public_journal_atom(self):
279 """
291 """
280 Produce an atom-1.0 feed via feedgenerator module
292 Produce an atom-1.0 feed via feedgenerator module
281 """
293 """
282 c.following = self.sa.query(UserFollowing)\
294 c.following = self.sa.query(UserFollowing)\
283 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
295 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
284 .options(joinedload(UserFollowing.follows_repository))\
296 .options(joinedload(UserFollowing.follows_repository))\
285 .all()
297 .all()
286
298
287 return self._atom_feed(c.following)
299 return self._atom_feed(c.following)
288
300
289 @LoginRequired(api_access=True)
301 @LoginRequired(api_access=True)
290 def public_journal_rss(self):
302 def public_journal_rss(self):
291 """
303 """
292 Produce an rss2 feed via feedgenerator module
304 Produce an rss2 feed via feedgenerator module
293 """
305 """
294 c.following = self.sa.query(UserFollowing)\
306 c.following = self.sa.query(UserFollowing)\
295 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
307 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
296 .options(joinedload(UserFollowing.follows_repository))\
308 .options(joinedload(UserFollowing.follows_repository))\
297 .all()
309 .all()
298
310
299 return self._rss_feed(c.following)
311 return self._rss_feed(c.following)
@@ -1,1137 +1,1158 b''
1 """Helper functions
1 """Helper functions
2
2
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
6 import random
7 import hashlib
7 import hashlib
8 import StringIO
8 import StringIO
9 import urllib
9 import urllib
10 import math
10 import math
11 import logging
11 import logging
12 import re
12 import re
13 import urlparse
13 import urlparse
14 import textwrap
14
15
15 from datetime import datetime
16 from datetime import datetime
16 from pygments.formatters.html import HtmlFormatter
17 from pygments.formatters.html import HtmlFormatter
17 from pygments import highlight as code_highlight
18 from pygments import highlight as code_highlight
18 from pylons import url, request, config
19 from pylons import url, request, config
19 from pylons.i18n.translation import _, ungettext
20 from pylons.i18n.translation import _, ungettext
20 from hashlib import md5
21 from hashlib import md5
21
22
22 from webhelpers.html import literal, HTML, escape
23 from webhelpers.html import literal, HTML, escape
23 from webhelpers.html.tools import *
24 from webhelpers.html.tools import *
24 from webhelpers.html.builder import make_tag
25 from webhelpers.html.builder import make_tag
25 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
26 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
26 end_form, file, form, hidden, image, javascript_link, link_to, \
27 end_form, file, form, hidden, image, javascript_link, link_to, \
27 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
28 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
28 submit, text, password, textarea, title, ul, xml_declaration, radio
29 submit, text, password, textarea, title, ul, xml_declaration, radio
29 from webhelpers.html.tools import auto_link, button_to, highlight, \
30 from webhelpers.html.tools import auto_link, button_to, highlight, \
30 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
31 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
31 from webhelpers.number import format_byte_size, format_bit_size
32 from webhelpers.number import format_byte_size, format_bit_size
32 from webhelpers.pylonslib import Flash as _Flash
33 from webhelpers.pylonslib import Flash as _Flash
33 from webhelpers.pylonslib.secure_form import secure_form
34 from webhelpers.pylonslib.secure_form import secure_form
34 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
35 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
35 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
36 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
36 replace_whitespace, urlify, truncate, wrap_paragraphs
37 replace_whitespace, urlify, truncate, wrap_paragraphs
37 from webhelpers.date import time_ago_in_words
38 from webhelpers.date import time_ago_in_words
38 from webhelpers.paginate import Page
39 from webhelpers.paginate import Page
39 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
40 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
40 convert_boolean_attrs, NotGiven, _make_safe_id_component
41 convert_boolean_attrs, NotGiven, _make_safe_id_component
41
42
42 from rhodecode.lib.annotate import annotate_highlight
43 from rhodecode.lib.annotate import annotate_highlight
43 from rhodecode.lib.utils import repo_name_slug
44 from rhodecode.lib.utils import repo_name_slug
44 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
45 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
45 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict
46 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict
46 from rhodecode.lib.markup_renderer import MarkupRenderer
47 from rhodecode.lib.markup_renderer import MarkupRenderer
47 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
48 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
48 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
49 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
49 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
50 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.db import URL_SEP, Permission
52 from rhodecode.model.db import URL_SEP, Permission
52
53
53 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
54
55
55
56
56 html_escape_table = {
57 html_escape_table = {
57 "&": "&amp;",
58 "&": "&amp;",
58 '"': "&quot;",
59 '"': "&quot;",
59 "'": "&apos;",
60 "'": "&apos;",
60 ">": "&gt;",
61 ">": "&gt;",
61 "<": "&lt;",
62 "<": "&lt;",
62 }
63 }
63
64
64
65
65 def html_escape(text):
66 def html_escape(text):
66 """Produce entities within text."""
67 """Produce entities within text."""
67 return "".join(html_escape_table.get(c, c) for c in text)
68 return "".join(html_escape_table.get(c, c) for c in text)
68
69
69
70
70 def shorter(text, size=20):
71 def shorter(text, size=20):
71 postfix = '...'
72 postfix = '...'
72 if len(text) > size:
73 if len(text) > size:
73 return text[:size - len(postfix)] + postfix
74 return text[:size - len(postfix)] + postfix
74 return text
75 return text
75
76
76
77
77 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
78 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
78 """
79 """
79 Reset button
80 Reset button
80 """
81 """
81 _set_input_attrs(attrs, type, name, value)
82 _set_input_attrs(attrs, type, name, value)
82 _set_id_attr(attrs, id, name)
83 _set_id_attr(attrs, id, name)
83 convert_boolean_attrs(attrs, ["disabled"])
84 convert_boolean_attrs(attrs, ["disabled"])
84 return HTML.input(**attrs)
85 return HTML.input(**attrs)
85
86
86 reset = _reset
87 reset = _reset
87 safeid = _make_safe_id_component
88 safeid = _make_safe_id_component
88
89
89
90
90 def FID(raw_id, path):
91 def FID(raw_id, path):
91 """
92 """
92 Creates a uniqe ID for filenode based on it's hash of path and revision
93 Creates a uniqe ID for filenode based on it's hash of path and revision
93 it's safe to use in urls
94 it's safe to use in urls
94
95
95 :param raw_id:
96 :param raw_id:
96 :param path:
97 :param path:
97 """
98 """
98
99
99 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
100 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
100
101
101
102
102 def get_token():
103 def get_token():
103 """Return the current authentication token, creating one if one doesn't
104 """Return the current authentication token, creating one if one doesn't
104 already exist.
105 already exist.
105 """
106 """
106 token_key = "_authentication_token"
107 token_key = "_authentication_token"
107 from pylons import session
108 from pylons import session
108 if not token_key in session:
109 if not token_key in session:
109 try:
110 try:
110 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
111 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
111 except AttributeError: # Python < 2.4
112 except AttributeError: # Python < 2.4
112 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
113 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
113 session[token_key] = token
114 session[token_key] = token
114 if hasattr(session, 'save'):
115 if hasattr(session, 'save'):
115 session.save()
116 session.save()
116 return session[token_key]
117 return session[token_key]
117
118
118
119
119 class _GetError(object):
120 class _GetError(object):
120 """Get error from form_errors, and represent it as span wrapped error
121 """Get error from form_errors, and represent it as span wrapped error
121 message
122 message
122
123
123 :param field_name: field to fetch errors for
124 :param field_name: field to fetch errors for
124 :param form_errors: form errors dict
125 :param form_errors: form errors dict
125 """
126 """
126
127
127 def __call__(self, field_name, form_errors):
128 def __call__(self, field_name, form_errors):
128 tmpl = """<span class="error_msg">%s</span>"""
129 tmpl = """<span class="error_msg">%s</span>"""
129 if form_errors and field_name in form_errors:
130 if form_errors and field_name in form_errors:
130 return literal(tmpl % form_errors.get(field_name))
131 return literal(tmpl % form_errors.get(field_name))
131
132
132 get_error = _GetError()
133 get_error = _GetError()
133
134
134
135
135 class _ToolTip(object):
136 class _ToolTip(object):
136
137
137 def __call__(self, tooltip_title, trim_at=50):
138 def __call__(self, tooltip_title, trim_at=50):
138 """
139 """
139 Special function just to wrap our text into nice formatted
140 Special function just to wrap our text into nice formatted
140 autowrapped text
141 autowrapped text
141
142
142 :param tooltip_title:
143 :param tooltip_title:
143 """
144 """
144 tooltip_title = escape(tooltip_title)
145 tooltip_title = escape(tooltip_title)
145 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
146 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
146 return tooltip_title
147 return tooltip_title
147 tooltip = _ToolTip()
148 tooltip = _ToolTip()
148
149
149
150
150 class _FilesBreadCrumbs(object):
151 class _FilesBreadCrumbs(object):
151
152
152 def __call__(self, repo_name, rev, paths):
153 def __call__(self, repo_name, rev, paths):
153 if isinstance(paths, str):
154 if isinstance(paths, str):
154 paths = safe_unicode(paths)
155 paths = safe_unicode(paths)
155 url_l = [link_to(repo_name, url('files_home',
156 url_l = [link_to(repo_name, url('files_home',
156 repo_name=repo_name,
157 repo_name=repo_name,
157 revision=rev, f_path=''),
158 revision=rev, f_path=''),
158 class_='ypjax-link')]
159 class_='ypjax-link')]
159 paths_l = paths.split('/')
160 paths_l = paths.split('/')
160 for cnt, p in enumerate(paths_l):
161 for cnt, p in enumerate(paths_l):
161 if p != '':
162 if p != '':
162 url_l.append(link_to(p,
163 url_l.append(link_to(p,
163 url('files_home',
164 url('files_home',
164 repo_name=repo_name,
165 repo_name=repo_name,
165 revision=rev,
166 revision=rev,
166 f_path='/'.join(paths_l[:cnt + 1])
167 f_path='/'.join(paths_l[:cnt + 1])
167 ),
168 ),
168 class_='ypjax-link'
169 class_='ypjax-link'
169 )
170 )
170 )
171 )
171
172
172 return literal('/'.join(url_l))
173 return literal('/'.join(url_l))
173
174
174 files_breadcrumbs = _FilesBreadCrumbs()
175 files_breadcrumbs = _FilesBreadCrumbs()
175
176
176
177
177 class CodeHtmlFormatter(HtmlFormatter):
178 class CodeHtmlFormatter(HtmlFormatter):
178 """
179 """
179 My code Html Formatter for source codes
180 My code Html Formatter for source codes
180 """
181 """
181
182
182 def wrap(self, source, outfile):
183 def wrap(self, source, outfile):
183 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
184 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
184
185
185 def _wrap_code(self, source):
186 def _wrap_code(self, source):
186 for cnt, it in enumerate(source):
187 for cnt, it in enumerate(source):
187 i, t = it
188 i, t = it
188 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
189 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
189 yield i, t
190 yield i, t
190
191
191 def _wrap_tablelinenos(self, inner):
192 def _wrap_tablelinenos(self, inner):
192 dummyoutfile = StringIO.StringIO()
193 dummyoutfile = StringIO.StringIO()
193 lncount = 0
194 lncount = 0
194 for t, line in inner:
195 for t, line in inner:
195 if t:
196 if t:
196 lncount += 1
197 lncount += 1
197 dummyoutfile.write(line)
198 dummyoutfile.write(line)
198
199
199 fl = self.linenostart
200 fl = self.linenostart
200 mw = len(str(lncount + fl - 1))
201 mw = len(str(lncount + fl - 1))
201 sp = self.linenospecial
202 sp = self.linenospecial
202 st = self.linenostep
203 st = self.linenostep
203 la = self.lineanchors
204 la = self.lineanchors
204 aln = self.anchorlinenos
205 aln = self.anchorlinenos
205 nocls = self.noclasses
206 nocls = self.noclasses
206 if sp:
207 if sp:
207 lines = []
208 lines = []
208
209
209 for i in range(fl, fl + lncount):
210 for i in range(fl, fl + lncount):
210 if i % st == 0:
211 if i % st == 0:
211 if i % sp == 0:
212 if i % sp == 0:
212 if aln:
213 if aln:
213 lines.append('<a href="#%s%d" class="special">%*d</a>' %
214 lines.append('<a href="#%s%d" class="special">%*d</a>' %
214 (la, i, mw, i))
215 (la, i, mw, i))
215 else:
216 else:
216 lines.append('<span class="special">%*d</span>' % (mw, i))
217 lines.append('<span class="special">%*d</span>' % (mw, i))
217 else:
218 else:
218 if aln:
219 if aln:
219 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
220 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
220 else:
221 else:
221 lines.append('%*d' % (mw, i))
222 lines.append('%*d' % (mw, i))
222 else:
223 else:
223 lines.append('')
224 lines.append('')
224 ls = '\n'.join(lines)
225 ls = '\n'.join(lines)
225 else:
226 else:
226 lines = []
227 lines = []
227 for i in range(fl, fl + lncount):
228 for i in range(fl, fl + lncount):
228 if i % st == 0:
229 if i % st == 0:
229 if aln:
230 if aln:
230 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
231 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
231 else:
232 else:
232 lines.append('%*d' % (mw, i))
233 lines.append('%*d' % (mw, i))
233 else:
234 else:
234 lines.append('')
235 lines.append('')
235 ls = '\n'.join(lines)
236 ls = '\n'.join(lines)
236
237
237 # in case you wonder about the seemingly redundant <div> here: since the
238 # in case you wonder about the seemingly redundant <div> here: since the
238 # content in the other cell also is wrapped in a div, some browsers in
239 # content in the other cell also is wrapped in a div, some browsers in
239 # some configurations seem to mess up the formatting...
240 # some configurations seem to mess up the formatting...
240 if nocls:
241 if nocls:
241 yield 0, ('<table class="%stable">' % self.cssclass +
242 yield 0, ('<table class="%stable">' % self.cssclass +
242 '<tr><td><div class="linenodiv" '
243 '<tr><td><div class="linenodiv" '
243 'style="background-color: #f0f0f0; padding-right: 10px">'
244 'style="background-color: #f0f0f0; padding-right: 10px">'
244 '<pre style="line-height: 125%">' +
245 '<pre style="line-height: 125%">' +
245 ls + '</pre></div></td><td id="hlcode" class="code">')
246 ls + '</pre></div></td><td id="hlcode" class="code">')
246 else:
247 else:
247 yield 0, ('<table class="%stable">' % self.cssclass +
248 yield 0, ('<table class="%stable">' % self.cssclass +
248 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
249 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
249 ls + '</pre></div></td><td id="hlcode" class="code">')
250 ls + '</pre></div></td><td id="hlcode" class="code">')
250 yield 0, dummyoutfile.getvalue()
251 yield 0, dummyoutfile.getvalue()
251 yield 0, '</td></tr></table>'
252 yield 0, '</td></tr></table>'
252
253
253
254
254 def pygmentize(filenode, **kwargs):
255 def pygmentize(filenode, **kwargs):
255 """pygmentize function using pygments
256 """pygmentize function using pygments
256
257
257 :param filenode:
258 :param filenode:
258 """
259 """
259
260
260 return literal(code_highlight(filenode.content,
261 return literal(code_highlight(filenode.content,
261 filenode.lexer, CodeHtmlFormatter(**kwargs)))
262 filenode.lexer, CodeHtmlFormatter(**kwargs)))
262
263
263
264
264 def pygmentize_annotation(repo_name, filenode, **kwargs):
265 def pygmentize_annotation(repo_name, filenode, **kwargs):
265 """
266 """
266 pygmentize function for annotation
267 pygmentize function for annotation
267
268
268 :param filenode:
269 :param filenode:
269 """
270 """
270
271
271 color_dict = {}
272 color_dict = {}
272
273
273 def gen_color(n=10000):
274 def gen_color(n=10000):
274 """generator for getting n of evenly distributed colors using
275 """generator for getting n of evenly distributed colors using
275 hsv color and golden ratio. It always return same order of colors
276 hsv color and golden ratio. It always return same order of colors
276
277
277 :returns: RGB tuple
278 :returns: RGB tuple
278 """
279 """
279
280
280 def hsv_to_rgb(h, s, v):
281 def hsv_to_rgb(h, s, v):
281 if s == 0.0:
282 if s == 0.0:
282 return v, v, v
283 return v, v, v
283 i = int(h * 6.0) # XXX assume int() truncates!
284 i = int(h * 6.0) # XXX assume int() truncates!
284 f = (h * 6.0) - i
285 f = (h * 6.0) - i
285 p = v * (1.0 - s)
286 p = v * (1.0 - s)
286 q = v * (1.0 - s * f)
287 q = v * (1.0 - s * f)
287 t = v * (1.0 - s * (1.0 - f))
288 t = v * (1.0 - s * (1.0 - f))
288 i = i % 6
289 i = i % 6
289 if i == 0:
290 if i == 0:
290 return v, t, p
291 return v, t, p
291 if i == 1:
292 if i == 1:
292 return q, v, p
293 return q, v, p
293 if i == 2:
294 if i == 2:
294 return p, v, t
295 return p, v, t
295 if i == 3:
296 if i == 3:
296 return p, q, v
297 return p, q, v
297 if i == 4:
298 if i == 4:
298 return t, p, v
299 return t, p, v
299 if i == 5:
300 if i == 5:
300 return v, p, q
301 return v, p, q
301
302
302 golden_ratio = 0.618033988749895
303 golden_ratio = 0.618033988749895
303 h = 0.22717784590367374
304 h = 0.22717784590367374
304
305
305 for _ in xrange(n):
306 for _ in xrange(n):
306 h += golden_ratio
307 h += golden_ratio
307 h %= 1
308 h %= 1
308 HSV_tuple = [h, 0.95, 0.95]
309 HSV_tuple = [h, 0.95, 0.95]
309 RGB_tuple = hsv_to_rgb(*HSV_tuple)
310 RGB_tuple = hsv_to_rgb(*HSV_tuple)
310 yield map(lambda x: str(int(x * 256)), RGB_tuple)
311 yield map(lambda x: str(int(x * 256)), RGB_tuple)
311
312
312 cgenerator = gen_color()
313 cgenerator = gen_color()
313
314
314 def get_color_string(cs):
315 def get_color_string(cs):
315 if cs in color_dict:
316 if cs in color_dict:
316 col = color_dict[cs]
317 col = color_dict[cs]
317 else:
318 else:
318 col = color_dict[cs] = cgenerator.next()
319 col = color_dict[cs] = cgenerator.next()
319 return "color: rgb(%s)! important;" % (', '.join(col))
320 return "color: rgb(%s)! important;" % (', '.join(col))
320
321
321 def url_func(repo_name):
322 def url_func(repo_name):
322
323
323 def _url_func(changeset):
324 def _url_func(changeset):
324 author = changeset.author
325 author = changeset.author
325 date = changeset.date
326 date = changeset.date
326 message = tooltip(changeset.message)
327 message = tooltip(changeset.message)
327
328
328 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
329 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
329 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
330 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
330 "</b> %s<br/></div>")
331 "</b> %s<br/></div>")
331
332
332 tooltip_html = tooltip_html % (author, date, message)
333 tooltip_html = tooltip_html % (author, date, message)
333 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
334 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
334 short_id(changeset.raw_id))
335 short_id(changeset.raw_id))
335 uri = link_to(
336 uri = link_to(
336 lnk_format,
337 lnk_format,
337 url('changeset_home', repo_name=repo_name,
338 url('changeset_home', repo_name=repo_name,
338 revision=changeset.raw_id),
339 revision=changeset.raw_id),
339 style=get_color_string(changeset.raw_id),
340 style=get_color_string(changeset.raw_id),
340 class_='tooltip',
341 class_='tooltip',
341 title=tooltip_html
342 title=tooltip_html
342 )
343 )
343
344
344 uri += '\n'
345 uri += '\n'
345 return uri
346 return uri
346 return _url_func
347 return _url_func
347
348
348 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
349 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
349
350
350
351
351 def is_following_repo(repo_name, user_id):
352 def is_following_repo(repo_name, user_id):
352 from rhodecode.model.scm import ScmModel
353 from rhodecode.model.scm import ScmModel
353 return ScmModel().is_following_repo(repo_name, user_id)
354 return ScmModel().is_following_repo(repo_name, user_id)
354
355
355 flash = _Flash()
356 flash = _Flash()
356
357
357 #==============================================================================
358 #==============================================================================
358 # SCM FILTERS available via h.
359 # SCM FILTERS available via h.
359 #==============================================================================
360 #==============================================================================
360 from rhodecode.lib.vcs.utils import author_name, author_email
361 from rhodecode.lib.vcs.utils import author_name, author_email
361 from rhodecode.lib.utils2 import credentials_filter, age as _age
362 from rhodecode.lib.utils2 import credentials_filter, age as _age
362 from rhodecode.model.db import User, ChangesetStatus
363 from rhodecode.model.db import User, ChangesetStatus
363
364
364 age = lambda x: _age(x)
365 age = lambda x: _age(x)
365 capitalize = lambda x: x.capitalize()
366 capitalize = lambda x: x.capitalize()
366 email = author_email
367 email = author_email
367 short_id = lambda x: x[:12]
368 short_id = lambda x: x[:12]
368 hide_credentials = lambda x: ''.join(credentials_filter(x))
369 hide_credentials = lambda x: ''.join(credentials_filter(x))
369
370
370
371
371 def fmt_date(date):
372 def fmt_date(date):
372 if date:
373 if date:
373 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
374 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
374 return date.strftime(_fmt).decode('utf8')
375 return date.strftime(_fmt).decode('utf8')
375
376
376 return ""
377 return ""
377
378
378
379
379 def is_git(repository):
380 def is_git(repository):
380 if hasattr(repository, 'alias'):
381 if hasattr(repository, 'alias'):
381 _type = repository.alias
382 _type = repository.alias
382 elif hasattr(repository, 'repo_type'):
383 elif hasattr(repository, 'repo_type'):
383 _type = repository.repo_type
384 _type = repository.repo_type
384 else:
385 else:
385 _type = repository
386 _type = repository
386 return _type == 'git'
387 return _type == 'git'
387
388
388
389
389 def is_hg(repository):
390 def is_hg(repository):
390 if hasattr(repository, 'alias'):
391 if hasattr(repository, 'alias'):
391 _type = repository.alias
392 _type = repository.alias
392 elif hasattr(repository, 'repo_type'):
393 elif hasattr(repository, 'repo_type'):
393 _type = repository.repo_type
394 _type = repository.repo_type
394 else:
395 else:
395 _type = repository
396 _type = repository
396 return _type == 'hg'
397 return _type == 'hg'
397
398
398
399
399 def email_or_none(author):
400 def email_or_none(author):
400 # extract email from the commit string
401 # extract email from the commit string
401 _email = email(author)
402 _email = email(author)
402 if _email != '':
403 if _email != '':
403 # check it against RhodeCode database, and use the MAIN email for this
404 # check it against RhodeCode database, and use the MAIN email for this
404 # user
405 # user
405 user = User.get_by_email(_email, case_insensitive=True, cache=True)
406 user = User.get_by_email(_email, case_insensitive=True, cache=True)
406 if user is not None:
407 if user is not None:
407 return user.email
408 return user.email
408 return _email
409 return _email
409
410
410 # See if it contains a username we can get an email from
411 # See if it contains a username we can get an email from
411 user = User.get_by_username(author_name(author), case_insensitive=True,
412 user = User.get_by_username(author_name(author), case_insensitive=True,
412 cache=True)
413 cache=True)
413 if user is not None:
414 if user is not None:
414 return user.email
415 return user.email
415
416
416 # No valid email, not a valid user in the system, none!
417 # No valid email, not a valid user in the system, none!
417 return None
418 return None
418
419
419
420
420 def person(author, show_attr="username_and_name"):
421 def person(author, show_attr="username_and_name"):
421 # attr to return from fetched user
422 # attr to return from fetched user
422 person_getter = lambda usr: getattr(usr, show_attr)
423 person_getter = lambda usr: getattr(usr, show_attr)
423
424
424 # Valid email in the attribute passed, see if they're in the system
425 # Valid email in the attribute passed, see if they're in the system
425 _email = email(author)
426 _email = email(author)
426 if _email != '':
427 if _email != '':
427 user = User.get_by_email(_email, case_insensitive=True, cache=True)
428 user = User.get_by_email(_email, case_insensitive=True, cache=True)
428 if user is not None:
429 if user is not None:
429 return person_getter(user)
430 return person_getter(user)
430 return _email
431 return _email
431
432
432 # Maybe it's a username?
433 # Maybe it's a username?
433 _author = author_name(author)
434 _author = author_name(author)
434 user = User.get_by_username(_author, case_insensitive=True,
435 user = User.get_by_username(_author, case_insensitive=True,
435 cache=True)
436 cache=True)
436 if user is not None:
437 if user is not None:
437 return person_getter(user)
438 return person_getter(user)
438
439
439 # Still nothing? Just pass back the author name then
440 # Still nothing? Just pass back the author name then
440 return _author
441 return _author
441
442
442
443
443 def person_by_id(id_, show_attr="username_and_name"):
444 def person_by_id(id_, show_attr="username_and_name"):
444 # attr to return from fetched user
445 # attr to return from fetched user
445 person_getter = lambda usr: getattr(usr, show_attr)
446 person_getter = lambda usr: getattr(usr, show_attr)
446
447
447 #maybe it's an ID ?
448 #maybe it's an ID ?
448 if str(id_).isdigit() or isinstance(id_, int):
449 if str(id_).isdigit() or isinstance(id_, int):
449 id_ = int(id_)
450 id_ = int(id_)
450 user = User.get(id_)
451 user = User.get(id_)
451 if user is not None:
452 if user is not None:
452 return person_getter(user)
453 return person_getter(user)
453 return id_
454 return id_
454
455
455
456
456 def desc_stylize(value):
457 def desc_stylize(value):
457 """
458 """
458 converts tags from value into html equivalent
459 converts tags from value into html equivalent
459
460
460 :param value:
461 :param value:
461 """
462 """
462 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
463 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
463 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
464 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
464 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
465 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
465 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
466 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
466 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z\-\/]*)\]',
467 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z\-\/]*)\]',
467 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
468 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
468 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
469 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
469 '<div class="metatag" tag="lang">\\2</div>', value)
470 '<div class="metatag" tag="lang">\\2</div>', value)
470 value = re.sub(r'\[([a-z]+)\]',
471 value = re.sub(r'\[([a-z]+)\]',
471 '<div class="metatag" tag="\\1">\\1</div>', value)
472 '<div class="metatag" tag="\\1">\\1</div>', value)
472
473
473 return value
474 return value
474
475
475
476
476 def bool2icon(value):
477 def bool2icon(value):
477 """Returns True/False values represented as small html image of true/false
478 """Returns True/False values represented as small html image of true/false
478 icons
479 icons
479
480
480 :param value: bool value
481 :param value: bool value
481 """
482 """
482
483
483 if value is True:
484 if value is True:
484 return HTML.tag('img', src=url("/images/icons/accept.png"),
485 return HTML.tag('img', src=url("/images/icons/accept.png"),
485 alt=_('True'))
486 alt=_('True'))
486
487
487 if value is False:
488 if value is False:
488 return HTML.tag('img', src=url("/images/icons/cancel.png"),
489 return HTML.tag('img', src=url("/images/icons/cancel.png"),
489 alt=_('False'))
490 alt=_('False'))
490
491
491 return value
492 return value
492
493
493
494
494 def action_parser(user_log, feed=False, parse_cs=False):
495 def action_parser(user_log, feed=False, parse_cs=False):
495 """
496 """
496 This helper will action_map the specified string action into translated
497 This helper will action_map the specified string action into translated
497 fancy names with icons and links
498 fancy names with icons and links
498
499
499 :param user_log: user log instance
500 :param user_log: user log instance
500 :param feed: use output for feeds (no html and fancy icons)
501 :param feed: use output for feeds (no html and fancy icons)
501 :param parse_cs: parse Changesets into VCS instances
502 :param parse_cs: parse Changesets into VCS instances
502 """
503 """
503
504
504 action = user_log.action
505 action = user_log.action
505 action_params = ' '
506 action_params = ' '
506
507
507 x = action.split(':')
508 x = action.split(':')
508
509
509 if len(x) > 1:
510 if len(x) > 1:
510 action, action_params = x
511 action, action_params = x
511
512
512 def get_cs_links():
513 def get_cs_links():
513 revs_limit = 3 # display this amount always
514 revs_limit = 3 # display this amount always
514 revs_top_limit = 50 # show upto this amount of changesets hidden
515 revs_top_limit = 50 # show upto this amount of changesets hidden
515 revs_ids = action_params.split(',')
516 revs_ids = action_params.split(',')
516 deleted = user_log.repository is None
517 deleted = user_log.repository is None
517 if deleted:
518 if deleted:
518 return ','.join(revs_ids)
519 return ','.join(revs_ids)
519
520
520 repo_name = user_log.repository.repo_name
521 repo_name = user_log.repository.repo_name
521
522
522 def lnk(rev, repo_name):
523 def lnk(rev, repo_name):
523 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
524 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
524 lazy_cs = True
525 lazy_cs = True
525 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
526 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
526 lazy_cs = False
527 lazy_cs = False
527 lbl = '?'
528 lbl = '?'
528 if rev.op == 'delete_branch':
529 if rev.op == 'delete_branch':
529 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
530 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
530 title = ''
531 title = ''
531 elif rev.op == 'tag':
532 elif rev.op == 'tag':
532 lbl = '%s' % _('Created tag: %s') % rev.ref_name
533 lbl = '%s' % _('Created tag: %s') % rev.ref_name
533 title = ''
534 title = ''
534 _url = '#'
535 _url = '#'
535
536
536 else:
537 else:
537 lbl = '%s' % (rev.short_id[:8])
538 lbl = '%s' % (rev.short_id[:8])
538 _url = url('changeset_home', repo_name=repo_name,
539 _url = url('changeset_home', repo_name=repo_name,
539 revision=rev.raw_id)
540 revision=rev.raw_id)
540 title = tooltip(rev.message)
541 title = tooltip(rev.message)
541 else:
542 else:
542 ## changeset cannot be found/striped/removed etc.
543 ## changeset cannot be found/striped/removed etc.
543 lbl = ('%s' % rev)[:12]
544 lbl = ('%s' % rev)[:12]
544 _url = '#'
545 _url = '#'
545 title = _('Changeset not found')
546 title = _('Changeset not found')
546 if parse_cs:
547 if parse_cs:
547 return link_to(lbl, _url, title=title, class_='tooltip')
548 return link_to(lbl, _url, title=title, class_='tooltip')
548 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
549 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
549 class_='lazy-cs' if lazy_cs else '')
550 class_='lazy-cs' if lazy_cs else '')
550
551
551 revs = []
552 revs = []
552 if len(filter(lambda v: v != '', revs_ids)) > 0:
553 if len(filter(lambda v: v != '', revs_ids)) > 0:
553 repo = None
554 repo = None
554 for rev in revs_ids[:revs_top_limit]:
555 for rev in revs_ids[:revs_top_limit]:
555 _op = _name = None
556 _op = _name = None
556 if len(rev.split('=>')) == 2:
557 if len(rev.split('=>')) == 2:
557 _op, _name = rev.split('=>')
558 _op, _name = rev.split('=>')
558
559
559 # we want parsed changesets, or new log store format is bad
560 # we want parsed changesets, or new log store format is bad
560 if parse_cs:
561 if parse_cs:
561 try:
562 try:
562 if repo is None:
563 if repo is None:
563 repo = user_log.repository.scm_instance
564 repo = user_log.repository.scm_instance
564 _rev = repo.get_changeset(rev)
565 _rev = repo.get_changeset(rev)
565 revs.append(_rev)
566 revs.append(_rev)
566 except ChangesetDoesNotExistError:
567 except ChangesetDoesNotExistError:
567 log.error('cannot find revision %s in this repo' % rev)
568 log.error('cannot find revision %s in this repo' % rev)
568 revs.append(rev)
569 revs.append(rev)
569 continue
570 continue
570 else:
571 else:
571 _rev = AttributeDict({
572 _rev = AttributeDict({
572 'short_id': rev[:12],
573 'short_id': rev[:12],
573 'raw_id': rev,
574 'raw_id': rev,
574 'message': '',
575 'message': '',
575 'op': _op,
576 'op': _op,
576 'ref_name': _name
577 'ref_name': _name
577 })
578 })
578 revs.append(_rev)
579 revs.append(_rev)
579 cs_links = []
580 cs_links = []
580 cs_links.append(" " + ', '.join(
581 cs_links.append(" " + ', '.join(
581 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
582 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
582 )
583 )
583 )
584 )
584
585
585 compare_view = (
586 compare_view = (
586 ' <div class="compare_view tooltip" title="%s">'
587 ' <div class="compare_view tooltip" title="%s">'
587 '<a href="%s">%s</a> </div>' % (
588 '<a href="%s">%s</a> </div>' % (
588 _('Show all combined changesets %s->%s') % (
589 _('Show all combined changesets %s->%s') % (
589 revs_ids[0][:12], revs_ids[-1][:12]
590 revs_ids[0][:12], revs_ids[-1][:12]
590 ),
591 ),
591 url('changeset_home', repo_name=repo_name,
592 url('changeset_home', repo_name=repo_name,
592 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
593 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
593 ),
594 ),
594 _('compare view')
595 _('compare view')
595 )
596 )
596 )
597 )
597
598
598 # if we have exactly one more than normally displayed
599 # if we have exactly one more than normally displayed
599 # just display it, takes less space than displaying
600 # just display it, takes less space than displaying
600 # "and 1 more revisions"
601 # "and 1 more revisions"
601 if len(revs_ids) == revs_limit + 1:
602 if len(revs_ids) == revs_limit + 1:
602 rev = revs[revs_limit]
603 rev = revs[revs_limit]
603 cs_links.append(", " + lnk(rev, repo_name))
604 cs_links.append(", " + lnk(rev, repo_name))
604
605
605 # hidden-by-default ones
606 # hidden-by-default ones
606 if len(revs_ids) > revs_limit + 1:
607 if len(revs_ids) > revs_limit + 1:
607 uniq_id = revs_ids[0]
608 uniq_id = revs_ids[0]
608 html_tmpl = (
609 html_tmpl = (
609 '<span> %s <a class="show_more" id="_%s" '
610 '<span> %s <a class="show_more" id="_%s" '
610 'href="#more">%s</a> %s</span>'
611 'href="#more">%s</a> %s</span>'
611 )
612 )
612 if not feed:
613 if not feed:
613 cs_links.append(html_tmpl % (
614 cs_links.append(html_tmpl % (
614 _('and'),
615 _('and'),
615 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
616 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
616 _('revisions')
617 _('revisions')
617 )
618 )
618 )
619 )
619
620
620 if not feed:
621 if not feed:
621 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
622 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
622 else:
623 else:
623 html_tmpl = '<span id="%s"> %s </span>'
624 html_tmpl = '<span id="%s"> %s </span>'
624
625
625 morelinks = ', '.join(
626 morelinks = ', '.join(
626 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
627 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
627 )
628 )
628
629
629 if len(revs_ids) > revs_top_limit:
630 if len(revs_ids) > revs_top_limit:
630 morelinks += ', ...'
631 morelinks += ', ...'
631
632
632 cs_links.append(html_tmpl % (uniq_id, morelinks))
633 cs_links.append(html_tmpl % (uniq_id, morelinks))
633 if len(revs) > 1:
634 if len(revs) > 1:
634 cs_links.append(compare_view)
635 cs_links.append(compare_view)
635 return ''.join(cs_links)
636 return ''.join(cs_links)
636
637
637 def get_fork_name():
638 def get_fork_name():
638 repo_name = action_params
639 repo_name = action_params
639 _url = url('summary_home', repo_name=repo_name)
640 _url = url('summary_home', repo_name=repo_name)
640 return _('fork name %s') % link_to(action_params, _url)
641 return _('fork name %s') % link_to(action_params, _url)
641
642
642 def get_user_name():
643 def get_user_name():
643 user_name = action_params
644 user_name = action_params
644 return user_name
645 return user_name
645
646
646 def get_users_group():
647 def get_users_group():
647 group_name = action_params
648 group_name = action_params
648 return group_name
649 return group_name
649
650
650 def get_pull_request():
651 def get_pull_request():
651 pull_request_id = action_params
652 pull_request_id = action_params
652 deleted = user_log.repository is None
653 deleted = user_log.repository is None
653 if deleted:
654 if deleted:
654 repo_name = user_log.repository_name
655 repo_name = user_log.repository_name
655 else:
656 else:
656 repo_name = user_log.repository.repo_name
657 repo_name = user_log.repository.repo_name
657 return link_to(_('Pull request #%s') % pull_request_id,
658 return link_to(_('Pull request #%s') % pull_request_id,
658 url('pullrequest_show', repo_name=repo_name,
659 url('pullrequest_show', repo_name=repo_name,
659 pull_request_id=pull_request_id))
660 pull_request_id=pull_request_id))
660
661
661 # action : translated str, callback(extractor), icon
662 # action : translated str, callback(extractor), icon
662 action_map = {
663 action_map = {
663 'user_deleted_repo': (_('[deleted] repository'),
664 'user_deleted_repo': (_('[deleted] repository'),
664 None, 'database_delete.png'),
665 None, 'database_delete.png'),
665 'user_created_repo': (_('[created] repository'),
666 'user_created_repo': (_('[created] repository'),
666 None, 'database_add.png'),
667 None, 'database_add.png'),
667 'user_created_fork': (_('[created] repository as fork'),
668 'user_created_fork': (_('[created] repository as fork'),
668 None, 'arrow_divide.png'),
669 None, 'arrow_divide.png'),
669 'user_forked_repo': (_('[forked] repository'),
670 'user_forked_repo': (_('[forked] repository'),
670 get_fork_name, 'arrow_divide.png'),
671 get_fork_name, 'arrow_divide.png'),
671 'user_updated_repo': (_('[updated] repository'),
672 'user_updated_repo': (_('[updated] repository'),
672 None, 'database_edit.png'),
673 None, 'database_edit.png'),
673 'admin_deleted_repo': (_('[delete] repository'),
674 'admin_deleted_repo': (_('[delete] repository'),
674 None, 'database_delete.png'),
675 None, 'database_delete.png'),
675 'admin_created_repo': (_('[created] repository'),
676 'admin_created_repo': (_('[created] repository'),
676 None, 'database_add.png'),
677 None, 'database_add.png'),
677 'admin_forked_repo': (_('[forked] repository'),
678 'admin_forked_repo': (_('[forked] repository'),
678 None, 'arrow_divide.png'),
679 None, 'arrow_divide.png'),
679 'admin_updated_repo': (_('[updated] repository'),
680 'admin_updated_repo': (_('[updated] repository'),
680 None, 'database_edit.png'),
681 None, 'database_edit.png'),
681 'admin_created_user': (_('[created] user'),
682 'admin_created_user': (_('[created] user'),
682 get_user_name, 'user_add.png'),
683 get_user_name, 'user_add.png'),
683 'admin_updated_user': (_('[updated] user'),
684 'admin_updated_user': (_('[updated] user'),
684 get_user_name, 'user_edit.png'),
685 get_user_name, 'user_edit.png'),
685 'admin_created_users_group': (_('[created] users group'),
686 'admin_created_users_group': (_('[created] users group'),
686 get_users_group, 'group_add.png'),
687 get_users_group, 'group_add.png'),
687 'admin_updated_users_group': (_('[updated] users group'),
688 'admin_updated_users_group': (_('[updated] users group'),
688 get_users_group, 'group_edit.png'),
689 get_users_group, 'group_edit.png'),
689 'user_commented_revision': (_('[commented] on revision in repository'),
690 'user_commented_revision': (_('[commented] on revision in repository'),
690 get_cs_links, 'comment_add.png'),
691 get_cs_links, 'comment_add.png'),
691 'user_commented_pull_request': (_('[commented] on pull request for'),
692 'user_commented_pull_request': (_('[commented] on pull request for'),
692 get_pull_request, 'comment_add.png'),
693 get_pull_request, 'comment_add.png'),
693 'user_closed_pull_request': (_('[closed] pull request for'),
694 'user_closed_pull_request': (_('[closed] pull request for'),
694 get_pull_request, 'tick.png'),
695 get_pull_request, 'tick.png'),
695 'push': (_('[pushed] into'),
696 'push': (_('[pushed] into'),
696 get_cs_links, 'script_add.png'),
697 get_cs_links, 'script_add.png'),
697 'push_local': (_('[committed via RhodeCode] into repository'),
698 'push_local': (_('[committed via RhodeCode] into repository'),
698 get_cs_links, 'script_edit.png'),
699 get_cs_links, 'script_edit.png'),
699 'push_remote': (_('[pulled from remote] into repository'),
700 'push_remote': (_('[pulled from remote] into repository'),
700 get_cs_links, 'connect.png'),
701 get_cs_links, 'connect.png'),
701 'pull': (_('[pulled] from'),
702 'pull': (_('[pulled] from'),
702 None, 'down_16.png'),
703 None, 'down_16.png'),
703 'started_following_repo': (_('[started following] repository'),
704 'started_following_repo': (_('[started following] repository'),
704 None, 'heart_add.png'),
705 None, 'heart_add.png'),
705 'stopped_following_repo': (_('[stopped following] repository'),
706 'stopped_following_repo': (_('[stopped following] repository'),
706 None, 'heart_delete.png'),
707 None, 'heart_delete.png'),
707 }
708 }
708
709
709 action_str = action_map.get(action, action)
710 action_str = action_map.get(action, action)
710 if feed:
711 if feed:
711 action = action_str[0].replace('[', '').replace(']', '')
712 action = action_str[0].replace('[', '').replace(']', '')
712 else:
713 else:
713 action = action_str[0]\
714 action = action_str[0]\
714 .replace('[', '<span class="journal_highlight">')\
715 .replace('[', '<span class="journal_highlight">')\
715 .replace(']', '</span>')
716 .replace(']', '</span>')
716
717
717 action_params_func = lambda: ""
718 action_params_func = lambda: ""
718
719
719 if callable(action_str[1]):
720 if callable(action_str[1]):
720 action_params_func = action_str[1]
721 action_params_func = action_str[1]
721
722
722 def action_parser_icon():
723 def action_parser_icon():
723 action = user_log.action
724 action = user_log.action
724 action_params = None
725 action_params = None
725 x = action.split(':')
726 x = action.split(':')
726
727
727 if len(x) > 1:
728 if len(x) > 1:
728 action, action_params = x
729 action, action_params = x
729
730
730 tmpl = """<img src="%s%s" alt="%s"/>"""
731 tmpl = """<img src="%s%s" alt="%s"/>"""
731 ico = action_map.get(action, ['', '', ''])[2]
732 ico = action_map.get(action, ['', '', ''])[2]
732 return literal(tmpl % ((url('/images/icons/')), ico, action))
733 return literal(tmpl % ((url('/images/icons/')), ico, action))
733
734
734 # returned callbacks we need to call to get
735 # returned callbacks we need to call to get
735 return [lambda: literal(action), action_params_func, action_parser_icon]
736 return [lambda: literal(action), action_params_func, action_parser_icon]
736
737
737
738
738
739
739 #==============================================================================
740 #==============================================================================
740 # PERMS
741 # PERMS
741 #==============================================================================
742 #==============================================================================
742 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
743 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
743 HasRepoPermissionAny, HasRepoPermissionAll
744 HasRepoPermissionAny, HasRepoPermissionAll
744
745
745
746
746 #==============================================================================
747 #==============================================================================
747 # GRAVATAR URL
748 # GRAVATAR URL
748 #==============================================================================
749 #==============================================================================
749
750
750 def gravatar_url(email_address, size=30):
751 def gravatar_url(email_address, size=30):
751 from pylons import url ## doh, we need to re-import url to mock it later
752 from pylons import url ## doh, we need to re-import url to mock it later
752 if(str2bool(config['app_conf'].get('use_gravatar')) and
753 if(str2bool(config['app_conf'].get('use_gravatar')) and
753 config['app_conf'].get('alternative_gravatar_url')):
754 config['app_conf'].get('alternative_gravatar_url')):
754 tmpl = config['app_conf'].get('alternative_gravatar_url', '')
755 tmpl = config['app_conf'].get('alternative_gravatar_url', '')
755 parsed_url = urlparse.urlparse(url.current(qualified=True))
756 parsed_url = urlparse.urlparse(url.current(qualified=True))
756 tmpl = tmpl.replace('{email}', email_address)\
757 tmpl = tmpl.replace('{email}', email_address)\
757 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
758 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
758 .replace('{netloc}', parsed_url.netloc)\
759 .replace('{netloc}', parsed_url.netloc)\
759 .replace('{scheme}', parsed_url.scheme)\
760 .replace('{scheme}', parsed_url.scheme)\
760 .replace('{size}', str(size))
761 .replace('{size}', str(size))
761 return tmpl
762 return tmpl
762
763
763 if (not str2bool(config['app_conf'].get('use_gravatar')) or
764 if (not str2bool(config['app_conf'].get('use_gravatar')) or
764 not email_address or email_address == 'anonymous@rhodecode.org'):
765 not email_address or email_address == 'anonymous@rhodecode.org'):
765 f = lambda a, l: min(l, key=lambda x: abs(x - a))
766 f = lambda a, l: min(l, key=lambda x: abs(x - a))
766 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
767 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
767
768
768 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
769 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
769 default = 'identicon'
770 default = 'identicon'
770 baseurl_nossl = "http://www.gravatar.com/avatar/"
771 baseurl_nossl = "http://www.gravatar.com/avatar/"
771 baseurl_ssl = "https://secure.gravatar.com/avatar/"
772 baseurl_ssl = "https://secure.gravatar.com/avatar/"
772 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
773 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
773
774
774 if isinstance(email_address, unicode):
775 if isinstance(email_address, unicode):
775 #hashlib crashes on unicode items
776 #hashlib crashes on unicode items
776 email_address = safe_str(email_address)
777 email_address = safe_str(email_address)
777 # construct the url
778 # construct the url
778 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
779 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
779 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
780 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
780
781
781 return gravatar_url
782 return gravatar_url
782
783
783
784
784 #==============================================================================
785 #==============================================================================
785 # REPO PAGER, PAGER FOR REPOSITORY
786 # REPO PAGER, PAGER FOR REPOSITORY
786 #==============================================================================
787 #==============================================================================
787 class RepoPage(Page):
788 class RepoPage(Page):
788
789
789 def __init__(self, collection, page=1, items_per_page=20,
790 def __init__(self, collection, page=1, items_per_page=20,
790 item_count=None, url=None, **kwargs):
791 item_count=None, url=None, **kwargs):
791
792
792 """Create a "RepoPage" instance. special pager for paging
793 """Create a "RepoPage" instance. special pager for paging
793 repository
794 repository
794 """
795 """
795 self._url_generator = url
796 self._url_generator = url
796
797
797 # Safe the kwargs class-wide so they can be used in the pager() method
798 # Safe the kwargs class-wide so they can be used in the pager() method
798 self.kwargs = kwargs
799 self.kwargs = kwargs
799
800
800 # Save a reference to the collection
801 # Save a reference to the collection
801 self.original_collection = collection
802 self.original_collection = collection
802
803
803 self.collection = collection
804 self.collection = collection
804
805
805 # The self.page is the number of the current page.
806 # The self.page is the number of the current page.
806 # The first page has the number 1!
807 # The first page has the number 1!
807 try:
808 try:
808 self.page = int(page) # make it int() if we get it as a string
809 self.page = int(page) # make it int() if we get it as a string
809 except (ValueError, TypeError):
810 except (ValueError, TypeError):
810 self.page = 1
811 self.page = 1
811
812
812 self.items_per_page = items_per_page
813 self.items_per_page = items_per_page
813
814
814 # Unless the user tells us how many items the collections has
815 # Unless the user tells us how many items the collections has
815 # we calculate that ourselves.
816 # we calculate that ourselves.
816 if item_count is not None:
817 if item_count is not None:
817 self.item_count = item_count
818 self.item_count = item_count
818 else:
819 else:
819 self.item_count = len(self.collection)
820 self.item_count = len(self.collection)
820
821
821 # Compute the number of the first and last available page
822 # Compute the number of the first and last available page
822 if self.item_count > 0:
823 if self.item_count > 0:
823 self.first_page = 1
824 self.first_page = 1
824 self.page_count = int(math.ceil(float(self.item_count) /
825 self.page_count = int(math.ceil(float(self.item_count) /
825 self.items_per_page))
826 self.items_per_page))
826 self.last_page = self.first_page + self.page_count - 1
827 self.last_page = self.first_page + self.page_count - 1
827
828
828 # Make sure that the requested page number is the range of
829 # Make sure that the requested page number is the range of
829 # valid pages
830 # valid pages
830 if self.page > self.last_page:
831 if self.page > self.last_page:
831 self.page = self.last_page
832 self.page = self.last_page
832 elif self.page < self.first_page:
833 elif self.page < self.first_page:
833 self.page = self.first_page
834 self.page = self.first_page
834
835
835 # Note: the number of items on this page can be less than
836 # Note: the number of items on this page can be less than
836 # items_per_page if the last page is not full
837 # items_per_page if the last page is not full
837 self.first_item = max(0, (self.item_count) - (self.page *
838 self.first_item = max(0, (self.item_count) - (self.page *
838 items_per_page))
839 items_per_page))
839 self.last_item = ((self.item_count - 1) - items_per_page *
840 self.last_item = ((self.item_count - 1) - items_per_page *
840 (self.page - 1))
841 (self.page - 1))
841
842
842 self.items = list(self.collection[self.first_item:self.last_item + 1])
843 self.items = list(self.collection[self.first_item:self.last_item + 1])
843
844
844 # Links to previous and next page
845 # Links to previous and next page
845 if self.page > self.first_page:
846 if self.page > self.first_page:
846 self.previous_page = self.page - 1
847 self.previous_page = self.page - 1
847 else:
848 else:
848 self.previous_page = None
849 self.previous_page = None
849
850
850 if self.page < self.last_page:
851 if self.page < self.last_page:
851 self.next_page = self.page + 1
852 self.next_page = self.page + 1
852 else:
853 else:
853 self.next_page = None
854 self.next_page = None
854
855
855 # No items available
856 # No items available
856 else:
857 else:
857 self.first_page = None
858 self.first_page = None
858 self.page_count = 0
859 self.page_count = 0
859 self.last_page = None
860 self.last_page = None
860 self.first_item = None
861 self.first_item = None
861 self.last_item = None
862 self.last_item = None
862 self.previous_page = None
863 self.previous_page = None
863 self.next_page = None
864 self.next_page = None
864 self.items = []
865 self.items = []
865
866
866 # This is a subclass of the 'list' type. Initialise the list now.
867 # This is a subclass of the 'list' type. Initialise the list now.
867 list.__init__(self, reversed(self.items))
868 list.__init__(self, reversed(self.items))
868
869
869
870
870 def changed_tooltip(nodes):
871 def changed_tooltip(nodes):
871 """
872 """
872 Generates a html string for changed nodes in changeset page.
873 Generates a html string for changed nodes in changeset page.
873 It limits the output to 30 entries
874 It limits the output to 30 entries
874
875
875 :param nodes: LazyNodesGenerator
876 :param nodes: LazyNodesGenerator
876 """
877 """
877 if nodes:
878 if nodes:
878 pref = ': <br/> '
879 pref = ': <br/> '
879 suf = ''
880 suf = ''
880 if len(nodes) > 30:
881 if len(nodes) > 30:
881 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
882 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
882 return literal(pref + '<br/> '.join([safe_unicode(x.path)
883 return literal(pref + '<br/> '.join([safe_unicode(x.path)
883 for x in nodes[:30]]) + suf)
884 for x in nodes[:30]]) + suf)
884 else:
885 else:
885 return ': ' + _('No Files')
886 return ': ' + _('No Files')
886
887
887
888
888 def repo_link(groups_and_repos, last_url=None):
889 def repo_link(groups_and_repos, last_url=None):
889 """
890 """
890 Makes a breadcrumbs link to repo within a group
891 Makes a breadcrumbs link to repo within a group
891 joins &raquo; on each group to create a fancy link
892 joins &raquo; on each group to create a fancy link
892
893
893 ex::
894 ex::
894 group >> subgroup >> repo
895 group >> subgroup >> repo
895
896
896 :param groups_and_repos:
897 :param groups_and_repos:
897 :param last_url:
898 :param last_url:
898 """
899 """
899 groups, repo_name = groups_and_repos
900 groups, repo_name = groups_and_repos
900 last_link = link_to(repo_name, last_url) if last_url else repo_name
901 last_link = link_to(repo_name, last_url) if last_url else repo_name
901
902
902 if not groups:
903 if not groups:
903 if last_url:
904 if last_url:
904 return last_link
905 return last_link
905 return repo_name
906 return repo_name
906 else:
907 else:
907 def make_link(group):
908 def make_link(group):
908 return link_to(group.name,
909 return link_to(group.name,
909 url('repos_group_home', group_name=group.group_name))
910 url('repos_group_home', group_name=group.group_name))
910 return literal(' &raquo; '.join(map(make_link, groups) + [last_link]))
911 return literal(' &raquo; '.join(map(make_link, groups) + [last_link]))
911
912
912
913
913 def fancy_file_stats(stats):
914 def fancy_file_stats(stats):
914 """
915 """
915 Displays a fancy two colored bar for number of added/deleted
916 Displays a fancy two colored bar for number of added/deleted
916 lines of code on file
917 lines of code on file
917
918
918 :param stats: two element list of added/deleted lines of code
919 :param stats: two element list of added/deleted lines of code
919 """
920 """
920 def cgen(l_type, a_v, d_v):
921 def cgen(l_type, a_v, d_v):
921 mapping = {'tr': 'top-right-rounded-corner-mid',
922 mapping = {'tr': 'top-right-rounded-corner-mid',
922 'tl': 'top-left-rounded-corner-mid',
923 'tl': 'top-left-rounded-corner-mid',
923 'br': 'bottom-right-rounded-corner-mid',
924 'br': 'bottom-right-rounded-corner-mid',
924 'bl': 'bottom-left-rounded-corner-mid'}
925 'bl': 'bottom-left-rounded-corner-mid'}
925 map_getter = lambda x: mapping[x]
926 map_getter = lambda x: mapping[x]
926
927
927 if l_type == 'a' and d_v:
928 if l_type == 'a' and d_v:
928 #case when added and deleted are present
929 #case when added and deleted are present
929 return ' '.join(map(map_getter, ['tl', 'bl']))
930 return ' '.join(map(map_getter, ['tl', 'bl']))
930
931
931 if l_type == 'a' and not d_v:
932 if l_type == 'a' and not d_v:
932 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
933 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
933
934
934 if l_type == 'd' and a_v:
935 if l_type == 'd' and a_v:
935 return ' '.join(map(map_getter, ['tr', 'br']))
936 return ' '.join(map(map_getter, ['tr', 'br']))
936
937
937 if l_type == 'd' and not a_v:
938 if l_type == 'd' and not a_v:
938 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
939 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
939
940
940 a, d = stats[0], stats[1]
941 a, d = stats[0], stats[1]
941 width = 100
942 width = 100
942
943
943 if a == 'b':
944 if a == 'b':
944 #binary mode
945 #binary mode
945 b_d = '<div class="bin%s %s" style="width:100%%">%s</div>' % (d, cgen('a', a_v='', d_v=0), 'bin')
946 b_d = '<div class="bin%s %s" style="width:100%%">%s</div>' % (d, cgen('a', a_v='', d_v=0), 'bin')
946 b_a = '<div class="bin1" style="width:0%%">%s</div>' % ('bin')
947 b_a = '<div class="bin1" style="width:0%%">%s</div>' % ('bin')
947 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
948 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
948
949
949 t = stats[0] + stats[1]
950 t = stats[0] + stats[1]
950 unit = float(width) / (t or 1)
951 unit = float(width) / (t or 1)
951
952
952 # needs > 9% of width to be visible or 0 to be hidden
953 # needs > 9% of width to be visible or 0 to be hidden
953 a_p = max(9, unit * a) if a > 0 else 0
954 a_p = max(9, unit * a) if a > 0 else 0
954 d_p = max(9, unit * d) if d > 0 else 0
955 d_p = max(9, unit * d) if d > 0 else 0
955 p_sum = a_p + d_p
956 p_sum = a_p + d_p
956
957
957 if p_sum > width:
958 if p_sum > width:
958 #adjust the percentage to be == 100% since we adjusted to 9
959 #adjust the percentage to be == 100% since we adjusted to 9
959 if a_p > d_p:
960 if a_p > d_p:
960 a_p = a_p - (p_sum - width)
961 a_p = a_p - (p_sum - width)
961 else:
962 else:
962 d_p = d_p - (p_sum - width)
963 d_p = d_p - (p_sum - width)
963
964
964 a_v = a if a > 0 else ''
965 a_v = a if a > 0 else ''
965 d_v = d if d > 0 else ''
966 d_v = d if d > 0 else ''
966
967
967 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
968 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
968 cgen('a', a_v, d_v), a_p, a_v
969 cgen('a', a_v, d_v), a_p, a_v
969 )
970 )
970 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
971 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
971 cgen('d', a_v, d_v), d_p, d_v
972 cgen('d', a_v, d_v), d_p, d_v
972 )
973 )
973 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
974 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
974
975
975
976
976 def urlify_text(text_):
977 def urlify_text(text_):
977
978
978 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
979 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
979 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
980 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
980
981
981 def url_func(match_obj):
982 def url_func(match_obj):
982 url_full = match_obj.groups()[0]
983 url_full = match_obj.groups()[0]
983 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
984 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
984
985
985 return literal(url_pat.sub(url_func, text_))
986 return literal(url_pat.sub(url_func, text_))
986
987
987
988
988 def urlify_changesets(text_, repository):
989 def urlify_changesets(text_, repository):
989 """
990 """
990 Extract revision ids from changeset and make link from them
991 Extract revision ids from changeset and make link from them
991
992
992 :param text_:
993 :param text_:
993 :param repository:
994 :param repository:
994 """
995 """
995
996
996 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
997 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
997
998
998 def url_func(match_obj):
999 def url_func(match_obj):
999 rev = match_obj.groups()[0]
1000 rev = match_obj.groups()[0]
1000 pref = ''
1001 pref = ''
1001 if match_obj.group().startswith(' '):
1002 if match_obj.group().startswith(' '):
1002 pref = ' '
1003 pref = ' '
1003 tmpl = (
1004 tmpl = (
1004 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1005 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1005 '%(rev)s'
1006 '%(rev)s'
1006 '</a>'
1007 '</a>'
1007 )
1008 )
1008 return tmpl % {
1009 return tmpl % {
1009 'pref': pref,
1010 'pref': pref,
1010 'cls': 'revision-link',
1011 'cls': 'revision-link',
1011 'url': url('changeset_home', repo_name=repository, revision=rev),
1012 'url': url('changeset_home', repo_name=repository, revision=rev),
1012 'rev': rev,
1013 'rev': rev,
1013 }
1014 }
1014
1015
1015 newtext = URL_PAT.sub(url_func, text_)
1016 newtext = URL_PAT.sub(url_func, text_)
1016
1017
1017 return newtext
1018 return newtext
1018
1019
1019
1020
1020 def urlify_commit(text_, repository=None, link_=None):
1021 def urlify_commit(text_, repository=None, link_=None):
1021 """
1022 """
1022 Parses given text message and makes proper links.
1023 Parses given text message and makes proper links.
1023 issues are linked to given issue-server, and rest is a changeset link
1024 issues are linked to given issue-server, and rest is a changeset link
1024 if link_ is given, in other case it's a plain text
1025 if link_ is given, in other case it's a plain text
1025
1026
1026 :param text_:
1027 :param text_:
1027 :param repository:
1028 :param repository:
1028 :param link_: changeset link
1029 :param link_: changeset link
1029 """
1030 """
1030 import traceback
1031 import traceback
1031
1032
1032 def escaper(string):
1033 def escaper(string):
1033 return string.replace('<', '&lt;').replace('>', '&gt;')
1034 return string.replace('<', '&lt;').replace('>', '&gt;')
1034
1035
1035 def linkify_others(t, l):
1036 def linkify_others(t, l):
1036 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1037 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1037 links = []
1038 links = []
1038 for e in urls.split(t):
1039 for e in urls.split(t):
1039 if not urls.match(e):
1040 if not urls.match(e):
1040 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1041 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1041 else:
1042 else:
1042 links.append(e)
1043 links.append(e)
1043
1044
1044 return ''.join(links)
1045 return ''.join(links)
1045
1046
1046 # urlify changesets - extrac revisions and make link out of them
1047 # urlify changesets - extrac revisions and make link out of them
1047 newtext = urlify_changesets(escaper(text_), repository)
1048 newtext = urlify_changesets(escaper(text_), repository)
1048
1049
1049 try:
1050 try:
1050 conf = config['app_conf']
1051 conf = config['app_conf']
1051
1052
1052 # allow multiple issue servers to be used
1053 # allow multiple issue servers to be used
1053 valid_indices = [
1054 valid_indices = [
1054 x.group(1)
1055 x.group(1)
1055 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1056 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1056 if x and 'issue_server_link%s' % x.group(1) in conf
1057 if x and 'issue_server_link%s' % x.group(1) in conf
1057 and 'issue_prefix%s' % x.group(1) in conf
1058 and 'issue_prefix%s' % x.group(1) in conf
1058 ]
1059 ]
1059
1060
1060 log.debug('found issue server suffixes `%s` during valuation of: %s'
1061 log.debug('found issue server suffixes `%s` during valuation of: %s'
1061 % (','.join(valid_indices), newtext))
1062 % (','.join(valid_indices), newtext))
1062
1063
1063 for pattern_index in valid_indices:
1064 for pattern_index in valid_indices:
1064 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1065 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1065 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1066 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1066 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1067 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1067
1068
1068 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1069 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1069 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1070 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1070 ISSUE_PREFIX))
1071 ISSUE_PREFIX))
1071
1072
1072 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1073 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1073
1074
1074 def url_func(match_obj):
1075 def url_func(match_obj):
1075 pref = ''
1076 pref = ''
1076 if match_obj.group().startswith(' '):
1077 if match_obj.group().startswith(' '):
1077 pref = ' '
1078 pref = ' '
1078
1079
1079 issue_id = ''.join(match_obj.groups())
1080 issue_id = ''.join(match_obj.groups())
1080 tmpl = (
1081 tmpl = (
1081 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1082 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1082 '%(issue-prefix)s%(id-repr)s'
1083 '%(issue-prefix)s%(id-repr)s'
1083 '</a>'
1084 '</a>'
1084 )
1085 )
1085 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1086 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1086 if repository:
1087 if repository:
1087 url = url.replace('{repo}', repository)
1088 url = url.replace('{repo}', repository)
1088 repo_name = repository.split(URL_SEP)[-1]
1089 repo_name = repository.split(URL_SEP)[-1]
1089 url = url.replace('{repo_name}', repo_name)
1090 url = url.replace('{repo_name}', repo_name)
1090
1091
1091 return tmpl % {
1092 return tmpl % {
1092 'pref': pref,
1093 'pref': pref,
1093 'cls': 'issue-tracker-link',
1094 'cls': 'issue-tracker-link',
1094 'url': url,
1095 'url': url,
1095 'id-repr': issue_id,
1096 'id-repr': issue_id,
1096 'issue-prefix': ISSUE_PREFIX,
1097 'issue-prefix': ISSUE_PREFIX,
1097 'serv': ISSUE_SERVER_LNK,
1098 'serv': ISSUE_SERVER_LNK,
1098 }
1099 }
1099 newtext = URL_PAT.sub(url_func, newtext)
1100 newtext = URL_PAT.sub(url_func, newtext)
1100 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1101 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1101
1102
1102 # if we actually did something above
1103 # if we actually did something above
1103 if link_:
1104 if link_:
1104 # wrap not links into final link => link_
1105 # wrap not links into final link => link_
1105 newtext = linkify_others(newtext, link_)
1106 newtext = linkify_others(newtext, link_)
1106 except:
1107 except:
1107 log.error(traceback.format_exc())
1108 log.error(traceback.format_exc())
1108 pass
1109 pass
1109
1110
1110 return literal(newtext)
1111 return literal(newtext)
1111
1112
1112
1113
1113 def rst(source):
1114 def rst(source):
1114 return literal('<div class="rst-block">%s</div>' %
1115 return literal('<div class="rst-block">%s</div>' %
1115 MarkupRenderer.rst(source))
1116 MarkupRenderer.rst(source))
1116
1117
1117
1118
1118 def rst_w_mentions(source):
1119 def rst_w_mentions(source):
1119 """
1120 """
1120 Wrapped rst renderer with @mention highlighting
1121 Wrapped rst renderer with @mention highlighting
1121
1122
1122 :param source:
1123 :param source:
1123 """
1124 """
1124 return literal('<div class="rst-block">%s</div>' %
1125 return literal('<div class="rst-block">%s</div>' %
1125 MarkupRenderer.rst_with_mentions(source))
1126 MarkupRenderer.rst_with_mentions(source))
1126
1127
1127
1128
1128 def changeset_status(repo, revision):
1129 def changeset_status(repo, revision):
1129 return ChangesetStatusModel().get_status(repo, revision)
1130 return ChangesetStatusModel().get_status(repo, revision)
1130
1131
1131
1132
1132 def changeset_status_lbl(changeset_status):
1133 def changeset_status_lbl(changeset_status):
1133 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1134 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1134
1135
1135
1136
1136 def get_permission_name(key):
1137 def get_permission_name(key):
1137 return dict(Permission.PERMS).get(key)
1138 return dict(Permission.PERMS).get(key)
1139
1140
1141 def journal_filter_help():
1142 return _(textwrap.dedent('''
1143 Example filter terms:
1144 repository:vcs
1145 username:marcin
1146 action:*push*
1147 ip:127.0.0.1
1148 date:20120101
1149 date:[20120101100000 TO 20120102]
1150
1151 Generate wildcards using '*' character:
1152 "repositroy:vcs*" - search everything starting with 'vcs'
1153 "repository:*vcs*" - search for repository containing 'vcs'
1154
1155 Optional AND / OR operators in queries
1156 "repository:vcs OR repository:test"
1157 "username:test AND repository:test*"
1158 '''))
@@ -1,73 +1,56 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('Admin journal')} - ${c.rhodecode_name}
5 ${_('Admin journal')} - ${c.rhodecode_name}
6 </%def>
6 </%def>
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 <form id="filter_form">
9 <form id="filter_form">
10 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="q_filter" size="15" type="text" name="filter" value="${c.search_term or _('quick filter...')}"/>
10 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.search_term or _('journal filter...')}"/>
11 <span class="tooltip" title="${h.tooltip(_('''
11 <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
12 Example search query:
13 "repository:vcs"
14 "username:marcin"
15
16 You can use wildcards using '*'
17 "repositroy:vcs*" - search everything starting with 'vcs'
18 "repository:*vcs*" - search for repository containing 'vcs'
19 Use AND / OR operators in queries
20 "repository:vcs OR repository:test"
21 "username:test AND repository:test*"
22 List of valid search filters:
23 repository:
24 username:
25 action:
26 ip:
27 date:
28 '''))}">?</span>
29 <input type='submit' value="${_('filter')}" class="ui-btn" style="padding:0px 2px 0px 2px;margin:0px"/>
12 <input type='submit' value="${_('filter')}" class="ui-btn" style="padding:0px 2px 0px 2px;margin:0px"/>
30 ${_('Admin journal')} - ${ungettext('%s entry', '%s entries', c.users_log.item_count) % (c.users_log.item_count)}
13 ${_('Admin journal')} - ${ungettext('%s entry', '%s entries', c.users_log.item_count) % (c.users_log.item_count)}
31 </form>
14 </form>
32 ${h.end_form()}
15 ${h.end_form()}
33 </%def>
16 </%def>
34
17
35 <%def name="page_nav()">
18 <%def name="page_nav()">
36 ${self.menu('admin')}
19 ${self.menu('admin')}
37 </%def>
20 </%def>
38 <%def name="main()">
21 <%def name="main()">
39 <div class="box">
22 <div class="box">
40 <!-- box / title -->
23 <!-- box / title -->
41 <div class="title">
24 <div class="title">
42 ${self.breadcrumbs()}
25 ${self.breadcrumbs()}
43 </div>
26 </div>
44 <!-- end box / title -->
27 <!-- end box / title -->
45 <div class="table">
28 <div class="table">
46 <div id="user_log">
29 <div id="user_log">
47 ${c.log_data}
30 ${c.log_data}
48 </div>
31 </div>
49 </div>
32 </div>
50 </div>
33 </div>
51
34
52 <script>
35 <script>
53 YUE.on('q_filter','click',function(){
36 YUE.on('j_filter','click',function(){
54 var qfilter = YUD.get('q_filter');
37 var jfilter = YUD.get('j_filter');
55 if(YUD.hasClass(qfilter, 'initial')){
38 if(YUD.hasClass(jfilter, 'initial')){
56 qfilter.value = '';
39 jfilter.value = '';
57 }
40 }
58 });
41 });
59 var fix_q_filter_width = function(len){
42 var fix_j_filter_width = function(len){
60 YUD.setStyle(YUD.get('q_filter'),'width',Math.max(80, len*6.50)+'px');
43 YUD.setStyle(YUD.get('j_filter'),'width',Math.max(80, len*6.50)+'px');
61 }
44 }
62 YUE.on('q_filter','keyup',function(){
45 YUE.on('j_filter','keyup',function(){
63 fix_q_filter_width(YUD.get('q_filter').value.length);
46 fix_j_filter_width(YUD.get('j_filter').value.length);
64 });
47 });
65 YUE.on('filter_form','submit',function(e){
48 YUE.on('filter_form','submit',function(e){
66 YUE.preventDefault(e)
49 YUE.preventDefault(e)
67 var val = YUD.get('q_filter').value;
50 var val = YUD.get('j_filter').value;
68 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
51 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
69 });
52 });
70 fix_q_filter_width(YUD.get('q_filter').value.length);
53 fix_j_filter_width(YUD.get('j_filter').value.length);
71 </script>
54 </script>
72 </%def>
55 </%def>
73
56
@@ -1,210 +1,238 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3 <%def name="title()">
3 <%def name="title()">
4 ${_('Journal')} - ${c.rhodecode_name}
4 ${_('Journal')} - ${c.rhodecode_name}
5 </%def>
5 </%def>
6 <%def name="breadcrumbs()">
6 <%def name="breadcrumbs()">
7 ${c.rhodecode_name}
7 <h5>
8 <form id="filter_form">
9 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.search_term or _('quick filter...')}"/>
10 <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
11 <input type='submit' value="${_('filter')}" class="ui-btn" style="padding:0px 2px 0px 2px;margin:0px"/>
12 ${_('journal')} - ${ungettext('%s entry', '%s entries', c.journal_pager.item_count) % (c.journal_pager.item_count)}
13 </form>
14 ${h.end_form()}
15 </h5>
8 </%def>
16 </%def>
9 <%def name="page_nav()">
17 <%def name="page_nav()">
10 ${self.menu('home')}
18 ${self.menu('home')}
11 </%def>
19 </%def>
12 <%def name="head_extra()">
20 <%def name="head_extra()">
13 <link href="${h.url('journal_atom', api_key=c.rhodecode_user.api_key)}" rel="alternate" title="${_('ATOM journal feed')}" type="application/atom+xml" />
21 <link href="${h.url('journal_atom', api_key=c.rhodecode_user.api_key)}" rel="alternate" title="${_('ATOM journal feed')}" type="application/atom+xml" />
14 <link href="${h.url('journal_rss', api_key=c.rhodecode_user.api_key)}" rel="alternate" title="${_('RSS journal feed')}" type="application/rss+xml" />
22 <link href="${h.url('journal_rss', api_key=c.rhodecode_user.api_key)}" rel="alternate" title="${_('RSS journal feed')}" type="application/rss+xml" />
15 </%def>
23 </%def>
16 <%def name="main()">
24 <%def name="main()">
17
25
18 <div class="box box-left">
26 <div class="box box-left">
19 <!-- box / title -->
27 <!-- box / title -->
20 <div class="title">
28 <div class="title">
21 <h5>${_('Journal')}</h5>
29 ${self.breadcrumbs()}
22 <ul class="links">
30 <ul class="links">
23 <li>
31 <li>
24 <span><a id="refresh" href="${h.url('journal')}"><img class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/></a></span>
32 <span><a id="refresh" href="${h.url('journal')}"><img class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/></a></span>
25 </li>
33 </li>
26 <li>
34 <li>
27 <span><a href="${h.url('journal_rss', api_key=c.rhodecode_user.api_key)}"><img class="icon" title="${_('RSS feed')}" alt="${_('RSS feed')}" src="${h.url('/images/icons/rss_16.png')}"/></a></span>
35 <span><a href="${h.url('journal_rss', api_key=c.rhodecode_user.api_key)}"><img class="icon" title="${_('RSS feed')}" alt="${_('RSS feed')}" src="${h.url('/images/icons/rss_16.png')}"/></a></span>
28 </li>
36 </li>
29 <li>
37 <li>
30 <span><a href="${h.url('journal_atom', api_key=c.rhodecode_user.api_key)}"><img class="icon" title="${_('ATOM feed')}" alt="${_('ATOM feed')}" src="${h.url('/images/icons/atom.png')}"/></a></span>
38 <span><a href="${h.url('journal_atom', api_key=c.rhodecode_user.api_key)}"><img class="icon" title="${_('ATOM feed')}" alt="${_('ATOM feed')}" src="${h.url('/images/icons/atom.png')}"/></a></span>
31 </li>
39 </li>
32 </ul>
40 </ul>
33 </div>
41 </div>
34 <div id="journal">${c.journal_data}</div>
42 <div id="journal">${c.journal_data}</div>
35 </div>
43 </div>
36 <div class="box box-right">
44 <div class="box box-right">
37 <!-- box / title -->
45 <!-- box / title -->
38 <div class="title">
46 <div class="title">
39 <h5>
47 <h5>
40 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" value="${_('quick filter...')}"/>
48 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" value="${_('quick filter...')}"/>
41 <a id="show_watched" class="link-white" href="#watched">${_('Watched')}</a> / <a id="show_my" class="link-white" href="#my">${_('My repos')}</a>
49 <a id="show_watched" class="link-white" href="#watched">${_('Watched')}</a> / <a id="show_my" class="link-white" href="#my">${_('My repos')}</a>
42 </h5>
50 </h5>
43 %if h.HasPermissionAny('hg.admin','hg.create.repository')():
51 %if h.HasPermissionAny('hg.admin','hg.create.repository')():
44 <ul class="links">
52 <ul class="links">
45 <li>
53 <li>
46 <span>${h.link_to(_('ADD'),h.url('admin_settings_create_repository'))}</span>
54 <span>${h.link_to(_('ADD'),h.url('admin_settings_create_repository'))}</span>
47 </li>
55 </li>
48 </ul>
56 </ul>
49 %endif
57 %endif
50 </div>
58 </div>
51 <!-- end box / title -->
59 <!-- end box / title -->
52 <div id="my" class="table" style="display:none">
60 <div id="my" class="table" style="display:none">
53 ## loaded via AJAX
61 ## loaded via AJAX
54 ${_('Loading...')}
62 ${_('Loading...')}
55 </div>
63 </div>
56
64
57 <div id="watched" class="table">
65 <div id="watched" class="table">
58 %if c.following:
66 %if c.following:
59 <table>
67 <table>
60 <thead>
68 <thead>
61 <tr>
69 <tr>
62 <th class="left">${_('Name')}</th>
70 <th class="left">${_('Name')}</th>
63 </thead>
71 </thead>
64 <tbody>
72 <tbody>
65 %for entry in c.following:
73 %for entry in c.following:
66 <tr>
74 <tr>
67 <td>
75 <td>
68 %if entry.follows_user_id:
76 %if entry.follows_user_id:
69 <img title="${_('following user')}" alt="${_('user')}" src="${h.url('/images/icons/user.png')}"/>
77 <img title="${_('following user')}" alt="${_('user')}" src="${h.url('/images/icons/user.png')}"/>
70 ${entry.follows_user.full_contact}
78 ${entry.follows_user.full_contact}
71 %endif
79 %endif
72
80
73 %if entry.follows_repo_id:
81 %if entry.follows_repo_id:
74 <div style="float:right;padding-right:5px">
82 <div style="float:right;padding-right:5px">
75 <span id="follow_toggle_${entry.follows_repository.repo_id}" class="following" title="${_('Stop following this repository')}"
83 <span id="follow_toggle_${entry.follows_repository.repo_id}" class="following" title="${_('Stop following this repository')}"
76 onclick="javascript:toggleFollowingRepo(this,${entry.follows_repository.repo_id},'${str(h.get_token())}')">
84 onclick="javascript:toggleFollowingRepo(this,${entry.follows_repository.repo_id},'${str(h.get_token())}')">
77 </span>
85 </span>
78 </div>
86 </div>
79
87
80 %if h.is_hg(entry.follows_repository):
88 %if h.is_hg(entry.follows_repository):
81 <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
89 <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
82 %elif h.is_git(entry.follows_repository):
90 %elif h.is_git(entry.follows_repository):
83 <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
91 <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
84 %endif
92 %endif
85
93
86 %if entry.follows_repository.private and c.visual.show_private_icon:
94 %if entry.follows_repository.private and c.visual.show_private_icon:
87 <img class="icon" title="${_('private repository')}" alt="${_('private repository')}" src="${h.url('/images/icons/lock.png')}"/>
95 <img class="icon" title="${_('private repository')}" alt="${_('private repository')}" src="${h.url('/images/icons/lock.png')}"/>
88 %elif not entry.follows_repository.private and c.visual.show_public_icon:
96 %elif not entry.follows_repository.private and c.visual.show_public_icon:
89 <img class="icon" title="${_('public repository')}" alt="${_('public repository')}" src="${h.url('/images/icons/lock_open.png')}"/>
97 <img class="icon" title="${_('public repository')}" alt="${_('public repository')}" src="${h.url('/images/icons/lock_open.png')}"/>
90 %endif
98 %endif
91 <span class="watched_repo">
99 <span class="watched_repo">
92 ${h.link_to(entry.follows_repository.repo_name,h.url('summary_home',repo_name=entry.follows_repository.repo_name))}
100 ${h.link_to(entry.follows_repository.repo_name,h.url('summary_home',repo_name=entry.follows_repository.repo_name))}
93 </span>
101 </span>
94 %endif
102 %endif
95 </td>
103 </td>
96 </tr>
104 </tr>
97 %endfor
105 %endfor
98 </tbody>
106 </tbody>
99 </table>
107 </table>
100 %else:
108 %else:
101 <div style="padding:5px 0px 10px 0px;">
109 <div style="padding:5px 0px 10px 0px;">
102 ${_('You are not following any users or repositories')}
110 ${_('You are not following any users or repositories')}
103 </div>
111 </div>
104 %endif
112 %endif
105 </div>
113 </div>
106 </div>
114 </div>
107
115
108 <script type="text/javascript">
116 <script type="text/javascript">
117
118 YUE.on('j_filter','click',function(){
119 var jfilter = YUD.get('j_filter');
120 if(YUD.hasClass(jfilter, 'initial')){
121 jfilter.value = '';
122 }
123 });
124 var fix_j_filter_width = function(len){
125 YUD.setStyle(YUD.get('j_filter'),'width',Math.max(80, len*6.50)+'px');
126 }
127 YUE.on('j_filter','keyup',function(){
128 fix_j_filter_width(YUD.get('j_filter').value.length);
129 });
130 YUE.on('filter_form','submit',function(e){
131 YUE.preventDefault(e)
132 var val = YUD.get('j_filter').value;
133 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
134 });
135 fix_j_filter_width(YUD.get('j_filter').value.length);
136
109 var show_my = function(e){
137 var show_my = function(e){
110 YUD.setStyle('watched','display','none');
138 YUD.setStyle('watched','display','none');
111 YUD.setStyle('my','display','');
139 YUD.setStyle('my','display','');
112
140
113 var url = "${h.url('admin_settings_my_repos')}";
141 var url = "${h.url('admin_settings_my_repos')}";
114 ypjax(url, 'my', function(){
142 ypjax(url, 'my', function(){
115 tooltip_activate();
143 tooltip_activate();
116 quick_repo_menu();
144 quick_repo_menu();
117 var nodes = YUQ('#my tr td a.repo_name');
145 var nodes = YUQ('#my tr td a.repo_name');
118 var func = function(node){
146 var func = function(node){
119 return node.parentNode.parentNode.parentNode;
147 return node.parentNode.parentNode.parentNode;
120 }
148 }
121 q_filter('q_filter',nodes,func);
149 q_filter('q_filter',nodes,func);
122 });
150 });
123
151
124 }
152 }
125 YUE.on('show_my','click',function(e){
153 YUE.on('show_my','click',function(e){
126 show_my(e);
154 show_my(e);
127 })
155 })
128 var show_watched = function(e){
156 var show_watched = function(e){
129 YUD.setStyle('my','display','none');
157 YUD.setStyle('my','display','none');
130 YUD.setStyle('watched','display','');
158 YUD.setStyle('watched','display','');
131 var nodes = YUQ('#watched .watched_repo a');
159 var nodes = YUQ('#watched .watched_repo a');
132 var target = 'q_filter';
160 var target = 'q_filter';
133 var func = function(node){
161 var func = function(node){
134 return node.parentNode.parentNode;
162 return node.parentNode.parentNode;
135 }
163 }
136 q_filter(target,nodes,func);
164 q_filter(target,nodes,func);
137 }
165 }
138 YUE.on('show_watched','click',function(e){
166 YUE.on('show_watched','click',function(e){
139 show_watched(e);
167 show_watched(e);
140 })
168 })
141 //init watched
169 //init watched
142 show_watched();
170 show_watched();
143
171
144 var tabs = {
172 var tabs = {
145 'watched': show_watched,
173 'watched': show_watched,
146 'my': show_my,
174 'my': show_my,
147 }
175 }
148 var url = location.href.split('#');
176 var url = location.href.split('#');
149 if (url[1]) {
177 if (url[1]) {
150 //We have a hash
178 //We have a hash
151 var tabHash = url[1];
179 var tabHash = url[1];
152 tabs[tabHash]();
180 tabs[tabHash]();
153 }
181 }
154
182
155 YUE.on('refresh','click',function(e){
183 YUE.on('refresh','click',function(e){
156 ypjax(e.currentTarget.href,"journal",function(){
184 ypjax("${h.url.current(filter=c.search_term)}","journal",function(){
157 show_more_event();
185 show_more_event();
158 tooltip_activate();
186 tooltip_activate();
159 show_changeset_tooltip();
187 show_changeset_tooltip();
160 });
188 });
161 YUE.preventDefault(e);
189 YUE.preventDefault(e);
162 });
190 });
163
191
164
192
165 // main table sorting
193 // main table sorting
166 var myColumnDefs = [
194 var myColumnDefs = [
167 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
195 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
168 {key:"name",label:"${_('Name')}",sortable:true,
196 {key:"name",label:"${_('Name')}",sortable:true,
169 sortOptions: { sortFunction: nameSort }},
197 sortOptions: { sortFunction: nameSort }},
170 {key:"tip",label:"${_('Tip')}",sortable:true,
198 {key:"tip",label:"${_('Tip')}",sortable:true,
171 sortOptions: { sortFunction: revisionSort }},
199 sortOptions: { sortFunction: revisionSort }},
172 {key:"action1",label:"",sortable:false},
200 {key:"action1",label:"",sortable:false},
173 {key:"action2",label:"",sortable:false},
201 {key:"action2",label:"",sortable:false},
174 ];
202 ];
175
203
176 var myDataSource = new YAHOO.util.DataSource(YUD.get("repos_list"));
204 var myDataSource = new YAHOO.util.DataSource(YUD.get("repos_list"));
177
205
178 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
206 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
179
207
180 myDataSource.responseSchema = {
208 myDataSource.responseSchema = {
181 fields: [
209 fields: [
182 {key:"menu"},
210 {key:"menu"},
183 {key:"name"},
211 {key:"name"},
184 {key:"tip"},
212 {key:"tip"},
185 {key:"action1"},
213 {key:"action1"},
186 {key:"action2"}
214 {key:"action2"}
187 ]
215 ]
188 };
216 };
189
217
190 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,
218 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,
191 {
219 {
192 sortedBy:{key:"name",dir:"asc"},
220 sortedBy:{key:"name",dir:"asc"},
193 MSG_SORTASC:"${_('Click to sort ascending')}",
221 MSG_SORTASC:"${_('Click to sort ascending')}",
194 MSG_SORTDESC:"${_('Click to sort descending')}",
222 MSG_SORTDESC:"${_('Click to sort descending')}",
195 MSG_EMPTY:"${_('No records found.')}",
223 MSG_EMPTY:"${_('No records found.')}",
196 MSG_ERROR:"${_('Data error.')}",
224 MSG_ERROR:"${_('Data error.')}",
197 MSG_LOADING:"${_('Loading...')}",
225 MSG_LOADING:"${_('Loading...')}",
198 }
226 }
199 );
227 );
200 myDataTable.subscribe('postRenderEvent',function(oArgs) {
228 myDataTable.subscribe('postRenderEvent',function(oArgs) {
201 tooltip_activate();
229 tooltip_activate();
202 quick_repo_menu();
230 quick_repo_menu();
203 var func = function(node){
231 var func = function(node){
204 return node.parentNode.parentNode.parentNode.parentNode;
232 return node.parentNode.parentNode.parentNode.parentNode;
205 }
233 }
206 q_filter('q_filter',YUQ('#my tr td a.repo_name'),func);
234 q_filter('q_filter',YUQ('#my tr td a.repo_name'),func);
207 });
235 });
208
236
209 </script>
237 </script>
210 </%def>
238 </%def>
@@ -1,103 +1,115 b''
1 import os
1 import os
2 import csv
2 import csv
3 import datetime
3 import datetime
4 from rhodecode.tests import *
4 from rhodecode.tests import *
5 from rhodecode.model.db import UserLog
5 from rhodecode.model.db import UserLog
6 from rhodecode.model.meta import Session
6 from rhodecode.model.meta import Session
7 from rhodecode.lib.utils2 import safe_unicode
7 from rhodecode.lib.utils2 import safe_unicode
8
8
9 dn = os.path.dirname
9 dn = os.path.dirname
10 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'fixtures')
10 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'fixtures')
11
11
12
12
13 class TestAdminController(TestController):
13 class TestAdminController(TestController):
14
14
15 @classmethod
15 @classmethod
16 def setup_class(cls):
16 def setup_class(cls):
17 UserLog.query().delete()
17 UserLog.query().delete()
18 Session().commit()
18 Session().commit()
19 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
19 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
20 for row in csv.DictReader(f):
20 for row in csv.DictReader(f):
21 ul = UserLog()
21 ul = UserLog()
22 for k, v in row.iteritems():
22 for k, v in row.iteritems():
23 v = safe_unicode(v)
23 v = safe_unicode(v)
24 if k == 'action_date':
24 if k == 'action_date':
25 v = datetime.datetime.strptime(v, '%Y-%m-%d %H:%M:%S.%f')
25 v = datetime.datetime.strptime(v, '%Y-%m-%d %H:%M:%S.%f')
26 setattr(ul, k, v)
26 setattr(ul, k, v)
27 Session().add(ul)
27 Session().add(ul)
28 Session().commit()
28 Session().commit()
29
29
30 @classmethod
30 @classmethod
31 def teardown_class(cls):
31 def teardown_class(cls):
32 UserLog.query().delete()
32 UserLog.query().delete()
33 Session().commit()
33 Session().commit()
34
34
35 def test_index(self):
35 def test_index(self):
36 self.log_user()
36 self.log_user()
37 response = self.app.get(url(controller='admin/admin', action='index'))
37 response = self.app.get(url(controller='admin/admin', action='index'))
38 response.mustcontain('Admin journal')
38 response.mustcontain('Admin journal')
39
39
40 def test_filter_all_entries(self):
40 def test_filter_all_entries(self):
41 self.log_user()
41 self.log_user()
42 response = self.app.get(url(controller='admin/admin', action='index',))
42 response = self.app.get(url(controller='admin/admin', action='index',))
43 response.mustcontain('2034 entries')
43 response.mustcontain('2034 entries')
44
44
45 def test_filter_journal_filter_exact_match_on_repository(self):
45 def test_filter_journal_filter_exact_match_on_repository(self):
46 self.log_user()
46 self.log_user()
47 response = self.app.get(url(controller='admin/admin', action='index',
47 response = self.app.get(url(controller='admin/admin', action='index',
48 filter='repository:rhodecode'))
48 filter='repository:rhodecode'))
49 response.mustcontain('3 entries')
49 response.mustcontain('3 entries')
50
50
51 def test_filter_journal_filter_wildcard_on_repository(self):
51 def test_filter_journal_filter_wildcard_on_repository(self):
52 self.log_user()
52 self.log_user()
53 response = self.app.get(url(controller='admin/admin', action='index',
53 response = self.app.get(url(controller='admin/admin', action='index',
54 filter='repository:*test*'))
54 filter='repository:*test*'))
55 response.mustcontain('862 entries')
55 response.mustcontain('862 entries')
56
56
57 def test_filter_journal_filter_prefix_on_repository(self):
57 def test_filter_journal_filter_prefix_on_repository(self):
58 self.log_user()
58 self.log_user()
59 response = self.app.get(url(controller='admin/admin', action='index',
59 response = self.app.get(url(controller='admin/admin', action='index',
60 filter='repository:test*'))
60 filter='repository:test*'))
61 response.mustcontain('257 entries')
61 response.mustcontain('257 entries')
62
62
63 def test_filter_journal_filter_prefix_on_repository_and_user(self):
63 def test_filter_journal_filter_prefix_on_repository_and_user(self):
64 self.log_user()
64 self.log_user()
65 response = self.app.get(url(controller='admin/admin', action='index',
65 response = self.app.get(url(controller='admin/admin', action='index',
66 filter='repository:test* AND username:demo'))
66 filter='repository:test* AND username:demo'))
67 response.mustcontain('130 entries')
67 response.mustcontain('130 entries')
68
68
69 def test_filter_journal_filter_prefix_on_repository_or_other_repo(self):
69 def test_filter_journal_filter_prefix_on_repository_or_other_repo(self):
70 self.log_user()
70 self.log_user()
71 response = self.app.get(url(controller='admin/admin', action='index',
71 response = self.app.get(url(controller='admin/admin', action='index',
72 filter='repository:test* OR repository:rhodecode'))
72 filter='repository:test* OR repository:rhodecode'))
73 response.mustcontain('260 entries') # 257 + 3
73 response.mustcontain('260 entries') # 257 + 3
74
74
75 def test_filter_journal_filter_exact_match_on_username(self):
75 def test_filter_journal_filter_exact_match_on_username(self):
76 self.log_user()
76 self.log_user()
77 response = self.app.get(url(controller='admin/admin', action='index',
77 response = self.app.get(url(controller='admin/admin', action='index',
78 filter='username:demo'))
78 filter='username:demo'))
79 response.mustcontain('1087 entries')
79 response.mustcontain('1087 entries')
80
80
81 def test_filter_journal_filter_wildcard_on_username(self):
81 def test_filter_journal_filter_wildcard_on_username(self):
82 self.log_user()
82 self.log_user()
83 response = self.app.get(url(controller='admin/admin', action='index',
83 response = self.app.get(url(controller='admin/admin', action='index',
84 filter='username:*test*'))
84 filter='username:*test*'))
85 response.mustcontain('100 entries')
85 response.mustcontain('100 entries')
86
86
87 def test_filter_journal_filter_prefix_on_username(self):
87 def test_filter_journal_filter_prefix_on_username(self):
88 self.log_user()
88 self.log_user()
89 response = self.app.get(url(controller='admin/admin', action='index',
89 response = self.app.get(url(controller='admin/admin', action='index',
90 filter='username:demo*'))
90 filter='username:demo*'))
91 response.mustcontain('1101 entries')
91 response.mustcontain('1101 entries')
92
92
93 def test_filter_journal_filter_prefix_on_user_or_other_user(self):
93 def test_filter_journal_filter_prefix_on_user_or_other_user(self):
94 self.log_user()
94 self.log_user()
95 response = self.app.get(url(controller='admin/admin', action='index',
95 response = self.app.get(url(controller='admin/admin', action='index',
96 filter='username:demo OR username:volcan'))
96 filter='username:demo OR username:volcan'))
97 response.mustcontain('1095 entries') # 1087 + 8
97 response.mustcontain('1095 entries') # 1087 + 8
98
98
99 def test_filter_journal_filter_wildcard_on_action(self):
99 def test_filter_journal_filter_wildcard_on_action(self):
100 self.log_user()
100 self.log_user()
101 response = self.app.get(url(controller='admin/admin', action='index',
101 response = self.app.get(url(controller='admin/admin', action='index',
102 filter='action:*pull_request*'))
102 filter='action:*pull_request*'))
103 response.mustcontain('187 entries') No newline at end of file
103 response.mustcontain('187 entries')
104
105 def test_filter_journal_filter_on_date(self):
106 self.log_user()
107 response = self.app.get(url(controller='admin/admin', action='index',
108 filter='date:20121010'))
109 response.mustcontain('47 entries')
110
111 def test_filter_journal_filter_on_date_2(self):
112 self.log_user()
113 response = self.app.get(url(controller='admin/admin', action='index',
114 filter='date:20121020'))
115 response.mustcontain('17 entries') No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now