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