From 13a90143c4d2eedce0936ae2f972d10ac917f97c Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Thu, 7 Apr 2011 09:08:38 +1000 Subject: [PATCH] mostly done, just need to add empty_text functionality, and sortable --- django_tables/proxies.py | 154 --------------------------------------- django_tables/rows.py | 30 +++----- django_tables/tables.py | 48 +++++++----- docs/conf.py | 2 +- setup.py | 2 +- tests/__init__.py | 3 +- tests/core.py | 128 ++++++++++++-------------------- tests/rows.py | 56 ++++++++++++++ 8 files changed, 146 insertions(+), 277 deletions(-) delete mode 100644 django_tables/proxies.py create mode 100644 tests/rows.py diff --git a/django_tables/proxies.py b/django_tables/proxies.py deleted file mode 100644 index 95b919d..0000000 --- a/django_tables/proxies.py +++ /dev/null @@ -1,154 +0,0 @@ -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) diff --git a/django_tables/rows.py b/django_tables/rows.py index d14d0b8..f9b9770 100644 --- a/django_tables/rows.py +++ b/django_tables/rows.py @@ -3,7 +3,6 @@ from itertools import imap, ifilter import inspect from django.utils.safestring import EscapeUnicode, SafeData from django.utils.functional import curry -from .proxies import TemplateSafeLazyProxy class BoundRow(object): @@ -113,25 +112,25 @@ class BoundRow(object): return raw if raw is not None else bound_column.default kwargs = { - 'value': TemplateSafeLazyProxy(value), - 'record': self.record, - 'column': bound_column.column, - 'bound_column': bound_column, - 'bound_row': self, - 'table': self._table, + 'value': value, # already a function + 'record': lambda: self.record, + 'column': lambda: bound_column.column, + 'bound_column': lambda: bound_column, + 'bound_row': lambda: self, + 'table': lambda: self._table, } render_FOO = 'render_' + bound_column.name render = getattr(self.table, render_FOO, bound_column.column.render) # just give a list of all available methods - available = ifilter(curry(hasattr, inspect), ('getfullargspec', 'getargspec')) - spec = getattr(inspect, next(available)) + funcs = ifilter(curry(hasattr, inspect), ('getfullargspec', 'getargspec')) + spec = getattr(inspect, next(funcs)) # 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] + kw[name] = kwargs[name]() return render(**kw) def __contains__(self, item): @@ -157,17 +156,6 @@ class BoundRows(object): def __init__(self, table): self.table = table - def page(self): - """ - If the table is paginated, return an iterable of :class:`.BoundRow` - objects that appear on the current page. - - :rtype: iterable of :class:`.BoundRow` objects, or :const:`None`. - """ - if not hasattr(self.table, 'page'): - return None - return iter(self.table.page.object_list) - def __iter__(self): """Convience method for :meth:`.BoundRows.all`""" for record in self.table.data: diff --git a/django_tables/tables.py b/django_tables/tables.py index d3a3bd1..91fcbdf 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -144,6 +144,7 @@ class TableOptions(object): order_by = (order_by, ) self.order_by = OrderByTuple(order_by) self.attrs = AttributeDict(getattr(options, 'attrs', {})) + self.empty_text = getattr(options, 'empty_text', None) class Table(StrAndUnicode): @@ -153,15 +154,15 @@ class Table(StrAndUnicode): :type data: ``list`` or ``QuerySet`` :param data: The :term:`table data`. - :type order_by: ``Table.DoNotOrder``, ``None``, ``tuple`` or ``basestring`` + :type order_by: ``None``, ``tuple`` or ``string`` :param order_by: sort the table based on these columns prior to display. (default :attr:`.Table.Meta.order_by`) 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``. + ``Meta.order_by`` option to be overridden. If the ``order_by is None`` + the table's ``Meta.order_by`` will be used. If you want to disable a + default ordering, simply use an empty ``tuple``, ``string``, or ``list``, + e.g. ``Table(…, order_by='')``. Example: @@ -169,34 +170,29 @@ class Table(StrAndUnicode): def obj_list(request): ... - # We don't want a default sort - order_by = request.GET.get('sort', SimpleTable.DoNotOrder) + # If there's no ?sort=…, we don't want to fallback to + # Table.Meta.order_by, thus we must not default to passing in None + order_by = request.GET.get('sort', ()) 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. - DoNotOrder = type('DoNotOrder', (), {}) TableDataClass = TableData - def __init__(self, data, order_by=None): + def __init__(self, data, order_by=None, sortable=None, empty_text=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. - if not order_by: + if order_by is None: self.order_by = self._meta.order_by - elif order_by is Table.DoNotOrder: - self.order_by = None else: self.order_by = order_by + self.sortable = sortable + self.empty_text = empty_text + # Make a copy so that modifying this will not touch the class # definition. Note that this is different from forms, where the # copy is made available in a ``fields`` attribute. See the @@ -234,6 +230,22 @@ class Table(StrAndUnicode): self._order_by = order_by self._data.order_by(order_by) + @property + def sortable(self): + return self._sortable if self._sortable is not None else self._meta.sortable + + @sortable.setter + def sortable(self, value): + self._sortable = value + + @property + def empty_text(self): + return self._empty_text if self._empty_text is not None else self._meta.empty_text + + @empty_text.setter + def empty_text(self, value): + self._empty_text = value + @property def rows(self): return self._rows diff --git a/docs/conf.py b/docs/conf.py index bfa6dd5..9703bd0 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.beta4' +release = '0.4.0.beta5' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index ecd501f..2af75a2 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.beta4', + version='0.4.0.beta5', description='Table framework for Django', author='Bradley Ayers', diff --git a/tests/__init__.py b/tests/__init__.py index d592d86..5b434e4 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -26,6 +26,7 @@ from .core import core from .templates import templates from .models import models from .utils import utils +from .rows import rows -everything = Tests([core, templates, models, utils]) +everything = Tests([core, templates, models, utils, rows]) diff --git a/tests/core.py b/tests/core.py index 8320e9b..66cbe4f 100644 --- a/tests/core.py +++ b/tests/core.py @@ -23,6 +23,10 @@ def context(): alpha = tables.Column() beta = tables.Column() + class SortedTable(UnsortedTable): + class Meta: + order_by = 'alpha' + table = UnsortedTable(memory_data) yield Context @@ -75,80 +79,43 @@ def datasource_untouched(context): @core.test -def sorting(context): - class MyUnsortedTable(tables.Table): - i = tables.Column() - alpha = tables.Column() - beta = tables.Column() - - # various different ways to say the same thing: don't sort - Assert(MyUnsortedTable([]).order_by) == () - Assert(MyUnsortedTable([], order_by=None).order_by) == () - Assert(MyUnsortedTable([], order_by=[]).order_by) == () - Assert(MyUnsortedTable([], order_by=()).order_by) == () +def sorting(ctx): + # fallback to Table.Meta + Assert(('alpha', )) == ctx.SortedTable([], order_by=None).order_by == ctx.SortedTable([]).order_by # values of order_by are wrapped in tuples before being returned - Assert(MyUnsortedTable([], order_by='alpha').order_by) == ('alpha',) - Assert(MyUnsortedTable([], order_by=('beta',)).order_by) == ('beta',) - - # a rewritten order_by is also wrapped - table = MyUnsortedTable([]) - table.order_by = 'alpha' - assert ('alpha', ) == table.order_by - - # default sort order can be specified in table options - class MySortedTable(MyUnsortedTable): - class Meta: - order_by = 'alpha' + Assert(ctx.SortedTable([], order_by='alpha').order_by) == ('alpha', ) + Assert(ctx.SortedTable([], order_by=('beta',)).order_by) == ('beta', ) - # order_by is inherited from the options if not explitly set - table = MySortedTable([]) - assert ('alpha', ) == table.order_by + # "no sorting" + table = ctx.SortedTable([]) + table.order_by = [] + Assert(()) == table.order_by == ctx.SortedTable([], order_by=[]).order_by - # ...but can be overloaded at __init___ - table = MySortedTable([], order_by='beta') - assert ('beta', ) == table.order_by + table = ctx.SortedTable([]) + table.order_by = () + Assert(()) == table.order_by == ctx.SortedTable([], order_by=()).order_by - # ...or rewritten later - table = MySortedTable(context.memory_data) - table.order_by = 'beta' - assert ('beta', ) == table.order_by - assert 3 == table.rows[0]['i'] - - # 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'] + table = ctx.SortedTable([]) + table.order_by = '' + Assert(()) == table.order_by == ctx.SortedTable([], order_by='').order_by + # apply a sorting + table = ctx.UnsortedTable([]) + table.order_by = 'alpha' + Assert(('alpha', )) == ctx.UnsortedTable([], order_by='alpha').order_by == table.order_by -@core.test -def boundrows_iteration(context): - records = [] - for row in context.table.rows: - records.append(row.record) - Assert(records) == context.memory_data + table = ctx.SortedTable([]) + table.order_by = 'alpha' + Assert(('alpha', )) == ctx.SortedTable([], order_by='alpha').order_by == table.order_by + # let's check the data + table = ctx.SortedTable(ctx.memory_data, order_by='beta') + Assert(3) == table.rows[0]['i'] -@core.test -def row_subscripting(context): - row = context.table.rows[0] - # attempt number indexing - Assert(row[0]) == 2 - Assert(row[1]) == 'b' - Assert(row[2]) == 'b' - with Assert.raises(IndexError) as error: - row[3] - # attempt column name indexing - Assert(row['i']) == 2 - Assert(row['alpha']) == 'b' - Assert(row['beta']) == 'b' - with Assert.raises(KeyError) as error: - row['gamma'] + # allow fallback to Table.Meta.order_by + table = ctx.SortedTable(ctx.memory_data) + Assert(1) == table.rows[0]['i'] @core.test @@ -168,7 +135,7 @@ def column_accessor(context): col2 = tables.Column(accessor='alpha.upper') table = SimpleTable(context.memory_data) row = table.rows[0] - Assert(row['col1']) == True + Assert(row['col1']) is True Assert(row['col2']) == 'B' @@ -179,31 +146,30 @@ def pagination(): # create some sample data data = [] - for i in range(1,101): - data.append({'name': 'Book Nr. %d' % i}) + for i in range(100): + data.append({'name': 'Book No. %d' % i}) books = BookTable(data) # external paginator paginator = Paginator(books.rows, 10) assert paginator.num_pages == 10 page = paginator.page(1) - assert page.has_previous() == False - assert page.has_next() == True + assert page.has_previous() is False + assert page.has_next() is True # integrated paginator - books.paginate(Paginator, page=1, per_page=10) - # rows is now paginated - assert len(list(books.rows.page())) == 10 - assert len(list(books.rows)) == 100 + books.paginate(page=1) + Assert(hasattr(books, 'page')) is True + + books.paginate(page=1, per_page=10) + Assert(len(list(books.page.object_list))) == 10 + # new attributes - assert books.paginator.num_pages == 10 - assert books.page.has_previous() == False - assert books.page.has_next() == True + Assert(books.paginator.num_pages) == 10 + Assert(books.page.has_previous()) is False + Assert(books.page.has_next()) is True + # exceptions are converted into 404s with Assert.raises(Http404) as error: books.paginate(Paginator, page=9999, per_page=10) books.paginate(Paginator, page='abc', per_page=10) - - -if __name__ == '__main__': - core.main() diff --git a/tests/rows.py b/tests/rows.py new file mode 100644 index 0000000..9bc274a --- /dev/null +++ b/tests/rows.py @@ -0,0 +1,56 @@ +"""Test the core table functionality.""" +from attest import Tests, Assert +import django_tables as tables +from django_tables import utils + + +rows = Tests() + + +@rows.test +def bound_rows(): + class SimpleTable(tables.Table): + name = tables.Column() + + data = [ + {'name': 'Bradley'}, + {'name': 'Chris'}, + {'name': 'Peter'}, + ] + + table = SimpleTable(data) + + # iteration + records = [] + for row in table.rows: + records.append(row.record) + Assert(records) == data + + +@rows.test +def bound_row(): + class SimpleTable(tables.Table): + name = tables.Column() + occupation = tables.Column() + age = tables.Column() + + record = {'name': 'Bradley', 'age': 20, 'occupation': 'programmer'} + + table = SimpleTable([record]) + row = table.rows[0] + + # integer indexing into a row + Assert(row[0]) == record['name'] + Assert(row[1]) == record['occupation'] + Assert(row[2]) == record['age'] + + with Assert.raises(IndexError) as error: + row[3] + + # column name indexing into a row + Assert(row['name']) == record['name'] + Assert(row['occupation']) == record['occupation'] + Assert(row['age']) == record['age'] + + with Assert.raises(KeyError) as error: + row['gamma'] -- 2.26.2