Latest Version: 0.9.6.2
  Dashboard > Pylons Cookbook > ... > Recipes > Hacking validate into a split functionality
  Pylons Cookbook Log In | Sign Up   View a printable version of the current page.  
  Hacking validate into a split functionality
Added by jonathan, last edited by jonathan on Apr 17, 2008
Labels: 
(None)

I really don't like the validate decorator

  • it encapsulates the validation & error generation into one action, only callable by as a decorator
  • as-is, you can get a post form without tripping an error
  • a ton of other issues

so... we hack it!

below is work-in-progress to replace validate. what did i do?

  • added kwarg auto_error (default True) , automatically redirects to an error
  • added kwarg post_gatekeeper , automatically fails if you get a post form
  • split validate into 3 functions
  • @validate - decorator
  • validate_form - can be called from wherever
  • validate_error - can be called from wherever

why?

let's say your schema validates.. but you run some operations in your action, and now you want to fail...

return validate_error( form="form", errors= { 'login':'i hate your username' }

let's say you don't want to use a decorator...

is_valid= validate_form ( form="form", schema=schema )
if not is_valid:
return validate_error( form="form", errors= { 'login':'i still hate your username' }

but wait! there's more!

what if we validated one or more schemas for a crazy reason?

that is ok!

enter...
compute_validation_store( schema= schema, validators= validators )

this returns a unique namespace within c.pylons_forms that all the validation data is stored in. ooh. neat.

this needs another pass at refactoring by someone... there's a bit of overkill and redundant data caching... however it makes for a VERY extensible system of formencode support. you can now dispatch errors!

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def validate(schema=None, validators=None, form=None, variable_decode=False,
             dict_char='.', list_char='-', post_only=True, state=None,
             on_get=False, post_gatekeeper= False, auto_error= True, 
             **htmlfill_kwargs):
    """Validate input either for a FormEncode schema, or individual validators

    @validate is used as an action decorator to attempt the validation of a 
    schema or validator list.
    
    this decorator wraps validate_form, and by default calls validate_error
    
    please read the docstring on validate_form for kwargs info    

    ``auto_error``
        Automatically calls validation_error, which redispatches to the form.
        This is enabled by default, preserving backwards compatibility.

    Example:

    .. code-block:: Python

        class SomeController(BaseController):

            def create(self, id):
                return render('/myform.mako')

            @validate(schema=model.forms.myshema(), form='create')
            def update(self, id):
                # Do something with self.form_result
                pass
    """
    def wrapper(func, self, *args, **kwargs):
        """Decorator Wrapper function"""
        validate_form( self , schema=schema, validators=validators, form=form, 
             variable_decode=variable_decode, dict_char=dict_char, list_char=list_char, 
             post_only=post_only, state=state, on_get=on_get, 
             **htmlfill_kwargs )

        if pylons.c.form_errors:
            log.debug("Errors found in validation, parsing form with htmlfill "
                      "for errors")
            request = pylons.request._current_obj()
            request.environ['REQUEST_METHOD'] = 'GET'

            if auto_error:
                return validation_error( self, form= form, errors= pylons.c.form_errors, *args, **kwargs)
            else:
                # pass, it'll return the func as normal
                pass 
        return func(self, *args, **kwargs)
    return decorator(wrapper)


    
def validate_form( self, schema=None, validators=None, form=None, 
             variable_decode=False, dict_char=None, list_char=None, 
             post_only=True, state=None, on_get=False, post_gatekeeper= False, 
             **htmlfill_kwargs ):
    """Validate input either for a FormEncode schema, or individual validators

    Given a form schema or dict of validators, validate will attempt to
    validate the schema or validator list.

    If validation was successful, the valid result dict will be saved
    as ``self.form_result``. Otherwise, the action will be re-run as if it was
    a GET, and the output will be filled by FormEncode's htmlfill to fill in
    the form field errors.
    
    the first argument is self -- a pylons controller object

    ``schema``
        Refers to a FormEncode Schema object to use during validation.
    ``form``
        Method used to display the form, which will be used to get the 
        HTML representation of the form for error filling.
    ``variable_decode``
        Boolean to indicate whether FormEncode's variable decode function 
        should be run on the form input before validation.
    ``dict_char``
        Passed through to FormEncode. Toggles the form field naming 
        scheme used to determine what is used to represent a dict. This
        option is only applicable when used with variable_decode=True.
    ``list_char``
        Passed through to FormEncode. Toggles the form field naming
        scheme used to determine what is used to represent a list. This
        option is only applicable when used with variable_decode=True.
    ``post_only``
        Boolean that indicates whether or not GET (query) variables should
        be included during validation.
        
        .. warning::
            ``post_only`` applies to *where* the arguments to be
            validated come from. It does *not* restrict the form to only
            working with post, merely only checking POST vars.
    ``state``
        Passed through to FormEncode for use in validators that utilize
        a state object.
    ``on_get``
        Whether to validate on GET requests. By default only POST requests
        are validated.
    ``post_gatekeeper``
        Boolean that indicates to fail on GET requests.  

    Example:

    .. code-block:: Python

        class SomeController(BaseController):

            def create(self, id):
                return render('/myform.mako')

            @validate(schema=model.forms.myshema(), form='create')
            def update(self, id):
                # Do something with self.form_result
                pass
    """
    request = pylons.request._current_obj()
    pylons.c.form_errors= {}
    
    if not pylons.c.pylons_forms:
        pylons.c.pylons_forms= {}

    # Zuul, Gozer would be proud of you.
    if post_gatekeeper and request.environ['REQUEST_METHOD'] == 'GET':
        pylons.c.form_errors['ErrorMain']= 'GETing a POST only form'
        return False

    # Skip the validation if on_get is False and its a GET
    if not on_get and request.environ['REQUEST_METHOD'] == 'GET':
        return func(self, *args, **kwargs)
    
    # If they want post args only, use just the post args
    if post_only:
        params = request.POST
    else:
        params = request.params
    
    is_unicode_params = isinstance(params, UnicodeMultiDict)
    params = params.mixed()
    if variable_decode:
        log.debug("Running variable_decode on params")
        decoded = variabledecode.variable_decode(params, dict_char,
                                                 list_char)
    else:
        decoded = params

    
    if schema:
        log.debug("Validating against a schema")
        try:
            self.form_result = schema.to_python(decoded, state)
        except formencode.Invalid, e:
            pylons.c.form_errors = e.unpack_errors(variable_decode, dict_char, list_char)
    if validators:
        log.debug("Validating against provided validators")
        if isinstance(validators, dict):
            if not hasattr(self, 'form_result'):
                self.form_result = {}
            for field, validator in validators.iteritems():
                try:
                    self.form_result[field] = \
                        validator.to_python(decoded.get(field), state)
                except formencode.Invalid, error:
                    pylons.c.form_errors[field] = error

    validation_store = compute_validation_store( schema=schema, validators=validators )
    pylons.c.pylons_forms[validation_store] = {
        'errors': pylons.c.form_errors.copy(),
        'result': {},
        'is_unicode_params' : is_unicode_params,
        'is_validated' : True,
        'params' : params.copy(),
    }
    if hasattr(self, 'form_result'):
       pylons.c.pylons_forms[validation_store]['result']= self.form_result

    pylons.c.form_validated = True
    pylons.c.form_params = params
    pylons.c.form_is_unicode_params = is_unicode_params
    pylons.c.form_last_validated= validation_store
    
    if pylons.c.form_errors:
        return False
    return True
    
def compute_validation_store( schema=None, validators=None ):
    """compute a validation store field
    """
    validation_store_schema= ''
    validation_store_validators= ''
    if schema:
        validation_store_schema = "%s"%schema.__class__
    if validators:
        if isinstance(validators, dict):
            validation_store_validators= []
            for field, validator in validators.iteritems():
                validation_store_validators.append(field)
            validation_store_validators.sort()
            validation_store_validators = ':'.join(validation_store)

    validation_store= '%s|%s' % ( validation_store_schema , validation_store_validators )
    return validation_store

def validation_error( self, form=None, errors= None, schema=None , validators= None, *args, **htmlfill_kwargs):
    """redispatch to a form generating function
    
    this requires that the form was validated with validate_form to set 
    certain environment variables in pylons.c
    
    Example:

    .. code-block:: Python

        class SomeController(BaseController):

            def form_print(self, id):
                return render('/myform.mako')

            @validate(schema=model.forms.myshema(), form='form_print')
            def form_submit(self, id):
                result= database_query()
                if not result:
                    errors= {'login' : "oh no you don't" }
                    return validation_error( 
                        self, 
                        form= 'form_print', 
                        errors= errors
                    ) 
    """
    # If there's no form supplied, just continue with the current
    # function call.
    if not form:
        raise ValueError( 'form is required' )

    if not errors:
        raise ValueError( 'errors dict is required' )
    
    validation_store = pylons.c.form_last_validated
    if schema or validators:
        validation_store = compute_validation_store( schema=schema, validators=validators )

    if validation_store not in pylons.c.pylons_forms:
        raise ValueError( 'form has not been validated' )

    request = pylons.request._current_obj()
    
    request.environ['pylons.routes_dict']['action'] = form
    response = self._dispatch_call()
    # XXX: Legacy WSGIResponse support
    legacy_response = False
    if hasattr(response, 'wsgi_response'):
        form_content = ''.join(response.content)
        legacy_response = True
    else:
        form_content = response
        response = pylons.response._current_obj()

    # Ensure htmlfill can safely combine the form_content, params and
    # errors variables (that they're all of the same string type)
    if not pylons.c.pylons_forms[validation_store]['is_unicode_params']:
        log.debug("Raw string form params: ensuring the '%s' form and "
                  "FormEncode errors are converted to raw strings for "
                  "htmlfill", form)
        encoding = determine_response_charset(response)

        # WSGIResponse's content may (unlikely) be unicode
        if isinstance(form_content, unicode):
            form_content = form_content.encode(encoding,
                                               response.errors)

        # FormEncode>=0.7 errors are unicode (due to being localized
        # via ugettext). Convert any of the possible formencode
        # unpack_errors formats to contain raw strings
        errors = encode_formencode_errors(errors, encoding,
                                          response.errors)
    elif not isinstance(form_content, unicode):
        log.debug("Unicode form params: ensuring the '%s' form is "
                  "converted to unicode for htmlfill", form)
        encoding = determine_response_charset(response)
        form_content = form_content.decode(encoding)

    print pylons.c.pylons_forms[validation_store]
    defaults= pylons.c.pylons_forms[validation_store]['result'].copy()
    for item in errors.keys():
        if item in defaults:
           del defaults[item]

    form_content = htmlfill.render(form_content, defaults=defaults,
                                   errors=errors, **htmlfill_kwargs)
    if legacy_response:
        # Let the Controller merge the legacy response
        response.content = form_content
        return response
    else:
        return form_content

Site running on a free Atlassian Confluence Open Source Project License granted to Pylons. Evaluate Confluence today.
Powered by Atlassian Confluence, the Enterprise Wiki. (Version: 2.3.3 Build:#645 Feb 13, 2007) - Bug/feature request - Contact Administrators
Top