##// END OF EJS Templates
Format datetime in notifications according to unified function
marcink -
r2445:9b623dcd beta
parent child Browse files
Show More
@@ -1,983 +1,984
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
13 13 from datetime import datetime
14 14 from pygments.formatters.html import HtmlFormatter
15 15 from pygments import highlight as code_highlight
16 16 from pylons import url, request, config
17 17 from pylons.i18n.translation import _, ungettext
18 18 from hashlib import md5
19 19
20 20 from webhelpers.html import literal, HTML, escape
21 21 from webhelpers.html.tools import *
22 22 from webhelpers.html.builder import make_tag
23 23 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
24 24 end_form, file, form, hidden, image, javascript_link, link_to, \
25 25 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
26 26 submit, text, password, textarea, title, ul, xml_declaration, radio
27 27 from webhelpers.html.tools import auto_link, button_to, highlight, \
28 28 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
29 29 from webhelpers.number import format_byte_size, format_bit_size
30 30 from webhelpers.pylonslib import Flash as _Flash
31 31 from webhelpers.pylonslib.secure_form import secure_form
32 32 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
33 33 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
34 34 replace_whitespace, urlify, truncate, wrap_paragraphs
35 35 from webhelpers.date import time_ago_in_words
36 36 from webhelpers.paginate import Page
37 37 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
38 38 convert_boolean_attrs, NotGiven, _make_safe_id_component
39 39
40 40 from rhodecode.lib.annotate import annotate_highlight
41 41 from rhodecode.lib.utils import repo_name_slug
42 42 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
43 43 get_changeset_safe
44 44 from rhodecode.lib.markup_renderer import MarkupRenderer
45 45 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
46 46 from rhodecode.lib.vcs.backends.base import BaseChangeset
47 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
47 48 from rhodecode.model.db import URL_SEP
48 49
49 50 log = logging.getLogger(__name__)
50 51
51 52
52 53 def shorter(text, size=20):
53 54 postfix = '...'
54 55 if len(text) > size:
55 56 return text[:size - len(postfix)] + postfix
56 57 return text
57 58
58 59
59 60 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
60 61 """
61 62 Reset button
62 63 """
63 64 _set_input_attrs(attrs, type, name, value)
64 65 _set_id_attr(attrs, id, name)
65 66 convert_boolean_attrs(attrs, ["disabled"])
66 67 return HTML.input(**attrs)
67 68
68 69 reset = _reset
69 70 safeid = _make_safe_id_component
70 71
71 72
72 73 def FID(raw_id, path):
73 74 """
74 75 Creates a uniqe ID for filenode based on it's hash of path and revision
75 76 it's safe to use in urls
76 77
77 78 :param raw_id:
78 79 :param path:
79 80 """
80 81
81 82 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
82 83
83 84
84 85 def get_token():
85 86 """Return the current authentication token, creating one if one doesn't
86 87 already exist.
87 88 """
88 89 token_key = "_authentication_token"
89 90 from pylons import session
90 91 if not token_key in session:
91 92 try:
92 93 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
93 94 except AttributeError: # Python < 2.4
94 95 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
95 96 session[token_key] = token
96 97 if hasattr(session, 'save'):
97 98 session.save()
98 99 return session[token_key]
99 100
100 101
101 102 class _GetError(object):
102 103 """Get error from form_errors, and represent it as span wrapped error
103 104 message
104 105
105 106 :param field_name: field to fetch errors for
106 107 :param form_errors: form errors dict
107 108 """
108 109
109 110 def __call__(self, field_name, form_errors):
110 111 tmpl = """<span class="error_msg">%s</span>"""
111 112 if form_errors and field_name in form_errors:
112 113 return literal(tmpl % form_errors.get(field_name))
113 114
114 115 get_error = _GetError()
115 116
116 117
117 118 class _ToolTip(object):
118 119
119 120 def __call__(self, tooltip_title, trim_at=50):
120 121 """
121 122 Special function just to wrap our text into nice formatted
122 123 autowrapped text
123 124
124 125 :param tooltip_title:
125 126 """
126 127 tooltip_title = escape(tooltip_title)
127 128 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
128 129 return tooltip_title
129 130 tooltip = _ToolTip()
130 131
131 132
132 133 class _FilesBreadCrumbs(object):
133 134
134 135 def __call__(self, repo_name, rev, paths):
135 136 if isinstance(paths, str):
136 137 paths = safe_unicode(paths)
137 138 url_l = [link_to(repo_name, url('files_home',
138 139 repo_name=repo_name,
139 140 revision=rev, f_path=''))]
140 141 paths_l = paths.split('/')
141 142 for cnt, p in enumerate(paths_l):
142 143 if p != '':
143 144 url_l.append(link_to(p,
144 145 url('files_home',
145 146 repo_name=repo_name,
146 147 revision=rev,
147 148 f_path='/'.join(paths_l[:cnt + 1])
148 149 )
149 150 )
150 151 )
151 152
152 153 return literal('/'.join(url_l))
153 154
154 155 files_breadcrumbs = _FilesBreadCrumbs()
155 156
156 157
157 158 class CodeHtmlFormatter(HtmlFormatter):
158 159 """
159 160 My code Html Formatter for source codes
160 161 """
161 162
162 163 def wrap(self, source, outfile):
163 164 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
164 165
165 166 def _wrap_code(self, source):
166 167 for cnt, it in enumerate(source):
167 168 i, t = it
168 169 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
169 170 yield i, t
170 171
171 172 def _wrap_tablelinenos(self, inner):
172 173 dummyoutfile = StringIO.StringIO()
173 174 lncount = 0
174 175 for t, line in inner:
175 176 if t:
176 177 lncount += 1
177 178 dummyoutfile.write(line)
178 179
179 180 fl = self.linenostart
180 181 mw = len(str(lncount + fl - 1))
181 182 sp = self.linenospecial
182 183 st = self.linenostep
183 184 la = self.lineanchors
184 185 aln = self.anchorlinenos
185 186 nocls = self.noclasses
186 187 if sp:
187 188 lines = []
188 189
189 190 for i in range(fl, fl + lncount):
190 191 if i % st == 0:
191 192 if i % sp == 0:
192 193 if aln:
193 194 lines.append('<a href="#%s%d" class="special">%*d</a>' %
194 195 (la, i, mw, i))
195 196 else:
196 197 lines.append('<span class="special">%*d</span>' % (mw, i))
197 198 else:
198 199 if aln:
199 200 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
200 201 else:
201 202 lines.append('%*d' % (mw, i))
202 203 else:
203 204 lines.append('')
204 205 ls = '\n'.join(lines)
205 206 else:
206 207 lines = []
207 208 for i in range(fl, fl + lncount):
208 209 if i % st == 0:
209 210 if aln:
210 211 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
211 212 else:
212 213 lines.append('%*d' % (mw, i))
213 214 else:
214 215 lines.append('')
215 216 ls = '\n'.join(lines)
216 217
217 218 # in case you wonder about the seemingly redundant <div> here: since the
218 219 # content in the other cell also is wrapped in a div, some browsers in
219 220 # some configurations seem to mess up the formatting...
220 221 if nocls:
221 222 yield 0, ('<table class="%stable">' % self.cssclass +
222 223 '<tr><td><div class="linenodiv" '
223 224 'style="background-color: #f0f0f0; padding-right: 10px">'
224 225 '<pre style="line-height: 125%">' +
225 226 ls + '</pre></div></td><td id="hlcode" class="code">')
226 227 else:
227 228 yield 0, ('<table class="%stable">' % self.cssclass +
228 229 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
229 230 ls + '</pre></div></td><td id="hlcode" class="code">')
230 231 yield 0, dummyoutfile.getvalue()
231 232 yield 0, '</td></tr></table>'
232 233
233 234
234 235 def pygmentize(filenode, **kwargs):
235 236 """pygmentize function using pygments
236 237
237 238 :param filenode:
238 239 """
239 240
240 241 return literal(code_highlight(filenode.content,
241 242 filenode.lexer, CodeHtmlFormatter(**kwargs)))
242 243
243 244
244 245 def pygmentize_annotation(repo_name, filenode, **kwargs):
245 246 """
246 247 pygmentize function for annotation
247 248
248 249 :param filenode:
249 250 """
250 251
251 252 color_dict = {}
252 253
253 254 def gen_color(n=10000):
254 255 """generator for getting n of evenly distributed colors using
255 256 hsv color and golden ratio. It always return same order of colors
256 257
257 258 :returns: RGB tuple
258 259 """
259 260
260 261 def hsv_to_rgb(h, s, v):
261 262 if s == 0.0:
262 263 return v, v, v
263 264 i = int(h * 6.0) # XXX assume int() truncates!
264 265 f = (h * 6.0) - i
265 266 p = v * (1.0 - s)
266 267 q = v * (1.0 - s * f)
267 268 t = v * (1.0 - s * (1.0 - f))
268 269 i = i % 6
269 270 if i == 0:
270 271 return v, t, p
271 272 if i == 1:
272 273 return q, v, p
273 274 if i == 2:
274 275 return p, v, t
275 276 if i == 3:
276 277 return p, q, v
277 278 if i == 4:
278 279 return t, p, v
279 280 if i == 5:
280 281 return v, p, q
281 282
282 283 golden_ratio = 0.618033988749895
283 284 h = 0.22717784590367374
284 285
285 286 for _ in xrange(n):
286 287 h += golden_ratio
287 288 h %= 1
288 289 HSV_tuple = [h, 0.95, 0.95]
289 290 RGB_tuple = hsv_to_rgb(*HSV_tuple)
290 291 yield map(lambda x: str(int(x * 256)), RGB_tuple)
291 292
292 293 cgenerator = gen_color()
293 294
294 295 def get_color_string(cs):
295 296 if cs in color_dict:
296 297 col = color_dict[cs]
297 298 else:
298 299 col = color_dict[cs] = cgenerator.next()
299 300 return "color: rgb(%s)! important;" % (', '.join(col))
300 301
301 302 def url_func(repo_name):
302 303
303 304 def _url_func(changeset):
304 305 author = changeset.author
305 306 date = changeset.date
306 307 message = tooltip(changeset.message)
307 308
308 309 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
309 310 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
310 311 "</b> %s<br/></div>")
311 312
312 313 tooltip_html = tooltip_html % (author, date, message)
313 314 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
314 315 short_id(changeset.raw_id))
315 316 uri = link_to(
316 317 lnk_format,
317 318 url('changeset_home', repo_name=repo_name,
318 319 revision=changeset.raw_id),
319 320 style=get_color_string(changeset.raw_id),
320 321 class_='tooltip',
321 322 title=tooltip_html
322 323 )
323 324
324 325 uri += '\n'
325 326 return uri
326 327 return _url_func
327 328
328 329 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
329 330
330 331
331 332 def is_following_repo(repo_name, user_id):
332 333 from rhodecode.model.scm import ScmModel
333 334 return ScmModel().is_following_repo(repo_name, user_id)
334 335
335 336 flash = _Flash()
336 337
337 338 #==============================================================================
338 339 # SCM FILTERS available via h.
339 340 #==============================================================================
340 341 from rhodecode.lib.vcs.utils import author_name, author_email
341 342 from rhodecode.lib.utils2 import credentials_filter, age as _age
342 343 from rhodecode.model.db import User
343 344
344 345 age = lambda x: _age(x)
345 346 capitalize = lambda x: x.capitalize()
346 347 email = author_email
347 348 short_id = lambda x: x[:12]
348 349 hide_credentials = lambda x: ''.join(credentials_filter(x))
349 350
350 351
351 352 def fmt_date(date):
352 353 if date:
353 return (date.strftime(_(u"%a, %d %b %Y %H:%M:%S").encode('utf8'))
354 .decode('utf8'))
354 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
355 return date.strftime(_fmt).decode('utf8')
355 356
356 357 return ""
357 358
358 359
359 360 def is_git(repository):
360 361 if hasattr(repository, 'alias'):
361 362 _type = repository.alias
362 363 elif hasattr(repository, 'repo_type'):
363 364 _type = repository.repo_type
364 365 else:
365 366 _type = repository
366 367 return _type == 'git'
367 368
368 369
369 370 def is_hg(repository):
370 371 if hasattr(repository, 'alias'):
371 372 _type = repository.alias
372 373 elif hasattr(repository, 'repo_type'):
373 374 _type = repository.repo_type
374 375 else:
375 376 _type = repository
376 377 return _type == 'hg'
377 378
378 379
379 380 def email_or_none(author):
380 381 _email = email(author)
381 382 if _email != '':
382 383 return _email
383 384
384 385 # See if it contains a username we can get an email from
385 386 user = User.get_by_username(author_name(author), case_insensitive=True,
386 387 cache=True)
387 388 if user is not None:
388 389 return user.email
389 390
390 391 # No valid email, not a valid user in the system, none!
391 392 return None
392 393
393 394
394 395 def person(author):
395 396 # attr to return from fetched user
396 397 person_getter = lambda usr: usr.username
397 398
398 399 # Valid email in the attribute passed, see if they're in the system
399 400 _email = email(author)
400 401 if _email != '':
401 402 user = User.get_by_email(_email, case_insensitive=True, cache=True)
402 403 if user is not None:
403 404 return person_getter(user)
404 405 return _email
405 406
406 407 # Maybe it's a username?
407 408 _author = author_name(author)
408 409 user = User.get_by_username(_author, case_insensitive=True,
409 410 cache=True)
410 411 if user is not None:
411 412 return person_getter(user)
412 413
413 414 # Still nothing? Just pass back the author name then
414 415 return _author
415 416
416 417
417 418 def bool2icon(value):
418 419 """Returns True/False values represented as small html image of true/false
419 420 icons
420 421
421 422 :param value: bool value
422 423 """
423 424
424 425 if value is True:
425 426 return HTML.tag('img', src=url("/images/icons/accept.png"),
426 427 alt=_('True'))
427 428
428 429 if value is False:
429 430 return HTML.tag('img', src=url("/images/icons/cancel.png"),
430 431 alt=_('False'))
431 432
432 433 return value
433 434
434 435
435 436 def action_parser(user_log, feed=False):
436 437 """
437 438 This helper will action_map the specified string action into translated
438 439 fancy names with icons and links
439 440
440 441 :param user_log: user log instance
441 442 :param feed: use output for feeds (no html and fancy icons)
442 443 """
443 444
444 445 action = user_log.action
445 446 action_params = ' '
446 447
447 448 x = action.split(':')
448 449
449 450 if len(x) > 1:
450 451 action, action_params = x
451 452
452 453 def get_cs_links():
453 454 revs_limit = 3 # display this amount always
454 455 revs_top_limit = 50 # show upto this amount of changesets hidden
455 456 revs_ids = action_params.split(',')
456 457 deleted = user_log.repository is None
457 458 if deleted:
458 459 return ','.join(revs_ids)
459 460
460 461 repo_name = user_log.repository.repo_name
461 462
462 463 repo = user_log.repository.scm_instance
463 464
464 465 def lnk(rev, repo_name):
465 466
466 467 if isinstance(rev, BaseChangeset):
467 468 lbl = 'r%s:%s' % (rev.revision, rev.short_id)
468 469 _url = url('changeset_home', repo_name=repo_name,
469 470 revision=rev.raw_id)
470 471 title = tooltip(rev.message)
471 472 else:
472 473 lbl = '%s' % rev
473 474 _url = '#'
474 475 title = _('Changeset not found')
475 476
476 477 return link_to(lbl, _url, title=title, class_='tooltip',)
477 478
478 479 revs = []
479 480 if len(filter(lambda v: v != '', revs_ids)) > 0:
480 481 for rev in revs_ids[:revs_top_limit]:
481 482 try:
482 483 rev = repo.get_changeset(rev)
483 484 revs.append(rev)
484 485 except ChangesetDoesNotExistError:
485 486 log.error('cannot find revision %s in this repo' % rev)
486 487 revs.append(rev)
487 488 continue
488 489 cs_links = []
489 490 cs_links.append(" " + ', '.join(
490 491 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
491 492 )
492 493 )
493 494
494 495 compare_view = (
495 496 ' <div class="compare_view tooltip" title="%s">'
496 497 '<a href="%s">%s</a> </div>' % (
497 498 _('Show all combined changesets %s->%s') % (
498 499 revs_ids[0], revs_ids[-1]
499 500 ),
500 501 url('changeset_home', repo_name=repo_name,
501 502 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
502 503 ),
503 504 _('compare view')
504 505 )
505 506 )
506 507
507 508 # if we have exactly one more than normally displayed
508 509 # just display it, takes less space than displaying
509 510 # "and 1 more revisions"
510 511 if len(revs_ids) == revs_limit + 1:
511 512 rev = revs[revs_limit]
512 513 cs_links.append(", " + lnk(rev, repo_name))
513 514
514 515 # hidden-by-default ones
515 516 if len(revs_ids) > revs_limit + 1:
516 517 uniq_id = revs_ids[0]
517 518 html_tmpl = (
518 519 '<span> %s <a class="show_more" id="_%s" '
519 520 'href="#more">%s</a> %s</span>'
520 521 )
521 522 if not feed:
522 523 cs_links.append(html_tmpl % (
523 524 _('and'),
524 525 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
525 526 _('revisions')
526 527 )
527 528 )
528 529
529 530 if not feed:
530 531 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
531 532 else:
532 533 html_tmpl = '<span id="%s"> %s </span>'
533 534
534 535 morelinks = ', '.join(
535 536 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
536 537 )
537 538
538 539 if len(revs_ids) > revs_top_limit:
539 540 morelinks += ', ...'
540 541
541 542 cs_links.append(html_tmpl % (uniq_id, morelinks))
542 543 if len(revs) > 1:
543 544 cs_links.append(compare_view)
544 545 return ''.join(cs_links)
545 546
546 547 def get_fork_name():
547 548 repo_name = action_params
548 549 return _('fork name ') + str(link_to(action_params, url('summary_home',
549 550 repo_name=repo_name,)))
550 551
551 552 def get_user_name():
552 553 user_name = action_params
553 554 return user_name
554 555
555 556 def get_users_group():
556 557 group_name = action_params
557 558 return group_name
558 559
559 560 # action : translated str, callback(extractor), icon
560 561 action_map = {
561 562 'user_deleted_repo': (_('[deleted] repository'),
562 563 None, 'database_delete.png'),
563 564 'user_created_repo': (_('[created] repository'),
564 565 None, 'database_add.png'),
565 566 'user_created_fork': (_('[created] repository as fork'),
566 567 None, 'arrow_divide.png'),
567 568 'user_forked_repo': (_('[forked] repository'),
568 569 get_fork_name, 'arrow_divide.png'),
569 570 'user_updated_repo': (_('[updated] repository'),
570 571 None, 'database_edit.png'),
571 572 'admin_deleted_repo': (_('[delete] repository'),
572 573 None, 'database_delete.png'),
573 574 'admin_created_repo': (_('[created] repository'),
574 575 None, 'database_add.png'),
575 576 'admin_forked_repo': (_('[forked] repository'),
576 577 None, 'arrow_divide.png'),
577 578 'admin_updated_repo': (_('[updated] repository'),
578 579 None, 'database_edit.png'),
579 580 'admin_created_user': (_('[created] user'),
580 581 get_user_name, 'user_add.png'),
581 582 'admin_updated_user': (_('[updated] user'),
582 583 get_user_name, 'user_edit.png'),
583 584 'admin_created_users_group': (_('[created] users group'),
584 585 get_users_group, 'group_add.png'),
585 586 'admin_updated_users_group': (_('[updated] users group'),
586 587 get_users_group, 'group_edit.png'),
587 588 'user_commented_revision': (_('[commented] on revision in repository'),
588 589 get_cs_links, 'comment_add.png'),
589 590 'push': (_('[pushed] into'),
590 591 get_cs_links, 'script_add.png'),
591 592 'push_local': (_('[committed via RhodeCode] into repository'),
592 593 get_cs_links, 'script_edit.png'),
593 594 'push_remote': (_('[pulled from remote] into repository'),
594 595 get_cs_links, 'connect.png'),
595 596 'pull': (_('[pulled] from'),
596 597 None, 'down_16.png'),
597 598 'started_following_repo': (_('[started following] repository'),
598 599 None, 'heart_add.png'),
599 600 'stopped_following_repo': (_('[stopped following] repository'),
600 601 None, 'heart_delete.png'),
601 602 }
602 603
603 604 action_str = action_map.get(action, action)
604 605 if feed:
605 606 action = action_str[0].replace('[', '').replace(']', '')
606 607 else:
607 608 action = action_str[0]\
608 609 .replace('[', '<span class="journal_highlight">')\
609 610 .replace(']', '</span>')
610 611
611 612 action_params_func = lambda: ""
612 613
613 614 if callable(action_str[1]):
614 615 action_params_func = action_str[1]
615 616
616 617 def action_parser_icon():
617 618 action = user_log.action
618 619 action_params = None
619 620 x = action.split(':')
620 621
621 622 if len(x) > 1:
622 623 action, action_params = x
623 624
624 625 tmpl = """<img src="%s%s" alt="%s"/>"""
625 626 ico = action_map.get(action, ['', '', ''])[2]
626 627 return literal(tmpl % ((url('/images/icons/')), ico, action))
627 628
628 629 # returned callbacks we need to call to get
629 630 return [lambda: literal(action), action_params_func, action_parser_icon]
630 631
631 632
632 633
633 634 #==============================================================================
634 635 # PERMS
635 636 #==============================================================================
636 637 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
637 638 HasRepoPermissionAny, HasRepoPermissionAll
638 639
639 640
640 641 #==============================================================================
641 642 # GRAVATAR URL
642 643 #==============================================================================
643 644
644 645 def gravatar_url(email_address, size=30):
645 646 if (not str2bool(config['app_conf'].get('use_gravatar')) or
646 647 not email_address or email_address == 'anonymous@rhodecode.org'):
647 648 f = lambda a, l: min(l, key=lambda x: abs(x - a))
648 649 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
649 650
650 651 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
651 652 default = 'identicon'
652 653 baseurl_nossl = "http://www.gravatar.com/avatar/"
653 654 baseurl_ssl = "https://secure.gravatar.com/avatar/"
654 655 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
655 656
656 657 if isinstance(email_address, unicode):
657 658 #hashlib crashes on unicode items
658 659 email_address = safe_str(email_address)
659 660 # construct the url
660 661 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
661 662 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
662 663
663 664 return gravatar_url
664 665
665 666
666 667 #==============================================================================
667 668 # REPO PAGER, PAGER FOR REPOSITORY
668 669 #==============================================================================
669 670 class RepoPage(Page):
670 671
671 672 def __init__(self, collection, page=1, items_per_page=20,
672 673 item_count=None, url=None, **kwargs):
673 674
674 675 """Create a "RepoPage" instance. special pager for paging
675 676 repository
676 677 """
677 678 self._url_generator = url
678 679
679 680 # Safe the kwargs class-wide so they can be used in the pager() method
680 681 self.kwargs = kwargs
681 682
682 683 # Save a reference to the collection
683 684 self.original_collection = collection
684 685
685 686 self.collection = collection
686 687
687 688 # The self.page is the number of the current page.
688 689 # The first page has the number 1!
689 690 try:
690 691 self.page = int(page) # make it int() if we get it as a string
691 692 except (ValueError, TypeError):
692 693 self.page = 1
693 694
694 695 self.items_per_page = items_per_page
695 696
696 697 # Unless the user tells us how many items the collections has
697 698 # we calculate that ourselves.
698 699 if item_count is not None:
699 700 self.item_count = item_count
700 701 else:
701 702 self.item_count = len(self.collection)
702 703
703 704 # Compute the number of the first and last available page
704 705 if self.item_count > 0:
705 706 self.first_page = 1
706 707 self.page_count = int(math.ceil(float(self.item_count) /
707 708 self.items_per_page))
708 709 self.last_page = self.first_page + self.page_count - 1
709 710
710 711 # Make sure that the requested page number is the range of
711 712 # valid pages
712 713 if self.page > self.last_page:
713 714 self.page = self.last_page
714 715 elif self.page < self.first_page:
715 716 self.page = self.first_page
716 717
717 718 # Note: the number of items on this page can be less than
718 719 # items_per_page if the last page is not full
719 720 self.first_item = max(0, (self.item_count) - (self.page *
720 721 items_per_page))
721 722 self.last_item = ((self.item_count - 1) - items_per_page *
722 723 (self.page - 1))
723 724
724 725 self.items = list(self.collection[self.first_item:self.last_item + 1])
725 726
726 727 # Links to previous and next page
727 728 if self.page > self.first_page:
728 729 self.previous_page = self.page - 1
729 730 else:
730 731 self.previous_page = None
731 732
732 733 if self.page < self.last_page:
733 734 self.next_page = self.page + 1
734 735 else:
735 736 self.next_page = None
736 737
737 738 # No items available
738 739 else:
739 740 self.first_page = None
740 741 self.page_count = 0
741 742 self.last_page = None
742 743 self.first_item = None
743 744 self.last_item = None
744 745 self.previous_page = None
745 746 self.next_page = None
746 747 self.items = []
747 748
748 749 # This is a subclass of the 'list' type. Initialise the list now.
749 750 list.__init__(self, reversed(self.items))
750 751
751 752
752 753 def changed_tooltip(nodes):
753 754 """
754 755 Generates a html string for changed nodes in changeset page.
755 756 It limits the output to 30 entries
756 757
757 758 :param nodes: LazyNodesGenerator
758 759 """
759 760 if nodes:
760 761 pref = ': <br/> '
761 762 suf = ''
762 763 if len(nodes) > 30:
763 764 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
764 765 return literal(pref + '<br/> '.join([safe_unicode(x.path)
765 766 for x in nodes[:30]]) + suf)
766 767 else:
767 768 return ': ' + _('No Files')
768 769
769 770
770 771 def repo_link(groups_and_repos):
771 772 """
772 773 Makes a breadcrumbs link to repo within a group
773 774 joins &raquo; on each group to create a fancy link
774 775
775 776 ex::
776 777 group >> subgroup >> repo
777 778
778 779 :param groups_and_repos:
779 780 """
780 781 groups, repo_name = groups_and_repos
781 782
782 783 if not groups:
783 784 return repo_name
784 785 else:
785 786 def make_link(group):
786 787 return link_to(group.name, url('repos_group_home',
787 788 group_name=group.group_name))
788 789 return literal(' &raquo; '.join(map(make_link, groups)) + \
789 790 " &raquo; " + repo_name)
790 791
791 792
792 793 def fancy_file_stats(stats):
793 794 """
794 795 Displays a fancy two colored bar for number of added/deleted
795 796 lines of code on file
796 797
797 798 :param stats: two element list of added/deleted lines of code
798 799 """
799 800
800 801 a, d, t = stats[0], stats[1], stats[0] + stats[1]
801 802 width = 100
802 803 unit = float(width) / (t or 1)
803 804
804 805 # needs > 9% of width to be visible or 0 to be hidden
805 806 a_p = max(9, unit * a) if a > 0 else 0
806 807 d_p = max(9, unit * d) if d > 0 else 0
807 808 p_sum = a_p + d_p
808 809
809 810 if p_sum > width:
810 811 #adjust the percentage to be == 100% since we adjusted to 9
811 812 if a_p > d_p:
812 813 a_p = a_p - (p_sum - width)
813 814 else:
814 815 d_p = d_p - (p_sum - width)
815 816
816 817 a_v = a if a > 0 else ''
817 818 d_v = d if d > 0 else ''
818 819
819 820 def cgen(l_type):
820 821 mapping = {'tr': 'top-right-rounded-corner-mid',
821 822 'tl': 'top-left-rounded-corner-mid',
822 823 'br': 'bottom-right-rounded-corner-mid',
823 824 'bl': 'bottom-left-rounded-corner-mid'}
824 825 map_getter = lambda x: mapping[x]
825 826
826 827 if l_type == 'a' and d_v:
827 828 #case when added and deleted are present
828 829 return ' '.join(map(map_getter, ['tl', 'bl']))
829 830
830 831 if l_type == 'a' and not d_v:
831 832 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
832 833
833 834 if l_type == 'd' and a_v:
834 835 return ' '.join(map(map_getter, ['tr', 'br']))
835 836
836 837 if l_type == 'd' and not a_v:
837 838 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
838 839
839 840 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
840 841 cgen('a'), a_p, a_v
841 842 )
842 843 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
843 844 cgen('d'), d_p, d_v
844 845 )
845 846 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
846 847
847 848
848 849 def urlify_text(text_):
849 850 import re
850 851
851 852 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
852 853 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
853 854
854 855 def url_func(match_obj):
855 856 url_full = match_obj.groups()[0]
856 857 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
857 858
858 859 return literal(url_pat.sub(url_func, text_))
859 860
860 861
861 862 def urlify_changesets(text_, repository):
862 863 """
863 864 Extract revision ids from changeset and make link from them
864 865
865 866 :param text_:
866 867 :param repository:
867 868 """
868 869 import re
869 870 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
870 871
871 872 def url_func(match_obj):
872 873 rev = match_obj.groups()[0]
873 874 pref = ''
874 875 if match_obj.group().startswith(' '):
875 876 pref = ' '
876 877 tmpl = (
877 878 '%(pref)s<a class="%(cls)s" href="%(url)s">'
878 879 '%(rev)s'
879 880 '</a>'
880 881 )
881 882 return tmpl % {
882 883 'pref': pref,
883 884 'cls': 'revision-link',
884 885 'url': url('changeset_home', repo_name=repository, revision=rev),
885 886 'rev': rev,
886 887 }
887 888
888 889 newtext = URL_PAT.sub(url_func, text_)
889 890
890 891 return newtext
891 892
892 893
893 894 def urlify_commit(text_, repository=None, link_=None):
894 895 """
895 896 Parses given text message and makes proper links.
896 897 issues are linked to given issue-server, and rest is a changeset link
897 898 if link_ is given, in other case it's a plain text
898 899
899 900 :param text_:
900 901 :param repository:
901 902 :param link_: changeset link
902 903 """
903 904 import re
904 905 import traceback
905 906
906 907 def escaper(string):
907 908 return string.replace('<', '&lt;').replace('>', '&gt;')
908 909
909 910 def linkify_others(t, l):
910 911 urls = re.compile(r'(\<a.*?\<\/a\>)',)
911 912 links = []
912 913 for e in urls.split(t):
913 914 if not urls.match(e):
914 915 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
915 916 else:
916 917 links.append(e)
917 918
918 919 return ''.join(links)
919 920
920 921 # urlify changesets - extrac revisions and make link out of them
921 922 text_ = urlify_changesets(escaper(text_), repository)
922 923
923 924 try:
924 925 conf = config['app_conf']
925 926
926 927 URL_PAT = re.compile(r'%s' % conf.get('issue_pat'))
927 928
928 929 if URL_PAT:
929 930 ISSUE_SERVER_LNK = conf.get('issue_server_link')
930 931 ISSUE_PREFIX = conf.get('issue_prefix')
931 932
932 933 def url_func(match_obj):
933 934 pref = ''
934 935 if match_obj.group().startswith(' '):
935 936 pref = ' '
936 937
937 938 issue_id = ''.join(match_obj.groups())
938 939 tmpl = (
939 940 '%(pref)s<a class="%(cls)s" href="%(url)s">'
940 941 '%(issue-prefix)s%(id-repr)s'
941 942 '</a>'
942 943 )
943 944 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
944 945 if repository:
945 946 url = url.replace('{repo}', repository)
946 947 repo_name = repository.split(URL_SEP)[-1]
947 948 url = url.replace('{repo_name}', repo_name)
948 949 return tmpl % {
949 950 'pref': pref,
950 951 'cls': 'issue-tracker-link',
951 952 'url': url,
952 953 'id-repr': issue_id,
953 954 'issue-prefix': ISSUE_PREFIX,
954 955 'serv': ISSUE_SERVER_LNK,
955 956 }
956 957
957 958 newtext = URL_PAT.sub(url_func, text_)
958 959
959 960 if link_:
960 961 # wrap not links into final link => link_
961 962 newtext = linkify_others(newtext, link_)
962 963
963 964 return literal(newtext)
964 965 except:
965 966 log.error(traceback.format_exc())
966 967 pass
967 968
968 969 return text_
969 970
970 971
971 972 def rst(source):
972 973 return literal('<div class="rst-block">%s</div>' %
973 974 MarkupRenderer.rst(source))
974 975
975 976
976 977 def rst_w_mentions(source):
977 978 """
978 979 Wrapped rst renderer with @mention highlighting
979 980
980 981 :param source:
981 982 """
982 983 return literal('<div class="rst-block">%s</div>' %
983 984 MarkupRenderer.rst_with_mentions(source))
@@ -1,229 +1,228
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) 2010-2012 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 from rhodecode.config.conf import DATETIME_FORMAT
36 35 from rhodecode.lib import helpers as h
37 36 from rhodecode.model import BaseModel
38 37 from rhodecode.model.db import Notification, User, UserNotification
39 38
40 39 log = logging.getLogger(__name__)
41 40
42 41
43 42 class NotificationModel(BaseModel):
44 43
45 44 def __get_user(self, user):
46 45 return self._get_instance(User, user, callback=User.get_by_username)
47 46
48 47 def __get_notification(self, notification):
49 48 if isinstance(notification, Notification):
50 49 return notification
51 50 elif isinstance(notification, (int, long)):
52 51 return Notification.get(notification)
53 52 else:
54 53 if notification:
55 54 raise Exception('notification must be int, long or Instance'
56 55 ' of Notification got %s' % type(notification))
57 56
58 57 def create(self, created_by, subject, body, recipients=None,
59 58 type_=Notification.TYPE_MESSAGE, with_email=True,
60 59 email_kwargs={}):
61 60 """
62 61
63 62 Creates notification of given type
64 63
65 64 :param created_by: int, str or User instance. User who created this
66 65 notification
67 66 :param subject:
68 67 :param body:
69 68 :param recipients: list of int, str or User objects, when None
70 69 is given send to all admins
71 70 :param type_: type of notification
72 71 :param with_email: send email with this notification
73 72 :param email_kwargs: additional dict to pass as args to email template
74 73 """
75 74 from rhodecode.lib.celerylib import tasks, run_task
76 75
77 76 if recipients and not getattr(recipients, '__iter__', False):
78 77 raise Exception('recipients must be a list of iterable')
79 78
80 79 created_by_obj = self.__get_user(created_by)
81 80
82 81 if recipients:
83 82 recipients_objs = []
84 83 for u in recipients:
85 84 obj = self.__get_user(u)
86 85 if obj:
87 86 recipients_objs.append(obj)
88 87 recipients_objs = set(recipients_objs)
89 88 log.debug('sending notifications %s to %s' % (
90 89 type_, recipients_objs)
91 90 )
92 91 else:
93 92 # empty recipients means to all admins
94 93 recipients_objs = User.query().filter(User.admin == True).all()
95 94 log.debug('sending notifications %s to admins: %s' % (
96 95 type_, recipients_objs)
97 96 )
98 97 notif = Notification.create(
99 98 created_by=created_by_obj, subject=subject,
100 99 body=body, recipients=recipients_objs, type_=type_
101 100 )
102 101
103 102 if with_email is False:
104 103 return notif
105 104
106 105 #don't send email to person who created this comment
107 106 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
108 107
109 108 # send email with notification to all other participants
110 109 for rec in rec_objs:
111 110 email_subject = NotificationModel().make_description(notif, False)
112 111 type_ = type_
113 112 email_body = body
114 113 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
115 114 kwargs.update(email_kwargs)
116 115 email_body_html = EmailNotificationModel()\
117 116 .get_email_tmpl(type_, **kwargs)
118 117
119 118 run_task(tasks.send_email, rec.email, email_subject, email_body,
120 119 email_body_html)
121 120
122 121 return notif
123 122
124 123 def delete(self, user, notification):
125 124 # we don't want to remove actual notification just the assignment
126 125 try:
127 126 notification = self.__get_notification(notification)
128 127 user = self.__get_user(user)
129 128 if notification and user:
130 129 obj = UserNotification.query()\
131 130 .filter(UserNotification.user == user)\
132 131 .filter(UserNotification.notification
133 132 == notification)\
134 133 .one()
135 134 self.sa.delete(obj)
136 135 return True
137 136 except Exception:
138 137 log.error(traceback.format_exc())
139 138 raise
140 139
141 140 def get_for_user(self, user):
142 141 user = self.__get_user(user)
143 142 return user.notifications
144 143
145 144 def mark_all_read_for_user(self, user):
146 145 user = self.__get_user(user)
147 146 UserNotification.query()\
148 147 .filter(UserNotification.read==False)\
149 148 .update({'read': True})
150 149
151 150 def get_unread_cnt_for_user(self, user):
152 151 user = self.__get_user(user)
153 152 return UserNotification.query()\
154 153 .filter(UserNotification.read == False)\
155 154 .filter(UserNotification.user == user).count()
156 155
157 156 def get_unread_for_user(self, user):
158 157 user = self.__get_user(user)
159 158 return [x.notification for x in UserNotification.query()\
160 159 .filter(UserNotification.read == False)\
161 160 .filter(UserNotification.user == user).all()]
162 161
163 162 def get_user_notification(self, user, notification):
164 163 user = self.__get_user(user)
165 164 notification = self.__get_notification(notification)
166 165
167 166 return UserNotification.query()\
168 167 .filter(UserNotification.notification == notification)\
169 168 .filter(UserNotification.user == user).scalar()
170 169
171 170 def make_description(self, notification, show_age=True):
172 171 """
173 172 Creates a human readable description based on properties
174 173 of notification object
175 174 """
176 175
177 176 _map = {
178 177 notification.TYPE_CHANGESET_COMMENT: _('commented on commit'),
179 178 notification.TYPE_MESSAGE: _('sent message'),
180 179 notification.TYPE_MENTION: _('mentioned you'),
181 180 notification.TYPE_REGISTRATION: _('registered in RhodeCode')
182 181 }
183 182
184 tmpl = "%(user)s %(action)s %(when)s"
183 # action == _map string
184 tmpl = "%(user)s %(action)s at %(when)s"
185 185 if show_age:
186 186 when = h.age(notification.created_on)
187 187 else:
188 DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
189 when = DTF(notification.created_on)
188 when = h.fmt_date(notification.created_on)
190 189
191 190 data = dict(
192 191 user=notification.created_by_user.username,
193 192 action=_map[notification.type_], when=when,
194 193 )
195 194 return tmpl % data
196 195
197 196
198 197 class EmailNotificationModel(BaseModel):
199 198
200 199 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
201 200 TYPE_PASSWORD_RESET = 'passoword_link'
202 201 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
203 202 TYPE_DEFAULT = 'default'
204 203
205 204 def __init__(self):
206 205 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
207 206 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
208 207
209 208 self.email_types = {
210 209 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
211 210 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
212 211 self.TYPE_REGISTRATION: 'email_templates/registration.html',
213 212 self.TYPE_DEFAULT: 'email_templates/default.html'
214 213 }
215 214
216 215 def get_email_tmpl(self, type_, **kwargs):
217 216 """
218 217 return generated template for email based on given type
219 218
220 219 :param type_:
221 220 """
222 221
223 222 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
224 223 email_template = self._tmpl_lookup.get_template(base)
225 224 # translator inject
226 225 _kwargs = {'_': _}
227 226 _kwargs.update(kwargs)
228 227 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
229 228 return email_template.render(**_kwargs)
General Comments 0
You need to be logged in to leave comments. Login now