root/pylons/decorators/__init__.py

Revision 2058:45f1838d28a5, 10.7 KB (checked in by Ben Bangert <ben@…>, 4 months ago)

* WARNING: Removed legacy pylons.c and pylons.g globals.

Line 
1"""Pylons Decorators
2
3Common decorators intended for use in controllers. Additional
4decorators for use with controllers are in the
5:mod:`~pylons.decorators.cache`, :mod:`~pylons.decorators.rest` and
6:mod:`~pylons.decorators.secure` modules.
7
8"""
9import logging
10import sys
11import warnings
12
13import formencode
14import simplejson
15from decorator import decorator
16from formencode import api, htmlfill, variabledecode
17from webob import UnicodeMultiDict
18
19from pylons.decorators.util import get_pylons
20from pylons.i18n import _ as pylons_gettext
21
22__all__ = ['jsonify', 'validate']
23
24log = logging.getLogger(__name__)
25
26def jsonify(func, *args, **kwargs):
27    """Action decorator that formats output for JSON
28
29    Given a function that will return content, this decorator will turn
30    the result into JSON, with a content-type of 'application/json' and
31    output it.
32   
33    """
34    pylons = get_pylons(args)
35    pylons.response.headers['Content-Type'] = 'application/json'
36    data = func(*args, **kwargs)
37    if isinstance(data, (list, tuple)):
38        msg = "JSON responses with Array envelopes are susceptible to " \
39              "cross-site data leak attacks, see " \
40              "http://pylonshq.com/warnings/JSONArray"
41        warnings.warn(msg, Warning, 2)
42        log.warning(msg)
43    log.debug("Returning JSON wrapped action output")
44    return simplejson.dumps(data)
45jsonify = decorator(jsonify)
46
47
48def validate(schema=None, validators=None, form=None, variable_decode=False,
49             dict_char='.', list_char='-', post_only=True, state=None,
50             on_get=False, **htmlfill_kwargs):
51    """Validate input either for a FormEncode schema, or individual
52    validators
53
54    Given a form schema or dict of validators, validate will attempt to
55    validate the schema or validator list.
56
57    If validation was successful, the valid result dict will be saved
58    as ``self.form_result``. Otherwise, the action will be re-run as if
59    it was a GET, and the output will be filled by FormEncode's
60    htmlfill to fill in the form field errors.
61
62    ``schema``
63        Refers to a FormEncode Schema object to use during validation.
64    ``form``
65        Method used to display the form, which will be used to get the
66        HTML representation of the form for error filling.
67    ``variable_decode``
68        Boolean to indicate whether FormEncode's variable decode
69        function should be run on the form input before validation.
70    ``dict_char``
71        Passed through to FormEncode. Toggles the form field naming
72        scheme used to determine what is used to represent a dict. This
73        option is only applicable when used with variable_decode=True.
74    ``list_char``
75        Passed through to FormEncode. Toggles the form field naming
76        scheme used to determine what is used to represent a list. This
77        option is only applicable when used with variable_decode=True.
78    ``post_only``
79        Boolean that indicates whether or not GET (query) variables
80        should be included during validation.
81       
82        .. warning::
83            ``post_only`` applies to *where* the arguments to be
84            validated come from. It does *not* restrict the form to
85            only working with post, merely only checking POST vars.
86    ``state``
87        Passed through to FormEncode for use in validators that utilize
88        a state object.
89    ``on_get``
90        Whether to validate on GET requests. By default only POST
91        requests are validated.
92
93    Example::
94
95        class SomeController(BaseController):
96
97            def create(self, id):
98                return render('/myform.mako')
99
100            @validate(schema=model.forms.myshema(), form='create')
101            def update(self, id):
102                # Do something with self.form_result
103                pass
104
105    """
106    if state is None:
107        state = PylonsFormEncodeState
108    def wrapper(func, self, *args, **kwargs):
109        """Decorator Wrapper function"""
110        request = self._py_object.request
111        errors = {}
112       
113        # Skip the validation if on_get is False and its a GET
114        if not on_get and request.environ['REQUEST_METHOD'] == 'GET':
115            return func(self, *args, **kwargs)
116       
117        # If they want post args only, use just the post args
118        if post_only:
119            params = request.POST
120        else:
121            params = request.params
122       
123        is_unicode_params = isinstance(params, UnicodeMultiDict)
124        params = params.mixed()
125        if variable_decode:
126            log.debug("Running variable_decode on params")
127            decoded = variabledecode.variable_decode(params, dict_char,
128                                                     list_char)
129        else:
130            decoded = params
131
132        if schema:
133            log.debug("Validating against a schema")
134            try:
135                self.form_result = schema.to_python(decoded, state)
136            except formencode.Invalid, e:
137                errors = e.unpack_errors(variable_decode, dict_char, list_char)
138        if validators:
139            log.debug("Validating against provided validators")
140            if isinstance(validators, dict):
141                if not hasattr(self, 'form_result'):
142                    self.form_result = {}
143                for field, validator in validators.iteritems():
144                    try:
145                        self.form_result[field] = \
146                            validator.to_python(decoded.get(field), state)
147                    except formencode.Invalid, error:
148                        errors[field] = error
149        if errors:
150            log.debug("Errors found in validation, parsing form with htmlfill "
151                      "for errors")
152            request.environ['REQUEST_METHOD'] = 'GET'
153            self._py_object.tmpl_context.form_errors = errors
154
155            # If there's no form supplied, just continue with the current
156            # function call.
157            if not form:
158                return func(self, *args, **kwargs)
159
160            request.environ['pylons.routes_dict']['action'] = form
161            response = self._dispatch_call()
162            # XXX: Legacy WSGIResponse support
163            legacy_response = False
164            if hasattr(response, 'content'):
165                form_content = ''.join(response.content)
166                legacy_response = True
167            else:
168                form_content = response
169                response = self._py_object.response
170           
171            # If the form_content is an exception response, return it
172            if hasattr(form_content, '_exception'):
173                return form_content
174           
175            # Ensure htmlfill can safely combine the form_content, params and
176            # errors variables (that they're all of the same string type)
177            if not is_unicode_params:
178                log.debug("Raw string form params: ensuring the '%s' form and "
179                          "FormEncode errors are converted to raw strings for "
180                          "htmlfill", form)
181                encoding = determine_response_charset(response)
182
183                # WSGIResponse's content may (unlikely) be unicode
184                if isinstance(form_content, unicode):
185                    form_content = form_content.encode(encoding,
186                                                       response.errors)
187
188                # FormEncode>=0.7 errors are unicode (due to being localized
189                # via ugettext). Convert any of the possible formencode
190                # unpack_errors formats to contain raw strings
191                errors = encode_formencode_errors(errors, encoding,
192                                                  response.errors)
193            elif not isinstance(form_content, unicode):
194                log.debug("Unicode form params: ensuring the '%s' form is "
195                          "converted to unicode for htmlfill", form)
196                encoding = determine_response_charset(response)
197                form_content = form_content.decode(encoding)
198
199            form_content = htmlfill.render(form_content, defaults=params,
200                                           errors=errors, **htmlfill_kwargs)
201            if legacy_response:
202                # Let the Controller merge the legacy response
203                response.content = form_content
204                return response
205            else:
206                return form_content
207        return func(self, *args, **kwargs)
208    return decorator(wrapper)
209
210
211def determine_response_charset(response):
212    """Determine the charset of the specified Response object,
213    returning the default system encoding when none is set"""
214    charset = response.charset
215    if charset is None:
216        charset = sys.getdefaultencoding()
217    log.debug("Determined result charset to be: %s", charset)
218    return charset
219
220
221def encode_formencode_errors(errors, encoding, encoding_errors='strict'):
222    """Encode any unicode values contained in a FormEncode errors dict
223    to raw strings of the specified encoding"""
224    if errors is None or isinstance(errors, str):
225        # None or Just incase this is FormEncode<=0.7
226        pass
227    elif isinstance(errors, unicode):
228        errors = errors.encode(encoding, encoding_errors)
229    elif isinstance(errors, dict):
230        for key, value in errors.iteritems():
231            errors[key] = encode_formencode_errors(value, encoding,
232                                                   encoding_errors)
233    else:
234        # Fallback to an iterable (a list)
235        errors = [encode_formencode_errors(error, encoding, encoding_errors) \
236                      for error in errors]
237    return errors
238
239
240def pylons_formencode_gettext(value):
241    """Translates a string ``value`` using pylons gettext first and if
242    that fails, formencode gettext.
243   
244    This allows to "merge" localized error messages from built-in
245    FormEncode's validators with application-specific validators.
246
247    """
248    trans = pylons_gettext(value)
249    if trans == value:
250        # translation failed, try formencode
251        trans = api._stdtrans(value)
252    return trans
253
254
255class PylonsFormEncodeState(object):
256    """A ``state`` for FormEncode validate API that includes smart
257    ``_`` hook.
258
259    The FormEncode library used by validate() decorator has some
260    provision for localizing error messages. In particular, it looks
261    for attribute ``_`` in the application-specific state object that
262    gets passed to every ``.to_python()`` call. If it is found, the
263    ``_`` is assumed to be a gettext-like function and is called to
264    localize error messages.
265
266    One complication is that FormEncode ships with localized error
267    messages for standard validators so the user may want to re-use
268    them instead of gathering and translating everything from scratch.
269    To allow this, we pass as ``_`` a function which looks up
270    translation both in application and formencode message catalogs.
271   
272    """
273    _ = staticmethod(pylons_formencode_gettext)
Note: See TracBrowser for help on using the browser.


Powered by Pylons - Contact Administrators