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