From 37eda2e6617c9e47475a9e3c03387dec898a166b Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Tue, 7 Jun 2011 21:39:43 +1000 Subject: [PATCH] * Table's __unicode__ method no longer returns as_html() * 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 | 30 +++++----- docs/conf.py | 4 +- docs/index.rst | 127 +++++++++++++++++++++++++++++++++++++++- example/settings.py | 7 --- setup.py | 2 +- tests/models.py | 43 ++++++++++++++ 6 files changed, 186 insertions(+), 27 deletions(-) diff --git a/django_tables/tables.py b/django_tables/tables.py index 5f479da..3112e75 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -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 diff --git a/docs/conf.py b/docs/conf.py index 6cc411c..c8cc665 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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. diff --git a/docs/index.rst b/docs/index.rst index c40d661..ca43428 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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:: diff --git a/example/settings.py b/example/settings.py index 3a2a28a..d8d8d9f 100644 --- a/example/settings.py +++ b/example/settings.py @@ -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 diff --git a/setup.py b/setup.py index c4453b7..68c1132 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', + version='0.5.1', description='Table framework for Django', author='Bradley Ayers', diff --git a/tests/models.py b/tests/models.py index a6f3493..99fd940 100644 --- a/tests/models.py +++ b/tests/models.py @@ -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(): """ -- 2.26.2