.. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .. % 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 ```` tag in the page template (note that the ```` must still be contained within an ```` 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 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 ```` 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 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 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 ```` tag. **PasswordField** Same as a text field but it doesn't display the characters as the user enters them. Corresponds to the ```` tag. **StaticField** A TextField with static content. **TextArea** Corresponds to the ``