Convert a bunch of files to use *nix line endings.
authorMichael Elsdoerfer <michael@elsdoerfer.com>
Tue, 24 Aug 2010 22:27:08 +0000 (00:27 +0200)
committerMichael Elsdoerfer <michael@elsdoerfer.com>
Tue, 24 Aug 2010 22:29:09 +0000 (00:29 +0200)
django_tables/__init__.py
django_tables/app/templatetags/tables.py
django_tables/base.py
django_tables/columns.py
tests/__init__.py
tests/test_memory.py
tests/test_models.py
tests/test_templates.py

index b41bdaf6722abd6d383c38348a871e9f99d7e788..12ea2307ac97b8fdf7f91e677a9820d583a4eaab 100644 (file)
@@ -1,7 +1,7 @@
-__version__ = (0, 3, 'dev')\r
-\r
-\r
-from memory import *\r
-from models import *\r
-from columns import *\r
+__version__ = (0, 3, 'dev')
+
+
+from memory import *
+from models import *
+from columns import *
 from options import *
\ No newline at end of file
index fe03ef84801b639702f4baea39e50def91b17c4b..7dc68033a4a17f6f7e22a9a4e88e95e3a9b32e74 100644 (file)
@@ -1,71 +1,71 @@
-"""\r
-Allows setting/changing/removing of chosen url query string parameters,\r
-while maintaining any existing others.\r
-\r
-Expects the current request to be available in the context as ``request``.\r
-\r
-Examples:\r
-\r
-    {% set_url_param page=next_page %}\r
-    {% set_url_param page="" %}\r
-    {% set_url_param filter="books" page=1 %}\r
-"""\r
-\r
-import urllib\r
-import tokenize\r
-import StringIO\r
-from django import template\r
-from django.utils.safestring import mark_safe\r
-\r
-register = template.Library()\r
-\r
-class SetUrlParamNode(template.Node):\r
-    def __init__(self, changes):\r
-        self.changes = changes\r
-\r
-    def render(self, context):\r
-        request = context.get('request', None)\r
-        if not request: return ""\r
-\r
-        # Note that we want params to **not** be a ``QueryDict`` (thus we\r
-        # don't use it's ``copy()`` method), as it would force all values\r
-        # to be unicode, and ``urllib.urlencode`` can't handle that.\r
-        params = dict(request.GET)\r
-        for key, newvalue in self.changes.items():\r
-            newvalue = newvalue.resolve(context)\r
-            if newvalue=='' or newvalue is None: params.pop(key, False)\r
-            else: params[key] = unicode(newvalue)\r
-        # ``urlencode`` chokes on unicode input, so convert everything to\r
-        # utf8. Note that if some query arguments passed to the site have\r
-        # their non-ascii characters screwed up when passed though this,\r
-        # it's most likely not our fault. Django (the ``QueryDict`` class\r
-        # to be exact) uses your projects DEFAULT_CHARSET to decode incoming\r
-        # query strings, whereas your browser might encode the url\r
-        # differently. For example, typing "ä" in my German Firefox's (v2)\r
-        # address bar results in "%E4" being passed to the server (in\r
-        # iso-8859-1), but Django might expect utf-8, where ä would be\r
-        # "%C3%A4"\r
-        def mkstr(s):\r
-            if isinstance(s, list): return map(mkstr, s)\r
-            else: return (isinstance(s, unicode) and [s.encode('utf-8')] or [s])[0]\r
-        params = dict([(mkstr(k), mkstr(v)) for k, v in params.items()])\r
-        # done, return (string is already safe)\r
-        return '?'+urllib.urlencode(params, doseq=True)\r
-\r
-def do_seturlparam(parser, token):\r
-    bits = token.contents.split()\r
-    qschanges = {}\r
-    for i in bits[1:]:\r
-        try:\r
-            a, b = i.split('=', 1); a = a.strip(); b = b.strip()\r
-            keys = list(tokenize.generate_tokens(StringIO.StringIO(a).readline))\r
-            if keys[0][0] == tokenize.NAME:\r
-                if b == '""': b = template.Variable('""')  # workaround bug #5270\r
-                else: b = parser.compile_filter(b)\r
-                qschanges[str(a)] = b\r
-            else: raise ValueError\r
-        except ValueError:\r
-            raise template.TemplateSyntaxError, "Argument syntax wrong: should be key=value"\r
-    return SetUrlParamNode(qschanges)\r
-\r
+"""
+Allows setting/changing/removing of chosen url query string parameters,
+while maintaining any existing others.
+
+Expects the current request to be available in the context as ``request``.
+
+Examples:
+
+    {% set_url_param page=next_page %}
+    {% set_url_param page="" %}
+    {% set_url_param filter="books" page=1 %}
+"""
+
+import urllib
+import tokenize
+import StringIO
+from django import template
+from django.utils.safestring import mark_safe
+
+register = template.Library()
+
+class SetUrlParamNode(template.Node):
+    def __init__(self, changes):
+        self.changes = changes
+
+    def render(self, context):
+        request = context.get('request', None)
+        if not request: return ""
+
+        # Note that we want params to **not** be a ``QueryDict`` (thus we
+        # don't use it's ``copy()`` method), as it would force all values
+        # to be unicode, and ``urllib.urlencode`` can't handle that.
+        params = dict(request.GET)
+        for key, newvalue in self.changes.items():
+            newvalue = newvalue.resolve(context)
+            if newvalue=='' or newvalue is None: params.pop(key, False)
+            else: params[key] = unicode(newvalue)
+        # ``urlencode`` chokes on unicode input, so convert everything to
+        # utf8. Note that if some query arguments passed to the site have
+        # their non-ascii characters screwed up when passed though this,
+        # it's most likely not our fault. Django (the ``QueryDict`` class
+        # to be exact) uses your projects DEFAULT_CHARSET to decode incoming
+        # query strings, whereas your browser might encode the url
+        # differently. For example, typing "ä" in my German Firefox's (v2)
+        # address bar results in "%E4" being passed to the server (in
+        # iso-8859-1), but Django might expect utf-8, where ä would be
+        # "%C3%A4"
+        def mkstr(s):
+            if isinstance(s, list): return map(mkstr, s)
+            else: return (isinstance(s, unicode) and [s.encode('utf-8')] or [s])[0]
+        params = dict([(mkstr(k), mkstr(v)) for k, v in params.items()])
+        # done, return (string is already safe)
+        return '?'+urllib.urlencode(params, doseq=True)
+
+def do_seturlparam(parser, token):
+    bits = token.contents.split()
+    qschanges = {}
+    for i in bits[1:]:
+        try:
+            a, b = i.split('=', 1); a = a.strip(); b = b.strip()
+            keys = list(tokenize.generate_tokens(StringIO.StringIO(a).readline))
+            if keys[0][0] == tokenize.NAME:
+                if b == '""': b = template.Variable('""')  # workaround bug #5270
+                else: b = parser.compile_filter(b)
+                qschanges[str(a)] = b
+            else: raise ValueError
+        except ValueError:
+            raise template.TemplateSyntaxError, "Argument syntax wrong: should be key=value"
+    return SetUrlParamNode(qschanges)
+
 register.tag('set_url_param', do_seturlparam)
\ No newline at end of file
index bd8ac9fb0071c3c85a75e1a0edf45b8e11c81aef..75b5e95473b644dacdf40942edece0e8bffd965e 100644 (file)
-import copy\r
-from django.http import Http404\r
-from django.core import paginator\r
-from django.utils.datastructures import SortedDict\r
-from django.utils.encoding import force_unicode, StrAndUnicode\r
-from django.utils.text import capfirst\r
-from columns import Column\r
-from options import options\r
-\r
-\r
-__all__ = ('BaseTable', 'options')\r
-\r
-\r
-class TableOptions(object):\r
-    def __init__(self, options=None):\r
-        super(TableOptions, self).__init__()\r
-        self.sortable = getattr(options, 'sortable', None)\r
-        self.order_by = getattr(options, 'order_by', None)\r
-\r
-\r
-class DeclarativeColumnsMetaclass(type):\r
-    """\r
-    Metaclass that converts Column attributes to a dictionary called\r
-    'base_columns', taking into account parent class 'base_columns'\r
-    as well.\r
-    """\r
-    def __new__(cls, name, bases, attrs, parent_cols_from=None):\r
-        """\r
-        The ``parent_cols_from`` argument determins from which attribute\r
-        we read the columns of a base class that this table might be\r
-        subclassing. This is useful for ``ModelTable`` (and possibly other\r
-        derivatives) which might want to differ between the declared columns\r
-        and others.\r
-\r
-        Note that if the attribute specified in ``parent_cols_from`` is not\r
-        found, we fall back to the default (``base_columns``), instead of\r
-        skipping over that base. This makes a table like the following work:\r
-\r
-            class MyNewTable(tables.ModelTable, MyNonModelTable):\r
-                pass\r
-\r
-        ``MyNewTable`` will be built by the ModelTable metaclass, which will\r
-        call this base with a modified ``parent_cols_from`` argument\r
-        specific to ModelTables. Since ``MyNonModelTable`` is not a\r
-        ModelTable, and thus does not provide that attribute, the columns\r
-        from that base class would otherwise be ignored.\r
-        """\r
-\r
-        # extract declared columns\r
-        columns = [(column_name, attrs.pop(column_name))\r
-           for column_name, obj in attrs.items()\r
-           if isinstance(obj, Column)]\r
-        columns.sort(lambda x, y: cmp(x[1].creation_counter,\r
-                                      y[1].creation_counter))\r
-\r
-        # If this class is subclassing other tables, add their fields as\r
-        # well. Note that we loop over the bases in *reverse* - this is\r
-        # necessary to preserve the correct order of columns.\r
-        for base in bases[::-1]:\r
-            col_attr = (parent_cols_from and hasattr(base, parent_cols_from)) \\r
-                and parent_cols_from\\r
-                or 'base_columns'\r
-            if hasattr(base, col_attr):\r
-                columns = getattr(base, col_attr).items() + columns\r
-        # Note that we are reusing an existing ``base_columns`` attribute.\r
-        # This is because in certain inheritance cases (mixing normal and\r
-        # ModelTables) this metaclass might be executed twice, and we need\r
-        # to avoid overriding previous data (because we pop() from attrs,\r
-        # the second time around columns might not be registered again).\r
-        # An example would be:\r
-        #    class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass\r
-        if not 'base_columns' in attrs:\r
-            attrs['base_columns'] = SortedDict()\r
-        attrs['base_columns'].update(SortedDict(columns))\r
-\r
-        attrs['_meta'] = TableOptions(attrs.get('Meta', None))\r
-        return type.__new__(cls, name, bases, attrs)\r
-\r
-\r
-def rmprefix(s):\r
-    """Normalize a column name by removing a potential sort prefix"""\r
-    return (s[:1]=='-' and [s[1:]] or [s])[0]\r
-\r
-def toggleprefix(s):\r
-    """Remove - prefix is existing, or add if missing."""\r
-    return ((s[:1] == '-') and [s[1:]] or ["-"+s])[0]\r
-\r
-class OrderByTuple(tuple, StrAndUnicode):\r
-        """Stores 'order by' instructions; Used to render output in a format\r
-        we understand as input (see __unicode__) - especially useful in\r
-        templates.\r
-\r
-        Also supports some functionality to interact with and modify\r
-        the order.\r
-        """\r
-        def __unicode__(self):\r
-            """Output in our input format."""\r
-            return ",".join(self)\r
-\r
-        def __contains__(self, name):\r
-            """Determine whether a column is part of this order."""\r
-            for o in self:\r
-                if rmprefix(o) == name:\r
-                    return True\r
-            return False\r
-\r
-        def is_reversed(self, name):\r
-            """Returns a bool indicating whether the column is ordered\r
-            reversed, None if it is missing."""\r
-            for o in self:\r
-                if o == '-'+name:\r
-                    return True\r
-            return False\r
-        def is_straight(self, name):\r
-            """The opposite of is_reversed."""\r
-            for o in self:\r
-                if o == name:\r
-                    return True\r
-            return False\r
-\r
-        def polarize(self, reverse, names=()):\r
-            """Return a new tuple with the columns from ``names`` set to\r
-            "reversed" (e.g. prefixed with a '-'). Note that the name is\r
-            ambiguous - do not confuse this with ``toggle()``.\r
-\r
-            If names is not specified, all columns are reversed. If a\r
-            column name is given that is currently not part of the order,\r
-            it is added.\r
-            """\r
-            prefix = reverse and '-' or ''\r
-            return OrderByTuple(\r
-                    [\r
-                      (\r
-                        # add either untouched, or reversed\r
-                        (names and rmprefix(o) not in names)\r
-                            and [o]\r
-                            or [prefix+rmprefix(o)]\r
-                      )[0]\r
-                    for o in self]\r
-                    +\r
-                    [prefix+name for name in names if not name in self]\r
-            )\r
-\r
-        def toggle(self, names=()):\r
-            """Return a new tuple with the columns from ``names`` toggled\r
-            with respect to their "reversed" state. E.g. a '-' prefix will\r
-            be removed is existing, or added if lacking. Do not confuse\r
-            with ``reverse()``.\r
-\r
-            If names is not specified, all columns are toggled. If a\r
-            column name is given that is currently not part of the order,\r
-            it is added in non-reverse form."""\r
-            return OrderByTuple(\r
-                    [\r
-                      (\r
-                        # add either untouched, or toggled\r
-                        (names and rmprefix(o) not in names)\r
-                            and [o]\r
-                            or ((o[:1] == '-') and [o[1:]] or ["-"+o])\r
-                      )[0]\r
-                    for o in self]\r
-                    +\r
-                    [name for name in names if not name in self]\r
-            )\r
-\r
-\r
-class Columns(object):\r
-    """Container for spawning BoundColumns.\r
-\r
-    This is bound to a table and provides it's ``columns`` property. It\r
-    provides access to those columns in different ways (iterator,\r
-    item-based, filtered and unfiltered etc)., stuff that would not be\r
-    possible with a simple iterator in the table class.\r
-\r
-    Note that when you define your column using a name override, e.g.\r
-    ``author_name = tables.Column(name="author")``, then the column will\r
-    be exposed by this container as "author", not "author_name".\r
-    """\r
-    def __init__(self, table):\r
-        self.table = table\r
-        self._columns = SortedDict()\r
-\r
-    def _reset(self):\r
-        """Used by parent table class."""\r
-        self._columns = SortedDict()\r
-\r
-    def _spawn_columns(self):\r
-        # (re)build the "_columns" cache of BoundColumn objects (note that\r
-        # ``base_columns`` might have changed since last time); creating\r
-        # BoundColumn instances can be costly, so we reuse existing ones.\r
-        new_columns = SortedDict()\r
-        for decl_name, column in self.table.base_columns.items():\r
-            # take into account name overrides\r
-            exposed_name = column.name or decl_name\r
-            if exposed_name in self._columns:\r
-                new_columns[exposed_name] = self._columns[exposed_name]\r
-            else:\r
-                new_columns[exposed_name] = BoundColumn(self.table, column, decl_name)\r
-        self._columns = new_columns\r
-\r
-    def all(self):\r
-        """Iterate through all columns, regardless of visiblity (as\r
-        opposed to ``__iter__``.\r
-\r
-        This is used internally a lot.\r
-        """\r
-        self._spawn_columns()\r
-        for column in self._columns.values():\r
-            yield column\r
-\r
-    def items(self):\r
-        self._spawn_columns()\r
-        for r in self._columns.items():\r
-            yield r\r
-\r
-    def names(self):\r
-        self._spawn_columns()\r
-        for r in self._columns.keys():\r
-            yield r\r
-\r
-    def index(self, name):\r
-        self._spawn_columns()\r
-        return self._columns.keyOrder.index(name)\r
-\r
-    def sortable(self):\r
-        """Iterate through all sortable columns.\r
-\r
-        This is primarily useful in templates, where iterating over the full\r
-        set and checking {% if column.sortable %} can be problematic in\r
-        conjunction with e.g. {{ forloop.last }} (the last column might not\r
-        be the actual last that is rendered).\r
-        """\r
-        for column in self.all():\r
-            if column.sortable:\r
-                yield column\r
-\r
-    def __iter__(self):\r
-        """Iterate through all *visible* bound columns.\r
-\r
-        This is primarily geared towards table rendering.\r
-        """\r
-        for column in self.all():\r
-            if column.visible:\r
-                yield column\r
-\r
-    def __contains__(self, item):\r
-        """Check by both column object and column name."""\r
-        self._spawn_columns()\r
-        if isinstance(item, basestring):\r
-            return item in self.names()\r
-        else:\r
-            return item in self.all()\r
-\r
-    def __len__(self):\r
-        self._spawn_columns()\r
-        return len([1 for c in self._columns.values() if c.visible])\r
-\r
-    def __getitem__(self, name):\r
-        """Return a column by name."""\r
-        self._spawn_columns()\r
-        return self._columns[name]\r
-\r
-\r
-class BoundColumn(StrAndUnicode):\r
-    """'Runtime' version of ``Column`` that is bound to a table instance,\r
-    and thus knows about the table's data.\r
-\r
-    Note that the name that is passed in tells us how this field is\r
-    delared in the bound table. The column itself can overwrite this name.\r
-    While the overwritten name will be hat mostly counts, we need to\r
-    remember the one used for declaration as well, or we won't know how\r
-    to read a column's value from the source.\r
-    """\r
-    def __init__(self, table, column, name):\r
-        self.table = table\r
-        self.column = column\r
-        self.declared_name = name\r
-        # expose some attributes of the column more directly\r
-        self.visible = column.visible\r
-\r
-    @property\r
-    def accessor(self):\r
-        """The key to use when accessing this column's values in the\r
-        source data.\r
-        """\r
-        return self.column.data if self.column.data else self.declared_name\r
-\r
-    def _get_sortable(self):\r
-        if self.column.sortable is not None:\r
-            return self.column.sortable\r
-        elif self.table._meta.sortable is not None:\r
-            return self.table._meta.sortable\r
-        else:\r
-            return True   # the default value\r
-    sortable = property(_get_sortable)\r
-\r
-    name = property(lambda s: s.column.name or s.declared_name)\r
-    name_reversed = property(lambda s: "-"+s.name)\r
-    def _get_name_toggled(self):\r
-        o = self.table.order_by\r
-        if (not self.name in o) or o.is_reversed(self.name): return self.name\r
-        else: return self.name_reversed\r
-    name_toggled = property(_get_name_toggled)\r
-\r
-    is_ordered = property(lambda s: s.name in s.table.order_by)\r
-    is_ordered_reverse = property(lambda s: s.table.order_by.is_reversed(s.name))\r
-    is_ordered_straight = property(lambda s: s.table.order_by.is_straight(s.name))\r
-    order_by = property(lambda s: s.table.order_by.polarize(False, [s.name]))\r
-    order_by_reversed = property(lambda s: s.table.order_by.polarize(True, [s.name]))\r
-    order_by_toggled = property(lambda s: s.table.order_by.toggle([s.name]))\r
-\r
-    def get_default(self, row):\r
-        """Since a column's ``default`` property may be a callable, we need\r
-        this function to resolve it when needed.\r
-\r
-        Make sure ``row`` is a ``BoundRow`` object, since that is what\r
-        we promise the callable will get.\r
-        """\r
-        if callable(self.column.default):\r
-            return self.column.default(row)\r
-        return self.column.default\r
-\r
-    def _get_values(self):\r
-        # TODO: build a list of values used\r
-        pass\r
-    values = property(_get_values)\r
-\r
-    def __unicode__(self):\r
-        s = self.column.verbose_name or self.name.replace('_', ' ')\r
-        return capfirst(force_unicode(s))\r
-\r
-    def as_html(self):\r
-        pass\r
-\r
-\r
-class BoundRow(object):\r
-    """Represents a single row of data, bound to a table.\r
-\r
-    Tables will spawn these row objects, wrapping around the actual data\r
-    stored in a row.\r
-    """\r
-    def __init__(self, table, data):\r
-        self.table = table\r
-        self.data = data\r
-\r
-    def __iter__(self):\r
-        for value in self.values:\r
-            yield value\r
-\r
-    def __getitem__(self, name):\r
-        """Returns this row's value for a column. All other access methods,\r
-        e.g. __iter__, lead ultimately to this."""\r
-\r
-        column = self.table.columns[name]\r
-\r
-        render_func = getattr(self.table, 'render_%s' % name, False)\r
-        if render_func:\r
-            return render_func(self.data)\r
-        else:\r
-            return self._default_render(column)\r
-\r
-    def _default_render(self, column):\r
-        """Returns a cell's content. This is used unless the user\r
-        provides a custom ``render_FOO`` method.\r
-        """\r
-        result = self.data[column.accessor]\r
-\r
-        # if the field we are pointing to is a callable, remove it\r
-        if callable(result):\r
-            result = result(self)\r
-        return result\r
-\r
-    def __contains__(self, item):\r
-        """Check by both row object and column name."""\r
-        if isinstance(item, basestring):\r
-            return item in self.table._columns\r
-        else:\r
-            return item in self\r
-\r
-    def _get_values(self):\r
-        for column in self.table.columns:\r
-            yield self[column.name]\r
-    values = property(_get_values)\r
-\r
-    def as_html(self):\r
-        pass\r
-\r
-\r
-class Rows(object):\r
-    """Container for spawning BoundRows.\r
-\r
-    This is bound to a table and provides it's ``rows`` property. It\r
-    provides functionality that would not be possible with a simple\r
-    iterator in the table class.\r
-    """\r
-\r
-    row_class = BoundRow\r
-\r
-    def __init__(self, table):\r
-        self.table = table\r
-\r
-    def _reset(self):\r
-        pass   # we currently don't use a cache\r
-\r
-    def all(self):\r
-        """Return all rows."""\r
-        for row in self.table.data:\r
-            yield self.row_class(self.table, row)\r
-\r
-    def page(self):\r
-        """Return rows on current page (if paginated)."""\r
-        if not hasattr(self.table, 'page'):\r
-            return None\r
-        return iter(self.table.page.object_list)\r
-\r
-    def __iter__(self):\r
-        return iter(self.all())\r
-\r
-    def __len__(self):\r
-        return len(self.table.data)\r
-\r
-    def __getitem__(self, key):\r
-        if isinstance(key, slice):\r
-            result = list()\r
-            for row in self.table.data[key]:\r
-                result.append(self.row_class(self.table, row))\r
-            return result\r
-        elif isinstance(key, int):\r
-            return self.row_class(self.table, self.table.data[key])\r
-        else:\r
-            raise TypeError('Key must be a slice or integer.')\r
-\r
-\r
-class BaseTable(object):\r
-    """A collection of columns, plus their associated data rows.\r
-    """\r
-\r
-    __metaclass__ = DeclarativeColumnsMetaclass\r
-\r
-    rows_class = Rows\r
-\r
-    # this value is not the same as None. it means 'use the default sort\r
-    # order', which may (or may not) be inherited from the table options.\r
-    # None means 'do not sort the data', ignoring the default.\r
-    DefaultOrder = type('DefaultSortType', (), {})()\r
-\r
-    def __init__(self, data, order_by=DefaultOrder):\r
-        """Create a new table instance with the iterable ``data``.\r
-\r
-        If ``order_by`` is specified, the data will be sorted accordingly.\r
-        Otherwise, the sort order can be specified in the table options.\r
-\r
-        Note that unlike a ``Form``, tables are always bound to data. Also\r
-        unlike a form, the ``columns`` attribute is read-only and returns\r
-        ``BoundColum`` wrappers, similar to the ``BoundField``'s you get\r
-        when iterating over a form. This is because the table iterator\r
-        already yields rows, and we need an attribute via which to expose\r
-        the (visible) set of (bound) columns - ``Table.columns`` is simply\r
-        the perfect fit for this. Instead, ``base_colums`` is copied to\r
-        table instances, so modifying that will not touch the class-wide\r
-        column list.\r
-        """\r
-        self._data = data\r
-        self._snapshot = None      # will store output dataset (ordered...)\r
-        self._rows = self.rows_class(self)\r
-        self._columns = Columns(self)\r
-\r
-        # None is a valid order, so we must use DefaultOrder as a flag\r
-        # to fall back to the table sort order. set the attr via the\r
-        # property, to wrap it in an OrderByTuple before being stored\r
-        if order_by != BaseTable.DefaultOrder:\r
-            self.order_by = order_by\r
-\r
-        else:\r
-            self.order_by = self._meta.order_by\r
-\r
-        # Make a copy so that modifying this will not touch the class\r
-        # definition. Note that this is different from forms, where the\r
-        # copy is made available in a ``fields`` attribute. See the\r
-        # ``Table`` class docstring for more information.\r
-        self.base_columns = copy.deepcopy(type(self).base_columns)\r
-\r
-    def _reset_snapshot(self, reason):\r
-        """Called to reset the current snaptshot, for example when\r
-        options change that could affect it.\r
-\r
-        ``reason`` is given so that subclasses can decide that a\r
-        given change may not affect their snaptshot.\r
-        """\r
-        self._snapshot = None\r
-\r
-    def _build_snapshot(self):\r
-        """Rebuild the table for the current set of options.\r
-\r
-        Whenver the table options change, e.g. say a new sort order,\r
-        this method will be asked to regenerate the actual table from\r
-        the linked data source.\r
-\r
-        Subclasses should override this.\r
-        """\r
-        return self._data\r
-\r
-    def _get_data(self):\r
-        if self._snapshot is None:\r
-            self._snapshot = self._build_snapshot()\r
-        return self._snapshot\r
-    data = property(lambda s: s._get_data())\r
-\r
-    def _resolve_sort_directions(self, order_by):\r
-        """Given an ``order_by`` tuple, this will toggle the hyphen-prefixes\r
-        according to each column's ``direction`` option, e.g. it translates\r
-        between the ascending/descending and the straight/reverse terminology.\r
-        """\r
-        result = []\r
-        for inst in order_by:\r
-            if self.columns[rmprefix(inst)].column.direction == Column.DESC:\r
-                inst = toggleprefix(inst)\r
-            result.append(inst)\r
-        return result\r
-\r
-    def _cols_to_fields(self, names):\r
-        """Utility function. Given a list of column names (as exposed to\r
-        the user), converts column names to the names we have to use to\r
-        retrieve a column's data from the source.\r
-\r
-        Usually, the name used in the table declaration is used for accessing\r
-        the source (while a column can define an alias-like name that will\r
-        be used to refer to it from the "outside"). However, a column can\r
-        override this by giving a specific source field name via ``data``.\r
-\r
-        Supports prefixed column names as used e.g. in order_by ("-field").\r
-        """\r
-        result = []\r
-        for ident in names:\r
-            # handle order prefix\r
-            if ident[:1] == '-':\r
-                name = ident[1:]\r
-                prefix = '-'\r
-            else:\r
-                name = ident\r
-                prefix = ''\r
-            # find the field name\r
-            column = self.columns[name]\r
-            result.append(prefix + column.accessor)\r
-        return result\r
-\r
-    def _validate_column_name(self, name, purpose):\r
-        """Return True/False, depending on whether the column ``name`` is\r
-        valid for ``purpose``. Used to validate things like ``order_by``\r
-        instructions.\r
-\r
-        Can be overridden by subclasses to impose further restrictions.\r
-        """\r
-        if purpose == 'order_by':\r
-            return name in self.columns and\\r
-                   self.columns[name].sortable\r
-        else:\r
-            return True\r
-\r
-    def _set_order_by(self, value):\r
-        self._reset_snapshot('order_by')\r
-        # accept both string and tuple instructions\r
-        order_by = (isinstance(value, basestring) \\r
-            and [value.split(',')] \\r
-            or [value])[0]\r
-        if order_by:\r
-            # validate, remove all invalid order instructions\r
-            validated_order_by = []\r
-            for o in order_by:\r
-                if self._validate_column_name(rmprefix(o), "order_by"):\r
-                    validated_order_by.append(o)\r
-                elif not options.IGNORE_INVALID_OPTIONS:\r
-                    raise ValueError('Column name %s is invalid.' % o)\r
-            self._order_by = OrderByTuple(validated_order_by)\r
-        else:\r
-            self._order_by = OrderByTuple()\r
-\r
-    order_by = property(lambda s: s._order_by, _set_order_by)\r
-\r
-    def __unicode__(self):\r
-        return self.as_html()\r
-\r
-    def __iter__(self):\r
-        for row in self.rows:\r
-            yield row\r
-\r
-    def __getitem__(self, key):\r
-        return self.rows[key]\r
-\r
-    # just to make those readonly\r
-    columns = property(lambda s: s._columns)\r
-    rows = property(lambda s: s._rows)\r
-\r
-    def as_html(self):\r
-        pass\r
-\r
-    def update(self):\r
-        """Update the table based on it's current options.\r
-\r
-        Normally, you won't have to call this method, since the table\r
-        updates itself (it's caches) automatically whenever you change\r
-        any of the properties. However, in some rare cases those\r
-        changes might not be picked up, for example if you manually\r
-        change ``base_columns`` or any of the columns in it.\r
-        """\r
-        self._build_snapshot()\r
-\r
-    def paginate(self, klass, *args, **kwargs):\r
-        page = kwargs.pop('page', 1)\r
-        self.paginator = klass(self.rows, *args, **kwargs)\r
-        try:\r
-            self.page = self.paginator.page(page)\r
-        except paginator.InvalidPage, e:\r
-            raise Http404(str(e))\r
+import copy
+from django.http import Http404
+from django.core import paginator
+from django.utils.datastructures import SortedDict
+from django.utils.encoding import force_unicode, StrAndUnicode
+from django.utils.text import capfirst
+from columns import Column
+from options import options
+
+
+__all__ = ('BaseTable', 'options')
+
+
+class TableOptions(object):
+    def __init__(self, options=None):
+        super(TableOptions, self).__init__()
+        self.sortable = getattr(options, 'sortable', None)
+        self.order_by = getattr(options, 'order_by', None)
+
+
+class DeclarativeColumnsMetaclass(type):
+    """
+    Metaclass that converts Column attributes to a dictionary called
+    'base_columns', taking into account parent class 'base_columns'
+    as well.
+    """
+    def __new__(cls, name, bases, attrs, parent_cols_from=None):
+        """
+        The ``parent_cols_from`` argument determins from which attribute
+        we read the columns of a base class that this table might be
+        subclassing. This is useful for ``ModelTable`` (and possibly other
+        derivatives) which might want to differ between the declared columns
+        and others.
+
+        Note that if the attribute specified in ``parent_cols_from`` is not
+        found, we fall back to the default (``base_columns``), instead of
+        skipping over that base. This makes a table like the following work:
+
+            class MyNewTable(tables.ModelTable, MyNonModelTable):
+                pass
+
+        ``MyNewTable`` will be built by the ModelTable metaclass, which will
+        call this base with a modified ``parent_cols_from`` argument
+        specific to ModelTables. Since ``MyNonModelTable`` is not a
+        ModelTable, and thus does not provide that attribute, the columns
+        from that base class would otherwise be ignored.
+        """
+
+        # extract declared columns
+        columns = [(column_name, attrs.pop(column_name))
+           for column_name, obj in attrs.items()
+           if isinstance(obj, Column)]
+        columns.sort(lambda x, y: cmp(x[1].creation_counter,
+                                      y[1].creation_counter))
+
+        # If this class is subclassing other tables, add their fields as
+        # well. Note that we loop over the bases in *reverse* - this is
+        # necessary to preserve the correct order of columns.
+        for base in bases[::-1]:
+            col_attr = (parent_cols_from and hasattr(base, parent_cols_from)) \
+                and parent_cols_from\
+                or 'base_columns'
+            if hasattr(base, col_attr):
+                columns = getattr(base, col_attr).items() + columns
+        # Note that we are reusing an existing ``base_columns`` attribute.
+        # This is because in certain inheritance cases (mixing normal and
+        # ModelTables) this metaclass might be executed twice, and we need
+        # to avoid overriding previous data (because we pop() from attrs,
+        # the second time around columns might not be registered again).
+        # An example would be:
+        #    class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass
+        if not 'base_columns' in attrs:
+            attrs['base_columns'] = SortedDict()
+        attrs['base_columns'].update(SortedDict(columns))
+
+        attrs['_meta'] = TableOptions(attrs.get('Meta', None))
+        return type.__new__(cls, name, bases, attrs)
+
+
+def rmprefix(s):
+    """Normalize a column name by removing a potential sort prefix"""
+    return (s[:1]=='-' and [s[1:]] or [s])[0]
+
+def toggleprefix(s):
+    """Remove - prefix is existing, or add if missing."""
+    return ((s[:1] == '-') and [s[1:]] or ["-"+s])[0]
+
+class OrderByTuple(tuple, StrAndUnicode):
+        """Stores 'order by' instructions; Used to render output in a format
+        we understand as input (see __unicode__) - especially useful in
+        templates.
+
+        Also supports some functionality to interact with and modify
+        the order.
+        """
+        def __unicode__(self):
+            """Output in our input format."""
+            return ",".join(self)
+
+        def __contains__(self, name):
+            """Determine whether a column is part of this order."""
+            for o in self:
+                if rmprefix(o) == name:
+                    return True
+            return False
+
+        def is_reversed(self, name):
+            """Returns a bool indicating whether the column is ordered
+            reversed, None if it is missing."""
+            for o in self:
+                if o == '-'+name:
+                    return True
+            return False
+        def is_straight(self, name):
+            """The opposite of is_reversed."""
+            for o in self:
+                if o == name:
+                    return True
+            return False
+
+        def polarize(self, reverse, names=()):
+            """Return a new tuple with the columns from ``names`` set to
+            "reversed" (e.g. prefixed with a '-'). Note that the name is
+            ambiguous - do not confuse this with ``toggle()``.
+
+            If names is not specified, all columns are reversed. If a
+            column name is given that is currently not part of the order,
+            it is added.
+            """
+            prefix = reverse and '-' or ''
+            return OrderByTuple(
+                    [
+                      (
+                        # add either untouched, or reversed
+                        (names and rmprefix(o) not in names)
+                            and [o]
+                            or [prefix+rmprefix(o)]
+                      )[0]
+                    for o in self]
+                    +
+                    [prefix+name for name in names if not name in self]
+            )
+
+        def toggle(self, names=()):
+            """Return a new tuple with the columns from ``names`` toggled
+            with respect to their "reversed" state. E.g. a '-' prefix will
+            be removed is existing, or added if lacking. Do not confuse
+            with ``reverse()``.
+
+            If names is not specified, all columns are toggled. If a
+            column name is given that is currently not part of the order,
+            it is added in non-reverse form."""
+            return OrderByTuple(
+                    [
+                      (
+                        # add either untouched, or toggled
+                        (names and rmprefix(o) not in names)
+                            and [o]
+                            or ((o[:1] == '-') and [o[1:]] or ["-"+o])
+                      )[0]
+                    for o in self]
+                    +
+                    [name for name in names if not name in self]
+            )
+
+
+class Columns(object):
+    """Container for spawning BoundColumns.
+
+    This is bound to a table and provides it's ``columns`` property. It
+    provides access to those columns in different ways (iterator,
+    item-based, filtered and unfiltered etc)., stuff that would not be
+    possible with a simple iterator in the table class.
+
+    Note that when you define your column using a name override, e.g.
+    ``author_name = tables.Column(name="author")``, then the column will
+    be exposed by this container as "author", not "author_name".
+    """
+    def __init__(self, table):
+        self.table = table
+        self._columns = SortedDict()
+
+    def _reset(self):
+        """Used by parent table class."""
+        self._columns = SortedDict()
+
+    def _spawn_columns(self):
+        # (re)build the "_columns" cache of BoundColumn objects (note that
+        # ``base_columns`` might have changed since last time); creating
+        # BoundColumn instances can be costly, so we reuse existing ones.
+        new_columns = SortedDict()
+        for decl_name, column in self.table.base_columns.items():
+            # take into account name overrides
+            exposed_name = column.name or decl_name
+            if exposed_name in self._columns:
+                new_columns[exposed_name] = self._columns[exposed_name]
+            else:
+                new_columns[exposed_name] = BoundColumn(self.table, column, decl_name)
+        self._columns = new_columns
+
+    def all(self):
+        """Iterate through all columns, regardless of visiblity (as
+        opposed to ``__iter__``.
+
+        This is used internally a lot.
+        """
+        self._spawn_columns()
+        for column in self._columns.values():
+            yield column
+
+    def items(self):
+        self._spawn_columns()
+        for r in self._columns.items():
+            yield r
+
+    def names(self):
+        self._spawn_columns()
+        for r in self._columns.keys():
+            yield r
+
+    def index(self, name):
+        self._spawn_columns()
+        return self._columns.keyOrder.index(name)
+
+    def sortable(self):
+        """Iterate through all sortable columns.
+
+        This is primarily useful in templates, where iterating over the full
+        set and checking {% if column.sortable %} can be problematic in
+        conjunction with e.g. {{ forloop.last }} (the last column might not
+        be the actual last that is rendered).
+        """
+        for column in self.all():
+            if column.sortable:
+                yield column
+
+    def __iter__(self):
+        """Iterate through all *visible* bound columns.
+
+        This is primarily geared towards table rendering.
+        """
+        for column in self.all():
+            if column.visible:
+                yield column
+
+    def __contains__(self, item):
+        """Check by both column object and column name."""
+        self._spawn_columns()
+        if isinstance(item, basestring):
+            return item in self.names()
+        else:
+            return item in self.all()
+
+    def __len__(self):
+        self._spawn_columns()
+        return len([1 for c in self._columns.values() if c.visible])
+
+    def __getitem__(self, name):
+        """Return a column by name."""
+        self._spawn_columns()
+        return self._columns[name]
+
+
+class BoundColumn(StrAndUnicode):
+    """'Runtime' version of ``Column`` that is bound to a table instance,
+    and thus knows about the table's data.
+
+    Note that the name that is passed in tells us how this field is
+    delared in the bound table. The column itself can overwrite this name.
+    While the overwritten name will be hat mostly counts, we need to
+    remember the one used for declaration as well, or we won't know how
+    to read a column's value from the source.
+    """
+    def __init__(self, table, column, name):
+        self.table = table
+        self.column = column
+        self.declared_name = name
+        # expose some attributes of the column more directly
+        self.visible = column.visible
+
+    @property
+    def accessor(self):
+        """The key to use when accessing this column's values in the
+        source data.
+        """
+        return self.column.data if self.column.data else self.declared_name
+
+    def _get_sortable(self):
+        if self.column.sortable is not None:
+            return self.column.sortable
+        elif self.table._meta.sortable is not None:
+            return self.table._meta.sortable
+        else:
+            return True   # the default value
+    sortable = property(_get_sortable)
+
+    name = property(lambda s: s.column.name or s.declared_name)
+    name_reversed = property(lambda s: "-"+s.name)
+    def _get_name_toggled(self):
+        o = self.table.order_by
+        if (not self.name in o) or o.is_reversed(self.name): return self.name
+        else: return self.name_reversed
+    name_toggled = property(_get_name_toggled)
+
+    is_ordered = property(lambda s: s.name in s.table.order_by)
+    is_ordered_reverse = property(lambda s: s.table.order_by.is_reversed(s.name))
+    is_ordered_straight = property(lambda s: s.table.order_by.is_straight(s.name))
+    order_by = property(lambda s: s.table.order_by.polarize(False, [s.name]))
+    order_by_reversed = property(lambda s: s.table.order_by.polarize(True, [s.name]))
+    order_by_toggled = property(lambda s: s.table.order_by.toggle([s.name]))
+
+    def get_default(self, row):
+        """Since a column's ``default`` property may be a callable, we need
+        this function to resolve it when needed.
+
+        Make sure ``row`` is a ``BoundRow`` object, since that is what
+        we promise the callable will get.
+        """
+        if callable(self.column.default):
+            return self.column.default(row)
+        return self.column.default
+
+    def _get_values(self):
+        # TODO: build a list of values used
+        pass
+    values = property(_get_values)
+
+    def __unicode__(self):
+        s = self.column.verbose_name or self.name.replace('_', ' ')
+        return capfirst(force_unicode(s))
+
+    def as_html(self):
+        pass
+
+
+class BoundRow(object):
+    """Represents a single row of data, bound to a table.
+
+    Tables will spawn these row objects, wrapping around the actual data
+    stored in a row.
+    """
+    def __init__(self, table, data):
+        self.table = table
+        self.data = data
+
+    def __iter__(self):
+        for value in self.values:
+            yield value
+
+    def __getitem__(self, name):
+        """Returns this row's value for a column. All other access methods,
+        e.g. __iter__, lead ultimately to this."""
+
+        column = self.table.columns[name]
+
+        render_func = getattr(self.table, 'render_%s' % name, False)
+        if render_func:
+            return render_func(self.data)
+        else:
+            return self._default_render(column)
+
+    def _default_render(self, column):
+        """Returns a cell's content. This is used unless the user
+        provides a custom ``render_FOO`` method.
+        """
+        result = self.data[column.accessor]
+
+        # if the field we are pointing to is a callable, remove it
+        if callable(result):
+            result = result(self)
+        return result
+
+    def __contains__(self, item):
+        """Check by both row object and column name."""
+        if isinstance(item, basestring):
+            return item in self.table._columns
+        else:
+            return item in self
+
+    def _get_values(self):
+        for column in self.table.columns:
+            yield self[column.name]
+    values = property(_get_values)
+
+    def as_html(self):
+        pass
+
+
+class Rows(object):
+    """Container for spawning BoundRows.
+
+    This is bound to a table and provides it's ``rows`` property. It
+    provides functionality that would not be possible with a simple
+    iterator in the table class.
+    """
+
+    row_class = BoundRow
+
+    def __init__(self, table):
+        self.table = table
+
+    def _reset(self):
+        pass   # we currently don't use a cache
+
+    def all(self):
+        """Return all rows."""
+        for row in self.table.data:
+            yield self.row_class(self.table, row)
+
+    def page(self):
+        """Return rows on current page (if paginated)."""
+        if not hasattr(self.table, 'page'):
+            return None
+        return iter(self.table.page.object_list)
+
+    def __iter__(self):
+        return iter(self.all())
+
+    def __len__(self):
+        return len(self.table.data)
+
+    def __getitem__(self, key):
+        if isinstance(key, slice):
+            result = list()
+            for row in self.table.data[key]:
+                result.append(self.row_class(self.table, row))
+            return result
+        elif isinstance(key, int):
+            return self.row_class(self.table, self.table.data[key])
+        else:
+            raise TypeError('Key must be a slice or integer.')
+
+
+class BaseTable(object):
+    """A collection of columns, plus their associated data rows.
+    """
+
+    __metaclass__ = DeclarativeColumnsMetaclass
+
+    rows_class = Rows
+
+    # 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', (), {})()
+
+    def __init__(self, data, order_by=DefaultOrder):
+        """Create a new table instance with the iterable ``data``.
+
+        If ``order_by`` is specified, the data will be sorted accordingly.
+        Otherwise, the sort order can be specified in the table options.
+
+        Note that unlike a ``Form``, tables are always bound to data. Also
+        unlike a form, the ``columns`` attribute is read-only and returns
+        ``BoundColum`` wrappers, similar to the ``BoundField``'s you get
+        when iterating over a form. This is because the table iterator
+        already yields rows, and we need an attribute via which to expose
+        the (visible) set of (bound) columns - ``Table.columns`` is simply
+        the perfect fit for this. Instead, ``base_colums`` is copied to
+        table instances, so modifying that will not touch the class-wide
+        column list.
+        """
+        self._data = data
+        self._snapshot = None      # will store output dataset (ordered...)
+        self._rows = self.rows_class(self)
+        self._columns = Columns(self)
+
+        # None is a valid order, so we must use DefaultOrder as a flag
+        # to fall back to the table sort order. set the attr via the
+        # property, to wrap it in an OrderByTuple before being stored
+        if order_by != BaseTable.DefaultOrder:
+            self.order_by = order_by
+
+        else:
+            self.order_by = self._meta.order_by
+
+        # 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
+        # ``Table`` class docstring for more information.
+        self.base_columns = copy.deepcopy(type(self).base_columns)
+
+    def _reset_snapshot(self, reason):
+        """Called to reset the current snaptshot, for example when
+        options change that could affect it.
+
+        ``reason`` is given so that subclasses can decide that a
+        given change may not affect their snaptshot.
+        """
+        self._snapshot = None
+
+    def _build_snapshot(self):
+        """Rebuild the table for the current set of options.
+
+        Whenver the table options change, e.g. say a new sort order,
+        this method will be asked to regenerate the actual table from
+        the linked data source.
+
+        Subclasses should override this.
+        """
+        return self._data
+
+    def _get_data(self):
+        if self._snapshot is None:
+            self._snapshot = self._build_snapshot()
+        return self._snapshot
+    data = property(lambda s: s._get_data())
+
+    def _resolve_sort_directions(self, order_by):
+        """Given an ``order_by`` tuple, this will toggle the hyphen-prefixes
+        according to each column's ``direction`` option, e.g. it translates
+        between the ascending/descending and the straight/reverse terminology.
+        """
+        result = []
+        for inst in order_by:
+            if self.columns[rmprefix(inst)].column.direction == Column.DESC:
+                inst = toggleprefix(inst)
+            result.append(inst)
+        return result
+
+    def _cols_to_fields(self, names):
+        """Utility function. Given a list of column names (as exposed to
+        the user), converts column names to the names we have to use to
+        retrieve a column's data from the source.
+
+        Usually, the name used in the table declaration is used for accessing
+        the source (while a column can define an alias-like name that will
+        be used to refer to it from the "outside"). However, a column can
+        override this by giving a specific source field name via ``data``.
+
+        Supports prefixed column names as used e.g. in order_by ("-field").
+        """
+        result = []
+        for ident in names:
+            # handle order prefix
+            if ident[:1] == '-':
+                name = ident[1:]
+                prefix = '-'
+            else:
+                name = ident
+                prefix = ''
+            # find the field name
+            column = self.columns[name]
+            result.append(prefix + column.accessor)
+        return result
+
+    def _validate_column_name(self, name, purpose):
+        """Return True/False, depending on whether the column ``name`` is
+        valid for ``purpose``. Used to validate things like ``order_by``
+        instructions.
+
+        Can be overridden by subclasses to impose further restrictions.
+        """
+        if purpose == 'order_by':
+            return name in self.columns and\
+                   self.columns[name].sortable
+        else:
+            return True
+
+    def _set_order_by(self, value):
+        self._reset_snapshot('order_by')
+        # accept both string and tuple instructions
+        order_by = (isinstance(value, basestring) \
+            and [value.split(',')] \
+            or [value])[0]
+        if order_by:
+            # validate, remove all invalid order instructions
+            validated_order_by = []
+            for o in order_by:
+                if self._validate_column_name(rmprefix(o), "order_by"):
+                    validated_order_by.append(o)
+                elif not options.IGNORE_INVALID_OPTIONS:
+                    raise ValueError('Column name %s is invalid.' % o)
+            self._order_by = OrderByTuple(validated_order_by)
+        else:
+            self._order_by = OrderByTuple()
+
+    order_by = property(lambda s: s._order_by, _set_order_by)
+
+    def __unicode__(self):
+        return self.as_html()
+
+    def __iter__(self):
+        for row in self.rows:
+            yield row
+
+    def __getitem__(self, key):
+        return self.rows[key]
+
+    # just to make those readonly
+    columns = property(lambda s: s._columns)
+    rows = property(lambda s: s._rows)
+
+    def as_html(self):
+        pass
+
+    def update(self):
+        """Update the table based on it's current options.
+
+        Normally, you won't have to call this method, since the table
+        updates itself (it's caches) automatically whenever you change
+        any of the properties. However, in some rare cases those
+        changes might not be picked up, for example if you manually
+        change ``base_columns`` or any of the columns in it.
+        """
+        self._build_snapshot()
+
+    def paginate(self, klass, *args, **kwargs):
+        page = kwargs.pop('page', 1)
+        self.paginator = klass(self.rows, *args, **kwargs)
+        try:
+            self.page = self.paginator.page(page)
+        except paginator.InvalidPage, e:
+            raise Http404(str(e))
index 571502925629b89cf8534ee158862c3f8fa3e6c1..70ae897df7fefed8dc94a88a54081e263f395f00 100644 (file)
@@ -1,85 +1,85 @@
-__all__ = (\r
-    'Column', 'TextColumn', 'NumberColumn',\r
-)\r
-\r
-class Column(object):\r
-    """Represents a single column of a table.\r
-\r
-    ``verbose_name`` defines a display name for this column used for output.\r
-\r
-    ``name`` is the internal name of the column. Normally you don't need to\r
-    specify this, as the attribute that you make the column available under\r
-    is used. However, in certain circumstances it can be useful to override\r
-    this default, e.g. when using ModelTables if you want a column to not\r
-    use the model field name.\r
-\r
-    ``default`` is the default value for this column. If the data source\r
-    does provide ``None`` for a row, the default will be used instead. Note\r
-    that whether this effects ordering might depend on the table type (model\r
-    or normal). Also, you can specify a callable, which will be passed a\r
-    ``BoundRow`` instance and is expected to return the default to be used.\r
-\r
-    Additionally, you may specify ``data``. It works very much like\r
-    ``default``, except it's effect does not depend on the actual cell\r
-    value. When given a function, it will always be called with a row object,\r
-    expected to return the cell value. If given a string, that name will be\r
-    used to read the data from the source (instead of the column's name).\r
-\r
-    Note the interaction with ``default``. If ``default`` is specified as\r
-    well, it will be used whenver ``data`` yields in a None value.\r
-\r
-    You can use ``visible`` to flag the column as hidden by default.\r
-    However, this can be overridden by the ``visibility`` argument to the\r
-    table constructor. If you want to make the column completely unavailable\r
-    to the user, set ``inaccessible`` to True.\r
-\r
-    Setting ``sortable`` to False will result in this column being unusable\r
-    in ordering. You can further change the *default* sort direction to\r
-    descending using ``direction``. Note that this option changes the actual\r
-    direction only indirectly. Normal und reverse order, the terms\r
-    django-tables exposes, now simply mean different things.\r
-    """\r
-\r
-    ASC = 1\r
-    DESC = 2\r
-\r
-    # Tracks each time a Column instance is created. Used to retain order.\r
-    creation_counter = 0\r
-\r
-    def __init__(self, verbose_name=None, name=None, default=None, data=None,\r
-                 visible=True, inaccessible=False, sortable=None,\r
-                 direction=ASC):\r
-        self.verbose_name = verbose_name\r
-        self.name = name\r
-        self.default = default\r
-        self.data = data\r
-        if callable(self.data):\r
-            raise DeprecationWarning(('The Column "data" argument may no '+\r
-                                      'longer be a callable. Add  a '+\r
-                                      '``render_%s`` method to your '+\r
-                                      'table instead.') % (name or 'FOO'))\r
-        self.visible = visible\r
-        self.inaccessible = inaccessible\r
-        self.sortable = sortable\r
-        self.direction = direction\r
-\r
-        self.creation_counter = Column.creation_counter\r
-        Column.creation_counter += 1\r
-\r
-    def _set_direction(self, value):\r
-        if isinstance(value, basestring):\r
-            if value in ('asc', 'desc'):\r
-                self._direction = (value == 'asc') and Column.ASC or Column.DESC\r
-            else:\r
-                raise ValueError('Invalid direction value: %s' % value)\r
-        else:\r
-            self._direction = value\r
-\r
-    direction = property(lambda s: s._direction, _set_direction)\r
-\r
-\r
-class TextColumn(Column):\r
-    pass\r
-\r
-class NumberColumn(Column):\r
+__all__ = (
+    'Column', 'TextColumn', 'NumberColumn',
+)
+
+class Column(object):
+    """Represents a single column of a table.
+
+    ``verbose_name`` defines a display name for this column used for output.
+
+    ``name`` is the internal name of the column. Normally you don't need to
+    specify this, as the attribute that you make the column available under
+    is used. However, in certain circumstances it can be useful to override
+    this default, e.g. when using ModelTables if you want a column to not
+    use the model field name.
+
+    ``default`` is the default value for this column. If the data source
+    does provide ``None`` for a row, the default will be used instead. Note
+    that whether this effects ordering might depend on the table type (model
+    or normal). Also, you can specify a callable, which will be passed a
+    ``BoundRow`` instance and is expected to return the default to be used.
+
+    Additionally, you may specify ``data``. It works very much like
+    ``default``, except it's effect does not depend on the actual cell
+    value. When given a function, it will always be called with a row object,
+    expected to return the cell value. If given a string, that name will be
+    used to read the data from the source (instead of the column's name).
+
+    Note the interaction with ``default``. If ``default`` is specified as
+    well, it will be used whenver ``data`` yields in a None value.
+
+    You can use ``visible`` to flag the column as hidden by default.
+    However, this can be overridden by the ``visibility`` argument to the
+    table constructor. If you want to make the column completely unavailable
+    to the user, set ``inaccessible`` to True.
+
+    Setting ``sortable`` to False will result in this column being unusable
+    in ordering. You can further change the *default* sort direction to
+    descending using ``direction``. Note that this option changes the actual
+    direction only indirectly. Normal und reverse order, the terms
+    django-tables exposes, now simply mean different things.
+    """
+
+    ASC = 1
+    DESC = 2
+
+    # Tracks each time a Column instance is created. Used to retain order.
+    creation_counter = 0
+
+    def __init__(self, verbose_name=None, name=None, default=None, data=None,
+                 visible=True, inaccessible=False, sortable=None,
+                 direction=ASC):
+        self.verbose_name = verbose_name
+        self.name = name
+        self.default = default
+        self.data = data
+        if callable(self.data):
+            raise DeprecationWarning(('The Column "data" argument may no '+
+                                      'longer be a callable. Add  a '+
+                                      '``render_%s`` method to your '+
+                                      'table instead.') % (name or 'FOO'))
+        self.visible = visible
+        self.inaccessible = inaccessible
+        self.sortable = sortable
+        self.direction = direction
+
+        self.creation_counter = Column.creation_counter
+        Column.creation_counter += 1
+
+    def _set_direction(self, value):
+        if isinstance(value, basestring):
+            if value in ('asc', 'desc'):
+                self._direction = (value == 'asc') and Column.ASC or Column.DESC
+            else:
+                raise ValueError('Invalid direction value: %s' % value)
+        else:
+            self._direction = value
+
+    direction = property(lambda s: s._direction, _set_direction)
+
+
+class TextColumn(Column):
+    pass
+
+class NumberColumn(Column):
     pass
\ No newline at end of file
index 9ec883c2cc302df4f6adfefd60a4d9bb0acebf88..85fb830ac0973ab2198cf35938d54fbf8d0e8ff0 100644 (file)
@@ -1,3 +1,3 @@
-# make django-tables available for import for tests\r
-import os, sys\r
+# make django-tables available for import for tests
+import os, sys
 sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
\ No newline at end of file
index cfb2bfe05c77816caf6c84383c0d453340d715cc..315a6978a219800a9090ec125a3880ba2f0a668f 100644 (file)
-"""Test the memory table functionality.\r
-\r
-TODO: A bunch of those tests probably fit better into test_basic, since\r
-they aren't really MemoryTable specific.\r
-"""\r
-\r
-from math import sqrt\r
-from nose.tools import assert_raises\r
-from django.core.paginator import Paginator\r
-import django_tables as tables\r
-\r
-\r
-def test_basic():\r
-    class StuffTable(tables.MemoryTable):\r
-        name = tables.Column()\r
-        answer = tables.Column(default=42)\r
-        c = tables.Column(name="count", default=1)\r
-        email = tables.Column(data="@")\r
-    stuff = StuffTable([\r
-        {'id': 1, 'name': 'Foo Bar', '@': 'foo@bar.org'},\r
-    ])\r
-\r
-    # access without order_by works\r
-    stuff.data\r
-    stuff.rows\r
-\r
-    # make sure BoundColumnn.name always gives us the right thing, whether\r
-    # the column explicitely defines a name or not.\r
-    stuff.columns['count'].name == 'count'\r
-    stuff.columns['answer'].name == 'answer'\r
-\r
-    for r in stuff.rows:\r
-        # unknown fields are removed/not-accessible\r
-        assert 'name' in r\r
-        assert not 'id' in r\r
-        # missing data is available as default\r
-        assert 'answer' in r\r
-        assert r['answer'] == 42   # note: different from prev. line!\r
-\r
-        # all that still works when name overrides are used\r
-        assert not 'c' in r\r
-        assert 'count' in r\r
-        assert r['count'] == 1\r
-\r
-        # columns with data= option work fine\r
-        assert r['email'] == 'foo@bar.org'\r
-\r
-    # try to splice rows by index\r
-    assert 'name' in stuff.rows[0]\r
-    assert isinstance(stuff.rows[0:], list)\r
-\r
-    # [bug] splicing the table gives us valid, working rows\r
-    assert list(stuff[0]) == list(stuff.rows[0])\r
-    assert stuff[0]['name'] == 'Foo Bar'\r
-\r
-    # changing an instance's base_columns does not change the class\r
-    assert id(stuff.base_columns) != id(StuffTable.base_columns)\r
-    stuff.base_columns['test'] = tables.Column()\r
-    assert not 'test' in StuffTable.base_columns\r
-\r
-    # optionally, exceptions can be raised when input is invalid\r
-    tables.options.IGNORE_INVALID_OPTIONS = False\r
-    try:\r
-        assert_raises(ValueError, setattr, stuff, 'order_by', '-name,made-up-column')\r
-        assert_raises(ValueError, setattr, stuff, 'order_by', ('made-up-column',))\r
-        # when a column name is overwritten, the original won't work anymore\r
-        assert_raises(ValueError, setattr, stuff, 'order_by', 'c')\r
-        # reset for future tests\r
-    finally:\r
-        tables.options.IGNORE_INVALID_OPTIONS = True\r
-\r
-\r
-class TestRender:\r
-    """Test use of the render_* methods.\r
-    """\r
-\r
-    def test(self):\r
-        class TestTable(tables.MemoryTable):\r
-            private_name = tables.Column(name='public_name')\r
-            def render_public_name(self, data):\r
-                # We are given the actual data dict and have direct access\r
-                # to additional values for which no field is defined.\r
-                return "%s:%s" % (data['private_name'], data['additional'])\r
-\r
-        table = TestTable([{'private_name': 'FOO', 'additional': 'BAR'}])\r
-        assert table.rows[0]['public_name'] == 'FOO:BAR'\r
-\r
-    def test_not_sorted(self):\r
-        """The render methods are not considered when sorting.\r
-        """\r
-        class TestTable(tables.MemoryTable):\r
-            foo = tables.Column()\r
-            def render_foo(self, data):\r
-                return -data['foo']  # try to cause a reverse sort\r
-        table = TestTable([{'foo': 1}, {'foo': 2}], order_by='asc')\r
-        # Result is properly sorted, and the render function has never been called\r
-        assert [r['foo'] for r in table.rows] == [-1, -2]\r
-\r
-\r
-def test_caches():\r
-    """Ensure the various caches are effective.\r
-    """\r
-\r
-    class BookTable(tables.MemoryTable):\r
-        name = tables.Column()\r
-        answer = tables.Column(default=42)\r
-    books = BookTable([\r
-        {'name': 'Foo: Bar'},\r
-    ])\r
-\r
-    assert id(list(books.columns)[0]) == id(list(books.columns)[0])\r
-    # TODO: row cache currently not used\r
-    #assert id(list(books.rows)[0]) == id(list(books.rows)[0])\r
-\r
-    # test that caches are reset after an update()\r
-    old_column_cache = id(list(books.columns)[0])\r
-    old_row_cache = id(list(books.rows)[0])\r
-    books.update()\r
-    assert id(list(books.columns)[0]) != old_column_cache\r
-    assert id(list(books.rows)[0]) != old_row_cache\r
-\r
-def test_meta_sortable():\r
-    """Specific tests for sortable table meta option."""\r
-\r
-    def mktable(default_sortable):\r
-        class BookTable(tables.MemoryTable):\r
-            id = tables.Column(sortable=True)\r
-            name = tables.Column(sortable=False)\r
-            author = tables.Column()\r
-            class Meta:\r
-                sortable = default_sortable\r
-        return BookTable([])\r
-\r
-    global_table = mktable(None)\r
-    for default_sortable, results in (\r
-        (None,      (True, False, True)),    # last bool is global default\r
-        (True,      (True, False, True)),    # last bool is table default\r
-        (False,     (True, False, False)),   # last bool is table default\r
-    ):\r
-        books = mktable(default_sortable)\r
-        assert [c.sortable for c in books.columns] == list(results)\r
-\r
-        # it also works if the meta option is manually changed after\r
-        # class and instance creation\r
-        global_table._meta.sortable = default_sortable\r
-        assert [c.sortable for c in global_table.columns] == list(results)\r
-\r
-\r
-def test_sort():\r
-    class BookTable(tables.MemoryTable):\r
-        id = tables.Column(direction='desc')\r
-        name = tables.Column()\r
-        pages = tables.Column(name='num_pages')  # test rewritten names\r
-        language = tables.Column(default='en')   # default affects sorting\r
-        rating = tables.Column(data='*')         # test data field option\r
-\r
-    books = BookTable([\r
-        {'id': 1, 'pages':  60, 'name': 'Z: The Book', '*': 5},    # language: en\r
-        {'id': 2, 'pages': 100, 'language': 'de', 'name': 'A: The Book', '*': 2},\r
-        {'id': 3, 'pages':  80, 'language': 'de', 'name': 'A: The Book, Vol. 2', '*': 4},\r
-        {'id': 4, 'pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'},   # rating (with data option) is missing\r
-    ])\r
-\r
-    # None is normalized to an empty order by tuple, ensuring iterability;\r
-    # it also supports all the cool methods that we offer for order_by.\r
-    # This is true for the default case...\r
-    assert books.order_by == ()\r
-    iter(books.order_by)\r
-    assert hasattr(books.order_by, 'toggle')\r
-    # ...as well as when explicitly set to None.\r
-    books.order_by = None\r
-    assert books.order_by == ()\r
-    iter(books.order_by)\r
-    assert hasattr(books.order_by, 'toggle')\r
-\r
-    # test various orderings\r
-    def test_order(order, result):\r
-        books.order_by = order\r
-        assert [b['id'] for b in books.rows] == result\r
-    test_order(('num_pages',), [1,3,2,4])\r
-    test_order(('-num_pages',), [4,2,3,1])\r
-    test_order(('name',), [2,4,3,1])\r
-    test_order(('language', 'num_pages'), [3,2,1,4])\r
-    # using a simple string (for convinience as well as querystring passing\r
-    test_order('-num_pages', [4,2,3,1])\r
-    test_order('language,num_pages', [3,2,1,4])\r
-    # if overwritten, the declared fieldname has no effect\r
-    test_order('pages,name', [2,4,3,1])   # == ('name',)\r
-    # sort by column with "data" option\r
-    test_order('rating', [4,2,3,1])\r
-\r
-    # test the column with a default ``direction`` set to descending\r
-    test_order('id', [4,3,2,1])\r
-    test_order('-id', [1,2,3,4])\r
-    # changing the direction afterwards is fine too\r
-    books.base_columns['id'].direction = 'asc'\r
-    test_order('id', [1,2,3,4])\r
-    test_order('-id', [4,3,2,1])\r
-    # a invalid direction string raises an exception\r
-    assert_raises(ValueError, setattr, books.base_columns['id'], 'direction', 'blub')\r
-\r
-    # [bug] test alternative order formats if passed to constructor\r
-    BookTable([], 'language,-num_pages')\r
-\r
-    # test invalid order instructions\r
-    books.order_by = 'xyz'\r
-    assert not books.order_by\r
-    books.base_columns['language'].sortable = False\r
-    books.order_by = 'language'\r
-    assert not books.order_by\r
-    test_order(('language', 'num_pages'), [1,3,2,4])  # as if: 'num_pages'\r
-\r
-    # [bug] order_by did not run through setter when passed to init\r
-    books = BookTable([], order_by='name')\r
-    assert books.order_by == ('name',)\r
-\r
-    # test table.order_by extensions\r
-    books.order_by = ''\r
-    assert books.order_by.polarize(False) == ()\r
-    assert books.order_by.polarize(True) == ()\r
-    assert books.order_by.toggle() == ()\r
-    assert books.order_by.polarize(False, ['id']) == ('id',)\r
-    assert books.order_by.polarize(True, ['id']) == ('-id',)\r
-    assert books.order_by.toggle(['id']) == ('id',)\r
-    books.order_by = 'id,-name'\r
-    assert books.order_by.polarize(False, ['name']) == ('id', 'name')\r
-    assert books.order_by.polarize(True, ['name']) == ('id', '-name')\r
-    assert books.order_by.toggle(['name']) == ('id', 'name')\r
-    # ``in`` operator works\r
-    books.order_by = 'name'\r
-    assert 'name' in books.order_by\r
-    books.order_by = '-name'\r
-    assert 'name' in books.order_by\r
-    assert not 'language' in books.order_by\r
-\r
-\r
-def test_callable():\r
-    """Data fields and the ``default`` option can be callables.\r
-    """\r
-\r
-    class MathTable(tables.MemoryTable):\r
-        lhs = tables.Column()\r
-        rhs = tables.Column()\r
-        op = tables.Column(default='+')\r
-        sum = tables.Column(default=lambda d: calc(d['op'], d['lhs'], d['rhs']))\r
-\r
-    math = MathTable([\r
-        {'lhs': 1, 'rhs': lambda x: x['lhs']*3},              # 1+3\r
-        {'lhs': 9, 'rhs': lambda x: x['lhs'], 'op': '/'},     # 9/9\r
-        {'lhs': lambda x: x['rhs']+3, 'rhs': 4, 'op': '-'},   # 7-4\r
-    ])\r
-\r
-    # function is called when queried\r
-    def calc(op, lhs, rhs):\r
-        if op == '+': return lhs+rhs\r
-        elif op == '/': return lhs/rhs\r
-        elif op == '-': return lhs-rhs\r
-    assert [calc(row['op'], row['lhs'], row['rhs']) for row in math] == [4,1,3]\r
-\r
-    # field function is called while sorting\r
-    math.order_by = ('-rhs',)\r
-    assert [row['rhs'] for row in math] == [9,4,3]\r
-\r
-    # default function is called while sorting\r
-    math.order_by = ('sum',)\r
-    assert [row['sum'] for row in math] == [1,3,4]\r
-\r
-\r
-# TODO: all the column stuff might warrant it's own test file\r
-def test_columns():\r
-    """Test Table.columns container functionality.\r
-    """\r
-\r
-    class BookTable(tables.MemoryTable):\r
-        id = tables.Column(sortable=False, visible=False)\r
-        name = tables.Column(sortable=True)\r
-        pages = tables.Column(sortable=True)\r
-        language = tables.Column(sortable=False)\r
-    books = BookTable([])\r
-\r
-    assert list(books.columns.sortable()) == [c for c in books.columns if c.sortable]\r
-\r
-    # .columns iterator only yields visible columns\r
-    assert len(list(books.columns)) == 3\r
-    # visiblity of columns can be changed at instance-time\r
-    books.columns['id'].visible = True\r
-    assert len(list(books.columns)) == 4\r
-\r
-\r
-def test_column_order():\r
-    """Test the order functionality of bound columns.\r
-    """\r
-\r
-    class BookTable(tables.MemoryTable):\r
-        id = tables.Column()\r
-        name = tables.Column()\r
-        pages = tables.Column()\r
-        language = tables.Column()\r
-    books = BookTable([])\r
-\r
-    # the basic name property is a no-brainer\r
-    books.order_by = ''\r
-    assert [c.name for c in books.columns] == ['id','name','pages','language']\r
-\r
-    # name_reversed will always reverse, no matter what\r
-    for test in ['', 'name', '-name']:\r
-        books.order_by = test\r
-        assert [c.name_reversed for c in books.columns] == ['-id','-name','-pages','-language']\r
-\r
-    # name_toggled will always toggle\r
-    books.order_by = ''\r
-    assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']\r
-    books.order_by = 'id'\r
-    assert [c.name_toggled for c in books.columns] == ['-id','name','pages','language']\r
-    books.order_by = '-name'\r
-    assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']\r
-    # other columns in an order_by will be dismissed\r
-    books.order_by = '-id,name'\r
-    assert [c.name_toggled for c in books.columns] == ['id','-name','pages','language']\r
-\r
-    # with multi-column order, this is slightly more complex\r
-    books.order_by =  ''\r
-    assert [str(c.order_by) for c in books.columns] == ['id','name','pages','language']\r
-    assert [str(c.order_by_reversed) for c in books.columns] == ['-id','-name','-pages','-language']\r
-    assert [str(c.order_by_toggled) for c in books.columns] == ['id','name','pages','language']\r
-    books.order_by =  'id'\r
-    assert [str(c.order_by) for c in books.columns] == ['id','id,name','id,pages','id,language']\r
-    assert [str(c.order_by_reversed) for c in books.columns] == ['-id','id,-name','id,-pages','id,-language']\r
-    assert [str(c.order_by_toggled) for c in books.columns] == ['-id','id,name','id,pages','id,language']\r
-    books.order_by =  '-pages,id'\r
-    assert [str(c.order_by) for c in books.columns] == ['-pages,id','-pages,id,name','pages,id','-pages,id,language']\r
-    assert [str(c.order_by_reversed) for c in books.columns] == ['-pages,-id','-pages,id,-name','-pages,id','-pages,id,-language']\r
-    assert [str(c.order_by_toggled) for c in books.columns] == ['-pages,-id','-pages,id,name','pages,id','-pages,id,language']\r
-\r
-    # querying whether a column is ordered is possible\r
-    books.order_by = ''\r
-    assert [c.is_ordered for c in books.columns] == [False, False, False, False]\r
-    books.order_by = 'name'\r
-    assert [c.is_ordered for c in books.columns] == [False, True, False, False]\r
-    assert [c.is_ordered_reverse for c in books.columns] == [False, False, False, False]\r
-    assert [c.is_ordered_straight for c in books.columns] == [False, True, False, False]\r
-    books.order_by = '-pages'\r
-    assert [c.is_ordered for c in books.columns] == [False, False, True, False]\r
-    assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]\r
-    assert [c.is_ordered_straight for c in books.columns] == [False, False, False, False]\r
-    # and even works with multi-column ordering\r
-    books.order_by = 'id,-pages'\r
-    assert [c.is_ordered for c in books.columns] == [True, False, True, False]\r
-    assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]\r
+"""Test the memory table functionality.
+
+TODO: A bunch of those tests probably fit better into test_basic, since
+they aren't really MemoryTable specific.
+"""
+
+from math import sqrt
+from nose.tools import assert_raises
+from django.core.paginator import Paginator
+import django_tables as tables
+
+
+def test_basic():
+    class StuffTable(tables.MemoryTable):
+        name = tables.Column()
+        answer = tables.Column(default=42)
+        c = tables.Column(name="count", default=1)
+        email = tables.Column(data="@")
+    stuff = StuffTable([
+        {'id': 1, 'name': 'Foo Bar', '@': 'foo@bar.org'},
+    ])
+
+    # access without order_by works
+    stuff.data
+    stuff.rows
+
+    # make sure BoundColumnn.name always gives us the right thing, whether
+    # the column explicitely defines a name or not.
+    stuff.columns['count'].name == 'count'
+    stuff.columns['answer'].name == 'answer'
+
+    for r in stuff.rows:
+        # unknown fields are removed/not-accessible
+        assert 'name' in r
+        assert not 'id' in r
+        # missing data is available as default
+        assert 'answer' in r
+        assert r['answer'] == 42   # note: different from prev. line!
+
+        # all that still works when name overrides are used
+        assert not 'c' in r
+        assert 'count' in r
+        assert r['count'] == 1
+
+        # columns with data= option work fine
+        assert r['email'] == 'foo@bar.org'
+
+    # try to splice rows by index
+    assert 'name' in stuff.rows[0]
+    assert isinstance(stuff.rows[0:], list)
+
+    # [bug] splicing the table gives us valid, working rows
+    assert list(stuff[0]) == list(stuff.rows[0])
+    assert stuff[0]['name'] == 'Foo Bar'
+
+    # changing an instance's base_columns does not change the class
+    assert id(stuff.base_columns) != id(StuffTable.base_columns)
+    stuff.base_columns['test'] = tables.Column()
+    assert not 'test' in StuffTable.base_columns
+
+    # optionally, exceptions can be raised when input is invalid
+    tables.options.IGNORE_INVALID_OPTIONS = False
+    try:
+        assert_raises(ValueError, setattr, stuff, 'order_by', '-name,made-up-column')
+        assert_raises(ValueError, setattr, stuff, 'order_by', ('made-up-column',))
+        # when a column name is overwritten, the original won't work anymore
+        assert_raises(ValueError, setattr, stuff, 'order_by', 'c')
+        # reset for future tests
+    finally:
+        tables.options.IGNORE_INVALID_OPTIONS = True
+
+
+class TestRender:
+    """Test use of the render_* methods.
+    """
+
+    def test(self):
+        class TestTable(tables.MemoryTable):
+            private_name = tables.Column(name='public_name')
+            def render_public_name(self, data):
+                # We are given the actual data dict and have direct access
+                # to additional values for which no field is defined.
+                return "%s:%s" % (data['private_name'], data['additional'])
+
+        table = TestTable([{'private_name': 'FOO', 'additional': 'BAR'}])
+        assert table.rows[0]['public_name'] == 'FOO:BAR'
+
+    def test_not_sorted(self):
+        """The render methods are not considered when sorting.
+        """
+        class TestTable(tables.MemoryTable):
+            foo = tables.Column()
+            def render_foo(self, data):
+                return -data['foo']  # try to cause a reverse sort
+        table = TestTable([{'foo': 1}, {'foo': 2}], order_by='asc')
+        # Result is properly sorted, and the render function has never been called
+        assert [r['foo'] for r in table.rows] == [-1, -2]
+
+
+def test_caches():
+    """Ensure the various caches are effective.
+    """
+
+    class BookTable(tables.MemoryTable):
+        name = tables.Column()
+        answer = tables.Column(default=42)
+    books = BookTable([
+        {'name': 'Foo: Bar'},
+    ])
+
+    assert id(list(books.columns)[0]) == id(list(books.columns)[0])
+    # TODO: row cache currently not used
+    #assert id(list(books.rows)[0]) == id(list(books.rows)[0])
+
+    # test that caches are reset after an update()
+    old_column_cache = id(list(books.columns)[0])
+    old_row_cache = id(list(books.rows)[0])
+    books.update()
+    assert id(list(books.columns)[0]) != old_column_cache
+    assert id(list(books.rows)[0]) != old_row_cache
+
+def test_meta_sortable():
+    """Specific tests for sortable table meta option."""
+
+    def mktable(default_sortable):
+        class BookTable(tables.MemoryTable):
+            id = tables.Column(sortable=True)
+            name = tables.Column(sortable=False)
+            author = tables.Column()
+            class Meta:
+                sortable = default_sortable
+        return BookTable([])
+
+    global_table = mktable(None)
+    for default_sortable, results in (
+        (None,      (True, False, True)),    # last bool is global default
+        (True,      (True, False, True)),    # last bool is table default
+        (False,     (True, False, False)),   # last bool is table default
+    ):
+        books = mktable(default_sortable)
+        assert [c.sortable for c in books.columns] == list(results)
+
+        # it also works if the meta option is manually changed after
+        # class and instance creation
+        global_table._meta.sortable = default_sortable
+        assert [c.sortable for c in global_table.columns] == list(results)
+
+
+def test_sort():
+    class BookTable(tables.MemoryTable):
+        id = tables.Column(direction='desc')
+        name = tables.Column()
+        pages = tables.Column(name='num_pages')  # test rewritten names
+        language = tables.Column(default='en')   # default affects sorting
+        rating = tables.Column(data='*')         # test data field option
+
+    books = BookTable([
+        {'id': 1, 'pages':  60, 'name': 'Z: The Book', '*': 5},    # language: en
+        {'id': 2, 'pages': 100, 'language': 'de', 'name': 'A: The Book', '*': 2},
+        {'id': 3, 'pages':  80, 'language': 'de', 'name': 'A: The Book, Vol. 2', '*': 4},
+        {'id': 4, 'pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'},   # rating (with data option) is missing
+    ])
+
+    # None is normalized to an empty order by tuple, ensuring iterability;
+    # it also supports all the cool methods that we offer for order_by.
+    # This is true for the default case...
+    assert books.order_by == ()
+    iter(books.order_by)
+    assert hasattr(books.order_by, 'toggle')
+    # ...as well as when explicitly set to None.
+    books.order_by = None
+    assert books.order_by == ()
+    iter(books.order_by)
+    assert hasattr(books.order_by, 'toggle')
+
+    # test various orderings
+    def test_order(order, result):
+        books.order_by = order
+        assert [b['id'] for b in books.rows] == result
+    test_order(('num_pages',), [1,3,2,4])
+    test_order(('-num_pages',), [4,2,3,1])
+    test_order(('name',), [2,4,3,1])
+    test_order(('language', 'num_pages'), [3,2,1,4])
+    # using a simple string (for convinience as well as querystring passing
+    test_order('-num_pages', [4,2,3,1])
+    test_order('language,num_pages', [3,2,1,4])
+    # if overwritten, the declared fieldname has no effect
+    test_order('pages,name', [2,4,3,1])   # == ('name',)
+    # sort by column with "data" option
+    test_order('rating', [4,2,3,1])
+
+    # test the column with a default ``direction`` set to descending
+    test_order('id', [4,3,2,1])
+    test_order('-id', [1,2,3,4])
+    # changing the direction afterwards is fine too
+    books.base_columns['id'].direction = 'asc'
+    test_order('id', [1,2,3,4])
+    test_order('-id', [4,3,2,1])
+    # a invalid direction string raises an exception
+    assert_raises(ValueError, setattr, books.base_columns['id'], 'direction', 'blub')
+
+    # [bug] test alternative order formats if passed to constructor
+    BookTable([], 'language,-num_pages')
+
+    # test invalid order instructions
+    books.order_by = 'xyz'
+    assert not books.order_by
+    books.base_columns['language'].sortable = False
+    books.order_by = 'language'
+    assert not books.order_by
+    test_order(('language', 'num_pages'), [1,3,2,4])  # as if: 'num_pages'
+
+    # [bug] order_by did not run through setter when passed to init
+    books = BookTable([], order_by='name')
+    assert books.order_by == ('name',)
+
+    # test table.order_by extensions
+    books.order_by = ''
+    assert books.order_by.polarize(False) == ()
+    assert books.order_by.polarize(True) == ()
+    assert books.order_by.toggle() == ()
+    assert books.order_by.polarize(False, ['id']) == ('id',)
+    assert books.order_by.polarize(True, ['id']) == ('-id',)
+    assert books.order_by.toggle(['id']) == ('id',)
+    books.order_by = 'id,-name'
+    assert books.order_by.polarize(False, ['name']) == ('id', 'name')
+    assert books.order_by.polarize(True, ['name']) == ('id', '-name')
+    assert books.order_by.toggle(['name']) == ('id', 'name')
+    # ``in`` operator works
+    books.order_by = 'name'
+    assert 'name' in books.order_by
+    books.order_by = '-name'
+    assert 'name' in books.order_by
+    assert not 'language' in books.order_by
+
+
+def test_callable():
+    """Data fields and the ``default`` option can be callables.
+    """
+
+    class MathTable(tables.MemoryTable):
+        lhs = tables.Column()
+        rhs = tables.Column()
+        op = tables.Column(default='+')
+        sum = tables.Column(default=lambda d: calc(d['op'], d['lhs'], d['rhs']))
+
+    math = MathTable([
+        {'lhs': 1, 'rhs': lambda x: x['lhs']*3},              # 1+3
+        {'lhs': 9, 'rhs': lambda x: x['lhs'], 'op': '/'},     # 9/9
+        {'lhs': lambda x: x['rhs']+3, 'rhs': 4, 'op': '-'},   # 7-4
+    ])
+
+    # function is called when queried
+    def calc(op, lhs, rhs):
+        if op == '+': return lhs+rhs
+        elif op == '/': return lhs/rhs
+        elif op == '-': return lhs-rhs
+    assert [calc(row['op'], row['lhs'], row['rhs']) for row in math] == [4,1,3]
+
+    # field function is called while sorting
+    math.order_by = ('-rhs',)
+    assert [row['rhs'] for row in math] == [9,4,3]
+
+    # default function is called while sorting
+    math.order_by = ('sum',)
+    assert [row['sum'] for row in math] == [1,3,4]
+
+
+# TODO: all the column stuff might warrant it's own test file
+def test_columns():
+    """Test Table.columns container functionality.
+    """
+
+    class BookTable(tables.MemoryTable):
+        id = tables.Column(sortable=False, visible=False)
+        name = tables.Column(sortable=True)
+        pages = tables.Column(sortable=True)
+        language = tables.Column(sortable=False)
+    books = BookTable([])
+
+    assert list(books.columns.sortable()) == [c for c in books.columns if c.sortable]
+
+    # .columns iterator only yields visible columns
+    assert len(list(books.columns)) == 3
+    # visiblity of columns can be changed at instance-time
+    books.columns['id'].visible = True
+    assert len(list(books.columns)) == 4
+
+
+def test_column_order():
+    """Test the order functionality of bound columns.
+    """
+
+    class BookTable(tables.MemoryTable):
+        id = tables.Column()
+        name = tables.Column()
+        pages = tables.Column()
+        language = tables.Column()
+    books = BookTable([])
+
+    # the basic name property is a no-brainer
+    books.order_by = ''
+    assert [c.name for c in books.columns] == ['id','name','pages','language']
+
+    # name_reversed will always reverse, no matter what
+    for test in ['', 'name', '-name']:
+        books.order_by = test
+        assert [c.name_reversed for c in books.columns] == ['-id','-name','-pages','-language']
+
+    # name_toggled will always toggle
+    books.order_by = ''
+    assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']
+    books.order_by = 'id'
+    assert [c.name_toggled for c in books.columns] == ['-id','name','pages','language']
+    books.order_by = '-name'
+    assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']
+    # other columns in an order_by will be dismissed
+    books.order_by = '-id,name'
+    assert [c.name_toggled for c in books.columns] == ['id','-name','pages','language']
+
+    # with multi-column order, this is slightly more complex
+    books.order_by =  ''
+    assert [str(c.order_by) for c in books.columns] == ['id','name','pages','language']
+    assert [str(c.order_by_reversed) for c in books.columns] == ['-id','-name','-pages','-language']
+    assert [str(c.order_by_toggled) for c in books.columns] == ['id','name','pages','language']
+    books.order_by =  'id'
+    assert [str(c.order_by) for c in books.columns] == ['id','id,name','id,pages','id,language']
+    assert [str(c.order_by_reversed) for c in books.columns] == ['-id','id,-name','id,-pages','id,-language']
+    assert [str(c.order_by_toggled) for c in books.columns] == ['-id','id,name','id,pages','id,language']
+    books.order_by =  '-pages,id'
+    assert [str(c.order_by) for c in books.columns] == ['-pages,id','-pages,id,name','pages,id','-pages,id,language']
+    assert [str(c.order_by_reversed) for c in books.columns] == ['-pages,-id','-pages,id,-name','-pages,id','-pages,id,-language']
+    assert [str(c.order_by_toggled) for c in books.columns] == ['-pages,-id','-pages,id,name','pages,id','-pages,id,language']
+
+    # querying whether a column is ordered is possible
+    books.order_by = ''
+    assert [c.is_ordered for c in books.columns] == [False, False, False, False]
+    books.order_by = 'name'
+    assert [c.is_ordered for c in books.columns] == [False, True, False, False]
+    assert [c.is_ordered_reverse for c in books.columns] == [False, False, False, False]
+    assert [c.is_ordered_straight for c in books.columns] == [False, True, False, False]
+    books.order_by = '-pages'
+    assert [c.is_ordered for c in books.columns] == [False, False, True, False]
+    assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]
+    assert [c.is_ordered_straight for c in books.columns] == [False, False, False, False]
+    # and even works with multi-column ordering
+    books.order_by = 'id,-pages'
+    assert [c.is_ordered for c in books.columns] == [True, False, True, False]
+    assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]
     assert [c.is_ordered_straight for c in books.columns] == [True, False, False, False]
\ No newline at end of file
index 300183adaf3815b9193ca9f5fb90b01aaa34fb32..c8973d636cec3b0b2fb46b90e9978557e107f812 100644 (file)
-"""Test ModelTable specific functionality.\r
-\r
-Sets up a temporary Django project using a memory SQLite database.\r
-"""\r
-\r
-from nose.tools import assert_raises, assert_equal\r
-from django.conf import settings\r
-from django.core.paginator import *\r
-import django_tables as tables\r
-\r
-\r
-def setup_module(module):\r
-    settings.configure(**{\r
-        'DATABASE_ENGINE': 'sqlite3',\r
-        'DATABASE_NAME': ':memory:',\r
-        'INSTALLED_APPS': ('tests.testapp',)\r
-    })\r
-\r
-    from django.db import models\r
-    from django.core.management import call_command\r
-\r
-    class City(models.Model):\r
-        name = models.TextField()\r
-        population = models.IntegerField(null=True)\r
-        class Meta:\r
-            app_label = 'testapp'\r
-    module.City = City\r
-\r
-    class Country(models.Model):\r
-        name = models.TextField()\r
-        population = models.IntegerField()\r
-        capital = models.ForeignKey(City, blank=True, null=True)\r
-        tld = models.TextField(verbose_name='Domain Extension', max_length=2)\r
-        system = models.TextField(blank=True, null=True)\r
-        null = models.TextField(blank=True, null=True)   # tests expect this to be always null!\r
-        null2 = models.TextField(blank=True, null=True)  #  - " -\r
-        def example_domain(self):\r
-            return 'example.%s' % self.tld\r
-        class Meta:\r
-            app_label = 'testapp'\r
-    module.Country = Country\r
-\r
-    # create the tables\r
-    call_command('syncdb', verbosity=1, interactive=False)\r
-\r
-    # create a couple of objects\r
-    berlin=City(name="Berlin"); berlin.save()\r
-    amsterdam=City(name="Amsterdam"); amsterdam.save()\r
-    Country(name="Austria", tld="au", population=8, system="republic").save()\r
-    Country(name="Germany", tld="de", population=81, capital=berlin).save()\r
-    Country(name="France", tld="fr", population=64, system="republic").save()\r
-    Country(name="Netherlands", tld="nl", population=16, system="monarchy", capital=amsterdam).save()\r
-\r
-\r
-class TestDeclaration:\r
-    """Test declaration, declared columns and default model field columns.\r
+"""Test ModelTable specific functionality.
+
+Sets up a temporary Django project using a memory SQLite database.
+"""
+
+from nose.tools import assert_raises, assert_equal
+from django.conf import settings
+from django.core.paginator import *
+import django_tables as tables
+
+
+def setup_module(module):
+    settings.configure(**{
+        'DATABASE_ENGINE': 'sqlite3',
+        'DATABASE_NAME': ':memory:',
+        'INSTALLED_APPS': ('tests.testapp',)
+    })
+
+    from django.db import models
+    from django.core.management import call_command
+
+    class City(models.Model):
+        name = models.TextField()
+        population = models.IntegerField(null=True)
+        class Meta:
+            app_label = 'testapp'
+    module.City = City
+
+    class Country(models.Model):
+        name = models.TextField()
+        population = models.IntegerField()
+        capital = models.ForeignKey(City, blank=True, null=True)
+        tld = models.TextField(verbose_name='Domain Extension', max_length=2)
+        system = models.TextField(blank=True, null=True)
+        null = models.TextField(blank=True, null=True)   # tests expect this to be always null!
+        null2 = models.TextField(blank=True, null=True)  #  - " -
+        def example_domain(self):
+            return 'example.%s' % self.tld
+        class Meta:
+            app_label = 'testapp'
+    module.Country = Country
+
+    # create the tables
+    call_command('syncdb', verbosity=1, interactive=False)
+
+    # create a couple of objects
+    berlin=City(name="Berlin"); berlin.save()
+    amsterdam=City(name="Amsterdam"); amsterdam.save()
+    Country(name="Austria", tld="au", population=8, system="republic").save()
+    Country(name="Germany", tld="de", population=81, capital=berlin).save()
+    Country(name="France", tld="fr", population=64, system="republic").save()
+    Country(name="Netherlands", tld="nl", population=16, system="monarchy", capital=amsterdam).save()
+
+
+class TestDeclaration:
+    """Test declaration, declared columns and default model field columns.
     """
 
-    def test_autogen_basic(self):\r
-        class CountryTable(tables.ModelTable):\r
-            class Meta:\r
-                model = Country\r
-\r
-        assert len(CountryTable.base_columns) == 8\r
-        assert 'name' in CountryTable.base_columns\r
-        assert not hasattr(CountryTable, 'name')\r
-\r
-        # Override one model column, add another custom one, exclude one\r
-        class CountryTable(tables.ModelTable):\r
-            capital = tables.TextColumn(verbose_name='Name of capital')\r
-            projected = tables.Column(verbose_name="Projected Population")\r
-            class Meta:\r
-                model = Country\r
-                exclude = ['tld']\r
-\r
-        assert len(CountryTable.base_columns) == 8\r
-        assert 'projected' in CountryTable.base_columns\r
-        assert 'capital' in CountryTable.base_columns\r
-        assert not 'tld' in CountryTable.base_columns\r
-\r
-        # Inheritance (with a different model) + field restrictions\r
-        class CityTable(CountryTable):\r
-            class Meta:\r
-                model = City\r
-                columns = ['id', 'name']\r
-                exclude = ['capital']\r
-\r
-        print CityTable.base_columns\r
-        assert len(CityTable.base_columns) == 4\r
-        assert 'id' in CityTable.base_columns\r
-        assert 'name' in CityTable.base_columns\r
-        assert 'projected' in CityTable.base_columns # declared in parent\r
-        assert not 'population' in CityTable.base_columns  # not in Meta:columns\r
+    def test_autogen_basic(self):
+        class CountryTable(tables.ModelTable):
+            class Meta:
+                model = Country
+
+        assert len(CountryTable.base_columns) == 8
+        assert 'name' in CountryTable.base_columns
+        assert not hasattr(CountryTable, 'name')
+
+        # Override one model column, add another custom one, exclude one
+        class CountryTable(tables.ModelTable):
+            capital = tables.TextColumn(verbose_name='Name of capital')
+            projected = tables.Column(verbose_name="Projected Population")
+            class Meta:
+                model = Country
+                exclude = ['tld']
+
+        assert len(CountryTable.base_columns) == 8
+        assert 'projected' in CountryTable.base_columns
+        assert 'capital' in CountryTable.base_columns
+        assert not 'tld' in CountryTable.base_columns
+
+        # Inheritance (with a different model) + field restrictions
+        class CityTable(CountryTable):
+            class Meta:
+                model = City
+                columns = ['id', 'name']
+                exclude = ['capital']
+
+        print CityTable.base_columns
+        assert len(CityTable.base_columns) == 4
+        assert 'id' in CityTable.base_columns
+        assert 'name' in CityTable.base_columns
+        assert 'projected' in CityTable.base_columns # declared in parent
+        assert not 'population' in CityTable.base_columns  # not in Meta:columns
         assert 'capital' in CityTable.base_columns  # in exclude, but only works on model fields (is that the right behaviour?)
 
     def test_columns_custom_order(self):
         """Using the columns meta option, you can also modify the ordering.
         """
         class CountryTable(tables.ModelTable):
-            foo = tables.Column()\r
-            class Meta:\r
+            foo = tables.Column()
+            class Meta:
                 model = Country
                 columns = ('system', 'population', 'foo', 'tld',)
 
-        assert [c.name for c in CountryTable().columns] == ['system', 'population', 'foo', 'tld']\r
-\r
-\r
-def test_basic():\r
-    """Some tests here are copied from ``test_basic.py`` but need to be\r
-    rerun with a ModelTable, as the implementation is different."""\r
-\r
-    class CountryTable(tables.ModelTable):\r
-        null = tables.Column(default="foo")\r
-        tld = tables.Column(name="domain")\r
-        class Meta:\r
-            model = Country\r
-            exclude = ('id',)\r
-    countries = CountryTable()\r
-\r
-    def test_country_table(table):\r
-        for r in table.rows:\r
-            # "normal" fields exist\r
-            assert 'name' in r\r
-            # unknown fields are removed/not accessible\r
-            assert not 'does-not-exist' in r\r
-            # ...so are excluded fields\r
-            assert not 'id' in r\r
-            # [bug] access to data that might be available, but does not\r
-            # have a corresponding column is denied.\r
-            assert_raises(Exception, "r['id']")\r
-            # missing data is available with default values\r
-            assert 'null' in r\r
-            assert r['null'] == "foo"   # note: different from prev. line!\r
-            # if everything else fails (no default), we get None back\r
-            assert r['null2'] is None\r
-\r
-            # all that still works when name overrides are used\r
-            assert not 'tld' in r\r
-            assert 'domain' in r\r
-            assert len(r['domain']) == 2   # valid country tld\r
-    test_country_table(countries)\r
-\r
-    # repeat the avove tests with a table that is not associated with a\r
-    # model, and all columns being created manually.\r
-    class CountryTable(tables.ModelTable):\r
-        name = tables.Column()\r
-        population = tables.Column()\r
-        capital = tables.Column()\r
-        system = tables.Column()\r
-        null = tables.Column(default="foo")\r
-        null2 = tables.Column()\r
-        tld = tables.Column(name="domain")\r
-    countries = CountryTable(Country)\r
-    test_country_table(countries)\r
-
-\r
-def test_caches():\r
-    """Make sure the caches work for model tables as well (parts are\r
-    reimplemented).\r
-    """\r
-    class CountryTable(tables.ModelTable):\r
-        class Meta:\r
-            model = Country\r
-            exclude = ('id',)\r
-    countries = CountryTable()\r
-\r
-    assert id(list(countries.columns)[0]) == id(list(countries.columns)[0])\r
-    # TODO: row cache currently not used\r
-    #assert id(list(countries.rows)[0]) == id(list(countries.rows)[0])\r
-\r
-    # test that caches are reset after an update()\r
-    old_column_cache = id(list(countries.columns)[0])\r
-    old_row_cache = id(list(countries.rows)[0])\r
-    countries.update()\r
-    assert id(list(countries.columns)[0]) != old_column_cache\r
-    assert id(list(countries.rows)[0]) != old_row_cache\r
-\r
-def test_sort():\r
-    class CountryTable(tables.ModelTable):\r
-        tld = tables.Column(name="domain")\r
-        population = tables.Column()\r
-        system = tables.Column(default="republic")\r
-        custom1 = tables.Column()\r
-        custom2 = tables.Column(sortable=True)\r
-        class Meta:\r
-            model = Country\r
-    countries = CountryTable()\r
-\r
-    def test_order(order, result, table=countries):\r
-        table.order_by = order\r
-        assert [r['id'] for r in table.rows] == result\r
-\r
-    # test various orderings\r
-    test_order(('population',), [1,4,3,2])\r
-    test_order(('-population',), [2,3,4,1])\r
-    test_order(('name',), [1,3,2,4])\r
-    # test sorting with a "rewritten" column name\r
-    countries.order_by = 'domain,tld'      # "tld" would be invalid...\r
-    countries.order_by == ('domain',)      # ...and is therefore removed\r
-    test_order(('-domain',), [4,3,2,1])\r
-    # test multiple order instructions; note: one row is missing a "system"\r
-    # value, but has a default set; however, that has no effect on sorting.\r
-    test_order(('system', '-population'), [2,4,3,1])\r
-    # using a simple string (for convinience as well as querystring passing)\r
-    test_order('-population', [2,3,4,1])\r
-    test_order('system,-population', [2,4,3,1])\r
-\r
-    # test column with a default ``direction`` set to descending\r
-    class CityTable(tables.ModelTable):\r
-        name = tables.Column(direction='desc')\r
-        class Meta:\r
-            model = City\r
-    cities = CityTable()\r
-    test_order('name', [1,2], table=cities)   # Berlin to Amsterdam\r
-    test_order('-name', [2,1], table=cities)  # Amsterdam to Berlin\r
-\r
-    # test invalid order instructions...\r
-    countries.order_by = 'invalid_field,population'\r
-    assert countries.order_by == ('population',)\r
-    # ...in case of ModelTables, this primarily means that only\r
-    # model-based colunns are currently sortable at all.\r
-    countries.order_by = ('custom1', 'custom2')\r
-    assert countries.order_by == ()\r
-\r
-def test_default_sort():\r
-    class SortedCountryTable(tables.ModelTable):\r
-        class Meta:\r
-            model = Country\r
-            order_by = '-name'\r
-\r
-    # the order_by option is provided by TableOptions\r
-    assert_equal('-name', SortedCountryTable()._meta.order_by)\r
-\r
-    # the default order can be inherited from the table\r
-    assert_equal(('-name',), SortedCountryTable().order_by)\r
-    assert_equal(4, SortedCountryTable().rows[0]['id'])\r
-\r
-    # and explicitly set (or reset) via __init__\r
-    assert_equal(2, SortedCountryTable(order_by='system').rows[0]['id'])\r
-    assert_equal(1, SortedCountryTable(order_by=None).rows[0]['id'])\r
-\r
-def test_callable():\r
-    """Some of the callable code is reimplemented for modeltables, so\r
-    test some specifics again.\r
-    """\r
-\r
-    class CountryTable(tables.ModelTable):\r
-        null = tables.Column(default=lambda s: s['example_domain'])\r
-        example_domain = tables.Column()\r
-        class Meta:\r
-            model = Country\r
-    countries = CountryTable(Country)\r
-\r
-    # model method is called\r
-    assert [row['example_domain'] for row in countries] == \\r
-                    ['example.'+row['tld'] for row in countries]\r
-\r
-    # column default method is called\r
-    assert [row['example_domain'] for row in countries] == \\r
-                    [row['null'] for row in countries]\r
-\r
-def test_relationships():\r
-    """Test relationship spanning."""\r
-\r
-    class CountryTable(tables.ModelTable):\r
-        # add relationship spanning columns (using different approaches)\r
-        capital_name = tables.Column(data='capital__name')\r
-        capital__population = tables.Column(name="capital_population")\r
-        invalid = tables.Column(data="capital__invalid")\r
-        class Meta:\r
-            model = Country\r
-    countries = CountryTable(Country.objects.select_related('capital'))\r
-\r
-    # ordering and field access works\r
-    countries.order_by = 'capital_name'\r
-    assert [row['capital_name'] for row in countries.rows] == \\r
-        [None, None, 'Amsterdam', 'Berlin']\r
-\r
-    countries.order_by = 'capital_population'\r
-    assert [row['capital_population'] for row in countries.rows] == \\r
-        [None, None, None, None]\r
-\r
-    # ordering by a column with an invalid relationship fails silently\r
-    countries.order_by = 'invalid'\r
-    assert countries.order_by == ()\r
-\r
-\r
-def test_pagination():\r
-    """Pretty much the same as static table pagination, but make sure we\r
-    provide the capability, at least for paginators that use it, to not\r
-    have the complete queryset loaded (by use of a count() query).\r
-\r
-    Note: This test changes the available cities, make sure it is last,\r
-    or that tests that follow are written appropriately.\r
-    """\r
-    from django.db import connection\r
-\r
-    class CityTable(tables.ModelTable):\r
-        class Meta:\r
-            model = City\r
-            columns = ['name']\r
-    cities = CityTable()\r
-\r
-    # add some sample data\r
-    City.objects.all().delete()\r
-    for i in range(1,101):\r
-        City.objects.create(name="City %d"%i)\r
-\r
-    # for query logging\r
-    settings.DEBUG = True\r
-\r
-    # external paginator\r
-    start_querycount = len(connection.queries)\r
-    paginator = Paginator(cities.rows, 10)\r
-    assert paginator.num_pages == 10\r
-    page = paginator.page(1)\r
-    assert len(page.object_list) == 10\r
-    assert page.has_previous() == False\r
-    assert page.has_next() == True\r
-    # Make sure the queryset is not loaded completely - there must be two\r
-    # queries, one a count(). This check is far from foolproof...\r
-    assert len(connection.queries)-start_querycount == 2\r
-\r
-    # using a queryset paginator is possible as well (although unnecessary)\r
-    paginator = QuerySetPaginator(cities.rows, 10)\r
-    assert paginator.num_pages == 10\r
-\r
-    # integrated paginator\r
-    start_querycount = len(connection.queries)\r
-    cities.paginate(Paginator, 10, page=1)\r
-    # rows is now paginated\r
-    assert len(list(cities.rows.page())) == 10\r
-    assert len(list(cities.rows.all())) == 100\r
-    # new attributes\r
-    assert cities.paginator.num_pages == 10\r
-    assert cities.page.has_previous() == False\r
-    assert cities.page.has_next() == True\r
-    assert len(connection.queries)-start_querycount == 2\r
-\r
-    # reset\r
+        assert [c.name for c in CountryTable().columns] == ['system', 'population', 'foo', 'tld']
+
+
+def test_basic():
+    """Some tests here are copied from ``test_basic.py`` but need to be
+    rerun with a ModelTable, as the implementation is different."""
+
+    class CountryTable(tables.ModelTable):
+        null = tables.Column(default="foo")
+        tld = tables.Column(name="domain")
+        class Meta:
+            model = Country
+            exclude = ('id',)
+    countries = CountryTable()
+
+    def test_country_table(table):
+        for r in table.rows:
+            # "normal" fields exist
+            assert 'name' in r
+            # unknown fields are removed/not accessible
+            assert not 'does-not-exist' in r
+            # ...so are excluded fields
+            assert not 'id' in r
+            # [bug] access to data that might be available, but does not
+            # have a corresponding column is denied.
+            assert_raises(Exception, "r['id']")
+            # missing data is available with default values
+            assert 'null' in r
+            assert r['null'] == "foo"   # note: different from prev. line!
+            # if everything else fails (no default), we get None back
+            assert r['null2'] is None
+
+            # all that still works when name overrides are used
+            assert not 'tld' in r
+            assert 'domain' in r
+            assert len(r['domain']) == 2   # valid country tld
+    test_country_table(countries)
+
+    # repeat the avove tests with a table that is not associated with a
+    # model, and all columns being created manually.
+    class CountryTable(tables.ModelTable):
+        name = tables.Column()
+        population = tables.Column()
+        capital = tables.Column()
+        system = tables.Column()
+        null = tables.Column(default="foo")
+        null2 = tables.Column()
+        tld = tables.Column(name="domain")
+    countries = CountryTable(Country)
+    test_country_table(countries)
+
+
+def test_caches():
+    """Make sure the caches work for model tables as well (parts are
+    reimplemented).
+    """
+    class CountryTable(tables.ModelTable):
+        class Meta:
+            model = Country
+            exclude = ('id',)
+    countries = CountryTable()
+
+    assert id(list(countries.columns)[0]) == id(list(countries.columns)[0])
+    # TODO: row cache currently not used
+    #assert id(list(countries.rows)[0]) == id(list(countries.rows)[0])
+
+    # test that caches are reset after an update()
+    old_column_cache = id(list(countries.columns)[0])
+    old_row_cache = id(list(countries.rows)[0])
+    countries.update()
+    assert id(list(countries.columns)[0]) != old_column_cache
+    assert id(list(countries.rows)[0]) != old_row_cache
+
+def test_sort():
+    class CountryTable(tables.ModelTable):
+        tld = tables.Column(name="domain")
+        population = tables.Column()
+        system = tables.Column(default="republic")
+        custom1 = tables.Column()
+        custom2 = tables.Column(sortable=True)
+        class Meta:
+            model = Country
+    countries = CountryTable()
+
+    def test_order(order, result, table=countries):
+        table.order_by = order
+        assert [r['id'] for r in table.rows] == result
+
+    # test various orderings
+    test_order(('population',), [1,4,3,2])
+    test_order(('-population',), [2,3,4,1])
+    test_order(('name',), [1,3,2,4])
+    # test sorting with a "rewritten" column name
+    countries.order_by = 'domain,tld'      # "tld" would be invalid...
+    countries.order_by == ('domain',)      # ...and is therefore removed
+    test_order(('-domain',), [4,3,2,1])
+    # test multiple order instructions; note: one row is missing a "system"
+    # value, but has a default set; however, that has no effect on sorting.
+    test_order(('system', '-population'), [2,4,3,1])
+    # using a simple string (for convinience as well as querystring passing)
+    test_order('-population', [2,3,4,1])
+    test_order('system,-population', [2,4,3,1])
+
+    # test column with a default ``direction`` set to descending
+    class CityTable(tables.ModelTable):
+        name = tables.Column(direction='desc')
+        class Meta:
+            model = City
+    cities = CityTable()
+    test_order('name', [1,2], table=cities)   # Berlin to Amsterdam
+    test_order('-name', [2,1], table=cities)  # Amsterdam to Berlin
+
+    # test invalid order instructions...
+    countries.order_by = 'invalid_field,population'
+    assert countries.order_by == ('population',)
+    # ...in case of ModelTables, this primarily means that only
+    # model-based colunns are currently sortable at all.
+    countries.order_by = ('custom1', 'custom2')
+    assert countries.order_by == ()
+
+def test_default_sort():
+    class SortedCountryTable(tables.ModelTable):
+        class Meta:
+            model = Country
+            order_by = '-name'
+
+    # the order_by option is provided by TableOptions
+    assert_equal('-name', SortedCountryTable()._meta.order_by)
+
+    # the default order can be inherited from the table
+    assert_equal(('-name',), SortedCountryTable().order_by)
+    assert_equal(4, SortedCountryTable().rows[0]['id'])
+
+    # and explicitly set (or reset) via __init__
+    assert_equal(2, SortedCountryTable(order_by='system').rows[0]['id'])
+    assert_equal(1, SortedCountryTable(order_by=None).rows[0]['id'])
+
+def test_callable():
+    """Some of the callable code is reimplemented for modeltables, so
+    test some specifics again.
+    """
+
+    class CountryTable(tables.ModelTable):
+        null = tables.Column(default=lambda s: s['example_domain'])
+        example_domain = tables.Column()
+        class Meta:
+            model = Country
+    countries = CountryTable(Country)
+
+    # model method is called
+    assert [row['example_domain'] for row in countries] == \
+                    ['example.'+row['tld'] for row in countries]
+
+    # column default method is called
+    assert [row['example_domain'] for row in countries] == \
+                    [row['null'] for row in countries]
+
+def test_relationships():
+    """Test relationship spanning."""
+
+    class CountryTable(tables.ModelTable):
+        # add relationship spanning columns (using different approaches)
+        capital_name = tables.Column(data='capital__name')
+        capital__population = tables.Column(name="capital_population")
+        invalid = tables.Column(data="capital__invalid")
+        class Meta:
+            model = Country
+    countries = CountryTable(Country.objects.select_related('capital'))
+
+    # ordering and field access works
+    countries.order_by = 'capital_name'
+    assert [row['capital_name'] for row in countries.rows] == \
+        [None, None, 'Amsterdam', 'Berlin']
+
+    countries.order_by = 'capital_population'
+    assert [row['capital_population'] for row in countries.rows] == \
+        [None, None, None, None]
+
+    # ordering by a column with an invalid relationship fails silently
+    countries.order_by = 'invalid'
+    assert countries.order_by == ()
+
+
+def test_pagination():
+    """Pretty much the same as static table pagination, but make sure we
+    provide the capability, at least for paginators that use it, to not
+    have the complete queryset loaded (by use of a count() query).
+
+    Note: This test changes the available cities, make sure it is last,
+    or that tests that follow are written appropriately.
+    """
+    from django.db import connection
+
+    class CityTable(tables.ModelTable):
+        class Meta:
+            model = City
+            columns = ['name']
+    cities = CityTable()
+
+    # add some sample data
+    City.objects.all().delete()
+    for i in range(1,101):
+        City.objects.create(name="City %d"%i)
+
+    # for query logging
+    settings.DEBUG = True
+
+    # external paginator
+    start_querycount = len(connection.queries)
+    paginator = Paginator(cities.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
+    # Make sure the queryset is not loaded completely - there must be two
+    # queries, one a count(). This check is far from foolproof...
+    assert len(connection.queries)-start_querycount == 2
+
+    # using a queryset paginator is possible as well (although unnecessary)
+    paginator = QuerySetPaginator(cities.rows, 10)
+    assert paginator.num_pages == 10
+
+    # integrated paginator
+    start_querycount = len(connection.queries)
+    cities.paginate(Paginator, 10, page=1)
+    # rows is now paginated
+    assert len(list(cities.rows.page())) == 10
+    assert len(list(cities.rows.all())) == 100
+    # new attributes
+    assert cities.paginator.num_pages == 10
+    assert cities.page.has_previous() == False
+    assert cities.page.has_next() == True
+    assert len(connection.queries)-start_querycount == 2
+
+    # reset
     settings.DEBUG = False
\ No newline at end of file
index 21b8a0ba3b373f18b98ff620b0cd45d9f26e2ebb..b056770dc5a6b594c2546c2c992e58c18919a664 100644 (file)
-"""Test template specific functionality.\r
-\r
-Make sure tables expose their functionality to templates right. This\r
-generally about testing "out"-functionality of the tables, whether\r
-via templates or otherwise. Whether a test belongs here or, say, in\r
-``test_basic``, is not always a clear-cut decision.\r
-"""\r
-\r
-from django.template import Template, Context, add_to_builtins\r
-from django.http import HttpRequest\r
-import django_tables as tables\r
-\r
-def test_order_by():\r
-    class BookTable(tables.MemoryTable):\r
-        id = tables.Column()\r
-        name = tables.Column()\r
-    books = BookTable([\r
-        {'id': 1, 'name': 'Foo: Bar'},\r
-    ])\r
-\r
-    # cast to a string we get a value ready to be passed to the querystring\r
-    books.order_by = ('name',)\r
-    assert str(books.order_by) == 'name'\r
-    books.order_by = ('name', '-id')\r
-    assert str(books.order_by) == 'name,-id'\r
-\r
-def test_columns_and_rows():\r
-    class CountryTable(tables.MemoryTable):\r
-        name = tables.TextColumn()\r
-        capital = tables.TextColumn(sortable=False)\r
-        population = tables.NumberColumn(verbose_name="Population Size")\r
-        currency = tables.NumberColumn(visible=False, inaccessible=True)\r
-        tld = tables.TextColumn(visible=False, verbose_name="Domain")\r
-        calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")\r
-\r
-    countries = CountryTable(\r
-        [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'cc': 49},\r
-         {'name': 'France', 'population': 64, 'currency': 'Euro (€)', 'tld': 'fr', 'cc': 33},\r
-         {'name': 'Netherlands', 'capital': 'Amsterdam', 'cc': '31'},\r
-         {'name': 'Austria', 'cc': 43, 'currency': 'Euro (€)', 'population': 8}])\r
-\r
-    assert len(list(countries.columns)) == 4\r
-    assert len(list(countries.rows)) == len(list(countries)) == 4\r
-\r
-    # column name override, hidden columns\r
-    assert [c.name for c in countries.columns] == ['name', 'capital', 'population', 'cc']\r
-    # verbose_name, and fallback to field name\r
-    assert [unicode(c) for c in countries.columns] == ['Name', 'Capital', 'Population Size', 'Phone Ext.']\r
-\r
-    # data yielded by each row matches the defined columns\r
-    for row in countries.rows:\r
-        assert len(list(row)) == len(list(countries.columns))\r
-\r
-    # we can access each column and row by name...\r
-    assert countries.columns['population'].column.verbose_name == "Population Size"\r
-    assert countries.columns['cc'].column.verbose_name == "Phone Ext."\r
-    # ...even invisible ones\r
-    assert countries.columns['tld'].column.verbose_name == "Domain"\r
-    # ...and even inaccessible ones (but accessible to the coder)\r
-    assert countries.columns['currency'].column == countries.base_columns['currency']\r
-    # this also works for rows\r
-    for row in countries:\r
-        row['tld'], row['cc'], row['population']\r
-\r
-    # certain data is available on columns\r
-    assert countries.columns['currency'].sortable == True\r
-    assert countries.columns['capital'].sortable == False\r
-    assert countries.columns['name'].visible == True\r
-    assert countries.columns['tld'].visible == False\r
-\r
-def test_render():\r
-    """For good measure, render some actual templates."""\r
-\r
-    class CountryTable(tables.MemoryTable):\r
-        name = tables.TextColumn()\r
-        capital = tables.TextColumn()\r
-        population = tables.NumberColumn(verbose_name="Population Size")\r
-        currency = tables.NumberColumn(visible=False, inaccessible=True)\r
-        tld = tables.TextColumn(visible=False, verbose_name="Domain")\r
-        calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")\r
-\r
-    countries = CountryTable(\r
-        [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'calling_code': 49},\r
-         {'name': 'France', 'population': 64, 'currency': 'Euro (€)', 'tld': 'fr', 'calling_code': 33},\r
-         {'name': 'Netherlands', 'capital': 'Amsterdam', 'calling_code': '31'},\r
-         {'name': 'Austria', 'calling_code': 43, 'currency': 'Euro (€)', 'population': 8}])\r
-\r
-    assert Template("{% for column in countries.columns %}{{ column }}/{{ column.name }} {% endfor %}").\\r
-        render(Context({'countries': countries})) == \\r
-        "Name/name Capital/capital Population Size/population Phone Ext./cc "\r
-\r
-    assert Template("{% for row in countries %}{% for value in row %}{{ value }} {% endfor %}{% endfor %}").\\r
-        render(Context({'countries': countries})) == \\r
-        "Germany Berlin 83 49 France None 64 33 Netherlands Amsterdam None 31 Austria None 8 43 "\r
-\r
-    print Template("{% for row in countries %}{% if countries.columns.name.visible %}{{ row.name }} {% endif %}{% if countries.columns.tld.visible %}{{ row.tld }} {% endif %}{% endfor %}").\\r
-        render(Context({'countries': countries})) == \\r
-        "Germany France Netherlands Austria"\r
-\r
-def test_templatetags():\r
-    add_to_builtins('django_tables.app.templatetags.tables')\r
-\r
-    # [bug] set url param tag handles an order_by tuple with multiple columns\r
-    class MyTable(tables.MemoryTable):\r
-        f1 = tables.Column()\r
-        f2 = tables.Column()\r
-    t = Template('{% set_url_param x=table.order_by %}')\r
-    table = MyTable([], order_by=('f1', 'f2'))\r
-    assert t.render(Context({'request': HttpRequest(), 'table': table})) == '?x=f1%2Cf2'\r
+"""Test template specific functionality.
+
+Make sure tables expose their functionality to templates right. This
+generally about testing "out"-functionality of the tables, whether
+via templates or otherwise. Whether a test belongs here or, say, in
+``test_basic``, is not always a clear-cut decision.
+"""
+
+from django.template import Template, Context, add_to_builtins
+from django.http import HttpRequest
+import django_tables as tables
+
+def test_order_by():
+    class BookTable(tables.MemoryTable):
+        id = tables.Column()
+        name = tables.Column()
+    books = BookTable([
+        {'id': 1, 'name': 'Foo: Bar'},
+    ])
+
+    # cast to a string we get a value ready to be passed to the querystring
+    books.order_by = ('name',)
+    assert str(books.order_by) == 'name'
+    books.order_by = ('name', '-id')
+    assert str(books.order_by) == 'name,-id'
+
+def test_columns_and_rows():
+    class CountryTable(tables.MemoryTable):
+        name = tables.TextColumn()
+        capital = tables.TextColumn(sortable=False)
+        population = tables.NumberColumn(verbose_name="Population Size")
+        currency = tables.NumberColumn(visible=False, inaccessible=True)
+        tld = tables.TextColumn(visible=False, verbose_name="Domain")
+        calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")
+
+    countries = CountryTable(
+        [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'cc': 49},
+         {'name': 'France', 'population': 64, 'currency': 'Euro (€)', 'tld': 'fr', 'cc': 33},
+         {'name': 'Netherlands', 'capital': 'Amsterdam', 'cc': '31'},
+         {'name': 'Austria', 'cc': 43, 'currency': 'Euro (€)', 'population': 8}])
+
+    assert len(list(countries.columns)) == 4
+    assert len(list(countries.rows)) == len(list(countries)) == 4
+
+    # column name override, hidden columns
+    assert [c.name for c in countries.columns] == ['name', 'capital', 'population', 'cc']
+    # verbose_name, and fallback to field name
+    assert [unicode(c) for c in countries.columns] == ['Name', 'Capital', 'Population Size', 'Phone Ext.']
+
+    # data yielded by each row matches the defined columns
+    for row in countries.rows:
+        assert len(list(row)) == len(list(countries.columns))
+
+    # we can access each column and row by name...
+    assert countries.columns['population'].column.verbose_name == "Population Size"
+    assert countries.columns['cc'].column.verbose_name == "Phone Ext."
+    # ...even invisible ones
+    assert countries.columns['tld'].column.verbose_name == "Domain"
+    # ...and even inaccessible ones (but accessible to the coder)
+    assert countries.columns['currency'].column == countries.base_columns['currency']
+    # this also works for rows
+    for row in countries:
+        row['tld'], row['cc'], row['population']
+
+    # certain data is available on columns
+    assert countries.columns['currency'].sortable == True
+    assert countries.columns['capital'].sortable == False
+    assert countries.columns['name'].visible == True
+    assert countries.columns['tld'].visible == False
+
+def test_render():
+    """For good measure, render some actual templates."""
+
+    class CountryTable(tables.MemoryTable):
+        name = tables.TextColumn()
+        capital = tables.TextColumn()
+        population = tables.NumberColumn(verbose_name="Population Size")
+        currency = tables.NumberColumn(visible=False, inaccessible=True)
+        tld = tables.TextColumn(visible=False, verbose_name="Domain")
+        calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")
+
+    countries = CountryTable(
+        [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'calling_code': 49},
+         {'name': 'France', 'population': 64, 'currency': 'Euro (€)', 'tld': 'fr', 'calling_code': 33},
+         {'name': 'Netherlands', 'capital': 'Amsterdam', 'calling_code': '31'},
+         {'name': 'Austria', 'calling_code': 43, 'currency': 'Euro (€)', 'population': 8}])
+
+    assert Template("{% for column in countries.columns %}{{ column }}/{{ column.name }} {% endfor %}").\
+        render(Context({'countries': countries})) == \
+        "Name/name Capital/capital Population Size/population Phone Ext./cc "
+
+    assert Template("{% for row in countries %}{% for value in row %}{{ value }} {% endfor %}{% endfor %}").\
+        render(Context({'countries': countries})) == \
+        "Germany Berlin 83 49 France None 64 33 Netherlands Amsterdam None 31 Austria None 8 43 "
+
+    print Template("{% for row in countries %}{% if countries.columns.name.visible %}{{ row.name }} {% endif %}{% if countries.columns.tld.visible %}{{ row.tld }} {% endif %}{% endfor %}").\
+        render(Context({'countries': countries})) == \
+        "Germany France Netherlands Austria"
+
+def test_templatetags():
+    add_to_builtins('django_tables.app.templatetags.tables')
+
+    # [bug] set url param tag handles an order_by tuple with multiple columns
+    class MyTable(tables.MemoryTable):
+        f1 = tables.Column()
+        f2 = tables.Column()
+    t = Template('{% set_url_param x=table.order_by %}')
+    table = MyTable([], order_by=('f1', 'f2'))
+    assert t.render(Context({'request': HttpRequest(), 'table': table})) == '?x=f1%2Cf2'