.. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.. % Copyright 2009 by Object Craft P/L, Melbourne, Australia.
.. % LICENCE - see LICENCE file distributed with this software for details.
.. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

.. highlight:: python

Albatross Forms Guide
=====================

Albatross Forms provides support for developing Albatross applications
which gather data from the user, validate it, and then return the user's
data to the app. Much of this work is mechanical and it is tedious and
error prone writing the same code on different pages in the application.

Using Forms lets the developer organise the presentation of related
data on a web page programmatically. The Forms support handles the basic
layout, type conversions and validation as the user interacts with the
form. By using centrally defined data types, presentation, validation
and error reporting can be done consistently and modified easily. It has
been our experience that development is much faster and lots of code
can be removed from the Albatross templates where it's hard to read,
difficult to test and gets mucked up by web designers using WYSIWYG
design tools.

Albatross Forms is designed to use CSS to change the layout of the
forms when they are displayed rather than encoding the HTML into the
form tags. This consolidates the web site's presentation and makes it
easier to change the presentation globally.

Concepts
--------

There are three main concepts that sit behind the Albatross Forms
implementation:

* **Field**

  A data input field. It can format its output and validate its
  input. It contains a copy of the value so that the user can edit it
  without needing to maintain a separate copy in the application.

* **Fieldset**

  Groups together a list of Fields and renders them in a table. It is
  conceptually related the HTML fieldset tag which groups related input
  fields together. Fieldsets are intended to only hold data fields. If
  you try to insert a Button into a Fieldset it's not likely to work,
  in part because Fieldset expects that each Field member will respond
  to certain methods, and in part because notionally is that buttons
  apply actions to all the fields in the fieldset.

* **Form**

  Manages the all of the fields in the form. Most interactions in
  the application are with Form instances: they coordinate loading
  values from model objects (typically attributes of classes) into the
  Fields, organise rendering and updating the values from the browser,
  validation, and storing the values back into the model objects.

In practice, the developer will assemble a ``Form`` instance
containing one or more ``Field`` instances and place this ``Form`` into
``ctx.locals`` (optionally fetching field values from an associated
data or *Model* class via the :meth:`load()` method). 

The developer then refers to this ``Form`` via a new ``<alx-form>`` tag in
the page template (note that the ``<alx-form>`` must still be contained
within an ``<al-form>`` tag in the page template).  When the template
is executed, the ``Form`` will be rendered to an HTML table containing
appropriate inputs (including any values associated with the ``Fields``).

When the user subsequently submits their responses, the developer
will call the ``Form`` instance :meth:`merge()` method from the
:meth:`page_process()` method and the user values will be merged back
to the associated data storage class (or *Model*).

Getting started
---------------

You need to have a version of Albatross which has the Albatross Forms
support included (or have installed it by hand yourself). You can quickly
test whether it is present by running:

.. code-block:: pycon

    >>> from albatross.ext import form

If it's missing, you'll see an import error.

Registering the <alx-form> tag
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

In each Albatross application, there is a point at which an ``App``
subclass is instantiated (usually in app.py or app.cgi or wherever the
main entry point of your application is). This instance needs to be
told about the new ``<alx-form>`` tag. This is done with code that looks
something like this:

.. code-block:: python

    import albatross
    from albatross.ext import form
    ...
    if __name__ == '__main__':
        app = albatross.SimpleApp()
        app.register_tagclasses(*form.tag_classes)
        app.run(Request())

Alternatively, if you subclass one of the Albatross application classes,
you can register the new tags in your subclass's constructor method
(__init__):

.. code-block:: python

    import albatross
    from albatross.ext import form
    ...
    class Application(albatross.SimpleApp):
        
        def __init__(self, *args):
            albatross.SimpleApp.__init__(self, *args)
            self.register_tagclasses(*form.tag_classes)


A simple example
----------------

Here is a simple example of how we could use Albatross Forms to collect
a username and password from the user.

We need to define a model class to hold the data:

.. code-block:: python

    import pwd, crypt

    class User:

        def __init__(self, username, password):
            self.username = username
            self.password = password

        def is_password_valid(self):
            try:
                pw = pwd.getpwnam(self.username)
            except KeyError:
                return False
            return (crypt.crypt(self.password, pw.pw_passwd) == pw.pw_passwd)

Next, we need to define a form to display the fields:

.. code-block:: python

    from albatross.ext.form import *

    class LoginForm(FieldsetForm):

        def __init__(self, user):
            fields = (
                TextField('Username', 'username'),
                PasswordField('Password', 'password'),
            )
            fieldsets = (Fieldset(fields), )
            buttons = Buttons((
                Button('Login', 'login'),
            ))
            FieldsetForm.__init__(self, 'User login', fieldsets, buttons=buttons)
            self.load(user)

We need to create an instance of the Login model and maintain that so
that any captured data is retained. In our login.py, we use:

.. code-block:: python

    def page_enter(ctx):
        if not ctx.has_value('user'):
            ctx.locals.user = User('', '')
            ctx.add_session_vars('user')
            ctx.locals.login_form = LoginForm(ctx.locals.user)
            ctx.add_session_vars('login_form')
        ctx.locals.login_error = ''

In login.html, to display the form to the user we use:

.. code-block:: albatross

    <al-form method="post">
        <alx-form name="login_form" errors />
        <al-expr expr="login_error" />
    </al-form>

When the user presses the "Login" button, it will come back to our
page_process method in login.py. We check if the username and password
are correct and punt them into the application proper (via the "search"
page) or tell them they've got it wrong:

.. code-block:: python

    def page_process(ctx):
        if ctx.req_equals('login'):
            # nothing to validate
            ctx.locals.login_form.merge(ctx.locals.user)
            if ctx.locals.user.is_password_valid():
                ctx.redirect('search')
            else:
                ctx.locals.login_error = 'Login incorrect'

Flow of Control
---------------

The flow of control through Albatross Forms is tied in with Albatross's
flow of control.

A common mistake is to reinitialise the form from the model part way
through the user's interaction with the page (ie, before they've saved
it). It winds up losing any changes that the user has made on the
form. The lesson here is that the form should only be loaded from the
model once when the user starts interacting with it; don't reload it on
each page refresh.

#. **Constructor**

   Create the form itself.

#. **Load values from model**

   This is often done in the form subclass constructor method (__init__).

#. **Display the form**

   Render the form to the web page, using <alx-form name="model_form"
   errors> in your Albatross template for the page.

#. **Validate**

   Check that the data that the user entered is correct. The call to validate
   will raise a FormValidationError exception.

#. **Merge**

   Update the data class (*Model*) with the data fields collected from
   the form.


Field types
-----------

Albatross Forms defines a number of standard fields. You can also
add your own, subclassing the standard fields to add validation or
type-casting. The standard fields are:

 **TextField**
    
    A normal text input. Corresponds to the ``<input type="text">`` tag.

 **PasswordField**

    Same as a text field but it doesn't display the characters as the user
    enters them. Corresponds to the ``<input type="password">`` tag.

 **StaticField**

    A TextField with static content.

 **TextArea**

    Corresponds to the ``<textarea>`` tag.
 
 **IntegerField**

    An integral value, :meth:`get_value()` will return an ``int`` type,
    non-integer values will result in a :exc:`FieldValidationError`
    on :meth:`validate()`.

 **FloatField**

    A floating point value, :meth:`get_value()` will return a ``float`` type,
    non-floating point values will result in a :exc:`FieldValidationError`
    on :meth:`validate()`.

 **Checkbox**

    Renders as <input type="checkbox">, :meth:`get_value()` will return a
    ``bool`` type.

 **SelectField**

    Returns one of the values listed in the ``(value, display_value)``
    list passed to the constructor. If the value can be converted to an
    ``int``, it will be; otherwise it will be returned as a ``str``.

 **RadioField**

    Returns one of the values listed in the ``(value, display_value)``
    list passed to the constructor. If the value can be converted to an
    ``int``, it will be; otherwise it will be returned as a ``str``.

 **FileField** (not implemented yet)

Internal storage within a field
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The interaction of the field with the browser is complicated because
the browser requires and returns string values, which results in the
loss of type information for the objects that the fields wrap.

To manage this, the field class notes when the field is rendered to
the browser and converts the value to a string using the object's
:meth:`get_display_value()` method. When the values are posted back to
the form by the browser, they are returned as strings. If you need to
access the value stored in a field, use the :meth:`get_value()` method
which tracks the type of the stored representation and does a conversion
(using :meth:`get_merge_value()`) when appropriate.


A more complex example
----------------------

Here is a rather more complex example that uses a number of different input
types.

Here's the model:

.. code-block:: python

    class User:
        def __init__(self):
            self.name = ''
            self.an_int = 0
            self.a_float = 0.0
            self.country = 0
            self.password = ''
            self.active = False

The form that describes how we want it laid out:

.. code-block:: python

        # Slightly abridged list of all the countries in the world.
        country_names = (
            'Australia',     'Belgium',      'Cuba',
            'Greenland',     'Madagascar',   'Netherlands',
            'Switzerland',   'Uzbekistan',   'Zimbabwe'
        )
        country_menu = [e for e in enumerate(country_names)]
        fields = (
            TextField('Name', 'name', required=True),
            IntegerField('Integer', 'an_int'),
            FloatField('Float', 'a_float'),
            SelectField('Country', country_menu, 'country'),
            PasswordField('Password', 'password',
                          required=True),
            Checkbox('Active', 'active'),
        )
        buttons = Buttons((
            Button('Save', 'save'),
            Button('Cancel', 'cancel'),
        ))
        fieldsets = (Fieldset(fields), )
        ctx.locals.test_form = FieldsetForm('User details',
                                            fieldsets,
                                            buttons=buttons)

Render the form on the page:

.. code-block:: albatross

     <al-form method="POST">
        <div class="alxform">
          <alx-form name="test_form" errors />
        </div>
      </al-form>

To render the form as static report:

.. code-block:: albatross

      <div class="alxform">
        <alx-form name="test_form" static />
      </div>

In the forms.py file, the code looks like:

.. code-block:: python

    def page_enter(ctx):
        if not ctx.has_value('test_form'):
            ctx.locals.description = User()
            ctx.add_session_vars('description')
            ctx.locals.test_form = test_form # see above

    def page_display(ctx):
        ctx.run_template('forms.html')

    def page_process(ctx):
        if ctx.req_equals('save'):
            try:
                ctx.locals.test_form.validate()
            except FormValidationError, e:
                ctx.locals.test_form.set_disabled(False)
                return
            ctx.locals.test_form.set_disabled(True)
            ctx.locals.test_form.merge(ctx.locals.description)
        elif ctx.req_equals('reset'):
            ctx.locals.test_form.clear()
            ctx.locals.test_form.set_disabled(False)
        elif ctx.req_equals('cancel'):
            if ctx.locals.test_form.disabled:
                ctx.locals.test_form.set_disabled(False)
            else:
                ctx.locals.test_form.clear()
                ctx.redirect('main')

Customising Fields
------------------

Here is an example of how we created a ``MoneyField`` that knows how to
validate a currency value. Both parser and formatter are modules that
we wrote to convert between formats. Our modules deal in dollars and
cents but that's hidden from the application code.

Note that the parser needs to be able to parse the output of the
formatter: the field will be initialised with the formatter's output when
it is rendered. It is reasonable to expect the parser to accept "$5.50"
if that is the format that the application is presenting to the user.

.. code-block:: python

    import parser, formatter

    class MoneyField(FloatField):

        def validate(self, form, s):
            s = s.strip()
            if not self.required and not s:
                return
            try:
                parser.money(s)
            except ValueError, e:
                raise FieldValidationError('Invalid value "%s" for money' % s)

        def get_merge_value(self, s):
            s = s.strip()
            if not self.required and not s:
                return
            return parser.money(s)

        def get_display_value(self, ctx, form):
            return formatter.money(self.get_value())

Attaching buttons to a form
---------------------------

It's common to have buttons at the bottom of a form even if they just say
"Save" and "Cancel".  This is supported in Albatross Forms by adding an
optional keyword arg when creating the Form object, for example:

.. code-block:: python

    from albatross.ext.form import *

    class LoginForm(FieldsetForm):

        def __init__(self, user):
            fields = (
                TextField('Username', 'username'),
                PasswordField('Password', 'password'),
            )
            fieldsets = (Fieldset(fields), )
            buttons = Buttons((
                Button('Login', 'login'),
            ))
            FieldsetForm.__init__(self, 'User login', fieldsets, buttons=buttons)

            self.load(user)

The ``Buttons`` class takes a list of ``Button`` instances in its
constructor and displays buttons in the bottom right hand corner of
the form display.

You check for the buttons being clicked by the user in the usual
Albatross way in *page_process*:

.. code-block:: python

    def page_process(ctx):
        if ctx.req_equals('save'):
            ...

Table support
-------------

Rendering tables using Albatross Forms is relatively straightforward,
using them for input is no harder.

Table support revolves around two classes: ``IteratorTable`` and
``IteratorTableRow``.

``IteratorTable`` acts as a field in a form in which the table is
rendered. The first argument to the ``IteratorTable`` constructor is the
name of the attribute in the class in which the the table is stored. This
is necessary so that Albatross can navigate through the form's fields
to update the values from the user's browser. It's a little arcane but
it isn't too bad.

``IteratorTableRow`` should be subclassed within the application to
render each row in turn.

The ``IteratorTable`` class steps through the list of objects that it's
passed and calls the ``IteratorTableRow`` subclass that's specified
with each object in turn. Each of these is responsible for rendering a
single row.

When the ``IteratorTable`` is rendered, it will display the header columns
(if specified) and then ask each row to render itself.

Here's an example which should render a list of name, address and phone
numbers in a table. First we define the model object:

.. code-block:: python

    class Entry:

        def __init__(self, name, address, phone):
            self.name = name
            self.address = address
            self.phone = phone

Now we'll define the components of the form to render a list of Entry
instances:

.. code-block:: python

    class EntryTableRow(IteratorTableRow):

        def __init__(self, entry):
            cols = (
                Col((TextField('Name', 'name'), )),
                Col((TextField('Address', 'address'), )),
                Col((TextField('Phone', 'phone'), )),
            )
            IteratorTableRow.__init__(self, cols)

            self.load(entry)


    class EntryTableForm(Form):

        def __init__(self, entries):
            headers = Row((
                HeaderCol((Label('Name'), )),
                HeaderCol((Label('Address'), )),
                HeaderCol((Label('Phone'), )),
            ))
            self.table = IteratorTable('table', headers, EntryTableRow, entries,
                                    html_attrs={'width': '100%'})
            Form.__init__(self, 'Address book', (self.table, ))

To create the form:

.. code-block:: python

    def page_enter(ctx):
        entries = [
            Entry('Ben Golding', 
                  'Object Craft, 123/100 Elizabeth St, Melbourne Vic 3000', 
                  '+61 3 9654-9099'),
            Entry('Dave Cole', 
                  'Object Craft, 123/100 Elizabeth St, Melbourne Vic 3000', 
                  '+61 3 9654-9099'),
        ]
        if not ctx.has_value('entry_table_form'):
            ctx.locals.entry_table_form = EntryTableForm(entries)
            ctx.add_session_vars('entry_table_form')

Rendering the list just requires:

.. code-block:: albatross

    <al-form method="post">
        <div class="alxform">
            <alx-form name="entry_table_form" static />
        </div>
    </al-form>

To support editing the fields, you would change how it renders using:

.. code-block:: albatross

    <al-form method="post">
        <div class="alxform">
            <alx-form name="entry_table_form" errors />
        </div>
    </al-form>

When the user has made changes, your page_process method can pick up
the changes using:

.. code-block:: python

    def page_process(ctx):
        if ctx.req_equals('save'):
            ctx.locals.entry_table_form.merge(ctx.locals.entries)
            save(ctx.locals.entries)

Paginating a table
^^^^^^^^^^^^^^^^^^

To add pagination to a table, you need to specify a pagesize and the
rest of the work is done for you.

.. code-block:: albatross

    <al-form method="post">
        <div class="alxform">
            <alx-form name="entry_table_form" errors pagesize="15" />
        </div>
    </al-form>

Currently, the simple paginator only emits "Prev" and "Next" links at the
bottom of the table.  It's straightforward to change the style of those links
by subclassing the ``IteratorTable`` class: pass in an instance of your own
custom subclass of ``PageSelectionDisplayBase`` as the
``page_selection_display`` argument of ``IteratorTable``'s constructor to
render the page links differently.

The ``pagesize`` argument can also be specified when creating an
``IteratorTable`` instance.  The argument specified overrides any that are
used in the ``<alx-form>`` tag.

Adding rows to a table
^^^^^^^^^^^^^^^^^^^^^^

The developer is responsible for keeping the form's idea of the number of rows
in the table in sync with the rows in the model. :

.. code-block:: python

    def page_process(ctx):
        ...
        if ctx.req_equals('add_entry'):
            entry = Entry('', '', '')
            ctx.locals.entries.append(entry)
            ctx.locals.entry_table_form.table.append(entry)

Note that the ``IteratorTable.append()`` method will call the row class
with the model data that's specified in the constructor.

Adding heterogenous rows to a table
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

If you were displaying a series of rows each of which was a product,
at the end of the table it would be great to display a total entry for
all of the included lines. In this example, we use ``append_row()``
to append a pre-formatted row (ie, an ``IteratorTableRow`` subclass
instance) to the table.

Note that while this works when rendering a form, I don't think it will
work if the form is used for input. :

.. code-block:: python

    class ProductTableRow(IteratorTableRow):

        def __init__(self, product):
            cols = (
                Col((TextField('Code', 'code'), )),
                Col((TextField('Name', 'name'), )),
                Col((FloatField('Cost', 'cost'), ), 
                     html_attrs={'class': 'number-right'}),
            )
            IteratorTableRow.__init__(self, cols)
            self.load(product)


    class ProductTotalRow(IteratorTableRow):

        def __init__(self, products):
            cols = (
                Col((Label(''), )),
                Col((Label('Total'), )),
                Col((FloatField('Total', value=products.total_amount(), static=True), ),
                     html_attrs={'class': 'number-right'}),
            )
            IteratorTableRow.__init__(self, cols)


    class ProductTableForm(FieldsetForm):

        def __init__(self, products):
            headers = Row((
                HeaderCol((Label('Product code'), )),
                HeaderCol((Label('Product name'), )),
                HeaderCol((Label('Cost'), ), 
                          html_attrs={'class': 'number-right'}),
            ))
            self.table = IteratorTable('table', headers, ProductTableRow, products,
                                    html_attrs={'width': '100%'})
            self.table.append_row(ProductTotalRow(products))
            buttons = Buttons((
                Button('save', 'Save'),
                Button('cancel', 'Cancel'),
            ))
            FieldsetForm.__init__(self, 'Product table', (self.table, ),
                                  buttons=buttons)

Deleting rows from a table
^^^^^^^^^^^^^^^^^^^^^^^^^^

When deleting rows from a table, I normally put a check box next to each
of the rows and include a "delete selected" button so that the user can
delete multiple rows at once.

In the table row constructor, I poke an is_selected value into the model
object as a placeholder for the selected check box. I feel like this is
impolite but it works very effectively. :

.. code-block:: python

    class EntryTableRow(IteratorTableRow):

        def __init__(self, entry):
            entry.is_selected = False         # placeholder
            cols = (
                Col((Checkbox('Selected', 'is_selected'), )),
                Col((TextField('Name', 'name'), )),
                Col((TextField('Address', 'address'), )),
                Col((TextField('Phone', 'phone'), )),
            )
            IteratorTableRow.__init__(self, cols)

            self.load(entry)

When processing the request, I step through each list element in the
model list in in sync with each child in the table form and delete both
of them when the checkbox is selected.

.. code-block:: python

    def page_process(ctx):
        if ctx.req_equals('delete_selected'):
            for entry, entry_form_child in zip(ctx.locals.entries,
                                               ctx.locals.entry_table_form.table.children):
                is_selected_field = entry_form_child.get_field('Selected')
                if is_selected_field.get_value():
                    ctx.locals.entries.remove(entry)
                    ctx.locals.entry_table_form.table.remove(entry_form_child)

Querying fields before merge
----------------------------

Sometimes it can be really nice (read: improve the user experience)
to be able to query some fields without merging the value from the
form back into an object. An example is when a user changes a input
select field in a form which has an attached value. In an app we have,
a user has a select list of product numbers. We make the app update the
product's name on the page when the product changes.

Note that we are treading on thin ice here: if the user has not entered
a valid value for the field, when ``get_value()`` tries to convert it
to the appropriate type, it will raise an exception that will need
to be dealt with. It is (generally) safe to reference values from a
``TextField`` or a ``SelectField`` though. :

.. code-block:: python

    class ProductFieldsetForm(FieldsetForm):

        products_menu = product_factory.all_products_menu()

        def __init__(self, product):
            self.product_id = product.product_id
            elements = (
                Fieldset((
                    SelectField('Product code', self.products_menu, 'product_id',
                                html_attrs={'onchange': 'javascript:product_form.submit()'}),
                    StaticField('Product', product.name),
                )),
            )
            FieldsetForm.__init__(self, 'Product selection', elements)
            self.load(product)

        def update(self):
            product_code_field = self.get_field('Product code')
            if self.product_id != product_code_field.get_value():
                self.product_id = product_code_field.get_value()
                product = product_factory.product_with_product_id(self.product_id)
                product_field = self.get_field('Product')
                product_field.set_value(product.name)

The interesting part here is where we query the form for a specific
field using its ``get_field()`` method.

We use the little bit of Javascript to make a change to the select list
force an update to the page. That means having to name the form when we
render the page:

.. code-block:: albatross

    <al-form method="post" name="product_form">
        <alx-form name="product_form_form" />
    </al-form>

In the app itself, we use:

.. code-block:: python

    def page_enter(ctx):
        if not ctx.has_value('product_form'):
            ctx.locals.product_form = ProductForm(ctx.locals.product)
            ctx.add_session_vars('product_form')

    def page_process(ctx):
        ctx.locals.product_form.update()      # update product name if product changed
        if ctx.req_equals('save'):
            ...

