##// END OF EJS Templates
forms: don't use secure forms with authentication token for GET requests...
Mads Kiilerich -
r5524:1346754f stable
parent child Browse files
Show More
@@ -1,198 +1,197 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.changelog
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 changelog controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 21, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30
31 31 from pylons import request, url, session, tmpl_context as c
32 32 from pylons.controllers.util import redirect
33 33 from pylons.i18n.translation import _
34 34 from webob.exc import HTTPNotFound, HTTPBadRequest
35 35
36 36 import kallithea.lib.helpers as h
37 37 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 38 from kallithea.lib.base import BaseRepoController, render
39 39 from kallithea.lib.helpers import RepoPage
40 40 from kallithea.lib.compat import json
41 41 from kallithea.lib.graphmod import graph_data
42 42 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError,\
43 43 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
44 44 from kallithea.lib.utils2 import safe_int, safe_str
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 def _load_changelog_summary():
51 51 p = safe_int(request.GET.get('page'), 1)
52 52 size = safe_int(request.GET.get('size'), 10)
53 53
54 54 def url_generator(**kw):
55 55 return url('changelog_summary_home',
56 56 repo_name=c.db_repo.repo_name, size=size, **kw)
57 57
58 58 collection = c.db_repo_scm_instance
59 59
60 60 c.repo_changesets = RepoPage(collection, page=p,
61 61 items_per_page=size,
62 62 url=url_generator)
63 63 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
64 64 c.comments = c.db_repo.get_comments(page_revisions)
65 65 c.statuses = c.db_repo.statuses(page_revisions)
66 66
67 67
68 68 class ChangelogController(BaseRepoController):
69 69
70 70 def __before__(self):
71 71 super(ChangelogController, self).__before__()
72 72 c.affected_files_cut_off = 60
73 73
74 74 @staticmethod
75 75 def __get_cs(rev, repo):
76 76 """
77 77 Safe way to get changeset. If error occur fail with error message.
78 78
79 79 :param rev: revision to fetch
80 80 :param repo: repo instance
81 81 """
82 82
83 83 try:
84 84 return c.db_repo_scm_instance.get_changeset(rev)
85 85 except EmptyRepositoryError as e:
86 86 h.flash(h.literal(_('There are no changesets yet')),
87 87 category='error')
88 88 except RepositoryError as e:
89 89 log.error(traceback.format_exc())
90 90 h.flash(safe_str(e), category='error')
91 91 raise HTTPBadRequest()
92 92
93 93 @LoginRequired()
94 94 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
95 95 'repository.admin')
96 96 def index(self, repo_name, revision=None, f_path=None):
97 97 # Fix URL after page size form submission via GET
98 98 # TODO: Somehow just don't send this extra junk in the GET URL
99 99 if request.GET.get('set'):
100 100 request.GET.pop('set', None)
101 request.GET.pop('_authentication_token', None)
102 101 if revision is None:
103 102 return redirect(url('changelog_home', repo_name=repo_name, **request.GET))
104 103 return redirect(url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
105 104
106 105 limit = 2000
107 106 default = 100
108 107 if request.GET.get('size'):
109 108 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
110 109 session['changelog_size'] = c.size
111 110 session.save()
112 111 else:
113 112 c.size = int(session.get('changelog_size', default))
114 113 # min size must be 1
115 114 c.size = max(c.size, 1)
116 115 p = safe_int(request.GET.get('page', 1), 1)
117 116 branch_name = request.GET.get('branch', None)
118 117 if (branch_name and
119 118 branch_name not in c.db_repo_scm_instance.branches and
120 119 branch_name not in c.db_repo_scm_instance.closed_branches and
121 120 not revision):
122 121 return redirect(url('changelog_file_home', repo_name=c.repo_name,
123 122 revision=branch_name, f_path=f_path or ''))
124 123
125 124 if revision == 'tip':
126 125 revision = None
127 126
128 127 c.changelog_for_path = f_path
129 128 try:
130 129
131 130 if f_path:
132 131 log.debug('generating changelog for path %s', f_path)
133 132 # get the history for the file !
134 133 tip_cs = c.db_repo_scm_instance.get_changeset()
135 134 try:
136 135 collection = tip_cs.get_file_history(f_path)
137 136 except (NodeDoesNotExistError, ChangesetError):
138 137 #this node is not present at tip !
139 138 try:
140 139 cs = self.__get_cs(revision, repo_name)
141 140 collection = cs.get_file_history(f_path)
142 141 except RepositoryError as e:
143 142 h.flash(safe_str(e), category='warning')
144 143 redirect(h.url('changelog_home', repo_name=repo_name))
145 144 collection = list(reversed(collection))
146 145 else:
147 146 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
148 147 branch_name=branch_name)
149 148 c.total_cs = len(collection)
150 149
151 150 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
152 151 items_per_page=c.size, branch=branch_name,)
153 152
154 153 page_revisions = [x.raw_id for x in c.pagination]
155 154 c.comments = c.db_repo.get_comments(page_revisions)
156 155 c.statuses = c.db_repo.statuses(page_revisions)
157 156 except EmptyRepositoryError as e:
158 157 h.flash(safe_str(e), category='warning')
159 158 return redirect(url('summary_home', repo_name=c.repo_name))
160 159 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
161 160 log.error(traceback.format_exc())
162 161 h.flash(safe_str(e), category='error')
163 162 return redirect(url('changelog_home', repo_name=c.repo_name))
164 163
165 164 c.branch_name = branch_name
166 165 c.branch_filters = [('', _('None'))] + \
167 166 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
168 167 if c.db_repo_scm_instance.closed_branches:
169 168 prefix = _('(closed)') + ' '
170 169 c.branch_filters += [('-', '-')] + \
171 170 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
172 171 revs = []
173 172 if not f_path:
174 173 revs = [x.revision for x in c.pagination]
175 174 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
176 175
177 176 c.revision = revision # requested revision ref
178 177 c.first_revision = c.pagination[0] # pagination is never empty here!
179 178 return render('changelog/changelog.html')
180 179
181 180 @LoginRequired()
182 181 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
183 182 'repository.admin')
184 183 def changelog_details(self, cs):
185 184 if request.environ.get('HTTP_X_PARTIAL_XHR'):
186 185 c.cs = c.db_repo_scm_instance.get_changeset(cs)
187 186 return render('changelog/changelog_details.html')
188 187 raise HTTPNotFound()
189 188
190 189 @LoginRequired()
191 190 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
192 191 'repository.admin')
193 192 def changelog_summary(self, repo_name):
194 193 if request.environ.get('HTTP_X_PARTIAL_XHR'):
195 194 _load_changelog_summary()
196 195
197 196 return render('changelog/changelog_summary_data.html')
198 197 raise HTTPNotFound()
@@ -1,1453 +1,1464 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 Helper functions
16 16
17 17 Consists of functions to typically be used within templates, but also
18 18 available to Controllers. This module is available to both as 'h'.
19 19 """
20 20 import hashlib
21 21 import StringIO
22 22 import math
23 23 import logging
24 24 import re
25 25 import urlparse
26 26 import textwrap
27 27
28 28 from pygments.formatters.html import HtmlFormatter
29 29 from pygments import highlight as code_highlight
30 30 from pylons import url
31 31 from pylons.i18n.translation import _, ungettext
32 32
33 33 from webhelpers.html import literal, HTML, escape
34 34 from webhelpers.html.tools import *
35 35 from webhelpers.html.builder import make_tag
36 36 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
37 37 end_form, file, hidden, image, javascript_link, link_to, \
38 38 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
39 submit, text, password, textarea, title, ul, xml_declaration, radio
39 submit, text, password, textarea, title, ul, xml_declaration, radio, \
40 form as insecure_form
40 41 from webhelpers.html.tools import auto_link, button_to, highlight, \
41 42 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
42 43 from webhelpers.number import format_byte_size, format_bit_size
43 44 from webhelpers.pylonslib import Flash as _Flash
44 from webhelpers.pylonslib.secure_form import secure_form as form, authentication_token
45 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
45 46 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
46 47 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
47 48 replace_whitespace, urlify, truncate, wrap_paragraphs
48 49 from webhelpers.date import time_ago_in_words
49 50 from webhelpers.paginate import Page as _Page
50 51 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
51 52 convert_boolean_attrs, NotGiven, _make_safe_id_component
52 53
53 54 from kallithea.lib.annotate import annotate_highlight
54 55 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
55 56 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
56 57 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict,\
57 58 safe_int
58 59 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
59 60 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
60 61 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
61 62 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
62 63 from kallithea.model.changeset_status import ChangesetStatusModel
63 64 from kallithea.model.db import URL_SEP, Permission
64 65
65 66 log = logging.getLogger(__name__)
66 67
67 68
68 69 def canonical_url(*args, **kargs):
69 70 '''Like url(x, qualified=True), but returns url that not only is qualified
70 71 but also canonical, as configured in canonical_url'''
71 72 from kallithea import CONFIG
72 73 try:
73 74 parts = CONFIG.get('canonical_url', '').split('://', 1)
74 75 kargs['host'] = parts[1].split('/', 1)[0]
75 76 kargs['protocol'] = parts[0]
76 77 except IndexError:
77 78 kargs['qualified'] = True
78 79 return url(*args, **kargs)
79 80
80 81 def canonical_hostname():
81 82 '''Return canonical hostname of system'''
82 83 from kallithea import CONFIG
83 84 try:
84 85 parts = CONFIG.get('canonical_url', '').split('://', 1)
85 86 return parts[1].split('/', 1)[0]
86 87 except IndexError:
87 88 parts = url('home', qualified=True).split('://', 1)
88 89 return parts[1].split('/', 1)[0]
89 90
90 91 def html_escape(s):
91 92 """Return string with all html escaped.
92 93 This is also safe for javascript in html but not necessarily correct.
93 94 """
94 95 return (s
95 96 .replace('&', '&amp;')
96 97 .replace(">", "&gt;")
97 98 .replace("<", "&lt;")
98 99 .replace('"', "&quot;")
99 100 .replace("'", "&apos;")
100 101 )
101 102
102 103 def shorter(s, size=20):
103 104 postfix = '...'
104 105 if len(s) > size:
105 106 return s[:size - len(postfix)] + postfix
106 107 return s
107 108
108 109
109 110 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
110 111 """
111 112 Reset button
112 113 """
113 114 _set_input_attrs(attrs, type, name, value)
114 115 _set_id_attr(attrs, id, name)
115 116 convert_boolean_attrs(attrs, ["disabled"])
116 117 return HTML.input(**attrs)
117 118
118 119 reset = _reset
119 120 safeid = _make_safe_id_component
120 121
121 122
122 123 def FID(raw_id, path):
123 124 """
124 125 Creates a unique ID for filenode based on it's hash of path and revision
125 126 it's safe to use in urls
126 127
127 128 :param raw_id:
128 129 :param path:
129 130 """
130 131
131 132 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
132 133
133 134
134 135 class _GetError(object):
135 136 """Get error from form_errors, and represent it as span wrapped error
136 137 message
137 138
138 139 :param field_name: field to fetch errors for
139 140 :param form_errors: form errors dict
140 141 """
141 142
142 143 def __call__(self, field_name, form_errors):
143 144 tmpl = """<span class="error_msg">%s</span>"""
144 145 if form_errors and field_name in form_errors:
145 146 return literal(tmpl % form_errors.get(field_name))
146 147
147 148 get_error = _GetError()
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 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
255 256
256 257 def _markup_whitespace(m):
257 258 groups = m.groups()
258 259 if groups[0]:
259 260 return '<u>\t</u>'
260 261 if groups[1]:
261 262 return ' <i></i>'
262 263
263 264 def markup_whitespace(s):
264 265 return _whitespace_re.sub(_markup_whitespace, s)
265 266
266 267 def pygmentize(filenode, **kwargs):
267 268 """
268 269 pygmentize function using pygments
269 270
270 271 :param filenode:
271 272 """
272 273 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
273 274 return literal(markup_whitespace(
274 275 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
275 276
276 277
277 278 def pygmentize_annotation(repo_name, filenode, **kwargs):
278 279 """
279 280 pygmentize function for annotation
280 281
281 282 :param filenode:
282 283 """
283 284
284 285 color_dict = {}
285 286
286 287 def gen_color(n=10000):
287 288 """generator for getting n of evenly distributed colors using
288 289 hsv color and golden ratio. It always return same order of colors
289 290
290 291 :returns: RGB tuple
291 292 """
292 293
293 294 def hsv_to_rgb(h, s, v):
294 295 if s == 0.0:
295 296 return v, v, v
296 297 i = int(h * 6.0) # XXX assume int() truncates!
297 298 f = (h * 6.0) - i
298 299 p = v * (1.0 - s)
299 300 q = v * (1.0 - s * f)
300 301 t = v * (1.0 - s * (1.0 - f))
301 302 i = i % 6
302 303 if i == 0:
303 304 return v, t, p
304 305 if i == 1:
305 306 return q, v, p
306 307 if i == 2:
307 308 return p, v, t
308 309 if i == 3:
309 310 return p, q, v
310 311 if i == 4:
311 312 return t, p, v
312 313 if i == 5:
313 314 return v, p, q
314 315
315 316 golden_ratio = 0.618033988749895
316 317 h = 0.22717784590367374
317 318
318 319 for _unused in xrange(n):
319 320 h += golden_ratio
320 321 h %= 1
321 322 HSV_tuple = [h, 0.95, 0.95]
322 323 RGB_tuple = hsv_to_rgb(*HSV_tuple)
323 324 yield map(lambda x: str(int(x * 256)), RGB_tuple)
324 325
325 326 cgenerator = gen_color()
326 327
327 328 def get_color_string(cs):
328 329 if cs in color_dict:
329 330 col = color_dict[cs]
330 331 else:
331 332 col = color_dict[cs] = cgenerator.next()
332 333 return "color: rgb(%s)! important;" % (', '.join(col))
333 334
334 335 def url_func(repo_name):
335 336
336 337 def _url_func(changeset):
337 338 author = escape(changeset.author)
338 339 date = changeset.date
339 340 message = escape(changeset.message)
340 341 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
341 342 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
342 343 "</b> %s<br/></div>") % (author, date, message)
343 344
344 345 lnk_format = show_id(changeset)
345 346 uri = link_to(
346 347 lnk_format,
347 348 url('changeset_home', repo_name=repo_name,
348 349 revision=changeset.raw_id),
349 350 style=get_color_string(changeset.raw_id),
350 351 class_='tooltip safe-html-title',
351 352 title=tooltip_html
352 353 )
353 354
354 355 uri += '\n'
355 356 return uri
356 357 return _url_func
357 358
358 359 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
359 360
360 361
361 362 def is_following_repo(repo_name, user_id):
362 363 from kallithea.model.scm import ScmModel
363 364 return ScmModel().is_following_repo(repo_name, user_id)
364 365
365 366 class _Message(object):
366 367 """A message returned by ``Flash.pop_messages()``.
367 368
368 369 Converting the message to a string returns the message text. Instances
369 370 also have the following attributes:
370 371
371 372 * ``message``: the message text.
372 373 * ``category``: the category specified when the message was created.
373 374 """
374 375
375 376 def __init__(self, category, message):
376 377 self.category = category
377 378 self.message = message
378 379
379 380 def __str__(self):
380 381 return self.message
381 382
382 383 __unicode__ = __str__
383 384
384 385 def __html__(self):
385 386 return escape(safe_unicode(self.message))
386 387
387 388 class Flash(_Flash):
388 389
389 390 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
390 391 """
391 392 Show a message to the user _and_ log it through the specified function
392 393
393 394 category: notice (default), warning, error, success
394 395 logf: a custom log function - such as log.debug
395 396
396 397 logf defaults to log.info, unless category equals 'success', in which
397 398 case logf defaults to log.debug.
398 399 """
399 400 if logf is None:
400 401 logf = log.info
401 402 if category == 'success':
402 403 logf = log.debug
403 404
404 405 logf('Flash %s: %s', category, message)
405 406
406 407 super(Flash, self).__call__(message, category, ignore_duplicate)
407 408
408 409 def pop_messages(self):
409 410 """Return all accumulated messages and delete them from the session.
410 411
411 412 The return value is a list of ``Message`` objects.
412 413 """
413 414 from pylons import session
414 415 messages = session.pop(self.session_key, [])
415 416 session.save()
416 417 return [_Message(*m) for m in messages]
417 418
418 419 flash = Flash()
419 420
420 421 #==============================================================================
421 422 # SCM FILTERS available via h.
422 423 #==============================================================================
423 424 from kallithea.lib.vcs.utils import author_name, author_email
424 425 from kallithea.lib.utils2 import credentials_filter, age as _age
425 426 from kallithea.model.db import User, ChangesetStatus, PullRequest
426 427
427 428 age = lambda x, y=False: _age(x, y)
428 429 capitalize = lambda x: x.capitalize()
429 430 email = author_email
430 431 short_id = lambda x: x[:12]
431 432 hide_credentials = lambda x: ''.join(credentials_filter(x))
432 433
433 434
434 435 def show_id(cs):
435 436 """
436 437 Configurable function that shows ID
437 438 by default it's r123:fffeeefffeee
438 439
439 440 :param cs: changeset instance
440 441 """
441 442 from kallithea import CONFIG
442 443 def_len = safe_int(CONFIG.get('show_sha_length', 12))
443 444 show_rev = str2bool(CONFIG.get('show_revision_number', False))
444 445
445 446 raw_id = cs.raw_id[:def_len]
446 447 if show_rev:
447 448 return 'r%s:%s' % (cs.revision, raw_id)
448 449 else:
449 450 return raw_id
450 451
451 452
452 453 def fmt_date(date):
453 454 if date:
454 455 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
455 456
456 457 return ""
457 458
458 459
459 460 def is_git(repository):
460 461 if hasattr(repository, 'alias'):
461 462 _type = repository.alias
462 463 elif hasattr(repository, 'repo_type'):
463 464 _type = repository.repo_type
464 465 else:
465 466 _type = repository
466 467 return _type == 'git'
467 468
468 469
469 470 def is_hg(repository):
470 471 if hasattr(repository, 'alias'):
471 472 _type = repository.alias
472 473 elif hasattr(repository, 'repo_type'):
473 474 _type = repository.repo_type
474 475 else:
475 476 _type = repository
476 477 return _type == 'hg'
477 478
478 479
479 480 def user_or_none(author):
480 481 email = author_email(author)
481 482 if email:
482 483 user = User.get_by_email(email, case_insensitive=True, cache=True)
483 484 if user is not None:
484 485 return user
485 486
486 487 user = User.get_by_username(author_name(author), case_insensitive=True, cache=True)
487 488 if user is not None:
488 489 return user
489 490
490 491 return None
491 492
492 493 def email_or_none(author):
493 494 if not author:
494 495 return None
495 496 user = user_or_none(author)
496 497 if user is not None:
497 498 return user.email # always use main email address - not necessarily the one used to find user
498 499
499 500 # extract email from the commit string
500 501 email = author_email(author)
501 502 if email:
502 503 return email
503 504
504 505 # No valid email, not a valid user in the system, none!
505 506 return None
506 507
507 508 def person(author, show_attr="username"):
508 509 """Find the user identified by 'author', return one of the users attributes,
509 510 default to the username attribute, None if there is no user"""
510 511 # attr to return from fetched user
511 512 person_getter = lambda usr: getattr(usr, show_attr)
512 513
513 514 # if author is already an instance use it for extraction
514 515 if isinstance(author, User):
515 516 return person_getter(author)
516 517
517 518 user = user_or_none(author)
518 519 if user is not None:
519 520 return person_getter(user)
520 521
521 522 # Still nothing? Just pass back the author name if any, else the email
522 523 return author_name(author) or email(author)
523 524
524 525
525 526 def person_by_id(id_, show_attr="username"):
526 527 # attr to return from fetched user
527 528 person_getter = lambda usr: getattr(usr, show_attr)
528 529
529 530 #maybe it's an ID ?
530 531 if str(id_).isdigit() or isinstance(id_, int):
531 532 id_ = int(id_)
532 533 user = User.get(id_)
533 534 if user is not None:
534 535 return person_getter(user)
535 536 return id_
536 537
537 538
538 539 def desc_stylize(value):
539 540 """
540 541 converts tags from value into html equivalent
541 542
542 543 :param value:
543 544 """
544 545 if not value:
545 546 return ''
546 547
547 548 value = re.sub(r'\[see\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
548 549 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
549 550 value = re.sub(r'\[license\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
550 551 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
551 552 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=&gt;\ *([a-zA-Z0-9\-\/]*)\]',
552 553 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
553 554 value = re.sub(r'\[(lang|language)\ \=&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
554 555 '<div class="metatag" tag="lang">\\2</div>', value)
555 556 value = re.sub(r'\[([a-z]+)\]',
556 557 '<div class="metatag" tag="\\1">\\1</div>', value)
557 558
558 559 return value
559 560
560 561
561 562 def boolicon(value):
562 563 """Returns boolean value of a value, represented as small html image of true/false
563 564 icons
564 565
565 566 :param value: value
566 567 """
567 568
568 569 if value:
569 570 return HTML.tag('i', class_="icon-ok")
570 571 else:
571 572 return HTML.tag('i', class_="icon-minus-circled")
572 573
573 574
574 575 def action_parser(user_log, feed=False, parse_cs=False):
575 576 """
576 577 This helper will action_map the specified string action into translated
577 578 fancy names with icons and links
578 579
579 580 :param user_log: user log instance
580 581 :param feed: use output for feeds (no html and fancy icons)
581 582 :param parse_cs: parse Changesets into VCS instances
582 583 """
583 584
584 585 action = user_log.action
585 586 action_params = ' '
586 587
587 588 x = action.split(':')
588 589
589 590 if len(x) > 1:
590 591 action, action_params = x
591 592
592 593 def get_cs_links():
593 594 revs_limit = 3 # display this amount always
594 595 revs_top_limit = 50 # show upto this amount of changesets hidden
595 596 revs_ids = action_params.split(',')
596 597 deleted = user_log.repository is None
597 598 if deleted:
598 599 return ','.join(revs_ids)
599 600
600 601 repo_name = user_log.repository.repo_name
601 602
602 603 def lnk(rev, repo_name):
603 604 lazy_cs = False
604 605 title_ = None
605 606 url_ = '#'
606 607 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
607 608 if rev.op and rev.ref_name:
608 609 if rev.op == 'delete_branch':
609 610 lbl = _('Deleted branch: %s') % rev.ref_name
610 611 elif rev.op == 'tag':
611 612 lbl = _('Created tag: %s') % rev.ref_name
612 613 else:
613 614 lbl = 'Unknown operation %s' % rev.op
614 615 else:
615 616 lazy_cs = True
616 617 lbl = rev.short_id[:8]
617 618 url_ = url('changeset_home', repo_name=repo_name,
618 619 revision=rev.raw_id)
619 620 else:
620 621 # changeset cannot be found - it might have been stripped or removed
621 622 lbl = rev[:12]
622 623 title_ = _('Changeset not found')
623 624 if parse_cs:
624 625 return link_to(lbl, url_, title=title_, class_='tooltip')
625 626 return link_to(lbl, url_, raw_id=rev.raw_id, repo_name=repo_name,
626 627 class_='lazy-cs' if lazy_cs else '')
627 628
628 629 def _get_op(rev_txt):
629 630 _op = None
630 631 _name = rev_txt
631 632 if len(rev_txt.split('=>')) == 2:
632 633 _op, _name = rev_txt.split('=>')
633 634 return _op, _name
634 635
635 636 revs = []
636 637 if len(filter(lambda v: v != '', revs_ids)) > 0:
637 638 repo = None
638 639 for rev in revs_ids[:revs_top_limit]:
639 640 _op, _name = _get_op(rev)
640 641
641 642 # we want parsed changesets, or new log store format is bad
642 643 if parse_cs:
643 644 try:
644 645 if repo is None:
645 646 repo = user_log.repository.scm_instance
646 647 _rev = repo.get_changeset(rev)
647 648 revs.append(_rev)
648 649 except ChangesetDoesNotExistError:
649 650 log.error('cannot find revision %s in this repo', rev)
650 651 revs.append(rev)
651 652 else:
652 653 _rev = AttributeDict({
653 654 'short_id': rev[:12],
654 655 'raw_id': rev,
655 656 'message': '',
656 657 'op': _op,
657 658 'ref_name': _name
658 659 })
659 660 revs.append(_rev)
660 661 cs_links = [" " + ', '.join(
661 662 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
662 663 )]
663 664 _op1, _name1 = _get_op(revs_ids[0])
664 665 _op2, _name2 = _get_op(revs_ids[-1])
665 666
666 667 _rev = '%s...%s' % (_name1, _name2)
667 668
668 669 compare_view = (
669 670 ' <div class="compare_view tooltip" title="%s">'
670 671 '<a href="%s">%s</a> </div>' % (
671 672 _('Show all combined changesets %s->%s') % (
672 673 revs_ids[0][:12], revs_ids[-1][:12]
673 674 ),
674 675 url('changeset_home', repo_name=repo_name,
675 676 revision=_rev
676 677 ),
677 678 _('Compare view')
678 679 )
679 680 )
680 681
681 682 # if we have exactly one more than normally displayed
682 683 # just display it, takes less space than displaying
683 684 # "and 1 more revisions"
684 685 if len(revs_ids) == revs_limit + 1:
685 686 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
686 687
687 688 # hidden-by-default ones
688 689 if len(revs_ids) > revs_limit + 1:
689 690 uniq_id = revs_ids[0]
690 691 html_tmpl = (
691 692 '<span> %s <a class="show_more" id="_%s" '
692 693 'href="#more">%s</a> %s</span>'
693 694 )
694 695 if not feed:
695 696 cs_links.append(html_tmpl % (
696 697 _('and'),
697 698 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
698 699 _('revisions')
699 700 )
700 701 )
701 702
702 703 if not feed:
703 704 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
704 705 else:
705 706 html_tmpl = '<span id="%s"> %s </span>'
706 707
707 708 morelinks = ', '.join(
708 709 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
709 710 )
710 711
711 712 if len(revs_ids) > revs_top_limit:
712 713 morelinks += ', ...'
713 714
714 715 cs_links.append(html_tmpl % (uniq_id, morelinks))
715 716 if len(revs) > 1:
716 717 cs_links.append(compare_view)
717 718 return ''.join(cs_links)
718 719
719 720 def get_fork_name():
720 721 repo_name = action_params
721 722 url_ = url('summary_home', repo_name=repo_name)
722 723 return _('Fork name %s') % link_to(action_params, url_)
723 724
724 725 def get_user_name():
725 726 user_name = action_params
726 727 return user_name
727 728
728 729 def get_users_group():
729 730 group_name = action_params
730 731 return group_name
731 732
732 733 def get_pull_request():
733 734 pull_request_id = action_params
734 735 nice_id = PullRequest.make_nice_id(pull_request_id)
735 736
736 737 deleted = user_log.repository is None
737 738 if deleted:
738 739 repo_name = user_log.repository_name
739 740 else:
740 741 repo_name = user_log.repository.repo_name
741 742
742 743 return link_to(_('Pull request %s') % nice_id,
743 744 url('pullrequest_show', repo_name=repo_name,
744 745 pull_request_id=pull_request_id))
745 746
746 747 def get_archive_name():
747 748 archive_name = action_params
748 749 return archive_name
749 750
750 751 # action : translated str, callback(extractor), icon
751 752 action_map = {
752 753 'user_deleted_repo': (_('[deleted] repository'),
753 754 None, 'icon-trashcan'),
754 755 'user_created_repo': (_('[created] repository'),
755 756 None, 'icon-plus'),
756 757 'user_created_fork': (_('[created] repository as fork'),
757 758 None, 'icon-fork'),
758 759 'user_forked_repo': (_('[forked] repository'),
759 760 get_fork_name, 'icon-fork'),
760 761 'user_updated_repo': (_('[updated] repository'),
761 762 None, 'icon-pencil'),
762 763 'user_downloaded_archive': (_('[downloaded] archive from repository'),
763 764 get_archive_name, 'icon-download-cloud'),
764 765 'admin_deleted_repo': (_('[delete] repository'),
765 766 None, 'icon-trashcan'),
766 767 'admin_created_repo': (_('[created] repository'),
767 768 None, 'icon-plus'),
768 769 'admin_forked_repo': (_('[forked] repository'),
769 770 None, 'icon-fork'),
770 771 'admin_updated_repo': (_('[updated] repository'),
771 772 None, 'icon-pencil'),
772 773 'admin_created_user': (_('[created] user'),
773 774 get_user_name, 'icon-user'),
774 775 'admin_updated_user': (_('[updated] user'),
775 776 get_user_name, 'icon-user'),
776 777 'admin_created_users_group': (_('[created] user group'),
777 778 get_users_group, 'icon-pencil'),
778 779 'admin_updated_users_group': (_('[updated] user group'),
779 780 get_users_group, 'icon-pencil'),
780 781 'user_commented_revision': (_('[commented] on revision in repository'),
781 782 get_cs_links, 'icon-comment'),
782 783 'user_commented_pull_request': (_('[commented] on pull request for'),
783 784 get_pull_request, 'icon-comment'),
784 785 'user_closed_pull_request': (_('[closed] pull request for'),
785 786 get_pull_request, 'icon-ok'),
786 787 'push': (_('[pushed] into'),
787 788 get_cs_links, 'icon-move-up'),
788 789 'push_local': (_('[committed via Kallithea] into repository'),
789 790 get_cs_links, 'icon-pencil'),
790 791 'push_remote': (_('[pulled from remote] into repository'),
791 792 get_cs_links, 'icon-move-up'),
792 793 'pull': (_('[pulled] from'),
793 794 None, 'icon-move-down'),
794 795 'started_following_repo': (_('[started following] repository'),
795 796 None, 'icon-heart'),
796 797 'stopped_following_repo': (_('[stopped following] repository'),
797 798 None, 'icon-heart-empty'),
798 799 }
799 800
800 801 action_str = action_map.get(action, action)
801 802 if feed:
802 803 action = action_str[0].replace('[', '').replace(']', '')
803 804 else:
804 805 action = action_str[0]\
805 806 .replace('[', '<span class="journal_highlight">')\
806 807 .replace(']', '</span>')
807 808
808 809 action_params_func = lambda: ""
809 810
810 811 if callable(action_str[1]):
811 812 action_params_func = action_str[1]
812 813
813 814 def action_parser_icon():
814 815 action = user_log.action
815 816 action_params = None
816 817 x = action.split(':')
817 818
818 819 if len(x) > 1:
819 820 action, action_params = x
820 821
821 822 tmpl = """<i class="%s" alt="%s"></i>"""
822 823 ico = action_map.get(action, ['', '', ''])[2]
823 824 return literal(tmpl % (ico, action))
824 825
825 826 # returned callbacks we need to call to get
826 827 return [lambda: literal(action), action_params_func, action_parser_icon]
827 828
828 829
829 830
830 831 #==============================================================================
831 832 # PERMS
832 833 #==============================================================================
833 834 from kallithea.lib.auth import HasPermissionAny, HasPermissionAll, \
834 835 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
835 836 HasRepoGroupPermissionAny
836 837
837 838
838 839 #==============================================================================
839 840 # GRAVATAR URL
840 841 #==============================================================================
841 842 def gravatar(email_address, cls='', size=30, ssl_enabled=True):
842 843 """return html element of the gravatar
843 844
844 845 This method will return an <img> with the resolution double the size (for
845 846 retina screens) of the image. If the url returned from gravatar_url is
846 847 empty then we fallback to using an icon.
847 848
848 849 """
849 850 src = gravatar_url(email_address, size*2, ssl_enabled)
850 851
851 852 # here it makes sense to use style="width: ..." (instead of, say, a
852 853 # stylesheet) because we using this to generate a high-res (retina) size
853 854 tmpl = '<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
854 855
855 856 # if src is empty then there was no gravatar, so we use a font icon
856 857 if not src:
857 858 tmpl = """<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
858 859
859 860 tmpl = tmpl.format(cls=cls, size=size, src=src)
860 861 return literal(tmpl)
861 862
862 863 def gravatar_url(email_address, size=30, ssl_enabled=True):
863 864 # doh, we need to re-import those to mock it later
864 865 from pylons import url
865 866 from pylons import tmpl_context as c
866 867
867 868 _def = 'anonymous@kallithea-scm.org' # default gravatar
868 869 _use_gravatar = c.visual.use_gravatar
869 870 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
870 871
871 872 email_address = email_address or _def
872 873
873 874 if not _use_gravatar or not email_address or email_address == _def:
874 875 return ""
875 876
876 877 if _use_gravatar:
877 878 _md5 = lambda s: hashlib.md5(s).hexdigest()
878 879
879 880 tmpl = _gravatar_url
880 881 parsed_url = urlparse.urlparse(url.current(qualified=True))
881 882 tmpl = tmpl.replace('{email}', email_address)\
882 883 .replace('{md5email}', _md5(safe_str(email_address).lower())) \
883 884 .replace('{netloc}', parsed_url.netloc)\
884 885 .replace('{scheme}', parsed_url.scheme)\
885 886 .replace('{size}', safe_str(size))
886 887 return tmpl
887 888
888 889 class Page(_Page):
889 890 """
890 891 Custom pager to match rendering style with YUI paginator
891 892 """
892 893
893 894 def _get_pos(self, cur_page, max_page, items):
894 895 edge = (items / 2) + 1
895 896 if (cur_page <= edge):
896 897 radius = max(items / 2, items - cur_page)
897 898 elif (max_page - cur_page) < edge:
898 899 radius = (items - 1) - (max_page - cur_page)
899 900 else:
900 901 radius = items / 2
901 902
902 903 left = max(1, (cur_page - (radius)))
903 904 right = min(max_page, cur_page + (radius))
904 905 return left, cur_page, right
905 906
906 907 def _range(self, regexp_match):
907 908 """
908 909 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
909 910
910 911 Arguments:
911 912
912 913 regexp_match
913 914 A "re" (regular expressions) match object containing the
914 915 radius of linked pages around the current page in
915 916 regexp_match.group(1) as a string
916 917
917 918 This function is supposed to be called as a callable in
918 919 re.sub.
919 920
920 921 """
921 922 radius = int(regexp_match.group(1))
922 923
923 924 # Compute the first and last page number within the radius
924 925 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
925 926 # -> leftmost_page = 5
926 927 # -> rightmost_page = 9
927 928 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
928 929 self.last_page,
929 930 (radius * 2) + 1)
930 931 nav_items = []
931 932
932 933 # Create a link to the first page (unless we are on the first page
933 934 # or there would be no need to insert '..' spacers)
934 935 if self.page != self.first_page and self.first_page < leftmost_page:
935 936 nav_items.append(self._pagerlink(self.first_page, self.first_page))
936 937
937 938 # Insert dots if there are pages between the first page
938 939 # and the currently displayed page range
939 940 if leftmost_page - self.first_page > 1:
940 941 # Wrap in a SPAN tag if nolink_attr is set
941 942 text_ = '..'
942 943 if self.dotdot_attr:
943 944 text_ = HTML.span(c=text_, **self.dotdot_attr)
944 945 nav_items.append(text_)
945 946
946 947 for thispage in xrange(leftmost_page, rightmost_page + 1):
947 948 # Highlight the current page number and do not use a link
948 949 text_ = str(thispage)
949 950 if thispage == self.page:
950 951 # Wrap in a SPAN tag if nolink_attr is set
951 952 if self.curpage_attr:
952 953 text_ = HTML.span(c=text_, **self.curpage_attr)
953 954 nav_items.append(text_)
954 955 # Otherwise create just a link to that page
955 956 else:
956 957 nav_items.append(self._pagerlink(thispage, text_))
957 958
958 959 # Insert dots if there are pages between the displayed
959 960 # page numbers and the end of the page range
960 961 if self.last_page - rightmost_page > 1:
961 962 text_ = '..'
962 963 # Wrap in a SPAN tag if nolink_attr is set
963 964 if self.dotdot_attr:
964 965 text_ = HTML.span(c=text_, **self.dotdot_attr)
965 966 nav_items.append(text_)
966 967
967 968 # Create a link to the very last page (unless we are on the last
968 969 # page or there would be no need to insert '..' spacers)
969 970 if self.page != self.last_page and rightmost_page < self.last_page:
970 971 nav_items.append(self._pagerlink(self.last_page, self.last_page))
971 972
972 973 #_page_link = url.current()
973 974 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
974 975 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
975 976 return self.separator.join(nav_items)
976 977
977 978 def pager(self, format='~2~', page_param='page', partial_param='partial',
978 979 show_if_single_page=False, separator=' ', onclick=None,
979 980 symbol_first='<<', symbol_last='>>',
980 981 symbol_previous='<', symbol_next='>',
981 982 link_attr={'class': 'pager_link', 'rel': 'prerender'},
982 983 curpage_attr={'class': 'pager_curpage'},
983 984 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
984 985
985 986 self.curpage_attr = curpage_attr
986 987 self.separator = separator
987 988 self.pager_kwargs = kwargs
988 989 self.page_param = page_param
989 990 self.partial_param = partial_param
990 991 self.onclick = onclick
991 992 self.link_attr = link_attr
992 993 self.dotdot_attr = dotdot_attr
993 994
994 995 # Don't show navigator if there is no more than one page
995 996 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
996 997 return ''
997 998
998 999 from string import Template
999 1000 # Replace ~...~ in token format by range of pages
1000 1001 result = re.sub(r'~(\d+)~', self._range, format)
1001 1002
1002 1003 # Interpolate '%' variables
1003 1004 result = Template(result).safe_substitute({
1004 1005 'first_page': self.first_page,
1005 1006 'last_page': self.last_page,
1006 1007 'page': self.page,
1007 1008 'page_count': self.page_count,
1008 1009 'items_per_page': self.items_per_page,
1009 1010 'first_item': self.first_item,
1010 1011 'last_item': self.last_item,
1011 1012 'item_count': self.item_count,
1012 1013 'link_first': self.page > self.first_page and \
1013 1014 self._pagerlink(self.first_page, symbol_first) or '',
1014 1015 'link_last': self.page < self.last_page and \
1015 1016 self._pagerlink(self.last_page, symbol_last) or '',
1016 1017 'link_previous': self.previous_page and \
1017 1018 self._pagerlink(self.previous_page, symbol_previous) \
1018 1019 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1019 1020 'link_next': self.next_page and \
1020 1021 self._pagerlink(self.next_page, symbol_next) \
1021 1022 or HTML.span(symbol_next, class_="yui-pg-next")
1022 1023 })
1023 1024
1024 1025 return literal(result)
1025 1026
1026 1027
1027 1028 #==============================================================================
1028 1029 # REPO PAGER, PAGER FOR REPOSITORY
1029 1030 #==============================================================================
1030 1031 class RepoPage(Page):
1031 1032
1032 1033 def __init__(self, collection, page=1, items_per_page=20,
1033 1034 item_count=None, url=None, **kwargs):
1034 1035
1035 1036 """Create a "RepoPage" instance. special pager for paging
1036 1037 repository
1037 1038 """
1038 1039 self._url_generator = url
1039 1040
1040 1041 # Safe the kwargs class-wide so they can be used in the pager() method
1041 1042 self.kwargs = kwargs
1042 1043
1043 1044 # Save a reference to the collection
1044 1045 self.original_collection = collection
1045 1046
1046 1047 self.collection = collection
1047 1048
1048 1049 # The self.page is the number of the current page.
1049 1050 # The first page has the number 1!
1050 1051 try:
1051 1052 self.page = int(page) # make it int() if we get it as a string
1052 1053 except (ValueError, TypeError):
1053 1054 self.page = 1
1054 1055
1055 1056 self.items_per_page = items_per_page
1056 1057
1057 1058 # Unless the user tells us how many items the collections has
1058 1059 # we calculate that ourselves.
1059 1060 if item_count is not None:
1060 1061 self.item_count = item_count
1061 1062 else:
1062 1063 self.item_count = len(self.collection)
1063 1064
1064 1065 # Compute the number of the first and last available page
1065 1066 if self.item_count > 0:
1066 1067 self.first_page = 1
1067 1068 self.page_count = int(math.ceil(float(self.item_count) /
1068 1069 self.items_per_page))
1069 1070 self.last_page = self.first_page + self.page_count - 1
1070 1071
1071 1072 # Make sure that the requested page number is the range of
1072 1073 # valid pages
1073 1074 if self.page > self.last_page:
1074 1075 self.page = self.last_page
1075 1076 elif self.page < self.first_page:
1076 1077 self.page = self.first_page
1077 1078
1078 1079 # Note: the number of items on this page can be less than
1079 1080 # items_per_page if the last page is not full
1080 1081 self.first_item = max(0, (self.item_count) - (self.page *
1081 1082 items_per_page))
1082 1083 self.last_item = ((self.item_count - 1) - items_per_page *
1083 1084 (self.page - 1))
1084 1085
1085 1086 self.items = list(self.collection[self.first_item:self.last_item + 1])
1086 1087
1087 1088 # Links to previous and next page
1088 1089 if self.page > self.first_page:
1089 1090 self.previous_page = self.page - 1
1090 1091 else:
1091 1092 self.previous_page = None
1092 1093
1093 1094 if self.page < self.last_page:
1094 1095 self.next_page = self.page + 1
1095 1096 else:
1096 1097 self.next_page = None
1097 1098
1098 1099 # No items available
1099 1100 else:
1100 1101 self.first_page = None
1101 1102 self.page_count = 0
1102 1103 self.last_page = None
1103 1104 self.first_item = None
1104 1105 self.last_item = None
1105 1106 self.previous_page = None
1106 1107 self.next_page = None
1107 1108 self.items = []
1108 1109
1109 1110 # This is a subclass of the 'list' type. Initialise the list now.
1110 1111 list.__init__(self, reversed(self.items))
1111 1112
1112 1113
1113 1114 def changed_tooltip(nodes):
1114 1115 """
1115 1116 Generates a html string for changed nodes in changeset page.
1116 1117 It limits the output to 30 entries
1117 1118
1118 1119 :param nodes: LazyNodesGenerator
1119 1120 """
1120 1121 if nodes:
1121 1122 pref = ': <br/> '
1122 1123 suf = ''
1123 1124 if len(nodes) > 30:
1124 1125 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1125 1126 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1126 1127 for x in nodes[:30]]) + suf)
1127 1128 else:
1128 1129 return ': ' + _('No files')
1129 1130
1130 1131
1131 1132 def repo_link(groups_and_repos):
1132 1133 """
1133 1134 Makes a breadcrumbs link to repo within a group
1134 1135 joins &raquo; on each group to create a fancy link
1135 1136
1136 1137 ex::
1137 1138 group >> subgroup >> repo
1138 1139
1139 1140 :param groups_and_repos:
1140 1141 :param last_url:
1141 1142 """
1142 1143 groups, just_name, repo_name = groups_and_repos
1143 1144 last_url = url('summary_home', repo_name=repo_name)
1144 1145 last_link = link_to(just_name, last_url)
1145 1146
1146 1147 def make_link(group):
1147 1148 return link_to(group.name,
1148 1149 url('repos_group_home', group_name=group.group_name))
1149 1150 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1150 1151
1151 1152
1152 1153 def fancy_file_stats(stats):
1153 1154 """
1154 1155 Displays a fancy two colored bar for number of added/deleted
1155 1156 lines of code on file
1156 1157
1157 1158 :param stats: two element list of added/deleted lines of code
1158 1159 """
1159 1160 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1160 1161 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1161 1162
1162 1163 def cgen(l_type, a_v, d_v):
1163 1164 mapping = {'tr': 'top-right-rounded-corner-mid',
1164 1165 'tl': 'top-left-rounded-corner-mid',
1165 1166 'br': 'bottom-right-rounded-corner-mid',
1166 1167 'bl': 'bottom-left-rounded-corner-mid'}
1167 1168 map_getter = lambda x: mapping[x]
1168 1169
1169 1170 if l_type == 'a' and d_v:
1170 1171 #case when added and deleted are present
1171 1172 return ' '.join(map(map_getter, ['tl', 'bl']))
1172 1173
1173 1174 if l_type == 'a' and not d_v:
1174 1175 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1175 1176
1176 1177 if l_type == 'd' and a_v:
1177 1178 return ' '.join(map(map_getter, ['tr', 'br']))
1178 1179
1179 1180 if l_type == 'd' and not a_v:
1180 1181 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1181 1182
1182 1183 a, d = stats['added'], stats['deleted']
1183 1184 width = 100
1184 1185
1185 1186 if stats['binary']:
1186 1187 #binary mode
1187 1188 lbl = ''
1188 1189 bin_op = 1
1189 1190
1190 1191 if BIN_FILENODE in stats['ops']:
1191 1192 lbl = 'bin+'
1192 1193
1193 1194 if NEW_FILENODE in stats['ops']:
1194 1195 lbl += _('new file')
1195 1196 bin_op = NEW_FILENODE
1196 1197 elif MOD_FILENODE in stats['ops']:
1197 1198 lbl += _('mod')
1198 1199 bin_op = MOD_FILENODE
1199 1200 elif DEL_FILENODE in stats['ops']:
1200 1201 lbl += _('del')
1201 1202 bin_op = DEL_FILENODE
1202 1203 elif RENAMED_FILENODE in stats['ops']:
1203 1204 lbl += _('rename')
1204 1205 bin_op = RENAMED_FILENODE
1205 1206
1206 1207 #chmod can go with other operations
1207 1208 if CHMOD_FILENODE in stats['ops']:
1208 1209 _org_lbl = _('chmod')
1209 1210 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1210 1211
1211 1212 #import ipdb;ipdb.set_trace()
1212 1213 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1213 1214 b_a = '<div class="bin bin1" style="width:0%"></div>'
1214 1215 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1215 1216
1216 1217 t = stats['added'] + stats['deleted']
1217 1218 unit = float(width) / (t or 1)
1218 1219
1219 1220 # needs > 9% of width to be visible or 0 to be hidden
1220 1221 a_p = max(9, unit * a) if a > 0 else 0
1221 1222 d_p = max(9, unit * d) if d > 0 else 0
1222 1223 p_sum = a_p + d_p
1223 1224
1224 1225 if p_sum > width:
1225 1226 #adjust the percentage to be == 100% since we adjusted to 9
1226 1227 if a_p > d_p:
1227 1228 a_p = a_p - (p_sum - width)
1228 1229 else:
1229 1230 d_p = d_p - (p_sum - width)
1230 1231
1231 1232 a_v = a if a > 0 else ''
1232 1233 d_v = d if d > 0 else ''
1233 1234
1234 1235 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1235 1236 cgen('a', a_v, d_v), a_p, a_v
1236 1237 )
1237 1238 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1238 1239 cgen('d', a_v, d_v), d_p, d_v
1239 1240 )
1240 1241 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1241 1242
1242 1243
1243 1244 def _urlify_text(s):
1244 1245 """
1245 1246 Extract urls from text and make html links out of them
1246 1247 """
1247 1248 def url_func(match_obj):
1248 1249 url_full = match_obj.group(1)
1249 1250 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1250 1251 return url_re.sub(url_func, s)
1251 1252
1252 1253 def urlify_text(s, truncate=None, stylize=False, truncatef=truncate):
1253 1254 """
1254 1255 Extract urls from text and make literal html links out of them
1255 1256 """
1256 1257 if truncate is not None:
1257 1258 s = truncatef(s, truncate)
1258 1259 s = html_escape(s)
1259 1260 if stylize:
1260 1261 s = desc_stylize(s)
1261 1262 s = _urlify_text(s)
1262 1263 return literal(s)
1263 1264
1264 1265 def urlify_changesets(text_, repository):
1265 1266 """
1266 1267 Extract revision ids from changeset and make link from them
1267 1268
1268 1269 :param text_:
1269 1270 :param repository: repo name to build the URL with
1270 1271 """
1271 1272 from pylons import url # doh, we need to re-import url to mock it later
1272 1273
1273 1274 def url_func(match_obj):
1274 1275 rev = match_obj.group(0)
1275 1276 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1276 1277 'url': url('changeset_home', repo_name=repository, revision=rev),
1277 1278 'rev': rev,
1278 1279 }
1279 1280
1280 1281 return re.sub(r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])', url_func, text_)
1281 1282
1282 1283 def linkify_others(t, l):
1283 1284 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1284 1285 links = []
1285 1286 for e in urls.split(t):
1286 1287 if not urls.match(e):
1287 1288 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1288 1289 else:
1289 1290 links.append(e)
1290 1291
1291 1292 return ''.join(links)
1292 1293
1293 1294 def urlify_commit(text_, repository, link_=None):
1294 1295 """
1295 1296 Parses given text message and makes proper links.
1296 1297 issues are linked to given issue-server, and rest is a changeset link
1297 1298 if link_ is given, in other case it's a plain text
1298 1299
1299 1300 :param text_:
1300 1301 :param repository:
1301 1302 :param link_: changeset link
1302 1303 """
1303 1304 newtext = html_escape(text_)
1304 1305
1305 1306 # urlify changesets - extract revisions and make link out of them
1306 1307 newtext = urlify_changesets(newtext, repository)
1307 1308
1308 1309 # extract http/https links and make them real urls
1309 1310 newtext = _urlify_text(newtext)
1310 1311
1311 1312 newtext = urlify_issues(newtext, repository, link_)
1312 1313
1313 1314 return literal(newtext)
1314 1315
1315 1316 def urlify_issues(newtext, repository, link_=None):
1316 1317 from kallithea import CONFIG as conf
1317 1318
1318 1319 # allow multiple issue servers to be used
1319 1320 valid_indices = [
1320 1321 x.group(1)
1321 1322 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1322 1323 if x and 'issue_server_link%s' % x.group(1) in conf
1323 1324 and 'issue_prefix%s' % x.group(1) in conf
1324 1325 ]
1325 1326
1326 1327 if valid_indices:
1327 1328 log.debug('found issue server suffixes `%s` during valuation of: %s',
1328 1329 ','.join(valid_indices), newtext)
1329 1330
1330 1331 for pattern_index in valid_indices:
1331 1332 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1332 1333 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1333 1334 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1334 1335
1335 1336 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s',
1336 1337 pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1337 1338 ISSUE_PREFIX)
1338 1339
1339 1340 URL_PAT = re.compile(ISSUE_PATTERN)
1340 1341
1341 1342 def url_func(match_obj):
1342 1343 pref = ''
1343 1344 if match_obj.group().startswith(' '):
1344 1345 pref = ' '
1345 1346
1346 1347 issue_id = ''.join(match_obj.groups())
1347 1348 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1348 1349 if repository:
1349 1350 issue_url = issue_url.replace('{repo}', repository)
1350 1351 repo_name = repository.split(URL_SEP)[-1]
1351 1352 issue_url = issue_url.replace('{repo_name}', repo_name)
1352 1353
1353 1354 return (
1354 1355 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1355 1356 '%(issue-prefix)s%(id-repr)s'
1356 1357 '</a>'
1357 1358 ) % {
1358 1359 'pref': pref,
1359 1360 'cls': 'issue-tracker-link',
1360 1361 'url': issue_url,
1361 1362 'id-repr': issue_id,
1362 1363 'issue-prefix': ISSUE_PREFIX,
1363 1364 'serv': ISSUE_SERVER_LNK,
1364 1365 }
1365 1366 newtext = URL_PAT.sub(url_func, newtext)
1366 1367 log.debug('processed prefix:`%s` => %s', pattern_index, newtext)
1367 1368
1368 1369 # if we actually did something above
1369 1370 if link_:
1370 1371 # wrap not links into final link => link_
1371 1372 newtext = linkify_others(newtext, link_)
1372 1373 return newtext
1373 1374
1374 1375
1375 1376 def rst(source):
1376 1377 return literal('<div class="rst-block">%s</div>' %
1377 1378 MarkupRenderer.rst(source))
1378 1379
1379 1380
1380 1381 def rst_w_mentions(source):
1381 1382 """
1382 1383 Wrapped rst renderer with @mention highlighting
1383 1384
1384 1385 :param source:
1385 1386 """
1386 1387 return literal('<div class="rst-block">%s</div>' %
1387 1388 MarkupRenderer.rst_with_mentions(source))
1388 1389
1389 1390 def short_ref(ref_type, ref_name):
1390 1391 if ref_type == 'rev':
1391 1392 return short_id(ref_name)
1392 1393 return ref_name
1393 1394
1394 1395 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1395 1396 """
1396 1397 Return full markup for a href to changeset_home for a changeset.
1397 1398 If ref_type is branch it will link to changelog.
1398 1399 ref_name is shortened if ref_type is 'rev'.
1399 1400 if rev is specified show it too, explicitly linking to that revision.
1400 1401 """
1401 1402 txt = short_ref(ref_type, ref_name)
1402 1403 if ref_type == 'branch':
1403 1404 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1404 1405 else:
1405 1406 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1406 1407 l = link_to(repo_name + '#' + txt, u)
1407 1408 if rev and ref_type != 'rev':
1408 1409 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1409 1410 return l
1410 1411
1411 1412 def changeset_status(repo, revision):
1412 1413 return ChangesetStatusModel().get_status(repo, revision)
1413 1414
1414 1415
1415 1416 def changeset_status_lbl(changeset_status):
1416 1417 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1417 1418
1418 1419
1419 1420 def get_permission_name(key):
1420 1421 return dict(Permission.PERMS).get(key)
1421 1422
1422 1423
1423 1424 def journal_filter_help():
1424 1425 return _(textwrap.dedent('''
1425 1426 Example filter terms:
1426 1427 repository:vcs
1427 1428 username:developer
1428 1429 action:*push*
1429 1430 ip:127.0.0.1
1430 1431 date:20120101
1431 1432 date:[20120101100000 TO 20120102]
1432 1433
1433 1434 Generate wildcards using '*' character:
1434 1435 "repository:vcs*" - search everything starting with 'vcs'
1435 1436 "repository:*vcs*" - search for repository containing 'vcs'
1436 1437
1437 1438 Optional AND / OR operators in queries
1438 1439 "repository:vcs OR repository:test"
1439 1440 "username:test AND repository:test*"
1440 1441 '''))
1441 1442
1442 1443
1443 1444 def not_mapped_error(repo_name):
1444 1445 flash(_('%s repository is not mapped to db perhaps'
1445 1446 ' it was created or renamed from the filesystem'
1446 1447 ' please run the application again'
1447 1448 ' in order to rescan repositories') % repo_name, category='error')
1448 1449
1449 1450
1450 1451 def ip_range(ip_addr):
1451 1452 from kallithea.model.db import UserIpMap
1452 1453 s, e = UserIpMap._get_ip_range(ip_addr)
1453 1454 return '%s - %s' % (s, e)
1455
1456
1457 def form(url, method="post", **attrs):
1458 """Like webhelpers.html.tags.form but automatically using secure_form with
1459 authentication_token for POST. authentication_token is thus never leaked
1460 in the URL."""
1461 if method.lower() == 'get':
1462 return insecure_form(url, method=method, **attrs)
1463 # webhelpers will turn everything but GET into POST
1464 return secure_form(url, method=method, **attrs)
General Comments 0
You need to be logged in to leave comments. Login now