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
7 __all__ = ('BaseTable', 'Table')
\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
14 for name, reverse in instructions:
\r
15 res = cmp(x.get(name), y.get(name))
\r
17 return reverse and -res or res
\r
21 if o.startswith('-'):
\r
22 instructions.append((o[1:], True,))
\r
24 instructions.append((o, False,))
\r
27 class DeclarativeColumnsMetaclass(type):
\r
29 Metaclass that converts Column attributes to a dictionary called
\r
30 'base_columns', taking into account parent class 'base_columns'
\r
33 def __new__(cls, name, bases, attrs, parent_cols_from=None):
\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
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
45 class MyNewTable(tables.ModelTable, MyNonModelTable):
\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
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
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
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
82 return type.__new__(cls, name, bases, attrs)
\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
89 def __unicode__(self):
\r
90 return ",".join(self)
\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
96 If ``order_by`` is specified, the data will be sorted accordingly.
\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
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
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
120 def _build_snapshot(self):
\r
121 """Rebuilds the table whenever it's options change.
\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
127 In the case of this base table implementation, a copy of the
\r
128 source data is created, and then modified appropriately.
\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
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
148 sort_table(snapshot, self._cols_to_fields(self.order_by))
\r
149 self._snapshot = snapshot
\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
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
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
166 Supports prefixed column names as used e.g. in order_by ("-field").
\r
169 for ident in names:
\r
170 if ident[:1] == '-':
\r
176 result.append(prefix + self.columns[name].declared_name)
\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
184 Can be overridden by subclasses to impose further restrictions.
\r
186 if purpose == 'order_by':
\r
187 return name in self.columns and\
\r
188 self.columns[name].column.sortable
\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
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
204 def __unicode__(self):
\r
205 return self.as_html()
\r
207 def __iter__(self):
\r
208 for row in self.rows:
\r
211 def __getitem__(self, name):
\r
213 column = self.columns[name]
\r
215 raise KeyError('Key %r not found in Table' % name)
\r
216 return BoundColumn(self, column, name)
\r
218 columns = property(lambda s: s._columns) # just to make it readonly
\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
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
235 class Columns(object):
\r
236 """Container for spawning BoundColumns.
\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
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
247 def __init__(self, table):
\r
249 self._columns = SortedDict()
\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
262 new_columns[exposed_name] = BoundColumn(self, column, decl_name)
\r
263 self._columns = new_columns
\r
266 """Iterate through all columns, regardless of visiblity (as
\r
267 opposed to ``__iter__``.
\r
269 This is used internally a lot.
\r
271 self._spawn_columns()
\r
272 for column in self._columns.values():
\r
276 self._spawn_columns()
\r
277 for r in self._columns.items():
\r
281 self._spawn_columns()
\r
282 for r in self._columns.keys():
\r
285 def index(self, name):
\r
286 self._spawn_columns()
\r
287 return self._columns.keyOrder.index(name)
\r
289 def __iter__(self):
\r
290 """Iterate through all *visible* bound columns.
\r
292 This is primarily geared towards table rendering.
\r
294 for column in self.all():
\r
295 if column.column.visible:
\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
304 return item in self.all()
\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
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
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
322 def __init__(self, table, column, name):
\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
330 name = property(lambda s: s.column.name or s.declared_name)
\r
332 def _get_values(self):
\r
333 # TODO: build a list of values used
\r
335 values = property(_get_values)
\r
337 def __unicode__(self):
\r
338 return capfirst(self.column.verbose_name or self.name.replace('_', ' '))
\r
343 class BoundRow(object):
\r
344 """Represents a single row of data, bound to a table.
\r
346 Tables will spawn these row objects, wrapping around the actual data
\r
349 def __init__(self, table, data):
\r
353 def __iter__(self):
\r
354 for value in self.values:
\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
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
368 return item in self
\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
378 class RowValue(StrAndUnicode):
\r
379 """Very basic wrapper around a single row value of a column.
\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
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
393 self.value = value[1]
\r
401 def __unicode__(self):
\r
402 return unicode(self.value)