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