1 """Classes and methods for TurboGears controllers."""
2
3 __all__ = ['Controller', 'absolute_url',
4 'error_handler', 'exception_handler',
5 'expose', 'get_server_name', 'flash', 'redirect',
6 'Root', 'RootController', 'url', 'validate']
7
8 import logging
9 import urllib
10 import urlparse
11 import types
12
13 from itertools import izip
14 from inspect import isclass
15
16 import cherrypy
17 from cherrypy import request, response, url as cp_url
18
19 from peak.rules import abstract, NoApplicableMethods
20 from peak.rules.core import always_overrides, Method
21
22 import turbogears.util as tg_util
23 from turbogears import view, database, errorhandling, config
24 from turbogears.decorator import weak_signature_decorator
25 from turbogears.errorhandling import error_handler, exception_handler
26 from turbogears.validators import Invalid
27
28 log = logging.getLogger('turbogears.controllers')
29
30 if config.get('tools.sessions.on', False):
31 if config.get('tools.sessions.storage_type') == 'PostgreSQL':
32 import psycopg2
33 config.update(
34 {'tools.sessions.get_db' : psycopg2.connect(
35 config.get('sessions.postgres.dsn'))
36 })
37
38
39
40 -def _process_output(output, template, format, content_type, fragment=False,
41 **options):
42 """Produce final output form from data returned from a controller method.
43
44 See the expose() arguments for more info since they are the same.
45
46 """
47 if isinstance(output, dict):
48
49 from turbogears.widgets import js_location
50
51 css = tg_util.setlike()
52 js = dict(izip(js_location, iter(tg_util.setlike, None)))
53 include_widgets = {}
54 include_widgets_lst = config.get('tg.include_widgets', [])
55
56 if config.get('tg.mochikit_all', False):
57 include_widgets_lst.insert(0, 'turbogears.mochikit')
58
59 for name in include_widgets_lst:
60 widget = tg_util.load_class(name)
61 if widget is None:
62 log.debug("Could not load widget %s", name)
63 continue
64 if isclass(widget):
65 widget = widget()
66 if hasattr(widget, 'retrieve_resources') and hasattr(widget, 'inject'):
67
68 widget.inject()
69
70 include_widgets['tg_%s' % name.rsplit('.', 1)[-1]] = widget
71 output.update(include_widgets)
72
73
74
75 for value in output.itervalues():
76 if hasattr(value, 'retrieve_resources'):
77
78 continue
79 else:
80 try:
81 css_resources = value.retrieve_css()
82 except (AttributeError, TypeError):
83 css_resources = []
84 try:
85 js_resources = value.retrieve_javascript()
86 except (AttributeError, TypeError):
87 js_resources = []
88 css.add_all(css_resources)
89 for script in js_resources:
90 location = getattr(script, 'location', js_location.head)
91 js[location].add(script)
92 css.sort(key=lambda obj: getattr(obj, 'order', 0))
93 output['tg_css'] = css
94 for location in iter(js_location):
95 js[location].sort(key=lambda obj: getattr(obj, 'order', 0))
96 output['tg_js_%s' % location] = js[location]
97
98 tg_flash = _get_flash()
99 if tg_flash:
100 output['tg_flash'] = tg_flash
101
102 headers = {'Content-Type': content_type}
103 output = view.render(output, template=template, format=format,
104 headers=headers, fragment=fragment, **options)
105 content_type = headers['Content-Type']
106
107 if content_type:
108 response.headers['Content-Type'] = content_type
109 else:
110 content_type = response.headers.get('Content-Type', 'text/plain')
111
112 if content_type.startswith('text/'):
113 if isinstance(output, unicode):
114 output = output.encode(tg_util.get_template_encoding_default())
115
116 return output
117
121
126 """Validate input.
127
128 @param form: a form instance that must be passed throught the validation
129 process... you must give a the same form instance as the one that will
130 be used to post data on the controller you are putting the validate
131 decorator on.
132 @type form: a form instance
133
134 @param validators: individual validators to use for parameters.
135 If you use a schema for validation then the schema instance must
136 be the sole argument.
137 If you use simple validators, then you must pass a dictionary with
138 each value name to validate as a key of the dictionary and the validator
139 instance (eg: tg.validators.Int() for integer) as the value.
140 @type validators: dictionary or schema instance
141
142 @param failsafe_schema: a schema for handling failsafe values.
143 The default is 'none', but you can also use 'values', 'map_errors',
144 or 'defaults' to map erroneous inputs to values, corresponding exceptions
145 or method defaults.
146 @type failsafe_schema: errorhandling.FailsafeSchema
147
148 @param failsafe_values: replacements for erroneous inputs. You can either
149 define replacements for every parameter, or a single replacement value
150 for all parameters. This is only used when failsafe_schema is 'values'.
151 @type failsafe_values: a dictionary or a single value
152
153 @param state_factory: If this is None, the initial state for validation
154 is set to None, otherwise this must be a callable that returns the initial
155 state to be used for validation.
156 @type state_factory: callable or None
157
158 """
159 def entangle(func):
160 if callable(form) and not hasattr(form, 'validate'):
161 init_form = form
162 else:
163 init_form = lambda self: form
164
165 def validate(func, *args, **kw):
166
167 if hasattr(request, 'validation_state'):
168 return func(*args, **kw)
169
170 form = init_form(args and args[0] or kw['self'])
171 args, kw = tg_util.to_kw(func, args, kw)
172
173 errors = {}
174 if state_factory is not None:
175 state = state_factory()
176 else:
177 state = None
178
179 if form:
180 value = kw.copy()
181 try:
182 kw.update(form.validate(value, state))
183 except Invalid, e:
184 errors = e.unpack_errors()
185 request.validation_exception = e
186 request.validated_form = form
187
188 if validators:
189 if isinstance(validators, dict):
190 for field, validator in validators.iteritems():
191 try:
192 kw[field] = validator.to_python(
193 kw.get(field, None), state)
194 except Invalid, error:
195 errors[field] = error
196 else:
197 try:
198 value = kw.copy()
199 kw.update(validators.to_python(value, state))
200 except Invalid, e:
201 errors = e.unpack_errors()
202 request.validation_exception = e
203 request.validation_errors = errors
204 request.input_values = kw.copy()
205 request.validation_state = state
206
207 if errors:
208 kw = errorhandling.dispatch_failsafe(failsafe_schema,
209 failsafe_values, errors, func, kw)
210 args, kw = tg_util.from_kw(func, args, kw)
211 return errorhandling.run_with_errors(errors, func, *args, **kw)
212
213 return validate
214 return weak_signature_decorator(entangle)
215
218 """Resolve ambiguousness by calling the first method."""
221
222 always_overrides(First, Method)
223 first = First.make_decorator('first')
224
225
226 -def _add_rule(_expose, found_default, as_format, accept_format, template,
227 rulefunc):
228 if as_format == 'default':
229 if found_default:
230 as_format = template.split(':', 1)[0]
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'"
236 " and 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
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') == '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 accept = request.headers.get('Accept', "").lower()
349 accept = tg_util.simplify_http_accept_header(accept)
350 if not hasattr(func, '_expose'):
351 _build_rules(func)
352 try:
353 if hasattr(request, 'in_transaction'):
354 output = func._expose(func, accept, func._allow_json,
355 *args, **kw)
356 else:
357 request.in_transaction = True
358 output = database.run_with_transaction(
359 func._expose, func, accept, func._allow_json,
360 *args, **kw)
361 except NoApplicableMethods, e:
362 args = e.args
363 if (args and args[0] and isinstance(args[0], tuple)
364 and args[0][0] is func):
365
366
367
368
369
370 status = cherrypy.request.wsgi_environ.get('identity.status')
371 if status and str(status) >= '400':
372 raise cherrypy.HTTPError(*str(status).split(None, 1))
373 raise cherrypy.NotFound
374
375
376 raise
377 return output
378 func.exposed = True
379 func._ruleinfo = []
380 allow_json_from_config = config.get('tg.allow_json', False)
381 func._allow_json = allow_json_from_config or template == 'json'
382 else:
383 expose = lambda func, *args, **kw: func(*args, **kw)
384
385 func._ruleinfo.insert(0, dict(as_format=as_format,
386 accept_format=accept_format, template=template,
387 rulefunc=lambda _func, accept, allow_json, *args, **kw:
388 _execute_func(_func, template, format, content_type,
389 fragment, options, args, kw)))
390
391 if allow_json:
392 func._allow_json = True
393
394 return expose
395 return weak_signature_decorator(entangle)
396
397
398 -def _execute_func(func, template, format, content_type, fragment, options,
399 args, kw):
400 """Call controller method and process its output."""
401
402 if config.get('tg.strict_parameters', False):
403 tg_util.remove_keys(kw, ['tg_random', 'tg_format']
404 + config.get('tg.ignore_parameters', []))
405
406 else:
407
408 try:
409 tg_kw = dict([(k, v) for k, v in kw.items() if k in func._tg_args])
410
411 except AttributeError:
412 tg_kw = {}
413
414
415 args, kw = tg_util.adapt_call(func, args, kw)
416
417 kw.update(tg_kw)
418
419 env = config.get('environment') or 'development'
420 if env == 'development':
421
422
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
429
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)) or (
439 hasattr(output, '__iter__') and hasattr(output, 'next')), (
440 "Method %s.%s() returned unexpected output."
441 " Output should be of type basestring, dict, list or generator."
442 % (args[0].__class__.__name__, func.__name__))
443
444 if isinstance(output, dict):
445 template = output.pop('tg_template', template)
446 format = output.pop('tg_format', format)
447
448 if isinstance(template, basestring) and template.startswith('.'):
449 template = func.__module__[:func.__module__.rfind('.')] + template
450
451 return _process_output(output, template, format, content_type,
452 fragment, **options)
453
456 """Set a message to be displayed in the browser on next page display."""
457 message = tg_util.to_utf8(message)
458 if len(message) > 4000:
459 log.warning('Flash message exceeding maximum cookie size!')
460 response.cookie['tg_flash'] = message
461 response.cookie['tg_flash']['path'] = '/'
462
465 """Retrieve the flash message (if one is set), clearing the message."""
466 request_cookie = request.cookie
467 response_cookie = response.cookie
468
469 def clearcookie():
470 response_cookie['tg_flash'] = ''
471 response_cookie['tg_flash']['expires'] = 0
472 response_cookie['tg_flash']['path'] = '/'
473
474 if 'tg_flash' in response_cookie:
475 message = response_cookie['tg_flash'].value
476 response_cookie.pop('tg_flash')
477 if 'tg_flash' in request_cookie:
478
479 clearcookie()
480 elif 'tg_flash' in request_cookie:
481 message = request_cookie.value_decode(request_cookie['tg_flash'].value)[0]
482 if 'tg_flash' not in response_cookie:
483 clearcookie()
484 else:
485 message = None
486 if message:
487 message = unicode(message, 'utf-8')
488 return message
489
492 """Base class for a web application's controller.
493
494 It is important that your controllers inherit from this class, otherwise
495 ``identity.SecureResource`` and ``identity.SecureObject`` will not work
496 correctly.
497
498 """
499
500 is_app_root = None
501
504 """Base class for the root of a web application.
505
506 Your web application must have one of these. The root of your application
507 is used to compute URLs used by your app.
508
509 """
510
511 is_app_root = True
512
513 Root = RootController
517 """Descriptor used by RESTMethod to tell if it is exposed."""
518
520 """Return True if object has a method for HTTP method of current request
521 """
522 if cls is None:
523 cls = obj
524 cp_methodname = cherrypy.request.method
525 methodname = cp_methodname.lower()
526 method = getattr(cls, methodname, None)
527 if callable(method) and getattr(method, 'exposed', False):
528 return True
529 raise cherrypy.HTTPError(405, '%s not allowed on %s' % (
530 cp_methodname, cherrypy.request.browser_url))
531
534 """Allow REST style dispatch based on different HTTP methods.
535
536 For an elaborate usage example see turbogears.tests.test_restmethod.
537
538 In short, instead of an exposed method, you define a sub-class of
539 RESTMethod inside the controller class and inside this class you define
540 exposed methods named after each HTTP method that should be supported.
541
542 Example::
543
544 class Controller(controllers.Controller):
545
546 class article(copntrollers.RESTMethod):
547 @expose()
548 def get(self, id):
549 ...
550
551 @expose()
552 def post(self, id):
553 ...
554
555 """
556 exposed = ExposedDescriptor()
557
559 methodname = cherrypy.request.method.lower()
560 self.result = getattr(self, methodname)(*l, **kw)
561
563 return iter(self.result)
564
565
566 -def url(tgpath, tgparams=None, **kw):
567 """Computes relocatable URLs.
568
569 tgpath can be a list or a string. If the path is absolute (starts with a
570 "/"), the server.webpath, SCRIPT_NAME and the approot of the application
571 are prepended to the path. In order for the approot to be detected
572 properly, the root object must extend controllers.RootController.
573
574 Query parameters for the URL can be passed in as a dictionary in
575 the second argument and/or as keyword parameters where keyword args
576 overwrite entries in the dictionary.
577
578 Values which are lists or tuples will create multiple key-value pairs.
579
580 tgpath may also already contain a (properly escaped) query string seperated
581 by a question mark, in which case additional query params are appended.
582
583 """
584 if not isinstance(tgpath, basestring):
585 tgpath = '/'.join(list(tgpath))
586 if tgpath.startswith('/'):
587 webpath = config.server.get('server.webpath', '')
588 if tg_util.request_available():
589 tgpath = cp_url(tgpath, relative='server')
590 if not request.script_name.startswith(webpath):
591
592 tgpath = webpath + tgpath
593 elif webpath:
594
595 tgpath = webpath + tgpath
596 if tgparams is None:
597 tgparams = kw
598 else:
599 try:
600 tgparams = tgparams.copy()
601 tgparams.update(kw)
602 except AttributeError:
603 raise TypeError('url() expects a dictionary for query parameters')
604 args = []
605 for key, value in tgparams.iteritems():
606 if value is None:
607 continue
608 if isinstance(value, (list, tuple)):
609 pairs = [(key, v) for v in value]
610 else:
611 pairs = [(key, value)]
612 for k, v in pairs:
613 if v is None:
614 continue
615 if isinstance(v, unicode):
616 v = v.encode('utf-8')
617 args.append((k, str(v)))
618 if args:
619 query_string = urllib.urlencode(args, True)
620 if '?' in tgpath:
621 tgpath += '&' + query_string
622 else:
623 tgpath += '?' + query_string
624 return tgpath
625
628 """Return name of the server this application runs on.
629
630 Respects 'Host' and 'X-Forwarded-Host' header.
631
632 See the docstring of the 'absolute_url' function for more information.
633
634 """
635 get = config.get
636 h = request.headers
637 host = get('tg.url_domain') or h.get('X-Forwarded-Host', h.get('Host'))
638 if not host:
639 host = '%s:%s' % (get('server.socket_host', 'localhost'),
640 get('server.socket_port', 8080))
641 return host
642
645 """Return absolute URL (including schema and host to this server).
646
647 Tries to account for 'Host' header and reverse proxying
648 ('X-Forwarded-Host').
649
650 The host name is determined this way:
651
652 * If the config setting 'tg.url_domain' is set and non-null, use this value.
653 * Else, if the 'base_url_filter.use_x_forwarded_host' config setting is
654 True, use the value from the 'Host' or 'X-Forwarded-Host' request header.
655 * Else, if config setting 'base_url_filter.on' is True and
656 'base_url_filter.base_url' is non-null, use its value for the host AND
657 scheme part of the URL.
658 * As a last fallback, use the value of 'server.socket_host' and
659 'server.socket_port' config settings (defaults to 'localhost:8080').
660
661 The URL scheme ('http' or 'http') used is determined in the following way:
662
663 * If 'base_url_filter.base_url' is used, use the scheme from this URL.
664 * If there is a 'X-Use-SSL' request header, use 'https'.
665 * Else, if the config setting 'tg.url_scheme' is set, use its value.
666 * Else, use the value of 'cherrypy.request.scheme'.
667
668 """
669 get = config.get
670 use_xfh = get('base_url_filter.use_x_forwarded_host', False)
671 if request.headers.get('X-Use-SSL'):
672 scheme = 'https'
673 else:
674 scheme = get('tg.url_scheme')
675 if not scheme:
676 scheme = request.scheme
677 base_url = '%s://%s' % (scheme, get_server_name())
678 if get('base_url_filter.on', False) and not use_xfh:
679 base_url = get('base_url_filter.base_url').rstrip('/')
680 return '%s%s' % (base_url, url(tgpath, params, **kw))
681
682
683 -def redirect(redirect_path, redirect_params=None, **kw):
684 """Redirect (via cherrypy.HTTPRedirect).
685
686 Raises the exception instead of returning it, this to allow
687 users to both call it as a function or to raise it as an exception.
688
689 """
690 if not isinstance(redirect_path, basestring):
691 redirect_path = '/'.join(list(redirect_path))
692 if not (redirect_path.startswith('/')
693 or redirect_path.startswith('http://')
694 or redirect_path.startswith('https://')):
695 redirect_path = urlparse.urljoin(request.path_info, redirect_path)
696 raise cherrypy.HTTPRedirect(url(redirect_path, redirect_params, **kw))
697