- """
- 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):
- 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' in request.POST:
- self.render = self._renderother
- else:
- self.render = self._renderfirst
- 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:
- controls = self.request.POST.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
- cstruct = self.form.deserialize(peppercorn.parse(controls))
- # ``self.form`` will now render with errors
- raise ValidationFailure(self.form, cstruct, None)