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