##// END OF EJS Templates
flake8: fix E202 whitespace before ')'
Mads Kiilerich -
r7725:e7683417 default
parent child Browse files
Show More
@@ -1,1306 +1,1306 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 json
22 22 import logging
23 23 import random
24 24 import re
25 25 import StringIO
26 26 import textwrap
27 27 import urlparse
28 28
29 29 from beaker.cache import cache_region
30 30 from pygments import highlight as code_highlight
31 31 from pygments.formatters.html import HtmlFormatter
32 32 from tg.i18n import ugettext as _
33 33 from webhelpers2.html import HTML, escape, literal
34 34 from webhelpers2.html.tags import NotGiven, Option, Options, _input, _make_safe_id_component, checkbox, end_form
35 35 from webhelpers2.html.tags import form as insecure_form
36 36 from webhelpers2.html.tags import hidden, link_to, password, radio
37 37 from webhelpers2.html.tags import select as webhelpers2_select
38 38 from webhelpers2.html.tags import submit, text, textarea
39 39 from webhelpers2.number import format_byte_size
40 40 from webhelpers2.text import chop_at, truncate, wrap_paragraphs
41 41 from webhelpers.pylonslib import Flash as _Flash
42 42
43 43 from kallithea.config.routing import url
44 44 from kallithea.lib.annotate import annotate_highlight
45 45 #==============================================================================
46 46 # PERMS
47 47 #==============================================================================
48 48 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoPermissionLevel
49 49 from kallithea.lib.markup_renderer import url_re
50 50 from kallithea.lib.pygmentsutils import get_custom_lexer
51 51 from kallithea.lib.utils2 import MENTIONS_REGEX, AttributeDict
52 52 from kallithea.lib.utils2 import age as _age
53 53 from kallithea.lib.utils2 import credentials_filter, safe_int, safe_str, safe_unicode, str2bool, time_to_datetime
54 54 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
55 55 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
56 56 #==============================================================================
57 57 # SCM FILTERS available via h.
58 58 #==============================================================================
59 59 from kallithea.lib.vcs.utils import author_email, author_name
60 60
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def canonical_url(*args, **kargs):
66 66 '''Like url(x, qualified=True), but returns url that not only is qualified
67 67 but also canonical, as configured in canonical_url'''
68 68 from kallithea import CONFIG
69 69 try:
70 70 parts = CONFIG.get('canonical_url', '').split('://', 1)
71 71 kargs['host'] = parts[1]
72 72 kargs['protocol'] = parts[0]
73 73 except IndexError:
74 74 kargs['qualified'] = True
75 75 return url(*args, **kargs)
76 76
77 77
78 78 def canonical_hostname():
79 79 '''Return canonical hostname of system'''
80 80 from kallithea import CONFIG
81 81 try:
82 82 parts = CONFIG.get('canonical_url', '').split('://', 1)
83 83 return parts[1].split('/', 1)[0]
84 84 except IndexError:
85 85 parts = url('home', qualified=True).split('://', 1)
86 86 return parts[1].split('/', 1)[0]
87 87
88 88
89 89 def html_escape(s):
90 90 """Return string with all html escaped.
91 91 This is also safe for javascript in html but not necessarily correct.
92 92 """
93 93 return (s
94 94 .replace('&', '&amp;')
95 95 .replace(">", "&gt;")
96 96 .replace("<", "&lt;")
97 97 .replace('"', "&quot;")
98 98 .replace("'", "&apos;") # Note: this is HTML5 not HTML4 and might not work in mails
99 99 )
100 100
101 101 def js(value):
102 102 """Convert Python value to the corresponding JavaScript representation.
103 103
104 104 This is necessary to safely insert arbitrary values into HTML <script>
105 105 sections e.g. using Mako template expression substitution.
106 106
107 107 Note: Rather than using this function, it's preferable to avoid the
108 108 insertion of values into HTML <script> sections altogether. Instead,
109 109 data should (to the extent possible) be passed to JavaScript using
110 110 data attributes or AJAX calls, eliminating the need for JS specific
111 111 escaping.
112 112
113 113 Note: This is not safe for use in attributes (e.g. onclick), because
114 114 quotes are not escaped.
115 115
116 116 Because the rules for parsing <script> varies between XHTML (where
117 117 normal rules apply for any special characters) and HTML (where
118 118 entities are not interpreted, but the literal string "</script>"
119 119 is forbidden), the function ensures that the result never contains
120 120 '&', '<' and '>', thus making it safe in both those contexts (but
121 121 not in attributes).
122 122 """
123 123 return literal(
124 124 ('(' + json.dumps(value) + ')')
125 125 # In JSON, the following can only appear in string literals.
126 126 .replace('&', r'\x26')
127 127 .replace('<', r'\x3c')
128 128 .replace('>', r'\x3e')
129 129 )
130 130
131 131
132 132 def jshtml(val):
133 133 """HTML escapes a string value, then converts the resulting string
134 134 to its corresponding JavaScript representation (see `js`).
135 135
136 136 This is used when a plain-text string (possibly containing special
137 137 HTML characters) will be used by a script in an HTML context (e.g.
138 138 element.innerHTML or jQuery's 'html' method).
139 139
140 140 If in doubt, err on the side of using `jshtml` over `js`, since it's
141 141 better to escape too much than too little.
142 142 """
143 143 return js(escape(val))
144 144
145 145
146 146 def shorter(s, size=20, firstline=False, postfix='...'):
147 147 """Truncate s to size, including the postfix string if truncating.
148 148 If firstline, truncate at newline.
149 149 """
150 150 if firstline:
151 151 s = s.split('\n', 1)[0].rstrip()
152 152 if len(s) > size:
153 153 return s[:size - len(postfix)] + postfix
154 154 return s
155 155
156 156
157 157 def reset(name, value, id=NotGiven, **attrs):
158 158 """Create a reset button, similar to webhelpers2.html.tags.submit ."""
159 159 return _input("reset", name, value, id, attrs)
160 160
161 161
162 162 def select(name, selected_values, options, id=NotGiven, **attrs):
163 163 """Convenient wrapper of webhelpers2 to let it accept options as a tuple list"""
164 164 if isinstance(options, list):
165 165 l = []
166 166 for x in options:
167 167 try:
168 168 value, label = x
169 169 except ValueError: # too many values to unpack
170 170 if isinstance(x, basestring):
171 171 value = label = x
172 172 else:
173 173 log.error('invalid select option %r', x)
174 174 raise
175 175 l.append(Option(label, value))
176 176 options = Options(l)
177 177 return webhelpers2_select(name, selected_values, options, id=id, **attrs)
178 178
179 179
180 180 safeid = _make_safe_id_component
181 181
182 182
183 183 def FID(raw_id, path):
184 184 """
185 185 Creates a unique ID for filenode based on it's hash of path and revision
186 186 it's safe to use in urls
187 187
188 188 :param raw_id:
189 189 :param path:
190 190 """
191 191
192 192 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
193 193
194 194
195 195 class _FilesBreadCrumbs(object):
196 196
197 197 def __call__(self, repo_name, rev, paths):
198 198 if isinstance(paths, str):
199 199 paths = safe_unicode(paths)
200 200 url_l = [link_to(repo_name, url('files_home',
201 201 repo_name=repo_name,
202 202 revision=rev, f_path=''),
203 203 class_='ypjax-link')]
204 204 paths_l = paths.split('/')
205 205 for cnt, p in enumerate(paths_l):
206 206 if p != '':
207 207 url_l.append(link_to(p,
208 208 url('files_home',
209 209 repo_name=repo_name,
210 210 revision=rev,
211 211 f_path='/'.join(paths_l[:cnt + 1])
212 212 ),
213 213 class_='ypjax-link'
214 214 )
215 215 )
216 216
217 217 return literal('/'.join(url_l))
218 218
219 219
220 220 files_breadcrumbs = _FilesBreadCrumbs()
221 221
222 222
223 223 class CodeHtmlFormatter(HtmlFormatter):
224 224 """
225 225 My code Html Formatter for source codes
226 226 """
227 227
228 228 def wrap(self, source, outfile):
229 229 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
230 230
231 231 def _wrap_code(self, source):
232 232 for cnt, it in enumerate(source):
233 233 i, t = it
234 234 t = '<span id="L%s">%s</span>' % (cnt + 1, t)
235 235 yield i, t
236 236
237 237 def _wrap_tablelinenos(self, inner):
238 238 dummyoutfile = StringIO.StringIO()
239 239 lncount = 0
240 240 for t, line in inner:
241 241 if t:
242 242 lncount += 1
243 243 dummyoutfile.write(line)
244 244
245 245 fl = self.linenostart
246 246 mw = len(str(lncount + fl - 1))
247 247 sp = self.linenospecial
248 248 st = self.linenostep
249 249 la = self.lineanchors
250 250 aln = self.anchorlinenos
251 251 nocls = self.noclasses
252 252 if sp:
253 253 lines = []
254 254
255 255 for i in range(fl, fl + lncount):
256 256 if i % st == 0:
257 257 if i % sp == 0:
258 258 if aln:
259 259 lines.append('<a href="#%s%d" class="special">%*d</a>' %
260 260 (la, i, mw, i))
261 261 else:
262 262 lines.append('<span class="special">%*d</span>' % (mw, i))
263 263 else:
264 264 if aln:
265 265 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
266 266 else:
267 267 lines.append('%*d' % (mw, i))
268 268 else:
269 269 lines.append('')
270 270 ls = '\n'.join(lines)
271 271 else:
272 272 lines = []
273 273 for i in range(fl, fl + lncount):
274 274 if i % st == 0:
275 275 if aln:
276 276 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
277 277 else:
278 278 lines.append('%*d' % (mw, i))
279 279 else:
280 280 lines.append('')
281 281 ls = '\n'.join(lines)
282 282
283 283 # in case you wonder about the seemingly redundant <div> here: since the
284 284 # content in the other cell also is wrapped in a div, some browsers in
285 285 # some configurations seem to mess up the formatting...
286 286 if nocls:
287 287 yield 0, ('<table class="%stable">' % self.cssclass +
288 288 '<tr><td><div class="linenodiv">'
289 289 '<pre>' + ls + '</pre></div></td>'
290 290 '<td id="hlcode" class="code">')
291 291 else:
292 292 yield 0, ('<table class="%stable">' % self.cssclass +
293 293 '<tr><td class="linenos"><div class="linenodiv">'
294 294 '<pre>' + ls + '</pre></div></td>'
295 295 '<td id="hlcode" class="code">')
296 296 yield 0, dummyoutfile.getvalue()
297 297 yield 0, '</td></tr></table>'
298 298
299 299
300 300 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
301 301
302 302
303 303 def _markup_whitespace(m):
304 304 groups = m.groups()
305 305 if groups[0]:
306 306 return '<u>\t</u>'
307 307 if groups[1]:
308 308 return ' <i></i>'
309 309
310 310
311 311 def markup_whitespace(s):
312 312 return _whitespace_re.sub(_markup_whitespace, s)
313 313
314 314
315 315 def pygmentize(filenode, **kwargs):
316 316 """
317 317 pygmentize function using pygments
318 318
319 319 :param filenode:
320 320 """
321 321 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
322 322 return literal(markup_whitespace(
323 323 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
324 324
325 325
326 326 def pygmentize_annotation(repo_name, filenode, **kwargs):
327 327 """
328 328 pygmentize function for annotation
329 329
330 330 :param filenode:
331 331 """
332 332
333 333 color_dict = {}
334 334
335 335 def gen_color(n=10000):
336 336 """generator for getting n of evenly distributed colors using
337 337 hsv color and golden ratio. It always return same order of colors
338 338
339 339 :returns: RGB tuple
340 340 """
341 341
342 342 def hsv_to_rgb(h, s, v):
343 343 if s == 0.0:
344 344 return v, v, v
345 345 i = int(h * 6.0) # XXX assume int() truncates!
346 346 f = (h * 6.0) - i
347 347 p = v * (1.0 - s)
348 348 q = v * (1.0 - s * f)
349 349 t = v * (1.0 - s * (1.0 - f))
350 350 i = i % 6
351 351 if i == 0:
352 352 return v, t, p
353 353 if i == 1:
354 354 return q, v, p
355 355 if i == 2:
356 356 return p, v, t
357 357 if i == 3:
358 358 return p, q, v
359 359 if i == 4:
360 360 return t, p, v
361 361 if i == 5:
362 362 return v, p, q
363 363
364 364 golden_ratio = 0.618033988749895
365 365 h = 0.22717784590367374
366 366
367 367 for _unused in xrange(n):
368 368 h += golden_ratio
369 369 h %= 1
370 370 HSV_tuple = [h, 0.95, 0.95]
371 371 RGB_tuple = hsv_to_rgb(*HSV_tuple)
372 372 yield map(lambda x: str(int(x * 256)), RGB_tuple)
373 373
374 374 cgenerator = gen_color()
375 375
376 376 def get_color_string(cs):
377 377 if cs in color_dict:
378 378 col = color_dict[cs]
379 379 else:
380 380 col = color_dict[cs] = cgenerator.next()
381 381 return "color: rgb(%s)! important;" % (', '.join(col))
382 382
383 383 def url_func(repo_name):
384 384
385 385 def _url_func(changeset):
386 386 author = escape(changeset.author)
387 387 date = changeset.date
388 388 message = escape(changeset.message)
389 389 tooltip_html = ("<b>Author:</b> %s<br/>"
390 390 "<b>Date:</b> %s</b><br/>"
391 391 "<b>Message:</b> %s") % (author, date, message)
392 392
393 393 lnk_format = show_id(changeset)
394 394 uri = link_to(
395 395 lnk_format,
396 396 url('changeset_home', repo_name=repo_name,
397 397 revision=changeset.raw_id),
398 398 style=get_color_string(changeset.raw_id),
399 399 **{'data-toggle': 'popover',
400 400 'data-content': tooltip_html}
401 401 )
402 402
403 403 uri += '\n'
404 404 return uri
405 405 return _url_func
406 406
407 407 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
408 408
409 409
410 410 class _Message(object):
411 411 """A message returned by ``Flash.pop_messages()``.
412 412
413 413 Converting the message to a string returns the message text. Instances
414 414 also have the following attributes:
415 415
416 416 * ``message``: the message text.
417 417 * ``category``: the category specified when the message was created.
418 418 """
419 419
420 420 def __init__(self, category, message):
421 421 self.category = category
422 422 self.message = message
423 423
424 424 def __str__(self):
425 425 return self.message
426 426
427 427 __unicode__ = __str__
428 428
429 429 def __html__(self):
430 430 return escape(safe_unicode(self.message))
431 431
432 432
433 433 class Flash(_Flash):
434 434
435 435 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
436 436 """
437 437 Show a message to the user _and_ log it through the specified function
438 438
439 439 category: notice (default), warning, error, success
440 440 logf: a custom log function - such as log.debug
441 441
442 442 logf defaults to log.info, unless category equals 'success', in which
443 443 case logf defaults to log.debug.
444 444 """
445 445 if logf is None:
446 446 logf = log.info
447 447 if category == 'success':
448 448 logf = log.debug
449 449
450 450 logf('Flash %s: %s', category, message)
451 451
452 452 super(Flash, self).__call__(message, category, ignore_duplicate)
453 453
454 454 def pop_messages(self):
455 455 """Return all accumulated messages and delete them from the session.
456 456
457 457 The return value is a list of ``Message`` objects.
458 458 """
459 459 from tg import session
460 460 messages = session.pop(self.session_key, [])
461 461 session.save()
462 462 return [_Message(*m) for m in messages]
463 463
464 464
465 465 flash = Flash()
466 466
467 467
468 468 age = lambda x, y=False: _age(x, y)
469 469 capitalize = lambda x: x.capitalize()
470 470 email = author_email
471 471 short_id = lambda x: x[:12]
472 472 hide_credentials = lambda x: ''.join(credentials_filter(x))
473 473
474 474
475 475 def show_id(cs):
476 476 """
477 477 Configurable function that shows ID
478 478 by default it's r123:fffeeefffeee
479 479
480 480 :param cs: changeset instance
481 481 """
482 482 from kallithea import CONFIG
483 483 def_len = safe_int(CONFIG.get('show_sha_length', 12))
484 484 show_rev = str2bool(CONFIG.get('show_revision_number', False))
485 485
486 486 raw_id = cs.raw_id[:def_len]
487 487 if show_rev:
488 488 return 'r%s:%s' % (cs.revision, raw_id)
489 489 else:
490 490 return raw_id
491 491
492 492
493 493 def fmt_date(date):
494 494 if date:
495 495 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf-8')
496 496
497 497 return ""
498 498
499 499
500 500 def is_git(repository):
501 501 if hasattr(repository, 'alias'):
502 502 _type = repository.alias
503 503 elif hasattr(repository, 'repo_type'):
504 504 _type = repository.repo_type
505 505 else:
506 506 _type = repository
507 507 return _type == 'git'
508 508
509 509
510 510 def is_hg(repository):
511 511 if hasattr(repository, 'alias'):
512 512 _type = repository.alias
513 513 elif hasattr(repository, 'repo_type'):
514 514 _type = repository.repo_type
515 515 else:
516 516 _type = repository
517 517 return _type == 'hg'
518 518
519 519
520 520 @cache_region('long_term', 'user_or_none')
521 521 def user_or_none(author):
522 522 """Try to match email part of VCS committer string with a local user - or return None"""
523 523 from kallithea.model.db import User
524 524 email = author_email(author)
525 525 if email:
526 526 return User.get_by_email(email, cache=True) # cache will only use sql_cache_short
527 527 return None
528 528
529 529
530 530 def email_or_none(author):
531 531 """Try to match email part of VCS committer string with a local user.
532 532 Return primary email of user, email part of the specified author name, or None."""
533 533 if not author:
534 534 return None
535 535 user = user_or_none(author)
536 536 if user is not None:
537 537 return user.email # always use main email address - not necessarily the one used to find user
538 538
539 539 # extract email from the commit string
540 540 email = author_email(author)
541 541 if email:
542 542 return email
543 543
544 544 # No valid email, not a valid user in the system, none!
545 545 return None
546 546
547 547
548 548 def person(author, show_attr="username"):
549 549 """Find the user identified by 'author', return one of the users attributes,
550 550 default to the username attribute, None if there is no user"""
551 551 from kallithea.model.db import User
552 552 # attr to return from fetched user
553 553 person_getter = lambda usr: getattr(usr, show_attr)
554 554
555 555 # if author is already an instance use it for extraction
556 556 if isinstance(author, User):
557 557 return person_getter(author)
558 558
559 559 user = user_or_none(author)
560 560 if user is not None:
561 561 return person_getter(user)
562 562
563 563 # Still nothing? Just pass back the author name if any, else the email
564 564 return author_name(author) or email(author)
565 565
566 566
567 567 def person_by_id(id_, show_attr="username"):
568 568 from kallithea.model.db import User
569 569 # attr to return from fetched user
570 570 person_getter = lambda usr: getattr(usr, show_attr)
571 571
572 572 # maybe it's an ID ?
573 573 if str(id_).isdigit() or isinstance(id_, int):
574 574 id_ = int(id_)
575 575 user = User.get(id_)
576 576 if user is not None:
577 577 return person_getter(user)
578 578 return id_
579 579
580 580
581 581 def boolicon(value):
582 582 """Returns boolean value of a value, represented as small html image of true/false
583 583 icons
584 584
585 585 :param value: value
586 586 """
587 587
588 588 if value:
589 589 return HTML.tag('i', class_="icon-ok")
590 590 else:
591 591 return HTML.tag('i', class_="icon-minus-circled")
592 592
593 593
594 594 def action_parser(user_log, feed=False, parse_cs=False):
595 595 """
596 596 This helper will action_map the specified string action into translated
597 597 fancy names with icons and links
598 598
599 599 :param user_log: user log instance
600 600 :param feed: use output for feeds (no html and fancy icons)
601 601 :param parse_cs: parse Changesets into VCS instances
602 602 """
603 603
604 604 action = user_log.action
605 605 action_params = ' '
606 606
607 607 x = action.split(':')
608 608
609 609 if len(x) > 1:
610 610 action, action_params = x
611 611
612 612 def get_cs_links():
613 613 revs_limit = 3 # display this amount always
614 614 revs_top_limit = 50 # show upto this amount of changesets hidden
615 615 revs_ids = action_params.split(',')
616 616 deleted = user_log.repository is None
617 617 if deleted:
618 618 return ','.join(revs_ids)
619 619
620 620 repo_name = user_log.repository.repo_name
621 621
622 622 def lnk(rev, repo_name):
623 623 lazy_cs = False
624 624 title_ = None
625 625 url_ = '#'
626 626 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
627 627 if rev.op and rev.ref_name:
628 628 if rev.op == 'delete_branch':
629 629 lbl = _('Deleted branch: %s') % rev.ref_name
630 630 elif rev.op == 'tag':
631 631 lbl = _('Created tag: %s') % rev.ref_name
632 632 else:
633 633 lbl = 'Unknown operation %s' % rev.op
634 634 else:
635 635 lazy_cs = True
636 636 lbl = rev.short_id[:8]
637 637 url_ = url('changeset_home', repo_name=repo_name,
638 638 revision=rev.raw_id)
639 639 else:
640 640 # changeset cannot be found - it might have been stripped or removed
641 641 lbl = rev[:12]
642 642 title_ = _('Changeset %s not found') % lbl
643 643 if parse_cs:
644 644 return link_to(lbl, url_, title=title_, **{'data-toggle': 'tooltip'})
645 645 return link_to(lbl, url_, class_='lazy-cs' if lazy_cs else '',
646 646 **{'data-raw_id': rev.raw_id, 'data-repo_name': repo_name})
647 647
648 648 def _get_op(rev_txt):
649 649 _op = None
650 650 _name = rev_txt
651 651 if len(rev_txt.split('=>')) == 2:
652 652 _op, _name = rev_txt.split('=>')
653 653 return _op, _name
654 654
655 655 revs = []
656 656 if len(filter(lambda v: v != '', revs_ids)) > 0:
657 657 repo = None
658 658 for rev in revs_ids[:revs_top_limit]:
659 659 _op, _name = _get_op(rev)
660 660
661 661 # we want parsed changesets, or new log store format is bad
662 662 if parse_cs:
663 663 try:
664 664 if repo is None:
665 665 repo = user_log.repository.scm_instance
666 666 _rev = repo.get_changeset(rev)
667 667 revs.append(_rev)
668 668 except ChangesetDoesNotExistError:
669 669 log.error('cannot find revision %s in this repo', rev)
670 670 revs.append(rev)
671 671 else:
672 672 _rev = AttributeDict({
673 673 'short_id': rev[:12],
674 674 'raw_id': rev,
675 675 'message': '',
676 676 'op': _op,
677 677 'ref_name': _name
678 678 })
679 679 revs.append(_rev)
680 680 cs_links = [" " + ', '.join(
681 681 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
682 682 )]
683 683 _op1, _name1 = _get_op(revs_ids[0])
684 684 _op2, _name2 = _get_op(revs_ids[-1])
685 685
686 686 _rev = '%s...%s' % (_name1, _name2)
687 687
688 688 compare_view = (
689 689 ' <div class="compare_view" data-toggle="tooltip" title="%s">'
690 690 '<a href="%s">%s</a> </div>' % (
691 691 _('Show all combined changesets %s->%s') % (
692 692 revs_ids[0][:12], revs_ids[-1][:12]
693 693 ),
694 694 url('changeset_home', repo_name=repo_name,
695 695 revision=_rev
696 696 ),
697 697 _('Compare view')
698 698 )
699 699 )
700 700
701 701 # if we have exactly one more than normally displayed
702 702 # just display it, takes less space than displaying
703 703 # "and 1 more revisions"
704 704 if len(revs_ids) == revs_limit + 1:
705 705 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
706 706
707 707 # hidden-by-default ones
708 708 if len(revs_ids) > revs_limit + 1:
709 709 uniq_id = revs_ids[0]
710 710 html_tmpl = (
711 711 '<span> %s <a class="show_more" id="_%s" '
712 712 'href="#more">%s</a> %s</span>'
713 713 )
714 714 if not feed:
715 715 cs_links.append(html_tmpl % (
716 716 _('and'),
717 717 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
718 718 _('revisions')
719 719 )
720 720 )
721 721
722 722 if not feed:
723 723 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
724 724 else:
725 725 html_tmpl = '<span id="%s"> %s </span>'
726 726
727 727 morelinks = ', '.join(
728 728 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
729 729 )
730 730
731 731 if len(revs_ids) > revs_top_limit:
732 732 morelinks += ', ...'
733 733
734 734 cs_links.append(html_tmpl % (uniq_id, morelinks))
735 735 if len(revs) > 1:
736 736 cs_links.append(compare_view)
737 737 return ''.join(cs_links)
738 738
739 739 def get_fork_name():
740 740 repo_name = action_params
741 741 url_ = url('summary_home', repo_name=repo_name)
742 742 return _('Fork name %s') % link_to(action_params, url_)
743 743
744 744 def get_user_name():
745 745 user_name = action_params
746 746 return user_name
747 747
748 748 def get_users_group():
749 749 group_name = action_params
750 750 return group_name
751 751
752 752 def get_pull_request():
753 753 from kallithea.model.db import PullRequest
754 754 pull_request_id = action_params
755 755 nice_id = PullRequest.make_nice_id(pull_request_id)
756 756
757 757 deleted = user_log.repository is None
758 758 if deleted:
759 759 repo_name = user_log.repository_name
760 760 else:
761 761 repo_name = user_log.repository.repo_name
762 762
763 763 return link_to(_('Pull request %s') % nice_id,
764 764 url('pullrequest_show', repo_name=repo_name,
765 765 pull_request_id=pull_request_id))
766 766
767 767 def get_archive_name():
768 768 archive_name = action_params
769 769 return archive_name
770 770
771 771 # action : translated str, callback(extractor), icon
772 772 action_map = {
773 773 'user_deleted_repo': (_('[deleted] repository'),
774 774 None, 'icon-trashcan'),
775 775 'user_created_repo': (_('[created] repository'),
776 776 None, 'icon-plus'),
777 777 'user_created_fork': (_('[created] repository as fork'),
778 778 None, 'icon-fork'),
779 779 'user_forked_repo': (_('[forked] repository'),
780 780 get_fork_name, 'icon-fork'),
781 781 'user_updated_repo': (_('[updated] repository'),
782 782 None, 'icon-pencil'),
783 783 'user_downloaded_archive': (_('[downloaded] archive from repository'),
784 784 get_archive_name, 'icon-download-cloud'),
785 785 'admin_deleted_repo': (_('[delete] repository'),
786 786 None, 'icon-trashcan'),
787 787 'admin_created_repo': (_('[created] repository'),
788 788 None, 'icon-plus'),
789 789 'admin_forked_repo': (_('[forked] repository'),
790 790 None, 'icon-fork'),
791 791 'admin_updated_repo': (_('[updated] repository'),
792 792 None, 'icon-pencil'),
793 793 'admin_created_user': (_('[created] user'),
794 794 get_user_name, 'icon-user'),
795 795 'admin_updated_user': (_('[updated] user'),
796 796 get_user_name, 'icon-user'),
797 797 'admin_created_users_group': (_('[created] user group'),
798 798 get_users_group, 'icon-pencil'),
799 799 'admin_updated_users_group': (_('[updated] user group'),
800 800 get_users_group, 'icon-pencil'),
801 801 'user_commented_revision': (_('[commented] on revision in repository'),
802 802 get_cs_links, 'icon-comment'),
803 803 'user_commented_pull_request': (_('[commented] on pull request for'),
804 804 get_pull_request, 'icon-comment'),
805 805 'user_closed_pull_request': (_('[closed] pull request for'),
806 806 get_pull_request, 'icon-ok'),
807 807 'push': (_('[pushed] into'),
808 808 get_cs_links, 'icon-move-up'),
809 809 'push_local': (_('[committed via Kallithea] into repository'),
810 810 get_cs_links, 'icon-pencil'),
811 811 'push_remote': (_('[pulled from remote] into repository'),
812 812 get_cs_links, 'icon-move-up'),
813 813 'pull': (_('[pulled] from'),
814 814 None, 'icon-move-down'),
815 815 'started_following_repo': (_('[started following] repository'),
816 816 None, 'icon-heart'),
817 817 'stopped_following_repo': (_('[stopped following] repository'),
818 818 None, 'icon-heart-empty'),
819 819 }
820 820
821 821 action_str = action_map.get(action, action)
822 822 if feed:
823 823 action = action_str[0].replace('[', '').replace(']', '')
824 824 else:
825 825 action = action_str[0] \
826 826 .replace('[', '<b>') \
827 827 .replace(']', '</b>')
828 828
829 829 action_params_func = lambda: ""
830 830
831 831 if callable(action_str[1]):
832 832 action_params_func = action_str[1]
833 833
834 834 def action_parser_icon():
835 835 action = user_log.action
836 836 action_params = None
837 837 x = action.split(':')
838 838
839 839 if len(x) > 1:
840 840 action, action_params = x
841 841
842 842 ico = action_map.get(action, ['', '', ''])[2]
843 843 html = """<i class="%s"></i>""" % ico
844 844 return literal(html)
845 845
846 846 # returned callbacks we need to call to get
847 847 return [lambda: literal(action), action_params_func, action_parser_icon]
848 848
849 849
850 850
851 851
852 852
853 853 #==============================================================================
854 854 # GRAVATAR URL
855 855 #==============================================================================
856 856 def gravatar_div(email_address, cls='', size=30, **div_attributes):
857 857 """Return an html literal with a span around a gravatar if they are enabled.
858 858 Extra keyword parameters starting with 'div_' will get the prefix removed
859 859 and '_' changed to '-' and be used as attributes on the div. The default
860 860 class is 'gravatar'.
861 861 """
862 862 from tg import tmpl_context as c
863 863 if not c.visual.use_gravatar:
864 864 return ''
865 865 if 'div_class' not in div_attributes:
866 866 div_attributes['div_class'] = "gravatar"
867 867 attributes = []
868 868 for k, v in sorted(div_attributes.items()):
869 869 assert k.startswith('div_'), k
870 870 attributes.append(' %s="%s"' % (k[4:].replace('_', '-'), escape(v)))
871 871 return literal("""<span%s>%s</span>""" %
872 872 (''.join(attributes),
873 873 gravatar(email_address, cls=cls, size=size)))
874 874
875 875
876 876 def gravatar(email_address, cls='', size=30):
877 877 """return html element of the gravatar
878 878
879 879 This method will return an <img> with the resolution double the size (for
880 880 retina screens) of the image. If the url returned from gravatar_url is
881 881 empty then we fallback to using an icon.
882 882
883 883 """
884 884 from tg import tmpl_context as c
885 885 if not c.visual.use_gravatar:
886 886 return ''
887 887
888 888 src = gravatar_url(email_address, size * 2)
889 889
890 890 if src:
891 891 # here it makes sense to use style="width: ..." (instead of, say, a
892 892 # stylesheet) because we using this to generate a high-res (retina) size
893 893 html = ('<i class="icon-gravatar {cls}"'
894 894 ' style="font-size: {size}px;background-size: {size}px;background-image: url(\'{src}\')"'
895 895 '></i>').format(cls=cls, size=size, src=src)
896 896
897 897 else:
898 898 # if src is empty then there was no gravatar, so we use a font icon
899 899 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
900 900 .format(cls=cls, size=size, src=src))
901 901
902 902 return literal(html)
903 903
904 904
905 905 def gravatar_url(email_address, size=30, default=''):
906 906 # doh, we need to re-import those to mock it later
907 907 from kallithea.config.routing import url
908 908 from kallithea.model.db import User
909 909 from tg import tmpl_context as c
910 910 if not c.visual.use_gravatar:
911 911 return ""
912 912
913 913 _def = 'anonymous@kallithea-scm.org' # default gravatar
914 914 email_address = email_address or _def
915 915
916 916 if email_address == _def:
917 917 return default
918 918
919 919 parsed_url = urlparse.urlparse(url.current(qualified=True))
920 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
920 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL) \
921 921 .replace('{email}', email_address) \
922 922 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
923 923 .replace('{netloc}', parsed_url.netloc) \
924 924 .replace('{scheme}', parsed_url.scheme) \
925 925 .replace('{size}', safe_str(size))
926 926 return url
927 927
928 928
929 929 def changed_tooltip(nodes):
930 930 """
931 931 Generates a html string for changed nodes in changeset page.
932 932 It limits the output to 30 entries
933 933
934 934 :param nodes: LazyNodesGenerator
935 935 """
936 936 if nodes:
937 937 pref = ': <br/> '
938 938 suf = ''
939 939 if len(nodes) > 30:
940 940 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
941 941 return literal(pref + '<br/> '.join([safe_unicode(x.path)
942 942 for x in nodes[:30]]) + suf)
943 943 else:
944 944 return ': ' + _('No files')
945 945
946 946
947 947 def fancy_file_stats(stats):
948 948 """
949 949 Displays a fancy two colored bar for number of added/deleted
950 950 lines of code on file
951 951
952 952 :param stats: two element list of added/deleted lines of code
953 953 """
954 954 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
955 955 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
956 956
957 957 a, d = stats['added'], stats['deleted']
958 958 width = 100
959 959
960 960 if stats['binary']:
961 961 # binary mode
962 962 lbl = ''
963 963 bin_op = 1
964 964
965 965 if BIN_FILENODE in stats['ops']:
966 966 lbl = 'bin+'
967 967
968 968 if NEW_FILENODE in stats['ops']:
969 969 lbl += _('new file')
970 970 bin_op = NEW_FILENODE
971 971 elif MOD_FILENODE in stats['ops']:
972 972 lbl += _('mod')
973 973 bin_op = MOD_FILENODE
974 974 elif DEL_FILENODE in stats['ops']:
975 975 lbl += _('del')
976 976 bin_op = DEL_FILENODE
977 977 elif RENAMED_FILENODE in stats['ops']:
978 978 lbl += _('rename')
979 979 bin_op = RENAMED_FILENODE
980 980
981 981 # chmod can go with other operations
982 982 if CHMOD_FILENODE in stats['ops']:
983 983 _org_lbl = _('chmod')
984 984 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
985 985
986 986 #import ipdb;ipdb.set_trace()
987 987 b_d = '<div class="bin bin%s progress-bar" style="width:100%%">%s</div>' % (bin_op, lbl)
988 988 b_a = '<div class="bin bin1" style="width:0%"></div>'
989 989 return literal('<div style="width:%spx" class="progress">%s%s</div>' % (width, b_a, b_d))
990 990
991 991 t = stats['added'] + stats['deleted']
992 992 unit = float(width) / (t or 1)
993 993
994 994 # needs > 9% of width to be visible or 0 to be hidden
995 995 a_p = max(9, unit * a) if a > 0 else 0
996 996 d_p = max(9, unit * d) if d > 0 else 0
997 997 p_sum = a_p + d_p
998 998
999 999 if p_sum > width:
1000 1000 # adjust the percentage to be == 100% since we adjusted to 9
1001 1001 if a_p > d_p:
1002 1002 a_p = a_p - (p_sum - width)
1003 1003 else:
1004 1004 d_p = d_p - (p_sum - width)
1005 1005
1006 1006 a_v = a if a > 0 else ''
1007 1007 d_v = d if d > 0 else ''
1008 1008
1009 1009 d_a = '<div class="added progress-bar" style="width:%s%%">%s</div>' % (
1010 1010 a_p, a_v
1011 1011 )
1012 1012 d_d = '<div class="deleted progress-bar" style="width:%s%%">%s</div>' % (
1013 1013 d_p, d_v
1014 1014 )
1015 1015 return literal('<div class="progress" style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1016 1016
1017 1017
1018 1018 _URLIFY_RE = re.compile(r'''
1019 1019 # URL markup
1020 1020 (?P<url>%s) |
1021 1021 # @mention markup
1022 1022 (?P<mention>%s) |
1023 1023 # Changeset hash markup
1024 1024 (?<!\w|[-_])
1025 1025 (?P<hash>[0-9a-f]{12,40})
1026 1026 (?!\w|[-_]) |
1027 1027 # Markup of *bold text*
1028 1028 (?:
1029 1029 (?:^|(?<=\s))
1030 1030 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
1031 1031 (?![*\w])
1032 1032 ) |
1033 1033 # "Stylize" markup
1034 1034 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1035 1035 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1036 1036 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
1037 1037 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
1038 1038 \[(?P<tag>[a-z]+)\]
1039 1039 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
1040 1040 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
1041 1041
1042 1042
1043 1043 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
1044 1044 """
1045 1045 Parses given text message and make literal html with markup.
1046 1046 The text will be truncated to the specified length.
1047 1047 Hashes are turned into changeset links to specified repository.
1048 1048 URLs links to what they say.
1049 1049 Issues are linked to given issue-server.
1050 1050 If link_ is provided, all text not already linking somewhere will link there.
1051 1051 """
1052 1052
1053 1053 def _replace(match_obj):
1054 1054 url = match_obj.group('url')
1055 1055 if url is not None:
1056 1056 return '<a href="%(url)s">%(url)s</a>' % {'url': url}
1057 1057 mention = match_obj.group('mention')
1058 1058 if mention is not None:
1059 1059 return '<b>%s</b>' % mention
1060 1060 hash_ = match_obj.group('hash')
1061 1061 if hash_ is not None and repo_name is not None:
1062 1062 from kallithea.config.routing import url # doh, we need to re-import url to mock it later
1063 1063 return '<a class="changeset_hash" href="%(url)s">%(hash)s</a>' % {
1064 1064 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
1065 1065 'hash': hash_,
1066 1066 }
1067 1067 bold = match_obj.group('bold')
1068 1068 if bold is not None:
1069 1069 return '<b>*%s*</b>' % _urlify(bold[1:-1])
1070 1070 if stylize:
1071 1071 seen = match_obj.group('seen')
1072 1072 if seen:
1073 1073 return '<div class="label label-meta" data-tag="see">see =&gt; %s</div>' % seen
1074 1074 license = match_obj.group('license')
1075 1075 if license:
1076 1076 return '<div class="label label-meta" data-tag="license"><a href="http://www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
1077 1077 tagtype = match_obj.group('tagtype')
1078 1078 if tagtype:
1079 1079 tagvalue = match_obj.group('tagvalue')
1080 1080 return '<div class="label label-meta" data-tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
1081 1081 lang = match_obj.group('lang')
1082 1082 if lang:
1083 1083 return '<div class="label label-meta" data-tag="lang">%s</div>' % lang
1084 1084 tag = match_obj.group('tag')
1085 1085 if tag:
1086 1086 return '<div class="label label-meta" data-tag="%s">%s</div>' % (tag, tag)
1087 1087 return match_obj.group(0)
1088 1088
1089 1089 def _urlify(s):
1090 1090 """
1091 1091 Extract urls from text and make html links out of them
1092 1092 """
1093 1093 return _URLIFY_RE.sub(_replace, s)
1094 1094
1095 1095 if truncate is None:
1096 1096 s = s.rstrip()
1097 1097 else:
1098 1098 s = truncatef(s, truncate, whole_word=True)
1099 1099 s = html_escape(s)
1100 1100 s = _urlify(s)
1101 1101 if repo_name is not None:
1102 1102 s = urlify_issues(s, repo_name)
1103 1103 if link_ is not None:
1104 1104 # make href around everything that isn't a href already
1105 1105 s = linkify_others(s, link_)
1106 1106 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1107 1107 # Turn HTML5 into more valid HTML4 as required by some mail readers.
1108 1108 # (This is not done in one step in html_escape, because character codes like
1109 1109 # &#123; risk to be seen as an issue reference due to the presence of '#'.)
1110 1110 s = s.replace("&apos;", "&#39;")
1111 1111 return literal(s)
1112 1112
1113 1113
1114 1114 def linkify_others(t, l):
1115 1115 """Add a default link to html with links.
1116 1116 HTML doesn't allow nesting of links, so the outer link must be broken up
1117 1117 in pieces and give space for other links.
1118 1118 """
1119 1119 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1120 1120 links = []
1121 1121 for e in urls.split(t):
1122 1122 if e.strip() and not urls.match(e):
1123 1123 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1124 1124 else:
1125 1125 links.append(e)
1126 1126
1127 1127 return ''.join(links)
1128 1128
1129 1129
1130 1130 # Global variable that will hold the actual urlify_issues function body.
1131 1131 # Will be set on first use when the global configuration has been read.
1132 1132 _urlify_issues_f = None
1133 1133
1134 1134
1135 1135 def urlify_issues(newtext, repo_name):
1136 1136 """Urlify issue references according to .ini configuration"""
1137 1137 global _urlify_issues_f
1138 1138 if _urlify_issues_f is None:
1139 1139 from kallithea import CONFIG
1140 1140 from kallithea.model.db import URL_SEP
1141 1141 assert CONFIG['sqlalchemy.url'] # make sure config has been loaded
1142 1142
1143 1143 # Build chain of urlify functions, starting with not doing any transformation
1144 1144 tmp_urlify_issues_f = lambda s: s
1145 1145
1146 1146 issue_pat_re = re.compile(r'issue_pat(.*)')
1147 1147 for k in CONFIG.keys():
1148 1148 # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
1149 1149 m = issue_pat_re.match(k)
1150 1150 if m is None:
1151 1151 continue
1152 1152 suffix = m.group(1)
1153 1153 issue_pat = CONFIG.get(k)
1154 1154 issue_server_link = CONFIG.get('issue_server_link%s' % suffix)
1155 1155 issue_sub = CONFIG.get('issue_sub%s' % suffix)
1156 1156 if not issue_pat or not issue_server_link or issue_sub is None: # issue_sub can be empty but should be present
1157 1157 log.error('skipping incomplete issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
1158 1158 continue
1159 1159
1160 1160 # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
1161 1161 try:
1162 1162 issue_re = re.compile(issue_pat)
1163 1163 except re.error as e:
1164 1164 log.error('skipping invalid issue pattern %r: %r -> %r %r. Error: %s', suffix, issue_pat, issue_server_link, issue_sub, str(e))
1165 1165 continue
1166 1166
1167 1167 log.debug('issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
1168 1168
1169 1169 def issues_replace(match_obj,
1170 1170 issue_server_link=issue_server_link, issue_sub=issue_sub):
1171 1171 try:
1172 1172 issue_url = match_obj.expand(issue_server_link)
1173 1173 except (IndexError, re.error) as e:
1174 1174 log.error('invalid issue_url setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1175 1175 issue_url = issue_server_link
1176 1176 issue_url = issue_url.replace('{repo}', repo_name)
1177 1177 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1178 1178 # if issue_sub is empty use the matched issue reference verbatim
1179 1179 if not issue_sub:
1180 1180 issue_text = match_obj.group()
1181 1181 else:
1182 1182 try:
1183 1183 issue_text = match_obj.expand(issue_sub)
1184 1184 except (IndexError, re.error) as e:
1185 1185 log.error('invalid issue_sub setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1186 1186 issue_text = match_obj.group()
1187 1187
1188 1188 return (
1189 1189 '<a class="issue-tracker-link" href="%(url)s">'
1190 1190 '%(text)s'
1191 1191 '</a>'
1192 1192 ) % {
1193 1193 'url': issue_url,
1194 1194 'text': issue_text,
1195 1195 }
1196 1196 tmp_urlify_issues_f = (lambda s,
1197 1197 issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f:
1198 1198 issue_re.sub(issues_replace, chain_f(s)))
1199 1199
1200 1200 # Set tmp function globally - atomically
1201 1201 _urlify_issues_f = tmp_urlify_issues_f
1202 1202
1203 1203 return _urlify_issues_f(newtext)
1204 1204
1205 1205
1206 1206 def render_w_mentions(source, repo_name=None):
1207 1207 """
1208 1208 Render plain text with revision hashes and issue references urlified
1209 1209 and with @mention highlighting.
1210 1210 """
1211 1211 s = safe_unicode(source)
1212 1212 s = urlify_text(s, repo_name=repo_name)
1213 1213 return literal('<div class="formatted-fixed">%s</div>' % s)
1214 1214
1215 1215
1216 1216 def short_ref(ref_type, ref_name):
1217 1217 if ref_type == 'rev':
1218 1218 return short_id(ref_name)
1219 1219 return ref_name
1220 1220
1221 1221
1222 1222 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1223 1223 """
1224 1224 Return full markup for a href to changeset_home for a changeset.
1225 1225 If ref_type is branch it will link to changelog.
1226 1226 ref_name is shortened if ref_type is 'rev'.
1227 1227 if rev is specified show it too, explicitly linking to that revision.
1228 1228 """
1229 1229 txt = short_ref(ref_type, ref_name)
1230 1230 if ref_type == 'branch':
1231 1231 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1232 1232 else:
1233 1233 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1234 1234 l = link_to(repo_name + '#' + txt, u)
1235 1235 if rev and ref_type != 'rev':
1236 1236 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1237 1237 return l
1238 1238
1239 1239
1240 1240 def changeset_status(repo, revision):
1241 1241 from kallithea.model.changeset_status import ChangesetStatusModel
1242 1242 return ChangesetStatusModel().get_status(repo, revision)
1243 1243
1244 1244
1245 1245 def changeset_status_lbl(changeset_status):
1246 1246 from kallithea.model.db import ChangesetStatus
1247 1247 return ChangesetStatus.get_status_lbl(changeset_status)
1248 1248
1249 1249
1250 1250 def get_permission_name(key):
1251 1251 from kallithea.model.db import Permission
1252 1252 return dict(Permission.PERMS).get(key)
1253 1253
1254 1254
1255 1255 def journal_filter_help():
1256 1256 return _(textwrap.dedent('''
1257 1257 Example filter terms:
1258 1258 repository:vcs
1259 1259 username:developer
1260 1260 action:*push*
1261 1261 ip:127.0.0.1
1262 1262 date:20120101
1263 1263 date:[20120101100000 TO 20120102]
1264 1264
1265 1265 Generate wildcards using '*' character:
1266 1266 "repository:vcs*" - search everything starting with 'vcs'
1267 1267 "repository:*vcs*" - search for repository containing 'vcs'
1268 1268
1269 1269 Optional AND / OR operators in queries
1270 1270 "repository:vcs OR repository:test"
1271 1271 "username:test AND repository:test*"
1272 1272 '''))
1273 1273
1274 1274
1275 1275 def not_mapped_error(repo_name):
1276 1276 flash(_('%s repository is not mapped to db perhaps'
1277 1277 ' it was created or renamed from the filesystem'
1278 1278 ' please run the application again'
1279 1279 ' in order to rescan repositories') % repo_name, category='error')
1280 1280
1281 1281
1282 1282 def ip_range(ip_addr):
1283 1283 from kallithea.model.db import UserIpMap
1284 1284 s, e = UserIpMap._get_ip_range(ip_addr)
1285 1285 return '%s - %s' % (s, e)
1286 1286
1287 1287
1288 1288 session_csrf_secret_name = "_session_csrf_secret_token"
1289 1289
1290 1290 def session_csrf_secret_token():
1291 1291 """Return (and create) the current session's CSRF protection token."""
1292 1292 from tg import session
1293 1293 if not session_csrf_secret_name in session:
1294 1294 session[session_csrf_secret_name] = str(random.getrandbits(128))
1295 1295 session.save()
1296 1296 return session[session_csrf_secret_name]
1297 1297
1298 1298 def form(url, method="post", **attrs):
1299 1299 """Like webhelpers.html.tags.form , but automatically adding
1300 1300 session_csrf_secret_token for POST. The secret is thus never leaked in GET
1301 1301 URLs.
1302 1302 """
1303 1303 form = insecure_form(url, method, **attrs)
1304 1304 if method.lower() == 'get':
1305 1305 return form
1306 1306 return form + HTML.div(hidden(session_csrf_secret_name, session_csrf_secret_token()), style="display: none;")
General Comments 0
You need to be logged in to leave comments. Login now