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 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
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)
179 class TableOptions(object):
181 Extracts and exposes options for a :class:`.Table` from a ``class Meta``
182 when the table is defined.
184 :param options: options for a table
185 :type options: :class:`Meta` on a :class:`.Table`
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)
202 class Table(StrAndUnicode):
204 A collection of columns, plus their associated data rows.
206 :type data: ``list`` or ``QuerySet``
207 :param data: The :term:`table data`.
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`)
213 :type sortable: ``bool``
214 :param sortable: Enable/disable sorting on this table
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`)
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='')``.
229 .. code-block:: python
231 def obj_list(request):
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)
239 __metaclass__ = DeclarativeColumnsMetaclass
240 TableDataClass = TableData
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)
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
260 self.order_by = self._meta.order_by
262 self.order_by = order_by
270 return self._order_by
273 def order_by(self, value):
275 Order the rows of the table based columns. ``value`` must be a sequence
279 order_by = value.split(',') if isinstance(value, basestring) else value
281 order_by = () if order_by is None else order_by
283 # everything's been converted to a iterable, accept iterable!
287 if name in self.columns and self.columns[name].sortable:
289 order_by = OrderByTuple(new)
290 self._order_by = order_by
291 self._data.order_by(order_by)
295 return (self._sequence if self._sequence is not None
296 else self._meta.sequence)
299 def sequence(self, value):
301 value = Sequence(value)
302 value.expand(self.base_columns.keys())
303 self._sequence = value
307 return (self._sortable if self._sortable is not None
308 else self._meta.sortable)
311 def sortable(self, value):
312 self._sortable = value
315 def empty_text(self):
316 return (self._empty_text if self._empty_text is not None
317 else self._meta.empty_text)
320 def empty_text(self, value):
321 self._empty_text = value
333 Render the table to a simple HTML table.
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
340 template = get_template('django_tables/basic_table.html')
341 return template.render(Context({'table': self}))
346 The attributes that should be applied to the ``<table>`` tag when
349 :rtype: :class:`~.utils.AttributeDict` object.
351 return self._attrs if self._attrs is not None else self._meta.attrs
354 def attrs(self, value):
357 def paginate(self, klass=Paginator, per_page=25, page=1, *args, **kwargs):
358 self.paginator = klass(self.rows, per_page, *args, **kwargs)
360 self.page = self.paginator.page(page)
361 except Exception as e:
362 raise Http404(str(e))