Package turbogears :: Module controllers

Source Code for Module turbogears.controllers

  1  """Classes and methods for TurboGears controllers.""" 
  2   
  3  import logging 
  4  import urllib 
  5  import urlparse 
  6  import time 
  7  import types 
  8   
  9  from itertools import izip 
 10  from inspect import isclass 
 11   
 12  import cherrypy 
 13  from cherrypy import request, response 
 14   
 15  from peak.rules import abstract, NoApplicableMethods 
 16  from peak.rules.core import always_overrides, Method 
 17   
 18  import turbogears.util as tg_util 
 19  from turbogears import view, database, errorhandling, config 
 20  from turbogears.decorator import weak_signature_decorator 
 21  from turbogears.errorhandling import error_handler, exception_handler 
 22  from turbogears.validators import Invalid 
 23   
 24  log = logging.getLogger("turbogears.controllers") 
 25   
 26  if config.get("session_filter.on", None): 
 27      if config.get("session_filter.storage_type", None) == "PostgreSQL": 
 28          import psycopg2 
 29          config.update( 
 30                  {'session_filter.get_db': psycopg2.connect( 
 31                      psycopg2.get('sessions.postgres.dsn')) 
 32                      }) 
33 # XXX: support for mysql/sqlite/etc here 34 35 36 -def _process_output(output, template, format, content_type, fragment=False, 37 **options):
38 """Produce final output form from data returned from a controller method. 39 40 See the expose() arguments for more info since they are the same. 41 42 """ 43 if isinstance(output, dict): 44 # import this here to prevent circular import in widgets.forms 45 from turbogears.widgets import js_location 46 47 css = tg_util.setlike() 48 js = dict(izip(js_location, iter(tg_util.setlike, None))) 49 include_widgets = {} 50 include_widgets_lst = config.get('tg.include_widgets', []) 51 52 if config.get('tg.mochikit_all', False): 53 include_widgets_lst.insert(0, 'turbogears.mochikit') 54 55 for name in include_widgets_lst: 56 widget = tg_util.load_class(name) 57 if widget is None: 58 log.debug("Could not load widget %s", name) 59 continue 60 if isclass(widget): 61 widget = widget() 62 if hasattr(widget, 'retrieve_resources') and hasattr(widget, 'inject'): 63 # it's a ToscaWidget, we register it for injection 64 widget.inject() 65 # XXX: widgets with same base name will override each other 66 include_widgets['tg_%s' % name.rsplit('.', 1)[-1]] = widget 67 output.update(include_widgets) 68 69 # collect JS/CSS resources from widgets in the output dict or 70 # tg.include_widgets 71 for value in output.itervalues(): 72 if hasattr(value, 'retrieve_resources'): 73 # it's a ToscaWidget, will be injected by the ToscaWidget middleware 74 continue 75 else: 76 try: 77 css_resources = value.retrieve_css() 78 except (AttributeError, TypeError): 79 css_resources = [] 80 try: 81 js_resources = value.retrieve_javascript() 82 except (AttributeError, TypeError): 83 js_resources = [] 84 css.add_all(css_resources) 85 for script in js_resources: 86 location = getattr(script, 'location', js_location.head) 87 js[location].add(script) 88 css.sort(key=lambda obj: getattr(obj, 'order', 0)) 89 output['tg_css'] = css 90 for location in iter(js_location): 91 js[location].sort(key=lambda obj: getattr(obj, 'order', 0)) 92 output['tg_js_%s' % location] = js[location] 93 94 tg_flash = _get_flash() 95 if tg_flash: 96 output['tg_flash'] = tg_flash 97 98 headers = {'Content-Type': content_type} 99 output = view.render(output, template=template, format=format, 100 headers=headers, fragment=fragment, **options) 101 content_type = headers['Content-Type'] 102 103 if content_type: 104 response.headers['Content-Type'] = content_type 105 else: 106 content_type = response.headers.get('Content-Type', 'text/plain') 107 108 if content_type.startswith('text/'): 109 if isinstance(output, unicode): 110 output = output.encode(tg_util.get_template_encoding_default()) 111 112 return output
113
114 115 -class BadFormatError(Exception):
116 """Output-format exception."""
117
118 119 -def validate(form=None, validators=None, 120 failsafe_schema=errorhandling.FailsafeSchema.none, 121 failsafe_values=None, state_factory=None):
122 """Validate input. 123 124 @param form: a form instance that must be passed throught the validation 125 process... you must give a the same form instance as the one that will 126 be used to post data on the controller you are putting the validate 127 decorator on. 128 @type form: a form instance 129 130 @param validators: individual validators to use for parameters. 131 If you use a schema for validation then the schema instance must 132 be the sole argument. 133 If you use simple validators, then you must pass a dictionary with 134 each value name to validate as a key of the dictionary and the validator 135 instance (eg: tg.validators.Int() for integer) as the value. 136 @type validators: dictionary or schema instance 137 138 @param failsafe_schema: a schema for handling failsafe values. 139 The default is 'none', but you can also use 'values', 'map_errors', 140 or 'defaults' to map erroneous inputs to values, corresponding exceptions 141 or method defaults. 142 @type failsafe_schema: errorhandling.FailsafeSchema 143 144 @param failsafe_values: replacements for erroneous inputs. You can either 145 define replacements for every parameter, or a single replacement value 146 for all parameters. This is only used when failsafe_schema is 'values'. 147 @type failsafe_values: a dictionary or a single value 148 149 @param state_factory: If this is None, the initial state for validation 150 is set to None, otherwise this must be a callable that returns the initial 151 state to be used for validation. 152 @type state_factory: callable or None 153 154 """ 155 def entangle(func): 156 if callable(form) and not hasattr(form, "validate"): 157 init_form = form 158 else: 159 init_form = lambda self: form 160 161 def validate(func, *args, **kw): 162 # do not validate a second time if already validated 163 if hasattr(request, 'validation_state'): 164 return func(*args, **kw) 165 166 form = init_form(args and args[0] or kw["self"]) 167 args, kw = tg_util.to_kw(func, args, kw) 168 169 errors = {} 170 if state_factory is not None: 171 state = state_factory() 172 else: 173 state = None 174 175 if form: 176 value = kw.copy() 177 try: 178 kw.update(form.validate(value, state)) 179 except Invalid, e: 180 errors = e.unpack_errors() 181 request.validation_exception = e 182 request.validated_form = form 183 184 if validators: 185 if isinstance(validators, dict): 186 for field, validator in validators.iteritems(): 187 try: 188 kw[field] = validator.to_python( 189 kw.get(field, None), state) 190 except Invalid, error: 191 errors[field] = error 192 else: 193 try: 194 value = kw.copy() 195 kw.update(validators.to_python(value, state)) 196 except Invalid, e: 197 errors = e.unpack_errors() 198 request.validation_exception = e 199 request.validation_errors = errors 200 request.input_values = kw.copy() 201 request.validation_state = state 202 203 if errors: 204 kw = errorhandling.dispatch_failsafe(failsafe_schema, 205 failsafe_values, errors, func, kw) 206 args, kw = tg_util.from_kw(func, args, kw) 207 return errorhandling.run_with_errors(errors, func, *args, **kw)
208 209 return validate 210 return weak_signature_decorator(entangle) 211
212 213 -class First(Method):
214 """Resolve ambiguousness by calling the first method."""
215 - def merge(self, other):
216 return self
217 218 always_overrides(First, Method) 219 first = First.make_decorator('first')
220 221 222 -def _add_rule(_expose, found_default, as_format, accept_format, template, 223 rulefunc):
224 if as_format == "default": 225 if found_default: 226 colon = template.find(":") 227 if colon == -1: 228 as_format = template 229 else: 230 as_format = template[:colon] 231 else: 232 found_default = True 233 ruleparts = ['kw.get("tg_format", "default") == "%s"' % as_format] 234 if accept_format: 235 ruleparts.append('(accept == "%s" and ' 236 'kw.get("tg_format", "default") == "default")' % accept_format) 237 rule = " or ".join(ruleparts) 238 log.debug("Generated rule %s", rule) 239 first(_expose, rule)(rulefunc) 240 return found_default
241
242 243 -def _build_rules(func):
244 @abstract() 245 def _expose(func, accept, allow_json, *args, **kw): 246 pass
247 248 if func._allow_json: 249 rule = ('allow_json and (kw.get("tg_format", None) == "json"' 250 ' or accept in ("application/json", "text/javascript"))') 251 log.debug("Adding allow_json rule for %s: %s", func, rule) 252 first(_expose, rule)( 253 lambda _func, accept, allow_json, *args, **kw: 254 _execute_func(_func, "json", "json", "application/json", 255 False, {}, args, kw)) 256 257 found_default = False 258 for ruleinfo in func._ruleinfo: 259 found_default = _add_rule(_expose, found_default, **ruleinfo) 260 261 func._expose = _expose 262
263 264 -def expose(template=None, allow_json=None, format=None, content_type=None, 265 fragment=False, as_format="default", accept_format=None, **options):
266 """Exposes a method to the web. 267 268 By putting the expose decorator on a method, you tell TurboGears that 269 the method should be accessible via URL traversal. Additionally, expose 270 handles the output processing (turning a dictionary into finished 271 output) and is also responsible for ensuring that the request is 272 wrapped in a database transaction. 273 274 You can apply multiple expose decorators to a method, if 275 you'd like to support multiple output formats. The decorator that's 276 listed first in your code without as_format or accept_format is 277 the default that is chosen when no format is specifically asked for. 278 Any other expose calls that are missing as_format and accept_format 279 will have as_format implicitly set to the whatever comes before 280 the ":" in the template name (or the whole template name if there 281 is no ":". For example, <code>expose("json")</code>, if it's not 282 the default expose, will have as_format set to "json". 283 284 When as_format is set, passing the same value in the tg_format 285 parameter in a request will choose the options for that expose 286 decorator. Similarly, accept_format will watch for matching 287 Accept headers. You can also use both. expose("json", as_format="json", 288 accept_format="application/json") will choose JSON output for either 289 case: tg_format=json as a parameter or Accept: application/json as a 290 request header. 291 292 Passing allow_json=True to an expose decorator 293 is equivalent to adding the decorator just mentioned. 294 295 Each expose decorator has its own set of options, and each one 296 can choose a different template or even template engine (you can 297 use Kid for HTML output and Cheetah for plain text, for example). 298 See the other expose parameters below to learn about the options 299 you can pass to the template engine. 300 301 Take a look at the 302 <a href="tests/test_expose-source.html">test_expose.py</a> suite 303 for more examples. 304 305 @param template: "templateengine:dotted.reference" reference along the 306 Python path for the template and the template engine. For 307 example, "kid:foo.bar" will have Kid render the bar template in 308 the foo package. 309 @keyparam format: format for the template engine to output (if the 310 template engine can render different formats. Kid, for example, 311 can render "html", "xml" or "xhtml") 312 @keyparam content_type: sets the content-type http header 313 @keyparam allow_json: allow the function to be exposed as json 314 @keyparam fragment: for template engines (like Kid) that generate 315 DOCTYPE declarations and the like, this is a signal to 316 just generate the immediate template fragment. Use this 317 if you're building up a page from multiple templates or 318 going to put something onto a page with .innerHTML. 319 @keyparam as_format: designates which value of tg_format will choose 320 this expose. 321 @keyparam accept_format: which value of an Accept: header will 322 choose this expose. 323 324 All additional keyword arguments are passed as keyword args to the render 325 method of the template engine. 326 327 """ 328 if not template: 329 template = format 330 331 if format == "json" or (format is None and template is None 332 and (allow_json is None or allow_json)): 333 template = "json" 334 allow_json = True 335 336 if content_type is None: 337 content_type = config.get("tg.content_type", None) 338 339 if config.get("tg.session.automatic_lock", None): 340 cherrypy.session.acquire_lock() 341 342 def entangle(func): 343 log.debug("Exposing %s", func) 344 log.debug("template: %s, format: %s, allow_json: %s, " 345 "content-type: %s", template, format, allow_json, content_type) 346 if not getattr(func, "exposed", False): 347 def expose(func, *args, **kw): 348 request.tg_template_enginename = view.base._choose_engine(template)[2] 349 accept = request.headers.get('Accept', "").lower() 350 accept = tg_util.simplify_http_accept_header(accept) 351 if not hasattr(func, "_expose"): 352 _build_rules(func) 353 try: 354 if hasattr(request, "in_transaction"): 355 output = func._expose(func, accept, func._allow_json, 356 *args, **kw) 357 else: 358 request.in_transaction = True 359 output = database.run_with_transaction( 360 func._expose, func, accept, func._allow_json, 361 *args, **kw) 362 except NoApplicableMethods, e: 363 args = e.args # args from the last generic function call 364 if (args and args[0] and isinstance(args[0], tuple) 365 and args[0][0] is func): 366 # The error refers to our call above. This means that 367 # no suitable controller method was found (probably due 368 # to wrong parameters). So we will raise a "not found" 369 # error unless a specific error status was already set 370 # (e.g. "unauthorized" was set by the identity provider): 371 status = cherrypy.response.status 372 if status and status // 100 == 4: 373 raise cherrypy.HTTPError(status) 374 raise cherrypy.NotFound 375 # If the error was raised elsewhere inside the controller, 376 # handle it like all other exceptions ("server error"): 377 raise 378 return output
379 func.exposed = True 380 func._ruleinfo = [] 381 allow_json_from_config = config.get("tg.allow_json", False) 382 func._allow_json = allow_json_from_config or template == 'json' 383 else: 384 expose = lambda func, *args, **kw: func(*args, **kw) 385 386 func._ruleinfo.insert(0, dict(as_format=as_format, 387 accept_format=accept_format, template=template, 388 rulefunc=lambda _func, accept, allow_json, *args, **kw: 389 _execute_func(_func, template, format, content_type, 390 fragment, options, args, kw))) 391 392 if allow_json: 393 func._allow_json = True 394 395 return expose 396 return weak_signature_decorator(entangle) 397
398 399 -def _execute_func(func, template, format, content_type, fragment, options, 400 args, kw):
401 """Call controller method and process it's output.""" 402 403 if config.get("tg.strict_parameters", False): 404 tg_util.remove_keys(kw, ["tg_random", "tg_format"] 405 + config.get("tg.ignore_parameters", [])) 406 407 else: 408 # get special parameters used by upstream decorators like paginate 409 try: 410 tg_kw = dict([(k, v) for k, v in kw.items() if k in func._tg_args]) 411 412 except AttributeError: 413 tg_kw = {} 414 415 # remove excessive parameters 416 args, kw = tg_util.adapt_call(func, args, kw) 417 # add special parameters again 418 kw.update(tg_kw) 419 420 if config.get('server.environment', 'development') == 'development': 421 # Only output this in development mode: If it's a field storage object, 422 # this means big memory usage, and we don't want that in production 423 log.debug("Calling %s with *(%s), **(%s)", func, args, kw) 424 425 output = errorhandling.try_call(func, *args, **kw) 426 427 if str(getattr(response, 'status', '')).startswith('204'): 428 # HTTP status 204 indicates a response with no body 429 # so there should be no content type header 430 try: 431 del response.headers['Content-Type'] 432 except (AttributeError, KeyError): 433 pass 434 return 435 436 else: 437 assert isinstance(output, 438 (basestring, dict, list, types.GeneratorType)), ( 439 "Method %s.%s() returned unexpected output. Output should " 440 "be of type basestring, dict, list or generator." % ( 441 args[0].__class__.__name__, func.__name__)) 442 443 if isinstance(output, dict): 444 template = output.pop("tg_template", template) 445 format = output.pop("tg_format", format) 446 447 if template and template.startswith("."): 448 template = func.__module__[:func.__module__.rfind('.')] + template 449 450 return _process_output(output, template, format, content_type, 451 fragment, **options)
452
453 454 -def flash(message):
455 """Set a message to be displayed in the browser on next page display.""" 456 message = tg_util.to_utf8(message) 457 if len(message) > 4000: 458 log.warning('Flash message exceeding maximum cookie size!') 459 response.simple_cookie['tg_flash'] = message 460 response.simple_cookie['tg_flash']['path'] = '/'
461
462 463 -def _get_flash():
464 """Retrieve the flash message (if one is set), clearing the message.""" 465 request_cookie = request.simple_cookie 466 response_cookie = response.simple_cookie 467 468 def clearcookie(): 469 response_cookie["tg_flash"] = "" 470 response_cookie["tg_flash"]['expires'] = 0 471 response_cookie['tg_flash']['path'] = '/'
472 473 if "tg_flash" in response_cookie: 474 message = response_cookie["tg_flash"].value 475 response_cookie.pop("tg_flash") 476 if "tg_flash" in request_cookie: 477 # New flash overrided old one sitting in cookie. Clear that old cookie. 478 clearcookie() 479 elif "tg_flash" in request_cookie: 480 message = request_cookie.value_decode(request_cookie["tg_flash"].value)[0] 481 if "tg_flash" not in response_cookie: 482 clearcookie() 483 else: 484 message = None 485 if message: 486 message = unicode(message, 'utf-8') 487 return message 488
489 490 -class Controller(object):
491 """Base class for a web application's controller. 492 493 It is important that your controllers inherit from this class, otherwise 494 ``identity.SecureResource`` and ``identity.SecureObject`` will not work 495 correctly. 496 497 """ 498 499 msglog = logging.getLogger('cherrypy.msg') 500 msglogfunc = {0: msglog.info, 1: msglog.warning, 2: msglog.error} 501 502 @classmethod
503 - def _cp_log_message(cls, msg, context='nocontext', severity=0):
504 """Default method for logging messages (errors and app-specific info)""" 505 log = cls.msglogfunc[severity] 506 text = ''.join((context, ': ', msg)) 507 log(text)
508 509 accesslog = logging.getLogger('turbogears.access') 510 511 @classmethod
512 - def _cp_log_access(cls):
513 """Default method for logging access""" 514 # Conforms to Combined Log Format 515 # http://httpd.apache.org/docs/2.2/logs.html#combined 516 tmpl = ('%(host)s %(ident)s %(authuser)s [%(date)s] "%(request)s"' 517 ' %(status)s %(bytes)s "%(referrer)s" "%(useragent)s"') 518 try: 519 username = request.user_name 520 if username and isinstance(username, unicode): 521 # might get concatenated to non-ascii byte strings 522 username = username.encode('utf-8') 523 else: 524 username = '-' 525 except AttributeError: 526 username = '-' 527 request_date = time.strftime('%d/%b/%Y:%H:%M:%S +0000', time.gmtime()) 528 request_info = { 529 'host': request.headers.get('X-Forwarded-For') 530 or request.remote_host or request.remote_addr, 531 'ident': '-', 532 'authuser': username, 533 'date': request_date, 534 'request': request.requestLine, 535 'status': response.status.split(None, 1)[0], 536 'bytes': response.headers.get('Content-Length') or '-', 537 'referrer': request.headers.get('referer', ''), 538 'useragent': request.headers.get('user-agent', ''), 539 } 540 cls.accesslog.info(tmpl, request_info)
541
542 543 -class RootController(Controller):
544 """Base class for the root of a web application. 545 546 Your web application must have one of these. The root of your application 547 is used to compute URLs used by your app. 548 549 """ 550 551 is_app_root = True
552 553 Root = RootController
554 555 556 -class ExposedDescriptor(object):
557 """Descriptor used by RESTMethod to tell if it is exposed.""" 558
559 - def __get__(self, obj, cls=None):
560 """Return True if object has a method for HTTP method of current request 561 """ 562 if cls is None: 563 cls = obj 564 cp_methodname = cherrypy.request.method 565 methodname = cp_methodname.lower() 566 method = getattr(cls, methodname, None) 567 if callable(method) and getattr(method, 'exposed', False): 568 return True 569 raise cherrypy.HTTPError(405, '%s not allowed on %s' % ( 570 cp_methodname, cherrypy.request.browser_url))
571
572 573 -class RESTMethod(Controller):
574 """Allow REST style dispatch based on different HTTP methods. 575 576 For an elaborate usage example see turbogears.tests.test_restmethod. 577 578 In short, instead of an exposed method, you define a sub-class of 579 RESTMethod inside the controller class and inside this class you define 580 exposed methods named after each HTTP method that should be supported. 581 582 Example:: 583 584 class Controller(controllers.Controller): 585 586 class article(copntrollers.RESTMethod): 587 @expose() 588 def get(self, id): 589 ... 590 591 @expose() 592 def post(self, id): 593 ... 594 595 """ 596 597 exposed = ExposedDescriptor() 598
599 - def __init__(self, *l, **kw):
600 methodname = cherrypy.request.method.lower() 601 self.result = getattr(self, methodname)(*l, **kw)
602
603 - def __iter__(self):
604 return iter(self.result)
605
606 607 -def url(tgpath, tgparams=None, **kw):
608 """Computes relocatable URLs. 609 610 tgpath can be a list or a string. If the path is absolute (starts with a 611 "/"), the server.webpath, SCRIPT_NAME and the approot of the application 612 are prepended to the path. In order for the approot to be detected 613 properly, the root object must extend controllers.RootController. 614 615 Query parameters for the URL can be passed in as a dictionary in 616 the second argument and/or as keyword parameters where keyword args 617 overwrite entries in the dictionary. 618 619 Values which are a list or a tuple are used to create multiple 620 key-value pairs. 621 622 tgpath may also already contain a (properly escaped) query string seperated 623 by a question mark ('?'), in which case additional query params are 624 appended. 625 626 """ 627 if not isinstance(tgpath, basestring): 628 tgpath = '/'.join(list(tgpath)) 629 if tgpath.startswith('/'): 630 webpath = (config.get('server.webpath') or '').rstrip('/') 631 if tg_util.request_available(): 632 check_app_root() 633 tgpath = request.app_root + tgpath 634 try: 635 webpath += request.wsgi_environ['SCRIPT_NAME'].rstrip('/') 636 except (AttributeError, KeyError): 637 pass 638 tgpath = webpath + tgpath 639 if tgparams is None: 640 tgparams = kw 641 else: 642 try: 643 tgparams = tgparams.copy() 644 tgparams.update(kw) 645 except AttributeError: 646 raise TypeError('url() expects a dictionary for query parameters') 647 args = [] 648 for key, value in tgparams.iteritems(): 649 if value is None: 650 continue 651 if isinstance(value, (list, tuple)): 652 pairs = [(key, v) for v in value] 653 else: 654 pairs = [(key, value)] 655 for k, v in pairs: 656 if v is None: 657 continue 658 if isinstance(v, unicode): 659 v = v.encode('utf8') 660 args.append((k, str(v))) 661 if args: 662 query_string = urllib.urlencode(args, True) 663 if '?' in tgpath: 664 tgpath += '&' + query_string 665 else: 666 tgpath += '?' + query_string 667 return tgpath
668
669 670 -def get_server_name():
671 """Return name of the server this application runs on. 672 673 Respects 'Host' and 'X-Forwarded-Host' header. 674 675 See the docstring of the 'absolute_url' function for more information. 676 677 """ 678 get = config.get 679 h = request.headers 680 host = get('tg.url_domain') or h.get('X-Forwarded-Host', h.get('Host')) 681 if not host: 682 host = '%s:%s' % (get('server.socket_host', 'localhost'), 683 get('server.socket_port', 8080)) 684 return host
685
686 687 -def absolute_url(tgpath='/', params=None, **kw):
688 """Return absolute URL (including schema and host to this server). 689 690 Tries to account for 'Host' header and reverse proxying 691 ('X-Forwarded-Host'). 692 693 The host name is determined this way: 694 695 * If the config setting 'tg.url_domain' is set and non-null, use this value. 696 * Else, if the 'base_url_filter.use_x_forwarded_host' config setting is 697 True, use the value from the 'Host' or 'X-Forwarded-Host' request header. 698 * Else, if config setting 'base_url_filter.on' is True and 699 'base_url_filter.base_url' is non-null, use its value for the host AND 700 scheme part of the URL. 701 * As a last fallback, use the value of 'server.socket_host' and 702 'server.socket_port' config settings (defaults to 'localhost:8080'). 703 704 The URL scheme ('http' or 'http') used is determined in the following way: 705 706 * If 'base_url_filter.base_url' is used, use the scheme from this URL. 707 * If there is a 'X-Use-SSL' request header, use 'https'. 708 * Else, if the config setting 'tg.url_scheme' is set, use its value. 709 * Else, use the value of 'cherrypy.request.scheme'. 710 711 """ 712 get = config.get 713 use_xfh = get('base_url_filter.use_x_forwarded_host', False) 714 if request.headers.get('X-Use-SSL'): 715 scheme = 'https' 716 else: 717 scheme = get('tg.url_scheme') 718 if not scheme: 719 scheme = request.scheme 720 base_url = '%s://%s' % (scheme, get_server_name()) 721 if get('base_url_filter.on', False) and not use_xfh: 722 base_url = get('base_url_filter.base_url').rstrip('/') 723 return '%s%s' % (base_url, url(tgpath, params, **kw))
724
725 726 -def check_app_root():
727 """Sets request.app_root if needed.""" 728 if hasattr(request, 'app_root'): 729 return 730 found_root = False 731 trail = request.object_trail 732 top = len(trail) - 1 733 # compute the app_root by stepping back through the object 734 # trail and collecting up the path elements after the first 735 # root we find 736 # we can eliminate this if we find a way to use 737 # CherryPy's mounting mechanism whenever a new root 738 # is hit. 739 rootlist = [] 740 for i in xrange(len(trail) - 1, -1, -1): 741 path, obj = trail[i] 742 if not found_root and isinstance(obj, RootController): 743 if i == top: 744 break 745 found_root = True 746 if found_root and i > 0: 747 rootlist.insert(0, path) 748 app_root = '/'.join(rootlist) 749 if not app_root.startswith('/'): 750 app_root = '/' + app_root 751 if app_root.endswith('/'): 752 app_root = app_root[:-1] 753 request.app_root = app_root
754
755 756 -def redirect(redirect_path, redirect_params=None, **kw):
757 """Redirect (via cherrypy.HTTPRedirect). 758 759 Raises the exception instead of returning it, this to allow 760 users to both call it as a function or to raise it as an exception. 761 762 """ 763 if not isinstance(redirect_path, basestring): 764 redirect_path = '/'.join(list(redirect_path)) 765 if not redirect_path.startswith('/'): 766 path = request.path_info 767 check_app_root() 768 if path.startswith(request.app_root): 769 path = path[len(request.app_root):] 770 redirect_path = urlparse.urljoin(path, redirect_path) 771 raise cherrypy.HTTPRedirect(url(tgpath=redirect_path, 772 tgparams=redirect_params, **kw))
773 774 775 __all__ = [ 776 "Controller", 777 "absolute_url", 778 "error_handler", 779 "exception_handler", 780 "expose", 781 "get_server_name", 782 "flash", 783 "redirect", 784 "Root", 785 "RootController", 786 "url", 787 "validate", 788 ] 789