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.
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):
…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
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
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`
+-----------------------+--------------------------+
-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
...
+.. _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:
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
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
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