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