Don't like ads? PRO users don't see any ads ;-)
Guest

Untitled

By: a guest on Jul 11th, 2012  |  syntax: None  |  size: 10.58 KB  |  hits: 21  |  expires: Never
download  |  raw  |  embed  |  report abuse  |  print
Text below is selected. Please press Ctrl+C to copy to your clipboard. (⌘+C on Mac)
  1. """
  2. Simple module to ease form handling with ``Deform``. The provided
  3. ``FormGenerator`` class handles repetitive tasks like validation,
  4. xhr requests, and recovering from exceptions thrown by the model.
  5.  
  6. ### Begin example scenario: Adding a new user
  7.  
  8. from myschemas import UserSchema
  9.  
  10. from formgenerator import (
  11.     FormGenerator,
  12.     Success,
  13.     Failure,
  14.     MultipleFailures,
  15.     )
  16.  
  17. # our database of users
  18. USERS = ['arthur', 'trillian', 'marvin']
  19.  
  20. # this function will be passed the validated controls, and
  21. # must return Success, Failure, or MultipleFailures
  22. def add_new_user(validatedcontrols):
  23.     username = validatedcontrols['username']
  24.     if username in USERS:
  25.         return Failure('username', 'username must be unique')
  26.     else:
  27.         USERS.append(username)
  28.         return Success()
  29.  
  30. # this is the response to use if the user is created successfully
  31. def user_created_successfully(username):
  32.     return Response('account created for %s' % username)
  33.  
  34. # the view function
  35. @view_config(route_name='createuser', renderer='form.html',
  36.              permission='admin')
  37. def createuser(request):
  38.     schema = UserSchema()
  39.     form = FormGenerator(request,
  40.                          schema,
  41.                          onsuccess=user_created_successfully,
  42.                          process_controls=add_new_user,
  43.                          )
  44.     return form.render()
  45.  
  46. ### End example scenario
  47.  
  48. Note that the ``onsuccess`` and ``process_controls`` functions are limited
  49. intentionally, because the ``FormGenerator`` can't hope to accommodate
  50. every possible use. Instead, the caller is advised to prepare functions
  51. that accept the appropriate parameters and pass those to the ``FormGenerator``
  52. instead.
  53.  
  54. Let's say you have a function that relies on the current ``request`` to produce
  55. a result:
  56.  
  57. def post_validate(request, kw):
  58.     if request.something:
  59.         return Success()
  60.     else:
  61.         return Failure('somefield', 'some error')
  62.  
  63. It would appear at first that the function cannot be used with the
  64. ``FormGenerator`` because the ``request`` parameter wouldn't be made available.
  65.  
  66. The python standard library is equipped to handle just such a case.
  67. Add the following import:
  68.  
  69. from functools import partial
  70.  
  71. And now when you create the ``FormGenerator``, your ``process_controls``
  72. function becomes:
  73.  
  74. def myview(request):
  75.     process_controls = partial(post_validate, request)
  76.     form = FormGenerator(...
  77.                          process_controls=process_controls,
  78.                          ...
  79.                          )
  80.     return form.render()
  81.  
  82. By partially applying parameters to a function, you can create a new function
  83. that takes only the parameters expected by the ``FormGenerator``. The same idea
  84. can be applied to your ``onsuccess`` function.
  85. """
  86.  
  87. import peppercorn
  88. import colander as co
  89. from functools import partial
  90. from pyramid.response import Response
  91.  
  92. from deform import (
  93.     Form,
  94.     ValidationFailure,
  95.     exception,
  96.     )
  97.  
  98. from abc import (
  99.     ABCMeta,
  100.     abstractproperty,
  101.     )
  102.  
  103. class Result:
  104.     """A simple data type used for communicating with a ``FormGenerator``.
  105.     ``FormGenerator`` expects supplied ``process_controls`` function to return
  106.     a subclass of ``Result`` to indicate success or failure."""
  107.     __metaclass__ = ABCMeta
  108.  
  109.     @abstractproperty
  110.     def succeeded(self):
  111.         """Must be subclassed. ``FormGenerator`` will check ``succeeded``
  112.         property to determine how to render the form."""
  113.         return NotImplemented
  114.  
  115. class Success(Result):
  116.     """A possible return type for a ``process_controls`` function supplied to
  117.     a ``FormGenerator``. Indicates form processing was successful."""
  118.  
  119.     def __init__(self, result=None):
  120.         """Allows caller to attach an arbitrary ``result`` object on success.
  121.         This is primarily used for returning a successfully created or
  122.         updated model object.
  123.         Note: if ``result`` is not None, it will be supplied as a parameter
  124.         to the ``FormGenerator``'s ``success`` function for post-processing."""
  125.         self.result = result
  126.  
  127.     @property
  128.     def succeeded(self):
  129.         """This subclass always returns ``True``."""
  130.         return True
  131.  
  132. class Failure(Result):
  133.     """A possible return type for a ``process_controls`` function supplied to
  134.     a ``FormGenerator``. Indicates form processing failed.
  135.     Usage: ``Failure(schema_field_name, error_string)``
  136.     """
  137.  
  138.     def __init__(self, field, error):
  139.         """Supply a ``field`` (a ``Colander`` schema node name) and an
  140.         ``error`` (a string) to be made accessible to ``FormGenerator``
  141.         for error handling."""
  142.         self.field = field
  143.         self.error = error
  144.  
  145.     @property
  146.     def succeeded(self):
  147.         """This subclass always returns ``False``."""
  148.         return False
  149.  
  150. class MultipleFailures(Result):
  151.     """A possible return type for a ``process_controls`` function supplied to
  152.     a ``FormGenerator``. Indicates form processing failed and contains
  153.     a list of ``Failure`` objects.
  154.     Usage: ``MultipleFailures([Failure('node1', 'error1'),
  155.                                Failure('node2', 'error2')])``
  156.     """
  157.  
  158.     def __init__(self, failures):
  159.         """Initialize with a list of ``Failure`` objects."""
  160.         self.failures = failures
  161.  
  162.     @property
  163.     def succeeded(self):
  164.         """This subclass always returns ``False``."""
  165.         return False
  166.  
  167. class FormGenerator(object):
  168.     """High-level interface to ``Deform`` that will either:
  169.     1) Render an empty form
  170.     2) Render a form with a supplied ``appstruct``
  171.     3) Re-render a form on failure, displaying errors
  172.  
  173.     The caller must supply:
  174.       ``request`` : a ``Pyramid`` request object
  175.  
  176.       ``schema``  : a ``Colander`` schema
  177.  
  178.       ``onsuccess`` : a function to run if form processing succeeds.
  179.           Note: this is generally treated as a function with no parameters,
  180.                 unless post-validation returns a ``Success`` object with
  181.                 an attached result. In that case, the ``FormGenerator``
  182.                 will call ``onsuccess(result)``. This can be used in cases
  183.                 where a post-processor needs access to an arbitrary value
  184.                 or the model object that was created/updated during processing.
  185.  
  186.       ``process_controls`` : a function that accepts the ``Deform`` validated
  187.           form controls for processing. This is most often a function
  188.           that creates or updates a record in your model.
  189.           Note: the response type of ``process_controls`` is assumed to be
  190.             either ``Success``, ``Failure``, or ``MultipleFailures``.
  191.             This allows ``process_controls`` to communicate error information
  192.             to the ``FormGenerator`` so it can manually fill in error fields.
  193.  
  194.     The caller can optionally supply:
  195.       ``appstruct``   : a dictionary of initial values to be filled in when
  196.           the form is rendered. Typically this is a dictionary representation
  197.           of an object in your model, used when updating existing records.
  198.  
  199.       ``responsedict``: when ajax is not used, the ``FormGenerator`` will
  200.           add the rendered form object into the ``responsedict`` with the
  201.           key "form". The ``responsedict`` will then be returned as-is
  202.           to be passed to the renderer of the calling view.
  203.  
  204.        ``ajax``       : True by default. Set ajax=False to disable.
  205.  
  206.        ``ajax_options``: passed directly to the ``Deform`` ``Form`` object.
  207.            Note: used by ``Deform`` to inject a JSON object literal
  208.            (represented as a python string) into the html representation
  209.            of the form. Very useful for issuing redirects on ajax calls.
  210.            See the ``Deform`` documentation for details.
  211.        """
  212.     def __init__(self,
  213.                  request,
  214.                  schema,
  215.                  onsuccess,
  216.                  process_controls,
  217.                  appstruct=co.null,
  218.                  responsedict={},
  219.                  ajax=True,
  220.                  ajax_options=None):
  221.         self.request = request
  222.         self.schema = schema
  223.         self.form = Form(schema, buttons=('submit',), use_ajax=ajax)
  224.         if ajax_options is not None:
  225.             self.form.ajax_options = ajax_options
  226.         self.onsuccess = onsuccess
  227.         self.responsedict = responsedict
  228.         self.ajax = request.is_xhr and ajax
  229.         self.process_controls = process_controls
  230.         self.appstruct = appstruct
  231.         if 'submit' in request.POST:
  232.             self.render = self._renderother
  233.         else:
  234.             self.render = self._renderfirst
  235.  
  236.     def _renderfirst(self):
  237.         """First rendering of a form. ``self.appstruct`` is a caller
  238.         supplied dictionary of values matching the schema, or
  239.         ``Colander.null``."""
  240.         html = self.form.render(appstruct=self.appstruct)
  241.         return self._respond(html)
  242.  
  243.     def _renderother(self):
  244.         """Rendering of a form when data has been submitted via POST."""
  245.         try:
  246.             controls = self.request.POST.items()
  247.             validated = self.form.validate(controls)
  248.             response = self.process_controls(validated)
  249.             if response.succeeded is True:
  250.                 # return early if successful
  251.                 return self._succeed(response)
  252.             else:
  253.                 # forces ``ValidationFailure``
  254.                 self.fail(controls, response)
  255.         except ValidationFailure, e:
  256.             html = e.render()
  257.             return self._respond(html)
  258.  
  259.     def _succeed(self, response):
  260.         """Return the caller supplied ``onsuccess`` function, passing in the
  261.         ``Success`` object's ``result`` attribute if it is not None."""
  262.         if response.result is not None:
  263.             return self.onsuccess(response.result)
  264.         else:
  265.             return self.onsuccess()
  266.  
  267.     def _respond(self, html):
  268.         """Returns a response as an html snippet (for ajax/xhr) or
  269.         return a response dict that is passed to the caller's renderer."""
  270.         if self.ajax:
  271.             return Response(html)
  272.         else:
  273.             self.responsedict['form'] = html
  274.             return self.responsedict
  275.  
  276.     def fail(self, controls, response):
  277.         """Raise a ``ValidationFailure`` after attaching error(s) to
  278.         supplied node(s)."""
  279.         # case: singleton failure
  280.         if isinstance(response, Failure):
  281.             failures = [response]
  282.         # case: multiple failures
  283.         else:
  284.             failures = response.failures
  285.  
  286.         # attach errors to specific nodes
  287.         for f in failures:
  288.             err = co.Invalid(self.form.schema[f.field], f.error)
  289.             self.form[f.field].error = err
  290.  
  291.         # recreate the cstruct from the controls
  292.         cstruct = self.form.deserialize(peppercorn.parse(controls))
  293.  
  294.         # ``self.form`` will now render with errors
  295.         raise ValidationFailure(self.form, cstruct, None)