##// END OF EJS Templates
yet another fix for commit linkifications
marcink -
r2966:566f0166 beta
parent child Browse files
Show More
@@ -1,1107 +1,1103 b''
1 """Helper functions
1 """Helper functions
2
2
3 Consists of functions to typically be used within templates, but also
3 Consists of functions to typically be used within templates, but also
4 available to Controllers. This module is available to both as 'h'.
4 available to Controllers. This module is available to both as 'h'.
5 """
5 """
6 import random
6 import random
7 import hashlib
7 import hashlib
8 import StringIO
8 import StringIO
9 import urllib
9 import urllib
10 import math
10 import math
11 import logging
11 import logging
12 import re
12 import re
13 import urlparse
13 import urlparse
14
14
15 from datetime import datetime
15 from datetime import datetime
16 from pygments.formatters.html import HtmlFormatter
16 from pygments.formatters.html import HtmlFormatter
17 from pygments import highlight as code_highlight
17 from pygments import highlight as code_highlight
18 from pylons import url, request, config
18 from pylons import url, request, config
19 from pylons.i18n.translation import _, ungettext
19 from pylons.i18n.translation import _, ungettext
20 from hashlib import md5
20 from hashlib import md5
21
21
22 from webhelpers.html import literal, HTML, escape
22 from webhelpers.html import literal, HTML, escape
23 from webhelpers.html.tools import *
23 from webhelpers.html.tools import *
24 from webhelpers.html.builder import make_tag
24 from webhelpers.html.builder import make_tag
25 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
25 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
26 end_form, file, form, hidden, image, javascript_link, link_to, \
26 end_form, file, form, hidden, image, javascript_link, link_to, \
27 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
27 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
28 submit, text, password, textarea, title, ul, xml_declaration, radio
28 submit, text, password, textarea, title, ul, xml_declaration, radio
29 from webhelpers.html.tools import auto_link, button_to, highlight, \
29 from webhelpers.html.tools import auto_link, button_to, highlight, \
30 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
30 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
31 from webhelpers.number import format_byte_size, format_bit_size
31 from webhelpers.number import format_byte_size, format_bit_size
32 from webhelpers.pylonslib import Flash as _Flash
32 from webhelpers.pylonslib import Flash as _Flash
33 from webhelpers.pylonslib.secure_form import secure_form
33 from webhelpers.pylonslib.secure_form import secure_form
34 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
34 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
35 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
35 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
36 replace_whitespace, urlify, truncate, wrap_paragraphs
36 replace_whitespace, urlify, truncate, wrap_paragraphs
37 from webhelpers.date import time_ago_in_words
37 from webhelpers.date import time_ago_in_words
38 from webhelpers.paginate import Page
38 from webhelpers.paginate import Page
39 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
39 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
40 convert_boolean_attrs, NotGiven, _make_safe_id_component
40 convert_boolean_attrs, NotGiven, _make_safe_id_component
41
41
42 from rhodecode.lib.annotate import annotate_highlight
42 from rhodecode.lib.annotate import annotate_highlight
43 from rhodecode.lib.utils import repo_name_slug
43 from rhodecode.lib.utils import repo_name_slug
44 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
44 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
45 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict
45 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict
46 from rhodecode.lib.markup_renderer import MarkupRenderer
46 from rhodecode.lib.markup_renderer import MarkupRenderer
47 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
47 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
48 from rhodecode.lib.vcs.backends.base import BaseChangeset
48 from rhodecode.lib.vcs.backends.base import BaseChangeset
49 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
49 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.db import URL_SEP, Permission
51 from rhodecode.model.db import URL_SEP, Permission
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 html_escape_table = {
56 html_escape_table = {
57 "&": "&",
57 "&": "&",
58 '"': """,
58 '"': """,
59 "'": "'",
59 "'": "'",
60 ">": ">",
60 ">": ">",
61 "<": "&lt;",
61 "<": "&lt;",
62 }
62 }
63
63
64
64
65 def html_escape(text):
65 def html_escape(text):
66 """Produce entities within text."""
66 """Produce entities within text."""
67 return "".join(html_escape_table.get(c, c) for c in text)
67 return "".join(html_escape_table.get(c, c) for c in text)
68
68
69
69
70 def shorter(text, size=20):
70 def shorter(text, size=20):
71 postfix = '...'
71 postfix = '...'
72 if len(text) > size:
72 if len(text) > size:
73 return text[:size - len(postfix)] + postfix
73 return text[:size - len(postfix)] + postfix
74 return text
74 return text
75
75
76
76
77 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
77 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
78 """
78 """
79 Reset button
79 Reset button
80 """
80 """
81 _set_input_attrs(attrs, type, name, value)
81 _set_input_attrs(attrs, type, name, value)
82 _set_id_attr(attrs, id, name)
82 _set_id_attr(attrs, id, name)
83 convert_boolean_attrs(attrs, ["disabled"])
83 convert_boolean_attrs(attrs, ["disabled"])
84 return HTML.input(**attrs)
84 return HTML.input(**attrs)
85
85
86 reset = _reset
86 reset = _reset
87 safeid = _make_safe_id_component
87 safeid = _make_safe_id_component
88
88
89
89
90 def FID(raw_id, path):
90 def FID(raw_id, path):
91 """
91 """
92 Creates a uniqe ID for filenode based on it's hash of path and revision
92 Creates a uniqe ID for filenode based on it's hash of path and revision
93 it's safe to use in urls
93 it's safe to use in urls
94
94
95 :param raw_id:
95 :param raw_id:
96 :param path:
96 :param path:
97 """
97 """
98
98
99 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
99 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
100
100
101
101
102 def get_token():
102 def get_token():
103 """Return the current authentication token, creating one if one doesn't
103 """Return the current authentication token, creating one if one doesn't
104 already exist.
104 already exist.
105 """
105 """
106 token_key = "_authentication_token"
106 token_key = "_authentication_token"
107 from pylons import session
107 from pylons import session
108 if not token_key in session:
108 if not token_key in session:
109 try:
109 try:
110 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
110 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
111 except AttributeError: # Python < 2.4
111 except AttributeError: # Python < 2.4
112 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
112 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
113 session[token_key] = token
113 session[token_key] = token
114 if hasattr(session, 'save'):
114 if hasattr(session, 'save'):
115 session.save()
115 session.save()
116 return session[token_key]
116 return session[token_key]
117
117
118
118
119 class _GetError(object):
119 class _GetError(object):
120 """Get error from form_errors, and represent it as span wrapped error
120 """Get error from form_errors, and represent it as span wrapped error
121 message
121 message
122
122
123 :param field_name: field to fetch errors for
123 :param field_name: field to fetch errors for
124 :param form_errors: form errors dict
124 :param form_errors: form errors dict
125 """
125 """
126
126
127 def __call__(self, field_name, form_errors):
127 def __call__(self, field_name, form_errors):
128 tmpl = """<span class="error_msg">%s</span>"""
128 tmpl = """<span class="error_msg">%s</span>"""
129 if form_errors and field_name in form_errors:
129 if form_errors and field_name in form_errors:
130 return literal(tmpl % form_errors.get(field_name))
130 return literal(tmpl % form_errors.get(field_name))
131
131
132 get_error = _GetError()
132 get_error = _GetError()
133
133
134
134
135 class _ToolTip(object):
135 class _ToolTip(object):
136
136
137 def __call__(self, tooltip_title, trim_at=50):
137 def __call__(self, tooltip_title, trim_at=50):
138 """
138 """
139 Special function just to wrap our text into nice formatted
139 Special function just to wrap our text into nice formatted
140 autowrapped text
140 autowrapped text
141
141
142 :param tooltip_title:
142 :param tooltip_title:
143 """
143 """
144 tooltip_title = escape(tooltip_title)
144 tooltip_title = escape(tooltip_title)
145 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
145 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
146 return tooltip_title
146 return tooltip_title
147 tooltip = _ToolTip()
147 tooltip = _ToolTip()
148
148
149
149
150 class _FilesBreadCrumbs(object):
150 class _FilesBreadCrumbs(object):
151
151
152 def __call__(self, repo_name, rev, paths):
152 def __call__(self, repo_name, rev, paths):
153 if isinstance(paths, str):
153 if isinstance(paths, str):
154 paths = safe_unicode(paths)
154 paths = safe_unicode(paths)
155 url_l = [link_to(repo_name, url('files_home',
155 url_l = [link_to(repo_name, url('files_home',
156 repo_name=repo_name,
156 repo_name=repo_name,
157 revision=rev, f_path=''),
157 revision=rev, f_path=''),
158 class_='ypjax-link')]
158 class_='ypjax-link')]
159 paths_l = paths.split('/')
159 paths_l = paths.split('/')
160 for cnt, p in enumerate(paths_l):
160 for cnt, p in enumerate(paths_l):
161 if p != '':
161 if p != '':
162 url_l.append(link_to(p,
162 url_l.append(link_to(p,
163 url('files_home',
163 url('files_home',
164 repo_name=repo_name,
164 repo_name=repo_name,
165 revision=rev,
165 revision=rev,
166 f_path='/'.join(paths_l[:cnt + 1])
166 f_path='/'.join(paths_l[:cnt + 1])
167 ),
167 ),
168 class_='ypjax-link'
168 class_='ypjax-link'
169 )
169 )
170 )
170 )
171
171
172 return literal('/'.join(url_l))
172 return literal('/'.join(url_l))
173
173
174 files_breadcrumbs = _FilesBreadCrumbs()
174 files_breadcrumbs = _FilesBreadCrumbs()
175
175
176
176
177 class CodeHtmlFormatter(HtmlFormatter):
177 class CodeHtmlFormatter(HtmlFormatter):
178 """
178 """
179 My code Html Formatter for source codes
179 My code Html Formatter for source codes
180 """
180 """
181
181
182 def wrap(self, source, outfile):
182 def wrap(self, source, outfile):
183 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
183 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
184
184
185 def _wrap_code(self, source):
185 def _wrap_code(self, source):
186 for cnt, it in enumerate(source):
186 for cnt, it in enumerate(source):
187 i, t = it
187 i, t = it
188 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
188 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
189 yield i, t
189 yield i, t
190
190
191 def _wrap_tablelinenos(self, inner):
191 def _wrap_tablelinenos(self, inner):
192 dummyoutfile = StringIO.StringIO()
192 dummyoutfile = StringIO.StringIO()
193 lncount = 0
193 lncount = 0
194 for t, line in inner:
194 for t, line in inner:
195 if t:
195 if t:
196 lncount += 1
196 lncount += 1
197 dummyoutfile.write(line)
197 dummyoutfile.write(line)
198
198
199 fl = self.linenostart
199 fl = self.linenostart
200 mw = len(str(lncount + fl - 1))
200 mw = len(str(lncount + fl - 1))
201 sp = self.linenospecial
201 sp = self.linenospecial
202 st = self.linenostep
202 st = self.linenostep
203 la = self.lineanchors
203 la = self.lineanchors
204 aln = self.anchorlinenos
204 aln = self.anchorlinenos
205 nocls = self.noclasses
205 nocls = self.noclasses
206 if sp:
206 if sp:
207 lines = []
207 lines = []
208
208
209 for i in range(fl, fl + lncount):
209 for i in range(fl, fl + lncount):
210 if i % st == 0:
210 if i % st == 0:
211 if i % sp == 0:
211 if i % sp == 0:
212 if aln:
212 if aln:
213 lines.append('<a href="#%s%d" class="special">%*d</a>' %
213 lines.append('<a href="#%s%d" class="special">%*d</a>' %
214 (la, i, mw, i))
214 (la, i, mw, i))
215 else:
215 else:
216 lines.append('<span class="special">%*d</span>' % (mw, i))
216 lines.append('<span class="special">%*d</span>' % (mw, i))
217 else:
217 else:
218 if aln:
218 if aln:
219 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
219 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
220 else:
220 else:
221 lines.append('%*d' % (mw, i))
221 lines.append('%*d' % (mw, i))
222 else:
222 else:
223 lines.append('')
223 lines.append('')
224 ls = '\n'.join(lines)
224 ls = '\n'.join(lines)
225 else:
225 else:
226 lines = []
226 lines = []
227 for i in range(fl, fl + lncount):
227 for i in range(fl, fl + lncount):
228 if i % st == 0:
228 if i % st == 0:
229 if aln:
229 if aln:
230 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
230 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
231 else:
231 else:
232 lines.append('%*d' % (mw, i))
232 lines.append('%*d' % (mw, i))
233 else:
233 else:
234 lines.append('')
234 lines.append('')
235 ls = '\n'.join(lines)
235 ls = '\n'.join(lines)
236
236
237 # in case you wonder about the seemingly redundant <div> here: since the
237 # in case you wonder about the seemingly redundant <div> here: since the
238 # content in the other cell also is wrapped in a div, some browsers in
238 # content in the other cell also is wrapped in a div, some browsers in
239 # some configurations seem to mess up the formatting...
239 # some configurations seem to mess up the formatting...
240 if nocls:
240 if nocls:
241 yield 0, ('<table class="%stable">' % self.cssclass +
241 yield 0, ('<table class="%stable">' % self.cssclass +
242 '<tr><td><div class="linenodiv" '
242 '<tr><td><div class="linenodiv" '
243 'style="background-color: #f0f0f0; padding-right: 10px">'
243 'style="background-color: #f0f0f0; padding-right: 10px">'
244 '<pre style="line-height: 125%">' +
244 '<pre style="line-height: 125%">' +
245 ls + '</pre></div></td><td id="hlcode" class="code">')
245 ls + '</pre></div></td><td id="hlcode" class="code">')
246 else:
246 else:
247 yield 0, ('<table class="%stable">' % self.cssclass +
247 yield 0, ('<table class="%stable">' % self.cssclass +
248 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
248 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
249 ls + '</pre></div></td><td id="hlcode" class="code">')
249 ls + '</pre></div></td><td id="hlcode" class="code">')
250 yield 0, dummyoutfile.getvalue()
250 yield 0, dummyoutfile.getvalue()
251 yield 0, '</td></tr></table>'
251 yield 0, '</td></tr></table>'
252
252
253
253
254 def pygmentize(filenode, **kwargs):
254 def pygmentize(filenode, **kwargs):
255 """pygmentize function using pygments
255 """pygmentize function using pygments
256
256
257 :param filenode:
257 :param filenode:
258 """
258 """
259
259
260 return literal(code_highlight(filenode.content,
260 return literal(code_highlight(filenode.content,
261 filenode.lexer, CodeHtmlFormatter(**kwargs)))
261 filenode.lexer, CodeHtmlFormatter(**kwargs)))
262
262
263
263
264 def pygmentize_annotation(repo_name, filenode, **kwargs):
264 def pygmentize_annotation(repo_name, filenode, **kwargs):
265 """
265 """
266 pygmentize function for annotation
266 pygmentize function for annotation
267
267
268 :param filenode:
268 :param filenode:
269 """
269 """
270
270
271 color_dict = {}
271 color_dict = {}
272
272
273 def gen_color(n=10000):
273 def gen_color(n=10000):
274 """generator for getting n of evenly distributed colors using
274 """generator for getting n of evenly distributed colors using
275 hsv color and golden ratio. It always return same order of colors
275 hsv color and golden ratio. It always return same order of colors
276
276
277 :returns: RGB tuple
277 :returns: RGB tuple
278 """
278 """
279
279
280 def hsv_to_rgb(h, s, v):
280 def hsv_to_rgb(h, s, v):
281 if s == 0.0:
281 if s == 0.0:
282 return v, v, v
282 return v, v, v
283 i = int(h * 6.0) # XXX assume int() truncates!
283 i = int(h * 6.0) # XXX assume int() truncates!
284 f = (h * 6.0) - i
284 f = (h * 6.0) - i
285 p = v * (1.0 - s)
285 p = v * (1.0 - s)
286 q = v * (1.0 - s * f)
286 q = v * (1.0 - s * f)
287 t = v * (1.0 - s * (1.0 - f))
287 t = v * (1.0 - s * (1.0 - f))
288 i = i % 6
288 i = i % 6
289 if i == 0:
289 if i == 0:
290 return v, t, p
290 return v, t, p
291 if i == 1:
291 if i == 1:
292 return q, v, p
292 return q, v, p
293 if i == 2:
293 if i == 2:
294 return p, v, t
294 return p, v, t
295 if i == 3:
295 if i == 3:
296 return p, q, v
296 return p, q, v
297 if i == 4:
297 if i == 4:
298 return t, p, v
298 return t, p, v
299 if i == 5:
299 if i == 5:
300 return v, p, q
300 return v, p, q
301
301
302 golden_ratio = 0.618033988749895
302 golden_ratio = 0.618033988749895
303 h = 0.22717784590367374
303 h = 0.22717784590367374
304
304
305 for _ in xrange(n):
305 for _ in xrange(n):
306 h += golden_ratio
306 h += golden_ratio
307 h %= 1
307 h %= 1
308 HSV_tuple = [h, 0.95, 0.95]
308 HSV_tuple = [h, 0.95, 0.95]
309 RGB_tuple = hsv_to_rgb(*HSV_tuple)
309 RGB_tuple = hsv_to_rgb(*HSV_tuple)
310 yield map(lambda x: str(int(x * 256)), RGB_tuple)
310 yield map(lambda x: str(int(x * 256)), RGB_tuple)
311
311
312 cgenerator = gen_color()
312 cgenerator = gen_color()
313
313
314 def get_color_string(cs):
314 def get_color_string(cs):
315 if cs in color_dict:
315 if cs in color_dict:
316 col = color_dict[cs]
316 col = color_dict[cs]
317 else:
317 else:
318 col = color_dict[cs] = cgenerator.next()
318 col = color_dict[cs] = cgenerator.next()
319 return "color: rgb(%s)! important;" % (', '.join(col))
319 return "color: rgb(%s)! important;" % (', '.join(col))
320
320
321 def url_func(repo_name):
321 def url_func(repo_name):
322
322
323 def _url_func(changeset):
323 def _url_func(changeset):
324 author = changeset.author
324 author = changeset.author
325 date = changeset.date
325 date = changeset.date
326 message = tooltip(changeset.message)
326 message = tooltip(changeset.message)
327
327
328 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
328 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
329 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
329 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
330 "</b> %s<br/></div>")
330 "</b> %s<br/></div>")
331
331
332 tooltip_html = tooltip_html % (author, date, message)
332 tooltip_html = tooltip_html % (author, date, message)
333 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
333 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
334 short_id(changeset.raw_id))
334 short_id(changeset.raw_id))
335 uri = link_to(
335 uri = link_to(
336 lnk_format,
336 lnk_format,
337 url('changeset_home', repo_name=repo_name,
337 url('changeset_home', repo_name=repo_name,
338 revision=changeset.raw_id),
338 revision=changeset.raw_id),
339 style=get_color_string(changeset.raw_id),
339 style=get_color_string(changeset.raw_id),
340 class_='tooltip',
340 class_='tooltip',
341 title=tooltip_html
341 title=tooltip_html
342 )
342 )
343
343
344 uri += '\n'
344 uri += '\n'
345 return uri
345 return uri
346 return _url_func
346 return _url_func
347
347
348 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
348 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
349
349
350
350
351 def is_following_repo(repo_name, user_id):
351 def is_following_repo(repo_name, user_id):
352 from rhodecode.model.scm import ScmModel
352 from rhodecode.model.scm import ScmModel
353 return ScmModel().is_following_repo(repo_name, user_id)
353 return ScmModel().is_following_repo(repo_name, user_id)
354
354
355 flash = _Flash()
355 flash = _Flash()
356
356
357 #==============================================================================
357 #==============================================================================
358 # SCM FILTERS available via h.
358 # SCM FILTERS available via h.
359 #==============================================================================
359 #==============================================================================
360 from rhodecode.lib.vcs.utils import author_name, author_email
360 from rhodecode.lib.vcs.utils import author_name, author_email
361 from rhodecode.lib.utils2 import credentials_filter, age as _age
361 from rhodecode.lib.utils2 import credentials_filter, age as _age
362 from rhodecode.model.db import User, ChangesetStatus
362 from rhodecode.model.db import User, ChangesetStatus
363
363
364 age = lambda x: _age(x)
364 age = lambda x: _age(x)
365 capitalize = lambda x: x.capitalize()
365 capitalize = lambda x: x.capitalize()
366 email = author_email
366 email = author_email
367 short_id = lambda x: x[:12]
367 short_id = lambda x: x[:12]
368 hide_credentials = lambda x: ''.join(credentials_filter(x))
368 hide_credentials = lambda x: ''.join(credentials_filter(x))
369
369
370
370
371 def fmt_date(date):
371 def fmt_date(date):
372 if date:
372 if date:
373 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
373 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
374 return date.strftime(_fmt).decode('utf8')
374 return date.strftime(_fmt).decode('utf8')
375
375
376 return ""
376 return ""
377
377
378
378
379 def is_git(repository):
379 def is_git(repository):
380 if hasattr(repository, 'alias'):
380 if hasattr(repository, 'alias'):
381 _type = repository.alias
381 _type = repository.alias
382 elif hasattr(repository, 'repo_type'):
382 elif hasattr(repository, 'repo_type'):
383 _type = repository.repo_type
383 _type = repository.repo_type
384 else:
384 else:
385 _type = repository
385 _type = repository
386 return _type == 'git'
386 return _type == 'git'
387
387
388
388
389 def is_hg(repository):
389 def is_hg(repository):
390 if hasattr(repository, 'alias'):
390 if hasattr(repository, 'alias'):
391 _type = repository.alias
391 _type = repository.alias
392 elif hasattr(repository, 'repo_type'):
392 elif hasattr(repository, 'repo_type'):
393 _type = repository.repo_type
393 _type = repository.repo_type
394 else:
394 else:
395 _type = repository
395 _type = repository
396 return _type == 'hg'
396 return _type == 'hg'
397
397
398
398
399 def email_or_none(author):
399 def email_or_none(author):
400 # extract email from the commit string
400 # extract email from the commit string
401 _email = email(author)
401 _email = email(author)
402 if _email != '':
402 if _email != '':
403 # check it against RhodeCode database, and use the MAIN email for this
403 # check it against RhodeCode database, and use the MAIN email for this
404 # user
404 # user
405 user = User.get_by_email(_email, case_insensitive=True, cache=True)
405 user = User.get_by_email(_email, case_insensitive=True, cache=True)
406 if user is not None:
406 if user is not None:
407 return user.email
407 return user.email
408 return _email
408 return _email
409
409
410 # See if it contains a username we can get an email from
410 # See if it contains a username we can get an email from
411 user = User.get_by_username(author_name(author), case_insensitive=True,
411 user = User.get_by_username(author_name(author), case_insensitive=True,
412 cache=True)
412 cache=True)
413 if user is not None:
413 if user is not None:
414 return user.email
414 return user.email
415
415
416 # No valid email, not a valid user in the system, none!
416 # No valid email, not a valid user in the system, none!
417 return None
417 return None
418
418
419
419
420 def person(author, show_attr="username_and_name"):
420 def person(author, show_attr="username_and_name"):
421 # attr to return from fetched user
421 # attr to return from fetched user
422 person_getter = lambda usr: getattr(usr, show_attr)
422 person_getter = lambda usr: getattr(usr, show_attr)
423
423
424 # Valid email in the attribute passed, see if they're in the system
424 # Valid email in the attribute passed, see if they're in the system
425 _email = email(author)
425 _email = email(author)
426 if _email != '':
426 if _email != '':
427 user = User.get_by_email(_email, case_insensitive=True, cache=True)
427 user = User.get_by_email(_email, case_insensitive=True, cache=True)
428 if user is not None:
428 if user is not None:
429 return person_getter(user)
429 return person_getter(user)
430 return _email
430 return _email
431
431
432 # Maybe it's a username?
432 # Maybe it's a username?
433 _author = author_name(author)
433 _author = author_name(author)
434 user = User.get_by_username(_author, case_insensitive=True,
434 user = User.get_by_username(_author, case_insensitive=True,
435 cache=True)
435 cache=True)
436 if user is not None:
436 if user is not None:
437 return person_getter(user)
437 return person_getter(user)
438
438
439 # Still nothing? Just pass back the author name then
439 # Still nothing? Just pass back the author name then
440 return _author
440 return _author
441
441
442
442
443 def person_by_id(id_, show_attr="username_and_name"):
443 def person_by_id(id_, show_attr="username_and_name"):
444 # attr to return from fetched user
444 # attr to return from fetched user
445 person_getter = lambda usr: getattr(usr, show_attr)
445 person_getter = lambda usr: getattr(usr, show_attr)
446
446
447 #maybe it's an ID ?
447 #maybe it's an ID ?
448 if str(id_).isdigit() or isinstance(id_, int):
448 if str(id_).isdigit() or isinstance(id_, int):
449 id_ = int(id_)
449 id_ = int(id_)
450 user = User.get(id_)
450 user = User.get(id_)
451 if user is not None:
451 if user is not None:
452 return person_getter(user)
452 return person_getter(user)
453 return id_
453 return id_
454
454
455
455
456 def desc_stylize(value):
456 def desc_stylize(value):
457 """
457 """
458 converts tags from value into html equivalent
458 converts tags from value into html equivalent
459
459
460 :param value:
460 :param value:
461 """
461 """
462 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
462 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
463 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
463 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
464 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
464 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
465 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
465 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
466 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z\-\/]*)\]',
466 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z\-\/]*)\]',
467 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
467 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
468 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
468 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
469 '<div class="metatag" tag="lang">\\2</div>', value)
469 '<div class="metatag" tag="lang">\\2</div>', value)
470 value = re.sub(r'\[([a-z]+)\]',
470 value = re.sub(r'\[([a-z]+)\]',
471 '<div class="metatag" tag="\\1">\\1</div>', value)
471 '<div class="metatag" tag="\\1">\\1</div>', value)
472
472
473 return value
473 return value
474
474
475
475
476 def bool2icon(value):
476 def bool2icon(value):
477 """Returns True/False values represented as small html image of true/false
477 """Returns True/False values represented as small html image of true/false
478 icons
478 icons
479
479
480 :param value: bool value
480 :param value: bool value
481 """
481 """
482
482
483 if value is True:
483 if value is True:
484 return HTML.tag('img', src=url("/images/icons/accept.png"),
484 return HTML.tag('img', src=url("/images/icons/accept.png"),
485 alt=_('True'))
485 alt=_('True'))
486
486
487 if value is False:
487 if value is False:
488 return HTML.tag('img', src=url("/images/icons/cancel.png"),
488 return HTML.tag('img', src=url("/images/icons/cancel.png"),
489 alt=_('False'))
489 alt=_('False'))
490
490
491 return value
491 return value
492
492
493
493
494 def action_parser(user_log, feed=False, parse_cs=False):
494 def action_parser(user_log, feed=False, parse_cs=False):
495 """
495 """
496 This helper will action_map the specified string action into translated
496 This helper will action_map the specified string action into translated
497 fancy names with icons and links
497 fancy names with icons and links
498
498
499 :param user_log: user log instance
499 :param user_log: user log instance
500 :param feed: use output for feeds (no html and fancy icons)
500 :param feed: use output for feeds (no html and fancy icons)
501 :param parse_cs: parse Changesets into VCS instances
501 :param parse_cs: parse Changesets into VCS instances
502 """
502 """
503
503
504 action = user_log.action
504 action = user_log.action
505 action_params = ' '
505 action_params = ' '
506
506
507 x = action.split(':')
507 x = action.split(':')
508
508
509 if len(x) > 1:
509 if len(x) > 1:
510 action, action_params = x
510 action, action_params = x
511
511
512 def get_cs_links():
512 def get_cs_links():
513 revs_limit = 3 # display this amount always
513 revs_limit = 3 # display this amount always
514 revs_top_limit = 50 # show upto this amount of changesets hidden
514 revs_top_limit = 50 # show upto this amount of changesets hidden
515 revs_ids = action_params.split(',')
515 revs_ids = action_params.split(',')
516 deleted = user_log.repository is None
516 deleted = user_log.repository is None
517 if deleted:
517 if deleted:
518 return ','.join(revs_ids)
518 return ','.join(revs_ids)
519
519
520 repo_name = user_log.repository.repo_name
520 repo_name = user_log.repository.repo_name
521
521
522 def lnk(rev, repo_name):
522 def lnk(rev, repo_name):
523 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
523 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
524 lbl = '%s' % (rev.short_id[:8])
524 lbl = '%s' % (rev.short_id[:8])
525 _url = url('changeset_home', repo_name=repo_name,
525 _url = url('changeset_home', repo_name=repo_name,
526 revision=rev.raw_id)
526 revision=rev.raw_id)
527 title = tooltip(rev.message)
527 title = tooltip(rev.message)
528 else:
528 else:
529 ## changeset cannot be found/striped/removed etc.
529 ## changeset cannot be found/striped/removed etc.
530 lbl = ('%s' % rev)[:12]
530 lbl = ('%s' % rev)[:12]
531 _url = '#'
531 _url = '#'
532 title = _('Changeset not found')
532 title = _('Changeset not found')
533 if parse_cs:
533 if parse_cs:
534 return link_to(lbl, _url, title=title, class_='tooltip')
534 return link_to(lbl, _url, title=title, class_='tooltip')
535 return link_to(lbl, _url, raw_id=rev.raw_id, class_='journal-cs')
535 return link_to(lbl, _url, raw_id=rev.raw_id, class_='journal-cs')
536
536
537 revs = []
537 revs = []
538 if len(filter(lambda v: v != '', revs_ids)) > 0:
538 if len(filter(lambda v: v != '', revs_ids)) > 0:
539 repo = None
539 repo = None
540 for rev in revs_ids[:revs_top_limit]:
540 for rev in revs_ids[:revs_top_limit]:
541 # we want parsed changesets, or new log store format is bad
541 # we want parsed changesets, or new log store format is bad
542 if parse_cs:
542 if parse_cs:
543 try:
543 try:
544 if repo is None:
544 if repo is None:
545 repo = user_log.repository.scm_instance
545 repo = user_log.repository.scm_instance
546 _rev = repo.get_changeset(rev)
546 _rev = repo.get_changeset(rev)
547 revs.append(_rev)
547 revs.append(_rev)
548 except ChangesetDoesNotExistError:
548 except ChangesetDoesNotExistError:
549 log.error('cannot find revision %s in this repo' % rev)
549 log.error('cannot find revision %s in this repo' % rev)
550 revs.append(rev)
550 revs.append(rev)
551 continue
551 continue
552 else:
552 else:
553 _rev = AttributeDict({
553 _rev = AttributeDict({
554 'short_id': rev[:12],
554 'short_id': rev[:12],
555 'raw_id': rev,
555 'raw_id': rev,
556 'message': '',
556 'message': '',
557 })
557 })
558 revs.append(_rev)
558 revs.append(_rev)
559 cs_links = []
559 cs_links = []
560 cs_links.append(" " + ', '.join(
560 cs_links.append(" " + ', '.join(
561 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
561 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
562 )
562 )
563 )
563 )
564
564
565 compare_view = (
565 compare_view = (
566 ' <div class="compare_view tooltip" title="%s">'
566 ' <div class="compare_view tooltip" title="%s">'
567 '<a href="%s">%s</a> </div>' % (
567 '<a href="%s">%s</a> </div>' % (
568 _('Show all combined changesets %s->%s') % (
568 _('Show all combined changesets %s->%s') % (
569 revs_ids[0][:12], revs_ids[-1][:12]
569 revs_ids[0][:12], revs_ids[-1][:12]
570 ),
570 ),
571 url('changeset_home', repo_name=repo_name,
571 url('changeset_home', repo_name=repo_name,
572 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
572 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
573 ),
573 ),
574 _('compare view')
574 _('compare view')
575 )
575 )
576 )
576 )
577
577
578 # if we have exactly one more than normally displayed
578 # if we have exactly one more than normally displayed
579 # just display it, takes less space than displaying
579 # just display it, takes less space than displaying
580 # "and 1 more revisions"
580 # "and 1 more revisions"
581 if len(revs_ids) == revs_limit + 1:
581 if len(revs_ids) == revs_limit + 1:
582 rev = revs[revs_limit]
582 rev = revs[revs_limit]
583 cs_links.append(", " + lnk(rev, repo_name))
583 cs_links.append(", " + lnk(rev, repo_name))
584
584
585 # hidden-by-default ones
585 # hidden-by-default ones
586 if len(revs_ids) > revs_limit + 1:
586 if len(revs_ids) > revs_limit + 1:
587 uniq_id = revs_ids[0]
587 uniq_id = revs_ids[0]
588 html_tmpl = (
588 html_tmpl = (
589 '<span> %s <a class="show_more" id="_%s" '
589 '<span> %s <a class="show_more" id="_%s" '
590 'href="#more">%s</a> %s</span>'
590 'href="#more">%s</a> %s</span>'
591 )
591 )
592 if not feed:
592 if not feed:
593 cs_links.append(html_tmpl % (
593 cs_links.append(html_tmpl % (
594 _('and'),
594 _('and'),
595 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
595 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
596 _('revisions')
596 _('revisions')
597 )
597 )
598 )
598 )
599
599
600 if not feed:
600 if not feed:
601 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
601 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
602 else:
602 else:
603 html_tmpl = '<span id="%s"> %s </span>'
603 html_tmpl = '<span id="%s"> %s </span>'
604
604
605 morelinks = ', '.join(
605 morelinks = ', '.join(
606 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
606 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
607 )
607 )
608
608
609 if len(revs_ids) > revs_top_limit:
609 if len(revs_ids) > revs_top_limit:
610 morelinks += ', ...'
610 morelinks += ', ...'
611
611
612 cs_links.append(html_tmpl % (uniq_id, morelinks))
612 cs_links.append(html_tmpl % (uniq_id, morelinks))
613 if len(revs) > 1:
613 if len(revs) > 1:
614 cs_links.append(compare_view)
614 cs_links.append(compare_view)
615 return ''.join(cs_links)
615 return ''.join(cs_links)
616
616
617 def get_fork_name():
617 def get_fork_name():
618 repo_name = action_params
618 repo_name = action_params
619 _url = url('summary_home', repo_name=repo_name)
619 _url = url('summary_home', repo_name=repo_name)
620 return _('fork name %s') % link_to(action_params, _url)
620 return _('fork name %s') % link_to(action_params, _url)
621
621
622 def get_user_name():
622 def get_user_name():
623 user_name = action_params
623 user_name = action_params
624 return user_name
624 return user_name
625
625
626 def get_users_group():
626 def get_users_group():
627 group_name = action_params
627 group_name = action_params
628 return group_name
628 return group_name
629
629
630 def get_pull_request():
630 def get_pull_request():
631 pull_request_id = action_params
631 pull_request_id = action_params
632 repo_name = user_log.repository.repo_name
632 repo_name = user_log.repository.repo_name
633 return link_to(_('Pull request #%s') % pull_request_id,
633 return link_to(_('Pull request #%s') % pull_request_id,
634 url('pullrequest_show', repo_name=repo_name,
634 url('pullrequest_show', repo_name=repo_name,
635 pull_request_id=pull_request_id))
635 pull_request_id=pull_request_id))
636
636
637 # action : translated str, callback(extractor), icon
637 # action : translated str, callback(extractor), icon
638 action_map = {
638 action_map = {
639 'user_deleted_repo': (_('[deleted] repository'),
639 'user_deleted_repo': (_('[deleted] repository'),
640 None, 'database_delete.png'),
640 None, 'database_delete.png'),
641 'user_created_repo': (_('[created] repository'),
641 'user_created_repo': (_('[created] repository'),
642 None, 'database_add.png'),
642 None, 'database_add.png'),
643 'user_created_fork': (_('[created] repository as fork'),
643 'user_created_fork': (_('[created] repository as fork'),
644 None, 'arrow_divide.png'),
644 None, 'arrow_divide.png'),
645 'user_forked_repo': (_('[forked] repository'),
645 'user_forked_repo': (_('[forked] repository'),
646 get_fork_name, 'arrow_divide.png'),
646 get_fork_name, 'arrow_divide.png'),
647 'user_updated_repo': (_('[updated] repository'),
647 'user_updated_repo': (_('[updated] repository'),
648 None, 'database_edit.png'),
648 None, 'database_edit.png'),
649 'admin_deleted_repo': (_('[delete] repository'),
649 'admin_deleted_repo': (_('[delete] repository'),
650 None, 'database_delete.png'),
650 None, 'database_delete.png'),
651 'admin_created_repo': (_('[created] repository'),
651 'admin_created_repo': (_('[created] repository'),
652 None, 'database_add.png'),
652 None, 'database_add.png'),
653 'admin_forked_repo': (_('[forked] repository'),
653 'admin_forked_repo': (_('[forked] repository'),
654 None, 'arrow_divide.png'),
654 None, 'arrow_divide.png'),
655 'admin_updated_repo': (_('[updated] repository'),
655 'admin_updated_repo': (_('[updated] repository'),
656 None, 'database_edit.png'),
656 None, 'database_edit.png'),
657 'admin_created_user': (_('[created] user'),
657 'admin_created_user': (_('[created] user'),
658 get_user_name, 'user_add.png'),
658 get_user_name, 'user_add.png'),
659 'admin_updated_user': (_('[updated] user'),
659 'admin_updated_user': (_('[updated] user'),
660 get_user_name, 'user_edit.png'),
660 get_user_name, 'user_edit.png'),
661 'admin_created_users_group': (_('[created] users group'),
661 'admin_created_users_group': (_('[created] users group'),
662 get_users_group, 'group_add.png'),
662 get_users_group, 'group_add.png'),
663 'admin_updated_users_group': (_('[updated] users group'),
663 'admin_updated_users_group': (_('[updated] users group'),
664 get_users_group, 'group_edit.png'),
664 get_users_group, 'group_edit.png'),
665 'user_commented_revision': (_('[commented] on revision in repository'),
665 'user_commented_revision': (_('[commented] on revision in repository'),
666 get_cs_links, 'comment_add.png'),
666 get_cs_links, 'comment_add.png'),
667 'user_commented_pull_request': (_('[commented] on pull request for'),
667 'user_commented_pull_request': (_('[commented] on pull request for'),
668 get_pull_request, 'comment_add.png'),
668 get_pull_request, 'comment_add.png'),
669 'user_closed_pull_request': (_('[closed] pull request for'),
669 'user_closed_pull_request': (_('[closed] pull request for'),
670 get_pull_request, 'tick.png'),
670 get_pull_request, 'tick.png'),
671 'push': (_('[pushed] into'),
671 'push': (_('[pushed] into'),
672 get_cs_links, 'script_add.png'),
672 get_cs_links, 'script_add.png'),
673 'push_local': (_('[committed via RhodeCode] into repository'),
673 'push_local': (_('[committed via RhodeCode] into repository'),
674 get_cs_links, 'script_edit.png'),
674 get_cs_links, 'script_edit.png'),
675 'push_remote': (_('[pulled from remote] into repository'),
675 'push_remote': (_('[pulled from remote] into repository'),
676 get_cs_links, 'connect.png'),
676 get_cs_links, 'connect.png'),
677 'pull': (_('[pulled] from'),
677 'pull': (_('[pulled] from'),
678 None, 'down_16.png'),
678 None, 'down_16.png'),
679 'started_following_repo': (_('[started following] repository'),
679 'started_following_repo': (_('[started following] repository'),
680 None, 'heart_add.png'),
680 None, 'heart_add.png'),
681 'stopped_following_repo': (_('[stopped following] repository'),
681 'stopped_following_repo': (_('[stopped following] repository'),
682 None, 'heart_delete.png'),
682 None, 'heart_delete.png'),
683 }
683 }
684
684
685 action_str = action_map.get(action, action)
685 action_str = action_map.get(action, action)
686 if feed:
686 if feed:
687 action = action_str[0].replace('[', '').replace(']', '')
687 action = action_str[0].replace('[', '').replace(']', '')
688 else:
688 else:
689 action = action_str[0]\
689 action = action_str[0]\
690 .replace('[', '<span class="journal_highlight">')\
690 .replace('[', '<span class="journal_highlight">')\
691 .replace(']', '</span>')
691 .replace(']', '</span>')
692
692
693 action_params_func = lambda: ""
693 action_params_func = lambda: ""
694
694
695 if callable(action_str[1]):
695 if callable(action_str[1]):
696 action_params_func = action_str[1]
696 action_params_func = action_str[1]
697
697
698 def action_parser_icon():
698 def action_parser_icon():
699 action = user_log.action
699 action = user_log.action
700 action_params = None
700 action_params = None
701 x = action.split(':')
701 x = action.split(':')
702
702
703 if len(x) > 1:
703 if len(x) > 1:
704 action, action_params = x
704 action, action_params = x
705
705
706 tmpl = """<img src="%s%s" alt="%s"/>"""
706 tmpl = """<img src="%s%s" alt="%s"/>"""
707 ico = action_map.get(action, ['', '', ''])[2]
707 ico = action_map.get(action, ['', '', ''])[2]
708 return literal(tmpl % ((url('/images/icons/')), ico, action))
708 return literal(tmpl % ((url('/images/icons/')), ico, action))
709
709
710 # returned callbacks we need to call to get
710 # returned callbacks we need to call to get
711 return [lambda: literal(action), action_params_func, action_parser_icon]
711 return [lambda: literal(action), action_params_func, action_parser_icon]
712
712
713
713
714
714
715 #==============================================================================
715 #==============================================================================
716 # PERMS
716 # PERMS
717 #==============================================================================
717 #==============================================================================
718 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
718 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
719 HasRepoPermissionAny, HasRepoPermissionAll
719 HasRepoPermissionAny, HasRepoPermissionAll
720
720
721
721
722 #==============================================================================
722 #==============================================================================
723 # GRAVATAR URL
723 # GRAVATAR URL
724 #==============================================================================
724 #==============================================================================
725
725
726 def gravatar_url(email_address, size=30):
726 def gravatar_url(email_address, size=30):
727 from pylons import url ## doh, we need to re-import url to mock it later
727 from pylons import url ## doh, we need to re-import url to mock it later
728 if(str2bool(config['app_conf'].get('use_gravatar')) and
728 if(str2bool(config['app_conf'].get('use_gravatar')) and
729 config['app_conf'].get('alternative_gravatar_url')):
729 config['app_conf'].get('alternative_gravatar_url')):
730 tmpl = config['app_conf'].get('alternative_gravatar_url', '')
730 tmpl = config['app_conf'].get('alternative_gravatar_url', '')
731 parsed_url = urlparse.urlparse(url.current(qualified=True))
731 parsed_url = urlparse.urlparse(url.current(qualified=True))
732 tmpl = tmpl.replace('{email}', email_address)\
732 tmpl = tmpl.replace('{email}', email_address)\
733 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
733 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
734 .replace('{netloc}', parsed_url.netloc)\
734 .replace('{netloc}', parsed_url.netloc)\
735 .replace('{scheme}', parsed_url.scheme)\
735 .replace('{scheme}', parsed_url.scheme)\
736 .replace('{size}', str(size))
736 .replace('{size}', str(size))
737 return tmpl
737 return tmpl
738
738
739 if (not str2bool(config['app_conf'].get('use_gravatar')) or
739 if (not str2bool(config['app_conf'].get('use_gravatar')) or
740 not email_address or email_address == 'anonymous@rhodecode.org'):
740 not email_address or email_address == 'anonymous@rhodecode.org'):
741 f = lambda a, l: min(l, key=lambda x: abs(x - a))
741 f = lambda a, l: min(l, key=lambda x: abs(x - a))
742 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
742 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
743
743
744 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
744 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
745 default = 'identicon'
745 default = 'identicon'
746 baseurl_nossl = "http://www.gravatar.com/avatar/"
746 baseurl_nossl = "http://www.gravatar.com/avatar/"
747 baseurl_ssl = "https://secure.gravatar.com/avatar/"
747 baseurl_ssl = "https://secure.gravatar.com/avatar/"
748 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
748 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
749
749
750 if isinstance(email_address, unicode):
750 if isinstance(email_address, unicode):
751 #hashlib crashes on unicode items
751 #hashlib crashes on unicode items
752 email_address = safe_str(email_address)
752 email_address = safe_str(email_address)
753 # construct the url
753 # construct the url
754 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
754 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
755 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
755 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
756
756
757 return gravatar_url
757 return gravatar_url
758
758
759
759
760 #==============================================================================
760 #==============================================================================
761 # REPO PAGER, PAGER FOR REPOSITORY
761 # REPO PAGER, PAGER FOR REPOSITORY
762 #==============================================================================
762 #==============================================================================
763 class RepoPage(Page):
763 class RepoPage(Page):
764
764
765 def __init__(self, collection, page=1, items_per_page=20,
765 def __init__(self, collection, page=1, items_per_page=20,
766 item_count=None, url=None, **kwargs):
766 item_count=None, url=None, **kwargs):
767
767
768 """Create a "RepoPage" instance. special pager for paging
768 """Create a "RepoPage" instance. special pager for paging
769 repository
769 repository
770 """
770 """
771 self._url_generator = url
771 self._url_generator = url
772
772
773 # Safe the kwargs class-wide so they can be used in the pager() method
773 # Safe the kwargs class-wide so they can be used in the pager() method
774 self.kwargs = kwargs
774 self.kwargs = kwargs
775
775
776 # Save a reference to the collection
776 # Save a reference to the collection
777 self.original_collection = collection
777 self.original_collection = collection
778
778
779 self.collection = collection
779 self.collection = collection
780
780
781 # The self.page is the number of the current page.
781 # The self.page is the number of the current page.
782 # The first page has the number 1!
782 # The first page has the number 1!
783 try:
783 try:
784 self.page = int(page) # make it int() if we get it as a string
784 self.page = int(page) # make it int() if we get it as a string
785 except (ValueError, TypeError):
785 except (ValueError, TypeError):
786 self.page = 1
786 self.page = 1
787
787
788 self.items_per_page = items_per_page
788 self.items_per_page = items_per_page
789
789
790 # Unless the user tells us how many items the collections has
790 # Unless the user tells us how many items the collections has
791 # we calculate that ourselves.
791 # we calculate that ourselves.
792 if item_count is not None:
792 if item_count is not None:
793 self.item_count = item_count
793 self.item_count = item_count
794 else:
794 else:
795 self.item_count = len(self.collection)
795 self.item_count = len(self.collection)
796
796
797 # Compute the number of the first and last available page
797 # Compute the number of the first and last available page
798 if self.item_count > 0:
798 if self.item_count > 0:
799 self.first_page = 1
799 self.first_page = 1
800 self.page_count = int(math.ceil(float(self.item_count) /
800 self.page_count = int(math.ceil(float(self.item_count) /
801 self.items_per_page))
801 self.items_per_page))
802 self.last_page = self.first_page + self.page_count - 1
802 self.last_page = self.first_page + self.page_count - 1
803
803
804 # Make sure that the requested page number is the range of
804 # Make sure that the requested page number is the range of
805 # valid pages
805 # valid pages
806 if self.page > self.last_page:
806 if self.page > self.last_page:
807 self.page = self.last_page
807 self.page = self.last_page
808 elif self.page < self.first_page:
808 elif self.page < self.first_page:
809 self.page = self.first_page
809 self.page = self.first_page
810
810
811 # Note: the number of items on this page can be less than
811 # Note: the number of items on this page can be less than
812 # items_per_page if the last page is not full
812 # items_per_page if the last page is not full
813 self.first_item = max(0, (self.item_count) - (self.page *
813 self.first_item = max(0, (self.item_count) - (self.page *
814 items_per_page))
814 items_per_page))
815 self.last_item = ((self.item_count - 1) - items_per_page *
815 self.last_item = ((self.item_count - 1) - items_per_page *
816 (self.page - 1))
816 (self.page - 1))
817
817
818 self.items = list(self.collection[self.first_item:self.last_item + 1])
818 self.items = list(self.collection[self.first_item:self.last_item + 1])
819
819
820 # Links to previous and next page
820 # Links to previous and next page
821 if self.page > self.first_page:
821 if self.page > self.first_page:
822 self.previous_page = self.page - 1
822 self.previous_page = self.page - 1
823 else:
823 else:
824 self.previous_page = None
824 self.previous_page = None
825
825
826 if self.page < self.last_page:
826 if self.page < self.last_page:
827 self.next_page = self.page + 1
827 self.next_page = self.page + 1
828 else:
828 else:
829 self.next_page = None
829 self.next_page = None
830
830
831 # No items available
831 # No items available
832 else:
832 else:
833 self.first_page = None
833 self.first_page = None
834 self.page_count = 0
834 self.page_count = 0
835 self.last_page = None
835 self.last_page = None
836 self.first_item = None
836 self.first_item = None
837 self.last_item = None
837 self.last_item = None
838 self.previous_page = None
838 self.previous_page = None
839 self.next_page = None
839 self.next_page = None
840 self.items = []
840 self.items = []
841
841
842 # This is a subclass of the 'list' type. Initialise the list now.
842 # This is a subclass of the 'list' type. Initialise the list now.
843 list.__init__(self, reversed(self.items))
843 list.__init__(self, reversed(self.items))
844
844
845
845
846 def changed_tooltip(nodes):
846 def changed_tooltip(nodes):
847 """
847 """
848 Generates a html string for changed nodes in changeset page.
848 Generates a html string for changed nodes in changeset page.
849 It limits the output to 30 entries
849 It limits the output to 30 entries
850
850
851 :param nodes: LazyNodesGenerator
851 :param nodes: LazyNodesGenerator
852 """
852 """
853 if nodes:
853 if nodes:
854 pref = ': <br/> '
854 pref = ': <br/> '
855 suf = ''
855 suf = ''
856 if len(nodes) > 30:
856 if len(nodes) > 30:
857 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
857 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
858 return literal(pref + '<br/> '.join([safe_unicode(x.path)
858 return literal(pref + '<br/> '.join([safe_unicode(x.path)
859 for x in nodes[:30]]) + suf)
859 for x in nodes[:30]]) + suf)
860 else:
860 else:
861 return ': ' + _('No Files')
861 return ': ' + _('No Files')
862
862
863
863
864 def repo_link(groups_and_repos):
864 def repo_link(groups_and_repos):
865 """
865 """
866 Makes a breadcrumbs link to repo within a group
866 Makes a breadcrumbs link to repo within a group
867 joins &raquo; on each group to create a fancy link
867 joins &raquo; on each group to create a fancy link
868
868
869 ex::
869 ex::
870 group >> subgroup >> repo
870 group >> subgroup >> repo
871
871
872 :param groups_and_repos:
872 :param groups_and_repos:
873 """
873 """
874 groups, repo_name = groups_and_repos
874 groups, repo_name = groups_and_repos
875
875
876 if not groups:
876 if not groups:
877 return repo_name
877 return repo_name
878 else:
878 else:
879 def make_link(group):
879 def make_link(group):
880 return link_to(group.name, url('repos_group_home',
880 return link_to(group.name, url('repos_group_home',
881 group_name=group.group_name))
881 group_name=group.group_name))
882 return literal(' &raquo; '.join(map(make_link, groups)) + \
882 return literal(' &raquo; '.join(map(make_link, groups)) + \
883 " &raquo; " + repo_name)
883 " &raquo; " + repo_name)
884
884
885
885
886 def fancy_file_stats(stats):
886 def fancy_file_stats(stats):
887 """
887 """
888 Displays a fancy two colored bar for number of added/deleted
888 Displays a fancy two colored bar for number of added/deleted
889 lines of code on file
889 lines of code on file
890
890
891 :param stats: two element list of added/deleted lines of code
891 :param stats: two element list of added/deleted lines of code
892 """
892 """
893
893
894 a, d, t = stats[0], stats[1], stats[0] + stats[1]
894 a, d, t = stats[0], stats[1], stats[0] + stats[1]
895 width = 100
895 width = 100
896 unit = float(width) / (t or 1)
896 unit = float(width) / (t or 1)
897
897
898 # needs > 9% of width to be visible or 0 to be hidden
898 # needs > 9% of width to be visible or 0 to be hidden
899 a_p = max(9, unit * a) if a > 0 else 0
899 a_p = max(9, unit * a) if a > 0 else 0
900 d_p = max(9, unit * d) if d > 0 else 0
900 d_p = max(9, unit * d) if d > 0 else 0
901 p_sum = a_p + d_p
901 p_sum = a_p + d_p
902
902
903 if p_sum > width:
903 if p_sum > width:
904 #adjust the percentage to be == 100% since we adjusted to 9
904 #adjust the percentage to be == 100% since we adjusted to 9
905 if a_p > d_p:
905 if a_p > d_p:
906 a_p = a_p - (p_sum - width)
906 a_p = a_p - (p_sum - width)
907 else:
907 else:
908 d_p = d_p - (p_sum - width)
908 d_p = d_p - (p_sum - width)
909
909
910 a_v = a if a > 0 else ''
910 a_v = a if a > 0 else ''
911 d_v = d if d > 0 else ''
911 d_v = d if d > 0 else ''
912
912
913 def cgen(l_type):
913 def cgen(l_type):
914 mapping = {'tr': 'top-right-rounded-corner-mid',
914 mapping = {'tr': 'top-right-rounded-corner-mid',
915 'tl': 'top-left-rounded-corner-mid',
915 'tl': 'top-left-rounded-corner-mid',
916 'br': 'bottom-right-rounded-corner-mid',
916 'br': 'bottom-right-rounded-corner-mid',
917 'bl': 'bottom-left-rounded-corner-mid'}
917 'bl': 'bottom-left-rounded-corner-mid'}
918 map_getter = lambda x: mapping[x]
918 map_getter = lambda x: mapping[x]
919
919
920 if l_type == 'a' and d_v:
920 if l_type == 'a' and d_v:
921 #case when added and deleted are present
921 #case when added and deleted are present
922 return ' '.join(map(map_getter, ['tl', 'bl']))
922 return ' '.join(map(map_getter, ['tl', 'bl']))
923
923
924 if l_type == 'a' and not d_v:
924 if l_type == 'a' and not d_v:
925 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
925 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
926
926
927 if l_type == 'd' and a_v:
927 if l_type == 'd' and a_v:
928 return ' '.join(map(map_getter, ['tr', 'br']))
928 return ' '.join(map(map_getter, ['tr', 'br']))
929
929
930 if l_type == 'd' and not a_v:
930 if l_type == 'd' and not a_v:
931 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
931 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
932
932
933 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
933 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
934 cgen('a'), a_p, a_v
934 cgen('a'), a_p, a_v
935 )
935 )
936 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
936 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
937 cgen('d'), d_p, d_v
937 cgen('d'), d_p, d_v
938 )
938 )
939 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
939 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
940
940
941
941
942 def urlify_text(text_):
942 def urlify_text(text_):
943 import re
944
943
945 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
944 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
946 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
945 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
947
946
948 def url_func(match_obj):
947 def url_func(match_obj):
949 url_full = match_obj.groups()[0]
948 url_full = match_obj.groups()[0]
950 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
949 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
951
950
952 return literal(url_pat.sub(url_func, text_))
951 return literal(url_pat.sub(url_func, text_))
953
952
954
953
955 def urlify_changesets(text_, repository):
954 def urlify_changesets(text_, repository):
956 """
955 """
957 Extract revision ids from changeset and make link from them
956 Extract revision ids from changeset and make link from them
958
957
959 :param text_:
958 :param text_:
960 :param repository:
959 :param repository:
961 """
960 """
962 import re
961
963 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
962 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
964
963
965 def url_func(match_obj):
964 def url_func(match_obj):
966 rev = match_obj.groups()[0]
965 rev = match_obj.groups()[0]
967 pref = ''
966 pref = ''
968 if match_obj.group().startswith(' '):
967 if match_obj.group().startswith(' '):
969 pref = ' '
968 pref = ' '
970 tmpl = (
969 tmpl = (
971 '%(pref)s<a class="%(cls)s" href="%(url)s">'
970 '%(pref)s<a class="%(cls)s" href="%(url)s">'
972 '%(rev)s'
971 '%(rev)s'
973 '</a>'
972 '</a>'
974 )
973 )
975 return tmpl % {
974 return tmpl % {
976 'pref': pref,
975 'pref': pref,
977 'cls': 'revision-link',
976 'cls': 'revision-link',
978 'url': url('changeset_home', repo_name=repository, revision=rev),
977 'url': url('changeset_home', repo_name=repository, revision=rev),
979 'rev': rev,
978 'rev': rev,
980 }
979 }
981
980
982 newtext = URL_PAT.sub(url_func, text_)
981 newtext = URL_PAT.sub(url_func, text_)
983
982
984 return newtext
983 return newtext
985
984
986
985
987 def urlify_commit(text_, repository=None, link_=None):
986 def urlify_commit(text_, repository=None, link_=None):
988 """
987 """
989 Parses given text message and makes proper links.
988 Parses given text message and makes proper links.
990 issues are linked to given issue-server, and rest is a changeset link
989 issues are linked to given issue-server, and rest is a changeset link
991 if link_ is given, in other case it's a plain text
990 if link_ is given, in other case it's a plain text
992
991
993 :param text_:
992 :param text_:
994 :param repository:
993 :param repository:
995 :param link_: changeset link
994 :param link_: changeset link
996 """
995 """
997 import re
998 import traceback
996 import traceback
999
997
1000 def escaper(string):
998 def escaper(string):
1001 return string.replace('<', '&lt;').replace('>', '&gt;')
999 return string.replace('<', '&lt;').replace('>', '&gt;')
1002
1000
1003 def linkify_others(t, l):
1001 def linkify_others(t, l):
1004 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1002 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1005 links = []
1003 links = []
1006 for e in urls.split(t):
1004 for e in urls.split(t):
1007 if not urls.match(e):
1005 if not urls.match(e):
1008 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1006 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1009 else:
1007 else:
1010 links.append(e)
1008 links.append(e)
1011
1009
1012 return ''.join(links)
1010 return ''.join(links)
1013
1011
1014 # urlify changesets - extrac revisions and make link out of them
1012 # urlify changesets - extrac revisions and make link out of them
1015 newtext = urlify_changesets(escaper(text_), repository)
1013 newtext = urlify_changesets(escaper(text_), repository)
1016
1014
1017 try:
1015 try:
1018 conf = config['app_conf']
1016 conf = config['app_conf']
1019
1017
1020 # allow multiple issue servers to be used
1018 # allow multiple issue servers to be used
1021 valid_indices = [
1019 valid_indices = [
1022 x.group(1)
1020 x.group(1)
1023 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1021 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1024 if x and 'issue_server_link%s' % x.group(1) in conf
1022 if x and 'issue_server_link%s' % x.group(1) in conf
1025 and 'issue_prefix%s' % x.group(1) in conf
1023 and 'issue_prefix%s' % x.group(1) in conf
1026 ]
1024 ]
1027
1025
1028 log.debug('found issue server suffixes `%s` during valuation of: %s'
1026 log.debug('found issue server suffixes `%s` during valuation of: %s'
1029 % (','.join(valid_indices), newtext))
1027 % (','.join(valid_indices), newtext))
1030
1028
1031 for pattern_index in valid_indices:
1029 for pattern_index in valid_indices:
1032 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1030 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1033 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1031 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1034 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1032 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1035
1033
1036 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1034 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1037 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1035 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1038 ISSUE_PREFIX))
1036 ISSUE_PREFIX))
1039
1037
1040 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1038 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1041
1039
1042 def url_func(match_obj):
1040 def url_func(match_obj):
1043 pref = ''
1041 pref = ''
1044 if match_obj.group().startswith(' '):
1042 if match_obj.group().startswith(' '):
1045 pref = ' '
1043 pref = ' '
1046
1044
1047 issue_id = ''.join(match_obj.groups())
1045 issue_id = ''.join(match_obj.groups())
1048 tmpl = (
1046 tmpl = (
1049 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1047 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1050 '%(issue-prefix)s%(id-repr)s'
1048 '%(issue-prefix)s%(id-repr)s'
1051 '</a>'
1049 '</a>'
1052 )
1050 )
1053 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1051 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1054 if repository:
1052 if repository:
1055 url = url.replace('{repo}', repository)
1053 url = url.replace('{repo}', repository)
1056 repo_name = repository.split(URL_SEP)[-1]
1054 repo_name = repository.split(URL_SEP)[-1]
1057 url = url.replace('{repo_name}', repo_name)
1055 url = url.replace('{repo_name}', repo_name)
1058
1056
1059 return tmpl % {
1057 return tmpl % {
1060 'pref': pref,
1058 'pref': pref,
1061 'cls': 'issue-tracker-link',
1059 'cls': 'issue-tracker-link',
1062 'url': url,
1060 'url': url,
1063 'id-repr': issue_id,
1061 'id-repr': issue_id,
1064 'issue-prefix': ISSUE_PREFIX,
1062 'issue-prefix': ISSUE_PREFIX,
1065 'serv': ISSUE_SERVER_LNK,
1063 'serv': ISSUE_SERVER_LNK,
1066 }
1064 }
1067 newtext = URL_PAT.sub(url_func, newtext)
1065 newtext = URL_PAT.sub(url_func, newtext)
1068 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1066 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1069
1067
1070 # if we actually did something above
1068 # if we actually did something above
1071 if link_:
1069 if link_:
1072 # wrap not links into final link => link_
1070 # wrap not links into final link => link_
1073 newtext = linkify_others(newtext, link_)
1071 newtext = linkify_others(newtext, link_)
1074
1075 return literal(newtext)
1076 except:
1072 except:
1077 log.error(traceback.format_exc())
1073 log.error(traceback.format_exc())
1078 pass
1074 pass
1079
1075
1080 return newtext
1076 return literal(newtext)
1081
1077
1082
1078
1083 def rst(source):
1079 def rst(source):
1084 return literal('<div class="rst-block">%s</div>' %
1080 return literal('<div class="rst-block">%s</div>' %
1085 MarkupRenderer.rst(source))
1081 MarkupRenderer.rst(source))
1086
1082
1087
1083
1088 def rst_w_mentions(source):
1084 def rst_w_mentions(source):
1089 """
1085 """
1090 Wrapped rst renderer with @mention highlighting
1086 Wrapped rst renderer with @mention highlighting
1091
1087
1092 :param source:
1088 :param source:
1093 """
1089 """
1094 return literal('<div class="rst-block">%s</div>' %
1090 return literal('<div class="rst-block">%s</div>' %
1095 MarkupRenderer.rst_with_mentions(source))
1091 MarkupRenderer.rst_with_mentions(source))
1096
1092
1097
1093
1098 def changeset_status(repo, revision):
1094 def changeset_status(repo, revision):
1099 return ChangesetStatusModel().get_status(repo, revision)
1095 return ChangesetStatusModel().get_status(repo, revision)
1100
1096
1101
1097
1102 def changeset_status_lbl(changeset_status):
1098 def changeset_status_lbl(changeset_status):
1103 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1099 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1104
1100
1105
1101
1106 def get_permission_name(key):
1102 def get_permission_name(key):
1107 return dict(Permission.PERMS).get(key)
1103 return dict(Permission.PERMS).get(key)
General Comments 0
You need to be logged in to leave comments. Login now