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