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
16 QUERYSET_ACCESSOR_SEPARATOR = '__'
21 Represents a column sequence, e.g. ("first_name", "...", "last_name")
23 This is used to represent ``Table.Meta.sequence`` or the Table
24 constructors's ``sequence`` keyword argument.
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
31 def expand(self, columns):
33 Expands the "..." item in the sequence into the appropriate column
34 names that should be placed there.
36 :raises: ``ValueError`` if the sequence is invalid for the columns.
39 if self.count("...") > 1:
40 raise ValueError("'...' must be used at most once in a sequence.")
42 # Check for columns in the sequence that don't exist in *columns*
43 extra = (set(self) - set(("...", ))).difference(columns)
45 raise ValueError(u"sequence contains columns that do not exist"
46 u" in the table. Remove '%s'."
49 diff = set(self) ^ set(columns)
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
57 target = head # start by adding things to the head
60 # now we'll start adding elements to the tail
64 target.append(columns.pop(columns.index(name)))
65 self[:] = list(chain(head, columns, tail))
68 class TableData(object):
70 Exposes a consistent API for :term:`table data`. It currently supports a
71 :class:`QuerySet`, or a :class:`list` of :class:`dict` objects.
73 This class is used by :class:`.Table` to wrap any
76 def __init__(self, data, table):
77 if isinstance(data, QuerySet):
79 elif isinstance(data, list):
82 raise ValueError('data must be a list or QuerySet object, not %s'
83 % data.__class__.__name__)
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')
93 def order_by(self, order_by):
95 Order the data based on column names in the table.
97 :param order_by: the ordering to apply
98 :type order_by: an :class:`~.utils.OrderByTuple` object
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)
107 self.queryset = self.queryset.order_by(*order_by)
109 self.list.sort(cmp=order_by.cmp)
111 def _translate_order_by(self, order_by):
112 """Translate from column names to column accessors"""
114 for name in order_by:
115 # handle order prefix
116 prefix, name = ((name[0], name[1:]) if name[0] == '-'
118 # find the accessor name
119 column = self._table.columns[name]
120 translated.append(prefix + column.accessor)
121 return OrderByTuple(translated)
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).
129 return (self.list.__iter__() if hasattr(self, 'list')
130 else self.queryset.__iter__())
132 def __getitem__(self, index):
133 """Forwards indexing accesses to underlying data"""
134 return (self.list if hasattr(self, 'list') else self.queryset)[index]
137 class DeclarativeColumnsMetaclass(type):
139 Metaclass that converts Column attributes on the class to a dictionary
140 called ``base_columns``, taking into account parent class ``base_columns``
144 def __new__(cls, name, bases, attrs):
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))
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.
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
164 # We explicitly pass in verbose_name, so that if the table is
165 # instantiated with non-queryset data, model field verbose_names
167 extra = SortedDict(((f.name, Column(verbose_name=f.verbose_name))
168 for f in opts.model._meta.fields))
169 attrs["base_columns"].update(extra)
170 # Explicit columns override both parent and generated columns
171 attrs["base_columns"].update(SortedDict(columns))
172 # Apply any explicit exclude setting
173 for ex in opts.exclude:
174 if ex in attrs["base_columns"]:
175 attrs["base_columns"].pop(ex)
176 # Now reorder the columns based on explicit sequence
178 opts.sequence.expand(attrs["base_columns"].keys())
179 attrs["base_columns"] = SortedDict(((x, attrs["base_columns"][x]) for x in opts.sequence))
180 return type.__new__(cls, name, bases, attrs)
183 class TableOptions(object):
185 Extracts and exposes options for a :class:`.Table` from a ``class Meta``
186 when the table is defined.
188 :param options: options for a table
189 :type options: :class:`Meta` on a :class:`.Table`
192 def __init__(self, options=None):
193 super(TableOptions, self).__init__()
194 self.attrs = AttributeDict(getattr(options, "attrs", {}))
195 self.empty_text = getattr(options, "empty_text", None)
196 self.exclude = getattr(options, "exclude", ())
197 order_by = getattr(options, "order_by", ())
198 if isinstance(order_by, basestring):
199 order_by = (order_by, )
200 self.order_by = OrderByTuple(order_by)
201 self.sequence = Sequence(getattr(options, "sequence", ()))
202 self.sortable = getattr(options, "sortable", True)
203 self.model = getattr(options, "model", None)
206 class Table(StrAndUnicode):
208 A collection of columns, plus their associated data rows.
210 :type data: ``list`` or ``QuerySet``
211 :param data: The :term:`table data`.
213 :type order_by: ``None``, ``tuple`` or ``string``
214 :param order_by: sort the table based on these columns prior to display.
215 (default :attr:`.Table.Meta.order_by`)
217 :type sortable: ``bool``
218 :param sortable: Enable/disable sorting on this table
220 :type empty_text: ``string``
221 :param empty_text: Empty text to render when the table has no data.
222 (default :attr:`.Table.Meta.empty_text`)
224 The ``order_by`` argument is optional and allows the table's
225 ``Meta.order_by`` option to be overridden. If the ``order_by is None``
226 the table's ``Meta.order_by`` will be used. If you want to disable a
227 default ordering, simply use an empty ``tuple``, ``string``, or ``list``,
228 e.g. ``Table(…, order_by='')``.
233 .. code-block:: python
235 def obj_list(request):
237 # If there's no ?sort=…, we don't want to fallback to
238 # Table.Meta.order_by, thus we must not default to passing in None
239 order_by = request.GET.get('sort', ())
240 table = SimpleTable(data, order_by=order_by)
243 __metaclass__ = DeclarativeColumnsMetaclass
244 TableDataClass = TableData
246 def __init__(self, data, order_by=None, sortable=None, empty_text=None,
247 exclude=None, attrs=None, sequence=None):
248 self._rows = BoundRows(self)
249 self._columns = BoundColumns(self)
250 self._data = self.TableDataClass(data=data, table=self)
252 self.empty_text = empty_text
253 self.sortable = sortable
254 # Make a copy so that modifying this will not touch the class
255 # definition. Note that this is different from forms, where the
256 # copy is made available in a ``fields`` attribute.
257 self.base_columns = copy.deepcopy(self.__class__.base_columns)
258 self.exclude = exclude or ()
259 for ex in self.exclude:
260 if ex in self.base_columns:
261 self.base_columns.pop(ex)
262 self.sequence = sequence
264 self.order_by = self._meta.order_by
266 self.order_by = order_by
274 return self._order_by
277 def order_by(self, value):
279 Order the rows of the table based columns. ``value`` must be a sequence
283 order_by = value.split(',') if isinstance(value, basestring) else value
285 order_by = () if order_by is None else order_by
287 # everything's been converted to a iterable, accept iterable!
291 if name in self.columns and self.columns[name].sortable:
293 order_by = OrderByTuple(new)
294 self._order_by = order_by
295 self._data.order_by(order_by)
299 return (self._sequence if self._sequence is not None
300 else self._meta.sequence)
303 def sequence(self, value):
305 value = Sequence(value)
306 value.expand(self.base_columns.keys())
307 self._sequence = value
311 return (self._sortable if self._sortable is not None
312 else self._meta.sortable)
315 def sortable(self, value):
316 self._sortable = value
319 def empty_text(self):
320 return (self._empty_text if self._empty_text is not None
321 else self._meta.empty_text)
324 def empty_text(self, value):
325 self._empty_text = value
337 Render the table to a simple HTML table.
339 The rendered table won't include pagination or sorting, as those
340 features require a RequestContext. Use the ``render_table`` template
341 tag (requires ``{% load django_tables2 %}``) if you require this extra
344 template = get_template('django_tables2/basic_table.html')
345 return template.render(Context({'table': self}))
350 The attributes that should be applied to the ``<table>`` tag when
353 :rtype: :class:`~.utils.AttributeDict` object.
355 return self._attrs if self._attrs is not None else self._meta.attrs
358 def attrs(self, value):
361 def paginate(self, klass=Paginator, per_page=25, page=1, *args, **kwargs):
362 self.paginator = klass(self.rows, per_page, *args, **kwargs)
364 self.page = self.paginator.page(page)
365 except Exception as e:
366 raise Http404(str(e))