From 22fdf66d21733a9a2747e831bb0e2c97ed356c97 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Tue, 7 Jun 2011 12:53:53 +1000 Subject: [PATCH] * Bumped version to 0.5.0 * Added the ability to explicitly specify the sequence of columns. This resolves #11 --- django_tables/tables.py | 90 ++++++++++++++++++++++++----------------- docs/conf.py | 2 +- docs/index.rst | 69 ++++++++++++++++++++++++------- setup.py | 2 +- tests/columns.py | 37 +++++++++++++---- 5 files changed, 139 insertions(+), 61 deletions(-) diff --git a/django_tables/tables.py b/django_tables/tables.py index d9fe7ae..5f479da 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -7,6 +7,7 @@ from django.template.loader import get_template from django.template import Context from django.utils.encoding import StrAndUnicode from django.db.models.query import QuerySet +from itertools import chain from .utils import OrderBy, OrderByTuple, Accessor, AttributeDict from .rows import BoundRows, BoundRow from .columns import BoundColumns, Column @@ -15,33 +16,54 @@ from .columns import BoundColumns, Column QUERYSET_ACCESSOR_SEPARATOR = '__' -def _validate_sequence(seq, column_names): +class Sequence(list): """ - 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. + Represents a column sequence, e.g. ("first_name", "...", "last_name") - :raises: ValueError on error. + This is used to represent ``Table.Meta.sequence`` or the Table + constructors's ``sequence`` keyword argument. + + The sequence must be a list of column names and is used to specify the + order of the columns on a table. Optionally a "..." item can be inserted, + which is treated as a *catch-all* for column names that aren't explicitly + specified. """ - 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)) + def expand(self, columns): + """ + Expands the "..." item in the sequence into the appropriate column + names that should be placed there. + + :raises: ``ValueError`` if the sequence is invalid for the columns. + """ + # validation + if self.count("...") > 1: + raise ValueError("'...' must be used at most once in a sequence.") + elif "..." in self: + # Check for columns in the sequence that don't exist in *columns* + extra = (set(self) - set(("...", ))).difference(columns) + if extra: + raise ValueError(u"sequence contains columns that do not exist" + u" in the table. Remove '%s'." + % "', '".join(extra)) + else: + diff = set(self) ^ set(columns) + if diff: + raise ValueError(u"sequence does not match columns. Fix '%s' " + u"or possibly add '...'." % "', '".join(diff)) + # everything looks good, let's expand the "..." item + columns = columns[:] # don't modify + head = [] + tail = [] + target = head # start by adding things to the head + for name in self: + if name == "...": + # now we'll start adding elements to the tail + target = tail + continue + else: + target.append(columns.pop(columns.index(name))) + self[:] = list(chain(head, columns, tail)) + class TableData(object): """ @@ -123,7 +145,7 @@ class DeclarativeColumnsMetaclass(type): """Ughhh document this :)""" # extract declared columns columns = [(name_, attrs.pop(name_)) for name_, column in attrs.items() - if isinstance(column, Column)] + if isinstance(column, Column)] columns.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) @@ -148,15 +170,8 @@ class DeclarativeColumnsMetaclass(type): 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)) + opts.sequence.expand(attrs["base_columns"].keys()) + attrs["base_columns"] = SortedDict(((x, attrs["base_columns"][x]) for x in opts.sequence)) return type.__new__(cls, name, bases, attrs) @@ -178,7 +193,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.sequence = Sequence(getattr(options, "sequence", ())) self.sortable = getattr(options, "sortable", True) @@ -233,7 +248,7 @@ class Table(StrAndUnicode): # 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.base_columns = copy.deepcopy(self.__class__.base_columns) self.exclude = exclude or () for ex in self.exclude: if ex in self.base_columns: @@ -285,7 +300,8 @@ class Table(StrAndUnicode): @sequence.setter def sequence(self, value): if value: - _validate_sequence(value, self.base_columns.keys()) + value = Sequence(value) + value.expand(self.base_columns.keys()) self._sequence = value @property diff --git a/docs/conf.py b/docs/conf.py index 23ee16d..6cc411c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,7 +56,7 @@ project = u'django-tables' # The short X.Y version. version = '0.5.0' # The full version, including alpha/beta/rc tags. -release = '0.5.0.dev' +release = '0.5.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index afda7fb..c40d661 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -175,9 +175,15 @@ Any iterable can be used as table data, and there's builtin support for Ordering ======== -Changing the way a table is ordered is easy and can be controlled via the -:attr:`.Table.Meta.order_by` option. The following examples all achieve the -same thing: +.. note:: + + If you want to change the order in which columns are displayed, see + :attr:`Table.Meta.sequence`. Alternatively if you're interested in the + order of records within the table, read on. + +Changing the way records in a table are ordered is easy and can be controlled +via the :attr:`.Table.Meta.order_by` option. The following examples all achieve +the same thing: .. code-block:: python @@ -741,7 +747,12 @@ API Reference .. attribute:: exclude Defines which columns should be excluded from the table. This is useful - in subclasses to exclude columns in a parent. e.g. + in subclasses to exclude columns in a parent. + + :type: tuple of ``string`` objects + :default: ``()`` + + Example:: >>> class Person(tables.Table): ... first_name = tables.Column() @@ -757,10 +768,6 @@ API Reference >>> ForgetfulPerson.base_columns {'first_name': } - :type: tuple of ``string`` objects - - Default: ``()`` - .. note:: This functionality is also available via the ``exclude`` keyword @@ -771,29 +778,61 @@ API Reference 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: ``()`` + :type: ``tuple`` + :default: ``()`` .. note:: This functionality is also available via the ``order_by`` keyword argument to a table's constructor. + .. attribute:: sequence + + The sequence of the table columns. This allows the default order of + columns (the order they were defined in the Table) to be overridden. + + :type: any iterable (e.g. ``tuple`` or ``list``) + :default: ``()`` + + The special item ``"..."`` can be used as a placeholder that will be + replaced with all the columns that weren't explicitly listed. This + allows you to add columns to the front or back when using inheritence. + + Example:: + + >>> class Person(tables.Table): + ... first_name = tables.Column() + ... last_name = tables.Column() + ... + ... class Meta: + ... sequence = ("last_name", "...") + ... + >>> Person.base_columns.keys() + ['last_name', 'first_name'] + + The ``"..."`` item can be used at most once in the sequence value. If + it's not used, every column *must* be explicitly included. e.g. in the + above example, ``sequence = ("last_name", )`` would be **invalid** + because neither ``"..."`` or ``"first_name"`` where included. + + .. note:: + + This functionality is also available via the ``sequence`` keyword + argument to a table's constructor. + .. attribute:: sortable Whether columns are by default sortable, or not. i.e. the fallback for value for a column's sortable value. + :type: ``bool`` + :default: ``True`` + If the ``Table`` and ``Column`` don't specify a value, a column's ``sortable`` value will fallback to this. object specify. This provides an easy mechanism to disable sorting on an entire table, without adding ``sortable=False`` to each ``Column`` in a ``Table``. - :type: :class:`bool` - - Default: :const:`True` - .. note:: This functionality is also available via the ``sortable`` keyword diff --git a/setup.py b/setup.py index ddfd3ac..c4453b7 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages setup( name='django-tables', - version='0.5.0.dev', + version='0.5.0', description='Table framework for Django', author='Bradley Ayers', diff --git a/tests/columns.py b/tests/columns.py index f154586..2af4142 100644 --- a/tests/columns.py +++ b/tests/columns.py @@ -62,22 +62,45 @@ def sequence(): # 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 = ("...", ) + Assert(["a", "b", "c"]) == TestTable4([]).columns.names() + Assert(["a", "b", "c"]) == TestTable([], sequence=("...", )).columns.names() + + class TestTable5(TestTable): class Meta: sequence = ("b", "...") - Assert(["b", "a", "c"]) == TestTable4([]).columns.names() + Assert(["b", "a", "c"]) == TestTable5([]).columns.names() Assert(["b", "a", "c"]) == TestTable([], sequence=("b", "...")).columns.names() - class TestTable5(TestTable): + class TestTable6(TestTable): class Meta: sequence = ("...", "b") - Assert(["a", "c", "b"]) == TestTable5([]).columns.names() + Assert(["a", "c", "b"]) == TestTable6([]).columns.names() Assert(["a", "c", "b"]) == TestTable([], sequence=("...", "b")).columns.names() - class TestTable6(tables.Table): + class TestTable7(TestTable): class Meta: - sequence = ("...") - Assert(["a", "b", "c"]) == TestTable6([]).columns.names() - Assert(["a", "b", "c"]) == TestTable([], sequence=("...")).columns.names() + sequence = ("b", "...", "a") + Assert(["b", "c", "a"]) == TestTable7([]).columns.names() + Assert(["b", "c", "a"]) == TestTable([], sequence=("b", "...", "a")).columns.names() + + # Let's test inheritence + class TestTable8(TestTable): + d = tables.Column() + e = tables.Column() + f = tables.Column() + + class Meta: + sequence = ("d", "...") + + class TestTable9(TestTable): + d = tables.Column() + e = tables.Column() + f = tables.Column() + + Assert(["d", "a", "b", "c", "e", "f"]) == TestTable8([]).columns.names() + Assert(["d", "a", "b", "c", "e", "f"]) == TestTable9([], sequence=("d", "...")).columns.names() linkcolumn = Tests() -- 2.26.2