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