df26d8f747b623807f6396561b4d229c70df4c7a
[django-tables2.git] / django_tables / tables.py
1 # -*- coding: utf8 -*-
2 import copy
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
13
14 __all__ = ('Table',)
15
16 QUERYSET_ACCESSOR_SEPARATOR = '__'
17
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.
21     """
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
26         self._table = table
27
28         # work with a copy of the data that has missing values populated with
29         # defaults.
30         if self._data:
31             self._data = copy.copy(self._data)
32             self._populate_missing_values(self._data)
33
34     def __len__(self):
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)
39
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)
44         if self._queryset:
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)
49         else:
50             self._data.sort(cmp=order_by.cmp)
51
52     def _translate_order_by(self, order_by):
53         """Translate from column names to column accessors"""
54         translated = []
55         for name in order_by:
56             # handle order prefix
57             prefix, name = ((name[0], name[1:]) if name[0] == '-'
58                                                 else ('', name))
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 '
63                                 'callable accessor')
64             translated.append(prefix + column.accessor)
65         return OrderByTuple(translated)
66
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
70         existing ones).
71         """
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)!
76             #
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
80             # during sorting.
81             modified_item = None
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
87                 #   returned None
88                 accessor = Accessor(bound_column.accessor)
89                 try:
90                     if accessor.resolve(item) is None:  # may raise ValueError
91                         raise ValueError('None values also need replacing')
92                 except ValueError:
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
98
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.
101
102         *formatting* – Apply column formatter after retrieving the value from
103                        the data.
104         """
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)
111         return value
112
113     def __getitem__(self, key):
114         return self._data[key]
115
116
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
120     well.
121     """
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
127         and others.
128
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:
132
133             class MyNewTable(tables.ModelTable, MyNonModelTable):
134                 pass
135
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.
141         """
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))
147
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))
154                                           else 'base_columns')
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)
169
170
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', ())
176
177
178 class Table(StrAndUnicode):
179     """A collection of columns, plus their associated data rows."""
180     __metaclass__ = DeclarativeColumnsMetaclass
181
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
187
188     def __init__(self, data, order_by=DefaultOrder):
189         """Create a new table instance with the iterable ``data``.
190
191         If ``order_by`` is specified, the data will be sorted accordingly.
192         Otherwise, the sort order can be specified in the table options.
193
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
202         column list.
203         """
204         self._rows = Rows(self)  # bound rows
205         self._columns = Columns(self)  # bound columns
206         self._data = self.TableDataClass(data=data, table=self)
207
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
211                                              else order_by)
212
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)
218
219     def __unicode__(self):
220         return self.as_html()
221
222     @property
223     def data(self):
224         return self._data
225
226     @property
227     def order_by(self):
228         return self._order_by
229
230     @order_by.setter
231     def order_by(self, value):
232         """Order the rows of the table based columns. ``value`` must be a
233         sequence of column names.
234         """
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
239         for o in order_by:
240             name = rmprefix(o)
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.' %
247                                  name)
248
249         new = OrderByTuple(order_by)
250         if not (hasattr(self, '_order_by') and self._order_by == new):
251             self._order_by = new
252             self._data.order_by(new)
253
254     @property
255     def rows(self):
256         return self._rows
257
258     @property
259     def columns(self):
260         return self._columns
261
262     def as_html(self):
263         """Render the table to a simple HTML table.
264
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
268         functionality.
269         """
270         template = get_template('django_tables/basic_table.html')
271         return template.render(Context({'table': self}))
272
273     def paginate(self, klass=Paginator, page=1, *args, **kwargs):
274         self.paginator = klass(self.rows, *args, **kwargs)
275         try:
276             self.page = self.paginator.page(page)
277         except Exception as e:
278             raise Http404(str(e))