* Added pagination
[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 OrderBy, OrderByTuple, Accessor, AttributeDict
10 from .rows import BoundRows, BoundRow
11 from .columns import BoundColumns, Column
12
13
14 QUERYSET_ACCESSOR_SEPARATOR = '__'
15
16 class TableData(object):
17     """
18     Exposes a consistent API for :term:`table data`. It currently supports a
19     :class:`QuerySet`, or a :class:`list` of :class:`dict` objects.
20
21     This class is used by :class:.Table` to wrap any
22     input table data.
23
24     """
25     def __init__(self, data, table):
26         from django.db.models.query import QuerySet
27         if isinstance(data, QuerySet):
28             self.queryset = data
29         elif isinstance(data, list):
30             self.list = data[:]
31         else:
32             raise ValueError('data must be a list or QuerySet object, not %s'
33                              % data.__class__.__name__)
34         self._table = table
35
36     def __len__(self):
37         # Use the queryset count() method to get the length, instead of
38         # loading all results into memory. This allows, for example,
39         # smart paginators that use len() to perform better.
40         return (self.queryset.count() if hasattr(self, 'queryset')
41                                       else len(self.list))
42
43     def order_by(self, order_by):
44         """
45         Order the data based on column names in the table.
46
47         :param order_by: the ordering to apply
48         :type order_by: an :class:`~.utils.OrderByTuple` object
49
50         """
51         # translate order_by to something suitable for this data
52         order_by = self._translate_order_by(order_by)
53         if hasattr(self, 'queryset'):
54             # need to convert the '.' separators to '__' (filter syntax)
55             order_by = [o.replace(Accessor.SEPARATOR,
56                                   QUERYSET_ACCESSOR_SEPARATOR)
57                         for o in order_by]
58             self.queryset = self.queryset.order_by(*order_by)
59         else:
60             self.list.sort(cmp=order_by.cmp)
61
62     def _translate_order_by(self, order_by):
63         """Translate from column names to column accessors"""
64         translated = []
65         for name in order_by:
66             # handle order prefix
67             prefix, name = ((name[0], name[1:]) if name[0] == '-'
68                                                 else ('', name))
69             # find the accessor name
70             column = self._table.columns[name]
71             translated.append(prefix + column.accessor)
72         return OrderByTuple(translated)
73
74     def __getitem__(self, index):
75         return (self.list if hasattr(self, 'list') else self.queryset)[index]
76
77
78 class DeclarativeColumnsMetaclass(type):
79     """
80     Metaclass that converts Column attributes on the class to a dictionary
81     called ``base_columns``, taking into account parent class ``base_columns``
82     as well.
83
84     """
85     def __new__(cls, name, bases, attrs, parent_cols_from=None):
86         """Ughhh document this :)
87
88         """
89         # extract declared columns
90         columns = [(name, attrs.pop(name)) for name, column in attrs.items()
91                                            if isinstance(column, Column)]
92         columns.sort(lambda x, y: cmp(x[1].creation_counter,
93                                       y[1].creation_counter))
94
95         # If this class is subclassing other tables, add their fields as
96         # well. Note that we loop over the bases in *reverse* - this is
97         # necessary to preserve the correct order of columns.
98         for base in bases[::-1]:
99             cols_attr = (parent_cols_from if (parent_cols_from and
100                                              hasattr(base, parent_cols_from))
101                                           else 'base_columns')
102             if hasattr(base, cols_attr):
103                 columns = getattr(base, cols_attr).items() + columns
104         # Note that we are reusing an existing ``base_columns`` attribute.
105         # This is because in certain inheritance cases (mixing normal and
106         # ModelTables) this metaclass might be executed twice, and we need
107         # to avoid overriding previous data (because we pop() from attrs,
108         # the second time around columns might not be registered again).
109         # An example would be:
110         #    class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass
111         if not 'base_columns' in attrs:
112             attrs['base_columns'] = SortedDict()
113         attrs['base_columns'].update(SortedDict(columns))
114         attrs['_meta'] = TableOptions(attrs.get('Meta', None))
115         return type.__new__(cls, name, bases, attrs)
116
117
118 class TableOptions(object):
119     """
120     Extracts and exposes options for a :class:`.Table` from a ``class Meta``
121     when the table is defined.
122     """
123     def __init__(self, options=None):
124         """
125
126         :param options: options for a table
127         :type options: :class:`Meta` on a :class:`.Table`
128
129         """
130         super(TableOptions, self).__init__()
131         self.sortable = getattr(options, 'sortable', None)
132         order_by = getattr(options, 'order_by', ())
133         if isinstance(order_by, basestring):
134             order_by = (order_by, )
135         self.order_by = OrderByTuple(order_by)
136         self.attrs = AttributeDict(getattr(options, 'attrs', {}))
137
138
139 class Table(StrAndUnicode):
140     """A collection of columns, plus their associated data rows.
141
142     :type data: :class:`list` or :class:`QuerySet`
143     :param data:
144         The :term:`table data`.
145
146     :param: :class:`tuple`-like or :class:`basestring`
147     :param order_by:
148         The description of how the table should be ordered. This allows the
149         :attr:`.Table.Meta.order_by` option to be overridden.
150
151     .. note::
152         Unlike a :class:`Form`, tables are always bound to data.
153
154     """
155     __metaclass__ = DeclarativeColumnsMetaclass
156
157     # this value is not the same as None. it means 'use the default sort
158     # order', which may (or may not) be inherited from the table options.
159     # None means 'do not sort the data', ignoring the default.
160     DefaultOrder = type('DefaultSortType', (), {})()
161     TableDataClass = TableData
162
163     def __init__(self, data, order_by=DefaultOrder):
164         self._rows = BoundRows(self)  # bound rows
165         self._columns = BoundColumns(self)  # bound columns
166         self._data = self.TableDataClass(data=data, table=self)
167
168         # None is a valid order, so we must use DefaultOrder as a flag
169         # to fall back to the table sort order.
170         self.order_by = (self._meta.order_by if order_by is Table.DefaultOrder
171                                              else order_by)
172
173         # Make a copy so that modifying this will not touch the class
174         # definition. Note that this is different from forms, where the
175         # copy is made available in a ``fields`` attribute. See the
176         # ``Table`` class docstring for more information.
177         self.base_columns = copy.deepcopy(type(self).base_columns)
178
179     def __unicode__(self):
180         return self.as_html()
181
182     @property
183     def data(self):
184         return self._data
185
186     @property
187     def order_by(self):
188         return self._order_by
189
190     @order_by.setter
191     def order_by(self, value):
192         """
193         Order the rows of the table based columns. ``value`` must be a sequence
194         of column names.
195
196         """
197         # accept both string and tuple instructions
198         order_by = value.split(',') if isinstance(value, basestring) else value
199         order_by = () if order_by is None else order_by
200         new = []
201         # validate, raise exception on failure
202         for o in order_by:
203             name = OrderBy(o).bare
204             if name in self.columns and self.columns[name].sortable:
205                 new.append(o)
206         order_by = OrderByTuple(new)
207         self._order_by = order_by
208         self._data.order_by(order_by)
209
210     @property
211     def rows(self):
212         return self._rows
213
214     @property
215     def columns(self):
216         return self._columns
217
218     def as_html(self):
219         """Render the table to a simple HTML table.
220
221         The rendered table won't include pagination or sorting, as those
222         features require a RequestContext. Use the ``render_table`` template
223         tag (requires ``{% load django_tables %}``) if you require this extra
224         functionality.
225
226         """
227         template = get_template('django_tables/basic_table.html')
228         return template.render(Context({'table': self}))
229
230     @property
231     def attrs(self):
232         """The attributes that should be applied to the ``<table>`` tag when
233         rendering HTML.
234
235         :rtype: :class:`~.utils.AttributeDict` object.
236
237         """
238         return self._meta.attrs
239
240     def paginate(self, klass=Paginator, per_page=25, page=1, *args, **kwargs):
241         self.paginator = klass(self.rows, per_page, *args, **kwargs)
242         try:
243             self.page = self.paginator.page(page)
244         except Exception as e:
245             raise Http404(str(e))