##// END OF EJS Templates
fixed problem with anchor links that contains non ascii chars...
marcink -
r1832:b9708d66 beta
parent child Browse files
Show More
@@ -1,752 +1,755
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
12 12 from datetime import datetime
13 13 from pygments.formatters.html import HtmlFormatter
14 14 from pygments import highlight as code_highlight
15 15 from pylons import url, request, config
16 16 from pylons.i18n.translation import _, ungettext
17 from hashlib import md5
17 18
18 19 from webhelpers.html import literal, HTML, escape
19 20 from webhelpers.html.tools import *
20 21 from webhelpers.html.builder import make_tag
21 22 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
22 23 end_form, file, form, hidden, image, javascript_link, link_to, \
23 24 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
24 25 submit, text, password, textarea, title, ul, xml_declaration, radio
25 26 from webhelpers.html.tools import auto_link, button_to, highlight, \
26 27 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
27 28 from webhelpers.number import format_byte_size, format_bit_size
28 29 from webhelpers.pylonslib import Flash as _Flash
29 30 from webhelpers.pylonslib.secure_form import secure_form
30 31 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
31 32 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
32 33 replace_whitespace, urlify, truncate, wrap_paragraphs
33 34 from webhelpers.date import time_ago_in_words
34 35 from webhelpers.paginate import Page
35 36 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
36 37 convert_boolean_attrs, NotGiven, _make_safe_id_component
37 38
38 39 from rhodecode.lib.annotate import annotate_highlight
39 40 from rhodecode.lib.utils import repo_name_slug
40 41 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
41 42 from rhodecode.lib.markup_renderer import MarkupRenderer
42 43
43 44
44 45 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
45 46 """
46 47 Reset button
47 48 """
48 49 _set_input_attrs(attrs, type, name, value)
49 50 _set_id_attr(attrs, id, name)
50 51 convert_boolean_attrs(attrs, ["disabled"])
51 52 return HTML.input(**attrs)
52 53
53 54 reset = _reset
54 55 safeid = _make_safe_id_component
55 56
56 57
57 58 def FID(raw_id, path):
58 59 """
59 Creates a uniqe ID for filenode based on it's path and revision
60 Creates a uniqe ID for filenode based on it's hash of path and revision
61 it's safe to use in urls
60 62
61 63 :param raw_id:
62 64 :param path:
63 65 """
64 return 'C-%s-%s' % (short_id(raw_id), safeid(safe_unicode(path)))
66
67 return 'C-%s-%s' % (short_id(raw_id), md5(path).hexdigest()[:12])
65 68
66 69
67 70 def get_token():
68 71 """Return the current authentication token, creating one if one doesn't
69 72 already exist.
70 73 """
71 74 token_key = "_authentication_token"
72 75 from pylons import session
73 76 if not token_key in session:
74 77 try:
75 78 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
76 79 except AttributeError: # Python < 2.4
77 80 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
78 81 session[token_key] = token
79 82 if hasattr(session, 'save'):
80 83 session.save()
81 84 return session[token_key]
82 85
83 86 class _GetError(object):
84 87 """Get error from form_errors, and represent it as span wrapped error
85 88 message
86 89
87 90 :param field_name: field to fetch errors for
88 91 :param form_errors: form errors dict
89 92 """
90 93
91 94 def __call__(self, field_name, form_errors):
92 95 tmpl = """<span class="error_msg">%s</span>"""
93 96 if form_errors and form_errors.has_key(field_name):
94 97 return literal(tmpl % form_errors.get(field_name))
95 98
96 99 get_error = _GetError()
97 100
98 101 class _ToolTip(object):
99 102
100 103 def __call__(self, tooltip_title, trim_at=50):
101 104 """Special function just to wrap our text into nice formatted
102 105 autowrapped text
103 106
104 107 :param tooltip_title:
105 108 """
106 109 return escape(tooltip_title)
107 110 tooltip = _ToolTip()
108 111
109 112 class _FilesBreadCrumbs(object):
110 113
111 114 def __call__(self, repo_name, rev, paths):
112 115 if isinstance(paths, str):
113 116 paths = safe_unicode(paths)
114 117 url_l = [link_to(repo_name, url('files_home',
115 118 repo_name=repo_name,
116 119 revision=rev, f_path=''))]
117 120 paths_l = paths.split('/')
118 121 for cnt, p in enumerate(paths_l):
119 122 if p != '':
120 123 url_l.append(link_to(p,
121 124 url('files_home',
122 125 repo_name=repo_name,
123 126 revision=rev,
124 127 f_path='/'.join(paths_l[:cnt + 1])
125 128 )
126 129 )
127 130 )
128 131
129 132 return literal('/'.join(url_l))
130 133
131 134 files_breadcrumbs = _FilesBreadCrumbs()
132 135
133 136 class CodeHtmlFormatter(HtmlFormatter):
134 137 """My code Html Formatter for source codes
135 138 """
136 139
137 140 def wrap(self, source, outfile):
138 141 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
139 142
140 143 def _wrap_code(self, source):
141 144 for cnt, it in enumerate(source):
142 145 i, t = it
143 146 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
144 147 yield i, t
145 148
146 149 def _wrap_tablelinenos(self, inner):
147 150 dummyoutfile = StringIO.StringIO()
148 151 lncount = 0
149 152 for t, line in inner:
150 153 if t:
151 154 lncount += 1
152 155 dummyoutfile.write(line)
153 156
154 157 fl = self.linenostart
155 158 mw = len(str(lncount + fl - 1))
156 159 sp = self.linenospecial
157 160 st = self.linenostep
158 161 la = self.lineanchors
159 162 aln = self.anchorlinenos
160 163 nocls = self.noclasses
161 164 if sp:
162 165 lines = []
163 166
164 167 for i in range(fl, fl + lncount):
165 168 if i % st == 0:
166 169 if i % sp == 0:
167 170 if aln:
168 171 lines.append('<a href="#%s%d" class="special">%*d</a>' %
169 172 (la, i, mw, i))
170 173 else:
171 174 lines.append('<span class="special">%*d</span>' % (mw, i))
172 175 else:
173 176 if aln:
174 177 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
175 178 else:
176 179 lines.append('%*d' % (mw, i))
177 180 else:
178 181 lines.append('')
179 182 ls = '\n'.join(lines)
180 183 else:
181 184 lines = []
182 185 for i in range(fl, fl + lncount):
183 186 if i % st == 0:
184 187 if aln:
185 188 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
186 189 else:
187 190 lines.append('%*d' % (mw, i))
188 191 else:
189 192 lines.append('')
190 193 ls = '\n'.join(lines)
191 194
192 195 # in case you wonder about the seemingly redundant <div> here: since the
193 196 # content in the other cell also is wrapped in a div, some browsers in
194 197 # some configurations seem to mess up the formatting...
195 198 if nocls:
196 199 yield 0, ('<table class="%stable">' % self.cssclass +
197 200 '<tr><td><div class="linenodiv" '
198 201 'style="background-color: #f0f0f0; padding-right: 10px">'
199 202 '<pre style="line-height: 125%">' +
200 203 ls + '</pre></div></td><td id="hlcode" class="code">')
201 204 else:
202 205 yield 0, ('<table class="%stable">' % self.cssclass +
203 206 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
204 207 ls + '</pre></div></td><td id="hlcode" class="code">')
205 208 yield 0, dummyoutfile.getvalue()
206 209 yield 0, '</td></tr></table>'
207 210
208 211
209 212 def pygmentize(filenode, **kwargs):
210 213 """pygmentize function using pygments
211 214
212 215 :param filenode:
213 216 """
214 217
215 218 return literal(code_highlight(filenode.content,
216 219 filenode.lexer, CodeHtmlFormatter(**kwargs)))
217 220
218 221
219 222 def pygmentize_annotation(repo_name, filenode, **kwargs):
220 223 """
221 224 pygmentize function for annotation
222 225
223 226 :param filenode:
224 227 """
225 228
226 229 color_dict = {}
227 230
228 231 def gen_color(n=10000):
229 232 """generator for getting n of evenly distributed colors using
230 233 hsv color and golden ratio. It always return same order of colors
231 234
232 235 :returns: RGB tuple
233 236 """
234 237
235 238 def hsv_to_rgb(h, s, v):
236 239 if s == 0.0:
237 240 return v, v, v
238 241 i = int(h * 6.0) # XXX assume int() truncates!
239 242 f = (h * 6.0) - i
240 243 p = v * (1.0 - s)
241 244 q = v * (1.0 - s * f)
242 245 t = v * (1.0 - s * (1.0 - f))
243 246 i = i % 6
244 247 if i == 0:
245 248 return v, t, p
246 249 if i == 1:
247 250 return q, v, p
248 251 if i == 2:
249 252 return p, v, t
250 253 if i == 3:
251 254 return p, q, v
252 255 if i == 4:
253 256 return t, p, v
254 257 if i == 5:
255 258 return v, p, q
256 259
257 260 golden_ratio = 0.618033988749895
258 261 h = 0.22717784590367374
259 262
260 263 for _ in xrange(n):
261 264 h += golden_ratio
262 265 h %= 1
263 266 HSV_tuple = [h, 0.95, 0.95]
264 267 RGB_tuple = hsv_to_rgb(*HSV_tuple)
265 268 yield map(lambda x: str(int(x * 256)), RGB_tuple)
266 269
267 270 cgenerator = gen_color()
268 271
269 272 def get_color_string(cs):
270 273 if cs in color_dict:
271 274 col = color_dict[cs]
272 275 else:
273 276 col = color_dict[cs] = cgenerator.next()
274 277 return "color: rgb(%s)! important;" % (', '.join(col))
275 278
276 279 def url_func(repo_name):
277 280
278 281 def _url_func(changeset):
279 282 author = changeset.author
280 283 date = changeset.date
281 284 message = tooltip(changeset.message)
282 285
283 286 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
284 287 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
285 288 "</b> %s<br/></div>")
286 289
287 290 tooltip_html = tooltip_html % (author, date, message)
288 291 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
289 292 short_id(changeset.raw_id))
290 293 uri = link_to(
291 294 lnk_format,
292 295 url('changeset_home', repo_name=repo_name,
293 296 revision=changeset.raw_id),
294 297 style=get_color_string(changeset.raw_id),
295 298 class_='tooltip',
296 299 title=tooltip_html
297 300 )
298 301
299 302 uri += '\n'
300 303 return uri
301 304 return _url_func
302 305
303 306 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
304 307
305 308
306 309 def is_following_repo(repo_name, user_id):
307 310 from rhodecode.model.scm import ScmModel
308 311 return ScmModel().is_following_repo(repo_name, user_id)
309 312
310 313 flash = _Flash()
311 314
312 315 #==============================================================================
313 316 # SCM FILTERS available via h.
314 317 #==============================================================================
315 318 from vcs.utils import author_name, author_email
316 319 from rhodecode.lib import credentials_filter, age as _age
317 320 from rhodecode.model.db import User
318 321
319 322 age = lambda x: _age(x)
320 323 capitalize = lambda x: x.capitalize()
321 324 email = author_email
322 325 short_id = lambda x: x[:12]
323 326 hide_credentials = lambda x: ''.join(credentials_filter(x))
324 327
325 328
326 329 def email_or_none(author):
327 330 _email = email(author)
328 331 if _email != '':
329 332 return _email
330 333
331 334 # See if it contains a username we can get an email from
332 335 user = User.get_by_username(author_name(author), case_insensitive=True,
333 336 cache=True)
334 337 if user is not None:
335 338 return user.email
336 339
337 340 # No valid email, not a valid user in the system, none!
338 341 return None
339 342
340 343
341 344 def person(author):
342 345 # attr to return from fetched user
343 346 person_getter = lambda usr: usr.username
344 347
345 348 # Valid email in the attribute passed, see if they're in the system
346 349 _email = email(author)
347 350 if _email != '':
348 351 user = User.get_by_email(_email, case_insensitive=True, cache=True)
349 352 if user is not None:
350 353 return person_getter(user)
351 354 return _email
352 355
353 356 # Maybe it's a username?
354 357 _author = author_name(author)
355 358 user = User.get_by_username(_author, case_insensitive=True,
356 359 cache=True)
357 360 if user is not None:
358 361 return person_getter(user)
359 362
360 363 # Still nothing? Just pass back the author name then
361 364 return _author
362 365
363 366 def bool2icon(value):
364 367 """Returns True/False values represented as small html image of true/false
365 368 icons
366 369
367 370 :param value: bool value
368 371 """
369 372
370 373 if value is True:
371 374 return HTML.tag('img', src=url("/images/icons/accept.png"),
372 375 alt=_('True'))
373 376
374 377 if value is False:
375 378 return HTML.tag('img', src=url("/images/icons/cancel.png"),
376 379 alt=_('False'))
377 380
378 381 return value
379 382
380 383
381 384 def action_parser(user_log, feed=False):
382 385 """This helper will action_map the specified string action into translated
383 386 fancy names with icons and links
384 387
385 388 :param user_log: user log instance
386 389 :param feed: use output for feeds (no html and fancy icons)
387 390 """
388 391
389 392 action = user_log.action
390 393 action_params = ' '
391 394
392 395 x = action.split(':')
393 396
394 397 if len(x) > 1:
395 398 action, action_params = x
396 399
397 400 def get_cs_links():
398 401 revs_limit = 3 #display this amount always
399 402 revs_top_limit = 50 #show upto this amount of changesets hidden
400 403 revs = action_params.split(',')
401 404 repo_name = user_log.repository.repo_name
402 405
403 406 from rhodecode.model.scm import ScmModel
404 407 repo = user_log.repository.scm_instance
405 408
406 409 message = lambda rev: get_changeset_safe(repo, rev).message
407 410 cs_links = []
408 411 cs_links.append(" " + ', '.join ([link_to(rev,
409 412 url('changeset_home',
410 413 repo_name=repo_name,
411 414 revision=rev), title=tooltip(message(rev)),
412 415 class_='tooltip') for rev in revs[:revs_limit] ]))
413 416
414 417 compare_view = (' <div class="compare_view tooltip" title="%s">'
415 418 '<a href="%s">%s</a> '
416 419 '</div>' % (_('Show all combined changesets %s->%s' \
417 420 % (revs[0], revs[-1])),
418 421 url('changeset_home', repo_name=repo_name,
419 422 revision='%s...%s' % (revs[0], revs[-1])
420 423 ),
421 424 _('compare view'))
422 425 )
423 426
424 427 if len(revs) > revs_limit:
425 428 uniq_id = revs[0]
426 429 html_tmpl = ('<span> %s '
427 430 '<a class="show_more" id="_%s" href="#more">%s</a> '
428 431 '%s</span>')
429 432 if not feed:
430 433 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
431 434 % (len(revs) - revs_limit),
432 435 _('revisions')))
433 436
434 437 if not feed:
435 438 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
436 439 else:
437 440 html_tmpl = '<span id="%s"> %s </span>'
438 441
439 442 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
440 443 url('changeset_home',
441 444 repo_name=repo_name, revision=rev),
442 445 title=message(rev), class_='tooltip')
443 446 for rev in revs[revs_limit:revs_top_limit]])))
444 447 if len(revs) > 1:
445 448 cs_links.append(compare_view)
446 449 return ''.join(cs_links)
447 450
448 451 def get_fork_name():
449 452 repo_name = action_params
450 453 return _('fork name ') + str(link_to(action_params, url('summary_home',
451 454 repo_name=repo_name,)))
452 455
453 456 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
454 457 'user_created_repo':(_('[created] repository'), None),
455 458 'user_created_fork':(_('[created] repository as fork'), None),
456 459 'user_forked_repo':(_('[forked] repository'), get_fork_name),
457 460 'user_updated_repo':(_('[updated] repository'), None),
458 461 'admin_deleted_repo':(_('[delete] repository'), None),
459 462 'admin_created_repo':(_('[created] repository'), None),
460 463 'admin_forked_repo':(_('[forked] repository'), None),
461 464 'admin_updated_repo':(_('[updated] repository'), None),
462 465 'push':(_('[pushed] into'), get_cs_links),
463 466 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
464 467 'push_remote':(_('[pulled from remote] into'), get_cs_links),
465 468 'pull':(_('[pulled] from'), None),
466 469 'started_following_repo':(_('[started following] repository'), None),
467 470 'stopped_following_repo':(_('[stopped following] repository'), None),
468 471 }
469 472
470 473 action_str = action_map.get(action, action)
471 474 if feed:
472 475 action = action_str[0].replace('[', '').replace(']', '')
473 476 else:
474 477 action = action_str[0].replace('[', '<span class="journal_highlight">')\
475 478 .replace(']', '</span>')
476 479
477 480 action_params_func = lambda :""
478 481
479 482 if callable(action_str[1]):
480 483 action_params_func = action_str[1]
481 484
482 485 return [literal(action), action_params_func]
483 486
484 487 def action_parser_icon(user_log):
485 488 action = user_log.action
486 489 action_params = None
487 490 x = action.split(':')
488 491
489 492 if len(x) > 1:
490 493 action, action_params = x
491 494
492 495 tmpl = """<img src="%s%s" alt="%s"/>"""
493 496 map = {'user_deleted_repo':'database_delete.png',
494 497 'user_created_repo':'database_add.png',
495 498 'user_created_fork':'arrow_divide.png',
496 499 'user_forked_repo':'arrow_divide.png',
497 500 'user_updated_repo':'database_edit.png',
498 501 'admin_deleted_repo':'database_delete.png',
499 502 'admin_created_repo':'database_add.png',
500 503 'admin_forked_repo':'arrow_divide.png',
501 504 'admin_updated_repo':'database_edit.png',
502 505 'push':'script_add.png',
503 506 'push_local':'script_edit.png',
504 507 'push_remote':'connect.png',
505 508 'pull':'down_16.png',
506 509 'started_following_repo':'heart_add.png',
507 510 'stopped_following_repo':'heart_delete.png',
508 511 }
509 512 return literal(tmpl % ((url('/images/icons/')),
510 513 map.get(action, action), action))
511 514
512 515
513 516 #==============================================================================
514 517 # PERMS
515 518 #==============================================================================
516 519 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
517 520 HasRepoPermissionAny, HasRepoPermissionAll
518 521
519 522 #==============================================================================
520 523 # GRAVATAR URL
521 524 #==============================================================================
522 525
523 526 def gravatar_url(email_address, size=30):
524 527 if (not str2bool(config['app_conf'].get('use_gravatar')) or
525 528 not email_address or email_address == 'anonymous@rhodecode.org'):
526 529 return url("/images/user%s.png" % size)
527 530
528 531 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
529 532 default = 'identicon'
530 533 baseurl_nossl = "http://www.gravatar.com/avatar/"
531 534 baseurl_ssl = "https://secure.gravatar.com/avatar/"
532 535 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
533 536
534 537 if isinstance(email_address, unicode):
535 538 #hashlib crashes on unicode items
536 539 email_address = safe_str(email_address)
537 540 # construct the url
538 541 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
539 542 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
540 543
541 544 return gravatar_url
542 545
543 546
544 547 #==============================================================================
545 548 # REPO PAGER, PAGER FOR REPOSITORY
546 549 #==============================================================================
547 550 class RepoPage(Page):
548 551
549 552 def __init__(self, collection, page=1, items_per_page=20,
550 553 item_count=None, url=None, **kwargs):
551 554
552 555 """Create a "RepoPage" instance. special pager for paging
553 556 repository
554 557 """
555 558 self._url_generator = url
556 559
557 560 # Safe the kwargs class-wide so they can be used in the pager() method
558 561 self.kwargs = kwargs
559 562
560 563 # Save a reference to the collection
561 564 self.original_collection = collection
562 565
563 566 self.collection = collection
564 567
565 568 # The self.page is the number of the current page.
566 569 # The first page has the number 1!
567 570 try:
568 571 self.page = int(page) # make it int() if we get it as a string
569 572 except (ValueError, TypeError):
570 573 self.page = 1
571 574
572 575 self.items_per_page = items_per_page
573 576
574 577 # Unless the user tells us how many items the collections has
575 578 # we calculate that ourselves.
576 579 if item_count is not None:
577 580 self.item_count = item_count
578 581 else:
579 582 self.item_count = len(self.collection)
580 583
581 584 # Compute the number of the first and last available page
582 585 if self.item_count > 0:
583 586 self.first_page = 1
584 587 self.page_count = int(math.ceil(float(self.item_count) /
585 588 self.items_per_page))
586 589 self.last_page = self.first_page + self.page_count - 1
587 590
588 591 # Make sure that the requested page number is the range of
589 592 # valid pages
590 593 if self.page > self.last_page:
591 594 self.page = self.last_page
592 595 elif self.page < self.first_page:
593 596 self.page = self.first_page
594 597
595 598 # Note: the number of items on this page can be less than
596 599 # items_per_page if the last page is not full
597 600 self.first_item = max(0, (self.item_count) - (self.page *
598 601 items_per_page))
599 602 self.last_item = ((self.item_count - 1) - items_per_page *
600 603 (self.page - 1))
601 604
602 605 self.items = list(self.collection[self.first_item:self.last_item + 1])
603 606
604 607
605 608 # Links to previous and next page
606 609 if self.page > self.first_page:
607 610 self.previous_page = self.page - 1
608 611 else:
609 612 self.previous_page = None
610 613
611 614 if self.page < self.last_page:
612 615 self.next_page = self.page + 1
613 616 else:
614 617 self.next_page = None
615 618
616 619 # No items available
617 620 else:
618 621 self.first_page = None
619 622 self.page_count = 0
620 623 self.last_page = None
621 624 self.first_item = None
622 625 self.last_item = None
623 626 self.previous_page = None
624 627 self.next_page = None
625 628 self.items = []
626 629
627 630 # This is a subclass of the 'list' type. Initialise the list now.
628 631 list.__init__(self, reversed(self.items))
629 632
630 633
631 634 def changed_tooltip(nodes):
632 635 """
633 636 Generates a html string for changed nodes in changeset page.
634 637 It limits the output to 30 entries
635 638
636 639 :param nodes: LazyNodesGenerator
637 640 """
638 641 if nodes:
639 642 pref = ': <br/> '
640 643 suf = ''
641 644 if len(nodes) > 30:
642 645 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
643 646 return literal(pref + '<br/> '.join([safe_unicode(x.path)
644 647 for x in nodes[:30]]) + suf)
645 648 else:
646 649 return ': ' + _('No Files')
647 650
648 651
649 652
650 653 def repo_link(groups_and_repos):
651 654 """
652 655 Makes a breadcrumbs link to repo within a group
653 656 joins &raquo; on each group to create a fancy link
654 657
655 658 ex::
656 659 group >> subgroup >> repo
657 660
658 661 :param groups_and_repos:
659 662 """
660 663 groups, repo_name = groups_and_repos
661 664
662 665 if not groups:
663 666 return repo_name
664 667 else:
665 668 def make_link(group):
666 669 return link_to(group.name, url('repos_group_home',
667 670 group_name=group.group_name))
668 671 return literal(' &raquo; '.join(map(make_link, groups)) + \
669 672 " &raquo; " + repo_name)
670 673
671 674 def fancy_file_stats(stats):
672 675 """
673 676 Displays a fancy two colored bar for number of added/deleted
674 677 lines of code on file
675 678
676 679 :param stats: two element list of added/deleted lines of code
677 680 """
678 681
679 682 a, d, t = stats[0], stats[1], stats[0] + stats[1]
680 683 width = 100
681 684 unit = float(width) / (t or 1)
682 685
683 686 # needs > 9% of width to be visible or 0 to be hidden
684 687 a_p = max(9, unit * a) if a > 0 else 0
685 688 d_p = max(9, unit * d) if d > 0 else 0
686 689 p_sum = a_p + d_p
687 690
688 691 if p_sum > width:
689 692 #adjust the percentage to be == 100% since we adjusted to 9
690 693 if a_p > d_p:
691 694 a_p = a_p - (p_sum - width)
692 695 else:
693 696 d_p = d_p - (p_sum - width)
694 697
695 698 a_v = a if a > 0 else ''
696 699 d_v = d if d > 0 else ''
697 700
698 701
699 702 def cgen(l_type):
700 703 mapping = {'tr':'top-right-rounded-corner',
701 704 'tl':'top-left-rounded-corner',
702 705 'br':'bottom-right-rounded-corner',
703 706 'bl':'bottom-left-rounded-corner'}
704 707 map_getter = lambda x:mapping[x]
705 708
706 709 if l_type == 'a' and d_v:
707 710 #case when added and deleted are present
708 711 return ' '.join(map(map_getter, ['tl', 'bl']))
709 712
710 713 if l_type == 'a' and not d_v:
711 714 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
712 715
713 716 if l_type == 'd' and a_v:
714 717 return ' '.join(map(map_getter, ['tr', 'br']))
715 718
716 719 if l_type == 'd' and not a_v:
717 720 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
718 721
719 722
720 723
721 724 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
722 725 a_p, a_v)
723 726 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
724 727 d_p, d_v)
725 728 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
726 729
727 730
728 731 def urlify_text(text):
729 732 import re
730 733
731 734 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
732 735 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
733 736
734 737 def url_func(match_obj):
735 738 url_full = match_obj.groups()[0]
736 739 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
737 740
738 741 return literal(url_pat.sub(url_func, text))
739 742
740 743
741 744 def rst(source):
742 745 return literal('<div class="rst-block">%s</div>' %
743 746 MarkupRenderer.rst(source))
744 747
745 748 def rst_w_mentions(source):
746 749 """
747 750 Wrapped rst renderer with @mention highlighting
748 751
749 752 :param source:
750 753 """
751 754 return literal('<div class="rst-block">%s</div>' %
752 755 MarkupRenderer.rst_with_mentions(source))
General Comments 0
You need to be logged in to leave comments. Login now