we've just written support for a choices=True setting, when we noticed that we don...
[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')\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     def _cmp(x, y):\r
14         for name, reverse in instructions:\r
15             res = cmp(x.get(name), y.get(name))\r
16             if res != 0:\r
17                 return reverse and -res or res\r
18         return 0\r
19     instructions = []\r
20     for o in order_by:\r
21         if o.startswith('-'):\r
22             instructions.append((o[1:], True,))\r
23         else:\r
24             instructions.append((o, False,))\r
25     data.sort(cmp=_cmp)\r
26 \r
27 class DeclarativeColumnsMetaclass(type):\r
28     """\r
29     Metaclass that converts Column attributes to a dictionary called\r
30     'base_columns', taking into account parent class 'base_columns'\r
31     as well.\r
32     """\r
33     def __new__(cls, name, bases, attrs, parent_cols_from=None):\r
34         """\r
35         The ``parent_cols_from`` argument determins from which attribute\r
36         we read the columns of a base class that this table might be\r
37         subclassing. This is useful for ``ModelTable`` (and possibly other\r
38         derivatives) which might want to differ between the declared columns\r
39         and others.\r
40 \r
41         Note that if the attribute specified in ``parent_cols_from`` is not\r
42         found, we fall back to the default (``base_columns``), instead of\r
43         skipping over that base. This makes a table like the following work:\r
44 \r
45             class MyNewTable(tables.ModelTable, MyNonModelTable):\r
46                 pass\r
47 \r
48         ``MyNewTable`` will be built by the ModelTable metaclass, which will\r
49         call this base with a modified ``parent_cols_from`` argument\r
50         specific to ModelTables. Since ``MyNonModelTable`` is not a\r
51         ModelTable, and thus does not provide that attribute, the columns\r
52         from that base class would otherwise be ignored.\r
53         """\r
54 \r
55         # extract declared columns\r
56         columns = [(column_name, attrs.pop(column_name))\r
57            for column_name, obj in attrs.items()\r
58            if isinstance(obj, Column)]\r
59         columns.sort(lambda x, y: cmp(x[1].creation_counter,\r
60                                       y[1].creation_counter))\r
61 \r
62         # If this class is subclassing other tables, add their fields as\r
63         # well. Note that we loop over the bases in *reverse* - this is\r
64         # necessary to preserve the correct order of columns.\r
65         for base in bases[::-1]:\r
66             col_attr = (parent_cols_from and hasattr(base, parent_cols_from)) \\r
67                 and parent_cols_from\\r
68                 or 'base_columns'\r
69             if hasattr(base, col_attr):\r
70                 columns = getattr(base, col_attr).items() + columns\r
71         # Note that we are reusing an existing ``base_columns`` attribute.\r
72         # This is because in certain inheritance cases (mixing normal and\r
73         # ModelTables) this metaclass might be executed twice, and we need\r
74         # to avoid overriding previous data (because we pop() from attrs,\r
75         # the second time around columns might not be registered again).\r
76         # An example would be:\r
77         #    class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass\r
78         if not 'base_columns' in attrs:\r
79             attrs['base_columns'] = SortedDict()\r
80         attrs['base_columns'].update(SortedDict(columns))\r
81 \r
82         return type.__new__(cls, name, bases, attrs)\r
83 \r
84 class OrderByTuple(tuple, StrAndUnicode):\r
85         """Stores 'order by' instructions; Currently only used to render\r
86         to the output (especially in templates) in a format we understand\r
87         as input.\r
88         """\r
89         def __unicode__(self):\r
90             return ",".join(self)\r
91 \r
92 class BaseTable(object):\r
93     def __init__(self, data, order_by=None):\r
94         """Create a new table instance with the iterable ``data``.\r
95 \r
96         If ``order_by`` is specified, the data will be sorted accordingly.\r
97 \r
98         Note that unlike a ``Form``, tables are always bound to data. Also\r
99         unlike a form, the ``columns`` attribute is read-only and returns\r
100         ``BoundColum`` wrappers, similar to the ``BoundField``'s you get\r
101         when iterating over a form. This is because the table iterator\r
102         already yields rows, and we need an attribute via which to expose\r
103         the (visible) set of (bound) columns - ``Table.columns`` is simply\r
104         the perfect fit for this. Instead, ``base_colums`` is copied to\r
105         table instances, so modifying that will not touch the class-wide\r
106         column list.\r
107         """\r
108         self._data = data\r
109         self._snapshot = None      # will store output dataset (ordered...)\r
110         self._rows = None          # will store BoundRow objects\r
111         self._columns = Columns(self)\r
112         self._order_by = order_by\r
113 \r
114         # Make a copy so that modifying this will not touch the class\r
115         # definition. Note that this is different from forms, where the\r
116         # copy is made available in a ``fields`` attribute. See the\r
117         # ``Table`` class docstring for more information.\r
118         self.base_columns = copy.deepcopy(type(self).base_columns)\r
119 \r
120     def _build_snapshot(self):\r
121         """Rebuilds the table whenever it's options change.\r
122 \r
123         Whenver the table options change, e.g. say a new sort order,\r
124         this method will be asked to regenerate the actual table from\r
125         the linked data source.\r
126 \r
127         In the case of this base table implementation, a copy of the\r
128         source data is created, and then modified appropriately.\r
129         """\r
130         snapshot = copy.copy(self._data)\r
131         for row in snapshot:\r
132             # delete unknown columns that are in the source data, but that\r
133             # we don't know about.\r
134             # TODO: does this even make sense? we might in some cases save\r
135             # memory, but at what performance cost?\r
136             decl_colnames = [c.declared_name for c in self.columns.all()]\r
137             for key in row.keys():\r
138                 if not key in decl_colnames:\r
139                     del row[key]\r
140             # add data for defined columns that is missing from the source.\r
141             # we do this so that colunn default values can affect sorting,\r
142             # which is the current design decision.\r
143             for column in self.columns.all():\r
144                 if not column.declared_name in row:\r
145                     row[column.declared_name] = column.column.default\r
146 \r
147         if self.order_by:\r
148             sort_table(snapshot, self._cols_to_fields(self.order_by))\r
149         self._snapshot = snapshot\r
150 \r
151     def _get_data(self):\r
152         if self._snapshot is None:\r
153             self._build_snapshot()\r
154         return self._snapshot\r
155     data = property(lambda s: s._get_data())\r
156 \r
157     def _cols_to_fields(self, names):\r
158         """Utility function. Given a list of column names (as exposed to\r
159         the user), converts column names to the names we have to use to\r
160         retrieve a column's data from the source.\r
161 \r
162         Right now, the name used in the table declaration is used for\r
163         access, but a column can define it's own alias-like name that will\r
164         be used to refer to the column from outside.\r
165 \r
166         Supports prefixed column names as used e.g. in order_by ("-field").\r
167         """\r
168         result = []\r
169         for ident in names:\r
170             if ident[:1] == '-':\r
171                 name = ident[1:]\r
172                 prefix = '-'\r
173             else:\r
174                 name = ident\r
175                 prefix = ''\r
176             result.append(prefix + self.columns[name].declared_name)\r
177         return result\r
178 \r
179     def _validate_column_name(self, name, purpose):\r
180         """Return True/False, depending on whether the column ``name`` is\r
181         valid for ``purpose``. Used to validate things like ``order_by``\r
182         instructions.\r
183 \r
184         Can be overridden by subclasses to impose further restrictions.\r
185         """\r
186         if purpose == 'order_by':\r
187             return name in self.columns and\\r
188                    self.columns[name].column.sortable\r
189         else:\r
190             return True\r
191 \r
192     def _set_order_by(self, value):\r
193         if self._snapshot is not None:\r
194             self._snapshot = None\r
195         # accept both string and tuple instructions\r
196         self._order_by = (isinstance(value, basestring) \\r
197             and [value.split(',')] \\r
198             or [value])[0]\r
199         # validate, remove all invalid order instructions\r
200         self._order_by = OrderByTuple([o for o in self._order_by\r
201             if self._validate_column_name((o[:1]=='-' and [o[1:]] or [o])[0], "order_by")])\r
202     order_by = property(lambda s: s._order_by, _set_order_by)\r
203 \r
204     def __unicode__(self):\r
205         return self.as_html()\r
206 \r
207     def __iter__(self):\r
208         for row in self.rows:\r
209             yield row\r
210 \r
211     def __getitem__(self, name):\r
212         try:\r
213             column = self.columns[name]\r
214         except KeyError:\r
215             raise KeyError('Key %r not found in Table' % name)\r
216         return BoundColumn(self, column, name)\r
217 \r
218     columns = property(lambda s: s._columns)  # just to make it readonly\r
219 \r
220     def _get_rows(self):\r
221         for row in self.data:\r
222             yield BoundRow(self, row)\r
223     rows = property(lambda s: s._get_rows())\r
224 \r
225     def as_html(self):\r
226         pass\r
227 \r
228 class Table(BaseTable):\r
229     "A collection of columns, plus their associated data rows."\r
230     # This is a separate class from BaseTable in order to abstract the way\r
231     # self.columns is specified.\r
232     __metaclass__ = DeclarativeColumnsMetaclass\r
233 \r
234 \r
235 class Columns(object):\r
236     """Container for spawning BoundColumns.\r
237 \r
238     This is bound to a table and provides it's ``columns`` property. It\r
239     provides access to those columns in different ways (iterator,\r
240     item-based, filtered and unfiltered etc)., stuff that would not be\r
241     possible with a simple iterator on the table class.\r
242 \r
243     Note that when you define your column using a name override, e.g.\r
244     ``author_name = tables.Column(name="author")``, then the column will\r
245     be exposed by this container as "author", not "author_name".\r
246     """\r
247     def __init__(self, table):\r
248         self.table = table\r
249         self._columns = SortedDict()\r
250 \r
251     def _spawn_columns(self):\r
252         # (re)build the "_columns" cache of BoundColumn objects (note that\r
253         # ``base_columns`` might have changed since last time); creating\r
254         # BoundColumn instances can be costly, so we reuse existing ones.\r
255         new_columns = SortedDict()\r
256         for decl_name, column in self.table.base_columns.items():\r
257             # take into account name overrides\r
258             exposed_name = column.name or decl_name\r
259             if exposed_name in self._columns:\r
260                 new_columns[exposed_name] = self._columns[exposed_name]\r
261             else:\r
262                 new_columns[exposed_name] = BoundColumn(self, column, decl_name)\r
263         self._columns = new_columns\r
264 \r
265     def all(self):\r
266         """Iterate through all columns, regardless of visiblity (as\r
267         opposed to ``__iter__``.\r
268 \r
269         This is used internally a lot.\r
270         """\r
271         self._spawn_columns()\r
272         for column in self._columns.values():\r
273             yield column\r
274 \r
275     def items(self):\r
276         self._spawn_columns()\r
277         for r in self._columns.items():\r
278             yield r\r
279 \r
280     def names(self):\r
281         self._spawn_columns()\r
282         for r in self._columns.keys():\r
283             yield r\r
284 \r
285     def index(self, name):\r
286         self._spawn_columns()\r
287         return self._columns.keyOrder.index(name)\r
288 \r
289     def __iter__(self):\r
290         """Iterate through all *visible* bound columns.\r
291 \r
292         This is primarily geared towards table rendering.\r
293         """\r
294         for column in self.all():\r
295             if column.column.visible:\r
296                 yield column\r
297 \r
298     def __contains__(self, item):\r
299         """Check by both column object and column name."""\r
300         self._spawn_columns()\r
301         if isinstance(item, basestring):\r
302             return item in self.names()\r
303         else:\r
304             return item in self.all()\r
305 \r
306     def __getitem__(self, name):\r
307         """Return a column by name."""\r
308         self._spawn_columns()\r
309         return self._columns[name]\r
310 \r
311 \r
312 class BoundColumn(StrAndUnicode):\r
313     """'Runtime' version of ``Column`` that is bound to a table instance,\r
314     and thus knows about the table's data.\r
315 \r
316     Note that the name that is passed in tells us how this field is\r
317     delared in the bound table. The column itself can overwrite this name.\r
318     While the overwritten name will be hat mostly counts, we need to\r
319     remember the one used for declaration as well, or we won't know how\r
320     to read a column's value from the source.\r
321     """\r
322     def __init__(self, table, column, name):\r
323         self.table = table\r
324         self.column = column\r
325         self.declared_name = name\r
326         # expose some attributes of the column more directly\r
327         self.sortable = column.sortable\r
328         self.visible = column.visible\r
329 \r
330     name = property(lambda s: s.column.name or s.declared_name)\r
331 \r
332     def _get_values(self):\r
333         # TODO: build a list of values used\r
334         pass\r
335     values = property(_get_values)\r
336 \r
337     def __unicode__(self):\r
338         return capfirst(self.column.verbose_name or self.name.replace('_', ' '))\r
339 \r
340     def as_html(self):\r
341         pass\r
342 \r
343 class BoundRow(object):\r
344     """Represents a single row of data, bound to a table.\r
345 \r
346     Tables will spawn these row objects, wrapping around the actual data\r
347     stored in a row.\r
348     """\r
349     def __init__(self, table, data):\r
350         self.table = table\r
351         self.data = data\r
352 \r
353     def __iter__(self):\r
354         for value in self.values:\r
355             yield value\r
356 \r
357     def __getitem__(self, name):\r
358         """Returns this row's value for a column. All other access methods,\r
359         e.g. __iter__, lead ultimately to this."""\r
360         column = self.table.columns[name]\r
361         return RowValue(self.data[column.declared_name], column)\r
362 \r
363     def __contains__(self, item):\r
364         """Check by both row object and column name."""\r
365         if isinstance(item, basestring):\r
366             return item in self.table._columns\r
367         else:\r
368             return item in self\r
369 \r
370     def _get_values(self):\r
371         for column in self.table.columns:\r
372             yield self[column.name]\r
373     values = property(_get_values)\r
374 \r
375     def as_html(self):\r
376         pass\r
377 \r
378 class RowValue(StrAndUnicode):\r
379     """Very basic wrapper around a single row value of a column.\r
380 \r
381     Instead of returning the row values directly, ``BoundRow`` spawns\r
382     instances of this class. That's necessary since the ``choices``\r
383     feature means that a single row value can consist of both the value\r
384     itself and an associated ID.\r
385     """\r
386     def __init__(self, value, column):\r
387         if column.column.choices == True:\r
388             if isinstance(value, dict):\r
389                 self.id = value.get('id')\r
390                 self.value = value.get('value')\r
391             elif isinstance(value, (tuple,list,)):\r
392                 self.id = value[0]\r
393                 self.value = value[1]\r
394             else:\r
395                 self.id = None\r
396                 self.value = value\r
397         else:\r
398             self.id = None\r
399             self.value = value\r
400 \r
401     def __unicode__(self):\r
402         return unicode(self.value)