##// END OF EJS Templates
flake8: fix E202 whitespace before ')'
Mads Kiilerich -
r7725:e7683417 default
parent child Browse files
Show More
@@ -1,1306 +1,1306 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Helper functions
15 Helper functions
16
16
17 Consists of functions to typically be used within templates, but also
17 Consists of functions to typically be used within templates, but also
18 available to Controllers. This module is available to both as 'h'.
18 available to Controllers. This module is available to both as 'h'.
19 """
19 """
20 import hashlib
20 import hashlib
21 import json
21 import json
22 import logging
22 import logging
23 import random
23 import random
24 import re
24 import re
25 import StringIO
25 import StringIO
26 import textwrap
26 import textwrap
27 import urlparse
27 import urlparse
28
28
29 from beaker.cache import cache_region
29 from beaker.cache import cache_region
30 from pygments import highlight as code_highlight
30 from pygments import highlight as code_highlight
31 from pygments.formatters.html import HtmlFormatter
31 from pygments.formatters.html import HtmlFormatter
32 from tg.i18n import ugettext as _
32 from tg.i18n import ugettext as _
33 from webhelpers2.html import HTML, escape, literal
33 from webhelpers2.html import HTML, escape, literal
34 from webhelpers2.html.tags import NotGiven, Option, Options, _input, _make_safe_id_component, checkbox, end_form
34 from webhelpers2.html.tags import NotGiven, Option, Options, _input, _make_safe_id_component, checkbox, end_form
35 from webhelpers2.html.tags import form as insecure_form
35 from webhelpers2.html.tags import form as insecure_form
36 from webhelpers2.html.tags import hidden, link_to, password, radio
36 from webhelpers2.html.tags import hidden, link_to, password, radio
37 from webhelpers2.html.tags import select as webhelpers2_select
37 from webhelpers2.html.tags import select as webhelpers2_select
38 from webhelpers2.html.tags import submit, text, textarea
38 from webhelpers2.html.tags import submit, text, textarea
39 from webhelpers2.number import format_byte_size
39 from webhelpers2.number import format_byte_size
40 from webhelpers2.text import chop_at, truncate, wrap_paragraphs
40 from webhelpers2.text import chop_at, truncate, wrap_paragraphs
41 from webhelpers.pylonslib import Flash as _Flash
41 from webhelpers.pylonslib import Flash as _Flash
42
42
43 from kallithea.config.routing import url
43 from kallithea.config.routing import url
44 from kallithea.lib.annotate import annotate_highlight
44 from kallithea.lib.annotate import annotate_highlight
45 #==============================================================================
45 #==============================================================================
46 # PERMS
46 # PERMS
47 #==============================================================================
47 #==============================================================================
48 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoPermissionLevel
48 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoPermissionLevel
49 from kallithea.lib.markup_renderer import url_re
49 from kallithea.lib.markup_renderer import url_re
50 from kallithea.lib.pygmentsutils import get_custom_lexer
50 from kallithea.lib.pygmentsutils import get_custom_lexer
51 from kallithea.lib.utils2 import MENTIONS_REGEX, AttributeDict
51 from kallithea.lib.utils2 import MENTIONS_REGEX, AttributeDict
52 from kallithea.lib.utils2 import age as _age
52 from kallithea.lib.utils2 import age as _age
53 from kallithea.lib.utils2 import credentials_filter, safe_int, safe_str, safe_unicode, str2bool, time_to_datetime
53 from kallithea.lib.utils2 import credentials_filter, safe_int, safe_str, safe_unicode, str2bool, time_to_datetime
54 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
54 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
55 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
55 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
56 #==============================================================================
56 #==============================================================================
57 # SCM FILTERS available via h.
57 # SCM FILTERS available via h.
58 #==============================================================================
58 #==============================================================================
59 from kallithea.lib.vcs.utils import author_email, author_name
59 from kallithea.lib.vcs.utils import author_email, author_name
60
60
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 def canonical_url(*args, **kargs):
65 def canonical_url(*args, **kargs):
66 '''Like url(x, qualified=True), but returns url that not only is qualified
66 '''Like url(x, qualified=True), but returns url that not only is qualified
67 but also canonical, as configured in canonical_url'''
67 but also canonical, as configured in canonical_url'''
68 from kallithea import CONFIG
68 from kallithea import CONFIG
69 try:
69 try:
70 parts = CONFIG.get('canonical_url', '').split('://', 1)
70 parts = CONFIG.get('canonical_url', '').split('://', 1)
71 kargs['host'] = parts[1]
71 kargs['host'] = parts[1]
72 kargs['protocol'] = parts[0]
72 kargs['protocol'] = parts[0]
73 except IndexError:
73 except IndexError:
74 kargs['qualified'] = True
74 kargs['qualified'] = True
75 return url(*args, **kargs)
75 return url(*args, **kargs)
76
76
77
77
78 def canonical_hostname():
78 def canonical_hostname():
79 '''Return canonical hostname of system'''
79 '''Return canonical hostname of system'''
80 from kallithea import CONFIG
80 from kallithea import CONFIG
81 try:
81 try:
82 parts = CONFIG.get('canonical_url', '').split('://', 1)
82 parts = CONFIG.get('canonical_url', '').split('://', 1)
83 return parts[1].split('/', 1)[0]
83 return parts[1].split('/', 1)[0]
84 except IndexError:
84 except IndexError:
85 parts = url('home', qualified=True).split('://', 1)
85 parts = url('home', qualified=True).split('://', 1)
86 return parts[1].split('/', 1)[0]
86 return parts[1].split('/', 1)[0]
87
87
88
88
89 def html_escape(s):
89 def html_escape(s):
90 """Return string with all html escaped.
90 """Return string with all html escaped.
91 This is also safe for javascript in html but not necessarily correct.
91 This is also safe for javascript in html but not necessarily correct.
92 """
92 """
93 return (s
93 return (s
94 .replace('&', '&amp;')
94 .replace('&', '&amp;')
95 .replace(">", "&gt;")
95 .replace(">", "&gt;")
96 .replace("<", "&lt;")
96 .replace("<", "&lt;")
97 .replace('"', "&quot;")
97 .replace('"', "&quot;")
98 .replace("'", "&apos;") # Note: this is HTML5 not HTML4 and might not work in mails
98 .replace("'", "&apos;") # Note: this is HTML5 not HTML4 and might not work in mails
99 )
99 )
100
100
101 def js(value):
101 def js(value):
102 """Convert Python value to the corresponding JavaScript representation.
102 """Convert Python value to the corresponding JavaScript representation.
103
103
104 This is necessary to safely insert arbitrary values into HTML <script>
104 This is necessary to safely insert arbitrary values into HTML <script>
105 sections e.g. using Mako template expression substitution.
105 sections e.g. using Mako template expression substitution.
106
106
107 Note: Rather than using this function, it's preferable to avoid the
107 Note: Rather than using this function, it's preferable to avoid the
108 insertion of values into HTML <script> sections altogether. Instead,
108 insertion of values into HTML <script> sections altogether. Instead,
109 data should (to the extent possible) be passed to JavaScript using
109 data should (to the extent possible) be passed to JavaScript using
110 data attributes or AJAX calls, eliminating the need for JS specific
110 data attributes or AJAX calls, eliminating the need for JS specific
111 escaping.
111 escaping.
112
112
113 Note: This is not safe for use in attributes (e.g. onclick), because
113 Note: This is not safe for use in attributes (e.g. onclick), because
114 quotes are not escaped.
114 quotes are not escaped.
115
115
116 Because the rules for parsing <script> varies between XHTML (where
116 Because the rules for parsing <script> varies between XHTML (where
117 normal rules apply for any special characters) and HTML (where
117 normal rules apply for any special characters) and HTML (where
118 entities are not interpreted, but the literal string "</script>"
118 entities are not interpreted, but the literal string "</script>"
119 is forbidden), the function ensures that the result never contains
119 is forbidden), the function ensures that the result never contains
120 '&', '<' and '>', thus making it safe in both those contexts (but
120 '&', '<' and '>', thus making it safe in both those contexts (but
121 not in attributes).
121 not in attributes).
122 """
122 """
123 return literal(
123 return literal(
124 ('(' + json.dumps(value) + ')')
124 ('(' + json.dumps(value) + ')')
125 # In JSON, the following can only appear in string literals.
125 # In JSON, the following can only appear in string literals.
126 .replace('&', r'\x26')
126 .replace('&', r'\x26')
127 .replace('<', r'\x3c')
127 .replace('<', r'\x3c')
128 .replace('>', r'\x3e')
128 .replace('>', r'\x3e')
129 )
129 )
130
130
131
131
132 def jshtml(val):
132 def jshtml(val):
133 """HTML escapes a string value, then converts the resulting string
133 """HTML escapes a string value, then converts the resulting string
134 to its corresponding JavaScript representation (see `js`).
134 to its corresponding JavaScript representation (see `js`).
135
135
136 This is used when a plain-text string (possibly containing special
136 This is used when a plain-text string (possibly containing special
137 HTML characters) will be used by a script in an HTML context (e.g.
137 HTML characters) will be used by a script in an HTML context (e.g.
138 element.innerHTML or jQuery's 'html' method).
138 element.innerHTML or jQuery's 'html' method).
139
139
140 If in doubt, err on the side of using `jshtml` over `js`, since it's
140 If in doubt, err on the side of using `jshtml` over `js`, since it's
141 better to escape too much than too little.
141 better to escape too much than too little.
142 """
142 """
143 return js(escape(val))
143 return js(escape(val))
144
144
145
145
146 def shorter(s, size=20, firstline=False, postfix='...'):
146 def shorter(s, size=20, firstline=False, postfix='...'):
147 """Truncate s to size, including the postfix string if truncating.
147 """Truncate s to size, including the postfix string if truncating.
148 If firstline, truncate at newline.
148 If firstline, truncate at newline.
149 """
149 """
150 if firstline:
150 if firstline:
151 s = s.split('\n', 1)[0].rstrip()
151 s = s.split('\n', 1)[0].rstrip()
152 if len(s) > size:
152 if len(s) > size:
153 return s[:size - len(postfix)] + postfix
153 return s[:size - len(postfix)] + postfix
154 return s
154 return s
155
155
156
156
157 def reset(name, value, id=NotGiven, **attrs):
157 def reset(name, value, id=NotGiven, **attrs):
158 """Create a reset button, similar to webhelpers2.html.tags.submit ."""
158 """Create a reset button, similar to webhelpers2.html.tags.submit ."""
159 return _input("reset", name, value, id, attrs)
159 return _input("reset", name, value, id, attrs)
160
160
161
161
162 def select(name, selected_values, options, id=NotGiven, **attrs):
162 def select(name, selected_values, options, id=NotGiven, **attrs):
163 """Convenient wrapper of webhelpers2 to let it accept options as a tuple list"""
163 """Convenient wrapper of webhelpers2 to let it accept options as a tuple list"""
164 if isinstance(options, list):
164 if isinstance(options, list):
165 l = []
165 l = []
166 for x in options:
166 for x in options:
167 try:
167 try:
168 value, label = x
168 value, label = x
169 except ValueError: # too many values to unpack
169 except ValueError: # too many values to unpack
170 if isinstance(x, basestring):
170 if isinstance(x, basestring):
171 value = label = x
171 value = label = x
172 else:
172 else:
173 log.error('invalid select option %r', x)
173 log.error('invalid select option %r', x)
174 raise
174 raise
175 l.append(Option(label, value))
175 l.append(Option(label, value))
176 options = Options(l)
176 options = Options(l)
177 return webhelpers2_select(name, selected_values, options, id=id, **attrs)
177 return webhelpers2_select(name, selected_values, options, id=id, **attrs)
178
178
179
179
180 safeid = _make_safe_id_component
180 safeid = _make_safe_id_component
181
181
182
182
183 def FID(raw_id, path):
183 def FID(raw_id, path):
184 """
184 """
185 Creates a unique ID for filenode based on it's hash of path and revision
185 Creates a unique ID for filenode based on it's hash of path and revision
186 it's safe to use in urls
186 it's safe to use in urls
187
187
188 :param raw_id:
188 :param raw_id:
189 :param path:
189 :param path:
190 """
190 """
191
191
192 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
192 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
193
193
194
194
195 class _FilesBreadCrumbs(object):
195 class _FilesBreadCrumbs(object):
196
196
197 def __call__(self, repo_name, rev, paths):
197 def __call__(self, repo_name, rev, paths):
198 if isinstance(paths, str):
198 if isinstance(paths, str):
199 paths = safe_unicode(paths)
199 paths = safe_unicode(paths)
200 url_l = [link_to(repo_name, url('files_home',
200 url_l = [link_to(repo_name, url('files_home',
201 repo_name=repo_name,
201 repo_name=repo_name,
202 revision=rev, f_path=''),
202 revision=rev, f_path=''),
203 class_='ypjax-link')]
203 class_='ypjax-link')]
204 paths_l = paths.split('/')
204 paths_l = paths.split('/')
205 for cnt, p in enumerate(paths_l):
205 for cnt, p in enumerate(paths_l):
206 if p != '':
206 if p != '':
207 url_l.append(link_to(p,
207 url_l.append(link_to(p,
208 url('files_home',
208 url('files_home',
209 repo_name=repo_name,
209 repo_name=repo_name,
210 revision=rev,
210 revision=rev,
211 f_path='/'.join(paths_l[:cnt + 1])
211 f_path='/'.join(paths_l[:cnt + 1])
212 ),
212 ),
213 class_='ypjax-link'
213 class_='ypjax-link'
214 )
214 )
215 )
215 )
216
216
217 return literal('/'.join(url_l))
217 return literal('/'.join(url_l))
218
218
219
219
220 files_breadcrumbs = _FilesBreadCrumbs()
220 files_breadcrumbs = _FilesBreadCrumbs()
221
221
222
222
223 class CodeHtmlFormatter(HtmlFormatter):
223 class CodeHtmlFormatter(HtmlFormatter):
224 """
224 """
225 My code Html Formatter for source codes
225 My code Html Formatter for source codes
226 """
226 """
227
227
228 def wrap(self, source, outfile):
228 def wrap(self, source, outfile):
229 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
229 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
230
230
231 def _wrap_code(self, source):
231 def _wrap_code(self, source):
232 for cnt, it in enumerate(source):
232 for cnt, it in enumerate(source):
233 i, t = it
233 i, t = it
234 t = '<span id="L%s">%s</span>' % (cnt + 1, t)
234 t = '<span id="L%s">%s</span>' % (cnt + 1, t)
235 yield i, t
235 yield i, t
236
236
237 def _wrap_tablelinenos(self, inner):
237 def _wrap_tablelinenos(self, inner):
238 dummyoutfile = StringIO.StringIO()
238 dummyoutfile = StringIO.StringIO()
239 lncount = 0
239 lncount = 0
240 for t, line in inner:
240 for t, line in inner:
241 if t:
241 if t:
242 lncount += 1
242 lncount += 1
243 dummyoutfile.write(line)
243 dummyoutfile.write(line)
244
244
245 fl = self.linenostart
245 fl = self.linenostart
246 mw = len(str(lncount + fl - 1))
246 mw = len(str(lncount + fl - 1))
247 sp = self.linenospecial
247 sp = self.linenospecial
248 st = self.linenostep
248 st = self.linenostep
249 la = self.lineanchors
249 la = self.lineanchors
250 aln = self.anchorlinenos
250 aln = self.anchorlinenos
251 nocls = self.noclasses
251 nocls = self.noclasses
252 if sp:
252 if sp:
253 lines = []
253 lines = []
254
254
255 for i in range(fl, fl + lncount):
255 for i in range(fl, fl + lncount):
256 if i % st == 0:
256 if i % st == 0:
257 if i % sp == 0:
257 if i % sp == 0:
258 if aln:
258 if aln:
259 lines.append('<a href="#%s%d" class="special">%*d</a>' %
259 lines.append('<a href="#%s%d" class="special">%*d</a>' %
260 (la, i, mw, i))
260 (la, i, mw, i))
261 else:
261 else:
262 lines.append('<span class="special">%*d</span>' % (mw, i))
262 lines.append('<span class="special">%*d</span>' % (mw, i))
263 else:
263 else:
264 if aln:
264 if aln:
265 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
265 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
266 else:
266 else:
267 lines.append('%*d' % (mw, i))
267 lines.append('%*d' % (mw, i))
268 else:
268 else:
269 lines.append('')
269 lines.append('')
270 ls = '\n'.join(lines)
270 ls = '\n'.join(lines)
271 else:
271 else:
272 lines = []
272 lines = []
273 for i in range(fl, fl + lncount):
273 for i in range(fl, fl + lncount):
274 if i % st == 0:
274 if i % st == 0:
275 if aln:
275 if aln:
276 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
276 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
277 else:
277 else:
278 lines.append('%*d' % (mw, i))
278 lines.append('%*d' % (mw, i))
279 else:
279 else:
280 lines.append('')
280 lines.append('')
281 ls = '\n'.join(lines)
281 ls = '\n'.join(lines)
282
282
283 # in case you wonder about the seemingly redundant <div> here: since the
283 # in case you wonder about the seemingly redundant <div> here: since the
284 # content in the other cell also is wrapped in a div, some browsers in
284 # content in the other cell also is wrapped in a div, some browsers in
285 # some configurations seem to mess up the formatting...
285 # some configurations seem to mess up the formatting...
286 if nocls:
286 if nocls:
287 yield 0, ('<table class="%stable">' % self.cssclass +
287 yield 0, ('<table class="%stable">' % self.cssclass +
288 '<tr><td><div class="linenodiv">'
288 '<tr><td><div class="linenodiv">'
289 '<pre>' + ls + '</pre></div></td>'
289 '<pre>' + ls + '</pre></div></td>'
290 '<td id="hlcode" class="code">')
290 '<td id="hlcode" class="code">')
291 else:
291 else:
292 yield 0, ('<table class="%stable">' % self.cssclass +
292 yield 0, ('<table class="%stable">' % self.cssclass +
293 '<tr><td class="linenos"><div class="linenodiv">'
293 '<tr><td class="linenos"><div class="linenodiv">'
294 '<pre>' + ls + '</pre></div></td>'
294 '<pre>' + ls + '</pre></div></td>'
295 '<td id="hlcode" class="code">')
295 '<td id="hlcode" class="code">')
296 yield 0, dummyoutfile.getvalue()
296 yield 0, dummyoutfile.getvalue()
297 yield 0, '</td></tr></table>'
297 yield 0, '</td></tr></table>'
298
298
299
299
300 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
300 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
301
301
302
302
303 def _markup_whitespace(m):
303 def _markup_whitespace(m):
304 groups = m.groups()
304 groups = m.groups()
305 if groups[0]:
305 if groups[0]:
306 return '<u>\t</u>'
306 return '<u>\t</u>'
307 if groups[1]:
307 if groups[1]:
308 return ' <i></i>'
308 return ' <i></i>'
309
309
310
310
311 def markup_whitespace(s):
311 def markup_whitespace(s):
312 return _whitespace_re.sub(_markup_whitespace, s)
312 return _whitespace_re.sub(_markup_whitespace, s)
313
313
314
314
315 def pygmentize(filenode, **kwargs):
315 def pygmentize(filenode, **kwargs):
316 """
316 """
317 pygmentize function using pygments
317 pygmentize function using pygments
318
318
319 :param filenode:
319 :param filenode:
320 """
320 """
321 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
321 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
322 return literal(markup_whitespace(
322 return literal(markup_whitespace(
323 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
323 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
324
324
325
325
326 def pygmentize_annotation(repo_name, filenode, **kwargs):
326 def pygmentize_annotation(repo_name, filenode, **kwargs):
327 """
327 """
328 pygmentize function for annotation
328 pygmentize function for annotation
329
329
330 :param filenode:
330 :param filenode:
331 """
331 """
332
332
333 color_dict = {}
333 color_dict = {}
334
334
335 def gen_color(n=10000):
335 def gen_color(n=10000):
336 """generator for getting n of evenly distributed colors using
336 """generator for getting n of evenly distributed colors using
337 hsv color and golden ratio. It always return same order of colors
337 hsv color and golden ratio. It always return same order of colors
338
338
339 :returns: RGB tuple
339 :returns: RGB tuple
340 """
340 """
341
341
342 def hsv_to_rgb(h, s, v):
342 def hsv_to_rgb(h, s, v):
343 if s == 0.0:
343 if s == 0.0:
344 return v, v, v
344 return v, v, v
345 i = int(h * 6.0) # XXX assume int() truncates!
345 i = int(h * 6.0) # XXX assume int() truncates!
346 f = (h * 6.0) - i
346 f = (h * 6.0) - i
347 p = v * (1.0 - s)
347 p = v * (1.0 - s)
348 q = v * (1.0 - s * f)
348 q = v * (1.0 - s * f)
349 t = v * (1.0 - s * (1.0 - f))
349 t = v * (1.0 - s * (1.0 - f))
350 i = i % 6
350 i = i % 6
351 if i == 0:
351 if i == 0:
352 return v, t, p
352 return v, t, p
353 if i == 1:
353 if i == 1:
354 return q, v, p
354 return q, v, p
355 if i == 2:
355 if i == 2:
356 return p, v, t
356 return p, v, t
357 if i == 3:
357 if i == 3:
358 return p, q, v
358 return p, q, v
359 if i == 4:
359 if i == 4:
360 return t, p, v
360 return t, p, v
361 if i == 5:
361 if i == 5:
362 return v, p, q
362 return v, p, q
363
363
364 golden_ratio = 0.618033988749895
364 golden_ratio = 0.618033988749895
365 h = 0.22717784590367374
365 h = 0.22717784590367374
366
366
367 for _unused in xrange(n):
367 for _unused in xrange(n):
368 h += golden_ratio
368 h += golden_ratio
369 h %= 1
369 h %= 1
370 HSV_tuple = [h, 0.95, 0.95]
370 HSV_tuple = [h, 0.95, 0.95]
371 RGB_tuple = hsv_to_rgb(*HSV_tuple)
371 RGB_tuple = hsv_to_rgb(*HSV_tuple)
372 yield map(lambda x: str(int(x * 256)), RGB_tuple)
372 yield map(lambda x: str(int(x * 256)), RGB_tuple)
373
373
374 cgenerator = gen_color()
374 cgenerator = gen_color()
375
375
376 def get_color_string(cs):
376 def get_color_string(cs):
377 if cs in color_dict:
377 if cs in color_dict:
378 col = color_dict[cs]
378 col = color_dict[cs]
379 else:
379 else:
380 col = color_dict[cs] = cgenerator.next()
380 col = color_dict[cs] = cgenerator.next()
381 return "color: rgb(%s)! important;" % (', '.join(col))
381 return "color: rgb(%s)! important;" % (', '.join(col))
382
382
383 def url_func(repo_name):
383 def url_func(repo_name):
384
384
385 def _url_func(changeset):
385 def _url_func(changeset):
386 author = escape(changeset.author)
386 author = escape(changeset.author)
387 date = changeset.date
387 date = changeset.date
388 message = escape(changeset.message)
388 message = escape(changeset.message)
389 tooltip_html = ("<b>Author:</b> %s<br/>"
389 tooltip_html = ("<b>Author:</b> %s<br/>"
390 "<b>Date:</b> %s</b><br/>"
390 "<b>Date:</b> %s</b><br/>"
391 "<b>Message:</b> %s") % (author, date, message)
391 "<b>Message:</b> %s") % (author, date, message)
392
392
393 lnk_format = show_id(changeset)
393 lnk_format = show_id(changeset)
394 uri = link_to(
394 uri = link_to(
395 lnk_format,
395 lnk_format,
396 url('changeset_home', repo_name=repo_name,
396 url('changeset_home', repo_name=repo_name,
397 revision=changeset.raw_id),
397 revision=changeset.raw_id),
398 style=get_color_string(changeset.raw_id),
398 style=get_color_string(changeset.raw_id),
399 **{'data-toggle': 'popover',
399 **{'data-toggle': 'popover',
400 'data-content': tooltip_html}
400 'data-content': tooltip_html}
401 )
401 )
402
402
403 uri += '\n'
403 uri += '\n'
404 return uri
404 return uri
405 return _url_func
405 return _url_func
406
406
407 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
407 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
408
408
409
409
410 class _Message(object):
410 class _Message(object):
411 """A message returned by ``Flash.pop_messages()``.
411 """A message returned by ``Flash.pop_messages()``.
412
412
413 Converting the message to a string returns the message text. Instances
413 Converting the message to a string returns the message text. Instances
414 also have the following attributes:
414 also have the following attributes:
415
415
416 * ``message``: the message text.
416 * ``message``: the message text.
417 * ``category``: the category specified when the message was created.
417 * ``category``: the category specified when the message was created.
418 """
418 """
419
419
420 def __init__(self, category, message):
420 def __init__(self, category, message):
421 self.category = category
421 self.category = category
422 self.message = message
422 self.message = message
423
423
424 def __str__(self):
424 def __str__(self):
425 return self.message
425 return self.message
426
426
427 __unicode__ = __str__
427 __unicode__ = __str__
428
428
429 def __html__(self):
429 def __html__(self):
430 return escape(safe_unicode(self.message))
430 return escape(safe_unicode(self.message))
431
431
432
432
433 class Flash(_Flash):
433 class Flash(_Flash):
434
434
435 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
435 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
436 """
436 """
437 Show a message to the user _and_ log it through the specified function
437 Show a message to the user _and_ log it through the specified function
438
438
439 category: notice (default), warning, error, success
439 category: notice (default), warning, error, success
440 logf: a custom log function - such as log.debug
440 logf: a custom log function - such as log.debug
441
441
442 logf defaults to log.info, unless category equals 'success', in which
442 logf defaults to log.info, unless category equals 'success', in which
443 case logf defaults to log.debug.
443 case logf defaults to log.debug.
444 """
444 """
445 if logf is None:
445 if logf is None:
446 logf = log.info
446 logf = log.info
447 if category == 'success':
447 if category == 'success':
448 logf = log.debug
448 logf = log.debug
449
449
450 logf('Flash %s: %s', category, message)
450 logf('Flash %s: %s', category, message)
451
451
452 super(Flash, self).__call__(message, category, ignore_duplicate)
452 super(Flash, self).__call__(message, category, ignore_duplicate)
453
453
454 def pop_messages(self):
454 def pop_messages(self):
455 """Return all accumulated messages and delete them from the session.
455 """Return all accumulated messages and delete them from the session.
456
456
457 The return value is a list of ``Message`` objects.
457 The return value is a list of ``Message`` objects.
458 """
458 """
459 from tg import session
459 from tg import session
460 messages = session.pop(self.session_key, [])
460 messages = session.pop(self.session_key, [])
461 session.save()
461 session.save()
462 return [_Message(*m) for m in messages]
462 return [_Message(*m) for m in messages]
463
463
464
464
465 flash = Flash()
465 flash = Flash()
466
466
467
467
468 age = lambda x, y=False: _age(x, y)
468 age = lambda x, y=False: _age(x, y)
469 capitalize = lambda x: x.capitalize()
469 capitalize = lambda x: x.capitalize()
470 email = author_email
470 email = author_email
471 short_id = lambda x: x[:12]
471 short_id = lambda x: x[:12]
472 hide_credentials = lambda x: ''.join(credentials_filter(x))
472 hide_credentials = lambda x: ''.join(credentials_filter(x))
473
473
474
474
475 def show_id(cs):
475 def show_id(cs):
476 """
476 """
477 Configurable function that shows ID
477 Configurable function that shows ID
478 by default it's r123:fffeeefffeee
478 by default it's r123:fffeeefffeee
479
479
480 :param cs: changeset instance
480 :param cs: changeset instance
481 """
481 """
482 from kallithea import CONFIG
482 from kallithea import CONFIG
483 def_len = safe_int(CONFIG.get('show_sha_length', 12))
483 def_len = safe_int(CONFIG.get('show_sha_length', 12))
484 show_rev = str2bool(CONFIG.get('show_revision_number', False))
484 show_rev = str2bool(CONFIG.get('show_revision_number', False))
485
485
486 raw_id = cs.raw_id[:def_len]
486 raw_id = cs.raw_id[:def_len]
487 if show_rev:
487 if show_rev:
488 return 'r%s:%s' % (cs.revision, raw_id)
488 return 'r%s:%s' % (cs.revision, raw_id)
489 else:
489 else:
490 return raw_id
490 return raw_id
491
491
492
492
493 def fmt_date(date):
493 def fmt_date(date):
494 if date:
494 if date:
495 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf-8')
495 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf-8')
496
496
497 return ""
497 return ""
498
498
499
499
500 def is_git(repository):
500 def is_git(repository):
501 if hasattr(repository, 'alias'):
501 if hasattr(repository, 'alias'):
502 _type = repository.alias
502 _type = repository.alias
503 elif hasattr(repository, 'repo_type'):
503 elif hasattr(repository, 'repo_type'):
504 _type = repository.repo_type
504 _type = repository.repo_type
505 else:
505 else:
506 _type = repository
506 _type = repository
507 return _type == 'git'
507 return _type == 'git'
508
508
509
509
510 def is_hg(repository):
510 def is_hg(repository):
511 if hasattr(repository, 'alias'):
511 if hasattr(repository, 'alias'):
512 _type = repository.alias
512 _type = repository.alias
513 elif hasattr(repository, 'repo_type'):
513 elif hasattr(repository, 'repo_type'):
514 _type = repository.repo_type
514 _type = repository.repo_type
515 else:
515 else:
516 _type = repository
516 _type = repository
517 return _type == 'hg'
517 return _type == 'hg'
518
518
519
519
520 @cache_region('long_term', 'user_or_none')
520 @cache_region('long_term', 'user_or_none')
521 def user_or_none(author):
521 def user_or_none(author):
522 """Try to match email part of VCS committer string with a local user - or return None"""
522 """Try to match email part of VCS committer string with a local user - or return None"""
523 from kallithea.model.db import User
523 from kallithea.model.db import User
524 email = author_email(author)
524 email = author_email(author)
525 if email:
525 if email:
526 return User.get_by_email(email, cache=True) # cache will only use sql_cache_short
526 return User.get_by_email(email, cache=True) # cache will only use sql_cache_short
527 return None
527 return None
528
528
529
529
530 def email_or_none(author):
530 def email_or_none(author):
531 """Try to match email part of VCS committer string with a local user.
531 """Try to match email part of VCS committer string with a local user.
532 Return primary email of user, email part of the specified author name, or None."""
532 Return primary email of user, email part of the specified author name, or None."""
533 if not author:
533 if not author:
534 return None
534 return None
535 user = user_or_none(author)
535 user = user_or_none(author)
536 if user is not None:
536 if user is not None:
537 return user.email # always use main email address - not necessarily the one used to find user
537 return user.email # always use main email address - not necessarily the one used to find user
538
538
539 # extract email from the commit string
539 # extract email from the commit string
540 email = author_email(author)
540 email = author_email(author)
541 if email:
541 if email:
542 return email
542 return email
543
543
544 # No valid email, not a valid user in the system, none!
544 # No valid email, not a valid user in the system, none!
545 return None
545 return None
546
546
547
547
548 def person(author, show_attr="username"):
548 def person(author, show_attr="username"):
549 """Find the user identified by 'author', return one of the users attributes,
549 """Find the user identified by 'author', return one of the users attributes,
550 default to the username attribute, None if there is no user"""
550 default to the username attribute, None if there is no user"""
551 from kallithea.model.db import User
551 from kallithea.model.db import User
552 # attr to return from fetched user
552 # attr to return from fetched user
553 person_getter = lambda usr: getattr(usr, show_attr)
553 person_getter = lambda usr: getattr(usr, show_attr)
554
554
555 # if author is already an instance use it for extraction
555 # if author is already an instance use it for extraction
556 if isinstance(author, User):
556 if isinstance(author, User):
557 return person_getter(author)
557 return person_getter(author)
558
558
559 user = user_or_none(author)
559 user = user_or_none(author)
560 if user is not None:
560 if user is not None:
561 return person_getter(user)
561 return person_getter(user)
562
562
563 # Still nothing? Just pass back the author name if any, else the email
563 # Still nothing? Just pass back the author name if any, else the email
564 return author_name(author) or email(author)
564 return author_name(author) or email(author)
565
565
566
566
567 def person_by_id(id_, show_attr="username"):
567 def person_by_id(id_, show_attr="username"):
568 from kallithea.model.db import User
568 from kallithea.model.db import User
569 # attr to return from fetched user
569 # attr to return from fetched user
570 person_getter = lambda usr: getattr(usr, show_attr)
570 person_getter = lambda usr: getattr(usr, show_attr)
571
571
572 # maybe it's an ID ?
572 # maybe it's an ID ?
573 if str(id_).isdigit() or isinstance(id_, int):
573 if str(id_).isdigit() or isinstance(id_, int):
574 id_ = int(id_)
574 id_ = int(id_)
575 user = User.get(id_)
575 user = User.get(id_)
576 if user is not None:
576 if user is not None:
577 return person_getter(user)
577 return person_getter(user)
578 return id_
578 return id_
579
579
580
580
581 def boolicon(value):
581 def boolicon(value):
582 """Returns boolean value of a value, represented as small html image of true/false
582 """Returns boolean value of a value, represented as small html image of true/false
583 icons
583 icons
584
584
585 :param value: value
585 :param value: value
586 """
586 """
587
587
588 if value:
588 if value:
589 return HTML.tag('i', class_="icon-ok")
589 return HTML.tag('i', class_="icon-ok")
590 else:
590 else:
591 return HTML.tag('i', class_="icon-minus-circled")
591 return HTML.tag('i', class_="icon-minus-circled")
592
592
593
593
594 def action_parser(user_log, feed=False, parse_cs=False):
594 def action_parser(user_log, feed=False, parse_cs=False):
595 """
595 """
596 This helper will action_map the specified string action into translated
596 This helper will action_map the specified string action into translated
597 fancy names with icons and links
597 fancy names with icons and links
598
598
599 :param user_log: user log instance
599 :param user_log: user log instance
600 :param feed: use output for feeds (no html and fancy icons)
600 :param feed: use output for feeds (no html and fancy icons)
601 :param parse_cs: parse Changesets into VCS instances
601 :param parse_cs: parse Changesets into VCS instances
602 """
602 """
603
603
604 action = user_log.action
604 action = user_log.action
605 action_params = ' '
605 action_params = ' '
606
606
607 x = action.split(':')
607 x = action.split(':')
608
608
609 if len(x) > 1:
609 if len(x) > 1:
610 action, action_params = x
610 action, action_params = x
611
611
612 def get_cs_links():
612 def get_cs_links():
613 revs_limit = 3 # display this amount always
613 revs_limit = 3 # display this amount always
614 revs_top_limit = 50 # show upto this amount of changesets hidden
614 revs_top_limit = 50 # show upto this amount of changesets hidden
615 revs_ids = action_params.split(',')
615 revs_ids = action_params.split(',')
616 deleted = user_log.repository is None
616 deleted = user_log.repository is None
617 if deleted:
617 if deleted:
618 return ','.join(revs_ids)
618 return ','.join(revs_ids)
619
619
620 repo_name = user_log.repository.repo_name
620 repo_name = user_log.repository.repo_name
621
621
622 def lnk(rev, repo_name):
622 def lnk(rev, repo_name):
623 lazy_cs = False
623 lazy_cs = False
624 title_ = None
624 title_ = None
625 url_ = '#'
625 url_ = '#'
626 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
626 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
627 if rev.op and rev.ref_name:
627 if rev.op and rev.ref_name:
628 if rev.op == 'delete_branch':
628 if rev.op == 'delete_branch':
629 lbl = _('Deleted branch: %s') % rev.ref_name
629 lbl = _('Deleted branch: %s') % rev.ref_name
630 elif rev.op == 'tag':
630 elif rev.op == 'tag':
631 lbl = _('Created tag: %s') % rev.ref_name
631 lbl = _('Created tag: %s') % rev.ref_name
632 else:
632 else:
633 lbl = 'Unknown operation %s' % rev.op
633 lbl = 'Unknown operation %s' % rev.op
634 else:
634 else:
635 lazy_cs = True
635 lazy_cs = True
636 lbl = rev.short_id[:8]
636 lbl = rev.short_id[:8]
637 url_ = url('changeset_home', repo_name=repo_name,
637 url_ = url('changeset_home', repo_name=repo_name,
638 revision=rev.raw_id)
638 revision=rev.raw_id)
639 else:
639 else:
640 # changeset cannot be found - it might have been stripped or removed
640 # changeset cannot be found - it might have been stripped or removed
641 lbl = rev[:12]
641 lbl = rev[:12]
642 title_ = _('Changeset %s not found') % lbl
642 title_ = _('Changeset %s not found') % lbl
643 if parse_cs:
643 if parse_cs:
644 return link_to(lbl, url_, title=title_, **{'data-toggle': 'tooltip'})
644 return link_to(lbl, url_, title=title_, **{'data-toggle': 'tooltip'})
645 return link_to(lbl, url_, class_='lazy-cs' if lazy_cs else '',
645 return link_to(lbl, url_, class_='lazy-cs' if lazy_cs else '',
646 **{'data-raw_id': rev.raw_id, 'data-repo_name': repo_name})
646 **{'data-raw_id': rev.raw_id, 'data-repo_name': repo_name})
647
647
648 def _get_op(rev_txt):
648 def _get_op(rev_txt):
649 _op = None
649 _op = None
650 _name = rev_txt
650 _name = rev_txt
651 if len(rev_txt.split('=>')) == 2:
651 if len(rev_txt.split('=>')) == 2:
652 _op, _name = rev_txt.split('=>')
652 _op, _name = rev_txt.split('=>')
653 return _op, _name
653 return _op, _name
654
654
655 revs = []
655 revs = []
656 if len(filter(lambda v: v != '', revs_ids)) > 0:
656 if len(filter(lambda v: v != '', revs_ids)) > 0:
657 repo = None
657 repo = None
658 for rev in revs_ids[:revs_top_limit]:
658 for rev in revs_ids[:revs_top_limit]:
659 _op, _name = _get_op(rev)
659 _op, _name = _get_op(rev)
660
660
661 # we want parsed changesets, or new log store format is bad
661 # we want parsed changesets, or new log store format is bad
662 if parse_cs:
662 if parse_cs:
663 try:
663 try:
664 if repo is None:
664 if repo is None:
665 repo = user_log.repository.scm_instance
665 repo = user_log.repository.scm_instance
666 _rev = repo.get_changeset(rev)
666 _rev = repo.get_changeset(rev)
667 revs.append(_rev)
667 revs.append(_rev)
668 except ChangesetDoesNotExistError:
668 except ChangesetDoesNotExistError:
669 log.error('cannot find revision %s in this repo', rev)
669 log.error('cannot find revision %s in this repo', rev)
670 revs.append(rev)
670 revs.append(rev)
671 else:
671 else:
672 _rev = AttributeDict({
672 _rev = AttributeDict({
673 'short_id': rev[:12],
673 'short_id': rev[:12],
674 'raw_id': rev,
674 'raw_id': rev,
675 'message': '',
675 'message': '',
676 'op': _op,
676 'op': _op,
677 'ref_name': _name
677 'ref_name': _name
678 })
678 })
679 revs.append(_rev)
679 revs.append(_rev)
680 cs_links = [" " + ', '.join(
680 cs_links = [" " + ', '.join(
681 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
681 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
682 )]
682 )]
683 _op1, _name1 = _get_op(revs_ids[0])
683 _op1, _name1 = _get_op(revs_ids[0])
684 _op2, _name2 = _get_op(revs_ids[-1])
684 _op2, _name2 = _get_op(revs_ids[-1])
685
685
686 _rev = '%s...%s' % (_name1, _name2)
686 _rev = '%s...%s' % (_name1, _name2)
687
687
688 compare_view = (
688 compare_view = (
689 ' <div class="compare_view" data-toggle="tooltip" title="%s">'
689 ' <div class="compare_view" data-toggle="tooltip" title="%s">'
690 '<a href="%s">%s</a> </div>' % (
690 '<a href="%s">%s</a> </div>' % (
691 _('Show all combined changesets %s->%s') % (
691 _('Show all combined changesets %s->%s') % (
692 revs_ids[0][:12], revs_ids[-1][:12]
692 revs_ids[0][:12], revs_ids[-1][:12]
693 ),
693 ),
694 url('changeset_home', repo_name=repo_name,
694 url('changeset_home', repo_name=repo_name,
695 revision=_rev
695 revision=_rev
696 ),
696 ),
697 _('Compare view')
697 _('Compare view')
698 )
698 )
699 )
699 )
700
700
701 # if we have exactly one more than normally displayed
701 # if we have exactly one more than normally displayed
702 # just display it, takes less space than displaying
702 # just display it, takes less space than displaying
703 # "and 1 more revisions"
703 # "and 1 more revisions"
704 if len(revs_ids) == revs_limit + 1:
704 if len(revs_ids) == revs_limit + 1:
705 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
705 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
706
706
707 # hidden-by-default ones
707 # hidden-by-default ones
708 if len(revs_ids) > revs_limit + 1:
708 if len(revs_ids) > revs_limit + 1:
709 uniq_id = revs_ids[0]
709 uniq_id = revs_ids[0]
710 html_tmpl = (
710 html_tmpl = (
711 '<span> %s <a class="show_more" id="_%s" '
711 '<span> %s <a class="show_more" id="_%s" '
712 'href="#more">%s</a> %s</span>'
712 'href="#more">%s</a> %s</span>'
713 )
713 )
714 if not feed:
714 if not feed:
715 cs_links.append(html_tmpl % (
715 cs_links.append(html_tmpl % (
716 _('and'),
716 _('and'),
717 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
717 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
718 _('revisions')
718 _('revisions')
719 )
719 )
720 )
720 )
721
721
722 if not feed:
722 if not feed:
723 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
723 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
724 else:
724 else:
725 html_tmpl = '<span id="%s"> %s </span>'
725 html_tmpl = '<span id="%s"> %s </span>'
726
726
727 morelinks = ', '.join(
727 morelinks = ', '.join(
728 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
728 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
729 )
729 )
730
730
731 if len(revs_ids) > revs_top_limit:
731 if len(revs_ids) > revs_top_limit:
732 morelinks += ', ...'
732 morelinks += ', ...'
733
733
734 cs_links.append(html_tmpl % (uniq_id, morelinks))
734 cs_links.append(html_tmpl % (uniq_id, morelinks))
735 if len(revs) > 1:
735 if len(revs) > 1:
736 cs_links.append(compare_view)
736 cs_links.append(compare_view)
737 return ''.join(cs_links)
737 return ''.join(cs_links)
738
738
739 def get_fork_name():
739 def get_fork_name():
740 repo_name = action_params
740 repo_name = action_params
741 url_ = url('summary_home', repo_name=repo_name)
741 url_ = url('summary_home', repo_name=repo_name)
742 return _('Fork name %s') % link_to(action_params, url_)
742 return _('Fork name %s') % link_to(action_params, url_)
743
743
744 def get_user_name():
744 def get_user_name():
745 user_name = action_params
745 user_name = action_params
746 return user_name
746 return user_name
747
747
748 def get_users_group():
748 def get_users_group():
749 group_name = action_params
749 group_name = action_params
750 return group_name
750 return group_name
751
751
752 def get_pull_request():
752 def get_pull_request():
753 from kallithea.model.db import PullRequest
753 from kallithea.model.db import PullRequest
754 pull_request_id = action_params
754 pull_request_id = action_params
755 nice_id = PullRequest.make_nice_id(pull_request_id)
755 nice_id = PullRequest.make_nice_id(pull_request_id)
756
756
757 deleted = user_log.repository is None
757 deleted = user_log.repository is None
758 if deleted:
758 if deleted:
759 repo_name = user_log.repository_name
759 repo_name = user_log.repository_name
760 else:
760 else:
761 repo_name = user_log.repository.repo_name
761 repo_name = user_log.repository.repo_name
762
762
763 return link_to(_('Pull request %s') % nice_id,
763 return link_to(_('Pull request %s') % nice_id,
764 url('pullrequest_show', repo_name=repo_name,
764 url('pullrequest_show', repo_name=repo_name,
765 pull_request_id=pull_request_id))
765 pull_request_id=pull_request_id))
766
766
767 def get_archive_name():
767 def get_archive_name():
768 archive_name = action_params
768 archive_name = action_params
769 return archive_name
769 return archive_name
770
770
771 # action : translated str, callback(extractor), icon
771 # action : translated str, callback(extractor), icon
772 action_map = {
772 action_map = {
773 'user_deleted_repo': (_('[deleted] repository'),
773 'user_deleted_repo': (_('[deleted] repository'),
774 None, 'icon-trashcan'),
774 None, 'icon-trashcan'),
775 'user_created_repo': (_('[created] repository'),
775 'user_created_repo': (_('[created] repository'),
776 None, 'icon-plus'),
776 None, 'icon-plus'),
777 'user_created_fork': (_('[created] repository as fork'),
777 'user_created_fork': (_('[created] repository as fork'),
778 None, 'icon-fork'),
778 None, 'icon-fork'),
779 'user_forked_repo': (_('[forked] repository'),
779 'user_forked_repo': (_('[forked] repository'),
780 get_fork_name, 'icon-fork'),
780 get_fork_name, 'icon-fork'),
781 'user_updated_repo': (_('[updated] repository'),
781 'user_updated_repo': (_('[updated] repository'),
782 None, 'icon-pencil'),
782 None, 'icon-pencil'),
783 'user_downloaded_archive': (_('[downloaded] archive from repository'),
783 'user_downloaded_archive': (_('[downloaded] archive from repository'),
784 get_archive_name, 'icon-download-cloud'),
784 get_archive_name, 'icon-download-cloud'),
785 'admin_deleted_repo': (_('[delete] repository'),
785 'admin_deleted_repo': (_('[delete] repository'),
786 None, 'icon-trashcan'),
786 None, 'icon-trashcan'),
787 'admin_created_repo': (_('[created] repository'),
787 'admin_created_repo': (_('[created] repository'),
788 None, 'icon-plus'),
788 None, 'icon-plus'),
789 'admin_forked_repo': (_('[forked] repository'),
789 'admin_forked_repo': (_('[forked] repository'),
790 None, 'icon-fork'),
790 None, 'icon-fork'),
791 'admin_updated_repo': (_('[updated] repository'),
791 'admin_updated_repo': (_('[updated] repository'),
792 None, 'icon-pencil'),
792 None, 'icon-pencil'),
793 'admin_created_user': (_('[created] user'),
793 'admin_created_user': (_('[created] user'),
794 get_user_name, 'icon-user'),
794 get_user_name, 'icon-user'),
795 'admin_updated_user': (_('[updated] user'),
795 'admin_updated_user': (_('[updated] user'),
796 get_user_name, 'icon-user'),
796 get_user_name, 'icon-user'),
797 'admin_created_users_group': (_('[created] user group'),
797 'admin_created_users_group': (_('[created] user group'),
798 get_users_group, 'icon-pencil'),
798 get_users_group, 'icon-pencil'),
799 'admin_updated_users_group': (_('[updated] user group'),
799 'admin_updated_users_group': (_('[updated] user group'),
800 get_users_group, 'icon-pencil'),
800 get_users_group, 'icon-pencil'),
801 'user_commented_revision': (_('[commented] on revision in repository'),
801 'user_commented_revision': (_('[commented] on revision in repository'),
802 get_cs_links, 'icon-comment'),
802 get_cs_links, 'icon-comment'),
803 'user_commented_pull_request': (_('[commented] on pull request for'),
803 'user_commented_pull_request': (_('[commented] on pull request for'),
804 get_pull_request, 'icon-comment'),
804 get_pull_request, 'icon-comment'),
805 'user_closed_pull_request': (_('[closed] pull request for'),
805 'user_closed_pull_request': (_('[closed] pull request for'),
806 get_pull_request, 'icon-ok'),
806 get_pull_request, 'icon-ok'),
807 'push': (_('[pushed] into'),
807 'push': (_('[pushed] into'),
808 get_cs_links, 'icon-move-up'),
808 get_cs_links, 'icon-move-up'),
809 'push_local': (_('[committed via Kallithea] into repository'),
809 'push_local': (_('[committed via Kallithea] into repository'),
810 get_cs_links, 'icon-pencil'),
810 get_cs_links, 'icon-pencil'),
811 'push_remote': (_('[pulled from remote] into repository'),
811 'push_remote': (_('[pulled from remote] into repository'),
812 get_cs_links, 'icon-move-up'),
812 get_cs_links, 'icon-move-up'),
813 'pull': (_('[pulled] from'),
813 'pull': (_('[pulled] from'),
814 None, 'icon-move-down'),
814 None, 'icon-move-down'),
815 'started_following_repo': (_('[started following] repository'),
815 'started_following_repo': (_('[started following] repository'),
816 None, 'icon-heart'),
816 None, 'icon-heart'),
817 'stopped_following_repo': (_('[stopped following] repository'),
817 'stopped_following_repo': (_('[stopped following] repository'),
818 None, 'icon-heart-empty'),
818 None, 'icon-heart-empty'),
819 }
819 }
820
820
821 action_str = action_map.get(action, action)
821 action_str = action_map.get(action, action)
822 if feed:
822 if feed:
823 action = action_str[0].replace('[', '').replace(']', '')
823 action = action_str[0].replace('[', '').replace(']', '')
824 else:
824 else:
825 action = action_str[0] \
825 action = action_str[0] \
826 .replace('[', '<b>') \
826 .replace('[', '<b>') \
827 .replace(']', '</b>')
827 .replace(']', '</b>')
828
828
829 action_params_func = lambda: ""
829 action_params_func = lambda: ""
830
830
831 if callable(action_str[1]):
831 if callable(action_str[1]):
832 action_params_func = action_str[1]
832 action_params_func = action_str[1]
833
833
834 def action_parser_icon():
834 def action_parser_icon():
835 action = user_log.action
835 action = user_log.action
836 action_params = None
836 action_params = None
837 x = action.split(':')
837 x = action.split(':')
838
838
839 if len(x) > 1:
839 if len(x) > 1:
840 action, action_params = x
840 action, action_params = x
841
841
842 ico = action_map.get(action, ['', '', ''])[2]
842 ico = action_map.get(action, ['', '', ''])[2]
843 html = """<i class="%s"></i>""" % ico
843 html = """<i class="%s"></i>""" % ico
844 return literal(html)
844 return literal(html)
845
845
846 # returned callbacks we need to call to get
846 # returned callbacks we need to call to get
847 return [lambda: literal(action), action_params_func, action_parser_icon]
847 return [lambda: literal(action), action_params_func, action_parser_icon]
848
848
849
849
850
850
851
851
852
852
853 #==============================================================================
853 #==============================================================================
854 # GRAVATAR URL
854 # GRAVATAR URL
855 #==============================================================================
855 #==============================================================================
856 def gravatar_div(email_address, cls='', size=30, **div_attributes):
856 def gravatar_div(email_address, cls='', size=30, **div_attributes):
857 """Return an html literal with a span around a gravatar if they are enabled.
857 """Return an html literal with a span around a gravatar if they are enabled.
858 Extra keyword parameters starting with 'div_' will get the prefix removed
858 Extra keyword parameters starting with 'div_' will get the prefix removed
859 and '_' changed to '-' and be used as attributes on the div. The default
859 and '_' changed to '-' and be used as attributes on the div. The default
860 class is 'gravatar'.
860 class is 'gravatar'.
861 """
861 """
862 from tg import tmpl_context as c
862 from tg import tmpl_context as c
863 if not c.visual.use_gravatar:
863 if not c.visual.use_gravatar:
864 return ''
864 return ''
865 if 'div_class' not in div_attributes:
865 if 'div_class' not in div_attributes:
866 div_attributes['div_class'] = "gravatar"
866 div_attributes['div_class'] = "gravatar"
867 attributes = []
867 attributes = []
868 for k, v in sorted(div_attributes.items()):
868 for k, v in sorted(div_attributes.items()):
869 assert k.startswith('div_'), k
869 assert k.startswith('div_'), k
870 attributes.append(' %s="%s"' % (k[4:].replace('_', '-'), escape(v)))
870 attributes.append(' %s="%s"' % (k[4:].replace('_', '-'), escape(v)))
871 return literal("""<span%s>%s</span>""" %
871 return literal("""<span%s>%s</span>""" %
872 (''.join(attributes),
872 (''.join(attributes),
873 gravatar(email_address, cls=cls, size=size)))
873 gravatar(email_address, cls=cls, size=size)))
874
874
875
875
876 def gravatar(email_address, cls='', size=30):
876 def gravatar(email_address, cls='', size=30):
877 """return html element of the gravatar
877 """return html element of the gravatar
878
878
879 This method will return an <img> with the resolution double the size (for
879 This method will return an <img> with the resolution double the size (for
880 retina screens) of the image. If the url returned from gravatar_url is
880 retina screens) of the image. If the url returned from gravatar_url is
881 empty then we fallback to using an icon.
881 empty then we fallback to using an icon.
882
882
883 """
883 """
884 from tg import tmpl_context as c
884 from tg import tmpl_context as c
885 if not c.visual.use_gravatar:
885 if not c.visual.use_gravatar:
886 return ''
886 return ''
887
887
888 src = gravatar_url(email_address, size * 2)
888 src = gravatar_url(email_address, size * 2)
889
889
890 if src:
890 if src:
891 # here it makes sense to use style="width: ..." (instead of, say, a
891 # here it makes sense to use style="width: ..." (instead of, say, a
892 # stylesheet) because we using this to generate a high-res (retina) size
892 # stylesheet) because we using this to generate a high-res (retina) size
893 html = ('<i class="icon-gravatar {cls}"'
893 html = ('<i class="icon-gravatar {cls}"'
894 ' style="font-size: {size}px;background-size: {size}px;background-image: url(\'{src}\')"'
894 ' style="font-size: {size}px;background-size: {size}px;background-image: url(\'{src}\')"'
895 '></i>').format(cls=cls, size=size, src=src)
895 '></i>').format(cls=cls, size=size, src=src)
896
896
897 else:
897 else:
898 # if src is empty then there was no gravatar, so we use a font icon
898 # if src is empty then there was no gravatar, so we use a font icon
899 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
899 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
900 .format(cls=cls, size=size, src=src))
900 .format(cls=cls, size=size, src=src))
901
901
902 return literal(html)
902 return literal(html)
903
903
904
904
905 def gravatar_url(email_address, size=30, default=''):
905 def gravatar_url(email_address, size=30, default=''):
906 # doh, we need to re-import those to mock it later
906 # doh, we need to re-import those to mock it later
907 from kallithea.config.routing import url
907 from kallithea.config.routing import url
908 from kallithea.model.db import User
908 from kallithea.model.db import User
909 from tg import tmpl_context as c
909 from tg import tmpl_context as c
910 if not c.visual.use_gravatar:
910 if not c.visual.use_gravatar:
911 return ""
911 return ""
912
912
913 _def = 'anonymous@kallithea-scm.org' # default gravatar
913 _def = 'anonymous@kallithea-scm.org' # default gravatar
914 email_address = email_address or _def
914 email_address = email_address or _def
915
915
916 if email_address == _def:
916 if email_address == _def:
917 return default
917 return default
918
918
919 parsed_url = urlparse.urlparse(url.current(qualified=True))
919 parsed_url = urlparse.urlparse(url.current(qualified=True))
920 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
920 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL) \
921 .replace('{email}', email_address) \
921 .replace('{email}', email_address) \
922 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
922 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
923 .replace('{netloc}', parsed_url.netloc) \
923 .replace('{netloc}', parsed_url.netloc) \
924 .replace('{scheme}', parsed_url.scheme) \
924 .replace('{scheme}', parsed_url.scheme) \
925 .replace('{size}', safe_str(size))
925 .replace('{size}', safe_str(size))
926 return url
926 return url
927
927
928
928
929 def changed_tooltip(nodes):
929 def changed_tooltip(nodes):
930 """
930 """
931 Generates a html string for changed nodes in changeset page.
931 Generates a html string for changed nodes in changeset page.
932 It limits the output to 30 entries
932 It limits the output to 30 entries
933
933
934 :param nodes: LazyNodesGenerator
934 :param nodes: LazyNodesGenerator
935 """
935 """
936 if nodes:
936 if nodes:
937 pref = ': <br/> '
937 pref = ': <br/> '
938 suf = ''
938 suf = ''
939 if len(nodes) > 30:
939 if len(nodes) > 30:
940 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
940 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
941 return literal(pref + '<br/> '.join([safe_unicode(x.path)
941 return literal(pref + '<br/> '.join([safe_unicode(x.path)
942 for x in nodes[:30]]) + suf)
942 for x in nodes[:30]]) + suf)
943 else:
943 else:
944 return ': ' + _('No files')
944 return ': ' + _('No files')
945
945
946
946
947 def fancy_file_stats(stats):
947 def fancy_file_stats(stats):
948 """
948 """
949 Displays a fancy two colored bar for number of added/deleted
949 Displays a fancy two colored bar for number of added/deleted
950 lines of code on file
950 lines of code on file
951
951
952 :param stats: two element list of added/deleted lines of code
952 :param stats: two element list of added/deleted lines of code
953 """
953 """
954 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
954 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
955 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
955 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
956
956
957 a, d = stats['added'], stats['deleted']
957 a, d = stats['added'], stats['deleted']
958 width = 100
958 width = 100
959
959
960 if stats['binary']:
960 if stats['binary']:
961 # binary mode
961 # binary mode
962 lbl = ''
962 lbl = ''
963 bin_op = 1
963 bin_op = 1
964
964
965 if BIN_FILENODE in stats['ops']:
965 if BIN_FILENODE in stats['ops']:
966 lbl = 'bin+'
966 lbl = 'bin+'
967
967
968 if NEW_FILENODE in stats['ops']:
968 if NEW_FILENODE in stats['ops']:
969 lbl += _('new file')
969 lbl += _('new file')
970 bin_op = NEW_FILENODE
970 bin_op = NEW_FILENODE
971 elif MOD_FILENODE in stats['ops']:
971 elif MOD_FILENODE in stats['ops']:
972 lbl += _('mod')
972 lbl += _('mod')
973 bin_op = MOD_FILENODE
973 bin_op = MOD_FILENODE
974 elif DEL_FILENODE in stats['ops']:
974 elif DEL_FILENODE in stats['ops']:
975 lbl += _('del')
975 lbl += _('del')
976 bin_op = DEL_FILENODE
976 bin_op = DEL_FILENODE
977 elif RENAMED_FILENODE in stats['ops']:
977 elif RENAMED_FILENODE in stats['ops']:
978 lbl += _('rename')
978 lbl += _('rename')
979 bin_op = RENAMED_FILENODE
979 bin_op = RENAMED_FILENODE
980
980
981 # chmod can go with other operations
981 # chmod can go with other operations
982 if CHMOD_FILENODE in stats['ops']:
982 if CHMOD_FILENODE in stats['ops']:
983 _org_lbl = _('chmod')
983 _org_lbl = _('chmod')
984 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
984 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
985
985
986 #import ipdb;ipdb.set_trace()
986 #import ipdb;ipdb.set_trace()
987 b_d = '<div class="bin bin%s progress-bar" style="width:100%%">%s</div>' % (bin_op, lbl)
987 b_d = '<div class="bin bin%s progress-bar" style="width:100%%">%s</div>' % (bin_op, lbl)
988 b_a = '<div class="bin bin1" style="width:0%"></div>'
988 b_a = '<div class="bin bin1" style="width:0%"></div>'
989 return literal('<div style="width:%spx" class="progress">%s%s</div>' % (width, b_a, b_d))
989 return literal('<div style="width:%spx" class="progress">%s%s</div>' % (width, b_a, b_d))
990
990
991 t = stats['added'] + stats['deleted']
991 t = stats['added'] + stats['deleted']
992 unit = float(width) / (t or 1)
992 unit = float(width) / (t or 1)
993
993
994 # needs > 9% of width to be visible or 0 to be hidden
994 # needs > 9% of width to be visible or 0 to be hidden
995 a_p = max(9, unit * a) if a > 0 else 0
995 a_p = max(9, unit * a) if a > 0 else 0
996 d_p = max(9, unit * d) if d > 0 else 0
996 d_p = max(9, unit * d) if d > 0 else 0
997 p_sum = a_p + d_p
997 p_sum = a_p + d_p
998
998
999 if p_sum > width:
999 if p_sum > width:
1000 # adjust the percentage to be == 100% since we adjusted to 9
1000 # adjust the percentage to be == 100% since we adjusted to 9
1001 if a_p > d_p:
1001 if a_p > d_p:
1002 a_p = a_p - (p_sum - width)
1002 a_p = a_p - (p_sum - width)
1003 else:
1003 else:
1004 d_p = d_p - (p_sum - width)
1004 d_p = d_p - (p_sum - width)
1005
1005
1006 a_v = a if a > 0 else ''
1006 a_v = a if a > 0 else ''
1007 d_v = d if d > 0 else ''
1007 d_v = d if d > 0 else ''
1008
1008
1009 d_a = '<div class="added progress-bar" style="width:%s%%">%s</div>' % (
1009 d_a = '<div class="added progress-bar" style="width:%s%%">%s</div>' % (
1010 a_p, a_v
1010 a_p, a_v
1011 )
1011 )
1012 d_d = '<div class="deleted progress-bar" style="width:%s%%">%s</div>' % (
1012 d_d = '<div class="deleted progress-bar" style="width:%s%%">%s</div>' % (
1013 d_p, d_v
1013 d_p, d_v
1014 )
1014 )
1015 return literal('<div class="progress" style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1015 return literal('<div class="progress" style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1016
1016
1017
1017
1018 _URLIFY_RE = re.compile(r'''
1018 _URLIFY_RE = re.compile(r'''
1019 # URL markup
1019 # URL markup
1020 (?P<url>%s) |
1020 (?P<url>%s) |
1021 # @mention markup
1021 # @mention markup
1022 (?P<mention>%s) |
1022 (?P<mention>%s) |
1023 # Changeset hash markup
1023 # Changeset hash markup
1024 (?<!\w|[-_])
1024 (?<!\w|[-_])
1025 (?P<hash>[0-9a-f]{12,40})
1025 (?P<hash>[0-9a-f]{12,40})
1026 (?!\w|[-_]) |
1026 (?!\w|[-_]) |
1027 # Markup of *bold text*
1027 # Markup of *bold text*
1028 (?:
1028 (?:
1029 (?:^|(?<=\s))
1029 (?:^|(?<=\s))
1030 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
1030 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
1031 (?![*\w])
1031 (?![*\w])
1032 ) |
1032 ) |
1033 # "Stylize" markup
1033 # "Stylize" markup
1034 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1034 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1035 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1035 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1036 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
1036 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
1037 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
1037 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
1038 \[(?P<tag>[a-z]+)\]
1038 \[(?P<tag>[a-z]+)\]
1039 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
1039 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
1040 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
1040 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
1041
1041
1042
1042
1043 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
1043 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
1044 """
1044 """
1045 Parses given text message and make literal html with markup.
1045 Parses given text message and make literal html with markup.
1046 The text will be truncated to the specified length.
1046 The text will be truncated to the specified length.
1047 Hashes are turned into changeset links to specified repository.
1047 Hashes are turned into changeset links to specified repository.
1048 URLs links to what they say.
1048 URLs links to what they say.
1049 Issues are linked to given issue-server.
1049 Issues are linked to given issue-server.
1050 If link_ is provided, all text not already linking somewhere will link there.
1050 If link_ is provided, all text not already linking somewhere will link there.
1051 """
1051 """
1052
1052
1053 def _replace(match_obj):
1053 def _replace(match_obj):
1054 url = match_obj.group('url')
1054 url = match_obj.group('url')
1055 if url is not None:
1055 if url is not None:
1056 return '<a href="%(url)s">%(url)s</a>' % {'url': url}
1056 return '<a href="%(url)s">%(url)s</a>' % {'url': url}
1057 mention = match_obj.group('mention')
1057 mention = match_obj.group('mention')
1058 if mention is not None:
1058 if mention is not None:
1059 return '<b>%s</b>' % mention
1059 return '<b>%s</b>' % mention
1060 hash_ = match_obj.group('hash')
1060 hash_ = match_obj.group('hash')
1061 if hash_ is not None and repo_name is not None:
1061 if hash_ is not None and repo_name is not None:
1062 from kallithea.config.routing import url # doh, we need to re-import url to mock it later
1062 from kallithea.config.routing import url # doh, we need to re-import url to mock it later
1063 return '<a class="changeset_hash" href="%(url)s">%(hash)s</a>' % {
1063 return '<a class="changeset_hash" href="%(url)s">%(hash)s</a>' % {
1064 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
1064 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
1065 'hash': hash_,
1065 'hash': hash_,
1066 }
1066 }
1067 bold = match_obj.group('bold')
1067 bold = match_obj.group('bold')
1068 if bold is not None:
1068 if bold is not None:
1069 return '<b>*%s*</b>' % _urlify(bold[1:-1])
1069 return '<b>*%s*</b>' % _urlify(bold[1:-1])
1070 if stylize:
1070 if stylize:
1071 seen = match_obj.group('seen')
1071 seen = match_obj.group('seen')
1072 if seen:
1072 if seen:
1073 return '<div class="label label-meta" data-tag="see">see =&gt; %s</div>' % seen
1073 return '<div class="label label-meta" data-tag="see">see =&gt; %s</div>' % seen
1074 license = match_obj.group('license')
1074 license = match_obj.group('license')
1075 if license:
1075 if license:
1076 return '<div class="label label-meta" data-tag="license"><a href="http://www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
1076 return '<div class="label label-meta" data-tag="license"><a href="http://www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
1077 tagtype = match_obj.group('tagtype')
1077 tagtype = match_obj.group('tagtype')
1078 if tagtype:
1078 if tagtype:
1079 tagvalue = match_obj.group('tagvalue')
1079 tagvalue = match_obj.group('tagvalue')
1080 return '<div class="label label-meta" data-tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
1080 return '<div class="label label-meta" data-tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
1081 lang = match_obj.group('lang')
1081 lang = match_obj.group('lang')
1082 if lang:
1082 if lang:
1083 return '<div class="label label-meta" data-tag="lang">%s</div>' % lang
1083 return '<div class="label label-meta" data-tag="lang">%s</div>' % lang
1084 tag = match_obj.group('tag')
1084 tag = match_obj.group('tag')
1085 if tag:
1085 if tag:
1086 return '<div class="label label-meta" data-tag="%s">%s</div>' % (tag, tag)
1086 return '<div class="label label-meta" data-tag="%s">%s</div>' % (tag, tag)
1087 return match_obj.group(0)
1087 return match_obj.group(0)
1088
1088
1089 def _urlify(s):
1089 def _urlify(s):
1090 """
1090 """
1091 Extract urls from text and make html links out of them
1091 Extract urls from text and make html links out of them
1092 """
1092 """
1093 return _URLIFY_RE.sub(_replace, s)
1093 return _URLIFY_RE.sub(_replace, s)
1094
1094
1095 if truncate is None:
1095 if truncate is None:
1096 s = s.rstrip()
1096 s = s.rstrip()
1097 else:
1097 else:
1098 s = truncatef(s, truncate, whole_word=True)
1098 s = truncatef(s, truncate, whole_word=True)
1099 s = html_escape(s)
1099 s = html_escape(s)
1100 s = _urlify(s)
1100 s = _urlify(s)
1101 if repo_name is not None:
1101 if repo_name is not None:
1102 s = urlify_issues(s, repo_name)
1102 s = urlify_issues(s, repo_name)
1103 if link_ is not None:
1103 if link_ is not None:
1104 # make href around everything that isn't a href already
1104 # make href around everything that isn't a href already
1105 s = linkify_others(s, link_)
1105 s = linkify_others(s, link_)
1106 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1106 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1107 # Turn HTML5 into more valid HTML4 as required by some mail readers.
1107 # Turn HTML5 into more valid HTML4 as required by some mail readers.
1108 # (This is not done in one step in html_escape, because character codes like
1108 # (This is not done in one step in html_escape, because character codes like
1109 # &#123; risk to be seen as an issue reference due to the presence of '#'.)
1109 # &#123; risk to be seen as an issue reference due to the presence of '#'.)
1110 s = s.replace("&apos;", "&#39;")
1110 s = s.replace("&apos;", "&#39;")
1111 return literal(s)
1111 return literal(s)
1112
1112
1113
1113
1114 def linkify_others(t, l):
1114 def linkify_others(t, l):
1115 """Add a default link to html with links.
1115 """Add a default link to html with links.
1116 HTML doesn't allow nesting of links, so the outer link must be broken up
1116 HTML doesn't allow nesting of links, so the outer link must be broken up
1117 in pieces and give space for other links.
1117 in pieces and give space for other links.
1118 """
1118 """
1119 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1119 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1120 links = []
1120 links = []
1121 for e in urls.split(t):
1121 for e in urls.split(t):
1122 if e.strip() and not urls.match(e):
1122 if e.strip() and not urls.match(e):
1123 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1123 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1124 else:
1124 else:
1125 links.append(e)
1125 links.append(e)
1126
1126
1127 return ''.join(links)
1127 return ''.join(links)
1128
1128
1129
1129
1130 # Global variable that will hold the actual urlify_issues function body.
1130 # Global variable that will hold the actual urlify_issues function body.
1131 # Will be set on first use when the global configuration has been read.
1131 # Will be set on first use when the global configuration has been read.
1132 _urlify_issues_f = None
1132 _urlify_issues_f = None
1133
1133
1134
1134
1135 def urlify_issues(newtext, repo_name):
1135 def urlify_issues(newtext, repo_name):
1136 """Urlify issue references according to .ini configuration"""
1136 """Urlify issue references according to .ini configuration"""
1137 global _urlify_issues_f
1137 global _urlify_issues_f
1138 if _urlify_issues_f is None:
1138 if _urlify_issues_f is None:
1139 from kallithea import CONFIG
1139 from kallithea import CONFIG
1140 from kallithea.model.db import URL_SEP
1140 from kallithea.model.db import URL_SEP
1141 assert CONFIG['sqlalchemy.url'] # make sure config has been loaded
1141 assert CONFIG['sqlalchemy.url'] # make sure config has been loaded
1142
1142
1143 # Build chain of urlify functions, starting with not doing any transformation
1143 # Build chain of urlify functions, starting with not doing any transformation
1144 tmp_urlify_issues_f = lambda s: s
1144 tmp_urlify_issues_f = lambda s: s
1145
1145
1146 issue_pat_re = re.compile(r'issue_pat(.*)')
1146 issue_pat_re = re.compile(r'issue_pat(.*)')
1147 for k in CONFIG.keys():
1147 for k in CONFIG.keys():
1148 # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
1148 # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
1149 m = issue_pat_re.match(k)
1149 m = issue_pat_re.match(k)
1150 if m is None:
1150 if m is None:
1151 continue
1151 continue
1152 suffix = m.group(1)
1152 suffix = m.group(1)
1153 issue_pat = CONFIG.get(k)
1153 issue_pat = CONFIG.get(k)
1154 issue_server_link = CONFIG.get('issue_server_link%s' % suffix)
1154 issue_server_link = CONFIG.get('issue_server_link%s' % suffix)
1155 issue_sub = CONFIG.get('issue_sub%s' % suffix)
1155 issue_sub = CONFIG.get('issue_sub%s' % suffix)
1156 if not issue_pat or not issue_server_link or issue_sub is None: # issue_sub can be empty but should be present
1156 if not issue_pat or not issue_server_link or issue_sub is None: # issue_sub can be empty but should be present
1157 log.error('skipping incomplete issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
1157 log.error('skipping incomplete issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
1158 continue
1158 continue
1159
1159
1160 # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
1160 # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
1161 try:
1161 try:
1162 issue_re = re.compile(issue_pat)
1162 issue_re = re.compile(issue_pat)
1163 except re.error as e:
1163 except re.error as e:
1164 log.error('skipping invalid issue pattern %r: %r -> %r %r. Error: %s', suffix, issue_pat, issue_server_link, issue_sub, str(e))
1164 log.error('skipping invalid issue pattern %r: %r -> %r %r. Error: %s', suffix, issue_pat, issue_server_link, issue_sub, str(e))
1165 continue
1165 continue
1166
1166
1167 log.debug('issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
1167 log.debug('issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
1168
1168
1169 def issues_replace(match_obj,
1169 def issues_replace(match_obj,
1170 issue_server_link=issue_server_link, issue_sub=issue_sub):
1170 issue_server_link=issue_server_link, issue_sub=issue_sub):
1171 try:
1171 try:
1172 issue_url = match_obj.expand(issue_server_link)
1172 issue_url = match_obj.expand(issue_server_link)
1173 except (IndexError, re.error) as e:
1173 except (IndexError, re.error) as e:
1174 log.error('invalid issue_url setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1174 log.error('invalid issue_url setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1175 issue_url = issue_server_link
1175 issue_url = issue_server_link
1176 issue_url = issue_url.replace('{repo}', repo_name)
1176 issue_url = issue_url.replace('{repo}', repo_name)
1177 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1177 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1178 # if issue_sub is empty use the matched issue reference verbatim
1178 # if issue_sub is empty use the matched issue reference verbatim
1179 if not issue_sub:
1179 if not issue_sub:
1180 issue_text = match_obj.group()
1180 issue_text = match_obj.group()
1181 else:
1181 else:
1182 try:
1182 try:
1183 issue_text = match_obj.expand(issue_sub)
1183 issue_text = match_obj.expand(issue_sub)
1184 except (IndexError, re.error) as e:
1184 except (IndexError, re.error) as e:
1185 log.error('invalid issue_sub setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1185 log.error('invalid issue_sub setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1186 issue_text = match_obj.group()
1186 issue_text = match_obj.group()
1187
1187
1188 return (
1188 return (
1189 '<a class="issue-tracker-link" href="%(url)s">'
1189 '<a class="issue-tracker-link" href="%(url)s">'
1190 '%(text)s'
1190 '%(text)s'
1191 '</a>'
1191 '</a>'
1192 ) % {
1192 ) % {
1193 'url': issue_url,
1193 'url': issue_url,
1194 'text': issue_text,
1194 'text': issue_text,
1195 }
1195 }
1196 tmp_urlify_issues_f = (lambda s,
1196 tmp_urlify_issues_f = (lambda s,
1197 issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f:
1197 issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f:
1198 issue_re.sub(issues_replace, chain_f(s)))
1198 issue_re.sub(issues_replace, chain_f(s)))
1199
1199
1200 # Set tmp function globally - atomically
1200 # Set tmp function globally - atomically
1201 _urlify_issues_f = tmp_urlify_issues_f
1201 _urlify_issues_f = tmp_urlify_issues_f
1202
1202
1203 return _urlify_issues_f(newtext)
1203 return _urlify_issues_f(newtext)
1204
1204
1205
1205
1206 def render_w_mentions(source, repo_name=None):
1206 def render_w_mentions(source, repo_name=None):
1207 """
1207 """
1208 Render plain text with revision hashes and issue references urlified
1208 Render plain text with revision hashes and issue references urlified
1209 and with @mention highlighting.
1209 and with @mention highlighting.
1210 """
1210 """
1211 s = safe_unicode(source)
1211 s = safe_unicode(source)
1212 s = urlify_text(s, repo_name=repo_name)
1212 s = urlify_text(s, repo_name=repo_name)
1213 return literal('<div class="formatted-fixed">%s</div>' % s)
1213 return literal('<div class="formatted-fixed">%s</div>' % s)
1214
1214
1215
1215
1216 def short_ref(ref_type, ref_name):
1216 def short_ref(ref_type, ref_name):
1217 if ref_type == 'rev':
1217 if ref_type == 'rev':
1218 return short_id(ref_name)
1218 return short_id(ref_name)
1219 return ref_name
1219 return ref_name
1220
1220
1221
1221
1222 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1222 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1223 """
1223 """
1224 Return full markup for a href to changeset_home for a changeset.
1224 Return full markup for a href to changeset_home for a changeset.
1225 If ref_type is branch it will link to changelog.
1225 If ref_type is branch it will link to changelog.
1226 ref_name is shortened if ref_type is 'rev'.
1226 ref_name is shortened if ref_type is 'rev'.
1227 if rev is specified show it too, explicitly linking to that revision.
1227 if rev is specified show it too, explicitly linking to that revision.
1228 """
1228 """
1229 txt = short_ref(ref_type, ref_name)
1229 txt = short_ref(ref_type, ref_name)
1230 if ref_type == 'branch':
1230 if ref_type == 'branch':
1231 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1231 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1232 else:
1232 else:
1233 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1233 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1234 l = link_to(repo_name + '#' + txt, u)
1234 l = link_to(repo_name + '#' + txt, u)
1235 if rev and ref_type != 'rev':
1235 if rev and ref_type != 'rev':
1236 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1236 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1237 return l
1237 return l
1238
1238
1239
1239
1240 def changeset_status(repo, revision):
1240 def changeset_status(repo, revision):
1241 from kallithea.model.changeset_status import ChangesetStatusModel
1241 from kallithea.model.changeset_status import ChangesetStatusModel
1242 return ChangesetStatusModel().get_status(repo, revision)
1242 return ChangesetStatusModel().get_status(repo, revision)
1243
1243
1244
1244
1245 def changeset_status_lbl(changeset_status):
1245 def changeset_status_lbl(changeset_status):
1246 from kallithea.model.db import ChangesetStatus
1246 from kallithea.model.db import ChangesetStatus
1247 return ChangesetStatus.get_status_lbl(changeset_status)
1247 return ChangesetStatus.get_status_lbl(changeset_status)
1248
1248
1249
1249
1250 def get_permission_name(key):
1250 def get_permission_name(key):
1251 from kallithea.model.db import Permission
1251 from kallithea.model.db import Permission
1252 return dict(Permission.PERMS).get(key)
1252 return dict(Permission.PERMS).get(key)
1253
1253
1254
1254
1255 def journal_filter_help():
1255 def journal_filter_help():
1256 return _(textwrap.dedent('''
1256 return _(textwrap.dedent('''
1257 Example filter terms:
1257 Example filter terms:
1258 repository:vcs
1258 repository:vcs
1259 username:developer
1259 username:developer
1260 action:*push*
1260 action:*push*
1261 ip:127.0.0.1
1261 ip:127.0.0.1
1262 date:20120101
1262 date:20120101
1263 date:[20120101100000 TO 20120102]
1263 date:[20120101100000 TO 20120102]
1264
1264
1265 Generate wildcards using '*' character:
1265 Generate wildcards using '*' character:
1266 "repository:vcs*" - search everything starting with 'vcs'
1266 "repository:vcs*" - search everything starting with 'vcs'
1267 "repository:*vcs*" - search for repository containing 'vcs'
1267 "repository:*vcs*" - search for repository containing 'vcs'
1268
1268
1269 Optional AND / OR operators in queries
1269 Optional AND / OR operators in queries
1270 "repository:vcs OR repository:test"
1270 "repository:vcs OR repository:test"
1271 "username:test AND repository:test*"
1271 "username:test AND repository:test*"
1272 '''))
1272 '''))
1273
1273
1274
1274
1275 def not_mapped_error(repo_name):
1275 def not_mapped_error(repo_name):
1276 flash(_('%s repository is not mapped to db perhaps'
1276 flash(_('%s repository is not mapped to db perhaps'
1277 ' it was created or renamed from the filesystem'
1277 ' it was created or renamed from the filesystem'
1278 ' please run the application again'
1278 ' please run the application again'
1279 ' in order to rescan repositories') % repo_name, category='error')
1279 ' in order to rescan repositories') % repo_name, category='error')
1280
1280
1281
1281
1282 def ip_range(ip_addr):
1282 def ip_range(ip_addr):
1283 from kallithea.model.db import UserIpMap
1283 from kallithea.model.db import UserIpMap
1284 s, e = UserIpMap._get_ip_range(ip_addr)
1284 s, e = UserIpMap._get_ip_range(ip_addr)
1285 return '%s - %s' % (s, e)
1285 return '%s - %s' % (s, e)
1286
1286
1287
1287
1288 session_csrf_secret_name = "_session_csrf_secret_token"
1288 session_csrf_secret_name = "_session_csrf_secret_token"
1289
1289
1290 def session_csrf_secret_token():
1290 def session_csrf_secret_token():
1291 """Return (and create) the current session's CSRF protection token."""
1291 """Return (and create) the current session's CSRF protection token."""
1292 from tg import session
1292 from tg import session
1293 if not session_csrf_secret_name in session:
1293 if not session_csrf_secret_name in session:
1294 session[session_csrf_secret_name] = str(random.getrandbits(128))
1294 session[session_csrf_secret_name] = str(random.getrandbits(128))
1295 session.save()
1295 session.save()
1296 return session[session_csrf_secret_name]
1296 return session[session_csrf_secret_name]
1297
1297
1298 def form(url, method="post", **attrs):
1298 def form(url, method="post", **attrs):
1299 """Like webhelpers.html.tags.form , but automatically adding
1299 """Like webhelpers.html.tags.form , but automatically adding
1300 session_csrf_secret_token for POST. The secret is thus never leaked in GET
1300 session_csrf_secret_token for POST. The secret is thus never leaked in GET
1301 URLs.
1301 URLs.
1302 """
1302 """
1303 form = insecure_form(url, method, **attrs)
1303 form = insecure_form(url, method, **attrs)
1304 if method.lower() == 'get':
1304 if method.lower() == 'get':
1305 return form
1305 return form
1306 return form + HTML.div(hidden(session_csrf_secret_name, session_csrf_secret_token()), style="display: none;")
1306 return form + HTML.div(hidden(session_csrf_secret_name, session_csrf_secret_token()), style="display: none;")
General Comments 0
You need to be logged in to leave comments. Login now