From b26c1fc04093ccf1ede246803715fd57e8db41fb Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Fri, 25 Feb 2011 02:54:02 +1000 Subject: [PATCH] fixed up a lot of shitty documentation, and fixed some bugs while I was at it. --- django_tables/__init__.py | 2 +- django_tables/columns.py | 122 ++++++++++++++++---- django_tables/rows.py | 4 +- django_tables/tables.py | 3 +- docs/index.rst | 232 ++++++++++++++++++++------------------ 5 files changed, 228 insertions(+), 135 deletions(-) diff --git a/django_tables/__init__.py b/django_tables/__init__.py index 7e93bcc..0624020 100644 --- a/django_tables/__init__.py +++ b/django_tables/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf8 -*- # (major, minor, bugfix, "pre-alpha" | "alpha" | "beta" | "final", release | 0) -VERSION = (0, 2, 0, 'alpha', 0) +VERSION = (0, 2, 0, 'alpha', 1) def get_version(): diff --git a/django_tables/columns.py b/django_tables/columns.py index e721b0b..d358262 100644 --- a/django_tables/columns.py +++ b/django_tables/columns.py @@ -7,27 +7,99 @@ from django.utils.text import capfirst class Column(object): """Represents a single column of a table. - ``verbose_name`` defines a display name for this column used for output. + :class:`Column` objects control the way a column (including the cells that + fall within it) are rendered. - You can use ``visible`` to flag the column as hidden by default. - However, this can be overridden by the ``visibility`` argument to the - table constructor. If you want to make the column completely unavailable - to the user, set ``inaccessible`` to True. - - Setting ``sortable`` to False will result in this column being unusable - in ordering. You can further change the *default* sort direction to - descending using ``direction``. Note that this option changes the actual - direction only indirectly. Normal und reverse order, the terms - django-tables exposes, now simply mean different things. - - Data can be formatted by using ``formatter``, which accepts a callable as - an argument (e.g. lambda x: x.upper()) """ - # Tracks each time a Column instance is created. Used to retain order. + #: Tracks each time a Column instance is created. Used to retain order. creation_counter = 0 def __init__(self, verbose_name=None, accessor=None, default=None, visible=True, sortable=None, formatter=None): + """Initialise a :class:`Column` object. + + :param verbose_name: + A pretty human readable version of the column name. Typically this + is used in the header cells in the HTML output. + + :param accessor: + A string or callable that specifies the attribute to access when + retrieving the value for a cell in this column from the data-set. + Multiple lookups can be achieved by providing a dot separated list + of lookups, e.g. ``"user.first_name"``. The functionality is + identical to that of Django's template variable syntax, e.g. ``{{ + user.first_name }}`` + + A callable should be used if the dot separated syntax is not + capable of describing the lookup properly. The callable will be + passed a single item from the data (if the table is using + :class:`QuerySet` data, this would be a :class:`Model` instance), + and is expected to return the correct value for the column. + + Consider the following: + + .. code-block:: python + + >>> import django_tables as tables + >>> data = [ + ... {'dot.separated.key': 1}, + ... {'dot.separated.key': 2}, + ... ] + ... + >>> class SlightlyComplexTable(tables.Table): + >>> dot_seperated_key = tables.Column(accessor=lambda x: x['dot.separated.key']) + ... + >>> table = SlightlyComplexTable(data) + >>> for row in table.rows: + >>> print row['dot_seperated_key'] + ... + 1 + 2 + + This would **not** have worked: + + .. code-block:: python + + dot_seperated_key = tables.Column(accessor='dot.separated.key') + + :param default: + The default value for the column. This can be a value or a callable + object [1]_. If an object in the data provides :const:`None` for a + column, the default will be used instead. + + The default value may affect ordering, depending on the type of + data the table is using. The only case where ordering is not + affected ing when a :class:`QuerySet` is used as the table data + (since sorting is performed by the database). + + .. [1] The provided callable object must not expect to receive any + arguments. + + :param visible: + If :const:`False`, this column will not be in HTML from output + generators (e.g. :meth:`as_html` or ``{% render_table %}``). + + When a field is not visible, it is removed from the table's + :attr:`~Column.columns` iterable. + + :param sortable: + If :const:`False`, this column will not be allowed to be used in + ordering the table. + + :param formatter: + A callable object that is used as a final step in formatting the + value for a cell. The callable will be passed the string that would + have otherwise been displayed in the cell. + + In the following table, cells in the *name* column have upper-case + values. + + .. code-block:: python + + class Example(tables.Table): + name = tables.Column(formatter=lambda x: x.upper()) + + """ if not (accessor is None or isinstance(accessor, basestring) or callable(accessor)): raise TypeError('accessor must be a string or callable, not %s' % @@ -47,26 +119,32 @@ class Column(object): @property def default(self): - """Since ``Column.default`` property may be a callable, this function - handles access. + """The default value for cells in this column. + + The default value passed into ``Column.default`` property may be a + callable, this function handles access. + """ return self._default() if callable(self._default) else self._default def render(self, table, bound_column, bound_row): """Returns a cell's content. This method can be overridden by ``render_FOO`` methods on the table or - by subclassing ``Column``. + by subclassing :class:`Column`. + """ return table.data.data_for_cell(bound_column=bound_column, bound_row=bound_row) class CheckBoxColumn(Column): - """A subclass of Column that renders its column data as a checkbox - - ``name`` is the html name of the checkbox. - """ + """A subclass of Column that renders its column data as a checkbox""" def __init__(self, attrs=None, *args, **kwargs): + """ + :param attrs: a dict of HTML element attributes to be added to the + ```` + + """ super(CheckBoxColumn, self).__init__(*args, **kwargs) self.attrs = attrs or {} diff --git a/django_tables/rows.py b/django_tables/rows.py index 1883baa..9544bd3 100644 --- a/django_tables/rows.py +++ b/django_tables/rows.py @@ -85,7 +85,9 @@ class BoundRow(object): custom = getattr(self.table, 'render_%s' % name, None) if custom: return custom(bound_column, self) - return bound_column.column.render(self.table, bound_column, self) + return bound_column.column.render(table=self.table, + bound_column=bound_column, + bound_row=self) def __contains__(self, item): """Check by both row object and column name.""" diff --git a/django_tables/tables.py b/django_tables/tables.py index aabc1a7..df26d8f 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -5,6 +5,7 @@ from django.utils.datastructures import SortedDict from django.http import Http404 from django.template.loader import get_template from django.template import Context +from django.utils.encoding import StrAndUnicode from .utils import rmprefix, toggleprefix, OrderByTuple, Accessor from .columns import Column from .rows import Rows, BoundRow @@ -174,7 +175,7 @@ class TableOptions(object): self.order_by = getattr(options, 'order_by', ()) -class Table(object): +class Table(StrAndUnicode): """A collection of columns, plus their associated data rows.""" __metaclass__ = DeclarativeColumnsMetaclass diff --git a/docs/index.rst b/docs/index.rst index 735f57f..40cdf4a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -166,35 +166,60 @@ run-time information of the table into the formatter. For example it would not be possible to incorporate the row number into the cell's value. -Column render method --------------------- +:meth:`Table.render_FOO` Method +------------------------------- This approach provides a lot of control, but is only suitable if you intend to customise the rendering for a single table (otherwise you'll end up having to copy & paste the method to every table you want to modify – which violates DRY). +The example below has a number of different techniques in use: + +* :meth:`Column.render` (accessible via :attr:`BoundColumn.column`) applies the + *formatter* function if it's been provided. This is evident in the order that + the square and angled brackets have been applied for the ``id`` column. +* Completely abitrary values can be returned by :meth:`render_FOO` methods, as + shown in :meth:`~SimpleTable.render_row_number` (a :attr:`_counter` attribute + is added to the :class:`SimpleTable` object to keep track of the row number). + + This is possible because :meth:`render_FOO` methods override the default + behaviour of retrieving a value from the data-source. + +.. code-block:: python + >>> import django_tables as tables >>> class SimpleTable(tables.Table): ... row_number = tables.Column() - ... id = tables.Column(formatter=lambda x: '#%d' % x) + ... id = tables.Column(formatter=lambda x: '[%s]' % x) ... age = tables.Column(formatter=lambda x: '%d years old' % x) ... ... def render_row_number(self, bound_column, bound_row): - ... value = + ... value = getattr(self, '_counter', 0) + ... self._counter = value + 1 + ... return 'Row %d' % value ... ... def render_id(self, bound_column, bound_row): - ... value = self.column. + ... value = bound_column.column.render(table=self, + ... bound_column=bound_column, + ... bound_row=bound_row) + ... return '<%s>' % value ... >>> table = SimpleTable([{'age': 31, 'id': 10}, {'age': 34, 'id': 11}]) >>> for cell in table.rows[0]: ... print cell ... - #10 + Row 0 + <[10]> 31 years old -If you want full control over the way the table is rendered, create -and render the template yourself: + +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 @@ -219,119 +244,65 @@ and render the template yourself: +Subclassing :class:`Column` +--------------------------- -:class:`Columns` Objects -======================== - -.. autoclass:: django_tables.columns.Columns - :members: __init__, all, items, names, sortable, visible, __iter__, - __contains__, __len__, __getitem__ - - -:class:`BoundColumn` Objects -============================ - -.. autoclass:: django_tables.columns.BoundColumn - :members: __init__, table, column, name, accessor, default, formatter, - sortable, verbose_name, visible - - -Column options --------------- - -Each column takes a certain set of column-specific arguments. - -There's also a set of common arguments available to all column types. All are -optional. Here's a summary of them. - - :attr:`~Column.verbose_name` - A pretty human readable version of the column name. Typically this is - used in the header cells in the HTML output. - - :attr:`~Column.accessor` - A string or callable that specifies the attribute to access when - retrieving the value for a cell in this column from the data-set. - Multiple lookups can be achieved by providing a dot separated list of - lookups, e.g. ``"user.first_name"``. The functionality is identical to - that of Django's template variable syntax, e.g. ``{{ user.first_name - }}`` - - A callable should be used if the dot separated syntax is not capable of - describing the lookup properly. The callable will be passed a single - item from the data (if the table is using :class:`QuerySet` data, this - would be a :class:`Model` instance), and is expected to return the - correct value for the column. - - Consider the following: - - .. code-block:: python - - >>> import django_tables as tables - >>> data = [ - ... {'dot.separated.key': 1}, - ... {'dot.separated.key': 2}, - ... ] - ... - >>> class SlightlyComplexTable(tables.Table): - >>> dot_seperated_key = tables.Column(accessor=lambda x: x['dot.separated.key']) - ... - >>> table = SlightlyComplexTable(data) - >>> for row in table.rows: - >>> print row['dot_seperated_key'] - ... - 1 - 2 +If you want to have a column behave the same way in many tables, it's best to +create a subclass of :class:`Column` and use that when defining the table. - This would not have worked: +To change the way cells are rendered, simply override the +:meth:`~Column.render` method. - .. code-block:: python - - dot_seperated_key = tables.Column(accessor='dot.separated.key') - - :attr:`~Column.default` - The default value for the column. This can be a value or a callable - object [1]_. If an object in the data provides :const:`None` for a - column, the default will be used instead. - - The default value may affect ordering, depending on the type of - data the table is using. The only case where ordering is not affected - ing when a :class:`QuerySet` is used as the table data (since sorting - is performed by the database). - - .. [1] The provided callable object must not expect to receive any - arguments. - - :attr:`~Column.visible` - If :const:`False`, this column will not be in the HTML output. - - When a field is not visible, it is removed from the table's - :attr:`~Column.columns` iterable. - - :attr:`~Column.sortable` - If :const:`False`, this column will not be allowed to be used in - ordering the table. +.. code-block:: python - :attr:`~Column.formatter` - A callable object that is used as a final step in formatting the value - for a cell. The callable will be passed the string that would have - otherwise been displayed in the cell. + >>> import django_tables as tables + >>> + >>> class AngryColumn(tables.Column): + ... def render(self, *args, **kwargs): + ... raw = super(AngryColumn, self).render(*args, **kwargs) + ... return raw.upper() + ... + >>> class Example(tables.Table): + ... normal = tables.Column() + ... angry = AngryColumn() + ... + >>> data = [{ + ... 'normal': 'May I have some food?', + ... 'angry': 'Give me the food now!', + ... }, { + ... 'normal': 'Hello!', + ... 'angry': 'What are you looking at?', + ... }] + ... + >>> table = Example(data) + >>> table.as_html() + u'
NormalAngry
May I have some food?GIVE ME THE FOOD NOW!
Hello!WHAT ARE YOU LOOKING AT?
\n' +Which, when displayed in a browser, would look something like this: -Rows -==== ++-----------------------+--------------------------+ +| Normal | Angry | ++=======================+==========================+ +| May I have some food? | GIVE ME THE FOOD NOW! | ++-----------------------+--------------------------+ +| Hello! | WHAT ARE YOU LOOKING AT? | ++-----------------------+--------------------------+ -:class:`Rows` Objects -===================== -.. autoclass:: django_tables.rows.Rows - :members: __init__, all, page, __iter__, __len__, count, __getitem__ +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 +rendered). This can be achieved by using the :func:`mark_safe` function. +.. code-block:: python -:class:`BoundRow` Objects -========================= + >>> from django.utils.safestring import mark_safe + >>> + >>> class ImageColumn(tables.Column): + ... def render(self, **kwargs): + ... raw = super(AngryColumn, self).render(**kwargs) + ... return mark_safe('' % raw) + ... -.. autoclass:: django_tables.rows.BoundRow - :members: __init__, values, __getitem__, __contains__, __iter__ .. _template_tags: @@ -404,3 +375,44 @@ which can be iterated over: {% endfor %} + + +API Reference +============= + +:class:`Column` Objects: +------------------------ + + +.. autoclass:: django_tables.columns.Column + :members: __init__, default, render + + +:class:`Columns` Objects +------------------------ + +.. autoclass:: django_tables.columns.Columns + :members: __init__, all, items, names, sortable, visible, __iter__, + __contains__, __len__, __getitem__ + + +:class:`BoundColumn` Objects +---------------------------- + +.. autoclass:: django_tables.columns.BoundColumn + :members: __init__, table, column, name, accessor, default, formatter, + sortable, verbose_name, visible + + +:class:`Rows` Objects +--------------------- + +.. autoclass:: django_tables.rows.Rows + :members: __init__, all, page, __iter__, __len__, count, __getitem__ + + +:class:`BoundRow` Objects +------------------------- + +.. autoclass:: django_tables.rows.BoundRow + :members: __init__, values, __getitem__, __contains__, __iter__ -- 2.26.2