Basic Usage =========== In this chapter, we'll walk through basic usage of Deform to render a form, and capture and validate input. The steps a developer must take to cause a form to be rendered and subsequently be ready to accept form submission input are: - Define a schema - Create a form object. - Assign non-default widgets to fields in the form (optional). - Render the form. Once the form is rendered, a user will interact with the form in their browser, and some point, they will submit it. When the user submits the form, the data provided by the user will either validate properly, or the form will need to be re-rendered with error markers which help to inform the user of which parts need to be filled in "properly" (as defined by the schema). We allow the user to continue filling in the form, submitting, and revalidating indefinitely. Defining A Schema ----------------- The first step to using Deform is to create a :term:`schema` which represents the data structure you wish to be captured via a form rendering. For example, let's imagine you want to create a form based roughly on a data structure you'll obtain by reading data from a relational database. An example of such a data structure might look something like this: .. code-block:: python :linenos: [ { 'name':'keith', 'age':20, }, { 'name':'fred', 'age':23, }, ] In other words, the database query we make returns a sequence of *people*. Each person is represented by some data. We need to edit this data. There won't be many people in this list, so we don't need any sort of paging or batching to make our way through the list. We can display it all on one form page. Deform designates a structure akin to the example above as an :term:`appstruct`. The term "appstruct" is shorthand for "application structure", because it's the kind of high-level structure that an application usually cares about. The data present in an appstruct is useful directly to an application itself. .. note:: An appstruct differs from other structures that Deform uses (such as :term:`pstruct` and :term:`cstruct` structures): pstructs and cstructs are typically only useful during intermediate parts of the rendering process. Usually, given some appstruct, you can define a :term:`schema` that would allow you to edit the data related to the appstruct. Let's define a schema which will attempt to serialize this particular appstruct to a form. Our application has these requirements of the resulting form: - It must be possible to add and remove a person. - It must be possible to change any person's name or age after they've been added. Here's a schema that will help us meet those requirements: .. code-block:: python :linenos: import colander class Person(colander.MappingSchema): name = colander.SchemaNode(colander.String()) age = colander.SchemaNode(colander.Integer(), validator=colander.Range(0, 200)) class People(colander.SequenceSchema): person = Person() class Schema(colander.MappingSchema): people = People() schema = Schema() The schemas used by Deform come from a package named :term:`Colander`. The canonical documentation for Colander exists at https://docs.pylonsproject.org/projects/colander/en/latest/. To compose complex schemas, you will need to read it to get comfortable with the default Colander data types. But for now, we can play it by ear. For ease of reading, we've actually defined *three* schemas above. As the result of our definitions, a ``Person`` represents: - A ``name``, which must be a string. - An ``age``, which must be deserializable to an integer. After deserialization happens, a validator ensures that the integer is between 0 and 200 inclusive. A ``People`` schema is a collection of ``Person`` schema nodes. Finally we create a collection of the ``People`` schema nodes into a single schema instance as ``schema``. Schema Node Objects ~~~~~~~~~~~~~~~~~~~ .. note:: This section repeats and contextualizes the :term:`Colander` documentation about schema nodes in order to prevent you from needing to switch away from this page to another while trying to learn about forms. But you can also get much the same information at https://docs.pylonsproject.org/projects/colander/en/latest/ A schema is composed of one or more *schema node* objects, each typically of the class :class:`colander.SchemaNode`, usually in a nested arrangement. Each schema node object has a required *type*, an optional *preparer* for adjusting data after deserialization, an optional *validator* for deserialized prepared data, an optional *default*, an optional *missing*, an optional *title*, an optional *description*, and a slightly less optional *name*. It also accepts *arbitrary* keyword arguments, which are attached directly as attributes to the node instance. The *type* of a schema node indicates its data type (such as :class:`colander.Int` or :class:`colander.String`). The *preparer* of a schema node is called after deserialization but before validation; it prepares a deserialized value for validation. Examples would be to prepend schemes that may be missing on url values or to filter HTML provided by a rich text editor. A preparer is not called during serialization, only during deserialization. The *validator* of a schema node is called after deserialization and preparation. It makes sure the value matches a constraint. An example of such a validator is provided in the schema above: ``validator=colander.Range(0, 200)``. A validator is not called after schema node serialization, only after node deserialization. The *default* of a schema node indicates the value to be serialized if a value for the schema node is not found in the input data during serialization. It should be the deserialized representation. The *missing* of a schema node indicates the value to be deserialized if a value for the schema node is not found in the input data during deserialization. It should be the deserialized representation. If a schema node does not have a ``missing`` value, a :exc:`colander.Invalid` exception will be raised if the data structure being deserialized does not contain a matching value. The *name* of a schema node is used to relate schema nodes to each other. It is also used as the title if a title is not provided. The *title* of a schema node is metadata about a schema node. It shows up in the legend above the form field(s) related to the schema node. By default, it is a capitalization of the *name*. The name of a schema node that is introduced as a class-level attribute of a :class:`colander.MappingSchema`, :class:`colander.TupleSchema` or a :class:`colander.SequenceSchema` is its class attribute name. For example: .. code-block:: python :linenos: import colander class Phone(colander.MappingSchema): location = colander.SchemaNode(colander.String(), validator=colander.OneOf(['home','work'])) number = colander.SchemaNode(colander.String()) The name of the schema node defined via ``location = colander.SchemaNode(..)`` within the schema above is ``location``. The title of the same schema node is ``Location``. The *description* of a schema node is metadata about a schema node. It shows up as help text for the form control related to a :term:`field`. By default, it is empty. Schema Objects ~~~~~~~~~~~~~~ In the examples above, if you've been paying attention, you'll have noticed that we're defining classes which subclass from :class:`colander.MappingSchema` and :class:`colander.SequenceSchema`. It's turtles all the way down. The result of creating an instance of any of :class:`colander.MappingSchema`, :class:`colander.TupleSchema`, or :class:`colander.SequenceSchema` object is *also* a :class:`colander.SchemaNode` object. Instantiating a :class:`colander.MappingSchema` creates a schema node which has a *type* value of :class:`colander.Mapping`. Instantiating a :class:`colander.TupleSchema` creates a schema node which has a *type* value of :class:`colander.Tuple`. Instantiating a :class:`colander.SequenceSchema` creates a schema node which has a *type* value of :class:`colander.Sequence`. Creating Schemas Without Using a Class Statement (Imperatively) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ See https://docs.pylonsproject.org/projects/colander/en/latest/basics.html#defining-a-schema-imperatively for information about how to create schemas without using a ``class`` statement. Creating a schema with or without ``class`` statements is purely a style decision. The outcome of creating a schema without ``class`` statements is the same as creating one with ``class`` statements. Rendering a Form ---------------- Earlier we defined a schema: .. code-block:: python :linenos: import colander class Person(colander.MappingSchema): name = colander.SchemaNode(colander.String()) age = colander.SchemaNode(colander.Integer(), validator=colander.Range(0, 200)) class People(colander.SequenceSchema): person = Person() class Schema(colander.MappingSchema): people = People() schema = Schema() Let us now use this schema to create, render, and validate a form. .. _creating_a_form: Creating a Form Object ~~~~~~~~~~~~~~~~~~~~~~ To create a form object, we do this: .. code-block:: python :linenos: from deform import Form myform = Form(schema, buttons=('submit',)) We used the ``schema`` object (an instance of :class:`colander.MappingSchema`) we created in the previous section as the first positional parameter to the :class:`deform.Form` class. We passed the value ``('submit',)`` as the value of the ``buttons`` keyword argument. This will cause a single ``submit`` input element labeled ``Submit`` to be injected at the bottom of the form rendering. We chose to pass in the button names as a sequence of strings, but we could have also passed a sequence of instances of the :class:`deform.Button` class. Either is permissible. Note that the first positional argument to :class:`deform.Form` must be a schema node representing a *mapping* object (a structure which maps a key to a value). We satisfied this constraint above by passing our ``schema`` object, which we obtained via the :class:`colander.MappingSchema` constructor, as the ``schema`` argument to the :class:`deform.Form` constructor Although different kinds of schema nodes can be present in a schema used by a Deform :class:`deform.Form` instance, a form instance cannot deal with a schema node representing a sequence, a tuple schema, a string, an integer, and so on, as the value of its ``schema`` parameter. Only a schema node representing a mapping is permissible. This typically means that the object passed as the ``schema`` argument to a :class:`deform.Form` constructor must be obtained as the result of using the :class:`colander.MappingSchema` constructor, or the equivalent imperative spelling. Rendering the Form ~~~~~~~~~~~~~~~~~~ Once we have created a Form object, we can render it without issue by calling the :meth:`deform.Field.render` method. The :class:`deform.Form` class is a subclass of the :class:`deform.Field` class, so this method is available to a :class:`deform.Form` instance. If we wanted to render an "add" form (a form without initial data), we'd just omit the appstruct while calling :meth:`deform.Field.render`. .. code-block:: python form = myform.render() Suppose we have some existing data already that we would like to edit using the form. Here the form is an "edit form" as opposed to an "add form". That data might look like this: .. code-block:: python :linenos: appstruct = [ { 'name':'keith', 'age':20, }, { 'name':'fred', 'age':23, }, ] To inject it into the serialized form as the data to be edited, we'd pass it in to the :meth:`deform.Field.render` method to get a form rendering: .. code-block:: python form = myform.render(appstruct) If, finally, instead we wanted to render a "read-only" variant of an edit form using the same appstruct, we'd pass the ``readonly`` flag as ``True`` to the :meth:`deform.Field.render` method. .. code-block:: python form = myform.render(appstruct, readonly=True) This would cause a page to be rendered in a crude form without any form controls, so the user to whom it is presented cannot edit it. Once any of the above statements runs, the ``form`` variable is now a Unicode object containing an HTML rendering of the edit form, useful for serving out to a browser. The root tag of the rendering will be the ``