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