From f7b6f1fe1b68d634a44eae55d3d8124a783d272c Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Sun, 22 May 2011 22:05:26 +1000 Subject: [PATCH] Column verbose_name now uses model field verbose_name if possible. Resolves issue #3 --- django_tables/columns.py | 36 +++++++++-- docs/index.rst | 135 +++++++++++++++++++++++++-------------- tests/models.py | 50 +++++++++++++++ tests/testapp/models.py | 13 +++- 4 files changed, 179 insertions(+), 55 deletions(-) diff --git a/django_tables/columns.py b/django_tables/columns.py index a899799..23734dc 100644 --- a/django_tables/columns.py +++ b/django_tables/columns.py @@ -299,10 +299,11 @@ class TemplateColumn(Column): class BoundColumn(object): - """A *runtime* version of :class:`.Column`. The difference between - ``BoundColumn`` and ``Column``, is that ``BoundColumn`` objects are of - the relationship between a ``Column`` and a :class:`.Table`. This - means that it knows the *name* given to the ``Column``. + """ + A *runtime* version of :class:`.Column`. The difference between + ``BoundColumn`` and ``Column``, is that ``BoundColumn`` objects are of the + relationship between a ``Column`` and a :class:`.Table`. This means that it + knows the *name* given to the ``Column``. For convenience, all :class:`.Column` properties are available from this class. @@ -395,9 +396,32 @@ class BoundColumn(object): Return the verbose name for this column, or fallback to prettified column name. + If the table is using queryset data, then use the corresponding + model field's ``verbose_name``. If it's traversing a relationship, + then get the last field in the accessor (i.e. stop when the + relationship turns from ORM relationships to object attributes [e.g. + person.upper should stop at person]). """ - return (self.column.verbose_name - or capfirst(force_unicode(self.name.replace('_', ' ')))) + # Favor an explicit verbose_name + if self.column.verbose_name: + return self.column.verbose_name + + # Reasonable fallback + name = self.name.replace('_', ' ') + + # Perhap use a model field's verbose_name + if hasattr(self.table.data, 'queryset'): + model = self.table.data.queryset.model + parts = self.accessor.split('.') + for part in parts: + field = model._meta.get_field(part) + if hasattr(field, 'rel') and hasattr(field.rel, 'to'): + model = field.rel.to + continue + break + if field: + name = field.verbose_name + return capfirst(name) @property def visible(self): diff --git a/docs/index.rst b/docs/index.rst index 7a471a6..3abe9a5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -80,15 +80,15 @@ In your template, the easiest way to :term:`render` the table is via the …which will render something like: -+--------------+------------+---------+--------+ -| Country Name | Population | Tz | Visit | -+==============+============+=========+========+ -| Australia | 21 | UTC +10 | 1 | -+--------------+------------+---------+--------+ -| Germany | 81 | UTC +1 | 2 | -+--------------+------------+---------+--------+ -| Mexico | 107 | UTC -6 | 0 | -+--------------+------------+---------+--------+ ++--------------+------------+-----------+--------+ +| Country Name | Population | Time Zone | Visit | ++==============+============+===========+========+ +| Australia | 21 | UTC +10 | 1 | ++--------------+------------+-----------+--------+ +| Germany | 81 | UTC +1 | 2 | ++--------------+------------+-----------+--------+ +| Mexico | 107 | UTC -6 | 0 | ++--------------+------------+-----------+--------+ This approach is easy, but it's not fully featured (e.g. no pagination, no sorting). Don't worry it's very easy to add these. First, you must render the @@ -273,6 +273,48 @@ To disable sorting for a specific table instance: table.sortable = False +.. _column-headers: + +Column headers +============== + +The header cell for each column comes from the column's +:meth:`~django_tables.columns.BoundColumn.header` method. By default this +method returns the column's ``verbose_name``, which is either explicitly +specified, or generated automatically based on the column name. + +When using queryset input data, rather than falling back to the column name if +a ``verbose_name`` has not been specified explicitly, the queryset model's +field ``verbose_name`` is used. + +Consider the following: + + >>> class Person(models.Model): + ... first_name = models.CharField(verbose_name='FIRST name', max_length=200) + ... last_name = models.CharField(max_length=200) + ... region = models.ForeignKey('Region') + ... + >>> class Region(models.Model): + ... name = models.CharField(max_length=200) + ... + >>> class PersonTable(tables.Table): + ... first_name = tables.Column() + ... ln = tables.Column(accessor='last_name') + ... region_name = tables.Column(accessor='region.name') + ... + >>> table = PersonTable(Person.objects.all()) + >>> table.columns['first_name'].verbose_name + u'FIRST name' + >>> table.columns['ln'].verbose_name + u'Last name' + >>> table.columns['region_name'].verbose_name + u'Name' + +As you can see in the last example, the results are not always desirable when +an accessor is used to cross relationships. To get around this be careful to +define a ``verbose_name`` on such columns. + + .. _pagination: Pagination @@ -379,42 +421,6 @@ arguments you're interested in, and the function will recieve them 31 -.. _custom-template: - -Custom Template ---------------- - -And of course if you want full control over the way the table is rendered, -ignore the built-in generation tools, and instead pass an instance of your -:class:`Table` subclass into your own template, and render it yourself: - -.. code-block:: django - - {% load django_tables %} - - - - {% for column in table.columns %} - - {% endfor %} - - - - {% for row in table.rows %} - - {% for cell in row %} - - {% endfor %} - - {% empty %} - {% if table.empty_text %} - - {% endif %} - {% endfor %} - -
{{ column }}
{{ cell }}
{{ table.empty_text }}
- - .. _subclassing-column: Subclassing :class:`Column` @@ -463,8 +469,8 @@ Which, when displayed in a browser, would look something like this: +-----------------------+--------------------------+ -If you plan on returning HTML from a :meth:`~Column.render` method, you must -remember to mark it as safe (otherwise it will be escaped when the table is +For complicated columns, it's sometimes necessary to return HTML from a :meth:`~Column.render` method, but the string +must be marked as safe (otherwise it will be escaped when the table is rendered). This can be achieved by using the :func:`mark_safe` function. .. code-block:: python @@ -477,6 +483,41 @@ rendered). This can be achieved by using the :func:`mark_safe` function. ... +.. _custom-template: + +Custom Template +--------------- + +And of course if you want full control over the way the table is rendered, +ignore the built-in generation tools, and instead pass an instance of your +:class:`Table` subclass into your own template, and render it yourself: + +.. code-block:: django + + {% load django_tables %} + + + + {% for column in table.columns %} + + {% endfor %} + + + + {% for row in table.rows %} + + {% for cell in row %} + + {% endfor %} + + {% empty %} + {% if table.empty_text %} + + {% endif %} + {% endfor %} + +
{{ column }}
{{ cell }}
{{ table.empty_text }}
+ .. _template_tags: diff --git a/tests/models.py b/tests/models.py index e68daab..aed6251 100644 --- a/tests/models.py +++ b/tests/models.py @@ -29,3 +29,53 @@ def boundrows_iteration(): expecteds = Person.objects.all() for expected, actual in itertools.izip(expecteds, records): Assert(expected) == actual + + +@models.test +def verbose_name(): + """ + When using queryset data as input for a table, default to using model field + verbose names rather than an autogenerated string based on the column name. + + However if a column does explicitly describe a verbose name, it should be + used. + """ + class PersonTable(tables.Table): + """ + The test_colX columns are to test that the accessor is used to + determine the field on the model, rather than the column name. + """ + first_name = tables.Column() + fn1 = tables.Column(accessor='first_name') + fn2 = tables.Column(accessor='first_name.upper') + fn3 = tables.Column(accessor='last_name', verbose_name='OVERRIDE') + last_name = tables.Column() + ln1 = tables.Column(accessor='last_name') + ln2 = tables.Column(accessor='last_name.upper') + ln3 = tables.Column(accessor='last_name', verbose_name='OVERRIDE') + region = tables.Column(accessor='occupation.region.name') + r1 = tables.Column(accessor='occupation.region.name') + r2 = tables.Column(accessor='occupation.region.name.upper') + r3 = tables.Column(accessor='occupation.region.name', verbose_name='OVERRIDE') + + # The Person model has a ``first_name`` and ``last_name`` field, but only + # the ``last_name`` field has an explicit ``verbose_name`` set. This means + # that we should expect that the two columns that use the ``last_name`` + # field should both use the model's ``last_name`` field's ``verbose_name``, + # however both fields that use the ``first_name`` field should just use a + # capitalized version of the column name as the column header. + table = PersonTable(Person.objects.all()) + # Should be generated (capitalized column name) + Assert('First name') == table.columns['first_name'].verbose_name + Assert('First name') == table.columns['fn1'].verbose_name + Assert('First name') == table.columns['fn2'].verbose_name + Assert('OVERRIDE') == table.columns['fn3'].verbose_name + # Should use the model field's verbose_name + Assert('Surname') == table.columns['last_name'].verbose_name + Assert('Surname') == table.columns['ln1'].verbose_name + Assert('Surname') == table.columns['ln2'].verbose_name + Assert('OVERRIDE') == table.columns['ln3'].verbose_name + Assert('Name') == table.columns['region'].verbose_name + Assert('Name') == table.columns['r1'].verbose_name + Assert('Name') == table.columns['r2'].verbose_name + Assert('OVERRIDE') == table.columns['r3'].verbose_name diff --git a/tests/testapp/models.py b/tests/testapp/models.py index 61b0d95..700a482 100644 --- a/tests/testapp/models.py +++ b/tests/testapp/models.py @@ -3,8 +3,9 @@ from django.db import models class Person(models.Model): first_name = models.CharField(max_length=200) - last_name = models.CharField(max_length=200) - occupation = models.ForeignKey('Occupation', related_name='people', null=True) + last_name = models.CharField(max_length=200, verbose_name='Surname') + occupation = models.ForeignKey('Occupation', related_name='people', + null=True, verbose_name='Occupation') def __unicode__(self): return self.first_name @@ -12,6 +13,14 @@ class Person(models.Model): class Occupation(models.Model): name = models.CharField(max_length=200) + region = models.ForeignKey('Region', null=True) + + def __unicode__(self): + return self.name + + +class Region(models.Model): + name = models.CharField(max_length=200) def __unicode__(self): return self.name -- 2.26.2