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
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
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
64 widget.inject()
65
66 include_widgets['tg_%s' % name.rsplit('.', 1)[-1]] = widget
67 output.update(include_widgets)
68
69
70
71 for value in output.itervalues():
72 if hasattr(value, 'retrieve_resources'):
73
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
117
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
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
214 """Resolve ambiguousness by calling the first method."""
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
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
364 if (args and args[0] and isinstance(args[0], tuple)
365 and args[0][0] is func):
366
367
368
369
370
371 status = cherrypy.response.status
372 if status and status // 100 == 4:
373 raise cherrypy.HTTPError(status)
374 raise cherrypy.NotFound
375
376
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
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
416 args, kw = tg_util.adapt_call(func, args, kw)
417
418 kw.update(tg_kw)
419
420 if config.get('server.environment', 'development') == '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)), (
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
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
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
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
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
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
513 """Default method for logging access"""
514
515
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
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
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
557 """Descriptor used by RESTMethod to tell if it is exposed."""
558
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
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
600 methodname = cherrypy.request.method.lower()
601 self.result = getattr(self, methodname)(*l, **kw)
602
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
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
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
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
734
735
736
737
738
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