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