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', 'options')
\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
13 Dict values can be callables.
\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
21 return reverse and -res or res
\r
25 if o.startswith('-'):
\r
26 instructions.append((o[1:], True,))
\r
28 instructions.append((o, False,))
\r
31 class DeclarativeColumnsMetaclass(type):
\r
33 Metaclass that converts Column attributes to a dictionary called
\r
34 'base_columns', taking into account parent class 'base_columns'
\r
37 def __new__(cls, name, bases, attrs, parent_cols_from=None):
\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
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
49 class MyNewTable(tables.ModelTable, MyNonModelTable):
\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
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
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
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
86 return type.__new__(cls, name, bases, attrs)
\r
89 """Normalize a column name by removing a potential sort prefix"""
\r
90 return (s[:1]=='-' and [s[1:]] or [s])[0]
\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
97 Also supports some functionality to interact with and modify
\r
100 def __unicode__(self):
\r
101 """Output in our input format."""
\r
102 return ",".join(self)
\r
104 def __contains__(self, name):
\r
105 """Determine whether a column is part of this order."""
\r
107 if rmprefix(o) == name:
\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
118 def is_straight(self, name):
\r
119 """The opposite of is_reversed."""
\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
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
134 prefix = reverse and '-' or ''
\r
135 return OrderByTuple(
\r
138 # add either untouched, or reversed
\r
139 (names and rmprefix(o) not in names)
\r
141 or [prefix+rmprefix(o)]
\r
145 [prefix+name for name in names if not name in self]
\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
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
160 # add either untouched, or toggled
\r
161 (names and rmprefix(o) not in names)
\r
163 or ((o[:1] == '-') and [o[1:]] or ["-"+o])
\r
166 + # !!!: test for addition
\r
167 [name for name in names if not name in self]
\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
176 class DefaultOptions(object):
\r
177 IGNORE_INVALID_OPTIONS = True
\r
178 options = DefaultOptions()
\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
184 If ``order_by`` is specified, the data will be sorted accordingly.
\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
197 self._snapshot = None # will store output dataset (ordered...)
\r
198 self._rows = Rows(self)
\r
199 self._columns = Columns(self)
\r
201 self.order_by = order_by
\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
209 def _build_snapshot(self):
\r
210 """Rebuilds the table whenever it's options change.
\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
216 In the case of this base table implementation, a copy of the
\r
217 source data is created, and then modified appropriately.
\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
225 self._columns._reset()
\r
226 self._rows._reset()
\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
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
244 name_in_source = column.column.data
\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
253 sort_table(snapshot, self._cols_to_fields(self.order_by))
\r
254 self._snapshot = snapshot
\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
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
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
272 Supports prefixed column names as used e.g. in order_by ("-field").
\r
275 for ident in names:
\r
276 # handle order prefix
\r
277 if ident[:1] == '-':
\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
288 name_in_source = column.declared_name
\r
289 result.append(prefix + name_in_source)
\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
297 Can be overridden by subclasses to impose further restrictions.
\r
299 if purpose == 'order_by':
\r
300 return name in self.columns and\
\r
301 self.columns[name].column.sortable
\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
313 # validate, remove all invalid order instructions
\r
314 validated_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
322 self._order_by = OrderByTuple()
\r
323 order_by = property(lambda s: s._order_by, _set_order_by)
\r
325 def __unicode__(self):
\r
326 return self.as_html()
\r
328 def __iter__(self):
\r
329 for row in self.rows:
\r
332 def __getitem__(self, key):
\r
333 return self.rows[key]
\r
335 # just to make those readonly
\r
336 columns = property(lambda s: s._columns)
\r
337 rows = property(lambda s: s._rows)
\r
343 """Update the table based on it's current options.
\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
351 self._build_snapshot()
\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
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
366 class Columns(object):
\r
367 """Container for spawning BoundColumns.
\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
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
378 def __init__(self, table):
\r
380 self._columns = SortedDict()
\r
383 """Used by parent table class."""
\r
384 self._columns = SortedDict()
\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
397 new_columns[exposed_name] = BoundColumn(self.table, column, decl_name)
\r
398 self._columns = new_columns
\r
401 """Iterate through all columns, regardless of visiblity (as
\r
402 opposed to ``__iter__``.
\r
404 This is used internally a lot.
\r
406 self._spawn_columns()
\r
407 for column in self._columns.values():
\r
411 self._spawn_columns()
\r
412 for r in self._columns.items():
\r
416 self._spawn_columns()
\r
417 for r in self._columns.keys():
\r
420 def index(self, name):
\r
421 self._spawn_columns()
\r
422 return self._columns.keyOrder.index(name)
\r
424 def __iter__(self):
\r
425 """Iterate through all *visible* bound columns.
\r
427 This is primarily geared towards table rendering.
\r
429 for column in self.all():
\r
430 if column.column.visible:
\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
439 return item in self.all()
\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
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
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
457 def __init__(self, table, column, name):
\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
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
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
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
484 Make sure ``row`` is a ``BoundRow`` object, since that is what
\r
485 we promise the callable will get.
\r
487 if callable(self.column.default):
\r
488 return self.column.default(row)
\r
489 return self.column.default
\r
491 def _get_values(self):
\r
492 # TODO: build a list of values used
\r
494 values = property(_get_values)
\r
496 def __unicode__(self):
\r
497 return capfirst(self.column.verbose_name or self.name.replace('_', ' '))
\r
502 class Rows(object):
\r
503 """Container for spawning BoundRows.
\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
509 def __init__(self, table, row_klass=None):
\r
511 self.row_klass = row_klass and row_klass or BoundRow
\r
514 pass # we currently don't use a cache
\r
517 """Return all rows."""
\r
518 for row in self.table.data:
\r
519 yield self.row_klass(self.table, row)
\r
522 """Return rows on current page (if paginated)."""
\r
523 if not hasattr(self.table, 'page'):
\r
525 return iter(self.table.page.object_list)
\r
527 def __iter__(self):
\r
528 return iter(self.all())
\r
531 return len(self.table.data)
\r
533 def __getitem__(self, key):
\r
534 if isinstance(key, slice):
\r
536 for row in self.table.data[key]:
\r
537 result.append(self.row_klass(self.table, row))
\r
539 elif isinstance(key, int):
\r
540 return self.row_klass(self.table, self.table.data[key])
\r
542 raise TypeError('Key must be a slice or integer.')
\r
544 class BoundRow(object):
\r
545 """Represents a single row of data, bound to a table.
\r
547 Tables will spawn these row objects, wrapping around the actual data
\r
550 def __init__(self, table, data):
\r
554 def __iter__(self):
\r
555 for value in self.values:
\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
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
566 # if the field we are pointing to is a callable, remove it
\r
567 if callable(result):
\r
568 result = result(self)
\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
576 return item in self
\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