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