from django.utils.datastructures import SortedDict
from django.utils.text import capfirst
from django.utils.safestring import mark_safe
-from django.template import Context, Template
+from django.template import RequestContext, Context, Template
from .utils import OrderBy, A, AttributeDict
used as the value for the checkbox, i.e. ``<input type="checkbox"
value="..." />``
- By default this column is not sortable.
+ This class implements some sensible defaults:
+
+ - The ``name`` attribute of the input is the name of the :term:`column
+ name` (can be overriden via ``attrs`` argument).
+ - The ``sortable`` parameter defaults to :const:`False`.
+ - The ``type`` attribute of the input is ``checkbox`` (can be overriden via
+ ``attrs`` argument).
+ - The header checkbox is left bare, i.e. ``<input type="checkbox"/>`` (use
+ the ``header_attrs`` argument to customise).
.. note:: The "apply some operation onto the selection" functionality is
not implemented in this column, and requires manually implemention.
:param attrs:
a :class:`dict` of HTML attributes that are added to the rendered
``<input type="checkbox" .../>`` tag
+ :param header_attrs:
+ same as *attrs*, but applied **only** to the header checkbox
"""
- def __init__(self, attrs=None, **extra):
+ def __init__(self, attrs=None, header_attrs=None, **extra):
params = {'sortable': False}
params.update(extra)
super(CheckBoxColumn, self).__init__(**params)
self.attrs = attrs or {}
+ self.header_attrs = header_attrs or {}
@property
def header(self):
- return mark_safe('<input type="checkbox"/>')
+ attrs = AttributeDict({
+ 'type': 'checkbox',
+ })
+ attrs.update(self.header_attrs)
+ return mark_safe('<input %s/>' % attrs.as_html())
def render(self, value, bound_column, **kwargs):
attrs = AttributeDict({
Both columns will have the same output.
+
+ .. important::
+ In order to use template tags or filters that require a
+ ``RequestContext``, the table **must** be rendered via
+ :ref:`{% render_table %} <template-tags.render_table>`.
+
"""
def __init__(self, template_code=None, **extra):
super(TemplateColumn, self).__init__(**extra)
self.template_code = template_code
- def render(self, record, **kwargs):
+ def render(self, record, table, **kwargs):
t = Template(self.template_code)
- return t.render(Context({'record': record}))
+ if hasattr(table, 'request'):
+ context = RequestContext(table.request, {'record': record})
+ else:
+ context = Context({'record': record})
+ return t.render(context)
class BoundColumn(object):
column.
"""
- return self.verbose_name
+ return self.column.header or self.verbose_name
@property
def name(self):
--- /dev/null
+from django.utils.functional import Promise
+
+
+class AbstractProxy(object):
+ """Delegates all operations (except ``.__subject__``) to another object"""
+ __slots__ = ()
+
+ #def __call__(self, *args, **kw):
+ # return self.__subject__(*args, **kw)
+
+ def __getattribute__(self, attr, oga=object.__getattribute__):
+ subject = oga(self,'__subject__')
+ if attr=='__subject__':
+ return subject
+ return getattr(subject,attr)
+
+ def __setattr__(self, attr, val, osa=object.__setattr__):
+ if attr == '__subject__':
+ osa(self, attr, val)
+ else:
+ setattr(self.__subject__, attr, val)
+
+ def __delattr__(self, attr, oda=object.__delattr__):
+ if attr=='__subject__':
+ oda(self,attr)
+ else:
+ delattr(self.__subject__, attr)
+
+ def __nonzero__(self):
+ return bool(self.__subject__)
+
+ def __getitem__(self, arg):
+ return self.__subject__[arg]
+
+ def __setitem__(self, arg, val):
+ self.__subject__[arg] = val
+
+ def __delitem__(self, arg):
+ del self.__subject__[arg]
+
+ def __getslice__(self, i, j):
+ return self.__subject__[i:j]
+
+
+ def __setslice__(self, i, j, val):
+ self.__subject__[i:j] = val
+
+ def __delslice__(self, i, j):
+ del self.__subject__[i:j]
+
+ def __contains__(self, ob):
+ return ob in self.__subject__
+
+ for name in 'repr str hash len abs complex int long float iter oct hex'.split():
+ exec "def __%s__(self): return %s(self.__subject__)" % (name, name)
+
+ for name in 'cmp', 'coerce', 'divmod':
+ exec "def __%s__(self,ob): return %s(self.__subject__,ob)" % (name, name)
+
+ for name, op in [
+ ('lt','<'), ('gt','>'), ('le','<='), ('ge','>='),
+ ('eq','=='), ('ne','!=')
+ ]:
+ exec "def __%s__(self,ob): return self.__subject__ %s ob" % (name, op)
+
+ for name, op in [('neg','-'), ('pos','+'), ('invert','~')]:
+ exec "def __%s__(self): return %s self.__subject__" % (name, op)
+
+ for name, op in [
+ ('or','|'), ('and','&'), ('xor','^'), ('lshift','<<'), ('rshift','>>'),
+ ('add','+'), ('sub','-'), ('mul','*'), ('div','/'), ('mod','%'),
+ ('truediv','/'), ('floordiv','//')
+ ]:
+ exec (
+ "def __%(name)s__(self,ob):\n"
+ " return self.__subject__ %(op)s ob\n"
+ "\n"
+ "def __r%(name)s__(self,ob):\n"
+ " return ob %(op)s self.__subject__\n"
+ "\n"
+ "def __i%(name)s__(self,ob):\n"
+ " self.__subject__ %(op)s=ob\n"
+ " return self\n"
+ ) % locals()
+
+ del name, op
+
+ # Oddball signatures
+
+ def __rdivmod__(self,ob):
+ return divmod(ob, self.__subject__)
+
+ def __pow__(self, *args):
+ return pow(self.__subject__, *args)
+
+ def __ipow__(self, ob):
+ self.__subject__ **= ob
+ return self
+
+ def __rpow__(self, ob):
+ return pow(ob, self.__subject__)
+
+
+class ObjectProxy(AbstractProxy):
+ """Proxy for a specific object"""
+
+ __slots__ = "__subject__"
+
+ def __init__(self, subject):
+ self.__subject__ = subject
+
+
+class CallbackProxy(AbstractProxy):
+ """Proxy for a dynamically-chosen object"""
+
+ __slots__ = '__callback__'
+
+ def __init__(self, func):
+ set_callback(self, func)
+
+set_callback = CallbackProxy.__callback__.__set__
+get_callback = CallbackProxy.__callback__.__get__
+CallbackProxy.__subject__ = property(lambda self, gc=get_callback: gc(self)())
+
+
+class LazyProxy(CallbackProxy):
+ """Proxy for a lazily-obtained object, that is cached on first use"""
+ __slots__ = "__cache__"
+
+get_cache = LazyProxy.__cache__.__get__
+set_cache = LazyProxy.__cache__.__set__
+
+def __subject__(self, get_cache=get_cache, set_cache=set_cache):
+ try:
+ return get_cache(self)
+ except AttributeError:
+ set_cache(self, get_callback(self)())
+ return get_cache(self)
+
+LazyProxy.__subject__ = property(__subject__, set_cache)
+del __subject__
+
+
+class TemplateSafeLazyProxy(LazyProxy):
+ """
+ A version of LazyProxy suitable for use in Django templates.
+
+ It's important that an ``alters_data`` attribute returns :const:`False`.
+
+ """
+ def __getattribute__(self, attr, *args, **kwargs):
+ if attr == 'alters_data':
+ return False
+ return LazyProxy.__getattribute__(self, attr, *args, **kwargs)
# -*- coding: utf-8 -*-
from django.utils.safestring import EscapeUnicode, SafeData
+from .proxies import TemplateSafeLazyProxy
+import itertools
class BoundRow(object):
- """Represents a *specific* row in a table.
+ """
+ Represents a *specific* row in a table.
- :class:`BoundRow` objects are a container that make it easy to access the
+ :class:`.BoundRow` objects are a container that make it easy to access the
final 'rendered' values for cells in a row. You can simply iterate over a
- :class:`BoundRow` object and it will take care to return values rendered
- using the correct method (e.g. :meth:`Column.render_FOO`)
+ :class:`.BoundRow` object and it will take care to return values rendered
+ using the correct method (e.g. :meth:`.Column.render_FOO`)
To access the rendered value of each cell in a row, just iterate over it:
...
KeyError: 'c'
+ :param table: is the :class:`Table` in which this row exists.
+ :param record: a single record from the :term:`table data` that is used to
+ populate the row. A record could be a :class:`Model` object, a
+ :class:`dict`, or something else.
+
"""
def __init__(self, table, record):
- """Initialise a new :class:`BoundRow` object where:
-
- * *table* is the :class:`Table` in which this row exists.
- * *record* is a single record from the data source that is posed to
- populate the row. A record could be a :class:`Model` object, a
- ``dict``, or something else.
-
- """
self._table = table
self._record = record
@property
def table(self):
- """The associated :term:`table`."""
+ """The associated :class:`.Table` object."""
return self._table
@property
def record(self):
- """The data record from the data source which is used to populate this
- row with data.
+ """
+ The data record from the data source which is used to populate this row
+ with data.
"""
return self._record
def __iter__(self):
- """Iterate over the rendered values for cells in the row.
+ """
+ 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.
+ Under the hood this method just makes a call to
+ :meth:`.BoundRow.__getitem__` for each cell.
"""
for column in self.table.columns:
yield self[column.name]
def __getitem__(self, name):
- """Returns the final rendered value for a cell in the row, given the
- name of a column.
+ """
+ Returns the final rendered value for a cell in the row, given the name
+ of a column.
"""
bound_column = self.table.columns[name]
- raw = bound_column.accessor.resolve(self.record)
+
+ def value():
+ try:
+ raw = bound_column.accessor.resolve(self.record)
+ except (TypeError, AttributeError, KeyError, ValueError) as e:
+ raw = None
+ return raw if raw is not None else bound_column.default
+
kwargs = {
- 'value': raw if raw is not None else bound_column.default,
+ 'value': TemplateSafeLazyProxy(value),
'record': self.record,
'column': bound_column.column,
'bound_column': bound_column,
"""
Container for spawning :class:`.BoundRow` objects.
- The :attr:`.tables.Table.rows` attribute is a :class:`.BoundRows` object.
+ The :attr:`.Table.rows` attribute is a :class:`.BoundRows` object.
It provides functionality that would not be possible with a simple iterator
in the table class.
+ :type table: :class:`.Table` object
+ :param table: the table in which the rows exist.
+
"""
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 an iterable for all :class:`BoundRow` objects in the table.
+ Return an iterable for all :class:`.BoundRow` objects in the table.
"""
for record in self.table.data:
def page(self):
"""
If the table is paginated, return an iterable of :class:`.BoundRow`
- objects that appear on the current page, otherwise :const:`None`.
+ objects that appear on the current page.
+ :rtype: iterable of :class:`.BoundRow` objects, or :const:`None`.
"""
if not hasattr(self.table, 'page'):
return None
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]:
- result.append(BoundRow(self.table, row))
- return result
+ return itertools.imap(lambda record: BoundRow(self.table, record),
+ self.table.data[key])
elif isinstance(key, int):
return BoundRow(self.table, self.table.data[key])
else:
}
table.paleblue a:link,
-table.paleblue a:visited {
+table.paleblue a:visited,
+table.paleblue + ul.pagination > li > a {
color: #5B80B2;
text-decoration: none;
font-weight: bold;
table.paleblue thead th,
table.paleblue thead td {
- background: #FCFCFC url(../img/nav-bg.gif) top left repeat-x;
+ background: #FCFCFC url(../img/header-bg.gif) top left repeat-x;
border-bottom: 1px solid #DDD;
padding: 2px 5px;
font-size: 11px;
table.paleblue tr.even {
background-color: white;
}
+
+table.paleblue + ul.pagination {
+ background: white url(../img/pagination-bg.gif) left 180% repeat-x;
+ overflow: auto;
+ padding: 10px;
+ border: 1px solid #DDD;
+}
+
+table.paleblue + ul.pagination > li {
+ float: left;
+ line-height: 22px;
+ margin-left: 10px;
+}
+
+table.paleblue + ul.pagination > li:first-child {
+ margin-left: 0;
+}
+
+div.table-container {
+ display: inline-block;
+}
if isinstance(data, QuerySet):
self.queryset = data
elif isinstance(data, list):
- self.list = data
+ self.list = data[:]
else:
raise ValueError('data must be a list or QuerySet object, not %s'
% data.__class__.__name__)
self._table = table
- # work with a copy of the data that has missing values populated with
- # defaults.
- if hasattr(self, 'list'):
- self.list = copy.copy(self.list)
- self._populate_missing_values(self.list)
-
def __len__(self):
# Use the queryset count() method to get the length, instead of
# loading all results into memory. This allows, for example,
translated.append(prefix + column.accessor)
return OrderByTuple(translated)
- def _populate_missing_values(self, data):
- """
- Populates self._data with missing values based on the default value
- for each column. It will create new items in the dataset (not modify
- existing ones).
-
- """
- for i, item in enumerate(data):
- # add data that is missing from the source. we do this now
- # so that the column's ``default`` values can affect
- # sorting (even when callables are used)!
- #
- # This is a design decision - the alternative would be to
- # resolve the values when they are accessed, and either do
- # not support sorting them at all, or run the callables
- # during sorting.
- modified_item = None
- for bound_column in self._table.columns.all():
- # the following will be True if:
- # * the source does not provide a value for the column
- # or the value is None
- # * the column did provide a data callable that
- # returned None
- accessor = Accessor(bound_column.accessor)
- try:
- if accessor.resolve(item) is None: # may raise ValueError
- raise ValueError('None values also need replacing')
- except ValueError:
- if modified_item is None:
- modified_item = copy.copy(item)
- modified_item[accessor.bits[0]] = bound_column.default
- if modified_item is not None:
- data[i] = modified_item
-
-
def __getitem__(self, index):
return (self.list if hasattr(self, 'list') else self.queryset)[index]
@order_by.setter
def order_by(self, value):
- """Order the rows of the table based columns. ``value`` must be a
- sequence of column names.
+ """
+ Order the rows of the table based columns. ``value`` must be a sequence
+ of column names.
+
"""
# accept both string and tuple instructions
order_by = value.split(',') if isinstance(value, basestring) else value
"""The attributes that should be applied to the ``<table>`` tag when
rendering HTML.
- :returns: :class:`~.utils.AttributeDict` object.
+ :rtype: :class:`~.utils.AttributeDict` object.
"""
return self._meta.attrs
- def paginate(self, klass=Paginator, page=1, *args, **kwargs):
- self.paginator = klass(self.rows, *args, **kwargs)
+ def paginate(self, klass=Paginator, per_page=25, page=1, *args, **kwargs):
+ self.paginator = klass(self.rows, per_page, *args, **kwargs)
try:
self.page = self.paginator.page(page)
except Exception as e:
{% spaceless %}
{% load django_tables %}
+{% if table.page %}
+<div class="table-container">
+{% endif %}
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
<thead>
<tr class="{% cycle "odd" "even" %}">
<th class="{% spaceless %}{% if column.sortable %}sortable {% endif %}{% if ob %}{% if ob.is_descending %}desc{% else %}asc{% endif %}{% endif %}{% endspaceless %}"><a href="{% if ob %}{% set_url_param sort=ob.opposite %}{% else %}{% set_url_param sort=column.name %}{% endif %}">{{ column.header }}</a></th>
{% endwith %}
{% else %}
- <th>{{ column.verbose_name }}</th>
+ <th>{{ column.header }}</th>
{% endif %}
{% endfor %}
</tr>
</thead>
<tbody>
- {% for row in table.rows %}
+ {% for row in table.page.object_list|default:table.rows %} {# support pagination #}
<tr class="{% cycle "odd" "even" %}">
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tbody>
</table>
+{% if table.page %}
+<ul class="pagination">
+ {% if table.page.has_previous %}
+ <li class="previous"><a href="{% set_url_param page=table.page.previous_page_number %}">Previous</a>
+ {% endif %}
+ <li class="current">Page {{ table.page.number }} of {{ table.paginator.num_pages }}</li>
+ {% if table.page.has_next %}
+ <li class="next"><a href="{% set_url_param page=table.page.next_page_number %}">Next</a>
+ {% endif %}
+ </span>
+</div>
+</div>
+{% endif %}
{% endspaceless %}
self.table_var = template.Variable(table_var_name)
def render(self, context):
- context = template.Context({
- 'request': context.get('request', None),
- 'table': self.table_var.resolve(context)
- })
- return get_template('django_tables/table.html').render(context)
+ table = self.table_var.resolve(context)
+ request = context.get('request', None)
+ context = template.Context({'request': request, 'table': table})
+ try:
+ table.request = request
+ return get_template('django_tables/table.html').render(context)
+ finally:
+ pass
+ #del table.request
@register.tag
# The short X.Y version.
version = '0.4.0'
# The full version, including alpha/beta/rc tags.
-release = '0.4.0.beta2'
+release = '0.4.0.beta3'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
table.order_by = 'name,-population' # equivalant
+.. _pagination:
+
+Pagination
+==========
+
+Pagination is easy, just call :meth:`.Table.paginate` and pass in the current
+page number, e.g.
+
+.. code-block:: python
+
+ def people_listing(request):
+ table = PeopleTable(Person.objects.all())
+ table.paginate(page=request.GET.get('page', 1))
+ return render_to_response('people_listing.html', {'table': table},
+ context_instance=RequestContext(request))
+
+The last set is to render the table. :meth:`.Table.as_html` doesn't support
+pagination, so you must use :ref:`{% render_table %}
+<template-tags.render_table>`.
+
.. _custom-rendering:
Custom rendering
{% load django_tables %}
{% render_table table %}
+This tag temporarily modifies the :class:`.Table` object while it is being
+rendered. It adds a ``request`` attribute to the table, which allows
+:class:`Column` objects to have access to a ``RequestContext``. See
+:class:`.TemplateColumn` for an example.
+
.. _template-tags.set_url_param:
--------------------------
.. autoclass:: django_tables.rows.BoundRows
- :members: __init__, all, page, __iter__, __len__, count
+ :members: all, page, __iter__, __len__, count
:class:`BoundRow` Objects
-------------------------
.. autoclass:: django_tables.rows.BoundRow
- :members: __init__, __getitem__, __contains__, __iter__, record, table
+ :members: __getitem__, __contains__, __iter__, record, table
:class:`AttributeDict` Objects
setup(
name='django-tables',
- version='0.4.0.beta2',
+ version='0.4.0.beta3',
description='Table framework for Django',
author='Bradley Ayers',
paginator = Paginator(books.rows, 10)
assert paginator.num_pages == 10
page = paginator.page(1)
- assert len(page.object_list) == 10
assert page.has_previous() == False
assert page.has_next() == True