##// END OF EJS Templates
pyramid: move language extraction into a seperate method.
marcink -
r1904:1a7936c3 default
parent child Browse files
Show More
@@ -1,607 +1,617 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 ipaddress
31 31 import pyramid.threadlocal
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 from pylons import config, tmpl_context as c, request, session, url
37 37 from pylons.controllers import WSGIController
38 38 from pylons.controllers.util import redirect
39 39 from pylons.i18n import translation
40 40 # marcink: don't remove this import
41 41 from pylons.templating import render_mako as render # noqa
42 42 from pylons.i18n.translation import _
43 43 from webob.exc import HTTPFound
44 44
45 45
46 46 import rhodecode
47 47 from rhodecode.authentication.base import VCS_TYPE
48 48 from rhodecode.lib import auth, utils2
49 49 from rhodecode.lib import helpers as h
50 50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 51 from rhodecode.lib.exceptions import UserCreationError
52 52 from rhodecode.lib.utils import (
53 53 get_repo_slug, set_rhodecode_config, password_changed,
54 54 get_enabled_hook_classes)
55 55 from rhodecode.lib.utils2 import (
56 56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 58 from rhodecode.model import meta
59 59 from rhodecode.model.db import Repository, User, ChangesetComment
60 60 from rhodecode.model.notification import NotificationModel
61 61 from rhodecode.model.scm import ScmModel
62 62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 def _filter_proxy(ip):
69 69 """
70 70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 71 ips. Those comma separated IPs are passed from various proxies in the
72 72 chain of request processing. The left-most being the original client.
73 73 We only care about the first IP which came from the org. client.
74 74
75 75 :param ip: ip string from headers
76 76 """
77 77 if ',' in ip:
78 78 _ips = ip.split(',')
79 79 _first_ip = _ips[0].strip()
80 80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 81 return _first_ip
82 82 return ip
83 83
84 84
85 85 def _filter_port(ip):
86 86 """
87 87 Removes a port from ip, there are 4 main cases to handle here.
88 88 - ipv4 eg. 127.0.0.1
89 89 - ipv6 eg. ::1
90 90 - ipv4+port eg. 127.0.0.1:8080
91 91 - ipv6+port eg. [::1]:8080
92 92
93 93 :param ip:
94 94 """
95 95 def is_ipv6(ip_addr):
96 96 if hasattr(socket, 'inet_pton'):
97 97 try:
98 98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 99 except socket.error:
100 100 return False
101 101 else:
102 102 # fallback to ipaddress
103 103 try:
104 104 ipaddress.IPv6Address(ip_addr)
105 105 except Exception:
106 106 return False
107 107 return True
108 108
109 109 if ':' not in ip: # must be ipv4 pure ip
110 110 return ip
111 111
112 112 if '[' in ip and ']' in ip: # ipv6 with port
113 113 return ip.split(']')[0][1:].lower()
114 114
115 115 # must be ipv6 or ipv4 with port
116 116 if is_ipv6(ip):
117 117 return ip
118 118 else:
119 119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 120 return ip
121 121
122 122
123 123 def get_ip_addr(environ):
124 124 proxy_key = 'HTTP_X_REAL_IP'
125 125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 126 def_key = 'REMOTE_ADDR'
127 127 _filters = lambda x: _filter_port(_filter_proxy(x))
128 128
129 129 ip = environ.get(proxy_key)
130 130 if ip:
131 131 return _filters(ip)
132 132
133 133 ip = environ.get(proxy_key2)
134 134 if ip:
135 135 return _filters(ip)
136 136
137 137 ip = environ.get(def_key, '0.0.0.0')
138 138 return _filters(ip)
139 139
140 140
141 141 def get_server_ip_addr(environ, log_errors=True):
142 142 hostname = environ.get('SERVER_NAME')
143 143 try:
144 144 return socket.gethostbyname(hostname)
145 145 except Exception as e:
146 146 if log_errors:
147 147 # in some cases this lookup is not possible, and we don't want to
148 148 # make it an exception in logs
149 149 log.exception('Could not retrieve server ip address: %s', e)
150 150 return hostname
151 151
152 152
153 153 def get_server_port(environ):
154 154 return environ.get('SERVER_PORT')
155 155
156 156
157 157 def get_access_path(environ):
158 158 path = environ.get('PATH_INFO')
159 159 org_req = environ.get('pylons.original_request')
160 160 if org_req:
161 161 path = org_req.environ.get('PATH_INFO')
162 162 return path
163 163
164 164
165 165 def get_user_agent(environ):
166 166 return environ.get('HTTP_USER_AGENT')
167 167
168 168
169 169 def vcs_operation_context(
170 170 environ, repo_name, username, action, scm, check_locking=True,
171 171 is_shadow_repo=False):
172 172 """
173 173 Generate the context for a vcs operation, e.g. push or pull.
174 174
175 175 This context is passed over the layers so that hooks triggered by the
176 176 vcs operation know details like the user, the user's IP address etc.
177 177
178 178 :param check_locking: Allows to switch of the computation of the locking
179 179 data. This serves mainly the need of the simplevcs middleware to be
180 180 able to disable this for certain operations.
181 181
182 182 """
183 183 # Tri-state value: False: unlock, None: nothing, True: lock
184 184 make_lock = None
185 185 locked_by = [None, None, None]
186 186 is_anonymous = username == User.DEFAULT_USER
187 187 if not is_anonymous and check_locking:
188 188 log.debug('Checking locking on repository "%s"', repo_name)
189 189 user = User.get_by_username(username)
190 190 repo = Repository.get_by_repo_name(repo_name)
191 191 make_lock, __, locked_by = repo.get_locking_state(
192 192 action, user.user_id)
193 193
194 194 settings_model = VcsSettingsModel(repo=repo_name)
195 195 ui_settings = settings_model.get_ui_settings()
196 196
197 197 extras = {
198 198 'ip': get_ip_addr(environ),
199 199 'username': username,
200 200 'action': action,
201 201 'repository': repo_name,
202 202 'scm': scm,
203 203 'config': rhodecode.CONFIG['__file__'],
204 204 'make_lock': make_lock,
205 205 'locked_by': locked_by,
206 206 'server_url': utils2.get_server_url(environ),
207 207 'user_agent': get_user_agent(environ),
208 208 'hooks': get_enabled_hook_classes(ui_settings),
209 209 'is_shadow_repo': is_shadow_repo,
210 210 }
211 211 return extras
212 212
213 213
214 214 class BasicAuth(AuthBasicAuthenticator):
215 215
216 216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
217 217 initial_call_detection=False, acl_repo_name=None):
218 218 self.realm = realm
219 219 self.initial_call = initial_call_detection
220 220 self.authfunc = authfunc
221 221 self.registry = registry
222 222 self.acl_repo_name = acl_repo_name
223 223 self._rc_auth_http_code = auth_http_code
224 224
225 225 def _get_response_from_code(self, http_code):
226 226 try:
227 227 return get_exception(safe_int(http_code))
228 228 except Exception:
229 229 log.exception('Failed to fetch response for code %s' % http_code)
230 230 return HTTPForbidden
231 231
232 232 def build_authentication(self):
233 233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 234 if self._rc_auth_http_code and not self.initial_call:
235 235 # return alternative HTTP code if alternative http return code
236 236 # is specified in RhodeCode config, but ONLY if it's not the
237 237 # FIRST call
238 238 custom_response_klass = self._get_response_from_code(
239 239 self._rc_auth_http_code)
240 240 return custom_response_klass(headers=head)
241 241 return HTTPUnauthorized(headers=head)
242 242
243 243 def authenticate(self, environ):
244 244 authorization = AUTHORIZATION(environ)
245 245 if not authorization:
246 246 return self.build_authentication()
247 247 (authmeth, auth) = authorization.split(' ', 1)
248 248 if 'basic' != authmeth.lower():
249 249 return self.build_authentication()
250 250 auth = auth.strip().decode('base64')
251 251 _parts = auth.split(':', 1)
252 252 if len(_parts) == 2:
253 253 username, password = _parts
254 254 if self.authfunc(
255 255 username, password, environ, VCS_TYPE,
256 256 registry=self.registry, acl_repo_name=self.acl_repo_name):
257 257 return username
258 258 if username and password:
259 259 # we mark that we actually executed authentication once, at
260 260 # that point we can use the alternative auth code
261 261 self.initial_call = False
262 262
263 263 return self.build_authentication()
264 264
265 265 __call__ = authenticate
266 266
267 267
268 268 def calculate_version_hash():
269 269 return md5(
270 270 config.get('beaker.session.secret', '') +
271 271 rhodecode.__version__)[:8]
272 272
273 273
274 def get_current_lang(request):
275 # NOTE(marcink): remove after pyramid move
276 try:
277 return translation.get_lang()[0]
278 except:
279 pass
280
281 return getattr(request, '_LOCALE_', None)
282
283
274 284 def attach_context_attributes(context, request, user_id):
275 285 """
276 286 Attach variables into template context called `c`, please note that
277 287 request could be pylons or pyramid request in here.
278 288 """
279 289 rc_config = SettingsModel().get_all_settings(cache=True)
280 290
281 291 context.rhodecode_version = rhodecode.__version__
282 292 context.rhodecode_edition = config.get('rhodecode.edition')
283 293 # unique secret + version does not leak the version but keep consistency
284 294 context.rhodecode_version_hash = calculate_version_hash()
285 295
286 296 # Default language set for the incoming request
287 context.language = translation.get_lang()[0]
297 context.language = get_current_lang(request)
288 298
289 299 # Visual options
290 300 context.visual = AttributeDict({})
291 301
292 302 # DB stored Visual Items
293 303 context.visual.show_public_icon = str2bool(
294 304 rc_config.get('rhodecode_show_public_icon'))
295 305 context.visual.show_private_icon = str2bool(
296 306 rc_config.get('rhodecode_show_private_icon'))
297 307 context.visual.stylify_metatags = str2bool(
298 308 rc_config.get('rhodecode_stylify_metatags'))
299 309 context.visual.dashboard_items = safe_int(
300 310 rc_config.get('rhodecode_dashboard_items', 100))
301 311 context.visual.admin_grid_items = safe_int(
302 312 rc_config.get('rhodecode_admin_grid_items', 100))
303 313 context.visual.repository_fields = str2bool(
304 314 rc_config.get('rhodecode_repository_fields'))
305 315 context.visual.show_version = str2bool(
306 316 rc_config.get('rhodecode_show_version'))
307 317 context.visual.use_gravatar = str2bool(
308 318 rc_config.get('rhodecode_use_gravatar'))
309 319 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
310 320 context.visual.default_renderer = rc_config.get(
311 321 'rhodecode_markup_renderer', 'rst')
312 322 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
313 323 context.visual.rhodecode_support_url = \
314 324 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
315 325
316 326 context.pre_code = rc_config.get('rhodecode_pre_code')
317 327 context.post_code = rc_config.get('rhodecode_post_code')
318 328 context.rhodecode_name = rc_config.get('rhodecode_title')
319 329 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
320 330 # if we have specified default_encoding in the request, it has more
321 331 # priority
322 332 if request.GET.get('default_encoding'):
323 333 context.default_encodings.insert(0, request.GET.get('default_encoding'))
324 334 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
325 335
326 336 # INI stored
327 337 context.labs_active = str2bool(
328 338 config.get('labs_settings_active', 'false'))
329 339 context.visual.allow_repo_location_change = str2bool(
330 340 config.get('allow_repo_location_change', True))
331 341 context.visual.allow_custom_hooks_settings = str2bool(
332 342 config.get('allow_custom_hooks_settings', True))
333 343 context.debug_style = str2bool(config.get('debug_style', False))
334 344
335 345 context.rhodecode_instanceid = config.get('instance_id')
336 346
337 347 context.visual.cut_off_limit_diff = safe_int(
338 348 config.get('cut_off_limit_diff'))
339 349 context.visual.cut_off_limit_file = safe_int(
340 350 config.get('cut_off_limit_file'))
341 351
342 352 # AppEnlight
343 353 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
344 354 context.appenlight_api_public_key = config.get(
345 355 'appenlight.api_public_key', '')
346 356 context.appenlight_server_url = config.get('appenlight.server_url', '')
347 357
348 358 # JS template context
349 359 context.template_context = {
350 360 'repo_name': None,
351 361 'repo_type': None,
352 362 'repo_landing_commit': None,
353 363 'rhodecode_user': {
354 364 'username': None,
355 365 'email': None,
356 366 'notification_status': False
357 367 },
358 368 'visual': {
359 369 'default_renderer': None
360 370 },
361 371 'commit_data': {
362 372 'commit_id': None
363 373 },
364 374 'pull_request_data': {'pull_request_id': None},
365 375 'timeago': {
366 376 'refresh_time': 120 * 1000,
367 377 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
368 378 },
369 379 'pylons_dispatch': {
370 380 # 'controller': request.environ['pylons.routes_dict']['controller'],
371 381 # 'action': request.environ['pylons.routes_dict']['action'],
372 382 },
373 383 'pyramid_dispatch': {
374 384
375 385 },
376 386 'extra': {'plugins': {}}
377 387 }
378 388 # END CONFIG VARS
379 389
380 390 # TODO: This dosn't work when called from pylons compatibility tween.
381 391 # Fix this and remove it from base controller.
382 392 # context.repo_name = get_repo_slug(request) # can be empty
383 393
384 394 diffmode = 'sideside'
385 395 if request.GET.get('diffmode'):
386 396 if request.GET['diffmode'] == 'unified':
387 397 diffmode = 'unified'
388 398 elif request.session.get('diffmode'):
389 399 diffmode = request.session['diffmode']
390 400
391 401 context.diffmode = diffmode
392 402
393 403 if request.session.get('diffmode') != diffmode:
394 404 request.session['diffmode'] = diffmode
395 405
396 406 context.csrf_token = auth.get_csrf_token()
397 407 context.backends = rhodecode.BACKENDS.keys()
398 408 context.backends.sort()
399 409 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
400 410 context.pyramid_request = pyramid.threadlocal.get_current_request()
401 411
402 412 # attach the whole call context to the request
403 413 request.call_context = context
404 414
405 415
406 416 def get_auth_user(request):
407 417 environ = request.environ
408 418 session = request.session
409 419
410 420 ip_addr = get_ip_addr(environ)
411 421 # make sure that we update permissions each time we call controller
412 422 _auth_token = (request.GET.get('auth_token', '') or
413 423 request.GET.get('api_key', ''))
414 424
415 425 if _auth_token:
416 426 # when using API_KEY we assume user exists, and
417 427 # doesn't need auth based on cookies.
418 428 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
419 429 authenticated = False
420 430 else:
421 431 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
422 432 try:
423 433 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
424 434 ip_addr=ip_addr)
425 435 except UserCreationError as e:
426 436 h.flash(e, 'error')
427 437 # container auth or other auth functions that create users
428 438 # on the fly can throw this exception signaling that there's
429 439 # issue with user creation, explanation should be provided
430 440 # in Exception itself. We then create a simple blank
431 441 # AuthUser
432 442 auth_user = AuthUser(ip_addr=ip_addr)
433 443
434 444 if password_changed(auth_user, session):
435 445 session.invalidate()
436 446 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
437 447 auth_user = AuthUser(ip_addr=ip_addr)
438 448
439 449 authenticated = cookie_store.get('is_authenticated')
440 450
441 451 if not auth_user.is_authenticated and auth_user.is_user_object:
442 452 # user is not authenticated and not empty
443 453 auth_user.set_authenticated(authenticated)
444 454
445 455 return auth_user
446 456
447 457
448 458 class BaseController(WSGIController):
449 459
450 460 def __before__(self):
451 461 """
452 462 __before__ is called before controller methods and after __call__
453 463 """
454 464 # on each call propagate settings calls into global settings.
455 465 set_rhodecode_config(config)
456 466 attach_context_attributes(c, request, c.rhodecode_user.user_id)
457 467
458 468 # TODO: Remove this when fixed in attach_context_attributes()
459 469 c.repo_name = get_repo_slug(request) # can be empty
460 470
461 471 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
462 472 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
463 473 self.sa = meta.Session
464 474 self.scm_model = ScmModel(self.sa)
465 475
466 476 # set user language
467 477 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
468 478 if user_lang:
469 479 translation.set_lang(user_lang)
470 480 log.debug('set language to %s for user %s',
471 481 user_lang, self._rhodecode_user)
472 482
473 483 def _dispatch_redirect(self, with_url, environ, start_response):
474 484 resp = HTTPFound(with_url)
475 485 environ['SCRIPT_NAME'] = '' # handle prefix middleware
476 486 environ['PATH_INFO'] = with_url
477 487 return resp(environ, start_response)
478 488
479 489 def __call__(self, environ, start_response):
480 490 """Invoke the Controller"""
481 491 # WSGIController.__call__ dispatches to the Controller method
482 492 # the request is routed to. This routing information is
483 493 # available in environ['pylons.routes_dict']
484 494 from rhodecode.lib import helpers as h
485 495
486 496 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
487 497 if environ.get('debugtoolbar.wants_pylons_context', False):
488 498 environ['debugtoolbar.pylons_context'] = c._current_obj()
489 499
490 500 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
491 501 environ['pylons.routes_dict']['action']])
492 502
493 503 self.rc_config = SettingsModel().get_all_settings(cache=True)
494 504 self.ip_addr = get_ip_addr(environ)
495 505
496 506 # The rhodecode auth user is looked up and passed through the
497 507 # environ by the pylons compatibility tween in pyramid.
498 508 # So we can just grab it from there.
499 509 auth_user = environ['rc_auth_user']
500 510
501 511 # set globals for auth user
502 512 request.user = auth_user
503 513 c.rhodecode_user = self._rhodecode_user = auth_user
504 514
505 515 log.info('IP: %s User: %s accessed %s [%s]' % (
506 516 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
507 517 _route_name)
508 518 )
509 519
510 520 user_obj = auth_user.get_instance()
511 521 if user_obj and user_obj.user_data.get('force_password_change'):
512 522 h.flash('You are required to change your password', 'warning',
513 523 ignore_duplicate=True)
514 524 return self._dispatch_redirect(
515 525 url('my_account_password'), environ, start_response)
516 526
517 527 return WSGIController.__call__(self, environ, start_response)
518 528
519 529
520 530 class BaseRepoController(BaseController):
521 531 """
522 532 Base class for controllers responsible for loading all needed data for
523 533 repository loaded items are
524 534
525 535 c.rhodecode_repo: instance of scm repository
526 536 c.rhodecode_db_repo: instance of db
527 537 c.repository_requirements_missing: shows that repository specific data
528 538 could not be displayed due to the missing requirements
529 539 c.repository_pull_requests: show number of open pull requests
530 540 """
531 541
532 542 def __before__(self):
533 543 super(BaseRepoController, self).__before__()
534 544 if c.repo_name: # extracted from routes
535 545 db_repo = Repository.get_by_repo_name(c.repo_name)
536 546 if not db_repo:
537 547 return
538 548
539 549 log.debug(
540 550 'Found repository in database %s with state `%s`',
541 551 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
542 552 route = getattr(request.environ.get('routes.route'), 'name', '')
543 553
544 554 # allow to delete repos that are somehow damages in filesystem
545 555 if route in ['delete_repo']:
546 556 return
547 557
548 558 if db_repo.repo_state in [Repository.STATE_PENDING]:
549 559 if route in ['repo_creating_home']:
550 560 return
551 561 check_url = url('repo_creating_home', repo_name=c.repo_name)
552 562 return redirect(check_url)
553 563
554 564 self.rhodecode_db_repo = db_repo
555 565
556 566 missing_requirements = False
557 567 try:
558 568 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
559 569 except RepositoryRequirementError as e:
560 570 missing_requirements = True
561 571 self._handle_missing_requirements(e)
562 572
563 573 if self.rhodecode_repo is None and not missing_requirements:
564 574 log.error('%s this repository is present in database but it '
565 575 'cannot be created as an scm instance', c.repo_name)
566 576
567 577 h.flash(_(
568 578 "The repository at %(repo_name)s cannot be located.") %
569 579 {'repo_name': c.repo_name},
570 580 category='error', ignore_duplicate=True)
571 581 redirect(h.route_path('home'))
572 582
573 583 # update last change according to VCS data
574 584 if not missing_requirements:
575 585 commit = db_repo.get_commit(
576 586 pre_load=["author", "date", "message", "parents"])
577 587 db_repo.update_commit_cache(commit)
578 588
579 589 # Prepare context
580 590 c.rhodecode_db_repo = db_repo
581 591 c.rhodecode_repo = self.rhodecode_repo
582 592 c.repository_requirements_missing = missing_requirements
583 593
584 594 self._update_global_counters(self.scm_model, db_repo)
585 595
586 596 def _update_global_counters(self, scm_model, db_repo):
587 597 """
588 598 Base variables that are exposed to every page of repository
589 599 """
590 600 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
591 601
592 602 def _handle_missing_requirements(self, error):
593 603 self.rhodecode_repo = None
594 604 log.error(
595 605 'Requirements are missing for repository %s: %s',
596 606 c.repo_name, error.message)
597 607
598 608 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
599 609 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
600 610 settings_update_url = url('repo', repo_name=c.repo_name)
601 611 path = request.path
602 612 should_redirect = (
603 613 path not in (summary_url, settings_update_url)
604 614 and '/settings' not in path or path == statistics_url
605 615 )
606 616 if should_redirect:
607 617 redirect(summary_url)
General Comments 0
You need to be logged in to leave comments. Login now