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