From 4b18960c5bd3beec7cc3bf4b82147f8bb5329329 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Fri, 25 Feb 2011 03:07:40 +1000 Subject: [PATCH] 0.4.0 alpha 1 --- .gitignore | 1 + django_tables/__init__.py | 44 ++++-- django_tables/columns.py | 248 ++++++++++++++++++++++++------- django_tables/rows.py | 96 ++++++++++-- django_tables/tables.py | 6 +- docs/conf.py | 12 +- docs/index.rst | 300 +++++++++++++++----------------------- setup.py | 9 +- 8 files changed, 448 insertions(+), 268 deletions(-) diff --git a/.gitignore b/.gitignore index e1f1a7b..53b958f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ /*.komodoproject /MANIFEST /dist/ +/build/ /docs/_build/ /django_tables.egg-info/ diff --git a/django_tables/__init__.py b/django_tables/__init__.py index afe96dd..38774d1 100644 --- a/django_tables/__init__.py +++ b/django_tables/__init__.py @@ -1,25 +1,43 @@ -# -*- coding: utf-8 -*- -__version__ = (0, 2, 0, 'dev') +# -*- coding: utf8 -*- +# (major, minor, bugfix, "pre-alpha" | "alpha" | "beta" | "final", release | 0) +VERSION = (0, 4, 0, 'alpha', 1) def get_version(): - version = '%s.%s' % (__version__[0], __version__[1]) - if __version__[2]: - version = '%s.%s' % (version, __version__[2]) - if __version__[3] != '': - version = '%s %s' % (version, __version__[3]) + version = '%s.%s' % (VERSION[0], VERSION[1]) + if VERSION[2]: + version = '%s.%s' % (version, VERSION[2]) + if VERSION[3:] == ('alpha', 0): + version = '%s pre-alpha' % version + else: + if VERSION[3] != 'final': + version = '%s %s %s' % (version, VERSION[3], VERSION[4]) return version + # We want to make get_version() available to setup.py even if Django is not -# available or we are not inside a Django project (so we do distutils stuff). +# available or we are not inside a Django project. try: - # this fails if project settings module isn't configured - from django.contrib import admin + import django except ImportError: import warnings - warnings.warn('django-tables requires Django to be configured (settings) ' - 'prior to use, however this has not been done. Version information ' - 'will still be available.') + warnings.warn('django-tables requires Django, however it is not installed.' + ' Version information will still be available.') else: + try: + # http://docs.djangoproject.com/en/dev/topics/settings/ says:: + # + # If you don't set DJANGO_SETTINGS_MODULE and don't call configure(), + # Django will raise an ImportError exception the first time a setting is + # accessed. + # + from django.conf import settings + settings.DEBUG # will raise ImportError if Django isn't configured + except ImportError: + # allow get_version() to remain available + import warnings + warnings.warn('django-tables requires Django to be configured... but ' + "it isn't! A bunch of stuff won't work :(") + from tables import * from columns import * diff --git a/django_tables/columns.py b/django_tables/columns.py index 532f9b0..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 {} @@ -86,41 +164,72 @@ class CheckBoxColumn(Column): class BoundColumn(StrAndUnicode): - """'Runtime' version of ``Column`` that is bound to a table instance, - and thus knows about the table's data. The difference between BoundColumn - and Column, is a BoundColumn is aware of actual values (e.g. its name) - where-as Column is not. + """A *runtime* version of :class:`Column`. The difference between + :class:`BoundColumn` and :class:`Column`, is that :class:`BoundColumn` + objects are of the relationship between a :class:`Column` and a + :class:`Table`. This means that it knows the *name* given to the + :class:`Column`. + + For convenience, all :class:`Column` properties are available from this + class. - For convenience, all Column properties are available from this class. """ def __init__(self, table, column, name): - """*table* - the table in which this column exists - *column* - the column class - *name* – the variable name used when the column was defined in the - table class + """Initialise a :class:`BoundColumn` object where: + + * *table* - a :class:`Table` object in which this column exists + * *column* - a :class:`Column` object + * *name* – the variable name used when the column was added to the + :class:`Table` subclass + """ - self.table = table - self.column = column - self.name = name + self._table = table + self._column = column + self._name = name def __unicode__(self): s = self.column.verbose_name or self.name.replace('_', ' ') return capfirst(force_unicode(s)) + @property + def table(self): + """Returns the :class:`Table` object that this column is part of.""" + return self._table + + @property + def column(self): + """Returns the :class:`Column` object for this column.""" + return self._column + + @property + def name(self): + """Returns the string used to identify this column.""" + return self._name + @property def accessor(self): + """Returns the string used to access data for this column out of the + data source. + + """ return self.column.accessor or self.name @property def default(self): + """Returns the default value for this column.""" return self.column.default @property def formatter(self): + """Returns a function or ``None`` that represents the formatter for + this column. + + """ return self.column.formatter @property def sortable(self): + """Returns a ``bool`` depending on whether this column is sortable.""" if self.column.sortable is not None: return self.column.sortable elif self.table._meta.sortable is not None: @@ -130,10 +239,12 @@ class BoundColumn(StrAndUnicode): @property def verbose_name(self): + """Returns the verbose name for this column.""" return self.column.verbose_name @property def visible(self): + """Returns a ``bool`` depending on whether this column is visible.""" return self.column.visible @@ -144,8 +255,22 @@ class Columns(object): provides access to those columns in different ways (iterator, item-based, filtered and unfiltered etc), stuff that would not be possible with a simple iterator in the table class. + + A :class:`Columns` object is a container for holding :class:`BoundColumn` + objects. It provides methods that make accessing columns easier than if + they were stored in a ``list`` or ``dict``. :class:`Columns` has a similar + API to a ``dict`` (it actually uses a :class:`SortedDict` interally). + + At the moment you'll only come across this class when you access a + :attr:`Table.columns` property. + """ def __init__(self, table): + """Initialise a :class:`Columns` object. + + *table* must be a :class:`Table` object. + + """ self.table = table # ``self._columns`` attribute stores the bound columns (columns that # have a real name, ) @@ -164,52 +289,65 @@ class Columns(object): self._columns = new_columns def all(self): - """Iterate through all columns, regardless of visiblity (as - opposed to ``__iter__``. + """Iterate through all :class:`BoundColumn` objects, regardless of + visiblity or sortability. - This is used internally a lot. """ self._spawn_columns() for column in self._columns.values(): yield column def items(self): + """Return an iterator of ``(name, column)`` pairs (where *column* is a + :class:`BoundColumn` object). + + """ self._spawn_columns() for r in self._columns.items(): yield r def names(self): + """Return an iterator of column names.""" self._spawn_columns() for r in self._columns.keys(): yield r - def index(self, name): - self._spawn_columns() - return self._columns.keyOrder.index(name) - def sortable(self): - """Iterate through all sortable columns. + """Same as :meth:`all` but only returns sortable :class:`BoundColumn` + objects. - This is primarily useful in templates, where iterating over the full - set and checking {% if column.sortable %} can be problematic in - conjunction with e.g. {{ forloop.last }} (the last column might not + This is useful in templates, where iterating over the full + set and checking ``{% if column.sortable %}`` can be problematic in + conjunction with e.g. ``{{ forloop.last }}`` (the last column might not be the actual last that is rendered). + """ for column in self.all(): if column.sortable: yield column - def __iter__(self): - """Iterate through all *visible* bound columns. + def visible(self): + """Same as :meth:`sortable` but only returns visible + :class:`BoundColumn` objects. + + This is geared towards table rendering. - This is primarily geared towards table rendering. """ for column in self.all(): if column.visible: yield column + def __iter__(self): + """Convenience API with identical functionality to :meth:`visible`.""" + return self.visible() + def __contains__(self, item): - """Check by both column object and column name.""" + """Check if a column is contained within a :class:`Columns` object. + + *item* can either be a :class:`BoundColumn` object, or the name of a + column. + + """ self._spawn_columns() if isinstance(item, basestring): return item in self.names() @@ -217,11 +355,21 @@ class Columns(object): return item in self.all() def __len__(self): + """Return how many :class:`BoundColumn` objects are contained.""" self._spawn_columns() return len([1 for c in self._columns.values() if c.visible]) def __getitem__(self, index): - """Return a column by name or index.""" + """Retrieve a specific :class:`BoundColumn` object. + + *index* can either be 0-indexed or the name of a column + + .. code-block:: python + + columns['speed'] # returns a bound column with name 'speed' + columns[0] # returns the first column + + """ self._spawn_columns() if isinstance(index, int): return self._columns.value_for_index(index) diff --git a/django_tables/rows.py b/django_tables/rows.py index 9491023..9544bd3 100644 --- a/django_tables/rows.py +++ b/django_tables/rows.py @@ -1,16 +1,78 @@ +# -*- coding: utf-8 -*- + class BoundRow(object): - """Represents a single row of in a table. + """Represents a *specific* row in a table. + + :class:`BoundRow` objects expose rendered versions of raw table data. This + means that formatting (via :attr:`Column.formatter` or an overridden + :meth:`Column.render` method) is applied to the values from the table's + data. + + To access the rendered value of each cell in a row, just iterate over it: + + .. code-block:: python + + >>> import django_tables as tables + >>> class SimpleTable(tables.Table): + ... a = tables.Column() + ... b = tables.CheckBoxColumn(attrs={'name': 'my_chkbox'}) + ... + >>> table = SimpleTable([{'a': 1, 'b': 2}]) + >>> row = table.rows[0] # we only have one row, so let's use it + >>> for cell in row: + ... print cell + ... + 1 + + + Alternatively you can treat it like a list and use indexing to retrieve a + specific cell. It should be noted that this will raise an IndexError on + failure. + + .. code-block:: python + + >>> row[0] + 1 + >>> row[1] + u'' + >>> row[2] + ... + IndexError: list index out of range + + Finally you can also treat it like a dictionary and use column names as the + keys. This will raise KeyError on failure (unlike the above indexing using + integers). + + .. code-block:: python + + >>> row['a'] + 1 + >>> row['b'] + u'' + >>> row['c'] + ... + KeyError: 'c' - BoundRow provides a layer on top of the table data that exposes final - rendered cell values for the table. This means that formatting (via - Column.formatter or overridden Column.render in subclasses) applied to the - values from the table's data. """ def __init__(self, table, data): + """Initialise a new :class:`BoundRow` object where: + + * *table* is the :class:`Table` in which this row exists. + * *data* is a chunk of data that describes the information for this + row. A "chunk" of data might be a :class:`Model` object, a ``dict``, + or perhaps something else. + + """ self.table = table self.data = data def __iter__(self): + """Iterate over the rendered values for cells in the row. + + Under the hood this method just makes a call to :meth:`__getitem__` for + each cell. + + """ for value in self.values: yield value @@ -23,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.""" @@ -35,6 +99,8 @@ class BoundRow(object): @property def values(self): for column in self.table.columns: + # this uses __getitem__, using the name (rather than the accessor) + # is correct – it's what __getitem__ expects. yield self[column.name] @@ -46,29 +112,41 @@ class Rows(object): iterator in the table class. """ def __init__(self, table): + """Initialise a :class:`Rows` object. *table* is the :class:`Table` + object in which the rows exist. + + """ self.table = table def all(self): - """Return all rows.""" + """Return an iterable for all :class:`BoundRow` objects in the table. + + """ for row in self.table.data: yield BoundRow(self.table, row) def page(self): - """Return rows on current page (if paginated).""" + """If the table is paginated, return an iterable of :class:`BoundRow` + objects that appear on the current page, otherwise return None. + + """ if not hasattr(self.table, 'page'): return None return iter(self.table.page.object_list) def __iter__(self): - return iter(self.all()) + """Convience method for all()""" + return self.all() def __len__(self): + """Returns the number of rows in the table.""" return len(self.table.data) # for compatibility with QuerySetPaginator count = __len__ def __getitem__(self, key): + """Allows normal list slicing syntax to be used.""" if isinstance(key, slice): result = list() for row in self.table.data[key]: diff --git a/django_tables/tables.py b/django_tables/tables.py index db87cc2..df26d8f 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -1,14 +1,13 @@ # -*- coding: utf8 -*- import copy -from django.db.models.query import QuerySet from django.core.paginator import Paginator 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 .memory import sort_table from .rows import Rows, BoundRow from .columns import Columns @@ -21,6 +20,7 @@ class TableData(object): set and a list of dicts. """ def __init__(self, data, table): + from django.db.models.query import QuerySet self._data = data if not isinstance(data, QuerySet) else None self._queryset = data if isinstance(data, QuerySet) else None self._table = table @@ -175,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/conf.py b/docs/conf.py index 2463f23..9fcd66f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,9 @@ import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.join(os.path.abspath('.'), os.pardir)) +import django_tables as tables +sys.path.pop(0) # -- General configuration ----------------------------------------------------- @@ -25,7 +27,7 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -48,9 +50,9 @@ project = u'django-tables' # built documents. # # The short X.Y version. -version = '0.2' +version = '.'.join(map(str, tables.VERSION[0:2])) # The full version, including alpha/beta/rc tags. -release = '0.2-dev' +release = tables.get_version() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -153,7 +155,7 @@ html_static_path = ['_static'] #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +html_show_copyright = False # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the diff --git a/docs/index.rst b/docs/index.rst index 1e3d091..40cdf4a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,3 +1,5 @@ +.. default-domain:: py + ===================================================== django-tables - An app for creating HTML tables ===================================================== @@ -52,8 +54,6 @@ database model API: ... tz = tables.Column(verbose_name='Time Zone') ... visits = tables.Column() -See :ref:`columns` for more information. - Providing data -------------- @@ -110,7 +110,7 @@ template tag: {% load django_tables %} {% render_table table %} -See :ref:`template tags` for more information. +See :ref:`template_tags` for more information. Ordering @@ -166,36 +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}]) - >>> row = table.rows[0] - >>> for cell in row: + >>> 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 @@ -220,161 +244,69 @@ and render the template yourself: +Subclassing :class:`Column` +--------------------------- -Columns -======= - -The :class:`Columns` class provides an container for :class:`BoundColumn` -instances. The simplest way to access the contained columns is to iterate over -the instance: - -Each :class:`Table` instance has an instance as its :attr:`~Table.columns` -property. Iterating over the instance yields only the visible columns. To -access all columns (including those that are hidden), use the -:func:`~Columns.all` method. - -Additionally, the :func:`~Columns.sortable` method provides access to all the -sortable columns. - - -Column options --------------- - -Each column takes a certain set of column-specific arguments (documented in the -:ref:`column reference `). - -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 - - This would not have worked: - - .. 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. +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. - 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. - - :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. - - -Rows -==== - -Row objects ------------ - -A row object represents a single row in a table. - -To access the rendered value of each cell in a row, you can iterate over the -row: +To change the way cells are rendered, simply override the +:meth:`~Column.render` method. .. code-block:: python >>> import django_tables as tables - >>> class SimpleTable(tables.Table): - ... a = tables.Column() - ... b = tables.CheckBoxColumn(attrs={'name': 'my_chkbox'}) + >>> + >>> class AngryColumn(tables.Column): + ... def render(self, *args, **kwargs): + ... raw = super(AngryColumn, self).render(*args, **kwargs) + ... return raw.upper() ... - >>> table = SimpleTable([{'a': 1, 'b': 2}]) - >>> row = table.rows[0] # we only have one row, so let's use it - >>> for cell in row: - ... print cell + >>> 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?', + ... }] ... - 1 - + >>> 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' -Alternatively you can treat it like a list and use indexing to retrieve a -specific cell. It should be noted that this will raise an IndexError on -failure. +Which, when displayed in a browser, would look something like this: -.. code-block:: python ++-----------------------+--------------------------+ +| Normal | Angry | ++=======================+==========================+ +| May I have some food? | GIVE ME THE FOOD NOW! | ++-----------------------+--------------------------+ +| Hello! | WHAT ARE YOU LOOKING AT? | ++-----------------------+--------------------------+ - >>> row[0] - 1 - >>> row[1] - u'' - >>> row[2] - ... - IndexError: list index out of range -Finally you can also treat it like a dictionary and use column names as the -keys. This will raise KeyError on failure (unlike the above indexing using -integers). +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 - >>> row['a'] - 1 - >>> row['b'] - u'' - >>> row['c'] + >>> 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) ... - KeyError: 'c' +.. _template_tags: + Template tags ============= @@ -445,44 +377,42 @@ which can be iterated over: -Custom render methods ---------------------- +API Reference +============= -Often, displaying a raw value of a table cell is not good enough. For -example, if your table has a ``rating`` column, you might want to show -an image showing the given number of **stars**, rather than the plain -numeric value. +:class:`Column` Objects: +------------------------ -While you can always write your templates so that the column in question -is treated separately, either by conditionally checking for a column name, -or by explicitely rendering each column manually (as opposed to simply -looping over the ``rows`` and ``columns`` attributes), this is often -tedious to do. -Instead, you can opt to move certain formatting responsibilites into -your Python code: +.. autoclass:: django_tables.columns.Column + :members: __init__, default, render -.. code-block:: python - class BookTable(tables.ModelTable): - name = tables.Column() - rating = tables.Column(accessor='rating_int') - - def render_rating(self, bound_table): - if bound_table.rating_count == 0: - return '' - else: - return '' % bound_table.rating_int - -When accessing ``table.rows[i].rating``, the ``render_rating`` method -will be called. Note the following: - -- What is passed is underlying raw data object, in this case, the model - instance. This gives you access to data values that may not have been defined - as a column. -- For the method name, the public name of the column must be used, not the - internal field name. That is, it's ``render_rating``, not - ``render_rating_int``. -- The method is called whenever the cell value is retrieved by you, whether from - Python code or within templates. However, operations by ``django-tables``, - like sorting, always work with the raw data. +: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__ diff --git a/setup.py b/setup.py index 2dfeb42..ba5d8f7 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,8 @@ # -*- coding: utf8 -*- -from distutils.core import setup +try: + from setuptools import setup +except ImportError: + from distutils.core import setup from distutils.command.install_data import install_data from distutils.command.install import INSTALL_SCHEMES import os @@ -86,6 +89,6 @@ setup( packages = packages, data_files = data_files, cmdclass = cmdclasses, - requires = ['django(>=1.1)'], - install_requires = ['django>=1.1'] + requires = ['Django(>=1.1)'], + install_requires = ['Django>=1.1'] ) -- 2.26.2