From 9589d92a4c5bbb42493bbb60edce7276ac0834dd Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Thu, 2 Jun 2011 22:31:13 +1000 Subject: [PATCH] Added the ability to exclude columns from an entire Table class or from a specific instance. Resolves issue #4. --- django_tables/columns.py | 8 +-- django_tables/tables.py | 54 ++++++++--------- docs/index.rst | 67 +++++++++++++++------ tests/core.py | 122 ++++++++++++++++++++++++--------------- 4 files changed, 158 insertions(+), 93 deletions(-) diff --git a/django_tables/columns.py b/django_tables/columns.py index 23734dc..cc94116 100644 --- a/django_tables/columns.py +++ b/django_tables/columns.py @@ -434,17 +434,17 @@ class BoundColumn(object): class BoundColumns(object): """ - Container for spawning BoundColumns. + Container for spawning :class:`.BoundColumn` objects. This is bound to a table and provides its :attr:`.Table.columns` property. It provides access to those columns in different ways (iterator, item-based, filtered and unfiltered etc), stuff that would not be possible with a simple iterator in the table class. - A :class:`.BoundColumns` object is a container for holding - :class:`.BoundColumn` objects. It provides methods that make accessing + A ``BoundColumns`` object is a container for holding + ``BoundColumn`` objects. It provides methods that make accessing columns easier than if they were stored in a ``list`` or - :class:`dict`. :class:`Columns` has a similar API to a ``dict`` (it + ``dict``. ``Columns`` has a similar API to a ``dict`` (it actually uses a ``SortedDict`` interally). At the moment you'll only come across this class when you access a diff --git a/django_tables/tables.py b/django_tables/tables.py index f3ba21e..7d516c5 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -19,7 +19,7 @@ class TableData(object): Exposes a consistent API for :term:`table data`. It currently supports a :class:`QuerySet`, or a :class:`list` of :class:`dict` objects. - This class is used by :class:.Table` to wrap any + This class is used by :class:`.Table` to wrap any input table data. """ @@ -92,7 +92,7 @@ class DeclarativeColumnsMetaclass(type): as well. """ - def __new__(cls, name, bases, attrs, parent_cols_from=None): + def __new__(cls, name, bases, attrs): """Ughhh document this :)""" # extract declared columns columns = [(name, attrs.pop(name)) for name, column in attrs.items() @@ -104,22 +104,22 @@ class DeclarativeColumnsMetaclass(type): # well. Note that we loop over the bases in *reverse* - this is # necessary to preserve the correct order of columns. for base in bases[::-1]: - cols_attr = (parent_cols_from if (parent_cols_from and - hasattr(base, parent_cols_from)) - else 'base_columns') - if hasattr(base, cols_attr): - columns = getattr(base, cols_attr).items() + columns + if hasattr(base, "base_columns"): + columns = base.base_columns.items() + columns # Note that we are reusing an existing ``base_columns`` attribute. # This is because in certain inheritance cases (mixing normal and # ModelTables) this metaclass might be executed twice, and we need # to avoid overriding previous data (because we pop() from attrs, # the second time around columns might not be registered again). # An example would be: - # class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass - if not 'base_columns' in attrs: - attrs['base_columns'] = SortedDict() - attrs['base_columns'].update(SortedDict(columns)) - attrs['_meta'] = TableOptions(attrs.get('Meta', None)) + # class MyNewTable(MyOldNonTable, tables.Table): pass + if not "base_columns" in attrs: + attrs["base_columns"] = SortedDict() + attrs["base_columns"].update(SortedDict(columns)) + attrs["_meta"] = opts = TableOptions(attrs.get("Meta", None)) + for ex in opts.exclude: + if ex in attrs["base_columns"]: + attrs["base_columns"].pop(ex) return type.__new__(cls, name, bases, attrs) @@ -127,23 +127,21 @@ class TableOptions(object): """ Extracts and exposes options for a :class:`.Table` from a ``class Meta`` when the table is defined. + + :param options: options for a table + :type options: :class:`Meta` on a :class:`.Table` """ def __init__(self, options=None): - """ - - :param options: options for a table - :type options: :class:`Meta` on a :class:`.Table` - - """ super(TableOptions, self).__init__() - self.sortable = getattr(options, 'sortable', True) - order_by = getattr(options, 'order_by', ()) + self.attrs = AttributeDict(getattr(options, "attrs", {})) + self.empty_text = getattr(options, "empty_text", None) + self.exclude = getattr(options, "exclude", ()) + order_by = getattr(options, "order_by", ()) if isinstance(order_by, basestring): order_by = (order_by, ) self.order_by = OrderByTuple(order_by) - self.attrs = AttributeDict(getattr(options, 'attrs', {})) - self.empty_text = getattr(options, 'empty_text', None) + self.sortable = getattr(options, "sortable", True) class Table(StrAndUnicode): @@ -186,9 +184,10 @@ class Table(StrAndUnicode): __metaclass__ = DeclarativeColumnsMetaclass TableDataClass = TableData - def __init__(self, data, order_by=None, sortable=None, empty_text=None): - self._rows = BoundRows(self) # bound rows - self._columns = BoundColumns(self) # bound columns + def __init__(self, data, order_by=None, sortable=None, empty_text=None, + exclude=None): + self._rows = BoundRows(self) + self._columns = BoundColumns(self) self._data = self.TableDataClass(data=data, table=self) self.empty_text = empty_text self.sortable = sortable @@ -196,11 +195,14 @@ class Table(StrAndUnicode): self.order_by = self._meta.order_by else: self.order_by = order_by - # Make a copy so that modifying this will not touch the class # definition. Note that this is different from forms, where the # copy is made available in a ``fields`` attribute. self.base_columns = copy.deepcopy(type(self).base_columns) + self.exclude = exclude or () + for ex in self.exclude: + if ex in self.base_columns: + self.base_columns.pop(ex) def __unicode__(self): return self.as_html() diff --git a/docs/index.rst b/docs/index.rst index 3abe9a5..5915148 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -636,6 +636,9 @@ API Reference .. class:: Table.Meta + Provides a way to define *global* settings for table, as opposed to + defining them for each instance. + .. attribute:: attrs Allows custom HTML attributes to be specified which will be added to @@ -643,9 +646,54 @@ API Reference :meth:`~django_tables.tables.Table.as_html` or the :ref:`template-tags.render_table` template tag. + This is typically used to enable a theme for a table (which is done by + adding a CSS class to the ```` element). i.e.:: + + class SimpleTable(tables.Table): + name = tables.Column() + + class Meta: + attrs = {"class": "paleblue"} + + :type: ``dict`` + Default: ``{}`` - :type: :class:`dict` + .. attribute:: empty_text + + Defines the text to display when the table has no rows. + + .. attribute:: exclude + + Defines which columns should be excluded from the table. This is useful + in subclasses to exclude columns in a parent. e.g. + + >>> class Person(tables.Table): + ... first_name = tables.Column() + ... last_name = tables.Column() + ... + >>> Person.base_columns + {'first_name': , + 'last_name': } + >>> class ForgetfulPerson(Person): + ... class Meta: + ... exclude = ("last_name", ) + ... + >>> ForgetfulPerson.base_columns + {'first_name': } + + :type: tuple of ``string`` objects + + Default: ``()`` + + .. attribute:: order_by + + The default ordering. e.g. ``('name', '-age')``. A hyphen ``-`` can be + used to prefix a column name to indicate *descending* order. + + :type: :class:`tuple` + + Default: ``()`` .. attribute:: sortable @@ -656,17 +704,9 @@ API Reference an easy mechanism to disable sorting on an entire table, without adding ``sortable=False`` to each ``Column`` in a ``Table``. - Default: :const:`True` - :type: :class:`bool` - .. attribute:: order_by - - The default ordering. e.g. ``('name', '-age')`` - - Default: ``()`` - - :type: :class:`tuple` + Default: :const:`True` :class:`TableData` Objects: @@ -676,13 +716,6 @@ API Reference :members: __init__, order_by, __getitem__, __len__ -:class:`TableOptions` Objects: ------------------------------- - -.. autoclass:: django_tables.tables.TableOptions - :members: - - :class:`Column` Objects: ------------------------ diff --git a/tests/core.py b/tests/core.py index b329b07..50af9ff 100644 --- a/tests/core.py +++ b/tests/core.py @@ -6,34 +6,30 @@ from django.core.paginator import Paginator import django_tables as tables from django_tables import utils + core = Tests() -@core.context -def context(): - class Context(object): - memory_data = [ - {'i': 2, 'alpha': 'b', 'beta': 'b'}, - {'i': 1, 'alpha': 'a', 'beta': 'c'}, - {'i': 3, 'alpha': 'c', 'beta': 'a'}, - ] +class UnsortedTable(tables.Table): + i = tables.Column() + alpha = tables.Column() + beta = tables.Column() - class UnsortedTable(tables.Table): - i = tables.Column() - alpha = tables.Column() - beta = tables.Column() - class SortedTable(UnsortedTable): - class Meta: - order_by = 'alpha' +class SortedTable(UnsortedTable): + class Meta: + order_by = 'alpha' - table = UnsortedTable(memory_data) - yield Context +MEMORY_DATA = [ + {'i': 2, 'alpha': 'b', 'beta': 'b'}, + {'i': 1, 'alpha': 'a', 'beta': 'c'}, + {'i': 3, 'alpha': 'c', 'beta': 'a'}, +] @core.test -def declarations(context): +def declarations(): """Test defining tables by declaration.""" class GeoAreaTable(tables.Table): name = tables.Column() @@ -61,60 +57,60 @@ def declarations(context): @core.test -def datasource_untouched(context): +def datasource_untouched(): """Ensure that data that is provided to the table (the datasource) is not modified by table operations. """ - original_data = copy.deepcopy(context.memory_data) + original_data = copy.deepcopy(MEMORY_DATA) - table = context.UnsortedTable(context.memory_data) + table = UnsortedTable(MEMORY_DATA) table.order_by = 'i' list(table.rows) - assert context.memory_data == Assert(original_data) + assert MEMORY_DATA == Assert(original_data) - table = context.UnsortedTable(context.memory_data) + table = UnsortedTable(MEMORY_DATA) table.order_by = 'beta' list(table.rows) - assert context.memory_data == Assert(original_data) + assert MEMORY_DATA == Assert(original_data) @core.test -def sorting(ctx): +def sorting(): # fallback to Table.Meta - Assert(('alpha', )) == ctx.SortedTable([], order_by=None).order_by == ctx.SortedTable([]).order_by + Assert(('alpha', )) == SortedTable([], order_by=None).order_by == SortedTable([]).order_by # values of order_by are wrapped in tuples before being returned - Assert(ctx.SortedTable([], order_by='alpha').order_by) == ('alpha', ) - Assert(ctx.SortedTable([], order_by=('beta',)).order_by) == ('beta', ) + Assert(SortedTable([], order_by='alpha').order_by) == ('alpha', ) + Assert(SortedTable([], order_by=('beta',)).order_by) == ('beta', ) # "no sorting" - table = ctx.SortedTable([]) + table = SortedTable([]) table.order_by = [] - Assert(()) == table.order_by == ctx.SortedTable([], order_by=[]).order_by + Assert(()) == table.order_by == SortedTable([], order_by=[]).order_by - table = ctx.SortedTable([]) + table = SortedTable([]) table.order_by = () - Assert(()) == table.order_by == ctx.SortedTable([], order_by=()).order_by + Assert(()) == table.order_by == SortedTable([], order_by=()).order_by - table = ctx.SortedTable([]) + table = SortedTable([]) table.order_by = '' - Assert(()) == table.order_by == ctx.SortedTable([], order_by='').order_by + Assert(()) == table.order_by == SortedTable([], order_by='').order_by # apply a sorting - table = ctx.UnsortedTable([]) + table = UnsortedTable([]) table.order_by = 'alpha' - Assert(('alpha', )) == ctx.UnsortedTable([], order_by='alpha').order_by == table.order_by + Assert(('alpha', )) == UnsortedTable([], order_by='alpha').order_by == table.order_by - table = ctx.SortedTable([]) + table = SortedTable([]) table.order_by = 'alpha' - Assert(('alpha', )) == ctx.SortedTable([], order_by='alpha').order_by == table.order_by + Assert(('alpha', )) == SortedTable([], order_by='alpha').order_by == table.order_by # let's check the data - table = ctx.SortedTable(ctx.memory_data, order_by='beta') + table = SortedTable(MEMORY_DATA, order_by='beta') Assert(3) == table.rows[0]['i'] # allow fallback to Table.Meta.order_by - table = ctx.SortedTable(ctx.memory_data) + table = SortedTable(MEMORY_DATA) Assert(1) == table.rows[0]['i'] # column's can't be sorted if they're not allowed to be @@ -147,7 +143,7 @@ def sorting(ctx): @core.test -def column_count(context): +def column_count(): class SimpleTable(tables.Table): visible = tables.Column(visible=True) hidden = tables.Column(visible=False) @@ -157,16 +153,50 @@ def column_count(context): @core.test -def column_accessor(context): - class SimpleTable(context.UnsortedTable): +def column_accessor(): + class SimpleTable(UnsortedTable): col1 = tables.Column(accessor='alpha.upper.isupper') col2 = tables.Column(accessor='alpha.upper') - table = SimpleTable(context.memory_data) + table = SimpleTable(MEMORY_DATA) row = table.rows[0] Assert(row['col1']) is True Assert(row['col2']) == 'B' +@core.test +def exclude_columns(): + """ + Defining ``Table.Meta.exclude`` or providing an ``exclude`` argument when + instantiating a table should have the same effect -- exclude those columns + from the table. It should have the same effect as not defining the + columns originally. + """ + # Table(..., exclude=...) + table = UnsortedTable([], exclude=("i")) + Assert([c.name for c in table.columns]) == ["alpha", "beta"] + + # Table.Meta: exclude=... + class PartialTable(UnsortedTable): + class Meta: + exclude = ("alpha", ) + table = PartialTable([]) + Assert([c.name for c in table.columns]) == ["i", "beta"] + + # Inheritence -- exclude in parent, add in child + class AddonTable(PartialTable): + added = tables.Column() + table = AddonTable([]) + Assert([c.name for c in table.columns]) == ["i", "beta", "added"] + + # Inheritence -- exclude in child + class ExcludeTable(UnsortedTable): + added = tables.Column() + class Meta: + exclude = ("alpha", ) + table = ExcludeTable([]) + Assert([c.name for c in table.columns]) == ["i", "beta", "added"] + + @core.test def pagination(): class BookTable(tables.Table): @@ -175,7 +205,7 @@ def pagination(): # create some sample data data = [] for i in range(100): - data.append({'name': 'Book No. %d' % i}) + data.append({"name": "Book No. %d" % i}) books = BookTable(data) # external paginator @@ -187,7 +217,7 @@ def pagination(): # integrated paginator books.paginate(page=1) - Assert(hasattr(books, 'page')) is True + Assert(hasattr(books, "page")) is True books.paginate(page=1, per_page=10) Assert(len(list(books.page.object_list))) == 10 -- 2.26.2