##// END OF EJS Templates
yet another fix for commit linkifications
marcink -
r2966:566f0166 beta
parent child Browse files
Show More
@@ -1,1107 +1,1103 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
15 15 from datetime import datetime
16 16 from pygments.formatters.html import HtmlFormatter
17 17 from pygments import highlight as code_highlight
18 18 from pylons import url, request, config
19 19 from pylons.i18n.translation import _, ungettext
20 20 from hashlib import md5
21 21
22 22 from webhelpers.html import literal, HTML, escape
23 23 from webhelpers.html.tools import *
24 24 from webhelpers.html.builder import make_tag
25 25 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
26 26 end_form, file, form, hidden, image, javascript_link, link_to, \
27 27 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
28 28 submit, text, password, textarea, title, ul, xml_declaration, radio
29 29 from webhelpers.html.tools import auto_link, button_to, highlight, \
30 30 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
31 31 from webhelpers.number import format_byte_size, format_bit_size
32 32 from webhelpers.pylonslib import Flash as _Flash
33 33 from webhelpers.pylonslib.secure_form import secure_form
34 34 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
35 35 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
36 36 replace_whitespace, urlify, truncate, wrap_paragraphs
37 37 from webhelpers.date import time_ago_in_words
38 38 from webhelpers.paginate import Page
39 39 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
40 40 convert_boolean_attrs, NotGiven, _make_safe_id_component
41 41
42 42 from rhodecode.lib.annotate import annotate_highlight
43 43 from rhodecode.lib.utils import repo_name_slug
44 44 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
45 45 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict
46 46 from rhodecode.lib.markup_renderer import MarkupRenderer
47 47 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
48 48 from rhodecode.lib.vcs.backends.base import BaseChangeset
49 49 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
50 50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 51 from rhodecode.model.db import URL_SEP, Permission
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 html_escape_table = {
57 57 "&": "&",
58 58 '"': """,
59 59 "'": "'",
60 60 ">": ">",
61 61 "<": "&lt;",
62 62 }
63 63
64 64
65 65 def html_escape(text):
66 66 """Produce entities within text."""
67 67 return "".join(html_escape_table.get(c, c) for c in text)
68 68
69 69
70 70 def shorter(text, size=20):
71 71 postfix = '...'
72 72 if len(text) > size:
73 73 return text[:size - len(postfix)] + postfix
74 74 return text
75 75
76 76
77 77 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
78 78 """
79 79 Reset button
80 80 """
81 81 _set_input_attrs(attrs, type, name, value)
82 82 _set_id_attr(attrs, id, name)
83 83 convert_boolean_attrs(attrs, ["disabled"])
84 84 return HTML.input(**attrs)
85 85
86 86 reset = _reset
87 87 safeid = _make_safe_id_component
88 88
89 89
90 90 def FID(raw_id, path):
91 91 """
92 92 Creates a uniqe ID for filenode based on it's hash of path and revision
93 93 it's safe to use in urls
94 94
95 95 :param raw_id:
96 96 :param path:
97 97 """
98 98
99 99 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
100 100
101 101
102 102 def get_token():
103 103 """Return the current authentication token, creating one if one doesn't
104 104 already exist.
105 105 """
106 106 token_key = "_authentication_token"
107 107 from pylons import session
108 108 if not token_key in session:
109 109 try:
110 110 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
111 111 except AttributeError: # Python < 2.4
112 112 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
113 113 session[token_key] = token
114 114 if hasattr(session, 'save'):
115 115 session.save()
116 116 return session[token_key]
117 117
118 118
119 119 class _GetError(object):
120 120 """Get error from form_errors, and represent it as span wrapped error
121 121 message
122 122
123 123 :param field_name: field to fetch errors for
124 124 :param form_errors: form errors dict
125 125 """
126 126
127 127 def __call__(self, field_name, form_errors):
128 128 tmpl = """<span class="error_msg">%s</span>"""
129 129 if form_errors and field_name in form_errors:
130 130 return literal(tmpl % form_errors.get(field_name))
131 131
132 132 get_error = _GetError()
133 133
134 134
135 135 class _ToolTip(object):
136 136
137 137 def __call__(self, tooltip_title, trim_at=50):
138 138 """
139 139 Special function just to wrap our text into nice formatted
140 140 autowrapped text
141 141
142 142 :param tooltip_title:
143 143 """
144 144 tooltip_title = escape(tooltip_title)
145 145 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
146 146 return tooltip_title
147 147 tooltip = _ToolTip()
148 148
149 149
150 150 class _FilesBreadCrumbs(object):
151 151
152 152 def __call__(self, repo_name, rev, paths):
153 153 if isinstance(paths, str):
154 154 paths = safe_unicode(paths)
155 155 url_l = [link_to(repo_name, url('files_home',
156 156 repo_name=repo_name,
157 157 revision=rev, f_path=''),
158 158 class_='ypjax-link')]
159 159 paths_l = paths.split('/')
160 160 for cnt, p in enumerate(paths_l):
161 161 if p != '':
162 162 url_l.append(link_to(p,
163 163 url('files_home',
164 164 repo_name=repo_name,
165 165 revision=rev,
166 166 f_path='/'.join(paths_l[:cnt + 1])
167 167 ),
168 168 class_='ypjax-link'
169 169 )
170 170 )
171 171
172 172 return literal('/'.join(url_l))
173 173
174 174 files_breadcrumbs = _FilesBreadCrumbs()
175 175
176 176
177 177 class CodeHtmlFormatter(HtmlFormatter):
178 178 """
179 179 My code Html Formatter for source codes
180 180 """
181 181
182 182 def wrap(self, source, outfile):
183 183 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
184 184
185 185 def _wrap_code(self, source):
186 186 for cnt, it in enumerate(source):
187 187 i, t = it
188 188 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
189 189 yield i, t
190 190
191 191 def _wrap_tablelinenos(self, inner):
192 192 dummyoutfile = StringIO.StringIO()
193 193 lncount = 0
194 194 for t, line in inner:
195 195 if t:
196 196 lncount += 1
197 197 dummyoutfile.write(line)
198 198
199 199 fl = self.linenostart
200 200 mw = len(str(lncount + fl - 1))
201 201 sp = self.linenospecial
202 202 st = self.linenostep
203 203 la = self.lineanchors
204 204 aln = self.anchorlinenos
205 205 nocls = self.noclasses
206 206 if sp:
207 207 lines = []
208 208
209 209 for i in range(fl, fl + lncount):
210 210 if i % st == 0:
211 211 if i % sp == 0:
212 212 if aln:
213 213 lines.append('<a href="#%s%d" class="special">%*d</a>' %
214 214 (la, i, mw, i))
215 215 else:
216 216 lines.append('<span class="special">%*d</span>' % (mw, i))
217 217 else:
218 218 if aln:
219 219 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
220 220 else:
221 221 lines.append('%*d' % (mw, i))
222 222 else:
223 223 lines.append('')
224 224 ls = '\n'.join(lines)
225 225 else:
226 226 lines = []
227 227 for i in range(fl, fl + lncount):
228 228 if i % st == 0:
229 229 if aln:
230 230 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
231 231 else:
232 232 lines.append('%*d' % (mw, i))
233 233 else:
234 234 lines.append('')
235 235 ls = '\n'.join(lines)
236 236
237 237 # in case you wonder about the seemingly redundant <div> here: since the
238 238 # content in the other cell also is wrapped in a div, some browsers in
239 239 # some configurations seem to mess up the formatting...
240 240 if nocls:
241 241 yield 0, ('<table class="%stable">' % self.cssclass +
242 242 '<tr><td><div class="linenodiv" '
243 243 'style="background-color: #f0f0f0; padding-right: 10px">'
244 244 '<pre style="line-height: 125%">' +
245 245 ls + '</pre></div></td><td id="hlcode" class="code">')
246 246 else:
247 247 yield 0, ('<table class="%stable">' % self.cssclass +
248 248 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
249 249 ls + '</pre></div></td><td id="hlcode" class="code">')
250 250 yield 0, dummyoutfile.getvalue()
251 251 yield 0, '</td></tr></table>'
252 252
253 253
254 254 def pygmentize(filenode, **kwargs):
255 255 """pygmentize function using pygments
256 256
257 257 :param filenode:
258 258 """
259 259
260 260 return literal(code_highlight(filenode.content,
261 261 filenode.lexer, CodeHtmlFormatter(**kwargs)))
262 262
263 263
264 264 def pygmentize_annotation(repo_name, filenode, **kwargs):
265 265 """
266 266 pygmentize function for annotation
267 267
268 268 :param filenode:
269 269 """
270 270
271 271 color_dict = {}
272 272
273 273 def gen_color(n=10000):
274 274 """generator for getting n of evenly distributed colors using
275 275 hsv color and golden ratio. It always return same order of colors
276 276
277 277 :returns: RGB tuple
278 278 """
279 279
280 280 def hsv_to_rgb(h, s, v):
281 281 if s == 0.0:
282 282 return v, v, v
283 283 i = int(h * 6.0) # XXX assume int() truncates!
284 284 f = (h * 6.0) - i
285 285 p = v * (1.0 - s)
286 286 q = v * (1.0 - s * f)
287 287 t = v * (1.0 - s * (1.0 - f))
288 288 i = i % 6
289 289 if i == 0:
290 290 return v, t, p
291 291 if i == 1:
292 292 return q, v, p
293 293 if i == 2:
294 294 return p, v, t
295 295 if i == 3:
296 296 return p, q, v
297 297 if i == 4:
298 298 return t, p, v
299 299 if i == 5:
300 300 return v, p, q
301 301
302 302 golden_ratio = 0.618033988749895
303 303 h = 0.22717784590367374
304 304
305 305 for _ in xrange(n):
306 306 h += golden_ratio
307 307 h %= 1
308 308 HSV_tuple = [h, 0.95, 0.95]
309 309 RGB_tuple = hsv_to_rgb(*HSV_tuple)
310 310 yield map(lambda x: str(int(x * 256)), RGB_tuple)
311 311
312 312 cgenerator = gen_color()
313 313
314 314 def get_color_string(cs):
315 315 if cs in color_dict:
316 316 col = color_dict[cs]
317 317 else:
318 318 col = color_dict[cs] = cgenerator.next()
319 319 return "color: rgb(%s)! important;" % (', '.join(col))
320 320
321 321 def url_func(repo_name):
322 322
323 323 def _url_func(changeset):
324 324 author = changeset.author
325 325 date = changeset.date
326 326 message = tooltip(changeset.message)
327 327
328 328 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
329 329 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
330 330 "</b> %s<br/></div>")
331 331
332 332 tooltip_html = tooltip_html % (author, date, message)
333 333 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
334 334 short_id(changeset.raw_id))
335 335 uri = link_to(
336 336 lnk_format,
337 337 url('changeset_home', repo_name=repo_name,
338 338 revision=changeset.raw_id),
339 339 style=get_color_string(changeset.raw_id),
340 340 class_='tooltip',
341 341 title=tooltip_html
342 342 )
343 343
344 344 uri += '\n'
345 345 return uri
346 346 return _url_func
347 347
348 348 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
349 349
350 350
351 351 def is_following_repo(repo_name, user_id):
352 352 from rhodecode.model.scm import ScmModel
353 353 return ScmModel().is_following_repo(repo_name, user_id)
354 354
355 355 flash = _Flash()
356 356
357 357 #==============================================================================
358 358 # SCM FILTERS available via h.
359 359 #==============================================================================
360 360 from rhodecode.lib.vcs.utils import author_name, author_email
361 361 from rhodecode.lib.utils2 import credentials_filter, age as _age
362 362 from rhodecode.model.db import User, ChangesetStatus
363 363
364 364 age = lambda x: _age(x)
365 365 capitalize = lambda x: x.capitalize()
366 366 email = author_email
367 367 short_id = lambda x: x[:12]
368 368 hide_credentials = lambda x: ''.join(credentials_filter(x))
369 369
370 370
371 371 def fmt_date(date):
372 372 if date:
373 373 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
374 374 return date.strftime(_fmt).decode('utf8')
375 375
376 376 return ""
377 377
378 378
379 379 def is_git(repository):
380 380 if hasattr(repository, 'alias'):
381 381 _type = repository.alias
382 382 elif hasattr(repository, 'repo_type'):
383 383 _type = repository.repo_type
384 384 else:
385 385 _type = repository
386 386 return _type == 'git'
387 387
388 388
389 389 def is_hg(repository):
390 390 if hasattr(repository, 'alias'):
391 391 _type = repository.alias
392 392 elif hasattr(repository, 'repo_type'):
393 393 _type = repository.repo_type
394 394 else:
395 395 _type = repository
396 396 return _type == 'hg'
397 397
398 398
399 399 def email_or_none(author):
400 400 # extract email from the commit string
401 401 _email = email(author)
402 402 if _email != '':
403 403 # check it against RhodeCode database, and use the MAIN email for this
404 404 # user
405 405 user = User.get_by_email(_email, case_insensitive=True, cache=True)
406 406 if user is not None:
407 407 return user.email
408 408 return _email
409 409
410 410 # See if it contains a username we can get an email from
411 411 user = User.get_by_username(author_name(author), case_insensitive=True,
412 412 cache=True)
413 413 if user is not None:
414 414 return user.email
415 415
416 416 # No valid email, not a valid user in the system, none!
417 417 return None
418 418
419 419
420 420 def person(author, show_attr="username_and_name"):
421 421 # attr to return from fetched user
422 422 person_getter = lambda usr: getattr(usr, show_attr)
423 423
424 424 # Valid email in the attribute passed, see if they're in the system
425 425 _email = email(author)
426 426 if _email != '':
427 427 user = User.get_by_email(_email, case_insensitive=True, cache=True)
428 428 if user is not None:
429 429 return person_getter(user)
430 430 return _email
431 431
432 432 # Maybe it's a username?
433 433 _author = author_name(author)
434 434 user = User.get_by_username(_author, case_insensitive=True,
435 435 cache=True)
436 436 if user is not None:
437 437 return person_getter(user)
438 438
439 439 # Still nothing? Just pass back the author name then
440 440 return _author
441 441
442 442
443 443 def person_by_id(id_, show_attr="username_and_name"):
444 444 # attr to return from fetched user
445 445 person_getter = lambda usr: getattr(usr, show_attr)
446 446
447 447 #maybe it's an ID ?
448 448 if str(id_).isdigit() or isinstance(id_, int):
449 449 id_ = int(id_)
450 450 user = User.get(id_)
451 451 if user is not None:
452 452 return person_getter(user)
453 453 return id_
454 454
455 455
456 456 def desc_stylize(value):
457 457 """
458 458 converts tags from value into html equivalent
459 459
460 460 :param value:
461 461 """
462 462 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
463 463 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
464 464 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
465 465 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
466 466 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z\-\/]*)\]',
467 467 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
468 468 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
469 469 '<div class="metatag" tag="lang">\\2</div>', value)
470 470 value = re.sub(r'\[([a-z]+)\]',
471 471 '<div class="metatag" tag="\\1">\\1</div>', value)
472 472
473 473 return value
474 474
475 475
476 476 def bool2icon(value):
477 477 """Returns True/False values represented as small html image of true/false
478 478 icons
479 479
480 480 :param value: bool value
481 481 """
482 482
483 483 if value is True:
484 484 return HTML.tag('img', src=url("/images/icons/accept.png"),
485 485 alt=_('True'))
486 486
487 487 if value is False:
488 488 return HTML.tag('img', src=url("/images/icons/cancel.png"),
489 489 alt=_('False'))
490 490
491 491 return value
492 492
493 493
494 494 def action_parser(user_log, feed=False, parse_cs=False):
495 495 """
496 496 This helper will action_map the specified string action into translated
497 497 fancy names with icons and links
498 498
499 499 :param user_log: user log instance
500 500 :param feed: use output for feeds (no html and fancy icons)
501 501 :param parse_cs: parse Changesets into VCS instances
502 502 """
503 503
504 504 action = user_log.action
505 505 action_params = ' '
506 506
507 507 x = action.split(':')
508 508
509 509 if len(x) > 1:
510 510 action, action_params = x
511 511
512 512 def get_cs_links():
513 513 revs_limit = 3 # display this amount always
514 514 revs_top_limit = 50 # show upto this amount of changesets hidden
515 515 revs_ids = action_params.split(',')
516 516 deleted = user_log.repository is None
517 517 if deleted:
518 518 return ','.join(revs_ids)
519 519
520 520 repo_name = user_log.repository.repo_name
521 521
522 522 def lnk(rev, repo_name):
523 523 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
524 524 lbl = '%s' % (rev.short_id[:8])
525 525 _url = url('changeset_home', repo_name=repo_name,
526 526 revision=rev.raw_id)
527 527 title = tooltip(rev.message)
528 528 else:
529 529 ## changeset cannot be found/striped/removed etc.
530 530 lbl = ('%s' % rev)[:12]
531 531 _url = '#'
532 532 title = _('Changeset not found')
533 533 if parse_cs:
534 534 return link_to(lbl, _url, title=title, class_='tooltip')
535 535 return link_to(lbl, _url, raw_id=rev.raw_id, class_='journal-cs')
536 536
537 537 revs = []
538 538 if len(filter(lambda v: v != '', revs_ids)) > 0:
539 539 repo = None
540 540 for rev in revs_ids[:revs_top_limit]:
541 541 # we want parsed changesets, or new log store format is bad
542 542 if parse_cs:
543 543 try:
544 544 if repo is None:
545 545 repo = user_log.repository.scm_instance
546 546 _rev = repo.get_changeset(rev)
547 547 revs.append(_rev)
548 548 except ChangesetDoesNotExistError:
549 549 log.error('cannot find revision %s in this repo' % rev)
550 550 revs.append(rev)
551 551 continue
552 552 else:
553 553 _rev = AttributeDict({
554 554 'short_id': rev[:12],
555 555 'raw_id': rev,
556 556 'message': '',
557 557 })
558 558 revs.append(_rev)
559 559 cs_links = []
560 560 cs_links.append(" " + ', '.join(
561 561 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
562 562 )
563 563 )
564 564
565 565 compare_view = (
566 566 ' <div class="compare_view tooltip" title="%s">'
567 567 '<a href="%s">%s</a> </div>' % (
568 568 _('Show all combined changesets %s->%s') % (
569 569 revs_ids[0][:12], revs_ids[-1][:12]
570 570 ),
571 571 url('changeset_home', repo_name=repo_name,
572 572 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
573 573 ),
574 574 _('compare view')
575 575 )
576 576 )
577 577
578 578 # if we have exactly one more than normally displayed
579 579 # just display it, takes less space than displaying
580 580 # "and 1 more revisions"
581 581 if len(revs_ids) == revs_limit + 1:
582 582 rev = revs[revs_limit]
583 583 cs_links.append(", " + lnk(rev, repo_name))
584 584
585 585 # hidden-by-default ones
586 586 if len(revs_ids) > revs_limit + 1:
587 587 uniq_id = revs_ids[0]
588 588 html_tmpl = (
589 589 '<span> %s <a class="show_more" id="_%s" '
590 590 'href="#more">%s</a> %s</span>'
591 591 )
592 592 if not feed:
593 593 cs_links.append(html_tmpl % (
594 594 _('and'),
595 595 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
596 596 _('revisions')
597 597 )
598 598 )
599 599
600 600 if not feed:
601 601 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
602 602 else:
603 603 html_tmpl = '<span id="%s"> %s </span>'
604 604
605 605 morelinks = ', '.join(
606 606 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
607 607 )
608 608
609 609 if len(revs_ids) > revs_top_limit:
610 610 morelinks += ', ...'
611 611
612 612 cs_links.append(html_tmpl % (uniq_id, morelinks))
613 613 if len(revs) > 1:
614 614 cs_links.append(compare_view)
615 615 return ''.join(cs_links)
616 616
617 617 def get_fork_name():
618 618 repo_name = action_params
619 619 _url = url('summary_home', repo_name=repo_name)
620 620 return _('fork name %s') % link_to(action_params, _url)
621 621
622 622 def get_user_name():
623 623 user_name = action_params
624 624 return user_name
625 625
626 626 def get_users_group():
627 627 group_name = action_params
628 628 return group_name
629 629
630 630 def get_pull_request():
631 631 pull_request_id = action_params
632 632 repo_name = user_log.repository.repo_name
633 633 return link_to(_('Pull request #%s') % pull_request_id,
634 634 url('pullrequest_show', repo_name=repo_name,
635 635 pull_request_id=pull_request_id))
636 636
637 637 # action : translated str, callback(extractor), icon
638 638 action_map = {
639 639 'user_deleted_repo': (_('[deleted] repository'),
640 640 None, 'database_delete.png'),
641 641 'user_created_repo': (_('[created] repository'),
642 642 None, 'database_add.png'),
643 643 'user_created_fork': (_('[created] repository as fork'),
644 644 None, 'arrow_divide.png'),
645 645 'user_forked_repo': (_('[forked] repository'),
646 646 get_fork_name, 'arrow_divide.png'),
647 647 'user_updated_repo': (_('[updated] repository'),
648 648 None, 'database_edit.png'),
649 649 'admin_deleted_repo': (_('[delete] repository'),
650 650 None, 'database_delete.png'),
651 651 'admin_created_repo': (_('[created] repository'),
652 652 None, 'database_add.png'),
653 653 'admin_forked_repo': (_('[forked] repository'),
654 654 None, 'arrow_divide.png'),
655 655 'admin_updated_repo': (_('[updated] repository'),
656 656 None, 'database_edit.png'),
657 657 'admin_created_user': (_('[created] user'),
658 658 get_user_name, 'user_add.png'),
659 659 'admin_updated_user': (_('[updated] user'),
660 660 get_user_name, 'user_edit.png'),
661 661 'admin_created_users_group': (_('[created] users group'),
662 662 get_users_group, 'group_add.png'),
663 663 'admin_updated_users_group': (_('[updated] users group'),
664 664 get_users_group, 'group_edit.png'),
665 665 'user_commented_revision': (_('[commented] on revision in repository'),
666 666 get_cs_links, 'comment_add.png'),
667 667 'user_commented_pull_request': (_('[commented] on pull request for'),
668 668 get_pull_request, 'comment_add.png'),
669 669 'user_closed_pull_request': (_('[closed] pull request for'),
670 670 get_pull_request, 'tick.png'),
671 671 'push': (_('[pushed] into'),
672 672 get_cs_links, 'script_add.png'),
673 673 'push_local': (_('[committed via RhodeCode] into repository'),
674 674 get_cs_links, 'script_edit.png'),
675 675 'push_remote': (_('[pulled from remote] into repository'),
676 676 get_cs_links, 'connect.png'),
677 677 'pull': (_('[pulled] from'),
678 678 None, 'down_16.png'),
679 679 'started_following_repo': (_('[started following] repository'),
680 680 None, 'heart_add.png'),
681 681 'stopped_following_repo': (_('[stopped following] repository'),
682 682 None, 'heart_delete.png'),
683 683 }
684 684
685 685 action_str = action_map.get(action, action)
686 686 if feed:
687 687 action = action_str[0].replace('[', '').replace(']', '')
688 688 else:
689 689 action = action_str[0]\
690 690 .replace('[', '<span class="journal_highlight">')\
691 691 .replace(']', '</span>')
692 692
693 693 action_params_func = lambda: ""
694 694
695 695 if callable(action_str[1]):
696 696 action_params_func = action_str[1]
697 697
698 698 def action_parser_icon():
699 699 action = user_log.action
700 700 action_params = None
701 701 x = action.split(':')
702 702
703 703 if len(x) > 1:
704 704 action, action_params = x
705 705
706 706 tmpl = """<img src="%s%s" alt="%s"/>"""
707 707 ico = action_map.get(action, ['', '', ''])[2]
708 708 return literal(tmpl % ((url('/images/icons/')), ico, action))
709 709
710 710 # returned callbacks we need to call to get
711 711 return [lambda: literal(action), action_params_func, action_parser_icon]
712 712
713 713
714 714
715 715 #==============================================================================
716 716 # PERMS
717 717 #==============================================================================
718 718 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
719 719 HasRepoPermissionAny, HasRepoPermissionAll
720 720
721 721
722 722 #==============================================================================
723 723 # GRAVATAR URL
724 724 #==============================================================================
725 725
726 726 def gravatar_url(email_address, size=30):
727 727 from pylons import url ## doh, we need to re-import url to mock it later
728 728 if(str2bool(config['app_conf'].get('use_gravatar')) and
729 729 config['app_conf'].get('alternative_gravatar_url')):
730 730 tmpl = config['app_conf'].get('alternative_gravatar_url', '')
731 731 parsed_url = urlparse.urlparse(url.current(qualified=True))
732 732 tmpl = tmpl.replace('{email}', email_address)\
733 733 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
734 734 .replace('{netloc}', parsed_url.netloc)\
735 735 .replace('{scheme}', parsed_url.scheme)\
736 736 .replace('{size}', str(size))
737 737 return tmpl
738 738
739 739 if (not str2bool(config['app_conf'].get('use_gravatar')) or
740 740 not email_address or email_address == 'anonymous@rhodecode.org'):
741 741 f = lambda a, l: min(l, key=lambda x: abs(x - a))
742 742 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
743 743
744 744 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
745 745 default = 'identicon'
746 746 baseurl_nossl = "http://www.gravatar.com/avatar/"
747 747 baseurl_ssl = "https://secure.gravatar.com/avatar/"
748 748 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
749 749
750 750 if isinstance(email_address, unicode):
751 751 #hashlib crashes on unicode items
752 752 email_address = safe_str(email_address)
753 753 # construct the url
754 754 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
755 755 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
756 756
757 757 return gravatar_url
758 758
759 759
760 760 #==============================================================================
761 761 # REPO PAGER, PAGER FOR REPOSITORY
762 762 #==============================================================================
763 763 class RepoPage(Page):
764 764
765 765 def __init__(self, collection, page=1, items_per_page=20,
766 766 item_count=None, url=None, **kwargs):
767 767
768 768 """Create a "RepoPage" instance. special pager for paging
769 769 repository
770 770 """
771 771 self._url_generator = url
772 772
773 773 # Safe the kwargs class-wide so they can be used in the pager() method
774 774 self.kwargs = kwargs
775 775
776 776 # Save a reference to the collection
777 777 self.original_collection = collection
778 778
779 779 self.collection = collection
780 780
781 781 # The self.page is the number of the current page.
782 782 # The first page has the number 1!
783 783 try:
784 784 self.page = int(page) # make it int() if we get it as a string
785 785 except (ValueError, TypeError):
786 786 self.page = 1
787 787
788 788 self.items_per_page = items_per_page
789 789
790 790 # Unless the user tells us how many items the collections has
791 791 # we calculate that ourselves.
792 792 if item_count is not None:
793 793 self.item_count = item_count
794 794 else:
795 795 self.item_count = len(self.collection)
796 796
797 797 # Compute the number of the first and last available page
798 798 if self.item_count > 0:
799 799 self.first_page = 1
800 800 self.page_count = int(math.ceil(float(self.item_count) /
801 801 self.items_per_page))
802 802 self.last_page = self.first_page + self.page_count - 1
803 803
804 804 # Make sure that the requested page number is the range of
805 805 # valid pages
806 806 if self.page > self.last_page:
807 807 self.page = self.last_page
808 808 elif self.page < self.first_page:
809 809 self.page = self.first_page
810 810
811 811 # Note: the number of items on this page can be less than
812 812 # items_per_page if the last page is not full
813 813 self.first_item = max(0, (self.item_count) - (self.page *
814 814 items_per_page))
815 815 self.last_item = ((self.item_count - 1) - items_per_page *
816 816 (self.page - 1))
817 817
818 818 self.items = list(self.collection[self.first_item:self.last_item + 1])
819 819
820 820 # Links to previous and next page
821 821 if self.page > self.first_page:
822 822 self.previous_page = self.page - 1
823 823 else:
824 824 self.previous_page = None
825 825
826 826 if self.page < self.last_page:
827 827 self.next_page = self.page + 1
828 828 else:
829 829 self.next_page = None
830 830
831 831 # No items available
832 832 else:
833 833 self.first_page = None
834 834 self.page_count = 0
835 835 self.last_page = None
836 836 self.first_item = None
837 837 self.last_item = None
838 838 self.previous_page = None
839 839 self.next_page = None
840 840 self.items = []
841 841
842 842 # This is a subclass of the 'list' type. Initialise the list now.
843 843 list.__init__(self, reversed(self.items))
844 844
845 845
846 846 def changed_tooltip(nodes):
847 847 """
848 848 Generates a html string for changed nodes in changeset page.
849 849 It limits the output to 30 entries
850 850
851 851 :param nodes: LazyNodesGenerator
852 852 """
853 853 if nodes:
854 854 pref = ': <br/> '
855 855 suf = ''
856 856 if len(nodes) > 30:
857 857 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
858 858 return literal(pref + '<br/> '.join([safe_unicode(x.path)
859 859 for x in nodes[:30]]) + suf)
860 860 else:
861 861 return ': ' + _('No Files')
862 862
863 863
864 864 def repo_link(groups_and_repos):
865 865 """
866 866 Makes a breadcrumbs link to repo within a group
867 867 joins &raquo; on each group to create a fancy link
868 868
869 869 ex::
870 870 group >> subgroup >> repo
871 871
872 872 :param groups_and_repos:
873 873 """
874 874 groups, repo_name = groups_and_repos
875 875
876 876 if not groups:
877 877 return repo_name
878 878 else:
879 879 def make_link(group):
880 880 return link_to(group.name, url('repos_group_home',
881 881 group_name=group.group_name))
882 882 return literal(' &raquo; '.join(map(make_link, groups)) + \
883 883 " &raquo; " + repo_name)
884 884
885 885
886 886 def fancy_file_stats(stats):
887 887 """
888 888 Displays a fancy two colored bar for number of added/deleted
889 889 lines of code on file
890 890
891 891 :param stats: two element list of added/deleted lines of code
892 892 """
893 893
894 894 a, d, t = stats[0], stats[1], stats[0] + stats[1]
895 895 width = 100
896 896 unit = float(width) / (t or 1)
897 897
898 898 # needs > 9% of width to be visible or 0 to be hidden
899 899 a_p = max(9, unit * a) if a > 0 else 0
900 900 d_p = max(9, unit * d) if d > 0 else 0
901 901 p_sum = a_p + d_p
902 902
903 903 if p_sum > width:
904 904 #adjust the percentage to be == 100% since we adjusted to 9
905 905 if a_p > d_p:
906 906 a_p = a_p - (p_sum - width)
907 907 else:
908 908 d_p = d_p - (p_sum - width)
909 909
910 910 a_v = a if a > 0 else ''
911 911 d_v = d if d > 0 else ''
912 912
913 913 def cgen(l_type):
914 914 mapping = {'tr': 'top-right-rounded-corner-mid',
915 915 'tl': 'top-left-rounded-corner-mid',
916 916 'br': 'bottom-right-rounded-corner-mid',
917 917 'bl': 'bottom-left-rounded-corner-mid'}
918 918 map_getter = lambda x: mapping[x]
919 919
920 920 if l_type == 'a' and d_v:
921 921 #case when added and deleted are present
922 922 return ' '.join(map(map_getter, ['tl', 'bl']))
923 923
924 924 if l_type == 'a' and not d_v:
925 925 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
926 926
927 927 if l_type == 'd' and a_v:
928 928 return ' '.join(map(map_getter, ['tr', 'br']))
929 929
930 930 if l_type == 'd' and not a_v:
931 931 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
932 932
933 933 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
934 934 cgen('a'), a_p, a_v
935 935 )
936 936 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
937 937 cgen('d'), d_p, d_v
938 938 )
939 939 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
940 940
941 941
942 942 def urlify_text(text_):
943 import re
944 943
945 944 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
946 945 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
947 946
948 947 def url_func(match_obj):
949 948 url_full = match_obj.groups()[0]
950 949 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
951 950
952 951 return literal(url_pat.sub(url_func, text_))
953 952
954 953
955 954 def urlify_changesets(text_, repository):
956 955 """
957 956 Extract revision ids from changeset and make link from them
958 957
959 958 :param text_:
960 959 :param repository:
961 960 """
962 import re
961
963 962 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
964 963
965 964 def url_func(match_obj):
966 965 rev = match_obj.groups()[0]
967 966 pref = ''
968 967 if match_obj.group().startswith(' '):
969 968 pref = ' '
970 969 tmpl = (
971 970 '%(pref)s<a class="%(cls)s" href="%(url)s">'
972 971 '%(rev)s'
973 972 '</a>'
974 973 )
975 974 return tmpl % {
976 975 'pref': pref,
977 976 'cls': 'revision-link',
978 977 'url': url('changeset_home', repo_name=repository, revision=rev),
979 978 'rev': rev,
980 979 }
981 980
982 981 newtext = URL_PAT.sub(url_func, text_)
983 982
984 983 return newtext
985 984
986 985
987 986 def urlify_commit(text_, repository=None, link_=None):
988 987 """
989 988 Parses given text message and makes proper links.
990 989 issues are linked to given issue-server, and rest is a changeset link
991 990 if link_ is given, in other case it's a plain text
992 991
993 992 :param text_:
994 993 :param repository:
995 994 :param link_: changeset link
996 995 """
997 import re
998 996 import traceback
999 997
1000 998 def escaper(string):
1001 999 return string.replace('<', '&lt;').replace('>', '&gt;')
1002 1000
1003 1001 def linkify_others(t, l):
1004 1002 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1005 1003 links = []
1006 1004 for e in urls.split(t):
1007 1005 if not urls.match(e):
1008 1006 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1009 1007 else:
1010 1008 links.append(e)
1011 1009
1012 1010 return ''.join(links)
1013 1011
1014 1012 # urlify changesets - extrac revisions and make link out of them
1015 1013 newtext = urlify_changesets(escaper(text_), repository)
1016 1014
1017 1015 try:
1018 1016 conf = config['app_conf']
1019 1017
1020 1018 # allow multiple issue servers to be used
1021 1019 valid_indices = [
1022 1020 x.group(1)
1023 1021 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1024 1022 if x and 'issue_server_link%s' % x.group(1) in conf
1025 1023 and 'issue_prefix%s' % x.group(1) in conf
1026 1024 ]
1027 1025
1028 1026 log.debug('found issue server suffixes `%s` during valuation of: %s'
1029 1027 % (','.join(valid_indices), newtext))
1030 1028
1031 1029 for pattern_index in valid_indices:
1032 1030 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1033 1031 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1034 1032 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1035 1033
1036 1034 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1037 1035 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1038 1036 ISSUE_PREFIX))
1039 1037
1040 1038 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1041 1039
1042 1040 def url_func(match_obj):
1043 1041 pref = ''
1044 1042 if match_obj.group().startswith(' '):
1045 1043 pref = ' '
1046 1044
1047 1045 issue_id = ''.join(match_obj.groups())
1048 1046 tmpl = (
1049 1047 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1050 1048 '%(issue-prefix)s%(id-repr)s'
1051 1049 '</a>'
1052 1050 )
1053 1051 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1054 1052 if repository:
1055 1053 url = url.replace('{repo}', repository)
1056 1054 repo_name = repository.split(URL_SEP)[-1]
1057 1055 url = url.replace('{repo_name}', repo_name)
1058 1056
1059 1057 return tmpl % {
1060 1058 'pref': pref,
1061 1059 'cls': 'issue-tracker-link',
1062 1060 'url': url,
1063 1061 'id-repr': issue_id,
1064 1062 'issue-prefix': ISSUE_PREFIX,
1065 1063 'serv': ISSUE_SERVER_LNK,
1066 1064 }
1067 1065 newtext = URL_PAT.sub(url_func, newtext)
1068 1066 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1069 1067
1070 1068 # if we actually did something above
1071 1069 if link_:
1072 1070 # wrap not links into final link => link_
1073 1071 newtext = linkify_others(newtext, link_)
1074
1075 return literal(newtext)
1076 1072 except:
1077 1073 log.error(traceback.format_exc())
1078 1074 pass
1079 1075
1080 return newtext
1076 return literal(newtext)
1081 1077
1082 1078
1083 1079 def rst(source):
1084 1080 return literal('<div class="rst-block">%s</div>' %
1085 1081 MarkupRenderer.rst(source))
1086 1082
1087 1083
1088 1084 def rst_w_mentions(source):
1089 1085 """
1090 1086 Wrapped rst renderer with @mention highlighting
1091 1087
1092 1088 :param source:
1093 1089 """
1094 1090 return literal('<div class="rst-block">%s</div>' %
1095 1091 MarkupRenderer.rst_with_mentions(source))
1096 1092
1097 1093
1098 1094 def changeset_status(repo, revision):
1099 1095 return ChangesetStatusModel().get_status(repo, revision)
1100 1096
1101 1097
1102 1098 def changeset_status_lbl(changeset_status):
1103 1099 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1104 1100
1105 1101
1106 1102 def get_permission_name(key):
1107 1103 return dict(Permission.PERMS).get(key)
General Comments 0
You need to be logged in to leave comments. Login now