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