Tables¶
How do I customize the rendering of a table?¶
Table rendering can be customized on multiple levels. You pass a template with the template argument, which
is either a template name or a Template object.
Customize the HTML attributes of the table tag via the attrs argument. See attrs.
To customize the row, see How do I customize the rendering of a row?
To customize the cell, see How do I customize the rendering of a cell?
To customize the rendering of the table, see table-as-div
How do you turn off pagination?¶
Specify page_size=None:
table = Table(
auto__model=Album,
page_size=None,
)
Or in the declarative style:
class MyTable(Table):
name = Column()
class Meta:
page_size = None
The paginator itself is a part of the table named page, so you can configure
it through the parts namespace, e.g. parts__page__title='Page'.
How do I customize the rendering of a cell?¶
You can customize the Cell rendering in several ways:
You can modify the html attributes via
cell__attrs. See attrs.Use
cell__templateto specify a template. You can give a string and it will be interpreted as a template name, or you can pass aTemplateobject.Pass a url (or callable that returns a url) to
cell__urlto make the cell a link (see next question).
How do I make a link in a cell?¶
This is such a common case that there’s a special case for it: pass the url and url_title parameters to the cell:
table = Table(
auto__model=Album,
columns__name__cell__url='http://example.com',
columns__name__cell__url_title='go to example',
)
How do I create a column based on computed data (i.e. a column not based on an attribute of the row)?¶
Let’s say we have a model like this:
class Foo(models.Model):
value = models.IntegerField()
And we want a computed column square that is the square of the value, then we can do:
table = Table(
auto__model=Foo,
columns__square=Column(
# computed value:
cell__value=lambda row, **_: row.value * row.value,
)
)
or we could do:
Table(
auto__model=Foo,
columns__square=Column(
attr='value',
cell__format=lambda value, **_: value * value,
)
)
This only affects the formatting when we render the cell value. Which might make more sense depending on your situation but for the simple case like we have here the two are equivalent.
How do I get iommi tables to understand my Django ModelField subclasses?¶
See Registrations.
How do I reorder columns?¶
By default the columns come in the order defined so if you have an explicit table defined, just move them around there. If the table is generated from a model definition, you can also move them in the model definition if you like, but that might not be a good idea. So to handle this case we can set the ordering on a column by giving it the after argument. Let’s start with a simple model:
class Foo(models.Model):
a = models.IntegerField()
b = models.IntegerField()
c = models.IntegerField()
If we just do Table(auto__model=Foo) we’ll get the columns in the order a, b, c. But let’s say I want to put c first, then we can pass it the after value -1:
table = Table(auto__model=Foo, columns__c__after=-1)
-1 means the first, other numbers mean index. We can also put columns after another named column like so:
table = Table(auto__model=Foo, columns__c__after='a')
this will put the columns in the order a, c, b.
There is a special value LAST (import from iommi.declarative) to put something last in a list:
table = Table(auto__model=Foo, columns__a__after=LAST)
How do I enable searching/filter on columns?¶
Pass the value filter__include=True to the column, to enable searching
in the advanced query language.
table = Table(
auto__model=Album,
columns__name__filter__include=True,
)
The filter namespace here is used to configure a Filter so you can
configure the behavior of the searching by passing parameters here.
The filter__field namespace is used to configure the Field, so here you
can pass any argument to Field here to customize it.
If you just want to have the filter available in the advanced query language,
you can turn off the field in the generated form by passing
filter__field__include=False:
How do I make a freetext search field?¶
If you want to filter based on a freetext query on one or more columns we’ve got a nice little feature for this:
table = Table(
auto__model=Album,
columns__name__filter=dict(
freetext=True,
include=True,
),
columns__year__filter__freetext=True,
columns__year__filter__include=True,
)
This will display one search box to search both year and name columns:
How do I customize HTML attributes, CSS classes or CSS style specifications?¶
The attrs namespace has special handling to make it easy to customize. There are three main cases:
First the straight forward case where a key/value pair is rendered in the output:
>>> from iommi.attrs import render_attrs
>>> from iommi.declarative.namespace import Namespace
>>> render_attrs(Namespace(foo='bar'))
' foo="bar"'
Then there’s a special handling for CSS classes:
>>> render_attrs(Namespace(class__foo=True, class__bar=True))
' class="bar foo"'
Note that the class names are sorted alphabetically on render.
Lastly there is the special handling of style:
>>> render_attrs(Namespace(style__font='Arial'))
' style="font: Arial"'
If you need to add a style with - in the name you have to do this:
>>> render_attrs(Namespace(**{'style__font-family': 'sans-serif'}))
' style="font-family: sans-serif"'
Everything together:
>>> render_attrs(
... Namespace(
... foo='bar',
... class__foo=True,
... class__bar=True,
... style__font='Arial',
... **{'style__font-family': 'serif'}
... )
... )
' class="bar foo" foo="bar" style="font-family: serif; font: Arial"'
How do I customize the rendering of a row?¶
You can customize the row rendering in two ways:
You can modify the html attributes via
row__attrs. See attrs.Use
row__templateto specify a template. You can give a string and it will be interpreted as a template name, or you can pass aTemplateobject.
In templates you can access the raw row via row. This would typically be one of your model objects. You can also access the cells of the table via cells. A naive template for a row would be <tr>{% for cell in cells %}<td>{{ cell }}{% endfor %}</tr>. You can access specific cells by their column names like {{ cells.artist }}.
To customize the cell, see How do I customize the rendering of a cell?
How do I customize the rendering of a header?¶
You can customize headers in two ways:
You can modify the html attributes via
header__attrs. See attrs.Use
header__templateto specify a template. You can give a string and it will be interpreted as a template name, or you can pass aTemplateobject.
How do I turn off the header?¶
Set header__template to None.
How do I add fields to a table that is generated from a model?¶
See the question column-computed-data
How do I specify which columns to show?¶
Pass include=False to hide the column or include=True to show it. By default columns are shown, except the primary key column that is by default hidden. You can also pass a callable here like so:
Table(
auto__model=Album,
columns__name__include=
lambda request, **_: request.GET.get('some_parameter') == 'hello!',
)
This will show the column name only if the GET parameter some_parameter is set to hello!.
To be more precise, include turns off the entire column. Sometimes you want to have the searching turned on, but disable the rendering of the column. To do this use the render_column parameter instead. This is useful for example to turn on filtering for a column, but not render it:
table = Table(
auto__model=Album,
columns__year__render_column=False,
columns__year__filter__include=True,
)
Use auto__include to specify the complete list of columns you want:
table = Table(
auto__model=Album,
auto__include=['name', 'artist'],
)
Instead of using auto__include, you can also use auto__exclude to just exclude the columns you don’t want:
table = Table(
auto__model=Album,
auto__exclude=['year'],
)
There is also a config option default_included which is by default True, which is where iommi’s default behavior of showing all columns comes from. If you set it to False columns are now opt-in:
table = Table(
auto__model=Album,
auto__default_included=False,
# Turn on only the name column
columns__name__include=True,
)
How do I access table data programmatically (like for example to dump to json)?¶
Here’s a simple example that prints a table to stdout:
def print_table(table):
for row in table.cells_for_rows():
for cell in row:
print(cell.render_formatted(), end=' ')
print()
table = Table(auto__model=Album).bind(request=req('get'))
print_table(table)
How do I turn off sorting? (on a column or table wide)¶
To turn off sorting on a column pass it sortable=False (you can also use a lambda here!):
table = Table(
auto__model=Album,
columns__name__sortable=False,
)
and to turn it off on the entire table:
table = Table(
auto__model=Album,
sortable=False,
)
How do I specify the title of a header?¶
The display_name property of a column is displayed in the header.
table = Table(
auto__model=Album,
columns__name__display_name='header title',
)
How do I set the default sort direction of a column to be descending instead of ascending?¶
table = Table(
auto__model=Album,
columns__name__sort_default_desc=True, # or a lambda!
)
How do I set the default sorting column of a table?¶
Tables are sorted by default on the order specified in the models Meta and then on pk. Set default_sort_order to set another default ordering:
table = Table(
auto__model=Album,
default_sort_order='year',
)
Or reversed:
table = Table(
auto__model=Album,
default_sort_order='-year',
)
table = Table(
auto__model=Album,
default_sort_order='artist',
)
How do I group columns?¶
table = Table(
auto__model=Album,
columns__name__group='foo',
columns__artist__group='bar',
columns__year__group='bar',
)
The grouping only works if the columns are next to each other, otherwise you’ll get multiple groups. The groups are rendered by default as a second header row above the normal header row with colspans to group the headers.
How do I group rows?¶
Use row_group. By default this will output a <th> tag. You can configure it like any other fragment if you want to change that to a <td>. Note that the order of the columns in the table is used for grouping. This is why in the example below the year column is moved to index zero: we want to group on year first.
table = Table(
auto__rows=Album.objects.order_by('year', 'artist', 'name'),
columns__artist=dict(
row_group__include=True,
render_column=False,
),
columns__year=dict(
after=0,
render_column=False,
row_group=dict(
include=True,
template=Template('''
<tr>
{{ row_group.iommi_open_tag }}
{{ value }} in our hearts
{{ row_group.iommi_close_tag }}
</tr>
'''),
),
),
)
How do I get rowspan on a table?¶
You can manually set the rowspan attribute via row__attrs__rowspan but this is tricky to get right because you also have to hide the cells that are “overwritten” by the rowspan. We supply a simpler method: auto_rowspan. It automatically makes sure the rowspan count is correct and the cells are hidden. It works by checking if the value of the cell is the same, and then it becomes part of the rowspan.
table = Table(
auto__model=Album,
columns__year__auto_rowspan=True,
columns__year__after=0, # put the column first
)
How do I enable bulk editing?¶
Editing multiple items at a time is easy in iommi with the built in bulk
editing. Enable it for a column by passing bulk__include=True:
table = Table(
auto__model=Album,
columns__year__bulk__include=True,
)
The bulk namespace here is used to configure a Field for the GUI so you
can pass any parameter you can pass to Field there to customize the
behavior and look of the bulk editing for the column.
The select column, used to pick the rows to bulk edit, is shown
automatically when bulk editing is enabled.
How do I enable bulk delete?¶
table = Table(
auto__model=Album,
bulk__actions__delete__include=True,
)
To enable the bulk delete, enable the delete action.
The select column, used to pick the rows to delete, is shown
automatically when a bulk action is enabled.
How do I make a custom bulk action?¶
Define a submit Action with a post handler. The select column, used to
pick the rows to operate on, is shown automatically when a bulk action is
enabled:
def my_action_post_handler(table, request, **_):
queryset = table.bulk_queryset()
queryset.update(name='Paranoid')
return HttpResponseRedirect(request.META['HTTP_REFERER'])
t = Table(
auto__model=Album,
bulk__actions__my_action=Action.submit(
post_handler=my_action_post_handler,
)
)
What is the difference between attr and _name?¶
attr is the attribute path of the value iommi reads from a row. In the simple case it’s just the attribute name, but if you want to read the attribute of an attribute you can use __-separated paths for this: attr='foo__bar' is functionally equivalent to cell__value=lambda row, **_: row.foo.bar. Set attr to None to not read any attribute from the row.
_name is the name used internally. By default attr is set to the value of _name. This name is used when accessing the column from Table.columns and it’s the name used in the GET parameter to sort by that column. This is a required field.
How do I show a reverse foreign key relationship?¶
By default reverse foreign key relationships are hidden. To turn it on, pass include=True to the column:
t = Table(
auto__model=Artist,
columns__albums__include=True,
)
How do I show a reverse many-to-many relationship?¶
By default reverse many-to-many relationships are hidden. To turn it on, pass include=True to the column:
t = Table(
auto__model=Genre,
columns__albums__include=True,
)
How do I insert arbitrary html into a Table?¶
Sometimes you want to insert some extra html, css, or Part into a
Table. You can do this with the container or outer namespaces.
For container, by default items are added after the table but you
can put them above with after=0.
For outer, you can put content before the h tag even.
t = Table(
auto__model=Genre,
container__children__foo='Foo',
container__children__bar=html.div('Bar', after=0),
outer__children__bar=html.div('Baz', after=0),
)
How do I add custom actions/links to a table?¶
For the entire table:
t = Table(
auto__model=Album,
actions__link=Action(attrs__href='/'),
)
Or as a column:
t = Table(
auto__model=Album,
columns__link=Column.link(attr=None, cell__url='/', cell__value='Link'),
)
How do I render additional rows?¶
Using row__template you can render the default row with {{ cells.render }} and then your own custom data:
t = Table(
auto__model=Album,
row__template=Template('''
{{ cells.render }}
<tr>
<td style="text-align: center" colspan="{{ cells|length }}">🤘🤘</td>
</tr>
'''),
)
How do I set an initial filter to a table?¶
The Query of a Table has a Form where you can set the initial value:
t = Table(
auto__model=Album,
columns__artist__filter__include=True,
query__form__fields__artist__initial=lambda **_: Artist.objects.get(name='Dio'),
)
How do I automatically create filters for indexed fields?¶
When you have a model with database indexes, you can use query_from_indexes=True to automatically create filters for all the indexed fields. This is useful to quickly get a table with a bunch of filters that you know are performant.
Consider a model where only some fields have database indexes:
table = Table(
auto__model=Track,
query_from_indexes=True,
)
Without query_from_indexes, you need to manually specify which columns should have filters using columns__<field>__filter__include=True.
How do I show row numbers?¶
Use cells.row_index to get the index of the row in the current rendering.
t = Table(
auto__model=Album,
columns__index=Column(
after=0,
cell__value=lambda row, cells, **_: cells.row_index
),
)
How do I show nested foreign key relationships?¶
Say you have a list of tracks and you want to show the album and then from that album, you also want to show the artist:
t = Table(
auto__model=Track,
auto__include=[
'name',
'album',
'album__artist', # <--
]
)
The column created is named album_artist (as __ is reserved for traversing a namespace), so that’s the name you need to reference if you need to add more configuration to that column:
t = Table(
auto__model=Track,
auto__include=[
'name',
'album',
'album__artist',
],
columns__album_artist__cell__attrs__style__background='blue',
)
How do I stop rendering the header?¶
Use header__template=None to not render the header, or
header__include=False to remove the processing of the header totally. The
difference being that you might want the header object to access
programmatically for some reason, so then it’s appropriate to use the
template=None method.
t = Table(
auto__model=Album,
header__include=False,
)
t = Table(
auto__model=Album,
header__template=None,
)
How do I render a Table as divs?¶
You can render a Table as a div with the shortcut Table.div:
table = Table.div(
auto__model=Album,
)
This shortcut changes the rendering of the entire table from <table> to <div> by specifying the tag configuration, changes the <tbody> to a <div> via tbody__tag, the row via row__tag and removes the header with header__template=None.
How do I do custom processing on rows before rendering?¶
Sometimes it’s useful to further process the rows before rendering, by fetching more data, doing calculations, etc. If you can use QuerySet.annotate(), that’s great, but sometimes that’s not enough. This is where preprocess_row and preprocess_rows come in. The first is called on each row, and the second is called for the entire list as a whole.
Note that this is all done after pagination.
Modifying row by row:
def preprocess_album(row, **_):
row.year += 1000
return row
table = Table(
auto__model=Album,
preprocess_row=preprocess_album,
)
Note that preprocess_row requires that you return the object. This is because you can return a different object if you’d like.
Modifying the entire list:
def preprocess_albums(rows, **_):
for i, row in enumerate(rows):
row.index = i
return rows
table = Table(
auto__model=Album,
preprocess_rows=preprocess_albums,
columns__index=Column.number(),
)
Note that preprocess_rows requires that you return the list. That is because you can also return a totally new list if you’d like.
How do I set an empty message?¶
By default iommi will render an empty table simply as empty:
table = Table(
auto__model=Album,
)
If you want to instead display an explicit message when the table is empty, you use empty_message:
table = Table(
auto__model=Album,
empty_message='Destruction of the empty spaces is my one and only crime',
)
This setting is probably something you want to set up in your Style, and not per table.
How do I make complex layouts for table rows?¶
You can have more complex layout using the panel system. Set row__layout to a
Panel and each row’s Cells is rendered using it (its layout). The
Panel.cell entries are linked to the matching table column for each row
automatically - iommi tracks the owning table and row in _parent_table and
_parent_table_cells:
class AlbumTable(Table):
class Meta:
auto__model = Album
auto__include = ['name', 'artist', 'year']
row__layout = Panel.div(
dict(
p_album=Panel.row(dict(
name=Panel.cell(**{
'col__attrs__class': {
'fw-bold': True,
'text-decoration-underline': True
}
}),
year=Panel.cell(),
)),
p_artist=Panel.row(dict(
p_artist_test=html.em('Artist:'),
artist=Panel.cell(),
)),
),
**{
'attrs__style__border-bottom': '1px solid #6ea8fe',
}
)
Panel.cell’s are mapped to their corresponding Table columns automatically, and checked. That means that
if you create a complex layout and forget a cell you will get an error, and vice versa.
The same way you can also use layouts for EditTable.
How do I group columns under a superheader?¶
Set group to the same value on adjacent columns and they get a shared header
cell, a “superheader”, that spans them. Customize how that superheader renders
via the table’s superheader namespace (its attrs and template):
table = Table(
auto__model=Album,
columns__name__group='Details',
columns__year__group='Details',
superheader__attrs__class={'text-center': True},
)
How do I change what a column sorts by?¶
By default a column sorts on its attr. Set sort_key to sort on something
else, like a field across a relationship:
table = Table(
auto__model=Album,
columns__artist__sort_key='artist__name',
)
If you need to change the actual sorting algorithm (for example to sort a table
built from a plain list in a special way), override the table’s sorter.
How do I avoid extra queries for a column across a relationship?¶
When a column reads data across a foreign key, iommi can tell the QuerySet to
fetch that data efficiently instead of doing one query per row. Set
data_retrieval_method to select (a SQL join via select_related) or
prefetch (a second query via prefetch_related):
from iommi.table import DataRetrievalMethods
table = Table(
auto__model=Album,
columns__artist__data_retrieval_method=DataRetrievalMethods.select,
)
How do I limit the choices for a column?¶
Column.choice takes a choices list. The same list is used to limit the
options offered in the column’s filter and bulk editing field:
table = Table(
auto__model=Album,
columns__year=Column.choice(
choices=[1980, 1981],
filter__include=True,
),
)
How do I read the model information iommi found?¶
When you build a table with auto__model, iommi introspects the model and fills
in model on the table and model_field/model_field_name on each column for
you. Reading these lets you write generic code that adapts to the model. For a
column based on a foreign key, model is the related model, so you can build a
link to that object’s admin page without hard-coding the model name:
def admin_url(column, value, **_):
if value is None:
return None
meta = column.model._meta
return f'/admin/{meta.app_label}/{meta.model_name}/{value.pk}/change/'
table = Table(
auto__model=Album,
columns__artist__cell__url=admin_url,
)
How do I use custom classes for a table’s internal parts?¶
Besides member_class (see How do I use a custom class for the columns/fields/filters of a component?), a Table builds several
other objects that you can swap out by setting the matching *_class:
cells_class for each rendered row, action_class for its actions, page_class
for when the table is rendered as a standalone page, and row_group_class for
grouped rows. Each Cells object in turn builds its individual Cell objects
from its own cell_class:
from iommi.table import Cells
class MyCells(Cells):
pass
class MyTable(Table):
class Meta:
cells_class = MyCells
table = MyTable(auto__model=Album)
How do I render the table actions below the table?¶
By default a table’s actions are rendered above the table. Set
actions_below=True to render them below it instead. To take full control of
how the actions are laid out, point actions_template at your own template:
table = Table(
auto__model=Album,
actions__foo=Action(display_name='Foo', attrs__href='#'),
actions_below=True,
)
How do I run code after a bulk edit, or limit which rows it touches?¶
post_bulk_edit is a hook called with the queryset and updates after a bulk
edit has been saved - handy for logging or to keep a denormalized value in sync.
bulk_filter and bulk_exclude restrict which rows a bulk operation is allowed to
touch, and the whole bulk form is wrapped in a bulk_container fragment you can
configure:
log = []
def post_bulk_edit(table, queryset, updates, **_):
log.append(f'Updated {queryset.count()} album(s): {updates}')
table = Table(
auto__model=Album,
columns__year__bulk__include=True,
post_bulk_edit=post_bulk_edit,
)
How do I customize the message shown for an invalid filter?¶
When the filter form has invalid input (for example a value that isn’t one of the
valid choices), iommi shows a message above the table. Override its text with
invalid_form_message:
table = Table(
auto__model=Album,
columns__artist__filter__include=True,
invalid_form_message='Your filter is invalid, please try again.',
)
How do I wrap just the table tag (e.g. for responsive scrolling)?¶
The <table> tag is wrapped in a Fragment (separate from the actions and
paginator). Configure it with table_tag_wrapper, for example to get Bootstrap’s
responsive horizontal scrolling around the table only:
table = Table(
auto__model=Album,
table_tag_wrapper__attrs__class={'table-responsive': True},
)