##// END OF EJS Templates
error-reporting: fix setting call_context on dummy-request available in sentry
super-admin -
r4865:2ecd5d81 default
parent child Browse files
Show More
@@ -1,627 +1,635 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36
37 37 import rhodecode
38 38 from rhodecode.apps._base import TemplateArgs
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 (password_changed, get_enabled_hook_classes)
45 45 from rhodecode.lib.utils2 import (
46 46 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
47 47 from rhodecode.model.db import Repository, User, ChangesetComment, UserBookmark
48 48 from rhodecode.model.notification import NotificationModel
49 49 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 def _filter_proxy(ip):
55 55 """
56 56 Passed in IP addresses in HEADERS can be in a special format of multiple
57 57 ips. Those comma separated IPs are passed from various proxies in the
58 58 chain of request processing. The left-most being the original client.
59 59 We only care about the first IP which came from the org. client.
60 60
61 61 :param ip: ip string from headers
62 62 """
63 63 if ',' in ip:
64 64 _ips = ip.split(',')
65 65 _first_ip = _ips[0].strip()
66 66 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
67 67 return _first_ip
68 68 return ip
69 69
70 70
71 71 def _filter_port(ip):
72 72 """
73 73 Removes a port from ip, there are 4 main cases to handle here.
74 74 - ipv4 eg. 127.0.0.1
75 75 - ipv6 eg. ::1
76 76 - ipv4+port eg. 127.0.0.1:8080
77 77 - ipv6+port eg. [::1]:8080
78 78
79 79 :param ip:
80 80 """
81 81 def is_ipv6(ip_addr):
82 82 if hasattr(socket, 'inet_pton'):
83 83 try:
84 84 socket.inet_pton(socket.AF_INET6, ip_addr)
85 85 except socket.error:
86 86 return False
87 87 else:
88 88 # fallback to ipaddress
89 89 try:
90 90 ipaddress.IPv6Address(safe_unicode(ip_addr))
91 91 except Exception:
92 92 return False
93 93 return True
94 94
95 95 if ':' not in ip: # must be ipv4 pure ip
96 96 return ip
97 97
98 98 if '[' in ip and ']' in ip: # ipv6 with port
99 99 return ip.split(']')[0][1:].lower()
100 100
101 101 # must be ipv6 or ipv4 with port
102 102 if is_ipv6(ip):
103 103 return ip
104 104 else:
105 105 ip, _port = ip.split(':')[:2] # means ipv4+port
106 106 return ip
107 107
108 108
109 109 def get_ip_addr(environ):
110 110 proxy_key = 'HTTP_X_REAL_IP'
111 111 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
112 112 def_key = 'REMOTE_ADDR'
113 113 _filters = lambda x: _filter_port(_filter_proxy(x))
114 114
115 115 ip = environ.get(proxy_key)
116 116 if ip:
117 117 return _filters(ip)
118 118
119 119 ip = environ.get(proxy_key2)
120 120 if ip:
121 121 return _filters(ip)
122 122
123 123 ip = environ.get(def_key, '0.0.0.0')
124 124 return _filters(ip)
125 125
126 126
127 127 def get_server_ip_addr(environ, log_errors=True):
128 128 hostname = environ.get('SERVER_NAME')
129 129 try:
130 130 return socket.gethostbyname(hostname)
131 131 except Exception as e:
132 132 if log_errors:
133 133 # in some cases this lookup is not possible, and we don't want to
134 134 # make it an exception in logs
135 135 log.exception('Could not retrieve server ip address: %s', e)
136 136 return hostname
137 137
138 138
139 139 def get_server_port(environ):
140 140 return environ.get('SERVER_PORT')
141 141
142 142
143 143 def get_access_path(environ):
144 144 path = environ.get('PATH_INFO')
145 145 org_req = environ.get('pylons.original_request')
146 146 if org_req:
147 147 path = org_req.environ.get('PATH_INFO')
148 148 return path
149 149
150 150
151 151 def get_user_agent(environ):
152 152 return environ.get('HTTP_USER_AGENT')
153 153
154 154
155 155 def vcs_operation_context(
156 156 environ, repo_name, username, action, scm, check_locking=True,
157 157 is_shadow_repo=False, check_branch_perms=False, detect_force_push=False):
158 158 """
159 159 Generate the context for a vcs operation, e.g. push or pull.
160 160
161 161 This context is passed over the layers so that hooks triggered by the
162 162 vcs operation know details like the user, the user's IP address etc.
163 163
164 164 :param check_locking: Allows to switch of the computation of the locking
165 165 data. This serves mainly the need of the simplevcs middleware to be
166 166 able to disable this for certain operations.
167 167
168 168 """
169 169 # Tri-state value: False: unlock, None: nothing, True: lock
170 170 make_lock = None
171 171 locked_by = [None, None, None]
172 172 is_anonymous = username == User.DEFAULT_USER
173 173 user = User.get_by_username(username)
174 174 if not is_anonymous and check_locking:
175 175 log.debug('Checking locking on repository "%s"', repo_name)
176 176 repo = Repository.get_by_repo_name(repo_name)
177 177 make_lock, __, locked_by = repo.get_locking_state(
178 178 action, user.user_id)
179 179 user_id = user.user_id
180 180 settings_model = VcsSettingsModel(repo=repo_name)
181 181 ui_settings = settings_model.get_ui_settings()
182 182
183 183 # NOTE(marcink): This should be also in sync with
184 184 # rhodecode/apps/ssh_support/lib/backends/base.py:update_environment scm_data
185 185 store = [x for x in ui_settings if x.key == '/']
186 186 repo_store = ''
187 187 if store:
188 188 repo_store = store[0].value
189 189
190 190 scm_data = {
191 191 'ip': get_ip_addr(environ),
192 192 'username': username,
193 193 'user_id': user_id,
194 194 'action': action,
195 195 'repository': repo_name,
196 196 'scm': scm,
197 197 'config': rhodecode.CONFIG['__file__'],
198 198 'repo_store': repo_store,
199 199 'make_lock': make_lock,
200 200 'locked_by': locked_by,
201 201 'server_url': utils2.get_server_url(environ),
202 202 'user_agent': get_user_agent(environ),
203 203 'hooks': get_enabled_hook_classes(ui_settings),
204 204 'is_shadow_repo': is_shadow_repo,
205 205 'detect_force_push': detect_force_push,
206 206 'check_branch_perms': check_branch_perms,
207 207 }
208 208 return scm_data
209 209
210 210
211 211 class BasicAuth(AuthBasicAuthenticator):
212 212
213 213 def __init__(self, realm, authfunc, registry, auth_http_code=None,
214 214 initial_call_detection=False, acl_repo_name=None, rc_realm=''):
215 215 self.realm = realm
216 216 self.rc_realm = rc_realm
217 217 self.initial_call = initial_call_detection
218 218 self.authfunc = authfunc
219 219 self.registry = registry
220 220 self.acl_repo_name = acl_repo_name
221 221 self._rc_auth_http_code = auth_http_code
222 222
223 223 def _get_response_from_code(self, http_code):
224 224 try:
225 225 return get_exception(safe_int(http_code))
226 226 except Exception:
227 227 log.exception('Failed to fetch response for code %s', http_code)
228 228 return HTTPForbidden
229 229
230 230 def get_rc_realm(self):
231 231 return safe_str(self.rc_realm)
232 232
233 233 def build_authentication(self):
234 234 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
235 235 if self._rc_auth_http_code and not self.initial_call:
236 236 # return alternative HTTP code if alternative http return code
237 237 # is specified in RhodeCode config, but ONLY if it's not the
238 238 # FIRST call
239 239 custom_response_klass = self._get_response_from_code(
240 240 self._rc_auth_http_code)
241 241 return custom_response_klass(headers=head)
242 242 return HTTPUnauthorized(headers=head)
243 243
244 244 def authenticate(self, environ):
245 245 authorization = AUTHORIZATION(environ)
246 246 if not authorization:
247 247 return self.build_authentication()
248 248 (authmeth, auth) = authorization.split(' ', 1)
249 249 if 'basic' != authmeth.lower():
250 250 return self.build_authentication()
251 251 auth = auth.strip().decode('base64')
252 252 _parts = auth.split(':', 1)
253 253 if len(_parts) == 2:
254 254 username, password = _parts
255 255 auth_data = self.authfunc(
256 256 username, password, environ, VCS_TYPE,
257 257 registry=self.registry, acl_repo_name=self.acl_repo_name)
258 258 if auth_data:
259 259 return {'username': username, 'auth_data': auth_data}
260 260 if username and password:
261 261 # we mark that we actually executed authentication once, at
262 262 # that point we can use the alternative auth code
263 263 self.initial_call = False
264 264
265 265 return self.build_authentication()
266 266
267 267 __call__ = authenticate
268 268
269 269
270 270 def calculate_version_hash(config):
271 271 return sha1(
272 272 config.get('beaker.session.secret', '') +
273 273 rhodecode.__version__)[:8]
274 274
275 275
276 276 def get_current_lang(request):
277 277 # NOTE(marcink): remove after pyramid move
278 278 try:
279 279 return translation.get_lang()[0]
280 280 except:
281 281 pass
282 282
283 283 return getattr(request, '_LOCALE_', request.locale_name)
284 284
285 285
286 286 def attach_context_attributes(context, request, user_id=None, is_api=None):
287 287 """
288 288 Attach variables into template context called `c`.
289 289 """
290 290 config = request.registry.settings
291 291
292 292 rc_config = SettingsModel().get_all_settings(cache=True, from_request=False)
293 293 context.rc_config = rc_config
294 294 context.rhodecode_version = rhodecode.__version__
295 295 context.rhodecode_edition = config.get('rhodecode.edition')
296 296 context.rhodecode_edition_id = config.get('rhodecode.edition_id')
297 297 # unique secret + version does not leak the version but keep consistency
298 298 context.rhodecode_version_hash = calculate_version_hash(config)
299 299
300 300 # Default language set for the incoming request
301 301 context.language = get_current_lang(request)
302 302
303 303 # Visual options
304 304 context.visual = AttributeDict({})
305 305
306 306 # DB stored Visual Items
307 307 context.visual.show_public_icon = str2bool(
308 308 rc_config.get('rhodecode_show_public_icon'))
309 309 context.visual.show_private_icon = str2bool(
310 310 rc_config.get('rhodecode_show_private_icon'))
311 311 context.visual.stylify_metatags = str2bool(
312 312 rc_config.get('rhodecode_stylify_metatags'))
313 313 context.visual.dashboard_items = safe_int(
314 314 rc_config.get('rhodecode_dashboard_items', 100))
315 315 context.visual.admin_grid_items = safe_int(
316 316 rc_config.get('rhodecode_admin_grid_items', 100))
317 317 context.visual.show_revision_number = str2bool(
318 318 rc_config.get('rhodecode_show_revision_number', True))
319 319 context.visual.show_sha_length = safe_int(
320 320 rc_config.get('rhodecode_show_sha_length', 100))
321 321 context.visual.repository_fields = str2bool(
322 322 rc_config.get('rhodecode_repository_fields'))
323 323 context.visual.show_version = str2bool(
324 324 rc_config.get('rhodecode_show_version'))
325 325 context.visual.use_gravatar = str2bool(
326 326 rc_config.get('rhodecode_use_gravatar'))
327 327 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
328 328 context.visual.default_renderer = rc_config.get(
329 329 'rhodecode_markup_renderer', 'rst')
330 330 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
331 331 context.visual.rhodecode_support_url = \
332 332 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
333 333
334 334 context.visual.affected_files_cut_off = 60
335 335
336 336 context.pre_code = rc_config.get('rhodecode_pre_code')
337 337 context.post_code = rc_config.get('rhodecode_post_code')
338 338 context.rhodecode_name = rc_config.get('rhodecode_title')
339 339 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
340 340 # if we have specified default_encoding in the request, it has more
341 341 # priority
342 342 if request.GET.get('default_encoding'):
343 343 context.default_encodings.insert(0, request.GET.get('default_encoding'))
344 344 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
345 345 context.clone_uri_id_tmpl = rc_config.get('rhodecode_clone_uri_id_tmpl')
346 346 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
347 347
348 348 # INI stored
349 349 context.labs_active = str2bool(
350 350 config.get('labs_settings_active', 'false'))
351 351 context.ssh_enabled = str2bool(
352 352 config.get('ssh.generate_authorized_keyfile', 'false'))
353 353 context.ssh_key_generator_enabled = str2bool(
354 354 config.get('ssh.enable_ui_key_generator', 'true'))
355 355
356 356 context.visual.allow_repo_location_change = str2bool(
357 357 config.get('allow_repo_location_change', True))
358 358 context.visual.allow_custom_hooks_settings = str2bool(
359 359 config.get('allow_custom_hooks_settings', True))
360 360 context.debug_style = str2bool(config.get('debug_style', False))
361 361
362 362 context.rhodecode_instanceid = config.get('instance_id')
363 363
364 364 context.visual.cut_off_limit_diff = safe_int(
365 365 config.get('cut_off_limit_diff'))
366 366 context.visual.cut_off_limit_file = safe_int(
367 367 config.get('cut_off_limit_file'))
368 368
369 369 context.license = AttributeDict({})
370 370 context.license.hide_license_info = str2bool(
371 371 config.get('license.hide_license_info', False))
372 372
373 373 # AppEnlight
374 374 context.appenlight_enabled = config.get('appenlight', False)
375 375 context.appenlight_api_public_key = config.get(
376 376 'appenlight.api_public_key', '')
377 377 context.appenlight_server_url = config.get('appenlight.server_url', '')
378 378
379 379 diffmode = {
380 380 "unified": "unified",
381 381 "sideside": "sideside"
382 382 }.get(request.GET.get('diffmode'))
383 383
384 384 if is_api is not None:
385 385 is_api = hasattr(request, 'rpc_user')
386 386 session_attrs = {
387 387 # defaults
388 388 "clone_url_format": "http",
389 389 "diffmode": "sideside",
390 390 "license_fingerprint": request.session.get('license_fingerprint')
391 391 }
392 392
393 393 if not is_api:
394 394 # don't access pyramid session for API calls
395 395 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
396 396 request.session['rc_user_session_attr.diffmode'] = diffmode
397 397
398 398 # session settings per user
399 399
400 400 for k, v in request.session.items():
401 401 pref = 'rc_user_session_attr.'
402 402 if k and k.startswith(pref):
403 403 k = k[len(pref):]
404 404 session_attrs[k] = v
405 405
406 406 context.user_session_attrs = session_attrs
407 407
408 408 # JS template context
409 409 context.template_context = {
410 410 'repo_name': None,
411 411 'repo_type': None,
412 412 'repo_landing_commit': None,
413 413 'rhodecode_user': {
414 414 'username': None,
415 415 'email': None,
416 416 'notification_status': False
417 417 },
418 418 'session_attrs': session_attrs,
419 419 'visual': {
420 420 'default_renderer': None
421 421 },
422 422 'commit_data': {
423 423 'commit_id': None
424 424 },
425 425 'pull_request_data': {'pull_request_id': None},
426 426 'timeago': {
427 427 'refresh_time': 120 * 1000,
428 428 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
429 429 },
430 430 'pyramid_dispatch': {
431 431
432 432 },
433 433 'extra': {'plugins': {}}
434 434 }
435 435 # END CONFIG VARS
436 436 if is_api:
437 437 csrf_token = None
438 438 else:
439 439 csrf_token = auth.get_csrf_token(session=request.session)
440 440
441 441 context.csrf_token = csrf_token
442 442 context.backends = rhodecode.BACKENDS.keys()
443 443
444 444 unread_count = 0
445 445 user_bookmark_list = []
446 446 if user_id:
447 447 unread_count = NotificationModel().get_unread_cnt_for_user(user_id)
448 448 user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id)
449 449 context.unread_notifications = unread_count
450 450 context.bookmark_items = user_bookmark_list
451 451
452 452 # web case
453 453 if hasattr(request, 'user'):
454 454 context.auth_user = request.user
455 455 context.rhodecode_user = request.user
456 456
457 457 # api case
458 458 if hasattr(request, 'rpc_user'):
459 459 context.auth_user = request.rpc_user
460 460 context.rhodecode_user = request.rpc_user
461 461
462 462 # attach the whole call context to the request
463 request.call_context = context
463 # use set_call_context method if request has it
464 # sometimes in Celery context requests is "different"
465 if hasattr(request, 'set_call_context'):
466 request.set_call_context(context)
467 else:
468 request.call_context = context
464 469
465 470
466 471 def get_auth_user(request):
467 472 environ = request.environ
468 473 session = request.session
469 474
470 475 ip_addr = get_ip_addr(environ)
471 476
472 477 # make sure that we update permissions each time we call controller
473 478 _auth_token = (
474 479 # ?auth_token=XXX
475 480 request.GET.get('auth_token', '')
476 481 # ?api_key=XXX !LEGACY
477 482 or request.GET.get('api_key', '')
478 483 # or headers....
479 484 or request.headers.get('X-Rc-Auth-Token', '')
480 485 )
481 486 if not _auth_token and request.matchdict:
482 487 url_auth_token = request.matchdict.get('_auth_token')
483 488 _auth_token = url_auth_token
484 489 if _auth_token:
485 490 log.debug('Using URL extracted auth token `...%s`', _auth_token[-4:])
486 491
487 492 if _auth_token:
488 493 # when using API_KEY we assume user exists, and
489 494 # doesn't need auth based on cookies.
490 495 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
491 496 authenticated = False
492 497 else:
493 498 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
494 499 try:
495 500 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
496 501 ip_addr=ip_addr)
497 502 except UserCreationError as e:
498 503 h.flash(e, 'error')
499 504 # container auth or other auth functions that create users
500 505 # on the fly can throw this exception signaling that there's
501 506 # issue with user creation, explanation should be provided
502 507 # in Exception itself. We then create a simple blank
503 508 # AuthUser
504 509 auth_user = AuthUser(ip_addr=ip_addr)
505 510
506 511 # in case someone changes a password for user it triggers session
507 512 # flush and forces a re-login
508 513 if password_changed(auth_user, session):
509 514 session.invalidate()
510 515 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
511 516 auth_user = AuthUser(ip_addr=ip_addr)
512 517
513 518 authenticated = cookie_store.get('is_authenticated')
514 519
515 520 if not auth_user.is_authenticated and auth_user.is_user_object:
516 521 # user is not authenticated and not empty
517 522 auth_user.set_authenticated(authenticated)
518 523
519 524 return auth_user, _auth_token
520 525
521 526
522 527 def h_filter(s):
523 528 """
524 529 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
525 530 we wrap this with additional functionality that converts None to empty
526 531 strings
527 532 """
528 533 if s is None:
529 534 return markupsafe.Markup()
530 535 return markupsafe.escape(s)
531 536
532 537
533 538 def add_events_routes(config):
534 539 """
535 540 Adds routing that can be used in events. Because some events are triggered
536 541 outside of pyramid context, we need to bootstrap request with some
537 542 routing registered
538 543 """
539 544
540 545 from rhodecode.apps._base import ADMIN_PREFIX
541 546
542 547 config.add_route(name='home', pattern='/')
543 548 config.add_route(name='main_page_repos_data', pattern='/_home_repos')
544 549 config.add_route(name='main_page_repo_groups_data', pattern='/_home_repo_groups')
545 550
546 551 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
547 552 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
548 553 config.add_route(name='repo_summary', pattern='/{repo_name}')
549 554 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
550 555 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
551 556
552 557 config.add_route(name='pullrequest_show',
553 558 pattern='/{repo_name}/pull-request/{pull_request_id}')
554 559 config.add_route(name='pull_requests_global',
555 560 pattern='/pull-request/{pull_request_id}')
556 561
557 562 config.add_route(name='repo_commit',
558 563 pattern='/{repo_name}/changeset/{commit_id}')
559 564 config.add_route(name='repo_files',
560 565 pattern='/{repo_name}/files/{commit_id}/{f_path}')
561 566
562 567 config.add_route(name='hovercard_user',
563 568 pattern='/_hovercard/user/{user_id}')
564 569
565 570 config.add_route(name='hovercard_user_group',
566 571 pattern='/_hovercard/user_group/{user_group_id}')
567 572
568 573 config.add_route(name='hovercard_pull_request',
569 574 pattern='/_hovercard/pull_request/{pull_request_id}')
570 575
571 576 config.add_route(name='hovercard_repo_commit',
572 577 pattern='/_hovercard/commit/{repo_name}/{commit_id}')
573 578
574 579
575 580 def bootstrap_config(request):
576 581 import pyramid.testing
577 582 registry = pyramid.testing.Registry('RcTestRegistry')
578 583
579 584 config = pyramid.testing.setUp(registry=registry, request=request)
580 585
581 586 # allow pyramid lookup in testing
582 587 config.include('pyramid_mako')
583 588 config.include('rhodecode.lib.rc_beaker')
584 589 config.include('rhodecode.lib.rc_cache')
585 590
586 591 add_events_routes(config)
587 592
588 593 return config
589 594
590 595
591 596 def bootstrap_request(**kwargs):
592 597 import pyramid.testing
593 598
594 599 class TestRequest(pyramid.testing.DummyRequest):
595 600 application_url = kwargs.pop('application_url', 'http://example.com')
596 601 host = kwargs.pop('host', 'example.com:80')
597 602 domain = kwargs.pop('domain', 'example.com')
598 603
599 604 def translate(self, msg):
600 605 return msg
601 606
602 607 def plularize(self, singular, plural, n):
603 608 return singular
604 609
605 610 def get_partial_renderer(self, tmpl_name):
606 611
607 612 from rhodecode.lib.partial_renderer import get_partial_renderer
608 613 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
609 614
610 615 _call_context = TemplateArgs()
611 616 _call_context.visual = TemplateArgs()
612 617 _call_context.visual.show_sha_length = 12
613 618 _call_context.visual.show_revision_number = True
614 619
615 620 @property
616 621 def call_context(self):
617 622 return self._call_context
618 623
624 def set_call_context(self, new_context):
625 self._call_context = new_context
626
619 627 class TestDummySession(pyramid.testing.DummySession):
620 628 def save(*arg, **kw):
621 629 pass
622 630
623 631 request = TestRequest(**kwargs)
624 632 request.session = TestDummySession()
625 633
626 634 return request
627 635
General Comments 0
You need to be logged in to leave comments. Login now