= 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:
{{{#!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.
{{{#!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:
{{{#!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:
{{{
}}}
To support editing the fields, you would change how it renders using:
{{{
}}}
When the user has made changes, your {{{page_process}}} method can pick up the changes using:
{{{#!python
def page_process(ctx):
if ctx.req_equals('save'):
ctx.locals.entry_table_form.merge(ctx.locals.entries)
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.
{{{#!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.
{{{#!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.
{{{#!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.
{{{#!python
def page_process(ctx):
[...]
elif 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)
}}}