##// END OF EJS Templates
helpers: Fix displayed author on changelog_summary_data when no email is set in commit author (Issue #137)
Marc Villetard -
r5170:3e1afbd6 stable
parent child Browse files
Show More
@@ -1,1452 +1,1452 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 pop_messages(self):
409 def pop_messages(self):
410 """Return all accumulated messages and delete them from the session.
410 """Return all accumulated messages and delete them from the session.
411
411
412 The return value is a list of ``Message`` objects.
412 The return value is a list of ``Message`` objects.
413 """
413 """
414 from pylons import session
414 from pylons import session
415 messages = session.pop(self.session_key, [])
415 messages = session.pop(self.session_key, [])
416 session.save()
416 session.save()
417 return [_Message(*m) for m in messages]
417 return [_Message(*m) for m in messages]
418
418
419 flash = Flash()
419 flash = Flash()
420
420
421 #==============================================================================
421 #==============================================================================
422 # SCM FILTERS available via h.
422 # SCM FILTERS available via h.
423 #==============================================================================
423 #==============================================================================
424 from kallithea.lib.vcs.utils import author_name, author_email
424 from kallithea.lib.vcs.utils import author_name, author_email
425 from kallithea.lib.utils2 import credentials_filter, age as _age
425 from kallithea.lib.utils2 import credentials_filter, age as _age
426 from kallithea.model.db import User, ChangesetStatus
426 from kallithea.model.db import User, ChangesetStatus
427
427
428 age = lambda x, y=False: _age(x, y)
428 age = lambda x, y=False: _age(x, y)
429 capitalize = lambda x: x.capitalize()
429 capitalize = lambda x: x.capitalize()
430 email = author_email
430 email = author_email
431 short_id = lambda x: x[:12]
431 short_id = lambda x: x[:12]
432 hide_credentials = lambda x: ''.join(credentials_filter(x))
432 hide_credentials = lambda x: ''.join(credentials_filter(x))
433
433
434
434
435 def show_id(cs):
435 def show_id(cs):
436 """
436 """
437 Configurable function that shows ID
437 Configurable function that shows ID
438 by default it's r123:fffeeefffeee
438 by default it's r123:fffeeefffeee
439
439
440 :param cs: changeset instance
440 :param cs: changeset instance
441 """
441 """
442 from kallithea import CONFIG
442 from kallithea import CONFIG
443 def_len = safe_int(CONFIG.get('show_sha_length', 12))
443 def_len = safe_int(CONFIG.get('show_sha_length', 12))
444 show_rev = str2bool(CONFIG.get('show_revision_number', False))
444 show_rev = str2bool(CONFIG.get('show_revision_number', False))
445
445
446 raw_id = cs.raw_id[:def_len]
446 raw_id = cs.raw_id[:def_len]
447 if show_rev:
447 if show_rev:
448 return 'r%s:%s' % (cs.revision, raw_id)
448 return 'r%s:%s' % (cs.revision, raw_id)
449 else:
449 else:
450 return '%s' % (raw_id)
450 return '%s' % (raw_id)
451
451
452
452
453 def fmt_date(date):
453 def fmt_date(date):
454 if date:
454 if date:
455 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
455 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
456
456
457 return ""
457 return ""
458
458
459
459
460 def is_git(repository):
460 def is_git(repository):
461 if hasattr(repository, 'alias'):
461 if hasattr(repository, 'alias'):
462 _type = repository.alias
462 _type = repository.alias
463 elif hasattr(repository, 'repo_type'):
463 elif hasattr(repository, 'repo_type'):
464 _type = repository.repo_type
464 _type = repository.repo_type
465 else:
465 else:
466 _type = repository
466 _type = repository
467 return _type == 'git'
467 return _type == 'git'
468
468
469
469
470 def is_hg(repository):
470 def is_hg(repository):
471 if hasattr(repository, 'alias'):
471 if hasattr(repository, 'alias'):
472 _type = repository.alias
472 _type = repository.alias
473 elif hasattr(repository, 'repo_type'):
473 elif hasattr(repository, 'repo_type'):
474 _type = repository.repo_type
474 _type = repository.repo_type
475 else:
475 else:
476 _type = repository
476 _type = repository
477 return _type == 'hg'
477 return _type == 'hg'
478
478
479
479
480 def user_or_none(author):
480 def user_or_none(author):
481 email = author_email(author)
481 email = author_email(author)
482 if email is not None:
482 if email:
483 user = User.get_by_email(email, case_insensitive=True, cache=True)
483 user = User.get_by_email(email, case_insensitive=True, cache=True)
484 if user is not None:
484 if user is not None:
485 return user
485 return user
486
486
487 user = User.get_by_username(author_name(author), case_insensitive=True, cache=True)
487 user = User.get_by_username(author_name(author), case_insensitive=True, cache=True)
488 if user is not None:
488 if user is not None:
489 return user
489 return user
490
490
491 return None
491 return None
492
492
493 def email_or_none(author):
493 def email_or_none(author):
494 if not author:
494 if not author:
495 return None
495 return None
496 user = user_or_none(author)
496 user = user_or_none(author)
497 if user is not None:
497 if user is not None:
498 return user.email # always use main email address - not necessarily the one used to find user
498 return user.email # always use main email address - not necessarily the one used to find user
499
499
500 # extract email from the commit string
500 # extract email from the commit string
501 email = author_email(author)
501 email = author_email(author)
502 if email:
502 if email:
503 return email
503 return email
504
504
505 # No valid email, not a valid user in the system, none!
505 # No valid email, not a valid user in the system, none!
506 return None
506 return None
507
507
508 def person(author, show_attr="username"):
508 def person(author, show_attr="username"):
509 """Find the user identified by 'author', return one of the users attributes,
509 """Find the user identified by 'author', return one of the users attributes,
510 default to the username attribute, None if there is no user"""
510 default to the username attribute, None if there is no user"""
511 # attr to return from fetched user
511 # attr to return from fetched user
512 person_getter = lambda usr: getattr(usr, show_attr)
512 person_getter = lambda usr: getattr(usr, show_attr)
513
513
514 # if author is already an instance use it for extraction
514 # if author is already an instance use it for extraction
515 if isinstance(author, User):
515 if isinstance(author, User):
516 return person_getter(author)
516 return person_getter(author)
517
517
518 user = user_or_none(author)
518 user = user_or_none(author)
519 if user is not None:
519 if user is not None:
520 return person_getter(user)
520 return person_getter(user)
521
521
522 # Still nothing? Just pass back the author name if any, else the email
522 # Still nothing? Just pass back the author name if any, else the email
523 return author_name(author) or email(author)
523 return author_name(author) or email(author)
524
524
525
525
526 def person_by_id(id_, show_attr="username"):
526 def person_by_id(id_, show_attr="username"):
527 # attr to return from fetched user
527 # attr to return from fetched user
528 person_getter = lambda usr: getattr(usr, show_attr)
528 person_getter = lambda usr: getattr(usr, show_attr)
529
529
530 #maybe it's an ID ?
530 #maybe it's an ID ?
531 if str(id_).isdigit() or isinstance(id_, int):
531 if str(id_).isdigit() or isinstance(id_, int):
532 id_ = int(id_)
532 id_ = int(id_)
533 user = User.get(id_)
533 user = User.get(id_)
534 if user is not None:
534 if user is not None:
535 return person_getter(user)
535 return person_getter(user)
536 return id_
536 return id_
537
537
538
538
539 def desc_stylize(value):
539 def desc_stylize(value):
540 """
540 """
541 converts tags from value into html equivalent
541 converts tags from value into html equivalent
542
542
543 :param value:
543 :param value:
544 """
544 """
545 if not value:
545 if not value:
546 return ''
546 return ''
547
547
548 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
548 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
549 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
549 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
550 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
550 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
551 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
551 '<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)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
552 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
553 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
553 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
554 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
554 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
555 '<div class="metatag" tag="lang">\\2</div>', value)
555 '<div class="metatag" tag="lang">\\2</div>', value)
556 value = re.sub(r'\[([a-z]+)\]',
556 value = re.sub(r'\[([a-z]+)\]',
557 '<div class="metatag" tag="\\1">\\1</div>', value)
557 '<div class="metatag" tag="\\1">\\1</div>', value)
558
558
559 return value
559 return value
560
560
561
561
562 def boolicon(value):
562 def boolicon(value):
563 """Returns boolean value of a value, represented as small html image of true/false
563 """Returns boolean value of a value, represented as small html image of true/false
564 icons
564 icons
565
565
566 :param value: value
566 :param value: value
567 """
567 """
568
568
569 if value:
569 if value:
570 return HTML.tag('i', class_="icon-ok")
570 return HTML.tag('i', class_="icon-ok")
571 else:
571 else:
572 return HTML.tag('i', class_="icon-minus-circled")
572 return HTML.tag('i', class_="icon-minus-circled")
573
573
574
574
575 def action_parser(user_log, feed=False, parse_cs=False):
575 def action_parser(user_log, feed=False, parse_cs=False):
576 """
576 """
577 This helper will action_map the specified string action into translated
577 This helper will action_map the specified string action into translated
578 fancy names with icons and links
578 fancy names with icons and links
579
579
580 :param user_log: user log instance
580 :param user_log: user log instance
581 :param feed: use output for feeds (no html and fancy icons)
581 :param feed: use output for feeds (no html and fancy icons)
582 :param parse_cs: parse Changesets into VCS instances
582 :param parse_cs: parse Changesets into VCS instances
583 """
583 """
584
584
585 action = user_log.action
585 action = user_log.action
586 action_params = ' '
586 action_params = ' '
587
587
588 x = action.split(':')
588 x = action.split(':')
589
589
590 if len(x) > 1:
590 if len(x) > 1:
591 action, action_params = x
591 action, action_params = x
592
592
593 def get_cs_links():
593 def get_cs_links():
594 revs_limit = 3 # display this amount always
594 revs_limit = 3 # display this amount always
595 revs_top_limit = 50 # show upto this amount of changesets hidden
595 revs_top_limit = 50 # show upto this amount of changesets hidden
596 revs_ids = action_params.split(',')
596 revs_ids = action_params.split(',')
597 deleted = user_log.repository is None
597 deleted = user_log.repository is None
598 if deleted:
598 if deleted:
599 return ','.join(revs_ids)
599 return ','.join(revs_ids)
600
600
601 repo_name = user_log.repository.repo_name
601 repo_name = user_log.repository.repo_name
602
602
603 def lnk(rev, repo_name):
603 def lnk(rev, repo_name):
604 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
604 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
605 lazy_cs = True
605 lazy_cs = True
606 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
606 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
607 lazy_cs = False
607 lazy_cs = False
608 lbl = '?'
608 lbl = '?'
609 if rev.op == 'delete_branch':
609 if rev.op == 'delete_branch':
610 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
610 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
611 title = ''
611 title = ''
612 elif rev.op == 'tag':
612 elif rev.op == 'tag':
613 lbl = '%s' % _('Created tag: %s') % rev.ref_name
613 lbl = '%s' % _('Created tag: %s') % rev.ref_name
614 title = ''
614 title = ''
615 _url = '#'
615 _url = '#'
616
616
617 else:
617 else:
618 lbl = '%s' % (rev.short_id[:8])
618 lbl = '%s' % (rev.short_id[:8])
619 _url = url('changeset_home', repo_name=repo_name,
619 _url = url('changeset_home', repo_name=repo_name,
620 revision=rev.raw_id)
620 revision=rev.raw_id)
621 title = tooltip(rev.message)
621 title = tooltip(rev.message)
622 else:
622 else:
623 ## changeset cannot be found/striped/removed etc.
623 ## changeset cannot be found/striped/removed etc.
624 lbl = ('%s' % rev)[:12]
624 lbl = ('%s' % rev)[:12]
625 _url = '#'
625 _url = '#'
626 title = _('Changeset not found')
626 title = _('Changeset not found')
627 if parse_cs:
627 if parse_cs:
628 return link_to(lbl, _url, title=title, class_='tooltip')
628 return link_to(lbl, _url, title=title, class_='tooltip')
629 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
629 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
630 class_='lazy-cs' if lazy_cs else '')
630 class_='lazy-cs' if lazy_cs else '')
631
631
632 def _get_op(rev_txt):
632 def _get_op(rev_txt):
633 _op = None
633 _op = None
634 _name = rev_txt
634 _name = rev_txt
635 if len(rev_txt.split('=>')) == 2:
635 if len(rev_txt.split('=>')) == 2:
636 _op, _name = rev_txt.split('=>')
636 _op, _name = rev_txt.split('=>')
637 return _op, _name
637 return _op, _name
638
638
639 revs = []
639 revs = []
640 if len(filter(lambda v: v != '', revs_ids)) > 0:
640 if len(filter(lambda v: v != '', revs_ids)) > 0:
641 repo = None
641 repo = None
642 for rev in revs_ids[:revs_top_limit]:
642 for rev in revs_ids[:revs_top_limit]:
643 _op, _name = _get_op(rev)
643 _op, _name = _get_op(rev)
644
644
645 # we want parsed changesets, or new log store format is bad
645 # we want parsed changesets, or new log store format is bad
646 if parse_cs:
646 if parse_cs:
647 try:
647 try:
648 if repo is None:
648 if repo is None:
649 repo = user_log.repository.scm_instance
649 repo = user_log.repository.scm_instance
650 _rev = repo.get_changeset(rev)
650 _rev = repo.get_changeset(rev)
651 revs.append(_rev)
651 revs.append(_rev)
652 except ChangesetDoesNotExistError:
652 except ChangesetDoesNotExistError:
653 log.error('cannot find revision %s in this repo' % rev)
653 log.error('cannot find revision %s in this repo' % rev)
654 revs.append(rev)
654 revs.append(rev)
655 continue
655 continue
656 else:
656 else:
657 _rev = AttributeDict({
657 _rev = AttributeDict({
658 'short_id': rev[:12],
658 'short_id': rev[:12],
659 'raw_id': rev,
659 'raw_id': rev,
660 'message': '',
660 'message': '',
661 'op': _op,
661 'op': _op,
662 'ref_name': _name
662 'ref_name': _name
663 })
663 })
664 revs.append(_rev)
664 revs.append(_rev)
665 cs_links = [" " + ', '.join(
665 cs_links = [" " + ', '.join(
666 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
666 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
667 )]
667 )]
668 _op1, _name1 = _get_op(revs_ids[0])
668 _op1, _name1 = _get_op(revs_ids[0])
669 _op2, _name2 = _get_op(revs_ids[-1])
669 _op2, _name2 = _get_op(revs_ids[-1])
670
670
671 _rev = '%s...%s' % (_name1, _name2)
671 _rev = '%s...%s' % (_name1, _name2)
672
672
673 compare_view = (
673 compare_view = (
674 ' <div class="compare_view tooltip" title="%s">'
674 ' <div class="compare_view tooltip" title="%s">'
675 '<a href="%s">%s</a> </div>' % (
675 '<a href="%s">%s</a> </div>' % (
676 _('Show all combined changesets %s->%s') % (
676 _('Show all combined changesets %s->%s') % (
677 revs_ids[0][:12], revs_ids[-1][:12]
677 revs_ids[0][:12], revs_ids[-1][:12]
678 ),
678 ),
679 url('changeset_home', repo_name=repo_name,
679 url('changeset_home', repo_name=repo_name,
680 revision=_rev
680 revision=_rev
681 ),
681 ),
682 _('compare view')
682 _('compare view')
683 )
683 )
684 )
684 )
685
685
686 # if we have exactly one more than normally displayed
686 # if we have exactly one more than normally displayed
687 # just display it, takes less space than displaying
687 # just display it, takes less space than displaying
688 # "and 1 more revisions"
688 # "and 1 more revisions"
689 if len(revs_ids) == revs_limit + 1:
689 if len(revs_ids) == revs_limit + 1:
690 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
690 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
691
691
692 # hidden-by-default ones
692 # hidden-by-default ones
693 if len(revs_ids) > revs_limit + 1:
693 if len(revs_ids) > revs_limit + 1:
694 uniq_id = revs_ids[0]
694 uniq_id = revs_ids[0]
695 html_tmpl = (
695 html_tmpl = (
696 '<span> %s <a class="show_more" id="_%s" '
696 '<span> %s <a class="show_more" id="_%s" '
697 'href="#more">%s</a> %s</span>'
697 'href="#more">%s</a> %s</span>'
698 )
698 )
699 if not feed:
699 if not feed:
700 cs_links.append(html_tmpl % (
700 cs_links.append(html_tmpl % (
701 _('and'),
701 _('and'),
702 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
702 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
703 _('revisions')
703 _('revisions')
704 )
704 )
705 )
705 )
706
706
707 if not feed:
707 if not feed:
708 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
708 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
709 else:
709 else:
710 html_tmpl = '<span id="%s"> %s </span>'
710 html_tmpl = '<span id="%s"> %s </span>'
711
711
712 morelinks = ', '.join(
712 morelinks = ', '.join(
713 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
713 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
714 )
714 )
715
715
716 if len(revs_ids) > revs_top_limit:
716 if len(revs_ids) > revs_top_limit:
717 morelinks += ', ...'
717 morelinks += ', ...'
718
718
719 cs_links.append(html_tmpl % (uniq_id, morelinks))
719 cs_links.append(html_tmpl % (uniq_id, morelinks))
720 if len(revs) > 1:
720 if len(revs) > 1:
721 cs_links.append(compare_view)
721 cs_links.append(compare_view)
722 return ''.join(cs_links)
722 return ''.join(cs_links)
723
723
724 def get_fork_name():
724 def get_fork_name():
725 repo_name = action_params
725 repo_name = action_params
726 _url = url('summary_home', repo_name=repo_name)
726 _url = url('summary_home', repo_name=repo_name)
727 return _('fork name %s') % link_to(action_params, _url)
727 return _('fork name %s') % link_to(action_params, _url)
728
728
729 def get_user_name():
729 def get_user_name():
730 user_name = action_params
730 user_name = action_params
731 return user_name
731 return user_name
732
732
733 def get_users_group():
733 def get_users_group():
734 group_name = action_params
734 group_name = action_params
735 return group_name
735 return group_name
736
736
737 def get_pull_request():
737 def get_pull_request():
738 pull_request_id = action_params
738 pull_request_id = action_params
739 deleted = user_log.repository is None
739 deleted = user_log.repository is None
740 if deleted:
740 if deleted:
741 repo_name = user_log.repository_name
741 repo_name = user_log.repository_name
742 else:
742 else:
743 repo_name = user_log.repository.repo_name
743 repo_name = user_log.repository.repo_name
744 return link_to(_('Pull request #%s') % pull_request_id,
744 return link_to(_('Pull request #%s') % pull_request_id,
745 url('pullrequest_show', repo_name=repo_name,
745 url('pullrequest_show', repo_name=repo_name,
746 pull_request_id=pull_request_id))
746 pull_request_id=pull_request_id))
747
747
748 def get_archive_name():
748 def get_archive_name():
749 archive_name = action_params
749 archive_name = action_params
750 return archive_name
750 return archive_name
751
751
752 # action : translated str, callback(extractor), icon
752 # action : translated str, callback(extractor), icon
753 action_map = {
753 action_map = {
754 'user_deleted_repo': (_('[deleted] repository'),
754 'user_deleted_repo': (_('[deleted] repository'),
755 None, 'icon-trashcan'),
755 None, 'icon-trashcan'),
756 'user_created_repo': (_('[created] repository'),
756 'user_created_repo': (_('[created] repository'),
757 None, 'icon-plus'),
757 None, 'icon-plus'),
758 'user_created_fork': (_('[created] repository as fork'),
758 'user_created_fork': (_('[created] repository as fork'),
759 None, 'icon-fork'),
759 None, 'icon-fork'),
760 'user_forked_repo': (_('[forked] repository'),
760 'user_forked_repo': (_('[forked] repository'),
761 get_fork_name, 'icon-fork'),
761 get_fork_name, 'icon-fork'),
762 'user_updated_repo': (_('[updated] repository'),
762 'user_updated_repo': (_('[updated] repository'),
763 None, 'icon-pencil'),
763 None, 'icon-pencil'),
764 'user_downloaded_archive': (_('[downloaded] archive from repository'),
764 'user_downloaded_archive': (_('[downloaded] archive from repository'),
765 get_archive_name, 'icon-download-cloud'),
765 get_archive_name, 'icon-download-cloud'),
766 'admin_deleted_repo': (_('[delete] repository'),
766 'admin_deleted_repo': (_('[delete] repository'),
767 None, 'icon-trashcan'),
767 None, 'icon-trashcan'),
768 'admin_created_repo': (_('[created] repository'),
768 'admin_created_repo': (_('[created] repository'),
769 None, 'icon-plus'),
769 None, 'icon-plus'),
770 'admin_forked_repo': (_('[forked] repository'),
770 'admin_forked_repo': (_('[forked] repository'),
771 None, 'icon-fork'),
771 None, 'icon-fork'),
772 'admin_updated_repo': (_('[updated] repository'),
772 'admin_updated_repo': (_('[updated] repository'),
773 None, 'icon-pencil'),
773 None, 'icon-pencil'),
774 'admin_created_user': (_('[created] user'),
774 'admin_created_user': (_('[created] user'),
775 get_user_name, 'icon-user'),
775 get_user_name, 'icon-user'),
776 'admin_updated_user': (_('[updated] user'),
776 'admin_updated_user': (_('[updated] user'),
777 get_user_name, 'icon-user'),
777 get_user_name, 'icon-user'),
778 'admin_created_users_group': (_('[created] user group'),
778 'admin_created_users_group': (_('[created] user group'),
779 get_users_group, 'icon-pencil'),
779 get_users_group, 'icon-pencil'),
780 'admin_updated_users_group': (_('[updated] user group'),
780 'admin_updated_users_group': (_('[updated] user group'),
781 get_users_group, 'icon-pencil'),
781 get_users_group, 'icon-pencil'),
782 'user_commented_revision': (_('[commented] on revision in repository'),
782 'user_commented_revision': (_('[commented] on revision in repository'),
783 get_cs_links, 'icon-comment'),
783 get_cs_links, 'icon-comment'),
784 'user_commented_pull_request': (_('[commented] on pull request for'),
784 'user_commented_pull_request': (_('[commented] on pull request for'),
785 get_pull_request, 'icon-comment'),
785 get_pull_request, 'icon-comment'),
786 'user_closed_pull_request': (_('[closed] pull request for'),
786 'user_closed_pull_request': (_('[closed] pull request for'),
787 get_pull_request, 'icon-ok'),
787 get_pull_request, 'icon-ok'),
788 'push': (_('[pushed] into'),
788 'push': (_('[pushed] into'),
789 get_cs_links, 'icon-move-up'),
789 get_cs_links, 'icon-move-up'),
790 'push_local': (_('[committed via Kallithea] into repository'),
790 'push_local': (_('[committed via Kallithea] into repository'),
791 get_cs_links, 'icon-pencil'),
791 get_cs_links, 'icon-pencil'),
792 'push_remote': (_('[pulled from remote] into repository'),
792 'push_remote': (_('[pulled from remote] into repository'),
793 get_cs_links, 'icon-move-up'),
793 get_cs_links, 'icon-move-up'),
794 'pull': (_('[pulled] from'),
794 'pull': (_('[pulled] from'),
795 None, 'icon-move-down'),
795 None, 'icon-move-down'),
796 'started_following_repo': (_('[started following] repository'),
796 'started_following_repo': (_('[started following] repository'),
797 None, 'icon-heart'),
797 None, 'icon-heart'),
798 'stopped_following_repo': (_('[stopped following] repository'),
798 'stopped_following_repo': (_('[stopped following] repository'),
799 None, 'icon-heart-empty'),
799 None, 'icon-heart-empty'),
800 }
800 }
801
801
802 action_str = action_map.get(action, action)
802 action_str = action_map.get(action, action)
803 if feed:
803 if feed:
804 action = action_str[0].replace('[', '').replace(']', '')
804 action = action_str[0].replace('[', '').replace(']', '')
805 else:
805 else:
806 action = action_str[0]\
806 action = action_str[0]\
807 .replace('[', '<span class="journal_highlight">')\
807 .replace('[', '<span class="journal_highlight">')\
808 .replace(']', '</span>')
808 .replace(']', '</span>')
809
809
810 action_params_func = lambda: ""
810 action_params_func = lambda: ""
811
811
812 if callable(action_str[1]):
812 if callable(action_str[1]):
813 action_params_func = action_str[1]
813 action_params_func = action_str[1]
814
814
815 def action_parser_icon():
815 def action_parser_icon():
816 action = user_log.action
816 action = user_log.action
817 action_params = None
817 action_params = None
818 x = action.split(':')
818 x = action.split(':')
819
819
820 if len(x) > 1:
820 if len(x) > 1:
821 action, action_params = x
821 action, action_params = x
822
822
823 tmpl = """<i class="%s" alt="%s"></i>"""
823 tmpl = """<i class="%s" alt="%s"></i>"""
824 ico = action_map.get(action, ['', '', ''])[2]
824 ico = action_map.get(action, ['', '', ''])[2]
825 return literal(tmpl % (ico, action))
825 return literal(tmpl % (ico, action))
826
826
827 # returned callbacks we need to call to get
827 # returned callbacks we need to call to get
828 return [lambda: literal(action), action_params_func, action_parser_icon]
828 return [lambda: literal(action), action_params_func, action_parser_icon]
829
829
830
830
831
831
832 #==============================================================================
832 #==============================================================================
833 # PERMS
833 # PERMS
834 #==============================================================================
834 #==============================================================================
835 from kallithea.lib.auth import HasPermissionAny, HasPermissionAll, \
835 from kallithea.lib.auth import HasPermissionAny, HasPermissionAll, \
836 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
836 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
837 HasRepoGroupPermissionAny
837 HasRepoGroupPermissionAny
838
838
839
839
840 #==============================================================================
840 #==============================================================================
841 # GRAVATAR URL
841 # GRAVATAR URL
842 #==============================================================================
842 #==============================================================================
843 def gravatar(email_address, cls='', size=30, ssl_enabled=True):
843 def gravatar(email_address, cls='', size=30, ssl_enabled=True):
844 """return html element of the gravatar
844 """return html element of the gravatar
845
845
846 This method will return an <img> with the resolution double the size (for
846 This method will return an <img> with the resolution double the size (for
847 retina screens) of the image. If the url returned from gravatar_url is
847 retina screens) of the image. If the url returned from gravatar_url is
848 empty then we fallback to using an icon.
848 empty then we fallback to using an icon.
849
849
850 """
850 """
851 src = gravatar_url(email_address, size*2, ssl_enabled)
851 src = gravatar_url(email_address, size*2, ssl_enabled)
852
852
853 # here it makes sense to use style="width: ..." (instead of, say, a
853 # here it makes sense to use style="width: ..." (instead of, say, a
854 # stylesheet) because we using this to generate a high-res (retina) size
854 # stylesheet) because we using this to generate a high-res (retina) size
855 tmpl = """<img alt="gravatar" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>"""
855 tmpl = """<img alt="gravatar" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>"""
856
856
857 # if src is empty then there was no gravatar, so we use a font icon
857 # if src is empty then there was no gravatar, so we use a font icon
858 if not src:
858 if not src:
859 tmpl = """<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
859 tmpl = """<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
860
860
861 tmpl = tmpl.format(cls=cls, size=size, src=src)
861 tmpl = tmpl.format(cls=cls, size=size, src=src)
862 return literal(tmpl)
862 return literal(tmpl)
863
863
864 def gravatar_url(email_address, size=30, ssl_enabled=True):
864 def gravatar_url(email_address, size=30, ssl_enabled=True):
865 # doh, we need to re-import those to mock it later
865 # doh, we need to re-import those to mock it later
866 from pylons import url
866 from pylons import url
867 from pylons import tmpl_context as c
867 from pylons import tmpl_context as c
868
868
869 _def = 'anonymous@kallithea-scm.org' # default gravatar
869 _def = 'anonymous@kallithea-scm.org' # default gravatar
870 _use_gravatar = c.visual.use_gravatar
870 _use_gravatar = c.visual.use_gravatar
871 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
871 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
872
872
873 email_address = email_address or _def
873 email_address = email_address or _def
874
874
875 if not _use_gravatar or not email_address or email_address == _def:
875 if not _use_gravatar or not email_address or email_address == _def:
876 return ""
876 return ""
877
877
878 if _use_gravatar:
878 if _use_gravatar:
879 _md5 = lambda s: hashlib.md5(s).hexdigest()
879 _md5 = lambda s: hashlib.md5(s).hexdigest()
880
880
881 tmpl = _gravatar_url
881 tmpl = _gravatar_url
882 parsed_url = urlparse.urlparse(url.current(qualified=True))
882 parsed_url = urlparse.urlparse(url.current(qualified=True))
883 tmpl = tmpl.replace('{email}', email_address)\
883 tmpl = tmpl.replace('{email}', email_address)\
884 .replace('{md5email}', _md5(safe_str(email_address).lower())) \
884 .replace('{md5email}', _md5(safe_str(email_address).lower())) \
885 .replace('{netloc}', parsed_url.netloc)\
885 .replace('{netloc}', parsed_url.netloc)\
886 .replace('{scheme}', parsed_url.scheme)\
886 .replace('{scheme}', parsed_url.scheme)\
887 .replace('{size}', safe_str(size))
887 .replace('{size}', safe_str(size))
888 return tmpl
888 return tmpl
889
889
890 class Page(_Page):
890 class Page(_Page):
891 """
891 """
892 Custom pager to match rendering style with YUI paginator
892 Custom pager to match rendering style with YUI paginator
893 """
893 """
894
894
895 def _get_pos(self, cur_page, max_page, items):
895 def _get_pos(self, cur_page, max_page, items):
896 edge = (items / 2) + 1
896 edge = (items / 2) + 1
897 if (cur_page <= edge):
897 if (cur_page <= edge):
898 radius = max(items / 2, items - cur_page)
898 radius = max(items / 2, items - cur_page)
899 elif (max_page - cur_page) < edge:
899 elif (max_page - cur_page) < edge:
900 radius = (items - 1) - (max_page - cur_page)
900 radius = (items - 1) - (max_page - cur_page)
901 else:
901 else:
902 radius = items / 2
902 radius = items / 2
903
903
904 left = max(1, (cur_page - (radius)))
904 left = max(1, (cur_page - (radius)))
905 right = min(max_page, cur_page + (radius))
905 right = min(max_page, cur_page + (radius))
906 return left, cur_page, right
906 return left, cur_page, right
907
907
908 def _range(self, regexp_match):
908 def _range(self, regexp_match):
909 """
909 """
910 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
910 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
911
911
912 Arguments:
912 Arguments:
913
913
914 regexp_match
914 regexp_match
915 A "re" (regular expressions) match object containing the
915 A "re" (regular expressions) match object containing the
916 radius of linked pages around the current page in
916 radius of linked pages around the current page in
917 regexp_match.group(1) as a string
917 regexp_match.group(1) as a string
918
918
919 This function is supposed to be called as a callable in
919 This function is supposed to be called as a callable in
920 re.sub.
920 re.sub.
921
921
922 """
922 """
923 radius = int(regexp_match.group(1))
923 radius = int(regexp_match.group(1))
924
924
925 # Compute the first and last page number within the radius
925 # Compute the first and last page number within the radius
926 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
926 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
927 # -> leftmost_page = 5
927 # -> leftmost_page = 5
928 # -> rightmost_page = 9
928 # -> rightmost_page = 9
929 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
929 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
930 self.last_page,
930 self.last_page,
931 (radius * 2) + 1)
931 (radius * 2) + 1)
932 nav_items = []
932 nav_items = []
933
933
934 # Create a link to the first page (unless we are on the first page
934 # Create a link to the first page (unless we are on the first page
935 # or there would be no need to insert '..' spacers)
935 # or there would be no need to insert '..' spacers)
936 if self.page != self.first_page and self.first_page < leftmost_page:
936 if self.page != self.first_page and self.first_page < leftmost_page:
937 nav_items.append(self._pagerlink(self.first_page, self.first_page))
937 nav_items.append(self._pagerlink(self.first_page, self.first_page))
938
938
939 # Insert dots if there are pages between the first page
939 # Insert dots if there are pages between the first page
940 # and the currently displayed page range
940 # and the currently displayed page range
941 if leftmost_page - self.first_page > 1:
941 if leftmost_page - self.first_page > 1:
942 # Wrap in a SPAN tag if nolink_attr is set
942 # Wrap in a SPAN tag if nolink_attr is set
943 text = '..'
943 text = '..'
944 if self.dotdot_attr:
944 if self.dotdot_attr:
945 text = HTML.span(c=text, **self.dotdot_attr)
945 text = HTML.span(c=text, **self.dotdot_attr)
946 nav_items.append(text)
946 nav_items.append(text)
947
947
948 for thispage in xrange(leftmost_page, rightmost_page + 1):
948 for thispage in xrange(leftmost_page, rightmost_page + 1):
949 # Highlight the current page number and do not use a link
949 # Highlight the current page number and do not use a link
950 if thispage == self.page:
950 if thispage == self.page:
951 text = '%s' % (thispage,)
951 text = '%s' % (thispage,)
952 # Wrap in a SPAN tag if nolink_attr is set
952 # Wrap in a SPAN tag if nolink_attr is set
953 if self.curpage_attr:
953 if self.curpage_attr:
954 text = HTML.span(c=text, **self.curpage_attr)
954 text = HTML.span(c=text, **self.curpage_attr)
955 nav_items.append(text)
955 nav_items.append(text)
956 # Otherwise create just a link to that page
956 # Otherwise create just a link to that page
957 else:
957 else:
958 text = '%s' % (thispage,)
958 text = '%s' % (thispage,)
959 nav_items.append(self._pagerlink(thispage, text))
959 nav_items.append(self._pagerlink(thispage, text))
960
960
961 # Insert dots if there are pages between the displayed
961 # Insert dots if there are pages between the displayed
962 # page numbers and the end of the page range
962 # page numbers and the end of the page range
963 if self.last_page - rightmost_page > 1:
963 if self.last_page - rightmost_page > 1:
964 text = '..'
964 text = '..'
965 # Wrap in a SPAN tag if nolink_attr is set
965 # Wrap in a SPAN tag if nolink_attr is set
966 if self.dotdot_attr:
966 if self.dotdot_attr:
967 text = HTML.span(c=text, **self.dotdot_attr)
967 text = HTML.span(c=text, **self.dotdot_attr)
968 nav_items.append(text)
968 nav_items.append(text)
969
969
970 # Create a link to the very last page (unless we are on the last
970 # Create a link to the very last page (unless we are on the last
971 # page or there would be no need to insert '..' spacers)
971 # page or there would be no need to insert '..' spacers)
972 if self.page != self.last_page and rightmost_page < self.last_page:
972 if self.page != self.last_page and rightmost_page < self.last_page:
973 nav_items.append(self._pagerlink(self.last_page, self.last_page))
973 nav_items.append(self._pagerlink(self.last_page, self.last_page))
974
974
975 #_page_link = url.current()
975 #_page_link = url.current()
976 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
976 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
977 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
977 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
978 return self.separator.join(nav_items)
978 return self.separator.join(nav_items)
979
979
980 def pager(self, format='~2~', page_param='page', partial_param='partial',
980 def pager(self, format='~2~', page_param='page', partial_param='partial',
981 show_if_single_page=False, separator=' ', onclick=None,
981 show_if_single_page=False, separator=' ', onclick=None,
982 symbol_first='<<', symbol_last='>>',
982 symbol_first='<<', symbol_last='>>',
983 symbol_previous='<', symbol_next='>',
983 symbol_previous='<', symbol_next='>',
984 link_attr={'class': 'pager_link', 'rel': 'prerender'},
984 link_attr={'class': 'pager_link', 'rel': 'prerender'},
985 curpage_attr={'class': 'pager_curpage'},
985 curpage_attr={'class': 'pager_curpage'},
986 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
986 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
987
987
988 self.curpage_attr = curpage_attr
988 self.curpage_attr = curpage_attr
989 self.separator = separator
989 self.separator = separator
990 self.pager_kwargs = kwargs
990 self.pager_kwargs = kwargs
991 self.page_param = page_param
991 self.page_param = page_param
992 self.partial_param = partial_param
992 self.partial_param = partial_param
993 self.onclick = onclick
993 self.onclick = onclick
994 self.link_attr = link_attr
994 self.link_attr = link_attr
995 self.dotdot_attr = dotdot_attr
995 self.dotdot_attr = dotdot_attr
996
996
997 # Don't show navigator if there is no more than one page
997 # Don't show navigator if there is no more than one page
998 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
998 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
999 return ''
999 return ''
1000
1000
1001 from string import Template
1001 from string import Template
1002 # Replace ~...~ in token format by range of pages
1002 # Replace ~...~ in token format by range of pages
1003 result = re.sub(r'~(\d+)~', self._range, format)
1003 result = re.sub(r'~(\d+)~', self._range, format)
1004
1004
1005 # Interpolate '%' variables
1005 # Interpolate '%' variables
1006 result = Template(result).safe_substitute({
1006 result = Template(result).safe_substitute({
1007 'first_page': self.first_page,
1007 'first_page': self.first_page,
1008 'last_page': self.last_page,
1008 'last_page': self.last_page,
1009 'page': self.page,
1009 'page': self.page,
1010 'page_count': self.page_count,
1010 'page_count': self.page_count,
1011 'items_per_page': self.items_per_page,
1011 'items_per_page': self.items_per_page,
1012 'first_item': self.first_item,
1012 'first_item': self.first_item,
1013 'last_item': self.last_item,
1013 'last_item': self.last_item,
1014 'item_count': self.item_count,
1014 'item_count': self.item_count,
1015 'link_first': self.page > self.first_page and \
1015 'link_first': self.page > self.first_page and \
1016 self._pagerlink(self.first_page, symbol_first) or '',
1016 self._pagerlink(self.first_page, symbol_first) or '',
1017 'link_last': self.page < self.last_page and \
1017 'link_last': self.page < self.last_page and \
1018 self._pagerlink(self.last_page, symbol_last) or '',
1018 self._pagerlink(self.last_page, symbol_last) or '',
1019 'link_previous': self.previous_page and \
1019 'link_previous': self.previous_page and \
1020 self._pagerlink(self.previous_page, symbol_previous) \
1020 self._pagerlink(self.previous_page, symbol_previous) \
1021 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1021 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1022 'link_next': self.next_page and \
1022 'link_next': self.next_page and \
1023 self._pagerlink(self.next_page, symbol_next) \
1023 self._pagerlink(self.next_page, symbol_next) \
1024 or HTML.span(symbol_next, class_="yui-pg-next")
1024 or HTML.span(symbol_next, class_="yui-pg-next")
1025 })
1025 })
1026
1026
1027 return literal(result)
1027 return literal(result)
1028
1028
1029
1029
1030 #==============================================================================
1030 #==============================================================================
1031 # REPO PAGER, PAGER FOR REPOSITORY
1031 # REPO PAGER, PAGER FOR REPOSITORY
1032 #==============================================================================
1032 #==============================================================================
1033 class RepoPage(Page):
1033 class RepoPage(Page):
1034
1034
1035 def __init__(self, collection, page=1, items_per_page=20,
1035 def __init__(self, collection, page=1, items_per_page=20,
1036 item_count=None, url=None, **kwargs):
1036 item_count=None, url=None, **kwargs):
1037
1037
1038 """Create a "RepoPage" instance. special pager for paging
1038 """Create a "RepoPage" instance. special pager for paging
1039 repository
1039 repository
1040 """
1040 """
1041 self._url_generator = url
1041 self._url_generator = url
1042
1042
1043 # Safe the kwargs class-wide so they can be used in the pager() method
1043 # Safe the kwargs class-wide so they can be used in the pager() method
1044 self.kwargs = kwargs
1044 self.kwargs = kwargs
1045
1045
1046 # Save a reference to the collection
1046 # Save a reference to the collection
1047 self.original_collection = collection
1047 self.original_collection = collection
1048
1048
1049 self.collection = collection
1049 self.collection = collection
1050
1050
1051 # The self.page is the number of the current page.
1051 # The self.page is the number of the current page.
1052 # The first page has the number 1!
1052 # The first page has the number 1!
1053 try:
1053 try:
1054 self.page = int(page) # make it int() if we get it as a string
1054 self.page = int(page) # make it int() if we get it as a string
1055 except (ValueError, TypeError):
1055 except (ValueError, TypeError):
1056 self.page = 1
1056 self.page = 1
1057
1057
1058 self.items_per_page = items_per_page
1058 self.items_per_page = items_per_page
1059
1059
1060 # Unless the user tells us how many items the collections has
1060 # Unless the user tells us how many items the collections has
1061 # we calculate that ourselves.
1061 # we calculate that ourselves.
1062 if item_count is not None:
1062 if item_count is not None:
1063 self.item_count = item_count
1063 self.item_count = item_count
1064 else:
1064 else:
1065 self.item_count = len(self.collection)
1065 self.item_count = len(self.collection)
1066
1066
1067 # Compute the number of the first and last available page
1067 # Compute the number of the first and last available page
1068 if self.item_count > 0:
1068 if self.item_count > 0:
1069 self.first_page = 1
1069 self.first_page = 1
1070 self.page_count = int(math.ceil(float(self.item_count) /
1070 self.page_count = int(math.ceil(float(self.item_count) /
1071 self.items_per_page))
1071 self.items_per_page))
1072 self.last_page = self.first_page + self.page_count - 1
1072 self.last_page = self.first_page + self.page_count - 1
1073
1073
1074 # Make sure that the requested page number is the range of
1074 # Make sure that the requested page number is the range of
1075 # valid pages
1075 # valid pages
1076 if self.page > self.last_page:
1076 if self.page > self.last_page:
1077 self.page = self.last_page
1077 self.page = self.last_page
1078 elif self.page < self.first_page:
1078 elif self.page < self.first_page:
1079 self.page = self.first_page
1079 self.page = self.first_page
1080
1080
1081 # Note: the number of items on this page can be less than
1081 # Note: the number of items on this page can be less than
1082 # items_per_page if the last page is not full
1082 # items_per_page if the last page is not full
1083 self.first_item = max(0, (self.item_count) - (self.page *
1083 self.first_item = max(0, (self.item_count) - (self.page *
1084 items_per_page))
1084 items_per_page))
1085 self.last_item = ((self.item_count - 1) - items_per_page *
1085 self.last_item = ((self.item_count - 1) - items_per_page *
1086 (self.page - 1))
1086 (self.page - 1))
1087
1087
1088 self.items = list(self.collection[self.first_item:self.last_item + 1])
1088 self.items = list(self.collection[self.first_item:self.last_item + 1])
1089
1089
1090 # Links to previous and next page
1090 # Links to previous and next page
1091 if self.page > self.first_page:
1091 if self.page > self.first_page:
1092 self.previous_page = self.page - 1
1092 self.previous_page = self.page - 1
1093 else:
1093 else:
1094 self.previous_page = None
1094 self.previous_page = None
1095
1095
1096 if self.page < self.last_page:
1096 if self.page < self.last_page:
1097 self.next_page = self.page + 1
1097 self.next_page = self.page + 1
1098 else:
1098 else:
1099 self.next_page = None
1099 self.next_page = None
1100
1100
1101 # No items available
1101 # No items available
1102 else:
1102 else:
1103 self.first_page = None
1103 self.first_page = None
1104 self.page_count = 0
1104 self.page_count = 0
1105 self.last_page = None
1105 self.last_page = None
1106 self.first_item = None
1106 self.first_item = None
1107 self.last_item = None
1107 self.last_item = None
1108 self.previous_page = None
1108 self.previous_page = None
1109 self.next_page = None
1109 self.next_page = None
1110 self.items = []
1110 self.items = []
1111
1111
1112 # This is a subclass of the 'list' type. Initialise the list now.
1112 # This is a subclass of the 'list' type. Initialise the list now.
1113 list.__init__(self, reversed(self.items))
1113 list.__init__(self, reversed(self.items))
1114
1114
1115
1115
1116 def changed_tooltip(nodes):
1116 def changed_tooltip(nodes):
1117 """
1117 """
1118 Generates a html string for changed nodes in changeset page.
1118 Generates a html string for changed nodes in changeset page.
1119 It limits the output to 30 entries
1119 It limits the output to 30 entries
1120
1120
1121 :param nodes: LazyNodesGenerator
1121 :param nodes: LazyNodesGenerator
1122 """
1122 """
1123 if nodes:
1123 if nodes:
1124 pref = ': <br/> '
1124 pref = ': <br/> '
1125 suf = ''
1125 suf = ''
1126 if len(nodes) > 30:
1126 if len(nodes) > 30:
1127 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1127 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1128 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1128 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1129 for x in nodes[:30]]) + suf)
1129 for x in nodes[:30]]) + suf)
1130 else:
1130 else:
1131 return ': ' + _('No Files')
1131 return ': ' + _('No Files')
1132
1132
1133
1133
1134 def repo_link(groups_and_repos):
1134 def repo_link(groups_and_repos):
1135 """
1135 """
1136 Makes a breadcrumbs link to repo within a group
1136 Makes a breadcrumbs link to repo within a group
1137 joins &raquo; on each group to create a fancy link
1137 joins &raquo; on each group to create a fancy link
1138
1138
1139 ex::
1139 ex::
1140 group >> subgroup >> repo
1140 group >> subgroup >> repo
1141
1141
1142 :param groups_and_repos:
1142 :param groups_and_repos:
1143 :param last_url:
1143 :param last_url:
1144 """
1144 """
1145 groups, just_name, repo_name = groups_and_repos
1145 groups, just_name, repo_name = groups_and_repos
1146 last_url = url('summary_home', repo_name=repo_name)
1146 last_url = url('summary_home', repo_name=repo_name)
1147 last_link = link_to(just_name, last_url)
1147 last_link = link_to(just_name, last_url)
1148
1148
1149 def make_link(group):
1149 def make_link(group):
1150 return link_to(group.name,
1150 return link_to(group.name,
1151 url('repos_group_home', group_name=group.group_name))
1151 url('repos_group_home', group_name=group.group_name))
1152 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1152 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1153
1153
1154
1154
1155 def fancy_file_stats(stats):
1155 def fancy_file_stats(stats):
1156 """
1156 """
1157 Displays a fancy two colored bar for number of added/deleted
1157 Displays a fancy two colored bar for number of added/deleted
1158 lines of code on file
1158 lines of code on file
1159
1159
1160 :param stats: two element list of added/deleted lines of code
1160 :param stats: two element list of added/deleted lines of code
1161 """
1161 """
1162 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1162 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1163 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1163 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1164
1164
1165 def cgen(l_type, a_v, d_v):
1165 def cgen(l_type, a_v, d_v):
1166 mapping = {'tr': 'top-right-rounded-corner-mid',
1166 mapping = {'tr': 'top-right-rounded-corner-mid',
1167 'tl': 'top-left-rounded-corner-mid',
1167 'tl': 'top-left-rounded-corner-mid',
1168 'br': 'bottom-right-rounded-corner-mid',
1168 'br': 'bottom-right-rounded-corner-mid',
1169 'bl': 'bottom-left-rounded-corner-mid'}
1169 'bl': 'bottom-left-rounded-corner-mid'}
1170 map_getter = lambda x: mapping[x]
1170 map_getter = lambda x: mapping[x]
1171
1171
1172 if l_type == 'a' and d_v:
1172 if l_type == 'a' and d_v:
1173 #case when added and deleted are present
1173 #case when added and deleted are present
1174 return ' '.join(map(map_getter, ['tl', 'bl']))
1174 return ' '.join(map(map_getter, ['tl', 'bl']))
1175
1175
1176 if l_type == 'a' and not d_v:
1176 if l_type == 'a' and not d_v:
1177 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1177 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1178
1178
1179 if l_type == 'd' and a_v:
1179 if l_type == 'd' and a_v:
1180 return ' '.join(map(map_getter, ['tr', 'br']))
1180 return ' '.join(map(map_getter, ['tr', 'br']))
1181
1181
1182 if l_type == 'd' and not a_v:
1182 if l_type == 'd' and not a_v:
1183 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1183 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1184
1184
1185 a, d = stats['added'], stats['deleted']
1185 a, d = stats['added'], stats['deleted']
1186 width = 100
1186 width = 100
1187
1187
1188 if stats['binary']:
1188 if stats['binary']:
1189 #binary mode
1189 #binary mode
1190 lbl = ''
1190 lbl = ''
1191 bin_op = 1
1191 bin_op = 1
1192
1192
1193 if BIN_FILENODE in stats['ops']:
1193 if BIN_FILENODE in stats['ops']:
1194 lbl = 'bin+'
1194 lbl = 'bin+'
1195
1195
1196 if NEW_FILENODE in stats['ops']:
1196 if NEW_FILENODE in stats['ops']:
1197 lbl += _('new file')
1197 lbl += _('new file')
1198 bin_op = NEW_FILENODE
1198 bin_op = NEW_FILENODE
1199 elif MOD_FILENODE in stats['ops']:
1199 elif MOD_FILENODE in stats['ops']:
1200 lbl += _('mod')
1200 lbl += _('mod')
1201 bin_op = MOD_FILENODE
1201 bin_op = MOD_FILENODE
1202 elif DEL_FILENODE in stats['ops']:
1202 elif DEL_FILENODE in stats['ops']:
1203 lbl += _('del')
1203 lbl += _('del')
1204 bin_op = DEL_FILENODE
1204 bin_op = DEL_FILENODE
1205 elif RENAMED_FILENODE in stats['ops']:
1205 elif RENAMED_FILENODE in stats['ops']:
1206 lbl += _('rename')
1206 lbl += _('rename')
1207 bin_op = RENAMED_FILENODE
1207 bin_op = RENAMED_FILENODE
1208
1208
1209 #chmod can go with other operations
1209 #chmod can go with other operations
1210 if CHMOD_FILENODE in stats['ops']:
1210 if CHMOD_FILENODE in stats['ops']:
1211 _org_lbl = _('chmod')
1211 _org_lbl = _('chmod')
1212 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1212 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1213
1213
1214 #import ipdb;ipdb.set_trace()
1214 #import ipdb;ipdb.set_trace()
1215 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1215 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1216 b_a = '<div class="bin bin1" style="width:0%%"></div>'
1216 b_a = '<div class="bin bin1" style="width:0%%"></div>'
1217 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1217 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1218
1218
1219 t = stats['added'] + stats['deleted']
1219 t = stats['added'] + stats['deleted']
1220 unit = float(width) / (t or 1)
1220 unit = float(width) / (t or 1)
1221
1221
1222 # needs > 9% of width to be visible or 0 to be hidden
1222 # needs > 9% of width to be visible or 0 to be hidden
1223 a_p = max(9, unit * a) if a > 0 else 0
1223 a_p = max(9, unit * a) if a > 0 else 0
1224 d_p = max(9, unit * d) if d > 0 else 0
1224 d_p = max(9, unit * d) if d > 0 else 0
1225 p_sum = a_p + d_p
1225 p_sum = a_p + d_p
1226
1226
1227 if p_sum > width:
1227 if p_sum > width:
1228 #adjust the percentage to be == 100% since we adjusted to 9
1228 #adjust the percentage to be == 100% since we adjusted to 9
1229 if a_p > d_p:
1229 if a_p > d_p:
1230 a_p = a_p - (p_sum - width)
1230 a_p = a_p - (p_sum - width)
1231 else:
1231 else:
1232 d_p = d_p - (p_sum - width)
1232 d_p = d_p - (p_sum - width)
1233
1233
1234 a_v = a if a > 0 else ''
1234 a_v = a if a > 0 else ''
1235 d_v = d if d > 0 else ''
1235 d_v = d if d > 0 else ''
1236
1236
1237 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1237 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1238 cgen('a', a_v, d_v), a_p, a_v
1238 cgen('a', a_v, d_v), a_p, a_v
1239 )
1239 )
1240 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1240 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1241 cgen('d', a_v, d_v), d_p, d_v
1241 cgen('d', a_v, d_v), d_p, d_v
1242 )
1242 )
1243 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1243 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1244
1244
1245
1245
1246 def urlify_text(text_, safe=True):
1246 def urlify_text(text_, safe=True):
1247 """
1247 """
1248 Extract urls from text and make html links out of them
1248 Extract urls from text and make html links out of them
1249
1249
1250 :param text_:
1250 :param text_:
1251 """
1251 """
1252
1252
1253 def url_func(match_obj):
1253 def url_func(match_obj):
1254 url_full = match_obj.groups()[0]
1254 url_full = match_obj.groups()[0]
1255 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1255 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1256 _newtext = url_re.sub(url_func, text_)
1256 _newtext = url_re.sub(url_func, text_)
1257 if safe:
1257 if safe:
1258 return literal(_newtext)
1258 return literal(_newtext)
1259 return _newtext
1259 return _newtext
1260
1260
1261
1261
1262 def urlify_changesets(text_, repository):
1262 def urlify_changesets(text_, repository):
1263 """
1263 """
1264 Extract revision ids from changeset and make link from them
1264 Extract revision ids from changeset and make link from them
1265
1265
1266 :param text_:
1266 :param text_:
1267 :param repository: repo name to build the URL with
1267 :param repository: repo name to build the URL with
1268 """
1268 """
1269 from pylons import url # doh, we need to re-import url to mock it later
1269 from pylons import url # doh, we need to re-import url to mock it later
1270
1270
1271 def url_func(match_obj):
1271 def url_func(match_obj):
1272 rev = match_obj.group(0)
1272 rev = match_obj.group(0)
1273 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1273 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1274 'url': url('changeset_home', repo_name=repository, revision=rev),
1274 'url': url('changeset_home', repo_name=repository, revision=rev),
1275 'rev': rev,
1275 'rev': rev,
1276 }
1276 }
1277
1277
1278 return re.sub(r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])', url_func, text_)
1278 return re.sub(r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])', url_func, text_)
1279
1279
1280 def linkify_others(t, l):
1280 def linkify_others(t, l):
1281 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1281 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1282 links = []
1282 links = []
1283 for e in urls.split(t):
1283 for e in urls.split(t):
1284 if not urls.match(e):
1284 if not urls.match(e):
1285 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1285 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1286 else:
1286 else:
1287 links.append(e)
1287 links.append(e)
1288
1288
1289 return ''.join(links)
1289 return ''.join(links)
1290
1290
1291 def urlify_commit(text_, repository, link_=None):
1291 def urlify_commit(text_, repository, link_=None):
1292 """
1292 """
1293 Parses given text message and makes proper links.
1293 Parses given text message and makes proper links.
1294 issues are linked to given issue-server, and rest is a changeset link
1294 issues are linked to given issue-server, and rest is a changeset link
1295 if link_ is given, in other case it's a plain text
1295 if link_ is given, in other case it's a plain text
1296
1296
1297 :param text_:
1297 :param text_:
1298 :param repository:
1298 :param repository:
1299 :param link_: changeset link
1299 :param link_: changeset link
1300 """
1300 """
1301 def escaper(string):
1301 def escaper(string):
1302 return string.replace('<', '&lt;').replace('>', '&gt;')
1302 return string.replace('<', '&lt;').replace('>', '&gt;')
1303
1303
1304 # urlify changesets - extract revisions and make link out of them
1304 # urlify changesets - extract revisions and make link out of them
1305 newtext = urlify_changesets(escaper(text_), repository)
1305 newtext = urlify_changesets(escaper(text_), repository)
1306
1306
1307 # extract http/https links and make them real urls
1307 # extract http/https links and make them real urls
1308 newtext = urlify_text(newtext, safe=False)
1308 newtext = urlify_text(newtext, safe=False)
1309
1309
1310 newtext = urlify_issues(newtext, repository, link_)
1310 newtext = urlify_issues(newtext, repository, link_)
1311
1311
1312 return literal(newtext)
1312 return literal(newtext)
1313
1313
1314 def urlify_issues(newtext, repository, link_=None):
1314 def urlify_issues(newtext, repository, link_=None):
1315 from kallithea import CONFIG as conf
1315 from kallithea import CONFIG as conf
1316
1316
1317 # allow multiple issue servers to be used
1317 # allow multiple issue servers to be used
1318 valid_indices = [
1318 valid_indices = [
1319 x.group(1)
1319 x.group(1)
1320 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1320 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1321 if x and 'issue_server_link%s' % x.group(1) in conf
1321 if x and 'issue_server_link%s' % x.group(1) in conf
1322 and 'issue_prefix%s' % x.group(1) in conf
1322 and 'issue_prefix%s' % x.group(1) in conf
1323 ]
1323 ]
1324
1324
1325 if valid_indices:
1325 if valid_indices:
1326 log.debug('found issue server suffixes `%s` during valuation of: %s'
1326 log.debug('found issue server suffixes `%s` during valuation of: %s'
1327 % (','.join(valid_indices), newtext))
1327 % (','.join(valid_indices), newtext))
1328
1328
1329 for pattern_index in valid_indices:
1329 for pattern_index in valid_indices:
1330 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1330 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1331 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1331 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1332 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1332 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1333
1333
1334 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1334 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1335 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1335 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1336 ISSUE_PREFIX))
1336 ISSUE_PREFIX))
1337
1337
1338 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1338 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1339
1339
1340 def url_func(match_obj):
1340 def url_func(match_obj):
1341 pref = ''
1341 pref = ''
1342 if match_obj.group().startswith(' '):
1342 if match_obj.group().startswith(' '):
1343 pref = ' '
1343 pref = ' '
1344
1344
1345 issue_id = ''.join(match_obj.groups())
1345 issue_id = ''.join(match_obj.groups())
1346 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1346 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1347 if repository:
1347 if repository:
1348 issue_url = issue_url.replace('{repo}', repository)
1348 issue_url = issue_url.replace('{repo}', repository)
1349 repo_name = repository.split(URL_SEP)[-1]
1349 repo_name = repository.split(URL_SEP)[-1]
1350 issue_url = issue_url.replace('{repo_name}', repo_name)
1350 issue_url = issue_url.replace('{repo_name}', repo_name)
1351
1351
1352 return (
1352 return (
1353 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1353 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1354 '%(issue-prefix)s%(id-repr)s'
1354 '%(issue-prefix)s%(id-repr)s'
1355 '</a>'
1355 '</a>'
1356 ) % {
1356 ) % {
1357 'pref': pref,
1357 'pref': pref,
1358 'cls': 'issue-tracker-link',
1358 'cls': 'issue-tracker-link',
1359 'url': issue_url,
1359 'url': issue_url,
1360 'id-repr': issue_id,
1360 'id-repr': issue_id,
1361 'issue-prefix': ISSUE_PREFIX,
1361 'issue-prefix': ISSUE_PREFIX,
1362 'serv': ISSUE_SERVER_LNK,
1362 'serv': ISSUE_SERVER_LNK,
1363 }
1363 }
1364 newtext = URL_PAT.sub(url_func, newtext)
1364 newtext = URL_PAT.sub(url_func, newtext)
1365 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1365 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1366
1366
1367 # if we actually did something above
1367 # if we actually did something above
1368 if link_:
1368 if link_:
1369 # wrap not links into final link => link_
1369 # wrap not links into final link => link_
1370 newtext = linkify_others(newtext, link_)
1370 newtext = linkify_others(newtext, link_)
1371 return newtext
1371 return newtext
1372
1372
1373
1373
1374 def rst(source):
1374 def rst(source):
1375 return literal('<div class="rst-block">%s</div>' %
1375 return literal('<div class="rst-block">%s</div>' %
1376 MarkupRenderer.rst(source))
1376 MarkupRenderer.rst(source))
1377
1377
1378
1378
1379 def rst_w_mentions(source):
1379 def rst_w_mentions(source):
1380 """
1380 """
1381 Wrapped rst renderer with @mention highlighting
1381 Wrapped rst renderer with @mention highlighting
1382
1382
1383 :param source:
1383 :param source:
1384 """
1384 """
1385 return literal('<div class="rst-block">%s</div>' %
1385 return literal('<div class="rst-block">%s</div>' %
1386 MarkupRenderer.rst_with_mentions(source))
1386 MarkupRenderer.rst_with_mentions(source))
1387
1387
1388 def short_ref(ref_type, ref_name):
1388 def short_ref(ref_type, ref_name):
1389 if ref_type == 'rev':
1389 if ref_type == 'rev':
1390 return short_id(ref_name)
1390 return short_id(ref_name)
1391 return ref_name
1391 return ref_name
1392
1392
1393 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1393 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1394 """
1394 """
1395 Return full markup for a href to changeset_home for a changeset.
1395 Return full markup for a href to changeset_home for a changeset.
1396 If ref_type is branch it will link to changelog.
1396 If ref_type is branch it will link to changelog.
1397 ref_name is shortened if ref_type is 'rev'.
1397 ref_name is shortened if ref_type is 'rev'.
1398 if rev is specified show it too, explicitly linking to that revision.
1398 if rev is specified show it too, explicitly linking to that revision.
1399 """
1399 """
1400 txt = short_ref(ref_type, ref_name)
1400 txt = short_ref(ref_type, ref_name)
1401 if ref_type == 'branch':
1401 if ref_type == 'branch':
1402 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1402 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1403 else:
1403 else:
1404 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1404 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1405 l = link_to(repo_name + '#' + txt, u)
1405 l = link_to(repo_name + '#' + txt, u)
1406 if rev and ref_type != 'rev':
1406 if rev and ref_type != 'rev':
1407 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1407 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1408 return l
1408 return l
1409
1409
1410 def changeset_status(repo, revision):
1410 def changeset_status(repo, revision):
1411 return ChangesetStatusModel().get_status(repo, revision)
1411 return ChangesetStatusModel().get_status(repo, revision)
1412
1412
1413
1413
1414 def changeset_status_lbl(changeset_status):
1414 def changeset_status_lbl(changeset_status):
1415 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1415 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1416
1416
1417
1417
1418 def get_permission_name(key):
1418 def get_permission_name(key):
1419 return dict(Permission.PERMS).get(key)
1419 return dict(Permission.PERMS).get(key)
1420
1420
1421
1421
1422 def journal_filter_help():
1422 def journal_filter_help():
1423 return _(textwrap.dedent('''
1423 return _(textwrap.dedent('''
1424 Example filter terms:
1424 Example filter terms:
1425 repository:vcs
1425 repository:vcs
1426 username:developer
1426 username:developer
1427 action:*push*
1427 action:*push*
1428 ip:127.0.0.1
1428 ip:127.0.0.1
1429 date:20120101
1429 date:20120101
1430 date:[20120101100000 TO 20120102]
1430 date:[20120101100000 TO 20120102]
1431
1431
1432 Generate wildcards using '*' character:
1432 Generate wildcards using '*' character:
1433 "repository:vcs*" - search everything starting with 'vcs'
1433 "repository:vcs*" - search everything starting with 'vcs'
1434 "repository:*vcs*" - search for repository containing 'vcs'
1434 "repository:*vcs*" - search for repository containing 'vcs'
1435
1435
1436 Optional AND / OR operators in queries
1436 Optional AND / OR operators in queries
1437 "repository:vcs OR repository:test"
1437 "repository:vcs OR repository:test"
1438 "username:test AND repository:test*"
1438 "username:test AND repository:test*"
1439 '''))
1439 '''))
1440
1440
1441
1441
1442 def not_mapped_error(repo_name):
1442 def not_mapped_error(repo_name):
1443 flash(_('%s repository is not mapped to db perhaps'
1443 flash(_('%s repository is not mapped to db perhaps'
1444 ' it was created or renamed from the filesystem'
1444 ' it was created or renamed from the filesystem'
1445 ' please run the application again'
1445 ' please run the application again'
1446 ' in order to rescan repositories') % repo_name, category='error')
1446 ' in order to rescan repositories') % repo_name, category='error')
1447
1447
1448
1448
1449 def ip_range(ip_addr):
1449 def ip_range(ip_addr):
1450 from kallithea.model.db import UserIpMap
1450 from kallithea.model.db import UserIpMap
1451 s, e = UserIpMap._get_ip_range(ip_addr)
1451 s, e = UserIpMap._get_ip_range(ip_addr)
1452 return '%s - %s' % (s, e)
1452 return '%s - %s' % (s, e)
General Comments 0
You need to be logged in to leave comments. Login now