##// END OF EJS Templates
removed bad translation string
marcink -
r3947:ff78716a beta
parent child Browse files
Show More
@@ -1,1413 +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 358 class _Message(object):
359 359 """A message returned by ``Flash.pop_messages()``.
360 360
361 361 Converting the message to a string returns the message text. Instances
362 362 also have the following attributes:
363 363
364 364 * ``message``: the message text.
365 365 * ``category``: the category specified when the message was created.
366 366 """
367 367
368 368 def __init__(self, category, message):
369 369 self.category=category
370 370 self.message=message
371 371
372 372 def __str__(self):
373 373 return self.message
374 374
375 375 __unicode__ = __str__
376 376
377 377 def __html__(self):
378 378 return escape(safe_unicode(self.message))
379 379
380 380 class Flash(_Flash):
381 381
382 382 def pop_messages(self):
383 383 """Return all accumulated messages and delete them from the session.
384 384
385 385 The return value is a list of ``Message`` objects.
386 386 """
387 387 from pylons import session
388 388 messages = session.pop(self.session_key, [])
389 389 session.save()
390 390 return [_Message(*m) for m in messages]
391 391
392 392 flash = Flash()
393 393
394 394 #==============================================================================
395 395 # SCM FILTERS available via h.
396 396 #==============================================================================
397 397 from rhodecode.lib.vcs.utils import author_name, author_email
398 398 from rhodecode.lib.utils2 import credentials_filter, age as _age
399 399 from rhodecode.model.db import User, ChangesetStatus
400 400
401 401 age = lambda x, y=False: _age(x, y)
402 402 capitalize = lambda x: x.capitalize()
403 403 email = author_email
404 404 short_id = lambda x: x[:12]
405 405 hide_credentials = lambda x: ''.join(credentials_filter(x))
406 406
407 407
408 408 def show_id(cs):
409 409 """
410 410 Configurable function that shows ID
411 411 by default it's r123:fffeeefffeee
412 412
413 413 :param cs: changeset instance
414 414 """
415 415 from rhodecode import CONFIG
416 416 def_len = safe_int(CONFIG.get('show_sha_length', 12))
417 417 show_rev = str2bool(CONFIG.get('show_revision_number', True))
418 418
419 419 raw_id = cs.raw_id[:def_len]
420 420 if show_rev:
421 421 return 'r%s:%s' % (cs.revision, raw_id)
422 422 else:
423 423 return '%s' % (raw_id)
424 424
425 425
426 426 def fmt_date(date):
427 427 if date:
428 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
428 _fmt = u"%a, %d %b %Y %H:%M:%S".encode('utf8')
429 429 return date.strftime(_fmt).decode('utf8')
430 430
431 431 return ""
432 432
433 433
434 434 def is_git(repository):
435 435 if hasattr(repository, 'alias'):
436 436 _type = repository.alias
437 437 elif hasattr(repository, 'repo_type'):
438 438 _type = repository.repo_type
439 439 else:
440 440 _type = repository
441 441 return _type == 'git'
442 442
443 443
444 444 def is_hg(repository):
445 445 if hasattr(repository, 'alias'):
446 446 _type = repository.alias
447 447 elif hasattr(repository, 'repo_type'):
448 448 _type = repository.repo_type
449 449 else:
450 450 _type = repository
451 451 return _type == 'hg'
452 452
453 453
454 454 def email_or_none(author):
455 455 # extract email from the commit string
456 456 _email = email(author)
457 457 if _email != '':
458 458 # check it against RhodeCode database, and use the MAIN email for this
459 459 # user
460 460 user = User.get_by_email(_email, case_insensitive=True, cache=True)
461 461 if user is not None:
462 462 return user.email
463 463 return _email
464 464
465 465 # See if it contains a username we can get an email from
466 466 user = User.get_by_username(author_name(author), case_insensitive=True,
467 467 cache=True)
468 468 if user is not None:
469 469 return user.email
470 470
471 471 # No valid email, not a valid user in the system, none!
472 472 return None
473 473
474 474
475 475 def person(author, show_attr="username_and_name"):
476 476 # attr to return from fetched user
477 477 person_getter = lambda usr: getattr(usr, show_attr)
478 478
479 479 # Valid email in the attribute passed, see if they're in the system
480 480 _email = email(author)
481 481 if _email != '':
482 482 user = User.get_by_email(_email, case_insensitive=True, cache=True)
483 483 if user is not None:
484 484 return person_getter(user)
485 485
486 486 # Maybe it's a username?
487 487 _author = author_name(author)
488 488 user = User.get_by_username(_author, case_insensitive=True,
489 489 cache=True)
490 490 if user is not None:
491 491 return person_getter(user)
492 492
493 493 # Still nothing? Just pass back the author name if any, else the email
494 494 return _author or _email
495 495
496 496
497 497 def person_by_id(id_, show_attr="username_and_name"):
498 498 # attr to return from fetched user
499 499 person_getter = lambda usr: getattr(usr, show_attr)
500 500
501 501 #maybe it's an ID ?
502 502 if str(id_).isdigit() or isinstance(id_, int):
503 503 id_ = int(id_)
504 504 user = User.get(id_)
505 505 if user is not None:
506 506 return person_getter(user)
507 507 return id_
508 508
509 509
510 510 def desc_stylize(value):
511 511 """
512 512 converts tags from value into html equivalent
513 513
514 514 :param value:
515 515 """
516 516 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
517 517 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
518 518 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
519 519 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
520 520 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
521 521 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
522 522 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
523 523 '<div class="metatag" tag="lang">\\2</div>', value)
524 524 value = re.sub(r'\[([a-z]+)\]',
525 525 '<div class="metatag" tag="\\1">\\1</div>', value)
526 526
527 527 return value
528 528
529 529
530 530 def boolicon(value):
531 531 """Returns boolean value of a value, represented as small html image of true/false
532 532 icons
533 533
534 534 :param value: value
535 535 """
536 536
537 537 if value:
538 538 return HTML.tag('img', src=url("/images/icons/accept.png"),
539 539 alt=_('True'))
540 540 else:
541 541 return HTML.tag('img', src=url("/images/icons/cancel.png"),
542 542 alt=_('False'))
543 543
544 544
545 545 def action_parser(user_log, feed=False, parse_cs=False):
546 546 """
547 547 This helper will action_map the specified string action into translated
548 548 fancy names with icons and links
549 549
550 550 :param user_log: user log instance
551 551 :param feed: use output for feeds (no html and fancy icons)
552 552 :param parse_cs: parse Changesets into VCS instances
553 553 """
554 554
555 555 action = user_log.action
556 556 action_params = ' '
557 557
558 558 x = action.split(':')
559 559
560 560 if len(x) > 1:
561 561 action, action_params = x
562 562
563 563 def get_cs_links():
564 564 revs_limit = 3 # display this amount always
565 565 revs_top_limit = 50 # show upto this amount of changesets hidden
566 566 revs_ids = action_params.split(',')
567 567 deleted = user_log.repository is None
568 568 if deleted:
569 569 return ','.join(revs_ids)
570 570
571 571 repo_name = user_log.repository.repo_name
572 572
573 573 def lnk(rev, repo_name):
574 574 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
575 575 lazy_cs = True
576 576 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
577 577 lazy_cs = False
578 578 lbl = '?'
579 579 if rev.op == 'delete_branch':
580 580 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
581 581 title = ''
582 582 elif rev.op == 'tag':
583 583 lbl = '%s' % _('Created tag: %s') % rev.ref_name
584 584 title = ''
585 585 _url = '#'
586 586
587 587 else:
588 588 lbl = '%s' % (rev.short_id[:8])
589 589 _url = url('changeset_home', repo_name=repo_name,
590 590 revision=rev.raw_id)
591 591 title = tooltip(rev.message)
592 592 else:
593 593 ## changeset cannot be found/striped/removed etc.
594 594 lbl = ('%s' % rev)[:12]
595 595 _url = '#'
596 596 title = _('Changeset not found')
597 597 if parse_cs:
598 598 return link_to(lbl, _url, title=title, class_='tooltip')
599 599 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
600 600 class_='lazy-cs' if lazy_cs else '')
601 601
602 602 def _get_op(rev_txt):
603 603 _op = None
604 604 _name = rev_txt
605 605 if len(rev_txt.split('=>')) == 2:
606 606 _op, _name = rev_txt.split('=>')
607 607 return _op, _name
608 608
609 609 revs = []
610 610 if len(filter(lambda v: v != '', revs_ids)) > 0:
611 611 repo = None
612 612 for rev in revs_ids[:revs_top_limit]:
613 613 _op, _name = _get_op(rev)
614 614
615 615 # we want parsed changesets, or new log store format is bad
616 616 if parse_cs:
617 617 try:
618 618 if repo is None:
619 619 repo = user_log.repository.scm_instance
620 620 _rev = repo.get_changeset(rev)
621 621 revs.append(_rev)
622 622 except ChangesetDoesNotExistError:
623 623 log.error('cannot find revision %s in this repo' % rev)
624 624 revs.append(rev)
625 625 continue
626 626 else:
627 627 _rev = AttributeDict({
628 628 'short_id': rev[:12],
629 629 'raw_id': rev,
630 630 'message': '',
631 631 'op': _op,
632 632 'ref_name': _name
633 633 })
634 634 revs.append(_rev)
635 635 cs_links = [" " + ', '.join(
636 636 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
637 637 )]
638 638 _op1, _name1 = _get_op(revs_ids[0])
639 639 _op2, _name2 = _get_op(revs_ids[-1])
640 640
641 641 _rev = '%s...%s' % (_name1, _name2)
642 642
643 643 compare_view = (
644 644 ' <div class="compare_view tooltip" title="%s">'
645 645 '<a href="%s">%s</a> </div>' % (
646 646 _('Show all combined changesets %s->%s') % (
647 647 revs_ids[0][:12], revs_ids[-1][:12]
648 648 ),
649 649 url('changeset_home', repo_name=repo_name,
650 650 revision=_rev
651 651 ),
652 652 _('compare view')
653 653 )
654 654 )
655 655
656 656 # if we have exactly one more than normally displayed
657 657 # just display it, takes less space than displaying
658 658 # "and 1 more revisions"
659 659 if len(revs_ids) == revs_limit + 1:
660 660 rev = revs[revs_limit]
661 661 cs_links.append(", " + lnk(rev, repo_name))
662 662
663 663 # hidden-by-default ones
664 664 if len(revs_ids) > revs_limit + 1:
665 665 uniq_id = revs_ids[0]
666 666 html_tmpl = (
667 667 '<span> %s <a class="show_more" id="_%s" '
668 668 'href="#more">%s</a> %s</span>'
669 669 )
670 670 if not feed:
671 671 cs_links.append(html_tmpl % (
672 672 _('and'),
673 673 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
674 674 _('revisions')
675 675 )
676 676 )
677 677
678 678 if not feed:
679 679 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
680 680 else:
681 681 html_tmpl = '<span id="%s"> %s </span>'
682 682
683 683 morelinks = ', '.join(
684 684 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
685 685 )
686 686
687 687 if len(revs_ids) > revs_top_limit:
688 688 morelinks += ', ...'
689 689
690 690 cs_links.append(html_tmpl % (uniq_id, morelinks))
691 691 if len(revs) > 1:
692 692 cs_links.append(compare_view)
693 693 return ''.join(cs_links)
694 694
695 695 def get_fork_name():
696 696 repo_name = action_params
697 697 _url = url('summary_home', repo_name=repo_name)
698 698 return _('fork name %s') % link_to(action_params, _url)
699 699
700 700 def get_user_name():
701 701 user_name = action_params
702 702 return user_name
703 703
704 704 def get_users_group():
705 705 group_name = action_params
706 706 return group_name
707 707
708 708 def get_pull_request():
709 709 pull_request_id = action_params
710 710 deleted = user_log.repository is None
711 711 if deleted:
712 712 repo_name = user_log.repository_name
713 713 else:
714 714 repo_name = user_log.repository.repo_name
715 715 return link_to(_('Pull request #%s') % pull_request_id,
716 716 url('pullrequest_show', repo_name=repo_name,
717 717 pull_request_id=pull_request_id))
718 718
719 719 def get_archive_name():
720 720 archive_name = action_params
721 721 return archive_name
722 722
723 723 # action : translated str, callback(extractor), icon
724 724 action_map = {
725 725 'user_deleted_repo': (_('[deleted] repository'),
726 726 None, 'database_delete.png'),
727 727 'user_created_repo': (_('[created] repository'),
728 728 None, 'database_add.png'),
729 729 'user_created_fork': (_('[created] repository as fork'),
730 730 None, 'arrow_divide.png'),
731 731 'user_forked_repo': (_('[forked] repository'),
732 732 get_fork_name, 'arrow_divide.png'),
733 733 'user_updated_repo': (_('[updated] repository'),
734 734 None, 'database_edit.png'),
735 735 'user_downloaded_archive': (_('[downloaded] archive from repository'),
736 736 get_archive_name, 'page_white_compressed.png'),
737 737 'admin_deleted_repo': (_('[delete] repository'),
738 738 None, 'database_delete.png'),
739 739 'admin_created_repo': (_('[created] repository'),
740 740 None, 'database_add.png'),
741 741 'admin_forked_repo': (_('[forked] repository'),
742 742 None, 'arrow_divide.png'),
743 743 'admin_updated_repo': (_('[updated] repository'),
744 744 None, 'database_edit.png'),
745 745 'admin_created_user': (_('[created] user'),
746 746 get_user_name, 'user_add.png'),
747 747 'admin_updated_user': (_('[updated] user'),
748 748 get_user_name, 'user_edit.png'),
749 749 'admin_created_users_group': (_('[created] user group'),
750 750 get_users_group, 'group_add.png'),
751 751 'admin_updated_users_group': (_('[updated] user group'),
752 752 get_users_group, 'group_edit.png'),
753 753 'user_commented_revision': (_('[commented] on revision in repository'),
754 754 get_cs_links, 'comment_add.png'),
755 755 'user_commented_pull_request': (_('[commented] on pull request for'),
756 756 get_pull_request, 'comment_add.png'),
757 757 'user_closed_pull_request': (_('[closed] pull request for'),
758 758 get_pull_request, 'tick.png'),
759 759 'push': (_('[pushed] into'),
760 760 get_cs_links, 'script_add.png'),
761 761 'push_local': (_('[committed via RhodeCode] into repository'),
762 762 get_cs_links, 'script_edit.png'),
763 763 'push_remote': (_('[pulled from remote] into repository'),
764 764 get_cs_links, 'connect.png'),
765 765 'pull': (_('[pulled] from'),
766 766 None, 'down_16.png'),
767 767 'started_following_repo': (_('[started following] repository'),
768 768 None, 'heart_add.png'),
769 769 'stopped_following_repo': (_('[stopped following] repository'),
770 770 None, 'heart_delete.png'),
771 771 }
772 772
773 773 action_str = action_map.get(action, action)
774 774 if feed:
775 775 action = action_str[0].replace('[', '').replace(']', '')
776 776 else:
777 777 action = action_str[0]\
778 778 .replace('[', '<span class="journal_highlight">')\
779 779 .replace(']', '</span>')
780 780
781 781 action_params_func = lambda: ""
782 782
783 783 if callable(action_str[1]):
784 784 action_params_func = action_str[1]
785 785
786 786 def action_parser_icon():
787 787 action = user_log.action
788 788 action_params = None
789 789 x = action.split(':')
790 790
791 791 if len(x) > 1:
792 792 action, action_params = x
793 793
794 794 tmpl = """<img src="%s%s" alt="%s"/>"""
795 795 ico = action_map.get(action, ['', '', ''])[2]
796 796 return literal(tmpl % ((url('/images/icons/')), ico, action))
797 797
798 798 # returned callbacks we need to call to get
799 799 return [lambda: literal(action), action_params_func, action_parser_icon]
800 800
801 801
802 802
803 803 #==============================================================================
804 804 # PERMS
805 805 #==============================================================================
806 806 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
807 807 HasRepoPermissionAny, HasRepoPermissionAll, HasReposGroupPermissionAll, \
808 808 HasReposGroupPermissionAny
809 809
810 810
811 811 #==============================================================================
812 812 # GRAVATAR URL
813 813 #==============================================================================
814 814
815 815 def gravatar_url(email_address, size=30, ssl_enabled=True):
816 816 from pylons import url # doh, we need to re-import url to mock it later
817 817 from rhodecode import CONFIG
818 818
819 819 _def = 'anonymous@rhodecode.org' # default gravatar
820 820 use_gravatar = str2bool(CONFIG.get('use_gravatar'))
821 821 alternative_gravatar_url = CONFIG.get('alternative_gravatar_url', '')
822 822 email_address = email_address or _def
823 823 if not use_gravatar or not email_address or email_address == _def:
824 824 f = lambda a, l: min(l, key=lambda x: abs(x - a))
825 825 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
826 826
827 827 if use_gravatar and alternative_gravatar_url:
828 828 tmpl = alternative_gravatar_url
829 829 parsed_url = urlparse.urlparse(url.current(qualified=True))
830 830 tmpl = tmpl.replace('{email}', email_address)\
831 831 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
832 832 .replace('{netloc}', parsed_url.netloc)\
833 833 .replace('{scheme}', parsed_url.scheme)\
834 834 .replace('{size}', str(size))
835 835 return tmpl
836 836
837 837 default = 'identicon'
838 838 baseurl_nossl = "http://www.gravatar.com/avatar/"
839 839 baseurl_ssl = "https://secure.gravatar.com/avatar/"
840 840 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
841 841
842 842 if isinstance(email_address, unicode):
843 843 #hashlib crashes on unicode items
844 844 email_address = safe_str(email_address)
845 845 # construct the url
846 846 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
847 847 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
848 848
849 849 return gravatar_url
850 850
851 851
852 852 class Page(_Page):
853 853 """
854 854 Custom pager to match rendering style with YUI paginator
855 855 """
856 856
857 857 def _get_pos(self, cur_page, max_page, items):
858 858 edge = (items / 2) + 1
859 859 if (cur_page <= edge):
860 860 radius = max(items / 2, items - cur_page)
861 861 elif (max_page - cur_page) < edge:
862 862 radius = (items - 1) - (max_page - cur_page)
863 863 else:
864 864 radius = items / 2
865 865
866 866 left = max(1, (cur_page - (radius)))
867 867 right = min(max_page, cur_page + (radius))
868 868 return left, cur_page, right
869 869
870 870 def _range(self, regexp_match):
871 871 """
872 872 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
873 873
874 874 Arguments:
875 875
876 876 regexp_match
877 877 A "re" (regular expressions) match object containing the
878 878 radius of linked pages around the current page in
879 879 regexp_match.group(1) as a string
880 880
881 881 This function is supposed to be called as a callable in
882 882 re.sub.
883 883
884 884 """
885 885 radius = int(regexp_match.group(1))
886 886
887 887 # Compute the first and last page number within the radius
888 888 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
889 889 # -> leftmost_page = 5
890 890 # -> rightmost_page = 9
891 891 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
892 892 self.last_page,
893 893 (radius * 2) + 1)
894 894 nav_items = []
895 895
896 896 # Create a link to the first page (unless we are on the first page
897 897 # or there would be no need to insert '..' spacers)
898 898 if self.page != self.first_page and self.first_page < leftmost_page:
899 899 nav_items.append(self._pagerlink(self.first_page, self.first_page))
900 900
901 901 # Insert dots if there are pages between the first page
902 902 # and the currently displayed page range
903 903 if leftmost_page - self.first_page > 1:
904 904 # Wrap in a SPAN tag if nolink_attr is set
905 905 text = '..'
906 906 if self.dotdot_attr:
907 907 text = HTML.span(c=text, **self.dotdot_attr)
908 908 nav_items.append(text)
909 909
910 910 for thispage in xrange(leftmost_page, rightmost_page + 1):
911 911 # Hilight the current page number and do not use a link
912 912 if thispage == self.page:
913 913 text = '%s' % (thispage,)
914 914 # Wrap in a SPAN tag if nolink_attr is set
915 915 if self.curpage_attr:
916 916 text = HTML.span(c=text, **self.curpage_attr)
917 917 nav_items.append(text)
918 918 # Otherwise create just a link to that page
919 919 else:
920 920 text = '%s' % (thispage,)
921 921 nav_items.append(self._pagerlink(thispage, text))
922 922
923 923 # Insert dots if there are pages between the displayed
924 924 # page numbers and the end of the page range
925 925 if self.last_page - rightmost_page > 1:
926 926 text = '..'
927 927 # Wrap in a SPAN tag if nolink_attr is set
928 928 if self.dotdot_attr:
929 929 text = HTML.span(c=text, **self.dotdot_attr)
930 930 nav_items.append(text)
931 931
932 932 # Create a link to the very last page (unless we are on the last
933 933 # page or there would be no need to insert '..' spacers)
934 934 if self.page != self.last_page and rightmost_page < self.last_page:
935 935 nav_items.append(self._pagerlink(self.last_page, self.last_page))
936 936
937 937 ## prerender links
938 938 nav_items.append(literal('<link rel="prerender" href="/rhodecode/changelog/1?page=%s">' % str(int(self.page)+1)))
939 939 return self.separator.join(nav_items)
940 940
941 941 def pager(self, format='~2~', page_param='page', partial_param='partial',
942 942 show_if_single_page=False, separator=' ', onclick=None,
943 943 symbol_first='<<', symbol_last='>>',
944 944 symbol_previous='<', symbol_next='>',
945 945 link_attr={'class': 'pager_link', 'rel': 'prerender'},
946 946 curpage_attr={'class': 'pager_curpage'},
947 947 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
948 948
949 949 self.curpage_attr = curpage_attr
950 950 self.separator = separator
951 951 self.pager_kwargs = kwargs
952 952 self.page_param = page_param
953 953 self.partial_param = partial_param
954 954 self.onclick = onclick
955 955 self.link_attr = link_attr
956 956 self.dotdot_attr = dotdot_attr
957 957
958 958 # Don't show navigator if there is no more than one page
959 959 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
960 960 return ''
961 961
962 962 from string import Template
963 963 # Replace ~...~ in token format by range of pages
964 964 result = re.sub(r'~(\d+)~', self._range, format)
965 965
966 966 # Interpolate '%' variables
967 967 result = Template(result).safe_substitute({
968 968 'first_page': self.first_page,
969 969 'last_page': self.last_page,
970 970 'page': self.page,
971 971 'page_count': self.page_count,
972 972 'items_per_page': self.items_per_page,
973 973 'first_item': self.first_item,
974 974 'last_item': self.last_item,
975 975 'item_count': self.item_count,
976 976 'link_first': self.page > self.first_page and \
977 977 self._pagerlink(self.first_page, symbol_first) or '',
978 978 'link_last': self.page < self.last_page and \
979 979 self._pagerlink(self.last_page, symbol_last) or '',
980 980 'link_previous': self.previous_page and \
981 981 self._pagerlink(self.previous_page, symbol_previous) \
982 982 or HTML.span(symbol_previous, class_="yui-pg-previous"),
983 983 'link_next': self.next_page and \
984 984 self._pagerlink(self.next_page, symbol_next) \
985 985 or HTML.span(symbol_next, class_="yui-pg-next")
986 986 })
987 987
988 988 return literal(result)
989 989
990 990
991 991 #==============================================================================
992 992 # REPO PAGER, PAGER FOR REPOSITORY
993 993 #==============================================================================
994 994 class RepoPage(Page):
995 995
996 996 def __init__(self, collection, page=1, items_per_page=20,
997 997 item_count=None, url=None, **kwargs):
998 998
999 999 """Create a "RepoPage" instance. special pager for paging
1000 1000 repository
1001 1001 """
1002 1002 self._url_generator = url
1003 1003
1004 1004 # Safe the kwargs class-wide so they can be used in the pager() method
1005 1005 self.kwargs = kwargs
1006 1006
1007 1007 # Save a reference to the collection
1008 1008 self.original_collection = collection
1009 1009
1010 1010 self.collection = collection
1011 1011
1012 1012 # The self.page is the number of the current page.
1013 1013 # The first page has the number 1!
1014 1014 try:
1015 1015 self.page = int(page) # make it int() if we get it as a string
1016 1016 except (ValueError, TypeError):
1017 1017 self.page = 1
1018 1018
1019 1019 self.items_per_page = items_per_page
1020 1020
1021 1021 # Unless the user tells us how many items the collections has
1022 1022 # we calculate that ourselves.
1023 1023 if item_count is not None:
1024 1024 self.item_count = item_count
1025 1025 else:
1026 1026 self.item_count = len(self.collection)
1027 1027
1028 1028 # Compute the number of the first and last available page
1029 1029 if self.item_count > 0:
1030 1030 self.first_page = 1
1031 1031 self.page_count = int(math.ceil(float(self.item_count) /
1032 1032 self.items_per_page))
1033 1033 self.last_page = self.first_page + self.page_count - 1
1034 1034
1035 1035 # Make sure that the requested page number is the range of
1036 1036 # valid pages
1037 1037 if self.page > self.last_page:
1038 1038 self.page = self.last_page
1039 1039 elif self.page < self.first_page:
1040 1040 self.page = self.first_page
1041 1041
1042 1042 # Note: the number of items on this page can be less than
1043 1043 # items_per_page if the last page is not full
1044 1044 self.first_item = max(0, (self.item_count) - (self.page *
1045 1045 items_per_page))
1046 1046 self.last_item = ((self.item_count - 1) - items_per_page *
1047 1047 (self.page - 1))
1048 1048
1049 1049 self.items = list(self.collection[self.first_item:self.last_item + 1])
1050 1050
1051 1051 # Links to previous and next page
1052 1052 if self.page > self.first_page:
1053 1053 self.previous_page = self.page - 1
1054 1054 else:
1055 1055 self.previous_page = None
1056 1056
1057 1057 if self.page < self.last_page:
1058 1058 self.next_page = self.page + 1
1059 1059 else:
1060 1060 self.next_page = None
1061 1061
1062 1062 # No items available
1063 1063 else:
1064 1064 self.first_page = None
1065 1065 self.page_count = 0
1066 1066 self.last_page = None
1067 1067 self.first_item = None
1068 1068 self.last_item = None
1069 1069 self.previous_page = None
1070 1070 self.next_page = None
1071 1071 self.items = []
1072 1072
1073 1073 # This is a subclass of the 'list' type. Initialise the list now.
1074 1074 list.__init__(self, reversed(self.items))
1075 1075
1076 1076
1077 1077 def changed_tooltip(nodes):
1078 1078 """
1079 1079 Generates a html string for changed nodes in changeset page.
1080 1080 It limits the output to 30 entries
1081 1081
1082 1082 :param nodes: LazyNodesGenerator
1083 1083 """
1084 1084 if nodes:
1085 1085 pref = ': <br/> '
1086 1086 suf = ''
1087 1087 if len(nodes) > 30:
1088 1088 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1089 1089 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1090 1090 for x in nodes[:30]]) + suf)
1091 1091 else:
1092 1092 return ': ' + _('No Files')
1093 1093
1094 1094
1095 1095 def repo_link(groups_and_repos):
1096 1096 """
1097 1097 Makes a breadcrumbs link to repo within a group
1098 1098 joins &raquo; on each group to create a fancy link
1099 1099
1100 1100 ex::
1101 1101 group >> subgroup >> repo
1102 1102
1103 1103 :param groups_and_repos:
1104 1104 :param last_url:
1105 1105 """
1106 1106 groups, just_name, repo_name = groups_and_repos
1107 1107 last_url = url('summary_home', repo_name=repo_name)
1108 1108 last_link = link_to(just_name, last_url)
1109 1109
1110 1110 def make_link(group):
1111 1111 return link_to(group.name,
1112 1112 url('repos_group_home', group_name=group.group_name))
1113 1113 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1114 1114
1115 1115
1116 1116 def fancy_file_stats(stats):
1117 1117 """
1118 1118 Displays a fancy two colored bar for number of added/deleted
1119 1119 lines of code on file
1120 1120
1121 1121 :param stats: two element list of added/deleted lines of code
1122 1122 """
1123 1123 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1124 1124 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1125 1125
1126 1126 def cgen(l_type, a_v, d_v):
1127 1127 mapping = {'tr': 'top-right-rounded-corner-mid',
1128 1128 'tl': 'top-left-rounded-corner-mid',
1129 1129 'br': 'bottom-right-rounded-corner-mid',
1130 1130 'bl': 'bottom-left-rounded-corner-mid'}
1131 1131 map_getter = lambda x: mapping[x]
1132 1132
1133 1133 if l_type == 'a' and d_v:
1134 1134 #case when added and deleted are present
1135 1135 return ' '.join(map(map_getter, ['tl', 'bl']))
1136 1136
1137 1137 if l_type == 'a' and not d_v:
1138 1138 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1139 1139
1140 1140 if l_type == 'd' and a_v:
1141 1141 return ' '.join(map(map_getter, ['tr', 'br']))
1142 1142
1143 1143 if l_type == 'd' and not a_v:
1144 1144 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1145 1145
1146 1146 a, d = stats['added'], stats['deleted']
1147 1147 width = 100
1148 1148
1149 1149 if stats['binary']:
1150 1150 #binary mode
1151 1151 lbl = ''
1152 1152 bin_op = 1
1153 1153
1154 1154 if BIN_FILENODE in stats['ops']:
1155 1155 lbl = 'bin+'
1156 1156
1157 1157 if NEW_FILENODE in stats['ops']:
1158 1158 lbl += _('new file')
1159 1159 bin_op = NEW_FILENODE
1160 1160 elif MOD_FILENODE in stats['ops']:
1161 1161 lbl += _('mod')
1162 1162 bin_op = MOD_FILENODE
1163 1163 elif DEL_FILENODE in stats['ops']:
1164 1164 lbl += _('del')
1165 1165 bin_op = DEL_FILENODE
1166 1166 elif RENAMED_FILENODE in stats['ops']:
1167 1167 lbl += _('rename')
1168 1168 bin_op = RENAMED_FILENODE
1169 1169
1170 1170 #chmod can go with other operations
1171 1171 if CHMOD_FILENODE in stats['ops']:
1172 1172 _org_lbl = _('chmod')
1173 1173 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1174 1174
1175 1175 #import ipdb;ipdb.set_trace()
1176 1176 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1177 1177 b_a = '<div class="bin bin1" style="width:0%%"></div>'
1178 1178 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1179 1179
1180 1180 t = stats['added'] + stats['deleted']
1181 1181 unit = float(width) / (t or 1)
1182 1182
1183 1183 # needs > 9% of width to be visible or 0 to be hidden
1184 1184 a_p = max(9, unit * a) if a > 0 else 0
1185 1185 d_p = max(9, unit * d) if d > 0 else 0
1186 1186 p_sum = a_p + d_p
1187 1187
1188 1188 if p_sum > width:
1189 1189 #adjust the percentage to be == 100% since we adjusted to 9
1190 1190 if a_p > d_p:
1191 1191 a_p = a_p - (p_sum - width)
1192 1192 else:
1193 1193 d_p = d_p - (p_sum - width)
1194 1194
1195 1195 a_v = a if a > 0 else ''
1196 1196 d_v = d if d > 0 else ''
1197 1197
1198 1198 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1199 1199 cgen('a', a_v, d_v), a_p, a_v
1200 1200 )
1201 1201 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1202 1202 cgen('d', a_v, d_v), d_p, d_v
1203 1203 )
1204 1204 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1205 1205
1206 1206
1207 1207 def urlify_text(text_, safe=True):
1208 1208 """
1209 1209 Extrac urls from text and make html links out of them
1210 1210
1211 1211 :param text_:
1212 1212 """
1213 1213
1214 1214 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
1215 1215 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1216 1216
1217 1217 def url_func(match_obj):
1218 1218 url_full = match_obj.groups()[0]
1219 1219 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1220 1220 _newtext = url_pat.sub(url_func, text_)
1221 1221 if safe:
1222 1222 return literal(_newtext)
1223 1223 return _newtext
1224 1224
1225 1225
1226 1226 def urlify_changesets(text_, repository):
1227 1227 """
1228 1228 Extract revision ids from changeset and make link from them
1229 1229
1230 1230 :param text_:
1231 1231 :param repository: repo name to build the URL with
1232 1232 """
1233 1233 from pylons import url # doh, we need to re-import url to mock it later
1234 1234 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1235 1235
1236 1236 def url_func(match_obj):
1237 1237 rev = match_obj.groups()[1]
1238 1238 pref = match_obj.groups()[0]
1239 1239 suf = match_obj.groups()[2]
1240 1240
1241 1241 tmpl = (
1242 1242 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1243 1243 '%(rev)s</a>%(suf)s'
1244 1244 )
1245 1245 return tmpl % {
1246 1246 'pref': pref,
1247 1247 'cls': 'revision-link',
1248 1248 'url': url('changeset_home', repo_name=repository, revision=rev),
1249 1249 'rev': rev,
1250 1250 'suf': suf
1251 1251 }
1252 1252
1253 1253 newtext = URL_PAT.sub(url_func, text_)
1254 1254
1255 1255 return newtext
1256 1256
1257 1257
1258 1258 def urlify_commit(text_, repository=None, link_=None):
1259 1259 """
1260 1260 Parses given text message and makes proper links.
1261 1261 issues are linked to given issue-server, and rest is a changeset link
1262 1262 if link_ is given, in other case it's a plain text
1263 1263
1264 1264 :param text_:
1265 1265 :param repository:
1266 1266 :param link_: changeset link
1267 1267 """
1268 1268 import traceback
1269 1269 from pylons import url # doh, we need to re-import url to mock it later
1270 1270
1271 1271 def escaper(string):
1272 1272 return string.replace('<', '&lt;').replace('>', '&gt;')
1273 1273
1274 1274 def linkify_others(t, l):
1275 1275 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1276 1276 links = []
1277 1277 for e in urls.split(t):
1278 1278 if not urls.match(e):
1279 1279 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1280 1280 else:
1281 1281 links.append(e)
1282 1282
1283 1283 return ''.join(links)
1284 1284
1285 1285 # urlify changesets - extrac revisions and make link out of them
1286 1286 newtext = urlify_changesets(escaper(text_), repository)
1287 1287
1288 1288 # extract http/https links and make them real urls
1289 1289 newtext = urlify_text(newtext, safe=False)
1290 1290
1291 1291 try:
1292 1292 from rhodecode import CONFIG
1293 1293 conf = CONFIG
1294 1294
1295 1295 # allow multiple issue servers to be used
1296 1296 valid_indices = [
1297 1297 x.group(1)
1298 1298 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1299 1299 if x and 'issue_server_link%s' % x.group(1) in conf
1300 1300 and 'issue_prefix%s' % x.group(1) in conf
1301 1301 ]
1302 1302
1303 1303 log.debug('found issue server suffixes `%s` during valuation of: %s'
1304 1304 % (','.join(valid_indices), newtext))
1305 1305
1306 1306 for pattern_index in valid_indices:
1307 1307 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1308 1308 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1309 1309 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1310 1310
1311 1311 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1312 1312 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1313 1313 ISSUE_PREFIX))
1314 1314
1315 1315 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1316 1316
1317 1317 def url_func(match_obj):
1318 1318 pref = ''
1319 1319 if match_obj.group().startswith(' '):
1320 1320 pref = ' '
1321 1321
1322 1322 issue_id = ''.join(match_obj.groups())
1323 1323 tmpl = (
1324 1324 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1325 1325 '%(issue-prefix)s%(id-repr)s'
1326 1326 '</a>'
1327 1327 )
1328 1328 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1329 1329 if repository:
1330 1330 url = url.replace('{repo}', repository)
1331 1331 repo_name = repository.split(URL_SEP)[-1]
1332 1332 url = url.replace('{repo_name}', repo_name)
1333 1333
1334 1334 return tmpl % {
1335 1335 'pref': pref,
1336 1336 'cls': 'issue-tracker-link',
1337 1337 'url': url,
1338 1338 'id-repr': issue_id,
1339 1339 'issue-prefix': ISSUE_PREFIX,
1340 1340 'serv': ISSUE_SERVER_LNK,
1341 1341 }
1342 1342 newtext = URL_PAT.sub(url_func, newtext)
1343 1343 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1344 1344
1345 1345 # if we actually did something above
1346 1346 if link_:
1347 1347 # wrap not links into final link => link_
1348 1348 newtext = linkify_others(newtext, link_)
1349 1349 except Exception:
1350 1350 log.error(traceback.format_exc())
1351 1351 pass
1352 1352
1353 1353 return literal(newtext)
1354 1354
1355 1355
1356 1356 def rst(source):
1357 1357 return literal('<div class="rst-block">%s</div>' %
1358 1358 MarkupRenderer.rst(source))
1359 1359
1360 1360
1361 1361 def rst_w_mentions(source):
1362 1362 """
1363 1363 Wrapped rst renderer with @mention highlighting
1364 1364
1365 1365 :param source:
1366 1366 """
1367 1367 return literal('<div class="rst-block">%s</div>' %
1368 1368 MarkupRenderer.rst_with_mentions(source))
1369 1369
1370 1370
1371 1371 def changeset_status(repo, revision):
1372 1372 return ChangesetStatusModel().get_status(repo, revision)
1373 1373
1374 1374
1375 1375 def changeset_status_lbl(changeset_status):
1376 1376 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1377 1377
1378 1378
1379 1379 def get_permission_name(key):
1380 1380 return dict(Permission.PERMS).get(key)
1381 1381
1382 1382
1383 1383 def journal_filter_help():
1384 1384 return _(textwrap.dedent('''
1385 1385 Example filter terms:
1386 1386 repository:vcs
1387 1387 username:marcin
1388 1388 action:*push*
1389 1389 ip:127.0.0.1
1390 1390 date:20120101
1391 1391 date:[20120101100000 TO 20120102]
1392 1392
1393 1393 Generate wildcards using '*' character:
1394 1394 "repositroy:vcs*" - search everything starting with 'vcs'
1395 1395 "repository:*vcs*" - search for repository containing 'vcs'
1396 1396
1397 1397 Optional AND / OR operators in queries
1398 1398 "repository:vcs OR repository:test"
1399 1399 "username:test AND repository:test*"
1400 1400 '''))
1401 1401
1402 1402
1403 1403 def not_mapped_error(repo_name):
1404 1404 flash(_('%s repository is not mapped to db perhaps'
1405 1405 ' it was created or renamed from the filesystem'
1406 1406 ' please run the application again'
1407 1407 ' in order to rescan repositories') % repo_name, category='error')
1408 1408
1409 1409
1410 1410 def ip_range(ip_addr):
1411 1411 from rhodecode.model.db import UserIpMap
1412 1412 s, e = UserIpMap._get_ip_range(ip_addr)
1413 1413 return '%s - %s' % (s, e)
General Comments 0
You need to be logged in to leave comments. Login now