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