From c5b6cbf19a4671f54ddfd835f18676ee820a93db Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Tue, 5 Apr 2011 17:44:35 +1000 Subject: [PATCH] * render_FOO methods now only receive the arguments they accept (no more **kwargs) * 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. --- django_tables/rows.py | 44 ++++++++++++++------------------- django_tables/tables.py | 55 ++++++++++++++++++++++++++++++----------- docs/conf.py | 2 +- docs/index.rst | 9 +++++-- setup.py | 2 +- tests/__init__.py | 2 -- tests/core.py | 23 +++++++++++++---- tests/models.py | 33 +++++++++++++++---------- tests/templates.py | 1 + tests/testapp/models.py | 11 +++++++++ 10 files changed, 118 insertions(+), 64 deletions(-) diff --git a/django_tables/rows.py b/django_tables/rows.py index 1b662b9..d14d0b8 100644 --- a/django_tables/rows.py +++ b/django_tables/rows.py @@ -1,7 +1,9 @@ # -*- 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): @@ -120,20 +122,17 @@ 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.""" @@ -158,14 +157,6 @@ class BoundRows(object): 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` @@ -179,7 +170,8 @@ class BoundRows(object): 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.""" @@ -191,8 +183,8 @@ class BoundRows(object): 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: diff --git a/django_tables/tables.py b/django_tables/tables.py index 0b3efa0..d3a3bd1 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -71,7 +71,17 @@ class TableData(object): 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] @@ -137,19 +147,32 @@ class TableOptions(object): 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 @@ -157,18 +180,22 @@ class Table(StrAndUnicode): # 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 diff --git a/docs/conf.py b/docs/conf.py index 67eef8a..bfa6dd5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,7 @@ project = u'django-tables' # 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. diff --git a/docs/index.rst b/docs/index.rst index 3e38c26..ac3622b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -241,8 +241,13 @@ copy & paste the method to every table you want to modify – which violates 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` diff --git a/setup.py b/setup.py index 1430d15..ecd501f 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages setup( name='django-tables', - version='0.4.0.beta3', + version='0.4.0.beta4', description='Table framework for Django', author='Bradley Ayers', diff --git a/tests/__init__.py b/tests/__init__.py index fdb9e0e..d592d86 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -16,8 +16,6 @@ settings.configure( } }, INSTALLED_APPS = [ - #'django.contrib.contenttypes', - #'django.contrib.auth', 'tests.testapp', 'django_tables', ] diff --git a/tests/core.py b/tests/core.py index 8341bdb..8320e9b 100644 --- a/tests/core.py +++ b/tests/core.py @@ -103,24 +103,37 @@ def sorting(context): # 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] @@ -181,7 +194,7 @@ def pagination(): 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 diff --git a/tests/models.py b/tests/models.py index cfeb176..6075f6f 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,34 +1,41 @@ 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 diff --git a/tests/templates.py b/tests/templates.py index a591ffa..f6f1700 100644 --- a/tests/templates.py +++ b/tests/templates.py @@ -65,6 +65,7 @@ def custom_rendering(context): 'None 31 Austria None 8 43 ') Assert(result) == template.render(context) + @templates.test def templatetag(context): # ensure it works with a multi-order-by diff --git a/tests/testapp/models.py b/tests/testapp/models.py index 48d828e..f08923b 100644 --- a/tests/testapp/models.py +++ b/tests/testapp/models.py @@ -4,3 +4,14 @@ from django.db import models 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 -- 2.26.2