django-tables\r
=============\r
\r
+A Django QuerySet renderer.\r
+\r
Installation\r
------------\r
\r
\r
The test suite uses py.test (from http://codespeak.net/py/dist/test.html).\r
\r
-Defining Tables\r
----------------\r
+Working with Tables\r
+-------------------\r
\r
-Using Tables\r
-------------\r
+A table class looks very much like a form:\r
+\r
+ import django_tables as tables\r
+ class CountryTable(tables.Table):\r
+ name = tables.Column(verbose_name="Country Name")\r
+ population = tables.Column(sortable=False, visible=False)\r
+ time_zone = tables.Column(name="tz", default="UTC+1")\r
+\r
+Instead of fields, you declare a column for every piece of data you want to\r
+expose to the user.\r
+\r
+To use the table, create an instance:\r
+\r
+ countries = CountryTable([{'name': 'Germany', population: 80},\r
+ {'name': 'France', population: 64}])\r
+\r
+Decide how you the table should be sorted:\r
+\r
+ countries.order_by = ('name',)\r
+ assert [row.name for row in countries.row] == ['France', 'Germany']\r
+\r
+ countries.order_by = ('-population',)\r
+ assert [row.name for row in countries.row] == ['Germany', 'France']\r
+\r
+If you pass the table object along into a template, you can do:\r
+\r
+ {% for column in countries.columns %}\r
+ {{column}}\r
+ {% endfor %}\r
+\r
+Which will give you:\r
+\r
+ Country Name\r
+ Time Zone\r
+\r
+Note that ``population`` is skipped (as it has ``visible=False``), that the\r
+declared verbose name for the ``name`` column is used, and that ``time_zone``\r
+is converted into a more beautiful string for output automatically.\r
+\r
+Common Workflow\r
+~~~~~~~~~~~~~~~\r
+\r
+Usually, you are going to use a table like this. Assuming ``CountryTable``\r
+is defined as above, your view will create an instance and pass it to the\r
+template:\r
+\r
+ def list_countries(request):\r
+ data = ...\r
+ countries = CountryTable(data, order_by=request.GET.get('sort'))\r
+ return render_to_response('list.html', {'table': countries})\r
+\r
+Note that we are giving the incoming "sort" query string value directly to\r
+the table, asking for a sort. All invalid column names will (by default) be\r
+ignored. In this example, only "name" and "tz" are allowed, since:\r
+\r
+ * "population" has sortable=False\r
+ * "time_zone" has it's name overwritten with "tz".\r
+\r
+Then, in the "list.html" template, write:\r
+\r
+ <table>\r
+ <tr>\r
+ {% for column in table.columns %}\r
+ <th><a href="?sort={{ column.name }}">{{ column }}</a></th>\r
+ {% endfor %}\r
+ </tr>\r
+ {% for row in table.rows %}\r
+ {% for value in row %}\r
+ <td>{{ value }}<td>\r
+ {% endfor %}\r
+ {% endfor %}\r
+ </table>\r
+\r
+This will output the data as an HTML table. Note how the table is now fully\r
+sortable, since our link passes along the column name via the querystring,\r
+which in turn will be used by the server for ordering. ``order_by`` accepts\r
+comma-separated strings as input, and "{{ table.order_by }}" will be rendered\r
+as a such a string.\r
+\r
+Instead of the iterator, you can use your knowledge of the table structure to\r
+access columns directly:\r
+\r
+ {% if table.columns.tz.visible %}\r
+ {{ table.columns.tz }}\r
+ {% endfor %}\r
+\r
+ModelTables\r
+-----------\r
+\r
+Like forms, tables can also be used with models:\r
+\r
+ class CountryTable(tables.ModelTable):\r
+ id = tables.Column(sortable=False, visible=False)\r
+ class Meta:\r
+ model = Country\r
+ exclude = ['clicks']\r
+\r
+The resulting table will have one column for each model field, with the\r
+exception of "clicks", which is excluded. The column for "id" is overwritten\r
+to both hide it and deny it sort capability.\r
+\r
+When instantiating a ModelTable, you usually pass it a queryset to provide\r
+the table data:\r
+\r
+ qs = Country.objects.filter(continent="europe")\r
+ countries = CountryTable(qs)\r
+\r
+However, you can also just do:\r
+\r
+ countries = CountryTable()\r
+\r
+and all rows exposed by the default manager of the model the table is based\r
+on will be used.\r
+\r
+If you are using model inheritance, then the following also works:\r
+\r
+ countries = CountryTable(CountrySubclass)\r
+\r
+Note that while you can pass any model, it really only makes sense if the\r
+model also provides fields for the columns you have defined.\r
+\r
+Custom Columns\r
+~~~~~~~~~~~~~~\r
+\r
+You an add custom columns to your ModelTable that are not based on actual\r
+model fields:\r
+\r
+ class CountryTable(tables.ModelTable):\r
+ custom = tables.Column(default="foo")\r
+ class Meta:\r
+ model = Country\r
+\r
+Just make sure your model objects do provide an attribute with that name.\r
+Functions are also supported, so ``Country.custom`` could be a callable.\r
+\r
+ModelTable Specialities\r
+~~~~~~~~~~~~~~~~~~~~~~~\r
+\r
+ModelTables currently have some restrictions with respect to ordering:\r
+\r
+ * Custom columns not based on a model field do not support ordering,\r
+ regardless of ``sortable`` property (it is ignored).\r
+\r
+ * A ModelTable column's ``default`` value does not affect ordering.\r
+ This differs from the non-model table behaviour.\r
+\r
+Columns\r
+-------\r
+\r
+verbose_name, default, visible, sortable\r
+ ``verbose_name`` defines a display name for this column used for output.\r
+\r
+ ``name`` is the internal name of the column. Normally you don't need to\r
+ specify this, as the attribute that you make the column available under\r
+ is used. However, in certain circumstances it can be useful to override\r
+ this default, e.g. when using ModelTables if you want a column to not\r
+ use the model field name.\r
+\r
+ ``default`` is the default value for this column. If the data source\r
+ does not provide None for a row, the default will be used instead. Note\r
+ that whether this effects ordering might depend on the table type (model\r
+ or normal).\r
+\r
+ You can use ``visible`` to flag the column as hidden by default.\r
+ However, this can be overridden by the ``visibility`` argument to the\r
+ table constructor. If you want to make the column completely unavailable\r
+ to the user, set ``inaccessible`` to True.\r
+\r
+ Setting ``sortable`` to False will result in this column being unusable\r
+ in ordering.\r
\r
-Create\r
+``django_tables.columns`` currently defines three classes, ``Column``,\r
+``TextColumn`` and ``NumberColumn``. However, the two subclasses currently\r
+don't do anything special at all, so you can simply use the base class.\r
+While this will likely change in the future (e.g. when grouping is added),\r
+the base column class will continue to work by itself.\r
\r
Tables and Pagination\r
---------------------\r
\r
TODO\r
----\r
- - Let columns change their default ordering (ascending/descending)\r
- - Filters\r
- - Grouping\r
- - Choices-like data
\ No newline at end of file
+ - as_html methods are all empty right now\r
+ - table.column[].values is a stub\r
+ - let columns change their default ordering (ascending/descending)\r
+ - filters\r
+ - grouping\r
+ - choices support for columns (internal column value will be looked up for output)
\ No newline at end of file
\r
``default`` is the default value for this column. If the data source\r
does not provide None for a row, the default will be used instead. Note\r
- that this currently affects ordering.\r
+ that whether this effects ordering might depend on the table type (model\r
+ or normal).\r
\r
You can use ``visible`` to flag the column as hidden by default.\r
However, this can be overridden by the ``visibility`` argument to the\r
from django.utils.datastructures import SortedDict\r
-from tables import BaseTable, DeclarativeColumnsMetaclass, Column\r
+from tables import BaseTable, DeclarativeColumnsMetaclass, Column, BoundRow\r
\r
__all__ = ('BaseModelTable', 'ModelTable')\r
\r
if (columns and not f.name in columns) or \\r
(exclude and f.name in exclude):\r
continue\r
- column = Column()\r
+ column = Column() # TODO: chose the right column type\r
if column:\r
field_list.append((f.name, column))\r
return SortedDict(field_list)\r
self.base_columns = columns\r
return self\r
\r
-class ModelDataProxy(object):\r
- pass\r
-\r
class BaseModelTable(BaseTable):\r
- def __init__(self, data, *args, **kwargs):\r
- super(BaseModelTable, self).__init__([], *args, **kwargs)\r
- if isinstance(data, models.Model):\r
- self.queryset = data._meta.default_manager.all()\r
+ """Table that is based on a model.\r
+\r
+ Similar to ModelForm, a column will automatically be created for all\r
+ the model's fields. You can modify this behaviour with a inner Meta\r
+ class:\r
+\r
+ class MyTable(ModelTable):\r
+ class Meta:\r
+ model = MyModel\r
+ exclude = ['fields', 'to', 'exclude']\r
+ fields = ['fields', 'to', 'include']\r
+\r
+ One difference to a normal table is the initial data argument. It can\r
+ be a queryset or a model (it's default manager will be used). If you\r
+ just don't any data at all, the model the table is based on will\r
+ provide it.\r
+ """\r
+ def __init__(self, data=None, *args, **kwargs):\r
+ if data == None:\r
+ self.queryset = self._meta.model._default_manager.all()\r
+ elif isinstance(data, models.Model):\r
+ self.queryset = data._default_manager.all()\r
else:\r
self.queryset = data\r
\r
- def _get_data(self):\r
- """Overridden. Return a proxy object so we don't need to load the\r
- complete queryset.\r
- # TODO: we probably simply want to build the queryset\r
+ super(BaseModelTable, self).__init__(self.queryset, *args, **kwargs)\r
+\r
+ def _cols_to_fields(self, names):\r
+ """Utility function. Given a list of column names (as exposed to the\r
+ user), converts overwritten column names to their corresponding model\r
+ field name.\r
+\r
+ Supports prefixed field names as used e.g. in order_by ("-field").\r
+ """\r
+ result = []\r
+ for ident in names:\r
+ if ident[:1] == '-':\r
+ name = ident[1:]\r
+ prefix = '-'\r
+ else:\r
+ name = ident\r
+ prefix = ''\r
+ result.append(prefix + self.columns[name].declared_name)\r
+ return result\r
+\r
+ def _validate_column_name(self, name, purpose):\r
+ """Overridden. Only allow model-based fields to be sorted."""\r
+ if purpose == 'order_by':\r
+ try:\r
+ decl_name = self.columns[name].declared_name\r
+ self._meta.model._meta.get_field(decl_name)\r
+ except Exception: #TODO: models.FieldDoesNotExist:\r
+ return False\r
+ return super(BaseModelTable, self)._validate_column_name(name, purpose)\r
+\r
+ def _build_snapshot(self):\r
+ """Overridden. The snapshot in this case is simply a queryset\r
+ with the necessary filters etc. attached.\r
"""\r
- if self._data_cache is None:\r
- self._data_cache = ModelDataProxy(self.queryset)\r
- return self._data_cache\r
+ queryset = self.queryset\r
+ if self.order_by:\r
+ queryset = queryset.order_by(*self._cols_to_fields(self.order_by))\r
+ self._snapshot = queryset\r
+\r
+ def _get_rows(self):\r
+ for row in self.data:\r
+ yield BoundModelRow(self, row)\r
\r
class ModelTable(BaseModelTable):\r
__metaclass__ = ModelTableMetaclass\r
+\r
+class BoundModelRow(BoundRow):\r
+ """Special version of the BoundRow class that can handle model instances\r
+ as data.\r
+\r
+ We could simply have ModelTable spawn the normal BoundRow objects\r
+ with the instance converted to a dict instead. However, this way allows\r
+ us to support non-field attributes and methods on the model as well.\r
+ """\r
+ def __getitem__(self, name):\r
+ """Overridden. Return this row's data for a certain column, with\r
+ custom handling for model tables.\r
+ """\r
+\r
+ # find the column for the requested field, for reference\r
+ boundcol = (name in self.table._columns) \\r
+ and self.table._columns[name]\\r
+ or None\r
+\r
+ # If the column has a name override (we know then that is was also\r
+ # used for access, e.g. if the condition is true, then\r
+ # ``boundcol.column.name == name``), we need to make sure we use the\r
+ # declaration name to access the model field.\r
+ if boundcol.column.name:\r
+ name = boundcol.declared_name\r
+\r
+ result = getattr(self.data, name, None)\r
+ if result is None:\r
+ if boundcol and boundcol.column.default is not None:\r
+ result = boundcol.column.default\r
+ else:\r
+ raise AttributeError()\r
+ return result
\ No newline at end of file
return self._snapshot\r
data = property(lambda s: s._get_data())\r
\r
+ def _validate_column_name(self, name, purpose):\r
+ """Return True/False, depending on whether the column ``name`` is\r
+ valid for ``purpose``. Used to validate things like ``order_by``\r
+ instructions.\r
+\r
+ Can be overridden by subclasses to impose further restrictions.\r
+ """\r
+ if purpose == 'order_by':\r
+ return name in self.columns and\\r
+ self.columns[name].column.sortable\r
+ else:\r
+ return True\r
+\r
def _set_order_by(self, value):\r
if self._snapshot is not None:\r
self._snapshot = None\r
and [value.split(',')] \\r
or [value])[0]\r
# validate, remove all invalid order instructions\r
- def can_be_used(o):\r
- c = (o[:1]=='-' and [o[1:]] or [o])[0]\r
- return c in self.base_columns and self.base_columns[c].sortable\r
- self._order_by = OrderByTuple([o for o in self._order_by if can_be_used(o)])\r
+ self._order_by = OrderByTuple([o for o in self._order_by\r
+ if self._validate_column_name((o[:1]=='-' and [o[1:]] or [o])[0], "order_by")])\r
# TODO: optionally, throw an exception\r
order_by = property(lambda s: s._order_by, _set_order_by)\r
\r
def _get_rows(self):\r
for row in self.data:\r
yield BoundRow(self, row)\r
- rows = property(_get_rows)\r
+ rows = property(lambda s: s._get_rows())\r
\r
def as_html(self):\r
pass\r
# ``base_columns`` might have changed since last time); creating\r
# BoundColumn instances can be costly, so we reuse existing ones.\r
new_columns = SortedDict()\r
- for name, column in self.table.base_columns.items():\r
- name = column.name or name # take into account name overrides\r
- if name in self._columns:\r
- new_columns[name] = self._columns[name]\r
+ for decl_name, column in self.table.base_columns.items():\r
+ # take into account name overrides\r
+ exposed_name = column.name or decl_name\r
+ if exposed_name in self._columns:\r
+ new_columns[exposed_name] = self._columns[exposed_name]\r
else:\r
- new_columns[name] = BoundColumn(self, column, name)\r
+ new_columns[exposed_name] = BoundColumn(self, column, decl_name)\r
self._columns = new_columns\r
\r
def all(self):\r
class BoundColumn(StrAndUnicode):\r
"""'Runtime' version of ``Column`` that is bound to a table instance,\r
and thus knows about the table's data.\r
+\r
+ Note name... TODO\r
"""\r
def __init__(self, table, column, name):\r
self.table = table\r
self.column = column\r
- self.name = column.name or name\r
+ self.declared_name = name\r
# expose some attributes of the column more directly\r
self.sortable = column.sortable\r
self.visible = column.visible\r
\r
+ name = property(lambda s: s.column.name or s.declared_name)\r
+\r
def _get_values(self):\r
# TODO: build a list of values used\r
pass\r
yield value\r
\r
def __getitem__(self, name):\r
- "Returns the value for the column with the given name."\r
+ """Returns this row's value for a column. All other access methods,\r
+ e.g. __iter__, lead ultimately to this."""\r
return self.data[name]\r
\r
def __contains__(self, item):\r
assert 'motto' in StateTable1.base_columns\r
assert 'motto' in StateTable2.base_columns\r
\r
-\r
def test_basic():\r
class BookTable(tables.Table):\r
name = tables.Column()\r
])\r
# access without order_by works\r
books.data\r
+ books.rows\r
\r
for r in books.rows:\r
- # unknown fields are removed\r
+ # unknown fields are removed/not-accessible\r
assert 'name' in r\r
assert not 'id' in r\r
# missing data is available as default\r
assert not 'test' in BookTable.base_columns\r
\r
# make sure the row and column caches work\r
- id(list(books.rows)[0]) == id(list(books.rows)[0])\r
- id(list(books.columns)[0]) == id(list(books.columns)[0])\r
+ assert id(list(books.columns)[0]) == id(list(books.columns)[0])\r
+ # TODO: row cache currently not used\r
+ #assert id(list(books.rows)[0]) == id(list(books.rows)[0])\r
\r
def test_sort():\r
class BookTable(tables.Table):\r
id = tables.Column()\r
name = tables.Column()\r
- pages = tables.Column()\r
+ pages = tables.Column(name='num_pages') # test rewritten names\r
language = tables.Column(default='en') # default affects sorting\r
\r
books = BookTable([\r
- {'id': 1, 'pages': 60, 'name': 'Z: The Book'}, # language: en\r
- {'id': 2, 'pages': 100, 'language': 'de', 'name': 'A: The Book'},\r
- {'id': 3, 'pages': 80, 'language': 'de', 'name': 'A: The Book, Vol. 2'},\r
- {'id': 4, 'pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'},\r
+ {'id': 1, 'num_pages': 60, 'name': 'Z: The Book'}, # language: en\r
+ {'id': 2, 'num_pages': 100, 'language': 'de', 'name': 'A: The Book'},\r
+ {'id': 3, 'num_pages': 80, 'language': 'de', 'name': 'A: The Book, Vol. 2'},\r
+ {'id': 4, 'num_pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'},\r
])\r
\r
def test_order(order, result):\r
assert [b['id'] for b in books.data] == result\r
\r
# test various orderings\r
- test_order(('pages',), [1,3,2,4])\r
- test_order(('-pages',), [4,2,3,1])\r
+ test_order(('num_pages',), [1,3,2,4])\r
+ test_order(('-num_pages',), [4,2,3,1])\r
test_order(('name',), [2,4,3,1])\r
- test_order(('language', 'pages'), [3,2,1,4])\r
+ test_order(('language', 'num_pages'), [3,2,1,4])\r
# using a simple string (for convinience as well as querystring passing\r
- test_order('-pages', [4,2,3,1])\r
- test_order('language,pages', [3,2,1,4])\r
+ test_order('-num_pages', [4,2,3,1])\r
+ test_order('language,num_pages', [3,2,1,4])\r
\r
# [bug] test alternative order formats if passed to constructor\r
- BookTable([], 'language,-pages')\r
+ BookTable([], 'language,-num_pages')\r
\r
# test invalid order instructions\r
books.order_by = 'xyz'\r
books.base_columns['language'].sortable = False\r
books.order_by = 'language'\r
assert not books.order_by\r
- test_order(('language', 'pages'), [1,3,2,4]) # as if: 'pages'
\ No newline at end of file
+ test_order(('language', 'num_pages'), [1,3,2,4]) # as if: 'num_pages'
\ No newline at end of file
"""\r
\r
from django.conf import settings\r
+import django_tables as tables\r
\r
def setup_module(module):\r
settings.configure(**{\r
'DATABASE_ENGINE': 'sqlite3',\r
'DATABASE_NAME': ':memory:',\r
+ 'INSTALLED_APPS': ('tests.testapp',)\r
})\r
\r
from django.db import models\r
+ from django.core.management import call_command\r
\r
class City(models.Model):\r
name = models.TextField()\r
population = models.IntegerField()\r
+ class Meta:\r
+ app_label = 'testapp'\r
module.City = City\r
\r
class Country(models.Model):\r
name = models.TextField()\r
population = models.IntegerField()\r
- capital = models.ForeignKey(City)\r
+ capital = models.ForeignKey(City, blank=True, null=True)\r
tld = models.TextField(verbose_name='Domain Extension', max_length=2)\r
+ system = models.TextField(blank=True, null=True)\r
+ null = models.TextField(blank=True, null=True) # tests expect this to be always null!\r
+ class Meta:\r
+ app_label = 'testapp'\r
module.Country = Country\r
\r
+ # create the tables\r
+ call_command('syncdb', verbosity=1, interactive=False)\r
\r
-def test_nothing():\r
- pass\r
-\r
-import django_tables as tables\r
+ # create a couple of objects\r
+ Country(name="Austria", tld="au", population=8, system="republic").save()\r
+ Country(name="Germany", tld="de", population=81).save()\r
+ Country(name="France", tld="fr", population=64, system="republic").save()\r
+ Country(name="Netherlands", tld="nl", population=16, system="monarchy").save()\r
\r
def test_declaration():\r
"""Test declaration, declared columns and default model field columns.\r
class Meta:\r
model = Country\r
\r
- assert len(CountryTable.base_columns) == 5\r
+ assert len(CountryTable.base_columns) == 7\r
assert 'name' in CountryTable.base_columns\r
assert not hasattr(CountryTable, 'name')\r
\r
model = Country\r
exclude = ['tld']\r
\r
- assert len(CountryTable.base_columns) == 5\r
+ assert len(CountryTable.base_columns) == 7\r
assert 'projected' in CountryTable.base_columns\r
assert 'capital' in CountryTable.base_columns\r
assert not 'tld' in CountryTable.base_columns\r
assert 'name' in CityTable.base_columns\r
assert 'projected' in CityTable.base_columns # declared in parent\r
assert not 'population' in CityTable.base_columns # not in Meta:columns\r
- assert 'capital' in CityTable.base_columns # in exclude, but only works on model fields (is that the right behaviour?)
\ No newline at end of file
+ assert 'capital' in CityTable.base_columns # in exclude, but only works on model fields (is that the right behaviour?)\r
+\r
+def test_basic():\r
+ """Some tests here are copied from ``test_basic.py`` but need to be\r
+ rerun with a ModelTable, as the implementation is different."""\r
+ class CountryTable(tables.ModelTable):\r
+ null = tables.Column(default="foo")\r
+ tld = tables.Column(name="domain")\r
+ class Meta:\r
+ model = Country\r
+ exclude = ('id',)\r
+ countries = CountryTable()\r
+\r
+ for r in countries.rows:\r
+ # "normal" fields exist\r
+ assert 'name' in r\r
+ # unknown fields are removed/not accessible\r
+ assert not 'does-not-exist' in r\r
+ # ...so are excluded fields\r
+ assert not 'id' in r\r
+ # missing data is available with default values\r
+ assert 'null' in r\r
+ assert r['null'] == "foo" # note: different from prev. line!\r
+\r
+ # all that still works when name overrides are used\r
+ assert not 'tld' in r\r
+ assert 'domain' in r\r
+ assert len(r['domain']) == 2 # valid country tld\r
+\r
+ # make sure the row and column caches work for model tables as well\r
+ assert id(list(countries.columns)[0]) == id(list(countries.columns)[0])\r
+ # TODO: row cache currently not used\r
+ #assert id(list(countries.rows)[0]) == id(list(countries.rows)[0])\r
+\r
+def test_sort():\r
+ class CountryTable(tables.ModelTable):\r
+ tld = tables.Column(name="domain")\r
+ system = tables.Column(default="republic")\r
+ custom1 = tables.Column()\r
+ custom2 = tables.Column(sortable=True)\r
+ class Meta:\r
+ model = Country\r
+ countries = CountryTable()\r
+\r
+ def test_order(order, result):\r
+ countries.order_by = order\r
+ assert [r['id'] for r in countries.rows] == result\r
+\r
+ # test various orderings\r
+ test_order(('population',), [1,4,3,2])\r
+ test_order(('-population',), [2,3,4,1])\r
+ test_order(('name',), [1,3,2,4])\r
+ # test sorting by a "rewritten" column name\r
+ countries.order_by = 'domain,tld'\r
+ countries.order_by == ('domain',)\r
+ test_order(('-domain',), [4,3,2,1])\r
+ # test multiple order instructions; note: one row is missing a "system"\r
+ # value, but has a default set; however, that has no effect on sorting.\r
+ test_order(('system', '-population'), [2,4,3,1])\r
+ # using a simple string (for convinience as well as querystring passing\r
+ test_order('-population', [2,3,4,1])\r
+ test_order('system,-population', [2,4,3,1])\r
+\r
+ # test invalid order instructions\r
+ countries.order_by = 'invalid_field,population'\r
+ assert countries.order_by == ('population',)\r
+ # ...for modeltables, this primarily means that only model-based colunns\r
+ # are currently sortable at all.\r
+ countries.order_by = ('custom1', 'custom2')\r
+ assert countries.order_by == ()\r
+\r
+def test_pagination():\r
+ pass\r
+\r
+# TODO: foreignkey columns: simply support foreignkeys, tuples and id, name dicts; support column choices attribute to validate id-only\r
+# TODO: pagination\r
+# TODO: support function column sources both for modeltables (methods on model) and static tables (functions in dict)\r
+# TODO: manual base columns change -> update() call (add as example in docstr here) -> rebuild snapshot: is row cache, column cache etc. reset?\r
+# TODO: test that boundcolumn.name works with name overrides and without\r
+# TODO: more beautiful auto column names\r
+# TODO: normal tables should handle name overrides differently; the backend data should still use declared_name
\ No newline at end of file