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