3 from django.core.paginator import Paginator
4 from django.utils.datastructures import SortedDict
5 from django.http import Http404
6 from django.template.loader import get_template
7 from django.template import Context
8 from django.utils.encoding import StrAndUnicode
9 from .utils import rmprefix, toggleprefix, OrderByTuple, Accessor
10 from .columns import Column
11 from .rows import Rows, BoundRow
12 from .columns import Columns
16 QUERYSET_ACCESSOR_SEPARATOR = '__'
18 class TableData(object):
19 """Exposes a consistent API for a table data. It currently supports a query
20 set and a list of dicts.
22 def __init__(self, data, table):
23 from django.db.models.query import QuerySet
24 self._data = data if not isinstance(data, QuerySet) else None
25 self._queryset = data if isinstance(data, QuerySet) else None
28 # work with a copy of the data that has missing values populated with
31 self._data = copy.copy(self._data)
32 self._populate_missing_values(self._data)
35 # Use the queryset count() method to get the length, instead of
36 # loading all results into memory. This allows, for example,
37 # smart paginators that use len() to perform better.
38 return self._queryset.count() if self._queryset else len(self._data)
40 def order_by(self, order_by):
41 """Order the data based on column names in the table."""
42 # translate order_by to something suitable for this data
43 order_by = self._translate_order_by(order_by)
45 # need to convert the '.' separators to '__' (filter syntax)
46 order_by = order_by.replace(Accessor.SEPARATOR,
47 QUERYSET_ACCESSOR_SEPARATOR)
48 self._queryset = self._queryset.order_by(**order_by)
50 self._data.sort(cmp=order_by.cmp)
52 def _translate_order_by(self, order_by):
53 """Translate from column names to column accessors"""
57 prefix, name = ((name[0], name[1:]) if name[0] == '-'
59 # find the accessor name
60 column = self._table.columns[name]
61 if not isinstance(column.accessor, basestring):
62 raise TypeError('unable to sort on a column that uses a '
64 translated.append(prefix + column.accessor)
65 return OrderByTuple(translated)
67 def _populate_missing_values(self, data):
68 """Populates self._data with missing values based on the default value
69 for each column. It will create new items in the dataset (not modify
72 for i, item in enumerate(data):
73 # add data that is missing from the source. we do this now
74 # so that the column's ``default`` values can affect
75 # sorting (even when callables are used)!
77 # This is a design decision - the alternative would be to
78 # resolve the values when they are accessed, and either do
79 # not support sorting them at all, or run the callables
82 for bound_column in self._table.columns.all():
83 # the following will be True if:
84 # * the source does not provide a value for the column
85 # or the value is None
86 # * the column did provide a data callable that
88 accessor = Accessor(bound_column.accessor)
90 if accessor.resolve(item) is None: # may raise ValueError
91 raise ValueError('None values also need replacing')
93 if modified_item is None:
94 modified_item = copy.copy(item)
95 modified_item[accessor.bits[0]] = bound_column.default
96 if modified_item is not None:
97 data[i] = modified_item
99 def data_for_cell(self, bound_column, bound_row, apply_formatter=True):
100 """Calculate the value of a cell given a bound row and bound column.
102 *formatting* – Apply column formatter after retrieving the value from
105 value = Accessor(bound_column.accessor).resolve(bound_row.data)
106 # try and use default value if we've only got 'None'
107 if value is None and bound_column.default is not None:
108 value = bound_column.default()
109 if apply_formatter and bound_column.formatter:
110 value = bound_column.formatter(value)
113 def __getitem__(self, key):
114 return self._data[key]
117 class DeclarativeColumnsMetaclass(type):
118 """Metaclass that converts Column attributes on the class to a dictionary
119 called 'base_columns', taking into account parent class 'base_columns' as
122 def __new__(cls, name, bases, attrs, parent_cols_from=None):
123 """The ``parent_cols_from`` argument determines from which attribute
124 we read the columns of a base class that this table might be
125 subclassing. This is useful for ``ModelTable`` (and possibly other
126 derivatives) which might want to differ between the declared columns
129 Note that if the attribute specified in ``parent_cols_from`` is not
130 found, we fall back to the default (``base_columns``), instead of
131 skipping over that base. This makes a table like the following work:
133 class MyNewTable(tables.ModelTable, MyNonModelTable):
136 ``MyNewTable`` will be built by the ModelTable metaclass, which will
137 call this base with a modified ``parent_cols_from`` argument
138 specific to ModelTables. Since ``MyNonModelTable`` is not a
139 ModelTable, and thus does not provide that attribute, the columns
140 from that base class would otherwise be ignored.
142 # extract declared columns
143 columns = [(name, attrs.pop(name)) for name, column in attrs.items()
144 if isinstance(column, Column)]
145 columns.sort(lambda x, y: cmp(x[1].creation_counter,
146 y[1].creation_counter))
148 # If this class is subclassing other tables, add their fields as
149 # well. Note that we loop over the bases in *reverse* - this is
150 # necessary to preserve the correct order of columns.
151 for base in bases[::-1]:
152 cols_attr = (parent_cols_from if (parent_cols_from and
153 hasattr(base, parent_cols_from))
155 if hasattr(base, cols_attr):
156 columns = getattr(base, cols_attr).items() + columns
157 # Note that we are reusing an existing ``base_columns`` attribute.
158 # This is because in certain inheritance cases (mixing normal and
159 # ModelTables) this metaclass might be executed twice, and we need
160 # to avoid overriding previous data (because we pop() from attrs,
161 # the second time around columns might not be registered again).
162 # An example would be:
163 # class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass
164 if not 'base_columns' in attrs:
165 attrs['base_columns'] = SortedDict()
166 attrs['base_columns'].update(SortedDict(columns))
167 attrs['_meta'] = TableOptions(attrs.get('Meta', None))
168 return type.__new__(cls, name, bases, attrs)
171 class TableOptions(object):
172 def __init__(self, options=None):
173 super(TableOptions, self).__init__()
174 self.sortable = getattr(options, 'sortable', None)
175 self.order_by = getattr(options, 'order_by', ())
178 class Table(StrAndUnicode):
179 """A collection of columns, plus their associated data rows."""
180 __metaclass__ = DeclarativeColumnsMetaclass
182 # this value is not the same as None. it means 'use the default sort
183 # order', which may (or may not) be inherited from the table options.
184 # None means 'do not sort the data', ignoring the default.
185 DefaultOrder = type('DefaultSortType', (), {})()
186 TableDataClass = TableData
188 def __init__(self, data, order_by=DefaultOrder):
189 """Create a new table instance with the iterable ``data``.
191 If ``order_by`` is specified, the data will be sorted accordingly.
192 Otherwise, the sort order can be specified in the table options.
194 Note that unlike a ``Form``, tables are always bound to data. Also
195 unlike a form, the ``columns`` attribute is read-only and returns
196 ``BoundColumn`` wrappers, similar to the ``BoundField``s you get
197 when iterating over a form. This is because the table iterator
198 already yields rows, and we need an attribute via which to expose
199 the (visible) set of (bound) columns - ``Table.columns`` is simply
200 the perfect fit for this. Instead, ``base_colums`` is copied to
201 table instances, so modifying that will not touch the class-wide
204 self._rows = Rows(self) # bound rows
205 self._columns = Columns(self) # bound columns
206 self._data = self.TableDataClass(data=data, table=self)
208 # None is a valid order, so we must use DefaultOrder as a flag
209 # to fall back to the table sort order.
210 self.order_by = (self._meta.order_by if order_by is Table.DefaultOrder
213 # Make a copy so that modifying this will not touch the class
214 # definition. Note that this is different from forms, where the
215 # copy is made available in a ``fields`` attribute. See the
216 # ``Table`` class docstring for more information.
217 self.base_columns = copy.deepcopy(type(self).base_columns)
219 def __unicode__(self):
220 return self.as_html()
228 return self._order_by
231 def order_by(self, value):
232 """Order the rows of the table based columns. ``value`` must be a
233 sequence of column names.
235 # accept both string and tuple instructions
236 order_by = value.split(',') if isinstance(value, basestring) else value
237 order_by = () if order_by is None else order_by
238 # validate, raise exception on failure
241 if name not in self.columns:
242 raise ValueError('Column "%s" can not be used for ordering as '
243 'it does not exist in the table' % name)
244 if not self.columns[name].sortable:
245 raise ValueError('Column "%s" can not be used for ordering as '
246 'the column has explicitly forbidden it.' %
249 new = OrderByTuple(order_by)
250 if not (hasattr(self, '_order_by') and self._order_by == new):
252 self._data.order_by(new)
263 """Render the table to a simple HTML table.
265 The rendered table won't include pagination or sorting, as those
266 features require a RequestContext. Use the ``render_table`` template
267 tag (requires ``{% load django_tables %}``) if you require this extra
270 template = get_template('django_tables/basic_table.html')
271 return template.render(Context({'table': self}))
273 def paginate(self, klass=Paginator, page=1, *args, **kwargs):
274 self.paginator = klass(self.rows, *args, **kwargs)
276 self.page = self.paginator.page(page)
277 except Exception as e:
278 raise Http404(str(e))