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:

   1 class Entry:
   2     def __init__(self, name, address, phone):
   3         self.name = name
   4         self.address = address
   5         self.phone = phone

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

   1 class EntryTableRow(IteratorTableRow):
   2     def __init__(self, entry):
   3         cols = (
   4             Col((TextField('Name', 'name'), )),
   5             Col((TextField('Address', 'address'), )),
   6             Col((TextField('Phone', 'phone'), )),
   7         )
   8         IteratorTableRow.__init__(self, cols)
   9         
  10         self.load(entry)
  11 
  12 
  13 class EntryTableForm(Form):
  14     def __init__(self, entries):
  15         headers = Row((
  16             HeaderCol((Label('Name'), )),
  17             HeaderCol((Label('Address'), )),
  18             HeaderCol((Label('Phone'), )),
  19         ))
  20         self.table = IteratorTable('table', headers, EntryTableRow, entries,
  21                                    html_attrs={'width': '100%'})
  22         Form.__init__(self, 'Address book', (self.table, ))

To create the form:

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

Rendering the list just requires:

<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:

<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:

   1 def page_process(ctx):
   2     if ctx.req_equals('save'):
   3         ctx.locals.entry_table_form.merge(ctx.locals.entries)
   4         save(ctx.locals.entries)

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.

   1 def page_process(ctx):
   2     [...]
   3     if ctx.req_equals('add_entry'):
   4         entry = Entry('', '', '')
   5         ctx.locals.entries.append(entry)
   6         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.

   1 class ProductTableRow(IteratorTableRow):
   2     def __init__(self, product):
   3         cols = (
   4             Col((TextField('Code', 'code'), )),
   5             Col((TextField('Name', 'name'), )),
   6             Col((FloatField('Cost', 'cost'), ), html_attrs={'class': 'number-right'}),
   7         )
   8         IteratorTableRow.__init__(self, cols)
   9         
  10         self.load(product)
  11 
  12 
  13 class ProductTotalRow(IteratorTableRow):
  14     def __init__(self, products):
  15         cols = (
  16             Col((Label(''), )),
  17             Col((Label('Total'), )),
  18             Col((FloatField('Total', value=products.total_amount(), static=True), ),
  19                             html_attrs={'class': 'number-right'}),
  20         )
  21         IteratorTableRow.__init__(self, cols)
  22 
  23 
  24 class ProductTableForm(FieldsetForm):
  25     def __init__(self, products):
  26         headers = Row((
  27             HeaderCol((Label('Product code'), )),
  28             HeaderCol((Label('Product name'), )),
  29             HeaderCol((Label('Cost'), ), html_attrs={'class': 'number-right'}),
  30         ))
  31         self.table = IteratorTable('table', headers, ProductTableRow, products,
  32                                    html_attrs={'width': '100%'})
  33         self.table.append_row(ProductTotalRow(products))
  34         buttons = Buttons((
  35             Button('save', 'Save'),
  36             Button('cancel', 'Cancel'),
  37         ))
  38         FieldsetForm.__init__(self, 'Product table', (self.table, ),
  39                               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.

   1 class EntryTableRow(IteratorTableRow):
   2     def __init__(self, entry):
   3         entry.is_selected = False         # placeholder
   4         cols = (
   5             Col((Checkbox('Selected', 'is_selected'), )),
   6             Col((TextField('Name', 'name'), )),
   7             Col((TextField('Address', 'address'), )),
   8             Col((TextField('Phone', 'phone'), )),
   9         )
  10         IteratorTableRow.__init__(self, cols)
  11         
  12         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.

   1 def page_process(ctx):
   2     [...]
   3     elif ctx.req_equals('delete_selected'):
   4         for entry, entry_form_child in zip(ctx.locals.entries,
   5                                            ctx.locals.entry_table_form.table.children):
   6             is_selected_field = entry_form_child.get_field('Selected')
   7             if is_selected_field.get_value():
   8                 ctx.locals.entries.remove(entry)
   9                 ctx.locals.entry_table_form.table.remove(entry_form_child)

None: Table_support (last edited 2011-02-15 06:05:17 by localhost)