##// END OF EJS Templates
helpers: remove usage of pylons session.
marcink -
r2095:ed5795d8 default
parent child Browse files
Show More
@@ -1,85 +1,85 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 import pytest
21 import pytest
22
22
23 from rhodecode.tests import assert_session_flash
23 from rhodecode.tests import assert_session_flash
24 from rhodecode.model.settings import SettingsModel
24 from rhodecode.model.settings import SettingsModel
25
25
26
26
27 def route_path(name, params=None, **kwargs):
27 def route_path(name, params=None, **kwargs):
28 import urllib
28 import urllib
29 from rhodecode.apps._base import ADMIN_PREFIX
29 from rhodecode.apps._base import ADMIN_PREFIX
30
30
31 base_url = {
31 base_url = {
32 'admin_defaults_repositories':
32 'admin_defaults_repositories':
33 ADMIN_PREFIX + '/defaults/repositories',
33 ADMIN_PREFIX + '/defaults/repositories',
34 'admin_defaults_repositories_update':
34 'admin_defaults_repositories_update':
35 ADMIN_PREFIX + '/defaults/repositories/update',
35 ADMIN_PREFIX + '/defaults/repositories/update',
36 }[name].format(**kwargs)
36 }[name].format(**kwargs)
37
37
38 if params:
38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 return base_url
40 return base_url
41
41
42
42
43 @pytest.mark.usefixtures("app")
43 @pytest.mark.usefixtures("app")
44 class TestDefaultsController(object):
44 class TestDefaultsView(object):
45
45
46 def test_index(self, autologin_user):
46 def test_index(self, autologin_user):
47 response = self.app.get(route_path('admin_defaults_repositories'))
47 response = self.app.get(route_path('admin_defaults_repositories'))
48 response.mustcontain('default_repo_private')
48 response.mustcontain('default_repo_private')
49 response.mustcontain('default_repo_enable_statistics')
49 response.mustcontain('default_repo_enable_statistics')
50 response.mustcontain('default_repo_enable_downloads')
50 response.mustcontain('default_repo_enable_downloads')
51 response.mustcontain('default_repo_enable_locking')
51 response.mustcontain('default_repo_enable_locking')
52
52
53 def test_update_params_true_hg(self, autologin_user, csrf_token):
53 def test_update_params_true_hg(self, autologin_user, csrf_token):
54 params = {
54 params = {
55 'default_repo_enable_locking': True,
55 'default_repo_enable_locking': True,
56 'default_repo_enable_downloads': True,
56 'default_repo_enable_downloads': True,
57 'default_repo_enable_statistics': True,
57 'default_repo_enable_statistics': True,
58 'default_repo_private': True,
58 'default_repo_private': True,
59 'default_repo_type': 'hg',
59 'default_repo_type': 'hg',
60 'csrf_token': csrf_token,
60 'csrf_token': csrf_token,
61 }
61 }
62 response = self.app.post(
62 response = self.app.post(
63 route_path('admin_defaults_repositories_update'), params=params)
63 route_path('admin_defaults_repositories_update'), params=params)
64 assert_session_flash(response, 'Default settings updated successfully')
64 assert_session_flash(response, 'Default settings updated successfully')
65
65
66 defs = SettingsModel().get_default_repo_settings()
66 defs = SettingsModel().get_default_repo_settings()
67 del params['csrf_token']
67 del params['csrf_token']
68 assert params == defs
68 assert params == defs
69
69
70 def test_update_params_false_git(self, autologin_user, csrf_token):
70 def test_update_params_false_git(self, autologin_user, csrf_token):
71 params = {
71 params = {
72 'default_repo_enable_locking': False,
72 'default_repo_enable_locking': False,
73 'default_repo_enable_downloads': False,
73 'default_repo_enable_downloads': False,
74 'default_repo_enable_statistics': False,
74 'default_repo_enable_statistics': False,
75 'default_repo_private': False,
75 'default_repo_private': False,
76 'default_repo_type': 'git',
76 'default_repo_type': 'git',
77 'csrf_token': csrf_token,
77 'csrf_token': csrf_token,
78 }
78 }
79 response = self.app.post(
79 response = self.app.post(
80 route_path('admin_defaults_repositories_update'), params=params)
80 route_path('admin_defaults_repositories_update'), params=params)
81 assert_session_flash(response, 'Default settings updated successfully')
81 assert_session_flash(response, 'Default settings updated successfully')
82
82
83 defs = SettingsModel().get_default_repo_settings()
83 defs = SettingsModel().get_default_repo_settings()
84 del params['csrf_token']
84 del params['csrf_token']
85 assert params == defs
85 assert params == defs
@@ -1,617 +1,627 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 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import markupsafe
30 import markupsafe
31 import ipaddress
31 import ipaddress
32 import pyramid.threadlocal
32 import pyramid.threadlocal
33
33
34 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.auth.basic import AuthBasicAuthenticator
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37
37
38 import rhodecode
38 import rhodecode
39 from rhodecode.authentication.base import VCS_TYPE
39 from rhodecode.authentication.base import VCS_TYPE
40 from rhodecode.lib import auth, utils2
40 from rhodecode.lib import auth, utils2
41 from rhodecode.lib import helpers as h
41 from rhodecode.lib import helpers as h
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 from rhodecode.lib.exceptions import UserCreationError
43 from rhodecode.lib.exceptions import UserCreationError
44 from rhodecode.lib.utils import (
44 from rhodecode.lib.utils import (
45 get_repo_slug, set_rhodecode_config, password_changed,
45 get_repo_slug, set_rhodecode_config, password_changed,
46 get_enabled_hook_classes)
46 get_enabled_hook_classes)
47 from rhodecode.lib.utils2 import (
47 from rhodecode.lib.utils2 import (
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
49 from rhodecode.model import meta
49 from rhodecode.model import meta
50 from rhodecode.model.db import Repository, User, ChangesetComment
50 from rhodecode.model.db import Repository, User, ChangesetComment
51 from rhodecode.model.notification import NotificationModel
51 from rhodecode.model.notification import NotificationModel
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
54
54
55 # NOTE(marcink): remove after base controller is no longer required
55 # NOTE(marcink): remove after base controller is no longer required
56 from pylons.controllers import WSGIController
56 from pylons.controllers import WSGIController
57 from pylons.i18n import translation
57 from pylons.i18n import translation
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 # hack to make the migration to pyramid easier
62 # hack to make the migration to pyramid easier
63 def render(template_name, extra_vars=None, cache_key=None,
63 def render(template_name, extra_vars=None, cache_key=None,
64 cache_type=None, cache_expire=None):
64 cache_type=None, cache_expire=None):
65 """Render a template with Mako
65 """Render a template with Mako
66
66
67 Accepts the cache options ``cache_key``, ``cache_type``, and
67 Accepts the cache options ``cache_key``, ``cache_type``, and
68 ``cache_expire``.
68 ``cache_expire``.
69
69
70 """
70 """
71 from pylons.templating import literal
71 from pylons.templating import literal
72 from pylons.templating import cached_template, pylons_globals
72 from pylons.templating import cached_template, pylons_globals
73
73
74 # Create a render callable for the cache function
74 # Create a render callable for the cache function
75 def render_template():
75 def render_template():
76 # Pull in extra vars if needed
76 # Pull in extra vars if needed
77 globs = extra_vars or {}
77 globs = extra_vars or {}
78
78
79 # Second, get the globals
79 # Second, get the globals
80 globs.update(pylons_globals())
80 globs.update(pylons_globals())
81
81
82 globs['_ungettext'] = globs['ungettext']
82 globs['_ungettext'] = globs['ungettext']
83 # Grab a template reference
83 # Grab a template reference
84 template = globs['app_globals'].mako_lookup.get_template(template_name)
84 template = globs['app_globals'].mako_lookup.get_template(template_name)
85
85
86 return literal(template.render_unicode(**globs))
86 return literal(template.render_unicode(**globs))
87
87
88 return cached_template(template_name, render_template, cache_key=cache_key,
88 return cached_template(template_name, render_template, cache_key=cache_key,
89 cache_type=cache_type, cache_expire=cache_expire)
89 cache_type=cache_type, cache_expire=cache_expire)
90
90
91 def _filter_proxy(ip):
91 def _filter_proxy(ip):
92 """
92 """
93 Passed in IP addresses in HEADERS can be in a special format of multiple
93 Passed in IP addresses in HEADERS can be in a special format of multiple
94 ips. Those comma separated IPs are passed from various proxies in the
94 ips. Those comma separated IPs are passed from various proxies in the
95 chain of request processing. The left-most being the original client.
95 chain of request processing. The left-most being the original client.
96 We only care about the first IP which came from the org. client.
96 We only care about the first IP which came from the org. client.
97
97
98 :param ip: ip string from headers
98 :param ip: ip string from headers
99 """
99 """
100 if ',' in ip:
100 if ',' in ip:
101 _ips = ip.split(',')
101 _ips = ip.split(',')
102 _first_ip = _ips[0].strip()
102 _first_ip = _ips[0].strip()
103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
104 return _first_ip
104 return _first_ip
105 return ip
105 return ip
106
106
107
107
108 def _filter_port(ip):
108 def _filter_port(ip):
109 """
109 """
110 Removes a port from ip, there are 4 main cases to handle here.
110 Removes a port from ip, there are 4 main cases to handle here.
111 - ipv4 eg. 127.0.0.1
111 - ipv4 eg. 127.0.0.1
112 - ipv6 eg. ::1
112 - ipv6 eg. ::1
113 - ipv4+port eg. 127.0.0.1:8080
113 - ipv4+port eg. 127.0.0.1:8080
114 - ipv6+port eg. [::1]:8080
114 - ipv6+port eg. [::1]:8080
115
115
116 :param ip:
116 :param ip:
117 """
117 """
118 def is_ipv6(ip_addr):
118 def is_ipv6(ip_addr):
119 if hasattr(socket, 'inet_pton'):
119 if hasattr(socket, 'inet_pton'):
120 try:
120 try:
121 socket.inet_pton(socket.AF_INET6, ip_addr)
121 socket.inet_pton(socket.AF_INET6, ip_addr)
122 except socket.error:
122 except socket.error:
123 return False
123 return False
124 else:
124 else:
125 # fallback to ipaddress
125 # fallback to ipaddress
126 try:
126 try:
127 ipaddress.IPv6Address(safe_unicode(ip_addr))
127 ipaddress.IPv6Address(safe_unicode(ip_addr))
128 except Exception:
128 except Exception:
129 return False
129 return False
130 return True
130 return True
131
131
132 if ':' not in ip: # must be ipv4 pure ip
132 if ':' not in ip: # must be ipv4 pure ip
133 return ip
133 return ip
134
134
135 if '[' in ip and ']' in ip: # ipv6 with port
135 if '[' in ip and ']' in ip: # ipv6 with port
136 return ip.split(']')[0][1:].lower()
136 return ip.split(']')[0][1:].lower()
137
137
138 # must be ipv6 or ipv4 with port
138 # must be ipv6 or ipv4 with port
139 if is_ipv6(ip):
139 if is_ipv6(ip):
140 return ip
140 return ip
141 else:
141 else:
142 ip, _port = ip.split(':')[:2] # means ipv4+port
142 ip, _port = ip.split(':')[:2] # means ipv4+port
143 return ip
143 return ip
144
144
145
145
146 def get_ip_addr(environ):
146 def get_ip_addr(environ):
147 proxy_key = 'HTTP_X_REAL_IP'
147 proxy_key = 'HTTP_X_REAL_IP'
148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
149 def_key = 'REMOTE_ADDR'
149 def_key = 'REMOTE_ADDR'
150 _filters = lambda x: _filter_port(_filter_proxy(x))
150 _filters = lambda x: _filter_port(_filter_proxy(x))
151
151
152 ip = environ.get(proxy_key)
152 ip = environ.get(proxy_key)
153 if ip:
153 if ip:
154 return _filters(ip)
154 return _filters(ip)
155
155
156 ip = environ.get(proxy_key2)
156 ip = environ.get(proxy_key2)
157 if ip:
157 if ip:
158 return _filters(ip)
158 return _filters(ip)
159
159
160 ip = environ.get(def_key, '0.0.0.0')
160 ip = environ.get(def_key, '0.0.0.0')
161 return _filters(ip)
161 return _filters(ip)
162
162
163
163
164 def get_server_ip_addr(environ, log_errors=True):
164 def get_server_ip_addr(environ, log_errors=True):
165 hostname = environ.get('SERVER_NAME')
165 hostname = environ.get('SERVER_NAME')
166 try:
166 try:
167 return socket.gethostbyname(hostname)
167 return socket.gethostbyname(hostname)
168 except Exception as e:
168 except Exception as e:
169 if log_errors:
169 if log_errors:
170 # in some cases this lookup is not possible, and we don't want to
170 # in some cases this lookup is not possible, and we don't want to
171 # make it an exception in logs
171 # make it an exception in logs
172 log.exception('Could not retrieve server ip address: %s', e)
172 log.exception('Could not retrieve server ip address: %s', e)
173 return hostname
173 return hostname
174
174
175
175
176 def get_server_port(environ):
176 def get_server_port(environ):
177 return environ.get('SERVER_PORT')
177 return environ.get('SERVER_PORT')
178
178
179
179
180 def get_access_path(environ):
180 def get_access_path(environ):
181 path = environ.get('PATH_INFO')
181 path = environ.get('PATH_INFO')
182 org_req = environ.get('pylons.original_request')
182 org_req = environ.get('pylons.original_request')
183 if org_req:
183 if org_req:
184 path = org_req.environ.get('PATH_INFO')
184 path = org_req.environ.get('PATH_INFO')
185 return path
185 return path
186
186
187
187
188 def get_user_agent(environ):
188 def get_user_agent(environ):
189 return environ.get('HTTP_USER_AGENT')
189 return environ.get('HTTP_USER_AGENT')
190
190
191
191
192 def vcs_operation_context(
192 def vcs_operation_context(
193 environ, repo_name, username, action, scm, check_locking=True,
193 environ, repo_name, username, action, scm, check_locking=True,
194 is_shadow_repo=False):
194 is_shadow_repo=False):
195 """
195 """
196 Generate the context for a vcs operation, e.g. push or pull.
196 Generate the context for a vcs operation, e.g. push or pull.
197
197
198 This context is passed over the layers so that hooks triggered by the
198 This context is passed over the layers so that hooks triggered by the
199 vcs operation know details like the user, the user's IP address etc.
199 vcs operation know details like the user, the user's IP address etc.
200
200
201 :param check_locking: Allows to switch of the computation of the locking
201 :param check_locking: Allows to switch of the computation of the locking
202 data. This serves mainly the need of the simplevcs middleware to be
202 data. This serves mainly the need of the simplevcs middleware to be
203 able to disable this for certain operations.
203 able to disable this for certain operations.
204
204
205 """
205 """
206 # Tri-state value: False: unlock, None: nothing, True: lock
206 # Tri-state value: False: unlock, None: nothing, True: lock
207 make_lock = None
207 make_lock = None
208 locked_by = [None, None, None]
208 locked_by = [None, None, None]
209 is_anonymous = username == User.DEFAULT_USER
209 is_anonymous = username == User.DEFAULT_USER
210 if not is_anonymous and check_locking:
210 if not is_anonymous and check_locking:
211 log.debug('Checking locking on repository "%s"', repo_name)
211 log.debug('Checking locking on repository "%s"', repo_name)
212 user = User.get_by_username(username)
212 user = User.get_by_username(username)
213 repo = Repository.get_by_repo_name(repo_name)
213 repo = Repository.get_by_repo_name(repo_name)
214 make_lock, __, locked_by = repo.get_locking_state(
214 make_lock, __, locked_by = repo.get_locking_state(
215 action, user.user_id)
215 action, user.user_id)
216
216
217 settings_model = VcsSettingsModel(repo=repo_name)
217 settings_model = VcsSettingsModel(repo=repo_name)
218 ui_settings = settings_model.get_ui_settings()
218 ui_settings = settings_model.get_ui_settings()
219
219
220 extras = {
220 extras = {
221 'ip': get_ip_addr(environ),
221 'ip': get_ip_addr(environ),
222 'username': username,
222 'username': username,
223 'action': action,
223 'action': action,
224 'repository': repo_name,
224 'repository': repo_name,
225 'scm': scm,
225 'scm': scm,
226 'config': rhodecode.CONFIG['__file__'],
226 'config': rhodecode.CONFIG['__file__'],
227 'make_lock': make_lock,
227 'make_lock': make_lock,
228 'locked_by': locked_by,
228 'locked_by': locked_by,
229 'server_url': utils2.get_server_url(environ),
229 'server_url': utils2.get_server_url(environ),
230 'user_agent': get_user_agent(environ),
230 'user_agent': get_user_agent(environ),
231 'hooks': get_enabled_hook_classes(ui_settings),
231 'hooks': get_enabled_hook_classes(ui_settings),
232 'is_shadow_repo': is_shadow_repo,
232 'is_shadow_repo': is_shadow_repo,
233 }
233 }
234 return extras
234 return extras
235
235
236
236
237 class BasicAuth(AuthBasicAuthenticator):
237 class BasicAuth(AuthBasicAuthenticator):
238
238
239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
240 initial_call_detection=False, acl_repo_name=None):
240 initial_call_detection=False, acl_repo_name=None):
241 self.realm = realm
241 self.realm = realm
242 self.initial_call = initial_call_detection
242 self.initial_call = initial_call_detection
243 self.authfunc = authfunc
243 self.authfunc = authfunc
244 self.registry = registry
244 self.registry = registry
245 self.acl_repo_name = acl_repo_name
245 self.acl_repo_name = acl_repo_name
246 self._rc_auth_http_code = auth_http_code
246 self._rc_auth_http_code = auth_http_code
247
247
248 def _get_response_from_code(self, http_code):
248 def _get_response_from_code(self, http_code):
249 try:
249 try:
250 return get_exception(safe_int(http_code))
250 return get_exception(safe_int(http_code))
251 except Exception:
251 except Exception:
252 log.exception('Failed to fetch response for code %s' % http_code)
252 log.exception('Failed to fetch response for code %s' % http_code)
253 return HTTPForbidden
253 return HTTPForbidden
254
254
255 def build_authentication(self):
255 def build_authentication(self):
256 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
256 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
257 if self._rc_auth_http_code and not self.initial_call:
257 if self._rc_auth_http_code and not self.initial_call:
258 # return alternative HTTP code if alternative http return code
258 # return alternative HTTP code if alternative http return code
259 # is specified in RhodeCode config, but ONLY if it's not the
259 # is specified in RhodeCode config, but ONLY if it's not the
260 # FIRST call
260 # FIRST call
261 custom_response_klass = self._get_response_from_code(
261 custom_response_klass = self._get_response_from_code(
262 self._rc_auth_http_code)
262 self._rc_auth_http_code)
263 return custom_response_klass(headers=head)
263 return custom_response_klass(headers=head)
264 return HTTPUnauthorized(headers=head)
264 return HTTPUnauthorized(headers=head)
265
265
266 def authenticate(self, environ):
266 def authenticate(self, environ):
267 authorization = AUTHORIZATION(environ)
267 authorization = AUTHORIZATION(environ)
268 if not authorization:
268 if not authorization:
269 return self.build_authentication()
269 return self.build_authentication()
270 (authmeth, auth) = authorization.split(' ', 1)
270 (authmeth, auth) = authorization.split(' ', 1)
271 if 'basic' != authmeth.lower():
271 if 'basic' != authmeth.lower():
272 return self.build_authentication()
272 return self.build_authentication()
273 auth = auth.strip().decode('base64')
273 auth = auth.strip().decode('base64')
274 _parts = auth.split(':', 1)
274 _parts = auth.split(':', 1)
275 if len(_parts) == 2:
275 if len(_parts) == 2:
276 username, password = _parts
276 username, password = _parts
277 if self.authfunc(
277 if self.authfunc(
278 username, password, environ, VCS_TYPE,
278 username, password, environ, VCS_TYPE,
279 registry=self.registry, acl_repo_name=self.acl_repo_name):
279 registry=self.registry, acl_repo_name=self.acl_repo_name):
280 return username
280 return username
281 if username and password:
281 if username and password:
282 # we mark that we actually executed authentication once, at
282 # we mark that we actually executed authentication once, at
283 # that point we can use the alternative auth code
283 # that point we can use the alternative auth code
284 self.initial_call = False
284 self.initial_call = False
285
285
286 return self.build_authentication()
286 return self.build_authentication()
287
287
288 __call__ = authenticate
288 __call__ = authenticate
289
289
290
290
291 def calculate_version_hash(config):
291 def calculate_version_hash(config):
292 return md5(
292 return md5(
293 config.get('beaker.session.secret', '') +
293 config.get('beaker.session.secret', '') +
294 rhodecode.__version__)[:8]
294 rhodecode.__version__)[:8]
295
295
296
296
297 def get_current_lang(request):
297 def get_current_lang(request):
298 # NOTE(marcink): remove after pyramid move
298 # NOTE(marcink): remove after pyramid move
299 try:
299 try:
300 return translation.get_lang()[0]
300 return translation.get_lang()[0]
301 except:
301 except:
302 pass
302 pass
303
303
304 return getattr(request, '_LOCALE_', request.locale_name)
304 return getattr(request, '_LOCALE_', request.locale_name)
305
305
306
306
307 def attach_context_attributes(context, request, user_id):
307 def attach_context_attributes(context, request, user_id):
308 """
308 """
309 Attach variables into template context called `c`, please note that
309 Attach variables into template context called `c`, please note that
310 request could be pylons or pyramid request in here.
310 request could be pylons or pyramid request in here.
311 """
311 """
312 # NOTE(marcink): remove check after pyramid migration
312 # NOTE(marcink): remove check after pyramid migration
313 if hasattr(request, 'registry'):
313 if hasattr(request, 'registry'):
314 config = request.registry.settings
314 config = request.registry.settings
315 else:
315 else:
316 from pylons import config
316 from pylons import config
317
317
318 rc_config = SettingsModel().get_all_settings(cache=True)
318 rc_config = SettingsModel().get_all_settings(cache=True)
319
319
320 context.rhodecode_version = rhodecode.__version__
320 context.rhodecode_version = rhodecode.__version__
321 context.rhodecode_edition = config.get('rhodecode.edition')
321 context.rhodecode_edition = config.get('rhodecode.edition')
322 # unique secret + version does not leak the version but keep consistency
322 # unique secret + version does not leak the version but keep consistency
323 context.rhodecode_version_hash = calculate_version_hash(config)
323 context.rhodecode_version_hash = calculate_version_hash(config)
324
324
325 # Default language set for the incoming request
325 # Default language set for the incoming request
326 context.language = get_current_lang(request)
326 context.language = get_current_lang(request)
327
327
328 # Visual options
328 # Visual options
329 context.visual = AttributeDict({})
329 context.visual = AttributeDict({})
330
330
331 # DB stored Visual Items
331 # DB stored Visual Items
332 context.visual.show_public_icon = str2bool(
332 context.visual.show_public_icon = str2bool(
333 rc_config.get('rhodecode_show_public_icon'))
333 rc_config.get('rhodecode_show_public_icon'))
334 context.visual.show_private_icon = str2bool(
334 context.visual.show_private_icon = str2bool(
335 rc_config.get('rhodecode_show_private_icon'))
335 rc_config.get('rhodecode_show_private_icon'))
336 context.visual.stylify_metatags = str2bool(
336 context.visual.stylify_metatags = str2bool(
337 rc_config.get('rhodecode_stylify_metatags'))
337 rc_config.get('rhodecode_stylify_metatags'))
338 context.visual.dashboard_items = safe_int(
338 context.visual.dashboard_items = safe_int(
339 rc_config.get('rhodecode_dashboard_items', 100))
339 rc_config.get('rhodecode_dashboard_items', 100))
340 context.visual.admin_grid_items = safe_int(
340 context.visual.admin_grid_items = safe_int(
341 rc_config.get('rhodecode_admin_grid_items', 100))
341 rc_config.get('rhodecode_admin_grid_items', 100))
342 context.visual.repository_fields = str2bool(
342 context.visual.repository_fields = str2bool(
343 rc_config.get('rhodecode_repository_fields'))
343 rc_config.get('rhodecode_repository_fields'))
344 context.visual.show_version = str2bool(
344 context.visual.show_version = str2bool(
345 rc_config.get('rhodecode_show_version'))
345 rc_config.get('rhodecode_show_version'))
346 context.visual.use_gravatar = str2bool(
346 context.visual.use_gravatar = str2bool(
347 rc_config.get('rhodecode_use_gravatar'))
347 rc_config.get('rhodecode_use_gravatar'))
348 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
348 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
349 context.visual.default_renderer = rc_config.get(
349 context.visual.default_renderer = rc_config.get(
350 'rhodecode_markup_renderer', 'rst')
350 'rhodecode_markup_renderer', 'rst')
351 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
351 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
352 context.visual.rhodecode_support_url = \
352 context.visual.rhodecode_support_url = \
353 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
353 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
354
354
355 context.visual.affected_files_cut_off = 60
355 context.visual.affected_files_cut_off = 60
356
356
357 context.pre_code = rc_config.get('rhodecode_pre_code')
357 context.pre_code = rc_config.get('rhodecode_pre_code')
358 context.post_code = rc_config.get('rhodecode_post_code')
358 context.post_code = rc_config.get('rhodecode_post_code')
359 context.rhodecode_name = rc_config.get('rhodecode_title')
359 context.rhodecode_name = rc_config.get('rhodecode_title')
360 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
360 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
361 # if we have specified default_encoding in the request, it has more
361 # if we have specified default_encoding in the request, it has more
362 # priority
362 # priority
363 if request.GET.get('default_encoding'):
363 if request.GET.get('default_encoding'):
364 context.default_encodings.insert(0, request.GET.get('default_encoding'))
364 context.default_encodings.insert(0, request.GET.get('default_encoding'))
365 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
365 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
366
366
367 # INI stored
367 # INI stored
368 context.labs_active = str2bool(
368 context.labs_active = str2bool(
369 config.get('labs_settings_active', 'false'))
369 config.get('labs_settings_active', 'false'))
370 context.visual.allow_repo_location_change = str2bool(
370 context.visual.allow_repo_location_change = str2bool(
371 config.get('allow_repo_location_change', True))
371 config.get('allow_repo_location_change', True))
372 context.visual.allow_custom_hooks_settings = str2bool(
372 context.visual.allow_custom_hooks_settings = str2bool(
373 config.get('allow_custom_hooks_settings', True))
373 config.get('allow_custom_hooks_settings', True))
374 context.debug_style = str2bool(config.get('debug_style', False))
374 context.debug_style = str2bool(config.get('debug_style', False))
375
375
376 context.rhodecode_instanceid = config.get('instance_id')
376 context.rhodecode_instanceid = config.get('instance_id')
377
377
378 context.visual.cut_off_limit_diff = safe_int(
378 context.visual.cut_off_limit_diff = safe_int(
379 config.get('cut_off_limit_diff'))
379 config.get('cut_off_limit_diff'))
380 context.visual.cut_off_limit_file = safe_int(
380 context.visual.cut_off_limit_file = safe_int(
381 config.get('cut_off_limit_file'))
381 config.get('cut_off_limit_file'))
382
382
383 # AppEnlight
383 # AppEnlight
384 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
384 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
385 context.appenlight_api_public_key = config.get(
385 context.appenlight_api_public_key = config.get(
386 'appenlight.api_public_key', '')
386 'appenlight.api_public_key', '')
387 context.appenlight_server_url = config.get('appenlight.server_url', '')
387 context.appenlight_server_url = config.get('appenlight.server_url', '')
388
388
389 # JS template context
389 # JS template context
390 context.template_context = {
390 context.template_context = {
391 'repo_name': None,
391 'repo_name': None,
392 'repo_type': None,
392 'repo_type': None,
393 'repo_landing_commit': None,
393 'repo_landing_commit': None,
394 'rhodecode_user': {
394 'rhodecode_user': {
395 'username': None,
395 'username': None,
396 'email': None,
396 'email': None,
397 'notification_status': False
397 'notification_status': False
398 },
398 },
399 'visual': {
399 'visual': {
400 'default_renderer': None
400 'default_renderer': None
401 },
401 },
402 'commit_data': {
402 'commit_data': {
403 'commit_id': None
403 'commit_id': None
404 },
404 },
405 'pull_request_data': {'pull_request_id': None},
405 'pull_request_data': {'pull_request_id': None},
406 'timeago': {
406 'timeago': {
407 'refresh_time': 120 * 1000,
407 'refresh_time': 120 * 1000,
408 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
408 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
409 },
409 },
410 'pyramid_dispatch': {
410 'pyramid_dispatch': {
411
411
412 },
412 },
413 'extra': {'plugins': {}}
413 'extra': {'plugins': {}}
414 }
414 }
415 # END CONFIG VARS
415 # END CONFIG VARS
416
416
417 # TODO: This dosn't work when called from pylons compatibility tween.
417 # TODO: This dosn't work when called from pylons compatibility tween.
418 # Fix this and remove it from base controller.
418 # Fix this and remove it from base controller.
419 # context.repo_name = get_repo_slug(request) # can be empty
419 # context.repo_name = get_repo_slug(request) # can be empty
420
420
421 diffmode = 'sideside'
421 diffmode = 'sideside'
422 if request.GET.get('diffmode'):
422 if request.GET.get('diffmode'):
423 if request.GET['diffmode'] == 'unified':
423 if request.GET['diffmode'] == 'unified':
424 diffmode = 'unified'
424 diffmode = 'unified'
425 elif request.session.get('diffmode'):
425 elif request.session.get('diffmode'):
426 diffmode = request.session['diffmode']
426 diffmode = request.session['diffmode']
427
427
428 context.diffmode = diffmode
428 context.diffmode = diffmode
429
429
430 if request.session.get('diffmode') != diffmode:
430 if request.session.get('diffmode') != diffmode:
431 request.session['diffmode'] = diffmode
431 request.session['diffmode'] = diffmode
432
432
433 context.csrf_token = auth.get_csrf_token(session=request.session)
433 context.csrf_token = auth.get_csrf_token(session=request.session)
434 context.backends = rhodecode.BACKENDS.keys()
434 context.backends = rhodecode.BACKENDS.keys()
435 context.backends.sort()
435 context.backends.sort()
436 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
436 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
437
437
438 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
438 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
439 # given request will ALWAYS be pyramid one
439 # given request will ALWAYS be pyramid one
440 pyramid_request = pyramid.threadlocal.get_current_request()
440 pyramid_request = pyramid.threadlocal.get_current_request()
441 context.pyramid_request = pyramid_request
441 context.pyramid_request = pyramid_request
442
442
443 # web case
443 # web case
444 if hasattr(pyramid_request, 'user'):
444 if hasattr(pyramid_request, 'user'):
445 context.auth_user = pyramid_request.user
445 context.auth_user = pyramid_request.user
446 context.rhodecode_user = pyramid_request.user
446 context.rhodecode_user = pyramid_request.user
447
447
448 # api case
448 # api case
449 if hasattr(pyramid_request, 'rpc_user'):
449 if hasattr(pyramid_request, 'rpc_user'):
450 context.auth_user = pyramid_request.rpc_user
450 context.auth_user = pyramid_request.rpc_user
451 context.rhodecode_user = pyramid_request.rpc_user
451 context.rhodecode_user = pyramid_request.rpc_user
452
452
453 # attach the whole call context to the request
453 # attach the whole call context to the request
454 request.call_context = context
454 request.call_context = context
455
455
456
456
457 def get_auth_user(request):
457 def get_auth_user(request):
458 environ = request.environ
458 environ = request.environ
459 session = request.session
459 session = request.session
460
460
461 ip_addr = get_ip_addr(environ)
461 ip_addr = get_ip_addr(environ)
462 # make sure that we update permissions each time we call controller
462 # make sure that we update permissions each time we call controller
463 _auth_token = (request.GET.get('auth_token', '') or
463 _auth_token = (request.GET.get('auth_token', '') or
464 request.GET.get('api_key', ''))
464 request.GET.get('api_key', ''))
465
465
466 if _auth_token:
466 if _auth_token:
467 # when using API_KEY we assume user exists, and
467 # when using API_KEY we assume user exists, and
468 # doesn't need auth based on cookies.
468 # doesn't need auth based on cookies.
469 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
469 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
470 authenticated = False
470 authenticated = False
471 else:
471 else:
472 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
472 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
473 try:
473 try:
474 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
474 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
475 ip_addr=ip_addr)
475 ip_addr=ip_addr)
476 except UserCreationError as e:
476 except UserCreationError as e:
477 h.flash(e, 'error')
477 h.flash(e, 'error')
478 # container auth or other auth functions that create users
478 # container auth or other auth functions that create users
479 # on the fly can throw this exception signaling that there's
479 # on the fly can throw this exception signaling that there's
480 # issue with user creation, explanation should be provided
480 # issue with user creation, explanation should be provided
481 # in Exception itself. We then create a simple blank
481 # in Exception itself. We then create a simple blank
482 # AuthUser
482 # AuthUser
483 auth_user = AuthUser(ip_addr=ip_addr)
483 auth_user = AuthUser(ip_addr=ip_addr)
484
484
485 if password_changed(auth_user, session):
485 if password_changed(auth_user, session):
486 session.invalidate()
486 session.invalidate()
487 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
487 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
488 auth_user = AuthUser(ip_addr=ip_addr)
488 auth_user = AuthUser(ip_addr=ip_addr)
489
489
490 authenticated = cookie_store.get('is_authenticated')
490 authenticated = cookie_store.get('is_authenticated')
491
491
492 if not auth_user.is_authenticated and auth_user.is_user_object:
492 if not auth_user.is_authenticated and auth_user.is_user_object:
493 # user is not authenticated and not empty
493 # user is not authenticated and not empty
494 auth_user.set_authenticated(authenticated)
494 auth_user.set_authenticated(authenticated)
495
495
496 return auth_user
496 return auth_user
497
497
498
498
499 class BaseController(WSGIController):
499 class BaseController(WSGIController):
500
500
501 def __before__(self):
501 def __before__(self):
502 """
502 """
503 __before__ is called before controller methods and after __call__
503 __before__ is called before controller methods and after __call__
504 """
504 """
505 # on each call propagate settings calls into global settings.
505 # on each call propagate settings calls into global settings.
506 from pylons import config
506 from pylons import config
507 from pylons import tmpl_context as c, request, url
507 from pylons import tmpl_context as c, request, url
508 set_rhodecode_config(config)
508 set_rhodecode_config(config)
509 attach_context_attributes(c, request, self._rhodecode_user.user_id)
509 attach_context_attributes(c, request, self._rhodecode_user.user_id)
510
510
511 # TODO: Remove this when fixed in attach_context_attributes()
511 # TODO: Remove this when fixed in attach_context_attributes()
512 c.repo_name = get_repo_slug(request) # can be empty
512 c.repo_name = get_repo_slug(request) # can be empty
513
513
514 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
514 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
515 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
515 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
516 self.sa = meta.Session
516 self.sa = meta.Session
517 self.scm_model = ScmModel(self.sa)
517 self.scm_model = ScmModel(self.sa)
518
518
519 # set user language
519 # set user language
520 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
520 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
521 if user_lang:
521 if user_lang:
522 translation.set_lang(user_lang)
522 translation.set_lang(user_lang)
523 log.debug('set language to %s for user %s',
523 log.debug('set language to %s for user %s',
524 user_lang, self._rhodecode_user)
524 user_lang, self._rhodecode_user)
525
525
526 def _dispatch_redirect(self, with_url, environ, start_response):
526 def _dispatch_redirect(self, with_url, environ, start_response):
527 from webob.exc import HTTPFound
527 from webob.exc import HTTPFound
528 resp = HTTPFound(with_url)
528 resp = HTTPFound(with_url)
529 environ['SCRIPT_NAME'] = '' # handle prefix middleware
529 environ['SCRIPT_NAME'] = '' # handle prefix middleware
530 environ['PATH_INFO'] = with_url
530 environ['PATH_INFO'] = with_url
531 return resp(environ, start_response)
531 return resp(environ, start_response)
532
532
533 def __call__(self, environ, start_response):
533 def __call__(self, environ, start_response):
534 """Invoke the Controller"""
534 """Invoke the Controller"""
535 # WSGIController.__call__ dispatches to the Controller method
535 # WSGIController.__call__ dispatches to the Controller method
536 # the request is routed to. This routing information is
536 # the request is routed to. This routing information is
537 # available in environ['pylons.routes_dict']
537 # available in environ['pylons.routes_dict']
538 from rhodecode.lib import helpers as h
538 from rhodecode.lib import helpers as h
539 from pylons import tmpl_context as c, request, url
539 from pylons import tmpl_context as c, request, url
540
540
541 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
541 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
542 if environ.get('debugtoolbar.wants_pylons_context', False):
542 if environ.get('debugtoolbar.wants_pylons_context', False):
543 environ['debugtoolbar.pylons_context'] = c._current_obj()
543 environ['debugtoolbar.pylons_context'] = c._current_obj()
544
544
545 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
545 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
546 environ['pylons.routes_dict']['action']])
546 environ['pylons.routes_dict']['action']])
547
547
548 self.rc_config = SettingsModel().get_all_settings(cache=True)
548 self.rc_config = SettingsModel().get_all_settings(cache=True)
549 self.ip_addr = get_ip_addr(environ)
549 self.ip_addr = get_ip_addr(environ)
550
550
551 # The rhodecode auth user is looked up and passed through the
551 # The rhodecode auth user is looked up and passed through the
552 # environ by the pylons compatibility tween in pyramid.
552 # environ by the pylons compatibility tween in pyramid.
553 # So we can just grab it from there.
553 # So we can just grab it from there.
554 auth_user = environ['rc_auth_user']
554 auth_user = environ['rc_auth_user']
555
555
556 # set globals for auth user
556 # set globals for auth user
557 request.user = auth_user
557 request.user = auth_user
558 self._rhodecode_user = auth_user
558 self._rhodecode_user = auth_user
559
559
560 log.info('IP: %s User: %s accessed %s [%s]' % (
560 log.info('IP: %s User: %s accessed %s [%s]' % (
561 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
561 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
562 _route_name)
562 _route_name)
563 )
563 )
564
564
565 user_obj = auth_user.get_instance()
565 user_obj = auth_user.get_instance()
566 if user_obj and user_obj.user_data.get('force_password_change'):
566 if user_obj and user_obj.user_data.get('force_password_change'):
567 h.flash('You are required to change your password', 'warning',
567 h.flash('You are required to change your password', 'warning',
568 ignore_duplicate=True)
568 ignore_duplicate=True)
569 return self._dispatch_redirect(
569 return self._dispatch_redirect(
570 url('my_account_password'), environ, start_response)
570 url('my_account_password'), environ, start_response)
571
571
572 return WSGIController.__call__(self, environ, start_response)
572 return WSGIController.__call__(self, environ, start_response)
573
573
574
574
575 def h_filter(s):
575 def h_filter(s):
576 """
576 """
577 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
577 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
578 we wrap this with additional functionality that converts None to empty
578 we wrap this with additional functionality that converts None to empty
579 strings
579 strings
580 """
580 """
581 if s is None:
581 if s is None:
582 return markupsafe.Markup()
582 return markupsafe.Markup()
583 return markupsafe.escape(s)
583 return markupsafe.escape(s)
584
584
585
585
586 def add_events_routes(config):
586 def add_events_routes(config):
587 """
587 """
588 Adds routing that can be used in events. Because some events are triggered
588 Adds routing that can be used in events. Because some events are triggered
589 outside of pyramid context, we need to bootstrap request with some
589 outside of pyramid context, we need to bootstrap request with some
590 routing registered
590 routing registered
591 """
591 """
592 config.add_route(name='home', pattern='/')
592 config.add_route(name='home', pattern='/')
593
593
594 config.add_route(name='repo_summary', pattern='/{repo_name}')
594 config.add_route(name='repo_summary', pattern='/{repo_name}')
595 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
595 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
596 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
596 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
597
597
598 config.add_route(name='pullrequest_show',
598 config.add_route(name='pullrequest_show',
599 pattern='/{repo_name}/pull-request/{pull_request_id}')
599 pattern='/{repo_name}/pull-request/{pull_request_id}')
600 config.add_route(name='pull_requests_global',
600 config.add_route(name='pull_requests_global',
601 pattern='/pull-request/{pull_request_id}')
601 pattern='/pull-request/{pull_request_id}')
602
602
603 config.add_route(name='repo_commit',
603 config.add_route(name='repo_commit',
604 pattern='/{repo_name}/changeset/{commit_id}')
604 pattern='/{repo_name}/changeset/{commit_id}')
605 config.add_route(name='repo_files',
605 config.add_route(name='repo_files',
606 pattern='/{repo_name}/files/{commit_id}/{f_path}')
606 pattern='/{repo_name}/files/{commit_id}/{f_path}')
607
607
608
608
609 def bootstrap_request(**kwargs):
609 def bootstrap_request(**kwargs):
610 import pyramid.testing
610 import pyramid.testing
611 request = pyramid.testing.DummyRequest(**kwargs)
611
612 request.application_url = kwargs.pop('application_url', 'http://example.com')
612 class TestRequest(pyramid.testing.DummyRequest):
613 request.host = kwargs.pop('host', 'example.com:80')
613 application_url = kwargs.pop('application_url', 'http://example.com')
614 request.domain = kwargs.pop('domain', 'example.com')
614 host = kwargs.pop('host', 'example.com:80')
615 domain = kwargs.pop('domain', 'example.com')
616
617 class TestDummySession(pyramid.testing.DummySession):
618 def save(*arg, **kw):
619 pass
620
621 request = TestRequest(**kwargs)
622 request.session = TestDummySession()
615
623
616 config = pyramid.testing.setUp(request=request)
624 config = pyramid.testing.setUp(request=request)
617 add_events_routes(config)
625 add_events_routes(config)
626 return request
627
@@ -1,2062 +1,2102 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 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27
27
28 import random
28 import random
29 import hashlib
29 import hashlib
30 import StringIO
30 import StringIO
31 import urllib
31 import urllib
32 import math
32 import math
33 import logging
33 import logging
34 import re
34 import re
35 import urlparse
35 import urlparse
36 import time
36 import time
37 import string
37 import string
38 import hashlib
38 import hashlib
39 from collections import OrderedDict
39 from collections import OrderedDict
40
40
41 import pygments
41 import pygments
42 import itertools
42 import itertools
43 import fnmatch
43 import fnmatch
44
44
45 from datetime import datetime
45 from datetime import datetime
46 from functools import partial
46 from functools import partial
47 from pygments.formatters.html import HtmlFormatter
47 from pygments.formatters.html import HtmlFormatter
48 from pygments import highlight as code_highlight
48 from pygments import highlight as code_highlight
49 from pygments.lexers import (
49 from pygments.lexers import (
50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51
51
52 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
53
53
54 from webhelpers.html import literal, HTML, escape
54 from webhelpers.html import literal, HTML, escape
55 from webhelpers.html.tools import *
55 from webhelpers.html.tools import *
56 from webhelpers.html.builder import make_tag
56 from webhelpers.html.builder import make_tag
57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
60 submit, text, password, textarea, title, ul, xml_declaration, radio
60 submit, text, password, textarea, title, ul, xml_declaration, radio
61 from webhelpers.html.tools import auto_link, button_to, highlight, \
61 from webhelpers.html.tools import auto_link, button_to, highlight, \
62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
63 from webhelpers.pylonslib import Flash as _Flash
64 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
63 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
65 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
64 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
66 replace_whitespace, urlify, truncate, wrap_paragraphs
65 replace_whitespace, urlify, truncate, wrap_paragraphs
67 from webhelpers.date import time_ago_in_words
66 from webhelpers.date import time_ago_in_words
68 from webhelpers.paginate import Page as _Page
67 from webhelpers.paginate import Page as _Page
69 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
68 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
70 convert_boolean_attrs, NotGiven, _make_safe_id_component
69 convert_boolean_attrs, NotGiven, _make_safe_id_component
71 from webhelpers2.number import format_byte_size
70 from webhelpers2.number import format_byte_size
72
71
73 from rhodecode.lib.action_parser import action_parser
72 from rhodecode.lib.action_parser import action_parser
74 from rhodecode.lib.ext_json import json
73 from rhodecode.lib.ext_json import json
75 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
74 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
76 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
75 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
77 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
76 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
78 AttributeDict, safe_int, md5, md5_safe
77 AttributeDict, safe_int, md5, md5_safe
79 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
78 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
80 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
79 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
81 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
80 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
82 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
81 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
83 from rhodecode.model.changeset_status import ChangesetStatusModel
82 from rhodecode.model.changeset_status import ChangesetStatusModel
84 from rhodecode.model.db import Permission, User, Repository
83 from rhodecode.model.db import Permission, User, Repository
85 from rhodecode.model.repo_group import RepoGroupModel
84 from rhodecode.model.repo_group import RepoGroupModel
86 from rhodecode.model.settings import IssueTrackerSettingsModel
85 from rhodecode.model.settings import IssueTrackerSettingsModel
87
86
88 log = logging.getLogger(__name__)
87 log = logging.getLogger(__name__)
89
88
90
89
91 DEFAULT_USER = User.DEFAULT_USER
90 DEFAULT_USER = User.DEFAULT_USER
92 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
91 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
93
92
94
93
95 def url(*args, **kw):
94 def url(*args, **kw):
96 from pylons import url as pylons_url
95 from pylons import url as pylons_url
97 return pylons_url(*args, **kw)
96 return pylons_url(*args, **kw)
98
97
99
98
100 def pylons_url_current(*args, **kw):
99 def pylons_url_current(*args, **kw):
101 """
100 """
102 This function overrides pylons.url.current() which returns the current
101 This function overrides pylons.url.current() which returns the current
103 path so that it will also work from a pyramid only context. This
102 path so that it will also work from a pyramid only context. This
104 should be removed once port to pyramid is complete.
103 should be removed once port to pyramid is complete.
105 """
104 """
106 from pylons import url as pylons_url
105 from pylons import url as pylons_url
107 if not args and not kw:
106 if not args and not kw:
108 request = get_current_request()
107 request = get_current_request()
109 return request.path
108 return request.path
110 return pylons_url.current(*args, **kw)
109 return pylons_url.current(*args, **kw)
111
110
112 url.current = pylons_url_current
111 url.current = pylons_url_current
113
112
114
113
115 def url_replace(**qargs):
114 def url_replace(**qargs):
116 """ Returns the current request url while replacing query string args """
115 """ Returns the current request url while replacing query string args """
117
116
118 request = get_current_request()
117 request = get_current_request()
119 new_args = request.GET.mixed()
118 new_args = request.GET.mixed()
120 new_args.update(qargs)
119 new_args.update(qargs)
121 return url('', **new_args)
120 return url('', **new_args)
122
121
123
122
124 def asset(path, ver=None, **kwargs):
123 def asset(path, ver=None, **kwargs):
125 """
124 """
126 Helper to generate a static asset file path for rhodecode assets
125 Helper to generate a static asset file path for rhodecode assets
127
126
128 eg. h.asset('images/image.png', ver='3923')
127 eg. h.asset('images/image.png', ver='3923')
129
128
130 :param path: path of asset
129 :param path: path of asset
131 :param ver: optional version query param to append as ?ver=
130 :param ver: optional version query param to append as ?ver=
132 """
131 """
133 request = get_current_request()
132 request = get_current_request()
134 query = {}
133 query = {}
135 query.update(kwargs)
134 query.update(kwargs)
136 if ver:
135 if ver:
137 query = {'ver': ver}
136 query = {'ver': ver}
138 return request.static_path(
137 return request.static_path(
139 'rhodecode:public/{}'.format(path), _query=query)
138 'rhodecode:public/{}'.format(path), _query=query)
140
139
141
140
142 default_html_escape_table = {
141 default_html_escape_table = {
143 ord('&'): u'&amp;',
142 ord('&'): u'&amp;',
144 ord('<'): u'&lt;',
143 ord('<'): u'&lt;',
145 ord('>'): u'&gt;',
144 ord('>'): u'&gt;',
146 ord('"'): u'&quot;',
145 ord('"'): u'&quot;',
147 ord("'"): u'&#39;',
146 ord("'"): u'&#39;',
148 }
147 }
149
148
150
149
151 def html_escape(text, html_escape_table=default_html_escape_table):
150 def html_escape(text, html_escape_table=default_html_escape_table):
152 """Produce entities within text."""
151 """Produce entities within text."""
153 return text.translate(html_escape_table)
152 return text.translate(html_escape_table)
154
153
155
154
156 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
155 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
157 """
156 """
158 Truncate string ``s`` at the first occurrence of ``sub``.
157 Truncate string ``s`` at the first occurrence of ``sub``.
159
158
160 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
159 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
161 """
160 """
162 suffix_if_chopped = suffix_if_chopped or ''
161 suffix_if_chopped = suffix_if_chopped or ''
163 pos = s.find(sub)
162 pos = s.find(sub)
164 if pos == -1:
163 if pos == -1:
165 return s
164 return s
166
165
167 if inclusive:
166 if inclusive:
168 pos += len(sub)
167 pos += len(sub)
169
168
170 chopped = s[:pos]
169 chopped = s[:pos]
171 left = s[pos:].strip()
170 left = s[pos:].strip()
172
171
173 if left and suffix_if_chopped:
172 if left and suffix_if_chopped:
174 chopped += suffix_if_chopped
173 chopped += suffix_if_chopped
175
174
176 return chopped
175 return chopped
177
176
178
177
179 def shorter(text, size=20):
178 def shorter(text, size=20):
180 postfix = '...'
179 postfix = '...'
181 if len(text) > size:
180 if len(text) > size:
182 return text[:size - len(postfix)] + postfix
181 return text[:size - len(postfix)] + postfix
183 return text
182 return text
184
183
185
184
186 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
185 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
187 """
186 """
188 Reset button
187 Reset button
189 """
188 """
190 _set_input_attrs(attrs, type, name, value)
189 _set_input_attrs(attrs, type, name, value)
191 _set_id_attr(attrs, id, name)
190 _set_id_attr(attrs, id, name)
192 convert_boolean_attrs(attrs, ["disabled"])
191 convert_boolean_attrs(attrs, ["disabled"])
193 return HTML.input(**attrs)
192 return HTML.input(**attrs)
194
193
195 reset = _reset
194 reset = _reset
196 safeid = _make_safe_id_component
195 safeid = _make_safe_id_component
197
196
198
197
199 def branding(name, length=40):
198 def branding(name, length=40):
200 return truncate(name, length, indicator="")
199 return truncate(name, length, indicator="")
201
200
202
201
203 def FID(raw_id, path):
202 def FID(raw_id, path):
204 """
203 """
205 Creates a unique ID for filenode based on it's hash of path and commit
204 Creates a unique ID for filenode based on it's hash of path and commit
206 it's safe to use in urls
205 it's safe to use in urls
207
206
208 :param raw_id:
207 :param raw_id:
209 :param path:
208 :param path:
210 """
209 """
211
210
212 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
211 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
213
212
214
213
215 class _GetError(object):
214 class _GetError(object):
216 """Get error from form_errors, and represent it as span wrapped error
215 """Get error from form_errors, and represent it as span wrapped error
217 message
216 message
218
217
219 :param field_name: field to fetch errors for
218 :param field_name: field to fetch errors for
220 :param form_errors: form errors dict
219 :param form_errors: form errors dict
221 """
220 """
222
221
223 def __call__(self, field_name, form_errors):
222 def __call__(self, field_name, form_errors):
224 tmpl = """<span class="error_msg">%s</span>"""
223 tmpl = """<span class="error_msg">%s</span>"""
225 if form_errors and field_name in form_errors:
224 if form_errors and field_name in form_errors:
226 return literal(tmpl % form_errors.get(field_name))
225 return literal(tmpl % form_errors.get(field_name))
227
226
228 get_error = _GetError()
227 get_error = _GetError()
229
228
230
229
231 class _ToolTip(object):
230 class _ToolTip(object):
232
231
233 def __call__(self, tooltip_title, trim_at=50):
232 def __call__(self, tooltip_title, trim_at=50):
234 """
233 """
235 Special function just to wrap our text into nice formatted
234 Special function just to wrap our text into nice formatted
236 autowrapped text
235 autowrapped text
237
236
238 :param tooltip_title:
237 :param tooltip_title:
239 """
238 """
240 tooltip_title = escape(tooltip_title)
239 tooltip_title = escape(tooltip_title)
241 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
240 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
242 return tooltip_title
241 return tooltip_title
243 tooltip = _ToolTip()
242 tooltip = _ToolTip()
244
243
245
244
246 def files_breadcrumbs(repo_name, commit_id, file_path):
245 def files_breadcrumbs(repo_name, commit_id, file_path):
247 if isinstance(file_path, str):
246 if isinstance(file_path, str):
248 file_path = safe_unicode(file_path)
247 file_path = safe_unicode(file_path)
249
248
250 # TODO: johbo: Is this always a url like path, or is this operating
249 # TODO: johbo: Is this always a url like path, or is this operating
251 # system dependent?
250 # system dependent?
252 path_segments = file_path.split('/')
251 path_segments = file_path.split('/')
253
252
254 repo_name_html = escape(repo_name)
253 repo_name_html = escape(repo_name)
255 if len(path_segments) == 1 and path_segments[0] == '':
254 if len(path_segments) == 1 and path_segments[0] == '':
256 url_segments = [repo_name_html]
255 url_segments = [repo_name_html]
257 else:
256 else:
258 url_segments = [
257 url_segments = [
259 link_to(
258 link_to(
260 repo_name_html,
259 repo_name_html,
261 route_path(
260 route_path(
262 'repo_files',
261 'repo_files',
263 repo_name=repo_name,
262 repo_name=repo_name,
264 commit_id=commit_id,
263 commit_id=commit_id,
265 f_path=''),
264 f_path=''),
266 class_='pjax-link')]
265 class_='pjax-link')]
267
266
268 last_cnt = len(path_segments) - 1
267 last_cnt = len(path_segments) - 1
269 for cnt, segment in enumerate(path_segments):
268 for cnt, segment in enumerate(path_segments):
270 if not segment:
269 if not segment:
271 continue
270 continue
272 segment_html = escape(segment)
271 segment_html = escape(segment)
273
272
274 if cnt != last_cnt:
273 if cnt != last_cnt:
275 url_segments.append(
274 url_segments.append(
276 link_to(
275 link_to(
277 segment_html,
276 segment_html,
278 route_path(
277 route_path(
279 'repo_files',
278 'repo_files',
280 repo_name=repo_name,
279 repo_name=repo_name,
281 commit_id=commit_id,
280 commit_id=commit_id,
282 f_path='/'.join(path_segments[:cnt + 1])),
281 f_path='/'.join(path_segments[:cnt + 1])),
283 class_='pjax-link'))
282 class_='pjax-link'))
284 else:
283 else:
285 url_segments.append(segment_html)
284 url_segments.append(segment_html)
286
285
287 return literal('/'.join(url_segments))
286 return literal('/'.join(url_segments))
288
287
289
288
290 class CodeHtmlFormatter(HtmlFormatter):
289 class CodeHtmlFormatter(HtmlFormatter):
291 """
290 """
292 My code Html Formatter for source codes
291 My code Html Formatter for source codes
293 """
292 """
294
293
295 def wrap(self, source, outfile):
294 def wrap(self, source, outfile):
296 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
295 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
297
296
298 def _wrap_code(self, source):
297 def _wrap_code(self, source):
299 for cnt, it in enumerate(source):
298 for cnt, it in enumerate(source):
300 i, t = it
299 i, t = it
301 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
300 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
302 yield i, t
301 yield i, t
303
302
304 def _wrap_tablelinenos(self, inner):
303 def _wrap_tablelinenos(self, inner):
305 dummyoutfile = StringIO.StringIO()
304 dummyoutfile = StringIO.StringIO()
306 lncount = 0
305 lncount = 0
307 for t, line in inner:
306 for t, line in inner:
308 if t:
307 if t:
309 lncount += 1
308 lncount += 1
310 dummyoutfile.write(line)
309 dummyoutfile.write(line)
311
310
312 fl = self.linenostart
311 fl = self.linenostart
313 mw = len(str(lncount + fl - 1))
312 mw = len(str(lncount + fl - 1))
314 sp = self.linenospecial
313 sp = self.linenospecial
315 st = self.linenostep
314 st = self.linenostep
316 la = self.lineanchors
315 la = self.lineanchors
317 aln = self.anchorlinenos
316 aln = self.anchorlinenos
318 nocls = self.noclasses
317 nocls = self.noclasses
319 if sp:
318 if sp:
320 lines = []
319 lines = []
321
320
322 for i in range(fl, fl + lncount):
321 for i in range(fl, fl + lncount):
323 if i % st == 0:
322 if i % st == 0:
324 if i % sp == 0:
323 if i % sp == 0:
325 if aln:
324 if aln:
326 lines.append('<a href="#%s%d" class="special">%*d</a>' %
325 lines.append('<a href="#%s%d" class="special">%*d</a>' %
327 (la, i, mw, i))
326 (la, i, mw, i))
328 else:
327 else:
329 lines.append('<span class="special">%*d</span>' % (mw, i))
328 lines.append('<span class="special">%*d</span>' % (mw, i))
330 else:
329 else:
331 if aln:
330 if aln:
332 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
331 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
333 else:
332 else:
334 lines.append('%*d' % (mw, i))
333 lines.append('%*d' % (mw, i))
335 else:
334 else:
336 lines.append('')
335 lines.append('')
337 ls = '\n'.join(lines)
336 ls = '\n'.join(lines)
338 else:
337 else:
339 lines = []
338 lines = []
340 for i in range(fl, fl + lncount):
339 for i in range(fl, fl + lncount):
341 if i % st == 0:
340 if i % st == 0:
342 if aln:
341 if aln:
343 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
342 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
344 else:
343 else:
345 lines.append('%*d' % (mw, i))
344 lines.append('%*d' % (mw, i))
346 else:
345 else:
347 lines.append('')
346 lines.append('')
348 ls = '\n'.join(lines)
347 ls = '\n'.join(lines)
349
348
350 # in case you wonder about the seemingly redundant <div> here: since the
349 # in case you wonder about the seemingly redundant <div> here: since the
351 # content in the other cell also is wrapped in a div, some browsers in
350 # content in the other cell also is wrapped in a div, some browsers in
352 # some configurations seem to mess up the formatting...
351 # some configurations seem to mess up the formatting...
353 if nocls:
352 if nocls:
354 yield 0, ('<table class="%stable">' % self.cssclass +
353 yield 0, ('<table class="%stable">' % self.cssclass +
355 '<tr><td><div class="linenodiv" '
354 '<tr><td><div class="linenodiv" '
356 'style="background-color: #f0f0f0; padding-right: 10px">'
355 'style="background-color: #f0f0f0; padding-right: 10px">'
357 '<pre style="line-height: 125%">' +
356 '<pre style="line-height: 125%">' +
358 ls + '</pre></div></td><td id="hlcode" class="code">')
357 ls + '</pre></div></td><td id="hlcode" class="code">')
359 else:
358 else:
360 yield 0, ('<table class="%stable">' % self.cssclass +
359 yield 0, ('<table class="%stable">' % self.cssclass +
361 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
360 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
362 ls + '</pre></div></td><td id="hlcode" class="code">')
361 ls + '</pre></div></td><td id="hlcode" class="code">')
363 yield 0, dummyoutfile.getvalue()
362 yield 0, dummyoutfile.getvalue()
364 yield 0, '</td></tr></table>'
363 yield 0, '</td></tr></table>'
365
364
366
365
367 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
366 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
368 def __init__(self, **kw):
367 def __init__(self, **kw):
369 # only show these line numbers if set
368 # only show these line numbers if set
370 self.only_lines = kw.pop('only_line_numbers', [])
369 self.only_lines = kw.pop('only_line_numbers', [])
371 self.query_terms = kw.pop('query_terms', [])
370 self.query_terms = kw.pop('query_terms', [])
372 self.max_lines = kw.pop('max_lines', 5)
371 self.max_lines = kw.pop('max_lines', 5)
373 self.line_context = kw.pop('line_context', 3)
372 self.line_context = kw.pop('line_context', 3)
374 self.url = kw.pop('url', None)
373 self.url = kw.pop('url', None)
375
374
376 super(CodeHtmlFormatter, self).__init__(**kw)
375 super(CodeHtmlFormatter, self).__init__(**kw)
377
376
378 def _wrap_code(self, source):
377 def _wrap_code(self, source):
379 for cnt, it in enumerate(source):
378 for cnt, it in enumerate(source):
380 i, t = it
379 i, t = it
381 t = '<pre>%s</pre>' % t
380 t = '<pre>%s</pre>' % t
382 yield i, t
381 yield i, t
383
382
384 def _wrap_tablelinenos(self, inner):
383 def _wrap_tablelinenos(self, inner):
385 yield 0, '<table class="code-highlight %stable">' % self.cssclass
384 yield 0, '<table class="code-highlight %stable">' % self.cssclass
386
385
387 last_shown_line_number = 0
386 last_shown_line_number = 0
388 current_line_number = 1
387 current_line_number = 1
389
388
390 for t, line in inner:
389 for t, line in inner:
391 if not t:
390 if not t:
392 yield t, line
391 yield t, line
393 continue
392 continue
394
393
395 if current_line_number in self.only_lines:
394 if current_line_number in self.only_lines:
396 if last_shown_line_number + 1 != current_line_number:
395 if last_shown_line_number + 1 != current_line_number:
397 yield 0, '<tr>'
396 yield 0, '<tr>'
398 yield 0, '<td class="line">...</td>'
397 yield 0, '<td class="line">...</td>'
399 yield 0, '<td id="hlcode" class="code"></td>'
398 yield 0, '<td id="hlcode" class="code"></td>'
400 yield 0, '</tr>'
399 yield 0, '</tr>'
401
400
402 yield 0, '<tr>'
401 yield 0, '<tr>'
403 if self.url:
402 if self.url:
404 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
403 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
405 self.url, current_line_number, current_line_number)
404 self.url, current_line_number, current_line_number)
406 else:
405 else:
407 yield 0, '<td class="line"><a href="">%i</a></td>' % (
406 yield 0, '<td class="line"><a href="">%i</a></td>' % (
408 current_line_number)
407 current_line_number)
409 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
408 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
410 yield 0, '</tr>'
409 yield 0, '</tr>'
411
410
412 last_shown_line_number = current_line_number
411 last_shown_line_number = current_line_number
413
412
414 current_line_number += 1
413 current_line_number += 1
415
414
416
415
417 yield 0, '</table>'
416 yield 0, '</table>'
418
417
419
418
420 def extract_phrases(text_query):
419 def extract_phrases(text_query):
421 """
420 """
422 Extracts phrases from search term string making sure phrases
421 Extracts phrases from search term string making sure phrases
423 contained in double quotes are kept together - and discarding empty values
422 contained in double quotes are kept together - and discarding empty values
424 or fully whitespace values eg.
423 or fully whitespace values eg.
425
424
426 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
425 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
427
426
428 """
427 """
429
428
430 in_phrase = False
429 in_phrase = False
431 buf = ''
430 buf = ''
432 phrases = []
431 phrases = []
433 for char in text_query:
432 for char in text_query:
434 if in_phrase:
433 if in_phrase:
435 if char == '"': # end phrase
434 if char == '"': # end phrase
436 phrases.append(buf)
435 phrases.append(buf)
437 buf = ''
436 buf = ''
438 in_phrase = False
437 in_phrase = False
439 continue
438 continue
440 else:
439 else:
441 buf += char
440 buf += char
442 continue
441 continue
443 else:
442 else:
444 if char == '"': # start phrase
443 if char == '"': # start phrase
445 in_phrase = True
444 in_phrase = True
446 phrases.append(buf)
445 phrases.append(buf)
447 buf = ''
446 buf = ''
448 continue
447 continue
449 elif char == ' ':
448 elif char == ' ':
450 phrases.append(buf)
449 phrases.append(buf)
451 buf = ''
450 buf = ''
452 continue
451 continue
453 else:
452 else:
454 buf += char
453 buf += char
455
454
456 phrases.append(buf)
455 phrases.append(buf)
457 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
456 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
458 return phrases
457 return phrases
459
458
460
459
461 def get_matching_offsets(text, phrases):
460 def get_matching_offsets(text, phrases):
462 """
461 """
463 Returns a list of string offsets in `text` that the list of `terms` match
462 Returns a list of string offsets in `text` that the list of `terms` match
464
463
465 >>> get_matching_offsets('some text here', ['some', 'here'])
464 >>> get_matching_offsets('some text here', ['some', 'here'])
466 [(0, 4), (10, 14)]
465 [(0, 4), (10, 14)]
467
466
468 """
467 """
469 offsets = []
468 offsets = []
470 for phrase in phrases:
469 for phrase in phrases:
471 for match in re.finditer(phrase, text):
470 for match in re.finditer(phrase, text):
472 offsets.append((match.start(), match.end()))
471 offsets.append((match.start(), match.end()))
473
472
474 return offsets
473 return offsets
475
474
476
475
477 def normalize_text_for_matching(x):
476 def normalize_text_for_matching(x):
478 """
477 """
479 Replaces all non alnum characters to spaces and lower cases the string,
478 Replaces all non alnum characters to spaces and lower cases the string,
480 useful for comparing two text strings without punctuation
479 useful for comparing two text strings without punctuation
481 """
480 """
482 return re.sub(r'[^\w]', ' ', x.lower())
481 return re.sub(r'[^\w]', ' ', x.lower())
483
482
484
483
485 def get_matching_line_offsets(lines, terms):
484 def get_matching_line_offsets(lines, terms):
486 """ Return a set of `lines` indices (starting from 1) matching a
485 """ Return a set of `lines` indices (starting from 1) matching a
487 text search query, along with `context` lines above/below matching lines
486 text search query, along with `context` lines above/below matching lines
488
487
489 :param lines: list of strings representing lines
488 :param lines: list of strings representing lines
490 :param terms: search term string to match in lines eg. 'some text'
489 :param terms: search term string to match in lines eg. 'some text'
491 :param context: number of lines above/below a matching line to add to result
490 :param context: number of lines above/below a matching line to add to result
492 :param max_lines: cut off for lines of interest
491 :param max_lines: cut off for lines of interest
493 eg.
492 eg.
494
493
495 text = '''
494 text = '''
496 words words words
495 words words words
497 words words words
496 words words words
498 some text some
497 some text some
499 words words words
498 words words words
500 words words words
499 words words words
501 text here what
500 text here what
502 '''
501 '''
503 get_matching_line_offsets(text, 'text', context=1)
502 get_matching_line_offsets(text, 'text', context=1)
504 {3: [(5, 9)], 6: [(0, 4)]]
503 {3: [(5, 9)], 6: [(0, 4)]]
505
504
506 """
505 """
507 matching_lines = {}
506 matching_lines = {}
508 phrases = [normalize_text_for_matching(phrase)
507 phrases = [normalize_text_for_matching(phrase)
509 for phrase in extract_phrases(terms)]
508 for phrase in extract_phrases(terms)]
510
509
511 for line_index, line in enumerate(lines, start=1):
510 for line_index, line in enumerate(lines, start=1):
512 match_offsets = get_matching_offsets(
511 match_offsets = get_matching_offsets(
513 normalize_text_for_matching(line), phrases)
512 normalize_text_for_matching(line), phrases)
514 if match_offsets:
513 if match_offsets:
515 matching_lines[line_index] = match_offsets
514 matching_lines[line_index] = match_offsets
516
515
517 return matching_lines
516 return matching_lines
518
517
519
518
520 def hsv_to_rgb(h, s, v):
519 def hsv_to_rgb(h, s, v):
521 """ Convert hsv color values to rgb """
520 """ Convert hsv color values to rgb """
522
521
523 if s == 0.0:
522 if s == 0.0:
524 return v, v, v
523 return v, v, v
525 i = int(h * 6.0) # XXX assume int() truncates!
524 i = int(h * 6.0) # XXX assume int() truncates!
526 f = (h * 6.0) - i
525 f = (h * 6.0) - i
527 p = v * (1.0 - s)
526 p = v * (1.0 - s)
528 q = v * (1.0 - s * f)
527 q = v * (1.0 - s * f)
529 t = v * (1.0 - s * (1.0 - f))
528 t = v * (1.0 - s * (1.0 - f))
530 i = i % 6
529 i = i % 6
531 if i == 0:
530 if i == 0:
532 return v, t, p
531 return v, t, p
533 if i == 1:
532 if i == 1:
534 return q, v, p
533 return q, v, p
535 if i == 2:
534 if i == 2:
536 return p, v, t
535 return p, v, t
537 if i == 3:
536 if i == 3:
538 return p, q, v
537 return p, q, v
539 if i == 4:
538 if i == 4:
540 return t, p, v
539 return t, p, v
541 if i == 5:
540 if i == 5:
542 return v, p, q
541 return v, p, q
543
542
544
543
545 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
544 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
546 """
545 """
547 Generator for getting n of evenly distributed colors using
546 Generator for getting n of evenly distributed colors using
548 hsv color and golden ratio. It always return same order of colors
547 hsv color and golden ratio. It always return same order of colors
549
548
550 :param n: number of colors to generate
549 :param n: number of colors to generate
551 :param saturation: saturation of returned colors
550 :param saturation: saturation of returned colors
552 :param lightness: lightness of returned colors
551 :param lightness: lightness of returned colors
553 :returns: RGB tuple
552 :returns: RGB tuple
554 """
553 """
555
554
556 golden_ratio = 0.618033988749895
555 golden_ratio = 0.618033988749895
557 h = 0.22717784590367374
556 h = 0.22717784590367374
558
557
559 for _ in xrange(n):
558 for _ in xrange(n):
560 h += golden_ratio
559 h += golden_ratio
561 h %= 1
560 h %= 1
562 HSV_tuple = [h, saturation, lightness]
561 HSV_tuple = [h, saturation, lightness]
563 RGB_tuple = hsv_to_rgb(*HSV_tuple)
562 RGB_tuple = hsv_to_rgb(*HSV_tuple)
564 yield map(lambda x: str(int(x * 256)), RGB_tuple)
563 yield map(lambda x: str(int(x * 256)), RGB_tuple)
565
564
566
565
567 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
566 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
568 """
567 """
569 Returns a function which when called with an argument returns a unique
568 Returns a function which when called with an argument returns a unique
570 color for that argument, eg.
569 color for that argument, eg.
571
570
572 :param n: number of colors to generate
571 :param n: number of colors to generate
573 :param saturation: saturation of returned colors
572 :param saturation: saturation of returned colors
574 :param lightness: lightness of returned colors
573 :param lightness: lightness of returned colors
575 :returns: css RGB string
574 :returns: css RGB string
576
575
577 >>> color_hash = color_hasher()
576 >>> color_hash = color_hasher()
578 >>> color_hash('hello')
577 >>> color_hash('hello')
579 'rgb(34, 12, 59)'
578 'rgb(34, 12, 59)'
580 >>> color_hash('hello')
579 >>> color_hash('hello')
581 'rgb(34, 12, 59)'
580 'rgb(34, 12, 59)'
582 >>> color_hash('other')
581 >>> color_hash('other')
583 'rgb(90, 224, 159)'
582 'rgb(90, 224, 159)'
584 """
583 """
585
584
586 color_dict = {}
585 color_dict = {}
587 cgenerator = unique_color_generator(
586 cgenerator = unique_color_generator(
588 saturation=saturation, lightness=lightness)
587 saturation=saturation, lightness=lightness)
589
588
590 def get_color_string(thing):
589 def get_color_string(thing):
591 if thing in color_dict:
590 if thing in color_dict:
592 col = color_dict[thing]
591 col = color_dict[thing]
593 else:
592 else:
594 col = color_dict[thing] = cgenerator.next()
593 col = color_dict[thing] = cgenerator.next()
595 return "rgb(%s)" % (', '.join(col))
594 return "rgb(%s)" % (', '.join(col))
596
595
597 return get_color_string
596 return get_color_string
598
597
599
598
600 def get_lexer_safe(mimetype=None, filepath=None):
599 def get_lexer_safe(mimetype=None, filepath=None):
601 """
600 """
602 Tries to return a relevant pygments lexer using mimetype/filepath name,
601 Tries to return a relevant pygments lexer using mimetype/filepath name,
603 defaulting to plain text if none could be found
602 defaulting to plain text if none could be found
604 """
603 """
605 lexer = None
604 lexer = None
606 try:
605 try:
607 if mimetype:
606 if mimetype:
608 lexer = get_lexer_for_mimetype(mimetype)
607 lexer = get_lexer_for_mimetype(mimetype)
609 if not lexer:
608 if not lexer:
610 lexer = get_lexer_for_filename(filepath)
609 lexer = get_lexer_for_filename(filepath)
611 except pygments.util.ClassNotFound:
610 except pygments.util.ClassNotFound:
612 pass
611 pass
613
612
614 if not lexer:
613 if not lexer:
615 lexer = get_lexer_by_name('text')
614 lexer = get_lexer_by_name('text')
616
615
617 return lexer
616 return lexer
618
617
619
618
620 def get_lexer_for_filenode(filenode):
619 def get_lexer_for_filenode(filenode):
621 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
620 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
622 return lexer
621 return lexer
623
622
624
623
625 def pygmentize(filenode, **kwargs):
624 def pygmentize(filenode, **kwargs):
626 """
625 """
627 pygmentize function using pygments
626 pygmentize function using pygments
628
627
629 :param filenode:
628 :param filenode:
630 """
629 """
631 lexer = get_lexer_for_filenode(filenode)
630 lexer = get_lexer_for_filenode(filenode)
632 return literal(code_highlight(filenode.content, lexer,
631 return literal(code_highlight(filenode.content, lexer,
633 CodeHtmlFormatter(**kwargs)))
632 CodeHtmlFormatter(**kwargs)))
634
633
635
634
636 def is_following_repo(repo_name, user_id):
635 def is_following_repo(repo_name, user_id):
637 from rhodecode.model.scm import ScmModel
636 from rhodecode.model.scm import ScmModel
638 return ScmModel().is_following_repo(repo_name, user_id)
637 return ScmModel().is_following_repo(repo_name, user_id)
639
638
640
639
641 class _Message(object):
640 class _Message(object):
642 """A message returned by ``Flash.pop_messages()``.
641 """A message returned by ``Flash.pop_messages()``.
643
642
644 Converting the message to a string returns the message text. Instances
643 Converting the message to a string returns the message text. Instances
645 also have the following attributes:
644 also have the following attributes:
646
645
647 * ``message``: the message text.
646 * ``message``: the message text.
648 * ``category``: the category specified when the message was created.
647 * ``category``: the category specified when the message was created.
649 """
648 """
650
649
651 def __init__(self, category, message):
650 def __init__(self, category, message):
652 self.category = category
651 self.category = category
653 self.message = message
652 self.message = message
654
653
655 def __str__(self):
654 def __str__(self):
656 return self.message
655 return self.message
657
656
658 __unicode__ = __str__
657 __unicode__ = __str__
659
658
660 def __html__(self):
659 def __html__(self):
661 return escape(safe_unicode(self.message))
660 return escape(safe_unicode(self.message))
662
661
663
662
664 class Flash(_Flash):
663 class Flash(object):
664 # List of allowed categories. If None, allow any category.
665 categories = ["warning", "notice", "error", "success"]
666
667 # Default category if none is specified.
668 default_category = "notice"
669
670 def __init__(self, session_key="flash", categories=None,
671 default_category=None):
672 """
673 Instantiate a ``Flash`` object.
674
675 ``session_key`` is the key to save the messages under in the user's
676 session.
665
677
666 def pop_messages(self, request=None):
678 ``categories`` is an optional list which overrides the default list
667 """Return all accumulated messages and delete them from the session.
679 of categories.
680
681 ``default_category`` overrides the default category used for messages
682 when none is specified.
683 """
684 self.session_key = session_key
685 if categories is not None:
686 self.categories = categories
687 if default_category is not None:
688 self.default_category = default_category
689 if self.categories and self.default_category not in self.categories:
690 raise ValueError(
691 "unrecognized default category %r" % (self.default_category,))
692
693 def pop_messages(self, session=None, request=None):
694 """
695 Return all accumulated messages and delete them from the session.
668
696
669 The return value is a list of ``Message`` objects.
697 The return value is a list of ``Message`` objects.
670 """
698 """
671 messages = []
699 messages = []
672
700
673 if request:
701 if not session:
702 if not request:
703 request = get_current_request()
674 session = request.session
704 session = request.session
675 else:
676 from pylons import session
677
705
678 # Pop the 'old' pylons flash messages. They are tuples of the form
706 # Pop the 'old' pylons flash messages. They are tuples of the form
679 # (category, message)
707 # (category, message)
680 for cat, msg in session.pop(self.session_key, []):
708 for cat, msg in session.pop(self.session_key, []):
681 messages.append(_Message(cat, msg))
709 messages.append(_Message(cat, msg))
682
710
683 # Pop the 'new' pyramid flash messages for each category as list
711 # Pop the 'new' pyramid flash messages for each category as list
684 # of strings.
712 # of strings.
685 for cat in self.categories:
713 for cat in self.categories:
686 for msg in session.pop_flash(queue=cat):
714 for msg in session.pop_flash(queue=cat):
687 messages.append(_Message(cat, msg))
715 messages.append(_Message(cat, msg))
688 # Map messages from the default queue to the 'notice' category.
716 # Map messages from the default queue to the 'notice' category.
689 for msg in session.pop_flash():
717 for msg in session.pop_flash():
690 messages.append(_Message('notice', msg))
718 messages.append(_Message('notice', msg))
691
719
692 session.save()
720 session.save()
693 return messages
721 return messages
694
722
695 def json_alerts(self, request=None):
723 def json_alerts(self, session=None, request=None):
696 payloads = []
724 payloads = []
697 messages = flash.pop_messages(request=request)
725 messages = flash.pop_messages(session=session, request=request)
698 if messages:
726 if messages:
699 for message in messages:
727 for message in messages:
700 subdata = {}
728 subdata = {}
701 if hasattr(message.message, 'rsplit'):
729 if hasattr(message.message, 'rsplit'):
702 flash_data = message.message.rsplit('|DELIM|', 1)
730 flash_data = message.message.rsplit('|DELIM|', 1)
703 org_message = flash_data[0]
731 org_message = flash_data[0]
704 if len(flash_data) > 1:
732 if len(flash_data) > 1:
705 subdata = json.loads(flash_data[1])
733 subdata = json.loads(flash_data[1])
706 else:
734 else:
707 org_message = message.message
735 org_message = message.message
708 payloads.append({
736 payloads.append({
709 'message': {
737 'message': {
710 'message': u'{}'.format(org_message),
738 'message': u'{}'.format(org_message),
711 'level': message.category,
739 'level': message.category,
712 'force': True,
740 'force': True,
713 'subdata': subdata
741 'subdata': subdata
714 }
742 }
715 })
743 })
716 return json.dumps(payloads)
744 return json.dumps(payloads)
717
745
746 def __call__(self, message, category=None, ignore_duplicate=False,
747 session=None, request=None):
748
749 if not session:
750 if not request:
751 request = get_current_request()
752 session = request.session
753
754 session.flash(
755 message, queue=category, allow_duplicate=not ignore_duplicate)
756
757
718 flash = Flash()
758 flash = Flash()
719
759
720 #==============================================================================
760 #==============================================================================
721 # SCM FILTERS available via h.
761 # SCM FILTERS available via h.
722 #==============================================================================
762 #==============================================================================
723 from rhodecode.lib.vcs.utils import author_name, author_email
763 from rhodecode.lib.vcs.utils import author_name, author_email
724 from rhodecode.lib.utils2 import credentials_filter, age as _age
764 from rhodecode.lib.utils2 import credentials_filter, age as _age
725 from rhodecode.model.db import User, ChangesetStatus
765 from rhodecode.model.db import User, ChangesetStatus
726
766
727 age = _age
767 age = _age
728 capitalize = lambda x: x.capitalize()
768 capitalize = lambda x: x.capitalize()
729 email = author_email
769 email = author_email
730 short_id = lambda x: x[:12]
770 short_id = lambda x: x[:12]
731 hide_credentials = lambda x: ''.join(credentials_filter(x))
771 hide_credentials = lambda x: ''.join(credentials_filter(x))
732
772
733
773
734 def age_component(datetime_iso, value=None, time_is_local=False):
774 def age_component(datetime_iso, value=None, time_is_local=False):
735 title = value or format_date(datetime_iso)
775 title = value or format_date(datetime_iso)
736 tzinfo = '+00:00'
776 tzinfo = '+00:00'
737
777
738 # detect if we have a timezone info, otherwise, add it
778 # detect if we have a timezone info, otherwise, add it
739 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
779 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
740 if time_is_local:
780 if time_is_local:
741 tzinfo = time.strftime("+%H:%M",
781 tzinfo = time.strftime("+%H:%M",
742 time.gmtime(
782 time.gmtime(
743 (datetime.now() - datetime.utcnow()).seconds + 1
783 (datetime.now() - datetime.utcnow()).seconds + 1
744 )
784 )
745 )
785 )
746
786
747 return literal(
787 return literal(
748 '<time class="timeago tooltip" '
788 '<time class="timeago tooltip" '
749 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
789 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
750 datetime_iso, title, tzinfo))
790 datetime_iso, title, tzinfo))
751
791
752
792
753 def _shorten_commit_id(commit_id):
793 def _shorten_commit_id(commit_id):
754 from rhodecode import CONFIG
794 from rhodecode import CONFIG
755 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
795 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
756 return commit_id[:def_len]
796 return commit_id[:def_len]
757
797
758
798
759 def show_id(commit):
799 def show_id(commit):
760 """
800 """
761 Configurable function that shows ID
801 Configurable function that shows ID
762 by default it's r123:fffeeefffeee
802 by default it's r123:fffeeefffeee
763
803
764 :param commit: commit instance
804 :param commit: commit instance
765 """
805 """
766 from rhodecode import CONFIG
806 from rhodecode import CONFIG
767 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
807 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
768
808
769 raw_id = _shorten_commit_id(commit.raw_id)
809 raw_id = _shorten_commit_id(commit.raw_id)
770 if show_idx:
810 if show_idx:
771 return 'r%s:%s' % (commit.idx, raw_id)
811 return 'r%s:%s' % (commit.idx, raw_id)
772 else:
812 else:
773 return '%s' % (raw_id, )
813 return '%s' % (raw_id, )
774
814
775
815
776 def format_date(date):
816 def format_date(date):
777 """
817 """
778 use a standardized formatting for dates used in RhodeCode
818 use a standardized formatting for dates used in RhodeCode
779
819
780 :param date: date/datetime object
820 :param date: date/datetime object
781 :return: formatted date
821 :return: formatted date
782 """
822 """
783
823
784 if date:
824 if date:
785 _fmt = "%a, %d %b %Y %H:%M:%S"
825 _fmt = "%a, %d %b %Y %H:%M:%S"
786 return safe_unicode(date.strftime(_fmt))
826 return safe_unicode(date.strftime(_fmt))
787
827
788 return u""
828 return u""
789
829
790
830
791 class _RepoChecker(object):
831 class _RepoChecker(object):
792
832
793 def __init__(self, backend_alias):
833 def __init__(self, backend_alias):
794 self._backend_alias = backend_alias
834 self._backend_alias = backend_alias
795
835
796 def __call__(self, repository):
836 def __call__(self, repository):
797 if hasattr(repository, 'alias'):
837 if hasattr(repository, 'alias'):
798 _type = repository.alias
838 _type = repository.alias
799 elif hasattr(repository, 'repo_type'):
839 elif hasattr(repository, 'repo_type'):
800 _type = repository.repo_type
840 _type = repository.repo_type
801 else:
841 else:
802 _type = repository
842 _type = repository
803 return _type == self._backend_alias
843 return _type == self._backend_alias
804
844
805 is_git = _RepoChecker('git')
845 is_git = _RepoChecker('git')
806 is_hg = _RepoChecker('hg')
846 is_hg = _RepoChecker('hg')
807 is_svn = _RepoChecker('svn')
847 is_svn = _RepoChecker('svn')
808
848
809
849
810 def get_repo_type_by_name(repo_name):
850 def get_repo_type_by_name(repo_name):
811 repo = Repository.get_by_repo_name(repo_name)
851 repo = Repository.get_by_repo_name(repo_name)
812 return repo.repo_type
852 return repo.repo_type
813
853
814
854
815 def is_svn_without_proxy(repository):
855 def is_svn_without_proxy(repository):
816 if is_svn(repository):
856 if is_svn(repository):
817 from rhodecode.model.settings import VcsSettingsModel
857 from rhodecode.model.settings import VcsSettingsModel
818 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
858 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
819 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
859 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
820 return False
860 return False
821
861
822
862
823 def discover_user(author):
863 def discover_user(author):
824 """
864 """
825 Tries to discover RhodeCode User based on the autho string. Author string
865 Tries to discover RhodeCode User based on the autho string. Author string
826 is typically `FirstName LastName <email@address.com>`
866 is typically `FirstName LastName <email@address.com>`
827 """
867 """
828
868
829 # if author is already an instance use it for extraction
869 # if author is already an instance use it for extraction
830 if isinstance(author, User):
870 if isinstance(author, User):
831 return author
871 return author
832
872
833 # Valid email in the attribute passed, see if they're in the system
873 # Valid email in the attribute passed, see if they're in the system
834 _email = author_email(author)
874 _email = author_email(author)
835 if _email != '':
875 if _email != '':
836 user = User.get_by_email(_email, case_insensitive=True, cache=True)
876 user = User.get_by_email(_email, case_insensitive=True, cache=True)
837 if user is not None:
877 if user is not None:
838 return user
878 return user
839
879
840 # Maybe it's a username, we try to extract it and fetch by username ?
880 # Maybe it's a username, we try to extract it and fetch by username ?
841 _author = author_name(author)
881 _author = author_name(author)
842 user = User.get_by_username(_author, case_insensitive=True, cache=True)
882 user = User.get_by_username(_author, case_insensitive=True, cache=True)
843 if user is not None:
883 if user is not None:
844 return user
884 return user
845
885
846 return None
886 return None
847
887
848
888
849 def email_or_none(author):
889 def email_or_none(author):
850 # extract email from the commit string
890 # extract email from the commit string
851 _email = author_email(author)
891 _email = author_email(author)
852
892
853 # If we have an email, use it, otherwise
893 # If we have an email, use it, otherwise
854 # see if it contains a username we can get an email from
894 # see if it contains a username we can get an email from
855 if _email != '':
895 if _email != '':
856 return _email
896 return _email
857 else:
897 else:
858 user = User.get_by_username(
898 user = User.get_by_username(
859 author_name(author), case_insensitive=True, cache=True)
899 author_name(author), case_insensitive=True, cache=True)
860
900
861 if user is not None:
901 if user is not None:
862 return user.email
902 return user.email
863
903
864 # No valid email, not a valid user in the system, none!
904 # No valid email, not a valid user in the system, none!
865 return None
905 return None
866
906
867
907
868 def link_to_user(author, length=0, **kwargs):
908 def link_to_user(author, length=0, **kwargs):
869 user = discover_user(author)
909 user = discover_user(author)
870 # user can be None, but if we have it already it means we can re-use it
910 # user can be None, but if we have it already it means we can re-use it
871 # in the person() function, so we save 1 intensive-query
911 # in the person() function, so we save 1 intensive-query
872 if user:
912 if user:
873 author = user
913 author = user
874
914
875 display_person = person(author, 'username_or_name_or_email')
915 display_person = person(author, 'username_or_name_or_email')
876 if length:
916 if length:
877 display_person = shorter(display_person, length)
917 display_person = shorter(display_person, length)
878
918
879 if user:
919 if user:
880 return link_to(
920 return link_to(
881 escape(display_person),
921 escape(display_person),
882 route_path('user_profile', username=user.username),
922 route_path('user_profile', username=user.username),
883 **kwargs)
923 **kwargs)
884 else:
924 else:
885 return escape(display_person)
925 return escape(display_person)
886
926
887
927
888 def person(author, show_attr="username_and_name"):
928 def person(author, show_attr="username_and_name"):
889 user = discover_user(author)
929 user = discover_user(author)
890 if user:
930 if user:
891 return getattr(user, show_attr)
931 return getattr(user, show_attr)
892 else:
932 else:
893 _author = author_name(author)
933 _author = author_name(author)
894 _email = email(author)
934 _email = email(author)
895 return _author or _email
935 return _author or _email
896
936
897
937
898 def author_string(email):
938 def author_string(email):
899 if email:
939 if email:
900 user = User.get_by_email(email, case_insensitive=True, cache=True)
940 user = User.get_by_email(email, case_insensitive=True, cache=True)
901 if user:
941 if user:
902 if user.first_name or user.last_name:
942 if user.first_name or user.last_name:
903 return '%s %s &lt;%s&gt;' % (
943 return '%s %s &lt;%s&gt;' % (
904 user.first_name, user.last_name, email)
944 user.first_name, user.last_name, email)
905 else:
945 else:
906 return email
946 return email
907 else:
947 else:
908 return email
948 return email
909 else:
949 else:
910 return None
950 return None
911
951
912
952
913 def person_by_id(id_, show_attr="username_and_name"):
953 def person_by_id(id_, show_attr="username_and_name"):
914 # attr to return from fetched user
954 # attr to return from fetched user
915 person_getter = lambda usr: getattr(usr, show_attr)
955 person_getter = lambda usr: getattr(usr, show_attr)
916
956
917 #maybe it's an ID ?
957 #maybe it's an ID ?
918 if str(id_).isdigit() or isinstance(id_, int):
958 if str(id_).isdigit() or isinstance(id_, int):
919 id_ = int(id_)
959 id_ = int(id_)
920 user = User.get(id_)
960 user = User.get(id_)
921 if user is not None:
961 if user is not None:
922 return person_getter(user)
962 return person_getter(user)
923 return id_
963 return id_
924
964
925
965
926 def gravatar_with_user(request, author, show_disabled=False):
966 def gravatar_with_user(request, author, show_disabled=False):
927 _render = request.get_partial_renderer('base/base.mako')
967 _render = request.get_partial_renderer('base/base.mako')
928 return _render('gravatar_with_user', author, show_disabled=show_disabled)
968 return _render('gravatar_with_user', author, show_disabled=show_disabled)
929
969
930
970
931 tags_paterns = OrderedDict((
971 tags_paterns = OrderedDict((
932 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
972 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
933 '<div class="metatag" tag="lang">\\2</div>')),
973 '<div class="metatag" tag="lang">\\2</div>')),
934
974
935 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
975 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
936 '<div class="metatag" tag="see">see: \\1 </div>')),
976 '<div class="metatag" tag="see">see: \\1 </div>')),
937
977
938 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((.*?)\)\]'),
978 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((.*?)\)\]'),
939 '<div class="metatag" tag="url"> <a href="\\2">\\1</a> </div>')),
979 '<div class="metatag" tag="url"> <a href="\\2">\\1</a> </div>')),
940
980
941 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
981 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
942 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
982 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
943
983
944 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
984 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
945 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
985 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
946
986
947 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
987 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
948 '<div class="metatag" tag="state \\1">\\1</div>')),
988 '<div class="metatag" tag="state \\1">\\1</div>')),
949
989
950 # label in grey
990 # label in grey
951 ('label', (re.compile(r'\[([a-z]+)\]'),
991 ('label', (re.compile(r'\[([a-z]+)\]'),
952 '<div class="metatag" tag="label">\\1</div>')),
992 '<div class="metatag" tag="label">\\1</div>')),
953
993
954 # generic catch all in grey
994 # generic catch all in grey
955 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
995 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
956 '<div class="metatag" tag="generic">\\1</div>')),
996 '<div class="metatag" tag="generic">\\1</div>')),
957 ))
997 ))
958
998
959
999
960 def extract_metatags(value):
1000 def extract_metatags(value):
961 """
1001 """
962 Extract supported meta-tags from given text value
1002 Extract supported meta-tags from given text value
963 """
1003 """
964 if not value:
1004 if not value:
965 return ''
1005 return ''
966
1006
967 tags = []
1007 tags = []
968 for key, val in tags_paterns.items():
1008 for key, val in tags_paterns.items():
969 pat, replace_html = val
1009 pat, replace_html = val
970 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1010 tags.extend([(key, x.group()) for x in pat.finditer(value)])
971 value = pat.sub('', value)
1011 value = pat.sub('', value)
972
1012
973 return tags, value
1013 return tags, value
974
1014
975
1015
976 def style_metatag(tag_type, value):
1016 def style_metatag(tag_type, value):
977 """
1017 """
978 converts tags from value into html equivalent
1018 converts tags from value into html equivalent
979 """
1019 """
980 if not value:
1020 if not value:
981 return ''
1021 return ''
982
1022
983 html_value = value
1023 html_value = value
984 tag_data = tags_paterns.get(tag_type)
1024 tag_data = tags_paterns.get(tag_type)
985 if tag_data:
1025 if tag_data:
986 pat, replace_html = tag_data
1026 pat, replace_html = tag_data
987 # convert to plain `unicode` instead of a markup tag to be used in
1027 # convert to plain `unicode` instead of a markup tag to be used in
988 # regex expressions. safe_unicode doesn't work here
1028 # regex expressions. safe_unicode doesn't work here
989 html_value = pat.sub(replace_html, unicode(value))
1029 html_value = pat.sub(replace_html, unicode(value))
990
1030
991 return html_value
1031 return html_value
992
1032
993
1033
994 def bool2icon(value):
1034 def bool2icon(value):
995 """
1035 """
996 Returns boolean value of a given value, represented as html element with
1036 Returns boolean value of a given value, represented as html element with
997 classes that will represent icons
1037 classes that will represent icons
998
1038
999 :param value: given value to convert to html node
1039 :param value: given value to convert to html node
1000 """
1040 """
1001
1041
1002 if value: # does bool conversion
1042 if value: # does bool conversion
1003 return HTML.tag('i', class_="icon-true")
1043 return HTML.tag('i', class_="icon-true")
1004 else: # not true as bool
1044 else: # not true as bool
1005 return HTML.tag('i', class_="icon-false")
1045 return HTML.tag('i', class_="icon-false")
1006
1046
1007
1047
1008 #==============================================================================
1048 #==============================================================================
1009 # PERMS
1049 # PERMS
1010 #==============================================================================
1050 #==============================================================================
1011 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1051 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1012 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1052 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1013 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1053 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1014 csrf_token_key
1054 csrf_token_key
1015
1055
1016
1056
1017 #==============================================================================
1057 #==============================================================================
1018 # GRAVATAR URL
1058 # GRAVATAR URL
1019 #==============================================================================
1059 #==============================================================================
1020 class InitialsGravatar(object):
1060 class InitialsGravatar(object):
1021 def __init__(self, email_address, first_name, last_name, size=30,
1061 def __init__(self, email_address, first_name, last_name, size=30,
1022 background=None, text_color='#fff'):
1062 background=None, text_color='#fff'):
1023 self.size = size
1063 self.size = size
1024 self.first_name = first_name
1064 self.first_name = first_name
1025 self.last_name = last_name
1065 self.last_name = last_name
1026 self.email_address = email_address
1066 self.email_address = email_address
1027 self.background = background or self.str2color(email_address)
1067 self.background = background or self.str2color(email_address)
1028 self.text_color = text_color
1068 self.text_color = text_color
1029
1069
1030 def get_color_bank(self):
1070 def get_color_bank(self):
1031 """
1071 """
1032 returns a predefined list of colors that gravatars can use.
1072 returns a predefined list of colors that gravatars can use.
1033 Those are randomized distinct colors that guarantee readability and
1073 Those are randomized distinct colors that guarantee readability and
1034 uniqueness.
1074 uniqueness.
1035
1075
1036 generated with: http://phrogz.net/css/distinct-colors.html
1076 generated with: http://phrogz.net/css/distinct-colors.html
1037 """
1077 """
1038 return [
1078 return [
1039 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1079 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1040 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1080 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1041 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1081 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1042 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1082 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1043 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1083 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1044 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1084 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1045 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1085 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1046 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1086 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1047 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1087 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1048 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1088 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1049 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1089 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1050 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1090 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1051 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1091 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1052 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1092 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1053 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1093 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1054 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1094 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1055 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1095 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1056 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1096 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1057 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1097 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1058 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1098 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1059 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1099 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1060 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1100 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1061 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1101 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1062 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1102 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1063 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1103 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1064 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1104 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1065 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1105 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1066 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1106 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1067 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1107 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1068 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1108 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1069 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1109 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1070 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1110 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1071 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1111 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1072 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1112 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1073 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1113 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1074 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1114 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1075 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1115 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1076 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1116 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1077 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1117 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1078 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1118 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1079 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1119 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1080 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1120 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1081 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1121 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1082 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1122 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1083 '#4f8c46', '#368dd9', '#5c0073'
1123 '#4f8c46', '#368dd9', '#5c0073'
1084 ]
1124 ]
1085
1125
1086 def rgb_to_hex_color(self, rgb_tuple):
1126 def rgb_to_hex_color(self, rgb_tuple):
1087 """
1127 """
1088 Converts an rgb_tuple passed to an hex color.
1128 Converts an rgb_tuple passed to an hex color.
1089
1129
1090 :param rgb_tuple: tuple with 3 ints represents rgb color space
1130 :param rgb_tuple: tuple with 3 ints represents rgb color space
1091 """
1131 """
1092 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1132 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1093
1133
1094 def email_to_int_list(self, email_str):
1134 def email_to_int_list(self, email_str):
1095 """
1135 """
1096 Get every byte of the hex digest value of email and turn it to integer.
1136 Get every byte of the hex digest value of email and turn it to integer.
1097 It's going to be always between 0-255
1137 It's going to be always between 0-255
1098 """
1138 """
1099 digest = md5_safe(email_str.lower())
1139 digest = md5_safe(email_str.lower())
1100 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1140 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1101
1141
1102 def pick_color_bank_index(self, email_str, color_bank):
1142 def pick_color_bank_index(self, email_str, color_bank):
1103 return self.email_to_int_list(email_str)[0] % len(color_bank)
1143 return self.email_to_int_list(email_str)[0] % len(color_bank)
1104
1144
1105 def str2color(self, email_str):
1145 def str2color(self, email_str):
1106 """
1146 """
1107 Tries to map in a stable algorithm an email to color
1147 Tries to map in a stable algorithm an email to color
1108
1148
1109 :param email_str:
1149 :param email_str:
1110 """
1150 """
1111 color_bank = self.get_color_bank()
1151 color_bank = self.get_color_bank()
1112 # pick position (module it's length so we always find it in the
1152 # pick position (module it's length so we always find it in the
1113 # bank even if it's smaller than 256 values
1153 # bank even if it's smaller than 256 values
1114 pos = self.pick_color_bank_index(email_str, color_bank)
1154 pos = self.pick_color_bank_index(email_str, color_bank)
1115 return color_bank[pos]
1155 return color_bank[pos]
1116
1156
1117 def normalize_email(self, email_address):
1157 def normalize_email(self, email_address):
1118 import unicodedata
1158 import unicodedata
1119 # default host used to fill in the fake/missing email
1159 # default host used to fill in the fake/missing email
1120 default_host = u'localhost'
1160 default_host = u'localhost'
1121
1161
1122 if not email_address:
1162 if not email_address:
1123 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1163 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1124
1164
1125 email_address = safe_unicode(email_address)
1165 email_address = safe_unicode(email_address)
1126
1166
1127 if u'@' not in email_address:
1167 if u'@' not in email_address:
1128 email_address = u'%s@%s' % (email_address, default_host)
1168 email_address = u'%s@%s' % (email_address, default_host)
1129
1169
1130 if email_address.endswith(u'@'):
1170 if email_address.endswith(u'@'):
1131 email_address = u'%s%s' % (email_address, default_host)
1171 email_address = u'%s%s' % (email_address, default_host)
1132
1172
1133 email_address = unicodedata.normalize('NFKD', email_address)\
1173 email_address = unicodedata.normalize('NFKD', email_address)\
1134 .encode('ascii', 'ignore')
1174 .encode('ascii', 'ignore')
1135 return email_address
1175 return email_address
1136
1176
1137 def get_initials(self):
1177 def get_initials(self):
1138 """
1178 """
1139 Returns 2 letter initials calculated based on the input.
1179 Returns 2 letter initials calculated based on the input.
1140 The algorithm picks first given email address, and takes first letter
1180 The algorithm picks first given email address, and takes first letter
1141 of part before @, and then the first letter of server name. In case
1181 of part before @, and then the first letter of server name. In case
1142 the part before @ is in a format of `somestring.somestring2` it replaces
1182 the part before @ is in a format of `somestring.somestring2` it replaces
1143 the server letter with first letter of somestring2
1183 the server letter with first letter of somestring2
1144
1184
1145 In case function was initialized with both first and lastname, this
1185 In case function was initialized with both first and lastname, this
1146 overrides the extraction from email by first letter of the first and
1186 overrides the extraction from email by first letter of the first and
1147 last name. We add special logic to that functionality, In case Full name
1187 last name. We add special logic to that functionality, In case Full name
1148 is compound, like Guido Von Rossum, we use last part of the last name
1188 is compound, like Guido Von Rossum, we use last part of the last name
1149 (Von Rossum) picking `R`.
1189 (Von Rossum) picking `R`.
1150
1190
1151 Function also normalizes the non-ascii characters to they ascii
1191 Function also normalizes the non-ascii characters to they ascii
1152 representation, eg Δ„ => A
1192 representation, eg Δ„ => A
1153 """
1193 """
1154 import unicodedata
1194 import unicodedata
1155 # replace non-ascii to ascii
1195 # replace non-ascii to ascii
1156 first_name = unicodedata.normalize(
1196 first_name = unicodedata.normalize(
1157 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1197 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1158 last_name = unicodedata.normalize(
1198 last_name = unicodedata.normalize(
1159 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1199 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1160
1200
1161 # do NFKD encoding, and also make sure email has proper format
1201 # do NFKD encoding, and also make sure email has proper format
1162 email_address = self.normalize_email(self.email_address)
1202 email_address = self.normalize_email(self.email_address)
1163
1203
1164 # first push the email initials
1204 # first push the email initials
1165 prefix, server = email_address.split('@', 1)
1205 prefix, server = email_address.split('@', 1)
1166
1206
1167 # check if prefix is maybe a 'first_name.last_name' syntax
1207 # check if prefix is maybe a 'first_name.last_name' syntax
1168 _dot_split = prefix.rsplit('.', 1)
1208 _dot_split = prefix.rsplit('.', 1)
1169 if len(_dot_split) == 2 and _dot_split[1]:
1209 if len(_dot_split) == 2 and _dot_split[1]:
1170 initials = [_dot_split[0][0], _dot_split[1][0]]
1210 initials = [_dot_split[0][0], _dot_split[1][0]]
1171 else:
1211 else:
1172 initials = [prefix[0], server[0]]
1212 initials = [prefix[0], server[0]]
1173
1213
1174 # then try to replace either first_name or last_name
1214 # then try to replace either first_name or last_name
1175 fn_letter = (first_name or " ")[0].strip()
1215 fn_letter = (first_name or " ")[0].strip()
1176 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1216 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1177
1217
1178 if fn_letter:
1218 if fn_letter:
1179 initials[0] = fn_letter
1219 initials[0] = fn_letter
1180
1220
1181 if ln_letter:
1221 if ln_letter:
1182 initials[1] = ln_letter
1222 initials[1] = ln_letter
1183
1223
1184 return ''.join(initials).upper()
1224 return ''.join(initials).upper()
1185
1225
1186 def get_img_data_by_type(self, font_family, img_type):
1226 def get_img_data_by_type(self, font_family, img_type):
1187 default_user = """
1227 default_user = """
1188 <svg xmlns="http://www.w3.org/2000/svg"
1228 <svg xmlns="http://www.w3.org/2000/svg"
1189 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1229 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1190 viewBox="-15 -10 439.165 429.164"
1230 viewBox="-15 -10 439.165 429.164"
1191
1231
1192 xml:space="preserve"
1232 xml:space="preserve"
1193 style="background:{background};" >
1233 style="background:{background};" >
1194
1234
1195 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1235 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1196 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1236 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1197 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1237 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1198 168.596,153.916,216.671,
1238 168.596,153.916,216.671,
1199 204.583,216.671z" fill="{text_color}"/>
1239 204.583,216.671z" fill="{text_color}"/>
1200 <path d="M407.164,374.717L360.88,
1240 <path d="M407.164,374.717L360.88,
1201 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1241 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1202 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1242 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1203 15.366-44.203,23.488-69.076,23.488c-24.877,
1243 15.366-44.203,23.488-69.076,23.488c-24.877,
1204 0-48.762-8.122-69.078-23.488
1244 0-48.762-8.122-69.078-23.488
1205 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1245 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1206 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1246 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1207 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1247 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1208 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1248 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1209 19.402-10.527 C409.699,390.129,
1249 19.402-10.527 C409.699,390.129,
1210 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1250 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1211 </svg>""".format(
1251 </svg>""".format(
1212 size=self.size,
1252 size=self.size,
1213 background='#979797', # @grey4
1253 background='#979797', # @grey4
1214 text_color=self.text_color,
1254 text_color=self.text_color,
1215 font_family=font_family)
1255 font_family=font_family)
1216
1256
1217 return {
1257 return {
1218 "default_user": default_user
1258 "default_user": default_user
1219 }[img_type]
1259 }[img_type]
1220
1260
1221 def get_img_data(self, svg_type=None):
1261 def get_img_data(self, svg_type=None):
1222 """
1262 """
1223 generates the svg metadata for image
1263 generates the svg metadata for image
1224 """
1264 """
1225
1265
1226 font_family = ','.join([
1266 font_family = ','.join([
1227 'proximanovaregular',
1267 'proximanovaregular',
1228 'Proxima Nova Regular',
1268 'Proxima Nova Regular',
1229 'Proxima Nova',
1269 'Proxima Nova',
1230 'Arial',
1270 'Arial',
1231 'Lucida Grande',
1271 'Lucida Grande',
1232 'sans-serif'
1272 'sans-serif'
1233 ])
1273 ])
1234 if svg_type:
1274 if svg_type:
1235 return self.get_img_data_by_type(font_family, svg_type)
1275 return self.get_img_data_by_type(font_family, svg_type)
1236
1276
1237 initials = self.get_initials()
1277 initials = self.get_initials()
1238 img_data = """
1278 img_data = """
1239 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1279 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1240 width="{size}" height="{size}"
1280 width="{size}" height="{size}"
1241 style="width: 100%; height: 100%; background-color: {background}"
1281 style="width: 100%; height: 100%; background-color: {background}"
1242 viewBox="0 0 {size} {size}">
1282 viewBox="0 0 {size} {size}">
1243 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1283 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1244 pointer-events="auto" fill="{text_color}"
1284 pointer-events="auto" fill="{text_color}"
1245 font-family="{font_family}"
1285 font-family="{font_family}"
1246 style="font-weight: 400; font-size: {f_size}px;">{text}
1286 style="font-weight: 400; font-size: {f_size}px;">{text}
1247 </text>
1287 </text>
1248 </svg>""".format(
1288 </svg>""".format(
1249 size=self.size,
1289 size=self.size,
1250 f_size=self.size/1.85, # scale the text inside the box nicely
1290 f_size=self.size/1.85, # scale the text inside the box nicely
1251 background=self.background,
1291 background=self.background,
1252 text_color=self.text_color,
1292 text_color=self.text_color,
1253 text=initials.upper(),
1293 text=initials.upper(),
1254 font_family=font_family)
1294 font_family=font_family)
1255
1295
1256 return img_data
1296 return img_data
1257
1297
1258 def generate_svg(self, svg_type=None):
1298 def generate_svg(self, svg_type=None):
1259 img_data = self.get_img_data(svg_type)
1299 img_data = self.get_img_data(svg_type)
1260 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1300 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1261
1301
1262
1302
1263 def initials_gravatar(email_address, first_name, last_name, size=30):
1303 def initials_gravatar(email_address, first_name, last_name, size=30):
1264 svg_type = None
1304 svg_type = None
1265 if email_address == User.DEFAULT_USER_EMAIL:
1305 if email_address == User.DEFAULT_USER_EMAIL:
1266 svg_type = 'default_user'
1306 svg_type = 'default_user'
1267 klass = InitialsGravatar(email_address, first_name, last_name, size)
1307 klass = InitialsGravatar(email_address, first_name, last_name, size)
1268 return klass.generate_svg(svg_type=svg_type)
1308 return klass.generate_svg(svg_type=svg_type)
1269
1309
1270
1310
1271 def gravatar_url(email_address, size=30, request=None):
1311 def gravatar_url(email_address, size=30, request=None):
1272 request = get_current_request()
1312 request = get_current_request()
1273 if request and hasattr(request, 'call_context'):
1313 if request and hasattr(request, 'call_context'):
1274 _use_gravatar = request.call_context.visual.use_gravatar
1314 _use_gravatar = request.call_context.visual.use_gravatar
1275 _gravatar_url = request.call_context.visual.gravatar_url
1315 _gravatar_url = request.call_context.visual.gravatar_url
1276 else:
1316 else:
1277 # doh, we need to re-import those to mock it later
1317 # doh, we need to re-import those to mock it later
1278 from pylons import tmpl_context as c
1318 from pylons import tmpl_context as c
1279
1319
1280 _use_gravatar = c.visual.use_gravatar
1320 _use_gravatar = c.visual.use_gravatar
1281 _gravatar_url = c.visual.gravatar_url
1321 _gravatar_url = c.visual.gravatar_url
1282
1322
1283 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1323 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1284
1324
1285 email_address = email_address or User.DEFAULT_USER_EMAIL
1325 email_address = email_address or User.DEFAULT_USER_EMAIL
1286 if isinstance(email_address, unicode):
1326 if isinstance(email_address, unicode):
1287 # hashlib crashes on unicode items
1327 # hashlib crashes on unicode items
1288 email_address = safe_str(email_address)
1328 email_address = safe_str(email_address)
1289
1329
1290 # empty email or default user
1330 # empty email or default user
1291 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1331 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1292 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1332 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1293
1333
1294 if _use_gravatar:
1334 if _use_gravatar:
1295 # TODO: Disuse pyramid thread locals. Think about another solution to
1335 # TODO: Disuse pyramid thread locals. Think about another solution to
1296 # get the host and schema here.
1336 # get the host and schema here.
1297 request = get_current_request()
1337 request = get_current_request()
1298 tmpl = safe_str(_gravatar_url)
1338 tmpl = safe_str(_gravatar_url)
1299 tmpl = tmpl.replace('{email}', email_address)\
1339 tmpl = tmpl.replace('{email}', email_address)\
1300 .replace('{md5email}', md5_safe(email_address.lower())) \
1340 .replace('{md5email}', md5_safe(email_address.lower())) \
1301 .replace('{netloc}', request.host)\
1341 .replace('{netloc}', request.host)\
1302 .replace('{scheme}', request.scheme)\
1342 .replace('{scheme}', request.scheme)\
1303 .replace('{size}', safe_str(size))
1343 .replace('{size}', safe_str(size))
1304 return tmpl
1344 return tmpl
1305 else:
1345 else:
1306 return initials_gravatar(email_address, '', '', size=size)
1346 return initials_gravatar(email_address, '', '', size=size)
1307
1347
1308
1348
1309 class Page(_Page):
1349 class Page(_Page):
1310 """
1350 """
1311 Custom pager to match rendering style with paginator
1351 Custom pager to match rendering style with paginator
1312 """
1352 """
1313
1353
1314 def _get_pos(self, cur_page, max_page, items):
1354 def _get_pos(self, cur_page, max_page, items):
1315 edge = (items / 2) + 1
1355 edge = (items / 2) + 1
1316 if (cur_page <= edge):
1356 if (cur_page <= edge):
1317 radius = max(items / 2, items - cur_page)
1357 radius = max(items / 2, items - cur_page)
1318 elif (max_page - cur_page) < edge:
1358 elif (max_page - cur_page) < edge:
1319 radius = (items - 1) - (max_page - cur_page)
1359 radius = (items - 1) - (max_page - cur_page)
1320 else:
1360 else:
1321 radius = items / 2
1361 radius = items / 2
1322
1362
1323 left = max(1, (cur_page - (radius)))
1363 left = max(1, (cur_page - (radius)))
1324 right = min(max_page, cur_page + (radius))
1364 right = min(max_page, cur_page + (radius))
1325 return left, cur_page, right
1365 return left, cur_page, right
1326
1366
1327 def _range(self, regexp_match):
1367 def _range(self, regexp_match):
1328 """
1368 """
1329 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1369 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1330
1370
1331 Arguments:
1371 Arguments:
1332
1372
1333 regexp_match
1373 regexp_match
1334 A "re" (regular expressions) match object containing the
1374 A "re" (regular expressions) match object containing the
1335 radius of linked pages around the current page in
1375 radius of linked pages around the current page in
1336 regexp_match.group(1) as a string
1376 regexp_match.group(1) as a string
1337
1377
1338 This function is supposed to be called as a callable in
1378 This function is supposed to be called as a callable in
1339 re.sub.
1379 re.sub.
1340
1380
1341 """
1381 """
1342 radius = int(regexp_match.group(1))
1382 radius = int(regexp_match.group(1))
1343
1383
1344 # Compute the first and last page number within the radius
1384 # Compute the first and last page number within the radius
1345 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1385 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1346 # -> leftmost_page = 5
1386 # -> leftmost_page = 5
1347 # -> rightmost_page = 9
1387 # -> rightmost_page = 9
1348 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1388 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1349 self.last_page,
1389 self.last_page,
1350 (radius * 2) + 1)
1390 (radius * 2) + 1)
1351 nav_items = []
1391 nav_items = []
1352
1392
1353 # Create a link to the first page (unless we are on the first page
1393 # Create a link to the first page (unless we are on the first page
1354 # or there would be no need to insert '..' spacers)
1394 # or there would be no need to insert '..' spacers)
1355 if self.page != self.first_page and self.first_page < leftmost_page:
1395 if self.page != self.first_page and self.first_page < leftmost_page:
1356 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1396 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1357
1397
1358 # Insert dots if there are pages between the first page
1398 # Insert dots if there are pages between the first page
1359 # and the currently displayed page range
1399 # and the currently displayed page range
1360 if leftmost_page - self.first_page > 1:
1400 if leftmost_page - self.first_page > 1:
1361 # Wrap in a SPAN tag if nolink_attr is set
1401 # Wrap in a SPAN tag if nolink_attr is set
1362 text = '..'
1402 text = '..'
1363 if self.dotdot_attr:
1403 if self.dotdot_attr:
1364 text = HTML.span(c=text, **self.dotdot_attr)
1404 text = HTML.span(c=text, **self.dotdot_attr)
1365 nav_items.append(text)
1405 nav_items.append(text)
1366
1406
1367 for thispage in xrange(leftmost_page, rightmost_page + 1):
1407 for thispage in xrange(leftmost_page, rightmost_page + 1):
1368 # Hilight the current page number and do not use a link
1408 # Hilight the current page number and do not use a link
1369 if thispage == self.page:
1409 if thispage == self.page:
1370 text = '%s' % (thispage,)
1410 text = '%s' % (thispage,)
1371 # Wrap in a SPAN tag if nolink_attr is set
1411 # Wrap in a SPAN tag if nolink_attr is set
1372 if self.curpage_attr:
1412 if self.curpage_attr:
1373 text = HTML.span(c=text, **self.curpage_attr)
1413 text = HTML.span(c=text, **self.curpage_attr)
1374 nav_items.append(text)
1414 nav_items.append(text)
1375 # Otherwise create just a link to that page
1415 # Otherwise create just a link to that page
1376 else:
1416 else:
1377 text = '%s' % (thispage,)
1417 text = '%s' % (thispage,)
1378 nav_items.append(self._pagerlink(thispage, text))
1418 nav_items.append(self._pagerlink(thispage, text))
1379
1419
1380 # Insert dots if there are pages between the displayed
1420 # Insert dots if there are pages between the displayed
1381 # page numbers and the end of the page range
1421 # page numbers and the end of the page range
1382 if self.last_page - rightmost_page > 1:
1422 if self.last_page - rightmost_page > 1:
1383 text = '..'
1423 text = '..'
1384 # Wrap in a SPAN tag if nolink_attr is set
1424 # Wrap in a SPAN tag if nolink_attr is set
1385 if self.dotdot_attr:
1425 if self.dotdot_attr:
1386 text = HTML.span(c=text, **self.dotdot_attr)
1426 text = HTML.span(c=text, **self.dotdot_attr)
1387 nav_items.append(text)
1427 nav_items.append(text)
1388
1428
1389 # Create a link to the very last page (unless we are on the last
1429 # Create a link to the very last page (unless we are on the last
1390 # page or there would be no need to insert '..' spacers)
1430 # page or there would be no need to insert '..' spacers)
1391 if self.page != self.last_page and rightmost_page < self.last_page:
1431 if self.page != self.last_page and rightmost_page < self.last_page:
1392 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1432 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1393
1433
1394 ## prerender links
1434 ## prerender links
1395 #_page_link = url.current()
1435 #_page_link = url.current()
1396 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1436 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1397 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1437 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1398 return self.separator.join(nav_items)
1438 return self.separator.join(nav_items)
1399
1439
1400 def pager(self, format='~2~', page_param='page', partial_param='partial',
1440 def pager(self, format='~2~', page_param='page', partial_param='partial',
1401 show_if_single_page=False, separator=' ', onclick=None,
1441 show_if_single_page=False, separator=' ', onclick=None,
1402 symbol_first='<<', symbol_last='>>',
1442 symbol_first='<<', symbol_last='>>',
1403 symbol_previous='<', symbol_next='>',
1443 symbol_previous='<', symbol_next='>',
1404 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1444 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1405 curpage_attr={'class': 'pager_curpage'},
1445 curpage_attr={'class': 'pager_curpage'},
1406 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1446 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1407
1447
1408 self.curpage_attr = curpage_attr
1448 self.curpage_attr = curpage_attr
1409 self.separator = separator
1449 self.separator = separator
1410 self.pager_kwargs = kwargs
1450 self.pager_kwargs = kwargs
1411 self.page_param = page_param
1451 self.page_param = page_param
1412 self.partial_param = partial_param
1452 self.partial_param = partial_param
1413 self.onclick = onclick
1453 self.onclick = onclick
1414 self.link_attr = link_attr
1454 self.link_attr = link_attr
1415 self.dotdot_attr = dotdot_attr
1455 self.dotdot_attr = dotdot_attr
1416
1456
1417 # Don't show navigator if there is no more than one page
1457 # Don't show navigator if there is no more than one page
1418 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1458 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1419 return ''
1459 return ''
1420
1460
1421 from string import Template
1461 from string import Template
1422 # Replace ~...~ in token format by range of pages
1462 # Replace ~...~ in token format by range of pages
1423 result = re.sub(r'~(\d+)~', self._range, format)
1463 result = re.sub(r'~(\d+)~', self._range, format)
1424
1464
1425 # Interpolate '%' variables
1465 # Interpolate '%' variables
1426 result = Template(result).safe_substitute({
1466 result = Template(result).safe_substitute({
1427 'first_page': self.first_page,
1467 'first_page': self.first_page,
1428 'last_page': self.last_page,
1468 'last_page': self.last_page,
1429 'page': self.page,
1469 'page': self.page,
1430 'page_count': self.page_count,
1470 'page_count': self.page_count,
1431 'items_per_page': self.items_per_page,
1471 'items_per_page': self.items_per_page,
1432 'first_item': self.first_item,
1472 'first_item': self.first_item,
1433 'last_item': self.last_item,
1473 'last_item': self.last_item,
1434 'item_count': self.item_count,
1474 'item_count': self.item_count,
1435 'link_first': self.page > self.first_page and \
1475 'link_first': self.page > self.first_page and \
1436 self._pagerlink(self.first_page, symbol_first) or '',
1476 self._pagerlink(self.first_page, symbol_first) or '',
1437 'link_last': self.page < self.last_page and \
1477 'link_last': self.page < self.last_page and \
1438 self._pagerlink(self.last_page, symbol_last) or '',
1478 self._pagerlink(self.last_page, symbol_last) or '',
1439 'link_previous': self.previous_page and \
1479 'link_previous': self.previous_page and \
1440 self._pagerlink(self.previous_page, symbol_previous) \
1480 self._pagerlink(self.previous_page, symbol_previous) \
1441 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1481 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1442 'link_next': self.next_page and \
1482 'link_next': self.next_page and \
1443 self._pagerlink(self.next_page, symbol_next) \
1483 self._pagerlink(self.next_page, symbol_next) \
1444 or HTML.span(symbol_next, class_="pg-next disabled")
1484 or HTML.span(symbol_next, class_="pg-next disabled")
1445 })
1485 })
1446
1486
1447 return literal(result)
1487 return literal(result)
1448
1488
1449
1489
1450 #==============================================================================
1490 #==============================================================================
1451 # REPO PAGER, PAGER FOR REPOSITORY
1491 # REPO PAGER, PAGER FOR REPOSITORY
1452 #==============================================================================
1492 #==============================================================================
1453 class RepoPage(Page):
1493 class RepoPage(Page):
1454
1494
1455 def __init__(self, collection, page=1, items_per_page=20,
1495 def __init__(self, collection, page=1, items_per_page=20,
1456 item_count=None, url=None, **kwargs):
1496 item_count=None, url=None, **kwargs):
1457
1497
1458 """Create a "RepoPage" instance. special pager for paging
1498 """Create a "RepoPage" instance. special pager for paging
1459 repository
1499 repository
1460 """
1500 """
1461 self._url_generator = url
1501 self._url_generator = url
1462
1502
1463 # Safe the kwargs class-wide so they can be used in the pager() method
1503 # Safe the kwargs class-wide so they can be used in the pager() method
1464 self.kwargs = kwargs
1504 self.kwargs = kwargs
1465
1505
1466 # Save a reference to the collection
1506 # Save a reference to the collection
1467 self.original_collection = collection
1507 self.original_collection = collection
1468
1508
1469 self.collection = collection
1509 self.collection = collection
1470
1510
1471 # The self.page is the number of the current page.
1511 # The self.page is the number of the current page.
1472 # The first page has the number 1!
1512 # The first page has the number 1!
1473 try:
1513 try:
1474 self.page = int(page) # make it int() if we get it as a string
1514 self.page = int(page) # make it int() if we get it as a string
1475 except (ValueError, TypeError):
1515 except (ValueError, TypeError):
1476 self.page = 1
1516 self.page = 1
1477
1517
1478 self.items_per_page = items_per_page
1518 self.items_per_page = items_per_page
1479
1519
1480 # Unless the user tells us how many items the collections has
1520 # Unless the user tells us how many items the collections has
1481 # we calculate that ourselves.
1521 # we calculate that ourselves.
1482 if item_count is not None:
1522 if item_count is not None:
1483 self.item_count = item_count
1523 self.item_count = item_count
1484 else:
1524 else:
1485 self.item_count = len(self.collection)
1525 self.item_count = len(self.collection)
1486
1526
1487 # Compute the number of the first and last available page
1527 # Compute the number of the first and last available page
1488 if self.item_count > 0:
1528 if self.item_count > 0:
1489 self.first_page = 1
1529 self.first_page = 1
1490 self.page_count = int(math.ceil(float(self.item_count) /
1530 self.page_count = int(math.ceil(float(self.item_count) /
1491 self.items_per_page))
1531 self.items_per_page))
1492 self.last_page = self.first_page + self.page_count - 1
1532 self.last_page = self.first_page + self.page_count - 1
1493
1533
1494 # Make sure that the requested page number is the range of
1534 # Make sure that the requested page number is the range of
1495 # valid pages
1535 # valid pages
1496 if self.page > self.last_page:
1536 if self.page > self.last_page:
1497 self.page = self.last_page
1537 self.page = self.last_page
1498 elif self.page < self.first_page:
1538 elif self.page < self.first_page:
1499 self.page = self.first_page
1539 self.page = self.first_page
1500
1540
1501 # Note: the number of items on this page can be less than
1541 # Note: the number of items on this page can be less than
1502 # items_per_page if the last page is not full
1542 # items_per_page if the last page is not full
1503 self.first_item = max(0, (self.item_count) - (self.page *
1543 self.first_item = max(0, (self.item_count) - (self.page *
1504 items_per_page))
1544 items_per_page))
1505 self.last_item = ((self.item_count - 1) - items_per_page *
1545 self.last_item = ((self.item_count - 1) - items_per_page *
1506 (self.page - 1))
1546 (self.page - 1))
1507
1547
1508 self.items = list(self.collection[self.first_item:self.last_item + 1])
1548 self.items = list(self.collection[self.first_item:self.last_item + 1])
1509
1549
1510 # Links to previous and next page
1550 # Links to previous and next page
1511 if self.page > self.first_page:
1551 if self.page > self.first_page:
1512 self.previous_page = self.page - 1
1552 self.previous_page = self.page - 1
1513 else:
1553 else:
1514 self.previous_page = None
1554 self.previous_page = None
1515
1555
1516 if self.page < self.last_page:
1556 if self.page < self.last_page:
1517 self.next_page = self.page + 1
1557 self.next_page = self.page + 1
1518 else:
1558 else:
1519 self.next_page = None
1559 self.next_page = None
1520
1560
1521 # No items available
1561 # No items available
1522 else:
1562 else:
1523 self.first_page = None
1563 self.first_page = None
1524 self.page_count = 0
1564 self.page_count = 0
1525 self.last_page = None
1565 self.last_page = None
1526 self.first_item = None
1566 self.first_item = None
1527 self.last_item = None
1567 self.last_item = None
1528 self.previous_page = None
1568 self.previous_page = None
1529 self.next_page = None
1569 self.next_page = None
1530 self.items = []
1570 self.items = []
1531
1571
1532 # This is a subclass of the 'list' type. Initialise the list now.
1572 # This is a subclass of the 'list' type. Initialise the list now.
1533 list.__init__(self, reversed(self.items))
1573 list.__init__(self, reversed(self.items))
1534
1574
1535
1575
1536 def breadcrumb_repo_link(repo):
1576 def breadcrumb_repo_link(repo):
1537 """
1577 """
1538 Makes a breadcrumbs path link to repo
1578 Makes a breadcrumbs path link to repo
1539
1579
1540 ex::
1580 ex::
1541 group >> subgroup >> repo
1581 group >> subgroup >> repo
1542
1582
1543 :param repo: a Repository instance
1583 :param repo: a Repository instance
1544 """
1584 """
1545
1585
1546 path = [
1586 path = [
1547 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1587 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1548 for group in repo.groups_with_parents
1588 for group in repo.groups_with_parents
1549 ] + [
1589 ] + [
1550 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1590 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1551 ]
1591 ]
1552
1592
1553 return literal(' &raquo; '.join(path))
1593 return literal(' &raquo; '.join(path))
1554
1594
1555
1595
1556 def format_byte_size_binary(file_size):
1596 def format_byte_size_binary(file_size):
1557 """
1597 """
1558 Formats file/folder sizes to standard.
1598 Formats file/folder sizes to standard.
1559 """
1599 """
1560 if file_size is None:
1600 if file_size is None:
1561 file_size = 0
1601 file_size = 0
1562
1602
1563 formatted_size = format_byte_size(file_size, binary=True)
1603 formatted_size = format_byte_size(file_size, binary=True)
1564 return formatted_size
1604 return formatted_size
1565
1605
1566
1606
1567 def urlify_text(text_, safe=True):
1607 def urlify_text(text_, safe=True):
1568 """
1608 """
1569 Extrac urls from text and make html links out of them
1609 Extrac urls from text and make html links out of them
1570
1610
1571 :param text_:
1611 :param text_:
1572 """
1612 """
1573
1613
1574 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1614 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1575 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1615 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1576
1616
1577 def url_func(match_obj):
1617 def url_func(match_obj):
1578 url_full = match_obj.groups()[0]
1618 url_full = match_obj.groups()[0]
1579 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1619 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1580 _newtext = url_pat.sub(url_func, text_)
1620 _newtext = url_pat.sub(url_func, text_)
1581 if safe:
1621 if safe:
1582 return literal(_newtext)
1622 return literal(_newtext)
1583 return _newtext
1623 return _newtext
1584
1624
1585
1625
1586 def urlify_commits(text_, repository):
1626 def urlify_commits(text_, repository):
1587 """
1627 """
1588 Extract commit ids from text and make link from them
1628 Extract commit ids from text and make link from them
1589
1629
1590 :param text_:
1630 :param text_:
1591 :param repository: repo name to build the URL with
1631 :param repository: repo name to build the URL with
1592 """
1632 """
1593
1633
1594 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1634 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1595
1635
1596 def url_func(match_obj):
1636 def url_func(match_obj):
1597 commit_id = match_obj.groups()[1]
1637 commit_id = match_obj.groups()[1]
1598 pref = match_obj.groups()[0]
1638 pref = match_obj.groups()[0]
1599 suf = match_obj.groups()[2]
1639 suf = match_obj.groups()[2]
1600
1640
1601 tmpl = (
1641 tmpl = (
1602 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1642 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1603 '%(commit_id)s</a>%(suf)s'
1643 '%(commit_id)s</a>%(suf)s'
1604 )
1644 )
1605 return tmpl % {
1645 return tmpl % {
1606 'pref': pref,
1646 'pref': pref,
1607 'cls': 'revision-link',
1647 'cls': 'revision-link',
1608 'url': route_url('repo_commit', repo_name=repository,
1648 'url': route_url('repo_commit', repo_name=repository,
1609 commit_id=commit_id),
1649 commit_id=commit_id),
1610 'commit_id': commit_id,
1650 'commit_id': commit_id,
1611 'suf': suf
1651 'suf': suf
1612 }
1652 }
1613
1653
1614 newtext = URL_PAT.sub(url_func, text_)
1654 newtext = URL_PAT.sub(url_func, text_)
1615
1655
1616 return newtext
1656 return newtext
1617
1657
1618
1658
1619 def _process_url_func(match_obj, repo_name, uid, entry,
1659 def _process_url_func(match_obj, repo_name, uid, entry,
1620 return_raw_data=False, link_format='html'):
1660 return_raw_data=False, link_format='html'):
1621 pref = ''
1661 pref = ''
1622 if match_obj.group().startswith(' '):
1662 if match_obj.group().startswith(' '):
1623 pref = ' '
1663 pref = ' '
1624
1664
1625 issue_id = ''.join(match_obj.groups())
1665 issue_id = ''.join(match_obj.groups())
1626
1666
1627 if link_format == 'html':
1667 if link_format == 'html':
1628 tmpl = (
1668 tmpl = (
1629 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1669 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1630 '%(issue-prefix)s%(id-repr)s'
1670 '%(issue-prefix)s%(id-repr)s'
1631 '</a>')
1671 '</a>')
1632 elif link_format == 'rst':
1672 elif link_format == 'rst':
1633 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1673 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1634 elif link_format == 'markdown':
1674 elif link_format == 'markdown':
1635 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1675 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1636 else:
1676 else:
1637 raise ValueError('Bad link_format:{}'.format(link_format))
1677 raise ValueError('Bad link_format:{}'.format(link_format))
1638
1678
1639 (repo_name_cleaned,
1679 (repo_name_cleaned,
1640 parent_group_name) = RepoGroupModel().\
1680 parent_group_name) = RepoGroupModel().\
1641 _get_group_name_and_parent(repo_name)
1681 _get_group_name_and_parent(repo_name)
1642
1682
1643 # variables replacement
1683 # variables replacement
1644 named_vars = {
1684 named_vars = {
1645 'id': issue_id,
1685 'id': issue_id,
1646 'repo': repo_name,
1686 'repo': repo_name,
1647 'repo_name': repo_name_cleaned,
1687 'repo_name': repo_name_cleaned,
1648 'group_name': parent_group_name
1688 'group_name': parent_group_name
1649 }
1689 }
1650 # named regex variables
1690 # named regex variables
1651 named_vars.update(match_obj.groupdict())
1691 named_vars.update(match_obj.groupdict())
1652 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1692 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1653
1693
1654 data = {
1694 data = {
1655 'pref': pref,
1695 'pref': pref,
1656 'cls': 'issue-tracker-link',
1696 'cls': 'issue-tracker-link',
1657 'url': _url,
1697 'url': _url,
1658 'id-repr': issue_id,
1698 'id-repr': issue_id,
1659 'issue-prefix': entry['pref'],
1699 'issue-prefix': entry['pref'],
1660 'serv': entry['url'],
1700 'serv': entry['url'],
1661 }
1701 }
1662 if return_raw_data:
1702 if return_raw_data:
1663 return {
1703 return {
1664 'id': issue_id,
1704 'id': issue_id,
1665 'url': _url
1705 'url': _url
1666 }
1706 }
1667 return tmpl % data
1707 return tmpl % data
1668
1708
1669
1709
1670 def process_patterns(text_string, repo_name, link_format='html'):
1710 def process_patterns(text_string, repo_name, link_format='html'):
1671 allowed_formats = ['html', 'rst', 'markdown']
1711 allowed_formats = ['html', 'rst', 'markdown']
1672 if link_format not in allowed_formats:
1712 if link_format not in allowed_formats:
1673 raise ValueError('Link format can be only one of:{} got {}'.format(
1713 raise ValueError('Link format can be only one of:{} got {}'.format(
1674 allowed_formats, link_format))
1714 allowed_formats, link_format))
1675
1715
1676 repo = None
1716 repo = None
1677 if repo_name:
1717 if repo_name:
1678 # Retrieving repo_name to avoid invalid repo_name to explode on
1718 # Retrieving repo_name to avoid invalid repo_name to explode on
1679 # IssueTrackerSettingsModel but still passing invalid name further down
1719 # IssueTrackerSettingsModel but still passing invalid name further down
1680 repo = Repository.get_by_repo_name(repo_name, cache=True)
1720 repo = Repository.get_by_repo_name(repo_name, cache=True)
1681
1721
1682 settings_model = IssueTrackerSettingsModel(repo=repo)
1722 settings_model = IssueTrackerSettingsModel(repo=repo)
1683 active_entries = settings_model.get_settings(cache=True)
1723 active_entries = settings_model.get_settings(cache=True)
1684
1724
1685 issues_data = []
1725 issues_data = []
1686 newtext = text_string
1726 newtext = text_string
1687
1727
1688 for uid, entry in active_entries.items():
1728 for uid, entry in active_entries.items():
1689 log.debug('found issue tracker entry with uid %s' % (uid,))
1729 log.debug('found issue tracker entry with uid %s' % (uid,))
1690
1730
1691 if not (entry['pat'] and entry['url']):
1731 if not (entry['pat'] and entry['url']):
1692 log.debug('skipping due to missing data')
1732 log.debug('skipping due to missing data')
1693 continue
1733 continue
1694
1734
1695 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1735 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1696 % (uid, entry['pat'], entry['url'], entry['pref']))
1736 % (uid, entry['pat'], entry['url'], entry['pref']))
1697
1737
1698 try:
1738 try:
1699 pattern = re.compile(r'%s' % entry['pat'])
1739 pattern = re.compile(r'%s' % entry['pat'])
1700 except re.error:
1740 except re.error:
1701 log.exception(
1741 log.exception(
1702 'issue tracker pattern: `%s` failed to compile',
1742 'issue tracker pattern: `%s` failed to compile',
1703 entry['pat'])
1743 entry['pat'])
1704 continue
1744 continue
1705
1745
1706 data_func = partial(
1746 data_func = partial(
1707 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1747 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1708 return_raw_data=True)
1748 return_raw_data=True)
1709
1749
1710 for match_obj in pattern.finditer(text_string):
1750 for match_obj in pattern.finditer(text_string):
1711 issues_data.append(data_func(match_obj))
1751 issues_data.append(data_func(match_obj))
1712
1752
1713 url_func = partial(
1753 url_func = partial(
1714 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1754 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1715 link_format=link_format)
1755 link_format=link_format)
1716
1756
1717 newtext = pattern.sub(url_func, newtext)
1757 newtext = pattern.sub(url_func, newtext)
1718 log.debug('processed prefix:uid `%s`' % (uid,))
1758 log.debug('processed prefix:uid `%s`' % (uid,))
1719
1759
1720 return newtext, issues_data
1760 return newtext, issues_data
1721
1761
1722
1762
1723 def urlify_commit_message(commit_text, repository=None):
1763 def urlify_commit_message(commit_text, repository=None):
1724 """
1764 """
1725 Parses given text message and makes proper links.
1765 Parses given text message and makes proper links.
1726 issues are linked to given issue-server, and rest is a commit link
1766 issues are linked to given issue-server, and rest is a commit link
1727
1767
1728 :param commit_text:
1768 :param commit_text:
1729 :param repository:
1769 :param repository:
1730 """
1770 """
1731 from pylons import url # doh, we need to re-import url to mock it later
1771 from pylons import url # doh, we need to re-import url to mock it later
1732
1772
1733 def escaper(string):
1773 def escaper(string):
1734 return string.replace('<', '&lt;').replace('>', '&gt;')
1774 return string.replace('<', '&lt;').replace('>', '&gt;')
1735
1775
1736 newtext = escaper(commit_text)
1776 newtext = escaper(commit_text)
1737
1777
1738 # extract http/https links and make them real urls
1778 # extract http/https links and make them real urls
1739 newtext = urlify_text(newtext, safe=False)
1779 newtext = urlify_text(newtext, safe=False)
1740
1780
1741 # urlify commits - extract commit ids and make link out of them, if we have
1781 # urlify commits - extract commit ids and make link out of them, if we have
1742 # the scope of repository present.
1782 # the scope of repository present.
1743 if repository:
1783 if repository:
1744 newtext = urlify_commits(newtext, repository)
1784 newtext = urlify_commits(newtext, repository)
1745
1785
1746 # process issue tracker patterns
1786 # process issue tracker patterns
1747 newtext, issues = process_patterns(newtext, repository or '')
1787 newtext, issues = process_patterns(newtext, repository or '')
1748
1788
1749 return literal(newtext)
1789 return literal(newtext)
1750
1790
1751
1791
1752 def render_binary(repo_name, file_obj):
1792 def render_binary(repo_name, file_obj):
1753 """
1793 """
1754 Choose how to render a binary file
1794 Choose how to render a binary file
1755 """
1795 """
1756 filename = file_obj.name
1796 filename = file_obj.name
1757
1797
1758 # images
1798 # images
1759 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1799 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1760 if fnmatch.fnmatch(filename, pat=ext):
1800 if fnmatch.fnmatch(filename, pat=ext):
1761 alt = filename
1801 alt = filename
1762 src = route_path(
1802 src = route_path(
1763 'repo_file_raw', repo_name=repo_name,
1803 'repo_file_raw', repo_name=repo_name,
1764 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1804 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1765 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1805 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1766
1806
1767
1807
1768 def renderer_from_filename(filename, exclude=None):
1808 def renderer_from_filename(filename, exclude=None):
1769 """
1809 """
1770 choose a renderer based on filename, this works only for text based files
1810 choose a renderer based on filename, this works only for text based files
1771 """
1811 """
1772
1812
1773 # ipython
1813 # ipython
1774 for ext in ['*.ipynb']:
1814 for ext in ['*.ipynb']:
1775 if fnmatch.fnmatch(filename, pat=ext):
1815 if fnmatch.fnmatch(filename, pat=ext):
1776 return 'jupyter'
1816 return 'jupyter'
1777
1817
1778 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1818 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1779 if is_markup:
1819 if is_markup:
1780 return is_markup
1820 return is_markup
1781 return None
1821 return None
1782
1822
1783
1823
1784 def render(source, renderer='rst', mentions=False, relative_urls=None,
1824 def render(source, renderer='rst', mentions=False, relative_urls=None,
1785 repo_name=None):
1825 repo_name=None):
1786
1826
1787 def maybe_convert_relative_links(html_source):
1827 def maybe_convert_relative_links(html_source):
1788 if relative_urls:
1828 if relative_urls:
1789 return relative_links(html_source, relative_urls)
1829 return relative_links(html_source, relative_urls)
1790 return html_source
1830 return html_source
1791
1831
1792 if renderer == 'rst':
1832 if renderer == 'rst':
1793 if repo_name:
1833 if repo_name:
1794 # process patterns on comments if we pass in repo name
1834 # process patterns on comments if we pass in repo name
1795 source, issues = process_patterns(
1835 source, issues = process_patterns(
1796 source, repo_name, link_format='rst')
1836 source, repo_name, link_format='rst')
1797
1837
1798 return literal(
1838 return literal(
1799 '<div class="rst-block">%s</div>' %
1839 '<div class="rst-block">%s</div>' %
1800 maybe_convert_relative_links(
1840 maybe_convert_relative_links(
1801 MarkupRenderer.rst(source, mentions=mentions)))
1841 MarkupRenderer.rst(source, mentions=mentions)))
1802 elif renderer == 'markdown':
1842 elif renderer == 'markdown':
1803 if repo_name:
1843 if repo_name:
1804 # process patterns on comments if we pass in repo name
1844 # process patterns on comments if we pass in repo name
1805 source, issues = process_patterns(
1845 source, issues = process_patterns(
1806 source, repo_name, link_format='markdown')
1846 source, repo_name, link_format='markdown')
1807
1847
1808 return literal(
1848 return literal(
1809 '<div class="markdown-block">%s</div>' %
1849 '<div class="markdown-block">%s</div>' %
1810 maybe_convert_relative_links(
1850 maybe_convert_relative_links(
1811 MarkupRenderer.markdown(source, flavored=True,
1851 MarkupRenderer.markdown(source, flavored=True,
1812 mentions=mentions)))
1852 mentions=mentions)))
1813 elif renderer == 'jupyter':
1853 elif renderer == 'jupyter':
1814 return literal(
1854 return literal(
1815 '<div class="ipynb">%s</div>' %
1855 '<div class="ipynb">%s</div>' %
1816 maybe_convert_relative_links(
1856 maybe_convert_relative_links(
1817 MarkupRenderer.jupyter(source)))
1857 MarkupRenderer.jupyter(source)))
1818
1858
1819 # None means just show the file-source
1859 # None means just show the file-source
1820 return None
1860 return None
1821
1861
1822
1862
1823 def commit_status(repo, commit_id):
1863 def commit_status(repo, commit_id):
1824 return ChangesetStatusModel().get_status(repo, commit_id)
1864 return ChangesetStatusModel().get_status(repo, commit_id)
1825
1865
1826
1866
1827 def commit_status_lbl(commit_status):
1867 def commit_status_lbl(commit_status):
1828 return dict(ChangesetStatus.STATUSES).get(commit_status)
1868 return dict(ChangesetStatus.STATUSES).get(commit_status)
1829
1869
1830
1870
1831 def commit_time(repo_name, commit_id):
1871 def commit_time(repo_name, commit_id):
1832 repo = Repository.get_by_repo_name(repo_name)
1872 repo = Repository.get_by_repo_name(repo_name)
1833 commit = repo.get_commit(commit_id=commit_id)
1873 commit = repo.get_commit(commit_id=commit_id)
1834 return commit.date
1874 return commit.date
1835
1875
1836
1876
1837 def get_permission_name(key):
1877 def get_permission_name(key):
1838 return dict(Permission.PERMS).get(key)
1878 return dict(Permission.PERMS).get(key)
1839
1879
1840
1880
1841 def journal_filter_help(request):
1881 def journal_filter_help(request):
1842 _ = request.translate
1882 _ = request.translate
1843
1883
1844 return _(
1884 return _(
1845 'Example filter terms:\n' +
1885 'Example filter terms:\n' +
1846 ' repository:vcs\n' +
1886 ' repository:vcs\n' +
1847 ' username:marcin\n' +
1887 ' username:marcin\n' +
1848 ' username:(NOT marcin)\n' +
1888 ' username:(NOT marcin)\n' +
1849 ' action:*push*\n' +
1889 ' action:*push*\n' +
1850 ' ip:127.0.0.1\n' +
1890 ' ip:127.0.0.1\n' +
1851 ' date:20120101\n' +
1891 ' date:20120101\n' +
1852 ' date:[20120101100000 TO 20120102]\n' +
1892 ' date:[20120101100000 TO 20120102]\n' +
1853 '\n' +
1893 '\n' +
1854 'Generate wildcards using \'*\' character:\n' +
1894 'Generate wildcards using \'*\' character:\n' +
1855 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1895 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1856 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1896 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1857 '\n' +
1897 '\n' +
1858 'Optional AND / OR operators in queries\n' +
1898 'Optional AND / OR operators in queries\n' +
1859 ' "repository:vcs OR repository:test"\n' +
1899 ' "repository:vcs OR repository:test"\n' +
1860 ' "username:test AND repository:test*"\n'
1900 ' "username:test AND repository:test*"\n'
1861 )
1901 )
1862
1902
1863
1903
1864 def search_filter_help(searcher, request):
1904 def search_filter_help(searcher, request):
1865 _ = request.translate
1905 _ = request.translate
1866
1906
1867 terms = ''
1907 terms = ''
1868 return _(
1908 return _(
1869 'Example filter terms for `{searcher}` search:\n' +
1909 'Example filter terms for `{searcher}` search:\n' +
1870 '{terms}\n' +
1910 '{terms}\n' +
1871 'Generate wildcards using \'*\' character:\n' +
1911 'Generate wildcards using \'*\' character:\n' +
1872 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1912 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1873 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1913 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1874 '\n' +
1914 '\n' +
1875 'Optional AND / OR operators in queries\n' +
1915 'Optional AND / OR operators in queries\n' +
1876 ' "repo_name:vcs OR repo_name:test"\n' +
1916 ' "repo_name:vcs OR repo_name:test"\n' +
1877 ' "owner:test AND repo_name:test*"\n' +
1917 ' "owner:test AND repo_name:test*"\n' +
1878 'More: {search_doc}'
1918 'More: {search_doc}'
1879 ).format(searcher=searcher.name,
1919 ).format(searcher=searcher.name,
1880 terms=terms, search_doc=searcher.query_lang_doc)
1920 terms=terms, search_doc=searcher.query_lang_doc)
1881
1921
1882
1922
1883 def not_mapped_error(repo_name):
1923 def not_mapped_error(repo_name):
1884 from rhodecode.translation import _
1924 from rhodecode.translation import _
1885 flash(_('%s repository is not mapped to db perhaps'
1925 flash(_('%s repository is not mapped to db perhaps'
1886 ' it was created or renamed from the filesystem'
1926 ' it was created or renamed from the filesystem'
1887 ' please run the application again'
1927 ' please run the application again'
1888 ' in order to rescan repositories') % repo_name, category='error')
1928 ' in order to rescan repositories') % repo_name, category='error')
1889
1929
1890
1930
1891 def ip_range(ip_addr):
1931 def ip_range(ip_addr):
1892 from rhodecode.model.db import UserIpMap
1932 from rhodecode.model.db import UserIpMap
1893 s, e = UserIpMap._get_ip_range(ip_addr)
1933 s, e = UserIpMap._get_ip_range(ip_addr)
1894 return '%s - %s' % (s, e)
1934 return '%s - %s' % (s, e)
1895
1935
1896
1936
1897 def form(url, method='post', needs_csrf_token=True, **attrs):
1937 def form(url, method='post', needs_csrf_token=True, **attrs):
1898 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1938 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1899 if method.lower() != 'get' and needs_csrf_token:
1939 if method.lower() != 'get' and needs_csrf_token:
1900 raise Exception(
1940 raise Exception(
1901 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1941 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1902 'CSRF token. If the endpoint does not require such token you can ' +
1942 'CSRF token. If the endpoint does not require such token you can ' +
1903 'explicitly set the parameter needs_csrf_token to false.')
1943 'explicitly set the parameter needs_csrf_token to false.')
1904
1944
1905 return wh_form(url, method=method, **attrs)
1945 return wh_form(url, method=method, **attrs)
1906
1946
1907
1947
1908 def secure_form(form_url, method="POST", multipart=False, **attrs):
1948 def secure_form(form_url, method="POST", multipart=False, **attrs):
1909 """Start a form tag that points the action to an url. This
1949 """Start a form tag that points the action to an url. This
1910 form tag will also include the hidden field containing
1950 form tag will also include the hidden field containing
1911 the auth token.
1951 the auth token.
1912
1952
1913 The url options should be given either as a string, or as a
1953 The url options should be given either as a string, or as a
1914 ``url()`` function. The method for the form defaults to POST.
1954 ``url()`` function. The method for the form defaults to POST.
1915
1955
1916 Options:
1956 Options:
1917
1957
1918 ``multipart``
1958 ``multipart``
1919 If set to True, the enctype is set to "multipart/form-data".
1959 If set to True, the enctype is set to "multipart/form-data".
1920 ``method``
1960 ``method``
1921 The method to use when submitting the form, usually either
1961 The method to use when submitting the form, usually either
1922 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1962 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1923 hidden input with name _method is added to simulate the verb
1963 hidden input with name _method is added to simulate the verb
1924 over POST.
1964 over POST.
1925
1965
1926 """
1966 """
1927 from webhelpers.pylonslib.secure_form import insecure_form
1967 from webhelpers.pylonslib.secure_form import insecure_form
1928
1968
1929 session = None
1969 session = None
1930
1970
1931 # TODO(marcink): after pyramid migration require request variable ALWAYS
1971 # TODO(marcink): after pyramid migration require request variable ALWAYS
1932 if 'request' in attrs:
1972 if 'request' in attrs:
1933 session = attrs['request'].session
1973 session = attrs['request'].session
1934 del attrs['request']
1974 del attrs['request']
1935
1975
1936 form = insecure_form(form_url, method, multipart, **attrs)
1976 form = insecure_form(form_url, method, multipart, **attrs)
1937 token = literal(
1977 token = literal(
1938 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1978 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1939 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1979 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1940
1980
1941 return literal("%s\n%s" % (form, token))
1981 return literal("%s\n%s" % (form, token))
1942
1982
1943
1983
1944 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1984 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1945 select_html = select(name, selected, options, **attrs)
1985 select_html = select(name, selected, options, **attrs)
1946 select2 = """
1986 select2 = """
1947 <script>
1987 <script>
1948 $(document).ready(function() {
1988 $(document).ready(function() {
1949 $('#%s').select2({
1989 $('#%s').select2({
1950 containerCssClass: 'drop-menu',
1990 containerCssClass: 'drop-menu',
1951 dropdownCssClass: 'drop-menu-dropdown',
1991 dropdownCssClass: 'drop-menu-dropdown',
1952 dropdownAutoWidth: true%s
1992 dropdownAutoWidth: true%s
1953 });
1993 });
1954 });
1994 });
1955 </script>
1995 </script>
1956 """
1996 """
1957 filter_option = """,
1997 filter_option = """,
1958 minimumResultsForSearch: -1
1998 minimumResultsForSearch: -1
1959 """
1999 """
1960 input_id = attrs.get('id') or name
2000 input_id = attrs.get('id') or name
1961 filter_enabled = "" if enable_filter else filter_option
2001 filter_enabled = "" if enable_filter else filter_option
1962 select_script = literal(select2 % (input_id, filter_enabled))
2002 select_script = literal(select2 % (input_id, filter_enabled))
1963
2003
1964 return literal(select_html+select_script)
2004 return literal(select_html+select_script)
1965
2005
1966
2006
1967 def get_visual_attr(tmpl_context_var, attr_name):
2007 def get_visual_attr(tmpl_context_var, attr_name):
1968 """
2008 """
1969 A safe way to get a variable from visual variable of template context
2009 A safe way to get a variable from visual variable of template context
1970
2010
1971 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
2011 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1972 :param attr_name: name of the attribute we fetch from the c.visual
2012 :param attr_name: name of the attribute we fetch from the c.visual
1973 """
2013 """
1974 visual = getattr(tmpl_context_var, 'visual', None)
2014 visual = getattr(tmpl_context_var, 'visual', None)
1975 if not visual:
2015 if not visual:
1976 return
2016 return
1977 else:
2017 else:
1978 return getattr(visual, attr_name, None)
2018 return getattr(visual, attr_name, None)
1979
2019
1980
2020
1981 def get_last_path_part(file_node):
2021 def get_last_path_part(file_node):
1982 if not file_node.path:
2022 if not file_node.path:
1983 return u''
2023 return u''
1984
2024
1985 path = safe_unicode(file_node.path.split('/')[-1])
2025 path = safe_unicode(file_node.path.split('/')[-1])
1986 return u'../' + path
2026 return u'../' + path
1987
2027
1988
2028
1989 def route_url(*args, **kwargs):
2029 def route_url(*args, **kwargs):
1990 """
2030 """
1991 Wrapper around pyramids `route_url` (fully qualified url) function.
2031 Wrapper around pyramids `route_url` (fully qualified url) function.
1992 It is used to generate URLs from within pylons views or templates.
2032 It is used to generate URLs from within pylons views or templates.
1993 This will be removed when pyramid migration if finished.
2033 This will be removed when pyramid migration if finished.
1994 """
2034 """
1995 req = get_current_request()
2035 req = get_current_request()
1996 return req.route_url(*args, **kwargs)
2036 return req.route_url(*args, **kwargs)
1997
2037
1998
2038
1999 def route_path(*args, **kwargs):
2039 def route_path(*args, **kwargs):
2000 """
2040 """
2001 Wrapper around pyramids `route_path` function. It is used to generate
2041 Wrapper around pyramids `route_path` function. It is used to generate
2002 URLs from within pylons views or templates. This will be removed when
2042 URLs from within pylons views or templates. This will be removed when
2003 pyramid migration if finished.
2043 pyramid migration if finished.
2004 """
2044 """
2005 req = get_current_request()
2045 req = get_current_request()
2006 return req.route_path(*args, **kwargs)
2046 return req.route_path(*args, **kwargs)
2007
2047
2008
2048
2009 def route_path_or_none(*args, **kwargs):
2049 def route_path_or_none(*args, **kwargs):
2010 try:
2050 try:
2011 return route_path(*args, **kwargs)
2051 return route_path(*args, **kwargs)
2012 except KeyError:
2052 except KeyError:
2013 return None
2053 return None
2014
2054
2015
2055
2016 def static_url(*args, **kwds):
2056 def static_url(*args, **kwds):
2017 """
2057 """
2018 Wrapper around pyramids `route_path` function. It is used to generate
2058 Wrapper around pyramids `route_path` function. It is used to generate
2019 URLs from within pylons views or templates. This will be removed when
2059 URLs from within pylons views or templates. This will be removed when
2020 pyramid migration if finished.
2060 pyramid migration if finished.
2021 """
2061 """
2022 req = get_current_request()
2062 req = get_current_request()
2023 return req.static_url(*args, **kwds)
2063 return req.static_url(*args, **kwds)
2024
2064
2025
2065
2026 def resource_path(*args, **kwds):
2066 def resource_path(*args, **kwds):
2027 """
2067 """
2028 Wrapper around pyramids `route_path` function. It is used to generate
2068 Wrapper around pyramids `route_path` function. It is used to generate
2029 URLs from within pylons views or templates. This will be removed when
2069 URLs from within pylons views or templates. This will be removed when
2030 pyramid migration if finished.
2070 pyramid migration if finished.
2031 """
2071 """
2032 req = get_current_request()
2072 req = get_current_request()
2033 return req.resource_path(*args, **kwds)
2073 return req.resource_path(*args, **kwds)
2034
2074
2035
2075
2036 def api_call_example(method, args):
2076 def api_call_example(method, args):
2037 """
2077 """
2038 Generates an API call example via CURL
2078 Generates an API call example via CURL
2039 """
2079 """
2040 args_json = json.dumps(OrderedDict([
2080 args_json = json.dumps(OrderedDict([
2041 ('id', 1),
2081 ('id', 1),
2042 ('auth_token', 'SECRET'),
2082 ('auth_token', 'SECRET'),
2043 ('method', method),
2083 ('method', method),
2044 ('args', args)
2084 ('args', args)
2045 ]))
2085 ]))
2046 return literal(
2086 return literal(
2047 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2087 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2048 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2088 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2049 "and needs to be of `api calls` role."
2089 "and needs to be of `api calls` role."
2050 .format(
2090 .format(
2051 api_url=route_url('apiv2'),
2091 api_url=route_url('apiv2'),
2052 token_url=route_url('my_account_auth_tokens'),
2092 token_url=route_url('my_account_auth_tokens'),
2053 data=args_json))
2093 data=args_json))
2054
2094
2055
2095
2056 def notification_description(notification, request):
2096 def notification_description(notification, request):
2057 """
2097 """
2058 Generate notification human readable description based on notification type
2098 Generate notification human readable description based on notification type
2059 """
2099 """
2060 from rhodecode.model.notification import NotificationModel
2100 from rhodecode.model.notification import NotificationModel
2061 return NotificationModel().make_description(
2101 return NotificationModel().make_description(
2062 notification, translate=request.translate)
2102 notification, translate=request.translate)
@@ -1,183 +1,183 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <!DOCTYPE html>
2 <!DOCTYPE html>
3
3
4 <%
4 <%
5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6
6
7 if hasattr(c, 'rhodecode_db_repo'):
7 if hasattr(c, 'rhodecode_db_repo'):
8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10
10
11 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
11 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
12 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
12 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
13 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
13 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
14 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
14 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
15 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.first_name
15 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.first_name
16 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.last_name
16 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.last_name
17
17
18 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
18 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
19 c.template_context['default_user'] = {
19 c.template_context['default_user'] = {
20 'username': h.DEFAULT_USER,
20 'username': h.DEFAULT_USER,
21 'user_id': 1
21 'user_id': 1
22 }
22 }
23
23
24 %>
24 %>
25 <html xmlns="http://www.w3.org/1999/xhtml">
25 <html xmlns="http://www.w3.org/1999/xhtml">
26 <head>
26 <head>
27 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-lite.min.js', ver=c.rhodecode_version_hash)}"></script>
27 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-lite.min.js', ver=c.rhodecode_version_hash)}"></script>
28 <link rel="import" href="${h.asset('js/rhodecode-components.html', ver=c.rhodecode_version_hash)}">
28 <link rel="import" href="${h.asset('js/rhodecode-components.html', ver=c.rhodecode_version_hash)}">
29 <title>${self.title()}</title>
29 <title>${self.title()}</title>
30 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
30 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
31
31
32 % if 'safari' in (request.user_agent or '').lower():
32 % if 'safari' in (request.user_agent or '').lower():
33 <meta name="referrer" content="origin">
33 <meta name="referrer" content="origin">
34 % else:
34 % else:
35 <meta name="referrer" content="origin-when-cross-origin">
35 <meta name="referrer" content="origin-when-cross-origin">
36 % endif
36 % endif
37
37
38 <%def name="robots()">
38 <%def name="robots()">
39 <meta name="robots" content="index, nofollow"/>
39 <meta name="robots" content="index, nofollow"/>
40 </%def>
40 </%def>
41 ${self.robots()}
41 ${self.robots()}
42 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
42 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
43
43
44 ## CSS definitions
44 ## CSS definitions
45 <%def name="css()">
45 <%def name="css()">
46 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
46 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
47 <!--[if lt IE 9]>
47 <!--[if lt IE 9]>
48 <link rel="stylesheet" type="text/css" href="${h.asset('css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
48 <link rel="stylesheet" type="text/css" href="${h.asset('css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
49 <![endif]-->
49 <![endif]-->
50 ## EXTRA FOR CSS
50 ## EXTRA FOR CSS
51 ${self.css_extra()}
51 ${self.css_extra()}
52 </%def>
52 </%def>
53 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
53 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
54 <%def name="css_extra()">
54 <%def name="css_extra()">
55 </%def>
55 </%def>
56
56
57 ${self.css()}
57 ${self.css()}
58
58
59 ## JAVASCRIPT
59 ## JAVASCRIPT
60 <%def name="js()">
60 <%def name="js()">
61 <script>
61 <script>
62 // setup Polymer options
62 // setup Polymer options
63 window.Polymer = {lazyRegister: true, dom: 'shadow'};
63 window.Polymer = {lazyRegister: true, dom: 'shadow'};
64
64
65 // Load webcomponentsjs polyfill if browser does not support native Web Components
65 // Load webcomponentsjs polyfill if browser does not support native Web Components
66 (function() {
66 (function() {
67 'use strict';
67 'use strict';
68 var onload = function() {
68 var onload = function() {
69 // For native Imports, manually fire WebComponentsReady so user code
69 // For native Imports, manually fire WebComponentsReady so user code
70 // can use the same code path for native and polyfill'd imports.
70 // can use the same code path for native and polyfill'd imports.
71 if (!window.HTMLImports) {
71 if (!window.HTMLImports) {
72 document.dispatchEvent(
72 document.dispatchEvent(
73 new CustomEvent('WebComponentsReady', {bubbles: true})
73 new CustomEvent('WebComponentsReady', {bubbles: true})
74 );
74 );
75 }
75 }
76 };
76 };
77 var webComponentsSupported = (
77 var webComponentsSupported = (
78 'registerElement' in document
78 'registerElement' in document
79 && 'import' in document.createElement('link')
79 && 'import' in document.createElement('link')
80 && 'content' in document.createElement('template')
80 && 'content' in document.createElement('template')
81 );
81 );
82 if (!webComponentsSupported) {
82 if (!webComponentsSupported) {
83 } else {
83 } else {
84 onload();
84 onload();
85 }
85 }
86 })();
86 })();
87 </script>
87 </script>
88
88
89 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
89 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
90 <script type="text/javascript">
90 <script type="text/javascript">
91 // register templateContext to pass template variables to JS
91 // register templateContext to pass template variables to JS
92 var templateContext = ${h.json.dumps(c.template_context)|n};
92 var templateContext = ${h.json.dumps(c.template_context)|n};
93
93
94 var APPLICATION_URL = "${h.route_path('home').rstrip('/')}";
94 var APPLICATION_URL = "${h.route_path('home').rstrip('/')}";
95 var ASSET_URL = "${h.asset('')}";
95 var ASSET_URL = "${h.asset('')}";
96 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
96 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
97 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
97 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
98
98
99 var APPENLIGHT = {
99 var APPENLIGHT = {
100 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
100 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
101 key: '${getattr(c, "appenlight_api_public_key", "")}',
101 key: '${getattr(c, "appenlight_api_public_key", "")}',
102 % if getattr(c, 'appenlight_server_url', None):
102 % if getattr(c, 'appenlight_server_url', None):
103 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
103 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
104 % endif
104 % endif
105 requestInfo: {
105 requestInfo: {
106 % if getattr(c, 'rhodecode_user', None):
106 % if getattr(c, 'rhodecode_user', None):
107 ip: '${c.rhodecode_user.ip_addr}',
107 ip: '${c.rhodecode_user.ip_addr}',
108 username: '${c.rhodecode_user.username}'
108 username: '${c.rhodecode_user.username}'
109 % endif
109 % endif
110 },
110 },
111 tags: {
111 tags: {
112 rhodecode_version: '${c.rhodecode_version}',
112 rhodecode_version: '${c.rhodecode_version}',
113 rhodecode_edition: '${c.rhodecode_edition}'
113 rhodecode_edition: '${c.rhodecode_edition}'
114 }
114 }
115 };
115 };
116
116
117 </script>
117 </script>
118 <%include file="/base/plugins_base.mako"/>
118 <%include file="/base/plugins_base.mako"/>
119 <!--[if lt IE 9]>
119 <!--[if lt IE 9]>
120 <script language="javascript" type="text/javascript" src="${h.asset('js/excanvas.min.js')}"></script>
120 <script language="javascript" type="text/javascript" src="${h.asset('js/excanvas.min.js')}"></script>
121 <![endif]-->
121 <![endif]-->
122 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
122 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
123 <script> var alertMessagePayloads = ${h.flash.json_alerts(request)|n}; </script>
123 <script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
124 ## avoide escaping the %N
124 ## avoide escaping the %N
125 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode-components.js', ver=c.rhodecode_version_hash)}"></script>
125 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode-components.js', ver=c.rhodecode_version_hash)}"></script>
126 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
126 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
127
127
128
128
129 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
129 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
130 ${self.js_extra()}
130 ${self.js_extra()}
131
131
132 <script type="text/javascript">
132 <script type="text/javascript">
133 Rhodecode = (function() {
133 Rhodecode = (function() {
134 function _Rhodecode() {
134 function _Rhodecode() {
135 this.comments = new CommentsController();
135 this.comments = new CommentsController();
136 }
136 }
137 return new _Rhodecode();
137 return new _Rhodecode();
138 })();
138 })();
139
139
140 $(document).ready(function(){
140 $(document).ready(function(){
141 show_more_event();
141 show_more_event();
142 timeagoActivate();
142 timeagoActivate();
143 clipboardActivate();
143 clipboardActivate();
144 })
144 })
145 </script>
145 </script>
146
146
147 </%def>
147 </%def>
148
148
149 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
149 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
150 <%def name="js_extra()"></%def>
150 <%def name="js_extra()"></%def>
151 ${self.js()}
151 ${self.js()}
152
152
153 <%def name="head_extra()"></%def>
153 <%def name="head_extra()"></%def>
154 ${self.head_extra()}
154 ${self.head_extra()}
155 ## extra stuff
155 ## extra stuff
156 %if c.pre_code:
156 %if c.pre_code:
157 ${c.pre_code|n}
157 ${c.pre_code|n}
158 %endif
158 %endif
159 </head>
159 </head>
160 <body id="body">
160 <body id="body">
161 <noscript>
161 <noscript>
162 <div class="noscript-error">
162 <div class="noscript-error">
163 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
163 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
164 </div>
164 </div>
165 </noscript>
165 </noscript>
166 ## IE hacks
166 ## IE hacks
167 <!--[if IE 7]>
167 <!--[if IE 7]>
168 <script>$(document.body).addClass('ie7')</script>
168 <script>$(document.body).addClass('ie7')</script>
169 <![endif]-->
169 <![endif]-->
170 <!--[if IE 8]>
170 <!--[if IE 8]>
171 <script>$(document.body).addClass('ie8')</script>
171 <script>$(document.body).addClass('ie8')</script>
172 <![endif]-->
172 <![endif]-->
173 <!--[if IE 9]>
173 <!--[if IE 9]>
174 <script>$(document.body).addClass('ie9')</script>
174 <script>$(document.body).addClass('ie9')</script>
175 <![endif]-->
175 <![endif]-->
176
176
177 ${next.body()}
177 ${next.body()}
178 %if c.post_code:
178 %if c.post_code:
179 ${c.post_code|n}
179 ${c.post_code|n}
180 %endif
180 %endif
181 <rhodecode-app></rhodecode-app>
181 <rhodecode-app></rhodecode-app>
182 </body>
182 </body>
183 </html>
183 </html>
@@ -1,265 +1,248 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 import os
21 import os
22 import time
22 import time
23 import logging
23 import logging
24 import datetime
24 import datetime
25 import hashlib
25 import hashlib
26 import tempfile
26 import tempfile
27 from os.path import join as jn
27 from os.path import join as jn
28
28
29 from tempfile import _RandomNameSequence
29 from tempfile import _RandomNameSequence
30
30
31 from paste.deploy import loadapp
31 from pylons import url
32 from paste.script.appinstall import SetupCommand
33
32
34 import pylons
35 import pylons.test
36 from pylons import config, url
37 from pylons.i18n.translation import _get_translator
38 from pylons.util import ContextObj
39
40 from routes.util import URLGenerator
41 from nose.plugins.skip import SkipTest
33 from nose.plugins.skip import SkipTest
42 import pytest
34 import pytest
43
35
44 from rhodecode import is_windows
45 from rhodecode.config.routing import ADMIN_PREFIX
46 from rhodecode.model.meta import Session
47 from rhodecode.model.db import User
36 from rhodecode.model.db import User
48 from rhodecode.lib import auth
37 from rhodecode.lib import auth
49 from rhodecode.lib import helpers as h
38 from rhodecode.lib import helpers as h
50 from rhodecode.lib.helpers import flash, link_to
39 from rhodecode.lib.helpers import flash, link_to
51 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.lib.utils2 import safe_str
52
41
53
42
54 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
55
44
56 __all__ = [
45 __all__ = [
57 'get_new_dir', 'TestController', 'SkipTest',
46 'get_new_dir', 'TestController', 'SkipTest',
58 'url', 'link_to', 'ldap_lib_installed', 'clear_all_caches',
47 'url', 'link_to', 'ldap_lib_installed', 'clear_all_caches',
59 'assert_session_flash', 'login_user', 'no_newline_id_generator',
48 'assert_session_flash', 'login_user', 'no_newline_id_generator',
60 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'SVN_REPO',
49 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'SVN_REPO',
61 'NEW_HG_REPO', 'NEW_GIT_REPO',
50 'NEW_HG_REPO', 'NEW_GIT_REPO',
62 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS',
51 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS',
63 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
52 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
64 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
53 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
65 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO',
54 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO',
66 'TEST_HG_REPO_CLONE', 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO',
55 'TEST_HG_REPO_CLONE', 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO',
67 'TEST_GIT_REPO_CLONE', 'TEST_GIT_REPO_PULL', 'SCM_TESTS',
56 'TEST_GIT_REPO_CLONE', 'TEST_GIT_REPO_PULL', 'SCM_TESTS',
68 ]
57 ]
69
58
70 # Invoke websetup with the current config file
59 # Invoke websetup with the current config file
71 # SetupCommand('setup-app').run([config_file])
60 # SetupCommand('setup-app').run([config_file])
72
61
73 # SOME GLOBALS FOR TESTS
62 # SOME GLOBALS FOR TESTS
74 TEST_DIR = tempfile.gettempdir()
63 TEST_DIR = tempfile.gettempdir()
75
64
76 TESTS_TMP_PATH = jn(TEST_DIR, 'rc_test_%s' % _RandomNameSequence().next())
65 TESTS_TMP_PATH = jn(TEST_DIR, 'rc_test_%s' % _RandomNameSequence().next())
77 TEST_USER_ADMIN_LOGIN = 'test_admin'
66 TEST_USER_ADMIN_LOGIN = 'test_admin'
78 TEST_USER_ADMIN_PASS = 'test12'
67 TEST_USER_ADMIN_PASS = 'test12'
79 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
68 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
80
69
81 TEST_USER_REGULAR_LOGIN = 'test_regular'
70 TEST_USER_REGULAR_LOGIN = 'test_regular'
82 TEST_USER_REGULAR_PASS = 'test12'
71 TEST_USER_REGULAR_PASS = 'test12'
83 TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com'
72 TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com'
84
73
85 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
74 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
86 TEST_USER_REGULAR2_PASS = 'test12'
75 TEST_USER_REGULAR2_PASS = 'test12'
87 TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com'
76 TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com'
88
77
89 HG_REPO = 'vcs_test_hg'
78 HG_REPO = 'vcs_test_hg'
90 GIT_REPO = 'vcs_test_git'
79 GIT_REPO = 'vcs_test_git'
91 SVN_REPO = 'vcs_test_svn'
80 SVN_REPO = 'vcs_test_svn'
92
81
93 NEW_HG_REPO = 'vcs_test_hg_new'
82 NEW_HG_REPO = 'vcs_test_hg_new'
94 NEW_GIT_REPO = 'vcs_test_git_new'
83 NEW_GIT_REPO = 'vcs_test_git_new'
95
84
96 HG_FORK = 'vcs_test_hg_fork'
85 HG_FORK = 'vcs_test_hg_fork'
97 GIT_FORK = 'vcs_test_git_fork'
86 GIT_FORK = 'vcs_test_git_fork'
98
87
99 ## VCS
88 ## VCS
100 SCM_TESTS = ['hg', 'git']
89 SCM_TESTS = ['hg', 'git']
101 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
90 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
102
91
103 TEST_GIT_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
92 TEST_GIT_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
104 TEST_GIT_REPO_CLONE = jn(TESTS_TMP_PATH, 'vcsgitclone%s' % uniq_suffix)
93 TEST_GIT_REPO_CLONE = jn(TESTS_TMP_PATH, 'vcsgitclone%s' % uniq_suffix)
105 TEST_GIT_REPO_PULL = jn(TESTS_TMP_PATH, 'vcsgitpull%s' % uniq_suffix)
94 TEST_GIT_REPO_PULL = jn(TESTS_TMP_PATH, 'vcsgitpull%s' % uniq_suffix)
106
95
107 TEST_HG_REPO = jn(TESTS_TMP_PATH, HG_REPO)
96 TEST_HG_REPO = jn(TESTS_TMP_PATH, HG_REPO)
108 TEST_HG_REPO_CLONE = jn(TESTS_TMP_PATH, 'vcshgclone%s' % uniq_suffix)
97 TEST_HG_REPO_CLONE = jn(TESTS_TMP_PATH, 'vcshgclone%s' % uniq_suffix)
109 TEST_HG_REPO_PULL = jn(TESTS_TMP_PATH, 'vcshgpull%s' % uniq_suffix)
98 TEST_HG_REPO_PULL = jn(TESTS_TMP_PATH, 'vcshgpull%s' % uniq_suffix)
110
99
111 TEST_REPO_PREFIX = 'vcs-test'
100 TEST_REPO_PREFIX = 'vcs-test'
112
101
113
102
114 # skip ldap tests if LDAP lib is not installed
103 # skip ldap tests if LDAP lib is not installed
115 ldap_lib_installed = False
104 ldap_lib_installed = False
116 try:
105 try:
117 import ldap
106 import ldap
118 ldap_lib_installed = True
107 ldap_lib_installed = True
119 except ImportError:
108 except ImportError:
109 ldap = None
120 # means that python-ldap is not installed
110 # means that python-ldap is not installed
121 pass
111 pass
122
112
123
113
124 def clear_all_caches():
114 def clear_all_caches():
125 from beaker.cache import cache_managers
115 from beaker.cache import cache_managers
126 for _cache in cache_managers.values():
116 for _cache in cache_managers.values():
127 _cache.clear()
117 _cache.clear()
128
118
129
119
130 def get_new_dir(title):
120 def get_new_dir(title):
131 """
121 """
132 Returns always new directory path.
122 Returns always new directory path.
133 """
123 """
134 from rhodecode.tests.vcs.utils import get_normalized_path
124 from rhodecode.tests.vcs.utils import get_normalized_path
135 name_parts = [TEST_REPO_PREFIX]
125 name_parts = [TEST_REPO_PREFIX]
136 if title:
126 if title:
137 name_parts.append(title)
127 name_parts.append(title)
138 hex_str = hashlib.sha1('%s %s' % (os.getpid(), time.time())).hexdigest()
128 hex_str = hashlib.sha1('%s %s' % (os.getpid(), time.time())).hexdigest()
139 name_parts.append(hex_str)
129 name_parts.append(hex_str)
140 name = '-'.join(name_parts)
130 name = '-'.join(name_parts)
141 path = os.path.join(TEST_DIR, name)
131 path = os.path.join(TEST_DIR, name)
142 return get_normalized_path(path)
132 return get_normalized_path(path)
143
133
144
134
145 @pytest.mark.usefixtures('app', 'index_location')
135 @pytest.mark.usefixtures('app', 'index_location')
146 class TestController(object):
136 class TestController(object):
147
137
148 maxDiff = None
138 maxDiff = None
149
139
150 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
140 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
151 password=TEST_USER_ADMIN_PASS):
141 password=TEST_USER_ADMIN_PASS):
152 self._logged_username = username
142 self._logged_username = username
153 self._session = login_user_session(self.app, username, password)
143 self._session = login_user_session(self.app, username, password)
154 self.csrf_token = auth.get_csrf_token(self._session)
144 self.csrf_token = auth.get_csrf_token(self._session)
155
145
156 return self._session['rhodecode_user']
146 return self._session['rhodecode_user']
157
147
158 def logout_user(self):
148 def logout_user(self):
159 logout_user_session(self.app, auth.get_csrf_token(self._session))
149 logout_user_session(self.app, auth.get_csrf_token(self._session))
160 self.csrf_token = None
150 self.csrf_token = None
161 self._logged_username = None
151 self._logged_username = None
162 self._session = None
152 self._session = None
163
153
164 def _get_logged_user(self):
154 def _get_logged_user(self):
165 return User.get_by_username(self._logged_username)
155 return User.get_by_username(self._logged_username)
166
156
167
157
168 def login_user_session(
158 def login_user_session(
169 app, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS):
159 app, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS):
170
160
171 response = app.post(
161 response = app.post(
172 h.route_path('login'),
162 h.route_path('login'),
173 {'username': username, 'password': password})
163 {'username': username, 'password': password})
174 if 'invalid user name' in response.body:
164 if 'invalid user name' in response.body:
175 pytest.fail('could not login using %s %s' % (username, password))
165 pytest.fail('could not login using %s %s' % (username, password))
176
166
177 assert response.status == '302 Found'
167 assert response.status == '302 Found'
178 response = response.follow()
168 response = response.follow()
179 assert response.status == '200 OK'
169 assert response.status == '200 OK'
180
170
181 session = response.get_session_from_response()
171 session = response.get_session_from_response()
182 assert 'rhodecode_user' in session
172 assert 'rhodecode_user' in session
183 rc_user = session['rhodecode_user']
173 rc_user = session['rhodecode_user']
184 assert rc_user.get('username') == username
174 assert rc_user.get('username') == username
185 assert rc_user.get('is_authenticated')
175 assert rc_user.get('is_authenticated')
186
176
187 return session
177 return session
188
178
189
179
190 def logout_user_session(app, csrf_token):
180 def logout_user_session(app, csrf_token):
191 app.post(h.route_path('logout'), {'csrf_token': csrf_token}, status=302)
181 app.post(h.route_path('logout'), {'csrf_token': csrf_token}, status=302)
192
182
193
183
194 def login_user(app, username=TEST_USER_ADMIN_LOGIN,
184 def login_user(app, username=TEST_USER_ADMIN_LOGIN,
195 password=TEST_USER_ADMIN_PASS):
185 password=TEST_USER_ADMIN_PASS):
196 return login_user_session(app, username, password)['rhodecode_user']
186 return login_user_session(app, username, password)['rhodecode_user']
197
187
198
188
199 def assert_session_flash(response=None, msg=None, category=None, no_=None):
189 def assert_session_flash(response, msg=None, category=None, no_=None):
200 """
190 """
201 Assert on a flash message in the current session.
191 Assert on a flash message in the current session.
202
192
203 :param msg: Required. The expected message. Will be evaluated if a
193 :param response: Response from give calll, it will contain flash
194 messages or bound session with them.
195 :param msg: The expected message. Will be evaluated if a
204 :class:`LazyString` is passed in.
196 :class:`LazyString` is passed in.
205 :param response: Optional. For functional testing, pass in the response
206 object. Otherwise don't pass in any value.
207 :param category: Optional. If passed, the message category will be
197 :param category: Optional. If passed, the message category will be
208 checked as well.
198 checked as well.
209 :param no_: Optional. If passed, the message will be checked to NOT be in the
199 :param no_: Optional. If passed, the message will be checked to NOT
210 flash session
200 be in the flash session
211 """
201 """
212 if msg is None and no_ is None:
202 if msg is None and no_ is None:
213 raise ValueError("Parameter msg or no_ is required.")
203 raise ValueError("Parameter msg or no_ is required.")
214
204
215 if msg and no_:
205 if msg and no_:
216 raise ValueError("Please specify either msg or no_, but not both")
206 raise ValueError("Please specify either msg or no_, but not both")
217
207
218 messages = flash.pop_messages()
208 session = response.get_session_from_response()
209 messages = flash.pop_messages(session=session)
219 msg = _eval_if_lazy(msg)
210 msg = _eval_if_lazy(msg)
220
211
221 assert messages, 'unable to find message `%s` in empty flash list' % msg
212 assert messages, 'unable to find message `%s` in empty flash list' % msg
222 message = messages[0]
213 message = messages[0]
223
214
224 message_text = _eval_if_lazy(message.message) or ''
215 message_text = _eval_if_lazy(message.message) or ''
225
216
226 if no_:
217 if no_:
227 if no_ in message_text:
218 if no_ in message_text:
228 msg = u'msg `%s` found in session flash.' % (no_,)
219 msg = u'msg `%s` found in session flash.' % (no_,)
229 pytest.fail(safe_str(msg))
220 pytest.fail(safe_str(msg))
230 else:
221 else:
231 if msg not in message_text:
222 if msg not in message_text:
232 fail_msg = u'msg `%s` not found in session ' \
223 fail_msg = u'msg `%s` not found in session ' \
233 u'flash: got `%s` (type:%s) instead' % (
224 u'flash: got `%s` (type:%s) instead' % (
234 msg, message_text, type(message_text))
225 msg, message_text, type(message_text))
235
226
236 pytest.fail(safe_str(fail_msg))
227 pytest.fail(safe_str(fail_msg))
237 if category:
228 if category:
238 assert category == message.category
229 assert category == message.category
239
230
240
231
241 def _eval_if_lazy(value):
232 def _eval_if_lazy(value):
242 return value.eval() if hasattr(value, 'eval') else value
233 return value.eval() if hasattr(value, 'eval') else value
243
234
244
235
245 def assert_session_flash_is_empty(response):
246 assert 'flash' in response.session, 'Response session has no flash key'
247
248 msg = 'flash messages are present in session:%s' % \
249 response.session['flash'][0]
250 pytest.fail(safe_str(msg))
251
252
253 def no_newline_id_generator(test_name):
236 def no_newline_id_generator(test_name):
254 """
237 """
255 Generates a test name without spaces or newlines characters. Used for
238 Generates a test name without spaces or newlines characters. Used for
256 nicer output of progress of test
239 nicer output of progress of test
257 """
240 """
258 org_name = test_name
241 org_name = test_name
259 test_name = test_name\
242 test_name = test_name\
260 .replace('\n', '_N') \
243 .replace('\n', '_N') \
261 .replace('\r', '_N') \
244 .replace('\r', '_N') \
262 .replace('\t', '_T') \
245 .replace('\t', '_T') \
263 .replace(' ', '_S')
246 .replace(' ', '_S')
264
247
265 return test_name or 'test-with-empty-name'
248 return test_name or 'test-with-empty-name'
@@ -1,1832 +1,1832 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 import collections
21 import collections
22 import datetime
22 import datetime
23 import hashlib
23 import hashlib
24 import os
24 import os
25 import re
25 import re
26 import pprint
26 import pprint
27 import shutil
27 import shutil
28 import socket
28 import socket
29 import subprocess32
29 import subprocess32
30 import time
30 import time
31 import uuid
31 import uuid
32 import dateutil.tz
32 import dateutil.tz
33
33
34 import mock
34 import mock
35 import pyramid.testing
35 import pyramid.testing
36 import pytest
36 import pytest
37 import colander
37 import colander
38 import requests
38 import requests
39
39
40 import rhodecode
40 import rhodecode
41 from rhodecode.lib.utils2 import AttributeDict
41 from rhodecode.lib.utils2 import AttributeDict
42 from rhodecode.model.changeset_status import ChangesetStatusModel
42 from rhodecode.model.changeset_status import ChangesetStatusModel
43 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.comment import CommentsModel
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
45 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
46 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
46 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.repo_group import RepoGroupModel
51 from rhodecode.model.user import UserModel
51 from rhodecode.model.user import UserModel
52 from rhodecode.model.settings import VcsSettingsModel
52 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.user_group import UserGroupModel
53 from rhodecode.model.user_group import UserGroupModel
54 from rhodecode.model.integration import IntegrationModel
54 from rhodecode.model.integration import IntegrationModel
55 from rhodecode.integrations import integration_type_registry
55 from rhodecode.integrations import integration_type_registry
56 from rhodecode.integrations.types.base import IntegrationTypeBase
56 from rhodecode.integrations.types.base import IntegrationTypeBase
57 from rhodecode.lib.utils import repo2db_mapper
57 from rhodecode.lib.utils import repo2db_mapper
58 from rhodecode.lib.vcs import create_vcsserver_proxy
58 from rhodecode.lib.vcs import create_vcsserver_proxy
59 from rhodecode.lib.vcs.backends import get_backend
59 from rhodecode.lib.vcs.backends import get_backend
60 from rhodecode.lib.vcs.nodes import FileNode
60 from rhodecode.lib.vcs.nodes import FileNode
61 from rhodecode.tests import (
61 from rhodecode.tests import (
62 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
62 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
63 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
63 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
64 TEST_USER_REGULAR_PASS)
64 TEST_USER_REGULAR_PASS)
65 from rhodecode.tests.utils import CustomTestApp, set_anonymous_access, add_test_routes
65 from rhodecode.tests.utils import CustomTestApp, set_anonymous_access, add_test_routes
66 from rhodecode.tests.fixture import Fixture
66 from rhodecode.tests.fixture import Fixture
67
67
68
68
69 def _split_comma(value):
69 def _split_comma(value):
70 return value.split(',')
70 return value.split(',')
71
71
72
72
73 def pytest_addoption(parser):
73 def pytest_addoption(parser):
74 parser.addoption(
74 parser.addoption(
75 '--keep-tmp-path', action='store_true',
75 '--keep-tmp-path', action='store_true',
76 help="Keep the test temporary directories")
76 help="Keep the test temporary directories")
77 parser.addoption(
77 parser.addoption(
78 '--backends', action='store', type=_split_comma,
78 '--backends', action='store', type=_split_comma,
79 default=['git', 'hg', 'svn'],
79 default=['git', 'hg', 'svn'],
80 help="Select which backends to test for backend specific tests.")
80 help="Select which backends to test for backend specific tests.")
81 parser.addoption(
81 parser.addoption(
82 '--dbs', action='store', type=_split_comma,
82 '--dbs', action='store', type=_split_comma,
83 default=['sqlite'],
83 default=['sqlite'],
84 help="Select which database to test for database specific tests. "
84 help="Select which database to test for database specific tests. "
85 "Possible options are sqlite,postgres,mysql")
85 "Possible options are sqlite,postgres,mysql")
86 parser.addoption(
86 parser.addoption(
87 '--appenlight', '--ae', action='store_true',
87 '--appenlight', '--ae', action='store_true',
88 help="Track statistics in appenlight.")
88 help="Track statistics in appenlight.")
89 parser.addoption(
89 parser.addoption(
90 '--appenlight-api-key', '--ae-key',
90 '--appenlight-api-key', '--ae-key',
91 help="API key for Appenlight.")
91 help="API key for Appenlight.")
92 parser.addoption(
92 parser.addoption(
93 '--appenlight-url', '--ae-url',
93 '--appenlight-url', '--ae-url',
94 default="https://ae.rhodecode.com",
94 default="https://ae.rhodecode.com",
95 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
95 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
96 parser.addoption(
96 parser.addoption(
97 '--sqlite-connection-string', action='store',
97 '--sqlite-connection-string', action='store',
98 default='', help="Connection string for the dbs tests with SQLite")
98 default='', help="Connection string for the dbs tests with SQLite")
99 parser.addoption(
99 parser.addoption(
100 '--postgres-connection-string', action='store',
100 '--postgres-connection-string', action='store',
101 default='', help="Connection string for the dbs tests with Postgres")
101 default='', help="Connection string for the dbs tests with Postgres")
102 parser.addoption(
102 parser.addoption(
103 '--mysql-connection-string', action='store',
103 '--mysql-connection-string', action='store',
104 default='', help="Connection string for the dbs tests with MySQL")
104 default='', help="Connection string for the dbs tests with MySQL")
105 parser.addoption(
105 parser.addoption(
106 '--repeat', type=int, default=100,
106 '--repeat', type=int, default=100,
107 help="Number of repetitions in performance tests.")
107 help="Number of repetitions in performance tests.")
108
108
109
109
110 def pytest_configure(config):
110 def pytest_configure(config):
111 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
111 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
112 from rhodecode.config import patches
112 from rhodecode.config import patches
113 patches.kombu_1_5_1_python_2_7_11()
113 patches.kombu_1_5_1_python_2_7_11()
114
114
115
115
116 def pytest_collection_modifyitems(session, config, items):
116 def pytest_collection_modifyitems(session, config, items):
117 # nottest marked, compare nose, used for transition from nose to pytest
117 # nottest marked, compare nose, used for transition from nose to pytest
118 remaining = [
118 remaining = [
119 i for i in items if getattr(i.obj, '__test__', True)]
119 i for i in items if getattr(i.obj, '__test__', True)]
120 items[:] = remaining
120 items[:] = remaining
121
121
122
122
123 def pytest_generate_tests(metafunc):
123 def pytest_generate_tests(metafunc):
124 # Support test generation based on --backend parameter
124 # Support test generation based on --backend parameter
125 if 'backend_alias' in metafunc.fixturenames:
125 if 'backend_alias' in metafunc.fixturenames:
126 backends = get_backends_from_metafunc(metafunc)
126 backends = get_backends_from_metafunc(metafunc)
127 scope = None
127 scope = None
128 if not backends:
128 if not backends:
129 pytest.skip("Not enabled for any of selected backends")
129 pytest.skip("Not enabled for any of selected backends")
130 metafunc.parametrize('backend_alias', backends, scope=scope)
130 metafunc.parametrize('backend_alias', backends, scope=scope)
131 elif hasattr(metafunc.function, 'backends'):
131 elif hasattr(metafunc.function, 'backends'):
132 backends = get_backends_from_metafunc(metafunc)
132 backends = get_backends_from_metafunc(metafunc)
133 if not backends:
133 if not backends:
134 pytest.skip("Not enabled for any of selected backends")
134 pytest.skip("Not enabled for any of selected backends")
135
135
136
136
137 def get_backends_from_metafunc(metafunc):
137 def get_backends_from_metafunc(metafunc):
138 requested_backends = set(metafunc.config.getoption('--backends'))
138 requested_backends = set(metafunc.config.getoption('--backends'))
139 if hasattr(metafunc.function, 'backends'):
139 if hasattr(metafunc.function, 'backends'):
140 # Supported backends by this test function, created from
140 # Supported backends by this test function, created from
141 # pytest.mark.backends
141 # pytest.mark.backends
142 backends = metafunc.function.backends.args
142 backends = metafunc.function.backends.args
143 elif hasattr(metafunc.cls, 'backend_alias'):
143 elif hasattr(metafunc.cls, 'backend_alias'):
144 # Support class attribute "backend_alias", this is mainly
144 # Support class attribute "backend_alias", this is mainly
145 # for legacy reasons for tests not yet using pytest.mark.backends
145 # for legacy reasons for tests not yet using pytest.mark.backends
146 backends = [metafunc.cls.backend_alias]
146 backends = [metafunc.cls.backend_alias]
147 else:
147 else:
148 backends = metafunc.config.getoption('--backends')
148 backends = metafunc.config.getoption('--backends')
149 return requested_backends.intersection(backends)
149 return requested_backends.intersection(backends)
150
150
151
151
152 @pytest.fixture(scope='session', autouse=True)
152 @pytest.fixture(scope='session', autouse=True)
153 def activate_example_rcextensions(request):
153 def activate_example_rcextensions(request):
154 """
154 """
155 Patch in an example rcextensions module which verifies passed in kwargs.
155 Patch in an example rcextensions module which verifies passed in kwargs.
156 """
156 """
157 from rhodecode.tests.other import example_rcextensions
157 from rhodecode.tests.other import example_rcextensions
158
158
159 old_extensions = rhodecode.EXTENSIONS
159 old_extensions = rhodecode.EXTENSIONS
160 rhodecode.EXTENSIONS = example_rcextensions
160 rhodecode.EXTENSIONS = example_rcextensions
161
161
162 @request.addfinalizer
162 @request.addfinalizer
163 def cleanup():
163 def cleanup():
164 rhodecode.EXTENSIONS = old_extensions
164 rhodecode.EXTENSIONS = old_extensions
165
165
166
166
167 @pytest.fixture
167 @pytest.fixture
168 def capture_rcextensions():
168 def capture_rcextensions():
169 """
169 """
170 Returns the recorded calls to entry points in rcextensions.
170 Returns the recorded calls to entry points in rcextensions.
171 """
171 """
172 calls = rhodecode.EXTENSIONS.calls
172 calls = rhodecode.EXTENSIONS.calls
173 calls.clear()
173 calls.clear()
174 # Note: At this moment, it is still the empty dict, but that will
174 # Note: At this moment, it is still the empty dict, but that will
175 # be filled during the test run and since it is a reference this
175 # be filled during the test run and since it is a reference this
176 # is enough to make it work.
176 # is enough to make it work.
177 return calls
177 return calls
178
178
179
179
180 @pytest.fixture(scope='session')
180 @pytest.fixture(scope='session')
181 def http_environ_session():
181 def http_environ_session():
182 """
182 """
183 Allow to use "http_environ" in session scope.
183 Allow to use "http_environ" in session scope.
184 """
184 """
185 return http_environ(
185 return http_environ(
186 http_host_stub=http_host_stub())
186 http_host_stub=http_host_stub())
187
187
188
188
189 @pytest.fixture
189 @pytest.fixture
190 def http_host_stub():
190 def http_host_stub():
191 """
191 """
192 Value of HTTP_HOST in the test run.
192 Value of HTTP_HOST in the test run.
193 """
193 """
194 return 'example.com:80'
194 return 'example.com:80'
195
195
196
196
197 @pytest.fixture
197 @pytest.fixture
198 def http_host_only_stub():
198 def http_host_only_stub():
199 """
199 """
200 Value of HTTP_HOST in the test run.
200 Value of HTTP_HOST in the test run.
201 """
201 """
202 return http_host_stub().split(':')[0]
202 return http_host_stub().split(':')[0]
203
203
204
204
205 @pytest.fixture
205 @pytest.fixture
206 def http_environ(http_host_stub):
206 def http_environ(http_host_stub):
207 """
207 """
208 HTTP extra environ keys.
208 HTTP extra environ keys.
209
209
210 User by the test application and as well for setting up the pylons
210 User by the test application and as well for setting up the pylons
211 environment. In the case of the fixture "app" it should be possible
211 environment. In the case of the fixture "app" it should be possible
212 to override this for a specific test case.
212 to override this for a specific test case.
213 """
213 """
214 return {
214 return {
215 'SERVER_NAME': http_host_only_stub(),
215 'SERVER_NAME': http_host_only_stub(),
216 'SERVER_PORT': http_host_stub.split(':')[1],
216 'SERVER_PORT': http_host_stub.split(':')[1],
217 'HTTP_HOST': http_host_stub,
217 'HTTP_HOST': http_host_stub,
218 'HTTP_USER_AGENT': 'rc-test-agent',
218 'HTTP_USER_AGENT': 'rc-test-agent',
219 'REQUEST_METHOD': 'GET'
219 'REQUEST_METHOD': 'GET'
220 }
220 }
221
221
222
222
223 @pytest.fixture(scope='function')
223 @pytest.fixture(scope='function')
224 def app(request, config_stub, pylonsapp, http_environ):
224 def app(request, config_stub, pylonsapp, http_environ):
225 app = CustomTestApp(
225 app = CustomTestApp(
226 pylonsapp,
226 pylonsapp,
227 extra_environ=http_environ)
227 extra_environ=http_environ)
228 if request.cls:
228 if request.cls:
229 request.cls.app = app
229 request.cls.app = app
230 return app
230 return app
231
231
232
232
233 @pytest.fixture(scope='session')
233 @pytest.fixture(scope='session')
234 def app_settings(pylonsapp, pylons_config):
234 def app_settings(pylonsapp, pylons_config):
235 """
235 """
236 Settings dictionary used to create the app.
236 Settings dictionary used to create the app.
237
237
238 Parses the ini file and passes the result through the sanitize and apply
238 Parses the ini file and passes the result through the sanitize and apply
239 defaults mechanism in `rhodecode.config.middleware`.
239 defaults mechanism in `rhodecode.config.middleware`.
240 """
240 """
241 from paste.deploy.loadwsgi import loadcontext, APP
241 from paste.deploy.loadwsgi import loadcontext, APP
242 from rhodecode.config.middleware import (
242 from rhodecode.config.middleware import (
243 sanitize_settings_and_apply_defaults)
243 sanitize_settings_and_apply_defaults)
244 context = loadcontext(APP, 'config:' + pylons_config)
244 context = loadcontext(APP, 'config:' + pylons_config)
245 settings = sanitize_settings_and_apply_defaults(context.config())
245 settings = sanitize_settings_and_apply_defaults(context.config())
246 return settings
246 return settings
247
247
248
248
249 @pytest.fixture(scope='session')
249 @pytest.fixture(scope='session')
250 def db(app_settings):
250 def db(app_settings):
251 """
251 """
252 Initializes the database connection.
252 Initializes the database connection.
253
253
254 It uses the same settings which are used to create the ``pylonsapp`` or
254 It uses the same settings which are used to create the ``pylonsapp`` or
255 ``app`` fixtures.
255 ``app`` fixtures.
256 """
256 """
257 from rhodecode.config.utils import initialize_database
257 from rhodecode.config.utils import initialize_database
258 initialize_database(app_settings)
258 initialize_database(app_settings)
259
259
260
260
261 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
261 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
262
262
263
263
264 def _autologin_user(app, *args):
264 def _autologin_user(app, *args):
265 session = login_user_session(app, *args)
265 session = login_user_session(app, *args)
266 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
266 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
267 return LoginData(csrf_token, session['rhodecode_user'])
267 return LoginData(csrf_token, session['rhodecode_user'])
268
268
269
269
270 @pytest.fixture
270 @pytest.fixture
271 def autologin_user(app):
271 def autologin_user(app):
272 """
272 """
273 Utility fixture which makes sure that the admin user is logged in
273 Utility fixture which makes sure that the admin user is logged in
274 """
274 """
275 return _autologin_user(app)
275 return _autologin_user(app)
276
276
277
277
278 @pytest.fixture
278 @pytest.fixture
279 def autologin_regular_user(app):
279 def autologin_regular_user(app):
280 """
280 """
281 Utility fixture which makes sure that the regular user is logged in
281 Utility fixture which makes sure that the regular user is logged in
282 """
282 """
283 return _autologin_user(
283 return _autologin_user(
284 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
284 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
285
285
286
286
287 @pytest.fixture(scope='function')
287 @pytest.fixture(scope='function')
288 def csrf_token(request, autologin_user):
288 def csrf_token(request, autologin_user):
289 return autologin_user.csrf_token
289 return autologin_user.csrf_token
290
290
291
291
292 @pytest.fixture(scope='function')
292 @pytest.fixture(scope='function')
293 def xhr_header(request):
293 def xhr_header(request):
294 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
294 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
295
295
296
296
297 @pytest.fixture
297 @pytest.fixture
298 def real_crypto_backend(monkeypatch):
298 def real_crypto_backend(monkeypatch):
299 """
299 """
300 Switch the production crypto backend on for this test.
300 Switch the production crypto backend on for this test.
301
301
302 During the test run the crypto backend is replaced with a faster
302 During the test run the crypto backend is replaced with a faster
303 implementation based on the MD5 algorithm.
303 implementation based on the MD5 algorithm.
304 """
304 """
305 monkeypatch.setattr(rhodecode, 'is_test', False)
305 monkeypatch.setattr(rhodecode, 'is_test', False)
306
306
307
307
308 @pytest.fixture(scope='class')
308 @pytest.fixture(scope='class')
309 def index_location(request, pylonsapp):
309 def index_location(request, pylonsapp):
310 index_location = pylonsapp.config['app_conf']['search.location']
310 index_location = pylonsapp.config['app_conf']['search.location']
311 if request.cls:
311 if request.cls:
312 request.cls.index_location = index_location
312 request.cls.index_location = index_location
313 return index_location
313 return index_location
314
314
315
315
316 @pytest.fixture(scope='session', autouse=True)
316 @pytest.fixture(scope='session', autouse=True)
317 def tests_tmp_path(request):
317 def tests_tmp_path(request):
318 """
318 """
319 Create temporary directory to be used during the test session.
319 Create temporary directory to be used during the test session.
320 """
320 """
321 if not os.path.exists(TESTS_TMP_PATH):
321 if not os.path.exists(TESTS_TMP_PATH):
322 os.makedirs(TESTS_TMP_PATH)
322 os.makedirs(TESTS_TMP_PATH)
323
323
324 if not request.config.getoption('--keep-tmp-path'):
324 if not request.config.getoption('--keep-tmp-path'):
325 @request.addfinalizer
325 @request.addfinalizer
326 def remove_tmp_path():
326 def remove_tmp_path():
327 shutil.rmtree(TESTS_TMP_PATH)
327 shutil.rmtree(TESTS_TMP_PATH)
328
328
329 return TESTS_TMP_PATH
329 return TESTS_TMP_PATH
330
330
331
331
332 @pytest.fixture
332 @pytest.fixture
333 def test_repo_group(request):
333 def test_repo_group(request):
334 """
334 """
335 Create a temporary repository group, and destroy it after
335 Create a temporary repository group, and destroy it after
336 usage automatically
336 usage automatically
337 """
337 """
338 fixture = Fixture()
338 fixture = Fixture()
339 repogroupid = 'test_repo_group_%s' % str(time.time()).replace('.', '')
339 repogroupid = 'test_repo_group_%s' % str(time.time()).replace('.', '')
340 repo_group = fixture.create_repo_group(repogroupid)
340 repo_group = fixture.create_repo_group(repogroupid)
341
341
342 def _cleanup():
342 def _cleanup():
343 fixture.destroy_repo_group(repogroupid)
343 fixture.destroy_repo_group(repogroupid)
344
344
345 request.addfinalizer(_cleanup)
345 request.addfinalizer(_cleanup)
346 return repo_group
346 return repo_group
347
347
348
348
349 @pytest.fixture
349 @pytest.fixture
350 def test_user_group(request):
350 def test_user_group(request):
351 """
351 """
352 Create a temporary user group, and destroy it after
352 Create a temporary user group, and destroy it after
353 usage automatically
353 usage automatically
354 """
354 """
355 fixture = Fixture()
355 fixture = Fixture()
356 usergroupid = 'test_user_group_%s' % str(time.time()).replace('.', '')
356 usergroupid = 'test_user_group_%s' % str(time.time()).replace('.', '')
357 user_group = fixture.create_user_group(usergroupid)
357 user_group = fixture.create_user_group(usergroupid)
358
358
359 def _cleanup():
359 def _cleanup():
360 fixture.destroy_user_group(user_group)
360 fixture.destroy_user_group(user_group)
361
361
362 request.addfinalizer(_cleanup)
362 request.addfinalizer(_cleanup)
363 return user_group
363 return user_group
364
364
365
365
366 @pytest.fixture(scope='session')
366 @pytest.fixture(scope='session')
367 def test_repo(request):
367 def test_repo(request):
368 container = TestRepoContainer()
368 container = TestRepoContainer()
369 request.addfinalizer(container._cleanup)
369 request.addfinalizer(container._cleanup)
370 return container
370 return container
371
371
372
372
373 class TestRepoContainer(object):
373 class TestRepoContainer(object):
374 """
374 """
375 Container for test repositories which are used read only.
375 Container for test repositories which are used read only.
376
376
377 Repositories will be created on demand and re-used during the lifetime
377 Repositories will be created on demand and re-used during the lifetime
378 of this object.
378 of this object.
379
379
380 Usage to get the svn test repository "minimal"::
380 Usage to get the svn test repository "minimal"::
381
381
382 test_repo = TestContainer()
382 test_repo = TestContainer()
383 repo = test_repo('minimal', 'svn')
383 repo = test_repo('minimal', 'svn')
384
384
385 """
385 """
386
386
387 dump_extractors = {
387 dump_extractors = {
388 'git': utils.extract_git_repo_from_dump,
388 'git': utils.extract_git_repo_from_dump,
389 'hg': utils.extract_hg_repo_from_dump,
389 'hg': utils.extract_hg_repo_from_dump,
390 'svn': utils.extract_svn_repo_from_dump,
390 'svn': utils.extract_svn_repo_from_dump,
391 }
391 }
392
392
393 def __init__(self):
393 def __init__(self):
394 self._cleanup_repos = []
394 self._cleanup_repos = []
395 self._fixture = Fixture()
395 self._fixture = Fixture()
396 self._repos = {}
396 self._repos = {}
397
397
398 def __call__(self, dump_name, backend_alias, config=None):
398 def __call__(self, dump_name, backend_alias, config=None):
399 key = (dump_name, backend_alias)
399 key = (dump_name, backend_alias)
400 if key not in self._repos:
400 if key not in self._repos:
401 repo = self._create_repo(dump_name, backend_alias, config)
401 repo = self._create_repo(dump_name, backend_alias, config)
402 self._repos[key] = repo.repo_id
402 self._repos[key] = repo.repo_id
403 return Repository.get(self._repos[key])
403 return Repository.get(self._repos[key])
404
404
405 def _create_repo(self, dump_name, backend_alias, config):
405 def _create_repo(self, dump_name, backend_alias, config):
406 repo_name = '%s-%s' % (backend_alias, dump_name)
406 repo_name = '%s-%s' % (backend_alias, dump_name)
407 backend_class = get_backend(backend_alias)
407 backend_class = get_backend(backend_alias)
408 dump_extractor = self.dump_extractors[backend_alias]
408 dump_extractor = self.dump_extractors[backend_alias]
409 repo_path = dump_extractor(dump_name, repo_name)
409 repo_path = dump_extractor(dump_name, repo_name)
410
410
411 vcs_repo = backend_class(repo_path, config=config)
411 vcs_repo = backend_class(repo_path, config=config)
412 repo2db_mapper({repo_name: vcs_repo})
412 repo2db_mapper({repo_name: vcs_repo})
413
413
414 repo = RepoModel().get_by_repo_name(repo_name)
414 repo = RepoModel().get_by_repo_name(repo_name)
415 self._cleanup_repos.append(repo_name)
415 self._cleanup_repos.append(repo_name)
416 return repo
416 return repo
417
417
418 def _cleanup(self):
418 def _cleanup(self):
419 for repo_name in reversed(self._cleanup_repos):
419 for repo_name in reversed(self._cleanup_repos):
420 self._fixture.destroy_repo(repo_name)
420 self._fixture.destroy_repo(repo_name)
421
421
422
422
423 @pytest.fixture
423 @pytest.fixture
424 def backend(request, backend_alias, pylonsapp, test_repo):
424 def backend(request, backend_alias, pylonsapp, test_repo):
425 """
425 """
426 Parametrized fixture which represents a single backend implementation.
426 Parametrized fixture which represents a single backend implementation.
427
427
428 It respects the option `--backends` to focus the test run on specific
428 It respects the option `--backends` to focus the test run on specific
429 backend implementations.
429 backend implementations.
430
430
431 It also supports `pytest.mark.xfail_backends` to mark tests as failing
431 It also supports `pytest.mark.xfail_backends` to mark tests as failing
432 for specific backends. This is intended as a utility for incremental
432 for specific backends. This is intended as a utility for incremental
433 development of a new backend implementation.
433 development of a new backend implementation.
434 """
434 """
435 if backend_alias not in request.config.getoption('--backends'):
435 if backend_alias not in request.config.getoption('--backends'):
436 pytest.skip("Backend %s not selected." % (backend_alias, ))
436 pytest.skip("Backend %s not selected." % (backend_alias, ))
437
437
438 utils.check_xfail_backends(request.node, backend_alias)
438 utils.check_xfail_backends(request.node, backend_alias)
439 utils.check_skip_backends(request.node, backend_alias)
439 utils.check_skip_backends(request.node, backend_alias)
440
440
441 repo_name = 'vcs_test_%s' % (backend_alias, )
441 repo_name = 'vcs_test_%s' % (backend_alias, )
442 backend = Backend(
442 backend = Backend(
443 alias=backend_alias,
443 alias=backend_alias,
444 repo_name=repo_name,
444 repo_name=repo_name,
445 test_name=request.node.name,
445 test_name=request.node.name,
446 test_repo_container=test_repo)
446 test_repo_container=test_repo)
447 request.addfinalizer(backend.cleanup)
447 request.addfinalizer(backend.cleanup)
448 return backend
448 return backend
449
449
450
450
451 @pytest.fixture
451 @pytest.fixture
452 def backend_git(request, pylonsapp, test_repo):
452 def backend_git(request, pylonsapp, test_repo):
453 return backend(request, 'git', pylonsapp, test_repo)
453 return backend(request, 'git', pylonsapp, test_repo)
454
454
455
455
456 @pytest.fixture
456 @pytest.fixture
457 def backend_hg(request, pylonsapp, test_repo):
457 def backend_hg(request, pylonsapp, test_repo):
458 return backend(request, 'hg', pylonsapp, test_repo)
458 return backend(request, 'hg', pylonsapp, test_repo)
459
459
460
460
461 @pytest.fixture
461 @pytest.fixture
462 def backend_svn(request, pylonsapp, test_repo):
462 def backend_svn(request, pylonsapp, test_repo):
463 return backend(request, 'svn', pylonsapp, test_repo)
463 return backend(request, 'svn', pylonsapp, test_repo)
464
464
465
465
466 @pytest.fixture
466 @pytest.fixture
467 def backend_random(backend_git):
467 def backend_random(backend_git):
468 """
468 """
469 Use this to express that your tests need "a backend.
469 Use this to express that your tests need "a backend.
470
470
471 A few of our tests need a backend, so that we can run the code. This
471 A few of our tests need a backend, so that we can run the code. This
472 fixture is intended to be used for such cases. It will pick one of the
472 fixture is intended to be used for such cases. It will pick one of the
473 backends and run the tests.
473 backends and run the tests.
474
474
475 The fixture `backend` would run the test multiple times for each
475 The fixture `backend` would run the test multiple times for each
476 available backend which is a pure waste of time if the test is
476 available backend which is a pure waste of time if the test is
477 independent of the backend type.
477 independent of the backend type.
478 """
478 """
479 # TODO: johbo: Change this to pick a random backend
479 # TODO: johbo: Change this to pick a random backend
480 return backend_git
480 return backend_git
481
481
482
482
483 @pytest.fixture
483 @pytest.fixture
484 def backend_stub(backend_git):
484 def backend_stub(backend_git):
485 """
485 """
486 Use this to express that your tests need a backend stub
486 Use this to express that your tests need a backend stub
487
487
488 TODO: mikhail: Implement a real stub logic instead of returning
488 TODO: mikhail: Implement a real stub logic instead of returning
489 a git backend
489 a git backend
490 """
490 """
491 return backend_git
491 return backend_git
492
492
493
493
494 @pytest.fixture
494 @pytest.fixture
495 def repo_stub(backend_stub):
495 def repo_stub(backend_stub):
496 """
496 """
497 Use this to express that your tests need a repository stub
497 Use this to express that your tests need a repository stub
498 """
498 """
499 return backend_stub.create_repo()
499 return backend_stub.create_repo()
500
500
501
501
502 class Backend(object):
502 class Backend(object):
503 """
503 """
504 Represents the test configuration for one supported backend
504 Represents the test configuration for one supported backend
505
505
506 Provides easy access to different test repositories based on
506 Provides easy access to different test repositories based on
507 `__getitem__`. Such repositories will only be created once per test
507 `__getitem__`. Such repositories will only be created once per test
508 session.
508 session.
509 """
509 """
510
510
511 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
511 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
512 _master_repo = None
512 _master_repo = None
513 _commit_ids = {}
513 _commit_ids = {}
514
514
515 def __init__(self, alias, repo_name, test_name, test_repo_container):
515 def __init__(self, alias, repo_name, test_name, test_repo_container):
516 self.alias = alias
516 self.alias = alias
517 self.repo_name = repo_name
517 self.repo_name = repo_name
518 self._cleanup_repos = []
518 self._cleanup_repos = []
519 self._test_name = test_name
519 self._test_name = test_name
520 self._test_repo_container = test_repo_container
520 self._test_repo_container = test_repo_container
521 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
521 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
522 # Fixture will survive in the end.
522 # Fixture will survive in the end.
523 self._fixture = Fixture()
523 self._fixture = Fixture()
524
524
525 def __getitem__(self, key):
525 def __getitem__(self, key):
526 return self._test_repo_container(key, self.alias)
526 return self._test_repo_container(key, self.alias)
527
527
528 def create_test_repo(self, key, config=None):
528 def create_test_repo(self, key, config=None):
529 return self._test_repo_container(key, self.alias, config)
529 return self._test_repo_container(key, self.alias, config)
530
530
531 @property
531 @property
532 def repo(self):
532 def repo(self):
533 """
533 """
534 Returns the "current" repository. This is the vcs_test repo or the
534 Returns the "current" repository. This is the vcs_test repo or the
535 last repo which has been created with `create_repo`.
535 last repo which has been created with `create_repo`.
536 """
536 """
537 from rhodecode.model.db import Repository
537 from rhodecode.model.db import Repository
538 return Repository.get_by_repo_name(self.repo_name)
538 return Repository.get_by_repo_name(self.repo_name)
539
539
540 @property
540 @property
541 def default_branch_name(self):
541 def default_branch_name(self):
542 VcsRepository = get_backend(self.alias)
542 VcsRepository = get_backend(self.alias)
543 return VcsRepository.DEFAULT_BRANCH_NAME
543 return VcsRepository.DEFAULT_BRANCH_NAME
544
544
545 @property
545 @property
546 def default_head_id(self):
546 def default_head_id(self):
547 """
547 """
548 Returns the default head id of the underlying backend.
548 Returns the default head id of the underlying backend.
549
549
550 This will be the default branch name in case the backend does have a
550 This will be the default branch name in case the backend does have a
551 default branch. In the other cases it will point to a valid head
551 default branch. In the other cases it will point to a valid head
552 which can serve as the base to create a new commit on top of it.
552 which can serve as the base to create a new commit on top of it.
553 """
553 """
554 vcsrepo = self.repo.scm_instance()
554 vcsrepo = self.repo.scm_instance()
555 head_id = (
555 head_id = (
556 vcsrepo.DEFAULT_BRANCH_NAME or
556 vcsrepo.DEFAULT_BRANCH_NAME or
557 vcsrepo.commit_ids[-1])
557 vcsrepo.commit_ids[-1])
558 return head_id
558 return head_id
559
559
560 @property
560 @property
561 def commit_ids(self):
561 def commit_ids(self):
562 """
562 """
563 Returns the list of commits for the last created repository
563 Returns the list of commits for the last created repository
564 """
564 """
565 return self._commit_ids
565 return self._commit_ids
566
566
567 def create_master_repo(self, commits):
567 def create_master_repo(self, commits):
568 """
568 """
569 Create a repository and remember it as a template.
569 Create a repository and remember it as a template.
570
570
571 This allows to easily create derived repositories to construct
571 This allows to easily create derived repositories to construct
572 more complex scenarios for diff, compare and pull requests.
572 more complex scenarios for diff, compare and pull requests.
573
573
574 Returns a commit map which maps from commit message to raw_id.
574 Returns a commit map which maps from commit message to raw_id.
575 """
575 """
576 self._master_repo = self.create_repo(commits=commits)
576 self._master_repo = self.create_repo(commits=commits)
577 return self._commit_ids
577 return self._commit_ids
578
578
579 def create_repo(
579 def create_repo(
580 self, commits=None, number_of_commits=0, heads=None,
580 self, commits=None, number_of_commits=0, heads=None,
581 name_suffix=u'', **kwargs):
581 name_suffix=u'', **kwargs):
582 """
582 """
583 Create a repository and record it for later cleanup.
583 Create a repository and record it for later cleanup.
584
584
585 :param commits: Optional. A sequence of dict instances.
585 :param commits: Optional. A sequence of dict instances.
586 Will add a commit per entry to the new repository.
586 Will add a commit per entry to the new repository.
587 :param number_of_commits: Optional. If set to a number, this number of
587 :param number_of_commits: Optional. If set to a number, this number of
588 commits will be added to the new repository.
588 commits will be added to the new repository.
589 :param heads: Optional. Can be set to a sequence of of commit
589 :param heads: Optional. Can be set to a sequence of of commit
590 names which shall be pulled in from the master repository.
590 names which shall be pulled in from the master repository.
591
591
592 """
592 """
593 self.repo_name = self._next_repo_name() + name_suffix
593 self.repo_name = self._next_repo_name() + name_suffix
594 repo = self._fixture.create_repo(
594 repo = self._fixture.create_repo(
595 self.repo_name, repo_type=self.alias, **kwargs)
595 self.repo_name, repo_type=self.alias, **kwargs)
596 self._cleanup_repos.append(repo.repo_name)
596 self._cleanup_repos.append(repo.repo_name)
597
597
598 commits = commits or [
598 commits = commits or [
599 {'message': 'Commit %s of %s' % (x, self.repo_name)}
599 {'message': 'Commit %s of %s' % (x, self.repo_name)}
600 for x in xrange(number_of_commits)]
600 for x in xrange(number_of_commits)]
601 self._add_commits_to_repo(repo.scm_instance(), commits)
601 self._add_commits_to_repo(repo.scm_instance(), commits)
602 if heads:
602 if heads:
603 self.pull_heads(repo, heads)
603 self.pull_heads(repo, heads)
604
604
605 return repo
605 return repo
606
606
607 def pull_heads(self, repo, heads):
607 def pull_heads(self, repo, heads):
608 """
608 """
609 Make sure that repo contains all commits mentioned in `heads`
609 Make sure that repo contains all commits mentioned in `heads`
610 """
610 """
611 vcsmaster = self._master_repo.scm_instance()
611 vcsmaster = self._master_repo.scm_instance()
612 vcsrepo = repo.scm_instance()
612 vcsrepo = repo.scm_instance()
613 vcsrepo.config.clear_section('hooks')
613 vcsrepo.config.clear_section('hooks')
614 commit_ids = [self._commit_ids[h] for h in heads]
614 commit_ids = [self._commit_ids[h] for h in heads]
615 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
615 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
616
616
617 def create_fork(self):
617 def create_fork(self):
618 repo_to_fork = self.repo_name
618 repo_to_fork = self.repo_name
619 self.repo_name = self._next_repo_name()
619 self.repo_name = self._next_repo_name()
620 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
620 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
621 self._cleanup_repos.append(self.repo_name)
621 self._cleanup_repos.append(self.repo_name)
622 return repo
622 return repo
623
623
624 def new_repo_name(self, suffix=u''):
624 def new_repo_name(self, suffix=u''):
625 self.repo_name = self._next_repo_name() + suffix
625 self.repo_name = self._next_repo_name() + suffix
626 self._cleanup_repos.append(self.repo_name)
626 self._cleanup_repos.append(self.repo_name)
627 return self.repo_name
627 return self.repo_name
628
628
629 def _next_repo_name(self):
629 def _next_repo_name(self):
630 return u"%s_%s" % (
630 return u"%s_%s" % (
631 self.invalid_repo_name.sub(u'_', self._test_name),
631 self.invalid_repo_name.sub(u'_', self._test_name),
632 len(self._cleanup_repos))
632 len(self._cleanup_repos))
633
633
634 def ensure_file(self, filename, content='Test content\n'):
634 def ensure_file(self, filename, content='Test content\n'):
635 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
635 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
636 commits = [
636 commits = [
637 {'added': [
637 {'added': [
638 FileNode(filename, content=content),
638 FileNode(filename, content=content),
639 ]},
639 ]},
640 ]
640 ]
641 self._add_commits_to_repo(self.repo.scm_instance(), commits)
641 self._add_commits_to_repo(self.repo.scm_instance(), commits)
642
642
643 def enable_downloads(self):
643 def enable_downloads(self):
644 repo = self.repo
644 repo = self.repo
645 repo.enable_downloads = True
645 repo.enable_downloads = True
646 Session().add(repo)
646 Session().add(repo)
647 Session().commit()
647 Session().commit()
648
648
649 def cleanup(self):
649 def cleanup(self):
650 for repo_name in reversed(self._cleanup_repos):
650 for repo_name in reversed(self._cleanup_repos):
651 self._fixture.destroy_repo(repo_name)
651 self._fixture.destroy_repo(repo_name)
652
652
653 def _add_commits_to_repo(self, repo, commits):
653 def _add_commits_to_repo(self, repo, commits):
654 commit_ids = _add_commits_to_repo(repo, commits)
654 commit_ids = _add_commits_to_repo(repo, commits)
655 if not commit_ids:
655 if not commit_ids:
656 return
656 return
657 self._commit_ids = commit_ids
657 self._commit_ids = commit_ids
658
658
659 # Creating refs for Git to allow fetching them from remote repository
659 # Creating refs for Git to allow fetching them from remote repository
660 if self.alias == 'git':
660 if self.alias == 'git':
661 refs = {}
661 refs = {}
662 for message in self._commit_ids:
662 for message in self._commit_ids:
663 # TODO: mikhail: do more special chars replacements
663 # TODO: mikhail: do more special chars replacements
664 ref_name = 'refs/test-refs/{}'.format(
664 ref_name = 'refs/test-refs/{}'.format(
665 message.replace(' ', ''))
665 message.replace(' ', ''))
666 refs[ref_name] = self._commit_ids[message]
666 refs[ref_name] = self._commit_ids[message]
667 self._create_refs(repo, refs)
667 self._create_refs(repo, refs)
668
668
669 def _create_refs(self, repo, refs):
669 def _create_refs(self, repo, refs):
670 for ref_name in refs:
670 for ref_name in refs:
671 repo.set_refs(ref_name, refs[ref_name])
671 repo.set_refs(ref_name, refs[ref_name])
672
672
673
673
674 @pytest.fixture
674 @pytest.fixture
675 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
675 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
676 """
676 """
677 Parametrized fixture which represents a single vcs backend implementation.
677 Parametrized fixture which represents a single vcs backend implementation.
678
678
679 See the fixture `backend` for more details. This one implements the same
679 See the fixture `backend` for more details. This one implements the same
680 concept, but on vcs level. So it does not provide model instances etc.
680 concept, but on vcs level. So it does not provide model instances etc.
681
681
682 Parameters are generated dynamically, see :func:`pytest_generate_tests`
682 Parameters are generated dynamically, see :func:`pytest_generate_tests`
683 for how this works.
683 for how this works.
684 """
684 """
685 if backend_alias not in request.config.getoption('--backends'):
685 if backend_alias not in request.config.getoption('--backends'):
686 pytest.skip("Backend %s not selected." % (backend_alias, ))
686 pytest.skip("Backend %s not selected." % (backend_alias, ))
687
687
688 utils.check_xfail_backends(request.node, backend_alias)
688 utils.check_xfail_backends(request.node, backend_alias)
689 utils.check_skip_backends(request.node, backend_alias)
689 utils.check_skip_backends(request.node, backend_alias)
690
690
691 repo_name = 'vcs_test_%s' % (backend_alias, )
691 repo_name = 'vcs_test_%s' % (backend_alias, )
692 repo_path = os.path.join(tests_tmp_path, repo_name)
692 repo_path = os.path.join(tests_tmp_path, repo_name)
693 backend = VcsBackend(
693 backend = VcsBackend(
694 alias=backend_alias,
694 alias=backend_alias,
695 repo_path=repo_path,
695 repo_path=repo_path,
696 test_name=request.node.name,
696 test_name=request.node.name,
697 test_repo_container=test_repo)
697 test_repo_container=test_repo)
698 request.addfinalizer(backend.cleanup)
698 request.addfinalizer(backend.cleanup)
699 return backend
699 return backend
700
700
701
701
702 @pytest.fixture
702 @pytest.fixture
703 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
703 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
704 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
704 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
705
705
706
706
707 @pytest.fixture
707 @pytest.fixture
708 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
708 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
709 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
709 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
710
710
711
711
712 @pytest.fixture
712 @pytest.fixture
713 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
713 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
714 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
714 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
715
715
716
716
717 @pytest.fixture
717 @pytest.fixture
718 def vcsbackend_random(vcsbackend_git):
718 def vcsbackend_random(vcsbackend_git):
719 """
719 """
720 Use this to express that your tests need "a vcsbackend".
720 Use this to express that your tests need "a vcsbackend".
721
721
722 The fixture `vcsbackend` would run the test multiple times for each
722 The fixture `vcsbackend` would run the test multiple times for each
723 available vcs backend which is a pure waste of time if the test is
723 available vcs backend which is a pure waste of time if the test is
724 independent of the vcs backend type.
724 independent of the vcs backend type.
725 """
725 """
726 # TODO: johbo: Change this to pick a random backend
726 # TODO: johbo: Change this to pick a random backend
727 return vcsbackend_git
727 return vcsbackend_git
728
728
729
729
730 @pytest.fixture
730 @pytest.fixture
731 def vcsbackend_stub(vcsbackend_git):
731 def vcsbackend_stub(vcsbackend_git):
732 """
732 """
733 Use this to express that your test just needs a stub of a vcsbackend.
733 Use this to express that your test just needs a stub of a vcsbackend.
734
734
735 Plan is to eventually implement an in-memory stub to speed tests up.
735 Plan is to eventually implement an in-memory stub to speed tests up.
736 """
736 """
737 return vcsbackend_git
737 return vcsbackend_git
738
738
739
739
740 class VcsBackend(object):
740 class VcsBackend(object):
741 """
741 """
742 Represents the test configuration for one supported vcs backend.
742 Represents the test configuration for one supported vcs backend.
743 """
743 """
744
744
745 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
745 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
746
746
747 def __init__(self, alias, repo_path, test_name, test_repo_container):
747 def __init__(self, alias, repo_path, test_name, test_repo_container):
748 self.alias = alias
748 self.alias = alias
749 self._repo_path = repo_path
749 self._repo_path = repo_path
750 self._cleanup_repos = []
750 self._cleanup_repos = []
751 self._test_name = test_name
751 self._test_name = test_name
752 self._test_repo_container = test_repo_container
752 self._test_repo_container = test_repo_container
753
753
754 def __getitem__(self, key):
754 def __getitem__(self, key):
755 return self._test_repo_container(key, self.alias).scm_instance()
755 return self._test_repo_container(key, self.alias).scm_instance()
756
756
757 @property
757 @property
758 def repo(self):
758 def repo(self):
759 """
759 """
760 Returns the "current" repository. This is the vcs_test repo of the last
760 Returns the "current" repository. This is the vcs_test repo of the last
761 repo which has been created.
761 repo which has been created.
762 """
762 """
763 Repository = get_backend(self.alias)
763 Repository = get_backend(self.alias)
764 return Repository(self._repo_path)
764 return Repository(self._repo_path)
765
765
766 @property
766 @property
767 def backend(self):
767 def backend(self):
768 """
768 """
769 Returns the backend implementation class.
769 Returns the backend implementation class.
770 """
770 """
771 return get_backend(self.alias)
771 return get_backend(self.alias)
772
772
773 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
773 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
774 repo_name = self._next_repo_name()
774 repo_name = self._next_repo_name()
775 self._repo_path = get_new_dir(repo_name)
775 self._repo_path = get_new_dir(repo_name)
776 repo_class = get_backend(self.alias)
776 repo_class = get_backend(self.alias)
777 src_url = None
777 src_url = None
778 if _clone_repo:
778 if _clone_repo:
779 src_url = _clone_repo.path
779 src_url = _clone_repo.path
780 repo = repo_class(self._repo_path, create=True, src_url=src_url)
780 repo = repo_class(self._repo_path, create=True, src_url=src_url)
781 self._cleanup_repos.append(repo)
781 self._cleanup_repos.append(repo)
782
782
783 commits = commits or [
783 commits = commits or [
784 {'message': 'Commit %s of %s' % (x, repo_name)}
784 {'message': 'Commit %s of %s' % (x, repo_name)}
785 for x in xrange(number_of_commits)]
785 for x in xrange(number_of_commits)]
786 _add_commits_to_repo(repo, commits)
786 _add_commits_to_repo(repo, commits)
787 return repo
787 return repo
788
788
789 def clone_repo(self, repo):
789 def clone_repo(self, repo):
790 return self.create_repo(_clone_repo=repo)
790 return self.create_repo(_clone_repo=repo)
791
791
792 def cleanup(self):
792 def cleanup(self):
793 for repo in self._cleanup_repos:
793 for repo in self._cleanup_repos:
794 shutil.rmtree(repo.path)
794 shutil.rmtree(repo.path)
795
795
796 def new_repo_path(self):
796 def new_repo_path(self):
797 repo_name = self._next_repo_name()
797 repo_name = self._next_repo_name()
798 self._repo_path = get_new_dir(repo_name)
798 self._repo_path = get_new_dir(repo_name)
799 return self._repo_path
799 return self._repo_path
800
800
801 def _next_repo_name(self):
801 def _next_repo_name(self):
802 return "%s_%s" % (
802 return "%s_%s" % (
803 self.invalid_repo_name.sub('_', self._test_name),
803 self.invalid_repo_name.sub('_', self._test_name),
804 len(self._cleanup_repos))
804 len(self._cleanup_repos))
805
805
806 def add_file(self, repo, filename, content='Test content\n'):
806 def add_file(self, repo, filename, content='Test content\n'):
807 imc = repo.in_memory_commit
807 imc = repo.in_memory_commit
808 imc.add(FileNode(filename, content=content))
808 imc.add(FileNode(filename, content=content))
809 imc.commit(
809 imc.commit(
810 message=u'Automatic commit from vcsbackend fixture',
810 message=u'Automatic commit from vcsbackend fixture',
811 author=u'Automatic')
811 author=u'Automatic')
812
812
813 def ensure_file(self, filename, content='Test content\n'):
813 def ensure_file(self, filename, content='Test content\n'):
814 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
814 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
815 self.add_file(self.repo, filename, content)
815 self.add_file(self.repo, filename, content)
816
816
817
817
818 def _add_commits_to_repo(vcs_repo, commits):
818 def _add_commits_to_repo(vcs_repo, commits):
819 commit_ids = {}
819 commit_ids = {}
820 if not commits:
820 if not commits:
821 return commit_ids
821 return commit_ids
822
822
823 imc = vcs_repo.in_memory_commit
823 imc = vcs_repo.in_memory_commit
824 commit = None
824 commit = None
825
825
826 for idx, commit in enumerate(commits):
826 for idx, commit in enumerate(commits):
827 message = unicode(commit.get('message', 'Commit %s' % idx))
827 message = unicode(commit.get('message', 'Commit %s' % idx))
828
828
829 for node in commit.get('added', []):
829 for node in commit.get('added', []):
830 imc.add(FileNode(node.path, content=node.content))
830 imc.add(FileNode(node.path, content=node.content))
831 for node in commit.get('changed', []):
831 for node in commit.get('changed', []):
832 imc.change(FileNode(node.path, content=node.content))
832 imc.change(FileNode(node.path, content=node.content))
833 for node in commit.get('removed', []):
833 for node in commit.get('removed', []):
834 imc.remove(FileNode(node.path))
834 imc.remove(FileNode(node.path))
835
835
836 parents = [
836 parents = [
837 vcs_repo.get_commit(commit_id=commit_ids[p])
837 vcs_repo.get_commit(commit_id=commit_ids[p])
838 for p in commit.get('parents', [])]
838 for p in commit.get('parents', [])]
839
839
840 operations = ('added', 'changed', 'removed')
840 operations = ('added', 'changed', 'removed')
841 if not any((commit.get(o) for o in operations)):
841 if not any((commit.get(o) for o in operations)):
842 imc.add(FileNode('file_%s' % idx, content=message))
842 imc.add(FileNode('file_%s' % idx, content=message))
843
843
844 commit = imc.commit(
844 commit = imc.commit(
845 message=message,
845 message=message,
846 author=unicode(commit.get('author', 'Automatic')),
846 author=unicode(commit.get('author', 'Automatic')),
847 date=commit.get('date'),
847 date=commit.get('date'),
848 branch=commit.get('branch'),
848 branch=commit.get('branch'),
849 parents=parents)
849 parents=parents)
850
850
851 commit_ids[commit.message] = commit.raw_id
851 commit_ids[commit.message] = commit.raw_id
852
852
853 return commit_ids
853 return commit_ids
854
854
855
855
856 @pytest.fixture
856 @pytest.fixture
857 def reposerver(request):
857 def reposerver(request):
858 """
858 """
859 Allows to serve a backend repository
859 Allows to serve a backend repository
860 """
860 """
861
861
862 repo_server = RepoServer()
862 repo_server = RepoServer()
863 request.addfinalizer(repo_server.cleanup)
863 request.addfinalizer(repo_server.cleanup)
864 return repo_server
864 return repo_server
865
865
866
866
867 class RepoServer(object):
867 class RepoServer(object):
868 """
868 """
869 Utility to serve a local repository for the duration of a test case.
869 Utility to serve a local repository for the duration of a test case.
870
870
871 Supports only Subversion so far.
871 Supports only Subversion so far.
872 """
872 """
873
873
874 url = None
874 url = None
875
875
876 def __init__(self):
876 def __init__(self):
877 self._cleanup_servers = []
877 self._cleanup_servers = []
878
878
879 def serve(self, vcsrepo):
879 def serve(self, vcsrepo):
880 if vcsrepo.alias != 'svn':
880 if vcsrepo.alias != 'svn':
881 raise TypeError("Backend %s not supported" % vcsrepo.alias)
881 raise TypeError("Backend %s not supported" % vcsrepo.alias)
882
882
883 proc = subprocess32.Popen(
883 proc = subprocess32.Popen(
884 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
884 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
885 '--root', vcsrepo.path])
885 '--root', vcsrepo.path])
886 self._cleanup_servers.append(proc)
886 self._cleanup_servers.append(proc)
887 self.url = 'svn://localhost'
887 self.url = 'svn://localhost'
888
888
889 def cleanup(self):
889 def cleanup(self):
890 for proc in self._cleanup_servers:
890 for proc in self._cleanup_servers:
891 proc.terminate()
891 proc.terminate()
892
892
893
893
894 @pytest.fixture
894 @pytest.fixture
895 def pr_util(backend, request, config_stub):
895 def pr_util(backend, request, config_stub):
896 """
896 """
897 Utility for tests of models and for functional tests around pull requests.
897 Utility for tests of models and for functional tests around pull requests.
898
898
899 It gives an instance of :class:`PRTestUtility` which provides various
899 It gives an instance of :class:`PRTestUtility` which provides various
900 utility methods around one pull request.
900 utility methods around one pull request.
901
901
902 This fixture uses `backend` and inherits its parameterization.
902 This fixture uses `backend` and inherits its parameterization.
903 """
903 """
904
904
905 util = PRTestUtility(backend)
905 util = PRTestUtility(backend)
906
906
907 @request.addfinalizer
907 @request.addfinalizer
908 def cleanup():
908 def cleanup():
909 util.cleanup()
909 util.cleanup()
910
910
911 return util
911 return util
912
912
913
913
914 class PRTestUtility(object):
914 class PRTestUtility(object):
915
915
916 pull_request = None
916 pull_request = None
917 pull_request_id = None
917 pull_request_id = None
918 mergeable_patcher = None
918 mergeable_patcher = None
919 mergeable_mock = None
919 mergeable_mock = None
920 notification_patcher = None
920 notification_patcher = None
921
921
922 def __init__(self, backend):
922 def __init__(self, backend):
923 self.backend = backend
923 self.backend = backend
924
924
925 def create_pull_request(
925 def create_pull_request(
926 self, commits=None, target_head=None, source_head=None,
926 self, commits=None, target_head=None, source_head=None,
927 revisions=None, approved=False, author=None, mergeable=False,
927 revisions=None, approved=False, author=None, mergeable=False,
928 enable_notifications=True, name_suffix=u'', reviewers=None,
928 enable_notifications=True, name_suffix=u'', reviewers=None,
929 title=u"Test", description=u"Description"):
929 title=u"Test", description=u"Description"):
930 self.set_mergeable(mergeable)
930 self.set_mergeable(mergeable)
931 if not enable_notifications:
931 if not enable_notifications:
932 # mock notification side effect
932 # mock notification side effect
933 self.notification_patcher = mock.patch(
933 self.notification_patcher = mock.patch(
934 'rhodecode.model.notification.NotificationModel.create')
934 'rhodecode.model.notification.NotificationModel.create')
935 self.notification_patcher.start()
935 self.notification_patcher.start()
936
936
937 if not self.pull_request:
937 if not self.pull_request:
938 if not commits:
938 if not commits:
939 commits = [
939 commits = [
940 {'message': 'c1'},
940 {'message': 'c1'},
941 {'message': 'c2'},
941 {'message': 'c2'},
942 {'message': 'c3'},
942 {'message': 'c3'},
943 ]
943 ]
944 target_head = 'c1'
944 target_head = 'c1'
945 source_head = 'c2'
945 source_head = 'c2'
946 revisions = ['c2']
946 revisions = ['c2']
947
947
948 self.commit_ids = self.backend.create_master_repo(commits)
948 self.commit_ids = self.backend.create_master_repo(commits)
949 self.target_repository = self.backend.create_repo(
949 self.target_repository = self.backend.create_repo(
950 heads=[target_head], name_suffix=name_suffix)
950 heads=[target_head], name_suffix=name_suffix)
951 self.source_repository = self.backend.create_repo(
951 self.source_repository = self.backend.create_repo(
952 heads=[source_head], name_suffix=name_suffix)
952 heads=[source_head], name_suffix=name_suffix)
953 self.author = author or UserModel().get_by_username(
953 self.author = author or UserModel().get_by_username(
954 TEST_USER_ADMIN_LOGIN)
954 TEST_USER_ADMIN_LOGIN)
955
955
956 model = PullRequestModel()
956 model = PullRequestModel()
957 self.create_parameters = {
957 self.create_parameters = {
958 'created_by': self.author,
958 'created_by': self.author,
959 'source_repo': self.source_repository.repo_name,
959 'source_repo': self.source_repository.repo_name,
960 'source_ref': self._default_branch_reference(source_head),
960 'source_ref': self._default_branch_reference(source_head),
961 'target_repo': self.target_repository.repo_name,
961 'target_repo': self.target_repository.repo_name,
962 'target_ref': self._default_branch_reference(target_head),
962 'target_ref': self._default_branch_reference(target_head),
963 'revisions': [self.commit_ids[r] for r in revisions],
963 'revisions': [self.commit_ids[r] for r in revisions],
964 'reviewers': reviewers or self._get_reviewers(),
964 'reviewers': reviewers or self._get_reviewers(),
965 'title': title,
965 'title': title,
966 'description': description,
966 'description': description,
967 }
967 }
968 self.pull_request = model.create(**self.create_parameters)
968 self.pull_request = model.create(**self.create_parameters)
969 assert model.get_versions(self.pull_request) == []
969 assert model.get_versions(self.pull_request) == []
970
970
971 self.pull_request_id = self.pull_request.pull_request_id
971 self.pull_request_id = self.pull_request.pull_request_id
972
972
973 if approved:
973 if approved:
974 self.approve()
974 self.approve()
975
975
976 Session().add(self.pull_request)
976 Session().add(self.pull_request)
977 Session().commit()
977 Session().commit()
978
978
979 return self.pull_request
979 return self.pull_request
980
980
981 def approve(self):
981 def approve(self):
982 self.create_status_votes(
982 self.create_status_votes(
983 ChangesetStatus.STATUS_APPROVED,
983 ChangesetStatus.STATUS_APPROVED,
984 *self.pull_request.reviewers)
984 *self.pull_request.reviewers)
985
985
986 def close(self):
986 def close(self):
987 PullRequestModel().close_pull_request(self.pull_request, self.author)
987 PullRequestModel().close_pull_request(self.pull_request, self.author)
988
988
989 def _default_branch_reference(self, commit_message):
989 def _default_branch_reference(self, commit_message):
990 reference = '%s:%s:%s' % (
990 reference = '%s:%s:%s' % (
991 'branch',
991 'branch',
992 self.backend.default_branch_name,
992 self.backend.default_branch_name,
993 self.commit_ids[commit_message])
993 self.commit_ids[commit_message])
994 return reference
994 return reference
995
995
996 def _get_reviewers(self):
996 def _get_reviewers(self):
997 return [
997 return [
998 (TEST_USER_REGULAR_LOGIN, ['default1'], False),
998 (TEST_USER_REGULAR_LOGIN, ['default1'], False),
999 (TEST_USER_REGULAR2_LOGIN, ['default2'], False),
999 (TEST_USER_REGULAR2_LOGIN, ['default2'], False),
1000 ]
1000 ]
1001
1001
1002 def update_source_repository(self, head=None):
1002 def update_source_repository(self, head=None):
1003 heads = [head or 'c3']
1003 heads = [head or 'c3']
1004 self.backend.pull_heads(self.source_repository, heads=heads)
1004 self.backend.pull_heads(self.source_repository, heads=heads)
1005
1005
1006 def add_one_commit(self, head=None):
1006 def add_one_commit(self, head=None):
1007 self.update_source_repository(head=head)
1007 self.update_source_repository(head=head)
1008 old_commit_ids = set(self.pull_request.revisions)
1008 old_commit_ids = set(self.pull_request.revisions)
1009 PullRequestModel().update_commits(self.pull_request)
1009 PullRequestModel().update_commits(self.pull_request)
1010 commit_ids = set(self.pull_request.revisions)
1010 commit_ids = set(self.pull_request.revisions)
1011 new_commit_ids = commit_ids - old_commit_ids
1011 new_commit_ids = commit_ids - old_commit_ids
1012 assert len(new_commit_ids) == 1
1012 assert len(new_commit_ids) == 1
1013 return new_commit_ids.pop()
1013 return new_commit_ids.pop()
1014
1014
1015 def remove_one_commit(self):
1015 def remove_one_commit(self):
1016 assert len(self.pull_request.revisions) == 2
1016 assert len(self.pull_request.revisions) == 2
1017 source_vcs = self.source_repository.scm_instance()
1017 source_vcs = self.source_repository.scm_instance()
1018 removed_commit_id = source_vcs.commit_ids[-1]
1018 removed_commit_id = source_vcs.commit_ids[-1]
1019
1019
1020 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1020 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1021 # remove the if once that's sorted out.
1021 # remove the if once that's sorted out.
1022 if self.backend.alias == "git":
1022 if self.backend.alias == "git":
1023 kwargs = {'branch_name': self.backend.default_branch_name}
1023 kwargs = {'branch_name': self.backend.default_branch_name}
1024 else:
1024 else:
1025 kwargs = {}
1025 kwargs = {}
1026 source_vcs.strip(removed_commit_id, **kwargs)
1026 source_vcs.strip(removed_commit_id, **kwargs)
1027
1027
1028 PullRequestModel().update_commits(self.pull_request)
1028 PullRequestModel().update_commits(self.pull_request)
1029 assert len(self.pull_request.revisions) == 1
1029 assert len(self.pull_request.revisions) == 1
1030 return removed_commit_id
1030 return removed_commit_id
1031
1031
1032 def create_comment(self, linked_to=None):
1032 def create_comment(self, linked_to=None):
1033 comment = CommentsModel().create(
1033 comment = CommentsModel().create(
1034 text=u"Test comment",
1034 text=u"Test comment",
1035 repo=self.target_repository.repo_name,
1035 repo=self.target_repository.repo_name,
1036 user=self.author,
1036 user=self.author,
1037 pull_request=self.pull_request)
1037 pull_request=self.pull_request)
1038 assert comment.pull_request_version_id is None
1038 assert comment.pull_request_version_id is None
1039
1039
1040 if linked_to:
1040 if linked_to:
1041 PullRequestModel()._link_comments_to_version(linked_to)
1041 PullRequestModel()._link_comments_to_version(linked_to)
1042
1042
1043 return comment
1043 return comment
1044
1044
1045 def create_inline_comment(
1045 def create_inline_comment(
1046 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1046 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1047 comment = CommentsModel().create(
1047 comment = CommentsModel().create(
1048 text=u"Test comment",
1048 text=u"Test comment",
1049 repo=self.target_repository.repo_name,
1049 repo=self.target_repository.repo_name,
1050 user=self.author,
1050 user=self.author,
1051 line_no=line_no,
1051 line_no=line_no,
1052 f_path=file_path,
1052 f_path=file_path,
1053 pull_request=self.pull_request)
1053 pull_request=self.pull_request)
1054 assert comment.pull_request_version_id is None
1054 assert comment.pull_request_version_id is None
1055
1055
1056 if linked_to:
1056 if linked_to:
1057 PullRequestModel()._link_comments_to_version(linked_to)
1057 PullRequestModel()._link_comments_to_version(linked_to)
1058
1058
1059 return comment
1059 return comment
1060
1060
1061 def create_version_of_pull_request(self):
1061 def create_version_of_pull_request(self):
1062 pull_request = self.create_pull_request()
1062 pull_request = self.create_pull_request()
1063 version = PullRequestModel()._create_version_from_snapshot(
1063 version = PullRequestModel()._create_version_from_snapshot(
1064 pull_request)
1064 pull_request)
1065 return version
1065 return version
1066
1066
1067 def create_status_votes(self, status, *reviewers):
1067 def create_status_votes(self, status, *reviewers):
1068 for reviewer in reviewers:
1068 for reviewer in reviewers:
1069 ChangesetStatusModel().set_status(
1069 ChangesetStatusModel().set_status(
1070 repo=self.pull_request.target_repo,
1070 repo=self.pull_request.target_repo,
1071 status=status,
1071 status=status,
1072 user=reviewer.user_id,
1072 user=reviewer.user_id,
1073 pull_request=self.pull_request)
1073 pull_request=self.pull_request)
1074
1074
1075 def set_mergeable(self, value):
1075 def set_mergeable(self, value):
1076 if not self.mergeable_patcher:
1076 if not self.mergeable_patcher:
1077 self.mergeable_patcher = mock.patch.object(
1077 self.mergeable_patcher = mock.patch.object(
1078 VcsSettingsModel, 'get_general_settings')
1078 VcsSettingsModel, 'get_general_settings')
1079 self.mergeable_mock = self.mergeable_patcher.start()
1079 self.mergeable_mock = self.mergeable_patcher.start()
1080 self.mergeable_mock.return_value = {
1080 self.mergeable_mock.return_value = {
1081 'rhodecode_pr_merge_enabled': value}
1081 'rhodecode_pr_merge_enabled': value}
1082
1082
1083 def cleanup(self):
1083 def cleanup(self):
1084 # In case the source repository is already cleaned up, the pull
1084 # In case the source repository is already cleaned up, the pull
1085 # request will already be deleted.
1085 # request will already be deleted.
1086 pull_request = PullRequest().get(self.pull_request_id)
1086 pull_request = PullRequest().get(self.pull_request_id)
1087 if pull_request:
1087 if pull_request:
1088 PullRequestModel().delete(pull_request, pull_request.author)
1088 PullRequestModel().delete(pull_request, pull_request.author)
1089 Session().commit()
1089 Session().commit()
1090
1090
1091 if self.notification_patcher:
1091 if self.notification_patcher:
1092 self.notification_patcher.stop()
1092 self.notification_patcher.stop()
1093
1093
1094 if self.mergeable_patcher:
1094 if self.mergeable_patcher:
1095 self.mergeable_patcher.stop()
1095 self.mergeable_patcher.stop()
1096
1096
1097
1097
1098 @pytest.fixture
1098 @pytest.fixture
1099 def user_admin(pylonsapp):
1099 def user_admin(pylonsapp):
1100 """
1100 """
1101 Provides the default admin test user as an instance of `db.User`.
1101 Provides the default admin test user as an instance of `db.User`.
1102 """
1102 """
1103 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1103 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1104 return user
1104 return user
1105
1105
1106
1106
1107 @pytest.fixture
1107 @pytest.fixture
1108 def user_regular(pylonsapp):
1108 def user_regular(pylonsapp):
1109 """
1109 """
1110 Provides the default regular test user as an instance of `db.User`.
1110 Provides the default regular test user as an instance of `db.User`.
1111 """
1111 """
1112 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1112 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1113 return user
1113 return user
1114
1114
1115
1115
1116 @pytest.fixture
1116 @pytest.fixture
1117 def user_util(request, pylonsapp):
1117 def user_util(request, pylonsapp):
1118 """
1118 """
1119 Provides a wired instance of `UserUtility` with integrated cleanup.
1119 Provides a wired instance of `UserUtility` with integrated cleanup.
1120 """
1120 """
1121 utility = UserUtility(test_name=request.node.name)
1121 utility = UserUtility(test_name=request.node.name)
1122 request.addfinalizer(utility.cleanup)
1122 request.addfinalizer(utility.cleanup)
1123 return utility
1123 return utility
1124
1124
1125
1125
1126 # TODO: johbo: Split this up into utilities per domain or something similar
1126 # TODO: johbo: Split this up into utilities per domain or something similar
1127 class UserUtility(object):
1127 class UserUtility(object):
1128
1128
1129 def __init__(self, test_name="test"):
1129 def __init__(self, test_name="test"):
1130 self._test_name = self._sanitize_name(test_name)
1130 self._test_name = self._sanitize_name(test_name)
1131 self.fixture = Fixture()
1131 self.fixture = Fixture()
1132 self.repo_group_ids = []
1132 self.repo_group_ids = []
1133 self.repos_ids = []
1133 self.repos_ids = []
1134 self.user_ids = []
1134 self.user_ids = []
1135 self.user_group_ids = []
1135 self.user_group_ids = []
1136 self.user_repo_permission_ids = []
1136 self.user_repo_permission_ids = []
1137 self.user_group_repo_permission_ids = []
1137 self.user_group_repo_permission_ids = []
1138 self.user_repo_group_permission_ids = []
1138 self.user_repo_group_permission_ids = []
1139 self.user_group_repo_group_permission_ids = []
1139 self.user_group_repo_group_permission_ids = []
1140 self.user_user_group_permission_ids = []
1140 self.user_user_group_permission_ids = []
1141 self.user_group_user_group_permission_ids = []
1141 self.user_group_user_group_permission_ids = []
1142 self.user_permissions = []
1142 self.user_permissions = []
1143
1143
1144 def _sanitize_name(self, name):
1144 def _sanitize_name(self, name):
1145 for char in ['[', ']']:
1145 for char in ['[', ']']:
1146 name = name.replace(char, '_')
1146 name = name.replace(char, '_')
1147 return name
1147 return name
1148
1148
1149 def create_repo_group(
1149 def create_repo_group(
1150 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1150 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1151 group_name = "{prefix}_repogroup_{count}".format(
1151 group_name = "{prefix}_repogroup_{count}".format(
1152 prefix=self._test_name,
1152 prefix=self._test_name,
1153 count=len(self.repo_group_ids))
1153 count=len(self.repo_group_ids))
1154 repo_group = self.fixture.create_repo_group(
1154 repo_group = self.fixture.create_repo_group(
1155 group_name, cur_user=owner)
1155 group_name, cur_user=owner)
1156 if auto_cleanup:
1156 if auto_cleanup:
1157 self.repo_group_ids.append(repo_group.group_id)
1157 self.repo_group_ids.append(repo_group.group_id)
1158 return repo_group
1158 return repo_group
1159
1159
1160 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None,
1160 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None,
1161 auto_cleanup=True, repo_type='hg'):
1161 auto_cleanup=True, repo_type='hg'):
1162 repo_name = "{prefix}_repository_{count}".format(
1162 repo_name = "{prefix}_repository_{count}".format(
1163 prefix=self._test_name,
1163 prefix=self._test_name,
1164 count=len(self.repos_ids))
1164 count=len(self.repos_ids))
1165
1165
1166 repository = self.fixture.create_repo(
1166 repository = self.fixture.create_repo(
1167 repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type)
1167 repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type)
1168 if auto_cleanup:
1168 if auto_cleanup:
1169 self.repos_ids.append(repository.repo_id)
1169 self.repos_ids.append(repository.repo_id)
1170 return repository
1170 return repository
1171
1171
1172 def create_user(self, auto_cleanup=True, **kwargs):
1172 def create_user(self, auto_cleanup=True, **kwargs):
1173 user_name = "{prefix}_user_{count}".format(
1173 user_name = "{prefix}_user_{count}".format(
1174 prefix=self._test_name,
1174 prefix=self._test_name,
1175 count=len(self.user_ids))
1175 count=len(self.user_ids))
1176 user = self.fixture.create_user(user_name, **kwargs)
1176 user = self.fixture.create_user(user_name, **kwargs)
1177 if auto_cleanup:
1177 if auto_cleanup:
1178 self.user_ids.append(user.user_id)
1178 self.user_ids.append(user.user_id)
1179 return user
1179 return user
1180
1180
1181 def create_user_with_group(self):
1181 def create_user_with_group(self):
1182 user = self.create_user()
1182 user = self.create_user()
1183 user_group = self.create_user_group(members=[user])
1183 user_group = self.create_user_group(members=[user])
1184 return user, user_group
1184 return user, user_group
1185
1185
1186 def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None,
1186 def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None,
1187 auto_cleanup=True, **kwargs):
1187 auto_cleanup=True, **kwargs):
1188 group_name = "{prefix}_usergroup_{count}".format(
1188 group_name = "{prefix}_usergroup_{count}".format(
1189 prefix=self._test_name,
1189 prefix=self._test_name,
1190 count=len(self.user_group_ids))
1190 count=len(self.user_group_ids))
1191 user_group = self.fixture.create_user_group(
1191 user_group = self.fixture.create_user_group(
1192 group_name, cur_user=owner, **kwargs)
1192 group_name, cur_user=owner, **kwargs)
1193
1193
1194 if auto_cleanup:
1194 if auto_cleanup:
1195 self.user_group_ids.append(user_group.users_group_id)
1195 self.user_group_ids.append(user_group.users_group_id)
1196 if members:
1196 if members:
1197 for user in members:
1197 for user in members:
1198 UserGroupModel().add_user_to_group(user_group, user)
1198 UserGroupModel().add_user_to_group(user_group, user)
1199 return user_group
1199 return user_group
1200
1200
1201 def grant_user_permission(self, user_name, permission_name):
1201 def grant_user_permission(self, user_name, permission_name):
1202 self._inherit_default_user_permissions(user_name, False)
1202 self._inherit_default_user_permissions(user_name, False)
1203 self.user_permissions.append((user_name, permission_name))
1203 self.user_permissions.append((user_name, permission_name))
1204
1204
1205 def grant_user_permission_to_repo_group(
1205 def grant_user_permission_to_repo_group(
1206 self, repo_group, user, permission_name):
1206 self, repo_group, user, permission_name):
1207 permission = RepoGroupModel().grant_user_permission(
1207 permission = RepoGroupModel().grant_user_permission(
1208 repo_group, user, permission_name)
1208 repo_group, user, permission_name)
1209 self.user_repo_group_permission_ids.append(
1209 self.user_repo_group_permission_ids.append(
1210 (repo_group.group_id, user.user_id))
1210 (repo_group.group_id, user.user_id))
1211 return permission
1211 return permission
1212
1212
1213 def grant_user_group_permission_to_repo_group(
1213 def grant_user_group_permission_to_repo_group(
1214 self, repo_group, user_group, permission_name):
1214 self, repo_group, user_group, permission_name):
1215 permission = RepoGroupModel().grant_user_group_permission(
1215 permission = RepoGroupModel().grant_user_group_permission(
1216 repo_group, user_group, permission_name)
1216 repo_group, user_group, permission_name)
1217 self.user_group_repo_group_permission_ids.append(
1217 self.user_group_repo_group_permission_ids.append(
1218 (repo_group.group_id, user_group.users_group_id))
1218 (repo_group.group_id, user_group.users_group_id))
1219 return permission
1219 return permission
1220
1220
1221 def grant_user_permission_to_repo(
1221 def grant_user_permission_to_repo(
1222 self, repo, user, permission_name):
1222 self, repo, user, permission_name):
1223 permission = RepoModel().grant_user_permission(
1223 permission = RepoModel().grant_user_permission(
1224 repo, user, permission_name)
1224 repo, user, permission_name)
1225 self.user_repo_permission_ids.append(
1225 self.user_repo_permission_ids.append(
1226 (repo.repo_id, user.user_id))
1226 (repo.repo_id, user.user_id))
1227 return permission
1227 return permission
1228
1228
1229 def grant_user_group_permission_to_repo(
1229 def grant_user_group_permission_to_repo(
1230 self, repo, user_group, permission_name):
1230 self, repo, user_group, permission_name):
1231 permission = RepoModel().grant_user_group_permission(
1231 permission = RepoModel().grant_user_group_permission(
1232 repo, user_group, permission_name)
1232 repo, user_group, permission_name)
1233 self.user_group_repo_permission_ids.append(
1233 self.user_group_repo_permission_ids.append(
1234 (repo.repo_id, user_group.users_group_id))
1234 (repo.repo_id, user_group.users_group_id))
1235 return permission
1235 return permission
1236
1236
1237 def grant_user_permission_to_user_group(
1237 def grant_user_permission_to_user_group(
1238 self, target_user_group, user, permission_name):
1238 self, target_user_group, user, permission_name):
1239 permission = UserGroupModel().grant_user_permission(
1239 permission = UserGroupModel().grant_user_permission(
1240 target_user_group, user, permission_name)
1240 target_user_group, user, permission_name)
1241 self.user_user_group_permission_ids.append(
1241 self.user_user_group_permission_ids.append(
1242 (target_user_group.users_group_id, user.user_id))
1242 (target_user_group.users_group_id, user.user_id))
1243 return permission
1243 return permission
1244
1244
1245 def grant_user_group_permission_to_user_group(
1245 def grant_user_group_permission_to_user_group(
1246 self, target_user_group, user_group, permission_name):
1246 self, target_user_group, user_group, permission_name):
1247 permission = UserGroupModel().grant_user_group_permission(
1247 permission = UserGroupModel().grant_user_group_permission(
1248 target_user_group, user_group, permission_name)
1248 target_user_group, user_group, permission_name)
1249 self.user_group_user_group_permission_ids.append(
1249 self.user_group_user_group_permission_ids.append(
1250 (target_user_group.users_group_id, user_group.users_group_id))
1250 (target_user_group.users_group_id, user_group.users_group_id))
1251 return permission
1251 return permission
1252
1252
1253 def revoke_user_permission(self, user_name, permission_name):
1253 def revoke_user_permission(self, user_name, permission_name):
1254 self._inherit_default_user_permissions(user_name, True)
1254 self._inherit_default_user_permissions(user_name, True)
1255 UserModel().revoke_perm(user_name, permission_name)
1255 UserModel().revoke_perm(user_name, permission_name)
1256
1256
1257 def _inherit_default_user_permissions(self, user_name, value):
1257 def _inherit_default_user_permissions(self, user_name, value):
1258 user = UserModel().get_by_username(user_name)
1258 user = UserModel().get_by_username(user_name)
1259 user.inherit_default_permissions = value
1259 user.inherit_default_permissions = value
1260 Session().add(user)
1260 Session().add(user)
1261 Session().commit()
1261 Session().commit()
1262
1262
1263 def cleanup(self):
1263 def cleanup(self):
1264 self._cleanup_permissions()
1264 self._cleanup_permissions()
1265 self._cleanup_repos()
1265 self._cleanup_repos()
1266 self._cleanup_repo_groups()
1266 self._cleanup_repo_groups()
1267 self._cleanup_user_groups()
1267 self._cleanup_user_groups()
1268 self._cleanup_users()
1268 self._cleanup_users()
1269
1269
1270 def _cleanup_permissions(self):
1270 def _cleanup_permissions(self):
1271 if self.user_permissions:
1271 if self.user_permissions:
1272 for user_name, permission_name in self.user_permissions:
1272 for user_name, permission_name in self.user_permissions:
1273 self.revoke_user_permission(user_name, permission_name)
1273 self.revoke_user_permission(user_name, permission_name)
1274
1274
1275 for permission in self.user_repo_permission_ids:
1275 for permission in self.user_repo_permission_ids:
1276 RepoModel().revoke_user_permission(*permission)
1276 RepoModel().revoke_user_permission(*permission)
1277
1277
1278 for permission in self.user_group_repo_permission_ids:
1278 for permission in self.user_group_repo_permission_ids:
1279 RepoModel().revoke_user_group_permission(*permission)
1279 RepoModel().revoke_user_group_permission(*permission)
1280
1280
1281 for permission in self.user_repo_group_permission_ids:
1281 for permission in self.user_repo_group_permission_ids:
1282 RepoGroupModel().revoke_user_permission(*permission)
1282 RepoGroupModel().revoke_user_permission(*permission)
1283
1283
1284 for permission in self.user_group_repo_group_permission_ids:
1284 for permission in self.user_group_repo_group_permission_ids:
1285 RepoGroupModel().revoke_user_group_permission(*permission)
1285 RepoGroupModel().revoke_user_group_permission(*permission)
1286
1286
1287 for permission in self.user_user_group_permission_ids:
1287 for permission in self.user_user_group_permission_ids:
1288 UserGroupModel().revoke_user_permission(*permission)
1288 UserGroupModel().revoke_user_permission(*permission)
1289
1289
1290 for permission in self.user_group_user_group_permission_ids:
1290 for permission in self.user_group_user_group_permission_ids:
1291 UserGroupModel().revoke_user_group_permission(*permission)
1291 UserGroupModel().revoke_user_group_permission(*permission)
1292
1292
1293 def _cleanup_repo_groups(self):
1293 def _cleanup_repo_groups(self):
1294 def _repo_group_compare(first_group_id, second_group_id):
1294 def _repo_group_compare(first_group_id, second_group_id):
1295 """
1295 """
1296 Gives higher priority to the groups with the most complex paths
1296 Gives higher priority to the groups with the most complex paths
1297 """
1297 """
1298 first_group = RepoGroup.get(first_group_id)
1298 first_group = RepoGroup.get(first_group_id)
1299 second_group = RepoGroup.get(second_group_id)
1299 second_group = RepoGroup.get(second_group_id)
1300 first_group_parts = (
1300 first_group_parts = (
1301 len(first_group.group_name.split('/')) if first_group else 0)
1301 len(first_group.group_name.split('/')) if first_group else 0)
1302 second_group_parts = (
1302 second_group_parts = (
1303 len(second_group.group_name.split('/')) if second_group else 0)
1303 len(second_group.group_name.split('/')) if second_group else 0)
1304 return cmp(second_group_parts, first_group_parts)
1304 return cmp(second_group_parts, first_group_parts)
1305
1305
1306 sorted_repo_group_ids = sorted(
1306 sorted_repo_group_ids = sorted(
1307 self.repo_group_ids, cmp=_repo_group_compare)
1307 self.repo_group_ids, cmp=_repo_group_compare)
1308 for repo_group_id in sorted_repo_group_ids:
1308 for repo_group_id in sorted_repo_group_ids:
1309 self.fixture.destroy_repo_group(repo_group_id)
1309 self.fixture.destroy_repo_group(repo_group_id)
1310
1310
1311 def _cleanup_repos(self):
1311 def _cleanup_repos(self):
1312 sorted_repos_ids = sorted(self.repos_ids)
1312 sorted_repos_ids = sorted(self.repos_ids)
1313 for repo_id in sorted_repos_ids:
1313 for repo_id in sorted_repos_ids:
1314 self.fixture.destroy_repo(repo_id)
1314 self.fixture.destroy_repo(repo_id)
1315
1315
1316 def _cleanup_user_groups(self):
1316 def _cleanup_user_groups(self):
1317 def _user_group_compare(first_group_id, second_group_id):
1317 def _user_group_compare(first_group_id, second_group_id):
1318 """
1318 """
1319 Gives higher priority to the groups with the most complex paths
1319 Gives higher priority to the groups with the most complex paths
1320 """
1320 """
1321 first_group = UserGroup.get(first_group_id)
1321 first_group = UserGroup.get(first_group_id)
1322 second_group = UserGroup.get(second_group_id)
1322 second_group = UserGroup.get(second_group_id)
1323 first_group_parts = (
1323 first_group_parts = (
1324 len(first_group.users_group_name.split('/'))
1324 len(first_group.users_group_name.split('/'))
1325 if first_group else 0)
1325 if first_group else 0)
1326 second_group_parts = (
1326 second_group_parts = (
1327 len(second_group.users_group_name.split('/'))
1327 len(second_group.users_group_name.split('/'))
1328 if second_group else 0)
1328 if second_group else 0)
1329 return cmp(second_group_parts, first_group_parts)
1329 return cmp(second_group_parts, first_group_parts)
1330
1330
1331 sorted_user_group_ids = sorted(
1331 sorted_user_group_ids = sorted(
1332 self.user_group_ids, cmp=_user_group_compare)
1332 self.user_group_ids, cmp=_user_group_compare)
1333 for user_group_id in sorted_user_group_ids:
1333 for user_group_id in sorted_user_group_ids:
1334 self.fixture.destroy_user_group(user_group_id)
1334 self.fixture.destroy_user_group(user_group_id)
1335
1335
1336 def _cleanup_users(self):
1336 def _cleanup_users(self):
1337 for user_id in self.user_ids:
1337 for user_id in self.user_ids:
1338 self.fixture.destroy_user(user_id)
1338 self.fixture.destroy_user(user_id)
1339
1339
1340
1340
1341 # TODO: Think about moving this into a pytest-pyro package and make it a
1341 # TODO: Think about moving this into a pytest-pyro package and make it a
1342 # pytest plugin
1342 # pytest plugin
1343 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1343 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1344 def pytest_runtest_makereport(item, call):
1344 def pytest_runtest_makereport(item, call):
1345 """
1345 """
1346 Adding the remote traceback if the exception has this information.
1346 Adding the remote traceback if the exception has this information.
1347
1347
1348 VCSServer attaches this information as the attribute `_vcs_server_traceback`
1348 VCSServer attaches this information as the attribute `_vcs_server_traceback`
1349 to the exception instance.
1349 to the exception instance.
1350 """
1350 """
1351 outcome = yield
1351 outcome = yield
1352 report = outcome.get_result()
1352 report = outcome.get_result()
1353 if call.excinfo:
1353 if call.excinfo:
1354 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1354 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1355
1355
1356
1356
1357 def _add_vcsserver_remote_traceback(report, exc):
1357 def _add_vcsserver_remote_traceback(report, exc):
1358 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1358 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1359
1359
1360 if vcsserver_traceback:
1360 if vcsserver_traceback:
1361 section = 'VCSServer remote traceback ' + report.when
1361 section = 'VCSServer remote traceback ' + report.when
1362 report.sections.append((section, vcsserver_traceback))
1362 report.sections.append((section, vcsserver_traceback))
1363
1363
1364
1364
1365 @pytest.fixture(scope='session')
1365 @pytest.fixture(scope='session')
1366 def testrun():
1366 def testrun():
1367 return {
1367 return {
1368 'uuid': uuid.uuid4(),
1368 'uuid': uuid.uuid4(),
1369 'start': datetime.datetime.utcnow().isoformat(),
1369 'start': datetime.datetime.utcnow().isoformat(),
1370 'timestamp': int(time.time()),
1370 'timestamp': int(time.time()),
1371 }
1371 }
1372
1372
1373
1373
1374 @pytest.fixture(autouse=True)
1374 @pytest.fixture(autouse=True)
1375 def collect_appenlight_stats(request, testrun):
1375 def collect_appenlight_stats(request, testrun):
1376 """
1376 """
1377 This fixture reports memory consumtion of single tests.
1377 This fixture reports memory consumtion of single tests.
1378
1378
1379 It gathers data based on `psutil` and sends them to Appenlight. The option
1379 It gathers data based on `psutil` and sends them to Appenlight. The option
1380 ``--ae`` has te be used to enable this fixture and the API key for your
1380 ``--ae`` has te be used to enable this fixture and the API key for your
1381 application has to be provided in ``--ae-key``.
1381 application has to be provided in ``--ae-key``.
1382 """
1382 """
1383 try:
1383 try:
1384 # cygwin cannot have yet psutil support.
1384 # cygwin cannot have yet psutil support.
1385 import psutil
1385 import psutil
1386 except ImportError:
1386 except ImportError:
1387 return
1387 return
1388
1388
1389 if not request.config.getoption('--appenlight'):
1389 if not request.config.getoption('--appenlight'):
1390 return
1390 return
1391 else:
1391 else:
1392 # Only request the pylonsapp fixture if appenlight tracking is
1392 # Only request the pylonsapp fixture if appenlight tracking is
1393 # enabled. This will speed up a test run of unit tests by 2 to 3
1393 # enabled. This will speed up a test run of unit tests by 2 to 3
1394 # seconds if appenlight is not enabled.
1394 # seconds if appenlight is not enabled.
1395 pylonsapp = request.getfuncargvalue("pylonsapp")
1395 pylonsapp = request.getfuncargvalue("pylonsapp")
1396 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1396 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1397 client = AppenlightClient(
1397 client = AppenlightClient(
1398 url=url,
1398 url=url,
1399 api_key=request.config.getoption('--appenlight-api-key'),
1399 api_key=request.config.getoption('--appenlight-api-key'),
1400 namespace=request.node.nodeid,
1400 namespace=request.node.nodeid,
1401 request=str(testrun['uuid']),
1401 request=str(testrun['uuid']),
1402 testrun=testrun)
1402 testrun=testrun)
1403
1403
1404 client.collect({
1404 client.collect({
1405 'message': "Starting",
1405 'message': "Starting",
1406 })
1406 })
1407
1407
1408 server_and_port = pylonsapp.config['vcs.server']
1408 server_and_port = pylonsapp.config['vcs.server']
1409 protocol = pylonsapp.config['vcs.server.protocol']
1409 protocol = pylonsapp.config['vcs.server.protocol']
1410 server = create_vcsserver_proxy(server_and_port, protocol)
1410 server = create_vcsserver_proxy(server_and_port, protocol)
1411 with server:
1411 with server:
1412 vcs_pid = server.get_pid()
1412 vcs_pid = server.get_pid()
1413 server.run_gc()
1413 server.run_gc()
1414 vcs_process = psutil.Process(vcs_pid)
1414 vcs_process = psutil.Process(vcs_pid)
1415 mem = vcs_process.memory_info()
1415 mem = vcs_process.memory_info()
1416 client.tag_before('vcsserver.rss', mem.rss)
1416 client.tag_before('vcsserver.rss', mem.rss)
1417 client.tag_before('vcsserver.vms', mem.vms)
1417 client.tag_before('vcsserver.vms', mem.vms)
1418
1418
1419 test_process = psutil.Process()
1419 test_process = psutil.Process()
1420 mem = test_process.memory_info()
1420 mem = test_process.memory_info()
1421 client.tag_before('test.rss', mem.rss)
1421 client.tag_before('test.rss', mem.rss)
1422 client.tag_before('test.vms', mem.vms)
1422 client.tag_before('test.vms', mem.vms)
1423
1423
1424 client.tag_before('time', time.time())
1424 client.tag_before('time', time.time())
1425
1425
1426 @request.addfinalizer
1426 @request.addfinalizer
1427 def send_stats():
1427 def send_stats():
1428 client.tag_after('time', time.time())
1428 client.tag_after('time', time.time())
1429 with server:
1429 with server:
1430 gc_stats = server.run_gc()
1430 gc_stats = server.run_gc()
1431 for tag, value in gc_stats.items():
1431 for tag, value in gc_stats.items():
1432 client.tag_after(tag, value)
1432 client.tag_after(tag, value)
1433 mem = vcs_process.memory_info()
1433 mem = vcs_process.memory_info()
1434 client.tag_after('vcsserver.rss', mem.rss)
1434 client.tag_after('vcsserver.rss', mem.rss)
1435 client.tag_after('vcsserver.vms', mem.vms)
1435 client.tag_after('vcsserver.vms', mem.vms)
1436
1436
1437 mem = test_process.memory_info()
1437 mem = test_process.memory_info()
1438 client.tag_after('test.rss', mem.rss)
1438 client.tag_after('test.rss', mem.rss)
1439 client.tag_after('test.vms', mem.vms)
1439 client.tag_after('test.vms', mem.vms)
1440
1440
1441 client.collect({
1441 client.collect({
1442 'message': "Finished",
1442 'message': "Finished",
1443 })
1443 })
1444 client.send_stats()
1444 client.send_stats()
1445
1445
1446 return client
1446 return client
1447
1447
1448
1448
1449 class AppenlightClient():
1449 class AppenlightClient():
1450
1450
1451 url_template = '{url}?protocol_version=0.5'
1451 url_template = '{url}?protocol_version=0.5'
1452
1452
1453 def __init__(
1453 def __init__(
1454 self, url, api_key, add_server=True, add_timestamp=True,
1454 self, url, api_key, add_server=True, add_timestamp=True,
1455 namespace=None, request=None, testrun=None):
1455 namespace=None, request=None, testrun=None):
1456 self.url = self.url_template.format(url=url)
1456 self.url = self.url_template.format(url=url)
1457 self.api_key = api_key
1457 self.api_key = api_key
1458 self.add_server = add_server
1458 self.add_server = add_server
1459 self.add_timestamp = add_timestamp
1459 self.add_timestamp = add_timestamp
1460 self.namespace = namespace
1460 self.namespace = namespace
1461 self.request = request
1461 self.request = request
1462 self.server = socket.getfqdn(socket.gethostname())
1462 self.server = socket.getfqdn(socket.gethostname())
1463 self.tags_before = {}
1463 self.tags_before = {}
1464 self.tags_after = {}
1464 self.tags_after = {}
1465 self.stats = []
1465 self.stats = []
1466 self.testrun = testrun or {}
1466 self.testrun = testrun or {}
1467
1467
1468 def tag_before(self, tag, value):
1468 def tag_before(self, tag, value):
1469 self.tags_before[tag] = value
1469 self.tags_before[tag] = value
1470
1470
1471 def tag_after(self, tag, value):
1471 def tag_after(self, tag, value):
1472 self.tags_after[tag] = value
1472 self.tags_after[tag] = value
1473
1473
1474 def collect(self, data):
1474 def collect(self, data):
1475 if self.add_server:
1475 if self.add_server:
1476 data.setdefault('server', self.server)
1476 data.setdefault('server', self.server)
1477 if self.add_timestamp:
1477 if self.add_timestamp:
1478 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1478 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1479 if self.namespace:
1479 if self.namespace:
1480 data.setdefault('namespace', self.namespace)
1480 data.setdefault('namespace', self.namespace)
1481 if self.request:
1481 if self.request:
1482 data.setdefault('request', self.request)
1482 data.setdefault('request', self.request)
1483 self.stats.append(data)
1483 self.stats.append(data)
1484
1484
1485 def send_stats(self):
1485 def send_stats(self):
1486 tags = [
1486 tags = [
1487 ('testrun', self.request),
1487 ('testrun', self.request),
1488 ('testrun.start', self.testrun['start']),
1488 ('testrun.start', self.testrun['start']),
1489 ('testrun.timestamp', self.testrun['timestamp']),
1489 ('testrun.timestamp', self.testrun['timestamp']),
1490 ('test', self.namespace),
1490 ('test', self.namespace),
1491 ]
1491 ]
1492 for key, value in self.tags_before.items():
1492 for key, value in self.tags_before.items():
1493 tags.append((key + '.before', value))
1493 tags.append((key + '.before', value))
1494 try:
1494 try:
1495 delta = self.tags_after[key] - value
1495 delta = self.tags_after[key] - value
1496 tags.append((key + '.delta', delta))
1496 tags.append((key + '.delta', delta))
1497 except Exception:
1497 except Exception:
1498 pass
1498 pass
1499 for key, value in self.tags_after.items():
1499 for key, value in self.tags_after.items():
1500 tags.append((key + '.after', value))
1500 tags.append((key + '.after', value))
1501 self.collect({
1501 self.collect({
1502 'message': "Collected tags",
1502 'message': "Collected tags",
1503 'tags': tags,
1503 'tags': tags,
1504 })
1504 })
1505
1505
1506 response = requests.post(
1506 response = requests.post(
1507 self.url,
1507 self.url,
1508 headers={
1508 headers={
1509 'X-appenlight-api-key': self.api_key},
1509 'X-appenlight-api-key': self.api_key},
1510 json=self.stats,
1510 json=self.stats,
1511 )
1511 )
1512
1512
1513 if not response.status_code == 200:
1513 if not response.status_code == 200:
1514 pprint.pprint(self.stats)
1514 pprint.pprint(self.stats)
1515 print response.headers
1515 print response.headers
1516 print response.text
1516 print response.text
1517 raise Exception('Sending to appenlight failed')
1517 raise Exception('Sending to appenlight failed')
1518
1518
1519
1519
1520 @pytest.fixture
1520 @pytest.fixture
1521 def gist_util(request, pylonsapp):
1521 def gist_util(request, pylonsapp):
1522 """
1522 """
1523 Provides a wired instance of `GistUtility` with integrated cleanup.
1523 Provides a wired instance of `GistUtility` with integrated cleanup.
1524 """
1524 """
1525 utility = GistUtility()
1525 utility = GistUtility()
1526 request.addfinalizer(utility.cleanup)
1526 request.addfinalizer(utility.cleanup)
1527 return utility
1527 return utility
1528
1528
1529
1529
1530 class GistUtility(object):
1530 class GistUtility(object):
1531 def __init__(self):
1531 def __init__(self):
1532 self.fixture = Fixture()
1532 self.fixture = Fixture()
1533 self.gist_ids = []
1533 self.gist_ids = []
1534
1534
1535 def create_gist(self, **kwargs):
1535 def create_gist(self, **kwargs):
1536 gist = self.fixture.create_gist(**kwargs)
1536 gist = self.fixture.create_gist(**kwargs)
1537 self.gist_ids.append(gist.gist_id)
1537 self.gist_ids.append(gist.gist_id)
1538 return gist
1538 return gist
1539
1539
1540 def cleanup(self):
1540 def cleanup(self):
1541 for id_ in self.gist_ids:
1541 for id_ in self.gist_ids:
1542 self.fixture.destroy_gists(str(id_))
1542 self.fixture.destroy_gists(str(id_))
1543
1543
1544
1544
1545 @pytest.fixture
1545 @pytest.fixture
1546 def enabled_backends(request):
1546 def enabled_backends(request):
1547 backends = request.config.option.backends
1547 backends = request.config.option.backends
1548 return backends[:]
1548 return backends[:]
1549
1549
1550
1550
1551 @pytest.fixture
1551 @pytest.fixture
1552 def settings_util(request):
1552 def settings_util(request):
1553 """
1553 """
1554 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1554 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1555 """
1555 """
1556 utility = SettingsUtility()
1556 utility = SettingsUtility()
1557 request.addfinalizer(utility.cleanup)
1557 request.addfinalizer(utility.cleanup)
1558 return utility
1558 return utility
1559
1559
1560
1560
1561 class SettingsUtility(object):
1561 class SettingsUtility(object):
1562 def __init__(self):
1562 def __init__(self):
1563 self.rhodecode_ui_ids = []
1563 self.rhodecode_ui_ids = []
1564 self.rhodecode_setting_ids = []
1564 self.rhodecode_setting_ids = []
1565 self.repo_rhodecode_ui_ids = []
1565 self.repo_rhodecode_ui_ids = []
1566 self.repo_rhodecode_setting_ids = []
1566 self.repo_rhodecode_setting_ids = []
1567
1567
1568 def create_repo_rhodecode_ui(
1568 def create_repo_rhodecode_ui(
1569 self, repo, section, value, key=None, active=True, cleanup=True):
1569 self, repo, section, value, key=None, active=True, cleanup=True):
1570 key = key or hashlib.sha1(
1570 key = key or hashlib.sha1(
1571 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1571 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1572
1572
1573 setting = RepoRhodeCodeUi()
1573 setting = RepoRhodeCodeUi()
1574 setting.repository_id = repo.repo_id
1574 setting.repository_id = repo.repo_id
1575 setting.ui_section = section
1575 setting.ui_section = section
1576 setting.ui_value = value
1576 setting.ui_value = value
1577 setting.ui_key = key
1577 setting.ui_key = key
1578 setting.ui_active = active
1578 setting.ui_active = active
1579 Session().add(setting)
1579 Session().add(setting)
1580 Session().commit()
1580 Session().commit()
1581
1581
1582 if cleanup:
1582 if cleanup:
1583 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1583 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1584 return setting
1584 return setting
1585
1585
1586 def create_rhodecode_ui(
1586 def create_rhodecode_ui(
1587 self, section, value, key=None, active=True, cleanup=True):
1587 self, section, value, key=None, active=True, cleanup=True):
1588 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1588 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1589
1589
1590 setting = RhodeCodeUi()
1590 setting = RhodeCodeUi()
1591 setting.ui_section = section
1591 setting.ui_section = section
1592 setting.ui_value = value
1592 setting.ui_value = value
1593 setting.ui_key = key
1593 setting.ui_key = key
1594 setting.ui_active = active
1594 setting.ui_active = active
1595 Session().add(setting)
1595 Session().add(setting)
1596 Session().commit()
1596 Session().commit()
1597
1597
1598 if cleanup:
1598 if cleanup:
1599 self.rhodecode_ui_ids.append(setting.ui_id)
1599 self.rhodecode_ui_ids.append(setting.ui_id)
1600 return setting
1600 return setting
1601
1601
1602 def create_repo_rhodecode_setting(
1602 def create_repo_rhodecode_setting(
1603 self, repo, name, value, type_, cleanup=True):
1603 self, repo, name, value, type_, cleanup=True):
1604 setting = RepoRhodeCodeSetting(
1604 setting = RepoRhodeCodeSetting(
1605 repo.repo_id, key=name, val=value, type=type_)
1605 repo.repo_id, key=name, val=value, type=type_)
1606 Session().add(setting)
1606 Session().add(setting)
1607 Session().commit()
1607 Session().commit()
1608
1608
1609 if cleanup:
1609 if cleanup:
1610 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1610 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1611 return setting
1611 return setting
1612
1612
1613 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1613 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1614 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1614 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1615 Session().add(setting)
1615 Session().add(setting)
1616 Session().commit()
1616 Session().commit()
1617
1617
1618 if cleanup:
1618 if cleanup:
1619 self.rhodecode_setting_ids.append(setting.app_settings_id)
1619 self.rhodecode_setting_ids.append(setting.app_settings_id)
1620
1620
1621 return setting
1621 return setting
1622
1622
1623 def cleanup(self):
1623 def cleanup(self):
1624 for id_ in self.rhodecode_ui_ids:
1624 for id_ in self.rhodecode_ui_ids:
1625 setting = RhodeCodeUi.get(id_)
1625 setting = RhodeCodeUi.get(id_)
1626 Session().delete(setting)
1626 Session().delete(setting)
1627
1627
1628 for id_ in self.rhodecode_setting_ids:
1628 for id_ in self.rhodecode_setting_ids:
1629 setting = RhodeCodeSetting.get(id_)
1629 setting = RhodeCodeSetting.get(id_)
1630 Session().delete(setting)
1630 Session().delete(setting)
1631
1631
1632 for id_ in self.repo_rhodecode_ui_ids:
1632 for id_ in self.repo_rhodecode_ui_ids:
1633 setting = RepoRhodeCodeUi.get(id_)
1633 setting = RepoRhodeCodeUi.get(id_)
1634 Session().delete(setting)
1634 Session().delete(setting)
1635
1635
1636 for id_ in self.repo_rhodecode_setting_ids:
1636 for id_ in self.repo_rhodecode_setting_ids:
1637 setting = RepoRhodeCodeSetting.get(id_)
1637 setting = RepoRhodeCodeSetting.get(id_)
1638 Session().delete(setting)
1638 Session().delete(setting)
1639
1639
1640 Session().commit()
1640 Session().commit()
1641
1641
1642
1642
1643 @pytest.fixture
1643 @pytest.fixture
1644 def no_notifications(request):
1644 def no_notifications(request):
1645 notification_patcher = mock.patch(
1645 notification_patcher = mock.patch(
1646 'rhodecode.model.notification.NotificationModel.create')
1646 'rhodecode.model.notification.NotificationModel.create')
1647 notification_patcher.start()
1647 notification_patcher.start()
1648 request.addfinalizer(notification_patcher.stop)
1648 request.addfinalizer(notification_patcher.stop)
1649
1649
1650
1650
1651 @pytest.fixture(scope='session')
1651 @pytest.fixture(scope='session')
1652 def repeat(request):
1652 def repeat(request):
1653 """
1653 """
1654 The number of repetitions is based on this fixture.
1654 The number of repetitions is based on this fixture.
1655
1655
1656 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1656 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1657 tests are not too slow in our default test suite.
1657 tests are not too slow in our default test suite.
1658 """
1658 """
1659 return request.config.getoption('--repeat')
1659 return request.config.getoption('--repeat')
1660
1660
1661
1661
1662 @pytest.fixture
1662 @pytest.fixture
1663 def rhodecode_fixtures():
1663 def rhodecode_fixtures():
1664 return Fixture()
1664 return Fixture()
1665
1665
1666
1666
1667 @pytest.fixture
1667 @pytest.fixture
1668 def request_stub():
1668 def request_stub():
1669 """
1669 """
1670 Stub request object.
1670 Stub request object.
1671 """
1671 """
1672 request = pyramid.testing.DummyRequest()
1672 from rhodecode.lib.base import bootstrap_request
1673 request.scheme = 'https'
1673 request = bootstrap_request(scheme='https')
1674 return request
1674 return request
1675
1675
1676
1676
1677 @pytest.fixture
1677 @pytest.fixture
1678 def context_stub():
1678 def context_stub():
1679 """
1679 """
1680 Stub context object.
1680 Stub context object.
1681 """
1681 """
1682 context = pyramid.testing.DummyResource()
1682 context = pyramid.testing.DummyResource()
1683 return context
1683 return context
1684
1684
1685
1685
1686 @pytest.fixture
1686 @pytest.fixture
1687 def config_stub(request, request_stub):
1687 def config_stub(request, request_stub):
1688 """
1688 """
1689 Set up pyramid.testing and return the Configurator.
1689 Set up pyramid.testing and return the Configurator.
1690 """
1690 """
1691 config = pyramid.testing.setUp(request=request_stub)
1691 config = pyramid.testing.setUp(request=request_stub)
1692 add_test_routes(config)
1692 add_test_routes(config)
1693
1693
1694 @request.addfinalizer
1694 @request.addfinalizer
1695 def cleanup():
1695 def cleanup():
1696 pyramid.testing.tearDown()
1696 pyramid.testing.tearDown()
1697
1697
1698 return config
1698 return config
1699
1699
1700
1700
1701 @pytest.fixture
1701 @pytest.fixture
1702 def StubIntegrationType():
1702 def StubIntegrationType():
1703 class _StubIntegrationType(IntegrationTypeBase):
1703 class _StubIntegrationType(IntegrationTypeBase):
1704 """ Test integration type class """
1704 """ Test integration type class """
1705
1705
1706 key = 'test'
1706 key = 'test'
1707 display_name = 'Test integration type'
1707 display_name = 'Test integration type'
1708 description = 'A test integration type for testing'
1708 description = 'A test integration type for testing'
1709 icon = 'test_icon_html_image'
1709 icon = 'test_icon_html_image'
1710
1710
1711 def __init__(self, settings):
1711 def __init__(self, settings):
1712 super(_StubIntegrationType, self).__init__(settings)
1712 super(_StubIntegrationType, self).__init__(settings)
1713 self.sent_events = [] # for testing
1713 self.sent_events = [] # for testing
1714
1714
1715 def send_event(self, event):
1715 def send_event(self, event):
1716 self.sent_events.append(event)
1716 self.sent_events.append(event)
1717
1717
1718 def settings_schema(self):
1718 def settings_schema(self):
1719 class SettingsSchema(colander.Schema):
1719 class SettingsSchema(colander.Schema):
1720 test_string_field = colander.SchemaNode(
1720 test_string_field = colander.SchemaNode(
1721 colander.String(),
1721 colander.String(),
1722 missing=colander.required,
1722 missing=colander.required,
1723 title='test string field',
1723 title='test string field',
1724 )
1724 )
1725 test_int_field = colander.SchemaNode(
1725 test_int_field = colander.SchemaNode(
1726 colander.Int(),
1726 colander.Int(),
1727 title='some integer setting',
1727 title='some integer setting',
1728 )
1728 )
1729 return SettingsSchema()
1729 return SettingsSchema()
1730
1730
1731
1731
1732 integration_type_registry.register_integration_type(_StubIntegrationType)
1732 integration_type_registry.register_integration_type(_StubIntegrationType)
1733 return _StubIntegrationType
1733 return _StubIntegrationType
1734
1734
1735 @pytest.fixture
1735 @pytest.fixture
1736 def stub_integration_settings():
1736 def stub_integration_settings():
1737 return {
1737 return {
1738 'test_string_field': 'some data',
1738 'test_string_field': 'some data',
1739 'test_int_field': 100,
1739 'test_int_field': 100,
1740 }
1740 }
1741
1741
1742
1742
1743 @pytest.fixture
1743 @pytest.fixture
1744 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1744 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1745 stub_integration_settings):
1745 stub_integration_settings):
1746 integration = IntegrationModel().create(
1746 integration = IntegrationModel().create(
1747 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1747 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1748 name='test repo integration',
1748 name='test repo integration',
1749 repo=repo_stub, repo_group=None, child_repos_only=None)
1749 repo=repo_stub, repo_group=None, child_repos_only=None)
1750
1750
1751 @request.addfinalizer
1751 @request.addfinalizer
1752 def cleanup():
1752 def cleanup():
1753 IntegrationModel().delete(integration)
1753 IntegrationModel().delete(integration)
1754
1754
1755 return integration
1755 return integration
1756
1756
1757
1757
1758 @pytest.fixture
1758 @pytest.fixture
1759 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1759 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1760 stub_integration_settings):
1760 stub_integration_settings):
1761 integration = IntegrationModel().create(
1761 integration = IntegrationModel().create(
1762 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1762 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1763 name='test repogroup integration',
1763 name='test repogroup integration',
1764 repo=None, repo_group=test_repo_group, child_repos_only=True)
1764 repo=None, repo_group=test_repo_group, child_repos_only=True)
1765
1765
1766 @request.addfinalizer
1766 @request.addfinalizer
1767 def cleanup():
1767 def cleanup():
1768 IntegrationModel().delete(integration)
1768 IntegrationModel().delete(integration)
1769
1769
1770 return integration
1770 return integration
1771
1771
1772
1772
1773 @pytest.fixture
1773 @pytest.fixture
1774 def repogroup_recursive_integration_stub(request, test_repo_group,
1774 def repogroup_recursive_integration_stub(request, test_repo_group,
1775 StubIntegrationType, stub_integration_settings):
1775 StubIntegrationType, stub_integration_settings):
1776 integration = IntegrationModel().create(
1776 integration = IntegrationModel().create(
1777 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1777 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1778 name='test recursive repogroup integration',
1778 name='test recursive repogroup integration',
1779 repo=None, repo_group=test_repo_group, child_repos_only=False)
1779 repo=None, repo_group=test_repo_group, child_repos_only=False)
1780
1780
1781 @request.addfinalizer
1781 @request.addfinalizer
1782 def cleanup():
1782 def cleanup():
1783 IntegrationModel().delete(integration)
1783 IntegrationModel().delete(integration)
1784
1784
1785 return integration
1785 return integration
1786
1786
1787
1787
1788 @pytest.fixture
1788 @pytest.fixture
1789 def global_integration_stub(request, StubIntegrationType,
1789 def global_integration_stub(request, StubIntegrationType,
1790 stub_integration_settings):
1790 stub_integration_settings):
1791 integration = IntegrationModel().create(
1791 integration = IntegrationModel().create(
1792 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1792 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1793 name='test global integration',
1793 name='test global integration',
1794 repo=None, repo_group=None, child_repos_only=None)
1794 repo=None, repo_group=None, child_repos_only=None)
1795
1795
1796 @request.addfinalizer
1796 @request.addfinalizer
1797 def cleanup():
1797 def cleanup():
1798 IntegrationModel().delete(integration)
1798 IntegrationModel().delete(integration)
1799
1799
1800 return integration
1800 return integration
1801
1801
1802
1802
1803 @pytest.fixture
1803 @pytest.fixture
1804 def root_repos_integration_stub(request, StubIntegrationType,
1804 def root_repos_integration_stub(request, StubIntegrationType,
1805 stub_integration_settings):
1805 stub_integration_settings):
1806 integration = IntegrationModel().create(
1806 integration = IntegrationModel().create(
1807 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1807 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1808 name='test global integration',
1808 name='test global integration',
1809 repo=None, repo_group=None, child_repos_only=True)
1809 repo=None, repo_group=None, child_repos_only=True)
1810
1810
1811 @request.addfinalizer
1811 @request.addfinalizer
1812 def cleanup():
1812 def cleanup():
1813 IntegrationModel().delete(integration)
1813 IntegrationModel().delete(integration)
1814
1814
1815 return integration
1815 return integration
1816
1816
1817
1817
1818 @pytest.fixture
1818 @pytest.fixture
1819 def local_dt_to_utc():
1819 def local_dt_to_utc():
1820 def _factory(dt):
1820 def _factory(dt):
1821 return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone(
1821 return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone(
1822 dateutil.tz.tzutc()).replace(tzinfo=None)
1822 dateutil.tz.tzutc()).replace(tzinfo=None)
1823 return _factory
1823 return _factory
1824
1824
1825
1825
1826 @pytest.fixture
1826 @pytest.fixture
1827 def disable_anonymous_user(request, pylonsapp):
1827 def disable_anonymous_user(request, pylonsapp):
1828 set_anonymous_access(False)
1828 set_anonymous_access(False)
1829
1829
1830 @request.addfinalizer
1830 @request.addfinalizer
1831 def cleanup():
1831 def cleanup():
1832 set_anonymous_access(True)
1832 set_anonymous_access(True)
General Comments 0
You need to be logged in to leave comments. Login now