##// END OF EJS Templates
helpers: refactor and optimize urlify_issues...
Mads Kiilerich -
r6199:39a59e69 default
parent child Browse files
Show More
@@ -1,1498 +1,1500 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Helper functions
15 Helper functions
16
16
17 Consists of functions to typically be used within templates, but also
17 Consists of functions to typically be used within templates, but also
18 available to Controllers. This module is available to both as 'h'.
18 available to Controllers. This module is available to both as 'h'.
19 """
19 """
20 import hashlib
20 import hashlib
21 import StringIO
21 import StringIO
22 import math
22 import math
23 import logging
23 import logging
24 import re
24 import re
25 import urlparse
25 import urlparse
26 import textwrap
26 import textwrap
27
27
28 from beaker.cache import cache_region
28 from beaker.cache import cache_region
29 from pygments.formatters.html import HtmlFormatter
29 from pygments.formatters.html import HtmlFormatter
30 from pygments import highlight as code_highlight
30 from pygments import highlight as code_highlight
31 from pylons.i18n.translation import _, ungettext
31 from pylons.i18n.translation import _, ungettext
32
32
33 from webhelpers.html import literal, HTML, escape
33 from webhelpers.html import literal, HTML, escape
34 from webhelpers.html.tools import *
34 from webhelpers.html.tools import *
35 from webhelpers.html.builder import make_tag
35 from webhelpers.html.builder import make_tag
36 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
36 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
37 end_form, file, hidden, image, javascript_link, link_to, \
37 end_form, file, hidden, image, javascript_link, link_to, \
38 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
38 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
39 submit, text, password, textarea, title, ul, xml_declaration, radio, \
39 submit, text, password, textarea, title, ul, xml_declaration, radio, \
40 form as insecure_form
40 form as insecure_form
41 from webhelpers.html.tools import auto_link, button_to, highlight, \
41 from webhelpers.html.tools import auto_link, button_to, highlight, \
42 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
42 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
43 from webhelpers.number import format_byte_size, format_bit_size
43 from webhelpers.number import format_byte_size, format_bit_size
44 from webhelpers.pylonslib import Flash as _Flash
44 from webhelpers.pylonslib import Flash as _Flash
45 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
45 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
46 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
46 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
47 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
47 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
48 replace_whitespace, urlify, truncate, wrap_paragraphs
48 replace_whitespace, urlify, truncate, wrap_paragraphs
49 from webhelpers.date import time_ago_in_words
49 from webhelpers.date import time_ago_in_words
50 from webhelpers.paginate import Page as _Page
50 from webhelpers.paginate import Page as _Page
51 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
51 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
52 convert_boolean_attrs, NotGiven, _make_safe_id_component
52 convert_boolean_attrs, NotGiven, _make_safe_id_component
53
53
54 from kallithea.config.routing import url
54 from kallithea.config.routing import url
55 from kallithea.lib.annotate import annotate_highlight
55 from kallithea.lib.annotate import annotate_highlight
56 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
56 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
57 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
57 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
58 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict, \
58 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict, \
59 safe_int, MENTIONS_REGEX
59 safe_int, MENTIONS_REGEX
60 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
60 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
61 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
61 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
62 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
62 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
63 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
63 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
64 from kallithea.model.changeset_status import ChangesetStatusModel
64 from kallithea.model.changeset_status import ChangesetStatusModel
65 from kallithea.model.db import URL_SEP, Permission
65 from kallithea.model.db import URL_SEP, Permission
66
66
67 log = logging.getLogger(__name__)
67 log = logging.getLogger(__name__)
68
68
69
69
70 def canonical_url(*args, **kargs):
70 def canonical_url(*args, **kargs):
71 '''Like url(x, qualified=True), but returns url that not only is qualified
71 '''Like url(x, qualified=True), but returns url that not only is qualified
72 but also canonical, as configured in canonical_url'''
72 but also canonical, as configured in canonical_url'''
73 from kallithea import CONFIG
73 from kallithea import CONFIG
74 try:
74 try:
75 parts = CONFIG.get('canonical_url', '').split('://', 1)
75 parts = CONFIG.get('canonical_url', '').split('://', 1)
76 kargs['host'] = parts[1].split('/', 1)[0]
76 kargs['host'] = parts[1].split('/', 1)[0]
77 kargs['protocol'] = parts[0]
77 kargs['protocol'] = parts[0]
78 except IndexError:
78 except IndexError:
79 kargs['qualified'] = True
79 kargs['qualified'] = True
80 return url(*args, **kargs)
80 return url(*args, **kargs)
81
81
82 def canonical_hostname():
82 def canonical_hostname():
83 '''Return canonical hostname of system'''
83 '''Return canonical hostname of system'''
84 from kallithea import CONFIG
84 from kallithea import CONFIG
85 try:
85 try:
86 parts = CONFIG.get('canonical_url', '').split('://', 1)
86 parts = CONFIG.get('canonical_url', '').split('://', 1)
87 return parts[1].split('/', 1)[0]
87 return parts[1].split('/', 1)[0]
88 except IndexError:
88 except IndexError:
89 parts = url('home', qualified=True).split('://', 1)
89 parts = url('home', qualified=True).split('://', 1)
90 return parts[1].split('/', 1)[0]
90 return parts[1].split('/', 1)[0]
91
91
92 def html_escape(s):
92 def html_escape(s):
93 """Return string with all html escaped.
93 """Return string with all html escaped.
94 This is also safe for javascript in html but not necessarily correct.
94 This is also safe for javascript in html but not necessarily correct.
95 """
95 """
96 return (s
96 return (s
97 .replace('&', '&amp;')
97 .replace('&', '&amp;')
98 .replace(">", "&gt;")
98 .replace(">", "&gt;")
99 .replace("<", "&lt;")
99 .replace("<", "&lt;")
100 .replace('"', "&quot;")
100 .replace('"', "&quot;")
101 .replace("'", "&apos;")
101 .replace("'", "&apos;")
102 )
102 )
103
103
104 def shorter(s, size=20, firstline=False, postfix='...'):
104 def shorter(s, size=20, firstline=False, postfix='...'):
105 """Truncate s to size, including the postfix string if truncating.
105 """Truncate s to size, including the postfix string if truncating.
106 If firstline, truncate at newline.
106 If firstline, truncate at newline.
107 """
107 """
108 if firstline:
108 if firstline:
109 s = s.split('\n', 1)[0].rstrip()
109 s = s.split('\n', 1)[0].rstrip()
110 if len(s) > size:
110 if len(s) > size:
111 return s[:size - len(postfix)] + postfix
111 return s[:size - len(postfix)] + postfix
112 return s
112 return s
113
113
114
114
115 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
115 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
116 """
116 """
117 Reset button
117 Reset button
118 """
118 """
119 _set_input_attrs(attrs, type, name, value)
119 _set_input_attrs(attrs, type, name, value)
120 _set_id_attr(attrs, id, name)
120 _set_id_attr(attrs, id, name)
121 convert_boolean_attrs(attrs, ["disabled"])
121 convert_boolean_attrs(attrs, ["disabled"])
122 return HTML.input(**attrs)
122 return HTML.input(**attrs)
123
123
124 reset = _reset
124 reset = _reset
125 safeid = _make_safe_id_component
125 safeid = _make_safe_id_component
126
126
127
127
128 def FID(raw_id, path):
128 def FID(raw_id, path):
129 """
129 """
130 Creates a unique ID for filenode based on it's hash of path and revision
130 Creates a unique ID for filenode based on it's hash of path and revision
131 it's safe to use in urls
131 it's safe to use in urls
132
132
133 :param raw_id:
133 :param raw_id:
134 :param path:
134 :param path:
135 """
135 """
136
136
137 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
137 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
138
138
139
139
140 class _GetError(object):
140 class _GetError(object):
141 """Get error from form_errors, and represent it as span wrapped error
141 """Get error from form_errors, and represent it as span wrapped error
142 message
142 message
143
143
144 :param field_name: field to fetch errors for
144 :param field_name: field to fetch errors for
145 :param form_errors: form errors dict
145 :param form_errors: form errors dict
146 """
146 """
147
147
148 def __call__(self, field_name, form_errors):
148 def __call__(self, field_name, form_errors):
149 tmpl = """<span class="error_msg">%s</span>"""
149 tmpl = """<span class="error_msg">%s</span>"""
150 if form_errors and field_name in form_errors:
150 if form_errors and field_name in form_errors:
151 return literal(tmpl % form_errors.get(field_name))
151 return literal(tmpl % form_errors.get(field_name))
152
152
153 get_error = _GetError()
153 get_error = _GetError()
154
154
155
155
156 class _FilesBreadCrumbs(object):
156 class _FilesBreadCrumbs(object):
157
157
158 def __call__(self, repo_name, rev, paths):
158 def __call__(self, repo_name, rev, paths):
159 if isinstance(paths, str):
159 if isinstance(paths, str):
160 paths = safe_unicode(paths)
160 paths = safe_unicode(paths)
161 url_l = [link_to(repo_name, url('files_home',
161 url_l = [link_to(repo_name, url('files_home',
162 repo_name=repo_name,
162 repo_name=repo_name,
163 revision=rev, f_path=''),
163 revision=rev, f_path=''),
164 class_='ypjax-link')]
164 class_='ypjax-link')]
165 paths_l = paths.split('/')
165 paths_l = paths.split('/')
166 for cnt, p in enumerate(paths_l):
166 for cnt, p in enumerate(paths_l):
167 if p != '':
167 if p != '':
168 url_l.append(link_to(p,
168 url_l.append(link_to(p,
169 url('files_home',
169 url('files_home',
170 repo_name=repo_name,
170 repo_name=repo_name,
171 revision=rev,
171 revision=rev,
172 f_path='/'.join(paths_l[:cnt + 1])
172 f_path='/'.join(paths_l[:cnt + 1])
173 ),
173 ),
174 class_='ypjax-link'
174 class_='ypjax-link'
175 )
175 )
176 )
176 )
177
177
178 return literal('/'.join(url_l))
178 return literal('/'.join(url_l))
179
179
180 files_breadcrumbs = _FilesBreadCrumbs()
180 files_breadcrumbs = _FilesBreadCrumbs()
181
181
182
182
183 class CodeHtmlFormatter(HtmlFormatter):
183 class CodeHtmlFormatter(HtmlFormatter):
184 """
184 """
185 My code Html Formatter for source codes
185 My code Html Formatter for source codes
186 """
186 """
187
187
188 def wrap(self, source, outfile):
188 def wrap(self, source, outfile):
189 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
189 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
190
190
191 def _wrap_code(self, source):
191 def _wrap_code(self, source):
192 for cnt, it in enumerate(source):
192 for cnt, it in enumerate(source):
193 i, t = it
193 i, t = it
194 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
194 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
195 yield i, t
195 yield i, t
196
196
197 def _wrap_tablelinenos(self, inner):
197 def _wrap_tablelinenos(self, inner):
198 dummyoutfile = StringIO.StringIO()
198 dummyoutfile = StringIO.StringIO()
199 lncount = 0
199 lncount = 0
200 for t, line in inner:
200 for t, line in inner:
201 if t:
201 if t:
202 lncount += 1
202 lncount += 1
203 dummyoutfile.write(line)
203 dummyoutfile.write(line)
204
204
205 fl = self.linenostart
205 fl = self.linenostart
206 mw = len(str(lncount + fl - 1))
206 mw = len(str(lncount + fl - 1))
207 sp = self.linenospecial
207 sp = self.linenospecial
208 st = self.linenostep
208 st = self.linenostep
209 la = self.lineanchors
209 la = self.lineanchors
210 aln = self.anchorlinenos
210 aln = self.anchorlinenos
211 nocls = self.noclasses
211 nocls = self.noclasses
212 if sp:
212 if sp:
213 lines = []
213 lines = []
214
214
215 for i in range(fl, fl + lncount):
215 for i in range(fl, fl + lncount):
216 if i % st == 0:
216 if i % st == 0:
217 if i % sp == 0:
217 if i % sp == 0:
218 if aln:
218 if aln:
219 lines.append('<a href="#%s%d" class="special">%*d</a>' %
219 lines.append('<a href="#%s%d" class="special">%*d</a>' %
220 (la, i, mw, i))
220 (la, i, mw, i))
221 else:
221 else:
222 lines.append('<span class="special">%*d</span>' % (mw, i))
222 lines.append('<span class="special">%*d</span>' % (mw, i))
223 else:
223 else:
224 if aln:
224 if aln:
225 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
225 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
226 else:
226 else:
227 lines.append('%*d' % (mw, i))
227 lines.append('%*d' % (mw, i))
228 else:
228 else:
229 lines.append('')
229 lines.append('')
230 ls = '\n'.join(lines)
230 ls = '\n'.join(lines)
231 else:
231 else:
232 lines = []
232 lines = []
233 for i in range(fl, fl + lncount):
233 for i in range(fl, fl + lncount):
234 if i % st == 0:
234 if i % st == 0:
235 if aln:
235 if aln:
236 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
236 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
237 else:
237 else:
238 lines.append('%*d' % (mw, i))
238 lines.append('%*d' % (mw, i))
239 else:
239 else:
240 lines.append('')
240 lines.append('')
241 ls = '\n'.join(lines)
241 ls = '\n'.join(lines)
242
242
243 # in case you wonder about the seemingly redundant <div> here: since the
243 # in case you wonder about the seemingly redundant <div> here: since the
244 # content in the other cell also is wrapped in a div, some browsers in
244 # content in the other cell also is wrapped in a div, some browsers in
245 # some configurations seem to mess up the formatting...
245 # some configurations seem to mess up the formatting...
246 if nocls:
246 if nocls:
247 yield 0, ('<table class="%stable">' % self.cssclass +
247 yield 0, ('<table class="%stable">' % self.cssclass +
248 '<tr><td><div class="linenodiv" '
248 '<tr><td><div class="linenodiv" '
249 'style="background-color: #f0f0f0; padding-right: 10px">'
249 'style="background-color: #f0f0f0; padding-right: 10px">'
250 '<pre style="line-height: 125%">' +
250 '<pre style="line-height: 125%">' +
251 ls + '</pre></div></td><td id="hlcode" class="code">')
251 ls + '</pre></div></td><td id="hlcode" class="code">')
252 else:
252 else:
253 yield 0, ('<table class="%stable">' % self.cssclass +
253 yield 0, ('<table class="%stable">' % self.cssclass +
254 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
254 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
255 ls + '</pre></div></td><td id="hlcode" class="code">')
255 ls + '</pre></div></td><td id="hlcode" class="code">')
256 yield 0, dummyoutfile.getvalue()
256 yield 0, dummyoutfile.getvalue()
257 yield 0, '</td></tr></table>'
257 yield 0, '</td></tr></table>'
258
258
259
259
260 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
260 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
261
261
262 def _markup_whitespace(m):
262 def _markup_whitespace(m):
263 groups = m.groups()
263 groups = m.groups()
264 if groups[0]:
264 if groups[0]:
265 return '<u>\t</u>'
265 return '<u>\t</u>'
266 if groups[1]:
266 if groups[1]:
267 return ' <i></i>'
267 return ' <i></i>'
268
268
269 def markup_whitespace(s):
269 def markup_whitespace(s):
270 return _whitespace_re.sub(_markup_whitespace, s)
270 return _whitespace_re.sub(_markup_whitespace, s)
271
271
272 def pygmentize(filenode, **kwargs):
272 def pygmentize(filenode, **kwargs):
273 """
273 """
274 pygmentize function using pygments
274 pygmentize function using pygments
275
275
276 :param filenode:
276 :param filenode:
277 """
277 """
278 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
278 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
279 return literal(markup_whitespace(
279 return literal(markup_whitespace(
280 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
280 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
281
281
282
282
283 def pygmentize_annotation(repo_name, filenode, **kwargs):
283 def pygmentize_annotation(repo_name, filenode, **kwargs):
284 """
284 """
285 pygmentize function for annotation
285 pygmentize function for annotation
286
286
287 :param filenode:
287 :param filenode:
288 """
288 """
289
289
290 color_dict = {}
290 color_dict = {}
291
291
292 def gen_color(n=10000):
292 def gen_color(n=10000):
293 """generator for getting n of evenly distributed colors using
293 """generator for getting n of evenly distributed colors using
294 hsv color and golden ratio. It always return same order of colors
294 hsv color and golden ratio. It always return same order of colors
295
295
296 :returns: RGB tuple
296 :returns: RGB tuple
297 """
297 """
298
298
299 def hsv_to_rgb(h, s, v):
299 def hsv_to_rgb(h, s, v):
300 if s == 0.0:
300 if s == 0.0:
301 return v, v, v
301 return v, v, v
302 i = int(h * 6.0) # XXX assume int() truncates!
302 i = int(h * 6.0) # XXX assume int() truncates!
303 f = (h * 6.0) - i
303 f = (h * 6.0) - i
304 p = v * (1.0 - s)
304 p = v * (1.0 - s)
305 q = v * (1.0 - s * f)
305 q = v * (1.0 - s * f)
306 t = v * (1.0 - s * (1.0 - f))
306 t = v * (1.0 - s * (1.0 - f))
307 i = i % 6
307 i = i % 6
308 if i == 0:
308 if i == 0:
309 return v, t, p
309 return v, t, p
310 if i == 1:
310 if i == 1:
311 return q, v, p
311 return q, v, p
312 if i == 2:
312 if i == 2:
313 return p, v, t
313 return p, v, t
314 if i == 3:
314 if i == 3:
315 return p, q, v
315 return p, q, v
316 if i == 4:
316 if i == 4:
317 return t, p, v
317 return t, p, v
318 if i == 5:
318 if i == 5:
319 return v, p, q
319 return v, p, q
320
320
321 golden_ratio = 0.618033988749895
321 golden_ratio = 0.618033988749895
322 h = 0.22717784590367374
322 h = 0.22717784590367374
323
323
324 for _unused in xrange(n):
324 for _unused in xrange(n):
325 h += golden_ratio
325 h += golden_ratio
326 h %= 1
326 h %= 1
327 HSV_tuple = [h, 0.95, 0.95]
327 HSV_tuple = [h, 0.95, 0.95]
328 RGB_tuple = hsv_to_rgb(*HSV_tuple)
328 RGB_tuple = hsv_to_rgb(*HSV_tuple)
329 yield map(lambda x: str(int(x * 256)), RGB_tuple)
329 yield map(lambda x: str(int(x * 256)), RGB_tuple)
330
330
331 cgenerator = gen_color()
331 cgenerator = gen_color()
332
332
333 def get_color_string(cs):
333 def get_color_string(cs):
334 if cs in color_dict:
334 if cs in color_dict:
335 col = color_dict[cs]
335 col = color_dict[cs]
336 else:
336 else:
337 col = color_dict[cs] = cgenerator.next()
337 col = color_dict[cs] = cgenerator.next()
338 return "color: rgb(%s)! important;" % (', '.join(col))
338 return "color: rgb(%s)! important;" % (', '.join(col))
339
339
340 def url_func(repo_name):
340 def url_func(repo_name):
341
341
342 def _url_func(changeset):
342 def _url_func(changeset):
343 author = escape(changeset.author)
343 author = escape(changeset.author)
344 date = changeset.date
344 date = changeset.date
345 message = escape(changeset.message)
345 message = escape(changeset.message)
346 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
346 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
347 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
347 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
348 "</b> %s<br/></div>") % (author, date, message)
348 "</b> %s<br/></div>") % (author, date, message)
349
349
350 lnk_format = show_id(changeset)
350 lnk_format = show_id(changeset)
351 uri = link_to(
351 uri = link_to(
352 lnk_format,
352 lnk_format,
353 url('changeset_home', repo_name=repo_name,
353 url('changeset_home', repo_name=repo_name,
354 revision=changeset.raw_id),
354 revision=changeset.raw_id),
355 style=get_color_string(changeset.raw_id),
355 style=get_color_string(changeset.raw_id),
356 class_='tooltip safe-html-title',
356 class_='tooltip safe-html-title',
357 title=tooltip_html
357 title=tooltip_html
358 )
358 )
359
359
360 uri += '\n'
360 uri += '\n'
361 return uri
361 return uri
362 return _url_func
362 return _url_func
363
363
364 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
364 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
365
365
366
366
367 def is_following_repo(repo_name, user_id):
367 def is_following_repo(repo_name, user_id):
368 from kallithea.model.scm import ScmModel
368 from kallithea.model.scm import ScmModel
369 return ScmModel().is_following_repo(repo_name, user_id)
369 return ScmModel().is_following_repo(repo_name, user_id)
370
370
371 class _Message(object):
371 class _Message(object):
372 """A message returned by ``Flash.pop_messages()``.
372 """A message returned by ``Flash.pop_messages()``.
373
373
374 Converting the message to a string returns the message text. Instances
374 Converting the message to a string returns the message text. Instances
375 also have the following attributes:
375 also have the following attributes:
376
376
377 * ``message``: the message text.
377 * ``message``: the message text.
378 * ``category``: the category specified when the message was created.
378 * ``category``: the category specified when the message was created.
379 """
379 """
380
380
381 def __init__(self, category, message):
381 def __init__(self, category, message):
382 self.category = category
382 self.category = category
383 self.message = message
383 self.message = message
384
384
385 def __str__(self):
385 def __str__(self):
386 return self.message
386 return self.message
387
387
388 __unicode__ = __str__
388 __unicode__ = __str__
389
389
390 def __html__(self):
390 def __html__(self):
391 return escape(safe_unicode(self.message))
391 return escape(safe_unicode(self.message))
392
392
393 class Flash(_Flash):
393 class Flash(_Flash):
394
394
395 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
395 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
396 """
396 """
397 Show a message to the user _and_ log it through the specified function
397 Show a message to the user _and_ log it through the specified function
398
398
399 category: notice (default), warning, error, success
399 category: notice (default), warning, error, success
400 logf: a custom log function - such as log.debug
400 logf: a custom log function - such as log.debug
401
401
402 logf defaults to log.info, unless category equals 'success', in which
402 logf defaults to log.info, unless category equals 'success', in which
403 case logf defaults to log.debug.
403 case logf defaults to log.debug.
404 """
404 """
405 if logf is None:
405 if logf is None:
406 logf = log.info
406 logf = log.info
407 if category == 'success':
407 if category == 'success':
408 logf = log.debug
408 logf = log.debug
409
409
410 logf('Flash %s: %s', category, message)
410 logf('Flash %s: %s', category, message)
411
411
412 super(Flash, self).__call__(message, category, ignore_duplicate)
412 super(Flash, self).__call__(message, category, ignore_duplicate)
413
413
414 def pop_messages(self):
414 def pop_messages(self):
415 """Return all accumulated messages and delete them from the session.
415 """Return all accumulated messages and delete them from the session.
416
416
417 The return value is a list of ``Message`` objects.
417 The return value is a list of ``Message`` objects.
418 """
418 """
419 from pylons import session
419 from pylons import session
420 messages = session.pop(self.session_key, [])
420 messages = session.pop(self.session_key, [])
421 session.save()
421 session.save()
422 return [_Message(*m) for m in messages]
422 return [_Message(*m) for m in messages]
423
423
424 flash = Flash()
424 flash = Flash()
425
425
426 #==============================================================================
426 #==============================================================================
427 # SCM FILTERS available via h.
427 # SCM FILTERS available via h.
428 #==============================================================================
428 #==============================================================================
429 from kallithea.lib.vcs.utils import author_name, author_email
429 from kallithea.lib.vcs.utils import author_name, author_email
430 from kallithea.lib.utils2 import credentials_filter, age as _age
430 from kallithea.lib.utils2 import credentials_filter, age as _age
431 from kallithea.model.db import User, ChangesetStatus, PullRequest
431 from kallithea.model.db import User, ChangesetStatus, PullRequest
432
432
433 age = lambda x, y=False: _age(x, y)
433 age = lambda x, y=False: _age(x, y)
434 capitalize = lambda x: x.capitalize()
434 capitalize = lambda x: x.capitalize()
435 email = author_email
435 email = author_email
436 short_id = lambda x: x[:12]
436 short_id = lambda x: x[:12]
437 hide_credentials = lambda x: ''.join(credentials_filter(x))
437 hide_credentials = lambda x: ''.join(credentials_filter(x))
438
438
439
439
440 def show_id(cs):
440 def show_id(cs):
441 """
441 """
442 Configurable function that shows ID
442 Configurable function that shows ID
443 by default it's r123:fffeeefffeee
443 by default it's r123:fffeeefffeee
444
444
445 :param cs: changeset instance
445 :param cs: changeset instance
446 """
446 """
447 from kallithea import CONFIG
447 from kallithea import CONFIG
448 def_len = safe_int(CONFIG.get('show_sha_length', 12))
448 def_len = safe_int(CONFIG.get('show_sha_length', 12))
449 show_rev = str2bool(CONFIG.get('show_revision_number', False))
449 show_rev = str2bool(CONFIG.get('show_revision_number', False))
450
450
451 raw_id = cs.raw_id[:def_len]
451 raw_id = cs.raw_id[:def_len]
452 if show_rev:
452 if show_rev:
453 return 'r%s:%s' % (cs.revision, raw_id)
453 return 'r%s:%s' % (cs.revision, raw_id)
454 else:
454 else:
455 return raw_id
455 return raw_id
456
456
457
457
458 def fmt_date(date):
458 def fmt_date(date):
459 if date:
459 if date:
460 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
460 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
461
461
462 return ""
462 return ""
463
463
464
464
465 def is_git(repository):
465 def is_git(repository):
466 if hasattr(repository, 'alias'):
466 if hasattr(repository, 'alias'):
467 _type = repository.alias
467 _type = repository.alias
468 elif hasattr(repository, 'repo_type'):
468 elif hasattr(repository, 'repo_type'):
469 _type = repository.repo_type
469 _type = repository.repo_type
470 else:
470 else:
471 _type = repository
471 _type = repository
472 return _type == 'git'
472 return _type == 'git'
473
473
474
474
475 def is_hg(repository):
475 def is_hg(repository):
476 if hasattr(repository, 'alias'):
476 if hasattr(repository, 'alias'):
477 _type = repository.alias
477 _type = repository.alias
478 elif hasattr(repository, 'repo_type'):
478 elif hasattr(repository, 'repo_type'):
479 _type = repository.repo_type
479 _type = repository.repo_type
480 else:
480 else:
481 _type = repository
481 _type = repository
482 return _type == 'hg'
482 return _type == 'hg'
483
483
484
484
485 @cache_region('long_term', 'user_or_none')
485 @cache_region('long_term', 'user_or_none')
486 def user_or_none(author):
486 def user_or_none(author):
487 """Try to match email part of VCS committer string with a local user - or return None"""
487 """Try to match email part of VCS committer string with a local user - or return None"""
488 email = author_email(author)
488 email = author_email(author)
489 if email:
489 if email:
490 return User.get_by_email(email, cache=True) # cache will only use sql_cache_short
490 return User.get_by_email(email, cache=True) # cache will only use sql_cache_short
491 return None
491 return None
492
492
493 def email_or_none(author):
493 def email_or_none(author):
494 """Try to match email part of VCS committer string with a local user.
494 """Try to match email part of VCS committer string with a local user.
495 Return primary email of user, email part of the specified author name, or None."""
495 Return primary email of user, email part of the specified author name, or None."""
496 if not author:
496 if not author:
497 return None
497 return None
498 user = user_or_none(author)
498 user = user_or_none(author)
499 if user is not None:
499 if user is not None:
500 return user.email # always use main email address - not necessarily the one used to find user
500 return user.email # always use main email address - not necessarily the one used to find user
501
501
502 # extract email from the commit string
502 # extract email from the commit string
503 email = author_email(author)
503 email = author_email(author)
504 if email:
504 if email:
505 return email
505 return email
506
506
507 # No valid email, not a valid user in the system, none!
507 # No valid email, not a valid user in the system, none!
508 return None
508 return None
509
509
510 def person(author, show_attr="username"):
510 def person(author, show_attr="username"):
511 """Find the user identified by 'author', return one of the users attributes,
511 """Find the user identified by 'author', return one of the users attributes,
512 default to the username attribute, None if there is no user"""
512 default to the username attribute, None if there is no user"""
513 # attr to return from fetched user
513 # attr to return from fetched user
514 person_getter = lambda usr: getattr(usr, show_attr)
514 person_getter = lambda usr: getattr(usr, show_attr)
515
515
516 # if author is already an instance use it for extraction
516 # if author is already an instance use it for extraction
517 if isinstance(author, User):
517 if isinstance(author, User):
518 return person_getter(author)
518 return person_getter(author)
519
519
520 user = user_or_none(author)
520 user = user_or_none(author)
521 if user is not None:
521 if user is not None:
522 return person_getter(user)
522 return person_getter(user)
523
523
524 # Still nothing? Just pass back the author name if any, else the email
524 # Still nothing? Just pass back the author name if any, else the email
525 return author_name(author) or email(author)
525 return author_name(author) or email(author)
526
526
527
527
528 def person_by_id(id_, show_attr="username"):
528 def person_by_id(id_, show_attr="username"):
529 # attr to return from fetched user
529 # attr to return from fetched user
530 person_getter = lambda usr: getattr(usr, show_attr)
530 person_getter = lambda usr: getattr(usr, show_attr)
531
531
532 #maybe it's an ID ?
532 #maybe it's an ID ?
533 if str(id_).isdigit() or isinstance(id_, int):
533 if str(id_).isdigit() or isinstance(id_, int):
534 id_ = int(id_)
534 id_ = int(id_)
535 user = User.get(id_)
535 user = User.get(id_)
536 if user is not None:
536 if user is not None:
537 return person_getter(user)
537 return person_getter(user)
538 return id_
538 return id_
539
539
540
540
541 def boolicon(value):
541 def boolicon(value):
542 """Returns boolean value of a value, represented as small html image of true/false
542 """Returns boolean value of a value, represented as small html image of true/false
543 icons
543 icons
544
544
545 :param value: value
545 :param value: value
546 """
546 """
547
547
548 if value:
548 if value:
549 return HTML.tag('i', class_="icon-ok")
549 return HTML.tag('i', class_="icon-ok")
550 else:
550 else:
551 return HTML.tag('i', class_="icon-minus-circled")
551 return HTML.tag('i', class_="icon-minus-circled")
552
552
553
553
554 def action_parser(user_log, feed=False, parse_cs=False):
554 def action_parser(user_log, feed=False, parse_cs=False):
555 """
555 """
556 This helper will action_map the specified string action into translated
556 This helper will action_map the specified string action into translated
557 fancy names with icons and links
557 fancy names with icons and links
558
558
559 :param user_log: user log instance
559 :param user_log: user log instance
560 :param feed: use output for feeds (no html and fancy icons)
560 :param feed: use output for feeds (no html and fancy icons)
561 :param parse_cs: parse Changesets into VCS instances
561 :param parse_cs: parse Changesets into VCS instances
562 """
562 """
563
563
564 action = user_log.action
564 action = user_log.action
565 action_params = ' '
565 action_params = ' '
566
566
567 x = action.split(':')
567 x = action.split(':')
568
568
569 if len(x) > 1:
569 if len(x) > 1:
570 action, action_params = x
570 action, action_params = x
571
571
572 def get_cs_links():
572 def get_cs_links():
573 revs_limit = 3 # display this amount always
573 revs_limit = 3 # display this amount always
574 revs_top_limit = 50 # show upto this amount of changesets hidden
574 revs_top_limit = 50 # show upto this amount of changesets hidden
575 revs_ids = action_params.split(',')
575 revs_ids = action_params.split(',')
576 deleted = user_log.repository is None
576 deleted = user_log.repository is None
577 if deleted:
577 if deleted:
578 return ','.join(revs_ids)
578 return ','.join(revs_ids)
579
579
580 repo_name = user_log.repository.repo_name
580 repo_name = user_log.repository.repo_name
581
581
582 def lnk(rev, repo_name):
582 def lnk(rev, repo_name):
583 lazy_cs = False
583 lazy_cs = False
584 title_ = None
584 title_ = None
585 url_ = '#'
585 url_ = '#'
586 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
586 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
587 if rev.op and rev.ref_name:
587 if rev.op and rev.ref_name:
588 if rev.op == 'delete_branch':
588 if rev.op == 'delete_branch':
589 lbl = _('Deleted branch: %s') % rev.ref_name
589 lbl = _('Deleted branch: %s') % rev.ref_name
590 elif rev.op == 'tag':
590 elif rev.op == 'tag':
591 lbl = _('Created tag: %s') % rev.ref_name
591 lbl = _('Created tag: %s') % rev.ref_name
592 else:
592 else:
593 lbl = 'Unknown operation %s' % rev.op
593 lbl = 'Unknown operation %s' % rev.op
594 else:
594 else:
595 lazy_cs = True
595 lazy_cs = True
596 lbl = rev.short_id[:8]
596 lbl = rev.short_id[:8]
597 url_ = url('changeset_home', repo_name=repo_name,
597 url_ = url('changeset_home', repo_name=repo_name,
598 revision=rev.raw_id)
598 revision=rev.raw_id)
599 else:
599 else:
600 # changeset cannot be found - it might have been stripped or removed
600 # changeset cannot be found - it might have been stripped or removed
601 lbl = rev[:12]
601 lbl = rev[:12]
602 title_ = _('Changeset not found')
602 title_ = _('Changeset not found')
603 if parse_cs:
603 if parse_cs:
604 return link_to(lbl, url_, title=title_, class_='tooltip')
604 return link_to(lbl, url_, title=title_, class_='tooltip')
605 return link_to(lbl, url_, raw_id=rev.raw_id, repo_name=repo_name,
605 return link_to(lbl, url_, raw_id=rev.raw_id, repo_name=repo_name,
606 class_='lazy-cs' if lazy_cs else '')
606 class_='lazy-cs' if lazy_cs else '')
607
607
608 def _get_op(rev_txt):
608 def _get_op(rev_txt):
609 _op = None
609 _op = None
610 _name = rev_txt
610 _name = rev_txt
611 if len(rev_txt.split('=>')) == 2:
611 if len(rev_txt.split('=>')) == 2:
612 _op, _name = rev_txt.split('=>')
612 _op, _name = rev_txt.split('=>')
613 return _op, _name
613 return _op, _name
614
614
615 revs = []
615 revs = []
616 if len(filter(lambda v: v != '', revs_ids)) > 0:
616 if len(filter(lambda v: v != '', revs_ids)) > 0:
617 repo = None
617 repo = None
618 for rev in revs_ids[:revs_top_limit]:
618 for rev in revs_ids[:revs_top_limit]:
619 _op, _name = _get_op(rev)
619 _op, _name = _get_op(rev)
620
620
621 # we want parsed changesets, or new log store format is bad
621 # we want parsed changesets, or new log store format is bad
622 if parse_cs:
622 if parse_cs:
623 try:
623 try:
624 if repo is None:
624 if repo is None:
625 repo = user_log.repository.scm_instance
625 repo = user_log.repository.scm_instance
626 _rev = repo.get_changeset(rev)
626 _rev = repo.get_changeset(rev)
627 revs.append(_rev)
627 revs.append(_rev)
628 except ChangesetDoesNotExistError:
628 except ChangesetDoesNotExistError:
629 log.error('cannot find revision %s in this repo', rev)
629 log.error('cannot find revision %s in this repo', rev)
630 revs.append(rev)
630 revs.append(rev)
631 else:
631 else:
632 _rev = AttributeDict({
632 _rev = AttributeDict({
633 'short_id': rev[:12],
633 'short_id': rev[:12],
634 'raw_id': rev,
634 'raw_id': rev,
635 'message': '',
635 'message': '',
636 'op': _op,
636 'op': _op,
637 'ref_name': _name
637 'ref_name': _name
638 })
638 })
639 revs.append(_rev)
639 revs.append(_rev)
640 cs_links = [" " + ', '.join(
640 cs_links = [" " + ', '.join(
641 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
641 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
642 )]
642 )]
643 _op1, _name1 = _get_op(revs_ids[0])
643 _op1, _name1 = _get_op(revs_ids[0])
644 _op2, _name2 = _get_op(revs_ids[-1])
644 _op2, _name2 = _get_op(revs_ids[-1])
645
645
646 _rev = '%s...%s' % (_name1, _name2)
646 _rev = '%s...%s' % (_name1, _name2)
647
647
648 compare_view = (
648 compare_view = (
649 ' <div class="compare_view tooltip" title="%s">'
649 ' <div class="compare_view tooltip" title="%s">'
650 '<a href="%s">%s</a> </div>' % (
650 '<a href="%s">%s</a> </div>' % (
651 _('Show all combined changesets %s->%s') % (
651 _('Show all combined changesets %s->%s') % (
652 revs_ids[0][:12], revs_ids[-1][:12]
652 revs_ids[0][:12], revs_ids[-1][:12]
653 ),
653 ),
654 url('changeset_home', repo_name=repo_name,
654 url('changeset_home', repo_name=repo_name,
655 revision=_rev
655 revision=_rev
656 ),
656 ),
657 _('Compare view')
657 _('Compare view')
658 )
658 )
659 )
659 )
660
660
661 # if we have exactly one more than normally displayed
661 # if we have exactly one more than normally displayed
662 # just display it, takes less space than displaying
662 # just display it, takes less space than displaying
663 # "and 1 more revisions"
663 # "and 1 more revisions"
664 if len(revs_ids) == revs_limit + 1:
664 if len(revs_ids) == revs_limit + 1:
665 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
665 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
666
666
667 # hidden-by-default ones
667 # hidden-by-default ones
668 if len(revs_ids) > revs_limit + 1:
668 if len(revs_ids) > revs_limit + 1:
669 uniq_id = revs_ids[0]
669 uniq_id = revs_ids[0]
670 html_tmpl = (
670 html_tmpl = (
671 '<span> %s <a class="show_more" id="_%s" '
671 '<span> %s <a class="show_more" id="_%s" '
672 'href="#more">%s</a> %s</span>'
672 'href="#more">%s</a> %s</span>'
673 )
673 )
674 if not feed:
674 if not feed:
675 cs_links.append(html_tmpl % (
675 cs_links.append(html_tmpl % (
676 _('and'),
676 _('and'),
677 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
677 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
678 _('revisions')
678 _('revisions')
679 )
679 )
680 )
680 )
681
681
682 if not feed:
682 if not feed:
683 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
683 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
684 else:
684 else:
685 html_tmpl = '<span id="%s"> %s </span>'
685 html_tmpl = '<span id="%s"> %s </span>'
686
686
687 morelinks = ', '.join(
687 morelinks = ', '.join(
688 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
688 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
689 )
689 )
690
690
691 if len(revs_ids) > revs_top_limit:
691 if len(revs_ids) > revs_top_limit:
692 morelinks += ', ...'
692 morelinks += ', ...'
693
693
694 cs_links.append(html_tmpl % (uniq_id, morelinks))
694 cs_links.append(html_tmpl % (uniq_id, morelinks))
695 if len(revs) > 1:
695 if len(revs) > 1:
696 cs_links.append(compare_view)
696 cs_links.append(compare_view)
697 return ''.join(cs_links)
697 return ''.join(cs_links)
698
698
699 def get_fork_name():
699 def get_fork_name():
700 repo_name = action_params
700 repo_name = action_params
701 url_ = url('summary_home', repo_name=repo_name)
701 url_ = url('summary_home', repo_name=repo_name)
702 return _('Fork name %s') % link_to(action_params, url_)
702 return _('Fork name %s') % link_to(action_params, url_)
703
703
704 def get_user_name():
704 def get_user_name():
705 user_name = action_params
705 user_name = action_params
706 return user_name
706 return user_name
707
707
708 def get_users_group():
708 def get_users_group():
709 group_name = action_params
709 group_name = action_params
710 return group_name
710 return group_name
711
711
712 def get_pull_request():
712 def get_pull_request():
713 pull_request_id = action_params
713 pull_request_id = action_params
714 nice_id = PullRequest.make_nice_id(pull_request_id)
714 nice_id = PullRequest.make_nice_id(pull_request_id)
715
715
716 deleted = user_log.repository is None
716 deleted = user_log.repository is None
717 if deleted:
717 if deleted:
718 repo_name = user_log.repository_name
718 repo_name = user_log.repository_name
719 else:
719 else:
720 repo_name = user_log.repository.repo_name
720 repo_name = user_log.repository.repo_name
721
721
722 return link_to(_('Pull request %s') % nice_id,
722 return link_to(_('Pull request %s') % nice_id,
723 url('pullrequest_show', repo_name=repo_name,
723 url('pullrequest_show', repo_name=repo_name,
724 pull_request_id=pull_request_id))
724 pull_request_id=pull_request_id))
725
725
726 def get_archive_name():
726 def get_archive_name():
727 archive_name = action_params
727 archive_name = action_params
728 return archive_name
728 return archive_name
729
729
730 # action : translated str, callback(extractor), icon
730 # action : translated str, callback(extractor), icon
731 action_map = {
731 action_map = {
732 'user_deleted_repo': (_('[deleted] repository'),
732 'user_deleted_repo': (_('[deleted] repository'),
733 None, 'icon-trashcan'),
733 None, 'icon-trashcan'),
734 'user_created_repo': (_('[created] repository'),
734 'user_created_repo': (_('[created] repository'),
735 None, 'icon-plus'),
735 None, 'icon-plus'),
736 'user_created_fork': (_('[created] repository as fork'),
736 'user_created_fork': (_('[created] repository as fork'),
737 None, 'icon-fork'),
737 None, 'icon-fork'),
738 'user_forked_repo': (_('[forked] repository'),
738 'user_forked_repo': (_('[forked] repository'),
739 get_fork_name, 'icon-fork'),
739 get_fork_name, 'icon-fork'),
740 'user_updated_repo': (_('[updated] repository'),
740 'user_updated_repo': (_('[updated] repository'),
741 None, 'icon-pencil'),
741 None, 'icon-pencil'),
742 'user_downloaded_archive': (_('[downloaded] archive from repository'),
742 'user_downloaded_archive': (_('[downloaded] archive from repository'),
743 get_archive_name, 'icon-download-cloud'),
743 get_archive_name, 'icon-download-cloud'),
744 'admin_deleted_repo': (_('[delete] repository'),
744 'admin_deleted_repo': (_('[delete] repository'),
745 None, 'icon-trashcan'),
745 None, 'icon-trashcan'),
746 'admin_created_repo': (_('[created] repository'),
746 'admin_created_repo': (_('[created] repository'),
747 None, 'icon-plus'),
747 None, 'icon-plus'),
748 'admin_forked_repo': (_('[forked] repository'),
748 'admin_forked_repo': (_('[forked] repository'),
749 None, 'icon-fork'),
749 None, 'icon-fork'),
750 'admin_updated_repo': (_('[updated] repository'),
750 'admin_updated_repo': (_('[updated] repository'),
751 None, 'icon-pencil'),
751 None, 'icon-pencil'),
752 'admin_created_user': (_('[created] user'),
752 'admin_created_user': (_('[created] user'),
753 get_user_name, 'icon-user'),
753 get_user_name, 'icon-user'),
754 'admin_updated_user': (_('[updated] user'),
754 'admin_updated_user': (_('[updated] user'),
755 get_user_name, 'icon-user'),
755 get_user_name, 'icon-user'),
756 'admin_created_users_group': (_('[created] user group'),
756 'admin_created_users_group': (_('[created] user group'),
757 get_users_group, 'icon-pencil'),
757 get_users_group, 'icon-pencil'),
758 'admin_updated_users_group': (_('[updated] user group'),
758 'admin_updated_users_group': (_('[updated] user group'),
759 get_users_group, 'icon-pencil'),
759 get_users_group, 'icon-pencil'),
760 'user_commented_revision': (_('[commented] on revision in repository'),
760 'user_commented_revision': (_('[commented] on revision in repository'),
761 get_cs_links, 'icon-comment'),
761 get_cs_links, 'icon-comment'),
762 'user_commented_pull_request': (_('[commented] on pull request for'),
762 'user_commented_pull_request': (_('[commented] on pull request for'),
763 get_pull_request, 'icon-comment'),
763 get_pull_request, 'icon-comment'),
764 'user_closed_pull_request': (_('[closed] pull request for'),
764 'user_closed_pull_request': (_('[closed] pull request for'),
765 get_pull_request, 'icon-ok'),
765 get_pull_request, 'icon-ok'),
766 'push': (_('[pushed] into'),
766 'push': (_('[pushed] into'),
767 get_cs_links, 'icon-move-up'),
767 get_cs_links, 'icon-move-up'),
768 'push_local': (_('[committed via Kallithea] into repository'),
768 'push_local': (_('[committed via Kallithea] into repository'),
769 get_cs_links, 'icon-pencil'),
769 get_cs_links, 'icon-pencil'),
770 'push_remote': (_('[pulled from remote] into repository'),
770 'push_remote': (_('[pulled from remote] into repository'),
771 get_cs_links, 'icon-move-up'),
771 get_cs_links, 'icon-move-up'),
772 'pull': (_('[pulled] from'),
772 'pull': (_('[pulled] from'),
773 None, 'icon-move-down'),
773 None, 'icon-move-down'),
774 'started_following_repo': (_('[started following] repository'),
774 'started_following_repo': (_('[started following] repository'),
775 None, 'icon-heart'),
775 None, 'icon-heart'),
776 'stopped_following_repo': (_('[stopped following] repository'),
776 'stopped_following_repo': (_('[stopped following] repository'),
777 None, 'icon-heart-empty'),
777 None, 'icon-heart-empty'),
778 }
778 }
779
779
780 action_str = action_map.get(action, action)
780 action_str = action_map.get(action, action)
781 if feed:
781 if feed:
782 action = action_str[0].replace('[', '').replace(']', '')
782 action = action_str[0].replace('[', '').replace(']', '')
783 else:
783 else:
784 action = action_str[0] \
784 action = action_str[0] \
785 .replace('[', '<span class="journal_highlight">') \
785 .replace('[', '<span class="journal_highlight">') \
786 .replace(']', '</span>')
786 .replace(']', '</span>')
787
787
788 action_params_func = lambda: ""
788 action_params_func = lambda: ""
789
789
790 if callable(action_str[1]):
790 if callable(action_str[1]):
791 action_params_func = action_str[1]
791 action_params_func = action_str[1]
792
792
793 def action_parser_icon():
793 def action_parser_icon():
794 action = user_log.action
794 action = user_log.action
795 action_params = None
795 action_params = None
796 x = action.split(':')
796 x = action.split(':')
797
797
798 if len(x) > 1:
798 if len(x) > 1:
799 action, action_params = x
799 action, action_params = x
800
800
801 tmpl = """<i class="%s" alt="%s"></i>"""
801 tmpl = """<i class="%s" alt="%s"></i>"""
802 ico = action_map.get(action, ['', '', ''])[2]
802 ico = action_map.get(action, ['', '', ''])[2]
803 return literal(tmpl % (ico, action))
803 return literal(tmpl % (ico, action))
804
804
805 # returned callbacks we need to call to get
805 # returned callbacks we need to call to get
806 return [lambda: literal(action), action_params_func, action_parser_icon]
806 return [lambda: literal(action), action_params_func, action_parser_icon]
807
807
808
808
809
809
810 #==============================================================================
810 #==============================================================================
811 # PERMS
811 # PERMS
812 #==============================================================================
812 #==============================================================================
813 from kallithea.lib.auth import HasPermissionAny, \
813 from kallithea.lib.auth import HasPermissionAny, \
814 HasRepoPermissionAny, HasRepoGroupPermissionAny
814 HasRepoPermissionAny, HasRepoGroupPermissionAny
815
815
816
816
817 #==============================================================================
817 #==============================================================================
818 # GRAVATAR URL
818 # GRAVATAR URL
819 #==============================================================================
819 #==============================================================================
820 def gravatar_div(email_address, cls='', size=30, **div_attributes):
820 def gravatar_div(email_address, cls='', size=30, **div_attributes):
821 """Return an html literal with a div around a gravatar if they are enabled.
821 """Return an html literal with a div around a gravatar if they are enabled.
822 Extra keyword parameters starting with 'div_' will get the prefix removed
822 Extra keyword parameters starting with 'div_' will get the prefix removed
823 and be used as attributes on the div. The default class is 'gravatar'.
823 and be used as attributes on the div. The default class is 'gravatar'.
824 """
824 """
825 from pylons import tmpl_context as c
825 from pylons import tmpl_context as c
826 if not c.visual.use_gravatar:
826 if not c.visual.use_gravatar:
827 return ''
827 return ''
828 if 'div_class' not in div_attributes:
828 if 'div_class' not in div_attributes:
829 div_attributes['div_class'] = "gravatar"
829 div_attributes['div_class'] = "gravatar"
830 attributes = []
830 attributes = []
831 for k, v in sorted(div_attributes.items()):
831 for k, v in sorted(div_attributes.items()):
832 assert k.startswith('div_'), k
832 assert k.startswith('div_'), k
833 attributes.append(' %s="%s"' % (k[4:], escape(v)))
833 attributes.append(' %s="%s"' % (k[4:], escape(v)))
834 return literal("""<div%s>%s</div>""" %
834 return literal("""<div%s>%s</div>""" %
835 (''.join(attributes),
835 (''.join(attributes),
836 gravatar(email_address, cls=cls, size=size)))
836 gravatar(email_address, cls=cls, size=size)))
837
837
838 def gravatar(email_address, cls='', size=30):
838 def gravatar(email_address, cls='', size=30):
839 """return html element of the gravatar
839 """return html element of the gravatar
840
840
841 This method will return an <img> with the resolution double the size (for
841 This method will return an <img> with the resolution double the size (for
842 retina screens) of the image. If the url returned from gravatar_url is
842 retina screens) of the image. If the url returned from gravatar_url is
843 empty then we fallback to using an icon.
843 empty then we fallback to using an icon.
844
844
845 """
845 """
846 from pylons import tmpl_context as c
846 from pylons import tmpl_context as c
847 if not c.visual.use_gravatar:
847 if not c.visual.use_gravatar:
848 return ''
848 return ''
849
849
850 src = gravatar_url(email_address, size * 2)
850 src = gravatar_url(email_address, size * 2)
851
851
852 if src:
852 if src:
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 html = ('<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
855 html = ('<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
856 .format(cls=cls, size=size, src=src))
856 .format(cls=cls, size=size, src=src))
857
857
858 else:
858 else:
859 # if src is empty then there was no gravatar, so we use a font icon
859 # if src is empty then there was no gravatar, so we use a font icon
860 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
860 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
861 .format(cls=cls, size=size, src=src))
861 .format(cls=cls, size=size, src=src))
862
862
863 return literal(html)
863 return literal(html)
864
864
865 def gravatar_url(email_address, size=30, default=''):
865 def gravatar_url(email_address, size=30, default=''):
866 # doh, we need to re-import those to mock it later
866 # doh, we need to re-import those to mock it later
867 from kallithea.config.routing import url
867 from kallithea.config.routing import url
868 from pylons import tmpl_context as c
868 from pylons import tmpl_context as c
869 if not c.visual.use_gravatar:
869 if not c.visual.use_gravatar:
870 return ""
870 return ""
871
871
872 _def = 'anonymous@kallithea-scm.org' # default gravatar
872 _def = 'anonymous@kallithea-scm.org' # default gravatar
873 email_address = email_address or _def
873 email_address = email_address or _def
874
874
875 if email_address == _def:
875 if email_address == _def:
876 return default
876 return default
877
877
878 parsed_url = urlparse.urlparse(url.current(qualified=True))
878 parsed_url = urlparse.urlparse(url.current(qualified=True))
879 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
879 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
880 .replace('{email}', email_address) \
880 .replace('{email}', email_address) \
881 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
881 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
882 .replace('{netloc}', parsed_url.netloc) \
882 .replace('{netloc}', parsed_url.netloc) \
883 .replace('{scheme}', parsed_url.scheme) \
883 .replace('{scheme}', parsed_url.scheme) \
884 .replace('{size}', safe_str(size))
884 .replace('{size}', safe_str(size))
885 return url
885 return url
886
886
887 class Page(_Page):
887 class Page(_Page):
888 """
888 """
889 Custom pager to match rendering style with YUI paginator
889 Custom pager to match rendering style with YUI paginator
890 """
890 """
891
891
892 def __init__(self, *args, **kwargs):
892 def __init__(self, *args, **kwargs):
893 kwargs.setdefault('url', url.current)
893 kwargs.setdefault('url', url.current)
894 _Page.__init__(self, *args, **kwargs)
894 _Page.__init__(self, *args, **kwargs)
895
895
896 def _get_pos(self, cur_page, max_page, items):
896 def _get_pos(self, cur_page, max_page, items):
897 edge = (items / 2) + 1
897 edge = (items / 2) + 1
898 if (cur_page <= edge):
898 if (cur_page <= edge):
899 radius = max(items / 2, items - cur_page)
899 radius = max(items / 2, items - cur_page)
900 elif (max_page - cur_page) < edge:
900 elif (max_page - cur_page) < edge:
901 radius = (items - 1) - (max_page - cur_page)
901 radius = (items - 1) - (max_page - cur_page)
902 else:
902 else:
903 radius = items / 2
903 radius = items / 2
904
904
905 left = max(1, (cur_page - (radius)))
905 left = max(1, (cur_page - (radius)))
906 right = min(max_page, cur_page + (radius))
906 right = min(max_page, cur_page + (radius))
907 return left, cur_page, right
907 return left, cur_page, right
908
908
909 def _range(self, regexp_match):
909 def _range(self, regexp_match):
910 """
910 """
911 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
911 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
912
912
913 Arguments:
913 Arguments:
914
914
915 regexp_match
915 regexp_match
916 A "re" (regular expressions) match object containing the
916 A "re" (regular expressions) match object containing the
917 radius of linked pages around the current page in
917 radius of linked pages around the current page in
918 regexp_match.group(1) as a string
918 regexp_match.group(1) as a string
919
919
920 This function is supposed to be called as a callable in
920 This function is supposed to be called as a callable in
921 re.sub.
921 re.sub.
922
922
923 """
923 """
924 radius = int(regexp_match.group(1))
924 radius = int(regexp_match.group(1))
925
925
926 # Compute the first and last page number within the radius
926 # Compute the first and last page number within the radius
927 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
927 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
928 # -> leftmost_page = 5
928 # -> leftmost_page = 5
929 # -> rightmost_page = 9
929 # -> rightmost_page = 9
930 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
930 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
931 self.last_page,
931 self.last_page,
932 (radius * 2) + 1)
932 (radius * 2) + 1)
933 nav_items = []
933 nav_items = []
934
934
935 # Create a link to the first page (unless we are on the first page
935 # Create a link to the first page (unless we are on the first page
936 # or there would be no need to insert '..' spacers)
936 # or there would be no need to insert '..' spacers)
937 if self.page != self.first_page and self.first_page < leftmost_page:
937 if self.page != self.first_page and self.first_page < leftmost_page:
938 nav_items.append(self._pagerlink(self.first_page, self.first_page))
938 nav_items.append(self._pagerlink(self.first_page, self.first_page))
939
939
940 # Insert dots if there are pages between the first page
940 # Insert dots if there are pages between the first page
941 # and the currently displayed page range
941 # and the currently displayed page range
942 if leftmost_page - self.first_page > 1:
942 if leftmost_page - self.first_page > 1:
943 # Wrap in a SPAN tag if nolink_attr is set
943 # Wrap in a SPAN tag if nolink_attr is set
944 text_ = '..'
944 text_ = '..'
945 if self.dotdot_attr:
945 if self.dotdot_attr:
946 text_ = HTML.span(c=text_, **self.dotdot_attr)
946 text_ = HTML.span(c=text_, **self.dotdot_attr)
947 nav_items.append(text_)
947 nav_items.append(text_)
948
948
949 for thispage in xrange(leftmost_page, rightmost_page + 1):
949 for thispage in xrange(leftmost_page, rightmost_page + 1):
950 # Highlight the current page number and do not use a link
950 # Highlight the current page number and do not use a link
951 text_ = str(thispage)
951 text_ = str(thispage)
952 if thispage == self.page:
952 if thispage == self.page:
953 # Wrap in a SPAN tag if nolink_attr is set
953 # Wrap in a SPAN tag if nolink_attr is set
954 if self.curpage_attr:
954 if self.curpage_attr:
955 text_ = HTML.span(c=text_, **self.curpage_attr)
955 text_ = HTML.span(c=text_, **self.curpage_attr)
956 nav_items.append(text_)
956 nav_items.append(text_)
957 # Otherwise create just a link to that page
957 # Otherwise create just a link to that page
958 else:
958 else:
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=None,
984 link_attr=None,
985 curpage_attr=None,
985 curpage_attr=None,
986 dotdot_attr=None, **kwargs):
986 dotdot_attr=None, **kwargs):
987 self.curpage_attr = curpage_attr or {'class': 'pager_curpage'}
987 self.curpage_attr = curpage_attr or {'class': 'pager_curpage'}
988 self.separator = separator
988 self.separator = separator
989 self.pager_kwargs = kwargs
989 self.pager_kwargs = kwargs
990 self.page_param = page_param
990 self.page_param = page_param
991 self.partial_param = partial_param
991 self.partial_param = partial_param
992 self.onclick = onclick
992 self.onclick = onclick
993 self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
993 self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
994 self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
994 self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
995
995
996 # Don't show navigator if there is no more than one page
996 # Don't show navigator if there is no more than one page
997 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
997 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
998 return ''
998 return ''
999
999
1000 from string import Template
1000 from string import Template
1001 # Replace ~...~ in token format by range of pages
1001 # Replace ~...~ in token format by range of pages
1002 result = re.sub(r'~(\d+)~', self._range, format)
1002 result = re.sub(r'~(\d+)~', self._range, format)
1003
1003
1004 # Interpolate '%' variables
1004 # Interpolate '%' variables
1005 result = Template(result).safe_substitute({
1005 result = Template(result).safe_substitute({
1006 'first_page': self.first_page,
1006 'first_page': self.first_page,
1007 'last_page': self.last_page,
1007 'last_page': self.last_page,
1008 'page': self.page,
1008 'page': self.page,
1009 'page_count': self.page_count,
1009 'page_count': self.page_count,
1010 'items_per_page': self.items_per_page,
1010 'items_per_page': self.items_per_page,
1011 'first_item': self.first_item,
1011 'first_item': self.first_item,
1012 'last_item': self.last_item,
1012 'last_item': self.last_item,
1013 'item_count': self.item_count,
1013 'item_count': self.item_count,
1014 'link_first': self.page > self.first_page and \
1014 'link_first': self.page > self.first_page and \
1015 self._pagerlink(self.first_page, symbol_first) or '',
1015 self._pagerlink(self.first_page, symbol_first) or '',
1016 'link_last': self.page < self.last_page and \
1016 'link_last': self.page < self.last_page and \
1017 self._pagerlink(self.last_page, symbol_last) or '',
1017 self._pagerlink(self.last_page, symbol_last) or '',
1018 'link_previous': self.previous_page and \
1018 'link_previous': self.previous_page and \
1019 self._pagerlink(self.previous_page, symbol_previous) \
1019 self._pagerlink(self.previous_page, symbol_previous) \
1020 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1020 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1021 'link_next': self.next_page and \
1021 'link_next': self.next_page and \
1022 self._pagerlink(self.next_page, symbol_next) \
1022 self._pagerlink(self.next_page, symbol_next) \
1023 or HTML.span(symbol_next, class_="yui-pg-next")
1023 or HTML.span(symbol_next, class_="yui-pg-next")
1024 })
1024 })
1025
1025
1026 return literal(result)
1026 return literal(result)
1027
1027
1028
1028
1029 #==============================================================================
1029 #==============================================================================
1030 # REPO PAGER, PAGER FOR REPOSITORY
1030 # REPO PAGER, PAGER FOR REPOSITORY
1031 #==============================================================================
1031 #==============================================================================
1032 class RepoPage(Page):
1032 class RepoPage(Page):
1033
1033
1034 def __init__(self, collection, page=1, items_per_page=20,
1034 def __init__(self, collection, page=1, items_per_page=20,
1035 item_count=None, **kwargs):
1035 item_count=None, **kwargs):
1036
1036
1037 """Create a "RepoPage" instance. special pager for paging
1037 """Create a "RepoPage" instance. special pager for paging
1038 repository
1038 repository
1039 """
1039 """
1040 # TODO: call baseclass __init__
1040 # TODO: call baseclass __init__
1041 self._url_generator = kwargs.pop('url', url.current)
1041 self._url_generator = kwargs.pop('url', url.current)
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 _URLIFY_RE = re.compile(r'''
1246 _URLIFY_RE = re.compile(r'''
1247 # URL markup
1247 # URL markup
1248 (?P<url>%s) |
1248 (?P<url>%s) |
1249 # @mention markup
1249 # @mention markup
1250 (?P<mention>%s) |
1250 (?P<mention>%s) |
1251 # Changeset hash markup
1251 # Changeset hash markup
1252 (?<!\w|[-_])
1252 (?<!\w|[-_])
1253 (?P<hash>[0-9a-f]{12,40})
1253 (?P<hash>[0-9a-f]{12,40})
1254 (?!\w|[-_]) |
1254 (?!\w|[-_]) |
1255 # Markup of *bold text*
1255 # Markup of *bold text*
1256 (?:
1256 (?:
1257 (?:^|(?<=\s))
1257 (?:^|(?<=\s))
1258 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
1258 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
1259 (?![*\w])
1259 (?![*\w])
1260 ) |
1260 ) |
1261 # "Stylize" markup
1261 # "Stylize" markup
1262 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1262 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1263 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1263 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1264 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
1264 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
1265 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
1265 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
1266 \[(?P<tag>[a-z]+)\]
1266 \[(?P<tag>[a-z]+)\]
1267 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
1267 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
1268 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
1268 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
1269
1269
1270
1270
1271
1271
1272 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
1272 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
1273 """
1273 """
1274 Parses given text message and make literal html with markup.
1274 Parses given text message and make literal html with markup.
1275 The text will be truncated to the specified length.
1275 The text will be truncated to the specified length.
1276 Hashes are turned into changeset links to specified repository.
1276 Hashes are turned into changeset links to specified repository.
1277 URLs links to what they say.
1277 URLs links to what they say.
1278 Issues are linked to given issue-server.
1278 Issues are linked to given issue-server.
1279 If link_ is provided, all text not already linking somewhere will link there.
1279 If link_ is provided, all text not already linking somewhere will link there.
1280 """
1280 """
1281
1281
1282 def _replace(match_obj):
1282 def _replace(match_obj):
1283 url = match_obj.group('url')
1283 url = match_obj.group('url')
1284 if url is not None:
1284 if url is not None:
1285 return '<a href="%(url)s">%(url)s</a>' % {'url': url}
1285 return '<a href="%(url)s">%(url)s</a>' % {'url': url}
1286 mention = match_obj.group('mention')
1286 mention = match_obj.group('mention')
1287 if mention is not None:
1287 if mention is not None:
1288 return '<b>%s</b>' % mention
1288 return '<b>%s</b>' % mention
1289 hash_ = match_obj.group('hash')
1289 hash_ = match_obj.group('hash')
1290 if hash_ is not None and repo_name is not None:
1290 if hash_ is not None and repo_name is not None:
1291 from kallithea.config.routing import url # doh, we need to re-import url to mock it later
1291 from kallithea.config.routing import url # doh, we need to re-import url to mock it later
1292 return '<a class="revision-link" href="%(url)s">%(hash)s</a>' % {
1292 return '<a class="revision-link" href="%(url)s">%(hash)s</a>' % {
1293 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
1293 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
1294 'hash': hash_,
1294 'hash': hash_,
1295 }
1295 }
1296 bold = match_obj.group('bold')
1296 bold = match_obj.group('bold')
1297 if bold is not None:
1297 if bold is not None:
1298 return '<b>*%s*</b>' % _urlify(bold[1:-1])
1298 return '<b>*%s*</b>' % _urlify(bold[1:-1])
1299 if stylize:
1299 if stylize:
1300 seen = match_obj.group('seen')
1300 seen = match_obj.group('seen')
1301 if seen:
1301 if seen:
1302 return '<div class="metatag" tag="see">see =&gt; %s</div>' % seen
1302 return '<div class="metatag" tag="see">see =&gt; %s</div>' % seen
1303 license = match_obj.group('license')
1303 license = match_obj.group('license')
1304 if license:
1304 if license:
1305 return '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
1305 return '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
1306 tagtype = match_obj.group('tagtype')
1306 tagtype = match_obj.group('tagtype')
1307 if tagtype:
1307 if tagtype:
1308 tagvalue = match_obj.group('tagvalue')
1308 tagvalue = match_obj.group('tagvalue')
1309 return '<div class="metatag" tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
1309 return '<div class="metatag" tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
1310 lang = match_obj.group('lang')
1310 lang = match_obj.group('lang')
1311 if lang:
1311 if lang:
1312 return '<div class="metatag" tag="lang">%s</div>' % lang
1312 return '<div class="metatag" tag="lang">%s</div>' % lang
1313 tag = match_obj.group('tag')
1313 tag = match_obj.group('tag')
1314 if tag:
1314 if tag:
1315 return '<div class="metatag" tag="%s">%s</div>' % (tag, tag)
1315 return '<div class="metatag" tag="%s">%s</div>' % (tag, tag)
1316 return match_obj.group(0)
1316 return match_obj.group(0)
1317
1317
1318 def _urlify(s):
1318 def _urlify(s):
1319 """
1319 """
1320 Extract urls from text and make html links out of them
1320 Extract urls from text and make html links out of them
1321 """
1321 """
1322 return _URLIFY_RE.sub(_replace, s)
1322 return _URLIFY_RE.sub(_replace, s)
1323
1323
1324 if truncate is None:
1324 if truncate is None:
1325 s = s.rstrip()
1325 s = s.rstrip()
1326 else:
1326 else:
1327 s = truncatef(s, truncate, whole_word=True)
1327 s = truncatef(s, truncate, whole_word=True)
1328 s = html_escape(s)
1328 s = html_escape(s)
1329 s = _urlify(s)
1329 s = _urlify(s)
1330 if repo_name is not None:
1330 if repo_name is not None:
1331 s = urlify_issues(s, repo_name)
1331 s = urlify_issues(s, repo_name)
1332 if link_ is not None:
1332 if link_ is not None:
1333 # make href around everything that isn't a href already
1333 # make href around everything that isn't a href already
1334 s = linkify_others(s, link_)
1334 s = linkify_others(s, link_)
1335 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1335 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1336 return literal(s)
1336 return literal(s)
1337
1337
1338
1338
1339 def linkify_others(t, l):
1339 def linkify_others(t, l):
1340 """Add a default link to html with links.
1340 """Add a default link to html with links.
1341 HTML doesn't allow nesting of links, so the outer link must be broken up
1341 HTML doesn't allow nesting of links, so the outer link must be broken up
1342 in pieces and give space for other links.
1342 in pieces and give space for other links.
1343 """
1343 """
1344 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1344 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1345 links = []
1345 links = []
1346 for e in urls.split(t):
1346 for e in urls.split(t):
1347 if e.strip() and not urls.match(e):
1347 if e.strip() and not urls.match(e):
1348 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1348 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1349 else:
1349 else:
1350 links.append(e)
1350 links.append(e)
1351
1351
1352 return ''.join(links)
1352 return ''.join(links)
1353
1353
1354
1354
1355 def _urlify_issues_replace_f(repo_name, ISSUE_SERVER_LNK, ISSUE_PREFIX):
1355 # Global variable that will hold the actual urlify_issues function body.
1356 def urlify_issues_replace(match_obj):
1356 # Will be set on first use when the global configuration has been read.
1357 pref = ''
1357 _urlify_issues_f = None
1358 if match_obj.group().startswith(' '):
1359 pref = ' '
1360
1361 issue_id = ''.join(match_obj.groups())
1362 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1363 issue_url = issue_url.replace('{repo}', repo_name)
1364 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1365
1366 return (
1367 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1368 '%(issue-prefix)s%(id-repr)s'
1369 '</a>'
1370 ) % {
1371 'pref': pref,
1372 'cls': 'issue-tracker-link',
1373 'url': issue_url,
1374 'id-repr': issue_id,
1375 'issue-prefix': ISSUE_PREFIX,
1376 'serv': ISSUE_SERVER_LNK,
1377 }
1378 return urlify_issues_replace
1379
1358
1380
1359
1381 def urlify_issues(newtext, repo_name):
1360 def urlify_issues(newtext, repo_name):
1382 from kallithea import CONFIG as conf
1361 """Urlify issue references according to .ini configuration"""
1362 global _urlify_issues_f
1363 if _urlify_issues_f is None:
1364 from kallithea import CONFIG
1365 assert CONFIG['sqlalchemy.url'] # make sure config has been loaded
1366
1367 # Build chain of urlify functions, starting with not doing any transformation
1368 tmp_urlify_issues_f = lambda s: s
1383
1369
1384 # allow multiple issue servers to be used
1370 issue_pat_re = re.compile(r'issue_pat(.*)')
1385 valid_indices = [
1371 for k in CONFIG.keys():
1386 x.group(1)
1372 # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
1387 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1373 m = issue_pat_re.match(k)
1388 if x and 'issue_server_link%s' % x.group(1) in conf
1374 if m is None:
1389 and 'issue_prefix%s' % x.group(1) in conf
1375 continue
1390 ]
1376 suffix = m.group(1)
1391
1377 issue_pat = CONFIG.get(k)
1392 if valid_indices:
1378 issue_server_link = CONFIG.get('issue_server_link%s' % suffix)
1393 log.debug('found issue server suffixes `%s` during valuation of: %s',
1379 issue_prefix = CONFIG.get('issue_prefix%s' % suffix)
1394 ','.join(valid_indices), newtext)
1380 if issue_pat and issue_server_link and issue_prefix:
1381 log.debug('issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_prefix)
1382 else:
1383 log.error('skipping incomplete issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_prefix)
1384 continue
1395
1385
1396 for pattern_index in valid_indices:
1386 # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
1397 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1387 issue_re = re.compile(issue_pat)
1398 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1388 def issues_replace(match_obj,
1399 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1389 issue_server_link=issue_server_link, issue_prefix=issue_prefix):
1390 leadingspace = ' ' if match_obj.group().startswith(' ') else ''
1391 issue_id = ''.join(match_obj.groups())
1392 issue_url = issue_server_link.replace('{id}', issue_id)
1393 issue_url = issue_url.replace('{repo}', repo_name)
1394 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1395 return (
1396 '%(leadingspace)s<a class="issue-tracker-link" href="%(url)s">'
1397 '%(issue-prefix)s%(id-repr)s'
1398 '</a>'
1399 ) % {
1400 'leadingspace': leadingspace,
1401 'url': issue_url,
1402 'id-repr': issue_id,
1403 'issue-prefix': issue_prefix,
1404 'serv': issue_server_link,
1405 }
1406 tmp_urlify_issues_f = (lambda s,
1407 issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f:
1408 issue_re.sub(issues_replace, chain_f(s)))
1400
1409
1401 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s',
1410 # Set tmp function globally - atomically
1402 pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1411 _urlify_issues_f = tmp_urlify_issues_f
1403 ISSUE_PREFIX)
1404
1405 URL_PAT = re.compile(ISSUE_PATTERN)
1406
1412
1407 urlify_issues_replace = _urlify_issues_replace_f(repo_name, ISSUE_SERVER_LNK, ISSUE_PREFIX)
1413 return _urlify_issues_f(newtext)
1408 newtext = URL_PAT.sub(urlify_issues_replace, newtext)
1409 log.debug('processed prefix:`%s` => %s', pattern_index, newtext)
1410
1411 return newtext
1412
1414
1413
1415
1414 def render_w_mentions(source, repo_name=None):
1416 def render_w_mentions(source, repo_name=None):
1415 """
1417 """
1416 Render plain text with revision hashes and issue references urlified
1418 Render plain text with revision hashes and issue references urlified
1417 and with @mention highlighting.
1419 and with @mention highlighting.
1418 """
1420 """
1419 s = safe_unicode(source)
1421 s = safe_unicode(source)
1420 s = urlify_text(s, repo_name=repo_name)
1422 s = urlify_text(s, repo_name=repo_name)
1421 return literal('<div class="formatted-fixed">%s</div>' % s)
1423 return literal('<div class="formatted-fixed">%s</div>' % s)
1422
1424
1423
1425
1424 def short_ref(ref_type, ref_name):
1426 def short_ref(ref_type, ref_name):
1425 if ref_type == 'rev':
1427 if ref_type == 'rev':
1426 return short_id(ref_name)
1428 return short_id(ref_name)
1427 return ref_name
1429 return ref_name
1428
1430
1429 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1431 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1430 """
1432 """
1431 Return full markup for a href to changeset_home for a changeset.
1433 Return full markup for a href to changeset_home for a changeset.
1432 If ref_type is branch it will link to changelog.
1434 If ref_type is branch it will link to changelog.
1433 ref_name is shortened if ref_type is 'rev'.
1435 ref_name is shortened if ref_type is 'rev'.
1434 if rev is specified show it too, explicitly linking to that revision.
1436 if rev is specified show it too, explicitly linking to that revision.
1435 """
1437 """
1436 txt = short_ref(ref_type, ref_name)
1438 txt = short_ref(ref_type, ref_name)
1437 if ref_type == 'branch':
1439 if ref_type == 'branch':
1438 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1440 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1439 else:
1441 else:
1440 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1442 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1441 l = link_to(repo_name + '#' + txt, u)
1443 l = link_to(repo_name + '#' + txt, u)
1442 if rev and ref_type != 'rev':
1444 if rev and ref_type != 'rev':
1443 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1445 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1444 return l
1446 return l
1445
1447
1446 def changeset_status(repo, revision):
1448 def changeset_status(repo, revision):
1447 return ChangesetStatusModel().get_status(repo, revision)
1449 return ChangesetStatusModel().get_status(repo, revision)
1448
1450
1449
1451
1450 def changeset_status_lbl(changeset_status):
1452 def changeset_status_lbl(changeset_status):
1451 return ChangesetStatus.get_status_lbl(changeset_status)
1453 return ChangesetStatus.get_status_lbl(changeset_status)
1452
1454
1453
1455
1454 def get_permission_name(key):
1456 def get_permission_name(key):
1455 return dict(Permission.PERMS).get(key)
1457 return dict(Permission.PERMS).get(key)
1456
1458
1457
1459
1458 def journal_filter_help():
1460 def journal_filter_help():
1459 return _(textwrap.dedent('''
1461 return _(textwrap.dedent('''
1460 Example filter terms:
1462 Example filter terms:
1461 repository:vcs
1463 repository:vcs
1462 username:developer
1464 username:developer
1463 action:*push*
1465 action:*push*
1464 ip:127.0.0.1
1466 ip:127.0.0.1
1465 date:20120101
1467 date:20120101
1466 date:[20120101100000 TO 20120102]
1468 date:[20120101100000 TO 20120102]
1467
1469
1468 Generate wildcards using '*' character:
1470 Generate wildcards using '*' character:
1469 "repository:vcs*" - search everything starting with 'vcs'
1471 "repository:vcs*" - search everything starting with 'vcs'
1470 "repository:*vcs*" - search for repository containing 'vcs'
1472 "repository:*vcs*" - search for repository containing 'vcs'
1471
1473
1472 Optional AND / OR operators in queries
1474 Optional AND / OR operators in queries
1473 "repository:vcs OR repository:test"
1475 "repository:vcs OR repository:test"
1474 "username:test AND repository:test*"
1476 "username:test AND repository:test*"
1475 '''))
1477 '''))
1476
1478
1477
1479
1478 def not_mapped_error(repo_name):
1480 def not_mapped_error(repo_name):
1479 flash(_('%s repository is not mapped to db perhaps'
1481 flash(_('%s repository is not mapped to db perhaps'
1480 ' it was created or renamed from the filesystem'
1482 ' it was created or renamed from the filesystem'
1481 ' please run the application again'
1483 ' please run the application again'
1482 ' in order to rescan repositories') % repo_name, category='error')
1484 ' in order to rescan repositories') % repo_name, category='error')
1483
1485
1484
1486
1485 def ip_range(ip_addr):
1487 def ip_range(ip_addr):
1486 from kallithea.model.db import UserIpMap
1488 from kallithea.model.db import UserIpMap
1487 s, e = UserIpMap._get_ip_range(ip_addr)
1489 s, e = UserIpMap._get_ip_range(ip_addr)
1488 return '%s - %s' % (s, e)
1490 return '%s - %s' % (s, e)
1489
1491
1490
1492
1491 def form(url, method="post", **attrs):
1493 def form(url, method="post", **attrs):
1492 """Like webhelpers.html.tags.form but automatically using secure_form with
1494 """Like webhelpers.html.tags.form but automatically using secure_form with
1493 authentication_token for POST. authentication_token is thus never leaked
1495 authentication_token for POST. authentication_token is thus never leaked
1494 in the URL."""
1496 in the URL."""
1495 if method.lower() == 'get':
1497 if method.lower() == 'get':
1496 return insecure_form(url, method=method, **attrs)
1498 return insecure_form(url, method=method, **attrs)
1497 # webhelpers will turn everything but GET into POST
1499 # webhelpers will turn everything but GET into POST
1498 return secure_form(url, method=method, **attrs)
1500 return secure_form(url, method=method, **attrs)
@@ -1,763 +1,767 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 import os
2 import os
3 import posixpath
3 import posixpath
4 import mimetypes
4 import mimetypes
5 from kallithea.tests.base import *
5 from kallithea.tests.base import *
6 from kallithea.model.db import Repository
6 from kallithea.model.db import Repository
7 from kallithea.model.meta import Session
7 from kallithea.model.meta import Session
8 from kallithea.tests.fixture import Fixture
8 from kallithea.tests.fixture import Fixture
9
9
10 fixture = Fixture()
10 fixture = Fixture()
11
11
12 ARCHIVE_SPECS = {
12 ARCHIVE_SPECS = {
13 '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
13 '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
14 '.tar.gz': ('application/x-gzip', 'tgz', ''),
14 '.tar.gz': ('application/x-gzip', 'tgz', ''),
15 '.zip': ('application/zip', 'zip', ''),
15 '.zip': ('application/zip', 'zip', ''),
16 }
16 }
17
17
18 HG_NODE_HISTORY = fixture.load_resource('hg_node_history_response.json')
18 HG_NODE_HISTORY = fixture.load_resource('hg_node_history_response.json')
19 GIT_NODE_HISTORY = fixture.load_resource('git_node_history_response.json')
19 GIT_NODE_HISTORY = fixture.load_resource('git_node_history_response.json')
20
20
21
21
22 def _set_downloads(repo_name, set_to):
22 def _set_downloads(repo_name, set_to):
23 repo = Repository.get_by_repo_name(repo_name)
23 repo = Repository.get_by_repo_name(repo_name)
24 repo.enable_downloads = set_to
24 repo.enable_downloads = set_to
25 Session().commit()
25 Session().commit()
26
26
27
27
28 class TestFilesController(TestController):
28 class TestFilesController(TestController):
29
29
30 def test_index(self):
30 def test_index(self):
31 self.log_user()
31 self.log_user()
32 response = self.app.get(url(controller='files', action='index',
32 response = self.app.get(url(controller='files', action='index',
33 repo_name=HG_REPO,
33 repo_name=HG_REPO,
34 revision='tip',
34 revision='tip',
35 f_path='/'))
35 f_path='/'))
36 # Test response...
36 # Test response...
37 response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/docs"><i class="icon-folder-open"></i><span>docs</span></a>' % HG_REPO)
37 response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/docs"><i class="icon-folder-open"></i><span>docs</span></a>' % HG_REPO)
38 response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/vcs"><i class="icon-folder-open"></i><span>vcs</span></a>' % HG_REPO)
38 response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/vcs"><i class="icon-folder-open"></i><span>vcs</span></a>' % HG_REPO)
39 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.gitignore"><i class="icon-doc"></i><span>.gitignore</span></a>' % HG_REPO)
39 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.gitignore"><i class="icon-doc"></i><span>.gitignore</span></a>' % HG_REPO)
40 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.hgignore"><i class="icon-doc"></i><span>.hgignore</span></a>' % HG_REPO)
40 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.hgignore"><i class="icon-doc"></i><span>.hgignore</span></a>' % HG_REPO)
41 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.hgtags"><i class="icon-doc"></i><span>.hgtags</span></a>' % HG_REPO)
41 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.hgtags"><i class="icon-doc"></i><span>.hgtags</span></a>' % HG_REPO)
42 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.travis.yml"><i class="icon-doc"></i><span>.travis.yml</span></a>' % HG_REPO)
42 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.travis.yml"><i class="icon-doc"></i><span>.travis.yml</span></a>' % HG_REPO)
43 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/MANIFEST.in"><i class="icon-doc"></i><span>MANIFEST.in</span></a>' % HG_REPO)
43 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/MANIFEST.in"><i class="icon-doc"></i><span>MANIFEST.in</span></a>' % HG_REPO)
44 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/README.rst"><i class="icon-doc"></i><span>README.rst</span></a>' % HG_REPO)
44 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/README.rst"><i class="icon-doc"></i><span>README.rst</span></a>' % HG_REPO)
45 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/run_test_and_report.sh"><i class="icon-doc"></i><span>run_test_and_report.sh</span></a>' % HG_REPO)
45 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/run_test_and_report.sh"><i class="icon-doc"></i><span>run_test_and_report.sh</span></a>' % HG_REPO)
46 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/setup.cfg"><i class="icon-doc"></i><span>setup.cfg</span></a>' % HG_REPO)
46 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/setup.cfg"><i class="icon-doc"></i><span>setup.cfg</span></a>' % HG_REPO)
47 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/setup.py"><i class="icon-doc"></i><span>setup.py</span></a>' % HG_REPO)
47 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/setup.py"><i class="icon-doc"></i><span>setup.py</span></a>' % HG_REPO)
48 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/test_and_report.sh"><i class="icon-doc"></i><span>test_and_report.sh</span></a>' % HG_REPO)
48 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/test_and_report.sh"><i class="icon-doc"></i><span>test_and_report.sh</span></a>' % HG_REPO)
49 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/tox.ini"><i class="icon-doc"></i><span>tox.ini</span></a>' % HG_REPO)
49 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/tox.ini"><i class="icon-doc"></i><span>tox.ini</span></a>' % HG_REPO)
50
50
51 def test_index_revision(self):
51 def test_index_revision(self):
52 self.log_user()
52 self.log_user()
53
53
54 response = self.app.get(
54 response = self.app.get(
55 url(controller='files', action='index',
55 url(controller='files', action='index',
56 repo_name=HG_REPO,
56 repo_name=HG_REPO,
57 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
57 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
58 f_path='/')
58 f_path='/')
59 )
59 )
60
60
61 #Test response...
61 #Test response...
62
62
63 response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs"><i class="icon-folder-open"></i><span>docs</span></a>' % HG_REPO)
63 response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs"><i class="icon-folder-open"></i><span>docs</span></a>' % HG_REPO)
64 response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests"><i class="icon-folder-open"></i><span>tests</span></a>' % HG_REPO)
64 response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests"><i class="icon-folder-open"></i><span>tests</span></a>' % HG_REPO)
65 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst"><i class="icon-doc"></i><span>README.rst</span></a>' % HG_REPO)
65 response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst"><i class="icon-doc"></i><span>README.rst</span></a>' % HG_REPO)
66 response.mustcontain('1.1 KiB')
66 response.mustcontain('1.1 KiB')
67
67
68 def test_index_different_branch(self):
68 def test_index_different_branch(self):
69 self.log_user()
69 self.log_user()
70
70
71 response = self.app.get(url(controller='files', action='index',
71 response = self.app.get(url(controller='files', action='index',
72 repo_name=HG_REPO,
72 repo_name=HG_REPO,
73 revision='97e8b885c04894463c51898e14387d80c30ed1ee',
73 revision='97e8b885c04894463c51898e14387d80c30ed1ee',
74 f_path='/'))
74 f_path='/'))
75
75
76 response.mustcontain("""<option selected="selected" value="97e8b885c04894463c51898e14387d80c30ed1ee">git at 97e8b885c048</option>""")
76 response.mustcontain("""<option selected="selected" value="97e8b885c04894463c51898e14387d80c30ed1ee">git at 97e8b885c048</option>""")
77
77
78 def test_index_paging(self):
78 def test_index_paging(self):
79 self.log_user()
79 self.log_user()
80
80
81 for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
81 for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
82 (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
82 (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
83 (109, '75feb4c33e81186c87eac740cee2447330288412'),
83 (109, '75feb4c33e81186c87eac740cee2447330288412'),
84 (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
84 (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
85 (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
85 (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
86
86
87 response = self.app.get(url(controller='files', action='index',
87 response = self.app.get(url(controller='files', action='index',
88 repo_name=HG_REPO,
88 repo_name=HG_REPO,
89 revision=r[1],
89 revision=r[1],
90 f_path='/'))
90 f_path='/'))
91
91
92 response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
92 response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
93
93
94 def test_file_source(self):
94 def test_file_source(self):
95 # Force the global cache to be populated now when we know the right .ini has been loaded.
96 # (Without this, the test would fail.)
97 import kallithea.lib.helpers
98 kallithea.lib.helpers._urlify_issues_f = None
95 self.log_user()
99 self.log_user()
96 response = self.app.get(url(controller='files', action='index',
100 response = self.app.get(url(controller='files', action='index',
97 repo_name=HG_REPO,
101 repo_name=HG_REPO,
98 revision='8911406ad776fdd3d0b9932a2e89677e57405a48',
102 revision='8911406ad776fdd3d0b9932a2e89677e57405a48',
99 f_path='vcs/nodes.py'))
103 f_path='vcs/nodes.py'))
100
104
101 response.mustcontain("""<div class="commit">Partially implemented <a class="issue-tracker-link" href="https://issues.example.com/vcs_test_hg/issue/16">#16</a>. filecontent/commit message/author/node name are safe_unicode now.<br/>"""
105 response.mustcontain("""<div class="commit">Partially implemented <a class="issue-tracker-link" href="https://issues.example.com/vcs_test_hg/issue/16">#16</a>. filecontent/commit message/author/node name are safe_unicode now.<br/>"""
102 """In addition some other __str__ are unicode as well<br/>"""
106 """In addition some other __str__ are unicode as well<br/>"""
103 """Added test for unicode<br/>"""
107 """Added test for unicode<br/>"""
104 """Improved test to clone into uniq repository.<br/>"""
108 """Improved test to clone into uniq repository.<br/>"""
105 """removed extra unicode conversion in diff.</div>
109 """removed extra unicode conversion in diff.</div>
106 """)
110 """)
107
111
108 response.mustcontain("""<option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">default at 8911406ad776</option>""")
112 response.mustcontain("""<option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">default at 8911406ad776</option>""")
109
113
110 def test_file_source_history(self):
114 def test_file_source_history(self):
111 self.log_user()
115 self.log_user()
112 response = self.app.get(url(controller='files', action='history',
116 response = self.app.get(url(controller='files', action='history',
113 repo_name=HG_REPO,
117 repo_name=HG_REPO,
114 revision='tip',
118 revision='tip',
115 f_path='vcs/nodes.py'),
119 f_path='vcs/nodes.py'),
116 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
120 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
117 assert response.body == HG_NODE_HISTORY
121 assert response.body == HG_NODE_HISTORY
118
122
119 def test_file_source_history_git(self):
123 def test_file_source_history_git(self):
120 self.log_user()
124 self.log_user()
121 response = self.app.get(url(controller='files', action='history',
125 response = self.app.get(url(controller='files', action='history',
122 repo_name=GIT_REPO,
126 repo_name=GIT_REPO,
123 revision='master',
127 revision='master',
124 f_path='vcs/nodes.py'),
128 f_path='vcs/nodes.py'),
125 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
129 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
126 assert response.body == GIT_NODE_HISTORY
130 assert response.body == GIT_NODE_HISTORY
127
131
128 def test_file_annotation(self):
132 def test_file_annotation(self):
129 self.log_user()
133 self.log_user()
130 response = self.app.get(url(controller='files', action='index',
134 response = self.app.get(url(controller='files', action='index',
131 repo_name=HG_REPO,
135 repo_name=HG_REPO,
132 revision='tip',
136 revision='tip',
133 f_path='vcs/nodes.py',
137 f_path='vcs/nodes.py',
134 annotate=True))
138 annotate=True))
135
139
136 response.mustcontain("""r356:25213a5fbb04""")
140 response.mustcontain("""r356:25213a5fbb04""")
137
141
138 def test_file_annotation_git(self):
142 def test_file_annotation_git(self):
139 self.log_user()
143 self.log_user()
140 response = self.app.get(url(controller='files', action='index',
144 response = self.app.get(url(controller='files', action='index',
141 repo_name=GIT_REPO,
145 repo_name=GIT_REPO,
142 revision='master',
146 revision='master',
143 f_path='vcs/nodes.py',
147 f_path='vcs/nodes.py',
144 annotate=True))
148 annotate=True))
145 response.mustcontain("""r345:c994f0de03b2""")
149 response.mustcontain("""r345:c994f0de03b2""")
146
150
147 def test_file_annotation_history(self):
151 def test_file_annotation_history(self):
148 self.log_user()
152 self.log_user()
149 response = self.app.get(url(controller='files', action='history',
153 response = self.app.get(url(controller='files', action='history',
150 repo_name=HG_REPO,
154 repo_name=HG_REPO,
151 revision='tip',
155 revision='tip',
152 f_path='vcs/nodes.py',
156 f_path='vcs/nodes.py',
153 annotate=True),
157 annotate=True),
154 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
158 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
155
159
156 assert response.body == HG_NODE_HISTORY
160 assert response.body == HG_NODE_HISTORY
157
161
158 def test_file_annotation_history_git(self):
162 def test_file_annotation_history_git(self):
159 self.log_user()
163 self.log_user()
160 response = self.app.get(url(controller='files', action='history',
164 response = self.app.get(url(controller='files', action='history',
161 repo_name=GIT_REPO,
165 repo_name=GIT_REPO,
162 revision='master',
166 revision='master',
163 f_path='vcs/nodes.py',
167 f_path='vcs/nodes.py',
164 annotate=True),
168 annotate=True),
165 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
169 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
166
170
167 assert response.body == GIT_NODE_HISTORY
171 assert response.body == GIT_NODE_HISTORY
168
172
169 def test_file_authors(self):
173 def test_file_authors(self):
170 self.log_user()
174 self.log_user()
171 response = self.app.get(url(controller='files', action='authors',
175 response = self.app.get(url(controller='files', action='authors',
172 repo_name=HG_REPO,
176 repo_name=HG_REPO,
173 revision='tip',
177 revision='tip',
174 f_path='vcs/nodes.py',
178 f_path='vcs/nodes.py',
175 annotate=True))
179 annotate=True))
176 response.mustcontain('Marcin Kuzminski')
180 response.mustcontain('Marcin Kuzminski')
177 response.mustcontain('Lukasz Balcerzak')
181 response.mustcontain('Lukasz Balcerzak')
178
182
179 def test_file_authors_git(self):
183 def test_file_authors_git(self):
180 self.log_user()
184 self.log_user()
181 response = self.app.get(url(controller='files', action='authors',
185 response = self.app.get(url(controller='files', action='authors',
182 repo_name=GIT_REPO,
186 repo_name=GIT_REPO,
183 revision='master',
187 revision='master',
184 f_path='vcs/nodes.py',
188 f_path='vcs/nodes.py',
185 annotate=True))
189 annotate=True))
186 response.mustcontain('Marcin Kuzminski')
190 response.mustcontain('Marcin Kuzminski')
187 response.mustcontain('Lukasz Balcerzak')
191 response.mustcontain('Lukasz Balcerzak')
188
192
189 def test_archival(self):
193 def test_archival(self):
190 self.log_user()
194 self.log_user()
191 _set_downloads(HG_REPO, set_to=True)
195 _set_downloads(HG_REPO, set_to=True)
192 for arch_ext, info in ARCHIVE_SPECS.items():
196 for arch_ext, info in ARCHIVE_SPECS.items():
193 short = '27cd5cce30c9%s' % arch_ext
197 short = '27cd5cce30c9%s' % arch_ext
194 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
198 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
195 filename = '%s-%s' % (HG_REPO, short)
199 filename = '%s-%s' % (HG_REPO, short)
196 response = self.app.get(url(controller='files',
200 response = self.app.get(url(controller='files',
197 action='archivefile',
201 action='archivefile',
198 repo_name=HG_REPO,
202 repo_name=HG_REPO,
199 fname=fname))
203 fname=fname))
200
204
201 assert response.status == '200 OK'
205 assert response.status == '200 OK'
202 heads = [
206 heads = [
203 ('Pragma', 'no-cache'),
207 ('Pragma', 'no-cache'),
204 ('Cache-Control', 'no-cache'),
208 ('Cache-Control', 'no-cache'),
205 ('Content-Disposition', 'attachment; filename=%s' % filename),
209 ('Content-Disposition', 'attachment; filename=%s' % filename),
206 ('Content-Type', '%s; charset=utf-8' % info[0]),
210 ('Content-Type', '%s; charset=utf-8' % info[0]),
207 ]
211 ]
208 assert response.response._headers.items() == heads
212 assert response.response._headers.items() == heads
209
213
210 def test_archival_wrong_ext(self):
214 def test_archival_wrong_ext(self):
211 self.log_user()
215 self.log_user()
212 _set_downloads(HG_REPO, set_to=True)
216 _set_downloads(HG_REPO, set_to=True)
213 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
217 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
214 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
218 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
215
219
216 response = self.app.get(url(controller='files',
220 response = self.app.get(url(controller='files',
217 action='archivefile',
221 action='archivefile',
218 repo_name=HG_REPO,
222 repo_name=HG_REPO,
219 fname=fname))
223 fname=fname))
220 response.mustcontain('Unknown archive type')
224 response.mustcontain('Unknown archive type')
221
225
222 def test_archival_wrong_revision(self):
226 def test_archival_wrong_revision(self):
223 self.log_user()
227 self.log_user()
224 _set_downloads(HG_REPO, set_to=True)
228 _set_downloads(HG_REPO, set_to=True)
225 for rev in ['00x000000', 'tar', 'wrong', '@##$@$42413232', '232dffcd']:
229 for rev in ['00x000000', 'tar', 'wrong', '@##$@$42413232', '232dffcd']:
226 fname = '%s.zip' % rev
230 fname = '%s.zip' % rev
227
231
228 response = self.app.get(url(controller='files',
232 response = self.app.get(url(controller='files',
229 action='archivefile',
233 action='archivefile',
230 repo_name=HG_REPO,
234 repo_name=HG_REPO,
231 fname=fname))
235 fname=fname))
232 response.mustcontain('Unknown revision')
236 response.mustcontain('Unknown revision')
233
237
234 #==========================================================================
238 #==========================================================================
235 # RAW FILE
239 # RAW FILE
236 #==========================================================================
240 #==========================================================================
237 def test_raw_file_ok(self):
241 def test_raw_file_ok(self):
238 self.log_user()
242 self.log_user()
239 response = self.app.get(url(controller='files', action='rawfile',
243 response = self.app.get(url(controller='files', action='rawfile',
240 repo_name=HG_REPO,
244 repo_name=HG_REPO,
241 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
245 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
242 f_path='vcs/nodes.py'))
246 f_path='vcs/nodes.py'))
243
247
244 assert response.content_disposition == "attachment; filename=nodes.py"
248 assert response.content_disposition == "attachment; filename=nodes.py"
245 assert response.content_type == mimetypes.guess_type("nodes.py")[0]
249 assert response.content_type == mimetypes.guess_type("nodes.py")[0]
246
250
247 def test_raw_file_wrong_cs(self):
251 def test_raw_file_wrong_cs(self):
248 self.log_user()
252 self.log_user()
249 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
253 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
250 f_path = 'vcs/nodes.py'
254 f_path = 'vcs/nodes.py'
251
255
252 response = self.app.get(url(controller='files', action='rawfile',
256 response = self.app.get(url(controller='files', action='rawfile',
253 repo_name=HG_REPO,
257 repo_name=HG_REPO,
254 revision=rev,
258 revision=rev,
255 f_path=f_path), status=404)
259 f_path=f_path), status=404)
256
260
257 msg = """Such revision does not exist for this repository"""
261 msg = """Such revision does not exist for this repository"""
258 response.mustcontain(msg)
262 response.mustcontain(msg)
259
263
260 def test_raw_file_wrong_f_path(self):
264 def test_raw_file_wrong_f_path(self):
261 self.log_user()
265 self.log_user()
262 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
266 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
263 f_path = 'vcs/ERRORnodes.py'
267 f_path = 'vcs/ERRORnodes.py'
264 response = self.app.get(url(controller='files', action='rawfile',
268 response = self.app.get(url(controller='files', action='rawfile',
265 repo_name=HG_REPO,
269 repo_name=HG_REPO,
266 revision=rev,
270 revision=rev,
267 f_path=f_path), status=404)
271 f_path=f_path), status=404)
268
272
269 msg = "There is no file nor directory at the given path: &#39;%s&#39; at revision %s" % (f_path, rev[:12])
273 msg = "There is no file nor directory at the given path: &#39;%s&#39; at revision %s" % (f_path, rev[:12])
270 response.mustcontain(msg)
274 response.mustcontain(msg)
271
275
272 #==========================================================================
276 #==========================================================================
273 # RAW RESPONSE - PLAIN
277 # RAW RESPONSE - PLAIN
274 #==========================================================================
278 #==========================================================================
275 def test_raw_ok(self):
279 def test_raw_ok(self):
276 self.log_user()
280 self.log_user()
277 response = self.app.get(url(controller='files', action='raw',
281 response = self.app.get(url(controller='files', action='raw',
278 repo_name=HG_REPO,
282 repo_name=HG_REPO,
279 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
283 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
280 f_path='vcs/nodes.py'))
284 f_path='vcs/nodes.py'))
281
285
282 assert response.content_type == "text/plain"
286 assert response.content_type == "text/plain"
283
287
284 def test_raw_wrong_cs(self):
288 def test_raw_wrong_cs(self):
285 self.log_user()
289 self.log_user()
286 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
290 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
287 f_path = 'vcs/nodes.py'
291 f_path = 'vcs/nodes.py'
288
292
289 response = self.app.get(url(controller='files', action='raw',
293 response = self.app.get(url(controller='files', action='raw',
290 repo_name=HG_REPO,
294 repo_name=HG_REPO,
291 revision=rev,
295 revision=rev,
292 f_path=f_path), status=404)
296 f_path=f_path), status=404)
293
297
294 msg = """Such revision does not exist for this repository"""
298 msg = """Such revision does not exist for this repository"""
295 response.mustcontain(msg)
299 response.mustcontain(msg)
296
300
297 def test_raw_wrong_f_path(self):
301 def test_raw_wrong_f_path(self):
298 self.log_user()
302 self.log_user()
299 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
303 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
300 f_path = 'vcs/ERRORnodes.py'
304 f_path = 'vcs/ERRORnodes.py'
301 response = self.app.get(url(controller='files', action='raw',
305 response = self.app.get(url(controller='files', action='raw',
302 repo_name=HG_REPO,
306 repo_name=HG_REPO,
303 revision=rev,
307 revision=rev,
304 f_path=f_path), status=404)
308 f_path=f_path), status=404)
305 msg = "There is no file nor directory at the given path: &#39;%s&#39; at revision %s" % (f_path, rev[:12])
309 msg = "There is no file nor directory at the given path: &#39;%s&#39; at revision %s" % (f_path, rev[:12])
306 response.mustcontain(msg)
310 response.mustcontain(msg)
307
311
308 def test_ajaxed_files_list(self):
312 def test_ajaxed_files_list(self):
309 self.log_user()
313 self.log_user()
310 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
314 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
311 response = self.app.get(
315 response = self.app.get(
312 url('files_nodelist_home', repo_name=HG_REPO, f_path='/',
316 url('files_nodelist_home', repo_name=HG_REPO, f_path='/',
313 revision=rev),
317 revision=rev),
314 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
318 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
315 )
319 )
316 response.mustcontain("vcs/web/simplevcs/views/repository.py")
320 response.mustcontain("vcs/web/simplevcs/views/repository.py")
317
321
318 # Hg - ADD FILE
322 # Hg - ADD FILE
319 def test_add_file_view_hg(self):
323 def test_add_file_view_hg(self):
320 self.log_user()
324 self.log_user()
321 response = self.app.get(url('files_add_home',
325 response = self.app.get(url('files_add_home',
322 repo_name=HG_REPO,
326 repo_name=HG_REPO,
323 revision='tip', f_path='/'))
327 revision='tip', f_path='/'))
324
328
325 def test_add_file_into_hg_missing_content(self):
329 def test_add_file_into_hg_missing_content(self):
326 self.log_user()
330 self.log_user()
327 response = self.app.post(url('files_add_home',
331 response = self.app.post(url('files_add_home',
328 repo_name=HG_REPO,
332 repo_name=HG_REPO,
329 revision='tip', f_path='/'),
333 revision='tip', f_path='/'),
330 params={
334 params={
331 'content': '',
335 'content': '',
332 '_authentication_token': self.authentication_token(),
336 '_authentication_token': self.authentication_token(),
333 },
337 },
334 status=302)
338 status=302)
335
339
336 self.checkSessionFlash(response, 'No content')
340 self.checkSessionFlash(response, 'No content')
337
341
338 def test_add_file_into_hg_missing_filename(self):
342 def test_add_file_into_hg_missing_filename(self):
339 self.log_user()
343 self.log_user()
340 response = self.app.post(url('files_add_home',
344 response = self.app.post(url('files_add_home',
341 repo_name=HG_REPO,
345 repo_name=HG_REPO,
342 revision='tip', f_path='/'),
346 revision='tip', f_path='/'),
343 params={
347 params={
344 'content': "foo",
348 'content': "foo",
345 '_authentication_token': self.authentication_token(),
349 '_authentication_token': self.authentication_token(),
346 },
350 },
347 status=302)
351 status=302)
348
352
349 self.checkSessionFlash(response, 'No filename')
353 self.checkSessionFlash(response, 'No filename')
350
354
351 @parametrize('location,filename', [
355 @parametrize('location,filename', [
352 ('/abs', 'foo'),
356 ('/abs', 'foo'),
353 ('../rel', 'foo'),
357 ('../rel', 'foo'),
354 ('file/../foo', 'foo'),
358 ('file/../foo', 'foo'),
355 ])
359 ])
356 def test_add_file_into_hg_bad_filenames(self, location, filename):
360 def test_add_file_into_hg_bad_filenames(self, location, filename):
357 self.log_user()
361 self.log_user()
358 response = self.app.post(url('files_add_home',
362 response = self.app.post(url('files_add_home',
359 repo_name=HG_REPO,
363 repo_name=HG_REPO,
360 revision='tip', f_path='/'),
364 revision='tip', f_path='/'),
361 params={
365 params={
362 'content': "foo",
366 'content': "foo",
363 'filename': filename,
367 'filename': filename,
364 'location': location,
368 'location': location,
365 '_authentication_token': self.authentication_token(),
369 '_authentication_token': self.authentication_token(),
366 },
370 },
367 status=302)
371 status=302)
368
372
369 self.checkSessionFlash(response, 'Location must be relative path and must not contain .. in path')
373 self.checkSessionFlash(response, 'Location must be relative path and must not contain .. in path')
370
374
371 @parametrize('cnt,location,filename', [
375 @parametrize('cnt,location,filename', [
372 (1, '', 'foo.txt'),
376 (1, '', 'foo.txt'),
373 (2, 'dir', 'foo.rst'),
377 (2, 'dir', 'foo.rst'),
374 (3, 'rel/dir', 'foo.bar'),
378 (3, 'rel/dir', 'foo.bar'),
375 ])
379 ])
376 def test_add_file_into_hg(self, cnt, location, filename):
380 def test_add_file_into_hg(self, cnt, location, filename):
377 self.log_user()
381 self.log_user()
378 repo = fixture.create_repo(u'commit-test-%s' % cnt, repo_type='hg')
382 repo = fixture.create_repo(u'commit-test-%s' % cnt, repo_type='hg')
379 response = self.app.post(url('files_add_home',
383 response = self.app.post(url('files_add_home',
380 repo_name=repo.repo_name,
384 repo_name=repo.repo_name,
381 revision='tip', f_path='/'),
385 revision='tip', f_path='/'),
382 params={
386 params={
383 'content': "foo",
387 'content': "foo",
384 'filename': filename,
388 'filename': filename,
385 'location': location,
389 'location': location,
386 '_authentication_token': self.authentication_token(),
390 '_authentication_token': self.authentication_token(),
387 },
391 },
388 status=302)
392 status=302)
389 try:
393 try:
390 self.checkSessionFlash(response, 'Successfully committed to %s'
394 self.checkSessionFlash(response, 'Successfully committed to %s'
391 % posixpath.join(location, filename))
395 % posixpath.join(location, filename))
392 finally:
396 finally:
393 fixture.destroy_repo(repo.repo_name)
397 fixture.destroy_repo(repo.repo_name)
394
398
395 # Git - add file
399 # Git - add file
396 def test_add_file_view_git(self):
400 def test_add_file_view_git(self):
397 self.log_user()
401 self.log_user()
398 response = self.app.get(url('files_add_home',
402 response = self.app.get(url('files_add_home',
399 repo_name=GIT_REPO,
403 repo_name=GIT_REPO,
400 revision='tip', f_path='/'))
404 revision='tip', f_path='/'))
401
405
402 def test_add_file_into_git_missing_content(self):
406 def test_add_file_into_git_missing_content(self):
403 self.log_user()
407 self.log_user()
404 response = self.app.post(url('files_add_home',
408 response = self.app.post(url('files_add_home',
405 repo_name=GIT_REPO,
409 repo_name=GIT_REPO,
406 revision='tip', f_path='/'),
410 revision='tip', f_path='/'),
407 params={
411 params={
408 'content': '',
412 'content': '',
409 '_authentication_token': self.authentication_token(),
413 '_authentication_token': self.authentication_token(),
410 },
414 },
411 status=302)
415 status=302)
412 self.checkSessionFlash(response, 'No content')
416 self.checkSessionFlash(response, 'No content')
413
417
414 def test_add_file_into_git_missing_filename(self):
418 def test_add_file_into_git_missing_filename(self):
415 self.log_user()
419 self.log_user()
416 response = self.app.post(url('files_add_home',
420 response = self.app.post(url('files_add_home',
417 repo_name=GIT_REPO,
421 repo_name=GIT_REPO,
418 revision='tip', f_path='/'),
422 revision='tip', f_path='/'),
419 params={
423 params={
420 'content': "foo",
424 'content': "foo",
421 '_authentication_token': self.authentication_token(),
425 '_authentication_token': self.authentication_token(),
422 },
426 },
423 status=302)
427 status=302)
424
428
425 self.checkSessionFlash(response, 'No filename')
429 self.checkSessionFlash(response, 'No filename')
426
430
427 @parametrize('location,filename', [
431 @parametrize('location,filename', [
428 ('/abs', 'foo'),
432 ('/abs', 'foo'),
429 ('../rel', 'foo'),
433 ('../rel', 'foo'),
430 ('file/../foo', 'foo'),
434 ('file/../foo', 'foo'),
431 ])
435 ])
432 def test_add_file_into_git_bad_filenames(self, location, filename):
436 def test_add_file_into_git_bad_filenames(self, location, filename):
433 self.log_user()
437 self.log_user()
434 response = self.app.post(url('files_add_home',
438 response = self.app.post(url('files_add_home',
435 repo_name=GIT_REPO,
439 repo_name=GIT_REPO,
436 revision='tip', f_path='/'),
440 revision='tip', f_path='/'),
437 params={
441 params={
438 'content': "foo",
442 'content': "foo",
439 'filename': filename,
443 'filename': filename,
440 'location': location,
444 'location': location,
441 '_authentication_token': self.authentication_token(),
445 '_authentication_token': self.authentication_token(),
442 },
446 },
443 status=302)
447 status=302)
444
448
445 self.checkSessionFlash(response, 'Location must be relative path and must not contain .. in path')
449 self.checkSessionFlash(response, 'Location must be relative path and must not contain .. in path')
446
450
447 @parametrize('cnt,location,filename', [
451 @parametrize('cnt,location,filename', [
448 (1, '', 'foo.txt'),
452 (1, '', 'foo.txt'),
449 (2, 'dir', 'foo.rst'),
453 (2, 'dir', 'foo.rst'),
450 (3, 'rel/dir', 'foo.bar'),
454 (3, 'rel/dir', 'foo.bar'),
451 ])
455 ])
452 def test_add_file_into_git(self, cnt, location, filename):
456 def test_add_file_into_git(self, cnt, location, filename):
453 self.log_user()
457 self.log_user()
454 repo = fixture.create_repo(u'commit-test-%s' % cnt, repo_type='git')
458 repo = fixture.create_repo(u'commit-test-%s' % cnt, repo_type='git')
455 response = self.app.post(url('files_add_home',
459 response = self.app.post(url('files_add_home',
456 repo_name=repo.repo_name,
460 repo_name=repo.repo_name,
457 revision='tip', f_path='/'),
461 revision='tip', f_path='/'),
458 params={
462 params={
459 'content': "foo",
463 'content': "foo",
460 'filename': filename,
464 'filename': filename,
461 'location': location,
465 'location': location,
462 '_authentication_token': self.authentication_token(),
466 '_authentication_token': self.authentication_token(),
463 },
467 },
464 status=302)
468 status=302)
465 try:
469 try:
466 self.checkSessionFlash(response, 'Successfully committed to %s'
470 self.checkSessionFlash(response, 'Successfully committed to %s'
467 % posixpath.join(location, filename))
471 % posixpath.join(location, filename))
468 finally:
472 finally:
469 fixture.destroy_repo(repo.repo_name)
473 fixture.destroy_repo(repo.repo_name)
470
474
471 # Hg - EDIT
475 # Hg - EDIT
472 def test_edit_file_view_hg(self):
476 def test_edit_file_view_hg(self):
473 self.log_user()
477 self.log_user()
474 response = self.app.get(url('files_edit_home',
478 response = self.app.get(url('files_edit_home',
475 repo_name=HG_REPO,
479 repo_name=HG_REPO,
476 revision='tip', f_path='vcs/nodes.py'))
480 revision='tip', f_path='vcs/nodes.py'))
477
481
478 def test_edit_file_view_not_on_branch_hg(self):
482 def test_edit_file_view_not_on_branch_hg(self):
479 self.log_user()
483 self.log_user()
480 repo = fixture.create_repo(u'test-edit-repo', repo_type='hg')
484 repo = fixture.create_repo(u'test-edit-repo', repo_type='hg')
481
485
482 ## add file
486 ## add file
483 location = 'vcs'
487 location = 'vcs'
484 filename = 'nodes.py'
488 filename = 'nodes.py'
485 response = self.app.post(url('files_add_home',
489 response = self.app.post(url('files_add_home',
486 repo_name=repo.repo_name,
490 repo_name=repo.repo_name,
487 revision='tip', f_path='/'),
491 revision='tip', f_path='/'),
488 params={
492 params={
489 'content': "def py():\n print 'hello'\n",
493 'content': "def py():\n print 'hello'\n",
490 'filename': filename,
494 'filename': filename,
491 'location': location,
495 'location': location,
492 '_authentication_token': self.authentication_token(),
496 '_authentication_token': self.authentication_token(),
493 },
497 },
494 status=302)
498 status=302)
495 response.follow()
499 response.follow()
496 try:
500 try:
497 self.checkSessionFlash(response, 'Successfully committed to %s'
501 self.checkSessionFlash(response, 'Successfully committed to %s'
498 % posixpath.join(location, filename))
502 % posixpath.join(location, filename))
499 response = self.app.get(url('files_edit_home',
503 response = self.app.get(url('files_edit_home',
500 repo_name=repo.repo_name,
504 repo_name=repo.repo_name,
501 revision='tip', f_path=posixpath.join(location, filename)),
505 revision='tip', f_path=posixpath.join(location, filename)),
502 status=302)
506 status=302)
503 self.checkSessionFlash(response,
507 self.checkSessionFlash(response,
504 'You can only edit files with revision being a valid branch')
508 'You can only edit files with revision being a valid branch')
505 finally:
509 finally:
506 fixture.destroy_repo(repo.repo_name)
510 fixture.destroy_repo(repo.repo_name)
507
511
508 def test_edit_file_view_commit_changes_hg(self):
512 def test_edit_file_view_commit_changes_hg(self):
509 self.log_user()
513 self.log_user()
510 repo = fixture.create_repo(u'test-edit-repo', repo_type='hg')
514 repo = fixture.create_repo(u'test-edit-repo', repo_type='hg')
511
515
512 ## add file
516 ## add file
513 location = 'vcs'
517 location = 'vcs'
514 filename = 'nodes.py'
518 filename = 'nodes.py'
515 response = self.app.post(url('files_add_home',
519 response = self.app.post(url('files_add_home',
516 repo_name=repo.repo_name,
520 repo_name=repo.repo_name,
517 revision='tip',
521 revision='tip',
518 f_path='/'),
522 f_path='/'),
519 params={
523 params={
520 'content': "def py():\n print 'hello'\n",
524 'content': "def py():\n print 'hello'\n",
521 'filename': filename,
525 'filename': filename,
522 'location': location,
526 'location': location,
523 '_authentication_token': self.authentication_token(),
527 '_authentication_token': self.authentication_token(),
524 },
528 },
525 status=302)
529 status=302)
526 response.follow()
530 response.follow()
527 try:
531 try:
528 self.checkSessionFlash(response, 'Successfully committed to %s'
532 self.checkSessionFlash(response, 'Successfully committed to %s'
529 % posixpath.join(location, filename))
533 % posixpath.join(location, filename))
530 response = self.app.post(url('files_edit_home',
534 response = self.app.post(url('files_edit_home',
531 repo_name=repo.repo_name,
535 repo_name=repo.repo_name,
532 revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
536 revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
533 f_path=posixpath.join(location, filename)),
537 f_path=posixpath.join(location, filename)),
534 params={
538 params={
535 'content': "def py():\n print 'hello world'\n",
539 'content': "def py():\n print 'hello world'\n",
536 'message': 'i committed',
540 'message': 'i committed',
537 '_authentication_token': self.authentication_token(),
541 '_authentication_token': self.authentication_token(),
538 },
542 },
539 status=302)
543 status=302)
540 self.checkSessionFlash(response, 'Successfully committed to %s'
544 self.checkSessionFlash(response, 'Successfully committed to %s'
541 % posixpath.join(location, filename))
545 % posixpath.join(location, filename))
542 finally:
546 finally:
543 fixture.destroy_repo(repo.repo_name)
547 fixture.destroy_repo(repo.repo_name)
544
548
545 # Git - edit
549 # Git - edit
546 def test_edit_file_view_git(self):
550 def test_edit_file_view_git(self):
547 self.log_user()
551 self.log_user()
548 response = self.app.get(url('files_edit_home',
552 response = self.app.get(url('files_edit_home',
549 repo_name=GIT_REPO,
553 repo_name=GIT_REPO,
550 revision='tip', f_path='vcs/nodes.py'))
554 revision='tip', f_path='vcs/nodes.py'))
551
555
552 def test_edit_file_view_not_on_branch_git(self):
556 def test_edit_file_view_not_on_branch_git(self):
553 self.log_user()
557 self.log_user()
554 repo = fixture.create_repo(u'test-edit-repo', repo_type='git')
558 repo = fixture.create_repo(u'test-edit-repo', repo_type='git')
555
559
556 ## add file
560 ## add file
557 location = 'vcs'
561 location = 'vcs'
558 filename = 'nodes.py'
562 filename = 'nodes.py'
559 response = self.app.post(url('files_add_home',
563 response = self.app.post(url('files_add_home',
560 repo_name=repo.repo_name,
564 repo_name=repo.repo_name,
561 revision='tip', f_path='/'),
565 revision='tip', f_path='/'),
562 params={
566 params={
563 'content': "def py():\n print 'hello'\n",
567 'content': "def py():\n print 'hello'\n",
564 'filename': filename,
568 'filename': filename,
565 'location': location,
569 'location': location,
566 '_authentication_token': self.authentication_token(),
570 '_authentication_token': self.authentication_token(),
567 },
571 },
568 status=302)
572 status=302)
569 response.follow()
573 response.follow()
570 try:
574 try:
571 self.checkSessionFlash(response, 'Successfully committed to %s'
575 self.checkSessionFlash(response, 'Successfully committed to %s'
572 % posixpath.join(location, filename))
576 % posixpath.join(location, filename))
573 response = self.app.get(url('files_edit_home',
577 response = self.app.get(url('files_edit_home',
574 repo_name=repo.repo_name,
578 repo_name=repo.repo_name,
575 revision='tip', f_path=posixpath.join(location, filename)),
579 revision='tip', f_path=posixpath.join(location, filename)),
576 status=302)
580 status=302)
577 self.checkSessionFlash(response,
581 self.checkSessionFlash(response,
578 'You can only edit files with revision being a valid branch')
582 'You can only edit files with revision being a valid branch')
579 finally:
583 finally:
580 fixture.destroy_repo(repo.repo_name)
584 fixture.destroy_repo(repo.repo_name)
581
585
582 def test_edit_file_view_commit_changes_git(self):
586 def test_edit_file_view_commit_changes_git(self):
583 self.log_user()
587 self.log_user()
584 repo = fixture.create_repo(u'test-edit-repo', repo_type='git')
588 repo = fixture.create_repo(u'test-edit-repo', repo_type='git')
585
589
586 ## add file
590 ## add file
587 location = 'vcs'
591 location = 'vcs'
588 filename = 'nodes.py'
592 filename = 'nodes.py'
589 response = self.app.post(url('files_add_home',
593 response = self.app.post(url('files_add_home',
590 repo_name=repo.repo_name,
594 repo_name=repo.repo_name,
591 revision='tip',
595 revision='tip',
592 f_path='/'),
596 f_path='/'),
593 params={
597 params={
594 'content': "def py():\n print 'hello'\n",
598 'content': "def py():\n print 'hello'\n",
595 'filename': filename,
599 'filename': filename,
596 'location': location,
600 'location': location,
597 '_authentication_token': self.authentication_token(),
601 '_authentication_token': self.authentication_token(),
598 },
602 },
599 status=302)
603 status=302)
600 response.follow()
604 response.follow()
601 try:
605 try:
602 self.checkSessionFlash(response, 'Successfully committed to %s'
606 self.checkSessionFlash(response, 'Successfully committed to %s'
603 % posixpath.join(location, filename))
607 % posixpath.join(location, filename))
604 response = self.app.post(url('files_edit_home',
608 response = self.app.post(url('files_edit_home',
605 repo_name=repo.repo_name,
609 repo_name=repo.repo_name,
606 revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
610 revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
607 f_path=posixpath.join(location, filename)),
611 f_path=posixpath.join(location, filename)),
608 params={
612 params={
609 'content': "def py():\n print 'hello world'\n",
613 'content': "def py():\n print 'hello world'\n",
610 'message': 'i committed',
614 'message': 'i committed',
611 '_authentication_token': self.authentication_token(),
615 '_authentication_token': self.authentication_token(),
612 },
616 },
613 status=302)
617 status=302)
614 self.checkSessionFlash(response, 'Successfully committed to %s'
618 self.checkSessionFlash(response, 'Successfully committed to %s'
615 % posixpath.join(location, filename))
619 % posixpath.join(location, filename))
616 finally:
620 finally:
617 fixture.destroy_repo(repo.repo_name)
621 fixture.destroy_repo(repo.repo_name)
618
622
619 # Hg - delete
623 # Hg - delete
620 def test_delete_file_view_hg(self):
624 def test_delete_file_view_hg(self):
621 self.log_user()
625 self.log_user()
622 response = self.app.get(url('files_delete_home',
626 response = self.app.get(url('files_delete_home',
623 repo_name=HG_REPO,
627 repo_name=HG_REPO,
624 revision='tip', f_path='vcs/nodes.py'))
628 revision='tip', f_path='vcs/nodes.py'))
625
629
626 def test_delete_file_view_not_on_branch_hg(self):
630 def test_delete_file_view_not_on_branch_hg(self):
627 self.log_user()
631 self.log_user()
628 repo = fixture.create_repo(u'test-delete-repo', repo_type='hg')
632 repo = fixture.create_repo(u'test-delete-repo', repo_type='hg')
629
633
630 ## add file
634 ## add file
631 location = 'vcs'
635 location = 'vcs'
632 filename = 'nodes.py'
636 filename = 'nodes.py'
633 response = self.app.post(url('files_add_home',
637 response = self.app.post(url('files_add_home',
634 repo_name=repo.repo_name,
638 repo_name=repo.repo_name,
635 revision='tip', f_path='/'),
639 revision='tip', f_path='/'),
636 params={
640 params={
637 'content': "def py():\n print 'hello'\n",
641 'content': "def py():\n print 'hello'\n",
638 'filename': filename,
642 'filename': filename,
639 'location': location,
643 'location': location,
640 '_authentication_token': self.authentication_token(),
644 '_authentication_token': self.authentication_token(),
641 },
645 },
642 status=302)
646 status=302)
643 response.follow()
647 response.follow()
644 try:
648 try:
645 self.checkSessionFlash(response, 'Successfully committed to %s'
649 self.checkSessionFlash(response, 'Successfully committed to %s'
646 % posixpath.join(location, filename))
650 % posixpath.join(location, filename))
647 response = self.app.get(url('files_delete_home',
651 response = self.app.get(url('files_delete_home',
648 repo_name=repo.repo_name,
652 repo_name=repo.repo_name,
649 revision='tip', f_path=posixpath.join(location, filename)),
653 revision='tip', f_path=posixpath.join(location, filename)),
650 status=302)
654 status=302)
651 self.checkSessionFlash(response,
655 self.checkSessionFlash(response,
652 'You can only delete files with revision being a valid branch')
656 'You can only delete files with revision being a valid branch')
653 finally:
657 finally:
654 fixture.destroy_repo(repo.repo_name)
658 fixture.destroy_repo(repo.repo_name)
655
659
656 def test_delete_file_view_commit_changes_hg(self):
660 def test_delete_file_view_commit_changes_hg(self):
657 self.log_user()
661 self.log_user()
658 repo = fixture.create_repo(u'test-delete-repo', repo_type='hg')
662 repo = fixture.create_repo(u'test-delete-repo', repo_type='hg')
659
663
660 ## add file
664 ## add file
661 location = 'vcs'
665 location = 'vcs'
662 filename = 'nodes.py'
666 filename = 'nodes.py'
663 response = self.app.post(url('files_add_home',
667 response = self.app.post(url('files_add_home',
664 repo_name=repo.repo_name,
668 repo_name=repo.repo_name,
665 revision='tip',
669 revision='tip',
666 f_path='/'),
670 f_path='/'),
667 params={
671 params={
668 'content': "def py():\n print 'hello'\n",
672 'content': "def py():\n print 'hello'\n",
669 'filename': filename,
673 'filename': filename,
670 'location': location,
674 'location': location,
671 '_authentication_token': self.authentication_token(),
675 '_authentication_token': self.authentication_token(),
672 },
676 },
673 status=302)
677 status=302)
674 response.follow()
678 response.follow()
675 try:
679 try:
676 self.checkSessionFlash(response, 'Successfully committed to %s'
680 self.checkSessionFlash(response, 'Successfully committed to %s'
677 % posixpath.join(location, filename))
681 % posixpath.join(location, filename))
678 response = self.app.post(url('files_delete_home',
682 response = self.app.post(url('files_delete_home',
679 repo_name=repo.repo_name,
683 repo_name=repo.repo_name,
680 revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
684 revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
681 f_path=posixpath.join(location, filename)),
685 f_path=posixpath.join(location, filename)),
682 params={
686 params={
683 'message': 'i committed',
687 'message': 'i committed',
684 '_authentication_token': self.authentication_token(),
688 '_authentication_token': self.authentication_token(),
685 },
689 },
686 status=302)
690 status=302)
687 self.checkSessionFlash(response,
691 self.checkSessionFlash(response,
688 'Successfully deleted file %s' % posixpath.join(location, filename))
692 'Successfully deleted file %s' % posixpath.join(location, filename))
689 finally:
693 finally:
690 fixture.destroy_repo(repo.repo_name)
694 fixture.destroy_repo(repo.repo_name)
691
695
692 # Git - delete
696 # Git - delete
693 def test_delete_file_view_git(self):
697 def test_delete_file_view_git(self):
694 self.log_user()
698 self.log_user()
695 response = self.app.get(url('files_delete_home',
699 response = self.app.get(url('files_delete_home',
696 repo_name=HG_REPO,
700 repo_name=HG_REPO,
697 revision='tip', f_path='vcs/nodes.py'))
701 revision='tip', f_path='vcs/nodes.py'))
698
702
699 def test_delete_file_view_not_on_branch_git(self):
703 def test_delete_file_view_not_on_branch_git(self):
700 self.log_user()
704 self.log_user()
701 repo = fixture.create_repo(u'test-delete-repo', repo_type='git')
705 repo = fixture.create_repo(u'test-delete-repo', repo_type='git')
702
706
703 ## add file
707 ## add file
704 location = 'vcs'
708 location = 'vcs'
705 filename = 'nodes.py'
709 filename = 'nodes.py'
706 response = self.app.post(url('files_add_home',
710 response = self.app.post(url('files_add_home',
707 repo_name=repo.repo_name,
711 repo_name=repo.repo_name,
708 revision='tip', f_path='/'),
712 revision='tip', f_path='/'),
709 params={
713 params={
710 'content': "def py():\n print 'hello'\n",
714 'content': "def py():\n print 'hello'\n",
711 'filename': filename,
715 'filename': filename,
712 'location': location,
716 'location': location,
713 '_authentication_token': self.authentication_token(),
717 '_authentication_token': self.authentication_token(),
714 },
718 },
715 status=302)
719 status=302)
716 response.follow()
720 response.follow()
717 try:
721 try:
718 self.checkSessionFlash(response, 'Successfully committed to %s'
722 self.checkSessionFlash(response, 'Successfully committed to %s'
719 % posixpath.join(location, filename))
723 % posixpath.join(location, filename))
720 response = self.app.get(url('files_delete_home',
724 response = self.app.get(url('files_delete_home',
721 repo_name=repo.repo_name,
725 repo_name=repo.repo_name,
722 revision='tip', f_path=posixpath.join(location, filename)),
726 revision='tip', f_path=posixpath.join(location, filename)),
723 status=302)
727 status=302)
724 self.checkSessionFlash(response,
728 self.checkSessionFlash(response,
725 'You can only delete files with revision being a valid branch')
729 'You can only delete files with revision being a valid branch')
726 finally:
730 finally:
727 fixture.destroy_repo(repo.repo_name)
731 fixture.destroy_repo(repo.repo_name)
728
732
729 def test_delete_file_view_commit_changes_git(self):
733 def test_delete_file_view_commit_changes_git(self):
730 self.log_user()
734 self.log_user()
731 repo = fixture.create_repo(u'test-delete-repo', repo_type='git')
735 repo = fixture.create_repo(u'test-delete-repo', repo_type='git')
732
736
733 ## add file
737 ## add file
734 location = 'vcs'
738 location = 'vcs'
735 filename = 'nodes.py'
739 filename = 'nodes.py'
736 response = self.app.post(url('files_add_home',
740 response = self.app.post(url('files_add_home',
737 repo_name=repo.repo_name,
741 repo_name=repo.repo_name,
738 revision='tip',
742 revision='tip',
739 f_path='/'),
743 f_path='/'),
740 params={
744 params={
741 'content': "def py():\n print 'hello'\n",
745 'content': "def py():\n print 'hello'\n",
742 'filename': filename,
746 'filename': filename,
743 'location': location,
747 'location': location,
744 '_authentication_token': self.authentication_token(),
748 '_authentication_token': self.authentication_token(),
745 },
749 },
746 status=302)
750 status=302)
747 response.follow()
751 response.follow()
748 try:
752 try:
749 self.checkSessionFlash(response, 'Successfully committed to %s'
753 self.checkSessionFlash(response, 'Successfully committed to %s'
750 % posixpath.join(location, filename))
754 % posixpath.join(location, filename))
751 response = self.app.post(url('files_delete_home',
755 response = self.app.post(url('files_delete_home',
752 repo_name=repo.repo_name,
756 repo_name=repo.repo_name,
753 revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
757 revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
754 f_path=posixpath.join(location, filename)),
758 f_path=posixpath.join(location, filename)),
755 params={
759 params={
756 'message': 'i committed',
760 'message': 'i committed',
757 '_authentication_token': self.authentication_token(),
761 '_authentication_token': self.authentication_token(),
758 },
762 },
759 status=302)
763 status=302)
760 self.checkSessionFlash(response,
764 self.checkSessionFlash(response,
761 'Successfully deleted file %s' % posixpath.join(location, filename))
765 'Successfully deleted file %s' % posixpath.join(location, filename))
762 finally:
766 finally:
763 fixture.destroy_repo(repo.repo_name)
767 fixture.destroy_repo(repo.repo_name)
General Comments 0
You need to be logged in to leave comments. Login now