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