* The default ordering of a table is now Table.Meta.order_by, rather than to not sort at all.
* Fixed a rendering bug with Django 1.3 (related to indexing into querysets)
* Model tests now work.
# -*- coding: utf-8 -*-
+from itertools import imap, ifilter
+import inspect
from django.utils.safestring import EscapeUnicode, SafeData
+from django.utils.functional import curry
from .proxies import TemplateSafeLazyProxy
-import itertools
class BoundRow(object):
}
render_FOO = 'render_' + bound_column.name
render = getattr(self.table, render_FOO, bound_column.column.render)
- try:
- return render(**kwargs)
- except TypeError as e:
- # Let's be helpful and provide a decent error message, since
- # render() underwent backwards incompatible changes.
- if e.message.startswith('render() got an unexpected keyword'):
- if hasattr(self.table, render_FOO):
- cls = self.table.__class__.__name__
- meth = render_FOO
- else:
- cls = kwargs['column'].__class__.__name__
- meth = 'render'
- msg = 'Did you forget to add **kwargs to %s.%s() ?' % (cls, meth)
- raise TypeError(e.message + '. ' + msg)
+
+ # just give a list of all available methods
+ available = ifilter(curry(hasattr, inspect), ('getfullargspec', 'getargspec'))
+ spec = getattr(inspect, next(available))
+ # only provide the arguments that the func is interested in
+ kw = {}
+ for name in spec(render).args:
+ if name == 'self':
+ continue
+ kw[name] = kwargs[name]
+ return render(**kw)
def __contains__(self, item):
"""Check by both row object and column name."""
def __init__(self, table):
self.table = table
- def all(self):
- """
- Return an iterable for all :class:`.BoundRow` objects in the table.
-
- """
- for record in self.table.data:
- yield BoundRow(self.table, record)
-
def page(self):
"""
If the table is paginated, return an iterable of :class:`.BoundRow`
def __iter__(self):
"""Convience method for :meth:`.BoundRows.all`"""
- return self.all()
+ for record in self.table.data:
+ yield BoundRow(self.table, record)
def __len__(self):
"""Returns the number of rows in the table."""
def __getitem__(self, key):
"""Allows normal list slicing syntax to be used."""
if isinstance(key, slice):
- return itertools.imap(lambda record: BoundRow(self.table, record),
- self.table.data[key])
+ return imap(lambda record: BoundRow(self.table, record),
+ self.table.data[key])
elif isinstance(key, int):
return BoundRow(self.table, self.table.data[key])
else:
translated.append(prefix + column.accessor)
return OrderByTuple(translated)
+ def __iter__(self):
+ """
+ for ... in ... default to using this. There's a bug in Django 1.3
+ with indexing into querysets, so this side-steps that problem (as well
+ as just being a better way to iterate).
+
+ """
+ return self.list.__iter__() if hasattr(self, 'list') else self.queryset.__iter__()
+
def __getitem__(self, index):
+ """Forwards indexing accesses to underlying data"""
return (self.list if hasattr(self, 'list') else self.queryset)[index]
class Table(StrAndUnicode):
- """A collection of columns, plus their associated data rows.
+ """
+ A collection of columns, plus their associated data rows.
- :type data: :class:`list` or :class:`QuerySet`
- :param data:
- The :term:`table data`.
+ :type data: ``list`` or ``QuerySet``
+ :param data: The :term:`table data`.
- :param: :class:`tuple`-like or :class:`basestring`
- :param order_by:
- The description of how the table should be ordered. This allows the
- :attr:`.Table.Meta.order_by` option to be overridden.
+ :type order_by: ``Table.DoNotOrder``, ``None``, ``tuple`` or ``basestring``
+ :param order_by: sort the table based on these columns prior to display.
+ (default :attr:`.Table.Meta.order_by`)
- .. note::
- Unlike a :class:`Form`, tables are always bound to data.
+ The ``order_by`` argument is optional and allows the table's
+ ``Meta.order_by`` option to be overridden. If the ``bool(order_by)``
+ evaluates to ``False``, the table's ``Meta.order_by`` will be used. If you
+ want to disable a default ordering, you must pass in the value
+ ``Table.DoNotOrder``.
+
+ Example:
+
+ .. code-block:: python
+
+ def obj_list(request):
+ ...
+ # We don't want a default sort
+ order_by = request.GET.get('sort', SimpleTable.DoNotOrder)
+ table = SimpleTable(data, order_by=order_by)
+ ...
"""
__metaclass__ = DeclarativeColumnsMetaclass
# this value is not the same as None. it means 'use the default sort
# order', which may (or may not) be inherited from the table options.
# None means 'do not sort the data', ignoring the default.
- DefaultOrder = type('DefaultSortType', (), {})()
+ DoNotOrder = type('DoNotOrder', (), {})
TableDataClass = TableData
- def __init__(self, data, order_by=DefaultOrder):
+ def __init__(self, data, order_by=None):
self._rows = BoundRows(self) # bound rows
self._columns = BoundColumns(self) # bound columns
self._data = self.TableDataClass(data=data, table=self)
# None is a valid order, so we must use DefaultOrder as a flag
# to fall back to the table sort order.
- self.order_by = (self._meta.order_by if order_by is Table.DefaultOrder
- else order_by)
+ if not order_by:
+ self.order_by = self._meta.order_by
+ elif order_by is Table.DoNotOrder:
+ self.order_by = None
+ else:
+ self.order_by = order_by
# Make a copy so that modifying this will not touch the class
# definition. Note that this is different from forms, where the
# The short X.Y version.
version = '0.4.0'
# The full version, including alpha/beta/rc tags.
-release = '0.4.0.beta3'
+release = '0.4.0.beta4'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
DRY).
For convenience, a bunch of commonly used/useful values are passed to
-``render_FOO`` functions, when writing the signature, accept the arguments
-you're interested in, and collect the rest in a ``**kwargs`` argument.
+``render_FOO`` functions, when writing the signature, simply accept the
+arguments you're interested in, and the function will recieve them
+
+.. note:: Due to the implementation, a "catch-extra" arguments (e.g. ``*args``
+ or ``**kwargs``) will not recieve any arguments. This is because
+ ``inspect.getargsspec()`` is used to check what arguments a ``render_FOO``
+ method expect, and only to supply those.
:param value: the value for the cell retrieved from the :term:`table data`
:param record: the entire record for the row from :term:`table data`
setup(
name='django-tables',
- version='0.4.0.beta3',
+ version='0.4.0.beta4',
description='Table framework for Django',
author='Bradley Ayers',
}
},
INSTALLED_APPS = [
- #'django.contrib.contenttypes',
- #'django.contrib.auth',
'tests.testapp',
'django_tables',
]
# order_by is inherited from the options if not explitly set
table = MySortedTable([])
- assert ('alpha',) == table.order_by
+ assert ('alpha', ) == table.order_by
# ...but can be overloaded at __init___
table = MySortedTable([], order_by='beta')
- assert ('beta',) == table.order_by
+ assert ('beta', ) == table.order_by
# ...or rewritten later
table = MySortedTable(context.memory_data)
table.order_by = 'beta'
- assert ('beta',) == table.order_by
+ assert ('beta', ) == table.order_by
assert 3 == table.rows[0]['i']
- # ...or reset to None (unsorted), ignoring the table default
+ # Explicitly pass in None, should default to table's Meta.order_by
table = MySortedTable(context.memory_data, order_by=None)
+ assert ('alpha', ) == table.order_by
+ assert 1 == table.rows[0]['i']
+
+ # ...or reset to Table.DoNotOrder (unsorted), ignoring the table default
+ table = MySortedTable(context.memory_data, order_by=MySortedTable.DoNotOrder)
assert () == table.order_by
assert 2 == table.rows[0]['i']
+@core.test
+def boundrows_iteration(context):
+ records = []
+ for row in context.table.rows:
+ records.append(row.record)
+ Assert(records) == context.memory_data
+
+
@core.test
def row_subscripting(context):
row = context.table.rows[0]
books.paginate(Paginator, page=1, per_page=10)
# rows is now paginated
assert len(list(books.rows.page())) == 10
- assert len(list(books.rows.all())) == 100
+ assert len(list(books.rows)) == 100
# new attributes
assert books.paginator.num_pages == 10
assert books.page.has_previous() == False
from django.conf import settings
+from django.test.simple import DjangoTestSuiteRunner
+from django.test.client import RequestFactory
+from django.template import Template, Context
from django.core.paginator import *
import django_tables as tables
+import itertools
from django_attest import TestContext
-from attest import Tests
-from .testapp.models import Person
+from attest import Tests, Assert
+from .testapp.models import Person, Occupation
models = Tests()
models.context(TestContext())
+runner = DjangoTestSuiteRunner()
+runner.setup_databases()
+
@models.context
def samples():
class PersonTable(tables.Table):
first_name = tables.Column()
last_name = tables.Column()
+ occupation = tables.Column()
- # we're going to test against User, so let's create a few
- Person.objects.create(first_name='Bradley', last_name='Ayers')
- Person.objects.create(first_name='Chris', last_name='Doble')
+ # Test data
+ occupation = Occupation.objects.create(name='Programmer')
+ Person.objects.create(first_name='Bradley', last_name='Ayers', occupation=occupation)
+ Person.objects.create(first_name='Chris', last_name='Doble', occupation=occupation)
yield PersonTable
@models.test
-def simple(client, UserTable):
- queryset = Person.objects.all()
- table = PersonTable(queryset)
-
- for index, row in enumerate(table.rows):
- person = queryset[index]
- Assert(person.username) == row['first_name']
- Assert(person.email) == row['last_name']
+def boundrows_iteration(client, PersonTable):
+ table = PersonTable(Person.objects.all())
+ records = [row.record for row in table.rows]
+ expecteds = Person.objects.all()
+ for expected, actual in itertools.izip(expecteds, records):
+ Assert(expected) == actual
'None 31 Austria None 8 43 ')
Assert(result) == template.render(context)
+
@templates.test
def templatetag(context):
# ensure it works with a multi-order-by
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')
+
+ def __unicode__(self):
+ return self.first_name
+
+
+class Occupation(models.Model):
+ name = models.CharField(max_length=200)
+
+ def __unicode__(self):
+ return self.name