##// END OF EJS Templates
core: dropped deprecated PartialRenderer that depends on pylons.
marcink -
r2310:d2e9d950 default
parent child Browse files
Show More
@@ -1,980 +1,908 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 shutil
31 import shutil
32 import tempfile
32 import tempfile
33 import traceback
33 import traceback
34 import tarfile
34 import tarfile
35 import warnings
35 import warnings
36 import hashlib
36 import hashlib
37 from os.path import join as jn
37 from os.path import join as jn
38
38
39 import paste
39 import paste
40 import pkg_resources
40 import pkg_resources
41 from paste.script.command import Command, BadCommand
41 from paste.script.command import Command, BadCommand
42 from webhelpers.text import collapse, remove_formatting, strip_tags
42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 from mako import exceptions
43 from mako import exceptions
44 from pyramid.threadlocal import get_current_registry
44 from pyramid.threadlocal import get_current_registry
45 from pyramid.request import Request
45 from pyramid.request import Request
46
46
47 from rhodecode.lib.fakemod import create_module
47 from rhodecode.lib.fakemod import create_module
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)
52 safe_str, safe_unicode, get_current_rhodecode_user, md5)
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 if isinstance(request, Request):
100 if isinstance(request, Request):
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 # TODO(marcink): remove after pylons migration...
109 # TODO(marcink): remove after pylons migration...
110 if not _repo:
110 if not _repo:
111 _repo = request.environ['pylons.routes_dict'].get('repo_name')
111 _repo = request.environ['pylons.routes_dict'].get('repo_name')
112
112
113 if _repo:
113 if _repo:
114 _repo = _repo.rstrip('/')
114 _repo = _repo.rstrip('/')
115 return _repo
115 return _repo
116
116
117
117
118 def get_repo_group_slug(request):
118 def get_repo_group_slug(request):
119 _group = ''
119 _group = ''
120 if isinstance(request, Request):
120 if isinstance(request, Request):
121 if hasattr(request, 'db_repo_group'):
121 if hasattr(request, 'db_repo_group'):
122 # if our requests has set db reference use it for name, this
122 # if our requests has set db reference use it for name, this
123 # translates the example.com/_<id> into proper repo group names
123 # translates the example.com/_<id> into proper repo group names
124 _group = request.db_repo_group.group_name
124 _group = request.db_repo_group.group_name
125 elif getattr(request, 'matchdict', None):
125 elif getattr(request, 'matchdict', None):
126 # pyramid
126 # pyramid
127 _group = request.matchdict.get('repo_group_name')
127 _group = request.matchdict.get('repo_group_name')
128
128
129 # TODO(marcink): remove after pylons migration...
129 # TODO(marcink): remove after pylons migration...
130 if not _group:
130 if not _group:
131 _group = request.environ['pylons.routes_dict'].get('group_name')
131 _group = request.environ['pylons.routes_dict'].get('group_name')
132
132
133 if _group:
133 if _group:
134 _group = _group.rstrip('/')
134 _group = _group.rstrip('/')
135 return _group
135 return _group
136
136
137
137
138 def get_user_group_slug(request):
138 def get_user_group_slug(request):
139 _user_group = ''
139 _user_group = ''
140 if isinstance(request, Request):
140 if isinstance(request, Request):
141
141
142 if hasattr(request, 'db_user_group'):
142 if hasattr(request, 'db_user_group'):
143 _user_group = request.db_user_group.users_group_name
143 _user_group = request.db_user_group.users_group_name
144 elif getattr(request, 'matchdict', None):
144 elif getattr(request, 'matchdict', None):
145 # pyramid
145 # pyramid
146 _user_group = request.matchdict.get('user_group_id')
146 _user_group = request.matchdict.get('user_group_id')
147
147
148 try:
148 try:
149 _user_group = UserGroup.get(_user_group)
149 _user_group = UserGroup.get(_user_group)
150 if _user_group:
150 if _user_group:
151 _user_group = _user_group.users_group_name
151 _user_group = _user_group.users_group_name
152 except Exception:
152 except Exception:
153 log.exception('Failed to get user group by id')
153 log.exception('Failed to get user group by id')
154 # catch all failures here
154 # catch all failures here
155 return None
155 return None
156
156
157 # TODO(marcink): remove after pylons migration...
157 # TODO(marcink): remove after pylons migration...
158 if not _user_group:
158 if not _user_group:
159 _user_group = request.environ['pylons.routes_dict'].get('user_group_id')
159 _user_group = request.environ['pylons.routes_dict'].get('user_group_id')
160
160
161 return _user_group
161 return _user_group
162
162
163
163
164 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
164 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
165 """
165 """
166 Scans given path for repos and return (name,(type,path)) tuple
166 Scans given path for repos and return (name,(type,path)) tuple
167
167
168 :param path: path to scan for repositories
168 :param path: path to scan for repositories
169 :param recursive: recursive search and return names with subdirs in front
169 :param recursive: recursive search and return names with subdirs in front
170 """
170 """
171
171
172 # remove ending slash for better results
172 # remove ending slash for better results
173 path = path.rstrip(os.sep)
173 path = path.rstrip(os.sep)
174 log.debug('now scanning in %s location recursive:%s...', path, recursive)
174 log.debug('now scanning in %s location recursive:%s...', path, recursive)
175
175
176 def _get_repos(p):
176 def _get_repos(p):
177 dirpaths = _get_dirpaths(p)
177 dirpaths = _get_dirpaths(p)
178 if not _is_dir_writable(p):
178 if not _is_dir_writable(p):
179 log.warning('repo path without write access: %s', p)
179 log.warning('repo path without write access: %s', p)
180
180
181 for dirpath in dirpaths:
181 for dirpath in dirpaths:
182 if os.path.isfile(os.path.join(p, dirpath)):
182 if os.path.isfile(os.path.join(p, dirpath)):
183 continue
183 continue
184 cur_path = os.path.join(p, dirpath)
184 cur_path = os.path.join(p, dirpath)
185
185
186 # skip removed repos
186 # skip removed repos
187 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
187 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
188 continue
188 continue
189
189
190 #skip .<somethin> dirs
190 #skip .<somethin> dirs
191 if dirpath.startswith('.'):
191 if dirpath.startswith('.'):
192 continue
192 continue
193
193
194 try:
194 try:
195 scm_info = get_scm(cur_path)
195 scm_info = get_scm(cur_path)
196 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
196 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
197 except VCSError:
197 except VCSError:
198 if not recursive:
198 if not recursive:
199 continue
199 continue
200 #check if this dir containts other repos for recursive scan
200 #check if this dir containts other repos for recursive scan
201 rec_path = os.path.join(p, dirpath)
201 rec_path = os.path.join(p, dirpath)
202 if os.path.isdir(rec_path):
202 if os.path.isdir(rec_path):
203 for inner_scm in _get_repos(rec_path):
203 for inner_scm in _get_repos(rec_path):
204 yield inner_scm
204 yield inner_scm
205
205
206 return _get_repos(path)
206 return _get_repos(path)
207
207
208
208
209 def _get_dirpaths(p):
209 def _get_dirpaths(p):
210 try:
210 try:
211 # OS-independable way of checking if we have at least read-only
211 # OS-independable way of checking if we have at least read-only
212 # access or not.
212 # access or not.
213 dirpaths = os.listdir(p)
213 dirpaths = os.listdir(p)
214 except OSError:
214 except OSError:
215 log.warning('ignoring repo path without read access: %s', p)
215 log.warning('ignoring repo path without read access: %s', p)
216 return []
216 return []
217
217
218 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
218 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
219 # decode paths and suddenly returns unicode objects itself. The items it
219 # decode paths and suddenly returns unicode objects itself. The items it
220 # cannot decode are returned as strings and cause issues.
220 # cannot decode are returned as strings and cause issues.
221 #
221 #
222 # Those paths are ignored here until a solid solution for path handling has
222 # Those paths are ignored here until a solid solution for path handling has
223 # been built.
223 # been built.
224 expected_type = type(p)
224 expected_type = type(p)
225
225
226 def _has_correct_type(item):
226 def _has_correct_type(item):
227 if type(item) is not expected_type:
227 if type(item) is not expected_type:
228 log.error(
228 log.error(
229 u"Ignoring path %s since it cannot be decoded into unicode.",
229 u"Ignoring path %s since it cannot be decoded into unicode.",
230 # Using "repr" to make sure that we see the byte value in case
230 # Using "repr" to make sure that we see the byte value in case
231 # of support.
231 # of support.
232 repr(item))
232 repr(item))
233 return False
233 return False
234 return True
234 return True
235
235
236 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
236 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
237
237
238 return dirpaths
238 return dirpaths
239
239
240
240
241 def _is_dir_writable(path):
241 def _is_dir_writable(path):
242 """
242 """
243 Probe if `path` is writable.
243 Probe if `path` is writable.
244
244
245 Due to trouble on Cygwin / Windows, this is actually probing if it is
245 Due to trouble on Cygwin / Windows, this is actually probing if it is
246 possible to create a file inside of `path`, stat does not produce reliable
246 possible to create a file inside of `path`, stat does not produce reliable
247 results in this case.
247 results in this case.
248 """
248 """
249 try:
249 try:
250 with tempfile.TemporaryFile(dir=path):
250 with tempfile.TemporaryFile(dir=path):
251 pass
251 pass
252 except OSError:
252 except OSError:
253 return False
253 return False
254 return True
254 return True
255
255
256
256
257 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
257 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
258 """
258 """
259 Returns True if given path is a valid repository False otherwise.
259 Returns True if given path is a valid repository False otherwise.
260 If expect_scm param is given also, compare if given scm is the same
260 If expect_scm param is given also, compare if given scm is the same
261 as expected from scm parameter. If explicit_scm is given don't try to
261 as expected from scm parameter. If explicit_scm is given don't try to
262 detect the scm, just use the given one to check if repo is valid
262 detect the scm, just use the given one to check if repo is valid
263
263
264 :param repo_name:
264 :param repo_name:
265 :param base_path:
265 :param base_path:
266 :param expect_scm:
266 :param expect_scm:
267 :param explicit_scm:
267 :param explicit_scm:
268
268
269 :return True: if given path is a valid repository
269 :return True: if given path is a valid repository
270 """
270 """
271 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
271 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
272 log.debug('Checking if `%s` is a valid path for repository. '
272 log.debug('Checking if `%s` is a valid path for repository. '
273 'Explicit type: %s', repo_name, explicit_scm)
273 'Explicit type: %s', repo_name, explicit_scm)
274
274
275 try:
275 try:
276 if explicit_scm:
276 if explicit_scm:
277 detected_scms = [get_scm_backend(explicit_scm)]
277 detected_scms = [get_scm_backend(explicit_scm)]
278 else:
278 else:
279 detected_scms = get_scm(full_path)
279 detected_scms = get_scm(full_path)
280
280
281 if expect_scm:
281 if expect_scm:
282 return detected_scms[0] == expect_scm
282 return detected_scms[0] == expect_scm
283 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
283 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
284 return True
284 return True
285 except VCSError:
285 except VCSError:
286 log.debug('path: %s is not a valid repo !', full_path)
286 log.debug('path: %s is not a valid repo !', full_path)
287 return False
287 return False
288
288
289
289
290 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
290 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
291 """
291 """
292 Returns True if given path is a repository group, False otherwise
292 Returns True if given path is a repository group, False otherwise
293
293
294 :param repo_name:
294 :param repo_name:
295 :param base_path:
295 :param base_path:
296 """
296 """
297 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
297 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
298 log.debug('Checking if `%s` is a valid path for repository group',
298 log.debug('Checking if `%s` is a valid path for repository group',
299 repo_group_name)
299 repo_group_name)
300
300
301 # check if it's not a repo
301 # check if it's not a repo
302 if is_valid_repo(repo_group_name, base_path):
302 if is_valid_repo(repo_group_name, base_path):
303 log.debug('Repo called %s exist, it is not a valid '
303 log.debug('Repo called %s exist, it is not a valid '
304 'repo group' % repo_group_name)
304 'repo group' % repo_group_name)
305 return False
305 return False
306
306
307 try:
307 try:
308 # we need to check bare git repos at higher level
308 # we need to check bare git repos at higher level
309 # since we might match branches/hooks/info/objects or possible
309 # since we might match branches/hooks/info/objects or possible
310 # other things inside bare git repo
310 # other things inside bare git repo
311 scm_ = get_scm(os.path.dirname(full_path))
311 scm_ = get_scm(os.path.dirname(full_path))
312 log.debug('path: %s is a vcs object:%s, not valid '
312 log.debug('path: %s is a vcs object:%s, not valid '
313 'repo group' % (full_path, scm_))
313 'repo group' % (full_path, scm_))
314 return False
314 return False
315 except VCSError:
315 except VCSError:
316 pass
316 pass
317
317
318 # check if it's a valid path
318 # check if it's a valid path
319 if skip_path_check or os.path.isdir(full_path):
319 if skip_path_check or os.path.isdir(full_path):
320 log.debug('path: %s is a valid repo group !', full_path)
320 log.debug('path: %s is a valid repo group !', full_path)
321 return True
321 return True
322
322
323 log.debug('path: %s is not a valid repo group !', full_path)
323 log.debug('path: %s is not a valid repo group !', full_path)
324 return False
324 return False
325
325
326
326
327 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
327 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
328 while True:
328 while True:
329 ok = raw_input(prompt)
329 ok = raw_input(prompt)
330 if ok.lower() in ('y', 'ye', 'yes'):
330 if ok.lower() in ('y', 'ye', 'yes'):
331 return True
331 return True
332 if ok.lower() in ('n', 'no', 'nop', 'nope'):
332 if ok.lower() in ('n', 'no', 'nop', 'nope'):
333 return False
333 return False
334 retries = retries - 1
334 retries = retries - 1
335 if retries < 0:
335 if retries < 0:
336 raise IOError
336 raise IOError
337 print(complaint)
337 print(complaint)
338
338
339 # propagated from mercurial documentation
339 # propagated from mercurial documentation
340 ui_sections = [
340 ui_sections = [
341 'alias', 'auth',
341 'alias', 'auth',
342 'decode/encode', 'defaults',
342 'decode/encode', 'defaults',
343 'diff', 'email',
343 'diff', 'email',
344 'extensions', 'format',
344 'extensions', 'format',
345 'merge-patterns', 'merge-tools',
345 'merge-patterns', 'merge-tools',
346 'hooks', 'http_proxy',
346 'hooks', 'http_proxy',
347 'smtp', 'patch',
347 'smtp', 'patch',
348 'paths', 'profiling',
348 'paths', 'profiling',
349 'server', 'trusted',
349 'server', 'trusted',
350 'ui', 'web', ]
350 'ui', 'web', ]
351
351
352
352
353 def config_data_from_db(clear_session=True, repo=None):
353 def config_data_from_db(clear_session=True, repo=None):
354 """
354 """
355 Read the configuration data from the database and return configuration
355 Read the configuration data from the database and return configuration
356 tuples.
356 tuples.
357 """
357 """
358 from rhodecode.model.settings import VcsSettingsModel
358 from rhodecode.model.settings import VcsSettingsModel
359
359
360 config = []
360 config = []
361
361
362 sa = meta.Session()
362 sa = meta.Session()
363 settings_model = VcsSettingsModel(repo=repo, sa=sa)
363 settings_model = VcsSettingsModel(repo=repo, sa=sa)
364
364
365 ui_settings = settings_model.get_ui_settings()
365 ui_settings = settings_model.get_ui_settings()
366
366
367 for setting in ui_settings:
367 for setting in ui_settings:
368 if setting.active:
368 if setting.active:
369 log.debug(
369 log.debug(
370 'settings ui from db: [%s] %s=%s',
370 'settings ui from db: [%s] %s=%s',
371 setting.section, setting.key, setting.value)
371 setting.section, setting.key, setting.value)
372 config.append((
372 config.append((
373 safe_str(setting.section), safe_str(setting.key),
373 safe_str(setting.section), safe_str(setting.key),
374 safe_str(setting.value)))
374 safe_str(setting.value)))
375 if setting.key == 'push_ssl':
375 if setting.key == 'push_ssl':
376 # force set push_ssl requirement to False, rhodecode
376 # force set push_ssl requirement to False, rhodecode
377 # handles that
377 # handles that
378 config.append((
378 config.append((
379 safe_str(setting.section), safe_str(setting.key), False))
379 safe_str(setting.section), safe_str(setting.key), False))
380 if clear_session:
380 if clear_session:
381 meta.Session.remove()
381 meta.Session.remove()
382
382
383 # TODO: mikhail: probably it makes no sense to re-read hooks information.
383 # TODO: mikhail: probably it makes no sense to re-read hooks information.
384 # It's already there and activated/deactivated
384 # It's already there and activated/deactivated
385 skip_entries = []
385 skip_entries = []
386 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
386 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
387 if 'pull' not in enabled_hook_classes:
387 if 'pull' not in enabled_hook_classes:
388 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
388 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
389 if 'push' not in enabled_hook_classes:
389 if 'push' not in enabled_hook_classes:
390 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
390 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
391 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
391 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
392 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
392 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
393
393
394 config = [entry for entry in config if entry[:2] not in skip_entries]
394 config = [entry for entry in config if entry[:2] not in skip_entries]
395
395
396 return config
396 return config
397
397
398
398
399 def make_db_config(clear_session=True, repo=None):
399 def make_db_config(clear_session=True, repo=None):
400 """
400 """
401 Create a :class:`Config` instance based on the values in the database.
401 Create a :class:`Config` instance based on the values in the database.
402 """
402 """
403 config = Config()
403 config = Config()
404 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
404 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
405 for section, option, value in config_data:
405 for section, option, value in config_data:
406 config.set(section, option, value)
406 config.set(section, option, value)
407 return config
407 return config
408
408
409
409
410 def get_enabled_hook_classes(ui_settings):
410 def get_enabled_hook_classes(ui_settings):
411 """
411 """
412 Return the enabled hook classes.
412 Return the enabled hook classes.
413
413
414 :param ui_settings: List of ui_settings as returned
414 :param ui_settings: List of ui_settings as returned
415 by :meth:`VcsSettingsModel.get_ui_settings`
415 by :meth:`VcsSettingsModel.get_ui_settings`
416
416
417 :return: a list with the enabled hook classes. The order is not guaranteed.
417 :return: a list with the enabled hook classes. The order is not guaranteed.
418 :rtype: list
418 :rtype: list
419 """
419 """
420 enabled_hooks = []
420 enabled_hooks = []
421 active_hook_keys = [
421 active_hook_keys = [
422 key for section, key, value, active in ui_settings
422 key for section, key, value, active in ui_settings
423 if section == 'hooks' and active]
423 if section == 'hooks' and active]
424
424
425 hook_names = {
425 hook_names = {
426 RhodeCodeUi.HOOK_PUSH: 'push',
426 RhodeCodeUi.HOOK_PUSH: 'push',
427 RhodeCodeUi.HOOK_PULL: 'pull',
427 RhodeCodeUi.HOOK_PULL: 'pull',
428 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
428 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
429 }
429 }
430
430
431 for key in active_hook_keys:
431 for key in active_hook_keys:
432 hook = hook_names.get(key)
432 hook = hook_names.get(key)
433 if hook:
433 if hook:
434 enabled_hooks.append(hook)
434 enabled_hooks.append(hook)
435
435
436 return enabled_hooks
436 return enabled_hooks
437
437
438
438
439 def set_rhodecode_config(config):
439 def set_rhodecode_config(config):
440 """
440 """
441 Updates pylons config with new settings from database
441 Updates pylons config with new settings from database
442
442
443 :param config:
443 :param config:
444 """
444 """
445 from rhodecode.model.settings import SettingsModel
445 from rhodecode.model.settings import SettingsModel
446 app_settings = SettingsModel().get_all_settings()
446 app_settings = SettingsModel().get_all_settings()
447
447
448 for k, v in app_settings.items():
448 for k, v in app_settings.items():
449 config[k] = v
449 config[k] = v
450
450
451
451
452 def get_rhodecode_realm():
452 def get_rhodecode_realm():
453 """
453 """
454 Return the rhodecode realm from database.
454 Return the rhodecode realm from database.
455 """
455 """
456 from rhodecode.model.settings import SettingsModel
456 from rhodecode.model.settings import SettingsModel
457 realm = SettingsModel().get_setting_by_name('realm')
457 realm = SettingsModel().get_setting_by_name('realm')
458 return safe_str(realm.app_settings_value)
458 return safe_str(realm.app_settings_value)
459
459
460
460
461 def get_rhodecode_base_path():
461 def get_rhodecode_base_path():
462 """
462 """
463 Returns the base path. The base path is the filesystem path which points
463 Returns the base path. The base path is the filesystem path which points
464 to the repository store.
464 to the repository store.
465 """
465 """
466 from rhodecode.model.settings import SettingsModel
466 from rhodecode.model.settings import SettingsModel
467 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
467 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
468 return safe_str(paths_ui.ui_value)
468 return safe_str(paths_ui.ui_value)
469
469
470
470
471 def map_groups(path):
471 def map_groups(path):
472 """
472 """
473 Given a full path to a repository, create all nested groups that this
473 Given a full path to a repository, create all nested groups that this
474 repo is inside. This function creates parent-child relationships between
474 repo is inside. This function creates parent-child relationships between
475 groups and creates default perms for all new groups.
475 groups and creates default perms for all new groups.
476
476
477 :param paths: full path to repository
477 :param paths: full path to repository
478 """
478 """
479 from rhodecode.model.repo_group import RepoGroupModel
479 from rhodecode.model.repo_group import RepoGroupModel
480 sa = meta.Session()
480 sa = meta.Session()
481 groups = path.split(Repository.NAME_SEP)
481 groups = path.split(Repository.NAME_SEP)
482 parent = None
482 parent = None
483 group = None
483 group = None
484
484
485 # last element is repo in nested groups structure
485 # last element is repo in nested groups structure
486 groups = groups[:-1]
486 groups = groups[:-1]
487 rgm = RepoGroupModel(sa)
487 rgm = RepoGroupModel(sa)
488 owner = User.get_first_super_admin()
488 owner = User.get_first_super_admin()
489 for lvl, group_name in enumerate(groups):
489 for lvl, group_name in enumerate(groups):
490 group_name = '/'.join(groups[:lvl] + [group_name])
490 group_name = '/'.join(groups[:lvl] + [group_name])
491 group = RepoGroup.get_by_group_name(group_name)
491 group = RepoGroup.get_by_group_name(group_name)
492 desc = '%s group' % group_name
492 desc = '%s group' % group_name
493
493
494 # skip folders that are now removed repos
494 # skip folders that are now removed repos
495 if REMOVED_REPO_PAT.match(group_name):
495 if REMOVED_REPO_PAT.match(group_name):
496 break
496 break
497
497
498 if group is None:
498 if group is None:
499 log.debug('creating group level: %s group_name: %s',
499 log.debug('creating group level: %s group_name: %s',
500 lvl, group_name)
500 lvl, group_name)
501 group = RepoGroup(group_name, parent)
501 group = RepoGroup(group_name, parent)
502 group.group_description = desc
502 group.group_description = desc
503 group.user = owner
503 group.user = owner
504 sa.add(group)
504 sa.add(group)
505 perm_obj = rgm._create_default_perms(group)
505 perm_obj = rgm._create_default_perms(group)
506 sa.add(perm_obj)
506 sa.add(perm_obj)
507 sa.flush()
507 sa.flush()
508
508
509 parent = group
509 parent = group
510 return group
510 return group
511
511
512
512
513 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
513 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
514 """
514 """
515 maps all repos given in initial_repo_list, non existing repositories
515 maps all repos given in initial_repo_list, non existing repositories
516 are created, if remove_obsolete is True it also checks for db entries
516 are created, if remove_obsolete is True it also checks for db entries
517 that are not in initial_repo_list and removes them.
517 that are not in initial_repo_list and removes them.
518
518
519 :param initial_repo_list: list of repositories found by scanning methods
519 :param initial_repo_list: list of repositories found by scanning methods
520 :param remove_obsolete: check for obsolete entries in database
520 :param remove_obsolete: check for obsolete entries in database
521 """
521 """
522 from rhodecode.model.repo import RepoModel
522 from rhodecode.model.repo import RepoModel
523 from rhodecode.model.scm import ScmModel
523 from rhodecode.model.scm import ScmModel
524 from rhodecode.model.repo_group import RepoGroupModel
524 from rhodecode.model.repo_group import RepoGroupModel
525 from rhodecode.model.settings import SettingsModel
525 from rhodecode.model.settings import SettingsModel
526
526
527 sa = meta.Session()
527 sa = meta.Session()
528 repo_model = RepoModel()
528 repo_model = RepoModel()
529 user = User.get_first_super_admin()
529 user = User.get_first_super_admin()
530 added = []
530 added = []
531
531
532 # creation defaults
532 # creation defaults
533 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
533 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
534 enable_statistics = defs.get('repo_enable_statistics')
534 enable_statistics = defs.get('repo_enable_statistics')
535 enable_locking = defs.get('repo_enable_locking')
535 enable_locking = defs.get('repo_enable_locking')
536 enable_downloads = defs.get('repo_enable_downloads')
536 enable_downloads = defs.get('repo_enable_downloads')
537 private = defs.get('repo_private')
537 private = defs.get('repo_private')
538
538
539 for name, repo in initial_repo_list.items():
539 for name, repo in initial_repo_list.items():
540 group = map_groups(name)
540 group = map_groups(name)
541 unicode_name = safe_unicode(name)
541 unicode_name = safe_unicode(name)
542 db_repo = repo_model.get_by_repo_name(unicode_name)
542 db_repo = repo_model.get_by_repo_name(unicode_name)
543 # found repo that is on filesystem not in RhodeCode database
543 # found repo that is on filesystem not in RhodeCode database
544 if not db_repo:
544 if not db_repo:
545 log.info('repository %s not found, creating now', name)
545 log.info('repository %s not found, creating now', name)
546 added.append(name)
546 added.append(name)
547 desc = (repo.description
547 desc = (repo.description
548 if repo.description != 'unknown'
548 if repo.description != 'unknown'
549 else '%s repository' % name)
549 else '%s repository' % name)
550
550
551 db_repo = repo_model._create_repo(
551 db_repo = repo_model._create_repo(
552 repo_name=name,
552 repo_name=name,
553 repo_type=repo.alias,
553 repo_type=repo.alias,
554 description=desc,
554 description=desc,
555 repo_group=getattr(group, 'group_id', None),
555 repo_group=getattr(group, 'group_id', None),
556 owner=user,
556 owner=user,
557 enable_locking=enable_locking,
557 enable_locking=enable_locking,
558 enable_downloads=enable_downloads,
558 enable_downloads=enable_downloads,
559 enable_statistics=enable_statistics,
559 enable_statistics=enable_statistics,
560 private=private,
560 private=private,
561 state=Repository.STATE_CREATED
561 state=Repository.STATE_CREATED
562 )
562 )
563 sa.commit()
563 sa.commit()
564 # we added that repo just now, and make sure we updated server info
564 # we added that repo just now, and make sure we updated server info
565 if db_repo.repo_type == 'git':
565 if db_repo.repo_type == 'git':
566 git_repo = db_repo.scm_instance()
566 git_repo = db_repo.scm_instance()
567 # update repository server-info
567 # update repository server-info
568 log.debug('Running update server info')
568 log.debug('Running update server info')
569 git_repo._update_server_info()
569 git_repo._update_server_info()
570
570
571 db_repo.update_commit_cache()
571 db_repo.update_commit_cache()
572
572
573 config = db_repo._config
573 config = db_repo._config
574 config.set('extensions', 'largefiles', '')
574 config.set('extensions', 'largefiles', '')
575 ScmModel().install_hooks(
575 ScmModel().install_hooks(
576 db_repo.scm_instance(config=config),
576 db_repo.scm_instance(config=config),
577 repo_type=db_repo.repo_type)
577 repo_type=db_repo.repo_type)
578
578
579 removed = []
579 removed = []
580 if remove_obsolete:
580 if remove_obsolete:
581 # remove from database those repositories that are not in the filesystem
581 # remove from database those repositories that are not in the filesystem
582 for repo in sa.query(Repository).all():
582 for repo in sa.query(Repository).all():
583 if repo.repo_name not in initial_repo_list.keys():
583 if repo.repo_name not in initial_repo_list.keys():
584 log.debug("Removing non-existing repository found in db `%s`",
584 log.debug("Removing non-existing repository found in db `%s`",
585 repo.repo_name)
585 repo.repo_name)
586 try:
586 try:
587 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
587 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
588 sa.commit()
588 sa.commit()
589 removed.append(repo.repo_name)
589 removed.append(repo.repo_name)
590 except Exception:
590 except Exception:
591 # don't hold further removals on error
591 # don't hold further removals on error
592 log.error(traceback.format_exc())
592 log.error(traceback.format_exc())
593 sa.rollback()
593 sa.rollback()
594
594
595 def splitter(full_repo_name):
595 def splitter(full_repo_name):
596 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
596 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
597 gr_name = None
597 gr_name = None
598 if len(_parts) == 2:
598 if len(_parts) == 2:
599 gr_name = _parts[0]
599 gr_name = _parts[0]
600 return gr_name
600 return gr_name
601
601
602 initial_repo_group_list = [splitter(x) for x in
602 initial_repo_group_list = [splitter(x) for x in
603 initial_repo_list.keys() if splitter(x)]
603 initial_repo_list.keys() if splitter(x)]
604
604
605 # remove from database those repository groups that are not in the
605 # remove from database those repository groups that are not in the
606 # filesystem due to parent child relationships we need to delete them
606 # filesystem due to parent child relationships we need to delete them
607 # in a specific order of most nested first
607 # in a specific order of most nested first
608 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
608 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
609 nested_sort = lambda gr: len(gr.split('/'))
609 nested_sort = lambda gr: len(gr.split('/'))
610 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
610 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
611 if group_name not in initial_repo_group_list:
611 if group_name not in initial_repo_group_list:
612 repo_group = RepoGroup.get_by_group_name(group_name)
612 repo_group = RepoGroup.get_by_group_name(group_name)
613 if (repo_group.children.all() or
613 if (repo_group.children.all() or
614 not RepoGroupModel().check_exist_filesystem(
614 not RepoGroupModel().check_exist_filesystem(
615 group_name=group_name, exc_on_failure=False)):
615 group_name=group_name, exc_on_failure=False)):
616 continue
616 continue
617
617
618 log.info(
618 log.info(
619 'Removing non-existing repository group found in db `%s`',
619 'Removing non-existing repository group found in db `%s`',
620 group_name)
620 group_name)
621 try:
621 try:
622 RepoGroupModel(sa).delete(group_name, fs_remove=False)
622 RepoGroupModel(sa).delete(group_name, fs_remove=False)
623 sa.commit()
623 sa.commit()
624 removed.append(group_name)
624 removed.append(group_name)
625 except Exception:
625 except Exception:
626 # don't hold further removals on error
626 # don't hold further removals on error
627 log.exception(
627 log.exception(
628 'Unable to remove repository group `%s`',
628 'Unable to remove repository group `%s`',
629 group_name)
629 group_name)
630 sa.rollback()
630 sa.rollback()
631 raise
631 raise
632
632
633 return added, removed
633 return added, removed
634
634
635
635
636 def get_default_cache_settings(settings):
636 def get_default_cache_settings(settings):
637 cache_settings = {}
637 cache_settings = {}
638 for key in settings.keys():
638 for key in settings.keys():
639 for prefix in ['beaker.cache.', 'cache.']:
639 for prefix in ['beaker.cache.', 'cache.']:
640 if key.startswith(prefix):
640 if key.startswith(prefix):
641 name = key.split(prefix)[1].strip()
641 name = key.split(prefix)[1].strip()
642 cache_settings[name] = settings[key].strip()
642 cache_settings[name] = settings[key].strip()
643 return cache_settings
643 return cache_settings
644
644
645
645
646 # set cache regions for beaker so celery can utilise it
646 # set cache regions for beaker so celery can utilise it
647 def add_cache(settings):
647 def add_cache(settings):
648 from rhodecode.lib import caches
648 from rhodecode.lib import caches
649 cache_settings = {'regions': None}
649 cache_settings = {'regions': None}
650 # main cache settings used as default ...
650 # main cache settings used as default ...
651 cache_settings.update(get_default_cache_settings(settings))
651 cache_settings.update(get_default_cache_settings(settings))
652
652
653 if cache_settings['regions']:
653 if cache_settings['regions']:
654 for region in cache_settings['regions'].split(','):
654 for region in cache_settings['regions'].split(','):
655 region = region.strip()
655 region = region.strip()
656 region_settings = {}
656 region_settings = {}
657 for key, value in cache_settings.items():
657 for key, value in cache_settings.items():
658 if key.startswith(region):
658 if key.startswith(region):
659 region_settings[key.split('.')[1]] = value
659 region_settings[key.split('.')[1]] = value
660
660
661 caches.configure_cache_region(
661 caches.configure_cache_region(
662 region, region_settings, cache_settings)
662 region, region_settings, cache_settings)
663
663
664
664
665 def load_rcextensions(root_path):
665 def load_rcextensions(root_path):
666 import rhodecode
666 import rhodecode
667 from rhodecode.config import conf
667 from rhodecode.config import conf
668
668
669 path = os.path.join(root_path, 'rcextensions', '__init__.py')
669 path = os.path.join(root_path, 'rcextensions', '__init__.py')
670 if os.path.isfile(path):
670 if os.path.isfile(path):
671 rcext = create_module('rc', path)
671 rcext = create_module('rc', path)
672 EXT = rhodecode.EXTENSIONS = rcext
672 EXT = rhodecode.EXTENSIONS = rcext
673 log.debug('Found rcextensions now loading %s...', rcext)
673 log.debug('Found rcextensions now loading %s...', rcext)
674
674
675 # Additional mappings that are not present in the pygments lexers
675 # Additional mappings that are not present in the pygments lexers
676 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
676 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
677
677
678 # auto check if the module is not missing any data, set to default if is
678 # auto check if the module is not missing any data, set to default if is
679 # this will help autoupdate new feature of rcext module
679 # this will help autoupdate new feature of rcext module
680 #from rhodecode.config import rcextensions
680 #from rhodecode.config import rcextensions
681 #for k in dir(rcextensions):
681 #for k in dir(rcextensions):
682 # if not k.startswith('_') and not hasattr(EXT, k):
682 # if not k.startswith('_') and not hasattr(EXT, k):
683 # setattr(EXT, k, getattr(rcextensions, k))
683 # setattr(EXT, k, getattr(rcextensions, k))
684
684
685
685
686 def get_custom_lexer(extension):
686 def get_custom_lexer(extension):
687 """
687 """
688 returns a custom lexer if it is defined in rcextensions module, or None
688 returns a custom lexer if it is defined in rcextensions module, or None
689 if there's no custom lexer defined
689 if there's no custom lexer defined
690 """
690 """
691 import rhodecode
691 import rhodecode
692 from pygments import lexers
692 from pygments import lexers
693
693
694 # custom override made by RhodeCode
694 # custom override made by RhodeCode
695 if extension in ['mako']:
695 if extension in ['mako']:
696 return lexers.get_lexer_by_name('html+mako')
696 return lexers.get_lexer_by_name('html+mako')
697
697
698 # check if we didn't define this extension as other lexer
698 # check if we didn't define this extension as other lexer
699 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
699 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
700 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
700 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
701 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
701 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
702 return lexers.get_lexer_by_name(_lexer_name)
702 return lexers.get_lexer_by_name(_lexer_name)
703
703
704
704
705 #==============================================================================
705 #==============================================================================
706 # TEST FUNCTIONS AND CREATORS
706 # TEST FUNCTIONS AND CREATORS
707 #==============================================================================
707 #==============================================================================
708 def create_test_index(repo_location, config):
708 def create_test_index(repo_location, config):
709 """
709 """
710 Makes default test index.
710 Makes default test index.
711 """
711 """
712 import rc_testdata
712 import rc_testdata
713
713
714 rc_testdata.extract_search_index(
714 rc_testdata.extract_search_index(
715 'vcs_search_index', os.path.dirname(config['search.location']))
715 'vcs_search_index', os.path.dirname(config['search.location']))
716
716
717
717
718 def create_test_directory(test_path):
718 def create_test_directory(test_path):
719 """
719 """
720 Create test directory if it doesn't exist.
720 Create test directory if it doesn't exist.
721 """
721 """
722 if not os.path.isdir(test_path):
722 if not os.path.isdir(test_path):
723 log.debug('Creating testdir %s', test_path)
723 log.debug('Creating testdir %s', test_path)
724 os.makedirs(test_path)
724 os.makedirs(test_path)
725
725
726
726
727 def create_test_database(test_path, config):
727 def create_test_database(test_path, config):
728 """
728 """
729 Makes a fresh database.
729 Makes a fresh database.
730 """
730 """
731 from rhodecode.lib.db_manage import DbManage
731 from rhodecode.lib.db_manage import DbManage
732
732
733 # PART ONE create db
733 # PART ONE create db
734 dbconf = config['sqlalchemy.db1.url']
734 dbconf = config['sqlalchemy.db1.url']
735 log.debug('making test db %s', dbconf)
735 log.debug('making test db %s', dbconf)
736
736
737 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
737 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
738 tests=True, cli_args={'force_ask': True})
738 tests=True, cli_args={'force_ask': True})
739 dbmanage.create_tables(override=True)
739 dbmanage.create_tables(override=True)
740 dbmanage.set_db_version()
740 dbmanage.set_db_version()
741 # for tests dynamically set new root paths based on generated content
741 # for tests dynamically set new root paths based on generated content
742 dbmanage.create_settings(dbmanage.config_prompt(test_path))
742 dbmanage.create_settings(dbmanage.config_prompt(test_path))
743 dbmanage.create_default_user()
743 dbmanage.create_default_user()
744 dbmanage.create_test_admin_and_users()
744 dbmanage.create_test_admin_and_users()
745 dbmanage.create_permissions()
745 dbmanage.create_permissions()
746 dbmanage.populate_default_permissions()
746 dbmanage.populate_default_permissions()
747 Session().commit()
747 Session().commit()
748
748
749
749
750 def create_test_repositories(test_path, config):
750 def create_test_repositories(test_path, config):
751 """
751 """
752 Creates test repositories in the temporary directory. Repositories are
752 Creates test repositories in the temporary directory. Repositories are
753 extracted from archives within the rc_testdata package.
753 extracted from archives within the rc_testdata package.
754 """
754 """
755 import rc_testdata
755 import rc_testdata
756 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
756 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
757
757
758 log.debug('making test vcs repositories')
758 log.debug('making test vcs repositories')
759
759
760 idx_path = config['search.location']
760 idx_path = config['search.location']
761 data_path = config['cache_dir']
761 data_path = config['cache_dir']
762
762
763 # clean index and data
763 # clean index and data
764 if idx_path and os.path.exists(idx_path):
764 if idx_path and os.path.exists(idx_path):
765 log.debug('remove %s', idx_path)
765 log.debug('remove %s', idx_path)
766 shutil.rmtree(idx_path)
766 shutil.rmtree(idx_path)
767
767
768 if data_path and os.path.exists(data_path):
768 if data_path and os.path.exists(data_path):
769 log.debug('remove %s', data_path)
769 log.debug('remove %s', data_path)
770 shutil.rmtree(data_path)
770 shutil.rmtree(data_path)
771
771
772 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
772 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
773 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
773 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
774
774
775 # Note: Subversion is in the process of being integrated with the system,
775 # Note: Subversion is in the process of being integrated with the system,
776 # until we have a properly packed version of the test svn repository, this
776 # until we have a properly packed version of the test svn repository, this
777 # tries to copy over the repo from a package "rc_testdata"
777 # tries to copy over the repo from a package "rc_testdata"
778 svn_repo_path = rc_testdata.get_svn_repo_archive()
778 svn_repo_path = rc_testdata.get_svn_repo_archive()
779 with tarfile.open(svn_repo_path) as tar:
779 with tarfile.open(svn_repo_path) as tar:
780 tar.extractall(jn(test_path, SVN_REPO))
780 tar.extractall(jn(test_path, SVN_REPO))
781
781
782
782
783 #==============================================================================
783 #==============================================================================
784 # PASTER COMMANDS
784 # PASTER COMMANDS
785 #==============================================================================
785 #==============================================================================
786 class BasePasterCommand(Command):
786 class BasePasterCommand(Command):
787 """
787 """
788 Abstract Base Class for paster commands.
788 Abstract Base Class for paster commands.
789
789
790 The celery commands are somewhat aggressive about loading
790 The celery commands are somewhat aggressive about loading
791 celery.conf, and since our module sets the `CELERY_LOADER`
791 celery.conf, and since our module sets the `CELERY_LOADER`
792 environment variable to our loader, we have to bootstrap a bit and
792 environment variable to our loader, we have to bootstrap a bit and
793 make sure we've had a chance to load the pylons config off of the
793 make sure we've had a chance to load the pylons config off of the
794 command line, otherwise everything fails.
794 command line, otherwise everything fails.
795 """
795 """
796 min_args = 1
796 min_args = 1
797 min_args_error = "Please provide a paster config file as an argument."
797 min_args_error = "Please provide a paster config file as an argument."
798 takes_config_file = 1
798 takes_config_file = 1
799 requires_config_file = True
799 requires_config_file = True
800
800
801 def notify_msg(self, msg, log=False):
801 def notify_msg(self, msg, log=False):
802 """Make a notification to user, additionally if logger is passed
802 """Make a notification to user, additionally if logger is passed
803 it logs this action using given logger
803 it logs this action using given logger
804
804
805 :param msg: message that will be printed to user
805 :param msg: message that will be printed to user
806 :param log: logging instance, to use to additionally log this message
806 :param log: logging instance, to use to additionally log this message
807
807
808 """
808 """
809 if log and isinstance(log, logging):
809 if log and isinstance(log, logging):
810 log(msg)
810 log(msg)
811
811
812 def run(self, args):
812 def run(self, args):
813 """
813 """
814 Overrides Command.run
814 Overrides Command.run
815
815
816 Checks for a config file argument and loads it.
816 Checks for a config file argument and loads it.
817 """
817 """
818 if len(args) < self.min_args:
818 if len(args) < self.min_args:
819 raise BadCommand(
819 raise BadCommand(
820 self.min_args_error % {'min_args': self.min_args,
820 self.min_args_error % {'min_args': self.min_args,
821 'actual_args': len(args)})
821 'actual_args': len(args)})
822
822
823 # Decrement because we're going to lob off the first argument.
823 # Decrement because we're going to lob off the first argument.
824 # @@ This is hacky
824 # @@ This is hacky
825 self.min_args -= 1
825 self.min_args -= 1
826 self.bootstrap_config(args[0])
826 self.bootstrap_config(args[0])
827 self.update_parser()
827 self.update_parser()
828 return super(BasePasterCommand, self).run(args[1:])
828 return super(BasePasterCommand, self).run(args[1:])
829
829
830 def update_parser(self):
830 def update_parser(self):
831 """
831 """
832 Abstract method. Allows for the class' parser to be updated
832 Abstract method. Allows for the class' parser to be updated
833 before the superclass' `run` method is called. Necessary to
833 before the superclass' `run` method is called. Necessary to
834 allow options/arguments to be passed through to the underlying
834 allow options/arguments to be passed through to the underlying
835 celery command.
835 celery command.
836 """
836 """
837 raise NotImplementedError("Abstract Method.")
837 raise NotImplementedError("Abstract Method.")
838
838
839 def bootstrap_config(self, conf):
839 def bootstrap_config(self, conf):
840 """
840 """
841 Loads the pylons configuration.
841 Loads the pylons configuration.
842 """
842 """
843 from pylons import config as pylonsconfig
843 from pylons import config as pylonsconfig
844
844
845 self.path_to_ini_file = os.path.realpath(conf)
845 self.path_to_ini_file = os.path.realpath(conf)
846 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
846 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
847 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
847 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
848
848
849 def _init_session(self):
849 def _init_session(self):
850 """
850 """
851 Inits SqlAlchemy Session
851 Inits SqlAlchemy Session
852 """
852 """
853 logging.config.fileConfig(self.path_to_ini_file)
853 logging.config.fileConfig(self.path_to_ini_file)
854 from pylons import config
854 from pylons import config
855 from rhodecode.config.utils import initialize_database
855 from rhodecode.config.utils import initialize_database
856
856
857 # get to remove repos !!
857 # get to remove repos !!
858 add_cache(config)
858 add_cache(config)
859 initialize_database(config)
859 initialize_database(config)
860
860
861
861
862 class PartialRenderer(object):
863 """
864 Partial renderer used to render chunks of html used in datagrids
865 use like::
866
867 _render = PartialRenderer('data_table/_dt_elements.mako')
868 _render('quick_menu', args, kwargs)
869 PartialRenderer.h,
870 c,
871 _,
872 ungettext
873 are the template stuff initialized inside and can be re-used later
874
875 :param tmpl_name: template path relate to /templates/ dir
876 """
877
878 def __init__(self, tmpl_name):
879 import rhodecode
880 from pylons import request, tmpl_context as c
881 from pylons.i18n.translation import _, ungettext
882 from rhodecode.lib import helpers as h
883
884 self.tmpl_name = tmpl_name
885 self.rhodecode = rhodecode
886 self.c = c
887 self._ = _
888 self.ungettext = ungettext
889 self.h = h
890 self.request = request
891
892 def _mako_lookup(self):
893 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
894 return _tmpl_lookup.get_template(self.tmpl_name)
895
896 def _update_kwargs_for_render(self, kwargs):
897 """
898 Inject params required for Mako rendering
899 """
900 _kwargs = {
901 '_': self._,
902 'h': self.h,
903 'c': self.c,
904 'request': self.request,
905 '_ungettext': self.ungettext,
906 }
907 _kwargs.update(kwargs)
908 return _kwargs
909
910 def _render_with_exc(self, render_func, args, kwargs):
911 try:
912 return render_func.render(*args, **kwargs)
913 except:
914 log.error(exceptions.text_error_template().render())
915 raise
916
917 def _get_template(self, template_obj, def_name):
918 if def_name:
919 tmpl = template_obj.get_def(def_name)
920 else:
921 tmpl = template_obj
922 return tmpl
923
924 def render(self, def_name, *args, **kwargs):
925 lookup_obj = self._mako_lookup()
926 tmpl = self._get_template(lookup_obj, def_name=def_name)
927 kwargs = self._update_kwargs_for_render(kwargs)
928 return self._render_with_exc(tmpl, args, kwargs)
929
930 def __call__(self, tmpl, *args, **kwargs):
931 return self.render(tmpl, *args, **kwargs)
932
933
934 def password_changed(auth_user, session):
862 def password_changed(auth_user, session):
935 # Never report password change in case of default user or anonymous user.
863 # Never report password change in case of default user or anonymous user.
936 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
864 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
937 return False
865 return False
938
866
939 password_hash = md5(auth_user.password) if auth_user.password else None
867 password_hash = md5(auth_user.password) if auth_user.password else None
940 rhodecode_user = session.get('rhodecode_user', {})
868 rhodecode_user = session.get('rhodecode_user', {})
941 session_password_hash = rhodecode_user.get('password', '')
869 session_password_hash = rhodecode_user.get('password', '')
942 return password_hash != session_password_hash
870 return password_hash != session_password_hash
943
871
944
872
945 def read_opensource_licenses():
873 def read_opensource_licenses():
946 global _license_cache
874 global _license_cache
947
875
948 if not _license_cache:
876 if not _license_cache:
949 licenses = pkg_resources.resource_string(
877 licenses = pkg_resources.resource_string(
950 'rhodecode', 'config/licenses.json')
878 'rhodecode', 'config/licenses.json')
951 _license_cache = json.loads(licenses)
879 _license_cache = json.loads(licenses)
952
880
953 return _license_cache
881 return _license_cache
954
882
955
883
956 def get_registry(request):
884 def get_registry(request):
957 """
885 """
958 Utility to get the pyramid registry from a request. During migration to
886 Utility to get the pyramid registry from a request. During migration to
959 pyramid we sometimes want to use the pyramid registry from pylons context.
887 pyramid we sometimes want to use the pyramid registry from pylons context.
960 Therefore this utility returns `request.registry` for pyramid requests and
888 Therefore this utility returns `request.registry` for pyramid requests and
961 uses `get_current_registry()` for pylons requests.
889 uses `get_current_registry()` for pylons requests.
962 """
890 """
963 try:
891 try:
964 return request.registry
892 return request.registry
965 except AttributeError:
893 except AttributeError:
966 return get_current_registry()
894 return get_current_registry()
967
895
968
896
969 def generate_platform_uuid():
897 def generate_platform_uuid():
970 """
898 """
971 Generates platform UUID based on it's name
899 Generates platform UUID based on it's name
972 """
900 """
973 import platform
901 import platform
974
902
975 try:
903 try:
976 uuid_list = [platform.platform()]
904 uuid_list = [platform.platform()]
977 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
905 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
978 except Exception as e:
906 except Exception as e:
979 log.error('Failed to generate host uuid: %s' % e)
907 log.error('Failed to generate host uuid: %s' % e)
980 return 'UNDEFINED'
908 return 'UNDEFINED'
@@ -1,377 +1,377 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 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 from pyramid.threadlocal import get_current_request
29 from sqlalchemy.sql.expression import false, true
30 from sqlalchemy.sql.expression import false, true
30
31
31 import rhodecode
32 import rhodecode
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib.utils import PartialRenderer
34 from rhodecode.model import BaseModel
34 from rhodecode.model import BaseModel
35 from rhodecode.model.db import Notification, User, UserNotification
35 from rhodecode.model.db import Notification, User, UserNotification
36 from rhodecode.model.meta import Session
36 from rhodecode.model.meta import Session
37 from rhodecode.translation import TranslationString
37 from rhodecode.translation import TranslationString
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class NotificationModel(BaseModel):
42 class NotificationModel(BaseModel):
43
43
44 cls = Notification
44 cls = Notification
45
45
46 def __get_notification(self, notification):
46 def __get_notification(self, notification):
47 if isinstance(notification, Notification):
47 if isinstance(notification, Notification):
48 return notification
48 return notification
49 elif isinstance(notification, (int, long)):
49 elif isinstance(notification, (int, long)):
50 return Notification.get(notification)
50 return Notification.get(notification)
51 else:
51 else:
52 if notification:
52 if notification:
53 raise Exception('notification must be int, long or Instance'
53 raise Exception('notification must be int, long or Instance'
54 ' of Notification got %s' % type(notification))
54 ' of Notification got %s' % type(notification))
55
55
56 def create(
56 def create(
57 self, created_by, notification_subject, notification_body,
57 self, created_by, notification_subject, notification_body,
58 notification_type=Notification.TYPE_MESSAGE, recipients=None,
58 notification_type=Notification.TYPE_MESSAGE, recipients=None,
59 mention_recipients=None, with_email=True, email_kwargs=None):
59 mention_recipients=None, with_email=True, email_kwargs=None):
60 """
60 """
61
61
62 Creates notification of given type
62 Creates notification of given type
63
63
64 :param created_by: int, str or User instance. User who created this
64 :param created_by: int, str or User instance. User who created this
65 notification
65 notification
66 :param notification_subject: subject of notification itself
66 :param notification_subject: subject of notification itself
67 :param notification_body: body of notification text
67 :param notification_body: body of notification text
68 :param notification_type: type of notification, based on that we
68 :param notification_type: type of notification, based on that we
69 pick templates
69 pick templates
70
70
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 created_by_obj = self._get_user(created_by)
84 created_by_obj = self._get_user(created_by)
85 # default MAIN body if not given
85 # default MAIN body if not given
86 email_kwargs = email_kwargs or {'body': notification_body}
86 email_kwargs = email_kwargs or {'body': notification_body}
87 mention_recipients = mention_recipients or set()
87 mention_recipients = mention_recipients or set()
88
88
89 if not created_by_obj:
89 if not created_by_obj:
90 raise Exception('unknown user %s' % created_by)
90 raise Exception('unknown user %s' % created_by)
91
91
92 if recipients is None:
92 if recipients is None:
93 # recipients is None means to all admins
93 # recipients is None means to all admins
94 recipients_objs = User.query().filter(User.admin == true()).all()
94 recipients_objs = User.query().filter(User.admin == true()).all()
95 log.debug('sending notifications %s to admins: %s',
95 log.debug('sending notifications %s to admins: %s',
96 notification_type, recipients_objs)
96 notification_type, recipients_objs)
97 else:
97 else:
98 recipients_objs = []
98 recipients_objs = []
99 for u in recipients:
99 for u in recipients:
100 obj = self._get_user(u)
100 obj = self._get_user(u)
101 if obj:
101 if obj:
102 recipients_objs.append(obj)
102 recipients_objs.append(obj)
103 else: # we didn't find this user, log the error and carry on
103 else: # we didn't find this user, log the error and carry on
104 log.error('cannot notify unknown user %r', u)
104 log.error('cannot notify unknown user %r', u)
105
105
106 recipients_objs = set(recipients_objs)
106 recipients_objs = set(recipients_objs)
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 notification = Notification.create(
115 notification = Notification.create(
116 created_by=created_by_obj, subject=notification_subject,
116 created_by=created_by_obj, subject=notification_subject,
117 body=notification_body, recipients=final_recipients,
117 body=notification_body, recipients=final_recipients,
118 type_=notification_type
118 type_=notification_type
119 )
119 )
120
120
121 if not with_email: # skip sending email, and just create notification
121 if not with_email: # skip sending email, and just create notification
122 return notification
122 return notification
123
123
124 # don't send email to person who created this comment
124 # don't send email to person who created this comment
125 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
125 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
126
126
127 # now notify all recipients in question
127 # now notify all recipients in question
128
128
129 for recipient in rec_objs.union(mention_recipients):
129 for recipient in rec_objs.union(mention_recipients):
130 # inject current recipient
130 # inject current recipient
131 email_kwargs['recipient'] = recipient
131 email_kwargs['recipient'] = recipient
132 email_kwargs['mention'] = recipient in mention_recipients
132 email_kwargs['mention'] = recipient in mention_recipients
133 (subject, headers, email_body,
133 (subject, headers, email_body,
134 email_body_plaintext) = EmailNotificationModel().render_email(
134 email_body_plaintext) = EmailNotificationModel().render_email(
135 notification_type, **email_kwargs)
135 notification_type, **email_kwargs)
136
136
137 log.debug(
137 log.debug(
138 'Creating notification email task for user:`%s`', recipient)
138 'Creating notification email task for user:`%s`', recipient)
139 task = run_task(
139 task = run_task(
140 tasks.send_email, recipient.email, subject,
140 tasks.send_email, recipient.email, subject,
141 email_body_plaintext, email_body)
141 email_body_plaintext, email_body)
142 log.debug('Created email task: %s', task)
142 log.debug('Created email task: %s', task)
143
143
144 return notification
144 return notification
145
145
146 def delete(self, user, notification):
146 def delete(self, user, notification):
147 # we don't want to remove actual notification just the assignment
147 # we don't want to remove actual notification just the assignment
148 try:
148 try:
149 notification = self.__get_notification(notification)
149 notification = self.__get_notification(notification)
150 user = self._get_user(user)
150 user = self._get_user(user)
151 if notification and user:
151 if notification and user:
152 obj = UserNotification.query()\
152 obj = UserNotification.query()\
153 .filter(UserNotification.user == user)\
153 .filter(UserNotification.user == user)\
154 .filter(UserNotification.notification == notification)\
154 .filter(UserNotification.notification == notification)\
155 .one()
155 .one()
156 Session().delete(obj)
156 Session().delete(obj)
157 return True
157 return True
158 except Exception:
158 except Exception:
159 log.error(traceback.format_exc())
159 log.error(traceback.format_exc())
160 raise
160 raise
161
161
162 def get_for_user(self, user, filter_=None):
162 def get_for_user(self, user, filter_=None):
163 """
163 """
164 Get mentions for given user, filter them if filter dict is given
164 Get mentions for given user, filter them if filter dict is given
165 """
165 """
166 user = self._get_user(user)
166 user = self._get_user(user)
167
167
168 q = UserNotification.query()\
168 q = UserNotification.query()\
169 .filter(UserNotification.user == user)\
169 .filter(UserNotification.user == user)\
170 .join((
170 .join((
171 Notification, UserNotification.notification_id ==
171 Notification, UserNotification.notification_id ==
172 Notification.notification_id))
172 Notification.notification_id))
173 if filter_ == ['all']:
173 if filter_ == ['all']:
174 q = q # no filter
174 q = q # no filter
175 elif filter_ == ['unread']:
175 elif filter_ == ['unread']:
176 q = q.filter(UserNotification.read == false())
176 q = q.filter(UserNotification.read == false())
177 elif filter_:
177 elif filter_:
178 q = q.filter(Notification.type_.in_(filter_))
178 q = q.filter(Notification.type_.in_(filter_))
179
179
180 return q
180 return q
181
181
182 def mark_read(self, user, notification):
182 def mark_read(self, user, notification):
183 try:
183 try:
184 notification = self.__get_notification(notification)
184 notification = self.__get_notification(notification)
185 user = self._get_user(user)
185 user = self._get_user(user)
186 if notification and user:
186 if notification and user:
187 obj = UserNotification.query()\
187 obj = UserNotification.query()\
188 .filter(UserNotification.user == user)\
188 .filter(UserNotification.user == user)\
189 .filter(UserNotification.notification == notification)\
189 .filter(UserNotification.notification == notification)\
190 .one()
190 .one()
191 obj.read = True
191 obj.read = True
192 Session().add(obj)
192 Session().add(obj)
193 return True
193 return True
194 except Exception:
194 except Exception:
195 log.error(traceback.format_exc())
195 log.error(traceback.format_exc())
196 raise
196 raise
197
197
198 def mark_all_read_for_user(self, user, filter_=None):
198 def mark_all_read_for_user(self, user, filter_=None):
199 user = self._get_user(user)
199 user = self._get_user(user)
200 q = UserNotification.query()\
200 q = UserNotification.query()\
201 .filter(UserNotification.user == user)\
201 .filter(UserNotification.user == user)\
202 .filter(UserNotification.read == false())\
202 .filter(UserNotification.read == false())\
203 .join((
203 .join((
204 Notification, UserNotification.notification_id ==
204 Notification, UserNotification.notification_id ==
205 Notification.notification_id))
205 Notification.notification_id))
206 if filter_ == ['unread']:
206 if filter_ == ['unread']:
207 q = q.filter(UserNotification.read == false())
207 q = q.filter(UserNotification.read == false())
208 elif filter_:
208 elif filter_:
209 q = q.filter(Notification.type_.in_(filter_))
209 q = q.filter(Notification.type_.in_(filter_))
210
210
211 # this is a little inefficient but sqlalchemy doesn't support
211 # this is a little inefficient but sqlalchemy doesn't support
212 # update on joined tables :(
212 # update on joined tables :(
213 for obj in q.all():
213 for obj in q.all():
214 obj.read = True
214 obj.read = True
215 Session().add(obj)
215 Session().add(obj)
216
216
217 def get_unread_cnt_for_user(self, user):
217 def get_unread_cnt_for_user(self, user):
218 user = self._get_user(user)
218 user = self._get_user(user)
219 return UserNotification.query()\
219 return UserNotification.query()\
220 .filter(UserNotification.read == false())\
220 .filter(UserNotification.read == false())\
221 .filter(UserNotification.user == user).count()
221 .filter(UserNotification.user == user).count()
222
222
223 def get_unread_for_user(self, user):
223 def get_unread_for_user(self, user):
224 user = self._get_user(user)
224 user = self._get_user(user)
225 return [x.notification for x in UserNotification.query()
225 return [x.notification for x in UserNotification.query()
226 .filter(UserNotification.read == false())
226 .filter(UserNotification.read == false())
227 .filter(UserNotification.user == user).all()]
227 .filter(UserNotification.user == user).all()]
228
228
229 def get_user_notification(self, user, notification):
229 def get_user_notification(self, user, notification):
230 user = self._get_user(user)
230 user = self._get_user(user)
231 notification = self.__get_notification(notification)
231 notification = self.__get_notification(notification)
232
232
233 return UserNotification.query()\
233 return UserNotification.query()\
234 .filter(UserNotification.notification == notification)\
234 .filter(UserNotification.notification == notification)\
235 .filter(UserNotification.user == user).scalar()
235 .filter(UserNotification.user == user).scalar()
236
236
237 def make_description(self, notification, translate, show_age=True):
237 def make_description(self, notification, translate, show_age=True):
238 """
238 """
239 Creates a human readable description based on properties
239 Creates a human readable description based on properties
240 of notification object
240 of notification object
241 """
241 """
242 _ = translate
242 _ = translate
243 _map = {
243 _map = {
244 notification.TYPE_CHANGESET_COMMENT: [
244 notification.TYPE_CHANGESET_COMMENT: [
245 _('%(user)s commented on commit %(date_or_age)s'),
245 _('%(user)s commented on commit %(date_or_age)s'),
246 _('%(user)s commented on commit at %(date_or_age)s'),
246 _('%(user)s commented on commit at %(date_or_age)s'),
247 ],
247 ],
248 notification.TYPE_MESSAGE: [
248 notification.TYPE_MESSAGE: [
249 _('%(user)s sent message %(date_or_age)s'),
249 _('%(user)s sent message %(date_or_age)s'),
250 _('%(user)s sent message at %(date_or_age)s'),
250 _('%(user)s sent message at %(date_or_age)s'),
251 ],
251 ],
252 notification.TYPE_MENTION: [
252 notification.TYPE_MENTION: [
253 _('%(user)s mentioned you %(date_or_age)s'),
253 _('%(user)s mentioned you %(date_or_age)s'),
254 _('%(user)s mentioned you at %(date_or_age)s'),
254 _('%(user)s mentioned you at %(date_or_age)s'),
255 ],
255 ],
256 notification.TYPE_REGISTRATION: [
256 notification.TYPE_REGISTRATION: [
257 _('%(user)s registered in RhodeCode %(date_or_age)s'),
257 _('%(user)s registered in RhodeCode %(date_or_age)s'),
258 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
258 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
259 ],
259 ],
260 notification.TYPE_PULL_REQUEST: [
260 notification.TYPE_PULL_REQUEST: [
261 _('%(user)s opened new pull request %(date_or_age)s'),
261 _('%(user)s opened new pull request %(date_or_age)s'),
262 _('%(user)s opened new pull request at %(date_or_age)s'),
262 _('%(user)s opened new pull request at %(date_or_age)s'),
263 ],
263 ],
264 notification.TYPE_PULL_REQUEST_COMMENT: [
264 notification.TYPE_PULL_REQUEST_COMMENT: [
265 _('%(user)s commented on pull request %(date_or_age)s'),
265 _('%(user)s commented on pull request %(date_or_age)s'),
266 _('%(user)s commented on pull request at %(date_or_age)s'),
266 _('%(user)s commented on pull request at %(date_or_age)s'),
267 ],
267 ],
268 }
268 }
269
269
270 templates = _map[notification.type_]
270 templates = _map[notification.type_]
271
271
272 if show_age:
272 if show_age:
273 template = templates[0]
273 template = templates[0]
274 date_or_age = h.age(notification.created_on)
274 date_or_age = h.age(notification.created_on)
275 if translate:
275 if translate:
276 date_or_age = translate(date_or_age)
276 date_or_age = translate(date_or_age)
277
277
278 if isinstance(date_or_age, TranslationString):
278 if isinstance(date_or_age, TranslationString):
279 date_or_age = date_or_age.interpolate()
279 date_or_age = date_or_age.interpolate()
280
280
281 else:
281 else:
282 template = templates[1]
282 template = templates[1]
283 date_or_age = h.format_date(notification.created_on)
283 date_or_age = h.format_date(notification.created_on)
284
284
285 return template % {
285 return template % {
286 'user': notification.created_by_user.username,
286 'user': notification.created_by_user.username,
287 'date_or_age': date_or_age,
287 'date_or_age': date_or_age,
288 }
288 }
289
289
290
290
291 class EmailNotificationModel(BaseModel):
291 class EmailNotificationModel(BaseModel):
292 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
292 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
293 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
293 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
294 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
294 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
295 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
295 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
296 TYPE_MAIN = Notification.TYPE_MESSAGE
296 TYPE_MAIN = Notification.TYPE_MESSAGE
297
297
298 TYPE_PASSWORD_RESET = 'password_reset'
298 TYPE_PASSWORD_RESET = 'password_reset'
299 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
299 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
300 TYPE_EMAIL_TEST = 'email_test'
300 TYPE_EMAIL_TEST = 'email_test'
301 TYPE_TEST = 'test'
301 TYPE_TEST = 'test'
302
302
303 email_types = {
303 email_types = {
304 TYPE_MAIN: 'email_templates/main.mako',
304 TYPE_MAIN: 'email_templates/main.mako',
305 TYPE_TEST: 'email_templates/test.mako',
305 TYPE_TEST: 'email_templates/test.mako',
306 TYPE_EMAIL_TEST: 'email_templates/email_test.mako',
306 TYPE_EMAIL_TEST: 'email_templates/email_test.mako',
307 TYPE_REGISTRATION: 'email_templates/user_registration.mako',
307 TYPE_REGISTRATION: 'email_templates/user_registration.mako',
308 TYPE_PASSWORD_RESET: 'email_templates/password_reset.mako',
308 TYPE_PASSWORD_RESET: 'email_templates/password_reset.mako',
309 TYPE_PASSWORD_RESET_CONFIRMATION: 'email_templates/password_reset_confirmation.mako',
309 TYPE_PASSWORD_RESET_CONFIRMATION: 'email_templates/password_reset_confirmation.mako',
310 TYPE_COMMIT_COMMENT: 'email_templates/commit_comment.mako',
310 TYPE_COMMIT_COMMENT: 'email_templates/commit_comment.mako',
311 TYPE_PULL_REQUEST: 'email_templates/pull_request_review.mako',
311 TYPE_PULL_REQUEST: 'email_templates/pull_request_review.mako',
312 TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.mako',
312 TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.mako',
313 }
313 }
314
314
315 def __init__(self):
315 def __init__(self):
316 """
316 """
317 Example usage::
317 Example usage::
318
318
319 (subject, headers, email_body,
319 (subject, headers, email_body,
320 email_body_plaintext) = EmailNotificationModel().render_email(
320 email_body_plaintext) = EmailNotificationModel().render_email(
321 EmailNotificationModel.TYPE_TEST, **email_kwargs)
321 EmailNotificationModel.TYPE_TEST, **email_kwargs)
322
322
323 """
323 """
324 super(EmailNotificationModel, self).__init__()
324 super(EmailNotificationModel, self).__init__()
325 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
325 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
326
326
327 def _update_kwargs_for_render(self, kwargs):
327 def _update_kwargs_for_render(self, kwargs):
328 """
328 """
329 Inject params required for Mako rendering
329 Inject params required for Mako rendering
330
330
331 :param kwargs:
331 :param kwargs:
332 """
332 """
333
333
334 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
334 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
335 instance_url = h.route_url('home')
335 instance_url = h.route_url('home')
336 _kwargs = {
336 _kwargs = {
337 'instance_url': instance_url,
337 'instance_url': instance_url,
338 'whitespace_filter': self.whitespace_filter
338 'whitespace_filter': self.whitespace_filter
339 }
339 }
340 _kwargs.update(kwargs)
340 _kwargs.update(kwargs)
341 return _kwargs
341 return _kwargs
342
342
343 def whitespace_filter(self, text):
343 def whitespace_filter(self, text):
344 return text.replace('\n', '').replace('\t', '')
344 return text.replace('\n', '').replace('\t', '')
345
345
346 def get_renderer(self, type_):
346 def get_renderer(self, type_, request):
347 template_name = self.email_types[type_]
347 template_name = self.email_types[type_]
348 return PartialRenderer(template_name)
348 return request.get_partial_renderer(template_name)
349
349
350 def render_email(self, type_, **kwargs):
350 def render_email(self, type_, **kwargs):
351 """
351 """
352 renders template for email, and returns a tuple of
352 renders template for email, and returns a tuple of
353 (subject, email_headers, email_html_body, email_plaintext_body)
353 (subject, email_headers, email_html_body, email_plaintext_body)
354 """
354 """
355 # translator and helpers inject
355 # translator and helpers inject
356 _kwargs = self._update_kwargs_for_render(kwargs)
356 _kwargs = self._update_kwargs_for_render(kwargs)
357
357 request = get_current_request()
358 email_template = self.get_renderer(type_)
358 email_template = self.get_renderer(type_, request=request)
359
359
360 subject = email_template.render('subject', **_kwargs)
360 subject = email_template.render('subject', **_kwargs)
361
361
362 try:
362 try:
363 headers = email_template.render('headers', **_kwargs)
363 headers = email_template.render('headers', **_kwargs)
364 except AttributeError:
364 except AttributeError:
365 # it's not defined in template, ok we can skip it
365 # it's not defined in template, ok we can skip it
366 headers = ''
366 headers = ''
367
367
368 try:
368 try:
369 body_plaintext = email_template.render('body_plaintext', **_kwargs)
369 body_plaintext = email_template.render('body_plaintext', **_kwargs)
370 except AttributeError:
370 except AttributeError:
371 # it's not defined in template, ok we can skip it
371 # it's not defined in template, ok we can skip it
372 body_plaintext = ''
372 body_plaintext = ''
373
373
374 # render WHOLE template
374 # render WHOLE template
375 body = email_template.render(None, **_kwargs)
375 body = email_template.render(None, **_kwargs)
376
376
377 return subject, headers, body, body_plaintext
377 return subject, headers, body, body_plaintext
@@ -1,124 +1,143 b''
1 import collections
1 import collections
2 # -*- coding: utf-8 -*-
3
4 # Copyright (C) 2010-2017 RhodeCode GmbH
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License, version 3
8 # (only), as published by the Free Software Foundation.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 #
18 # This program is dual-licensed. If you wish to learn more about the
19 # RhodeCode Enterprise Edition, including its added features, Support services,
20 # and proprietary license terms, please see https://rhodecode.com/licenses/
2
21
3 import pytest
22 import pytest
4
23
5 from rhodecode.lib.utils import PartialRenderer
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
6 from rhodecode.lib.utils2 import AttributeDict
25 from rhodecode.lib.utils2 import AttributeDict
7 from rhodecode.model.notification import EmailNotificationModel
26 from rhodecode.model.notification import EmailNotificationModel
8
27
9
28
10 def test_get_template_obj(app):
29 def test_get_template_obj(app, request_stub):
11 template = EmailNotificationModel().get_renderer(
30 template = EmailNotificationModel().get_renderer(
12 EmailNotificationModel.TYPE_TEST)
31 EmailNotificationModel.TYPE_TEST, request_stub)
13 assert isinstance(template, PartialRenderer)
32 assert isinstance(template, PyramidPartialRenderer)
14
33
15
34
16 def test_render_email(app, http_host_only_stub):
35 def test_render_email(app, http_host_only_stub):
17 kwargs = {}
36 kwargs = {}
18 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
37 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
19 EmailNotificationModel.TYPE_TEST, **kwargs)
38 EmailNotificationModel.TYPE_TEST, **kwargs)
20
39
21 # subject
40 # subject
22 assert subject == 'Test "Subject" hello "world"'
41 assert subject == 'Test "Subject" hello "world"'
23
42
24 # headers
43 # headers
25 assert headers == 'X=Y'
44 assert headers == 'X=Y'
26
45
27 # body plaintext
46 # body plaintext
28 assert body_plaintext == 'Email Plaintext Body'
47 assert body_plaintext == 'Email Plaintext Body'
29
48
30 # body
49 # body
31 notification_footer = 'This is a notification from RhodeCode. http://%s/' \
50 notification_footer = 'This is a notification from RhodeCode. http://%s/' \
32 % http_host_only_stub
51 % http_host_only_stub
33 assert notification_footer in body
52 assert notification_footer in body
34 assert 'Email Body' in body
53 assert 'Email Body' in body
35
54
36
55
37 def test_render_pr_email(app, user_admin):
56 def test_render_pr_email(app, user_admin):
38
57
39 ref = collections.namedtuple('Ref',
58 ref = collections.namedtuple('Ref',
40 'name, type')(
59 'name, type')(
41 'fxies123', 'book'
60 'fxies123', 'book'
42 )
61 )
43
62
44 pr = collections.namedtuple('PullRequest',
63 pr = collections.namedtuple('PullRequest',
45 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
64 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
46 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
65 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
47
66
48 source_repo = target_repo = collections.namedtuple('Repo',
67 source_repo = target_repo = collections.namedtuple('Repo',
49 'type, repo_name')(
68 'type, repo_name')(
50 'hg', 'pull_request_1')
69 'hg', 'pull_request_1')
51
70
52 kwargs = {
71 kwargs = {
53 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
72 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
54 'pull_request': pr,
73 'pull_request': pr,
55 'pull_request_commits': [],
74 'pull_request_commits': [],
56
75
57 'pull_request_target_repo': target_repo,
76 'pull_request_target_repo': target_repo,
58 'pull_request_target_repo_url': 'x',
77 'pull_request_target_repo_url': 'x',
59
78
60 'pull_request_source_repo': source_repo,
79 'pull_request_source_repo': source_repo,
61 'pull_request_source_repo_url': 'x',
80 'pull_request_source_repo_url': 'x',
62
81
63 'pull_request_url': 'http://localhost/pr1',
82 'pull_request_url': 'http://localhost/pr1',
64 }
83 }
65
84
66 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
85 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
67 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
86 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
68
87
69 # subject
88 # subject
70 assert subject == 'Marcin Kuzminski wants you to review pull request #200: "Example Pull Request"'
89 assert subject == 'Marcin Kuzminski wants you to review pull request #200: "Example Pull Request"'
71
90
72
91
73 @pytest.mark.parametrize('mention', [
92 @pytest.mark.parametrize('mention', [
74 True,
93 True,
75 False
94 False
76 ])
95 ])
77 @pytest.mark.parametrize('email_type', [
96 @pytest.mark.parametrize('email_type', [
78 EmailNotificationModel.TYPE_COMMIT_COMMENT,
97 EmailNotificationModel.TYPE_COMMIT_COMMENT,
79 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
98 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
80 ])
99 ])
81 def test_render_comment_subject_no_newlines(app, mention, email_type):
100 def test_render_comment_subject_no_newlines(app, mention, email_type):
82 ref = collections.namedtuple('Ref',
101 ref = collections.namedtuple('Ref',
83 'name, type')(
102 'name, type')(
84 'fxies123', 'book'
103 'fxies123', 'book'
85 )
104 )
86
105
87 pr = collections.namedtuple('PullRequest',
106 pr = collections.namedtuple('PullRequest',
88 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
107 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
89 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
108 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
90
109
91 source_repo = target_repo = collections.namedtuple('Repo',
110 source_repo = target_repo = collections.namedtuple('Repo',
92 'type, repo_name')(
111 'type, repo_name')(
93 'hg', 'pull_request_1')
112 'hg', 'pull_request_1')
94
113
95 kwargs = {
114 kwargs = {
96 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
115 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
97 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
116 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
98 'status_change': 'approved',
117 'status_change': 'approved',
99 'commit_target_repo': AttributeDict(),
118 'commit_target_repo': AttributeDict(),
100 'repo_name': 'test-repo',
119 'repo_name': 'test-repo',
101 'comment_file': 'test-file.py',
120 'comment_file': 'test-file.py',
102 'comment_line': 'n100',
121 'comment_line': 'n100',
103 'comment_type': 'note',
122 'comment_type': 'note',
104 'commit_comment_url': 'http://comment-url',
123 'commit_comment_url': 'http://comment-url',
105 'instance_url': 'http://rc-instance',
124 'instance_url': 'http://rc-instance',
106 'comment_body': 'hello world',
125 'comment_body': 'hello world',
107 'mention': mention,
126 'mention': mention,
108
127
109 'pr_comment_url': 'http://comment-url',
128 'pr_comment_url': 'http://comment-url',
110 'pr_source_repo': AttributeDict(repo_name='foobar'),
129 'pr_source_repo': AttributeDict(repo_name='foobar'),
111 'pr_source_repo_url': 'http://soirce-repo/url',
130 'pr_source_repo_url': 'http://soirce-repo/url',
112 'pull_request': pr,
131 'pull_request': pr,
113 'pull_request_commits': [],
132 'pull_request_commits': [],
114
133
115 'pull_request_target_repo': target_repo,
134 'pull_request_target_repo': target_repo,
116 'pull_request_target_repo_url': 'x',
135 'pull_request_target_repo_url': 'x',
117
136
118 'pull_request_source_repo': source_repo,
137 'pull_request_source_repo': source_repo,
119 'pull_request_source_repo_url': 'x',
138 'pull_request_source_repo_url': 'x',
120 }
139 }
121 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
140 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
122 email_type, **kwargs)
141 email_type, **kwargs)
123
142
124 assert '\n' not in subject
143 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now