##// END OF EJS Templates
helpers: inline url markup in urlify_text...
Mads Kiilerich -
r6147:ca830f9d default
parent child Browse files
Show More
@@ -1,1497 +1,1506 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 StringIO
21 import StringIO
22 import math
22 import math
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 pylons import url
31 from pylons import url
32 from pylons.i18n.translation import _, ungettext
32 from pylons.i18n.translation import _, ungettext
33
33
34 from webhelpers.html import literal, HTML, escape
34 from webhelpers.html import literal, HTML, escape
35 from webhelpers.html.tools import *
35 from webhelpers.html.tools import *
36 from webhelpers.html.builder import make_tag
36 from webhelpers.html.builder import make_tag
37 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
37 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
38 end_form, file, hidden, image, javascript_link, link_to, \
38 end_form, file, hidden, image, javascript_link, link_to, \
39 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
39 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
40 submit, text, password, textarea, title, ul, xml_declaration, radio, \
40 submit, text, password, textarea, title, ul, xml_declaration, radio, \
41 form as insecure_form
41 form as insecure_form
42 from webhelpers.html.tools import auto_link, button_to, highlight, \
42 from webhelpers.html.tools import auto_link, button_to, highlight, \
43 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
43 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
44 from webhelpers.number import format_byte_size, format_bit_size
44 from webhelpers.number import format_byte_size, format_bit_size
45 from webhelpers.pylonslib import Flash as _Flash
45 from webhelpers.pylonslib import Flash as _Flash
46 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
46 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
47 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
47 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
48 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
48 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
49 replace_whitespace, urlify, truncate, wrap_paragraphs
49 replace_whitespace, urlify, truncate, wrap_paragraphs
50 from webhelpers.date import time_ago_in_words
50 from webhelpers.date import time_ago_in_words
51 from webhelpers.paginate import Page as _Page
51 from webhelpers.paginate import Page as _Page
52 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
52 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
53 convert_boolean_attrs, NotGiven, _make_safe_id_component
53 convert_boolean_attrs, NotGiven, _make_safe_id_component
54
54
55 from kallithea.lib.annotate import annotate_highlight
55 from kallithea.lib.annotate import annotate_highlight
56 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
56 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
57 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
57 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
58 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict, \
58 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict, \
59 safe_int, MENTIONS_REGEX
59 safe_int, MENTIONS_REGEX
60 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
60 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
61 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
61 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
62 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
62 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
63 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
63 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
64 from kallithea.model.changeset_status import ChangesetStatusModel
64 from kallithea.model.changeset_status import ChangesetStatusModel
65 from kallithea.model.db import URL_SEP, Permission
65 from kallithea.model.db import URL_SEP, Permission
66
66
67 log = logging.getLogger(__name__)
67 log = logging.getLogger(__name__)
68
68
69
69
70 def canonical_url(*args, **kargs):
70 def canonical_url(*args, **kargs):
71 '''Like url(x, qualified=True), but returns url that not only is qualified
71 '''Like url(x, qualified=True), but returns url that not only is qualified
72 but also canonical, as configured in canonical_url'''
72 but also canonical, as configured in canonical_url'''
73 from kallithea import CONFIG
73 from kallithea import CONFIG
74 try:
74 try:
75 parts = CONFIG.get('canonical_url', '').split('://', 1)
75 parts = CONFIG.get('canonical_url', '').split('://', 1)
76 kargs['host'] = parts[1].split('/', 1)[0]
76 kargs['host'] = parts[1].split('/', 1)[0]
77 kargs['protocol'] = parts[0]
77 kargs['protocol'] = parts[0]
78 except IndexError:
78 except IndexError:
79 kargs['qualified'] = True
79 kargs['qualified'] = True
80 return url(*args, **kargs)
80 return url(*args, **kargs)
81
81
82 def canonical_hostname():
82 def canonical_hostname():
83 '''Return canonical hostname of system'''
83 '''Return canonical hostname of system'''
84 from kallithea import CONFIG
84 from kallithea import CONFIG
85 try:
85 try:
86 parts = CONFIG.get('canonical_url', '').split('://', 1)
86 parts = CONFIG.get('canonical_url', '').split('://', 1)
87 return parts[1].split('/', 1)[0]
87 return parts[1].split('/', 1)[0]
88 except IndexError:
88 except IndexError:
89 parts = url('home', qualified=True).split('://', 1)
89 parts = url('home', qualified=True).split('://', 1)
90 return parts[1].split('/', 1)[0]
90 return parts[1].split('/', 1)[0]
91
91
92 def html_escape(s):
92 def html_escape(s):
93 """Return string with all html escaped.
93 """Return string with all html escaped.
94 This is also safe for javascript in html but not necessarily correct.
94 This is also safe for javascript in html but not necessarily correct.
95 """
95 """
96 return (s
96 return (s
97 .replace('&', '&amp;')
97 .replace('&', '&amp;')
98 .replace(">", "&gt;")
98 .replace(">", "&gt;")
99 .replace("<", "&lt;")
99 .replace("<", "&lt;")
100 .replace('"', "&quot;")
100 .replace('"', "&quot;")
101 .replace("'", "&apos;")
101 .replace("'", "&apos;")
102 )
102 )
103
103
104 def shorter(s, size=20, firstline=False, postfix='...'):
104 def shorter(s, size=20, firstline=False, postfix='...'):
105 """Truncate s to size, including the postfix string if truncating.
105 """Truncate s to size, including the postfix string if truncating.
106 If firstline, truncate at newline.
106 If firstline, truncate at newline.
107 """
107 """
108 if firstline:
108 if firstline:
109 s = s.split('\n', 1)[0].rstrip()
109 s = s.split('\n', 1)[0].rstrip()
110 if len(s) > size:
110 if len(s) > size:
111 return s[:size - len(postfix)] + postfix
111 return s[:size - len(postfix)] + postfix
112 return s
112 return s
113
113
114
114
115 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
115 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
116 """
116 """
117 Reset button
117 Reset button
118 """
118 """
119 _set_input_attrs(attrs, type, name, value)
119 _set_input_attrs(attrs, type, name, value)
120 _set_id_attr(attrs, id, name)
120 _set_id_attr(attrs, id, name)
121 convert_boolean_attrs(attrs, ["disabled"])
121 convert_boolean_attrs(attrs, ["disabled"])
122 return HTML.input(**attrs)
122 return HTML.input(**attrs)
123
123
124 reset = _reset
124 reset = _reset
125 safeid = _make_safe_id_component
125 safeid = _make_safe_id_component
126
126
127
127
128 def FID(raw_id, path):
128 def FID(raw_id, path):
129 """
129 """
130 Creates a unique ID for filenode based on it's hash of path and revision
130 Creates a unique ID for filenode based on it's hash of path and revision
131 it's safe to use in urls
131 it's safe to use in urls
132
132
133 :param raw_id:
133 :param raw_id:
134 :param path:
134 :param path:
135 """
135 """
136
136
137 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
137 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
138
138
139
139
140 class _GetError(object):
140 class _GetError(object):
141 """Get error from form_errors, and represent it as span wrapped error
141 """Get error from form_errors, and represent it as span wrapped error
142 message
142 message
143
143
144 :param field_name: field to fetch errors for
144 :param field_name: field to fetch errors for
145 :param form_errors: form errors dict
145 :param form_errors: form errors dict
146 """
146 """
147
147
148 def __call__(self, field_name, form_errors):
148 def __call__(self, field_name, form_errors):
149 tmpl = """<span class="error_msg">%s</span>"""
149 tmpl = """<span class="error_msg">%s</span>"""
150 if form_errors and field_name in form_errors:
150 if form_errors and field_name in form_errors:
151 return literal(tmpl % form_errors.get(field_name))
151 return literal(tmpl % form_errors.get(field_name))
152
152
153 get_error = _GetError()
153 get_error = _GetError()
154
154
155
155
156 class _FilesBreadCrumbs(object):
156 class _FilesBreadCrumbs(object):
157
157
158 def __call__(self, repo_name, rev, paths):
158 def __call__(self, repo_name, rev, paths):
159 if isinstance(paths, str):
159 if isinstance(paths, str):
160 paths = safe_unicode(paths)
160 paths = safe_unicode(paths)
161 url_l = [link_to(repo_name, url('files_home',
161 url_l = [link_to(repo_name, url('files_home',
162 repo_name=repo_name,
162 repo_name=repo_name,
163 revision=rev, f_path=''),
163 revision=rev, f_path=''),
164 class_='ypjax-link')]
164 class_='ypjax-link')]
165 paths_l = paths.split('/')
165 paths_l = paths.split('/')
166 for cnt, p in enumerate(paths_l):
166 for cnt, p in enumerate(paths_l):
167 if p != '':
167 if p != '':
168 url_l.append(link_to(p,
168 url_l.append(link_to(p,
169 url('files_home',
169 url('files_home',
170 repo_name=repo_name,
170 repo_name=repo_name,
171 revision=rev,
171 revision=rev,
172 f_path='/'.join(paths_l[:cnt + 1])
172 f_path='/'.join(paths_l[:cnt + 1])
173 ),
173 ),
174 class_='ypjax-link'
174 class_='ypjax-link'
175 )
175 )
176 )
176 )
177
177
178 return literal('/'.join(url_l))
178 return literal('/'.join(url_l))
179
179
180 files_breadcrumbs = _FilesBreadCrumbs()
180 files_breadcrumbs = _FilesBreadCrumbs()
181
181
182
182
183 class CodeHtmlFormatter(HtmlFormatter):
183 class CodeHtmlFormatter(HtmlFormatter):
184 """
184 """
185 My code Html Formatter for source codes
185 My code Html Formatter for source codes
186 """
186 """
187
187
188 def wrap(self, source, outfile):
188 def wrap(self, source, outfile):
189 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
189 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
190
190
191 def _wrap_code(self, source):
191 def _wrap_code(self, source):
192 for cnt, it in enumerate(source):
192 for cnt, it in enumerate(source):
193 i, t = it
193 i, t = it
194 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
194 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
195 yield i, t
195 yield i, t
196
196
197 def _wrap_tablelinenos(self, inner):
197 def _wrap_tablelinenos(self, inner):
198 dummyoutfile = StringIO.StringIO()
198 dummyoutfile = StringIO.StringIO()
199 lncount = 0
199 lncount = 0
200 for t, line in inner:
200 for t, line in inner:
201 if t:
201 if t:
202 lncount += 1
202 lncount += 1
203 dummyoutfile.write(line)
203 dummyoutfile.write(line)
204
204
205 fl = self.linenostart
205 fl = self.linenostart
206 mw = len(str(lncount + fl - 1))
206 mw = len(str(lncount + fl - 1))
207 sp = self.linenospecial
207 sp = self.linenospecial
208 st = self.linenostep
208 st = self.linenostep
209 la = self.lineanchors
209 la = self.lineanchors
210 aln = self.anchorlinenos
210 aln = self.anchorlinenos
211 nocls = self.noclasses
211 nocls = self.noclasses
212 if sp:
212 if sp:
213 lines = []
213 lines = []
214
214
215 for i in range(fl, fl + lncount):
215 for i in range(fl, fl + lncount):
216 if i % st == 0:
216 if i % st == 0:
217 if i % sp == 0:
217 if i % sp == 0:
218 if aln:
218 if aln:
219 lines.append('<a href="#%s%d" class="special">%*d</a>' %
219 lines.append('<a href="#%s%d" class="special">%*d</a>' %
220 (la, i, mw, i))
220 (la, i, mw, i))
221 else:
221 else:
222 lines.append('<span class="special">%*d</span>' % (mw, i))
222 lines.append('<span class="special">%*d</span>' % (mw, i))
223 else:
223 else:
224 if aln:
224 if aln:
225 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
225 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
226 else:
226 else:
227 lines.append('%*d' % (mw, i))
227 lines.append('%*d' % (mw, i))
228 else:
228 else:
229 lines.append('')
229 lines.append('')
230 ls = '\n'.join(lines)
230 ls = '\n'.join(lines)
231 else:
231 else:
232 lines = []
232 lines = []
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 aln:
235 if aln:
236 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
236 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
237 else:
237 else:
238 lines.append('%*d' % (mw, i))
238 lines.append('%*d' % (mw, i))
239 else:
239 else:
240 lines.append('')
240 lines.append('')
241 ls = '\n'.join(lines)
241 ls = '\n'.join(lines)
242
242
243 # in case you wonder about the seemingly redundant <div> here: since the
243 # in case you wonder about the seemingly redundant <div> here: since the
244 # content in the other cell also is wrapped in a div, some browsers in
244 # content in the other cell also is wrapped in a div, some browsers in
245 # some configurations seem to mess up the formatting...
245 # some configurations seem to mess up the formatting...
246 if nocls:
246 if nocls:
247 yield 0, ('<table class="%stable">' % self.cssclass +
247 yield 0, ('<table class="%stable">' % self.cssclass +
248 '<tr><td><div class="linenodiv" '
248 '<tr><td><div class="linenodiv" '
249 'style="background-color: #f0f0f0; padding-right: 10px">'
249 'style="background-color: #f0f0f0; padding-right: 10px">'
250 '<pre style="line-height: 125%">' +
250 '<pre style="line-height: 125%">' +
251 ls + '</pre></div></td><td id="hlcode" class="code">')
251 ls + '</pre></div></td><td id="hlcode" class="code">')
252 else:
252 else:
253 yield 0, ('<table class="%stable">' % self.cssclass +
253 yield 0, ('<table class="%stable">' % self.cssclass +
254 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
254 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
255 ls + '</pre></div></td><td id="hlcode" class="code">')
255 ls + '</pre></div></td><td id="hlcode" class="code">')
256 yield 0, dummyoutfile.getvalue()
256 yield 0, dummyoutfile.getvalue()
257 yield 0, '</td></tr></table>'
257 yield 0, '</td></tr></table>'
258
258
259
259
260 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
260 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
261
261
262 def _markup_whitespace(m):
262 def _markup_whitespace(m):
263 groups = m.groups()
263 groups = m.groups()
264 if groups[0]:
264 if groups[0]:
265 return '<u>\t</u>'
265 return '<u>\t</u>'
266 if groups[1]:
266 if groups[1]:
267 return ' <i></i>'
267 return ' <i></i>'
268
268
269 def markup_whitespace(s):
269 def markup_whitespace(s):
270 return _whitespace_re.sub(_markup_whitespace, s)
270 return _whitespace_re.sub(_markup_whitespace, s)
271
271
272 def pygmentize(filenode, **kwargs):
272 def pygmentize(filenode, **kwargs):
273 """
273 """
274 pygmentize function using pygments
274 pygmentize function using pygments
275
275
276 :param filenode:
276 :param filenode:
277 """
277 """
278 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
278 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
279 return literal(markup_whitespace(
279 return literal(markup_whitespace(
280 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
280 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
281
281
282
282
283 def pygmentize_annotation(repo_name, filenode, **kwargs):
283 def pygmentize_annotation(repo_name, filenode, **kwargs):
284 """
284 """
285 pygmentize function for annotation
285 pygmentize function for annotation
286
286
287 :param filenode:
287 :param filenode:
288 """
288 """
289
289
290 color_dict = {}
290 color_dict = {}
291
291
292 def gen_color(n=10000):
292 def gen_color(n=10000):
293 """generator for getting n of evenly distributed colors using
293 """generator for getting n of evenly distributed colors using
294 hsv color and golden ratio. It always return same order of colors
294 hsv color and golden ratio. It always return same order of colors
295
295
296 :returns: RGB tuple
296 :returns: RGB tuple
297 """
297 """
298
298
299 def hsv_to_rgb(h, s, v):
299 def hsv_to_rgb(h, s, v):
300 if s == 0.0:
300 if s == 0.0:
301 return v, v, v
301 return v, v, v
302 i = int(h * 6.0) # XXX assume int() truncates!
302 i = int(h * 6.0) # XXX assume int() truncates!
303 f = (h * 6.0) - i
303 f = (h * 6.0) - i
304 p = v * (1.0 - s)
304 p = v * (1.0 - s)
305 q = v * (1.0 - s * f)
305 q = v * (1.0 - s * f)
306 t = v * (1.0 - s * (1.0 - f))
306 t = v * (1.0 - s * (1.0 - f))
307 i = i % 6
307 i = i % 6
308 if i == 0:
308 if i == 0:
309 return v, t, p
309 return v, t, p
310 if i == 1:
310 if i == 1:
311 return q, v, p
311 return q, v, p
312 if i == 2:
312 if i == 2:
313 return p, v, t
313 return p, v, t
314 if i == 3:
314 if i == 3:
315 return p, q, v
315 return p, q, v
316 if i == 4:
316 if i == 4:
317 return t, p, v
317 return t, p, v
318 if i == 5:
318 if i == 5:
319 return v, p, q
319 return v, p, q
320
320
321 golden_ratio = 0.618033988749895
321 golden_ratio = 0.618033988749895
322 h = 0.22717784590367374
322 h = 0.22717784590367374
323
323
324 for _unused in xrange(n):
324 for _unused in xrange(n):
325 h += golden_ratio
325 h += golden_ratio
326 h %= 1
326 h %= 1
327 HSV_tuple = [h, 0.95, 0.95]
327 HSV_tuple = [h, 0.95, 0.95]
328 RGB_tuple = hsv_to_rgb(*HSV_tuple)
328 RGB_tuple = hsv_to_rgb(*HSV_tuple)
329 yield map(lambda x: str(int(x * 256)), RGB_tuple)
329 yield map(lambda x: str(int(x * 256)), RGB_tuple)
330
330
331 cgenerator = gen_color()
331 cgenerator = gen_color()
332
332
333 def get_color_string(cs):
333 def get_color_string(cs):
334 if cs in color_dict:
334 if cs in color_dict:
335 col = color_dict[cs]
335 col = color_dict[cs]
336 else:
336 else:
337 col = color_dict[cs] = cgenerator.next()
337 col = color_dict[cs] = cgenerator.next()
338 return "color: rgb(%s)! important;" % (', '.join(col))
338 return "color: rgb(%s)! important;" % (', '.join(col))
339
339
340 def url_func(repo_name):
340 def url_func(repo_name):
341
341
342 def _url_func(changeset):
342 def _url_func(changeset):
343 author = escape(changeset.author)
343 author = escape(changeset.author)
344 date = changeset.date
344 date = changeset.date
345 message = escape(changeset.message)
345 message = escape(changeset.message)
346 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
346 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
347 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
347 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
348 "</b> %s<br/></div>") % (author, date, message)
348 "</b> %s<br/></div>") % (author, date, message)
349
349
350 lnk_format = show_id(changeset)
350 lnk_format = show_id(changeset)
351 uri = link_to(
351 uri = link_to(
352 lnk_format,
352 lnk_format,
353 url('changeset_home', repo_name=repo_name,
353 url('changeset_home', repo_name=repo_name,
354 revision=changeset.raw_id),
354 revision=changeset.raw_id),
355 style=get_color_string(changeset.raw_id),
355 style=get_color_string(changeset.raw_id),
356 class_='tooltip safe-html-title',
356 class_='tooltip safe-html-title',
357 title=tooltip_html
357 title=tooltip_html
358 )
358 )
359
359
360 uri += '\n'
360 uri += '\n'
361 return uri
361 return uri
362 return _url_func
362 return _url_func
363
363
364 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
364 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
365
365
366
366
367 def is_following_repo(repo_name, user_id):
367 def is_following_repo(repo_name, user_id):
368 from kallithea.model.scm import ScmModel
368 from kallithea.model.scm import ScmModel
369 return ScmModel().is_following_repo(repo_name, user_id)
369 return ScmModel().is_following_repo(repo_name, user_id)
370
370
371 class _Message(object):
371 class _Message(object):
372 """A message returned by ``Flash.pop_messages()``.
372 """A message returned by ``Flash.pop_messages()``.
373
373
374 Converting the message to a string returns the message text. Instances
374 Converting the message to a string returns the message text. Instances
375 also have the following attributes:
375 also have the following attributes:
376
376
377 * ``message``: the message text.
377 * ``message``: the message text.
378 * ``category``: the category specified when the message was created.
378 * ``category``: the category specified when the message was created.
379 """
379 """
380
380
381 def __init__(self, category, message):
381 def __init__(self, category, message):
382 self.category = category
382 self.category = category
383 self.message = message
383 self.message = message
384
384
385 def __str__(self):
385 def __str__(self):
386 return self.message
386 return self.message
387
387
388 __unicode__ = __str__
388 __unicode__ = __str__
389
389
390 def __html__(self):
390 def __html__(self):
391 return escape(safe_unicode(self.message))
391 return escape(safe_unicode(self.message))
392
392
393 class Flash(_Flash):
393 class Flash(_Flash):
394
394
395 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
395 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
396 """
396 """
397 Show a message to the user _and_ log it through the specified function
397 Show a message to the user _and_ log it through the specified function
398
398
399 category: notice (default), warning, error, success
399 category: notice (default), warning, error, success
400 logf: a custom log function - such as log.debug
400 logf: a custom log function - such as log.debug
401
401
402 logf defaults to log.info, unless category equals 'success', in which
402 logf defaults to log.info, unless category equals 'success', in which
403 case logf defaults to log.debug.
403 case logf defaults to log.debug.
404 """
404 """
405 if logf is None:
405 if logf is None:
406 logf = log.info
406 logf = log.info
407 if category == 'success':
407 if category == 'success':
408 logf = log.debug
408 logf = log.debug
409
409
410 logf('Flash %s: %s', category, message)
410 logf('Flash %s: %s', category, message)
411
411
412 super(Flash, self).__call__(message, category, ignore_duplicate)
412 super(Flash, self).__call__(message, category, ignore_duplicate)
413
413
414 def pop_messages(self):
414 def pop_messages(self):
415 """Return all accumulated messages and delete them from the session.
415 """Return all accumulated messages and delete them from the session.
416
416
417 The return value is a list of ``Message`` objects.
417 The return value is a list of ``Message`` objects.
418 """
418 """
419 from pylons import session
419 from pylons import session
420 messages = session.pop(self.session_key, [])
420 messages = session.pop(self.session_key, [])
421 session.save()
421 session.save()
422 return [_Message(*m) for m in messages]
422 return [_Message(*m) for m in messages]
423
423
424 flash = Flash()
424 flash = Flash()
425
425
426 #==============================================================================
426 #==============================================================================
427 # SCM FILTERS available via h.
427 # SCM FILTERS available via h.
428 #==============================================================================
428 #==============================================================================
429 from kallithea.lib.vcs.utils import author_name, author_email
429 from kallithea.lib.vcs.utils import author_name, author_email
430 from kallithea.lib.utils2 import credentials_filter, age as _age
430 from kallithea.lib.utils2 import credentials_filter, age as _age
431 from kallithea.model.db import User, ChangesetStatus, PullRequest
431 from kallithea.model.db import User, ChangesetStatus, PullRequest
432
432
433 age = lambda x, y=False: _age(x, y)
433 age = lambda x, y=False: _age(x, y)
434 capitalize = lambda x: x.capitalize()
434 capitalize = lambda x: x.capitalize()
435 email = author_email
435 email = author_email
436 short_id = lambda x: x[:12]
436 short_id = lambda x: x[:12]
437 hide_credentials = lambda x: ''.join(credentials_filter(x))
437 hide_credentials = lambda x: ''.join(credentials_filter(x))
438
438
439
439
440 def show_id(cs):
440 def show_id(cs):
441 """
441 """
442 Configurable function that shows ID
442 Configurable function that shows ID
443 by default it's r123:fffeeefffeee
443 by default it's r123:fffeeefffeee
444
444
445 :param cs: changeset instance
445 :param cs: changeset instance
446 """
446 """
447 from kallithea import CONFIG
447 from kallithea import CONFIG
448 def_len = safe_int(CONFIG.get('show_sha_length', 12))
448 def_len = safe_int(CONFIG.get('show_sha_length', 12))
449 show_rev = str2bool(CONFIG.get('show_revision_number', False))
449 show_rev = str2bool(CONFIG.get('show_revision_number', False))
450
450
451 raw_id = cs.raw_id[:def_len]
451 raw_id = cs.raw_id[:def_len]
452 if show_rev:
452 if show_rev:
453 return 'r%s:%s' % (cs.revision, raw_id)
453 return 'r%s:%s' % (cs.revision, raw_id)
454 else:
454 else:
455 return raw_id
455 return raw_id
456
456
457
457
458 def fmt_date(date):
458 def fmt_date(date):
459 if date:
459 if date:
460 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
460 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
461
461
462 return ""
462 return ""
463
463
464
464
465 def is_git(repository):
465 def is_git(repository):
466 if hasattr(repository, 'alias'):
466 if hasattr(repository, 'alias'):
467 _type = repository.alias
467 _type = repository.alias
468 elif hasattr(repository, 'repo_type'):
468 elif hasattr(repository, 'repo_type'):
469 _type = repository.repo_type
469 _type = repository.repo_type
470 else:
470 else:
471 _type = repository
471 _type = repository
472 return _type == 'git'
472 return _type == 'git'
473
473
474
474
475 def is_hg(repository):
475 def is_hg(repository):
476 if hasattr(repository, 'alias'):
476 if hasattr(repository, 'alias'):
477 _type = repository.alias
477 _type = repository.alias
478 elif hasattr(repository, 'repo_type'):
478 elif hasattr(repository, 'repo_type'):
479 _type = repository.repo_type
479 _type = repository.repo_type
480 else:
480 else:
481 _type = repository
481 _type = repository
482 return _type == 'hg'
482 return _type == 'hg'
483
483
484
484
485 @cache_region('long_term', 'user_or_none')
485 @cache_region('long_term', 'user_or_none')
486 def user_or_none(author):
486 def user_or_none(author):
487 """Try to match email part of VCS committer string with a local user - or return None"""
487 """Try to match email part of VCS committer string with a local user - or return None"""
488 email = author_email(author)
488 email = author_email(author)
489 if email:
489 if email:
490 user = User.get_by_email(email, cache=True) # cache will only use sql_cache_short
490 user = User.get_by_email(email, cache=True) # cache will only use sql_cache_short
491 if user is not None:
491 if user is not None:
492 return user
492 return user
493 return None
493 return None
494
494
495 def email_or_none(author):
495 def email_or_none(author):
496 """Try to match email part of VCS committer string with a local user.
496 """Try to match email part of VCS committer string with a local user.
497 Return primary email of user, email part of the specified author name, or None."""
497 Return primary email of user, email part of the specified author name, or None."""
498 if not author:
498 if not author:
499 return None
499 return None
500 user = user_or_none(author)
500 user = user_or_none(author)
501 if user is not None:
501 if user is not None:
502 return user.email # always use main email address - not necessarily the one used to find user
502 return user.email # always use main email address - not necessarily the one used to find user
503
503
504 # extract email from the commit string
504 # extract email from the commit string
505 email = author_email(author)
505 email = author_email(author)
506 if email:
506 if email:
507 return email
507 return email
508
508
509 # No valid email, not a valid user in the system, none!
509 # No valid email, not a valid user in the system, none!
510 return None
510 return None
511
511
512 def person(author, show_attr="username"):
512 def person(author, show_attr="username"):
513 """Find the user identified by 'author', return one of the users attributes,
513 """Find the user identified by 'author', return one of the users attributes,
514 default to the username attribute, None if there is no user"""
514 default to the username attribute, None if there is no user"""
515 # attr to return from fetched user
515 # attr to return from fetched user
516 person_getter = lambda usr: getattr(usr, show_attr)
516 person_getter = lambda usr: getattr(usr, show_attr)
517
517
518 # if author is already an instance use it for extraction
518 # if author is already an instance use it for extraction
519 if isinstance(author, User):
519 if isinstance(author, User):
520 return person_getter(author)
520 return person_getter(author)
521
521
522 user = user_or_none(author)
522 user = user_or_none(author)
523 if user is not None:
523 if user is not None:
524 return person_getter(user)
524 return person_getter(user)
525
525
526 # Still nothing? Just pass back the author name if any, else the email
526 # Still nothing? Just pass back the author name if any, else the email
527 return author_name(author) or email(author)
527 return author_name(author) or email(author)
528
528
529
529
530 def person_by_id(id_, show_attr="username"):
530 def person_by_id(id_, show_attr="username"):
531 # attr to return from fetched user
531 # attr to return from fetched user
532 person_getter = lambda usr: getattr(usr, show_attr)
532 person_getter = lambda usr: getattr(usr, show_attr)
533
533
534 #maybe it's an ID ?
534 #maybe it's an ID ?
535 if str(id_).isdigit() or isinstance(id_, int):
535 if str(id_).isdigit() or isinstance(id_, int):
536 id_ = int(id_)
536 id_ = int(id_)
537 user = User.get(id_)
537 user = User.get(id_)
538 if user is not None:
538 if user is not None:
539 return person_getter(user)
539 return person_getter(user)
540 return id_
540 return id_
541
541
542
542
543 def desc_stylize(value):
543 def desc_stylize(value):
544 """
544 """
545 converts tags from value into html equivalent
545 converts tags from value into html equivalent
546
546
547 :param value:
547 :param value:
548 """
548 """
549 if not value:
549 if not value:
550 return ''
550 return ''
551
551
552 value = re.sub(r'\[see\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
552 value = re.sub(r'\[see\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
553 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
553 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
554 value = re.sub(r'\[license\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
554 value = re.sub(r'\[license\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
555 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
555 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
556 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=&gt;\ *([a-zA-Z0-9\-\/]*)\]',
556 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=&gt;\ *([a-zA-Z0-9\-\/]*)\]',
557 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
557 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
558 value = re.sub(r'\[(lang|language)\ \=&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
558 value = re.sub(r'\[(lang|language)\ \=&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
559 '<div class="metatag" tag="lang">\\2</div>', value)
559 '<div class="metatag" tag="lang">\\2</div>', value)
560 value = re.sub(r'\[([a-z]+)\]',
560 value = re.sub(r'\[([a-z]+)\]',
561 '<div class="metatag" tag="\\1">\\1</div>', value)
561 '<div class="metatag" tag="\\1">\\1</div>', value)
562
562
563 return value
563 return value
564
564
565
565
566 def boolicon(value):
566 def boolicon(value):
567 """Returns boolean value of a value, represented as small html image of true/false
567 """Returns boolean value of a value, represented as small html image of true/false
568 icons
568 icons
569
569
570 :param value: value
570 :param value: value
571 """
571 """
572
572
573 if value:
573 if value:
574 return HTML.tag('i', class_="icon-ok")
574 return HTML.tag('i', class_="icon-ok")
575 else:
575 else:
576 return HTML.tag('i', class_="icon-minus-circled")
576 return HTML.tag('i', class_="icon-minus-circled")
577
577
578
578
579 def action_parser(user_log, feed=False, parse_cs=False):
579 def action_parser(user_log, feed=False, parse_cs=False):
580 """
580 """
581 This helper will action_map the specified string action into translated
581 This helper will action_map the specified string action into translated
582 fancy names with icons and links
582 fancy names with icons and links
583
583
584 :param user_log: user log instance
584 :param user_log: user log instance
585 :param feed: use output for feeds (no html and fancy icons)
585 :param feed: use output for feeds (no html and fancy icons)
586 :param parse_cs: parse Changesets into VCS instances
586 :param parse_cs: parse Changesets into VCS instances
587 """
587 """
588
588
589 action = user_log.action
589 action = user_log.action
590 action_params = ' '
590 action_params = ' '
591
591
592 x = action.split(':')
592 x = action.split(':')
593
593
594 if len(x) > 1:
594 if len(x) > 1:
595 action, action_params = x
595 action, action_params = x
596
596
597 def get_cs_links():
597 def get_cs_links():
598 revs_limit = 3 # display this amount always
598 revs_limit = 3 # display this amount always
599 revs_top_limit = 50 # show upto this amount of changesets hidden
599 revs_top_limit = 50 # show upto this amount of changesets hidden
600 revs_ids = action_params.split(',')
600 revs_ids = action_params.split(',')
601 deleted = user_log.repository is None
601 deleted = user_log.repository is None
602 if deleted:
602 if deleted:
603 return ','.join(revs_ids)
603 return ','.join(revs_ids)
604
604
605 repo_name = user_log.repository.repo_name
605 repo_name = user_log.repository.repo_name
606
606
607 def lnk(rev, repo_name):
607 def lnk(rev, repo_name):
608 lazy_cs = False
608 lazy_cs = False
609 title_ = None
609 title_ = None
610 url_ = '#'
610 url_ = '#'
611 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
611 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
612 if rev.op and rev.ref_name:
612 if rev.op and rev.ref_name:
613 if rev.op == 'delete_branch':
613 if rev.op == 'delete_branch':
614 lbl = _('Deleted branch: %s') % rev.ref_name
614 lbl = _('Deleted branch: %s') % rev.ref_name
615 elif rev.op == 'tag':
615 elif rev.op == 'tag':
616 lbl = _('Created tag: %s') % rev.ref_name
616 lbl = _('Created tag: %s') % rev.ref_name
617 else:
617 else:
618 lbl = 'Unknown operation %s' % rev.op
618 lbl = 'Unknown operation %s' % rev.op
619 else:
619 else:
620 lazy_cs = True
620 lazy_cs = True
621 lbl = rev.short_id[:8]
621 lbl = rev.short_id[:8]
622 url_ = url('changeset_home', repo_name=repo_name,
622 url_ = url('changeset_home', repo_name=repo_name,
623 revision=rev.raw_id)
623 revision=rev.raw_id)
624 else:
624 else:
625 # changeset cannot be found - it might have been stripped or removed
625 # changeset cannot be found - it might have been stripped or removed
626 lbl = rev[:12]
626 lbl = rev[:12]
627 title_ = _('Changeset not found')
627 title_ = _('Changeset not found')
628 if parse_cs:
628 if parse_cs:
629 return link_to(lbl, url_, title=title_, class_='tooltip')
629 return link_to(lbl, url_, title=title_, class_='tooltip')
630 return link_to(lbl, url_, raw_id=rev.raw_id, repo_name=repo_name,
630 return link_to(lbl, url_, raw_id=rev.raw_id, repo_name=repo_name,
631 class_='lazy-cs' if lazy_cs else '')
631 class_='lazy-cs' if lazy_cs else '')
632
632
633 def _get_op(rev_txt):
633 def _get_op(rev_txt):
634 _op = None
634 _op = None
635 _name = rev_txt
635 _name = rev_txt
636 if len(rev_txt.split('=>')) == 2:
636 if len(rev_txt.split('=>')) == 2:
637 _op, _name = rev_txt.split('=>')
637 _op, _name = rev_txt.split('=>')
638 return _op, _name
638 return _op, _name
639
639
640 revs = []
640 revs = []
641 if len(filter(lambda v: v != '', revs_ids)) > 0:
641 if len(filter(lambda v: v != '', revs_ids)) > 0:
642 repo = None
642 repo = None
643 for rev in revs_ids[:revs_top_limit]:
643 for rev in revs_ids[:revs_top_limit]:
644 _op, _name = _get_op(rev)
644 _op, _name = _get_op(rev)
645
645
646 # we want parsed changesets, or new log store format is bad
646 # we want parsed changesets, or new log store format is bad
647 if parse_cs:
647 if parse_cs:
648 try:
648 try:
649 if repo is None:
649 if repo is None:
650 repo = user_log.repository.scm_instance
650 repo = user_log.repository.scm_instance
651 _rev = repo.get_changeset(rev)
651 _rev = repo.get_changeset(rev)
652 revs.append(_rev)
652 revs.append(_rev)
653 except ChangesetDoesNotExistError:
653 except ChangesetDoesNotExistError:
654 log.error('cannot find revision %s in this repo', rev)
654 log.error('cannot find revision %s in this repo', rev)
655 revs.append(rev)
655 revs.append(rev)
656 else:
656 else:
657 _rev = AttributeDict({
657 _rev = AttributeDict({
658 'short_id': rev[:12],
658 'short_id': rev[:12],
659 'raw_id': rev,
659 'raw_id': rev,
660 'message': '',
660 'message': '',
661 'op': _op,
661 'op': _op,
662 'ref_name': _name
662 'ref_name': _name
663 })
663 })
664 revs.append(_rev)
664 revs.append(_rev)
665 cs_links = [" " + ', '.join(
665 cs_links = [" " + ', '.join(
666 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
666 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
667 )]
667 )]
668 _op1, _name1 = _get_op(revs_ids[0])
668 _op1, _name1 = _get_op(revs_ids[0])
669 _op2, _name2 = _get_op(revs_ids[-1])
669 _op2, _name2 = _get_op(revs_ids[-1])
670
670
671 _rev = '%s...%s' % (_name1, _name2)
671 _rev = '%s...%s' % (_name1, _name2)
672
672
673 compare_view = (
673 compare_view = (
674 ' <div class="compare_view tooltip" title="%s">'
674 ' <div class="compare_view tooltip" title="%s">'
675 '<a href="%s">%s</a> </div>' % (
675 '<a href="%s">%s</a> </div>' % (
676 _('Show all combined changesets %s->%s') % (
676 _('Show all combined changesets %s->%s') % (
677 revs_ids[0][:12], revs_ids[-1][:12]
677 revs_ids[0][:12], revs_ids[-1][:12]
678 ),
678 ),
679 url('changeset_home', repo_name=repo_name,
679 url('changeset_home', repo_name=repo_name,
680 revision=_rev
680 revision=_rev
681 ),
681 ),
682 _('Compare view')
682 _('Compare view')
683 )
683 )
684 )
684 )
685
685
686 # if we have exactly one more than normally displayed
686 # if we have exactly one more than normally displayed
687 # just display it, takes less space than displaying
687 # just display it, takes less space than displaying
688 # "and 1 more revisions"
688 # "and 1 more revisions"
689 if len(revs_ids) == revs_limit + 1:
689 if len(revs_ids) == revs_limit + 1:
690 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
690 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
691
691
692 # hidden-by-default ones
692 # hidden-by-default ones
693 if len(revs_ids) > revs_limit + 1:
693 if len(revs_ids) > revs_limit + 1:
694 uniq_id = revs_ids[0]
694 uniq_id = revs_ids[0]
695 html_tmpl = (
695 html_tmpl = (
696 '<span> %s <a class="show_more" id="_%s" '
696 '<span> %s <a class="show_more" id="_%s" '
697 'href="#more">%s</a> %s</span>'
697 'href="#more">%s</a> %s</span>'
698 )
698 )
699 if not feed:
699 if not feed:
700 cs_links.append(html_tmpl % (
700 cs_links.append(html_tmpl % (
701 _('and'),
701 _('and'),
702 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
702 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
703 _('revisions')
703 _('revisions')
704 )
704 )
705 )
705 )
706
706
707 if not feed:
707 if not feed:
708 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
708 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
709 else:
709 else:
710 html_tmpl = '<span id="%s"> %s </span>'
710 html_tmpl = '<span id="%s"> %s </span>'
711
711
712 morelinks = ', '.join(
712 morelinks = ', '.join(
713 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
713 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
714 )
714 )
715
715
716 if len(revs_ids) > revs_top_limit:
716 if len(revs_ids) > revs_top_limit:
717 morelinks += ', ...'
717 morelinks += ', ...'
718
718
719 cs_links.append(html_tmpl % (uniq_id, morelinks))
719 cs_links.append(html_tmpl % (uniq_id, morelinks))
720 if len(revs) > 1:
720 if len(revs) > 1:
721 cs_links.append(compare_view)
721 cs_links.append(compare_view)
722 return ''.join(cs_links)
722 return ''.join(cs_links)
723
723
724 def get_fork_name():
724 def get_fork_name():
725 repo_name = action_params
725 repo_name = action_params
726 url_ = url('summary_home', repo_name=repo_name)
726 url_ = url('summary_home', repo_name=repo_name)
727 return _('Fork name %s') % link_to(action_params, url_)
727 return _('Fork name %s') % link_to(action_params, url_)
728
728
729 def get_user_name():
729 def get_user_name():
730 user_name = action_params
730 user_name = action_params
731 return user_name
731 return user_name
732
732
733 def get_users_group():
733 def get_users_group():
734 group_name = action_params
734 group_name = action_params
735 return group_name
735 return group_name
736
736
737 def get_pull_request():
737 def get_pull_request():
738 pull_request_id = action_params
738 pull_request_id = action_params
739 nice_id = PullRequest.make_nice_id(pull_request_id)
739 nice_id = PullRequest.make_nice_id(pull_request_id)
740
740
741 deleted = user_log.repository is None
741 deleted = user_log.repository is None
742 if deleted:
742 if deleted:
743 repo_name = user_log.repository_name
743 repo_name = user_log.repository_name
744 else:
744 else:
745 repo_name = user_log.repository.repo_name
745 repo_name = user_log.repository.repo_name
746
746
747 return link_to(_('Pull request %s') % nice_id,
747 return link_to(_('Pull request %s') % nice_id,
748 url('pullrequest_show', repo_name=repo_name,
748 url('pullrequest_show', repo_name=repo_name,
749 pull_request_id=pull_request_id))
749 pull_request_id=pull_request_id))
750
750
751 def get_archive_name():
751 def get_archive_name():
752 archive_name = action_params
752 archive_name = action_params
753 return archive_name
753 return archive_name
754
754
755 # action : translated str, callback(extractor), icon
755 # action : translated str, callback(extractor), icon
756 action_map = {
756 action_map = {
757 'user_deleted_repo': (_('[deleted] repository'),
757 'user_deleted_repo': (_('[deleted] repository'),
758 None, 'icon-trashcan'),
758 None, 'icon-trashcan'),
759 'user_created_repo': (_('[created] repository'),
759 'user_created_repo': (_('[created] repository'),
760 None, 'icon-plus'),
760 None, 'icon-plus'),
761 'user_created_fork': (_('[created] repository as fork'),
761 'user_created_fork': (_('[created] repository as fork'),
762 None, 'icon-fork'),
762 None, 'icon-fork'),
763 'user_forked_repo': (_('[forked] repository'),
763 'user_forked_repo': (_('[forked] repository'),
764 get_fork_name, 'icon-fork'),
764 get_fork_name, 'icon-fork'),
765 'user_updated_repo': (_('[updated] repository'),
765 'user_updated_repo': (_('[updated] repository'),
766 None, 'icon-pencil'),
766 None, 'icon-pencil'),
767 'user_downloaded_archive': (_('[downloaded] archive from repository'),
767 'user_downloaded_archive': (_('[downloaded] archive from repository'),
768 get_archive_name, 'icon-download-cloud'),
768 get_archive_name, 'icon-download-cloud'),
769 'admin_deleted_repo': (_('[delete] repository'),
769 'admin_deleted_repo': (_('[delete] repository'),
770 None, 'icon-trashcan'),
770 None, 'icon-trashcan'),
771 'admin_created_repo': (_('[created] repository'),
771 'admin_created_repo': (_('[created] repository'),
772 None, 'icon-plus'),
772 None, 'icon-plus'),
773 'admin_forked_repo': (_('[forked] repository'),
773 'admin_forked_repo': (_('[forked] repository'),
774 None, 'icon-fork'),
774 None, 'icon-fork'),
775 'admin_updated_repo': (_('[updated] repository'),
775 'admin_updated_repo': (_('[updated] repository'),
776 None, 'icon-pencil'),
776 None, 'icon-pencil'),
777 'admin_created_user': (_('[created] user'),
777 'admin_created_user': (_('[created] user'),
778 get_user_name, 'icon-user'),
778 get_user_name, 'icon-user'),
779 'admin_updated_user': (_('[updated] user'),
779 'admin_updated_user': (_('[updated] user'),
780 get_user_name, 'icon-user'),
780 get_user_name, 'icon-user'),
781 'admin_created_users_group': (_('[created] user group'),
781 'admin_created_users_group': (_('[created] user group'),
782 get_users_group, 'icon-pencil'),
782 get_users_group, 'icon-pencil'),
783 'admin_updated_users_group': (_('[updated] user group'),
783 'admin_updated_users_group': (_('[updated] user group'),
784 get_users_group, 'icon-pencil'),
784 get_users_group, 'icon-pencil'),
785 'user_commented_revision': (_('[commented] on revision in repository'),
785 'user_commented_revision': (_('[commented] on revision in repository'),
786 get_cs_links, 'icon-comment'),
786 get_cs_links, 'icon-comment'),
787 'user_commented_pull_request': (_('[commented] on pull request for'),
787 'user_commented_pull_request': (_('[commented] on pull request for'),
788 get_pull_request, 'icon-comment'),
788 get_pull_request, 'icon-comment'),
789 'user_closed_pull_request': (_('[closed] pull request for'),
789 'user_closed_pull_request': (_('[closed] pull request for'),
790 get_pull_request, 'icon-ok'),
790 get_pull_request, 'icon-ok'),
791 'push': (_('[pushed] into'),
791 'push': (_('[pushed] into'),
792 get_cs_links, 'icon-move-up'),
792 get_cs_links, 'icon-move-up'),
793 'push_local': (_('[committed via Kallithea] into repository'),
793 'push_local': (_('[committed via Kallithea] into repository'),
794 get_cs_links, 'icon-pencil'),
794 get_cs_links, 'icon-pencil'),
795 'push_remote': (_('[pulled from remote] into repository'),
795 'push_remote': (_('[pulled from remote] into repository'),
796 get_cs_links, 'icon-move-up'),
796 get_cs_links, 'icon-move-up'),
797 'pull': (_('[pulled] from'),
797 'pull': (_('[pulled] from'),
798 None, 'icon-move-down'),
798 None, 'icon-move-down'),
799 'started_following_repo': (_('[started following] repository'),
799 'started_following_repo': (_('[started following] repository'),
800 None, 'icon-heart'),
800 None, 'icon-heart'),
801 'stopped_following_repo': (_('[stopped following] repository'),
801 'stopped_following_repo': (_('[stopped following] repository'),
802 None, 'icon-heart-empty'),
802 None, 'icon-heart-empty'),
803 }
803 }
804
804
805 action_str = action_map.get(action, action)
805 action_str = action_map.get(action, action)
806 if feed:
806 if feed:
807 action = action_str[0].replace('[', '').replace(']', '')
807 action = action_str[0].replace('[', '').replace(']', '')
808 else:
808 else:
809 action = action_str[0] \
809 action = action_str[0] \
810 .replace('[', '<span class="journal_highlight">') \
810 .replace('[', '<span class="journal_highlight">') \
811 .replace(']', '</span>')
811 .replace(']', '</span>')
812
812
813 action_params_func = lambda: ""
813 action_params_func = lambda: ""
814
814
815 if callable(action_str[1]):
815 if callable(action_str[1]):
816 action_params_func = action_str[1]
816 action_params_func = action_str[1]
817
817
818 def action_parser_icon():
818 def action_parser_icon():
819 action = user_log.action
819 action = user_log.action
820 action_params = None
820 action_params = None
821 x = action.split(':')
821 x = action.split(':')
822
822
823 if len(x) > 1:
823 if len(x) > 1:
824 action, action_params = x
824 action, action_params = x
825
825
826 tmpl = """<i class="%s" alt="%s"></i>"""
826 tmpl = """<i class="%s" alt="%s"></i>"""
827 ico = action_map.get(action, ['', '', ''])[2]
827 ico = action_map.get(action, ['', '', ''])[2]
828 return literal(tmpl % (ico, action))
828 return literal(tmpl % (ico, action))
829
829
830 # returned callbacks we need to call to get
830 # returned callbacks we need to call to get
831 return [lambda: literal(action), action_params_func, action_parser_icon]
831 return [lambda: literal(action), action_params_func, action_parser_icon]
832
832
833
833
834
834
835 #==============================================================================
835 #==============================================================================
836 # PERMS
836 # PERMS
837 #==============================================================================
837 #==============================================================================
838 from kallithea.lib.auth import HasPermissionAny, \
838 from kallithea.lib.auth import HasPermissionAny, \
839 HasRepoPermissionAny, HasRepoGroupPermissionAny
839 HasRepoPermissionAny, HasRepoGroupPermissionAny
840
840
841
841
842 #==============================================================================
842 #==============================================================================
843 # GRAVATAR URL
843 # GRAVATAR URL
844 #==============================================================================
844 #==============================================================================
845 def gravatar_div(email_address, cls='', size=30, **div_attributes):
845 def gravatar_div(email_address, cls='', size=30, **div_attributes):
846 """Return an html literal with a div around a gravatar if they are enabled.
846 """Return an html literal with a div around a gravatar if they are enabled.
847 Extra keyword parameters starting with 'div_' will get the prefix removed
847 Extra keyword parameters starting with 'div_' will get the prefix removed
848 and be used as attributes on the div. The default class is 'gravatar'.
848 and be used as attributes on the div. The default class is 'gravatar'.
849 """
849 """
850 from pylons import tmpl_context as c
850 from pylons 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:], escape(v)))
858 attributes.append(' %s="%s"' % (k[4:], escape(v)))
859 return literal("""<div%s>%s</div>""" %
859 return literal("""<div%s>%s</div>""" %
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 def gravatar(email_address, cls='', size=30):
863 def gravatar(email_address, cls='', size=30):
864 """return html element of the gravatar
864 """return html element of the gravatar
865
865
866 This method will return an <img> with the resolution double the size (for
866 This method will return an <img> with the resolution double the size (for
867 retina screens) of the image. If the url returned from gravatar_url is
867 retina screens) of the image. If the url returned from gravatar_url is
868 empty then we fallback to using an icon.
868 empty then we fallback to using an icon.
869
869
870 """
870 """
871 from pylons import tmpl_context as c
871 from pylons import tmpl_context as c
872 if not c.visual.use_gravatar:
872 if not c.visual.use_gravatar:
873 return ''
873 return ''
874
874
875 src = gravatar_url(email_address, size * 2)
875 src = gravatar_url(email_address, size * 2)
876
876
877 if src:
877 if src:
878 # here it makes sense to use style="width: ..." (instead of, say, a
878 # here it makes sense to use style="width: ..." (instead of, say, a
879 # stylesheet) because we using this to generate a high-res (retina) size
879 # stylesheet) because we using this to generate a high-res (retina) size
880 html = ('<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
880 html = ('<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
881 .format(cls=cls, size=size, src=src))
881 .format(cls=cls, size=size, src=src))
882
882
883 else:
883 else:
884 # if src is empty then there was no gravatar, so we use a font icon
884 # if src is empty then there was no gravatar, so we use a font icon
885 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
885 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
886 .format(cls=cls, size=size, src=src))
886 .format(cls=cls, size=size, src=src))
887
887
888 return literal(html)
888 return literal(html)
889
889
890 def gravatar_url(email_address, size=30, default=''):
890 def gravatar_url(email_address, size=30, default=''):
891 # doh, we need to re-import those to mock it later
891 # doh, we need to re-import those to mock it later
892 from pylons import url
892 from pylons import url
893 from pylons import tmpl_context as c
893 from pylons import tmpl_context as c
894 if not c.visual.use_gravatar:
894 if not c.visual.use_gravatar:
895 return ""
895 return ""
896
896
897 _def = 'anonymous@kallithea-scm.org' # default gravatar
897 _def = 'anonymous@kallithea-scm.org' # default gravatar
898 email_address = email_address or _def
898 email_address = email_address or _def
899
899
900 if email_address == _def:
900 if email_address == _def:
901 return default
901 return default
902
902
903 parsed_url = urlparse.urlparse(url.current(qualified=True))
903 parsed_url = urlparse.urlparse(url.current(qualified=True))
904 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
904 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
905 .replace('{email}', email_address) \
905 .replace('{email}', email_address) \
906 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
906 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
907 .replace('{netloc}', parsed_url.netloc) \
907 .replace('{netloc}', parsed_url.netloc) \
908 .replace('{scheme}', parsed_url.scheme) \
908 .replace('{scheme}', parsed_url.scheme) \
909 .replace('{size}', safe_str(size))
909 .replace('{size}', safe_str(size))
910 return url
910 return url
911
911
912 class Page(_Page):
912 class Page(_Page):
913 """
913 """
914 Custom pager to match rendering style with YUI paginator
914 Custom pager to match rendering style with YUI paginator
915 """
915 """
916
916
917 def __init__(self, *args, **kwargs):
917 def __init__(self, *args, **kwargs):
918 kwargs.setdefault('url', url.current)
918 kwargs.setdefault('url', url.current)
919 _Page.__init__(self, *args, **kwargs)
919 _Page.__init__(self, *args, **kwargs)
920
920
921 def _get_pos(self, cur_page, max_page, items):
921 def _get_pos(self, cur_page, max_page, items):
922 edge = (items / 2) + 1
922 edge = (items / 2) + 1
923 if (cur_page <= edge):
923 if (cur_page <= edge):
924 radius = max(items / 2, items - cur_page)
924 radius = max(items / 2, items - cur_page)
925 elif (max_page - cur_page) < edge:
925 elif (max_page - cur_page) < edge:
926 radius = (items - 1) - (max_page - cur_page)
926 radius = (items - 1) - (max_page - cur_page)
927 else:
927 else:
928 radius = items / 2
928 radius = items / 2
929
929
930 left = max(1, (cur_page - (radius)))
930 left = max(1, (cur_page - (radius)))
931 right = min(max_page, cur_page + (radius))
931 right = min(max_page, cur_page + (radius))
932 return left, cur_page, right
932 return left, cur_page, right
933
933
934 def _range(self, regexp_match):
934 def _range(self, regexp_match):
935 """
935 """
936 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
936 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
937
937
938 Arguments:
938 Arguments:
939
939
940 regexp_match
940 regexp_match
941 A "re" (regular expressions) match object containing the
941 A "re" (regular expressions) match object containing the
942 radius of linked pages around the current page in
942 radius of linked pages around the current page in
943 regexp_match.group(1) as a string
943 regexp_match.group(1) as a string
944
944
945 This function is supposed to be called as a callable in
945 This function is supposed to be called as a callable in
946 re.sub.
946 re.sub.
947
947
948 """
948 """
949 radius = int(regexp_match.group(1))
949 radius = int(regexp_match.group(1))
950
950
951 # Compute the first and last page number within the radius
951 # Compute the first and last page number within the radius
952 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
952 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
953 # -> leftmost_page = 5
953 # -> leftmost_page = 5
954 # -> rightmost_page = 9
954 # -> rightmost_page = 9
955 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
955 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
956 self.last_page,
956 self.last_page,
957 (radius * 2) + 1)
957 (radius * 2) + 1)
958 nav_items = []
958 nav_items = []
959
959
960 # Create a link to the first page (unless we are on the first page
960 # Create a link to the first page (unless we are on the first page
961 # or there would be no need to insert '..' spacers)
961 # or there would be no need to insert '..' spacers)
962 if self.page != self.first_page and self.first_page < leftmost_page:
962 if self.page != self.first_page and self.first_page < leftmost_page:
963 nav_items.append(self._pagerlink(self.first_page, self.first_page))
963 nav_items.append(self._pagerlink(self.first_page, self.first_page))
964
964
965 # Insert dots if there are pages between the first page
965 # Insert dots if there are pages between the first page
966 # and the currently displayed page range
966 # and the currently displayed page range
967 if leftmost_page - self.first_page > 1:
967 if leftmost_page - self.first_page > 1:
968 # Wrap in a SPAN tag if nolink_attr is set
968 # Wrap in a SPAN tag if nolink_attr is set
969 text_ = '..'
969 text_ = '..'
970 if self.dotdot_attr:
970 if self.dotdot_attr:
971 text_ = HTML.span(c=text_, **self.dotdot_attr)
971 text_ = HTML.span(c=text_, **self.dotdot_attr)
972 nav_items.append(text_)
972 nav_items.append(text_)
973
973
974 for thispage in xrange(leftmost_page, rightmost_page + 1):
974 for thispage in xrange(leftmost_page, rightmost_page + 1):
975 # Highlight the current page number and do not use a link
975 # Highlight the current page number and do not use a link
976 text_ = str(thispage)
976 text_ = str(thispage)
977 if thispage == self.page:
977 if thispage == self.page:
978 # Wrap in a SPAN tag if nolink_attr is set
978 # Wrap in a SPAN tag if nolink_attr is set
979 if self.curpage_attr:
979 if self.curpage_attr:
980 text_ = HTML.span(c=text_, **self.curpage_attr)
980 text_ = HTML.span(c=text_, **self.curpage_attr)
981 nav_items.append(text_)
981 nav_items.append(text_)
982 # Otherwise create just a link to that page
982 # Otherwise create just a link to that page
983 else:
983 else:
984 nav_items.append(self._pagerlink(thispage, text_))
984 nav_items.append(self._pagerlink(thispage, text_))
985
985
986 # Insert dots if there are pages between the displayed
986 # Insert dots if there are pages between the displayed
987 # page numbers and the end of the page range
987 # page numbers and the end of the page range
988 if self.last_page - rightmost_page > 1:
988 if self.last_page - rightmost_page > 1:
989 text_ = '..'
989 text_ = '..'
990 # Wrap in a SPAN tag if nolink_attr is set
990 # Wrap in a SPAN tag if nolink_attr is set
991 if self.dotdot_attr:
991 if self.dotdot_attr:
992 text_ = HTML.span(c=text_, **self.dotdot_attr)
992 text_ = HTML.span(c=text_, **self.dotdot_attr)
993 nav_items.append(text_)
993 nav_items.append(text_)
994
994
995 # Create a link to the very last page (unless we are on the last
995 # Create a link to the very last page (unless we are on the last
996 # page or there would be no need to insert '..' spacers)
996 # page or there would be no need to insert '..' spacers)
997 if self.page != self.last_page and rightmost_page < self.last_page:
997 if self.page != self.last_page and rightmost_page < self.last_page:
998 nav_items.append(self._pagerlink(self.last_page, self.last_page))
998 nav_items.append(self._pagerlink(self.last_page, self.last_page))
999
999
1000 #_page_link = url.current()
1000 #_page_link = url.current()
1001 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1001 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1002 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1002 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1003 return self.separator.join(nav_items)
1003 return self.separator.join(nav_items)
1004
1004
1005 def pager(self, format='~2~', page_param='page', partial_param='partial',
1005 def pager(self, format='~2~', page_param='page', partial_param='partial',
1006 show_if_single_page=False, separator=' ', onclick=None,
1006 show_if_single_page=False, separator=' ', onclick=None,
1007 symbol_first='<<', symbol_last='>>',
1007 symbol_first='<<', symbol_last='>>',
1008 symbol_previous='<', symbol_next='>',
1008 symbol_previous='<', symbol_next='>',
1009 link_attr=None,
1009 link_attr=None,
1010 curpage_attr=None,
1010 curpage_attr=None,
1011 dotdot_attr=None, **kwargs):
1011 dotdot_attr=None, **kwargs):
1012 self.curpage_attr = curpage_attr or {'class': 'pager_curpage'}
1012 self.curpage_attr = curpage_attr or {'class': 'pager_curpage'}
1013 self.separator = separator
1013 self.separator = separator
1014 self.pager_kwargs = kwargs
1014 self.pager_kwargs = kwargs
1015 self.page_param = page_param
1015 self.page_param = page_param
1016 self.partial_param = partial_param
1016 self.partial_param = partial_param
1017 self.onclick = onclick
1017 self.onclick = onclick
1018 self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
1018 self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
1019 self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
1019 self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
1020
1020
1021 # Don't show navigator if there is no more than one page
1021 # Don't show navigator if there is no more than one page
1022 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1022 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1023 return ''
1023 return ''
1024
1024
1025 from string import Template
1025 from string import Template
1026 # Replace ~...~ in token format by range of pages
1026 # Replace ~...~ in token format by range of pages
1027 result = re.sub(r'~(\d+)~', self._range, format)
1027 result = re.sub(r'~(\d+)~', self._range, format)
1028
1028
1029 # Interpolate '%' variables
1029 # Interpolate '%' variables
1030 result = Template(result).safe_substitute({
1030 result = Template(result).safe_substitute({
1031 'first_page': self.first_page,
1031 'first_page': self.first_page,
1032 'last_page': self.last_page,
1032 'last_page': self.last_page,
1033 'page': self.page,
1033 'page': self.page,
1034 'page_count': self.page_count,
1034 'page_count': self.page_count,
1035 'items_per_page': self.items_per_page,
1035 'items_per_page': self.items_per_page,
1036 'first_item': self.first_item,
1036 'first_item': self.first_item,
1037 'last_item': self.last_item,
1037 'last_item': self.last_item,
1038 'item_count': self.item_count,
1038 'item_count': self.item_count,
1039 'link_first': self.page > self.first_page and \
1039 'link_first': self.page > self.first_page and \
1040 self._pagerlink(self.first_page, symbol_first) or '',
1040 self._pagerlink(self.first_page, symbol_first) or '',
1041 'link_last': self.page < self.last_page and \
1041 'link_last': self.page < self.last_page and \
1042 self._pagerlink(self.last_page, symbol_last) or '',
1042 self._pagerlink(self.last_page, symbol_last) or '',
1043 'link_previous': self.previous_page and \
1043 'link_previous': self.previous_page and \
1044 self._pagerlink(self.previous_page, symbol_previous) \
1044 self._pagerlink(self.previous_page, symbol_previous) \
1045 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1045 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1046 'link_next': self.next_page and \
1046 'link_next': self.next_page and \
1047 self._pagerlink(self.next_page, symbol_next) \
1047 self._pagerlink(self.next_page, symbol_next) \
1048 or HTML.span(symbol_next, class_="yui-pg-next")
1048 or HTML.span(symbol_next, class_="yui-pg-next")
1049 })
1049 })
1050
1050
1051 return literal(result)
1051 return literal(result)
1052
1052
1053
1053
1054 #==============================================================================
1054 #==============================================================================
1055 # REPO PAGER, PAGER FOR REPOSITORY
1055 # REPO PAGER, PAGER FOR REPOSITORY
1056 #==============================================================================
1056 #==============================================================================
1057 class RepoPage(Page):
1057 class RepoPage(Page):
1058
1058
1059 def __init__(self, collection, page=1, items_per_page=20,
1059 def __init__(self, collection, page=1, items_per_page=20,
1060 item_count=None, **kwargs):
1060 item_count=None, **kwargs):
1061
1061
1062 """Create a "RepoPage" instance. special pager for paging
1062 """Create a "RepoPage" instance. special pager for paging
1063 repository
1063 repository
1064 """
1064 """
1065 # TODO: call baseclass __init__
1065 # TODO: call baseclass __init__
1066 self._url_generator = kwargs.pop('url', url.current)
1066 self._url_generator = kwargs.pop('url', url.current)
1067
1067
1068 # Safe the kwargs class-wide so they can be used in the pager() method
1068 # Safe the kwargs class-wide so they can be used in the pager() method
1069 self.kwargs = kwargs
1069 self.kwargs = kwargs
1070
1070
1071 # Save a reference to the collection
1071 # Save a reference to the collection
1072 self.original_collection = collection
1072 self.original_collection = collection
1073
1073
1074 self.collection = collection
1074 self.collection = collection
1075
1075
1076 # The self.page is the number of the current page.
1076 # The self.page is the number of the current page.
1077 # The first page has the number 1!
1077 # The first page has the number 1!
1078 try:
1078 try:
1079 self.page = int(page) # make it int() if we get it as a string
1079 self.page = int(page) # make it int() if we get it as a string
1080 except (ValueError, TypeError):
1080 except (ValueError, TypeError):
1081 self.page = 1
1081 self.page = 1
1082
1082
1083 self.items_per_page = items_per_page
1083 self.items_per_page = items_per_page
1084
1084
1085 # Unless the user tells us how many items the collections has
1085 # Unless the user tells us how many items the collections has
1086 # we calculate that ourselves.
1086 # we calculate that ourselves.
1087 if item_count is not None:
1087 if item_count is not None:
1088 self.item_count = item_count
1088 self.item_count = item_count
1089 else:
1089 else:
1090 self.item_count = len(self.collection)
1090 self.item_count = len(self.collection)
1091
1091
1092 # Compute the number of the first and last available page
1092 # Compute the number of the first and last available page
1093 if self.item_count > 0:
1093 if self.item_count > 0:
1094 self.first_page = 1
1094 self.first_page = 1
1095 self.page_count = int(math.ceil(float(self.item_count) /
1095 self.page_count = int(math.ceil(float(self.item_count) /
1096 self.items_per_page))
1096 self.items_per_page))
1097 self.last_page = self.first_page + self.page_count - 1
1097 self.last_page = self.first_page + self.page_count - 1
1098
1098
1099 # Make sure that the requested page number is the range of
1099 # Make sure that the requested page number is the range of
1100 # valid pages
1100 # valid pages
1101 if self.page > self.last_page:
1101 if self.page > self.last_page:
1102 self.page = self.last_page
1102 self.page = self.last_page
1103 elif self.page < self.first_page:
1103 elif self.page < self.first_page:
1104 self.page = self.first_page
1104 self.page = self.first_page
1105
1105
1106 # Note: the number of items on this page can be less than
1106 # Note: the number of items on this page can be less than
1107 # items_per_page if the last page is not full
1107 # items_per_page if the last page is not full
1108 self.first_item = max(0, (self.item_count) - (self.page *
1108 self.first_item = max(0, (self.item_count) - (self.page *
1109 items_per_page))
1109 items_per_page))
1110 self.last_item = ((self.item_count - 1) - items_per_page *
1110 self.last_item = ((self.item_count - 1) - items_per_page *
1111 (self.page - 1))
1111 (self.page - 1))
1112
1112
1113 self.items = list(self.collection[self.first_item:self.last_item + 1])
1113 self.items = list(self.collection[self.first_item:self.last_item + 1])
1114
1114
1115 # Links to previous and next page
1115 # Links to previous and next page
1116 if self.page > self.first_page:
1116 if self.page > self.first_page:
1117 self.previous_page = self.page - 1
1117 self.previous_page = self.page - 1
1118 else:
1118 else:
1119 self.previous_page = None
1119 self.previous_page = None
1120
1120
1121 if self.page < self.last_page:
1121 if self.page < self.last_page:
1122 self.next_page = self.page + 1
1122 self.next_page = self.page + 1
1123 else:
1123 else:
1124 self.next_page = None
1124 self.next_page = None
1125
1125
1126 # No items available
1126 # No items available
1127 else:
1127 else:
1128 self.first_page = None
1128 self.first_page = None
1129 self.page_count = 0
1129 self.page_count = 0
1130 self.last_page = None
1130 self.last_page = None
1131 self.first_item = None
1131 self.first_item = None
1132 self.last_item = None
1132 self.last_item = None
1133 self.previous_page = None
1133 self.previous_page = None
1134 self.next_page = None
1134 self.next_page = None
1135 self.items = []
1135 self.items = []
1136
1136
1137 # This is a subclass of the 'list' type. Initialise the list now.
1137 # This is a subclass of the 'list' type. Initialise the list now.
1138 list.__init__(self, reversed(self.items))
1138 list.__init__(self, reversed(self.items))
1139
1139
1140
1140
1141 def changed_tooltip(nodes):
1141 def changed_tooltip(nodes):
1142 """
1142 """
1143 Generates a html string for changed nodes in changeset page.
1143 Generates a html string for changed nodes in changeset page.
1144 It limits the output to 30 entries
1144 It limits the output to 30 entries
1145
1145
1146 :param nodes: LazyNodesGenerator
1146 :param nodes: LazyNodesGenerator
1147 """
1147 """
1148 if nodes:
1148 if nodes:
1149 pref = ': <br/> '
1149 pref = ': <br/> '
1150 suf = ''
1150 suf = ''
1151 if len(nodes) > 30:
1151 if len(nodes) > 30:
1152 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1152 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1153 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1153 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1154 for x in nodes[:30]]) + suf)
1154 for x in nodes[:30]]) + suf)
1155 else:
1155 else:
1156 return ': ' + _('No files')
1156 return ': ' + _('No files')
1157
1157
1158
1158
1159 def repo_link(groups_and_repos):
1159 def repo_link(groups_and_repos):
1160 """
1160 """
1161 Makes a breadcrumbs link to repo within a group
1161 Makes a breadcrumbs link to repo within a group
1162 joins &raquo; on each group to create a fancy link
1162 joins &raquo; on each group to create a fancy link
1163
1163
1164 ex::
1164 ex::
1165 group >> subgroup >> repo
1165 group >> subgroup >> repo
1166
1166
1167 :param groups_and_repos:
1167 :param groups_and_repos:
1168 :param last_url:
1168 :param last_url:
1169 """
1169 """
1170 groups, just_name, repo_name = groups_and_repos
1170 groups, just_name, repo_name = groups_and_repos
1171 last_url = url('summary_home', repo_name=repo_name)
1171 last_url = url('summary_home', repo_name=repo_name)
1172 last_link = link_to(just_name, last_url)
1172 last_link = link_to(just_name, last_url)
1173
1173
1174 def make_link(group):
1174 def make_link(group):
1175 return link_to(group.name,
1175 return link_to(group.name,
1176 url('repos_group_home', group_name=group.group_name))
1176 url('repos_group_home', group_name=group.group_name))
1177 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1177 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1178
1178
1179
1179
1180 def fancy_file_stats(stats):
1180 def fancy_file_stats(stats):
1181 """
1181 """
1182 Displays a fancy two colored bar for number of added/deleted
1182 Displays a fancy two colored bar for number of added/deleted
1183 lines of code on file
1183 lines of code on file
1184
1184
1185 :param stats: two element list of added/deleted lines of code
1185 :param stats: two element list of added/deleted lines of code
1186 """
1186 """
1187 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1187 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1188 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1188 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1189
1189
1190 def cgen(l_type, a_v, d_v):
1190 def cgen(l_type, a_v, d_v):
1191 mapping = {'tr': 'top-right-rounded-corner-mid',
1191 mapping = {'tr': 'top-right-rounded-corner-mid',
1192 'tl': 'top-left-rounded-corner-mid',
1192 'tl': 'top-left-rounded-corner-mid',
1193 'br': 'bottom-right-rounded-corner-mid',
1193 'br': 'bottom-right-rounded-corner-mid',
1194 'bl': 'bottom-left-rounded-corner-mid'}
1194 'bl': 'bottom-left-rounded-corner-mid'}
1195 map_getter = lambda x: mapping[x]
1195 map_getter = lambda x: mapping[x]
1196
1196
1197 if l_type == 'a' and d_v:
1197 if l_type == 'a' and d_v:
1198 #case when added and deleted are present
1198 #case when added and deleted are present
1199 return ' '.join(map(map_getter, ['tl', 'bl']))
1199 return ' '.join(map(map_getter, ['tl', 'bl']))
1200
1200
1201 if l_type == 'a' and not d_v:
1201 if l_type == 'a' and not d_v:
1202 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1202 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1203
1203
1204 if l_type == 'd' and a_v:
1204 if l_type == 'd' and a_v:
1205 return ' '.join(map(map_getter, ['tr', 'br']))
1205 return ' '.join(map(map_getter, ['tr', 'br']))
1206
1206
1207 if l_type == 'd' and not a_v:
1207 if l_type == 'd' and not a_v:
1208 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1208 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1209
1209
1210 a, d = stats['added'], stats['deleted']
1210 a, d = stats['added'], stats['deleted']
1211 width = 100
1211 width = 100
1212
1212
1213 if stats['binary']:
1213 if stats['binary']:
1214 #binary mode
1214 #binary mode
1215 lbl = ''
1215 lbl = ''
1216 bin_op = 1
1216 bin_op = 1
1217
1217
1218 if BIN_FILENODE in stats['ops']:
1218 if BIN_FILENODE in stats['ops']:
1219 lbl = 'bin+'
1219 lbl = 'bin+'
1220
1220
1221 if NEW_FILENODE in stats['ops']:
1221 if NEW_FILENODE in stats['ops']:
1222 lbl += _('new file')
1222 lbl += _('new file')
1223 bin_op = NEW_FILENODE
1223 bin_op = NEW_FILENODE
1224 elif MOD_FILENODE in stats['ops']:
1224 elif MOD_FILENODE in stats['ops']:
1225 lbl += _('mod')
1225 lbl += _('mod')
1226 bin_op = MOD_FILENODE
1226 bin_op = MOD_FILENODE
1227 elif DEL_FILENODE in stats['ops']:
1227 elif DEL_FILENODE in stats['ops']:
1228 lbl += _('del')
1228 lbl += _('del')
1229 bin_op = DEL_FILENODE
1229 bin_op = DEL_FILENODE
1230 elif RENAMED_FILENODE in stats['ops']:
1230 elif RENAMED_FILENODE in stats['ops']:
1231 lbl += _('rename')
1231 lbl += _('rename')
1232 bin_op = RENAMED_FILENODE
1232 bin_op = RENAMED_FILENODE
1233
1233
1234 #chmod can go with other operations
1234 #chmod can go with other operations
1235 if CHMOD_FILENODE in stats['ops']:
1235 if CHMOD_FILENODE in stats['ops']:
1236 _org_lbl = _('chmod')
1236 _org_lbl = _('chmod')
1237 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1237 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1238
1238
1239 #import ipdb;ipdb.set_trace()
1239 #import ipdb;ipdb.set_trace()
1240 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1240 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1241 b_a = '<div class="bin bin1" style="width:0%"></div>'
1241 b_a = '<div class="bin bin1" style="width:0%"></div>'
1242 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1242 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1243
1243
1244 t = stats['added'] + stats['deleted']
1244 t = stats['added'] + stats['deleted']
1245 unit = float(width) / (t or 1)
1245 unit = float(width) / (t or 1)
1246
1246
1247 # needs > 9% of width to be visible or 0 to be hidden
1247 # needs > 9% of width to be visible or 0 to be hidden
1248 a_p = max(9, unit * a) if a > 0 else 0
1248 a_p = max(9, unit * a) if a > 0 else 0
1249 d_p = max(9, unit * d) if d > 0 else 0
1249 d_p = max(9, unit * d) if d > 0 else 0
1250 p_sum = a_p + d_p
1250 p_sum = a_p + d_p
1251
1251
1252 if p_sum > width:
1252 if p_sum > width:
1253 #adjust the percentage to be == 100% since we adjusted to 9
1253 #adjust the percentage to be == 100% since we adjusted to 9
1254 if a_p > d_p:
1254 if a_p > d_p:
1255 a_p = a_p - (p_sum - width)
1255 a_p = a_p - (p_sum - width)
1256 else:
1256 else:
1257 d_p = d_p - (p_sum - width)
1257 d_p = d_p - (p_sum - width)
1258
1258
1259 a_v = a if a > 0 else ''
1259 a_v = a if a > 0 else ''
1260 d_v = d if d > 0 else ''
1260 d_v = d if d > 0 else ''
1261
1261
1262 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1262 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1263 cgen('a', a_v, d_v), a_p, a_v
1263 cgen('a', a_v, d_v), a_p, a_v
1264 )
1264 )
1265 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1265 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1266 cgen('d', a_v, d_v), d_p, d_v
1266 cgen('d', a_v, d_v), d_p, d_v
1267 )
1267 )
1268 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1268 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1269
1269
1270
1270
1271 def _urlify_text_replace(match_obj):
1271 _URLIFY_RE = re.compile(r'''
1272 url_full = match_obj.group(1)
1272 # URL markup
1273 return '<a href="%(url)s">%(url)s</a>' % {'url': url_full}
1273 (?P<url>%s)
1274
1274 ''' % (url_re.pattern),
1275 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
1275
1276
1276 def _urlify_text(s):
1277 """
1278 Extract urls from text and make html links out of them
1279 """
1280 return url_re.sub(_urlify_text_replace, s)
1281
1277
1282
1278
1283 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
1279 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
1284 """
1280 """
1285 Parses given text message and make literal html with markup.
1281 Parses given text message and make literal html with markup.
1286 The text will be truncated to the specified length.
1282 The text will be truncated to the specified length.
1287 Hashes are turned into changeset links to specified repository.
1283 Hashes are turned into changeset links to specified repository.
1288 URLs links to what they say.
1284 URLs links to what they say.
1289 Issues are linked to given issue-server.
1285 Issues are linked to given issue-server.
1290 If link_ is provided, all text not already linking somewhere will link there.
1286 If link_ is provided, all text not already linking somewhere will link there.
1291 """
1287 """
1288
1289 def _replace(match_obj):
1290 url = match_obj.group('url')
1291 if url is not None:
1292 return '<a href="%(url)s">%(url)s</a>' % {'url': url}
1293 return match_obj.group(0)
1294
1295 def _urlify(s):
1296 """
1297 Extract urls from text and make html links out of them
1298 """
1299 return _URLIFY_RE.sub(_replace, s)
1300
1292 if truncate is None:
1301 if truncate is None:
1293 s = s.rstrip()
1302 s = s.rstrip()
1294 else:
1303 else:
1295 s = truncatef(s, truncate, whole_word=True)
1304 s = truncatef(s, truncate, whole_word=True)
1296 s = html_escape(s)
1305 s = html_escape(s)
1297 if repo_name is not None:
1306 if repo_name is not None:
1298 s = urlify_changesets(s, repo_name)
1307 s = urlify_changesets(s, repo_name)
1299 if stylize:
1308 if stylize:
1300 s = desc_stylize(s)
1309 s = desc_stylize(s)
1301 s = _urlify_text(s)
1310 s = _urlify(s)
1302 if repo_name is not None:
1311 if repo_name is not None:
1303 s = urlify_issues(s, repo_name, link_)
1312 s = urlify_issues(s, repo_name, link_)
1304 s = MENTIONS_REGEX.sub(_mentions_replace, s)
1313 s = MENTIONS_REGEX.sub(_mentions_replace, s)
1305 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1314 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1306 return literal(s)
1315 return literal(s)
1307
1316
1308
1317
1309 def _urlify_changeset_replace_f(repo_name):
1318 def _urlify_changeset_replace_f(repo_name):
1310 from pylons import url # doh, we need to re-import url to mock it later
1319 from pylons import url # doh, we need to re-import url to mock it later
1311 def urlify_changeset_replace(match_obj):
1320 def urlify_changeset_replace(match_obj):
1312 rev = match_obj.group(0)
1321 rev = match_obj.group(0)
1313 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1322 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1314 'url': url('changeset_home', repo_name=repo_name, revision=rev),
1323 'url': url('changeset_home', repo_name=repo_name, revision=rev),
1315 'rev': rev,
1324 'rev': rev,
1316 }
1325 }
1317 return urlify_changeset_replace
1326 return urlify_changeset_replace
1318
1327
1319
1328
1320 urilify_changeset_re = r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])'
1329 urilify_changeset_re = r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])'
1321
1330
1322 def urlify_changesets(text_, repo_name):
1331 def urlify_changesets(text_, repo_name):
1323 """
1332 """
1324 Extract revision ids from changeset and make link from them
1333 Extract revision ids from changeset and make link from them
1325 """
1334 """
1326 urlify_changeset_replace = _urlify_changeset_replace_f(repo_name)
1335 urlify_changeset_replace = _urlify_changeset_replace_f(repo_name)
1327 return re.sub(urilify_changeset_re, urlify_changeset_replace, text_)
1336 return re.sub(urilify_changeset_re, urlify_changeset_replace, text_)
1328
1337
1329
1338
1330 def linkify_others(t, l):
1339 def linkify_others(t, l):
1331 """Add a default link to html with links.
1340 """Add a default link to html with links.
1332 HTML doesn't allow nesting of links, so the outer link must be broken up
1341 HTML doesn't allow nesting of links, so the outer link must be broken up
1333 in pieces and give space for other links.
1342 in pieces and give space for other links.
1334 """
1343 """
1335 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1344 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1336 links = []
1345 links = []
1337 for e in urls.split(t):
1346 for e in urls.split(t):
1338 if not urls.match(e):
1347 if not urls.match(e):
1339 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1348 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1340 else:
1349 else:
1341 links.append(e)
1350 links.append(e)
1342
1351
1343 return ''.join(links)
1352 return ''.join(links)
1344
1353
1345
1354
1346 def _urlify_issues_replace_f(repo_name, ISSUE_SERVER_LNK, ISSUE_PREFIX):
1355 def _urlify_issues_replace_f(repo_name, ISSUE_SERVER_LNK, ISSUE_PREFIX):
1347 def urlify_issues_replace(match_obj):
1356 def urlify_issues_replace(match_obj):
1348 pref = ''
1357 pref = ''
1349 if match_obj.group().startswith(' '):
1358 if match_obj.group().startswith(' '):
1350 pref = ' '
1359 pref = ' '
1351
1360
1352 issue_id = ''.join(match_obj.groups())
1361 issue_id = ''.join(match_obj.groups())
1353 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1362 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1354 issue_url = issue_url.replace('{repo}', repo_name)
1363 issue_url = issue_url.replace('{repo}', repo_name)
1355 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1364 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1356
1365
1357 return (
1366 return (
1358 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1367 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1359 '%(issue-prefix)s%(id-repr)s'
1368 '%(issue-prefix)s%(id-repr)s'
1360 '</a>'
1369 '</a>'
1361 ) % {
1370 ) % {
1362 'pref': pref,
1371 'pref': pref,
1363 'cls': 'issue-tracker-link',
1372 'cls': 'issue-tracker-link',
1364 'url': issue_url,
1373 'url': issue_url,
1365 'id-repr': issue_id,
1374 'id-repr': issue_id,
1366 'issue-prefix': ISSUE_PREFIX,
1375 'issue-prefix': ISSUE_PREFIX,
1367 'serv': ISSUE_SERVER_LNK,
1376 'serv': ISSUE_SERVER_LNK,
1368 }
1377 }
1369 return urlify_issues_replace
1378 return urlify_issues_replace
1370
1379
1371
1380
1372 def urlify_issues(newtext, repo_name, link_=None):
1381 def urlify_issues(newtext, repo_name, link_=None):
1373 from kallithea import CONFIG as conf
1382 from kallithea import CONFIG as conf
1374
1383
1375 # allow multiple issue servers to be used
1384 # allow multiple issue servers to be used
1376 valid_indices = [
1385 valid_indices = [
1377 x.group(1)
1386 x.group(1)
1378 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1387 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1379 if x and 'issue_server_link%s' % x.group(1) in conf
1388 if x and 'issue_server_link%s' % x.group(1) in conf
1380 and 'issue_prefix%s' % x.group(1) in conf
1389 and 'issue_prefix%s' % x.group(1) in conf
1381 ]
1390 ]
1382
1391
1383 if valid_indices:
1392 if valid_indices:
1384 log.debug('found issue server suffixes `%s` during valuation of: %s',
1393 log.debug('found issue server suffixes `%s` during valuation of: %s',
1385 ','.join(valid_indices), newtext)
1394 ','.join(valid_indices), newtext)
1386
1395
1387 for pattern_index in valid_indices:
1396 for pattern_index in valid_indices:
1388 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1397 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1389 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1398 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1390 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1399 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1391
1400
1392 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s',
1401 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s',
1393 pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1402 pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1394 ISSUE_PREFIX)
1403 ISSUE_PREFIX)
1395
1404
1396 URL_PAT = re.compile(ISSUE_PATTERN)
1405 URL_PAT = re.compile(ISSUE_PATTERN)
1397
1406
1398 urlify_issues_replace = _urlify_issues_replace_f(repo_name, ISSUE_SERVER_LNK, ISSUE_PREFIX)
1407 urlify_issues_replace = _urlify_issues_replace_f(repo_name, ISSUE_SERVER_LNK, ISSUE_PREFIX)
1399 newtext = URL_PAT.sub(urlify_issues_replace, newtext)
1408 newtext = URL_PAT.sub(urlify_issues_replace, newtext)
1400 log.debug('processed prefix:`%s` => %s', pattern_index, newtext)
1409 log.debug('processed prefix:`%s` => %s', pattern_index, newtext)
1401
1410
1402 # if we actually did something above
1411 # if we actually did something above
1403 if link_:
1412 if link_:
1404 # wrap not links into final link => link_
1413 # wrap not links into final link => link_
1405 newtext = linkify_others(newtext, link_)
1414 newtext = linkify_others(newtext, link_)
1406 return newtext
1415 return newtext
1407
1416
1408
1417
1409 def _mentions_replace(match_obj):
1418 def _mentions_replace(match_obj):
1410 return '<b>@%s</b>' % match_obj.group(1)
1419 return '<b>@%s</b>' % match_obj.group(1)
1411
1420
1412
1421
1413 def render_w_mentions(source, repo_name=None):
1422 def render_w_mentions(source, repo_name=None):
1414 """
1423 """
1415 Render plain text with revision hashes and issue references urlified
1424 Render plain text with revision hashes and issue references urlified
1416 and with @mention highlighting.
1425 and with @mention highlighting.
1417 """
1426 """
1418 s = safe_unicode(source)
1427 s = safe_unicode(source)
1419 s = urlify_text(s, repo_name=repo_name)
1428 s = urlify_text(s, repo_name=repo_name)
1420 return literal('<div class="formatted-fixed">%s</div>' % s)
1429 return literal('<div class="formatted-fixed">%s</div>' % s)
1421
1430
1422
1431
1423 def short_ref(ref_type, ref_name):
1432 def short_ref(ref_type, ref_name):
1424 if ref_type == 'rev':
1433 if ref_type == 'rev':
1425 return short_id(ref_name)
1434 return short_id(ref_name)
1426 return ref_name
1435 return ref_name
1427
1436
1428 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1437 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1429 """
1438 """
1430 Return full markup for a href to changeset_home for a changeset.
1439 Return full markup for a href to changeset_home for a changeset.
1431 If ref_type is branch it will link to changelog.
1440 If ref_type is branch it will link to changelog.
1432 ref_name is shortened if ref_type is 'rev'.
1441 ref_name is shortened if ref_type is 'rev'.
1433 if rev is specified show it too, explicitly linking to that revision.
1442 if rev is specified show it too, explicitly linking to that revision.
1434 """
1443 """
1435 txt = short_ref(ref_type, ref_name)
1444 txt = short_ref(ref_type, ref_name)
1436 if ref_type == 'branch':
1445 if ref_type == 'branch':
1437 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1446 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1438 else:
1447 else:
1439 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1448 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1440 l = link_to(repo_name + '#' + txt, u)
1449 l = link_to(repo_name + '#' + txt, u)
1441 if rev and ref_type != 'rev':
1450 if rev and ref_type != 'rev':
1442 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1451 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1443 return l
1452 return l
1444
1453
1445 def changeset_status(repo, revision):
1454 def changeset_status(repo, revision):
1446 return ChangesetStatusModel().get_status(repo, revision)
1455 return ChangesetStatusModel().get_status(repo, revision)
1447
1456
1448
1457
1449 def changeset_status_lbl(changeset_status):
1458 def changeset_status_lbl(changeset_status):
1450 return ChangesetStatus.get_status_lbl(changeset_status)
1459 return ChangesetStatus.get_status_lbl(changeset_status)
1451
1460
1452
1461
1453 def get_permission_name(key):
1462 def get_permission_name(key):
1454 return dict(Permission.PERMS).get(key)
1463 return dict(Permission.PERMS).get(key)
1455
1464
1456
1465
1457 def journal_filter_help():
1466 def journal_filter_help():
1458 return _(textwrap.dedent('''
1467 return _(textwrap.dedent('''
1459 Example filter terms:
1468 Example filter terms:
1460 repository:vcs
1469 repository:vcs
1461 username:developer
1470 username:developer
1462 action:*push*
1471 action:*push*
1463 ip:127.0.0.1
1472 ip:127.0.0.1
1464 date:20120101
1473 date:20120101
1465 date:[20120101100000 TO 20120102]
1474 date:[20120101100000 TO 20120102]
1466
1475
1467 Generate wildcards using '*' character:
1476 Generate wildcards using '*' character:
1468 "repository:vcs*" - search everything starting with 'vcs'
1477 "repository:vcs*" - search everything starting with 'vcs'
1469 "repository:*vcs*" - search for repository containing 'vcs'
1478 "repository:*vcs*" - search for repository containing 'vcs'
1470
1479
1471 Optional AND / OR operators in queries
1480 Optional AND / OR operators in queries
1472 "repository:vcs OR repository:test"
1481 "repository:vcs OR repository:test"
1473 "username:test AND repository:test*"
1482 "username:test AND repository:test*"
1474 '''))
1483 '''))
1475
1484
1476
1485
1477 def not_mapped_error(repo_name):
1486 def not_mapped_error(repo_name):
1478 flash(_('%s repository is not mapped to db perhaps'
1487 flash(_('%s repository is not mapped to db perhaps'
1479 ' it was created or renamed from the filesystem'
1488 ' it was created or renamed from the filesystem'
1480 ' please run the application again'
1489 ' please run the application again'
1481 ' in order to rescan repositories') % repo_name, category='error')
1490 ' in order to rescan repositories') % repo_name, category='error')
1482
1491
1483
1492
1484 def ip_range(ip_addr):
1493 def ip_range(ip_addr):
1485 from kallithea.model.db import UserIpMap
1494 from kallithea.model.db import UserIpMap
1486 s, e = UserIpMap._get_ip_range(ip_addr)
1495 s, e = UserIpMap._get_ip_range(ip_addr)
1487 return '%s - %s' % (s, e)
1496 return '%s - %s' % (s, e)
1488
1497
1489
1498
1490 def form(url, method="post", **attrs):
1499 def form(url, method="post", **attrs):
1491 """Like webhelpers.html.tags.form but automatically using secure_form with
1500 """Like webhelpers.html.tags.form but automatically using secure_form with
1492 authentication_token for POST. authentication_token is thus never leaked
1501 authentication_token for POST. authentication_token is thus never leaked
1493 in the URL."""
1502 in the URL."""
1494 if method.lower() == 'get':
1503 if method.lower() == 'get':
1495 return insecure_form(url, method=method, **attrs)
1504 return insecure_form(url, method=method, **attrs)
1496 # webhelpers will turn everything but GET into POST
1505 # webhelpers will turn everything but GET into POST
1497 return secure_form(url, method=method, **attrs)
1506 return secure_form(url, method=method, **attrs)
@@ -1,198 +1,199 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.lib.markup_renderer
15 kallithea.lib.markup_renderer
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Renderer for markup languages with ability to parse using rst or markdown
18 Renderer for markup languages with ability to parse using rst or markdown
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: Oct 27, 2011
22 :created_on: Oct 27, 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
28
29 import re
29 import re
30 import logging
30 import logging
31 import traceback
31 import traceback
32
32
33 from kallithea.lib.utils2 import safe_unicode, MENTIONS_REGEX
33 from kallithea.lib.utils2 import safe_unicode, MENTIONS_REGEX
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 url_re = re.compile(r'''(\bhttps?://(?:[\da-zA-Z0-9@:.-]+)'''
38 url_re = re.compile(r'''\bhttps?://(?:[\da-zA-Z0-9@:.-]+)'''
39 r'''(?:[/a-zA-Z0-9_=@#~&+%.,:;?!*()-]*[/a-zA-Z0-9_=@#~])?)''')
39 r'''(?:[/a-zA-Z0-9_=@#~&+%.,:;?!*()-]*[/a-zA-Z0-9_=@#~])?''')
40
40
41
41 class MarkupRenderer(object):
42 class MarkupRenderer(object):
42 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
43 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
43
44
44 MARKDOWN_PAT = re.compile(r'md|mkdn?|mdown|markdown', re.IGNORECASE)
45 MARKDOWN_PAT = re.compile(r'md|mkdn?|mdown|markdown', re.IGNORECASE)
45 RST_PAT = re.compile(r're?st', re.IGNORECASE)
46 RST_PAT = re.compile(r're?st', re.IGNORECASE)
46 PLAIN_PAT = re.compile(r'readme', re.IGNORECASE)
47 PLAIN_PAT = re.compile(r'readme', re.IGNORECASE)
47
48
48 def _detect_renderer(self, source, filename=None):
49 def _detect_renderer(self, source, filename=None):
49 """
50 """
50 runs detection of what renderer should be used for generating html
51 runs detection of what renderer should be used for generating html
51 from a markup language
52 from a markup language
52
53
53 filename can be also explicitly a renderer name
54 filename can be also explicitly a renderer name
54
55
55 :param source:
56 :param source:
56 :param filename:
57 :param filename:
57 """
58 """
58
59
59 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
60 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
60 detected_renderer = 'markdown'
61 detected_renderer = 'markdown'
61 elif MarkupRenderer.RST_PAT.findall(filename):
62 elif MarkupRenderer.RST_PAT.findall(filename):
62 detected_renderer = 'rst'
63 detected_renderer = 'rst'
63 elif MarkupRenderer.PLAIN_PAT.findall(filename):
64 elif MarkupRenderer.PLAIN_PAT.findall(filename):
64 detected_renderer = 'rst'
65 detected_renderer = 'rst'
65 else:
66 else:
66 detected_renderer = 'plain'
67 detected_renderer = 'plain'
67
68
68 return getattr(MarkupRenderer, detected_renderer)
69 return getattr(MarkupRenderer, detected_renderer)
69
70
70 @classmethod
71 @classmethod
71 def _flavored_markdown(cls, text):
72 def _flavored_markdown(cls, text):
72 """
73 """
73 Github style flavored markdown
74 Github style flavored markdown
74
75
75 :param text:
76 :param text:
76 """
77 """
77 from hashlib import md5
78 from hashlib import md5
78
79
79 # Extract pre blocks.
80 # Extract pre blocks.
80 extractions = {}
81 extractions = {}
81 def pre_extraction_callback(matchobj):
82 def pre_extraction_callback(matchobj):
82 digest = md5(matchobj.group(0)).hexdigest()
83 digest = md5(matchobj.group(0)).hexdigest()
83 extractions[digest] = matchobj.group(0)
84 extractions[digest] = matchobj.group(0)
84 return "{gfm-extraction-%s}" % digest
85 return "{gfm-extraction-%s}" % digest
85 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
86 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
86 text = re.sub(pattern, pre_extraction_callback, text)
87 text = re.sub(pattern, pre_extraction_callback, text)
87
88
88 # Prevent foo_bar_baz from ending up with an italic word in the middle.
89 # Prevent foo_bar_baz from ending up with an italic word in the middle.
89 def italic_callback(matchobj):
90 def italic_callback(matchobj):
90 s = matchobj.group(0)
91 s = matchobj.group(0)
91 if list(s).count('_') >= 2:
92 if list(s).count('_') >= 2:
92 return s.replace('_', '\_')
93 return s.replace('_', '\_')
93 return s
94 return s
94 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
95 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
95
96
96 # In very clear cases, let newlines become <br /> tags.
97 # In very clear cases, let newlines become <br /> tags.
97 def newline_callback(matchobj):
98 def newline_callback(matchobj):
98 if len(matchobj.group(1)) == 1:
99 if len(matchobj.group(1)) == 1:
99 return matchobj.group(0).rstrip() + ' \n'
100 return matchobj.group(0).rstrip() + ' \n'
100 else:
101 else:
101 return matchobj.group(0)
102 return matchobj.group(0)
102 pattern = re.compile(r'^[\w\<][^\n]*(\n+)', re.MULTILINE)
103 pattern = re.compile(r'^[\w\<][^\n]*(\n+)', re.MULTILINE)
103 text = re.sub(pattern, newline_callback, text)
104 text = re.sub(pattern, newline_callback, text)
104
105
105 # Insert pre block extractions.
106 # Insert pre block extractions.
106 def pre_insert_callback(matchobj):
107 def pre_insert_callback(matchobj):
107 return '\n\n' + extractions[matchobj.group(1)]
108 return '\n\n' + extractions[matchobj.group(1)]
108 text = re.sub(r'{gfm-extraction-([0-9a-f]{32})\}',
109 text = re.sub(r'{gfm-extraction-([0-9a-f]{32})\}',
109 pre_insert_callback, text)
110 pre_insert_callback, text)
110
111
111 return text
112 return text
112
113
113 def render(self, source, filename=None):
114 def render(self, source, filename=None):
114 """
115 """
115 Renders a given filename using detected renderer
116 Renders a given filename using detected renderer
116 it detects renderers based on file extension or mimetype.
117 it detects renderers based on file extension or mimetype.
117 At last it will just do a simple html replacing new lines with <br/>
118 At last it will just do a simple html replacing new lines with <br/>
118
119
119 :param file_name:
120 :param file_name:
120 :param source:
121 :param source:
121 """
122 """
122
123
123 renderer = self._detect_renderer(source, filename)
124 renderer = self._detect_renderer(source, filename)
124 readme_data = renderer(source)
125 readme_data = renderer(source)
125 return readme_data
126 return readme_data
126
127
127 @classmethod
128 @classmethod
128 def plain(cls, source, universal_newline=True):
129 def plain(cls, source, universal_newline=True):
129 source = safe_unicode(source)
130 source = safe_unicode(source)
130 if universal_newline:
131 if universal_newline:
131 newline = '\n'
132 newline = '\n'
132 source = newline.join(source.splitlines())
133 source = newline.join(source.splitlines())
133
134
134 def url_func(match_obj):
135 def url_func(match_obj):
135 url_full = match_obj.groups()[0]
136 url_full = match_obj.group(0)
136 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
137 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
137 source = url_re.sub(url_func, source)
138 source = url_re.sub(url_func, source)
138 return '<br />' + source.replace("\n", '<br />')
139 return '<br />' + source.replace("\n", '<br />')
139
140
140 @classmethod
141 @classmethod
141 def markdown(cls, source, safe=True, flavored=False):
142 def markdown(cls, source, safe=True, flavored=False):
142 source = safe_unicode(source)
143 source = safe_unicode(source)
143 try:
144 try:
144 import markdown as __markdown
145 import markdown as __markdown
145 if flavored:
146 if flavored:
146 source = cls._flavored_markdown(source)
147 source = cls._flavored_markdown(source)
147 return __markdown.markdown(source, ['codehilite', 'extra'])
148 return __markdown.markdown(source, ['codehilite', 'extra'])
148 except ImportError:
149 except ImportError:
149 log.warning('Install markdown to use this function')
150 log.warning('Install markdown to use this function')
150 return cls.plain(source)
151 return cls.plain(source)
151 except Exception:
152 except Exception:
152 log.error(traceback.format_exc())
153 log.error(traceback.format_exc())
153 if safe:
154 if safe:
154 log.debug('Falling back to render in plain mode')
155 log.debug('Falling back to render in plain mode')
155 return cls.plain(source)
156 return cls.plain(source)
156 else:
157 else:
157 raise
158 raise
158
159
159 @classmethod
160 @classmethod
160 def rst(cls, source, safe=True):
161 def rst(cls, source, safe=True):
161 source = safe_unicode(source)
162 source = safe_unicode(source)
162 try:
163 try:
163 from docutils.core import publish_parts
164 from docutils.core import publish_parts
164 from docutils.parsers.rst import directives
165 from docutils.parsers.rst import directives
165 docutils_settings = dict([(alias, None) for alias in
166 docutils_settings = dict([(alias, None) for alias in
166 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
167 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
167
168
168 docutils_settings.update({'input_encoding': 'unicode',
169 docutils_settings.update({'input_encoding': 'unicode',
169 'report_level': 4})
170 'report_level': 4})
170
171
171 for k, v in docutils_settings.iteritems():
172 for k, v in docutils_settings.iteritems():
172 directives.register_directive(k, v)
173 directives.register_directive(k, v)
173
174
174 parts = publish_parts(source=source,
175 parts = publish_parts(source=source,
175 writer_name="html4css1",
176 writer_name="html4css1",
176 settings_overrides=docutils_settings)
177 settings_overrides=docutils_settings)
177
178
178 return parts['html_title'] + parts["fragment"]
179 return parts['html_title'] + parts["fragment"]
179 except ImportError:
180 except ImportError:
180 log.warning('Install docutils to use this function')
181 log.warning('Install docutils to use this function')
181 return cls.plain(source)
182 return cls.plain(source)
182 except Exception:
183 except Exception:
183 log.error(traceback.format_exc())
184 log.error(traceback.format_exc())
184 if safe:
185 if safe:
185 log.debug('Falling back to render in plain mode')
186 log.debug('Falling back to render in plain mode')
186 return cls.plain(source)
187 return cls.plain(source)
187 else:
188 else:
188 raise
189 raise
189
190
190 @classmethod
191 @classmethod
191 def rst_with_mentions(cls, source):
192 def rst_with_mentions(cls, source):
192 mention_pat = re.compile(MENTIONS_REGEX)
193 mention_pat = re.compile(MENTIONS_REGEX)
193
194
194 def wrapp(match_obj):
195 def wrapp(match_obj):
195 uname = match_obj.groups()[0]
196 uname = match_obj.groups()[0]
196 return '\ **@%(uname)s**\ ' % {'uname': uname}
197 return '\ **@%(uname)s**\ ' % {'uname': uname}
197 mention_hl = mention_pat.sub(wrapp, source).strip()
198 mention_hl = mention_pat.sub(wrapp, source).strip()
198 return cls.rst(mention_hl)
199 return cls.rst(mention_hl)
General Comments 0
You need to be logged in to leave comments. Login now