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