pylons.decorators
Covered: 146 lines
Missed: 17 lines
Skipped 45 lines
Percent: 89 %
  1
"""Pylons Decorators
  3
Common decorators intended for use in controllers. Additional
  4
decorators for use with controllers are in the
  5
:mod:`~pylons.decorators.cache`, :mod:`~pylons.decorators.rest` and
  6
:mod:`~pylons.decorators.secure` modules.
  8
"""
  9
import logging
 10
import sys
 11
import warnings
 13
import formencode
 14
import simplejson
 15
from decorator import decorator
 16
from formencode import api, htmlfill, variabledecode
 17
from webob.multidict import UnicodeMultiDict
 19
from pylons.decorators.util import get_pylons
 20
from pylons.i18n import _ as pylons_gettext
 22
__all__ = ['jsonify', 'validate']
 24
log = logging.getLogger(__name__)
 26
@decorator
 27
def jsonify(func, *args, **kwargs):
 28
    """Action decorator that formats output for JSON
 30
    Given a function that will return content, this decorator will turn
 31
    the result into JSON, with a content-type of 'application/json' and
 32
    output it.
 34
    """
 35
    pylons = get_pylons(args)
 36
    pylons.response.headers['Content-Type'] = 'application/json'
 37
    data = func(*args, **kwargs)
 38
    if isinstance(data, (list, tuple)):
 39
        msg = "JSON responses with Array envelopes are susceptible to " \
 40
              "cross-site data leak attacks, see " \
 41
              "http://pylonshq.com/warnings/JSONArray"
 42
        warnings.warn(msg, Warning, 2)
 43
        log.warning(msg)
 44
    log.debug("Returning JSON wrapped action output")
 45
    return simplejson.dumps(data)
 48
def 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
 54
    Given a form schema or dict of validators, validate will attempt to
 55
    validate the schema or validator list.
 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.
 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.
 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.
 93
    Example::
 95
        class SomeController(BaseController):
 97
            def create(self, id):
 98
                return render('/myform.mako')
100
            @validate(schema=model.forms.myshema(), form='create')
101
            def update(self, id):
103
                pass
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 = {}
114
        if not on_get and request.environ['REQUEST_METHOD'] == 'GET':
115
            return func(self, *args, **kwargs)
118
        if post_only:
119
            params = request.POST
120
        else:
121
            params = request.params
123
        params = params.mixed()
124
        if variable_decode:
125
            log.debug("Running variable_decode on params")
126
            decoded = variabledecode.variable_decode(params, dict_char,
127
                                                     list_char)
128
        else:
129
            decoded = params
131
        if schema:
132
            log.debug("Validating against a schema")
133
            try:
134
                self.form_result = schema.to_python(decoded, state)
135
            except formencode.Invalid, e:
136
                errors = e.unpack_errors(variable_decode, dict_char, list_char)
137
        if validators:
138
            log.debug("Validating against provided validators")
139
            if isinstance(validators, dict):
140
                if not hasattr(self, 'form_result'):
141
                    self.form_result = {}
142
                for field, validator in validators.iteritems():
143
                    try:
144
                        self.form_result[field] = \
145
                            validator.to_python(decoded.get(field), state)
146
                    except formencode.Invalid, error:
147
                        errors[field] = error
148
        if errors:
149
            log.debug("Errors found in validation, parsing form with htmlfill "
150
                      "for errors")
151
            request.environ['REQUEST_METHOD'] = 'GET'
152
            self._py_object.tmpl_context.form_errors = errors
156
            if not form:
157
                return func(self, *args, **kwargs)
159
            request.environ['pylons.routes_dict']['action'] = form
160
            response = self._dispatch_call()
163
            if hasattr(response, '_exception'):
164
                return response
166
            htmlfill_kwargs2 = htmlfill_kwargs.copy()
167
            htmlfill_kwargs2.setdefault('encoding', request.charset)
168
            return htmlfill.render(response, defaults=params, errors=errors,
169
                                   **htmlfill_kwargs2)
170
        return func(self, *args, **kwargs)
171
    return decorator(wrapper)
174
def pylons_formencode_gettext(value):
175
    """Translates a string ``value`` using pylons gettext first and if
176
    that fails, formencode gettext.
178
    This allows to "merge" localized error messages from built-in
179
    FormEncode's validators with application-specific validators.
181
    """
182
    trans = pylons_gettext(value)
183
    if trans == value:
185
        trans = api._stdtrans(value)
186
    return trans
189
class PylonsFormEncodeState(object):
190
    """A ``state`` for FormEncode validate API that includes smart
191
    ``_`` hook.
193
    The FormEncode library used by validate() decorator has some
194
    provision for localizing error messages. In particular, it looks
195
    for attribute ``_`` in the application-specific state object that
196
    gets passed to every ``.to_python()`` call. If it is found, the
197
    ``_`` is assumed to be a gettext-like function and is called to
198
    localize error messages.
200
    One complication is that FormEncode ships with localized error
201
    messages for standard validators so the user may want to re-use
202
    them instead of gathering and translating everything from scratch.
203
    To allow this, we pass as ``_`` a function which looks up
204
    translation both in application and formencode message catalogs.
206
    """
207
    _ = staticmethod(pylons_formencode_gettext)