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
|
|
|