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