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