From ec35a46dcb44c5d38deb568c3bf2ccf34e984691 Mon Sep 17 00:00:00 2001 From: winhamwr Date: Thu, 7 Apr 2011 13:35:08 -0400 Subject: [PATCH 01/13] Switched to using a column's declared name consistently as the reference to that column. This allows one database-backed column to be exposed as multiple table columns with different formatting. Removed data in favor of render_FOO and name in favor of model_rel --- django_tables/base.py | 103 ++++++++++++++++--------------- django_tables/columns.py | 29 +++------ django_tables/memory.py | 28 +++------ django_tables/models.py | 71 +++++++++------------ docs/columns.rst | 34 +++++----- docs/index.rst | 18 +++--- docs/templates.rst | 14 ++--- tests/test_memory.py | 16 ++--- tests/test_models.py | 130 +++++++++++++++++++++++++-------------- tests/test_templates.py | 41 +++++++++++- 10 files changed, 262 insertions(+), 222 deletions(-) diff --git a/django_tables/base.py b/django_tables/base.py index 75b5e954..cfcadf8b 100644 --- a/django_tables/base.py +++ b/django_tables/base.py @@ -190,12 +190,10 @@ def _spawn_columns(self): # BoundColumn instances can be costly, so we reuse existing ones. new_columns = SortedDict() for decl_name, column in self.table.base_columns.items(): - # take into account name overrides - exposed_name = column.name or decl_name - if exposed_name in self._columns: - new_columns[exposed_name] = self._columns[exposed_name] + if decl_name in self._columns: + new_columns[decl_name] = self._columns[decl_name] else: - new_columns[exposed_name] = BoundColumn(self.table, column, decl_name) + new_columns[decl_name] = BoundColumn(self.table, column, decl_name) self._columns = new_columns def all(self): @@ -262,28 +260,26 @@ def __getitem__(self, name): class BoundColumn(StrAndUnicode): - """'Runtime' version of ``Column`` that is bound to a table instance, + """ + 'Runtime' version of ``Column`` that is bound to a table instance, and thus knows about the table's data. - - Note that the name that is passed in tells us how this field is - delared in the bound table. The column itself can overwrite this name. - While the overwritten name will be hat mostly counts, we need to - remember the one used for declaration as well, or we won't know how - to read a column's value from the source. """ - def __init__(self, table, column, name): + def __init__(self, table, column, declared_name): self.table = table self.column = column - self.declared_name = name + self.declared_name = declared_name # expose some attributes of the column more directly self.visible = column.visible @property - def accessor(self): - """The key to use when accessing this column's values in the + def src_accessor(self): + """ + The key to use when accessing this column's values in the source data. """ - return self.column.data if self.column.data else self.declared_name + if self.column.model_rel: + return self.column.model_rel + return self.declared_name def _get_sortable(self): if self.column.sortable is not None: @@ -294,7 +290,7 @@ def _get_sortable(self): return True # the default value sortable = property(_get_sortable) - name = property(lambda s: s.column.name or s.declared_name) + name = property(lambda s: s.declared_name) name_reversed = property(lambda s: "-"+s.name) def _get_name_toggled(self): o = self.table.order_by @@ -320,11 +316,6 @@ def get_default(self, row): return self.column.default(row) return self.column.default - def _get_values(self): - # TODO: build a list of values used - pass - values = property(_get_values) - def __unicode__(self): s = self.column.verbose_name or self.name.replace('_', ' ') return capfirst(force_unicode(s)) @@ -334,7 +325,8 @@ def as_html(self): class BoundRow(object): - """Represents a single row of data, bound to a table. + """ + Represents a single row of data, bound to a table. Tables will spawn these row objects, wrapping around the actual data stored in a row. @@ -348,9 +340,10 @@ def __iter__(self): yield value def __getitem__(self, name): - """Returns this row's value for a column. All other access methods, - e.g. __iter__, lead ultimately to this.""" - + """ + Returns this row's value for a column. All other access methods, + e.g. __iter__, lead ultimately to this. + """ column = self.table.columns[name] render_func = getattr(self.table, 'render_%s' % name, False) @@ -360,10 +353,11 @@ def __getitem__(self, name): return self._default_render(column) def _default_render(self, column): - """Returns a cell's content. This is used unless the user + """ + Returns a cell's content. This is used unless the user provides a custom ``render_FOO`` method. """ - result = self.data[column.accessor] + result = self.data[column.src_accessor] # if the field we are pointing to is a callable, remove it if callable(result): @@ -371,7 +365,9 @@ def _default_render(self, column): return result def __contains__(self, item): - """Check by both row object and column name.""" + """ + Check by both row object and column name. + """ if isinstance(item, basestring): return item in self.table._columns else: @@ -387,7 +383,8 @@ def as_html(self): class Rows(object): - """Container for spawning BoundRows. + """ + Container for spawning BoundRows. This is bound to a table and provides it's ``rows`` property. It provides functionality that would not be possible with a simple @@ -403,12 +400,16 @@ def _reset(self): pass # we currently don't use a cache def all(self): - """Return all rows.""" + """ + Return all rows. + """ for row in self.table.data: yield self.row_class(self.table, row) def page(self): - """Return rows on current page (if paginated).""" + """ + Return rows on current page (if paginated). + """ if not hasattr(self.table, 'page'): return None return iter(self.table.page.object_list) @@ -432,7 +433,8 @@ def __getitem__(self, key): class BaseTable(object): - """A collection of columns, plus their associated data rows. + """ + A collection of columns, plus their associated data rows. """ __metaclass__ = DeclarativeColumnsMetaclass @@ -445,7 +447,8 @@ class BaseTable(object): DefaultOrder = type('DefaultSortType', (), {})() def __init__(self, data, order_by=DefaultOrder): - """Create a new table instance with the iterable ``data``. + """ + Create a new table instance with the iterable ``data``. If ``order_by`` is specified, the data will be sorted accordingly. Otherwise, the sort order can be specified in the table options. @@ -481,7 +484,8 @@ def __init__(self, data, order_by=DefaultOrder): self.base_columns = copy.deepcopy(type(self).base_columns) def _reset_snapshot(self, reason): - """Called to reset the current snaptshot, for example when + """ + Called to reset the current snaptshot, for example when options change that could affect it. ``reason`` is given so that subclasses can decide that a @@ -490,7 +494,8 @@ def _reset_snapshot(self, reason): self._snapshot = None def _build_snapshot(self): - """Rebuild the table for the current set of options. + """ + Rebuild the table for the current set of options. Whenver the table options change, e.g. say a new sort order, this method will be asked to regenerate the actual table from @@ -507,7 +512,8 @@ def _get_data(self): data = property(lambda s: s._get_data()) def _resolve_sort_directions(self, order_by): - """Given an ``order_by`` tuple, this will toggle the hyphen-prefixes + """ + Given an ``order_by`` tuple, this will toggle the hyphen-prefixes according to each column's ``direction`` option, e.g. it translates between the ascending/descending and the straight/reverse terminology. """ @@ -518,22 +524,22 @@ def _resolve_sort_directions(self, order_by): result.append(inst) return result - def _cols_to_fields(self, names): - """Utility function. Given a list of column names (as exposed to + def _col_names_to_src_names(self, names): + """ + Utility function. Given a list of column names (as exposed to the user), converts column names to the names we have to use to retrieve a column's data from the source. Usually, the name used in the table declaration is used for accessing - the source (while a column can define an alias-like name that will - be used to refer to it from the "outside"). However, a column can - override this by giving a specific source field name via ``data``. + the source. However, a column can override this by giving a specific + source field name via ``model_rel``. Supports prefixed column names as used e.g. in order_by ("-field"). """ - result = [] + src_names = [] for ident in names: # handle order prefix - if ident[:1] == '-': + if ident.startswith('-'): name = ident[1:] prefix = '-' else: @@ -541,11 +547,12 @@ def _cols_to_fields(self, names): prefix = '' # find the field name column = self.columns[name] - result.append(prefix + column.accessor) - return result + src_names.append(prefix + column.src_accessor) + return src_names def _validate_column_name(self, name, purpose): - """Return True/False, depending on whether the column ``name`` is + """ + Return True/False, depending on whether the column ``name`` is valid for ``purpose``. Used to validate things like ``order_by`` instructions. diff --git a/django_tables/columns.py b/django_tables/columns.py index 3307f092..9124640b 100644 --- a/django_tables/columns.py +++ b/django_tables/columns.py @@ -7,11 +7,11 @@ class Column(object): ``verbose_name`` defines a display name for this column used for output. - ``name`` is the internal name of the column. Normally you don't need to - specify this, as the attribute that you make the column available under - is used. However, in certain circumstances it can be useful to override - this default, e.g. when using ModelTables if you want a column to not - use the model field name. + ``model_rel`` is the Django ORM-style name of the relationship path from + your Model to the field represented by this column. By default, the + column"s label is assumed to be the path to the field, but in cases where + two different columns are both backed by the same field, you can use + the same model_rel on two columns. ``default`` is the default value for this column. If the data source does provide ``None`` for a row, the default will be used instead. Note @@ -19,15 +19,6 @@ class Column(object): or normal). Also, you can specify a callable, which will be passed a ``BoundRow`` instance and is expected to return the default to be used. - Additionally, you may specify ``data``. It works very much like - ``default``, except it's effect does not depend on the actual cell - value. When given a function, it will always be called with a row object, - expected to return the cell value. If given a string, that name will be - used to read the data from the source (instead of the column's name). - - Note the interaction with ``default``. If ``default`` is specified as - well, it will be used whenver ``data`` yields in a None value. - You can use ``visible`` to flag the column as hidden by default. However, this can be overridden by the ``visibility`` argument to the table constructor. If you want to make the column completely unavailable @@ -46,18 +37,12 @@ class Column(object): # Tracks each time a Column instance is created. Used to retain order. creation_counter = 0 - def __init__(self, verbose_name=None, name=None, default=None, data=None, + def __init__(self, verbose_name=None, model_rel=None, default=None, visible=True, inaccessible=False, sortable=None, direction=ASC): self.verbose_name = verbose_name - self.name = name + self.model_rel = model_rel self.default = default - self.data = data - if callable(self.data): - raise DeprecationWarning(('The Column "data" argument may no '+ - 'longer be a callable. Add a '+ - '``render_%s`` method to your '+ - 'table instead.') % (name or 'FOO')) self.visible = visible self.inaccessible = inaccessible self.sortable = sortable diff --git a/django_tables/memory.py b/django_tables/memory.py index 40d147cc..a4b93d84 100644 --- a/django_tables/memory.py +++ b/django_tables/memory.py @@ -47,38 +47,28 @@ def _build_snapshot(self): # probably much better to do this on-demand instead, when the # data is *needed* for the first time. """ - # reset caches self._columns._reset() self._rows._reset() snapshot = copy.copy(self._data) - for row in snapshot: - # add data that is missing from the source. we do this now so - # that the colunn ``default`` and ``data`` values can affect - # sorting (even when callables are used)! + # Fill in ``default`` values where needed + for src_row in snapshot: + # We do this now so that column ``default`` values can affect + # sorting, even with callables # This is a design decision - the alternative would be to # resolve the values when they are accessed, and either do not # support sorting them at all, or run the callables during # sorting. for column in self.columns.all(): - name_in_source = column.declared_name - if column.column.data: - if callable(column.column.data): - # if data is a callable, use it's return value - row[name_in_source] = column.column.data(BoundRow(self, row)) - else: - name_in_source = column.column.data - - # the following will be True if: - # * the source does not provide that column or provides None - # * the column did provide a data callable that returned None - if row.get(name_in_source, None) is None: - row[name_in_source] = column.get_default(BoundRow(self, row)) + if src_row.get(column.src_accessor, None) is None: + # No value was provided in the source, so use the default + src_row[column.src_accessor] = column.get_default( + BoundRow(self, src_row)) if self.order_by: actual_order_by = self._resolve_sort_directions(self.order_by) - sort_table(snapshot, self._cols_to_fields(actual_order_by)) + sort_table(snapshot, self._col_names_to_src_names(actual_order_by)) return snapshot diff --git a/django_tables/models.py b/django_tables/models.py index b3ccb097..b7cefac7 100644 --- a/django_tables/models.py +++ b/django_tables/models.py @@ -55,11 +55,12 @@ class BoundModelRow(BoundRow): """ def _default_render(self, boundcol): - """In the case of a model table, the accessor may use ``__`` to + """ + In the case of a model table, the accessor may use ``__`` to span instances. We need to resolve this. """ # try to resolve relationships spanning attributes - bits = boundcol.accessor.split('__') + bits = boundcol.src_accessor.split('__') current = self.data for bit in bits: # note the difference between the attribute being None and not @@ -70,7 +71,7 @@ def _default_render(self, boundcol): # also ``_validate_column_name``, where such a mechanism is # already implemented). if not hasattr(current, bit): - raise ValueError("Could not resolve %s from %s" % (bit, boundcol.accessor)) + raise ValueError("Could not resolve %s from %s" % (bit, boundcol.src_accessor)) current = getattr(current, bit) if callable(current): @@ -148,7 +149,6 @@ class Meta: just don't any data at all, the model the table is based on will provide it. """ - __metaclass__ = ModelTableMetaclass rows_class = ModelRows @@ -167,9 +167,10 @@ def __init__(self, data=None, *args, **kwargs): super(ModelTable, self).__init__(self.queryset, *args, **kwargs) def _validate_column_name(self, name, purpose): - """Overridden. Only allow model-based fields and valid model - spanning relationships to be sorted.""" - + """ + Overridden. Only allow model-based fields and valid model + spanning relationships to be sorted. + """ # let the base class sort out the easy ones result = super(ModelTable, self)._validate_column_name(name, purpose) if not result: @@ -178,51 +179,32 @@ def _validate_column_name(self, name, purpose): if purpose == 'order_by': column = self.columns[name] - # "data" can really be used in two different ways. It is - # slightly confusing and potentially should be changed. - # It can either refer to an attribute/field which the table - # column should represent, or can be a callable (or a string - # pointing to a callable attribute) that is used to render to - # cell. The difference is that in the latter case, there may - # still be an actual source model field behind the column, - # stored in "declared_name". In other words, we want to filter - # out column names that are not oderable, and the column name - # we need to check may either be stored in "data" or in - # "declared_name", depending on if and what kind of value is - # in "data". This is the reason why we try twice. - # - # See also bug #282964. - # # TODO: It might be faster to try to resolve the given name # manually recursing the model metadata rather than # constructing a queryset. - for lookup in (column.column.data, column.declared_name): - if not lookup or callable(lookup): - continue - try: - # Let Django validate the lookup by asking it to build - # the final query; the way to do this has changed in - # Django 1.2, and we try to support both versions. - _temp = self.queryset.order_by(lookup).query - if hasattr(_temp, 'as_sql'): - _temp.as_sql() - else: - from django.db import DEFAULT_DB_ALIAS - _temp.get_compiler(DEFAULT_DB_ALIAS).as_sql() - break - except FieldError: - pass - else: + try: + # Let Django validate the lookup by asking it to build + # the final query; the way to do this has changed in + # Django 1.2, and we try to support both versions. + _temp = self.queryset.order_by(column.src_accessor).query + if hasattr(_temp, 'as_sql'): + _temp.as_sql() + else: + from django.db import DEFAULT_DB_ALIAS + _temp.get_compiler(DEFAULT_DB_ALIAS).as_sql() + except FieldError: return False + else: + return False # if we haven't failed by now, the column should be valid return True def _build_snapshot(self): - """Overridden. The snapshot in this case is simply a queryset + """ + Overridden. The snapshot in this case is simply a queryset with the necessary filters etc. attached. """ - # reset caches self._columns._reset() self._rows._reset() @@ -230,5 +212,10 @@ def _build_snapshot(self): queryset = self.queryset if self.order_by: actual_order_by = self._resolve_sort_directions(self.order_by) - queryset = queryset.order_by(*self._cols_to_fields(actual_order_by)) + print "Order By: %s" % actual_order_by + queryset = queryset.order_by( + *self._col_names_to_src_names(actual_order_by)) + else: + print "No order by" + return queryset diff --git a/docs/columns.rst b/docs/columns.rst index e9c7a1af..202ce26f 100644 --- a/docs/columns.rst +++ b/docs/columns.rst @@ -16,39 +16,33 @@ There are no required arguments. The following is fine: .. code-block:: python class MyTable(tables.MemoryTable): - c = tables.Column() + count = tables.Column() -It will result in a column named ``c`` in the table. You can specify the -``name`` to override this: +It will result in a column named ``count`` in the table that references the +value ``count`` in your source data (if present). . You can specify the +``model_rel`` to override the source data this field is referencing : .. code-block:: python - c = tables.Column(name="count") + count = tables.Column(model_rel="src_count") -The column is now called and accessed via "count", although the table will -still use ``c`` to read it's values from the source. You can however modify -that as well, by specifying ``data``: +The column is still called and accessed via "count", but now the table will + use ``src_count`` to read it's values from the source. -.. code-block:: python - - c = tables.Column(name="count", data="count") - -For practicual purposes, ``c`` is now meaningless. While in most cases -you will just define your column using the name you want it to have, the -above is useful when working with columns automatically generated from -models: +This is useful for occasions where you'd like to place meaningful names mapping +to input fields with less-useful names. .. code-block:: python class BookTable(tables.ModelTable): - book_name = tables.Column(name="name") - author = tables.Column(data="info__author__name") + book_name = tables.Column(model_rel="name") + author_name = tables.Column(model_rel="info__author__name") class Meta: model = Book -The overwritten ``book_name`` field/column will now be exposed as the -cleaner ``name``, and the new ``author`` column retrieves it's values from -``Book.info.author.name``. +The overwritten ``Book.name`` field will now be exposed via the table as +the ``book_name`` column, and the new ``author_name`` column retrieves it's +values from ``Book.info.author.name``. Apart from their internal name, you can define a string that will be used when for display via ``verbose_name``: diff --git a/docs/index.rst b/docs/index.rst index a79fb6e8..68815780 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,10 +23,15 @@ The API looks similar to that of Django's ``ModelForms``: import django_tables as tables + MY_TIMEZONE = -5 class CountryTable(tables.MemoryTable): name = tables.Column(verbose_name="Country Name") population = tables.Column(sortable=False, visible=False) - time_zone = tables.Column(name="tz", default="UTC+1") + time_zone = tables.Column(model_rel="tz", default="+1") + time_diff = tables.Column(model_rel="tz") + + def render_time_diff(self, country): + return int(country.tz) - MY_TIMEZONE Instead of fields, you declare a column for every piece of data you want to expose to the user. @@ -79,15 +84,14 @@ template: def list_countries(request): data = ... - countries = CountryTable(data, order_by=request.GET.get('sort')) + countries = CountryTable(data, order_by=request.GET.get('sort', 'name')) return render_to_response('list.html', {'table': countries}) Note that we are giving the incoming ``sort`` query string value directly to the table, asking for a sort. All invalid column names will (by default) be -ignored. In this example, only ``name`` and ``tz`` are allowed, since: +ignored. In this example, only ``name`` and ``time_zone`` are allowed, since: * ``population`` has ``sortable=False`` - * ``time_zone`` has it's name overwritten with ``tz``. Then, in the ``list.html`` template, write: @@ -114,13 +118,13 @@ which in turn will be used by the server for ordering. ``order_by`` accepts comma-separated strings as input, and ``{{ column.name_toggled }}`` will be rendered as a such a string. -Instead of the iterator, you can alos use your knowledge of the table +Instead of the iterator, you can also use your knowledge of the table structure to access columns directly: .. code-block:: django - {% if table.columns.tz.visible %} - {{ table.columns.tz }} + {% if table.columns.time_zone.visible %} + {{ table.columns.time_zone }} {% endfor %} diff --git a/docs/templates.rst b/docs/templates.rst index c3ee8bb7..da683b36 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -47,23 +47,23 @@ your Python code: class BookTable(tables.ModelTable): name = tables.Column() - rating_int = tables.Column(name="rating") + rating_int = tables.Column(model_rel="rating_count") - def render_rating(self, instance): - if instance.rating_count == 0: + def render_rating(self, book): + if book.rating_count == 0: return '' else: - return '' % instance.rating_int + return '' % book.rating_count -When accessing ``table.rows[i].rating``, the ``render_rating`` method +When accessing ``table.rows[i].rating_int``, the ``render_rating_int`` method will be called. Note the following: - What is passed is underlying raw data object, in this case, the model instance. This gives you access to data values that may not have been defined as a column. - For the method name, the public name of the column must be used, not - the internal field name. That is, it's ``render_rating``, not - ``render_rating_int``. + the internal field name. That is, it's ``render_rating_int``, not + ``render_rating``. - The method is called whenever the cell value is retrieved by you, whether from Python code or within templates. However, operations by ``django-tables``, like sorting, always work with the raw data. diff --git a/tests/test_memory.py b/tests/test_memory.py index fe5ef6b3..1f9252e6 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -14,8 +14,8 @@ def test_basic(): class StuffTable(tables.MemoryTable): name = tables.Column() answer = tables.Column(default=42) - c = tables.Column(name="count", default=1) - email = tables.Column(data="@") + count = tables.Column(model_rel="c", default=1) + email = tables.Column(model_rel="@") stuff = StuffTable([ {'id': 1, 'name': 'Foo Bar', '@': 'foo@bar.org'}, ]) @@ -26,8 +26,8 @@ class StuffTable(tables.MemoryTable): # make sure BoundColumnn.name always gives us the right thing, whether # the column explicitely defines a name or not. - stuff.columns['count'].name == 'count' - stuff.columns['answer'].name == 'answer' + assert stuff.columns['count'].name == 'count' + assert stuff.columns['answer'].name == 'answer' for r in stuff.rows: # unknown fields are removed/not-accessible @@ -42,7 +42,7 @@ class StuffTable(tables.MemoryTable): assert 'count' in r assert r['count'] == 1 - # columns with data= option work fine + # columns with model_rel= option work fine assert r['email'] == 'foo@bar.org' # try to splice rows by index @@ -76,7 +76,7 @@ class TestRender: def test(self): class TestTable(tables.MemoryTable): - private_name = tables.Column(name='public_name') + public_name = tables.Column(model_rel='private_name') def render_public_name(self, data): # We are given the actual data dict and have direct access # to additional values for which no field is defined. @@ -150,9 +150,9 @@ def test_sort(): class BookTable(tables.MemoryTable): id = tables.Column(direction='desc') name = tables.Column() - pages = tables.Column(name='num_pages') # test rewritten names + num_pages = tables.Column(model_rel='pages') # test rewritten names language = tables.Column(default='en') # default affects sorting - rating = tables.Column(data='*') # test data field option + rating = tables.Column(model_rel='*') # test data field option books = BookTable([ {'id': 1, 'pages': 60, 'name': 'Z: The Book', '*': 5}, # language: en diff --git a/tests/test_models.py b/tests/test_models.py index 0b3f79a3..774a6a09 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -44,12 +44,20 @@ class Meta: call_command('syncdb', verbosity=1, interactive=False) # create a couple of objects - berlin=City(name="Berlin"); berlin.save() - amsterdam=City(name="Amsterdam"); amsterdam.save() - Country(name="Austria", tld="au", population=8, system="republic").save() - Country(name="Germany", tld="de", population=81, capital=berlin).save() - Country(name="France", tld="fr", population=64, system="republic").save() - Country(name="Netherlands", tld="nl", population=16, system="monarchy", capital=amsterdam).save() + berlin = City.objects.create(name="Berlin", population=30) + amsterdam = City.objects.create(name="Amsterdam", population=6) + Country.objects.create( + name="Austria", tld="au", population=8, system="republic") + Country.objects.create( + name="Germany", tld="de", population=81, capital=berlin) + Country.objects.create( + name="France", tld="fr", population=64, system="republic") + Country.objects.create( + name="Netherlands", + tld="nl", + population=16, + system="monarchy", + capital=amsterdam) class TestDeclaration: @@ -114,41 +122,44 @@ class Meta: assert [c.column.verbose_name for c in CountryTable().columns] == ['Domain Extension'] +def _test_country_table(table): + for r in table.rows: + # "normal" fields exist + assert 'name' in r + # unknown fields are removed/not accessible + assert not 'does-not-exist' in r + # ...so are excluded fields + assert not 'id' in r + # [bug] access to data that might be available, but does not + # have a corresponding column is denied. + assert_raises(Exception, "r['id']") + # missing data is available with default values + assert 'null' in r + assert r['null'] == "foo" # note: different from prev. line! + # if everything else fails (no default), we get None back + assert r['null2'] is None + + # all that still works when name overrides are used + assert 'tld' in r + assert 'domain' in r + assert len(r['domain']) == 2 # valid country tld def test_basic(): - """Some tests here are copied from ``test_basic.py`` but need to be - rerun with a ModelTable, as the implementation is different.""" + """ + Some tests here are copied from ``test_basic.py`` but need to be + rerun with a ModelTable, as the implementation is different. + """ class CountryTable(tables.ModelTable): null = tables.Column(default="foo") - tld = tables.Column(name="domain") + domain = tables.Column(model_rel="tld") class Meta: model = Country exclude = ('id',) - countries = CountryTable() - def test_country_table(table): - for r in table.rows: - # "normal" fields exist - assert 'name' in r - # unknown fields are removed/not accessible - assert not 'does-not-exist' in r - # ...so are excluded fields - assert not 'id' in r - # [bug] access to data that might be available, but does not - # have a corresponding column is denied. - assert_raises(Exception, "r['id']") - # missing data is available with default values - assert 'null' in r - assert r['null'] == "foo" # note: different from prev. line! - # if everything else fails (no default), we get None back - assert r['null2'] is None - - # all that still works when name overrides are used - assert not 'tld' in r - assert 'domain' in r - assert len(r['domain']) == 2 # valid country tld - test_country_table(countries) + + countries = CountryTable() + _test_country_table(countries) # repeat the avove tests with a table that is not associated with a # model, and all columns being created manually. @@ -159,10 +170,27 @@ class CountryTable(tables.ModelTable): system = tables.Column() null = tables.Column(default="foo") null2 = tables.Column() - tld = tables.Column(name="domain") + domain = tables.Column(model_rel="tld") + tld = tables.Column() + + countries = CountryTable(Country) - test_country_table(countries) + _test_country_table(countries) + +def test_with_filter(): + class CountryTable(tables.ModelTable): + null = tables.Column(default="foo") + domain = tables.Column(model_rel="tld") + class Meta: + model = Country + exclude = ('id',) + countries = CountryTable(Country.objects.filter(name="France")) + + assert len(countries.rows) == 1 + row = countries.rows[0] + assert row['name'] == 'France' + _test_country_table(countries) def test_invalid_accessor(): """Test that a column being backed by a non-existent model property @@ -171,7 +199,7 @@ def test_invalid_accessor(): Regression-Test: There used to be a NameError here. """ class CountryTable(tables.ModelTable): - name = tables.Column(data='something-i-made-up') + name = tables.Column(model_rel='something-i-made-up') countries = CountryTable(Country) assert_raises(ValueError, countries[0].__getitem__, 'name') @@ -199,7 +227,7 @@ class Meta: def test_sort(): class CountryTable(tables.ModelTable): - tld = tables.Column(name="domain") + domain = tables.Column(model_rel="tld") population = tables.Column() system = tables.Column(default="republic") custom1 = tables.Column() @@ -208,9 +236,10 @@ class Meta: model = Country countries = CountryTable() - def test_order(order, result, table=countries): + def test_order(order, expected, table=countries): table.order_by = order - assert [r['id'] for r in table.rows] == result + actual = [r['id'] for r in table.rows] + assert actual == expected, "actual= %s" % repr(actual) # test various orderings test_order(('population',), [1,4,3,2]) @@ -219,6 +248,8 @@ def test_order(order, result, table=countries): # test sorting with a "rewritten" column name countries.order_by = 'domain,tld' # "tld" would be invalid... countries.order_by == ('domain',) # ...and is therefore removed + countries.order_by = ('-domain','tld') # "tld" would be invalid... + countries.order_by == ('-domain',) # ...and is therefore removed test_order(('-domain',), [4,3,2,1]) # test multiple order instructions; note: one row is missing a "system" # value, but has a default set; however, that has no effect on sorting. @@ -242,7 +273,7 @@ class Meta: # ...in case of ModelTables, this primarily means that only # model-based colunns are currently sortable at all. countries.order_by = ('custom1', 'custom2') - assert countries.order_by == () + assert countries.order_by == (), "Actual: %s" % repr(countries.order_by) def test_default_sort(): class SortedCountryTable(tables.ModelTable): @@ -287,11 +318,17 @@ def test_relationships(): class CountryTable(tables.ModelTable): # add relationship spanning columns (using different approaches) - capital_name = tables.Column(data='capital__name') - capital__population = tables.Column(name="capital_population") - invalid = tables.Column(data="capital__invalid") + capital_name = tables.Column(model_rel='capital__name') + capital_name_link = tables.Column(model_rel='capital__name') + cap_pop = tables.Column(model_rel="capital__population") + invalid = tables.Column(model_rel="capital__invalid") class Meta: model = Country + + def render_capital_name_link(self, country): + return '%s' % ( + country.capital.name, country.capital.name) + countries = CountryTable(Country.objects.select_related('capital')) # ordering and field access works @@ -299,13 +336,14 @@ class Meta: assert [row['capital_name'] for row in countries.rows] == \ [None, None, 'Amsterdam', 'Berlin'] - countries.order_by = 'capital_population' - assert [row['capital_population'] for row in countries.rows] == \ - [None, None, None, None] + countries.order_by = 'cap_pop' + actual = [row['cap_pop'] for row in countries.rows] + assert actual == \ + [None, None, 6, 30], "Actual: %s" % repr(actual) # ordering by a column with an invalid relationship fails silently countries.order_by = 'invalid' - assert countries.order_by == () + assert countries.order_by == (), "Actual: %s" % repr(countries.order_by) def test_pagination(): diff --git a/tests/test_templates.py b/tests/test_templates.py index 50f4dcb6..2c12ecaf 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -32,7 +32,8 @@ class CountryTable(tables.MemoryTable): population = tables.NumberColumn(verbose_name="Population Size") currency = tables.NumberColumn(visible=False, inaccessible=True) tld = tables.TextColumn(visible=False, verbose_name="Domain") - calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.") + cc = tables.NumberColumn( + model_rel="calling_code", verbose_name="Phone Ext.") countries = CountryTable( [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'cc': 49}, @@ -70,7 +71,9 @@ class CountryTable(tables.MemoryTable): assert countries.columns['tld'].visible == False def test_render(): - """For good measure, render some actual templates.""" + """ + For good measure, render some actual templates. + """ class CountryTable(tables.MemoryTable): name = tables.TextColumn() @@ -78,7 +81,8 @@ class CountryTable(tables.MemoryTable): population = tables.NumberColumn(verbose_name="Population Size") currency = tables.NumberColumn(visible=False, inaccessible=True) tld = tables.TextColumn(visible=False, verbose_name="Domain") - calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.") + cc = tables.NumberColumn( + model_rel="calling_code", verbose_name="Phone Ext.") countries = CountryTable( [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'calling_code': 49}, @@ -98,6 +102,37 @@ class CountryTable(tables.MemoryTable): render(Context({'countries': countries})) == \ "Germany France Netherlands Austria" +def test_custom_render(): + class CountryTable(tables.MemoryTable): + name = tables.TextColumn() + capital = tables.TextColumn() + population = tables.NumberColumn(verbose_name="Population Size") + currency = tables.NumberColumn(visible=False, inaccessible=True) + tld = tables.TextColumn(visible=False, verbose_name="Domain") + cc = tables.NumberColumn( + model_rel="calling_code", verbose_name="Phone Ext.") + + def render_cc(self, src_row): + return "SUCCESS" + + def render_calling_code(self, src_row): + # We should never get here + assert False + + countries = CountryTable( + [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'calling_code': 49}, + {'name': 'France', 'population': 64, 'currency': 'Euro (€)', 'tld': 'fr', 'calling_code': 33}, + {'name': 'Netherlands', 'capital': 'Amsterdam', 'calling_code': '31'}, + {'name': 'Austria', 'calling_code': 43, 'currency': 'Euro (€)', 'population': 8}]) + + assert Template("{% for column in countries.columns %}{{ column }}/{{ column.name }} {% endfor %}").\ + render(Context({'countries': countries})) == \ + "Name/name Capital/capital Population Size/population Phone Ext./cc " + + assert Template("{% for row in countries %}{% for value in row %}{{ value }} {% endfor %}{% endfor %}").\ + render(Context({'countries': countries})) == \ + "Germany Berlin 83 SUCCESS France None 64 SUCCESS Netherlands Amsterdam None SUCCESS Austria None 8 SUCCESS " + def test_templatetags(): add_to_builtins('django_tables.app.templatetags.tables') From 8fca6838c357e6048eb900fdcfc4e46c24039b05 Mon Sep 17 00:00:00 2001 From: jlward Date: Thu, 7 Apr 2011 12:51:37 -0700 Subject: [PATCH 02/13] fixed a problem with non visible columns --- django_tables/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/django_tables/base.py b/django_tables/base.py index cfcadf8b..5b5771d1 100644 --- a/django_tables/base.py +++ b/django_tables/base.py @@ -269,7 +269,9 @@ def __init__(self, table, column, declared_name): self.column = column self.declared_name = declared_name # expose some attributes of the column more directly - self.visible = column.visible + self.visible = False + if column: + self.visible = column.visible @property def src_accessor(self): From 806814785fd1d568416bddff5400449c1fc5e6dc Mon Sep 17 00:00:00 2001 From: jlward Date: Thu, 7 Apr 2011 12:52:25 -0700 Subject: [PATCH 03/13] removed print statements --- django_tables/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/django_tables/models.py b/django_tables/models.py index b7cefac7..8439f56b 100644 --- a/django_tables/models.py +++ b/django_tables/models.py @@ -212,10 +212,7 @@ def _build_snapshot(self): queryset = self.queryset if self.order_by: actual_order_by = self._resolve_sort_directions(self.order_by) - print "Order By: %s" % actual_order_by queryset = queryset.order_by( *self._col_names_to_src_names(actual_order_by)) - else: - print "No order by" return queryset From fa2f5bc667cf0aaf0793629784f1df7da9e31b25 Mon Sep 17 00:00:00 2001 From: Jason Ward Date: Thu, 7 Apr 2011 15:55:52 -0400 Subject: [PATCH 04/13] fixed a bug with non visable columns --- django_tables/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/django_tables/base.py b/django_tables/base.py index 75b5e954..4a33142e 100644 --- a/django_tables/base.py +++ b/django_tables/base.py @@ -276,7 +276,9 @@ def __init__(self, table, column, name): self.column = column self.declared_name = name # expose some attributes of the column more directly - self.visible = column.visible + self.visible = False + if column: + self.visible = column.visible @property def accessor(self): From 8228715b76fdbfc390fa642c3b6ebf13576149d3 Mon Sep 17 00:00:00 2001 From: Jason Ward Date: Thu, 7 Apr 2011 15:56:41 -0400 Subject: [PATCH 05/13] Revert "fixed a bug with non visable columns" This reverts commit fa2f5bc667cf0aaf0793629784f1df7da9e31b25. --- django_tables/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/django_tables/base.py b/django_tables/base.py index 4a33142e..75b5e954 100644 --- a/django_tables/base.py +++ b/django_tables/base.py @@ -276,9 +276,7 @@ def __init__(self, table, column, name): self.column = column self.declared_name = name # expose some attributes of the column more directly - self.visible = False - if column: - self.visible = column.visible + self.visible = column.visible @property def accessor(self): From 582f2c16385cee03ead681fd712590233a957476 Mon Sep 17 00:00:00 2001 From: winhamwr Date: Thu, 7 Apr 2011 17:31:36 -0400 Subject: [PATCH 06/13] Removed some print statements and added a test verifying the current Meta: columns behavior regarding auto-genned model fields --- django_tables/models.py | 3 --- tests/test_models.py | 23 ++++++++++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/django_tables/models.py b/django_tables/models.py index b7cefac7..8439f56b 100644 --- a/django_tables/models.py +++ b/django_tables/models.py @@ -212,10 +212,7 @@ def _build_snapshot(self): queryset = self.queryset if self.order_by: actual_order_by = self._resolve_sort_directions(self.order_by) - print "Order By: %s" % actual_order_by queryset = queryset.order_by( *self._col_names_to_src_names(actual_order_by)) - else: - print "No order by" return queryset diff --git a/tests/test_models.py b/tests/test_models.py index 774a6a09..a98d484c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -61,7 +61,8 @@ class Meta: class TestDeclaration: - """Test declaration, declared columns and default model field columns. + """ + Test declaration, declared columns and default model field columns. """ def test_autogen_basic(self): @@ -93,13 +94,29 @@ class Meta: columns = ['id', 'name'] exclude = ['capital'] - print CityTable.base_columns assert len(CityTable.base_columns) == 4 assert 'id' in CityTable.base_columns assert 'name' in CityTable.base_columns assert 'projected' in CityTable.base_columns # declared in parent assert not 'population' in CityTable.base_columns # not in Meta:columns - assert 'capital' in CityTable.base_columns # in exclude, but only works on model fields (is that the right behaviour?) + # in exclude, but only works on model fields (is that the right behaviour?) + assert 'capital' in CityTable.base_columns + + # Define one column so that all automatically-generated columns are + # excluded + class CountryTable(tables.ModelTable): + capital = tables.TextColumn(verbose_name='Name of capital') + projected = tables.Column(verbose_name="Projected Population") + + class Meta: + model = Country + columns = ['capital'] + + num_columns = len(CountryTable.base_columns) + assert num_columns == 2, "Actual: %s" % num_columns + assert 'projected' in CountryTable.base_columns + assert 'capital' in CountryTable.base_columns + assert not 'tld' in CountryTable.base_columns def test_columns_custom_order(self): """Using the columns meta option, you can also modify the ordering. From 8af2d0e732bf9d0969fd44b6eaf69ea08047873d Mon Sep 17 00:00:00 2001 From: winhamwr Date: Thu, 7 Apr 2011 18:01:19 -0400 Subject: [PATCH 07/13] Half-done documentation explaining the need for overloaded columns --- README | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/README b/README index 2c28c878..7dbbd1aa 100644 --- a/README +++ b/README @@ -1,4 +1,67 @@ -django-tables - a Django QuerySet renderer. +================================== +DataTabler - Easy Tables in Django +================================== -Documentation: - http://elsdoerfer.name/docs/django-tables/ +:Version: 0.1.0dev +:Web: http://github.com/winhamwr/django-tables/ +:Source: http://github.com/winhamwr/django-tables/ + +.. datatabler-synopsis: + +DataTabler is an easy way to turn your Django Querysets, dictionary lists or +other structured data in to an HTML table with server-side sorting and +consistent formatting. Define the data relationship you want to display in one +place, and then DRY to table victory. + +DataTabler was derived from `django-tables`_ and is a very similar library. + +.. datatabler-overview: + +Overview +======== + +Dealing with sorting, consistent formatting and derived fields when deplaying +querysets and other structured data can be a pain. How much do you put in +your view, versus the template and how much should you try to DRY with include +templates and templatetags? What about creating CRUD links, but only for users +with permissions on the individual objects? + +With DataTabler, you define the columns you want to expose in the table up +front, along with sensible names, free sorting and the power of python for +DRYing up any non-display logic. + +.. datatabler-example: + +Example +======= + +Let's say you'd like to display a table of documents in a document management +system. You need an edit link, author (sortable by last name) and a preview +of the document. On another page you don't need the preview anymore, but you +need to be able to . Your Document model looks like this: +:: + + # models.py + class Document(models.Model): + name = models.CharField(max_length=200) + html = models.TextField(blank=True) + author = models.ForeignKey(User, related_name='documents_authored_set') + + +With DataTabler, you'd define a ``Table`` (anywhere, but tables.py is nice) like +so: +:: + + # tables.py + class DocumentTable(datatabler.ModelTable): + edit_link = Column(sortable=False, verbose_name=' ') +:: + + # view.py + def list_documents(request): + +License +======= + +This software is licensed under the `New BSD License`. See the ``LICENSE`` +file in the top distribution directory for the full license text. From 72deeda7e312c9eb2e758985476b8373132e6efb Mon Sep 17 00:00:00 2001 From: winhamwr Date: Thu, 7 Apr 2011 18:01:44 -0400 Subject: [PATCH 08/13] Switched to restructured txt for readme --- README => README.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README => README.rst (100%) diff --git a/README b/README.rst similarity index 100% rename from README rename to README.rst From 327179d0194a76fe51efeca4c973c446883963d6 Mon Sep 17 00:00:00 2001 From: winhamwr Date: Fri, 8 Apr 2011 13:15:18 -0400 Subject: [PATCH 09/13] Modified the ModelTable order_by column validation to use the model's _default_manager --- django_tables/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/django_tables/models.py b/django_tables/models.py index 8439f56b..ac7aa643 100644 --- a/django_tables/models.py +++ b/django_tables/models.py @@ -186,7 +186,12 @@ def _validate_column_name(self, name, purpose): # Let Django validate the lookup by asking it to build # the final query; the way to do this has changed in # Django 1.2, and we try to support both versions. - _temp = self.queryset.order_by(column.src_accessor).query + + # Using the model._default_manager to get a standard manager + # in case we're sorting on a "fake" queryset that doesn't + # implement the SQL compiler + _temp = self.queryset.model._default_manager.order_by( + column.src_accessor).query if hasattr(_temp, 'as_sql'): _temp.as_sql() else: From 21c86e173da874ed1cafc702032797db97587259 Mon Sep 17 00:00:00 2001 From: Jason Ward Date: Mon, 11 Apr 2011 13:41:16 -0400 Subject: [PATCH 10/13] added a helper function --- django_tables/utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 django_tables/utils.py diff --git a/django_tables/utils.py b/django_tables/utils.py new file mode 100644 index 00000000..4a478c94 --- /dev/null +++ b/django_tables/utils.py @@ -0,0 +1,10 @@ +def get_order_by(query_dict, order_by_param, secondary=None): + ''' + ``query_dict`` is either the get or post data + ``order_by_param`` is the variable name with which to sort on + ``default`` which column to order on in a default case + ''' + order_by = query_dict.get(order_by_param) + if order_by and not order_by.lstrip('-') == secondary: + order_by = [order_by, secondary] + return order_by From 822719d0a51a590635e4de07e8fd98557c94bef3 Mon Sep 17 00:00:00 2001 From: Jason Ward Date: Mon, 25 Apr 2011 13:54:47 -0400 Subject: [PATCH 11/13] allowed for passing in an empty list --- django_tables/models.py | 4 +++- tests/test_models.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/django_tables/models.py b/django_tables/models.py index 8439f56b..47eb70f9 100644 --- a/django_tables/models.py +++ b/django_tables/models.py @@ -154,7 +154,9 @@ class Meta: rows_class = ModelRows def __init__(self, data=None, *args, **kwargs): - if data == None: + if not data: + data = None + if data is None: if self._meta.model is None: raise ValueError('Table without a model association needs ' 'to be initialized with data') diff --git a/tests/test_models.py b/tests/test_models.py index 774a6a09..0a1e5332 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -192,6 +192,17 @@ class Meta: _test_country_table(countries) +def test_with_empty_list(): + class CountryTable(tables.ModelTable): + null = tables.Column(default="foo") + domain = tables.Column(model_rel="tld") + class Meta: + model = Country + exclude = ('id',) + + # Should be able to pass in an empty list and call order_by on it + countries = CountryTable([], order_by='domain') + def test_invalid_accessor(): """Test that a column being backed by a non-existent model property is handled correctly. From 477075566199e1de043e91a3d70a1f4972b85f79 Mon Sep 17 00:00:00 2001 From: Jason Ward Date: Mon, 25 Apr 2011 14:28:55 -0400 Subject: [PATCH 12/13] made an empty list stop populating the table with every single item from the model --- django_tables/models.py | 2 +- tests/test_models.py | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/django_tables/models.py b/django_tables/models.py index 47eb70f9..719de346 100644 --- a/django_tables/models.py +++ b/django_tables/models.py @@ -160,7 +160,7 @@ def __init__(self, data=None, *args, **kwargs): if self._meta.model is None: raise ValueError('Table without a model association needs ' 'to be initialized with data') - self.queryset = self._meta.model._default_manager.all() + self.queryset = self._meta.model._default_manager.none() elif hasattr(data, '_default_manager'): # saves us db.models import self.queryset = data._default_manager.all() else: diff --git a/tests/test_models.py b/tests/test_models.py index 0a1e5332..cd21ab7b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -202,6 +202,7 @@ class Meta: # Should be able to pass in an empty list and call order_by on it countries = CountryTable([], order_by='domain') + assert len(countries.rows) == 0 def test_invalid_accessor(): """Test that a column being backed by a non-existent model property @@ -223,7 +224,7 @@ class CountryTable(tables.ModelTable): class Meta: model = Country exclude = ('id',) - countries = CountryTable() + countries = CountryTable(Country.objects.all()) assert id(list(countries.columns)[0]) == id(list(countries.columns)[0]) # TODO: row cache currently not used @@ -245,7 +246,7 @@ class CountryTable(tables.ModelTable): custom2 = tables.Column(sortable=True) class Meta: model = Country - countries = CountryTable() + countries = CountryTable(Country.objects.all()) def test_order(order, expected, table=countries): table.order_by = order @@ -274,7 +275,7 @@ class CityTable(tables.ModelTable): name = tables.Column(direction='desc') class Meta: model = City - cities = CityTable() + cities = CityTable(City.objects.all()) test_order('name', [1,2], table=cities) # Berlin to Amsterdam test_order('-name', [2,1], table=cities) # Amsterdam to Berlin @@ -292,16 +293,17 @@ class Meta: model = Country order_by = '-name' + countries = Country.objects.all() # the order_by option is provided by TableOptions - assert_equal('-name', SortedCountryTable()._meta.order_by) + assert_equal('-name', SortedCountryTable(countries)._meta.order_by) # the default order can be inherited from the table - assert_equal(('-name',), SortedCountryTable().order_by) - assert_equal(4, SortedCountryTable().rows[0]['id']) + assert_equal(('-name',), SortedCountryTable(countries).order_by) + assert_equal(4, SortedCountryTable(countries).rows[0]['id']) # and explicitly set (or reset) via __init__ - assert_equal(2, SortedCountryTable(order_by='system').rows[0]['id']) - assert_equal(1, SortedCountryTable(order_by=None).rows[0]['id']) + assert_equal(2, SortedCountryTable(countries, order_by='system').rows[0]['id']) + assert_equal(1, SortedCountryTable(countries, order_by=None).rows[0]['id']) def test_callable(): """Some of the callable code is reimplemented for modeltables, so @@ -371,12 +373,12 @@ class CityTable(tables.ModelTable): class Meta: model = City columns = ['name'] - cities = CityTable() # add some sample data City.objects.all().delete() for i in range(1,101): City.objects.create(name="City %d"%i) + cities = CityTable(City.objects.all()) # for query logging settings.DEBUG = True @@ -389,9 +391,9 @@ class Meta: assert len(page.object_list) == 10 assert page.has_previous() == False assert page.has_next() == True - # Make sure the queryset is not loaded completely - there must be two - # queries, one a count(). This check is far from foolproof... - assert len(connection.queries)-start_querycount == 2 + # Make sure the queryset is not loaded completely - there must be one + # query: count(). This check is far from foolproof... + assert len(connection.queries)-start_querycount == 1, len(connection.queries)-start_querycount # using a queryset paginator is possible as well (although unnecessary) paginator = QuerySetPaginator(cities.rows, 10) @@ -407,7 +409,7 @@ class Meta: assert cities.paginator.num_pages == 10 assert cities.page.has_previous() == False assert cities.page.has_next() == True - assert len(connection.queries)-start_querycount == 2 + assert len(connection.queries)-start_querycount == 0 # reset settings.DEBUG = False From 0828a110573f7e8b4d82e1a04e22a85a4af476b5 Mon Sep 17 00:00:00 2001 From: Jason Ward Date: Wed, 27 Apr 2011 17:30:23 -0400 Subject: [PATCH 13/13] added a test to show that getting no results from a filter will not fully populate a table with all rows of the model --- tests/test_models.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index f16be190..1d0520e9 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -221,6 +221,19 @@ class Meta: countries = CountryTable([], order_by='domain') assert len(countries.rows) == 0 +def test_with_no_results_query(): + class CountryTable(tables.ModelTable): + null = tables.Column(default="foo") + domain = tables.Column(model_rel="tld") + class Meta: + model = Country + exclude = ('id',) + + # Should be able to pass in an empty list and call order_by on it + countries = CountryTable(Country.objects.filter(name='does not exist'), order_by='domain') + assert len(countries.rows) == 0 + + def test_invalid_accessor(): """Test that a column being backed by a non-existent model property is handled correctly.