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