Column verbose_name now uses model field verbose_name if possible. Resolves issue #3
authorBradley Ayers <bradley.ayers@gmail.com>
Sun, 22 May 2011 12:05:26 +0000 (22:05 +1000)
committerBradley Ayers <bradley.ayers@gmail.com>
Sun, 22 May 2011 12:05:26 +0000 (22:05 +1000)
django_tables/columns.py
docs/index.rst
tests/models.py
tests/testapp/models.py

index a899799740d45e899ba74bc5e3e68483f2513565..23734dc57a42454c43190c72b46a558e948c378b 100644 (file)
@@ -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):
index 7a471a6bdcf6a729354d3c63ce6fcc72a6645b20..3abe9a5402e8c1d397911a635632c4761991f923 100644 (file)
@@ -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 %}
-    <table>
-        <thead>
-            <tr>
-            {% for column in table.columns %}
-                <th><a href="{% set_url_param sort=column.name_toggled %}">{{ column }}</a></th>
-            {% endfor %}
-            </tr>
-        </thead>
-        <tbody>
-            {% for row in table.rows %}
-            <tr>
-                {% for cell in row %}
-                <td>{{ cell }}</td>
-                {% endfor %}
-            </tr>
-            {% empty %}
-                {% if table.empty_text %}
-                <tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
-                {% endif %}
-            {% endfor %}
-        </tbody>
-    </table>
-
-
 .. _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 %}
+    <table>
+        <thead>
+            <tr>
+            {% for column in table.columns %}
+                <th><a href="{% set_url_param sort=column.name_toggled %}">{{ column }}</a></th>
+            {% endfor %}
+            </tr>
+        </thead>
+        <tbody>
+            {% for row in table.rows %}
+            <tr>
+                {% for cell in row %}
+                <td>{{ cell }}</td>
+                {% endfor %}
+            </tr>
+            {% empty %}
+                {% if table.empty_text %}
+                <tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
+                {% endif %}
+            {% endfor %}
+        </tbody>
+    </table>
+
 
 .. _template_tags:
 
index e68daabc5f633c066ba40fb06ddb8471e0fc53da..aed6251bd680be8820642e4791f1393f8c27681f 100644 (file)
@@ -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
index 61b0d95f24df70b2927f4c9b0343469eee158cb4..700a482bf8998bc716d642c77802330e4ea39792 100644 (file)
@@ -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