added helpful functionality for working with order_by, rendering order_by functionali...
[django-tables2.git] / django_tables / tables.py
1 import copy\r
2 from django.utils.datastructures import SortedDict\r
3 from django.utils.encoding import StrAndUnicode\r
4 from django.utils.text import capfirst\r
5 from columns import Column\r
6 \r
7 __all__ = ('BaseTable', 'Table', 'options')\r
8 \r
9 def sort_table(data, order_by):\r
10     """Sort a list of dicts according to the fieldnames in the\r
11     ``order_by`` iterable. Prefix with hypen for reverse.\r
12 \r
13     Dict values can be callables.\r
14     """\r
15     def _cmp(x, y):\r
16         for name, reverse in instructions:\r
17             lhs, rhs = x.get(name), y.get(name)\r
18             res = cmp((callable(lhs) and [lhs(x)] or [lhs])[0],\r
19                       (callable(rhs) and [rhs(y)] or [rhs])[0])\r
20             if res != 0:\r
21                 return reverse and -res or res\r
22         return 0\r
23     instructions = []\r
24     for o in order_by:\r
25         if o.startswith('-'):\r
26             instructions.append((o[1:], True,))\r
27         else:\r
28             instructions.append((o, False,))\r
29     data.sort(cmp=_cmp)\r
30 \r
31 class DeclarativeColumnsMetaclass(type):\r
32     """\r
33     Metaclass that converts Column attributes to a dictionary called\r
34     'base_columns', taking into account parent class 'base_columns'\r
35     as well.\r
36     """\r
37     def __new__(cls, name, bases, attrs, parent_cols_from=None):\r
38         """\r
39         The ``parent_cols_from`` argument determins from which attribute\r
40         we read the columns of a base class that this table might be\r
41         subclassing. This is useful for ``ModelTable`` (and possibly other\r
42         derivatives) which might want to differ between the declared columns\r
43         and others.\r
44 \r
45         Note that if the attribute specified in ``parent_cols_from`` is not\r
46         found, we fall back to the default (``base_columns``), instead of\r
47         skipping over that base. This makes a table like the following work:\r
48 \r
49             class MyNewTable(tables.ModelTable, MyNonModelTable):\r
50                 pass\r
51 \r
52         ``MyNewTable`` will be built by the ModelTable metaclass, which will\r
53         call this base with a modified ``parent_cols_from`` argument\r
54         specific to ModelTables. Since ``MyNonModelTable`` is not a\r
55         ModelTable, and thus does not provide that attribute, the columns\r
56         from that base class would otherwise be ignored.\r
57         """\r
58 \r
59         # extract declared columns\r
60         columns = [(column_name, attrs.pop(column_name))\r
61            for column_name, obj in attrs.items()\r
62            if isinstance(obj, Column)]\r
63         columns.sort(lambda x, y: cmp(x[1].creation_counter,\r
64                                       y[1].creation_counter))\r
65 \r
66         # If this class is subclassing other tables, add their fields as\r
67         # well. Note that we loop over the bases in *reverse* - this is\r
68         # necessary to preserve the correct order of columns.\r
69         for base in bases[::-1]:\r
70             col_attr = (parent_cols_from and hasattr(base, parent_cols_from)) \\r
71                 and parent_cols_from\\r
72                 or 'base_columns'\r
73             if hasattr(base, col_attr):\r
74                 columns = getattr(base, col_attr).items() + columns\r
75         # Note that we are reusing an existing ``base_columns`` attribute.\r
76         # This is because in certain inheritance cases (mixing normal and\r
77         # ModelTables) this metaclass might be executed twice, and we need\r
78         # to avoid overriding previous data (because we pop() from attrs,\r
79         # the second time around columns might not be registered again).\r
80         # An example would be:\r
81         #    class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass\r
82         if not 'base_columns' in attrs:\r
83             attrs['base_columns'] = SortedDict()\r
84         attrs['base_columns'].update(SortedDict(columns))\r
85 \r
86         return type.__new__(cls, name, bases, attrs)\r
87 \r
88 def rmprefix(s):\r
89     """Normalize a column name by removing a potential sort prefix"""\r
90     return (s[:1]=='-' and [s[1:]] or [s])[0]\r
91 \r
92 class OrderByTuple(tuple, StrAndUnicode):\r
93         """Stores 'order by' instructions; Used to render output in a format\r
94         we understand as input (see __unicode__) - especially useful in\r
95         templates.\r
96 \r
97         Also supports some functionality to interact with and modify\r
98         the order.\r
99         """\r
100         def __unicode__(self):\r
101             """Output in our input format."""\r
102             return ",".join(self)\r
103 \r
104         def __contains__(self, name):\r
105             """Determine whether a column is part of this order."""\r
106             for o in self:\r
107                 if rmprefix(o) == name:\r
108                     return True\r
109             return False\r
110 \r
111         def is_reversed(self, name):\r
112             """Returns a bool indicating whether the column is ordered\r
113             reversed, None if it is missing."""\r
114             for o in self:\r
115                 if o == '-'+name:\r
116                     return True\r
117             return False\r
118         def is_straight(self, name):\r
119             """The opposite of is_reversed."""\r
120             for o in self:\r
121                 if o == name:\r
122                     return True\r
123             return False\r
124 \r
125         def polarize(self, reverse, names=()):\r
126             """Return a new tuple with the columns from ``names`` set to\r
127             "reversed" (e.g. prefixed with a '-'). Note that the name is\r
128             ambiguous - do not confuse this with ``toggle()``.\r
129 \r
130             If names is not specified, all columns are reversed. If a\r
131             column name is given that is currently not part of the order,\r
132             it is added.\r
133             """\r
134             prefix = reverse and '-' or ''\r
135             return OrderByTuple(\r
136                     [\r
137                       (\r
138                         # add either untouched, or reversed\r
139                         (names and rmprefix(o) not in names)\r
140                             and [o]\r
141                             or [prefix+rmprefix(o)]\r
142                       )[0]\r
143                     for o in self]\r
144                     +\r
145                     [prefix+name for name in names if not name in self]\r
146             )\r
147 \r
148         def toggle(self, names=()):\r
149             """Return a new tuple with the columns from ``names`` toggled\r
150             with respect to their "reversed" state. E.g. a '-' prefix will\r
151             be removed is existing, or added if lacking. Do not confuse\r
152             with ``reverse()``.\r
153 \r
154             If names is not specified, all columns are toggled. If a\r
155             column name is given that is currently not part of the order,\r
156             it is added in non-reverse form."""\r
157             return OrderByTuple(\r
158                     [\r
159                       (\r
160                         # add either untouched, or toggled\r
161                         (names and rmprefix(o) not in names)\r
162                             and [o]\r
163                             or ((o[:1] == '-') and [o[1:]] or ["-"+o])\r
164                       )[0]\r
165                     for o in self]\r
166                     +  # !!!: test for addition\r
167                     [name for name in names if not name in self]\r
168             )\r
169 \r
170 \r
171 # A common use case is passing incoming query values directly into the\r
172 # table constructor - data that can easily be invalid, say if manually\r
173 # modified by a user. So by default, such errors will be silently\r
174 # ignored. Set the option below to False if you want an exceptions to be\r
175 # raised instead.\r
176 class DefaultOptions(object):\r
177     IGNORE_INVALID_OPTIONS = True\r
178 options = DefaultOptions()\r
179 \r
180 class BaseTable(object):\r
181     def __init__(self, data, order_by=None):\r
182         """Create a new table instance with the iterable ``data``.\r
183 \r
184         If ``order_by`` is specified, the data will be sorted accordingly.\r
185 \r
186         Note that unlike a ``Form``, tables are always bound to data. Also\r
187         unlike a form, the ``columns`` attribute is read-only and returns\r
188         ``BoundColum`` wrappers, similar to the ``BoundField``'s you get\r
189         when iterating over a form. This is because the table iterator\r
190         already yields rows, and we need an attribute via which to expose\r
191         the (visible) set of (bound) columns - ``Table.columns`` is simply\r
192         the perfect fit for this. Instead, ``base_colums`` is copied to\r
193         table instances, so modifying that will not touch the class-wide\r
194         column list.\r
195         """\r
196         self._data = data\r
197         self._snapshot = None      # will store output dataset (ordered...)\r
198         self._rows = Rows(self)\r
199         self._columns = Columns(self)\r
200 \r
201         self.order_by = order_by\r
202 \r
203         # Make a copy so that modifying this will not touch the class\r
204         # definition. Note that this is different from forms, where the\r
205         # copy is made available in a ``fields`` attribute. See the\r
206         # ``Table`` class docstring for more information.\r
207         self.base_columns = copy.deepcopy(type(self).base_columns)\r
208 \r
209     def _build_snapshot(self):\r
210         """Rebuilds the table whenever it's options change.\r
211 \r
212         Whenver the table options change, e.g. say a new sort order,\r
213         this method will be asked to regenerate the actual table from\r
214         the linked data source.\r
215 \r
216         In the case of this base table implementation, a copy of the\r
217         source data is created, and then modified appropriately.\r
218 \r
219         # TODO: currently this is called whenever data changes; it is\r
220         # probably much better to do this on-demand instead, when the\r
221         # data is *needed* for the first time.\r
222         """\r
223 \r
224         # reset caches\r
225         self._columns._reset()\r
226         self._rows._reset()\r
227 \r
228         snapshot = copy.copy(self._data)\r
229         for row in snapshot:\r
230             # add data that is missing from the source. we do this now so\r
231             # that the colunn ``default`` and ``data`` values can affect\r
232             # sorting (even when callables are used)!\r
233             # This is a design decision - the alternative would be to\r
234             # resolve the values when they are accessed, and either do not\r
235             # support sorting them at all, or run the callables during\r
236             # sorting.\r
237             for column in self.columns.all():\r
238                 name_in_source = column.declared_name\r
239                 if column.column.data:\r
240                     if callable(column.column.data):\r
241                         # if data is a callable, use it's return value\r
242                         row[name_in_source] = column.column.data(BoundRow(self, row))\r
243                     else:\r
244                         name_in_source = column.column.data\r
245 \r
246                 # the following will be True if:\r
247                 #  * the source does not provide that column or provides None\r
248                 #  * the column did provide a data callable that returned None\r
249                 if row.get(name_in_source, None) is None:\r
250                     row[name_in_source] = column.get_default(BoundRow(self, row))\r
251 \r
252         if self.order_by:\r
253             sort_table(snapshot, self._cols_to_fields(self.order_by))\r
254         self._snapshot = snapshot\r
255 \r
256     def _get_data(self):\r
257         if self._snapshot is None:\r
258             self._build_snapshot()\r
259         return self._snapshot\r
260     data = property(lambda s: s._get_data())\r
261 \r
262     def _cols_to_fields(self, names):\r
263         """Utility function. Given a list of column names (as exposed to\r
264         the user), converts column names to the names we have to use to\r
265         retrieve a column's data from the source.\r
266 \r
267         Usually, the name used in the table declaration is used for accessing\r
268         the source (while a column can define an alias-like name that will\r
269         be used to refer to it from the "outside"). However, a column can\r
270         override this by giving a specific source field name via ``data``.\r
271 \r
272         Supports prefixed column names as used e.g. in order_by ("-field").\r
273         """\r
274         result = []\r
275         for ident in names:\r
276             # handle order prefix\r
277             if ident[:1] == '-':\r
278                 name = ident[1:]\r
279                 prefix = '-'\r
280             else:\r
281                 name = ident\r
282                 prefix = ''\r
283             # find the field name\r
284             column = self.columns[name]\r
285             if column.column.data and not callable(column.column.data):\r
286                 name_in_source = column.column.data\r
287             else:\r
288                 name_in_source = column.declared_name\r
289             result.append(prefix + name_in_source)\r
290         return result\r
291 \r
292     def _validate_column_name(self, name, purpose):\r
293         """Return True/False, depending on whether the column ``name`` is\r
294         valid for ``purpose``. Used to validate things like ``order_by``\r
295         instructions.\r
296 \r
297         Can be overridden by subclasses to impose further restrictions.\r
298         """\r
299         if purpose == 'order_by':\r
300             return name in self.columns and\\r
301                    self.columns[name].column.sortable\r
302         else:\r
303             return True\r
304 \r
305     def _set_order_by(self, value):\r
306         if self._snapshot is not None:\r
307             self._snapshot = None\r
308         # accept both string and tuple instructions\r
309         order_by = (isinstance(value, basestring) \\r
310             and [value.split(',')] \\r
311             or [value])[0]\r
312         if order_by:\r
313             # validate, remove all invalid order instructions\r
314             validated_order_by = []\r
315             for o in order_by:\r
316                 if self._validate_column_name(rmprefix(o), "order_by"):\r
317                     validated_order_by.append(o)\r
318                 elif not options.IGNORE_INVALID_OPTIONS:\r
319                     raise ValueError('Column name %s is invalid.' % o)\r
320             self._order_by = OrderByTuple(validated_order_by)\r
321         else:\r
322             self._order_by = OrderByTuple()\r
323     order_by = property(lambda s: s._order_by, _set_order_by)\r
324 \r
325     def __unicode__(self):\r
326         return self.as_html()\r
327 \r
328     def __iter__(self):\r
329         for row in self.rows:\r
330             yield row\r
331 \r
332     def __getitem__(self, key):\r
333         return self.rows[key]\r
334 \r
335     # just to make those readonly\r
336     columns = property(lambda s: s._columns)\r
337     rows = property(lambda s: s._rows)\r
338 \r
339     def as_html(self):\r
340         pass\r
341 \r
342     def update(self):\r
343         """Update the table based on it's current options.\r
344 \r
345         Normally, you won't have to call this method, since the table\r
346         updates itself (it's caches) automatically whenever you change\r
347         any of the properties. However, in some rare cases those\r
348         changes might not be picked up, for example if you manually\r
349         change ``base_columns`` or any of the columns in it.\r
350         """\r
351         self._build_snapshot()\r
352 \r
353     def paginate(self, klass, *args, **kwargs):\r
354         page = kwargs.pop('page', 1)\r
355         self.paginator = klass(self.rows, *args, **kwargs)\r
356         self.page = self.paginator.page(page)\r
357 \r
358 \r
359 class Table(BaseTable):\r
360     "A collection of columns, plus their associated data rows."\r
361     # This is a separate class from BaseTable in order to abstract the way\r
362     # self.columns is specified.\r
363     __metaclass__ = DeclarativeColumnsMetaclass\r
364 \r
365 \r
366 class Columns(object):\r
367     """Container for spawning BoundColumns.\r
368 \r
369     This is bound to a table and provides it's ``columns`` property. It\r
370     provides access to those columns in different ways (iterator,\r
371     item-based, filtered and unfiltered etc)., stuff that would not be\r
372     possible with a simple iterator in the table class.\r
373 \r
374     Note that when you define your column using a name override, e.g.\r
375     ``author_name = tables.Column(name="author")``, then the column will\r
376     be exposed by this container as "author", not "author_name".\r
377     """\r
378     def __init__(self, table):\r
379         self.table = table\r
380         self._columns = SortedDict()\r
381 \r
382     def _reset(self):\r
383         """Used by parent table class."""\r
384         self._columns = SortedDict()\r
385 \r
386     def _spawn_columns(self):\r
387         # (re)build the "_columns" cache of BoundColumn objects (note that\r
388         # ``base_columns`` might have changed since last time); creating\r
389         # BoundColumn instances can be costly, so we reuse existing ones.\r
390         new_columns = SortedDict()\r
391         for decl_name, column in self.table.base_columns.items():\r
392             # take into account name overrides\r
393             exposed_name = column.name or decl_name\r
394             if exposed_name in self._columns:\r
395                 new_columns[exposed_name] = self._columns[exposed_name]\r
396             else:\r
397                 new_columns[exposed_name] = BoundColumn(self.table, column, decl_name)\r
398         self._columns = new_columns\r
399 \r
400     def all(self):\r
401         """Iterate through all columns, regardless of visiblity (as\r
402         opposed to ``__iter__``.\r
403 \r
404         This is used internally a lot.\r
405         """\r
406         self._spawn_columns()\r
407         for column in self._columns.values():\r
408             yield column\r
409 \r
410     def items(self):\r
411         self._spawn_columns()\r
412         for r in self._columns.items():\r
413             yield r\r
414 \r
415     def names(self):\r
416         self._spawn_columns()\r
417         for r in self._columns.keys():\r
418             yield r\r
419 \r
420     def index(self, name):\r
421         self._spawn_columns()\r
422         return self._columns.keyOrder.index(name)\r
423 \r
424     def __iter__(self):\r
425         """Iterate through all *visible* bound columns.\r
426 \r
427         This is primarily geared towards table rendering.\r
428         """\r
429         for column in self.all():\r
430             if column.column.visible:\r
431                 yield column\r
432 \r
433     def __contains__(self, item):\r
434         """Check by both column object and column name."""\r
435         self._spawn_columns()\r
436         if isinstance(item, basestring):\r
437             return item in self.names()\r
438         else:\r
439             return item in self.all()\r
440 \r
441     def __getitem__(self, name):\r
442         """Return a column by name."""\r
443         self._spawn_columns()\r
444         return self._columns[name]\r
445 \r
446 \r
447 class BoundColumn(StrAndUnicode):\r
448     """'Runtime' version of ``Column`` that is bound to a table instance,\r
449     and thus knows about the table's data.\r
450 \r
451     Note that the name that is passed in tells us how this field is\r
452     delared in the bound table. The column itself can overwrite this name.\r
453     While the overwritten name will be hat mostly counts, we need to\r
454     remember the one used for declaration as well, or we won't know how\r
455     to read a column's value from the source.\r
456     """\r
457     def __init__(self, table, column, name):\r
458         self.table = table\r
459         self.column = column\r
460         self.declared_name = name\r
461         # expose some attributes of the column more directly\r
462         self.sortable = column.sortable\r
463         self.visible = column.visible\r
464 \r
465     name = property(lambda s: s.column.name or s.declared_name)\r
466     name_reversed = property(lambda s: "-"+s.name)\r
467     def _get_name_toggled(self):\r
468         o = self.table.order_by\r
469         if (not self.name in o) or o.is_reversed(self.name): return self.name\r
470         else: return self.name_reversed\r
471     name_toggled = property(_get_name_toggled)\r
472 \r
473     is_ordered = property(lambda s: s.name in s.table.order_by)\r
474     is_ordered_reverse = property(lambda s: s.table.order_by.is_reversed(s.name))\r
475     is_ordered_straight = property(lambda s: s.table.order_by.is_straight(s.name))\r
476     order_by = property(lambda s: s.table.order_by.polarize(False, [s.name]))\r
477     order_by_reversed = property(lambda s: s.table.order_by.polarize(True, [s.name]))\r
478     order_by_toggled = property(lambda s: s.table.order_by.toggle([s.name]))\r
479 \r
480     def get_default(self, row):\r
481         """Since a column's ``default`` property may be a callable, we need\r
482         this function to resolve it when needed.\r
483 \r
484         Make sure ``row`` is a ``BoundRow`` object, since that is what\r
485         we promise the callable will get.\r
486         """\r
487         if callable(self.column.default):\r
488             return self.column.default(row)\r
489         return self.column.default\r
490 \r
491     def _get_values(self):\r
492         # TODO: build a list of values used\r
493         pass\r
494     values = property(_get_values)\r
495 \r
496     def __unicode__(self):\r
497         return capfirst(self.column.verbose_name or self.name.replace('_', ' '))\r
498 \r
499     def as_html(self):\r
500         pass\r
501 \r
502 class Rows(object):\r
503     """Container for spawning BoundRows.\r
504 \r
505     This is bound to a table and provides it's ``rows`` property. It\r
506     provides functionality that would not be possible with a simple\r
507     iterator in the table class.\r
508     """\r
509     def __init__(self, table, row_klass=None):\r
510         self.table = table\r
511         self.row_klass = row_klass and row_klass or BoundRow\r
512 \r
513     def _reset(self):\r
514         pass   # we currently don't use a cache\r
515 \r
516     def all(self):\r
517         """Return all rows."""\r
518         for row in self.table.data:\r
519             yield self.row_klass(self.table, row)\r
520 \r
521     def page(self):\r
522         """Return rows on current page (if paginated)."""\r
523         if not hasattr(self.table, 'page'):\r
524             return None\r
525         return iter(self.table.page.object_list)\r
526 \r
527     def __iter__(self):\r
528         return iter(self.all())\r
529 \r
530     def __len__(self):\r
531         return len(self.table.data)\r
532 \r
533     def __getitem__(self, key):\r
534         if isinstance(key, slice):\r
535             result = list()\r
536             for row in self.table.data[key]:\r
537                 result.append(self.row_klass(self.table, row))\r
538             return result\r
539         elif isinstance(key, int):\r
540             return self.row_klass(self.table, self.table.data[key])\r
541         else:\r
542             raise TypeError('Key must be a slice or integer.')\r
543 \r
544 class BoundRow(object):\r
545     """Represents a single row of data, bound to a table.\r
546 \r
547     Tables will spawn these row objects, wrapping around the actual data\r
548     stored in a row.\r
549     """\r
550     def __init__(self, table, data):\r
551         self.table = table\r
552         self.data = data\r
553 \r
554     def __iter__(self):\r
555         for value in self.values:\r
556             yield value\r
557 \r
558     def __getitem__(self, name):\r
559         """Returns this row's value for a column. All other access methods,\r
560         e.g. __iter__, lead ultimately to this."""\r
561 \r
562         # We are supposed to return ``name``, but the column might be\r
563         # named differently in the source data.\r
564         result =  self.data[self.table._cols_to_fields([name])[0]]\r
565 \r
566         # if the field we are pointing to is a callable, remove it\r
567         if callable(result):\r
568             result = result(self)\r
569         return result\r
570 \r
571     def __contains__(self, item):\r
572         """Check by both row object and column name."""\r
573         if isinstance(item, basestring):\r
574             return item in self.table._columns\r
575         else:\r
576             return item in self\r
577 \r
578     def _get_values(self):\r
579         for column in self.table.columns:\r
580             yield self[column.name]\r
581     values = property(_get_values)\r
582 \r
583     def as_html(self):\r
584         pass