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 |