-__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
-"""\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
-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))
-__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
-# 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
-"""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
-"""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
-"""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'