Skip to content

Instantly share code, notes, and snippets.

@jvanasco
Forked from ericrasmussen/formgenerator.py
Created February 9, 2012 00:55
Show Gist options
  • Save jvanasco/1776061 to your computer and use it in GitHub Desktop.
Save jvanasco/1776061 to your computer and use it in GitHub Desktop.
FormGenerator to extend Deform functionality
"""
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