mostly done, just need to add empty_text functionality, and sortable
authorBradley Ayers <bradley.ayers@gmail.com>
Wed, 6 Apr 2011 23:08:38 +0000 (09:08 +1000)
committerBradley Ayers <bradley.ayers@gmail.com>
Wed, 6 Apr 2011 23:08:38 +0000 (09:08 +1000)
django_tables/proxies.py [deleted file]
django_tables/rows.py
django_tables/tables.py
docs/conf.py
setup.py
tests/__init__.py
tests/core.py
tests/rows.py [new file with mode: 0644]

diff --git a/django_tables/proxies.py b/django_tables/proxies.py
deleted file mode 100644 (file)
index 95b919d..0000000
+++ /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)
index d14d0b8044a620555c795623242c05848a83cde5..f9b97702fb494ec1f7b011ffb60411cf120421f5 100644 (file)
@@ -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:
index d3a3bd15f8dd8831d28eb9930875a71ccc0e8b1c..91fcbdf0da3d48960f9e9d9ac74ade98c6c84820 100644 (file)
@@ -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
index bfa6dd5d8a91ad972f4a26c2cd07acf8f8b89a25..9703bd00889e07716c606a13cfbfae8387a8f0fb 100644 (file)
@@ -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.
index ecd501f4d39d206b59e1995a4c6551657ef919c3..2af75a2136eb1579b6287b110d4a6a0385e6db45 100755 (executable)
--- 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',
index d592d868211675bc851599bda28bd007e9f8d1c0..5b434e4ab63a262890c38e69ebb569d3b4660664 100644 (file)
@@ -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])
index 8320e9b2fc4fa49de2c62038f6b4936116bbebdb..66cbe4fb11240cd283149d7727ee7093ddb878b6 100644 (file)
@@ -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 (file)
index 0000000..9bc274a
--- /dev/null
@@ -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']