From c2a84ed674758fe15588949cb6a919e507f66a06 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Tue, 7 Jun 2011 09:12:52 +1000 Subject: [PATCH] currently broken --- django_tables/columns.py | 50 +++++++++++++++++++++---------- django_tables/tables.py | 64 ++++++++++++++++++++++++++++++++++++---- docs/index.rst | 17 +++++++---- tests/columns.py | 48 ++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 27 deletions(-) diff --git a/django_tables/columns.py b/django_tables/columns.py index 5043120..e3626d2 100644 --- a/django_tables/columns.py +++ b/django_tables/columns.py @@ -480,23 +480,37 @@ class BoundColumns(object): columns[name] = BoundColumn(self.table, column, name) self._columns = columns - def all(self): + def iternames(self): + return (name for name, column in self.iteritems()) + + def names(self): + return list(self.iternames()) + + def iterall(self): """ Return an iterator that exposes all :class:`.BoundColumn` objects, regardless of visiblity or sortability. """ - self._spawn_columns() - return (column for name, column in self._columns.iteritems()) + return (column for name, column in self.iteritems()) - def items(self): + def all(self): + return list(self.iterall()) + + def iteritems(self): """ Return an iterator of ``(name, column)`` pairs (where ``column`` is a :class:`.BoundColumn` object). """ self._spawn_columns() - return self._columns.iteritems() + if self.table.sequence: + return ((x, self._columns[x]) for x in self.table.sequence) + else: + return self._columns.iteritems() - def sortable(self): + def items(self): + return list(self.iteritems()) + + def itersortable(self): """ Same as :meth:`.BoundColumns.all` but only returns sortable :class:`.BoundColumn` objects. @@ -508,7 +522,10 @@ class BoundColumns(object): """ return ifilter(lambda x: x.sortable, self.all()) - def visible(self): + def sortable(self): + return list(self.itersortable()) + + def itervisible(self): """ Same as :meth:`.sortable` but only returns visible :class:`.BoundColumn` objects. @@ -517,11 +534,14 @@ class BoundColumns(object): """ return ifilter(lambda x: x.visible, self.all()) + def visible(self): + return list(self.itervisible()) + def __iter__(self): """ Convenience API with identical functionality to :meth:`visible`. """ - return self.visible() + return self.itervisible() def __contains__(self, item): """ @@ -530,21 +550,19 @@ class BoundColumns(object): *item* can either be a :class:`.BoundColumn` object, or the name of a column. """ - self._spawn_columns() if isinstance(item, basestring): - for key in self._columns.keys(): - if item == key: - return True - return False + return item in self.iternames() else: - return item in self.all() + # let's assume we were given a column + return item in self.iterall() def __len__(self): """ - Return how many :class:`BoundColumn` objects are contained. + Return how many :class:`BoundColumn` objects are contained (and + visible). """ self._spawn_columns() - return len([1 for c in self._columns.values() if c.visible]) + return len(self.visible()) def __getitem__(self, index): """ diff --git a/django_tables/tables.py b/django_tables/tables.py index fc9fc5d..d9fe7ae 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -15,6 +15,34 @@ from .columns import BoundColumns, Column QUERYSET_ACCESSOR_SEPARATOR = '__' +def _validate_sequence(seq, column_names): + """ + Validates a sequence against a list of column names. It checks that "..." + is used at most once, either at the head or tail, and that if it's not + used, every column is specified. + + :raises: ValueError on error. + """ + if (seq.count("...") > 1 or (seq.count("...") == 1 and seq[0] != "..." + and seq[-1] != "...")): + raise ValueError("'...' must be used at most once in 'sequence', " + "either at the head or tail.") + + if "..." in seq: + extra = (set(seq) - set(("...", ))).difference(column_names) + if extra: + raise ValueError(u"'sequence' defined but names columns that do " + u"not exist in the table. Remove '%s'." + % "', '".join(extra)) + else: + diff = set(seq) ^ set(column_names) + if diff: + print 'seq', seq + print 'column_names', column_names + raise ValueError(u"'sequence' defined but does not match columns. " + u"Fix '%s' or possibly add '...' to head or tail." + % "', '".join(diff)) + class TableData(object): """ Exposes a consistent API for :term:`table data`. It currently supports a @@ -94,7 +122,7 @@ class DeclarativeColumnsMetaclass(type): def __new__(cls, name, bases, attrs): """Ughhh document this :)""" # extract declared columns - columns = [(name, attrs.pop(name)) for name, column in attrs.items() + columns = [(name_, attrs.pop(name_)) for name_, column in attrs.items() if isinstance(column, Column)] columns.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) @@ -119,6 +147,16 @@ class DeclarativeColumnsMetaclass(type): for ex in opts.exclude: if ex in attrs["base_columns"]: attrs["base_columns"].pop(ex) + if opts.sequence: + _validate_sequence(opts.sequence, attrs["base_columns"].keys()) + if "..." not in opts.sequence: + attrs["base_columns"] = SortedDict(((s, attrs["base_columns"][s]) for s in opts.sequence)) + elif opts.sequence[0] == "...": + for s in opts.sequence[1:]: + attrs["base_columns"][s] = attrs["base_columns"].pop(s) # append + elif opts.sequence[-1] == "...": + for s in opts.sequence[:-1]: + attrs["base_columns"].insert(0, s, attrs["base_columns"].pop(s)) return type.__new__(cls, name, bases, attrs) @@ -140,6 +178,7 @@ class TableOptions(object): if isinstance(order_by, basestring): order_by = (order_by, ) self.order_by = OrderByTuple(order_by) + self.sequence = getattr(options, "sequence", None) self.sortable = getattr(options, "sortable", True) @@ -184,17 +223,13 @@ class Table(StrAndUnicode): TableDataClass = TableData def __init__(self, data, order_by=None, sortable=None, empty_text=None, - exclude=None, attrs=None): + exclude=None, attrs=None, sequence=None): self._rows = BoundRows(self) self._columns = BoundColumns(self) self._data = self.TableDataClass(data=data, table=self) self.attrs = attrs self.empty_text = empty_text self.sortable = sortable - if order_by is None: - 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. @@ -203,6 +238,12 @@ class Table(StrAndUnicode): for ex in self.exclude: if ex in self.base_columns: self.base_columns.pop(ex) + self.sequence = sequence + if order_by is None: + self.order_by = self._meta.order_by + else: + self.order_by = order_by + def __unicode__(self): return self.as_html() @@ -236,6 +277,17 @@ class Table(StrAndUnicode): self._order_by = order_by self._data.order_by(order_by) + @property + def sequence(self): + return (self._sequence if self._sequence is not None + else self._meta.sequence) + + @sequence.setter + def sequence(self, value): + if value: + _validate_sequence(value, self.base_columns.keys()) + self._sequence = value + @property def sortable(self): return (self._sortable if self._sortable is not None diff --git a/docs/index.rst b/docs/index.rst index be8b607..afda7fb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -705,6 +705,9 @@ API Reference :meth:`~django_tables.tables.Table.as_html` or the :ref:`template-tags.render_table` template tag. + :type: ``dict`` + :default: ``{}`` + This is typically used to enable a theme for a table (which is done by adding a CSS class to the ```` element). i.e.:: @@ -714,10 +717,6 @@ API Reference class Meta: attrs = {"class": "paleblue"} - :type: ``dict`` - - Default: ``{}`` - .. note:: This functionality is also available via the ``attrs`` keyword @@ -727,6 +726,13 @@ API Reference Defines the text to display when the table has no rows. + :type: ``string`` + :default: ``None`` + + If the table is empty and ``bool(empty_text)`` is ``True``, a row is + displayed containing ``empty_text``. This is allows a message such as + *There are currently no FOO.* to be displayed. + .. note:: This functionality is also available via the ``empty_text`` keyword @@ -776,7 +782,8 @@ API Reference .. attribute:: sortable - The default value for determining if a :class:`.Column` is sortable. + Whether columns are by default sortable, or not. i.e. the fallback for + value for a column's sortable value. If the ``Table`` and ``Column`` don't specify a value, a column's ``sortable`` value will fallback to this. object specify. This provides diff --git a/tests/columns.py b/tests/columns.py index 939a588..f154586 100644 --- a/tests/columns.py +++ b/tests/columns.py @@ -4,6 +4,7 @@ from attest import Tests, Assert from django_attest import TransactionTestContext from django.test.client import RequestFactory from django.template import Context, Template +from django.core.exceptions import ImproperlyConfigured import django_tables as tables from django_tables import utils, A from .testapp.models import Person @@ -32,6 +33,53 @@ def sortable(): Assert(SimpleTable([]).columns['name'].sortable) is True +@general.test +def sequence(): + """ + Ensures that the sequence of columns is configurable. + """ + class TestTable(tables.Table): + a = tables.Column() + b = tables.Column() + c = tables.Column() + Assert(["a", "b", "c"]) == TestTable([]).columns.names() + Assert(["b", "a", "c"]) == TestTable([], sequence=("b", "a", "c")).columns.names() + + class TestTable2(TestTable): + class Meta: + sequence = ("b", "a", "c") + Assert(["b", "a", "c"]) == TestTable2([]).columns.names() + Assert(["a", "b", "c"]) == TestTable2([], sequence=("a", "b", "c")).columns.names() + + # BAD, all columns must be specified, or must use "..." + with Assert.raises(ValueError): + class TestTable3(TestTable): + class Meta: + sequence = ("a", ) + with Assert.raises(ValueError): + TestTable([], sequence=("a", )) + + # GOOD, using a single "..." allows you to only specify some columns. The + # remaining columns are ordered based on their definition order + class TestTable4(TestTable): + class Meta: + sequence = ("b", "...") + Assert(["b", "a", "c"]) == TestTable4([]).columns.names() + Assert(["b", "a", "c"]) == TestTable([], sequence=("b", "...")).columns.names() + + class TestTable5(TestTable): + class Meta: + sequence = ("...", "b") + Assert(["a", "c", "b"]) == TestTable5([]).columns.names() + Assert(["a", "c", "b"]) == TestTable([], sequence=("...", "b")).columns.names() + + class TestTable6(tables.Table): + class Meta: + sequence = ("...") + Assert(["a", "b", "c"]) == TestTable6([]).columns.names() + Assert(["a", "b", "c"]) == TestTable([], sequence=("...")).columns.names() + + linkcolumn = Tests() linkcolumn.context(TransactionTestContext()) -- 2.26.2