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