Skip to content

Because writing HTML is fiddly and all of your tables are basically the same

License

Notifications You must be signed in to change notification settings

lormayna/flask_table

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Flask Table

Because writing HTML is fiddly and all of your tables are basically the same.

Build Status Coverage Status

Quick Start

# import things
from flask_table import Table, Col

# Declare your table
class ItemTable(Table):
    name = Col('Name')
    description = Col('Description')

# Get some objects
class Item(object):
    def __init__(self, name, description):
        self.name = name
        self.description = description
items = [Item('Name1', 'Description1'),
         Item('Name2', 'Description2'),
         Item('Name3', 'Description3')]
# Or, equivalently, some dicts
items = [dict(name='Name1', description='Description1'),
         dict(name='Name2', description='Description2'),
         dict(name='Name3', description='Description3')]

# Or, more likely, load items from your database with something like
items = ItemModel.query.all()

# Populate the table
table = ItemTable(items)

# Print the html
print(table.__html__())
# or just {{ table }} from within a Jinja template

Which gives something like:

<table>
<thead><tr><th>Name</th><th>Description</th></tr></thead>
<tbody>
<tr><td>Name1</td><td>Description1</td></tr>
<tr><td>Name2</td><td>Description2</td></tr>
<tr><td>Name3</td><td>Description3</td></tr>
</tbody>
</table>

Or as HTML:

NameDescription
Name1Description1
Name2Description2
Name3Description3

Extra things:

  • The attribute used for each column in the declaration of the column is used as the default thing to lookup in each item.

  • The thing that you pass when you populate the table must:

    • be iterable
    • contain dicts or objects - there's nothing saying it can't contain some of each
  • There are also LinkCol and ButtonCol that allow links and buttons, which is where the Flask-specific-ness comes in.

  • There are also DateCol and DatetimeCol that format dates and datetimes.

  • Oh, and BoolCol, which does Yes/No.

  • But most importantly, Col is easy to subclass.

Included Col Types

  • OptCol - converts values according to a dictionary of choices. Eg for turning stored codes into human readable text.

  • BoolCol (subclass of OptCol) - converts values to yes/no.

  • DateCol - for dates (uses format_date from babel.dates).

  • DatetimeCol - for date-times (uses format_datetime from babel.dates).

  • LinkCol - creates a link by specifying an endpoint and url_kwargs.

  • ButtonCol (subclass of LinkCol) creates a button that posts the the given address.

More about OptCol

When creating the column, you pass some choices. This should be a dict with the keys being the values that will be found on the item's attribute, and the values will be the text to be displayed.

You can also set a default_key, or a default_value. The default value will be used if the value found from the item isn't in the choices dict. The default key works in much the same way, but means that if your default is already in your choices, you can just point to it rather than repeat it.

And you can use coerce_fn if you need to alter the value from the item before looking it up in the dict.

More about BoolCol

A subclass of OptCol where the choices are:

{True: 'Yes', False: 'No'}

and the coerce_fn is bool. So the value from the item is coerced to a bool and then looked up in the choices to get the text to display.

[[Possible future work: mark 'Yes' and 'No' for translation.]]

[[Possible future work: make it easier to override 'Yes' and 'No'.]]

[[Possible future work: add a BoolNaCol or similar that has a separate option for None]]

More about DateCol

Formats a date from the item. Can specify a date_format to use, which defaults to 'short', which is passed to babel.dates.format_date.

More about DatetimeCol

Formats a datetime from the item. Can specify a datetime_format to use, which defaults to 'short', which is passed to babel.dates.format_datetime.

More about LinkCol

Gives a way of putting a link into a td. You must specify an endpoint for the url. You should also specify some url_kwargs. This should be a dict which will be passed as the second argument of url_for, except the values will be treated as attributes to be looked up on the item. These keys obey the same rules as elsewhere, so can be things like 'category.name' or ('category', 'name').

[[Possible future work: make it so some constants can be passed as part of the url_kwargs]]

The text for the link is acquired in almost the same way as with other columns. However, other columns can be given no attr or attr_list and will use the attribute that the column was given in the table class, but LinkCol does not, and instead falls back to the heading of the column. This make more sense for things like an "Edit" link.

[[Possible future work: make it so you can specify a specific fallback for the td that is different to the th]]

[[Possible future work: make it so you can specify attributes for the HTML anchor element.]]

More about ButtonCol

Has all the same options as LinkCol but instead adds a form and a button that gets posted to the url.

[[Possible future work: make it so you can specify hidden fields to be added into the form.]]

[[Possible future work: make it so you can specify attributes for the HTML form or button elements.]]

Subclassing Col

(Look in examples/subclassing.py for a more concrete example)

Suppose our item has an attribute, but we don't want to output the value directly, we need to alter it first. If the value that we get from the item gives us all the information we need, then we can just override the td_format method:

class LangCol(Col):
    def td_format(self, content):
        if content == 'en_GB':
            return 'British English'
        elif content == 'de_DE':
            return 'German'
        elif content == 'fr_FR':
            return 'French'
        else:
            return 'Not Specified'

If you need access to all of information in the item, then we can go a stage earlier in the process and override the td_contents method:

from flask import Markup

def td_contents(self, i, attr_list):
    # by default this does
    # return self.td_format(self.from_attr_list(i, attr_list))
    return Markup.escape(self.from_attr_list(i, attr_list) + ' for ' + item.name)

At present, you do still need to be careful about escaping things as you override these methods. Also, because of the way that the Markup class works, you need to be careful about how you concatenate these with other strings.

Setting a class on the <table> element

If you set a classes attribute on the Table class, this gets added as a class on the <table> element. The classes attribute should be an iterable of strings, all of which will be added.

For example, if:

class MyTable(Table):
    classes = ['class1', 'class2']
	...

Then the table created would be:

<table class="class1 class2">
    ...
</table>

Manipulating <tr>s

(Look in examples/rows.py for a more concrete example)

Suppose you want to change something about the tr element for some or all items. You can do this by overriding your table's tr_format method. By default, this method returns:

'<tr>{}</tr>'

which betrays the fact that it has .format() called on it, to put in the tds. If you override the method, keep that in mind.

So, we might want to use something like:

class ItemTable(Table):
    name = Col('Name')
    description = Col('Description')

    def tr_format(self, item):
        if item.important():
            return '<tr class="important">{}</tr>'
        else:
            return '<tr>{}</tr>'

which would give all trs for items that returned a true value for the important() method, a class of "important".

Dynamically Creating Tables

(Look in examples/dynamic.py for a more concrete example)

You can define a table dynamically too.

TableCls = create_table('TableCls')\
    .add_column('name', Col('Name'))\
    .add_column('description', Col('Description'))

which is equivalent to

class TableCls(Table):
    name = Col('Name')
    description = Col('Description')

but makes it easier to add columns dynamically.

For example, you may wish to only add a column based on a condition.

TableCls = create_table('TableCls')\
    .add_column('name', Col('Name'))

if condition:
    TableCls.add_column('description', Col('Description'))

which is equivalent to

class TableCls(Table):
    name = Col('Name')
    description = Col('Description', show=condition)

thanks to the show option. Use whichever you think makes your code more readable. Though you may still need the dynamic option for something like

TableCls = create_table('TableCls')
for i in range(num):
    TableCls.add_column(str(i), Col(str(i)))

Sortable Tables

(Look in examples/sortable.py for a more concrete example)

Define a table and set its allow_sort attribute to True. Now all columns will be default try to turn their header into a link for sorting, unless you set allow_sort to False for a column.

You also must declare a sort_url method for that table. Given a col_key, this determines the url for link in the header. If reverse is True, then that means that the table has just been sorted by that column and the url can adjust accordingly, ie to now give the address for the table sorted in the reverse direction. It is, however, entirely up to your flask view method to interpret the values given to it from this url and to order the results before giving the to the table. The table itself will not do any reordering of the items it is given.

class SortableTable(Table):
    name = Col('Name')
    allow_sort = True

    def sort_url(self, col_key, reverse=False):
        if reverse:
            direction =  'desc'
        else:
            direction = 'asc'
        return url_for('index', sort=col_key, direction=direction)

The Examples

The examples directory contains a few pieces of sample code to show some of the concepts and features. They are all intended to be runnable. Some of them just output the code they generate, but some (just one, sortable.py, at present) actually creates a Flask app that you can access.

You should be able to just run them directly with python, but if you have cloned the repository for the sake of dev, and created a virtualenv, you may find that they generate an import error for flask_table. This is because flask_table hasn't been installed, and can be rectified by running something like PYTHONPATH=.:./lib/python3.3/site-packages python examples/simple.py, which will use the local version of flask_table including any changes.

Also, if there is anything that you think is not clear and would be helped by an example, please just ask and I'll happily write one. Only you can help me realise which bits are tricky or non-obvious and help me to work on explaining the bits that need explaining.

Other Things

At the time of first writing, I was not aware of the work of Django-Tables. However, I have now found it and started adapting ideas from it, where appropriate. For example, allowing items to be dicts as well as objects.

About

Because writing HTML is fiddly and all of your tables are basically the same

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Python 84.5%
  • HTML 15.3%
  • Shell 0.2%