* Table's __unicode__ method no longer returns as_html()
authorBradley Ayers <bradley.ayers@gmail.com>
Tue, 7 Jun 2011 11:39:43 +0000 (21:39 +1000)
committerBradley Ayers <bradley.ayers@gmail.com>
Tue, 7 Jun 2011 11:41:27 +0000 (21:41 +1000)
* Added the ability to automatically generate columns based on a model via the Meta.model option (see docs). Resolves #2.
* Added some documentation describing how table mixins work.

django_tables/tables.py
docs/conf.py
docs/index.rst
example/settings.py
setup.py
tests/models.py

index 5f479dafa1e5c840263ae9294ea3aaa4e85fa44d..3112e7536864c22bb7f4bc650b018bab30a7535e 100644 (file)
@@ -142,7 +142,8 @@ class DeclarativeColumnsMetaclass(type):
     """
 
     def __new__(cls, name, bases, attrs):
-        """Ughhh document this :)"""
+
+        attrs["_meta"] = opts = TableOptions(attrs.get("Meta", None))
         # extract declared columns
         columns = [(name_, attrs.pop(name_)) for name_, column in attrs.items()
                                              if isinstance(column, Column)]
@@ -152,23 +153,23 @@ class DeclarativeColumnsMetaclass(type):
         # If this class is subclassing other tables, add their fields as
         # well. Note that we loop over the bases in *reverse* - this is
         # necessary to preserve the correct order of columns.
+        parent_columns = []
         for base in bases[::-1]:
             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(MyOldNonTable, tables.Table): pass
-        if not "base_columns" in attrs:
-            attrs["base_columns"] = SortedDict()
+                parent_columns = base.base_columns.items() + parent_columns
+        # Start with the parent columns
+        attrs["base_columns"] = SortedDict(parent_columns)
+        # Possibly add some generated columns based on a model
+        if opts.model:
+            extra = SortedDict(((f.name, Column()) for f in opts.model._meta.fields))
+            attrs["base_columns"].update(extra)
+        # Explicit columns override both parent and generated columns
         attrs["base_columns"].update(SortedDict(columns))
-        attrs["_meta"] = opts = TableOptions(attrs.get("Meta", None))
+        # Apply any explicit exclude setting
         for ex in opts.exclude:
             if ex in attrs["base_columns"]:
                 attrs["base_columns"].pop(ex)
+        # Now reorder the columns based on explicit sequence
         if opts.sequence:
             opts.sequence.expand(attrs["base_columns"].keys())
             attrs["base_columns"] = SortedDict(((x, attrs["base_columns"][x]) for x in opts.sequence))
@@ -195,6 +196,7 @@ class TableOptions(object):
         self.order_by = OrderByTuple(order_by)
         self.sequence = Sequence(getattr(options, "sequence", ()))
         self.sortable = getattr(options, "sortable", True)
+        self.model = getattr(options, "model", None)
 
 
 class Table(StrAndUnicode):
@@ -259,10 +261,6 @@ class Table(StrAndUnicode):
         else:
             self.order_by = order_by
 
-
-    def __unicode__(self):
-        return self.as_html()
-
     @property
     def data(self):
         return self._data
index 6cc411cd9283e1857771c1bcd695a0df7cea6391..c8cc665b151bf1c900536eaef142ed1140ed9ce6 100644 (file)
@@ -54,9 +54,9 @@ project = u'django-tables'
 # built documents.
 #
 # The short X.Y version.
-version = '0.5.0'
+version = '0.5.1'
 # The full version, including alpha/beta/rc tags.
-release = '0.5.0'
+release = '0.5.1'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
index c40d661dedd3843f0406697878718bfd0d548545..ca434285be84bf97204fc4d2c4ad46e16bf4b27f 100644 (file)
@@ -680,6 +680,114 @@ when one isn't explicitly defined.
     the future.
 
 
+Table Mixins
+============
+
+It's possible to create a mixin for a table that overrides something, however
+unless it itself is a subclass of :class:`.Table` class
+variable instances of :class:`.Column` will **not** be added to the class which is using the mixin.
+
+Example::
+
+    >>> class UselessMixin(object):
+    ...     extra = tables.Column()
+    ...
+    >>> class TestTable(UselessMixin, tables.Table):
+    ...     name = tables.Column()
+    ...
+    >>> TestTable.base_columns.keys()
+    ['name']
+
+To have a mixin contribute a column, it needs to be a subclass of
+:class:`~django_tables.tables.Table`. With this in mind the previous example
+*should* have been written as follows::
+
+    >>> class UsefulMixin(tables.Table):
+    ...     extra = tables.Column()
+    ...
+    >>> class TestTable(UsefulMixin, tables.Table):
+    ...     name = tables.Column()
+    ...
+    >>> TestTable.base_columns.keys()
+    ['extra', 'name']
+
+
+Tables for models
+=================
+
+Most of the time you'll probably be making tables to display queryset data, and
+writing such tables involves a lot of duplicate code, e.g.::
+
+    >>> class Person(models.Model):
+    ...     first_name = models.CharField(max_length=200)
+    ...     last_name = models.CharField(max_length=200)
+    ...     user = models.ForeignKey("auth.User")
+    ...     dob = models.DateField()
+    ...
+    >>> class PersonTable(tables.Table):
+    ...     first_name = tables.Column()
+    ...     last_name = tables.Column()
+    ...     user = tables.Column()
+    ...     dob = tables.Column()
+    ...
+
+Often a table will become quite complex after time, e.g. `table.render_foo`_,
+changing ``verbose_name`` on columns, or adding an extra
+:class:`~.CheckBoxColumn`.
+
+``django-tables`` offers the :attr:`.Table.Meta.model` option to ease the pain.
+The ``model`` option causes the table automatically generate columns for the
+fields in the model. This means that the above table could be re-written as
+follows::
+
+    >>> class PersonTable(tables.Table):
+    ...     class Meta:
+    ...         model = Person
+    ...
+    >>> PersonTable.base_columns.keys()
+    ['first_name', 'last_name', 'user', 'dob']
+
+If you want to customise one of the columns, simply define it the way you would
+normally::
+
+    >>> from django_tables import A
+    >>> class PersonTable(tables.Table):
+    ...     user = tables.LinkColumn("admin:auth_user_change", args=[A("user.pk")])
+    ...
+    ...     class Meta:
+    ...         model = Person
+    ...
+    >>> PersonTable.base_columns.keys()
+    ['first_name', 'last_name', 'dob', 'user']
+
+It's not immediately obvious but if you look carefully you'll notice that the
+order of the fields has now changed -- ``user`` is now last, rather than
+``dob``. This follows the same behaviour of Django's model forms, and can be
+fixed in a similar way -- the :attr:`.Table.Meta.sequence` option::
+
+    >>> class PersonTable(tables.Table):
+    ...     user = tables.LinkColumn("admin:auth_user_change", args=[A("user.pk")])
+    ...
+    ...     class Meta:
+    ...         model = Person
+    ...         sequence = ("first_name", "last_name", "user", "dob")
+    ...
+    >>> PersonTable.base_columns.keys()
+    ['first_name', 'last_name', 'user', 'dob']
+
+… or use a shorter approach that makes use of the special ``"..."`` item::
+
+    >>> class PersonTable(tables.Table):
+    ...     user = tables.LinkColumn("admin:auth_user_change", args=[A("user.pk")])
+    ...
+    ...     class Meta:
+    ...         model = Person
+    ...         sequence = ("...", "dob")
+    ...
+    >>> PersonTable.base_columns.keys()
+    ['first_name', 'last_name', 'user', 'dob']
+
+
 API Reference
 =============
 
@@ -773,6 +881,23 @@ API Reference
             This functionality is also available via the ``exclude`` keyword
             argument to a table's constructor.
 
+            However, unlike some of the other ``Meta`` options, providing the
+            ``exclude`` keyword to a table's constructor **won't override** the
+            ``Meta.exclude``. Instead, it will be effectively be *added*
+            to it. i.e. you can't use the constructor's ``exclude`` argument to
+            *undo* an exclusion.
+
+    .. attribute:: model
+
+        A model to inspect and automatically create corresponding columns.
+
+        :type: Django model
+        :default: ``None``
+
+        This option allows a Django model to be specified to cause the table to
+        automatically generate columns that correspond to the fields in a
+        model.
+
     .. attribute:: order_by
 
         The default ordering. e.g. ``('name', '-age')``. A hyphen ``-`` can be
@@ -813,7 +938,7 @@ API Reference
         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.
+        because neither ``"..."`` or ``"first_name"`` were included.
 
         .. note::
 
index 3a2a28adce4d513e44f16b6c019f03aa90618e4b..d8d8d9f414f00ab8cd7c5c5a49f2c1410a2def36 100644 (file)
@@ -2,15 +2,8 @@
 from os.path import dirname, join, abspath
 import sys
 
-
 ROOT = dirname(abspath(__file__))
 
-
-sys.path.insert(0, join(ROOT, '..'))
-import django_tables
-sys.path.pop(0)
-
-
 DEBUG = True
 TEMPLATE_DEBUG = DEBUG
 
index c4453b72158ae707a8c6d8cea81c725dd52d5e6a..68c1132a61aec8a7ece41ee57375c174188277b7 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='django-tables',
-    version='0.5.0',
+    version='0.5.1',
     description='Table framework for Django',
 
     author='Bradley Ayers',
index a6f349315fde5e5150a714b0705d59e4e86ae563..99fd940979c996e8c6474a4152b97c235f8883ef 100644 (file)
@@ -31,6 +31,49 @@ def boundrows_iteration():
         Assert(expected) == actual
 
 
+@models.test
+def model_table():
+    """
+    The ``model`` option on a table causes the table to dynamically add columns
+    based on the fields.
+    """
+    class OccupationTable(tables.Table):
+        class Meta:
+            model = Occupation
+    Assert(["id", "name", "region"]) == OccupationTable.base_columns.keys()
+
+    class OccupationTable2(tables.Table):
+        extra = tables.Column()
+        class Meta:
+            model = Occupation
+    Assert(["id", "name", "region", "extra"]) == OccupationTable2.base_columns.keys()
+
+    # be aware here, we already have *models* variable, but we're importing
+    # over the top
+    from django.db import models
+    class ComplexModel(models.Model):
+        char = models.CharField(max_length=200)
+        fk = models.ForeignKey("self")
+        m2m = models.ManyToManyField("self")
+
+    class ComplexTable(tables.Table):
+        class Meta:
+            model = ComplexModel
+    Assert(["id", "char", "fk"]) == ComplexTable.base_columns.keys()
+
+
+@models.test
+def mixins():
+    class TableMixin(tables.Table):
+        extra = tables.Column()
+
+    class OccupationTable(TableMixin, tables.Table):
+        extra2 = tables.Column()
+        class Meta:
+            model = Occupation
+    Assert(["extra", "id", "name", "region", "extra2"]) == OccupationTable.base_columns.keys()
+
+
 @models.test
 def verbose_name():
     """