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