##// END OF EJS Templates
notifications: properly inject the custom email headers into templates.
dan -
r4461:2b31e8f8 stable
parent child Browse files
Show More
@@ -1,800 +1,800 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Utilities library for RhodeCode
22 Utilities library for RhodeCode
23 """
23 """
24
24
25 import datetime
25 import datetime
26 import decorator
26 import decorator
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30 import re
30 import re
31 import sys
31 import sys
32 import shutil
32 import shutil
33 import socket
33 import socket
34 import tempfile
34 import tempfile
35 import traceback
35 import traceback
36 import tarfile
36 import tarfile
37 import warnings
37 import warnings
38 import hashlib
38 import hashlib
39 from os.path import join as jn
39 from os.path import join as jn
40
40
41 import paste
41 import paste
42 import pkg_resources
42 import pkg_resources
43 from webhelpers2.text import collapse, remove_formatting
43 from webhelpers2.text import collapse, remove_formatting
44 from mako import exceptions
44 from mako import exceptions
45 from pyramid.threadlocal import get_current_registry
45 from pyramid.threadlocal import get_current_registry
46 from rhodecode.lib.request import Request
46 from rhodecode.lib.request import Request
47
47
48 from rhodecode.lib.vcs.backends.base import Config
48 from rhodecode.lib.vcs.backends.base import Config
49 from rhodecode.lib.vcs.exceptions import VCSError
49 from rhodecode.lib.vcs.exceptions import VCSError
50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
51 from rhodecode.lib.utils2 import (
51 from rhodecode.lib.utils2 import (
52 safe_str, safe_unicode, get_current_rhodecode_user, md5, sha1)
52 safe_str, safe_unicode, get_current_rhodecode_user, md5, sha1)
53 from rhodecode.model import meta
53 from rhodecode.model import meta
54 from rhodecode.model.db import (
54 from rhodecode.model.db import (
55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
62
62
63 # String which contains characters that are not allowed in slug names for
63 # String which contains characters that are not allowed in slug names for
64 # repositories or repository groups. It is properly escaped to use it in
64 # repositories or repository groups. It is properly escaped to use it in
65 # regular expressions.
65 # regular expressions.
66 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
66 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
67
67
68 # Regex that matches forbidden characters in repo/group slugs.
68 # Regex that matches forbidden characters in repo/group slugs.
69 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
69 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
70
70
71 # Regex that matches allowed characters in repo/group slugs.
71 # Regex that matches allowed characters in repo/group slugs.
72 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
72 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
73
73
74 # Regex that matches whole repo/group slugs.
74 # Regex that matches whole repo/group slugs.
75 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
75 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
76
76
77 _license_cache = None
77 _license_cache = None
78
78
79
79
80 def repo_name_slug(value):
80 def repo_name_slug(value):
81 """
81 """
82 Return slug of name of repository
82 Return slug of name of repository
83 This function is called on each creation/modification
83 This function is called on each creation/modification
84 of repository to prevent bad names in repo
84 of repository to prevent bad names in repo
85 """
85 """
86 replacement_char = '-'
86 replacement_char = '-'
87
87
88 slug = remove_formatting(value)
88 slug = remove_formatting(value)
89 slug = SLUG_BAD_CHAR_RE.sub('', slug)
89 slug = SLUG_BAD_CHAR_RE.sub('', slug)
90 slug = re.sub('[\s]+', '-', slug)
90 slug = re.sub('[\s]+', '-', slug)
91 slug = collapse(slug, replacement_char)
91 slug = collapse(slug, replacement_char)
92 return slug
92 return slug
93
93
94
94
95 #==============================================================================
95 #==============================================================================
96 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
96 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
97 #==============================================================================
97 #==============================================================================
98 def get_repo_slug(request):
98 def get_repo_slug(request):
99 _repo = ''
99 _repo = ''
100
100
101 if hasattr(request, 'db_repo'):
101 if hasattr(request, 'db_repo'):
102 # if our requests has set db reference use it for name, this
102 # if our requests has set db reference use it for name, this
103 # translates the example.com/_<id> into proper repo names
103 # translates the example.com/_<id> into proper repo names
104 _repo = request.db_repo.repo_name
104 _repo = request.db_repo.repo_name
105 elif getattr(request, 'matchdict', None):
105 elif getattr(request, 'matchdict', None):
106 # pyramid
106 # pyramid
107 _repo = request.matchdict.get('repo_name')
107 _repo = request.matchdict.get('repo_name')
108
108
109 if _repo:
109 if _repo:
110 _repo = _repo.rstrip('/')
110 _repo = _repo.rstrip('/')
111 return _repo
111 return _repo
112
112
113
113
114 def get_repo_group_slug(request):
114 def get_repo_group_slug(request):
115 _group = ''
115 _group = ''
116 if hasattr(request, 'db_repo_group'):
116 if hasattr(request, 'db_repo_group'):
117 # if our requests has set db reference use it for name, this
117 # if our requests has set db reference use it for name, this
118 # translates the example.com/_<id> into proper repo group names
118 # translates the example.com/_<id> into proper repo group names
119 _group = request.db_repo_group.group_name
119 _group = request.db_repo_group.group_name
120 elif getattr(request, 'matchdict', None):
120 elif getattr(request, 'matchdict', None):
121 # pyramid
121 # pyramid
122 _group = request.matchdict.get('repo_group_name')
122 _group = request.matchdict.get('repo_group_name')
123
123
124 if _group:
124 if _group:
125 _group = _group.rstrip('/')
125 _group = _group.rstrip('/')
126 return _group
126 return _group
127
127
128
128
129 def get_user_group_slug(request):
129 def get_user_group_slug(request):
130 _user_group = ''
130 _user_group = ''
131
131
132 if hasattr(request, 'db_user_group'):
132 if hasattr(request, 'db_user_group'):
133 _user_group = request.db_user_group.users_group_name
133 _user_group = request.db_user_group.users_group_name
134 elif getattr(request, 'matchdict', None):
134 elif getattr(request, 'matchdict', None):
135 # pyramid
135 # pyramid
136 _user_group = request.matchdict.get('user_group_id')
136 _user_group = request.matchdict.get('user_group_id')
137 _user_group_name = request.matchdict.get('user_group_name')
137 _user_group_name = request.matchdict.get('user_group_name')
138 try:
138 try:
139 if _user_group:
139 if _user_group:
140 _user_group = UserGroup.get(_user_group)
140 _user_group = UserGroup.get(_user_group)
141 elif _user_group_name:
141 elif _user_group_name:
142 _user_group = UserGroup.get_by_group_name(_user_group_name)
142 _user_group = UserGroup.get_by_group_name(_user_group_name)
143
143
144 if _user_group:
144 if _user_group:
145 _user_group = _user_group.users_group_name
145 _user_group = _user_group.users_group_name
146 except Exception:
146 except Exception:
147 log.exception('Failed to get user group by id and name')
147 log.exception('Failed to get user group by id and name')
148 # catch all failures here
148 # catch all failures here
149 return None
149 return None
150
150
151 return _user_group
151 return _user_group
152
152
153
153
154 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
154 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
155 """
155 """
156 Scans given path for repos and return (name,(type,path)) tuple
156 Scans given path for repos and return (name,(type,path)) tuple
157
157
158 :param path: path to scan for repositories
158 :param path: path to scan for repositories
159 :param recursive: recursive search and return names with subdirs in front
159 :param recursive: recursive search and return names with subdirs in front
160 """
160 """
161
161
162 # remove ending slash for better results
162 # remove ending slash for better results
163 path = path.rstrip(os.sep)
163 path = path.rstrip(os.sep)
164 log.debug('now scanning in %s location recursive:%s...', path, recursive)
164 log.debug('now scanning in %s location recursive:%s...', path, recursive)
165
165
166 def _get_repos(p):
166 def _get_repos(p):
167 dirpaths = _get_dirpaths(p)
167 dirpaths = _get_dirpaths(p)
168 if not _is_dir_writable(p):
168 if not _is_dir_writable(p):
169 log.warning('repo path without write access: %s', p)
169 log.warning('repo path without write access: %s', p)
170
170
171 for dirpath in dirpaths:
171 for dirpath in dirpaths:
172 if os.path.isfile(os.path.join(p, dirpath)):
172 if os.path.isfile(os.path.join(p, dirpath)):
173 continue
173 continue
174 cur_path = os.path.join(p, dirpath)
174 cur_path = os.path.join(p, dirpath)
175
175
176 # skip removed repos
176 # skip removed repos
177 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
177 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
178 continue
178 continue
179
179
180 #skip .<somethin> dirs
180 #skip .<somethin> dirs
181 if dirpath.startswith('.'):
181 if dirpath.startswith('.'):
182 continue
182 continue
183
183
184 try:
184 try:
185 scm_info = get_scm(cur_path)
185 scm_info = get_scm(cur_path)
186 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
186 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
187 except VCSError:
187 except VCSError:
188 if not recursive:
188 if not recursive:
189 continue
189 continue
190 #check if this dir containts other repos for recursive scan
190 #check if this dir containts other repos for recursive scan
191 rec_path = os.path.join(p, dirpath)
191 rec_path = os.path.join(p, dirpath)
192 if os.path.isdir(rec_path):
192 if os.path.isdir(rec_path):
193 for inner_scm in _get_repos(rec_path):
193 for inner_scm in _get_repos(rec_path):
194 yield inner_scm
194 yield inner_scm
195
195
196 return _get_repos(path)
196 return _get_repos(path)
197
197
198
198
199 def _get_dirpaths(p):
199 def _get_dirpaths(p):
200 try:
200 try:
201 # OS-independable way of checking if we have at least read-only
201 # OS-independable way of checking if we have at least read-only
202 # access or not.
202 # access or not.
203 dirpaths = os.listdir(p)
203 dirpaths = os.listdir(p)
204 except OSError:
204 except OSError:
205 log.warning('ignoring repo path without read access: %s', p)
205 log.warning('ignoring repo path without read access: %s', p)
206 return []
206 return []
207
207
208 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
208 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
209 # decode paths and suddenly returns unicode objects itself. The items it
209 # decode paths and suddenly returns unicode objects itself. The items it
210 # cannot decode are returned as strings and cause issues.
210 # cannot decode are returned as strings and cause issues.
211 #
211 #
212 # Those paths are ignored here until a solid solution for path handling has
212 # Those paths are ignored here until a solid solution for path handling has
213 # been built.
213 # been built.
214 expected_type = type(p)
214 expected_type = type(p)
215
215
216 def _has_correct_type(item):
216 def _has_correct_type(item):
217 if type(item) is not expected_type:
217 if type(item) is not expected_type:
218 log.error(
218 log.error(
219 u"Ignoring path %s since it cannot be decoded into unicode.",
219 u"Ignoring path %s since it cannot be decoded into unicode.",
220 # Using "repr" to make sure that we see the byte value in case
220 # Using "repr" to make sure that we see the byte value in case
221 # of support.
221 # of support.
222 repr(item))
222 repr(item))
223 return False
223 return False
224 return True
224 return True
225
225
226 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
226 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
227
227
228 return dirpaths
228 return dirpaths
229
229
230
230
231 def _is_dir_writable(path):
231 def _is_dir_writable(path):
232 """
232 """
233 Probe if `path` is writable.
233 Probe if `path` is writable.
234
234
235 Due to trouble on Cygwin / Windows, this is actually probing if it is
235 Due to trouble on Cygwin / Windows, this is actually probing if it is
236 possible to create a file inside of `path`, stat does not produce reliable
236 possible to create a file inside of `path`, stat does not produce reliable
237 results in this case.
237 results in this case.
238 """
238 """
239 try:
239 try:
240 with tempfile.TemporaryFile(dir=path):
240 with tempfile.TemporaryFile(dir=path):
241 pass
241 pass
242 except OSError:
242 except OSError:
243 return False
243 return False
244 return True
244 return True
245
245
246
246
247 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None, config=None):
247 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None, config=None):
248 """
248 """
249 Returns True if given path is a valid repository False otherwise.
249 Returns True if given path is a valid repository False otherwise.
250 If expect_scm param is given also, compare if given scm is the same
250 If expect_scm param is given also, compare if given scm is the same
251 as expected from scm parameter. If explicit_scm is given don't try to
251 as expected from scm parameter. If explicit_scm is given don't try to
252 detect the scm, just use the given one to check if repo is valid
252 detect the scm, just use the given one to check if repo is valid
253
253
254 :param repo_name:
254 :param repo_name:
255 :param base_path:
255 :param base_path:
256 :param expect_scm:
256 :param expect_scm:
257 :param explicit_scm:
257 :param explicit_scm:
258 :param config:
258 :param config:
259
259
260 :return True: if given path is a valid repository
260 :return True: if given path is a valid repository
261 """
261 """
262 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
262 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
263 log.debug('Checking if `%s` is a valid path for repository. '
263 log.debug('Checking if `%s` is a valid path for repository. '
264 'Explicit type: %s', repo_name, explicit_scm)
264 'Explicit type: %s', repo_name, explicit_scm)
265
265
266 try:
266 try:
267 if explicit_scm:
267 if explicit_scm:
268 detected_scms = [get_scm_backend(explicit_scm)(
268 detected_scms = [get_scm_backend(explicit_scm)(
269 full_path, config=config).alias]
269 full_path, config=config).alias]
270 else:
270 else:
271 detected_scms = get_scm(full_path)
271 detected_scms = get_scm(full_path)
272
272
273 if expect_scm:
273 if expect_scm:
274 return detected_scms[0] == expect_scm
274 return detected_scms[0] == expect_scm
275 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
275 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
276 return True
276 return True
277 except VCSError:
277 except VCSError:
278 log.debug('path: %s is not a valid repo !', full_path)
278 log.debug('path: %s is not a valid repo !', full_path)
279 return False
279 return False
280
280
281
281
282 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
282 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
283 """
283 """
284 Returns True if given path is a repository group, False otherwise
284 Returns True if given path is a repository group, False otherwise
285
285
286 :param repo_name:
286 :param repo_name:
287 :param base_path:
287 :param base_path:
288 """
288 """
289 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
289 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
290 log.debug('Checking if `%s` is a valid path for repository group',
290 log.debug('Checking if `%s` is a valid path for repository group',
291 repo_group_name)
291 repo_group_name)
292
292
293 # check if it's not a repo
293 # check if it's not a repo
294 if is_valid_repo(repo_group_name, base_path):
294 if is_valid_repo(repo_group_name, base_path):
295 log.debug('Repo called %s exist, it is not a valid repo group', repo_group_name)
295 log.debug('Repo called %s exist, it is not a valid repo group', repo_group_name)
296 return False
296 return False
297
297
298 try:
298 try:
299 # we need to check bare git repos at higher level
299 # we need to check bare git repos at higher level
300 # since we might match branches/hooks/info/objects or possible
300 # since we might match branches/hooks/info/objects or possible
301 # other things inside bare git repo
301 # other things inside bare git repo
302 maybe_repo = os.path.dirname(full_path)
302 maybe_repo = os.path.dirname(full_path)
303 if maybe_repo == base_path:
303 if maybe_repo == base_path:
304 # skip root level repo check, we know root location CANNOT BE a repo group
304 # skip root level repo check, we know root location CANNOT BE a repo group
305 return False
305 return False
306
306
307 scm_ = get_scm(maybe_repo)
307 scm_ = get_scm(maybe_repo)
308 log.debug('path: %s is a vcs object:%s, not valid repo group', full_path, scm_)
308 log.debug('path: %s is a vcs object:%s, not valid repo group', full_path, scm_)
309 return False
309 return False
310 except VCSError:
310 except VCSError:
311 pass
311 pass
312
312
313 # check if it's a valid path
313 # check if it's a valid path
314 if skip_path_check or os.path.isdir(full_path):
314 if skip_path_check or os.path.isdir(full_path):
315 log.debug('path: %s is a valid repo group !', full_path)
315 log.debug('path: %s is a valid repo group !', full_path)
316 return True
316 return True
317
317
318 log.debug('path: %s is not a valid repo group !', full_path)
318 log.debug('path: %s is not a valid repo group !', full_path)
319 return False
319 return False
320
320
321
321
322 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
322 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
323 while True:
323 while True:
324 ok = raw_input(prompt)
324 ok = raw_input(prompt)
325 if ok.lower() in ('y', 'ye', 'yes'):
325 if ok.lower() in ('y', 'ye', 'yes'):
326 return True
326 return True
327 if ok.lower() in ('n', 'no', 'nop', 'nope'):
327 if ok.lower() in ('n', 'no', 'nop', 'nope'):
328 return False
328 return False
329 retries = retries - 1
329 retries = retries - 1
330 if retries < 0:
330 if retries < 0:
331 raise IOError
331 raise IOError
332 print(complaint)
332 print(complaint)
333
333
334 # propagated from mercurial documentation
334 # propagated from mercurial documentation
335 ui_sections = [
335 ui_sections = [
336 'alias', 'auth',
336 'alias', 'auth',
337 'decode/encode', 'defaults',
337 'decode/encode', 'defaults',
338 'diff', 'email',
338 'diff', 'email',
339 'extensions', 'format',
339 'extensions', 'format',
340 'merge-patterns', 'merge-tools',
340 'merge-patterns', 'merge-tools',
341 'hooks', 'http_proxy',
341 'hooks', 'http_proxy',
342 'smtp', 'patch',
342 'smtp', 'patch',
343 'paths', 'profiling',
343 'paths', 'profiling',
344 'server', 'trusted',
344 'server', 'trusted',
345 'ui', 'web', ]
345 'ui', 'web', ]
346
346
347
347
348 def config_data_from_db(clear_session=True, repo=None):
348 def config_data_from_db(clear_session=True, repo=None):
349 """
349 """
350 Read the configuration data from the database and return configuration
350 Read the configuration data from the database and return configuration
351 tuples.
351 tuples.
352 """
352 """
353 from rhodecode.model.settings import VcsSettingsModel
353 from rhodecode.model.settings import VcsSettingsModel
354
354
355 config = []
355 config = []
356
356
357 sa = meta.Session()
357 sa = meta.Session()
358 settings_model = VcsSettingsModel(repo=repo, sa=sa)
358 settings_model = VcsSettingsModel(repo=repo, sa=sa)
359
359
360 ui_settings = settings_model.get_ui_settings()
360 ui_settings = settings_model.get_ui_settings()
361
361
362 ui_data = []
362 ui_data = []
363 for setting in ui_settings:
363 for setting in ui_settings:
364 if setting.active:
364 if setting.active:
365 ui_data.append((setting.section, setting.key, setting.value))
365 ui_data.append((setting.section, setting.key, setting.value))
366 config.append((
366 config.append((
367 safe_str(setting.section), safe_str(setting.key),
367 safe_str(setting.section), safe_str(setting.key),
368 safe_str(setting.value)))
368 safe_str(setting.value)))
369 if setting.key == 'push_ssl':
369 if setting.key == 'push_ssl':
370 # force set push_ssl requirement to False, rhodecode
370 # force set push_ssl requirement to False, rhodecode
371 # handles that
371 # handles that
372 config.append((
372 config.append((
373 safe_str(setting.section), safe_str(setting.key), False))
373 safe_str(setting.section), safe_str(setting.key), False))
374 log.debug(
374 log.debug(
375 'settings ui from db@repo[%s]: %s',
375 'settings ui from db@repo[%s]: %s',
376 repo,
376 repo,
377 ','.join(map(lambda s: '[{}] {}={}'.format(*s), ui_data)))
377 ','.join(map(lambda s: '[{}] {}={}'.format(*s), ui_data)))
378 if clear_session:
378 if clear_session:
379 meta.Session.remove()
379 meta.Session.remove()
380
380
381 # TODO: mikhail: probably it makes no sense to re-read hooks information.
381 # TODO: mikhail: probably it makes no sense to re-read hooks information.
382 # It's already there and activated/deactivated
382 # It's already there and activated/deactivated
383 skip_entries = []
383 skip_entries = []
384 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
384 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
385 if 'pull' not in enabled_hook_classes:
385 if 'pull' not in enabled_hook_classes:
386 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
386 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
387 if 'push' not in enabled_hook_classes:
387 if 'push' not in enabled_hook_classes:
388 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
388 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
389 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
389 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
390 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
390 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
391
391
392 config = [entry for entry in config if entry[:2] not in skip_entries]
392 config = [entry for entry in config if entry[:2] not in skip_entries]
393
393
394 return config
394 return config
395
395
396
396
397 def make_db_config(clear_session=True, repo=None):
397 def make_db_config(clear_session=True, repo=None):
398 """
398 """
399 Create a :class:`Config` instance based on the values in the database.
399 Create a :class:`Config` instance based on the values in the database.
400 """
400 """
401 config = Config()
401 config = Config()
402 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
402 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
403 for section, option, value in config_data:
403 for section, option, value in config_data:
404 config.set(section, option, value)
404 config.set(section, option, value)
405 return config
405 return config
406
406
407
407
408 def get_enabled_hook_classes(ui_settings):
408 def get_enabled_hook_classes(ui_settings):
409 """
409 """
410 Return the enabled hook classes.
410 Return the enabled hook classes.
411
411
412 :param ui_settings: List of ui_settings as returned
412 :param ui_settings: List of ui_settings as returned
413 by :meth:`VcsSettingsModel.get_ui_settings`
413 by :meth:`VcsSettingsModel.get_ui_settings`
414
414
415 :return: a list with the enabled hook classes. The order is not guaranteed.
415 :return: a list with the enabled hook classes. The order is not guaranteed.
416 :rtype: list
416 :rtype: list
417 """
417 """
418 enabled_hooks = []
418 enabled_hooks = []
419 active_hook_keys = [
419 active_hook_keys = [
420 key for section, key, value, active in ui_settings
420 key for section, key, value, active in ui_settings
421 if section == 'hooks' and active]
421 if section == 'hooks' and active]
422
422
423 hook_names = {
423 hook_names = {
424 RhodeCodeUi.HOOK_PUSH: 'push',
424 RhodeCodeUi.HOOK_PUSH: 'push',
425 RhodeCodeUi.HOOK_PULL: 'pull',
425 RhodeCodeUi.HOOK_PULL: 'pull',
426 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
426 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
427 }
427 }
428
428
429 for key in active_hook_keys:
429 for key in active_hook_keys:
430 hook = hook_names.get(key)
430 hook = hook_names.get(key)
431 if hook:
431 if hook:
432 enabled_hooks.append(hook)
432 enabled_hooks.append(hook)
433
433
434 return enabled_hooks
434 return enabled_hooks
435
435
436
436
437 def set_rhodecode_config(config):
437 def set_rhodecode_config(config):
438 """
438 """
439 Updates pyramid config with new settings from database
439 Updates pyramid config with new settings from database
440
440
441 :param config:
441 :param config:
442 """
442 """
443 from rhodecode.model.settings import SettingsModel
443 from rhodecode.model.settings import SettingsModel
444 app_settings = SettingsModel().get_all_settings()
444 app_settings = SettingsModel().get_all_settings()
445
445
446 for k, v in app_settings.items():
446 for k, v in app_settings.items():
447 config[k] = v
447 config[k] = v
448
448
449
449
450 def get_rhodecode_realm():
450 def get_rhodecode_realm():
451 """
451 """
452 Return the rhodecode realm from database.
452 Return the rhodecode realm from database.
453 """
453 """
454 from rhodecode.model.settings import SettingsModel
454 from rhodecode.model.settings import SettingsModel
455 realm = SettingsModel().get_setting_by_name('realm')
455 realm = SettingsModel().get_setting_by_name('realm')
456 return safe_str(realm.app_settings_value)
456 return safe_str(realm.app_settings_value)
457
457
458
458
459 def get_rhodecode_base_path():
459 def get_rhodecode_base_path():
460 """
460 """
461 Returns the base path. The base path is the filesystem path which points
461 Returns the base path. The base path is the filesystem path which points
462 to the repository store.
462 to the repository store.
463 """
463 """
464 from rhodecode.model.settings import SettingsModel
464 from rhodecode.model.settings import SettingsModel
465 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
465 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
466 return safe_str(paths_ui.ui_value)
466 return safe_str(paths_ui.ui_value)
467
467
468
468
469 def map_groups(path):
469 def map_groups(path):
470 """
470 """
471 Given a full path to a repository, create all nested groups that this
471 Given a full path to a repository, create all nested groups that this
472 repo is inside. This function creates parent-child relationships between
472 repo is inside. This function creates parent-child relationships between
473 groups and creates default perms for all new groups.
473 groups and creates default perms for all new groups.
474
474
475 :param paths: full path to repository
475 :param paths: full path to repository
476 """
476 """
477 from rhodecode.model.repo_group import RepoGroupModel
477 from rhodecode.model.repo_group import RepoGroupModel
478 sa = meta.Session()
478 sa = meta.Session()
479 groups = path.split(Repository.NAME_SEP)
479 groups = path.split(Repository.NAME_SEP)
480 parent = None
480 parent = None
481 group = None
481 group = None
482
482
483 # last element is repo in nested groups structure
483 # last element is repo in nested groups structure
484 groups = groups[:-1]
484 groups = groups[:-1]
485 rgm = RepoGroupModel(sa)
485 rgm = RepoGroupModel(sa)
486 owner = User.get_first_super_admin()
486 owner = User.get_first_super_admin()
487 for lvl, group_name in enumerate(groups):
487 for lvl, group_name in enumerate(groups):
488 group_name = '/'.join(groups[:lvl] + [group_name])
488 group_name = '/'.join(groups[:lvl] + [group_name])
489 group = RepoGroup.get_by_group_name(group_name)
489 group = RepoGroup.get_by_group_name(group_name)
490 desc = '%s group' % group_name
490 desc = '%s group' % group_name
491
491
492 # skip folders that are now removed repos
492 # skip folders that are now removed repos
493 if REMOVED_REPO_PAT.match(group_name):
493 if REMOVED_REPO_PAT.match(group_name):
494 break
494 break
495
495
496 if group is None:
496 if group is None:
497 log.debug('creating group level: %s group_name: %s',
497 log.debug('creating group level: %s group_name: %s',
498 lvl, group_name)
498 lvl, group_name)
499 group = RepoGroup(group_name, parent)
499 group = RepoGroup(group_name, parent)
500 group.group_description = desc
500 group.group_description = desc
501 group.user = owner
501 group.user = owner
502 sa.add(group)
502 sa.add(group)
503 perm_obj = rgm._create_default_perms(group)
503 perm_obj = rgm._create_default_perms(group)
504 sa.add(perm_obj)
504 sa.add(perm_obj)
505 sa.flush()
505 sa.flush()
506
506
507 parent = group
507 parent = group
508 return group
508 return group
509
509
510
510
511 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
511 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
512 """
512 """
513 maps all repos given in initial_repo_list, non existing repositories
513 maps all repos given in initial_repo_list, non existing repositories
514 are created, if remove_obsolete is True it also checks for db entries
514 are created, if remove_obsolete is True it also checks for db entries
515 that are not in initial_repo_list and removes them.
515 that are not in initial_repo_list and removes them.
516
516
517 :param initial_repo_list: list of repositories found by scanning methods
517 :param initial_repo_list: list of repositories found by scanning methods
518 :param remove_obsolete: check for obsolete entries in database
518 :param remove_obsolete: check for obsolete entries in database
519 """
519 """
520 from rhodecode.model.repo import RepoModel
520 from rhodecode.model.repo import RepoModel
521 from rhodecode.model.repo_group import RepoGroupModel
521 from rhodecode.model.repo_group import RepoGroupModel
522 from rhodecode.model.settings import SettingsModel
522 from rhodecode.model.settings import SettingsModel
523
523
524 sa = meta.Session()
524 sa = meta.Session()
525 repo_model = RepoModel()
525 repo_model = RepoModel()
526 user = User.get_first_super_admin()
526 user = User.get_first_super_admin()
527 added = []
527 added = []
528
528
529 # creation defaults
529 # creation defaults
530 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
530 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
531 enable_statistics = defs.get('repo_enable_statistics')
531 enable_statistics = defs.get('repo_enable_statistics')
532 enable_locking = defs.get('repo_enable_locking')
532 enable_locking = defs.get('repo_enable_locking')
533 enable_downloads = defs.get('repo_enable_downloads')
533 enable_downloads = defs.get('repo_enable_downloads')
534 private = defs.get('repo_private')
534 private = defs.get('repo_private')
535
535
536 for name, repo in initial_repo_list.items():
536 for name, repo in initial_repo_list.items():
537 group = map_groups(name)
537 group = map_groups(name)
538 unicode_name = safe_unicode(name)
538 unicode_name = safe_unicode(name)
539 db_repo = repo_model.get_by_repo_name(unicode_name)
539 db_repo = repo_model.get_by_repo_name(unicode_name)
540 # found repo that is on filesystem not in RhodeCode database
540 # found repo that is on filesystem not in RhodeCode database
541 if not db_repo:
541 if not db_repo:
542 log.info('repository %s not found, creating now', name)
542 log.info('repository %s not found, creating now', name)
543 added.append(name)
543 added.append(name)
544 desc = (repo.description
544 desc = (repo.description
545 if repo.description != 'unknown'
545 if repo.description != 'unknown'
546 else '%s repository' % name)
546 else '%s repository' % name)
547
547
548 db_repo = repo_model._create_repo(
548 db_repo = repo_model._create_repo(
549 repo_name=name,
549 repo_name=name,
550 repo_type=repo.alias,
550 repo_type=repo.alias,
551 description=desc,
551 description=desc,
552 repo_group=getattr(group, 'group_id', None),
552 repo_group=getattr(group, 'group_id', None),
553 owner=user,
553 owner=user,
554 enable_locking=enable_locking,
554 enable_locking=enable_locking,
555 enable_downloads=enable_downloads,
555 enable_downloads=enable_downloads,
556 enable_statistics=enable_statistics,
556 enable_statistics=enable_statistics,
557 private=private,
557 private=private,
558 state=Repository.STATE_CREATED
558 state=Repository.STATE_CREATED
559 )
559 )
560 sa.commit()
560 sa.commit()
561 # we added that repo just now, and make sure we updated server info
561 # we added that repo just now, and make sure we updated server info
562 if db_repo.repo_type == 'git':
562 if db_repo.repo_type == 'git':
563 git_repo = db_repo.scm_instance()
563 git_repo = db_repo.scm_instance()
564 # update repository server-info
564 # update repository server-info
565 log.debug('Running update server info')
565 log.debug('Running update server info')
566 git_repo._update_server_info()
566 git_repo._update_server_info()
567
567
568 db_repo.update_commit_cache()
568 db_repo.update_commit_cache()
569
569
570 config = db_repo._config
570 config = db_repo._config
571 config.set('extensions', 'largefiles', '')
571 config.set('extensions', 'largefiles', '')
572 repo = db_repo.scm_instance(config=config)
572 repo = db_repo.scm_instance(config=config)
573 repo.install_hooks()
573 repo.install_hooks()
574
574
575 removed = []
575 removed = []
576 if remove_obsolete:
576 if remove_obsolete:
577 # remove from database those repositories that are not in the filesystem
577 # remove from database those repositories that are not in the filesystem
578 for repo in sa.query(Repository).all():
578 for repo in sa.query(Repository).all():
579 if repo.repo_name not in initial_repo_list.keys():
579 if repo.repo_name not in initial_repo_list.keys():
580 log.debug("Removing non-existing repository found in db `%s`",
580 log.debug("Removing non-existing repository found in db `%s`",
581 repo.repo_name)
581 repo.repo_name)
582 try:
582 try:
583 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
583 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
584 sa.commit()
584 sa.commit()
585 removed.append(repo.repo_name)
585 removed.append(repo.repo_name)
586 except Exception:
586 except Exception:
587 # don't hold further removals on error
587 # don't hold further removals on error
588 log.error(traceback.format_exc())
588 log.error(traceback.format_exc())
589 sa.rollback()
589 sa.rollback()
590
590
591 def splitter(full_repo_name):
591 def splitter(full_repo_name):
592 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
592 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
593 gr_name = None
593 gr_name = None
594 if len(_parts) == 2:
594 if len(_parts) == 2:
595 gr_name = _parts[0]
595 gr_name = _parts[0]
596 return gr_name
596 return gr_name
597
597
598 initial_repo_group_list = [splitter(x) for x in
598 initial_repo_group_list = [splitter(x) for x in
599 initial_repo_list.keys() if splitter(x)]
599 initial_repo_list.keys() if splitter(x)]
600
600
601 # remove from database those repository groups that are not in the
601 # remove from database those repository groups that are not in the
602 # filesystem due to parent child relationships we need to delete them
602 # filesystem due to parent child relationships we need to delete them
603 # in a specific order of most nested first
603 # in a specific order of most nested first
604 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
604 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
605 nested_sort = lambda gr: len(gr.split('/'))
605 nested_sort = lambda gr: len(gr.split('/'))
606 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
606 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
607 if group_name not in initial_repo_group_list:
607 if group_name not in initial_repo_group_list:
608 repo_group = RepoGroup.get_by_group_name(group_name)
608 repo_group = RepoGroup.get_by_group_name(group_name)
609 if (repo_group.children.all() or
609 if (repo_group.children.all() or
610 not RepoGroupModel().check_exist_filesystem(
610 not RepoGroupModel().check_exist_filesystem(
611 group_name=group_name, exc_on_failure=False)):
611 group_name=group_name, exc_on_failure=False)):
612 continue
612 continue
613
613
614 log.info(
614 log.info(
615 'Removing non-existing repository group found in db `%s`',
615 'Removing non-existing repository group found in db `%s`',
616 group_name)
616 group_name)
617 try:
617 try:
618 RepoGroupModel(sa).delete(group_name, fs_remove=False)
618 RepoGroupModel(sa).delete(group_name, fs_remove=False)
619 sa.commit()
619 sa.commit()
620 removed.append(group_name)
620 removed.append(group_name)
621 except Exception:
621 except Exception:
622 # don't hold further removals on error
622 # don't hold further removals on error
623 log.exception(
623 log.exception(
624 'Unable to remove repository group `%s`',
624 'Unable to remove repository group `%s`',
625 group_name)
625 group_name)
626 sa.rollback()
626 sa.rollback()
627 raise
627 raise
628
628
629 return added, removed
629 return added, removed
630
630
631
631
632 def load_rcextensions(root_path):
632 def load_rcextensions(root_path):
633 import rhodecode
633 import rhodecode
634 from rhodecode.config import conf
634 from rhodecode.config import conf
635
635
636 path = os.path.join(root_path)
636 path = os.path.join(root_path)
637 sys.path.append(path)
637 sys.path.append(path)
638
638
639 try:
639 try:
640 rcextensions = __import__('rcextensions')
640 rcextensions = __import__('rcextensions')
641 except ImportError:
641 except ImportError:
642 if os.path.isdir(os.path.join(path, 'rcextensions')):
642 if os.path.isdir(os.path.join(path, 'rcextensions')):
643 log.warn('Unable to load rcextensions from %s', path)
643 log.warn('Unable to load rcextensions from %s', path)
644 rcextensions = None
644 rcextensions = None
645
645
646 if rcextensions:
646 if rcextensions:
647 log.debug('Found rcextensions module loaded %s...', rcextensions)
647 log.info('Loaded rcextensions from %s...', rcextensions)
648 rhodecode.EXTENSIONS = rcextensions
648 rhodecode.EXTENSIONS = rcextensions
649
649
650 # Additional mappings that are not present in the pygments lexers
650 # Additional mappings that are not present in the pygments lexers
651 conf.LANGUAGES_EXTENSIONS_MAP.update(
651 conf.LANGUAGES_EXTENSIONS_MAP.update(
652 getattr(rhodecode.EXTENSIONS, 'EXTRA_MAPPINGS', {}))
652 getattr(rhodecode.EXTENSIONS, 'EXTRA_MAPPINGS', {}))
653
653
654
654
655 def get_custom_lexer(extension):
655 def get_custom_lexer(extension):
656 """
656 """
657 returns a custom lexer if it is defined in rcextensions module, or None
657 returns a custom lexer if it is defined in rcextensions module, or None
658 if there's no custom lexer defined
658 if there's no custom lexer defined
659 """
659 """
660 import rhodecode
660 import rhodecode
661 from pygments import lexers
661 from pygments import lexers
662
662
663 # custom override made by RhodeCode
663 # custom override made by RhodeCode
664 if extension in ['mako']:
664 if extension in ['mako']:
665 return lexers.get_lexer_by_name('html+mako')
665 return lexers.get_lexer_by_name('html+mako')
666
666
667 # check if we didn't define this extension as other lexer
667 # check if we didn't define this extension as other lexer
668 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
668 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
669 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
669 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
670 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
670 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
671 return lexers.get_lexer_by_name(_lexer_name)
671 return lexers.get_lexer_by_name(_lexer_name)
672
672
673
673
674 #==============================================================================
674 #==============================================================================
675 # TEST FUNCTIONS AND CREATORS
675 # TEST FUNCTIONS AND CREATORS
676 #==============================================================================
676 #==============================================================================
677 def create_test_index(repo_location, config):
677 def create_test_index(repo_location, config):
678 """
678 """
679 Makes default test index.
679 Makes default test index.
680 """
680 """
681 import rc_testdata
681 import rc_testdata
682
682
683 rc_testdata.extract_search_index(
683 rc_testdata.extract_search_index(
684 'vcs_search_index', os.path.dirname(config['search.location']))
684 'vcs_search_index', os.path.dirname(config['search.location']))
685
685
686
686
687 def create_test_directory(test_path):
687 def create_test_directory(test_path):
688 """
688 """
689 Create test directory if it doesn't exist.
689 Create test directory if it doesn't exist.
690 """
690 """
691 if not os.path.isdir(test_path):
691 if not os.path.isdir(test_path):
692 log.debug('Creating testdir %s', test_path)
692 log.debug('Creating testdir %s', test_path)
693 os.makedirs(test_path)
693 os.makedirs(test_path)
694
694
695
695
696 def create_test_database(test_path, config):
696 def create_test_database(test_path, config):
697 """
697 """
698 Makes a fresh database.
698 Makes a fresh database.
699 """
699 """
700 from rhodecode.lib.db_manage import DbManage
700 from rhodecode.lib.db_manage import DbManage
701
701
702 # PART ONE create db
702 # PART ONE create db
703 dbconf = config['sqlalchemy.db1.url']
703 dbconf = config['sqlalchemy.db1.url']
704 log.debug('making test db %s', dbconf)
704 log.debug('making test db %s', dbconf)
705
705
706 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
706 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
707 tests=True, cli_args={'force_ask': True})
707 tests=True, cli_args={'force_ask': True})
708 dbmanage.create_tables(override=True)
708 dbmanage.create_tables(override=True)
709 dbmanage.set_db_version()
709 dbmanage.set_db_version()
710 # for tests dynamically set new root paths based on generated content
710 # for tests dynamically set new root paths based on generated content
711 dbmanage.create_settings(dbmanage.config_prompt(test_path))
711 dbmanage.create_settings(dbmanage.config_prompt(test_path))
712 dbmanage.create_default_user()
712 dbmanage.create_default_user()
713 dbmanage.create_test_admin_and_users()
713 dbmanage.create_test_admin_and_users()
714 dbmanage.create_permissions()
714 dbmanage.create_permissions()
715 dbmanage.populate_default_permissions()
715 dbmanage.populate_default_permissions()
716 Session().commit()
716 Session().commit()
717
717
718
718
719 def create_test_repositories(test_path, config):
719 def create_test_repositories(test_path, config):
720 """
720 """
721 Creates test repositories in the temporary directory. Repositories are
721 Creates test repositories in the temporary directory. Repositories are
722 extracted from archives within the rc_testdata package.
722 extracted from archives within the rc_testdata package.
723 """
723 """
724 import rc_testdata
724 import rc_testdata
725 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
725 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
726
726
727 log.debug('making test vcs repositories')
727 log.debug('making test vcs repositories')
728
728
729 idx_path = config['search.location']
729 idx_path = config['search.location']
730 data_path = config['cache_dir']
730 data_path = config['cache_dir']
731
731
732 # clean index and data
732 # clean index and data
733 if idx_path and os.path.exists(idx_path):
733 if idx_path and os.path.exists(idx_path):
734 log.debug('remove %s', idx_path)
734 log.debug('remove %s', idx_path)
735 shutil.rmtree(idx_path)
735 shutil.rmtree(idx_path)
736
736
737 if data_path and os.path.exists(data_path):
737 if data_path and os.path.exists(data_path):
738 log.debug('remove %s', data_path)
738 log.debug('remove %s', data_path)
739 shutil.rmtree(data_path)
739 shutil.rmtree(data_path)
740
740
741 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
741 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
742 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
742 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
743
743
744 # Note: Subversion is in the process of being integrated with the system,
744 # Note: Subversion is in the process of being integrated with the system,
745 # until we have a properly packed version of the test svn repository, this
745 # until we have a properly packed version of the test svn repository, this
746 # tries to copy over the repo from a package "rc_testdata"
746 # tries to copy over the repo from a package "rc_testdata"
747 svn_repo_path = rc_testdata.get_svn_repo_archive()
747 svn_repo_path = rc_testdata.get_svn_repo_archive()
748 with tarfile.open(svn_repo_path) as tar:
748 with tarfile.open(svn_repo_path) as tar:
749 tar.extractall(jn(test_path, SVN_REPO))
749 tar.extractall(jn(test_path, SVN_REPO))
750
750
751
751
752 def password_changed(auth_user, session):
752 def password_changed(auth_user, session):
753 # Never report password change in case of default user or anonymous user.
753 # Never report password change in case of default user or anonymous user.
754 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
754 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
755 return False
755 return False
756
756
757 password_hash = md5(auth_user.password) if auth_user.password else None
757 password_hash = md5(auth_user.password) if auth_user.password else None
758 rhodecode_user = session.get('rhodecode_user', {})
758 rhodecode_user = session.get('rhodecode_user', {})
759 session_password_hash = rhodecode_user.get('password', '')
759 session_password_hash = rhodecode_user.get('password', '')
760 return password_hash != session_password_hash
760 return password_hash != session_password_hash
761
761
762
762
763 def read_opensource_licenses():
763 def read_opensource_licenses():
764 global _license_cache
764 global _license_cache
765
765
766 if not _license_cache:
766 if not _license_cache:
767 licenses = pkg_resources.resource_string(
767 licenses = pkg_resources.resource_string(
768 'rhodecode', 'config/licenses.json')
768 'rhodecode', 'config/licenses.json')
769 _license_cache = json.loads(licenses)
769 _license_cache = json.loads(licenses)
770
770
771 return _license_cache
771 return _license_cache
772
772
773
773
774 def generate_platform_uuid():
774 def generate_platform_uuid():
775 """
775 """
776 Generates platform UUID based on it's name
776 Generates platform UUID based on it's name
777 """
777 """
778 import platform
778 import platform
779
779
780 try:
780 try:
781 uuid_list = [platform.platform()]
781 uuid_list = [platform.platform()]
782 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
782 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
783 except Exception as e:
783 except Exception as e:
784 log.error('Failed to generate host uuid: %s', e)
784 log.error('Failed to generate host uuid: %s', e)
785 return 'UNDEFINED'
785 return 'UNDEFINED'
786
786
787
787
788 def send_test_email(recipients, email_body='TEST EMAIL'):
788 def send_test_email(recipients, email_body='TEST EMAIL'):
789 """
789 """
790 Simple code for generating test emails.
790 Simple code for generating test emails.
791 Usage::
791 Usage::
792
792
793 from rhodecode.lib import utils
793 from rhodecode.lib import utils
794 utils.send_test_email()
794 utils.send_test_email()
795 """
795 """
796 from rhodecode.lib.celerylib import tasks, run_task
796 from rhodecode.lib.celerylib import tasks, run_task
797
797
798 email_body = email_body_plaintext = email_body
798 email_body = email_body_plaintext = email_body
799 subject = 'SUBJECT FROM: {}'.format(socket.gethostname())
799 subject = 'SUBJECT FROM: {}'.format(socket.gethostname())
800 tasks.send_email(recipients, subject, email_body_plaintext, email_body)
800 tasks.send_email(recipients, subject, email_body_plaintext, email_body)
@@ -1,427 +1,435 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Model for notifications
23 Model for notifications
24 """
24 """
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 import premailer
29 import premailer
30 from pyramid.threadlocal import get_current_request
30 from pyramid.threadlocal import get_current_request
31 from sqlalchemy.sql.expression import false, true
31 from sqlalchemy.sql.expression import false, true
32
32
33 import rhodecode
33 import rhodecode
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.model import BaseModel
35 from rhodecode.model import BaseModel
36 from rhodecode.model.db import Notification, User, UserNotification
36 from rhodecode.model.db import Notification, User, UserNotification
37 from rhodecode.model.meta import Session
37 from rhodecode.model.meta import Session
38 from rhodecode.translation import TranslationString
38 from rhodecode.translation import TranslationString
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class NotificationModel(BaseModel):
43 class NotificationModel(BaseModel):
44
44
45 cls = Notification
45 cls = Notification
46
46
47 def __get_notification(self, notification):
47 def __get_notification(self, notification):
48 if isinstance(notification, Notification):
48 if isinstance(notification, Notification):
49 return notification
49 return notification
50 elif isinstance(notification, (int, long)):
50 elif isinstance(notification, (int, long)):
51 return Notification.get(notification)
51 return Notification.get(notification)
52 else:
52 else:
53 if notification:
53 if notification:
54 raise Exception('notification must be int, long or Instance'
54 raise Exception('notification must be int, long or Instance'
55 ' of Notification got %s' % type(notification))
55 ' of Notification got %s' % type(notification))
56
56
57 def create(
57 def create(
58 self, created_by, notification_subject, notification_body,
58 self, created_by, notification_subject, notification_body,
59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
60 mention_recipients=None, with_email=True, email_kwargs=None):
60 mention_recipients=None, with_email=True, email_kwargs=None):
61 """
61 """
62
62
63 Creates notification of given type
63 Creates notification of given type
64
64
65 :param created_by: int, str or User instance. User who created this
65 :param created_by: int, str or User instance. User who created this
66 notification
66 notification
67 :param notification_subject: subject of notification itself
67 :param notification_subject: subject of notification itself
68 :param notification_body: body of notification text
68 :param notification_body: body of notification text
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
71
72 :param recipients: list of int, str or User objects, when None
72 :param recipients: list of int, str or User objects, when None
73 is given send to all admins
73 is given send to all admins
74 :param mention_recipients: list of int, str or User objects,
74 :param mention_recipients: list of int, str or User objects,
75 that were mentioned
75 that were mentioned
76 :param with_email: send email with this notification
76 :param with_email: send email with this notification
77 :param email_kwargs: dict with arguments to generate email
77 :param email_kwargs: dict with arguments to generate email
78 """
78 """
79
79
80 from rhodecode.lib.celerylib import tasks, run_task
80 from rhodecode.lib.celerylib import tasks, run_task
81
81
82 if recipients and not getattr(recipients, '__iter__', False):
82 if recipients and not getattr(recipients, '__iter__', False):
83 raise Exception('recipients must be an iterable object')
83 raise Exception('recipients must be an iterable object')
84
84
85 created_by_obj = self._get_user(created_by)
85 created_by_obj = self._get_user(created_by)
86 # default MAIN body if not given
86 # default MAIN body if not given
87 email_kwargs = email_kwargs or {'body': notification_body}
87 email_kwargs = email_kwargs or {'body': notification_body}
88 mention_recipients = mention_recipients or set()
88 mention_recipients = mention_recipients or set()
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 if recipients is None:
93 if recipients is None:
94 # recipients is None means to all admins
94 # recipients is None means to all admins
95 recipients_objs = User.query().filter(User.admin == true()).all()
95 recipients_objs = User.query().filter(User.admin == true()).all()
96 log.debug('sending notifications %s to admins: %s',
96 log.debug('sending notifications %s to admins: %s',
97 notification_type, recipients_objs)
97 notification_type, recipients_objs)
98 else:
98 else:
99 recipients_objs = set()
99 recipients_objs = set()
100 for u in recipients:
100 for u in recipients:
101 obj = self._get_user(u)
101 obj = self._get_user(u)
102 if obj:
102 if obj:
103 recipients_objs.add(obj)
103 recipients_objs.add(obj)
104 else: # we didn't find this user, log the error and carry on
104 else: # we didn't find this user, log the error and carry on
105 log.error('cannot notify unknown user %r', u)
105 log.error('cannot notify unknown user %r', u)
106
106
107 if not recipients_objs:
107 if not recipients_objs:
108 raise Exception('no valid recipients specified')
108 raise Exception('no valid recipients specified')
109
109
110 log.debug('sending notifications %s to %s',
110 log.debug('sending notifications %s to %s',
111 notification_type, recipients_objs)
111 notification_type, recipients_objs)
112
112
113 # add mentioned users into recipients
113 # add mentioned users into recipients
114 final_recipients = set(recipients_objs).union(mention_recipients)
114 final_recipients = set(recipients_objs).union(mention_recipients)
115
115
116 notification = Notification.create(
116 notification = Notification.create(
117 created_by=created_by_obj, subject=notification_subject,
117 created_by=created_by_obj, subject=notification_subject,
118 body=notification_body, recipients=final_recipients,
118 body=notification_body, recipients=final_recipients,
119 type_=notification_type
119 type_=notification_type
120 )
120 )
121
121
122 if not with_email: # skip sending email, and just create notification
122 if not with_email: # skip sending email, and just create notification
123 return notification
123 return notification
124
124
125 # don't send email to person who created this comment
125 # don't send email to person who created this comment
126 rec_objs = set(recipients_objs).difference({created_by_obj})
126 rec_objs = set(recipients_objs).difference({created_by_obj})
127
127
128 # now notify all recipients in question
128 # now notify all recipients in question
129
129
130 for recipient in rec_objs.union(mention_recipients):
130 for recipient in rec_objs.union(mention_recipients):
131 # inject current recipient
131 # inject current recipient
132 email_kwargs['recipient'] = recipient
132 email_kwargs['recipient'] = recipient
133 email_kwargs['mention'] = recipient in mention_recipients
133 email_kwargs['mention'] = recipient in mention_recipients
134 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
134 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
135 notification_type, **email_kwargs)
135 notification_type, **email_kwargs)
136
136
137 extra_headers = None
137 extra_headers = None
138 if 'thread_ids' in email_kwargs:
138 if 'thread_ids' in email_kwargs:
139 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
139 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
140
140
141 log.debug('Creating notification email task for user:`%s`', recipient)
141 log.debug('Creating notification email task for user:`%s`', recipient)
142 task = run_task(
142 task = run_task(
143 tasks.send_email, recipient.email, subject,
143 tasks.send_email, recipient.email, subject,
144 email_body_plaintext, email_body, extra_headers=extra_headers)
144 email_body_plaintext, email_body, extra_headers=extra_headers)
145 log.debug('Created email task: %s', task)
145 log.debug('Created email task: %s', task)
146
146
147 return notification
147 return notification
148
148
149 def delete(self, user, notification):
149 def delete(self, user, notification):
150 # we don't want to remove actual notification just the assignment
150 # we don't want to remove actual notification just the assignment
151 try:
151 try:
152 notification = self.__get_notification(notification)
152 notification = self.__get_notification(notification)
153 user = self._get_user(user)
153 user = self._get_user(user)
154 if notification and user:
154 if notification and user:
155 obj = UserNotification.query()\
155 obj = UserNotification.query()\
156 .filter(UserNotification.user == user)\
156 .filter(UserNotification.user == user)\
157 .filter(UserNotification.notification == notification)\
157 .filter(UserNotification.notification == notification)\
158 .one()
158 .one()
159 Session().delete(obj)
159 Session().delete(obj)
160 return True
160 return True
161 except Exception:
161 except Exception:
162 log.error(traceback.format_exc())
162 log.error(traceback.format_exc())
163 raise
163 raise
164
164
165 def get_for_user(self, user, filter_=None):
165 def get_for_user(self, user, filter_=None):
166 """
166 """
167 Get mentions for given user, filter them if filter dict is given
167 Get mentions for given user, filter them if filter dict is given
168 """
168 """
169 user = self._get_user(user)
169 user = self._get_user(user)
170
170
171 q = UserNotification.query()\
171 q = UserNotification.query()\
172 .filter(UserNotification.user == user)\
172 .filter(UserNotification.user == user)\
173 .join((
173 .join((
174 Notification, UserNotification.notification_id ==
174 Notification, UserNotification.notification_id ==
175 Notification.notification_id))
175 Notification.notification_id))
176 if filter_ == ['all']:
176 if filter_ == ['all']:
177 q = q # no filter
177 q = q # no filter
178 elif filter_ == ['unread']:
178 elif filter_ == ['unread']:
179 q = q.filter(UserNotification.read == false())
179 q = q.filter(UserNotification.read == false())
180 elif filter_:
180 elif filter_:
181 q = q.filter(Notification.type_.in_(filter_))
181 q = q.filter(Notification.type_.in_(filter_))
182
182
183 return q
183 return q
184
184
185 def mark_read(self, user, notification):
185 def mark_read(self, user, notification):
186 try:
186 try:
187 notification = self.__get_notification(notification)
187 notification = self.__get_notification(notification)
188 user = self._get_user(user)
188 user = self._get_user(user)
189 if notification and user:
189 if notification and user:
190 obj = UserNotification.query()\
190 obj = UserNotification.query()\
191 .filter(UserNotification.user == user)\
191 .filter(UserNotification.user == user)\
192 .filter(UserNotification.notification == notification)\
192 .filter(UserNotification.notification == notification)\
193 .one()
193 .one()
194 obj.read = True
194 obj.read = True
195 Session().add(obj)
195 Session().add(obj)
196 return True
196 return True
197 except Exception:
197 except Exception:
198 log.error(traceback.format_exc())
198 log.error(traceback.format_exc())
199 raise
199 raise
200
200
201 def mark_all_read_for_user(self, user, filter_=None):
201 def mark_all_read_for_user(self, user, filter_=None):
202 user = self._get_user(user)
202 user = self._get_user(user)
203 q = UserNotification.query()\
203 q = UserNotification.query()\
204 .filter(UserNotification.user == user)\
204 .filter(UserNotification.user == user)\
205 .filter(UserNotification.read == false())\
205 .filter(UserNotification.read == false())\
206 .join((
206 .join((
207 Notification, UserNotification.notification_id ==
207 Notification, UserNotification.notification_id ==
208 Notification.notification_id))
208 Notification.notification_id))
209 if filter_ == ['unread']:
209 if filter_ == ['unread']:
210 q = q.filter(UserNotification.read == false())
210 q = q.filter(UserNotification.read == false())
211 elif filter_:
211 elif filter_:
212 q = q.filter(Notification.type_.in_(filter_))
212 q = q.filter(Notification.type_.in_(filter_))
213
213
214 # this is a little inefficient but sqlalchemy doesn't support
214 # this is a little inefficient but sqlalchemy doesn't support
215 # update on joined tables :(
215 # update on joined tables :(
216 for obj in q.all():
216 for obj in q.all():
217 obj.read = True
217 obj.read = True
218 Session().add(obj)
218 Session().add(obj)
219
219
220 def get_unread_cnt_for_user(self, user):
220 def get_unread_cnt_for_user(self, user):
221 user = self._get_user(user)
221 user = self._get_user(user)
222 return UserNotification.query()\
222 return UserNotification.query()\
223 .filter(UserNotification.read == false())\
223 .filter(UserNotification.read == false())\
224 .filter(UserNotification.user == user).count()
224 .filter(UserNotification.user == user).count()
225
225
226 def get_unread_for_user(self, user):
226 def get_unread_for_user(self, user):
227 user = self._get_user(user)
227 user = self._get_user(user)
228 return [x.notification for x in UserNotification.query()
228 return [x.notification for x in UserNotification.query()
229 .filter(UserNotification.read == false())
229 .filter(UserNotification.read == false())
230 .filter(UserNotification.user == user).all()]
230 .filter(UserNotification.user == user).all()]
231
231
232 def get_user_notification(self, user, notification):
232 def get_user_notification(self, user, notification):
233 user = self._get_user(user)
233 user = self._get_user(user)
234 notification = self.__get_notification(notification)
234 notification = self.__get_notification(notification)
235
235
236 return UserNotification.query()\
236 return UserNotification.query()\
237 .filter(UserNotification.notification == notification)\
237 .filter(UserNotification.notification == notification)\
238 .filter(UserNotification.user == user).scalar()
238 .filter(UserNotification.user == user).scalar()
239
239
240 def make_description(self, notification, translate, show_age=True):
240 def make_description(self, notification, translate, show_age=True):
241 """
241 """
242 Creates a human readable description based on properties
242 Creates a human readable description based on properties
243 of notification object
243 of notification object
244 """
244 """
245 _ = translate
245 _ = translate
246 _map = {
246 _map = {
247 notification.TYPE_CHANGESET_COMMENT: [
247 notification.TYPE_CHANGESET_COMMENT: [
248 _('%(user)s commented on commit %(date_or_age)s'),
248 _('%(user)s commented on commit %(date_or_age)s'),
249 _('%(user)s commented on commit at %(date_or_age)s'),
249 _('%(user)s commented on commit at %(date_or_age)s'),
250 ],
250 ],
251 notification.TYPE_MESSAGE: [
251 notification.TYPE_MESSAGE: [
252 _('%(user)s sent message %(date_or_age)s'),
252 _('%(user)s sent message %(date_or_age)s'),
253 _('%(user)s sent message at %(date_or_age)s'),
253 _('%(user)s sent message at %(date_or_age)s'),
254 ],
254 ],
255 notification.TYPE_MENTION: [
255 notification.TYPE_MENTION: [
256 _('%(user)s mentioned you %(date_or_age)s'),
256 _('%(user)s mentioned you %(date_or_age)s'),
257 _('%(user)s mentioned you at %(date_or_age)s'),
257 _('%(user)s mentioned you at %(date_or_age)s'),
258 ],
258 ],
259 notification.TYPE_REGISTRATION: [
259 notification.TYPE_REGISTRATION: [
260 _('%(user)s registered in RhodeCode %(date_or_age)s'),
260 _('%(user)s registered in RhodeCode %(date_or_age)s'),
261 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
261 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
262 ],
262 ],
263 notification.TYPE_PULL_REQUEST: [
263 notification.TYPE_PULL_REQUEST: [
264 _('%(user)s opened new pull request %(date_or_age)s'),
264 _('%(user)s opened new pull request %(date_or_age)s'),
265 _('%(user)s opened new pull request at %(date_or_age)s'),
265 _('%(user)s opened new pull request at %(date_or_age)s'),
266 ],
266 ],
267 notification.TYPE_PULL_REQUEST_UPDATE: [
267 notification.TYPE_PULL_REQUEST_UPDATE: [
268 _('%(user)s updated pull request %(date_or_age)s'),
268 _('%(user)s updated pull request %(date_or_age)s'),
269 _('%(user)s updated pull request at %(date_or_age)s'),
269 _('%(user)s updated pull request at %(date_or_age)s'),
270 ],
270 ],
271 notification.TYPE_PULL_REQUEST_COMMENT: [
271 notification.TYPE_PULL_REQUEST_COMMENT: [
272 _('%(user)s commented on pull request %(date_or_age)s'),
272 _('%(user)s commented on pull request %(date_or_age)s'),
273 _('%(user)s commented on pull request at %(date_or_age)s'),
273 _('%(user)s commented on pull request at %(date_or_age)s'),
274 ],
274 ],
275 }
275 }
276
276
277 templates = _map[notification.type_]
277 templates = _map[notification.type_]
278
278
279 if show_age:
279 if show_age:
280 template = templates[0]
280 template = templates[0]
281 date_or_age = h.age(notification.created_on)
281 date_or_age = h.age(notification.created_on)
282 if translate:
282 if translate:
283 date_or_age = translate(date_or_age)
283 date_or_age = translate(date_or_age)
284
284
285 if isinstance(date_or_age, TranslationString):
285 if isinstance(date_or_age, TranslationString):
286 date_or_age = date_or_age.interpolate()
286 date_or_age = date_or_age.interpolate()
287
287
288 else:
288 else:
289 template = templates[1]
289 template = templates[1]
290 date_or_age = h.format_date(notification.created_on)
290 date_or_age = h.format_date(notification.created_on)
291
291
292 return template % {
292 return template % {
293 'user': notification.created_by_user.username,
293 'user': notification.created_by_user.username,
294 'date_or_age': date_or_age,
294 'date_or_age': date_or_age,
295 }
295 }
296
296
297
297
298 # Templates for Titles, that could be overwritten by rcextensions
298 # Templates for Titles, that could be overwritten by rcextensions
299 # Title of email for pull-request update
299 # Title of email for pull-request update
300 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
300 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
301 # Title of email for request for pull request review
301 # Title of email for request for pull request review
302 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
302 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
303
303
304 # Title of email for general comment on pull request
304 # Title of email for general comment on pull request
305 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
305 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
306 # Title of email for general comment which includes status change on pull request
306 # Title of email for general comment which includes status change on pull request
307 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
307 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
308 # Title of email for inline comment on a file in pull request
308 # Title of email for inline comment on a file in pull request
309 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
309 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
310
310
311 # Title of email for general comment on commit
311 # Title of email for general comment on commit
312 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
312 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
313 # Title of email for general comment which includes status change on commit
313 # Title of email for general comment which includes status change on commit
314 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
314 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
315 # Title of email for inline comment on a file in commit
315 # Title of email for inline comment on a file in commit
316 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
316 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
317
317
318
318
319 class EmailNotificationModel(BaseModel):
319 class EmailNotificationModel(BaseModel):
320 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
320 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
321 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
321 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
322 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
322 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
323 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
323 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
324 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
324 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
325 TYPE_MAIN = Notification.TYPE_MESSAGE
325 TYPE_MAIN = Notification.TYPE_MESSAGE
326
326
327 TYPE_PASSWORD_RESET = 'password_reset'
327 TYPE_PASSWORD_RESET = 'password_reset'
328 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
328 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
329 TYPE_EMAIL_TEST = 'email_test'
329 TYPE_EMAIL_TEST = 'email_test'
330 TYPE_EMAIL_EXCEPTION = 'exception'
330 TYPE_EMAIL_EXCEPTION = 'exception'
331 TYPE_TEST = 'test'
331 TYPE_TEST = 'test'
332
332
333 email_types = {
333 email_types = {
334 TYPE_MAIN:
334 TYPE_MAIN:
335 'rhodecode:templates/email_templates/main.mako',
335 'rhodecode:templates/email_templates/main.mako',
336 TYPE_TEST:
336 TYPE_TEST:
337 'rhodecode:templates/email_templates/test.mako',
337 'rhodecode:templates/email_templates/test.mako',
338 TYPE_EMAIL_EXCEPTION:
338 TYPE_EMAIL_EXCEPTION:
339 'rhodecode:templates/email_templates/exception_tracker.mako',
339 'rhodecode:templates/email_templates/exception_tracker.mako',
340 TYPE_EMAIL_TEST:
340 TYPE_EMAIL_TEST:
341 'rhodecode:templates/email_templates/email_test.mako',
341 'rhodecode:templates/email_templates/email_test.mako',
342 TYPE_REGISTRATION:
342 TYPE_REGISTRATION:
343 'rhodecode:templates/email_templates/user_registration.mako',
343 'rhodecode:templates/email_templates/user_registration.mako',
344 TYPE_PASSWORD_RESET:
344 TYPE_PASSWORD_RESET:
345 'rhodecode:templates/email_templates/password_reset.mako',
345 'rhodecode:templates/email_templates/password_reset.mako',
346 TYPE_PASSWORD_RESET_CONFIRMATION:
346 TYPE_PASSWORD_RESET_CONFIRMATION:
347 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
347 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
348 TYPE_COMMIT_COMMENT:
348 TYPE_COMMIT_COMMENT:
349 'rhodecode:templates/email_templates/commit_comment.mako',
349 'rhodecode:templates/email_templates/commit_comment.mako',
350 TYPE_PULL_REQUEST:
350 TYPE_PULL_REQUEST:
351 'rhodecode:templates/email_templates/pull_request_review.mako',
351 'rhodecode:templates/email_templates/pull_request_review.mako',
352 TYPE_PULL_REQUEST_COMMENT:
352 TYPE_PULL_REQUEST_COMMENT:
353 'rhodecode:templates/email_templates/pull_request_comment.mako',
353 'rhodecode:templates/email_templates/pull_request_comment.mako',
354 TYPE_PULL_REQUEST_UPDATE:
354 TYPE_PULL_REQUEST_UPDATE:
355 'rhodecode:templates/email_templates/pull_request_update.mako',
355 'rhodecode:templates/email_templates/pull_request_update.mako',
356 }
356 }
357
357
358 premailer_instance = premailer.Premailer(
358 premailer_instance = premailer.Premailer(
359 cssutils_logging_level=logging.ERROR,
359 cssutils_logging_level=logging.ERROR,
360 cssutils_logging_handler=logging.getLogger().handlers[0]
360 cssutils_logging_handler=logging.getLogger().handlers[0]
361 if logging.getLogger().handlers else None,
361 if logging.getLogger().handlers else None,
362 )
362 )
363
363
364 def __init__(self):
364 def __init__(self):
365 """
365 """
366 Example usage::
366 Example usage::
367
367
368 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
368 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
369 EmailNotificationModel.TYPE_TEST, **email_kwargs)
369 EmailNotificationModel.TYPE_TEST, **email_kwargs)
370
370
371 """
371 """
372 super(EmailNotificationModel, self).__init__()
372 super(EmailNotificationModel, self).__init__()
373 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
373 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
374
374
375 def _update_kwargs_for_render(self, kwargs):
375 def _update_kwargs_for_render(self, kwargs):
376 """
376 """
377 Inject params required for Mako rendering
377 Inject params required for Mako rendering
378
378
379 :param kwargs:
379 :param kwargs:
380 """
380 """
381
381
382 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
382 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
383 kwargs['rhodecode_version'] = rhodecode.__version__
383 kwargs['rhodecode_version'] = rhodecode.__version__
384 instance_url = h.route_url('home')
384 instance_url = h.route_url('home')
385 _kwargs = {
385 _kwargs = {
386 'instance_url': instance_url,
386 'instance_url': instance_url,
387 'whitespace_filter': self.whitespace_filter
387 'whitespace_filter': self.whitespace_filter,
388 'email_pr_update_subject_template': EMAIL_PR_UPDATE_SUBJECT_TEMPLATE,
389 'email_pr_review_subject_template': EMAIL_PR_REVIEW_SUBJECT_TEMPLATE,
390 'email_pr_comment_subject_template': EMAIL_PR_COMMENT_SUBJECT_TEMPLATE,
391 'email_pr_comment_status_change_subject_template': EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
392 'email_pr_comment_file_subject_template': EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE,
393 'email_comment_subject_template': EMAIL_COMMENT_SUBJECT_TEMPLATE,
394 'email_comment_status_change_subject_template': EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
395 'email_comment_file_subject_template': EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE,
388 }
396 }
389 _kwargs.update(kwargs)
397 _kwargs.update(kwargs)
390 return _kwargs
398 return _kwargs
391
399
392 def whitespace_filter(self, text):
400 def whitespace_filter(self, text):
393 return text.replace('\n', '').replace('\t', '')
401 return text.replace('\n', '').replace('\t', '')
394
402
395 def get_renderer(self, type_, request):
403 def get_renderer(self, type_, request):
396 template_name = self.email_types[type_]
404 template_name = self.email_types[type_]
397 return request.get_partial_renderer(template_name)
405 return request.get_partial_renderer(template_name)
398
406
399 def render_email(self, type_, **kwargs):
407 def render_email(self, type_, **kwargs):
400 """
408 """
401 renders template for email, and returns a tuple of
409 renders template for email, and returns a tuple of
402 (subject, email_headers, email_html_body, email_plaintext_body)
410 (subject, email_headers, email_html_body, email_plaintext_body)
403 """
411 """
404 # translator and helpers inject
412 # translator and helpers inject
405 _kwargs = self._update_kwargs_for_render(kwargs)
413 _kwargs = self._update_kwargs_for_render(kwargs)
406 request = get_current_request()
414 request = get_current_request()
407 email_template = self.get_renderer(type_, request=request)
415 email_template = self.get_renderer(type_, request=request)
408
416
409 subject = email_template.render('subject', **_kwargs)
417 subject = email_template.render('subject', **_kwargs)
410
418
411 try:
419 try:
412 body_plaintext = email_template.render('body_plaintext', **_kwargs)
420 body_plaintext = email_template.render('body_plaintext', **_kwargs)
413 except AttributeError:
421 except AttributeError:
414 # it's not defined in template, ok we can skip it
422 # it's not defined in template, ok we can skip it
415 body_plaintext = ''
423 body_plaintext = ''
416
424
417 # render WHOLE template
425 # render WHOLE template
418 body = email_template.render(None, **_kwargs)
426 body = email_template.render(None, **_kwargs)
419
427
420 try:
428 try:
421 # Inline CSS styles and conversion
429 # Inline CSS styles and conversion
422 body = self.premailer_instance.transform(body)
430 body = self.premailer_instance.transform(body)
423 except Exception:
431 except Exception:
424 log.exception('Failed to parse body with premailer')
432 log.exception('Failed to parse body with premailer')
425 pass
433 pass
426
434
427 return subject, body, body_plaintext
435 return subject, body, body_plaintext
General Comments 0
You need to be logged in to leave comments. Login now