-
-
Save jvanasco/1776061 to your computer and use it in GitHub Desktop.
FormGenerator to extend Deform functionality
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Simple module to ease form handling with ``Deform``. The provided | |
``FormGenerator`` class handles repetitive tasks like validation, | |
xhr requests, and recovering from exceptions thrown by the model. | |
### Begin example scenario: Adding a new user | |
from myschemas import UserSchema | |
from formgenerator import ( | |
FormGenerator, | |
Success, | |
Failure, | |
MultipleFailures, | |
) | |
# our database of users | |
USERS = ['arthur', 'trillian', 'marvin'] | |
# this function will be passed the validated controls, and | |
# must return Success, Failure, or MultipleFailures | |
def add_new_user(validatedcontrols): | |
username = validatedcontrols['username'] | |
if username in USERS: | |
return Failure('username', 'username must be unique') | |
else: | |
USERS.append(username) | |
return Success() | |
# this is the response to use if the user is created successfully | |
def user_created_successfully(username): | |
return Response('account created for %s' % username) | |
# the view function | |
@view_config(route_name='createuser', renderer='form.html', | |
permission='admin') | |
def createuser(request): | |
schema = UserSchema() | |
form = FormGenerator(request, | |
schema, | |
onsuccess=user_created_successfully, | |
process_controls=add_new_user, | |
) | |
return form.render() | |
### End example scenario | |
Note that the ``onsuccess`` and ``process_controls`` functions are limited | |
intentionally, because the ``FormGenerator`` can't hope to accommodate | |
every possible use. Instead, the caller is advised to prepare functions | |
that accept the appropriate parameters and pass those to the ``FormGenerator`` | |
instead. | |
Let's say you have a function that relies on the current ``request`` to produce | |
a result: | |
def post_validate(request, kw): | |
if request.something: | |
return Success() | |
else: | |
return Failure('somefield', 'some error') | |
It would appear at first that the function cannot be used with the | |
``FormGenerator`` because the ``request`` parameter wouldn't be made available. | |
The python standard library is equipped to handle just such a case. | |
Add the following import: | |
from functools import partial | |
And now when you create the ``FormGenerator``, your ``process_controls`` | |
function becomes: | |
def myview(request): | |
process_controls = partial(post_validate, request) | |
form = FormGenerator(... | |
process_controls=process_controls, | |
... | |
) | |
return form.render() | |
By partially applying parameters to a function, you can create a new function | |
that takes only the parameters expected by the ``FormGenerator``. The same idea | |
can be applied to your ``onsuccess`` function. | |
""" | |
import peppercorn | |
import colander as co | |
from functools import partial | |
from pyramid.response import Response | |
from deform import ( | |
Form, | |
ValidationFailure, | |
exception, | |
) | |
from abc import ( | |
ABCMeta, | |
abstractproperty, | |
) | |
class Result: | |
"""A simple data type used for communicating with a ``FormGenerator``. | |
``FormGenerator`` expects supplied ``process_controls`` function to return | |
a subclass of ``Result`` to indicate success or failure.""" | |
__metaclass__ = ABCMeta | |
@abstractproperty | |
def succeeded(self): | |
"""Must be subclassed. ``FormGenerator`` will check ``succeeded`` | |
property to determine how to render the form.""" | |
return NotImplemented | |
class Success(Result): | |
"""A possible return type for a ``process_controls`` function supplied to | |
a ``FormGenerator``. Indicates form processing was successful.""" | |
def __init__(self, result=None): | |
"""Allows caller to attach an arbitrary ``result`` object on success. | |
This is primarily used for returning a successfully created or | |
updated model object. | |
Note: if ``result`` is not None, it will be supplied as a parameter | |
to the ``FormGenerator``'s ``success`` function for post-processing.""" | |
self.result = result | |
@property | |
def succeeded(self): | |
"""This subclass always returns ``True``.""" | |
return True | |
class Failure(Result): | |
"""A possible return type for a ``process_controls`` function supplied to | |
a ``FormGenerator``. Indicates form processing failed. | |
Usage: ``Failure(schema_field_name, error_string)`` | |
""" | |
def __init__(self, field, error): | |
"""Supply a ``field`` (a ``Colander`` schema node name) and an | |
``error`` (a string) to be made accessible to ``FormGenerator`` | |
for error handling.""" | |
self.field = field | |
self.error = error | |
@property | |
def succeeded(self): | |
"""This subclass always returns ``False``.""" | |
return False | |
class MultipleFailures(Result): | |
"""A possible return type for a ``process_controls`` function supplied to | |
a ``FormGenerator``. Indicates form processing failed and contains | |
a list of ``Failure`` objects. | |
Usage: ``MultipleFailures([Failure('node1', 'error1'), | |
Failure('node2', 'error2')])`` | |
""" | |
def __init__(self, failures): | |
"""Initialize with a list of ``Failure`` objects.""" | |
self.failures = failures | |
@property | |
def succeeded(self): | |
"""This subclass always returns ``False``.""" | |
return False | |
class FormGenerator(object): | |
"""High-level interface to ``Deform`` that will either: | |
1) Render an empty form | |
2) Render a form with a supplied ``appstruct`` | |
3) Re-render a form on failure, displaying errors | |
The caller must supply: | |
``request`` : a ``Pyramid`` request object | |
``schema`` : a ``Colander`` schema | |
``onsuccess`` : a function to run if form processing succeeds. | |
Note: this is generally treated as a function with no parameters, | |
unless post-validation returns a ``Success`` object with | |
an attached result. In that case, the ``FormGenerator`` | |
will call ``onsuccess(result)``. This can be used in cases | |
where a post-processor needs access to an arbitrary value | |
or the model object that was created/updated during processing. | |
``process_controls`` : a function that accepts the ``Deform`` validated | |
form controls for processing. This is most often a function | |
that creates or updates a record in your model. | |
Note: the response type of ``process_controls`` is assumed to be | |
either ``Success``, ``Failure``, or ``MultipleFailures``. | |
This allows ``process_controls`` to communicate error information | |
to the ``FormGenerator`` so it can manually fill in error fields. | |
The caller can optionally supply: | |
``appstruct`` : a dictionary of initial values to be filled in when | |
the form is rendered. Typically this is a dictionary representation | |
of an object in your model, used when updating existing records. | |
``responsedict``: when ajax is not used, the ``FormGenerator`` will | |
add the rendered form object into the ``responsedict`` with the | |
key "form". The ``responsedict`` will then be returned as-is | |
to be passed to the renderer of the calling view. | |
``ajax`` : True by default. Set ajax=False to disable. | |
``ajax_options``: passed directly to the ``Deform`` ``Form`` object. | |
Note: used by ``Deform`` to inject a JSON object literal | |
(represented as a python string) into the html representation | |
of the form. Very useful for issuing redirects on ajax calls. | |
See the ``Deform`` documentation for details. | |
""" | |
def __init__(self, | |
request, | |
schema, | |
onsuccess, | |
process_controls, | |
appstruct=co.null, | |
responsedict={}, | |
ajax=True, | |
ajax_options=None, | |
submit_autodetect=True, | |
render_state=None, | |
params_source='POST' | |
): | |
self.request = request | |
self.schema = schema | |
self.form = Form(schema, buttons=('submit',), use_ajax=ajax) | |
if ajax_options is not None: | |
self.form.ajax_options = ajax_options | |
self.onsuccess = onsuccess | |
self.responsedict = responsedict | |
self.ajax = request.is_xhr and ajax | |
self.process_controls = process_controls | |
self.appstruct = appstruct | |
if submit_autodetect: | |
if 'submit' in request.POST: | |
self.render = self._renderother | |
else: | |
self.render = self._renderfirst | |
else: | |
if render_state == 'first': | |
self.render = self._renderfirst | |
else: | |
self.render = self._renderother | |
self.params_source= params_source | |
self.cstruct= None | |
def _renderfirst(self): | |
"""First rendering of a form. ``self.appstruct`` is a caller | |
supplied dictionary of values matching the schema, or | |
``Colander.null``.""" | |
html = self.form.render(appstruct=self.appstruct) | |
return self._respond(html) | |
def _renderother(self): | |
"""Rendering of a form when data has been submitted via POST.""" | |
try: | |
if self.params_source == 'POST': | |
controls = self.request.POST.items() | |
elif self.params_source == 'GET': | |
controls = self.request.GET.items() | |
elif self.params_source == 'GETPOST': | |
controls = self.request.params.items() | |
validated = self.form.validate(controls) | |
response = self.process_controls(validated) | |
if response.succeeded is True: | |
# return early if successful | |
return self._succeed(response) | |
else: | |
# forces ``ValidationFailure`` | |
self.fail(controls, response) | |
except ValidationFailure, e: | |
html = e.render() | |
return self._respond(html) | |
def _succeed(self, response): | |
"""Return the caller supplied ``onsuccess`` function, passing in the | |
``Success`` object's ``result`` attribute if it is not None.""" | |
if response.result is not None: | |
return self.onsuccess(response.result) | |
else: | |
return self.onsuccess() | |
def _respond(self, html): | |
"""Returns a response as an html snippet (for ajax/xhr) or | |
return a response dict that is passed to the caller's renderer.""" | |
if self.ajax: | |
return Response(html) | |
else: | |
self.responsedict['form'] = html | |
return self.responsedict | |
def fail(self, controls, response): | |
"""Raise a ``ValidationFailure`` after attaching error(s) to | |
supplied node(s).""" | |
# case: singleton failure | |
if isinstance(response, Failure): | |
failures = [response] | |
# case: multiple failures | |
else: | |
failures = response.failures | |
# attach errors to specific nodes | |
for f in failures: | |
err = co.Invalid(self.form.schema[f.field], f.error) | |
self.form[f.field].error = err | |
# recreate the cstruct from the controls | |
self.cstruct = self.form.deserialize(peppercorn.parse(controls)) | |
# ``self.form`` will now render with errors | |
raise ValidationFailure(self.form, self.cstruct, None) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment