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