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