##// END OF EJS Templates
helpers: fix tzinfo can be undefined if wrong object is passed.
marcink -
r1352:7f65a6d5 default
parent child Browse files
Show More
@@ -1,2004 +1,2003 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27
27
28 import random
28 import random
29 import hashlib
29 import hashlib
30 import StringIO
30 import StringIO
31 import urllib
31 import urllib
32 import math
32 import math
33 import logging
33 import logging
34 import re
34 import re
35 import urlparse
35 import urlparse
36 import time
36 import time
37 import string
37 import string
38 import hashlib
38 import hashlib
39 import pygments
39 import pygments
40
40
41 from datetime import datetime
41 from datetime import datetime
42 from functools import partial
42 from functools import partial
43 from pygments.formatters.html import HtmlFormatter
43 from pygments.formatters.html import HtmlFormatter
44 from pygments import highlight as code_highlight
44 from pygments import highlight as code_highlight
45 from pygments.lexers import (
45 from pygments.lexers import (
46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
47 from pylons import url as pylons_url
47 from pylons import url as pylons_url
48 from pylons.i18n.translation import _, ungettext
48 from pylons.i18n.translation import _, ungettext
49 from pyramid.threadlocal import get_current_request
49 from pyramid.threadlocal import get_current_request
50
50
51 from webhelpers.html import literal, HTML, escape
51 from webhelpers.html import literal, HTML, escape
52 from webhelpers.html.tools import *
52 from webhelpers.html.tools import *
53 from webhelpers.html.builder import make_tag
53 from webhelpers.html.builder import make_tag
54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
57 submit, text, password, textarea, title, ul, xml_declaration, radio
57 submit, text, password, textarea, title, ul, xml_declaration, radio
58 from webhelpers.html.tools import auto_link, button_to, highlight, \
58 from webhelpers.html.tools import auto_link, button_to, highlight, \
59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
60 from webhelpers.pylonslib import Flash as _Flash
60 from webhelpers.pylonslib import Flash as _Flash
61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
63 replace_whitespace, urlify, truncate, wrap_paragraphs
63 replace_whitespace, urlify, truncate, wrap_paragraphs
64 from webhelpers.date import time_ago_in_words
64 from webhelpers.date import time_ago_in_words
65 from webhelpers.paginate import Page as _Page
65 from webhelpers.paginate import Page as _Page
66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
67 convert_boolean_attrs, NotGiven, _make_safe_id_component
67 convert_boolean_attrs, NotGiven, _make_safe_id_component
68 from webhelpers2.number import format_byte_size
68 from webhelpers2.number import format_byte_size
69
69
70 from rhodecode.lib.action_parser import action_parser
70 from rhodecode.lib.action_parser import action_parser
71 from rhodecode.lib.ext_json import json
71 from rhodecode.lib.ext_json import json
72 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
72 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
73 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
73 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
74 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
74 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
75 AttributeDict, safe_int, md5, md5_safe
75 AttributeDict, safe_int, md5, md5_safe
76 from rhodecode.lib.markup_renderer import MarkupRenderer
76 from rhodecode.lib.markup_renderer import MarkupRenderer
77 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
77 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
78 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
78 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
79 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
79 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
80 from rhodecode.model.changeset_status import ChangesetStatusModel
80 from rhodecode.model.changeset_status import ChangesetStatusModel
81 from rhodecode.model.db import Permission, User, Repository
81 from rhodecode.model.db import Permission, User, Repository
82 from rhodecode.model.repo_group import RepoGroupModel
82 from rhodecode.model.repo_group import RepoGroupModel
83 from rhodecode.model.settings import IssueTrackerSettingsModel
83 from rhodecode.model.settings import IssueTrackerSettingsModel
84
84
85 log = logging.getLogger(__name__)
85 log = logging.getLogger(__name__)
86
86
87
87
88 DEFAULT_USER = User.DEFAULT_USER
88 DEFAULT_USER = User.DEFAULT_USER
89 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
89 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
90
90
91
91
92 def url(*args, **kw):
92 def url(*args, **kw):
93 return pylons_url(*args, **kw)
93 return pylons_url(*args, **kw)
94
94
95
95
96 def pylons_url_current(*args, **kw):
96 def pylons_url_current(*args, **kw):
97 """
97 """
98 This function overrides pylons.url.current() which returns the current
98 This function overrides pylons.url.current() which returns the current
99 path so that it will also work from a pyramid only context. This
99 path so that it will also work from a pyramid only context. This
100 should be removed once port to pyramid is complete.
100 should be removed once port to pyramid is complete.
101 """
101 """
102 if not args and not kw:
102 if not args and not kw:
103 request = get_current_request()
103 request = get_current_request()
104 return request.path
104 return request.path
105 return pylons_url.current(*args, **kw)
105 return pylons_url.current(*args, **kw)
106
106
107 url.current = pylons_url_current
107 url.current = pylons_url_current
108
108
109
109
110 def url_replace(**qargs):
110 def url_replace(**qargs):
111 """ Returns the current request url while replacing query string args """
111 """ Returns the current request url while replacing query string args """
112
112
113 request = get_current_request()
113 request = get_current_request()
114 new_args = request.GET.mixed()
114 new_args = request.GET.mixed()
115 new_args.update(qargs)
115 new_args.update(qargs)
116 return url('', **new_args)
116 return url('', **new_args)
117
117
118
118
119 def asset(path, ver=None):
119 def asset(path, ver=None):
120 """
120 """
121 Helper to generate a static asset file path for rhodecode assets
121 Helper to generate a static asset file path for rhodecode assets
122
122
123 eg. h.asset('images/image.png', ver='3923')
123 eg. h.asset('images/image.png', ver='3923')
124
124
125 :param path: path of asset
125 :param path: path of asset
126 :param ver: optional version query param to append as ?ver=
126 :param ver: optional version query param to append as ?ver=
127 """
127 """
128 request = get_current_request()
128 request = get_current_request()
129 query = {}
129 query = {}
130 if ver:
130 if ver:
131 query = {'ver': ver}
131 query = {'ver': ver}
132 return request.static_path(
132 return request.static_path(
133 'rhodecode:public/{}'.format(path), _query=query)
133 'rhodecode:public/{}'.format(path), _query=query)
134
134
135
135
136 default_html_escape_table = {
136 default_html_escape_table = {
137 ord('&'): u'&amp;',
137 ord('&'): u'&amp;',
138 ord('<'): u'&lt;',
138 ord('<'): u'&lt;',
139 ord('>'): u'&gt;',
139 ord('>'): u'&gt;',
140 ord('"'): u'&quot;',
140 ord('"'): u'&quot;',
141 ord("'"): u'&#39;',
141 ord("'"): u'&#39;',
142 }
142 }
143
143
144
144
145 def html_escape(text, html_escape_table=default_html_escape_table):
145 def html_escape(text, html_escape_table=default_html_escape_table):
146 """Produce entities within text."""
146 """Produce entities within text."""
147 return text.translate(html_escape_table)
147 return text.translate(html_escape_table)
148
148
149
149
150 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
150 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
151 """
151 """
152 Truncate string ``s`` at the first occurrence of ``sub``.
152 Truncate string ``s`` at the first occurrence of ``sub``.
153
153
154 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
154 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
155 """
155 """
156 suffix_if_chopped = suffix_if_chopped or ''
156 suffix_if_chopped = suffix_if_chopped or ''
157 pos = s.find(sub)
157 pos = s.find(sub)
158 if pos == -1:
158 if pos == -1:
159 return s
159 return s
160
160
161 if inclusive:
161 if inclusive:
162 pos += len(sub)
162 pos += len(sub)
163
163
164 chopped = s[:pos]
164 chopped = s[:pos]
165 left = s[pos:].strip()
165 left = s[pos:].strip()
166
166
167 if left and suffix_if_chopped:
167 if left and suffix_if_chopped:
168 chopped += suffix_if_chopped
168 chopped += suffix_if_chopped
169
169
170 return chopped
170 return chopped
171
171
172
172
173 def shorter(text, size=20):
173 def shorter(text, size=20):
174 postfix = '...'
174 postfix = '...'
175 if len(text) > size:
175 if len(text) > size:
176 return text[:size - len(postfix)] + postfix
176 return text[:size - len(postfix)] + postfix
177 return text
177 return text
178
178
179
179
180 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
180 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
181 """
181 """
182 Reset button
182 Reset button
183 """
183 """
184 _set_input_attrs(attrs, type, name, value)
184 _set_input_attrs(attrs, type, name, value)
185 _set_id_attr(attrs, id, name)
185 _set_id_attr(attrs, id, name)
186 convert_boolean_attrs(attrs, ["disabled"])
186 convert_boolean_attrs(attrs, ["disabled"])
187 return HTML.input(**attrs)
187 return HTML.input(**attrs)
188
188
189 reset = _reset
189 reset = _reset
190 safeid = _make_safe_id_component
190 safeid = _make_safe_id_component
191
191
192
192
193 def branding(name, length=40):
193 def branding(name, length=40):
194 return truncate(name, length, indicator="")
194 return truncate(name, length, indicator="")
195
195
196
196
197 def FID(raw_id, path):
197 def FID(raw_id, path):
198 """
198 """
199 Creates a unique ID for filenode based on it's hash of path and commit
199 Creates a unique ID for filenode based on it's hash of path and commit
200 it's safe to use in urls
200 it's safe to use in urls
201
201
202 :param raw_id:
202 :param raw_id:
203 :param path:
203 :param path:
204 """
204 """
205
205
206 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
206 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
207
207
208
208
209 class _GetError(object):
209 class _GetError(object):
210 """Get error from form_errors, and represent it as span wrapped error
210 """Get error from form_errors, and represent it as span wrapped error
211 message
211 message
212
212
213 :param field_name: field to fetch errors for
213 :param field_name: field to fetch errors for
214 :param form_errors: form errors dict
214 :param form_errors: form errors dict
215 """
215 """
216
216
217 def __call__(self, field_name, form_errors):
217 def __call__(self, field_name, form_errors):
218 tmpl = """<span class="error_msg">%s</span>"""
218 tmpl = """<span class="error_msg">%s</span>"""
219 if form_errors and field_name in form_errors:
219 if form_errors and field_name in form_errors:
220 return literal(tmpl % form_errors.get(field_name))
220 return literal(tmpl % form_errors.get(field_name))
221
221
222 get_error = _GetError()
222 get_error = _GetError()
223
223
224
224
225 class _ToolTip(object):
225 class _ToolTip(object):
226
226
227 def __call__(self, tooltip_title, trim_at=50):
227 def __call__(self, tooltip_title, trim_at=50):
228 """
228 """
229 Special function just to wrap our text into nice formatted
229 Special function just to wrap our text into nice formatted
230 autowrapped text
230 autowrapped text
231
231
232 :param tooltip_title:
232 :param tooltip_title:
233 """
233 """
234 tooltip_title = escape(tooltip_title)
234 tooltip_title = escape(tooltip_title)
235 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
235 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
236 return tooltip_title
236 return tooltip_title
237 tooltip = _ToolTip()
237 tooltip = _ToolTip()
238
238
239
239
240 def files_breadcrumbs(repo_name, commit_id, file_path):
240 def files_breadcrumbs(repo_name, commit_id, file_path):
241 if isinstance(file_path, str):
241 if isinstance(file_path, str):
242 file_path = safe_unicode(file_path)
242 file_path = safe_unicode(file_path)
243
243
244 # TODO: johbo: Is this always a url like path, or is this operating
244 # TODO: johbo: Is this always a url like path, or is this operating
245 # system dependent?
245 # system dependent?
246 path_segments = file_path.split('/')
246 path_segments = file_path.split('/')
247
247
248 repo_name_html = escape(repo_name)
248 repo_name_html = escape(repo_name)
249 if len(path_segments) == 1 and path_segments[0] == '':
249 if len(path_segments) == 1 and path_segments[0] == '':
250 url_segments = [repo_name_html]
250 url_segments = [repo_name_html]
251 else:
251 else:
252 url_segments = [
252 url_segments = [
253 link_to(
253 link_to(
254 repo_name_html,
254 repo_name_html,
255 url('files_home',
255 url('files_home',
256 repo_name=repo_name,
256 repo_name=repo_name,
257 revision=commit_id,
257 revision=commit_id,
258 f_path=''),
258 f_path=''),
259 class_='pjax-link')]
259 class_='pjax-link')]
260
260
261 last_cnt = len(path_segments) - 1
261 last_cnt = len(path_segments) - 1
262 for cnt, segment in enumerate(path_segments):
262 for cnt, segment in enumerate(path_segments):
263 if not segment:
263 if not segment:
264 continue
264 continue
265 segment_html = escape(segment)
265 segment_html = escape(segment)
266
266
267 if cnt != last_cnt:
267 if cnt != last_cnt:
268 url_segments.append(
268 url_segments.append(
269 link_to(
269 link_to(
270 segment_html,
270 segment_html,
271 url('files_home',
271 url('files_home',
272 repo_name=repo_name,
272 repo_name=repo_name,
273 revision=commit_id,
273 revision=commit_id,
274 f_path='/'.join(path_segments[:cnt + 1])),
274 f_path='/'.join(path_segments[:cnt + 1])),
275 class_='pjax-link'))
275 class_='pjax-link'))
276 else:
276 else:
277 url_segments.append(segment_html)
277 url_segments.append(segment_html)
278
278
279 return literal('/'.join(url_segments))
279 return literal('/'.join(url_segments))
280
280
281
281
282 class CodeHtmlFormatter(HtmlFormatter):
282 class CodeHtmlFormatter(HtmlFormatter):
283 """
283 """
284 My code Html Formatter for source codes
284 My code Html Formatter for source codes
285 """
285 """
286
286
287 def wrap(self, source, outfile):
287 def wrap(self, source, outfile):
288 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
288 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
289
289
290 def _wrap_code(self, source):
290 def _wrap_code(self, source):
291 for cnt, it in enumerate(source):
291 for cnt, it in enumerate(source):
292 i, t = it
292 i, t = it
293 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
293 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
294 yield i, t
294 yield i, t
295
295
296 def _wrap_tablelinenos(self, inner):
296 def _wrap_tablelinenos(self, inner):
297 dummyoutfile = StringIO.StringIO()
297 dummyoutfile = StringIO.StringIO()
298 lncount = 0
298 lncount = 0
299 for t, line in inner:
299 for t, line in inner:
300 if t:
300 if t:
301 lncount += 1
301 lncount += 1
302 dummyoutfile.write(line)
302 dummyoutfile.write(line)
303
303
304 fl = self.linenostart
304 fl = self.linenostart
305 mw = len(str(lncount + fl - 1))
305 mw = len(str(lncount + fl - 1))
306 sp = self.linenospecial
306 sp = self.linenospecial
307 st = self.linenostep
307 st = self.linenostep
308 la = self.lineanchors
308 la = self.lineanchors
309 aln = self.anchorlinenos
309 aln = self.anchorlinenos
310 nocls = self.noclasses
310 nocls = self.noclasses
311 if sp:
311 if sp:
312 lines = []
312 lines = []
313
313
314 for i in range(fl, fl + lncount):
314 for i in range(fl, fl + lncount):
315 if i % st == 0:
315 if i % st == 0:
316 if i % sp == 0:
316 if i % sp == 0:
317 if aln:
317 if aln:
318 lines.append('<a href="#%s%d" class="special">%*d</a>' %
318 lines.append('<a href="#%s%d" class="special">%*d</a>' %
319 (la, i, mw, i))
319 (la, i, mw, i))
320 else:
320 else:
321 lines.append('<span class="special">%*d</span>' % (mw, i))
321 lines.append('<span class="special">%*d</span>' % (mw, i))
322 else:
322 else:
323 if aln:
323 if aln:
324 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
324 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
325 else:
325 else:
326 lines.append('%*d' % (mw, i))
326 lines.append('%*d' % (mw, i))
327 else:
327 else:
328 lines.append('')
328 lines.append('')
329 ls = '\n'.join(lines)
329 ls = '\n'.join(lines)
330 else:
330 else:
331 lines = []
331 lines = []
332 for i in range(fl, fl + lncount):
332 for i in range(fl, fl + lncount):
333 if i % st == 0:
333 if i % st == 0:
334 if aln:
334 if aln:
335 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
335 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
336 else:
336 else:
337 lines.append('%*d' % (mw, i))
337 lines.append('%*d' % (mw, i))
338 else:
338 else:
339 lines.append('')
339 lines.append('')
340 ls = '\n'.join(lines)
340 ls = '\n'.join(lines)
341
341
342 # in case you wonder about the seemingly redundant <div> here: since the
342 # in case you wonder about the seemingly redundant <div> here: since the
343 # content in the other cell also is wrapped in a div, some browsers in
343 # content in the other cell also is wrapped in a div, some browsers in
344 # some configurations seem to mess up the formatting...
344 # some configurations seem to mess up the formatting...
345 if nocls:
345 if nocls:
346 yield 0, ('<table class="%stable">' % self.cssclass +
346 yield 0, ('<table class="%stable">' % self.cssclass +
347 '<tr><td><div class="linenodiv" '
347 '<tr><td><div class="linenodiv" '
348 'style="background-color: #f0f0f0; padding-right: 10px">'
348 'style="background-color: #f0f0f0; padding-right: 10px">'
349 '<pre style="line-height: 125%">' +
349 '<pre style="line-height: 125%">' +
350 ls + '</pre></div></td><td id="hlcode" class="code">')
350 ls + '</pre></div></td><td id="hlcode" class="code">')
351 else:
351 else:
352 yield 0, ('<table class="%stable">' % self.cssclass +
352 yield 0, ('<table class="%stable">' % self.cssclass +
353 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
353 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
354 ls + '</pre></div></td><td id="hlcode" class="code">')
354 ls + '</pre></div></td><td id="hlcode" class="code">')
355 yield 0, dummyoutfile.getvalue()
355 yield 0, dummyoutfile.getvalue()
356 yield 0, '</td></tr></table>'
356 yield 0, '</td></tr></table>'
357
357
358
358
359 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
359 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
360 def __init__(self, **kw):
360 def __init__(self, **kw):
361 # only show these line numbers if set
361 # only show these line numbers if set
362 self.only_lines = kw.pop('only_line_numbers', [])
362 self.only_lines = kw.pop('only_line_numbers', [])
363 self.query_terms = kw.pop('query_terms', [])
363 self.query_terms = kw.pop('query_terms', [])
364 self.max_lines = kw.pop('max_lines', 5)
364 self.max_lines = kw.pop('max_lines', 5)
365 self.line_context = kw.pop('line_context', 3)
365 self.line_context = kw.pop('line_context', 3)
366 self.url = kw.pop('url', None)
366 self.url = kw.pop('url', None)
367
367
368 super(CodeHtmlFormatter, self).__init__(**kw)
368 super(CodeHtmlFormatter, self).__init__(**kw)
369
369
370 def _wrap_code(self, source):
370 def _wrap_code(self, source):
371 for cnt, it in enumerate(source):
371 for cnt, it in enumerate(source):
372 i, t = it
372 i, t = it
373 t = '<pre>%s</pre>' % t
373 t = '<pre>%s</pre>' % t
374 yield i, t
374 yield i, t
375
375
376 def _wrap_tablelinenos(self, inner):
376 def _wrap_tablelinenos(self, inner):
377 yield 0, '<table class="code-highlight %stable">' % self.cssclass
377 yield 0, '<table class="code-highlight %stable">' % self.cssclass
378
378
379 last_shown_line_number = 0
379 last_shown_line_number = 0
380 current_line_number = 1
380 current_line_number = 1
381
381
382 for t, line in inner:
382 for t, line in inner:
383 if not t:
383 if not t:
384 yield t, line
384 yield t, line
385 continue
385 continue
386
386
387 if current_line_number in self.only_lines:
387 if current_line_number in self.only_lines:
388 if last_shown_line_number + 1 != current_line_number:
388 if last_shown_line_number + 1 != current_line_number:
389 yield 0, '<tr>'
389 yield 0, '<tr>'
390 yield 0, '<td class="line">...</td>'
390 yield 0, '<td class="line">...</td>'
391 yield 0, '<td id="hlcode" class="code"></td>'
391 yield 0, '<td id="hlcode" class="code"></td>'
392 yield 0, '</tr>'
392 yield 0, '</tr>'
393
393
394 yield 0, '<tr>'
394 yield 0, '<tr>'
395 if self.url:
395 if self.url:
396 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
396 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
397 self.url, current_line_number, current_line_number)
397 self.url, current_line_number, current_line_number)
398 else:
398 else:
399 yield 0, '<td class="line"><a href="">%i</a></td>' % (
399 yield 0, '<td class="line"><a href="">%i</a></td>' % (
400 current_line_number)
400 current_line_number)
401 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
401 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
402 yield 0, '</tr>'
402 yield 0, '</tr>'
403
403
404 last_shown_line_number = current_line_number
404 last_shown_line_number = current_line_number
405
405
406 current_line_number += 1
406 current_line_number += 1
407
407
408
408
409 yield 0, '</table>'
409 yield 0, '</table>'
410
410
411
411
412 def extract_phrases(text_query):
412 def extract_phrases(text_query):
413 """
413 """
414 Extracts phrases from search term string making sure phrases
414 Extracts phrases from search term string making sure phrases
415 contained in double quotes are kept together - and discarding empty values
415 contained in double quotes are kept together - and discarding empty values
416 or fully whitespace values eg.
416 or fully whitespace values eg.
417
417
418 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
418 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
419
419
420 """
420 """
421
421
422 in_phrase = False
422 in_phrase = False
423 buf = ''
423 buf = ''
424 phrases = []
424 phrases = []
425 for char in text_query:
425 for char in text_query:
426 if in_phrase:
426 if in_phrase:
427 if char == '"': # end phrase
427 if char == '"': # end phrase
428 phrases.append(buf)
428 phrases.append(buf)
429 buf = ''
429 buf = ''
430 in_phrase = False
430 in_phrase = False
431 continue
431 continue
432 else:
432 else:
433 buf += char
433 buf += char
434 continue
434 continue
435 else:
435 else:
436 if char == '"': # start phrase
436 if char == '"': # start phrase
437 in_phrase = True
437 in_phrase = True
438 phrases.append(buf)
438 phrases.append(buf)
439 buf = ''
439 buf = ''
440 continue
440 continue
441 elif char == ' ':
441 elif char == ' ':
442 phrases.append(buf)
442 phrases.append(buf)
443 buf = ''
443 buf = ''
444 continue
444 continue
445 else:
445 else:
446 buf += char
446 buf += char
447
447
448 phrases.append(buf)
448 phrases.append(buf)
449 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
449 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
450 return phrases
450 return phrases
451
451
452
452
453 def get_matching_offsets(text, phrases):
453 def get_matching_offsets(text, phrases):
454 """
454 """
455 Returns a list of string offsets in `text` that the list of `terms` match
455 Returns a list of string offsets in `text` that the list of `terms` match
456
456
457 >>> get_matching_offsets('some text here', ['some', 'here'])
457 >>> get_matching_offsets('some text here', ['some', 'here'])
458 [(0, 4), (10, 14)]
458 [(0, 4), (10, 14)]
459
459
460 """
460 """
461 offsets = []
461 offsets = []
462 for phrase in phrases:
462 for phrase in phrases:
463 for match in re.finditer(phrase, text):
463 for match in re.finditer(phrase, text):
464 offsets.append((match.start(), match.end()))
464 offsets.append((match.start(), match.end()))
465
465
466 return offsets
466 return offsets
467
467
468
468
469 def normalize_text_for_matching(x):
469 def normalize_text_for_matching(x):
470 """
470 """
471 Replaces all non alnum characters to spaces and lower cases the string,
471 Replaces all non alnum characters to spaces and lower cases the string,
472 useful for comparing two text strings without punctuation
472 useful for comparing two text strings without punctuation
473 """
473 """
474 return re.sub(r'[^\w]', ' ', x.lower())
474 return re.sub(r'[^\w]', ' ', x.lower())
475
475
476
476
477 def get_matching_line_offsets(lines, terms):
477 def get_matching_line_offsets(lines, terms):
478 """ Return a set of `lines` indices (starting from 1) matching a
478 """ Return a set of `lines` indices (starting from 1) matching a
479 text search query, along with `context` lines above/below matching lines
479 text search query, along with `context` lines above/below matching lines
480
480
481 :param lines: list of strings representing lines
481 :param lines: list of strings representing lines
482 :param terms: search term string to match in lines eg. 'some text'
482 :param terms: search term string to match in lines eg. 'some text'
483 :param context: number of lines above/below a matching line to add to result
483 :param context: number of lines above/below a matching line to add to result
484 :param max_lines: cut off for lines of interest
484 :param max_lines: cut off for lines of interest
485 eg.
485 eg.
486
486
487 text = '''
487 text = '''
488 words words words
488 words words words
489 words words words
489 words words words
490 some text some
490 some text some
491 words words words
491 words words words
492 words words words
492 words words words
493 text here what
493 text here what
494 '''
494 '''
495 get_matching_line_offsets(text, 'text', context=1)
495 get_matching_line_offsets(text, 'text', context=1)
496 {3: [(5, 9)], 6: [(0, 4)]]
496 {3: [(5, 9)], 6: [(0, 4)]]
497
497
498 """
498 """
499 matching_lines = {}
499 matching_lines = {}
500 phrases = [normalize_text_for_matching(phrase)
500 phrases = [normalize_text_for_matching(phrase)
501 for phrase in extract_phrases(terms)]
501 for phrase in extract_phrases(terms)]
502
502
503 for line_index, line in enumerate(lines, start=1):
503 for line_index, line in enumerate(lines, start=1):
504 match_offsets = get_matching_offsets(
504 match_offsets = get_matching_offsets(
505 normalize_text_for_matching(line), phrases)
505 normalize_text_for_matching(line), phrases)
506 if match_offsets:
506 if match_offsets:
507 matching_lines[line_index] = match_offsets
507 matching_lines[line_index] = match_offsets
508
508
509 return matching_lines
509 return matching_lines
510
510
511
511
512 def hsv_to_rgb(h, s, v):
512 def hsv_to_rgb(h, s, v):
513 """ Convert hsv color values to rgb """
513 """ Convert hsv color values to rgb """
514
514
515 if s == 0.0:
515 if s == 0.0:
516 return v, v, v
516 return v, v, v
517 i = int(h * 6.0) # XXX assume int() truncates!
517 i = int(h * 6.0) # XXX assume int() truncates!
518 f = (h * 6.0) - i
518 f = (h * 6.0) - i
519 p = v * (1.0 - s)
519 p = v * (1.0 - s)
520 q = v * (1.0 - s * f)
520 q = v * (1.0 - s * f)
521 t = v * (1.0 - s * (1.0 - f))
521 t = v * (1.0 - s * (1.0 - f))
522 i = i % 6
522 i = i % 6
523 if i == 0:
523 if i == 0:
524 return v, t, p
524 return v, t, p
525 if i == 1:
525 if i == 1:
526 return q, v, p
526 return q, v, p
527 if i == 2:
527 if i == 2:
528 return p, v, t
528 return p, v, t
529 if i == 3:
529 if i == 3:
530 return p, q, v
530 return p, q, v
531 if i == 4:
531 if i == 4:
532 return t, p, v
532 return t, p, v
533 if i == 5:
533 if i == 5:
534 return v, p, q
534 return v, p, q
535
535
536
536
537 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
537 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
538 """
538 """
539 Generator for getting n of evenly distributed colors using
539 Generator for getting n of evenly distributed colors using
540 hsv color and golden ratio. It always return same order of colors
540 hsv color and golden ratio. It always return same order of colors
541
541
542 :param n: number of colors to generate
542 :param n: number of colors to generate
543 :param saturation: saturation of returned colors
543 :param saturation: saturation of returned colors
544 :param lightness: lightness of returned colors
544 :param lightness: lightness of returned colors
545 :returns: RGB tuple
545 :returns: RGB tuple
546 """
546 """
547
547
548 golden_ratio = 0.618033988749895
548 golden_ratio = 0.618033988749895
549 h = 0.22717784590367374
549 h = 0.22717784590367374
550
550
551 for _ in xrange(n):
551 for _ in xrange(n):
552 h += golden_ratio
552 h += golden_ratio
553 h %= 1
553 h %= 1
554 HSV_tuple = [h, saturation, lightness]
554 HSV_tuple = [h, saturation, lightness]
555 RGB_tuple = hsv_to_rgb(*HSV_tuple)
555 RGB_tuple = hsv_to_rgb(*HSV_tuple)
556 yield map(lambda x: str(int(x * 256)), RGB_tuple)
556 yield map(lambda x: str(int(x * 256)), RGB_tuple)
557
557
558
558
559 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
559 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
560 """
560 """
561 Returns a function which when called with an argument returns a unique
561 Returns a function which when called with an argument returns a unique
562 color for that argument, eg.
562 color for that argument, eg.
563
563
564 :param n: number of colors to generate
564 :param n: number of colors to generate
565 :param saturation: saturation of returned colors
565 :param saturation: saturation of returned colors
566 :param lightness: lightness of returned colors
566 :param lightness: lightness of returned colors
567 :returns: css RGB string
567 :returns: css RGB string
568
568
569 >>> color_hash = color_hasher()
569 >>> color_hash = color_hasher()
570 >>> color_hash('hello')
570 >>> color_hash('hello')
571 'rgb(34, 12, 59)'
571 'rgb(34, 12, 59)'
572 >>> color_hash('hello')
572 >>> color_hash('hello')
573 'rgb(34, 12, 59)'
573 'rgb(34, 12, 59)'
574 >>> color_hash('other')
574 >>> color_hash('other')
575 'rgb(90, 224, 159)'
575 'rgb(90, 224, 159)'
576 """
576 """
577
577
578 color_dict = {}
578 color_dict = {}
579 cgenerator = unique_color_generator(
579 cgenerator = unique_color_generator(
580 saturation=saturation, lightness=lightness)
580 saturation=saturation, lightness=lightness)
581
581
582 def get_color_string(thing):
582 def get_color_string(thing):
583 if thing in color_dict:
583 if thing in color_dict:
584 col = color_dict[thing]
584 col = color_dict[thing]
585 else:
585 else:
586 col = color_dict[thing] = cgenerator.next()
586 col = color_dict[thing] = cgenerator.next()
587 return "rgb(%s)" % (', '.join(col))
587 return "rgb(%s)" % (', '.join(col))
588
588
589 return get_color_string
589 return get_color_string
590
590
591
591
592 def get_lexer_safe(mimetype=None, filepath=None):
592 def get_lexer_safe(mimetype=None, filepath=None):
593 """
593 """
594 Tries to return a relevant pygments lexer using mimetype/filepath name,
594 Tries to return a relevant pygments lexer using mimetype/filepath name,
595 defaulting to plain text if none could be found
595 defaulting to plain text if none could be found
596 """
596 """
597 lexer = None
597 lexer = None
598 try:
598 try:
599 if mimetype:
599 if mimetype:
600 lexer = get_lexer_for_mimetype(mimetype)
600 lexer = get_lexer_for_mimetype(mimetype)
601 if not lexer:
601 if not lexer:
602 lexer = get_lexer_for_filename(filepath)
602 lexer = get_lexer_for_filename(filepath)
603 except pygments.util.ClassNotFound:
603 except pygments.util.ClassNotFound:
604 pass
604 pass
605
605
606 if not lexer:
606 if not lexer:
607 lexer = get_lexer_by_name('text')
607 lexer = get_lexer_by_name('text')
608
608
609 return lexer
609 return lexer
610
610
611
611
612 def get_lexer_for_filenode(filenode):
612 def get_lexer_for_filenode(filenode):
613 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
613 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
614 return lexer
614 return lexer
615
615
616
616
617 def pygmentize(filenode, **kwargs):
617 def pygmentize(filenode, **kwargs):
618 """
618 """
619 pygmentize function using pygments
619 pygmentize function using pygments
620
620
621 :param filenode:
621 :param filenode:
622 """
622 """
623 lexer = get_lexer_for_filenode(filenode)
623 lexer = get_lexer_for_filenode(filenode)
624 return literal(code_highlight(filenode.content, lexer,
624 return literal(code_highlight(filenode.content, lexer,
625 CodeHtmlFormatter(**kwargs)))
625 CodeHtmlFormatter(**kwargs)))
626
626
627
627
628 def is_following_repo(repo_name, user_id):
628 def is_following_repo(repo_name, user_id):
629 from rhodecode.model.scm import ScmModel
629 from rhodecode.model.scm import ScmModel
630 return ScmModel().is_following_repo(repo_name, user_id)
630 return ScmModel().is_following_repo(repo_name, user_id)
631
631
632
632
633 class _Message(object):
633 class _Message(object):
634 """A message returned by ``Flash.pop_messages()``.
634 """A message returned by ``Flash.pop_messages()``.
635
635
636 Converting the message to a string returns the message text. Instances
636 Converting the message to a string returns the message text. Instances
637 also have the following attributes:
637 also have the following attributes:
638
638
639 * ``message``: the message text.
639 * ``message``: the message text.
640 * ``category``: the category specified when the message was created.
640 * ``category``: the category specified when the message was created.
641 """
641 """
642
642
643 def __init__(self, category, message):
643 def __init__(self, category, message):
644 self.category = category
644 self.category = category
645 self.message = message
645 self.message = message
646
646
647 def __str__(self):
647 def __str__(self):
648 return self.message
648 return self.message
649
649
650 __unicode__ = __str__
650 __unicode__ = __str__
651
651
652 def __html__(self):
652 def __html__(self):
653 return escape(safe_unicode(self.message))
653 return escape(safe_unicode(self.message))
654
654
655
655
656 class Flash(_Flash):
656 class Flash(_Flash):
657
657
658 def pop_messages(self):
658 def pop_messages(self):
659 """Return all accumulated messages and delete them from the session.
659 """Return all accumulated messages and delete them from the session.
660
660
661 The return value is a list of ``Message`` objects.
661 The return value is a list of ``Message`` objects.
662 """
662 """
663 from pylons import session
663 from pylons import session
664
664
665 messages = []
665 messages = []
666
666
667 # Pop the 'old' pylons flash messages. They are tuples of the form
667 # Pop the 'old' pylons flash messages. They are tuples of the form
668 # (category, message)
668 # (category, message)
669 for cat, msg in session.pop(self.session_key, []):
669 for cat, msg in session.pop(self.session_key, []):
670 messages.append(_Message(cat, msg))
670 messages.append(_Message(cat, msg))
671
671
672 # Pop the 'new' pyramid flash messages for each category as list
672 # Pop the 'new' pyramid flash messages for each category as list
673 # of strings.
673 # of strings.
674 for cat in self.categories:
674 for cat in self.categories:
675 for msg in session.pop_flash(queue=cat):
675 for msg in session.pop_flash(queue=cat):
676 messages.append(_Message(cat, msg))
676 messages.append(_Message(cat, msg))
677 # Map messages from the default queue to the 'notice' category.
677 # Map messages from the default queue to the 'notice' category.
678 for msg in session.pop_flash():
678 for msg in session.pop_flash():
679 messages.append(_Message('notice', msg))
679 messages.append(_Message('notice', msg))
680
680
681 session.save()
681 session.save()
682 return messages
682 return messages
683
683
684 def json_alerts(self):
684 def json_alerts(self):
685 payloads = []
685 payloads = []
686 messages = flash.pop_messages()
686 messages = flash.pop_messages()
687 if messages:
687 if messages:
688 for message in messages:
688 for message in messages:
689 subdata = {}
689 subdata = {}
690 if hasattr(message.message, 'rsplit'):
690 if hasattr(message.message, 'rsplit'):
691 flash_data = message.message.rsplit('|DELIM|', 1)
691 flash_data = message.message.rsplit('|DELIM|', 1)
692 org_message = flash_data[0]
692 org_message = flash_data[0]
693 if len(flash_data) > 1:
693 if len(flash_data) > 1:
694 subdata = json.loads(flash_data[1])
694 subdata = json.loads(flash_data[1])
695 else:
695 else:
696 org_message = message.message
696 org_message = message.message
697 payloads.append({
697 payloads.append({
698 'message': {
698 'message': {
699 'message': u'{}'.format(org_message),
699 'message': u'{}'.format(org_message),
700 'level': message.category,
700 'level': message.category,
701 'force': True,
701 'force': True,
702 'subdata': subdata
702 'subdata': subdata
703 }
703 }
704 })
704 })
705 return json.dumps(payloads)
705 return json.dumps(payloads)
706
706
707 flash = Flash()
707 flash = Flash()
708
708
709 #==============================================================================
709 #==============================================================================
710 # SCM FILTERS available via h.
710 # SCM FILTERS available via h.
711 #==============================================================================
711 #==============================================================================
712 from rhodecode.lib.vcs.utils import author_name, author_email
712 from rhodecode.lib.vcs.utils import author_name, author_email
713 from rhodecode.lib.utils2 import credentials_filter, age as _age
713 from rhodecode.lib.utils2 import credentials_filter, age as _age
714 from rhodecode.model.db import User, ChangesetStatus
714 from rhodecode.model.db import User, ChangesetStatus
715
715
716 age = _age
716 age = _age
717 capitalize = lambda x: x.capitalize()
717 capitalize = lambda x: x.capitalize()
718 email = author_email
718 email = author_email
719 short_id = lambda x: x[:12]
719 short_id = lambda x: x[:12]
720 hide_credentials = lambda x: ''.join(credentials_filter(x))
720 hide_credentials = lambda x: ''.join(credentials_filter(x))
721
721
722
722
723 def age_component(datetime_iso, value=None, time_is_local=False):
723 def age_component(datetime_iso, value=None, time_is_local=False):
724 title = value or format_date(datetime_iso)
724 title = value or format_date(datetime_iso)
725 tzinfo = '+00:00'
725
726
726 # detect if we have a timezone info, otherwise, add it
727 # detect if we have a timezone info, otherwise, add it
727 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
728 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
728 tzinfo = '+00:00'
729
730 if time_is_local:
729 if time_is_local:
731 tzinfo = time.strftime("+%H:%M",
730 tzinfo = time.strftime("+%H:%M",
732 time.gmtime(
731 time.gmtime(
733 (datetime.now() - datetime.utcnow()).seconds + 1
732 (datetime.now() - datetime.utcnow()).seconds + 1
734 )
733 )
735 )
734 )
736
735
737 return literal(
736 return literal(
738 '<time class="timeago tooltip" '
737 '<time class="timeago tooltip" '
739 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
738 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
740 datetime_iso, title, tzinfo))
739 datetime_iso, title, tzinfo))
741
740
742
741
743 def _shorten_commit_id(commit_id):
742 def _shorten_commit_id(commit_id):
744 from rhodecode import CONFIG
743 from rhodecode import CONFIG
745 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
744 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
746 return commit_id[:def_len]
745 return commit_id[:def_len]
747
746
748
747
749 def show_id(commit):
748 def show_id(commit):
750 """
749 """
751 Configurable function that shows ID
750 Configurable function that shows ID
752 by default it's r123:fffeeefffeee
751 by default it's r123:fffeeefffeee
753
752
754 :param commit: commit instance
753 :param commit: commit instance
755 """
754 """
756 from rhodecode import CONFIG
755 from rhodecode import CONFIG
757 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
756 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
758
757
759 raw_id = _shorten_commit_id(commit.raw_id)
758 raw_id = _shorten_commit_id(commit.raw_id)
760 if show_idx:
759 if show_idx:
761 return 'r%s:%s' % (commit.idx, raw_id)
760 return 'r%s:%s' % (commit.idx, raw_id)
762 else:
761 else:
763 return '%s' % (raw_id, )
762 return '%s' % (raw_id, )
764
763
765
764
766 def format_date(date):
765 def format_date(date):
767 """
766 """
768 use a standardized formatting for dates used in RhodeCode
767 use a standardized formatting for dates used in RhodeCode
769
768
770 :param date: date/datetime object
769 :param date: date/datetime object
771 :return: formatted date
770 :return: formatted date
772 """
771 """
773
772
774 if date:
773 if date:
775 _fmt = "%a, %d %b %Y %H:%M:%S"
774 _fmt = "%a, %d %b %Y %H:%M:%S"
776 return safe_unicode(date.strftime(_fmt))
775 return safe_unicode(date.strftime(_fmt))
777
776
778 return u""
777 return u""
779
778
780
779
781 class _RepoChecker(object):
780 class _RepoChecker(object):
782
781
783 def __init__(self, backend_alias):
782 def __init__(self, backend_alias):
784 self._backend_alias = backend_alias
783 self._backend_alias = backend_alias
785
784
786 def __call__(self, repository):
785 def __call__(self, repository):
787 if hasattr(repository, 'alias'):
786 if hasattr(repository, 'alias'):
788 _type = repository.alias
787 _type = repository.alias
789 elif hasattr(repository, 'repo_type'):
788 elif hasattr(repository, 'repo_type'):
790 _type = repository.repo_type
789 _type = repository.repo_type
791 else:
790 else:
792 _type = repository
791 _type = repository
793 return _type == self._backend_alias
792 return _type == self._backend_alias
794
793
795 is_git = _RepoChecker('git')
794 is_git = _RepoChecker('git')
796 is_hg = _RepoChecker('hg')
795 is_hg = _RepoChecker('hg')
797 is_svn = _RepoChecker('svn')
796 is_svn = _RepoChecker('svn')
798
797
799
798
800 def get_repo_type_by_name(repo_name):
799 def get_repo_type_by_name(repo_name):
801 repo = Repository.get_by_repo_name(repo_name)
800 repo = Repository.get_by_repo_name(repo_name)
802 return repo.repo_type
801 return repo.repo_type
803
802
804
803
805 def is_svn_without_proxy(repository):
804 def is_svn_without_proxy(repository):
806 if is_svn(repository):
805 if is_svn(repository):
807 from rhodecode.model.settings import VcsSettingsModel
806 from rhodecode.model.settings import VcsSettingsModel
808 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
807 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
809 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
808 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
810 return False
809 return False
811
810
812
811
813 def discover_user(author):
812 def discover_user(author):
814 """
813 """
815 Tries to discover RhodeCode User based on the autho string. Author string
814 Tries to discover RhodeCode User based on the autho string. Author string
816 is typically `FirstName LastName <email@address.com>`
815 is typically `FirstName LastName <email@address.com>`
817 """
816 """
818
817
819 # if author is already an instance use it for extraction
818 # if author is already an instance use it for extraction
820 if isinstance(author, User):
819 if isinstance(author, User):
821 return author
820 return author
822
821
823 # Valid email in the attribute passed, see if they're in the system
822 # Valid email in the attribute passed, see if they're in the system
824 _email = author_email(author)
823 _email = author_email(author)
825 if _email != '':
824 if _email != '':
826 user = User.get_by_email(_email, case_insensitive=True, cache=True)
825 user = User.get_by_email(_email, case_insensitive=True, cache=True)
827 if user is not None:
826 if user is not None:
828 return user
827 return user
829
828
830 # Maybe it's a username, we try to extract it and fetch by username ?
829 # Maybe it's a username, we try to extract it and fetch by username ?
831 _author = author_name(author)
830 _author = author_name(author)
832 user = User.get_by_username(_author, case_insensitive=True, cache=True)
831 user = User.get_by_username(_author, case_insensitive=True, cache=True)
833 if user is not None:
832 if user is not None:
834 return user
833 return user
835
834
836 return None
835 return None
837
836
838
837
839 def email_or_none(author):
838 def email_or_none(author):
840 # extract email from the commit string
839 # extract email from the commit string
841 _email = author_email(author)
840 _email = author_email(author)
842
841
843 # If we have an email, use it, otherwise
842 # If we have an email, use it, otherwise
844 # see if it contains a username we can get an email from
843 # see if it contains a username we can get an email from
845 if _email != '':
844 if _email != '':
846 return _email
845 return _email
847 else:
846 else:
848 user = User.get_by_username(
847 user = User.get_by_username(
849 author_name(author), case_insensitive=True, cache=True)
848 author_name(author), case_insensitive=True, cache=True)
850
849
851 if user is not None:
850 if user is not None:
852 return user.email
851 return user.email
853
852
854 # No valid email, not a valid user in the system, none!
853 # No valid email, not a valid user in the system, none!
855 return None
854 return None
856
855
857
856
858 def link_to_user(author, length=0, **kwargs):
857 def link_to_user(author, length=0, **kwargs):
859 user = discover_user(author)
858 user = discover_user(author)
860 # user can be None, but if we have it already it means we can re-use it
859 # user can be None, but if we have it already it means we can re-use it
861 # in the person() function, so we save 1 intensive-query
860 # in the person() function, so we save 1 intensive-query
862 if user:
861 if user:
863 author = user
862 author = user
864
863
865 display_person = person(author, 'username_or_name_or_email')
864 display_person = person(author, 'username_or_name_or_email')
866 if length:
865 if length:
867 display_person = shorter(display_person, length)
866 display_person = shorter(display_person, length)
868
867
869 if user:
868 if user:
870 return link_to(
869 return link_to(
871 escape(display_person),
870 escape(display_person),
872 url('user_profile', username=user.username),
871 url('user_profile', username=user.username),
873 **kwargs)
872 **kwargs)
874 else:
873 else:
875 return escape(display_person)
874 return escape(display_person)
876
875
877
876
878 def person(author, show_attr="username_and_name"):
877 def person(author, show_attr="username_and_name"):
879 user = discover_user(author)
878 user = discover_user(author)
880 if user:
879 if user:
881 return getattr(user, show_attr)
880 return getattr(user, show_attr)
882 else:
881 else:
883 _author = author_name(author)
882 _author = author_name(author)
884 _email = email(author)
883 _email = email(author)
885 return _author or _email
884 return _author or _email
886
885
887
886
888 def author_string(email):
887 def author_string(email):
889 if email:
888 if email:
890 user = User.get_by_email(email, case_insensitive=True, cache=True)
889 user = User.get_by_email(email, case_insensitive=True, cache=True)
891 if user:
890 if user:
892 if user.firstname or user.lastname:
891 if user.firstname or user.lastname:
893 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
892 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
894 else:
893 else:
895 return email
894 return email
896 else:
895 else:
897 return email
896 return email
898 else:
897 else:
899 return None
898 return None
900
899
901
900
902 def person_by_id(id_, show_attr="username_and_name"):
901 def person_by_id(id_, show_attr="username_and_name"):
903 # attr to return from fetched user
902 # attr to return from fetched user
904 person_getter = lambda usr: getattr(usr, show_attr)
903 person_getter = lambda usr: getattr(usr, show_attr)
905
904
906 #maybe it's an ID ?
905 #maybe it's an ID ?
907 if str(id_).isdigit() or isinstance(id_, int):
906 if str(id_).isdigit() or isinstance(id_, int):
908 id_ = int(id_)
907 id_ = int(id_)
909 user = User.get(id_)
908 user = User.get(id_)
910 if user is not None:
909 if user is not None:
911 return person_getter(user)
910 return person_getter(user)
912 return id_
911 return id_
913
912
914
913
915 def gravatar_with_user(author, show_disabled=False):
914 def gravatar_with_user(author, show_disabled=False):
916 from rhodecode.lib.utils import PartialRenderer
915 from rhodecode.lib.utils import PartialRenderer
917 _render = PartialRenderer('base/base.mako')
916 _render = PartialRenderer('base/base.mako')
918 return _render('gravatar_with_user', author, show_disabled=show_disabled)
917 return _render('gravatar_with_user', author, show_disabled=show_disabled)
919
918
920
919
921 def desc_stylize(value):
920 def desc_stylize(value):
922 """
921 """
923 converts tags from value into html equivalent
922 converts tags from value into html equivalent
924
923
925 :param value:
924 :param value:
926 """
925 """
927 if not value:
926 if not value:
928 return ''
927 return ''
929
928
930 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
929 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
931 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
930 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
932 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
931 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
933 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
932 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
934 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
933 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
935 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
934 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
936 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
935 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
937 '<div class="metatag" tag="lang">\\2</div>', value)
936 '<div class="metatag" tag="lang">\\2</div>', value)
938 value = re.sub(r'\[([a-z]+)\]',
937 value = re.sub(r'\[([a-z]+)\]',
939 '<div class="metatag" tag="\\1">\\1</div>', value)
938 '<div class="metatag" tag="\\1">\\1</div>', value)
940
939
941 return value
940 return value
942
941
943
942
944 def escaped_stylize(value):
943 def escaped_stylize(value):
945 """
944 """
946 converts tags from value into html equivalent, but escaping its value first
945 converts tags from value into html equivalent, but escaping its value first
947 """
946 """
948 if not value:
947 if not value:
949 return ''
948 return ''
950
949
951 # Using default webhelper escape method, but has to force it as a
950 # Using default webhelper escape method, but has to force it as a
952 # plain unicode instead of a markup tag to be used in regex expressions
951 # plain unicode instead of a markup tag to be used in regex expressions
953 value = unicode(escape(safe_unicode(value)))
952 value = unicode(escape(safe_unicode(value)))
954
953
955 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
954 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
956 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
955 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
957 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
956 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
958 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
957 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
959 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
958 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
960 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
959 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
961 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
960 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
962 '<div class="metatag" tag="lang">\\2</div>', value)
961 '<div class="metatag" tag="lang">\\2</div>', value)
963 value = re.sub(r'\[([a-z]+)\]',
962 value = re.sub(r'\[([a-z]+)\]',
964 '<div class="metatag" tag="\\1">\\1</div>', value)
963 '<div class="metatag" tag="\\1">\\1</div>', value)
965
964
966 return value
965 return value
967
966
968
967
969 def bool2icon(value):
968 def bool2icon(value):
970 """
969 """
971 Returns boolean value of a given value, represented as html element with
970 Returns boolean value of a given value, represented as html element with
972 classes that will represent icons
971 classes that will represent icons
973
972
974 :param value: given value to convert to html node
973 :param value: given value to convert to html node
975 """
974 """
976
975
977 if value: # does bool conversion
976 if value: # does bool conversion
978 return HTML.tag('i', class_="icon-true")
977 return HTML.tag('i', class_="icon-true")
979 else: # not true as bool
978 else: # not true as bool
980 return HTML.tag('i', class_="icon-false")
979 return HTML.tag('i', class_="icon-false")
981
980
982
981
983 #==============================================================================
982 #==============================================================================
984 # PERMS
983 # PERMS
985 #==============================================================================
984 #==============================================================================
986 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
985 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
987 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
986 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
988 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
987 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
989 csrf_token_key
988 csrf_token_key
990
989
991
990
992 #==============================================================================
991 #==============================================================================
993 # GRAVATAR URL
992 # GRAVATAR URL
994 #==============================================================================
993 #==============================================================================
995 class InitialsGravatar(object):
994 class InitialsGravatar(object):
996 def __init__(self, email_address, first_name, last_name, size=30,
995 def __init__(self, email_address, first_name, last_name, size=30,
997 background=None, text_color='#fff'):
996 background=None, text_color='#fff'):
998 self.size = size
997 self.size = size
999 self.first_name = first_name
998 self.first_name = first_name
1000 self.last_name = last_name
999 self.last_name = last_name
1001 self.email_address = email_address
1000 self.email_address = email_address
1002 self.background = background or self.str2color(email_address)
1001 self.background = background or self.str2color(email_address)
1003 self.text_color = text_color
1002 self.text_color = text_color
1004
1003
1005 def get_color_bank(self):
1004 def get_color_bank(self):
1006 """
1005 """
1007 returns a predefined list of colors that gravatars can use.
1006 returns a predefined list of colors that gravatars can use.
1008 Those are randomized distinct colors that guarantee readability and
1007 Those are randomized distinct colors that guarantee readability and
1009 uniqueness.
1008 uniqueness.
1010
1009
1011 generated with: http://phrogz.net/css/distinct-colors.html
1010 generated with: http://phrogz.net/css/distinct-colors.html
1012 """
1011 """
1013 return [
1012 return [
1014 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1013 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1015 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1014 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1016 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1015 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1017 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1016 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1018 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1017 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1019 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1018 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1020 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1019 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1021 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1020 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1022 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1021 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1023 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1022 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1024 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1023 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1025 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1024 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1026 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1025 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1027 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1026 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1028 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1027 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1029 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1028 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1030 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1029 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1031 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1030 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1032 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1031 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1033 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1032 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1034 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1033 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1035 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1034 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1036 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1035 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1037 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1036 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1038 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1037 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1039 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1038 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1040 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1039 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1041 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1040 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1042 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1041 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1043 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1042 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1044 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1043 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1045 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1044 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1046 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1045 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1047 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1046 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1048 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1047 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1049 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1048 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1050 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1049 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1051 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1050 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1052 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1051 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1053 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1052 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1054 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1053 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1055 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1054 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1056 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1055 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1057 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1056 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1058 '#4f8c46', '#368dd9', '#5c0073'
1057 '#4f8c46', '#368dd9', '#5c0073'
1059 ]
1058 ]
1060
1059
1061 def rgb_to_hex_color(self, rgb_tuple):
1060 def rgb_to_hex_color(self, rgb_tuple):
1062 """
1061 """
1063 Converts an rgb_tuple passed to an hex color.
1062 Converts an rgb_tuple passed to an hex color.
1064
1063
1065 :param rgb_tuple: tuple with 3 ints represents rgb color space
1064 :param rgb_tuple: tuple with 3 ints represents rgb color space
1066 """
1065 """
1067 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1066 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1068
1067
1069 def email_to_int_list(self, email_str):
1068 def email_to_int_list(self, email_str):
1070 """
1069 """
1071 Get every byte of the hex digest value of email and turn it to integer.
1070 Get every byte of the hex digest value of email and turn it to integer.
1072 It's going to be always between 0-255
1071 It's going to be always between 0-255
1073 """
1072 """
1074 digest = md5_safe(email_str.lower())
1073 digest = md5_safe(email_str.lower())
1075 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1074 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1076
1075
1077 def pick_color_bank_index(self, email_str, color_bank):
1076 def pick_color_bank_index(self, email_str, color_bank):
1078 return self.email_to_int_list(email_str)[0] % len(color_bank)
1077 return self.email_to_int_list(email_str)[0] % len(color_bank)
1079
1078
1080 def str2color(self, email_str):
1079 def str2color(self, email_str):
1081 """
1080 """
1082 Tries to map in a stable algorithm an email to color
1081 Tries to map in a stable algorithm an email to color
1083
1082
1084 :param email_str:
1083 :param email_str:
1085 """
1084 """
1086 color_bank = self.get_color_bank()
1085 color_bank = self.get_color_bank()
1087 # pick position (module it's length so we always find it in the
1086 # pick position (module it's length so we always find it in the
1088 # bank even if it's smaller than 256 values
1087 # bank even if it's smaller than 256 values
1089 pos = self.pick_color_bank_index(email_str, color_bank)
1088 pos = self.pick_color_bank_index(email_str, color_bank)
1090 return color_bank[pos]
1089 return color_bank[pos]
1091
1090
1092 def normalize_email(self, email_address):
1091 def normalize_email(self, email_address):
1093 import unicodedata
1092 import unicodedata
1094 # default host used to fill in the fake/missing email
1093 # default host used to fill in the fake/missing email
1095 default_host = u'localhost'
1094 default_host = u'localhost'
1096
1095
1097 if not email_address:
1096 if not email_address:
1098 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1097 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1099
1098
1100 email_address = safe_unicode(email_address)
1099 email_address = safe_unicode(email_address)
1101
1100
1102 if u'@' not in email_address:
1101 if u'@' not in email_address:
1103 email_address = u'%s@%s' % (email_address, default_host)
1102 email_address = u'%s@%s' % (email_address, default_host)
1104
1103
1105 if email_address.endswith(u'@'):
1104 if email_address.endswith(u'@'):
1106 email_address = u'%s%s' % (email_address, default_host)
1105 email_address = u'%s%s' % (email_address, default_host)
1107
1106
1108 email_address = unicodedata.normalize('NFKD', email_address)\
1107 email_address = unicodedata.normalize('NFKD', email_address)\
1109 .encode('ascii', 'ignore')
1108 .encode('ascii', 'ignore')
1110 return email_address
1109 return email_address
1111
1110
1112 def get_initials(self):
1111 def get_initials(self):
1113 """
1112 """
1114 Returns 2 letter initials calculated based on the input.
1113 Returns 2 letter initials calculated based on the input.
1115 The algorithm picks first given email address, and takes first letter
1114 The algorithm picks first given email address, and takes first letter
1116 of part before @, and then the first letter of server name. In case
1115 of part before @, and then the first letter of server name. In case
1117 the part before @ is in a format of `somestring.somestring2` it replaces
1116 the part before @ is in a format of `somestring.somestring2` it replaces
1118 the server letter with first letter of somestring2
1117 the server letter with first letter of somestring2
1119
1118
1120 In case function was initialized with both first and lastname, this
1119 In case function was initialized with both first and lastname, this
1121 overrides the extraction from email by first letter of the first and
1120 overrides the extraction from email by first letter of the first and
1122 last name. We add special logic to that functionality, In case Full name
1121 last name. We add special logic to that functionality, In case Full name
1123 is compound, like Guido Von Rossum, we use last part of the last name
1122 is compound, like Guido Von Rossum, we use last part of the last name
1124 (Von Rossum) picking `R`.
1123 (Von Rossum) picking `R`.
1125
1124
1126 Function also normalizes the non-ascii characters to they ascii
1125 Function also normalizes the non-ascii characters to they ascii
1127 representation, eg Δ„ => A
1126 representation, eg Δ„ => A
1128 """
1127 """
1129 import unicodedata
1128 import unicodedata
1130 # replace non-ascii to ascii
1129 # replace non-ascii to ascii
1131 first_name = unicodedata.normalize(
1130 first_name = unicodedata.normalize(
1132 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1131 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1133 last_name = unicodedata.normalize(
1132 last_name = unicodedata.normalize(
1134 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1133 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1135
1134
1136 # do NFKD encoding, and also make sure email has proper format
1135 # do NFKD encoding, and also make sure email has proper format
1137 email_address = self.normalize_email(self.email_address)
1136 email_address = self.normalize_email(self.email_address)
1138
1137
1139 # first push the email initials
1138 # first push the email initials
1140 prefix, server = email_address.split('@', 1)
1139 prefix, server = email_address.split('@', 1)
1141
1140
1142 # check if prefix is maybe a 'firstname.lastname' syntax
1141 # check if prefix is maybe a 'firstname.lastname' syntax
1143 _dot_split = prefix.rsplit('.', 1)
1142 _dot_split = prefix.rsplit('.', 1)
1144 if len(_dot_split) == 2:
1143 if len(_dot_split) == 2:
1145 initials = [_dot_split[0][0], _dot_split[1][0]]
1144 initials = [_dot_split[0][0], _dot_split[1][0]]
1146 else:
1145 else:
1147 initials = [prefix[0], server[0]]
1146 initials = [prefix[0], server[0]]
1148
1147
1149 # then try to replace either firtname or lastname
1148 # then try to replace either firtname or lastname
1150 fn_letter = (first_name or " ")[0].strip()
1149 fn_letter = (first_name or " ")[0].strip()
1151 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1150 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1152
1151
1153 if fn_letter:
1152 if fn_letter:
1154 initials[0] = fn_letter
1153 initials[0] = fn_letter
1155
1154
1156 if ln_letter:
1155 if ln_letter:
1157 initials[1] = ln_letter
1156 initials[1] = ln_letter
1158
1157
1159 return ''.join(initials).upper()
1158 return ''.join(initials).upper()
1160
1159
1161 def get_img_data_by_type(self, font_family, img_type):
1160 def get_img_data_by_type(self, font_family, img_type):
1162 default_user = """
1161 default_user = """
1163 <svg xmlns="http://www.w3.org/2000/svg"
1162 <svg xmlns="http://www.w3.org/2000/svg"
1164 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1163 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1165 viewBox="-15 -10 439.165 429.164"
1164 viewBox="-15 -10 439.165 429.164"
1166
1165
1167 xml:space="preserve"
1166 xml:space="preserve"
1168 style="background:{background};" >
1167 style="background:{background};" >
1169
1168
1170 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1169 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1171 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1170 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1172 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1171 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1173 168.596,153.916,216.671,
1172 168.596,153.916,216.671,
1174 204.583,216.671z" fill="{text_color}"/>
1173 204.583,216.671z" fill="{text_color}"/>
1175 <path d="M407.164,374.717L360.88,
1174 <path d="M407.164,374.717L360.88,
1176 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1175 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1177 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1176 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1178 15.366-44.203,23.488-69.076,23.488c-24.877,
1177 15.366-44.203,23.488-69.076,23.488c-24.877,
1179 0-48.762-8.122-69.078-23.488
1178 0-48.762-8.122-69.078-23.488
1180 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1179 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1181 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1180 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1182 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1181 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1183 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1182 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1184 19.402-10.527 C409.699,390.129,
1183 19.402-10.527 C409.699,390.129,
1185 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1184 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1186 </svg>""".format(
1185 </svg>""".format(
1187 size=self.size,
1186 size=self.size,
1188 background='#979797', # @grey4
1187 background='#979797', # @grey4
1189 text_color=self.text_color,
1188 text_color=self.text_color,
1190 font_family=font_family)
1189 font_family=font_family)
1191
1190
1192 return {
1191 return {
1193 "default_user": default_user
1192 "default_user": default_user
1194 }[img_type]
1193 }[img_type]
1195
1194
1196 def get_img_data(self, svg_type=None):
1195 def get_img_data(self, svg_type=None):
1197 """
1196 """
1198 generates the svg metadata for image
1197 generates the svg metadata for image
1199 """
1198 """
1200
1199
1201 font_family = ','.join([
1200 font_family = ','.join([
1202 'proximanovaregular',
1201 'proximanovaregular',
1203 'Proxima Nova Regular',
1202 'Proxima Nova Regular',
1204 'Proxima Nova',
1203 'Proxima Nova',
1205 'Arial',
1204 'Arial',
1206 'Lucida Grande',
1205 'Lucida Grande',
1207 'sans-serif'
1206 'sans-serif'
1208 ])
1207 ])
1209 if svg_type:
1208 if svg_type:
1210 return self.get_img_data_by_type(font_family, svg_type)
1209 return self.get_img_data_by_type(font_family, svg_type)
1211
1210
1212 initials = self.get_initials()
1211 initials = self.get_initials()
1213 img_data = """
1212 img_data = """
1214 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1213 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1215 width="{size}" height="{size}"
1214 width="{size}" height="{size}"
1216 style="width: 100%; height: 100%; background-color: {background}"
1215 style="width: 100%; height: 100%; background-color: {background}"
1217 viewBox="0 0 {size} {size}">
1216 viewBox="0 0 {size} {size}">
1218 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1217 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1219 pointer-events="auto" fill="{text_color}"
1218 pointer-events="auto" fill="{text_color}"
1220 font-family="{font_family}"
1219 font-family="{font_family}"
1221 style="font-weight: 400; font-size: {f_size}px;">{text}
1220 style="font-weight: 400; font-size: {f_size}px;">{text}
1222 </text>
1221 </text>
1223 </svg>""".format(
1222 </svg>""".format(
1224 size=self.size,
1223 size=self.size,
1225 f_size=self.size/1.85, # scale the text inside the box nicely
1224 f_size=self.size/1.85, # scale the text inside the box nicely
1226 background=self.background,
1225 background=self.background,
1227 text_color=self.text_color,
1226 text_color=self.text_color,
1228 text=initials.upper(),
1227 text=initials.upper(),
1229 font_family=font_family)
1228 font_family=font_family)
1230
1229
1231 return img_data
1230 return img_data
1232
1231
1233 def generate_svg(self, svg_type=None):
1232 def generate_svg(self, svg_type=None):
1234 img_data = self.get_img_data(svg_type)
1233 img_data = self.get_img_data(svg_type)
1235 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1234 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1236
1235
1237
1236
1238 def initials_gravatar(email_address, first_name, last_name, size=30):
1237 def initials_gravatar(email_address, first_name, last_name, size=30):
1239 svg_type = None
1238 svg_type = None
1240 if email_address == User.DEFAULT_USER_EMAIL:
1239 if email_address == User.DEFAULT_USER_EMAIL:
1241 svg_type = 'default_user'
1240 svg_type = 'default_user'
1242 klass = InitialsGravatar(email_address, first_name, last_name, size)
1241 klass = InitialsGravatar(email_address, first_name, last_name, size)
1243 return klass.generate_svg(svg_type=svg_type)
1242 return klass.generate_svg(svg_type=svg_type)
1244
1243
1245
1244
1246 def gravatar_url(email_address, size=30):
1245 def gravatar_url(email_address, size=30):
1247 # doh, we need to re-import those to mock it later
1246 # doh, we need to re-import those to mock it later
1248 from pylons import tmpl_context as c
1247 from pylons import tmpl_context as c
1249
1248
1250 _use_gravatar = c.visual.use_gravatar
1249 _use_gravatar = c.visual.use_gravatar
1251 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1250 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1252
1251
1253 email_address = email_address or User.DEFAULT_USER_EMAIL
1252 email_address = email_address or User.DEFAULT_USER_EMAIL
1254 if isinstance(email_address, unicode):
1253 if isinstance(email_address, unicode):
1255 # hashlib crashes on unicode items
1254 # hashlib crashes on unicode items
1256 email_address = safe_str(email_address)
1255 email_address = safe_str(email_address)
1257
1256
1258 # empty email or default user
1257 # empty email or default user
1259 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1258 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1260 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1259 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1261
1260
1262 if _use_gravatar:
1261 if _use_gravatar:
1263 # TODO: Disuse pyramid thread locals. Think about another solution to
1262 # TODO: Disuse pyramid thread locals. Think about another solution to
1264 # get the host and schema here.
1263 # get the host and schema here.
1265 request = get_current_request()
1264 request = get_current_request()
1266 tmpl = safe_str(_gravatar_url)
1265 tmpl = safe_str(_gravatar_url)
1267 tmpl = tmpl.replace('{email}', email_address)\
1266 tmpl = tmpl.replace('{email}', email_address)\
1268 .replace('{md5email}', md5_safe(email_address.lower())) \
1267 .replace('{md5email}', md5_safe(email_address.lower())) \
1269 .replace('{netloc}', request.host)\
1268 .replace('{netloc}', request.host)\
1270 .replace('{scheme}', request.scheme)\
1269 .replace('{scheme}', request.scheme)\
1271 .replace('{size}', safe_str(size))
1270 .replace('{size}', safe_str(size))
1272 return tmpl
1271 return tmpl
1273 else:
1272 else:
1274 return initials_gravatar(email_address, '', '', size=size)
1273 return initials_gravatar(email_address, '', '', size=size)
1275
1274
1276
1275
1277 class Page(_Page):
1276 class Page(_Page):
1278 """
1277 """
1279 Custom pager to match rendering style with paginator
1278 Custom pager to match rendering style with paginator
1280 """
1279 """
1281
1280
1282 def _get_pos(self, cur_page, max_page, items):
1281 def _get_pos(self, cur_page, max_page, items):
1283 edge = (items / 2) + 1
1282 edge = (items / 2) + 1
1284 if (cur_page <= edge):
1283 if (cur_page <= edge):
1285 radius = max(items / 2, items - cur_page)
1284 radius = max(items / 2, items - cur_page)
1286 elif (max_page - cur_page) < edge:
1285 elif (max_page - cur_page) < edge:
1287 radius = (items - 1) - (max_page - cur_page)
1286 radius = (items - 1) - (max_page - cur_page)
1288 else:
1287 else:
1289 radius = items / 2
1288 radius = items / 2
1290
1289
1291 left = max(1, (cur_page - (radius)))
1290 left = max(1, (cur_page - (radius)))
1292 right = min(max_page, cur_page + (radius))
1291 right = min(max_page, cur_page + (radius))
1293 return left, cur_page, right
1292 return left, cur_page, right
1294
1293
1295 def _range(self, regexp_match):
1294 def _range(self, regexp_match):
1296 """
1295 """
1297 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1296 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1298
1297
1299 Arguments:
1298 Arguments:
1300
1299
1301 regexp_match
1300 regexp_match
1302 A "re" (regular expressions) match object containing the
1301 A "re" (regular expressions) match object containing the
1303 radius of linked pages around the current page in
1302 radius of linked pages around the current page in
1304 regexp_match.group(1) as a string
1303 regexp_match.group(1) as a string
1305
1304
1306 This function is supposed to be called as a callable in
1305 This function is supposed to be called as a callable in
1307 re.sub.
1306 re.sub.
1308
1307
1309 """
1308 """
1310 radius = int(regexp_match.group(1))
1309 radius = int(regexp_match.group(1))
1311
1310
1312 # Compute the first and last page number within the radius
1311 # Compute the first and last page number within the radius
1313 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1312 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1314 # -> leftmost_page = 5
1313 # -> leftmost_page = 5
1315 # -> rightmost_page = 9
1314 # -> rightmost_page = 9
1316 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1315 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1317 self.last_page,
1316 self.last_page,
1318 (radius * 2) + 1)
1317 (radius * 2) + 1)
1319 nav_items = []
1318 nav_items = []
1320
1319
1321 # Create a link to the first page (unless we are on the first page
1320 # Create a link to the first page (unless we are on the first page
1322 # or there would be no need to insert '..' spacers)
1321 # or there would be no need to insert '..' spacers)
1323 if self.page != self.first_page and self.first_page < leftmost_page:
1322 if self.page != self.first_page and self.first_page < leftmost_page:
1324 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1323 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1325
1324
1326 # Insert dots if there are pages between the first page
1325 # Insert dots if there are pages between the first page
1327 # and the currently displayed page range
1326 # and the currently displayed page range
1328 if leftmost_page - self.first_page > 1:
1327 if leftmost_page - self.first_page > 1:
1329 # Wrap in a SPAN tag if nolink_attr is set
1328 # Wrap in a SPAN tag if nolink_attr is set
1330 text = '..'
1329 text = '..'
1331 if self.dotdot_attr:
1330 if self.dotdot_attr:
1332 text = HTML.span(c=text, **self.dotdot_attr)
1331 text = HTML.span(c=text, **self.dotdot_attr)
1333 nav_items.append(text)
1332 nav_items.append(text)
1334
1333
1335 for thispage in xrange(leftmost_page, rightmost_page + 1):
1334 for thispage in xrange(leftmost_page, rightmost_page + 1):
1336 # Hilight the current page number and do not use a link
1335 # Hilight the current page number and do not use a link
1337 if thispage == self.page:
1336 if thispage == self.page:
1338 text = '%s' % (thispage,)
1337 text = '%s' % (thispage,)
1339 # Wrap in a SPAN tag if nolink_attr is set
1338 # Wrap in a SPAN tag if nolink_attr is set
1340 if self.curpage_attr:
1339 if self.curpage_attr:
1341 text = HTML.span(c=text, **self.curpage_attr)
1340 text = HTML.span(c=text, **self.curpage_attr)
1342 nav_items.append(text)
1341 nav_items.append(text)
1343 # Otherwise create just a link to that page
1342 # Otherwise create just a link to that page
1344 else:
1343 else:
1345 text = '%s' % (thispage,)
1344 text = '%s' % (thispage,)
1346 nav_items.append(self._pagerlink(thispage, text))
1345 nav_items.append(self._pagerlink(thispage, text))
1347
1346
1348 # Insert dots if there are pages between the displayed
1347 # Insert dots if there are pages between the displayed
1349 # page numbers and the end of the page range
1348 # page numbers and the end of the page range
1350 if self.last_page - rightmost_page > 1:
1349 if self.last_page - rightmost_page > 1:
1351 text = '..'
1350 text = '..'
1352 # Wrap in a SPAN tag if nolink_attr is set
1351 # Wrap in a SPAN tag if nolink_attr is set
1353 if self.dotdot_attr:
1352 if self.dotdot_attr:
1354 text = HTML.span(c=text, **self.dotdot_attr)
1353 text = HTML.span(c=text, **self.dotdot_attr)
1355 nav_items.append(text)
1354 nav_items.append(text)
1356
1355
1357 # Create a link to the very last page (unless we are on the last
1356 # Create a link to the very last page (unless we are on the last
1358 # page or there would be no need to insert '..' spacers)
1357 # page or there would be no need to insert '..' spacers)
1359 if self.page != self.last_page and rightmost_page < self.last_page:
1358 if self.page != self.last_page and rightmost_page < self.last_page:
1360 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1359 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1361
1360
1362 ## prerender links
1361 ## prerender links
1363 #_page_link = url.current()
1362 #_page_link = url.current()
1364 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1363 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1365 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1364 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1366 return self.separator.join(nav_items)
1365 return self.separator.join(nav_items)
1367
1366
1368 def pager(self, format='~2~', page_param='page', partial_param='partial',
1367 def pager(self, format='~2~', page_param='page', partial_param='partial',
1369 show_if_single_page=False, separator=' ', onclick=None,
1368 show_if_single_page=False, separator=' ', onclick=None,
1370 symbol_first='<<', symbol_last='>>',
1369 symbol_first='<<', symbol_last='>>',
1371 symbol_previous='<', symbol_next='>',
1370 symbol_previous='<', symbol_next='>',
1372 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1371 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1373 curpage_attr={'class': 'pager_curpage'},
1372 curpage_attr={'class': 'pager_curpage'},
1374 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1373 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1375
1374
1376 self.curpage_attr = curpage_attr
1375 self.curpage_attr = curpage_attr
1377 self.separator = separator
1376 self.separator = separator
1378 self.pager_kwargs = kwargs
1377 self.pager_kwargs = kwargs
1379 self.page_param = page_param
1378 self.page_param = page_param
1380 self.partial_param = partial_param
1379 self.partial_param = partial_param
1381 self.onclick = onclick
1380 self.onclick = onclick
1382 self.link_attr = link_attr
1381 self.link_attr = link_attr
1383 self.dotdot_attr = dotdot_attr
1382 self.dotdot_attr = dotdot_attr
1384
1383
1385 # Don't show navigator if there is no more than one page
1384 # Don't show navigator if there is no more than one page
1386 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1385 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1387 return ''
1386 return ''
1388
1387
1389 from string import Template
1388 from string import Template
1390 # Replace ~...~ in token format by range of pages
1389 # Replace ~...~ in token format by range of pages
1391 result = re.sub(r'~(\d+)~', self._range, format)
1390 result = re.sub(r'~(\d+)~', self._range, format)
1392
1391
1393 # Interpolate '%' variables
1392 # Interpolate '%' variables
1394 result = Template(result).safe_substitute({
1393 result = Template(result).safe_substitute({
1395 'first_page': self.first_page,
1394 'first_page': self.first_page,
1396 'last_page': self.last_page,
1395 'last_page': self.last_page,
1397 'page': self.page,
1396 'page': self.page,
1398 'page_count': self.page_count,
1397 'page_count': self.page_count,
1399 'items_per_page': self.items_per_page,
1398 'items_per_page': self.items_per_page,
1400 'first_item': self.first_item,
1399 'first_item': self.first_item,
1401 'last_item': self.last_item,
1400 'last_item': self.last_item,
1402 'item_count': self.item_count,
1401 'item_count': self.item_count,
1403 'link_first': self.page > self.first_page and \
1402 'link_first': self.page > self.first_page and \
1404 self._pagerlink(self.first_page, symbol_first) or '',
1403 self._pagerlink(self.first_page, symbol_first) or '',
1405 'link_last': self.page < self.last_page and \
1404 'link_last': self.page < self.last_page and \
1406 self._pagerlink(self.last_page, symbol_last) or '',
1405 self._pagerlink(self.last_page, symbol_last) or '',
1407 'link_previous': self.previous_page and \
1406 'link_previous': self.previous_page and \
1408 self._pagerlink(self.previous_page, symbol_previous) \
1407 self._pagerlink(self.previous_page, symbol_previous) \
1409 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1408 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1410 'link_next': self.next_page and \
1409 'link_next': self.next_page and \
1411 self._pagerlink(self.next_page, symbol_next) \
1410 self._pagerlink(self.next_page, symbol_next) \
1412 or HTML.span(symbol_next, class_="pg-next disabled")
1411 or HTML.span(symbol_next, class_="pg-next disabled")
1413 })
1412 })
1414
1413
1415 return literal(result)
1414 return literal(result)
1416
1415
1417
1416
1418 #==============================================================================
1417 #==============================================================================
1419 # REPO PAGER, PAGER FOR REPOSITORY
1418 # REPO PAGER, PAGER FOR REPOSITORY
1420 #==============================================================================
1419 #==============================================================================
1421 class RepoPage(Page):
1420 class RepoPage(Page):
1422
1421
1423 def __init__(self, collection, page=1, items_per_page=20,
1422 def __init__(self, collection, page=1, items_per_page=20,
1424 item_count=None, url=None, **kwargs):
1423 item_count=None, url=None, **kwargs):
1425
1424
1426 """Create a "RepoPage" instance. special pager for paging
1425 """Create a "RepoPage" instance. special pager for paging
1427 repository
1426 repository
1428 """
1427 """
1429 self._url_generator = url
1428 self._url_generator = url
1430
1429
1431 # Safe the kwargs class-wide so they can be used in the pager() method
1430 # Safe the kwargs class-wide so they can be used in the pager() method
1432 self.kwargs = kwargs
1431 self.kwargs = kwargs
1433
1432
1434 # Save a reference to the collection
1433 # Save a reference to the collection
1435 self.original_collection = collection
1434 self.original_collection = collection
1436
1435
1437 self.collection = collection
1436 self.collection = collection
1438
1437
1439 # The self.page is the number of the current page.
1438 # The self.page is the number of the current page.
1440 # The first page has the number 1!
1439 # The first page has the number 1!
1441 try:
1440 try:
1442 self.page = int(page) # make it int() if we get it as a string
1441 self.page = int(page) # make it int() if we get it as a string
1443 except (ValueError, TypeError):
1442 except (ValueError, TypeError):
1444 self.page = 1
1443 self.page = 1
1445
1444
1446 self.items_per_page = items_per_page
1445 self.items_per_page = items_per_page
1447
1446
1448 # Unless the user tells us how many items the collections has
1447 # Unless the user tells us how many items the collections has
1449 # we calculate that ourselves.
1448 # we calculate that ourselves.
1450 if item_count is not None:
1449 if item_count is not None:
1451 self.item_count = item_count
1450 self.item_count = item_count
1452 else:
1451 else:
1453 self.item_count = len(self.collection)
1452 self.item_count = len(self.collection)
1454
1453
1455 # Compute the number of the first and last available page
1454 # Compute the number of the first and last available page
1456 if self.item_count > 0:
1455 if self.item_count > 0:
1457 self.first_page = 1
1456 self.first_page = 1
1458 self.page_count = int(math.ceil(float(self.item_count) /
1457 self.page_count = int(math.ceil(float(self.item_count) /
1459 self.items_per_page))
1458 self.items_per_page))
1460 self.last_page = self.first_page + self.page_count - 1
1459 self.last_page = self.first_page + self.page_count - 1
1461
1460
1462 # Make sure that the requested page number is the range of
1461 # Make sure that the requested page number is the range of
1463 # valid pages
1462 # valid pages
1464 if self.page > self.last_page:
1463 if self.page > self.last_page:
1465 self.page = self.last_page
1464 self.page = self.last_page
1466 elif self.page < self.first_page:
1465 elif self.page < self.first_page:
1467 self.page = self.first_page
1466 self.page = self.first_page
1468
1467
1469 # Note: the number of items on this page can be less than
1468 # Note: the number of items on this page can be less than
1470 # items_per_page if the last page is not full
1469 # items_per_page if the last page is not full
1471 self.first_item = max(0, (self.item_count) - (self.page *
1470 self.first_item = max(0, (self.item_count) - (self.page *
1472 items_per_page))
1471 items_per_page))
1473 self.last_item = ((self.item_count - 1) - items_per_page *
1472 self.last_item = ((self.item_count - 1) - items_per_page *
1474 (self.page - 1))
1473 (self.page - 1))
1475
1474
1476 self.items = list(self.collection[self.first_item:self.last_item + 1])
1475 self.items = list(self.collection[self.first_item:self.last_item + 1])
1477
1476
1478 # Links to previous and next page
1477 # Links to previous and next page
1479 if self.page > self.first_page:
1478 if self.page > self.first_page:
1480 self.previous_page = self.page - 1
1479 self.previous_page = self.page - 1
1481 else:
1480 else:
1482 self.previous_page = None
1481 self.previous_page = None
1483
1482
1484 if self.page < self.last_page:
1483 if self.page < self.last_page:
1485 self.next_page = self.page + 1
1484 self.next_page = self.page + 1
1486 else:
1485 else:
1487 self.next_page = None
1486 self.next_page = None
1488
1487
1489 # No items available
1488 # No items available
1490 else:
1489 else:
1491 self.first_page = None
1490 self.first_page = None
1492 self.page_count = 0
1491 self.page_count = 0
1493 self.last_page = None
1492 self.last_page = None
1494 self.first_item = None
1493 self.first_item = None
1495 self.last_item = None
1494 self.last_item = None
1496 self.previous_page = None
1495 self.previous_page = None
1497 self.next_page = None
1496 self.next_page = None
1498 self.items = []
1497 self.items = []
1499
1498
1500 # This is a subclass of the 'list' type. Initialise the list now.
1499 # This is a subclass of the 'list' type. Initialise the list now.
1501 list.__init__(self, reversed(self.items))
1500 list.__init__(self, reversed(self.items))
1502
1501
1503
1502
1504 def changed_tooltip(nodes):
1503 def changed_tooltip(nodes):
1505 """
1504 """
1506 Generates a html string for changed nodes in commit page.
1505 Generates a html string for changed nodes in commit page.
1507 It limits the output to 30 entries
1506 It limits the output to 30 entries
1508
1507
1509 :param nodes: LazyNodesGenerator
1508 :param nodes: LazyNodesGenerator
1510 """
1509 """
1511 if nodes:
1510 if nodes:
1512 pref = ': <br/> '
1511 pref = ': <br/> '
1513 suf = ''
1512 suf = ''
1514 if len(nodes) > 30:
1513 if len(nodes) > 30:
1515 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1514 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1516 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1515 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1517 for x in nodes[:30]]) + suf)
1516 for x in nodes[:30]]) + suf)
1518 else:
1517 else:
1519 return ': ' + _('No Files')
1518 return ': ' + _('No Files')
1520
1519
1521
1520
1522 def breadcrumb_repo_link(repo):
1521 def breadcrumb_repo_link(repo):
1523 """
1522 """
1524 Makes a breadcrumbs path link to repo
1523 Makes a breadcrumbs path link to repo
1525
1524
1526 ex::
1525 ex::
1527 group >> subgroup >> repo
1526 group >> subgroup >> repo
1528
1527
1529 :param repo: a Repository instance
1528 :param repo: a Repository instance
1530 """
1529 """
1531
1530
1532 path = [
1531 path = [
1533 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1532 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1534 for group in repo.groups_with_parents
1533 for group in repo.groups_with_parents
1535 ] + [
1534 ] + [
1536 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1535 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1537 ]
1536 ]
1538
1537
1539 return literal(' &raquo; '.join(path))
1538 return literal(' &raquo; '.join(path))
1540
1539
1541
1540
1542 def format_byte_size_binary(file_size):
1541 def format_byte_size_binary(file_size):
1543 """
1542 """
1544 Formats file/folder sizes to standard.
1543 Formats file/folder sizes to standard.
1545 """
1544 """
1546 formatted_size = format_byte_size(file_size, binary=True)
1545 formatted_size = format_byte_size(file_size, binary=True)
1547 return formatted_size
1546 return formatted_size
1548
1547
1549
1548
1550 def fancy_file_stats(stats):
1549 def fancy_file_stats(stats):
1551 """
1550 """
1552 Displays a fancy two colored bar for number of added/deleted
1551 Displays a fancy two colored bar for number of added/deleted
1553 lines of code on file
1552 lines of code on file
1554
1553
1555 :param stats: two element list of added/deleted lines of code
1554 :param stats: two element list of added/deleted lines of code
1556 """
1555 """
1557 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1556 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1558 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1557 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1559
1558
1560 def cgen(l_type, a_v, d_v):
1559 def cgen(l_type, a_v, d_v):
1561 mapping = {'tr': 'top-right-rounded-corner-mid',
1560 mapping = {'tr': 'top-right-rounded-corner-mid',
1562 'tl': 'top-left-rounded-corner-mid',
1561 'tl': 'top-left-rounded-corner-mid',
1563 'br': 'bottom-right-rounded-corner-mid',
1562 'br': 'bottom-right-rounded-corner-mid',
1564 'bl': 'bottom-left-rounded-corner-mid'}
1563 'bl': 'bottom-left-rounded-corner-mid'}
1565 map_getter = lambda x: mapping[x]
1564 map_getter = lambda x: mapping[x]
1566
1565
1567 if l_type == 'a' and d_v:
1566 if l_type == 'a' and d_v:
1568 #case when added and deleted are present
1567 #case when added and deleted are present
1569 return ' '.join(map(map_getter, ['tl', 'bl']))
1568 return ' '.join(map(map_getter, ['tl', 'bl']))
1570
1569
1571 if l_type == 'a' and not d_v:
1570 if l_type == 'a' and not d_v:
1572 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1571 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1573
1572
1574 if l_type == 'd' and a_v:
1573 if l_type == 'd' and a_v:
1575 return ' '.join(map(map_getter, ['tr', 'br']))
1574 return ' '.join(map(map_getter, ['tr', 'br']))
1576
1575
1577 if l_type == 'd' and not a_v:
1576 if l_type == 'd' and not a_v:
1578 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1577 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1579
1578
1580 a, d = stats['added'], stats['deleted']
1579 a, d = stats['added'], stats['deleted']
1581 width = 100
1580 width = 100
1582
1581
1583 if stats['binary']: # binary operations like chmod/rename etc
1582 if stats['binary']: # binary operations like chmod/rename etc
1584 lbl = []
1583 lbl = []
1585 bin_op = 0 # undefined
1584 bin_op = 0 # undefined
1586
1585
1587 # prefix with bin for binary files
1586 # prefix with bin for binary files
1588 if BIN_FILENODE in stats['ops']:
1587 if BIN_FILENODE in stats['ops']:
1589 lbl += ['bin']
1588 lbl += ['bin']
1590
1589
1591 if NEW_FILENODE in stats['ops']:
1590 if NEW_FILENODE in stats['ops']:
1592 lbl += [_('new file')]
1591 lbl += [_('new file')]
1593 bin_op = NEW_FILENODE
1592 bin_op = NEW_FILENODE
1594 elif MOD_FILENODE in stats['ops']:
1593 elif MOD_FILENODE in stats['ops']:
1595 lbl += [_('mod')]
1594 lbl += [_('mod')]
1596 bin_op = MOD_FILENODE
1595 bin_op = MOD_FILENODE
1597 elif DEL_FILENODE in stats['ops']:
1596 elif DEL_FILENODE in stats['ops']:
1598 lbl += [_('del')]
1597 lbl += [_('del')]
1599 bin_op = DEL_FILENODE
1598 bin_op = DEL_FILENODE
1600 elif RENAMED_FILENODE in stats['ops']:
1599 elif RENAMED_FILENODE in stats['ops']:
1601 lbl += [_('rename')]
1600 lbl += [_('rename')]
1602 bin_op = RENAMED_FILENODE
1601 bin_op = RENAMED_FILENODE
1603
1602
1604 # chmod can go with other operations, so we add a + to lbl if needed
1603 # chmod can go with other operations, so we add a + to lbl if needed
1605 if CHMOD_FILENODE in stats['ops']:
1604 if CHMOD_FILENODE in stats['ops']:
1606 lbl += [_('chmod')]
1605 lbl += [_('chmod')]
1607 if bin_op == 0:
1606 if bin_op == 0:
1608 bin_op = CHMOD_FILENODE
1607 bin_op = CHMOD_FILENODE
1609
1608
1610 lbl = '+'.join(lbl)
1609 lbl = '+'.join(lbl)
1611 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1610 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1612 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1611 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1613 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1612 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1614 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1613 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1615
1614
1616 t = stats['added'] + stats['deleted']
1615 t = stats['added'] + stats['deleted']
1617 unit = float(width) / (t or 1)
1616 unit = float(width) / (t or 1)
1618
1617
1619 # needs > 9% of width to be visible or 0 to be hidden
1618 # needs > 9% of width to be visible or 0 to be hidden
1620 a_p = max(9, unit * a) if a > 0 else 0
1619 a_p = max(9, unit * a) if a > 0 else 0
1621 d_p = max(9, unit * d) if d > 0 else 0
1620 d_p = max(9, unit * d) if d > 0 else 0
1622 p_sum = a_p + d_p
1621 p_sum = a_p + d_p
1623
1622
1624 if p_sum > width:
1623 if p_sum > width:
1625 #adjust the percentage to be == 100% since we adjusted to 9
1624 #adjust the percentage to be == 100% since we adjusted to 9
1626 if a_p > d_p:
1625 if a_p > d_p:
1627 a_p = a_p - (p_sum - width)
1626 a_p = a_p - (p_sum - width)
1628 else:
1627 else:
1629 d_p = d_p - (p_sum - width)
1628 d_p = d_p - (p_sum - width)
1630
1629
1631 a_v = a if a > 0 else ''
1630 a_v = a if a > 0 else ''
1632 d_v = d if d > 0 else ''
1631 d_v = d if d > 0 else ''
1633
1632
1634 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1633 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1635 cgen('a', a_v, d_v), a_p, a_v
1634 cgen('a', a_v, d_v), a_p, a_v
1636 )
1635 )
1637 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1636 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1638 cgen('d', a_v, d_v), d_p, d_v
1637 cgen('d', a_v, d_v), d_p, d_v
1639 )
1638 )
1640 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1639 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1641
1640
1642
1641
1643 def urlify_text(text_, safe=True):
1642 def urlify_text(text_, safe=True):
1644 """
1643 """
1645 Extrac urls from text and make html links out of them
1644 Extrac urls from text and make html links out of them
1646
1645
1647 :param text_:
1646 :param text_:
1648 """
1647 """
1649
1648
1650 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1649 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1651 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1650 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1652
1651
1653 def url_func(match_obj):
1652 def url_func(match_obj):
1654 url_full = match_obj.groups()[0]
1653 url_full = match_obj.groups()[0]
1655 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1654 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1656 _newtext = url_pat.sub(url_func, text_)
1655 _newtext = url_pat.sub(url_func, text_)
1657 if safe:
1656 if safe:
1658 return literal(_newtext)
1657 return literal(_newtext)
1659 return _newtext
1658 return _newtext
1660
1659
1661
1660
1662 def urlify_commits(text_, repository):
1661 def urlify_commits(text_, repository):
1663 """
1662 """
1664 Extract commit ids from text and make link from them
1663 Extract commit ids from text and make link from them
1665
1664
1666 :param text_:
1665 :param text_:
1667 :param repository: repo name to build the URL with
1666 :param repository: repo name to build the URL with
1668 """
1667 """
1669 from pylons import url # doh, we need to re-import url to mock it later
1668 from pylons import url # doh, we need to re-import url to mock it later
1670 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1669 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1671
1670
1672 def url_func(match_obj):
1671 def url_func(match_obj):
1673 commit_id = match_obj.groups()[1]
1672 commit_id = match_obj.groups()[1]
1674 pref = match_obj.groups()[0]
1673 pref = match_obj.groups()[0]
1675 suf = match_obj.groups()[2]
1674 suf = match_obj.groups()[2]
1676
1675
1677 tmpl = (
1676 tmpl = (
1678 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1677 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1679 '%(commit_id)s</a>%(suf)s'
1678 '%(commit_id)s</a>%(suf)s'
1680 )
1679 )
1681 return tmpl % {
1680 return tmpl % {
1682 'pref': pref,
1681 'pref': pref,
1683 'cls': 'revision-link',
1682 'cls': 'revision-link',
1684 'url': url('changeset_home', repo_name=repository,
1683 'url': url('changeset_home', repo_name=repository,
1685 revision=commit_id, qualified=True),
1684 revision=commit_id, qualified=True),
1686 'commit_id': commit_id,
1685 'commit_id': commit_id,
1687 'suf': suf
1686 'suf': suf
1688 }
1687 }
1689
1688
1690 newtext = URL_PAT.sub(url_func, text_)
1689 newtext = URL_PAT.sub(url_func, text_)
1691
1690
1692 return newtext
1691 return newtext
1693
1692
1694
1693
1695 def _process_url_func(match_obj, repo_name, uid, entry,
1694 def _process_url_func(match_obj, repo_name, uid, entry,
1696 return_raw_data=False):
1695 return_raw_data=False):
1697 pref = ''
1696 pref = ''
1698 if match_obj.group().startswith(' '):
1697 if match_obj.group().startswith(' '):
1699 pref = ' '
1698 pref = ' '
1700
1699
1701 issue_id = ''.join(match_obj.groups())
1700 issue_id = ''.join(match_obj.groups())
1702 tmpl = (
1701 tmpl = (
1703 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1702 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1704 '%(issue-prefix)s%(id-repr)s'
1703 '%(issue-prefix)s%(id-repr)s'
1705 '</a>')
1704 '</a>')
1706
1705
1707 (repo_name_cleaned,
1706 (repo_name_cleaned,
1708 parent_group_name) = RepoGroupModel().\
1707 parent_group_name) = RepoGroupModel().\
1709 _get_group_name_and_parent(repo_name)
1708 _get_group_name_and_parent(repo_name)
1710
1709
1711 # variables replacement
1710 # variables replacement
1712 named_vars = {
1711 named_vars = {
1713 'id': issue_id,
1712 'id': issue_id,
1714 'repo': repo_name,
1713 'repo': repo_name,
1715 'repo_name': repo_name_cleaned,
1714 'repo_name': repo_name_cleaned,
1716 'group_name': parent_group_name
1715 'group_name': parent_group_name
1717 }
1716 }
1718 # named regex variables
1717 # named regex variables
1719 named_vars.update(match_obj.groupdict())
1718 named_vars.update(match_obj.groupdict())
1720 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1719 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1721
1720
1722 data = {
1721 data = {
1723 'pref': pref,
1722 'pref': pref,
1724 'cls': 'issue-tracker-link',
1723 'cls': 'issue-tracker-link',
1725 'url': _url,
1724 'url': _url,
1726 'id-repr': issue_id,
1725 'id-repr': issue_id,
1727 'issue-prefix': entry['pref'],
1726 'issue-prefix': entry['pref'],
1728 'serv': entry['url'],
1727 'serv': entry['url'],
1729 }
1728 }
1730 if return_raw_data:
1729 if return_raw_data:
1731 return {
1730 return {
1732 'id': issue_id,
1731 'id': issue_id,
1733 'url': _url
1732 'url': _url
1734 }
1733 }
1735 return tmpl % data
1734 return tmpl % data
1736
1735
1737
1736
1738 def process_patterns(text_string, repo_name, config=None):
1737 def process_patterns(text_string, repo_name, config=None):
1739 repo = None
1738 repo = None
1740 if repo_name:
1739 if repo_name:
1741 # Retrieving repo_name to avoid invalid repo_name to explode on
1740 # Retrieving repo_name to avoid invalid repo_name to explode on
1742 # IssueTrackerSettingsModel but still passing invalid name further down
1741 # IssueTrackerSettingsModel but still passing invalid name further down
1743 repo = Repository.get_by_repo_name(repo_name, cache=True)
1742 repo = Repository.get_by_repo_name(repo_name, cache=True)
1744
1743
1745 settings_model = IssueTrackerSettingsModel(repo=repo)
1744 settings_model = IssueTrackerSettingsModel(repo=repo)
1746 active_entries = settings_model.get_settings(cache=True)
1745 active_entries = settings_model.get_settings(cache=True)
1747
1746
1748 issues_data = []
1747 issues_data = []
1749 newtext = text_string
1748 newtext = text_string
1750 for uid, entry in active_entries.items():
1749 for uid, entry in active_entries.items():
1751 log.debug('found issue tracker entry with uid %s' % (uid,))
1750 log.debug('found issue tracker entry with uid %s' % (uid,))
1752
1751
1753 if not (entry['pat'] and entry['url']):
1752 if not (entry['pat'] and entry['url']):
1754 log.debug('skipping due to missing data')
1753 log.debug('skipping due to missing data')
1755 continue
1754 continue
1756
1755
1757 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1756 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1758 % (uid, entry['pat'], entry['url'], entry['pref']))
1757 % (uid, entry['pat'], entry['url'], entry['pref']))
1759
1758
1760 try:
1759 try:
1761 pattern = re.compile(r'%s' % entry['pat'])
1760 pattern = re.compile(r'%s' % entry['pat'])
1762 except re.error:
1761 except re.error:
1763 log.exception(
1762 log.exception(
1764 'issue tracker pattern: `%s` failed to compile',
1763 'issue tracker pattern: `%s` failed to compile',
1765 entry['pat'])
1764 entry['pat'])
1766 continue
1765 continue
1767
1766
1768 data_func = partial(
1767 data_func = partial(
1769 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1768 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1770 return_raw_data=True)
1769 return_raw_data=True)
1771
1770
1772 for match_obj in pattern.finditer(text_string):
1771 for match_obj in pattern.finditer(text_string):
1773 issues_data.append(data_func(match_obj))
1772 issues_data.append(data_func(match_obj))
1774
1773
1775 url_func = partial(
1774 url_func = partial(
1776 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1775 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1777
1776
1778 newtext = pattern.sub(url_func, newtext)
1777 newtext = pattern.sub(url_func, newtext)
1779 log.debug('processed prefix:uid `%s`' % (uid,))
1778 log.debug('processed prefix:uid `%s`' % (uid,))
1780
1779
1781 return newtext, issues_data
1780 return newtext, issues_data
1782
1781
1783
1782
1784 def urlify_commit_message(commit_text, repository=None):
1783 def urlify_commit_message(commit_text, repository=None):
1785 """
1784 """
1786 Parses given text message and makes proper links.
1785 Parses given text message and makes proper links.
1787 issues are linked to given issue-server, and rest is a commit link
1786 issues are linked to given issue-server, and rest is a commit link
1788
1787
1789 :param commit_text:
1788 :param commit_text:
1790 :param repository:
1789 :param repository:
1791 """
1790 """
1792 from pylons import url # doh, we need to re-import url to mock it later
1791 from pylons import url # doh, we need to re-import url to mock it later
1793
1792
1794 def escaper(string):
1793 def escaper(string):
1795 return string.replace('<', '&lt;').replace('>', '&gt;')
1794 return string.replace('<', '&lt;').replace('>', '&gt;')
1796
1795
1797 newtext = escaper(commit_text)
1796 newtext = escaper(commit_text)
1798
1797
1799 # extract http/https links and make them real urls
1798 # extract http/https links and make them real urls
1800 newtext = urlify_text(newtext, safe=False)
1799 newtext = urlify_text(newtext, safe=False)
1801
1800
1802 # urlify commits - extract commit ids and make link out of them, if we have
1801 # urlify commits - extract commit ids and make link out of them, if we have
1803 # the scope of repository present.
1802 # the scope of repository present.
1804 if repository:
1803 if repository:
1805 newtext = urlify_commits(newtext, repository)
1804 newtext = urlify_commits(newtext, repository)
1806
1805
1807 # process issue tracker patterns
1806 # process issue tracker patterns
1808 newtext, issues = process_patterns(newtext, repository or '')
1807 newtext, issues = process_patterns(newtext, repository or '')
1809
1808
1810 return literal(newtext)
1809 return literal(newtext)
1811
1810
1812
1811
1813 def rst(source, mentions=False):
1812 def rst(source, mentions=False):
1814 return literal('<div class="rst-block">%s</div>' %
1813 return literal('<div class="rst-block">%s</div>' %
1815 MarkupRenderer.rst(source, mentions=mentions))
1814 MarkupRenderer.rst(source, mentions=mentions))
1816
1815
1817
1816
1818 def markdown(source, mentions=False):
1817 def markdown(source, mentions=False):
1819 return literal('<div class="markdown-block">%s</div>' %
1818 return literal('<div class="markdown-block">%s</div>' %
1820 MarkupRenderer.markdown(source, flavored=True,
1819 MarkupRenderer.markdown(source, flavored=True,
1821 mentions=mentions))
1820 mentions=mentions))
1822
1821
1823 def renderer_from_filename(filename, exclude=None):
1822 def renderer_from_filename(filename, exclude=None):
1824 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1823 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1825
1824
1826
1825
1827 def render(source, renderer='rst', mentions=False):
1826 def render(source, renderer='rst', mentions=False):
1828 if renderer == 'rst':
1827 if renderer == 'rst':
1829 return rst(source, mentions=mentions)
1828 return rst(source, mentions=mentions)
1830 if renderer == 'markdown':
1829 if renderer == 'markdown':
1831 return markdown(source, mentions=mentions)
1830 return markdown(source, mentions=mentions)
1832
1831
1833
1832
1834 def commit_status(repo, commit_id):
1833 def commit_status(repo, commit_id):
1835 return ChangesetStatusModel().get_status(repo, commit_id)
1834 return ChangesetStatusModel().get_status(repo, commit_id)
1836
1835
1837
1836
1838 def commit_status_lbl(commit_status):
1837 def commit_status_lbl(commit_status):
1839 return dict(ChangesetStatus.STATUSES).get(commit_status)
1838 return dict(ChangesetStatus.STATUSES).get(commit_status)
1840
1839
1841
1840
1842 def commit_time(repo_name, commit_id):
1841 def commit_time(repo_name, commit_id):
1843 repo = Repository.get_by_repo_name(repo_name)
1842 repo = Repository.get_by_repo_name(repo_name)
1844 commit = repo.get_commit(commit_id=commit_id)
1843 commit = repo.get_commit(commit_id=commit_id)
1845 return commit.date
1844 return commit.date
1846
1845
1847
1846
1848 def get_permission_name(key):
1847 def get_permission_name(key):
1849 return dict(Permission.PERMS).get(key)
1848 return dict(Permission.PERMS).get(key)
1850
1849
1851
1850
1852 def journal_filter_help():
1851 def journal_filter_help():
1853 return _(
1852 return _(
1854 'Example filter terms:\n' +
1853 'Example filter terms:\n' +
1855 ' repository:vcs\n' +
1854 ' repository:vcs\n' +
1856 ' username:marcin\n' +
1855 ' username:marcin\n' +
1857 ' action:*push*\n' +
1856 ' action:*push*\n' +
1858 ' ip:127.0.0.1\n' +
1857 ' ip:127.0.0.1\n' +
1859 ' date:20120101\n' +
1858 ' date:20120101\n' +
1860 ' date:[20120101100000 TO 20120102]\n' +
1859 ' date:[20120101100000 TO 20120102]\n' +
1861 '\n' +
1860 '\n' +
1862 'Generate wildcards using \'*\' character:\n' +
1861 'Generate wildcards using \'*\' character:\n' +
1863 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1862 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1864 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1863 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1865 '\n' +
1864 '\n' +
1866 'Optional AND / OR operators in queries\n' +
1865 'Optional AND / OR operators in queries\n' +
1867 ' "repository:vcs OR repository:test"\n' +
1866 ' "repository:vcs OR repository:test"\n' +
1868 ' "username:test AND repository:test*"\n'
1867 ' "username:test AND repository:test*"\n'
1869 )
1868 )
1870
1869
1871
1870
1872 def not_mapped_error(repo_name):
1871 def not_mapped_error(repo_name):
1873 flash(_('%s repository is not mapped to db perhaps'
1872 flash(_('%s repository is not mapped to db perhaps'
1874 ' it was created or renamed from the filesystem'
1873 ' it was created or renamed from the filesystem'
1875 ' please run the application again'
1874 ' please run the application again'
1876 ' in order to rescan repositories') % repo_name, category='error')
1875 ' in order to rescan repositories') % repo_name, category='error')
1877
1876
1878
1877
1879 def ip_range(ip_addr):
1878 def ip_range(ip_addr):
1880 from rhodecode.model.db import UserIpMap
1879 from rhodecode.model.db import UserIpMap
1881 s, e = UserIpMap._get_ip_range(ip_addr)
1880 s, e = UserIpMap._get_ip_range(ip_addr)
1882 return '%s - %s' % (s, e)
1881 return '%s - %s' % (s, e)
1883
1882
1884
1883
1885 def form(url, method='post', needs_csrf_token=True, **attrs):
1884 def form(url, method='post', needs_csrf_token=True, **attrs):
1886 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1885 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1887 if method.lower() != 'get' and needs_csrf_token:
1886 if method.lower() != 'get' and needs_csrf_token:
1888 raise Exception(
1887 raise Exception(
1889 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1888 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1890 'CSRF token. If the endpoint does not require such token you can ' +
1889 'CSRF token. If the endpoint does not require such token you can ' +
1891 'explicitly set the parameter needs_csrf_token to false.')
1890 'explicitly set the parameter needs_csrf_token to false.')
1892
1891
1893 return wh_form(url, method=method, **attrs)
1892 return wh_form(url, method=method, **attrs)
1894
1893
1895
1894
1896 def secure_form(url, method="POST", multipart=False, **attrs):
1895 def secure_form(url, method="POST", multipart=False, **attrs):
1897 """Start a form tag that points the action to an url. This
1896 """Start a form tag that points the action to an url. This
1898 form tag will also include the hidden field containing
1897 form tag will also include the hidden field containing
1899 the auth token.
1898 the auth token.
1900
1899
1901 The url options should be given either as a string, or as a
1900 The url options should be given either as a string, or as a
1902 ``url()`` function. The method for the form defaults to POST.
1901 ``url()`` function. The method for the form defaults to POST.
1903
1902
1904 Options:
1903 Options:
1905
1904
1906 ``multipart``
1905 ``multipart``
1907 If set to True, the enctype is set to "multipart/form-data".
1906 If set to True, the enctype is set to "multipart/form-data".
1908 ``method``
1907 ``method``
1909 The method to use when submitting the form, usually either
1908 The method to use when submitting the form, usually either
1910 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1909 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1911 hidden input with name _method is added to simulate the verb
1910 hidden input with name _method is added to simulate the verb
1912 over POST.
1911 over POST.
1913
1912
1914 """
1913 """
1915 from webhelpers.pylonslib.secure_form import insecure_form
1914 from webhelpers.pylonslib.secure_form import insecure_form
1916 form = insecure_form(url, method, multipart, **attrs)
1915 form = insecure_form(url, method, multipart, **attrs)
1917 token = csrf_input()
1916 token = csrf_input()
1918 return literal("%s\n%s" % (form, token))
1917 return literal("%s\n%s" % (form, token))
1919
1918
1920 def csrf_input():
1919 def csrf_input():
1921 return literal(
1920 return literal(
1922 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1921 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1923 csrf_token_key, csrf_token_key, get_csrf_token()))
1922 csrf_token_key, csrf_token_key, get_csrf_token()))
1924
1923
1925 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1924 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1926 select_html = select(name, selected, options, **attrs)
1925 select_html = select(name, selected, options, **attrs)
1927 select2 = """
1926 select2 = """
1928 <script>
1927 <script>
1929 $(document).ready(function() {
1928 $(document).ready(function() {
1930 $('#%s').select2({
1929 $('#%s').select2({
1931 containerCssClass: 'drop-menu',
1930 containerCssClass: 'drop-menu',
1932 dropdownCssClass: 'drop-menu-dropdown',
1931 dropdownCssClass: 'drop-menu-dropdown',
1933 dropdownAutoWidth: true%s
1932 dropdownAutoWidth: true%s
1934 });
1933 });
1935 });
1934 });
1936 </script>
1935 </script>
1937 """
1936 """
1938 filter_option = """,
1937 filter_option = """,
1939 minimumResultsForSearch: -1
1938 minimumResultsForSearch: -1
1940 """
1939 """
1941 input_id = attrs.get('id') or name
1940 input_id = attrs.get('id') or name
1942 filter_enabled = "" if enable_filter else filter_option
1941 filter_enabled = "" if enable_filter else filter_option
1943 select_script = literal(select2 % (input_id, filter_enabled))
1942 select_script = literal(select2 % (input_id, filter_enabled))
1944
1943
1945 return literal(select_html+select_script)
1944 return literal(select_html+select_script)
1946
1945
1947
1946
1948 def get_visual_attr(tmpl_context_var, attr_name):
1947 def get_visual_attr(tmpl_context_var, attr_name):
1949 """
1948 """
1950 A safe way to get a variable from visual variable of template context
1949 A safe way to get a variable from visual variable of template context
1951
1950
1952 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1951 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1953 :param attr_name: name of the attribute we fetch from the c.visual
1952 :param attr_name: name of the attribute we fetch from the c.visual
1954 """
1953 """
1955 visual = getattr(tmpl_context_var, 'visual', None)
1954 visual = getattr(tmpl_context_var, 'visual', None)
1956 if not visual:
1955 if not visual:
1957 return
1956 return
1958 else:
1957 else:
1959 return getattr(visual, attr_name, None)
1958 return getattr(visual, attr_name, None)
1960
1959
1961
1960
1962 def get_last_path_part(file_node):
1961 def get_last_path_part(file_node):
1963 if not file_node.path:
1962 if not file_node.path:
1964 return u''
1963 return u''
1965
1964
1966 path = safe_unicode(file_node.path.split('/')[-1])
1965 path = safe_unicode(file_node.path.split('/')[-1])
1967 return u'../' + path
1966 return u'../' + path
1968
1967
1969
1968
1970 def route_path(*args, **kwds):
1969 def route_path(*args, **kwds):
1971 """
1970 """
1972 Wrapper around pyramids `route_path` function. It is used to generate
1971 Wrapper around pyramids `route_path` function. It is used to generate
1973 URLs from within pylons views or templates. This will be removed when
1972 URLs from within pylons views or templates. This will be removed when
1974 pyramid migration if finished.
1973 pyramid migration if finished.
1975 """
1974 """
1976 req = get_current_request()
1975 req = get_current_request()
1977 return req.route_path(*args, **kwds)
1976 return req.route_path(*args, **kwds)
1978
1977
1979
1978
1980 def route_path_or_none(*args, **kwargs):
1979 def route_path_or_none(*args, **kwargs):
1981 try:
1980 try:
1982 return route_path(*args, **kwargs)
1981 return route_path(*args, **kwargs)
1983 except KeyError:
1982 except KeyError:
1984 return None
1983 return None
1985
1984
1986
1985
1987 def static_url(*args, **kwds):
1986 def static_url(*args, **kwds):
1988 """
1987 """
1989 Wrapper around pyramids `route_path` function. It is used to generate
1988 Wrapper around pyramids `route_path` function. It is used to generate
1990 URLs from within pylons views or templates. This will be removed when
1989 URLs from within pylons views or templates. This will be removed when
1991 pyramid migration if finished.
1990 pyramid migration if finished.
1992 """
1991 """
1993 req = get_current_request()
1992 req = get_current_request()
1994 return req.static_url(*args, **kwds)
1993 return req.static_url(*args, **kwds)
1995
1994
1996
1995
1997 def resource_path(*args, **kwds):
1996 def resource_path(*args, **kwds):
1998 """
1997 """
1999 Wrapper around pyramids `route_path` function. It is used to generate
1998 Wrapper around pyramids `route_path` function. It is used to generate
2000 URLs from within pylons views or templates. This will be removed when
1999 URLs from within pylons views or templates. This will be removed when
2001 pyramid migration if finished.
2000 pyramid migration if finished.
2002 """
2001 """
2003 req = get_current_request()
2002 req = get_current_request()
2004 return req.resource_path(*args, **kwds)
2003 return req.resource_path(*args, **kwds)
General Comments 0
You need to be logged in to leave comments. Login now