* Table's __unicode__ method no longer returns as_html()
[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 django.db.models.query import QuerySet
10 from itertools import chain
11 from .utils import OrderBy, OrderByTuple, Accessor, AttributeDict
12 from .rows import BoundRows, BoundRow
13 from .columns import BoundColumns, Column
14
15
16 QUERYSET_ACCESSOR_SEPARATOR = '__'
17
18
19 class Sequence(list):
20     """
21     Represents a column sequence, e.g. ("first_name", "...", "last_name")
22
23     This is used to represent ``Table.Meta.sequence`` or the Table
24     constructors's ``sequence`` keyword argument.
25
26     The sequence must be a list of column names and is used to specify the
27     order of the columns on a table. Optionally a "..." item can be inserted,
28     which is treated as a *catch-all* for column names that aren't explicitly
29     specified.
30     """
31     def expand(self, columns):
32         """
33         Expands the "..." item in the sequence into the appropriate column
34         names that should be placed there.
35
36         :raises: ``ValueError`` if the sequence is invalid for the columns.
37         """
38         # validation
39         if self.count("...") > 1:
40             raise ValueError("'...' must be used at most once in a sequence.")
41         elif "..." in self:
42             # Check for columns in the sequence that don't exist in *columns*
43             extra = (set(self) - set(("...", ))).difference(columns)
44             if extra:
45                 raise ValueError(u"sequence contains columns that do not exist"
46                                  u" in the table. Remove '%s'."
47                                  % "', '".join(extra))
48         else:
49             diff = set(self) ^ set(columns)
50             if diff:
51                 raise ValueError(u"sequence does not match columns. Fix '%s' "
52                                  u"or possibly add '...'." % "', '".join(diff))
53         # everything looks good, let's expand the "..." item
54         columns = columns[:]  # don't modify
55         head = []
56         tail = []
57         target = head  # start by adding things to the head
58         for name in self:
59             if name == "...":
60                 # now we'll start adding elements to the tail
61                 target = tail
62                 continue
63             else:
64                 target.append(columns.pop(columns.index(name)))
65         self[:] = list(chain(head, columns, tail))
66
67
68 class TableData(object):
69     """
70     Exposes a consistent API for :term:`table data`. It currently supports a
71     :class:`QuerySet`, or a :class:`list` of :class:`dict` objects.
72
73     This class is used by :class:`.Table` to wrap any
74     input table data.
75     """
76     def __init__(self, data, table):
77         if isinstance(data, QuerySet):
78             self.queryset = data
79         elif isinstance(data, list):
80             self.list = data[:]
81         else:
82             raise ValueError('data must be a list or QuerySet object, not %s'
83                              % data.__class__.__name__)
84         self._table = table
85
86     def __len__(self):
87         # Use the queryset count() method to get the length, instead of
88         # loading all results into memory. This allows, for example,
89         # smart paginators that use len() to perform better.
90         return (self.queryset.count() if hasattr(self, 'queryset')
91                                       else len(self.list))
92
93     def order_by(self, order_by):
94         """
95         Order the data based on column names in the table.
96
97         :param order_by: the ordering to apply
98         :type order_by: an :class:`~.utils.OrderByTuple` object
99         """
100         # translate order_by to something suitable for this data
101         order_by = self._translate_order_by(order_by)
102         if hasattr(self, 'queryset'):
103             # need to convert the '.' separators to '__' (filter syntax)
104             order_by = [o.replace(Accessor.SEPARATOR,
105                                   QUERYSET_ACCESSOR_SEPARATOR)
106                         for o in order_by]
107             self.queryset = self.queryset.order_by(*order_by)
108         else:
109             self.list.sort(cmp=order_by.cmp)
110
111     def _translate_order_by(self, order_by):
112         """Translate from column names to column accessors"""
113         translated = []
114         for name in order_by:
115             # handle order prefix
116             prefix, name = ((name[0], name[1:]) if name[0] == '-'
117                                                 else ('', name))
118             # find the accessor name
119             column = self._table.columns[name]
120             translated.append(prefix + column.accessor)
121         return OrderByTuple(translated)
122
123     def __iter__(self):
124         """
125         for ... in ... default to using this. There's a bug in Django 1.3
126         with indexing into querysets, so this side-steps that problem (as well
127         as just being a better way to iterate).
128         """
129         return (self.list.__iter__() if hasattr(self, 'list')
130                                      else self.queryset.__iter__())
131
132     def __getitem__(self, index):
133         """Forwards indexing accesses to underlying data"""
134         return (self.list if hasattr(self, 'list') else self.queryset)[index]
135
136
137 class DeclarativeColumnsMetaclass(type):
138     """
139     Metaclass that converts Column attributes on the class to a dictionary
140     called ``base_columns``, taking into account parent class ``base_columns``
141     as well.
142     """
143
144     def __new__(cls, name, bases, attrs):
145
146         attrs["_meta"] = opts = TableOptions(attrs.get("Meta", None))
147         # extract declared columns
148         columns = [(name_, attrs.pop(name_)) for name_, column in attrs.items()
149                                              if isinstance(column, Column)]
150         columns.sort(lambda x, y: cmp(x[1].creation_counter,
151                                       y[1].creation_counter))
152
153         # If this class is subclassing other tables, add their fields as
154         # well. Note that we loop over the bases in *reverse* - this is
155         # necessary to preserve the correct order of columns.
156         parent_columns = []
157         for base in bases[::-1]:
158             if hasattr(base, "base_columns"):
159                 parent_columns = base.base_columns.items() + parent_columns
160         # Start with the parent columns
161         attrs["base_columns"] = SortedDict(parent_columns)
162         # Possibly add some generated columns based on a model
163         if opts.model:
164             extra = SortedDict(((f.name, Column()) for f in opts.model._meta.fields))
165             attrs["base_columns"].update(extra)
166         # Explicit columns override both parent and generated columns
167         attrs["base_columns"].update(SortedDict(columns))
168         # Apply any explicit exclude setting
169         for ex in opts.exclude:
170             if ex in attrs["base_columns"]:
171                 attrs["base_columns"].pop(ex)
172         # Now reorder the columns based on explicit sequence
173         if opts.sequence:
174             opts.sequence.expand(attrs["base_columns"].keys())
175             attrs["base_columns"] = SortedDict(((x, attrs["base_columns"][x]) for x in opts.sequence))
176         return type.__new__(cls, name, bases, attrs)
177
178
179 class TableOptions(object):
180     """
181     Extracts and exposes options for a :class:`.Table` from a ``class Meta``
182     when the table is defined.
183
184     :param options: options for a table
185     :type options: :class:`Meta` on a :class:`.Table`
186     """
187
188     def __init__(self, options=None):
189         super(TableOptions, self).__init__()
190         self.attrs = AttributeDict(getattr(options, "attrs", {}))
191         self.empty_text = getattr(options, "empty_text", None)
192         self.exclude = getattr(options, "exclude", ())
193         order_by = getattr(options, "order_by", ())
194         if isinstance(order_by, basestring):
195             order_by = (order_by, )
196         self.order_by = OrderByTuple(order_by)
197         self.sequence = Sequence(getattr(options, "sequence", ()))
198         self.sortable = getattr(options, "sortable", True)
199         self.model = getattr(options, "model", None)
200
201
202 class Table(StrAndUnicode):
203     """
204     A collection of columns, plus their associated data rows.
205
206     :type data:  ``list`` or ``QuerySet``
207     :param data: The :term:`table data`.
208
209     :type order_by: ``None``, ``tuple`` or ``string``
210     :param order_by: sort the table based on these columns prior to display.
211         (default :attr:`.Table.Meta.order_by`)
212
213     :type sortable: ``bool``
214     :param sortable: Enable/disable sorting on this table
215
216     :type empty_text: ``string``
217     :param empty_text: Empty text to render when the table has no data.
218         (default :attr:`.Table.Meta.empty_text`)
219
220     The ``order_by`` argument is optional and allows the table's
221     ``Meta.order_by`` option to be overridden. If the ``order_by is None``
222     the table's ``Meta.order_by`` will be used. If you want to disable a
223     default ordering, simply use an empty ``tuple``, ``string``, or ``list``,
224     e.g. ``Table(…, order_by='')``.
225
226
227     Example:
228
229     .. code-block:: python
230
231         def obj_list(request):
232             ...
233             # If there's no ?sort=…, we don't want to fallback to
234             # Table.Meta.order_by, thus we must not default to passing in None
235             order_by = request.GET.get('sort', ())
236             table = SimpleTable(data, order_by=order_by)
237             ...
238     """
239     __metaclass__ = DeclarativeColumnsMetaclass
240     TableDataClass = TableData
241
242     def __init__(self, data, order_by=None, sortable=None, empty_text=None,
243                  exclude=None, attrs=None, sequence=None):
244         self._rows = BoundRows(self)
245         self._columns = BoundColumns(self)
246         self._data = self.TableDataClass(data=data, table=self)
247         self.attrs = attrs
248         self.empty_text = empty_text
249         self.sortable = sortable
250         # Make a copy so that modifying this will not touch the class
251         # definition. Note that this is different from forms, where the
252         # copy is made available in a ``fields`` attribute.
253         self.base_columns = copy.deepcopy(self.__class__.base_columns)
254         self.exclude = exclude or ()
255         for ex in self.exclude:
256             if ex in self.base_columns:
257                 self.base_columns.pop(ex)
258         self.sequence = sequence
259         if order_by is None:
260             self.order_by = self._meta.order_by
261         else:
262             self.order_by = order_by
263
264     @property
265     def data(self):
266         return self._data
267
268     @property
269     def order_by(self):
270         return self._order_by
271
272     @order_by.setter
273     def order_by(self, value):
274         """
275         Order the rows of the table based columns. ``value`` must be a sequence
276         of column names.
277         """
278         # accept string
279         order_by = value.split(',') if isinstance(value, basestring) else value
280         # accept None
281         order_by = () if order_by is None else order_by
282         new = []
283         # everything's been converted to a iterable, accept iterable!
284         for o in order_by:
285             ob = OrderBy(o)
286             name = ob.bare
287             if name in self.columns and self.columns[name].sortable:
288                 new.append(ob)
289         order_by = OrderByTuple(new)
290         self._order_by = order_by
291         self._data.order_by(order_by)
292
293     @property
294     def sequence(self):
295         return (self._sequence if self._sequence is not None
296                                else self._meta.sequence)
297
298     @sequence.setter
299     def sequence(self, value):
300         if value:
301             value = Sequence(value)
302             value.expand(self.base_columns.keys())
303         self._sequence = value
304
305     @property
306     def sortable(self):
307         return (self._sortable if self._sortable is not None
308                                else self._meta.sortable)
309
310     @sortable.setter
311     def sortable(self, value):
312         self._sortable = value
313
314     @property
315     def empty_text(self):
316         return (self._empty_text if self._empty_text is not None
317                                  else self._meta.empty_text)
318
319     @empty_text.setter
320     def empty_text(self, value):
321         self._empty_text = value
322
323     @property
324     def rows(self):
325         return self._rows
326
327     @property
328     def columns(self):
329         return self._columns
330
331     def as_html(self):
332         """
333         Render the table to a simple HTML table.
334
335         The rendered table won't include pagination or sorting, as those
336         features require a RequestContext. Use the ``render_table`` template
337         tag (requires ``{% load django_tables %}``) if you require this extra
338         functionality.
339         """
340         template = get_template('django_tables/basic_table.html')
341         return template.render(Context({'table': self}))
342
343     @property
344     def attrs(self):
345         """
346         The attributes that should be applied to the ``<table>`` tag when
347         rendering HTML.
348
349         :rtype: :class:`~.utils.AttributeDict` object.
350         """
351         return self._attrs if self._attrs is not None else self._meta.attrs
352
353     @attrs.setter
354     def attrs(self, value):
355         self._attrs = value
356
357     def paginate(self, klass=Paginator, per_page=25, page=1, *args, **kwargs):
358         self.paginator = klass(self.rows, per_page, *args, **kwargs)
359         try:
360             self.page = self.paginator.page(page)
361         except Exception as e:
362             raise Http404(str(e))