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