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
14 QUERYSET_ACCESSOR_SEPARATOR = '__'
16 class TableData(object):
18 Exposes a consistent API for :term:`table data`. It currently supports a
19 :class:`QuerySet`, or a :class:`list` of :class:`dict` objects.
21 This class is used by :class:.Table` to wrap any
25 def __init__(self, data, table):
26 from django.db.models.query import QuerySet
27 if isinstance(data, QuerySet):
29 elif isinstance(data, list):
32 raise ValueError('data must be a list or QuerySet object, not %s'
33 % data.__class__.__name__)
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')
43 def order_by(self, order_by):
45 Order the data based on column names in the table.
47 :param order_by: the ordering to apply
48 :type order_by: an :class:`~.utils.OrderByTuple` object
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)
58 self.queryset = self.queryset.order_by(*order_by)
60 self.list.sort(cmp=order_by.cmp)
62 def _translate_order_by(self, order_by):
63 """Translate from column names to column accessors"""
67 prefix, name = ((name[0], name[1:]) if name[0] == '-'
69 # find the accessor name
70 column = self._table.columns[name]
71 translated.append(prefix + column.accessor)
72 return OrderByTuple(translated)
74 def __getitem__(self, index):
75 return (self.list if hasattr(self, 'list') else self.queryset)[index]
78 class DeclarativeColumnsMetaclass(type):
80 Metaclass that converts Column attributes on the class to a dictionary
81 called ``base_columns``, taking into account parent class ``base_columns``
85 def __new__(cls, name, bases, attrs, parent_cols_from=None):
86 """Ughhh document this :)
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))
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))
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)
118 class TableOptions(object):
120 Extracts and exposes options for a :class:`.Table` from a ``class Meta``
121 when the table is defined.
123 def __init__(self, options=None):
126 :param options: options for a table
127 :type options: :class:`Meta` on a :class:`.Table`
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', {}))
139 class Table(StrAndUnicode):
140 """A collection of columns, plus their associated data rows.
142 :type data: :class:`list` or :class:`QuerySet`
144 The :term:`table data`.
146 :param: :class:`tuple`-like or :class:`basestring`
148 The description of how the table should be ordered. This allows the
149 :attr:`.Table.Meta.order_by` option to be overridden.
152 Unlike a :class:`Form`, tables are always bound to data.
155 __metaclass__ = DeclarativeColumnsMetaclass
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
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)
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
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)
179 def __unicode__(self):
180 return self.as_html()
188 return self._order_by
191 def order_by(self, value):
193 Order the rows of the table based columns. ``value`` must be a sequence
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
201 # validate, raise exception on failure
203 name = OrderBy(o).bare
204 if name in self.columns and self.columns[name].sortable:
206 order_by = OrderByTuple(new)
207 self._order_by = order_by
208 self._data.order_by(order_by)
219 """Render the table to a simple HTML table.
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
227 template = get_template('django_tables/basic_table.html')
228 return template.render(Context({'table': self}))
232 """The attributes that should be applied to the ``<table>`` tag when
235 :rtype: :class:`~.utils.AttributeDict` object.
238 return self._meta.attrs
240 def paginate(self, klass=Paginator, per_page=25, page=1, *args, **kwargs):
241 self.paginator = klass(self.rows, per_page, *args, **kwargs)
243 self.page = self.paginator.page(page)
244 except Exception as e:
245 raise Http404(str(e))