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