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