pax_global_header00006660000000000000000000000064126422711620014515gustar00rootroot0000000000000052 comment=6676312f1ddec6ef544783c0895a603888dc78e1 django-tables2-1.0.7/000077500000000000000000000000001264227116200143165ustar00rootroot00000000000000django-tables2-1.0.7/.coveragerc000066400000000000000000000001211264227116200164310ustar00rootroot00000000000000[run] source = django_tables2 tests [html] directory = reports/htmlcov django-tables2-1.0.7/.gitignore000066400000000000000000000003071264227116200163060ustar00rootroot00000000000000*.pyc /.env /reports /*.sublime-* /*.komodoproject /*.tmproj /*.egg-info/ /*.egg /.tox /.coverage /MANIFEST /dist/ /build/ /docs/_build/ /example/database.sqlite /example/.env /report.pylint .cache/ django-tables2-1.0.7/.travis.yml000066400000000000000000000013751264227116200164350ustar00rootroot00000000000000language: python # django 1.9 is only supported on py27 and py35 # django 1.8 is the first realease supporting py35 matrix: include: - python: 2.7 env: TOXENV=py27-1.7 - python: 2.7 env: TOXENV=py27-1.8 - python: 3.3 env: TOXENV=py33-1.7 - python: 3.3 env: TOXENV=py33-1.8 - python: 3.4 env: TOXENV=py34-1.7 - python: 3.4 env: TOXENV=py34-1.8 - python: 3.4 env: TOXENV=py34-1.9 - python: 3.5 env: TOXENV=py35-1.8 - python: 3.5 env: TOXENV=py35-1.9 - python: 2.7 env: TOXENV=py27-master - python: 3.5 env: TOXENV=py35-master allow_failures: - env: TOXENV=py27-master - env: TOXENV=py35-master install: - pip install tox script: - tox django-tables2-1.0.7/CHANGELOG.md000066400000000000000000000242461264227116200161370ustar00rootroot00000000000000# Change log ## v1.0.7 (2016-01-03) - Explicitly check if `column.verbose_name` is not None to support empty column headers (fixes #280) - Cleanup the example project to make it work with modern Django versions. - Do not sort queryset when orderable=False (#204 by [@bmihelac](https://github.com/bmihelac)) - `show_header` attribute on `Table` allows disabling the header (#175 by [@kviktor](https://github.com/kviktor)) - `LinkColumn` now tries to call `get_absolute_url` on a record if no `viewname` is provided (#283, fixes #231). - Add `request` argument to `Table.as_html()` to allow passing correct request objects instead of poorly generated ones #282 - Add coverage reporting to build #282 - Drop support for python 3.2 (because of coverage), support ends feb 2016 #282 - move `build_request` from `django_table2.utils` to `tests.utils` and amend tests #282 ## v1.0.6 (2015-12-29) - Support for custom text value in LinkColumn (#277 by [@toudi](https://github.com/toudi)) - Refactor LinkColumn.render_link() to not escape twice #279 - Removed `Attrs` (wrapper for dict), deprecated on 2012-09-18 - Convert README.md to rst in setup.py to make PyPI look nice (fixes #97) ## v1.0.5 (2015-12-17) - First version released by new maintainer [@jieter](https://github.com/jieter) - Dropped support for django 1.5 and 1.6, add python 3.5 with django 1.8 and 1.9 to the build matrix (#273) - Prevent `SingleTableView` from calling `get_queryset` twice. (fixes #155) - Don't call managers when resolving accessors. (#214 by [@mbertheau](https://github.com/mbertheau), fixes #211) ## v1.0.4 (2015-05-09) - Fix bug in retrieving `field.verbose_name` under Django 1.8. v1.0.3 ------ - Remove setup.cfg as PyPI doesn't actually support it, instead it's a distutils2 thing that's been discontinued. v1.0.2 ------ - Add setup.cfg to declare README.md for PyPI. v1.0.1 ------ - Convert README to markdown so it's formatted nicely on PyPI. v1.0.0 ------ - Travis CI builds pass. - Added Python 3.4 support. - Added Django 1.7 and Django 1.8 support. - Dropped Python 2.6 and 3.2 support. - Drop Django 1.2 support - Convert tests to using py.test. v0.16.0 ------- - Django 1.8 fixes - `BoundColumn.verbose_name` now titlises only if no verbose_name was given. `verbose_name` is used verbatim. - Add max_length attribute to person CharField - Add Swedish translation - Update docs presentation on readthedocs v0.15.0 ------- - Add UK, Russian, Spanish, Portuguese, and Polish translations - Add support for computed table `attrs`. v0.14.0 ------- - `querystring` and `seturlparam` template tags now require the request to be in the context (backwards incompatible) -- #127 - Add Travis CI support - Add support for Django 1.5 - Add L10N control for columns #120 (ignored in < Django 1.3) - Drop Python 2.6.4 support in favour of Python 3.2 support - Non-queryset data ordering is different between Python 3 and 2. When comparing different types, their truth values are now compared before falling back to string representations of their type. v0.13.0 ------- - Add FileColumn. v0.12.1 ------- - When resolving an accessor, *all* exceptions are smothered into `None`. v0.12.0 ------- - Improve performance by removing unnecessary queries - Simplified pagination: - `Table.page` is an instance attribute (no longer `@property`) - Exceptions raised by paginators (e.g. `EmptyPage`) are no longer smothered by `Table.page` - Pagination exceptions are raised by `Table.paginate` - `RequestConfig` can handles pagination errors silently, can be disabled by including `silent=False` in the `paginate` argument value - Add `DateTimeColumn` and `DateColumn` to handle formatting `datetime` and timezones. - Add `BooleanColumn` to handle bool values - `render_table` can now build and render a table for a queryset, rather than needing to be passed a table instance - Table columns created automatically from a model now use specialised columns - `Column.render` is now skipped if the value is considered *empty*, the default value is used instead. Empty values are specified via `Column.empty_values`, by default is `(None, '')` (backward incompatible) - Default values can now be specified on table instances or `Table.Meta` - Accessor's now honor `alters_data` during resolving. Fixes issue that would delete all your data when a column had an accessor of `delete` - Add `default` and `value` to context of `TemplateColumn` - Add cardinality indication to the pagination area of a table - `Attrs` is deprecated, use `dict` instead v0.11.0 ------- - Add `URLColumn` to render URLs in a data source into hyperlinks - Add `EmailColumn` to render email addresses into hyperlinks - `TemplateColumn` can now Django's template loaders to render from a file v0.10.4 ------- - Fix more bugs on Python 2.6.4, all tests now pass. v0.10.3 ------- - Fix issues for Python 2.6.4 -- thanks Steve Sapovits & brianmay - Reduce Django 1.3 dependency to Table.as_html -- thanks brianmay v0.10.2 ------- - Fix MANIFEST.in to include example templates, thanks TWAC. - Upgrade django-attest to fix problem with tests on Django 1.3.1 v0.10.1 ------- - Fixed support for Django 1.4's paginator (thanks koledennix) - Some juggling of internal implementation. `TableData` now supports slicing and returns new `TableData` instances. `BoundRows` now takes a single argument `data` (a `TableData` instance). - Add support for `get_pagination` on `SingleTableMixin`. - `SingleTableMixin` and `SingleTableView` are now importable directly from `django_tables2`. v0.10.0 ------- - Renamed `BoundColumn.order_by` to `order_by_alias` and never returns `None` (**Backwards incompatible**). Templates are affected if they use something like: {% querystring table.prefixed_order_by_field=column.order_by.opposite|default:column.name %} Which should be rewritten as: {% querystring table.prefixed_order_by_field=column.order_by_alias.next %} - Added `next` shortcut to `OrderBy` returned from `BoundColumn.order_by_alias` - Added `OrderByTuple.get()` - Deprecated `BoundColumn.sortable`, `Column.sortable`, `Table.sortable`, `sortable` CSS class, `BoundColumns.itersortable`, `BoundColumns.sortable`; use `orderable` instead of `sortable`. - Added `BoundColumn.is_ordered` - Introduced concept of an `order by alias`, see glossary in the docs for details. v0.9.6 ------ - Fix bug that caused an ordered column's th to have no HTML attributes. v0.9.5 ------ - Updated example project to add colspan on footer cell so table border renders correctly in Webkit. - Fix regression that caused 'sortable' class on . - Table.__init__ no longer *always* calls .order_by() on querysets, fixes #55. This does introduce a slight backwards incompatibility. `Table.order_by` now has the possibility of returning `None`, previously it would *always* return an `OrderByTuple`. - DeclarativeColumnsMetaclass.__new__ now uses super() - Testing now requires pylint and Attest >=0.5.3 v0.9.4 ------ - Fix regression that caused column verbose_name values that were marked as safe to be escaped. Now any verbose_name values that are instances of SafeData are used unmodified. v0.9.3 ------ - Fix regression in `SingleTableMixin`. - Remove stray `print` statement. v0.9.2 ------ - `SingleTableView` now uses `RequestConfig`. This fixes issues with `order_by_field`, `page_field`, and `per_page_field` not being honored. - Add `Table.Meta.per_page` and change `Table.paginate` to use it as default. - Add `title` template filter. It differs from Django's built-in `title` filter because it operates on an individual word basis and leaves words containing capitals untouched. **Warning**: use `{% load ... from ... %}` to avoid inadvertantly replacing Django's builtin `title` template filter. - `BoundColumn.verbose_name` no longer does `capfirst`, titlising is now the responsbility of `Column.header`. - `BoundColumn.__unicode__` now uses `BoundColumn.header` rather than `BoundColumn.verbose_name`. v0.9.1 ------ - Fix version in setup.py (doh) v0.9.0 ------ - Add support for column attributes (see Attrs) - Add BoundRows.items() to yield (bound_column, cell) pairs - Tried to make docs more concise. Much stronger promotion of using RequestConfig and {% querystring %} v0.8.4 ------ - Removed random 'print' statements. - Tweaked 'paleblue' theme css to be more flexible - removed `whitespace: no-wrap` - header background image to support more than 2 rows of text v0.8.3 ------ - Fixed stupid import mistake. Tests didn't pick it up due to them ignoring `ImportError`. v0.8.2 ------ - `SingleTableView` now inherits from `ListView` which enables automatic `foo_list.html` template name resolution (thanks dramon for reporting) - `render_table` template tag no suppresses exceptions when `DEBUG=True` v0.8.1 ------ - Fixed bug in render_table when giving it a template (issue #41) v0.8.0 ------ - Added translation support in the default template via `{% trans %}` - Removed `basic_table.html`, `Table.as_html()` now renders `table.html` but will clobber the querystring of the current request. Use the `render_table` template tag instead - `render_table` now supports an optional second argument -- the template to use when rendering the table - `Table` now supports declaring which template to use when rendering to HTML - Django >=1.3 is now required - Added support for using django-haystack's `SearchQuerySet` as a data source - The default template `table.html` now includes block tags to make it easy to extend to change small pieces - Fixed table template parsing problems being hidden due to a subsequent exception being raised - Http404 exceptions are no longer raised during a call to `Table.paginate()`, instead it now occurs when `Table.page` is accessed - Fixed bug where a table couldn't be rendered more than once if it was paginated - Accessing `Table.page` now returns a new page every time, rather than reusing a single object v0.7.8 ------ - Tables now support using both `sequence` and `exclude` (issue #32). - `Sequence` class moved to `django_tables2/utils.py`. - Table instances now support modification to the `exclude` property. - Removed `BoundColumns._spawn_columns`. - `Table.data`, `Table.rows`, and `Table.columns` are now attributes rather than properties. django-tables2-1.0.7/LICENSE000066400000000000000000000031671264227116200153320ustar00rootroot00000000000000All changes made to django-tables2 since forking from django-tables are Copyright (c) 2011, Bradley Ayers All rights reserved. Redistribution is permitted under the same terms as the original django-tables license. The original django-tables license is included below. Copyright (c) 2008, Michael Elsdörfer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-tables2-1.0.7/MANIFEST.in000066400000000000000000000004401264227116200160520ustar00rootroot00000000000000include README.md include CHANGELOG.md include LICENSE recursive-include docs * recursive-include django_tables2/templates * recursive-include django_tables2/static * recursive-include django_tables2/locale * recursive-include example/app/fixtures * recursive-include example/templates * django-tables2-1.0.7/README.md000066400000000000000000000040111264227116200155710ustar00rootroot00000000000000# django-tables2 - An app for creating HTML tables [![Build status](https://travis-ci.org/bradleyayers/django-tables2.svg)](https://travis-ci.org/bradleyayers/django-tables2) django-tables2 simplifies the task of turning sets of data into HTML tables. It has native support for pagination and sorting. It does for HTML tables what `django.forms` does for HTML forms. e.g. ![An example table rendered using django-tables2](http://dl.dropbox.com/u/33499139/django-tables2/example.png) Its features include: - Any iterable can be a data-source, but special support for Django querysets is included. - The builtin UI does not rely on JavaScript. - Support for automatic table generation based on a Django model. - Supports custom column functionality via subclassing. - Pagination. - Column based table sorting. - Template tag to enable trivial rendering to HTML. - Generic view mixin. # Example Creating a table for a model `Simple` is as simple as: ```python import django_tables2 as tables class SimpleTable(tables.Table): class Meta: model = Simple ``` This would then be used in a view: ```python def simple_list(request): queryset = Simple.objects.all() table = SimpleTable(queryset) return render_to_response("simple_list.html", {"table": table}, context_instance=RequestContext(request)) ``` And finally in the template: ``` {% load django_tables2 %} {% render_table table %} ``` This example shows one of the simplest cases, but django-tables2 can do a lot more! Check out the [documentation](http://django-tables2.readthedocs.org/en/latest/) for more details. # Building the documentation If you want to build the docs from within a virtualenv, and Sphinx is installed globally, use: make html SPHINXBUILD="python $(which sphinx-build)" # Publishing a release 1. Bump the version in `django-tables2/__init__.py`. 2. Update `CHANGELOG.md`. 3. Create a tag `git tag -a v1.0.6 -m 'tagging v1.0.6'` 4. Run `python setup.py sdist upload --sign --identity=`. django-tables2-1.0.7/django_tables2/000077500000000000000000000000001264227116200171745ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/__init__.py000066400000000000000000000006751264227116200213150ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=W0611 from .tables import Table from .columns import (BooleanColumn, Column, CheckBoxColumn, DateColumn, DateTimeColumn, EmailColumn, FileColumn, LinkColumn, TemplateColumn, URLColumn, TimeColumn) from .config import RequestConfig from .utils import A try: from .views import SingleTableMixin, SingleTableView except ImportError: pass __version__ = "1.0.7" django-tables2-1.0.7/django_tables2/columns/000077500000000000000000000000001264227116200206545ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/columns/__init__.py000066400000000000000000000006711264227116200227710ustar00rootroot00000000000000from .base import library, BoundColumn, BoundColumns, Column from .booleancolumn import BooleanColumn from .checkboxcolumn import CheckBoxColumn from .datecolumn import DateColumn from .datetimecolumn import DateTimeColumn from .emailcolumn import EmailColumn from .filecolumn import FileColumn from .linkcolumn import LinkColumn from .templatecolumn import TemplateColumn from .urlcolumn import URLColumn from .timecolumn import TimeColumn django-tables2-1.0.7/django_tables2/columns/base.py000066400000000000000000000534631264227116200221530ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals import warnings from collections import OrderedDict from itertools import islice import six from django import VERSION as django_version from django.db.models.fields import FieldDoesNotExist from django_tables2.templatetags.django_tables2 import title from django_tables2.utils import A, AttributeDict, OrderBy, OrderByTuple from ..utils import python_2_unicode_compatible class Library(object): """ A collection of columns. """ def __init__(self): self.columns = [] def register(self, column): self.columns.append(column) return column def column_for_field(self, field): """ Return a column object suitable for model field. :returns: column object of `None` """ # iterate in reverse order as columns are registered in order # of least to most specialised (i.e. Column is registered # first). This also allows user-registered columns to be # favoured. for candidate in reversed(self.columns): if not hasattr(candidate, "from_field"): continue column = candidate.from_field(field) if column is None: continue return column # The library is a mechanism for announcing what columns are available. Its # current use is to allow the table metaclass to ask columns if they're a # suitable match for a model field, and if so to return an approach instance. library = Library() @library.register class Column(object): # pylint: disable=R0902 """ Represents a single column of a table. `.Column` objects control the way a column (including the cells that fall within it) are rendered. .. attribute:: attrs HTML attributes for elements that make up the column. :type: `dict` This API is extended by subclasses to allow arbitrary HTML attributes to be added to the output. By default `.Column` supports: - *th* -- ``table/thead/th`` elements - *td* -- ``table/tbody/tr/td`` elements - *cell* -- fallback if *th* or *td* isn't defined .. attribute:: accessor An accessor that describes how to extract values for this column from the :term:`table data`. :type: string or `~.Accessor` .. attribute:: default The default value for the column. This can be a value or a callable object [1]_. If an object in the data provides `None` for a column, the default will be used instead. The default value may affect ordering, depending on the type of data the table is using. The only case where ordering is not affected is when a `.QuerySet` is used as the table data (since sorting is performed by the database). .. [1] The provided callable object must not expect to receive any arguments. .. attribute:: order_by Allows one or more accessors to be used for ordering rather than *accessor*. :type: `unicode`, `tuple`, `~.Accessor` .. attribute:: orderable If `False`, this column will not be allowed to influence row ordering/sorting. :type: `bool` .. attribute:: verbose_name A human readable version of the column name. :type: `unicode` .. attribute:: visible If `True`, this column will be included in the HTML output. :type: `bool` .. attribute:: localize * If `True`, cells of this column will be localized in the HTML output by the localize filter. * If `False`, cells of this column will be unlocalized in the HTML output by the unlocalize filter. * If `None` (the default), cell will be rendered as is and localization will depend on ``USE_L10N`` setting. :type: `bool` """ #: Tracks each time a Column instance is created. Used to retain order. creation_counter = 0 empty_values = (None, '') def __init__(self, verbose_name=None, accessor=None, default=None, visible=True, orderable=None, attrs=None, order_by=None, sortable=None, empty_values=None, localize=None): if not (accessor is None or isinstance(accessor, six.string_types) or callable(accessor)): raise TypeError('accessor must be a string or callable, not %s' % type(accessor).__name__) if callable(accessor) and default is not None: raise TypeError('accessor must be string when default is used, not callable') self.accessor = A(accessor) if accessor else None self._default = default self.verbose_name = verbose_name self.visible = visible if sortable is not None: warnings.warn('`sortable` is deprecated, use `orderable` instead.', DeprecationWarning) # if orderable hasn't been specified, we'll use sortable's value if orderable is None: orderable = sortable self.orderable = orderable self.attrs = attrs or {} # massage order_by into an OrderByTuple or None order_by = (order_by, ) if isinstance(order_by, six.string_types) else order_by self.order_by = OrderByTuple(order_by) if order_by is not None else None if empty_values is not None: self.empty_values = empty_values self.localize = localize self.creation_counter = Column.creation_counter Column.creation_counter += 1 @property def default(self): # handle callables return self._default() if callable(self._default) else self._default @property def header(self): """ The value used for the column heading (e.g. inside the ```` tag). By default this returns `~.Column.verbose_name`. :returns: `unicode` or `None` .. note:: This property typically isn't accessed directly when a table is rendered. Instead, `.BoundColumn.header` is accessed which in turn accesses this property. This allows the header to fallback to the column name (it's only available on a `.BoundColumn` object hence accessing that first) when this property doesn't return something useful. """ return self.verbose_name def render(self, value): """ Returns the content for a specific cell. This method can be overridden by :ref:`table.render_FOO` methods on the table or by subclassing `.Column`. :returns: `unicode` If the value for this cell is in `.empty_values`, this method is skipped and an appropriate default value is rendered instead. Subclasses should set `.empty_values` to ``()`` if they want to handle all values in `.render`. """ return value @property def sortable(self): """ *deprecated* -- use `.orderable` instead. """ warnings.warn('`sortable` is deprecated, use `orderable` instead.', DeprecationWarning) return self.orderable @classmethod def from_field(cls, field): """ Return a specialised column for the model field or `None`. :param field: the field that needs a suitable column :type field: model field instance :returns: `.Column` object or `None` If the column isn't specialised for the given model field, it should return `None`. This gives other columns the opportunity to do better. If the column is specialised, it should return an instance of itself that's configured appropriately for the field. """ # Since this method is inherited by every subclass, only provide a # column if this class was asked directly. if cls is Column: if hasattr(field, "get_related_field"): verbose_name = field.get_related_field().verbose_name else: verbose_name = getattr(field, 'verbose_name', field.name) return cls(verbose_name=verbose_name) @python_2_unicode_compatible class BoundColumn(object): """ A *run-time* version of `.Column`. The difference between `.BoundColumn` and `.Column`, is that `.BoundColumn` objects include the relationship between a `.Column` and a `.Table`. In practice, this means that a `.BoundColumn` knows the *"variable name"* given to the `.Column` when it was declared on the `.Table`. For convenience, all `.Column` properties are available from thisclass. :type table: `.Table` object :param table: the table in which this column exists :type column: `.Column` object :param column: the type of column :type name: string object :param name: the variable name of the column used to when defining the `.Table`. In this example the name is ``age``: .. code-block:: python class SimpleTable(tables.Table): age = tables.Column() """ def __init__(self, table, column, name): self.table = table self.column = column self.name = name def __str__(self): return six.text_type(self.header) @property def accessor(self): """ Returns the string used to access data for this column out of the data source. """ return self.column.accessor or A(self.name) @property def attrs(self): """ Proxy to `.Column.attrs` but injects some values of our own. A ``th`` and ``td`` are guaranteed to be defined (irrespective of what's actually defined in the column attrs. This makes writing templates easier. """ # Work on a copy of the attrs object since we're tweaking stuff attrs = dict(self.column.attrs) # Find the relevant th attributes (fall back to cell if th isn't # explicitly specified). attrs["td"] = td = AttributeDict(attrs.get('td', attrs.get('cell', {}))) attrs["th"] = th = AttributeDict(attrs.get("th", attrs.get("cell", {}))) # make set of existing classes. th_class = set((c for c in th.get("class", "").split(" ") if c)) # pylint: disable=C0103 td_class = set((c for c in td.get("class", "").split(" ") if c)) # pylint: disable=C0103 # add classes for ordering if self.orderable: th_class.add("orderable") th_class.add("sortable") # backwards compatible if self.is_ordered: th_class.add("desc" if self.order_by_alias.is_descending else "asc") # Always add the column name as a class th_class.add(self.name) td_class.add(self.name) if th_class: th['class'] = " ".join(sorted(th_class)) if td_class: td['class'] = " ".join(sorted(td_class)) return attrs @property def default(self): """ Returns the default value for this column. """ value = self.column.default if value is None: value = self.table.default return value @property def header(self): """ The value that should be used in the header cell for this column. """ # favour Column.header column_header = self.column.header if column_header: return column_header # fall back to automatic best guess return self.verbose_name @property def order_by(self): """ Returns an `.OrderByTuple` of appropriately prefixed data source keys used to sort this column. See `.order_by_alias` for details. """ if self.column.order_by is not None: order_by = self.column.order_by else: # default to using column accessor as data source sort key order_by = OrderByTuple((self.accessor, )) return order_by.opposite if self.order_by_alias.is_descending else order_by @property def order_by_alias(self): """ Returns an `OrderBy` describing the current state of ordering for this column. The following attempts to explain the difference between `order_by` and `.order_by_alias`. `.order_by_alias` returns and `.OrderBy` instance that's based on the *name* of the column, rather than the keys used to order the table data. Understanding the difference is essential. Having an alias *and* a keys version is necessary because an N-tuple (of data source keys) can be used by the column to order the data, and it's ambiguous when mapping from N-tuple to column (since multiple columns could use the same N-tuple). The solution is to use order by *aliases* (which are really just prefixed column names) that describe the ordering *state* of the column, rather than the specific keys in the data source should be ordered. e.g.:: >>> class SimpleTable(tables.Table): ... name = tables.Column(order_by=("firstname", "last_name")) ... >>> table = SimpleTable([], order_by=("-name", )) >>> table.columns["name"].order_by_alias "-name" >>> table.columns["name"].order_by ("-first_name", "-last_name") The `OrderBy` returned has been patched to include an extra attribute ``next``, which returns a version of the alias that would be transitioned to if the user toggles sorting on this column, e.g.:: not sorted -> ascending ascending -> descending descending -> ascending This is useful otherwise in templates you'd need something like: {% if column.is_ordered %} {% querystring table.prefixed_order_by_field=column.order_by_alias.opposite %} {% else %} {% querystring table.prefixed_order_by_field=column.order_by_alias %} {% endif %} """ order_by = OrderBy((self.table.order_by or {}).get(self.name, self.name)) order_by.next = order_by.opposite if self.is_ordered else order_by return order_by @property def is_ordered(self): return self.name in (self.table.order_by or ()) @property def sortable(self): """ *deprecated* -- use `orderable` instead. """ warnings.warn('`%s.sortable` is deprecated, use `orderable`' % type(self).__name__, DeprecationWarning) return self.orderable @property def orderable(self): """ Return a `bool` depending on whether this column supports ordering. """ if self.column.orderable is not None: return self.column.orderable return self.table.orderable @property def verbose_name(self): """ Return the verbose name for this column, or fallback to the titlised column name. If the table is using queryset data, then use the corresponding model field's `~.db.Field.verbose_name`. If it's traversing a relationship, then get the last field in the accessor (i.e. stop when the relationship turns from ORM relationships to object attributes [e.g. person.upper should stop at person]). """ # Favor an explicit defined verbose_name if self.column.verbose_name is not None: return self.column.verbose_name # This is our reasonable fallback, should the next section not result # in anything useful. name = title(self.name.replace('_', ' ')) # Try to use a model field's verbose_name if hasattr(self.table.data, 'queryset') and hasattr(self.table.data.queryset, 'model'): model = self.table.data.queryset.model parts = self.accessor.split('.') field = None for part in parts: try: if django_version < (1, 8, 0): field, _, _, _ = model._meta.get_field_by_name(part) else: field = model._meta.get_field(part) except FieldDoesNotExist: break if hasattr(field, 'rel') and hasattr(field.rel, 'to'): model = field.rel.to continue break if field: if hasattr(field, 'field'): name = field.field.verbose_name else: name = getattr(field, 'verbose_name', field.name) return name @property def visible(self): """ Returns a `bool` depending on whether this column is visible. """ return self.column.visible @property def localize(self): ''' Returns `True`, `False` or `None` as described in ``Column.localize`` ''' return self.column.localize class BoundColumns(object): """ Container for spawning `.BoundColumn` objects. This is bound to a table and provides its `.Table.columns` property. It provides access to those columns in different ways (iterator, item-based, filtered and unfiltered etc), stuff that would not be possible with a simple iterator in the table class. A `BoundColumns` object is a container for holding `BoundColumn` objects. It provides methods that make accessing columns easier than if they were stored in a `list` or `dict`. `Columns` has a similar API to a `dict` (it actually uses a `~collections.OrderedDict` interally). At the moment you'll only come across this class when you access a `.Table.columns` property. :type table: `.Table` object :param table: the table containing the columns """ def __init__(self, table): self.table = table self.columns = OrderedDict() for name, column in six.iteritems(table.base_columns): self.columns[name] = bc = BoundColumn(table, column, name) bc.render = getattr(table, 'render_' + name, column.render) def iternames(self): return (name for name, column in self.iteritems()) def names(self): return list(self.iternames()) def iterall(self): """ Return an iterator that exposes all `.BoundColumn` objects, regardless of visiblity or sortability. """ return (column for name, column in self.iteritems()) def all(self): return list(self.iterall()) def iteritems(self): """ Return an iterator of ``(name, column)`` pairs (where ``column`` is a `BoundColumn`). This method is the mechanism for retrieving columns that takes into consideration all of the ordering and filtering modifiers that a table supports (e.g. `~Table.Meta.exclude` and `~Table.Meta.sequence`). """ for name in self.table.sequence: if name not in self.table.exclude: yield (name, self.columns[name]) def items(self): return list(self.iteritems()) def iterorderable(self): """ Same as `BoundColumns.all` but only returns orderable columns. This is useful in templates, where iterating over the full set and checking ``{% if column.sortable %}`` can be problematic in conjunction with e.g. ``{{ forloop.last }}`` (the last column might not be the actual last that is rendered). """ return (x for x in self.iterall() if x.orderable) def itersortable(self): warnings.warn('`itersortable` is deprecated, use `iterorderable` instead.', DeprecationWarning) return self.iterorderable() def orderable(self): return list(self.iterorderable()) def sortable(self): warnings.warn("`sortable` is deprecated, use `orderable` instead.", DeprecationWarning) return self.orderable def itervisible(self): """ Same as `.iterorderable` but only returns visible `.BoundColumn` objects. This is geared towards table rendering. """ return (x for x in self.iterall() if x.visible) def visible(self): return list(self.itervisible()) def __iter__(self): """ Convenience API, alias of `.itervisible`. """ return self.itervisible() def __contains__(self, item): """ Check if a column is contained within a `Columns` object. *item* can either be a `BoundColumn` object, or the name of a column. """ if isinstance(item, six.string_types): return item in self.iternames() else: # let's assume we were given a column return item in self.iterall() def __len__(self): """ Return how many :class:`BoundColumn` objects are contained (and visible). """ return len(self.visible()) def __getitem__(self, index): """ Retrieve a specific `BoundColumn` object. *index* can either be 0-indexed or the name of a column .. code-block:: python columns['speed'] # returns a bound column with name 'speed' columns[0] # returns the first column """ if isinstance(index, int): try: return next(islice(self.iterall(), index, index + 1)) except StopIteration: raise IndexError elif isinstance(index, six.string_types): for column in self.iterall(): if column.name == index: return column raise KeyError("Column with name '%s' does not exist; " "choices are: %s" % (index, self.names())) else: raise TypeError('row indices must be integers or str, not %s' % type(index).__name__) django-tables2-1.0.7/django_tables2/columns/booleancolumn.py000066400000000000000000000033551264227116200240710ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from .base import Column, library from django.db import models from django.utils.html import escape from django.utils.safestring import mark_safe from django_tables2.utils import AttributeDict import six @library.register class BooleanColumn(Column): """ A column suitable for rendering boolean data. :param null: is `None` different from `False`? :type null: `bool` :param yesno: text to display for True/False values, comma separated :type yesno: iterable or string Rendered values are wrapped in a ```` to allow customisation by themes. By default the span is given the class ``true``, ``false``. In addition to *attrs* keys supported by `.Column`, the following are available: - *span* -- adds attributes to the tag """ def __init__(self, null=False, yesno="✔,✘", **kwargs): self.yesno = (yesno.split(',') if isinstance(yesno, six.string_types) else tuple(yesno)) if null: kwargs["empty_values"] = () super(BooleanColumn, self).__init__(**kwargs) def render(self, value): value = bool(value) text = self.yesno[int(not value)] html = '%s' attrs = {"class": six.text_type(value).lower()} attrs.update(self.attrs.get("span", {})) return mark_safe(html % (AttributeDict(attrs).as_html(), escape(text))) @classmethod def from_field(cls, field): if isinstance(field, models.BooleanField): return cls(verbose_name=field.verbose_name, null=False) if isinstance(field, models.NullBooleanField): return cls(verbose_name=field.verbose_name, null=True) django-tables2-1.0.7/django_tables2/columns/checkboxcolumn.py000066400000000000000000000067031264227116200242400ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.utils.safestring import mark_safe from django_tables2.utils import AttributeDict import warnings from .base import Column, library @library.register class CheckBoxColumn(Column): """ A subclass of `.Column` that renders as a checkbox form input. This column allows a user to *select* a set of rows. The selection information can then be used to apply some operation (e.g. "delete") onto the set of objects that correspond to the selected rows. The value that is extracted from the :term:`table data` for this column is used as the value for the checkbox, i.e. ```` This class implements some sensible defaults: - HTML input's ``name`` attribute is the :term:`column name` (can override via *attrs* argument). - *orderable* defaults to `False`. .. note:: You'd expect that you could select multiple checkboxes in the rendered table and then *do something* with that. This functionality isn't implemented. If you want something to actually happen, you'll need to implement that yourself. In addition to *attrs* keys supported by `.Column`, the following are available: - *input* -- ```` elements in both ```` and ````. - *th__input* -- Replaces *input* attrs in header cells. - *td__input* -- Replaces *input* attrs in body cells. """ def __init__(self, attrs=None, **extra): # For backwards compatibility, passing in a normal dict effectively # should assign attributes to the `` tag. valid = set(("input", "th__input", "td__input", "th", "td", "cell")) if attrs and not set(attrs) & set(valid): # if none of the keys in attrs are actually valid, assume it's some # old code that should be be interpreted as {"td__input": ...} warnings.warn('attrs keys must be one of %s, interpreting as {"td__input": %s}' % (', '.join(valid), attrs), DeprecationWarning) attrs = {"td__input": attrs} # This is done for backwards compatible too, there used to be a # ``header_attrs`` argument, but this has been deprecated. We'll # maintain it for a while by translating it into ``head.checkbox``. if "header_attrs" in extra: warnings.warn('header_attrs argument is deprecated, ' 'use attrs={"th__input": ...} instead', DeprecationWarning) attrs.setdefault('th__input', {}).update(extra.pop('header_attrs')) kwargs = {'orderable': False, 'attrs': attrs} kwargs.update(extra) super(CheckBoxColumn, self).__init__(**kwargs) @property def header(self): default = {'type': 'checkbox'} general = self.attrs.get('input') specific = self.attrs.get('th__input') attrs = AttributeDict(default, **(specific or general or {})) return mark_safe('' % attrs.as_html()) def render(self, value, bound_column): # pylint: disable=W0221 default = { 'type': 'checkbox', 'name': bound_column.name, 'value': value } general = self.attrs.get('input') specific = self.attrs.get('td__input') attrs = AttributeDict(default, **(specific or general or {})) return mark_safe('' % attrs.as_html()) django-tables2-1.0.7/django_tables2/columns/datecolumn.py000066400000000000000000000021141264227116200233570ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from .base import library from .templatecolumn import TemplateColumn @library.register class DateColumn(TemplateColumn): """ A column that renders dates in the local timezone. :param format: format string in same format as Django's ``date`` template filter (optional) :type format: `unicode` :param short: if *format* is not specified, use Django's ``SHORT_DATE_FORMAT`` setting, otherwise use ``DATE_FORMAT`` :type short: `bool` """ def __init__(self, format=None, short=True, *args, **kwargs): # pylint: disable=W0622 if format is None: format = 'SHORT_DATE_FORMAT' if short else 'DATE_FORMAT' template = '{{ value|date:"%s"|default:default }}' % format super(DateColumn, self).__init__(template_code=template, *args, **kwargs) @classmethod def from_field(cls, field): if isinstance(field, models.DateField): return cls(verbose_name=field.verbose_name) django-tables2-1.0.7/django_tables2/columns/datetimecolumn.py000066400000000000000000000020401264227116200242340ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from .base import library from .templatecolumn import TemplateColumn @library.register class DateTimeColumn(TemplateColumn): """ A column that renders datetimes in the local timezone. :param format: format string for datetime (optional) :type format: `unicode` :param short: if *format* is not specifid, use Django's ``SHORT_DATETIME_FORMAT``, else ``DATETIME_FORMAT`` :type short: `bool` """ def __init__(self, format=None, short=True, *args, **kwargs): # pylint: disable=W0622 if format is None: format = 'SHORT_DATETIME_FORMAT' if short else 'DATETIME_FORMAT' template = '{{ value|date:"%s"|default:default }}' % format super(DateTimeColumn, self).__init__(template_code=template, *args, **kwargs) @classmethod def from_field(cls, field): if isinstance(field, models.DateTimeField): return cls(verbose_name=field.verbose_name) django-tables2-1.0.7/django_tables2/columns/emailcolumn.py000066400000000000000000000021151264227116200235320ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from .base import library from .linkcolumn import BaseLinkColumn @library.register class EmailColumn(BaseLinkColumn): """ A subclass of `.BaseLinkColumn` that renders the cell value as a hyperlink. It's common to have a email value in a row hyperlinked to other page. :param attrs: a `dict` of HTML attributes that are added to the rendered ``...`` tag Example: .. code-block:: python # models.py class Person(models.Model): name = models.CharField(max_length=200) email = models.EmailField() # tables.py class PeopleTable(tables.Table): name = tables.Column() email = tables.EmailColumn() """ def render(self, value): return self.render_link("mailto:%s" % value, text=value) @classmethod def from_field(cls, field): if isinstance(field, models.EmailField): return cls(verbose_name=field.verbose_name) django-tables2-1.0.7/django_tables2/columns/filecolumn.py000066400000000000000000000052111264227116200233620ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from django.utils.safestring import mark_safe from django_tables2.utils import AttributeDict import os from .base import Column, library @library.register class FileColumn(Column): """ Attempts to render `.FieldFile` (or other storage backend `.File`) as a hyperlink. When the file is accessible via a URL, the file is rendered as a hyperlink. The `.basename` is used as the text:: receipt.pdf When unable to determine the URL, a ``span`` is used instead:: receipt.pdf `.Column.attrs` keys ``a`` and ``span`` can be used to add additional attributes. :type verify_exists: bool :param verify_exists: attempt to determine if the file exists If *verify_exists*, the HTML class ``exists`` or ``missing`` is added to the element to indicate the integrity of the storage. """ def __init__(self, verify_exists=True, **kwargs): self.verify_exists = True super(FileColumn, self).__init__(**kwargs) def render(self, value): storage = getattr(value, "storage", None) exists = None url = None if storage: # we'll assume value is a `django.db.models.fields.files.FieldFile` if self.verify_exists: exists = storage.exists(value.name) url = storage.url(value.name) else: if self.verify_exists and hasattr(value, "name"): # ignore negatives, perhaps the file has a name but it doesn't # represent a local path... better to stay neutral than give a # false negative. exists = os.path.exists(value.name) or exists tag = 'a' if url else 'span' attrs = AttributeDict(self.attrs.get(tag, {})) attrs['title'] = value.name if url: attrs['href'] = url # add "exists" or "missing" to the class list classes = [c for c in attrs.get('class', '').split(' ') if c] if exists is True: classes.append("exists") elif exists is False: classes.append("missing") attrs['class'] = " ".join(classes) html = '<{tag} {attrs}>{text}'.format( tag=tag, attrs=attrs.as_html(), text=os.path.basename(value.name)) return mark_safe(html) @classmethod def from_field(cls, field): if isinstance(field, models.FileField): return cls(verbose_name=field.verbose_name) django-tables2-1.0.7/django_tables2/columns/linkcolumn.py000066400000000000000000000153071264227116200234070ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals import warnings from django.core.urlresolvers import reverse from django.utils.html import format_html from django_tables2.utils import A, AttributeDict from .base import Column, library class BaseLinkColumn(Column): """ The base for other columns that render links. Adds support for an ``a`` key in *attrs** which is added to the rendered ```` tag. """ def __init__(self, attrs=None, *args, **kwargs): valid = set(("a", "th", "td", "cell")) if attrs and not set(attrs) & set(valid): # if none of the keys in attrs are actually valid, assume it's some # old code that should be be interpreted as {"a": ...} warnings.warn('attrs keys must be one of %s, interpreting as {"a": %s}' % (', '.join(valid), attrs), DeprecationWarning) attrs = {"a": attrs} kwargs['attrs'] = attrs super(BaseLinkColumn, self).__init__(*args, **kwargs) def render_link(self, uri, text, attrs=None): """ Render a hyperlink. :param uri: URI for the hyperlink :param text: value wrapped in ```` :param attrs: ```` tag attributes """ attrs = AttributeDict(attrs if attrs is not None else self.attrs.get('a', {})) attrs['href'] = uri return format_html('{text}', attrs=attrs.as_html(), text=text) @library.register class LinkColumn(BaseLinkColumn): """ Renders a normal value as an internal hyperlink to another page. It's common to have the primary value in a row hyperlinked to the page dedicated to that record. The first arguments are identical to that of `~django.core.urlresolvers.reverse` and allows an internal URL to be described. If this argument is `None`, then `get_absolute_url`. (see Django references) will be used. The last argument *attrs* allows custom HTML attributes to be added to the rendered ```` tag. :param viewname: See `~django.core.urlresolvers.reverse`. Or use `None` to use Model's `get_absolute_url` :param urlconf: See `~django.core.urlresolvers.reverse`. :param args: See `~django.core.urlresolvers.reverse`. ** :param kwargs: See `~django.core.urlresolvers.reverse`. ** :param current_app: See `~django.core.urlresolvers.reverse`. :param attrs: a `dict` of HTML attributes that are added to the rendered ```` tag :param text: Either static text, or a callable. If set, this value will be used to render the text inside link instead of value (default) ** In order to create a link to a URL that relies on information in the current row, `.Accessor` objects can be used in the *args* or *kwargs* arguments. The accessor will be resolved using the row's record before `~django.core.urlresolvers.reverse` is called. Example: .. code-block:: python # models.py class Person(models.Model): name = models.CharField(max_length=200) # urls.py urlpatterns = patterns('', url('people/(\d+)/', views.people_detail, name='people_detail') ) # tables.py from django_tables2.utils import A # alias for Accessor class PeopleTable(tables.Table): name = tables.LinkColumn('people_detail', args=[A('pk')]) In order to override the text value (i.e. text) consider the following example: .. code-block:: python # tables.py from django_tables2.utils import A # alias for Accessor class PeopleTable(tables.Table): name = tables.LinkColumn('people_detail', text='static text', args=[A('pk')]) age = tables.LinkColumn('people_detail', text=lambda record: record.name, args=[A('pk')]) In the first example, a static text would be rendered ('static text') In the second example, you can specify a callable which accepts a record object (and thus can return anything from it) In addition to *attrs* keys supported by `.Column`, the following are available: - *a* -- ```` elements in ````. """ def __init__(self, viewname=None, urlconf=None, args=None, kwargs=None, current_app=None, attrs=None, text=None, **extra): super(LinkColumn, self).__init__(attrs, **extra) self.viewname = viewname self.urlconf = urlconf self.args = args self.kwargs = kwargs self.current_app = current_app self.text_value = text def compose_url(self, record): '''Compose the url if the column is constructed with a viewname.''' if isinstance(self.viewname, A): viewname = self.viewname.resolve(record) else: viewname = self.viewname # The following params + if statements create optional arguments to # pass to Django's reverse() function. params = {} if self.urlconf: params['urlconf'] = (self.urlconf.resolve(record) if isinstance(self.urlconf, A) else self.urlconf) if self.args: params['args'] = [a.resolve(record) if isinstance(a, A) else a for a in self.args] if self.kwargs: params['kwargs'] = {} for key, val in self.kwargs.items(): # If we're dealing with an Accessor (A), resolve it, otherwise # use the value verbatim. params['kwargs'][str(key)] = (val.resolve(record) if isinstance(val, A) else val) if self.current_app: params['current_app'] = (self.current_app.resolve(record) if isinstance(self.current_app, A) else self.current_app) return reverse(viewname, **params) def render(self, value, record, bound_column): # pylint: disable=W0221 if self.viewname is None: if not hasattr(record, 'get_absolute_url'): raise TypeError('if viewname=None, record must define a get_absolute_url') url = record.get_absolute_url() else: url = self.compose_url(record) text_value = value if self.text_value: text_value = self.text_value if callable(text_value): text_value = text_value(record) return self.render_link(url, text=text_value) django-tables2-1.0.7/django_tables2/columns/templatecolumn.py000066400000000000000000000047311264227116200242640ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.template import Context, Template from django.template.loader import render_to_string from .base import Column, library @library.register class TemplateColumn(Column): """ A subclass of `.Column` that renders some template code to use as the cell value. :type  template_code: `unicode` :param template_code: the template code to render :type  template_name: `unicode` :param template_name: the name of the template to render A `~django.template.Template` object is created from the *template_code* or *template_name* and rendered with a context containing: - *record* -- data record for the current row - *value* -- value from `record` that corresponds to the current column - *default* -- appropriate default value to use as fallback Example: .. code-block:: python class ExampleTable(tables.Table): foo = tables.TemplateColumn('{{ record.bar }}') # contents of `myapp/bar_column.html` is `{{ value }}` bar = tables.TemplateColumn(template_name='myapp/name2_column.html') Both columns will have the same output. .. important:: In order to use template tags or filters that require a `~django.template.RequestContext`, the table **must** be rendered via :ref:`{% render_table %} `. """ empty_values = () def __init__(self, template_code=None, template_name=None, **extra): super(TemplateColumn, self).__init__(**extra) self.template_code = template_code self.template_name = template_name if not self.template_code and not self.template_name: raise ValueError('A template must be provided') def render(self, record, table, value, bound_column, **kwargs): # If the table is being rendered using `render_table`, it hackily # attaches the context to the table as a gift to `TemplateColumn`. If # the table is being rendered via `Table.as_html`, this won't exist. context = getattr(table, 'context', Context()) context.update({'default': bound_column.default, 'record': record, 'value': value}) try: if self.template_code: return Template(self.template_code).render(context) else: return render_to_string(self.template_name, context) finally: context.pop() django-tables2-1.0.7/django_tables2/columns/timecolumn.py000066400000000000000000000017351264227116200234100ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.conf import settings from django.db import models from .base import library from .templatecolumn import TemplateColumn @library.register class TimeColumn(TemplateColumn): """ A column that renders times in the local timezone. :param format: format string in same format as Django's ``time`` template filter (optional) :type format: `unicode` :param short: if *format* is not specified, use Django's ``TIME_FORMAT`` setting """ def __init__(self, format=None, *args, **kwargs): if format is None: format = settings.TIME_FORMAT template = '{{ value|date:"%s"|default:default }}' % format super(TimeColumn, self).__init__(template_code=template, *args, **kwargs) @classmethod def from_field(cls, field): if isinstance(field, models.TimeField): return cls(verbose_name=field.verbose_name) django-tables2-1.0.7/django_tables2/columns/urlcolumn.py000066400000000000000000000015451264227116200232530ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from .base import library from .linkcolumn import BaseLinkColumn @library.register class URLColumn(BaseLinkColumn): """ Renders URL values as hyperlinks. Example:: >>> class CompaniesTable(tables.Table): ... www = tables.URLColumn() ... >>> table = CompaniesTable([{"www": "http://google.com"}]) >>> table.rows[0]["www"] u'http://google.com' Additional attributes for the ```` tag can be specified via ``attrs['a']``. """ def render(self, value): return self.render_link(value, value) @classmethod def from_field(cls, field): if isinstance(field, models.URLField): return cls(verbose_name=field.verbose_name) django-tables2-1.0.7/django_tables2/config.py000066400000000000000000000044511264227116200210170ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from django.core.paginator import EmptyPage, PageNotAnInteger class RequestConfig(object): """ A configurator that uses request data to setup a table. :type paginate: `dict` or `bool` :param paginate: indicates whether to paginate, and if so, what default values to use. If the value evaluates to `False`, pagination will be disabled. A `dict` can be used to specify default values for the call to `~.tables.Table.paginate` (e.g. to define a default *per_page* value). A special *silent* item can be used to enable automatic handling of pagination exceptions using the following algorithm: - If `~django.core.paginator.PageNotAnInteger`` is raised, show the first page. - If `~django.core.paginator.EmptyPage` is raised, show the last page. """ def __init__(self, request, paginate=True): self.request = request self.paginate = paginate def configure(self, table): """ Configure a table using information from the request. """ order_by = self.request.GET.getlist(table.prefixed_order_by_field) if order_by: table.order_by = order_by if self.paginate: if hasattr(self.paginate, "items"): kwargs = dict(self.paginate) else: kwargs = {} # extract some options from the request for arg in ("page", "per_page"): name = getattr(table, "prefixed_%s_field" % arg) try: kwargs[arg] = int(self.request.GET[name]) except (ValueError, KeyError): pass silent = kwargs.pop('silent', True) if not silent: table.paginate(**kwargs) else: try: table.paginate(**kwargs) except PageNotAnInteger: table.page = table.paginator.page(1) except EmptyPage: table.page = table.paginator.page(table.paginator.num_pages) django-tables2-1.0.7/django_tables2/locale/000077500000000000000000000000001264227116200204335ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/de/000077500000000000000000000000001264227116200210235ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/de/LC_MESSAGES/000077500000000000000000000000001264227116200226105ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/de/LC_MESSAGES/django.mo000066400000000000000000000012061264227116200244060ustar00rootroot00000000000000Dlr?W^~%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2015-04-09 12:44+0200 PO-Revision-Date: 2015-04-09 12:45+0100 Language: de MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); Last-Translator: Tim Schneider Language-Team: X-Generator: Poedit 1.7.5 %(count)s von %(total)sWeiterSeite %(current)s von %(total)sZurückdjango-tables2-1.0.7/django_tables2/locale/de/LC_MESSAGES/django.po000066400000000000000000000020211264227116200244050ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2015-04-09 12:44+0200\n" "PO-Revision-Date: 2015-04-09 12:45+0100\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Last-Translator: Tim Schneider \n" "Language-Team: \n" "X-Generator: Poedit 1.7.5\n" #: templates/django_tables2/table.html:55 msgid "Previous" msgstr "Zurück" #: templates/django_tables2/table.html:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Seite %(current)s von %(total)s" #: templates/django_tables2/table.html:63 msgid "Next" msgstr "Weiter" #: templates/django_tables2/table.html:66 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s von %(total)s" django-tables2-1.0.7/django_tables2/locale/el/000077500000000000000000000000001264227116200210335ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/el/LC_MESSAGES/000077500000000000000000000000001264227116200226205ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/el/LC_MESSAGES/django.mo000066400000000000000000000011771264227116200244250ustar00rootroot00000000000000DlG/)>h%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2013-03-19 21:56+0200 PO-Revision-Date: 2013-03-19 21:56+0200 Last-Translator: Serafeim Papastefanos Language-Team: el MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit %(count)s από %(total)sΕπόμενηΣελίδα %(current)s από %(total)sΠροηγούμενηdjango-tables2-1.0.7/django_tables2/locale/el/LC_MESSAGES/django.po000066400000000000000000000016551264227116200244310ustar00rootroot00000000000000# This file is distributed under the same license as the django-tables2 package # #, fuzzy msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-03-19 21:56+0200\n" "PO-Revision-Date: 2013-03-19 21:56+0200\n" "Last-Translator: Serafeim Papastefanos \n" "Language-Team: el \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: .\templates\django_tables2\table.html.py:55 msgid "Previous" msgstr "Προηγούμενη" #: .\templates\django_tables2\table.html.py:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Σελίδα %(current)s από %(total)s" #: .\templates\django_tables2\table.html.py:63 msgid "Next" msgstr "Επόμενη" #: .\templates\django_tables2\table.html.py:66 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s από %(total)s" django-tables2-1.0.7/django_tables2/locale/en/000077500000000000000000000000001264227116200210355ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/en/LC_MESSAGES/000077500000000000000000000000001264227116200226225ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/en/LC_MESSAGES/django.mo000066400000000000000000000006101264227116200244160ustar00rootroot00000000000000$,8N9Project-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2012-09-18 03:12+0200 PO-Revision-Date: 2011-11-06 10:41+1000 Last-Translator: Bradley Ayers Language-Team: English Language: en MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit django-tables2-1.0.7/django_tables2/locale/en/LC_MESSAGES/django.po000066400000000000000000000014641264227116200244310ustar00rootroot00000000000000# This file is distributed under the same license as the django-tables2 package # msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2012-09-18 03:12+0200\n" "PO-Revision-Date: 2011-11-06 10:41+1000\n" "Last-Translator: Bradley Ayers \n" "Language-Team: English \n" "Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/table.html:55 msgid "Previous" msgstr "" #: templates/django_tables2/table.html:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "" #: templates/django_tables2/table.html:63 msgid "Next" msgstr "" #: templates/django_tables2/table.html:66 #, python-format msgid "%(count)s of %(total)s" msgstr "" django-tables2-1.0.7/django_tables2/locale/es/000077500000000000000000000000001264227116200210425ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/es/LC_MESSAGES/000077500000000000000000000000001264227116200226275ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/es/LC_MESSAGES/django.mo000066400000000000000000000011441264227116200244260ustar00rootroot00000000000000DlL 0 :[%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2013-08-21 07:06-0500 PO-Revision-Date: 2013-08-21 07:06-0500 Last-Translator: Pablo Martín Language-Team: LANGUAGE Language: es MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit %(count)s de %(total)sSiguientePágina %(current)s de %(total)sAnteriordjango-tables2-1.0.7/django_tables2/locale/es/LC_MESSAGES/django.po000066400000000000000000000015711264227116200244350ustar00rootroot00000000000000# This file is distributed under the same license as the django-tables2 package # msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-08-21 07:06-0500\n" "PO-Revision-Date: 2013-08-21 07:06-0500\n" "Last-Translator: Pablo Martín \n" "Language-Team: LANGUAGE \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/table.html:55 msgid "Previous" msgstr "Anterior" #: templates/django_tables2/table.html:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Página %(current)s de %(total)s" #: templates/django_tables2/table.html:63 msgid "Next" msgstr "Siguiente" #: templates/django_tables2/table.html:66 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s de %(total)s" django-tables2-1.0.7/django_tables2/locale/fr/000077500000000000000000000000001264227116200210425ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/fr/LC_MESSAGES/000077500000000000000000000000001264227116200226275ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/fr/LC_MESSAGES/django.mo000066400000000000000000000012011264227116200244200ustar00rootroot00000000000000Dli6NV u%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2012-09-18 03:09+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1) %(count)s des %(total)sSuivantPage %(current)s sur %(total)sPrécédentdjango-tables2-1.0.7/django_tables2/locale/fr/LC_MESSAGES/django.po000066400000000000000000000020211264227116200244240ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2012-09-18 03:09+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" #: templates/django_tables2/table.html:55 msgid "Previous" msgstr "Précédent" #: templates/django_tables2/table.html:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Page %(current)s sur %(total)s" #: templates/django_tables2/table.html:63 msgid "Next" msgstr "Suivant" #: templates/django_tables2/table.html:66 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s des %(total)s" django-tables2-1.0.7/django_tables2/locale/pl/000077500000000000000000000000001264227116200210465ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/pl/LC_MESSAGES/000077500000000000000000000000001264227116200226335ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/pl/LC_MESSAGES/django.mo000066400000000000000000000013441264227116200244340ustar00rootroot00000000000000Dl  %(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: 0.1 Report-Msgid-Bugs-To: POT-Creation-Date: 2013-08-22 09:55+0200 PO-Revision-Date: 2013-08-22 09:57+0100 Last-Translator: Michał Pasternak Language-Team: PL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); X-Generator: Poedit 1.5.5 Language: polish %(count)s z %(total)sNastępnaStrona %(current)s z %(total)sPoprzedniadjango-tables2-1.0.7/django_tables2/locale/pl/LC_MESSAGES/django.po000066400000000000000000000022061264227116200244350ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-08-22 09:55+0200\n" "PO-Revision-Date: 2013-08-22 09:57+0100\n" "Last-Translator: Michał Pasternak \n" "Language-Team: PL \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2);\n" "X-Generator: Poedit 1.5.5\n" "Language: polish\n" #: .\templates\django_tables2\table.html.py:55 msgid "Previous" msgstr "Poprzednia" #: .\templates\django_tables2\table.html.py:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Strona %(current)s z %(total)s" #: .\templates\django_tables2\table.html.py:63 msgid "Next" msgstr "Następna" #: .\templates\django_tables2\table.html.py:66 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s z %(total)s" django-tables2-1.0.7/django_tables2/locale/pt_BR/000077500000000000000000000000001264227116200214415ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/pt_BR/LC_MESSAGES/000077500000000000000000000000001264227116200232265ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/pt_BR/LC_MESSAGES/django.mo000066400000000000000000000012441264227116200250260ustar00rootroot00000000000000DlZq z%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: 0.14.0 Report-Msgid-Bugs-To: POT-Creation-Date: 2014-02-02 00:39-0300 PO-Revision-Date: 2014-02-02 00:44-0300 Last-Translator: Fabio C. Barrionuevo da Luz Language-Team: Portuguese (Brazil) Language: pt_BR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); %(count)s de %(total)sPróximoPágina %(current)s de %(total)sAnteriordjango-tables2-1.0.7/django_tables2/locale/pt_BR/LC_MESSAGES/django.po000066400000000000000000000020051264227116200250250ustar00rootroot00000000000000# This file is distributed under the same license as the django-tables2 package # Fabio C. Barrionuevo da Luz , 2014. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.14.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-02-02 00:39-0300\n" "PO-Revision-Date: 2014-02-02 00:44-0300\n" "Last-Translator: Fabio C. Barrionuevo da Luz \n" "Language-Team: Portuguese (Brazil) \n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: templates/django_tables2/table.html:55 msgid "Previous" msgstr "Anterior" #: templates/django_tables2/table.html:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Página %(current)s de %(total)s" #: templates/django_tables2/table.html:63 msgid "Next" msgstr "Próximo" #: templates/django_tables2/table.html:66 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s de %(total)s" django-tables2-1.0.7/django_tables2/locale/pt_PT/000077500000000000000000000000001264227116200214615ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/pt_PT/LC_MESSAGES/000077500000000000000000000000001264227116200232465ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/pt_PT/LC_MESSAGES/django.mo000066400000000000000000000011451264227116200250460ustar00rootroot00000000000000DlN2 ;\%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2013-04-16 11:42+0100 PO-Revision-Date: 2011-11-06 10:41+1000 Last-Translator: Bradley Ayers Language-Team: English Language: en MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit %(count)s de %(total)sSeguintePágina %(current)s de %(total)sAnteriordjango-tables2-1.0.7/django_tables2/locale/pt_PT/LC_MESSAGES/django.po000066400000000000000000000015721264227116200250550ustar00rootroot00000000000000# This file is distributed under the same license as the django-tables2 package # msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-04-16 11:42+0100\n" "PO-Revision-Date: 2011-11-06 10:41+1000\n" "Last-Translator: Bradley Ayers \n" "Language-Team: English \n" "Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/table.html:55 msgid "Previous" msgstr "Anterior" #: templates/django_tables2/table.html:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Página %(current)s de %(total)s" #: templates/django_tables2/table.html:63 msgid "Next" msgstr "Seguinte" #: templates/django_tables2/table.html:66 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s de %(total)s" django-tables2-1.0.7/django_tables2/locale/ru/000077500000000000000000000000001264227116200210615ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/ru/LC_MESSAGES/000077500000000000000000000000001264227116200226465ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/ru/LC_MESSAGES/django.mo000066400000000000000000000014021264227116200244420ustar00rootroot00000000000000Dl+%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2013-12-10 08:25+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: Andrii Pryz Language-Team: RU Language: ru MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); %(count)s из %(total)sСледующаяСтраница %(current)s из %(total)sПредыдущаяdjango-tables2-1.0.7/django_tables2/locale/ru/LC_MESSAGES/django.po000066400000000000000000000022251264227116200244510ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-12-10 08:25+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Andrii Pryz \n" "Language-Team: RU \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: templates/django_tables2/table.html:55 msgid "Previous" msgstr "Предыдущая" #: templates/django_tables2/table.html:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Страница %(current)s из %(total)s" #: templates/django_tables2/table.html:63 msgid "Next" msgstr "Следующая" #: templates/django_tables2/table.html:66 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s из %(total)s" django-tables2-1.0.7/django_tables2/locale/sv/000077500000000000000000000000001264227116200210635ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/sv/LC_MESSAGES/000077500000000000000000000000001264227116200226505ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/sv/LC_MESSAGES/django.mo000066400000000000000000000011361264227116200244500ustar00rootroot00000000000000DlH,3 Q%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2012-09-18 03:12+0200 PO-Revision-Date: 2014-12-04 10:25+0100 Last-Translator: Petter Jönsson Language-Team: Swedish Language: sv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit %(count)s av %(total)sNästaSida %(current)s av %(total)sFöregåendedjango-tables2-1.0.7/django_tables2/locale/sv/LC_MESSAGES/django.po000066400000000000000000000015631264227116200244570ustar00rootroot00000000000000# This file is distributed under the same license as the django-tables2 package # msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2012-09-18 03:12+0200\n" "PO-Revision-Date: 2014-12-04 10:25+0100\n" "Last-Translator: Petter Jönsson \n" "Language-Team: Swedish \n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/table.html:55 msgid "Previous" msgstr "Föregående" #: templates/django_tables2/table.html:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Sida %(current)s av %(total)s" #: templates/django_tables2/table.html:63 msgid "Next" msgstr "Nästa" #: templates/django_tables2/table.html:66 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s av %(total)s" django-tables2-1.0.7/django_tables2/locale/uk/000077500000000000000000000000001264227116200210525ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/uk/LC_MESSAGES/000077500000000000000000000000001264227116200226375ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/locale/uk/LC_MESSAGES/django.mo000066400000000000000000000013721264227116200244410ustar00rootroot00000000000000Dl)%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2013-12-10 08:25+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: Andrii Pryz Language-Team: UK Language: uk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); %(count)s з %(total)sНаступнаСторінка %(current)s з %(total)sПопередняdjango-tables2-1.0.7/django_tables2/locale/uk/LC_MESSAGES/django.po000066400000000000000000000022151264227116200244410ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-12-10 08:25+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Andrii Pryz \n" "Language-Team: UK \n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: templates/django_tables2/table.html:55 msgid "Previous" msgstr "Попередня" #: templates/django_tables2/table.html:59 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Сторінка %(current)s з %(total)s" #: templates/django_tables2/table.html:63 msgid "Next" msgstr "Наступна" #: templates/django_tables2/table.html:66 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s з %(total)s" django-tables2-1.0.7/django_tables2/models.py000066400000000000000000000000771264227116200210350ustar00rootroot00000000000000# coding: utf-8 """Needed to make this package a Django app""" django-tables2-1.0.7/django_tables2/rows.py000066400000000000000000000141631264227116200205450ustar00rootroot00000000000000# coding: utf-8 import six from django.db import models from django.db.models.fields import FieldDoesNotExist from .utils import A, getargspec class BoundRow(object): """ Represents a *specific* row in a table. `.BoundRow` objects are a container that make it easy to access the final 'rendered' values for cells in a row. You can simply iterate over a `.BoundRow` object and it will take care to return values rendered using the correct method (e.g. :ref:`table.render_FOO`) To access the rendered value of each cell in a row, just iterate over it: .. code-block:: python >>> import django_tables2 as tables >>> class SimpleTable(tables.Table): ... a = tables.Column() ... b = tables.CheckBoxColumn(attrs={'name': 'my_chkbox'}) ... >>> table = SimpleTable([{'a': 1, 'b': 2}]) >>> row = table.rows[0] # we only have one row, so let's use it >>> for cell in row: ... print(cell) ... 1 Alternatively you can treat it like a list and use indexing to retrieve a specific cell. It should be noted that this will raise an IndexError on failure. .. code-block:: python >>> row[0] 1 >>> row[1] u'' >>> row[2] ... IndexError: list index out of range Finally you can also treat it like a dictionary and use column names as the keys. This will raise KeyError on failure (unlike the above indexing using integers). .. code-block:: python >>> row['a'] 1 >>> row['b'] u'' >>> row['c'] ... KeyError: 'c' :param table: is the `.Table` in which this row exists. :param record: a single record from the :term:`table data` that is used to populate the row. A record could be a `~django.db.Model` object, a `dict`, or something else. """ def __init__(self, record, table): self._record = record self._table = table @property def table(self): """The associated `.Table` object.""" return self._table @property def record(self): """ The data record from the data source which is used to populate this row with data. """ return self._record def __iter__(self): """ Iterate over the rendered values for cells in the row. Under the hood this method just makes a call to `.BoundRow.__getitem__` for each cell. """ for column, value in self.items(): # this uses __getitem__, using the name (rather than the accessor) # is correct – it's what __getitem__ expects. yield value def __getitem__(self, name): """ Returns the final rendered value for a cell in the row, given the name of a column. """ bound_column = self.table.columns[name] value = None # We need to take special care here to allow get_FOO_display() # methods on a model to be used if available. See issue #30. path, _, remainder = bound_column.accessor.rpartition('.') penultimate = A(path).resolve(self.record, quiet=True) # If the penultimate is a model and the remainder is a field # using choices, use get_FOO_display(). if isinstance(penultimate, models.Model): try: field = penultimate._meta.get_field(remainder) display = getattr(penultimate, 'get_%s_display' % remainder, None) if getattr(field, "choices", ()) and display: value = display() remainder = None except FieldDoesNotExist: pass # Fall back to just using the original accessor (we just need # to follow the remainder). if remainder: value = A(remainder).resolve(penultimate, quiet=True) if value in bound_column.column.empty_values: return bound_column.default available = { 'value': value, 'record': self.record, 'column': bound_column.column, 'bound_column': bound_column, 'bound_row': self, 'table': self._table, } expected = {} # provide only the arguments expected by `render` argspec = getargspec(bound_column.render) args, varkw = argspec[0], argspec[2] if varkw: expected = available else: for key, value in available.items(): if key in args[1:]: expected[key] = value return bound_column.render(**expected) def __contains__(self, item): """Check by both row object and column name.""" if isinstance(item, six.string_types): return item in self.table._columns else: return item in self def items(self): """ Returns iterator yielding ``(bound_column, cell)`` pairs. *cell* is ``row[name]`` -- the rendered unicode value that should be ``rendered within ````. """ for column in self.table.columns: yield (column, self[column.name]) class BoundRows(object): """ Container for spawning `.BoundRow` objects. :param data: iterable of records :param table: the table in which the rows exist This is used for `.Table.rows`. """ def __init__(self, data, table): self.data = data self.table = table def __iter__(self): for record in self.data: yield BoundRow(record, table=self.table) def __len__(self): return len(self.data) def __getitem__(self, key): """ Slicing returns a new `.BoundRows` instance, indexing returns a single `.BoundRow` instance. """ container = BoundRows if isinstance(key, slice) else BoundRow return container(self.data[key], table=self.table) django-tables2-1.0.7/django_tables2/static/000077500000000000000000000000001264227116200204635ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/static/django_tables2/000077500000000000000000000000001264227116200233415ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/static/django_tables2/themes/000077500000000000000000000000001264227116200246265ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/000077500000000000000000000000001264227116200264175ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/css/000077500000000000000000000000001264227116200272075ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/css/screen.css000066400000000000000000000053261264227116200312060ustar00rootroot00000000000000table.paleblue { border-collapse: collapse; border-color: #CCC; border: 1px solid #DDD; } table.paleblue, table.paleblue + ul.pagination { font: normal 11px/14px 'Lucida Grande', Verdana, Helvetica, Arial, sans-serif; } table.paleblue a:link, table.paleblue a:visited, table.paleblue + ul.pagination > li > a { color: #5B80B2; text-decoration: none; font-weight: bold; } table.paleblue a:hover { color: #036; } table.paleblue td, table.paleblue th { padding: 5px; line-height: 13px; border-bottom: 1px solid #EEE; border-left: 1px solid #DDD; text-align: left; } table.paleblue thead th:first-child, table.paleblue thead td:first-child { border-left: none !important; } table.paleblue thead th, table.paleblue thead td { background: #FCFCFC url(../img/header-bg.png) left bottom repeat-x; border-bottom: 1px solid #DDD; padding: 2px 5px; font-size: 11px; vertical-align: middle; color: #666; } table.paleblue thead th > a:link, table.paleblue thead th > a:visited { color: #666; } table.paleblue thead th.orderable > a { padding-right: 20px; background: url(../img/arrow-inactive-up.png) right center no-repeat; } table.paleblue thead th.orderable.asc > a { background-image: url(../img/arrow-active-up.png); } table.paleblue thead th.orderable.desc > a { background-image: url(../img/arrow-active-down.png); } table.paleblue tr.odd { background-color: #EDF3FE; } table.paleblue tr.even { background-color: white; } table.paleblue + ul.pagination { background: white url(../img/pagination-bg.gif) left 180% repeat-x; overflow: auto; margin: 0; padding: 10px; border: 1px solid #DDD; list-style: none; } table.paleblue + ul.pagination > li { float: left; line-height: 22px; margin-left: 10px; } table.paleblue + ul.pagination > li:first-child { margin-left: 0; } table.paleblue + ul.pagination > li.cardinality { float: right; color: #8d8d8d; } table.paleblue > tbody > tr > td > span.true, table.paleblue > tbody > tr > td > span.false { background-position: top left; background-repeat: no-repeat; display: inline-block; height: 10px; overflow: hidden; text-indent: -200px; width: 10px; } table.paleblue > tbody > tr > td > .missing { background: transparent url(../img/missing.png) right center no-repeat; color: #717171; padding-right: 20px; } table.paleblue > tbody > tr > td > .missing:hover { color: #333; } table.paleblue > tbody > tr > td > span.true { background-image: url(../img/true.gif); } table.paleblue > tbody > tr > td > span.false { background-image: url(../img/false.gif); } div.table-container { display: inline-block; } django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/img/000077500000000000000000000000001264227116200271735ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/img/arrow-active-down.png000066400000000000000000000003301264227116200332450ustar00rootroot00000000000000PNG  IHDR tEXtSoftwareAdobe ImageReadyqe<zIDATxbk?R8 ^UKx|A }0X\ r>22t:dx R #)vfVbc()Ȋ@ .F`"LIENDB`django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/img/arrow-active-up.png000066400000000000000000000003121264227116200327220ustar00rootroot00000000000000PNG  IHDR tEXtSoftwareAdobe ImageReadyqe<lIDATxbd@z-K5 Ç`q&tEn@A@@U!H'_0aAX@T?H###H/ @[ ` @=.E!cIENDB`arrow-inactive-down.png000066400000000000000000000003661264227116200335260ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/imgPNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxb?%B0 `A,\p3!gK||/V@ްaȰ`` dt @ C,--srr2?~!!!=`5f }fh`h\1 MRBČIENDB`django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/img/arrow-inactive-up.png000066400000000000000000000003221264227116200332520ustar00rootroot00000000000000PNG  IHDR tEXtSoftwareAdobe ImageReadyqe<tIDATx| ( {Х`(1#ARDKg?PJi} P'd> 􅬵hIZ*+m9'@}UUVJA!=s5ˍ=Ӡ.>6WIENDB`django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/img/false.gif000066400000000000000000000002601264227116200307520ustar00rootroot00000000000000GIF89a !!33..HH``bbXXZZuurr!, - @4(D@:@Ю1A;p cM0ITa;django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/img/header-bg.png000066400000000000000000000002021264227116200315110ustar00rootroot00000000000000PNG  IHDRStEXtSoftwareAdobe ImageReadyqe<$IDATxb?t0NaM=z`> IENDB`django-tables2-1.0.7/django_tables2/static/django_tables2/themes/paleblue/img/missing.png000066400000000000000000000007751264227116200313630ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATx|S=KA|`LU~`XDJ w(҆TB9,r zM,l-J4B7k^ff,㜳( ~OH]S}q֖"pOuL_W{ē'E>+D yq8:Y Amv So/Sm־Nsٜ/D4e*ڮaV'Vx|0PLk$wħ`e*Twxܟ`` tag. :type: `dict` When accessing the attribute, the value is always returned as an `.AttributeDict` to allow easily conversion to HTML. .. attribute:: columns The columns in the table. :type: `.BoundColumns` .. attribute:: default Text to render in empty cells (determined by `.Column.empty_values`, default `.Table.Meta.default`) :type: `unicode` .. attribute:: empty_text Empty text to render when the table has no data. (default `.Table.Meta.empty_text`) :type: `unicode` .. attribute:: exclude The names of columns that shouldn't be included in the table. :type: iterable of `unicode` .. attribute:: order_by_field If not `None`, defines the name of the *order by* querystring field. :type: `unicode` .. attribute:: page The current page in the context of pagination. Added during the call to `.Table.paginate`. .. attribute:: page_field If not `None`, defines the name of the *current page* querystring field. :type: `unicode` .. attribute:: paginator The current paginator for the table. Added during the call to `.Table.paginate`. .. attribute:: per_page_field If not `None`, defines the name of the *per page* querystring field. :type: `unicode` .. attribute:: show_header If `False`, the table will not have a header (``), default value is `True` :type: `bool` .. attribute:: prefix A prefix for querystring fields to avoid name-clashes when using multiple tables on a single page. :type: `unicode` .. attribute:: rows The rows of the table (ignoring pagination). :type: `.BoundRows` .. attribute:: sequence The sequence/order of columns the columns (from left to right). :type: iterable Items in the sequence must be :term:`column names `, or ``"..."`` (string containing three periods). ``...`` can be used as a catch-all for columns that aren't specified. .. attribute:: orderable Enable/disable column ordering on this table :type: `bool` .. attribute:: template The template to render when using ``{% render_table %}`` (default ``"django_tables2/table.html"``) :type: `unicode` """ TableDataClass = TableData def __init__(self, data, order_by=None, orderable=None, empty_text=None, exclude=None, attrs=None, sequence=None, prefix=None, order_by_field=None, page_field=None, per_page_field=None, template=None, sortable=None, default=None, request=None, show_header=None): super(TableBase, self).__init__() self.exclude = exclude or () self.sequence = sequence self.data = self.TableDataClass(data=data, table=self) if default is None: default = self._meta.default self.default = default self.rows = BoundRows(data=self.data, table=self) self.attrs = AttributeDict(computed_values(attrs if attrs is not None else self._meta.attrs)) self.empty_text = empty_text if empty_text is not None else self._meta.empty_text if sortable is not None: warnings.warn("`sortable` is deprecated, use `orderable` instead.", DeprecationWarning) if orderable is None: orderable = sortable self.orderable = orderable self.prefix = prefix self.order_by_field = order_by_field self.page_field = page_field self.per_page_field = per_page_field self.show_header = show_header # Make a copy so that modifying this will not touch the class # definition. Note that this is different from forms, where the # copy is made available in a ``fields`` attribute. self.base_columns = copy.deepcopy(type(self).base_columns) # Keep fully expanded ``sequence`` at _sequence so it's easily accessible # during render. The priority is as follows: # 1. sequence passed in as an argument # 2. sequence declared in ``Meta`` # 3. sequence defaults to '...' if sequence is not None: self._sequence = Sequence(sequence) self._sequence.expand(self.base_columns.keys()) elif self._meta.sequence: self._sequence = self._meta.sequence else: self._sequence = Sequence(self._meta.fields + ('...',)) self._sequence.expand(self.base_columns.keys()) self.columns = columns.BoundColumns(self) # `None` value for order_by means no order is specified. This means we # `shouldn't touch our data's ordering in any way. *However* # `table.order_by = None` means "remove any ordering from the data" # (it's equivalent to `table.order_by = ()`). if order_by is None and self._meta.order_by is not None: order_by = self._meta.order_by if order_by is None: self._order_by = None # If possible inspect the ordering on the data we were given and # update the table to reflect that. order_by = self.data.ordering if order_by is not None: self.order_by = order_by else: self.order_by = order_by self.template = template # If a request is passed, configure for request if request: RequestConfig(request).configure(self) def as_html(self, request=None): """ Render the table to a simple HTML table, adding `request` to the context. """ template = get_template(self.template) context = {'table': self} if request: if VERSION < (1, 8): context = RequestContext(request, context) else: context['request'] = request return template.render(context) @property def attrs(self): return self._attrs @attrs.setter def attrs(self, value): self._attrs = value @property def show_header(self): return (self._show_header if self._show_header is not None else self._meta.show_header) @show_header.setter def show_header(self, value): self._show_header = value @property def empty_text(self): return self._empty_text @empty_text.setter def empty_text(self, value): self._empty_text = value @property def order_by(self): return self._order_by @order_by.setter def order_by(self, value): """ Order the rows of the table based on columns. :param value: iterable of order by aliases. """ # collapse empty values to () order_by = () if not value else value # accept string order_by = order_by.split(',') if isinstance(order_by, six.string_types) else order_by valid = [] # everything's been converted to a iterable, accept iterable! for alias in order_by: name = OrderBy(alias).bare if name in self.columns and self.columns[name].orderable: valid.append(alias) self._order_by = OrderByTuple(valid) self.data.order_by(self._order_by) @property def order_by_field(self): return (self._order_by_field if self._order_by_field is not None else self._meta.order_by_field) @order_by_field.setter def order_by_field(self, value): self._order_by_field = value @property def page_field(self): return (self._page_field if self._page_field is not None else self._meta.page_field) @page_field.setter def page_field(self, value): self._page_field = value def paginate(self, klass=Paginator, per_page=None, page=1, *args, **kwargs): """ Paginates the table using a paginator and creates a ``page`` property containing information for the current page. :type klass: Paginator class :param klass: a paginator class to paginate the results :type per_page: `int` :param per_page: how many records are displayed on each page :type page: `int` :param page: which page should be displayed. Extra arguments are passed to the paginator. Pagination exceptions (`~django.core.paginator.EmptyPage` and `~django.core.paginator.PageNotAnInteger`) may be raised from this method and should be handled by the caller. """ per_page = per_page or self._meta.per_page self.paginator = klass(self.rows, per_page, *args, **kwargs) self.page = self.paginator.page(page) @property def per_page_field(self): return (self._per_page_field if self._per_page_field is not None else self._meta.per_page_field) @per_page_field.setter def per_page_field(self, value): self._per_page_field = value @property def prefix(self): return (self._prefix if self._prefix is not None else self._meta.prefix) @prefix.setter def prefix(self, value): self._prefix = value @property def prefixed_order_by_field(self): return "%s%s" % (self.prefix, self.order_by_field) @property def prefixed_page_field(self): return "%s%s" % (self.prefix, self.page_field) @property def prefixed_per_page_field(self): return "%s%s" % (self.prefix, self.per_page_field) @property def sequence(self): return self._sequence @sequence.setter def sequence(self, value): if value: value = Sequence(value) value.expand(self.base_columns.keys()) self._sequence = value @property def orderable(self): return (self._orderable if self._orderable is not None else self._meta.orderable) @orderable.setter def orderable(self, value): self._orderable = value @property def sortable(self): warnings.warn("`sortable` is deprecated, use `orderable` instead.", DeprecationWarning) return self.orderable @sortable.setter def sortable(self, value): warnings.warn("`sortable` is deprecated, use `orderable` instead.", DeprecationWarning) self.orderable = value @property def template(self): return (self._template if self._template is not None else self._meta.template) @template.setter def template(self, value): self._template = value # Python 2/3 compatible way to enable the metaclass Table = DeclarativeColumnsMetaclass(str('Table'), (TableBase, ), {}) django-tables2-1.0.7/django_tables2/templates/000077500000000000000000000000001264227116200211725ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/templates/django_tables2/000077500000000000000000000000001264227116200240505ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/templates/django_tables2/table.html000066400000000000000000000064661264227116200260410ustar00rootroot00000000000000{% spaceless %} {% load django_tables2 %} {% load i18n %} {% if table.page %}
{% endif %} {% block table %} {% nospaceless %} {% block table.thead %} {% if table.show_header %} {% for column in table.columns %} {% if column.orderable %} {{ column.header }} {% else %} {{ column.header }} {% endif %} {% endfor %} {% endif %} {% endblock table.thead %} {% block table.tbody %} {% for row in table.page.object_list|default:table.rows %} {# support pagination #} {% block table.tbody.row %} {# avoid cycle for Django 1.2-1.6 compatibility #} {% for column, cell in row.items %} {% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %} {% endfor %} {% endblock table.tbody.row %} {% empty %} {% if table.empty_text %} {% block table.tbody.empty_text %} {{ table.empty_text }} {% endblock table.tbody.empty_text %} {% endif %} {% endfor %} {% endblock table.tbody %} {% block table.tfoot %} {% endblock table.tfoot %} {% endnospaceless %} {% endblock table %} {% if table.page %} {% with table.page.paginator.count as total %} {% with table.page.object_list|length as count %} {% block pagination %}
    {% if table.page.has_previous %} {% nospaceless %}{% block pagination.previous %}{% endblock pagination.previous %}{% endnospaceless %} {% endif %} {% if table.page.has_previous or table.page.has_next %} {% nospaceless %}{% block pagination.current %}
  • {% blocktrans with table.page.number as current and table.paginator.num_pages as total %}Page {{ current }} of {{ total }}{% endblocktrans %}
  • {% endblock pagination.current %}{% endnospaceless %} {% endif %} {% if table.page.has_next %} {% nospaceless %}{% block pagination.next %}{% endblock pagination.next %}{% endnospaceless %} {% endif %} {% nospaceless %}{% block pagination.cardinality %}
  • {% if total != count %}{% blocktrans %}{{ count }} of {{ total }}{% endblocktrans %}{% else %}{{ total }}{% endif %} {% if total == 1 %}{{ table.data.verbose_name }}{% else %}{{ table.data.verbose_name_plural }}{% endif %}
  • {% endblock pagination.cardinality %}{% endnospaceless %}
{% endblock pagination %} {% endwith %} {% endwith %}
{% endif %} {% endspaceless %} django-tables2-1.0.7/django_tables2/templatetags/000077500000000000000000000000001264227116200216665ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/templatetags/__init__.py000066400000000000000000000000001264227116200237650ustar00rootroot00000000000000django-tables2-1.0.7/django_tables2/templatetags/django_tables2.py000066400000000000000000000234011264227116200251160ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals import re import tokenize from collections import OrderedDict import six from django import template from django.core.exceptions import ImproperlyConfigured from django.template import Node, TemplateSyntaxError, Variable from django.template.defaultfilters import title as old_title from django.template.defaultfilters import stringfilter from django.template.loader import get_template, select_template from django.utils.html import escape from django.utils.http import urlencode from django.utils.safestring import mark_safe import django_tables2 as tables from django_tables2.config import RequestConfig register = template.Library() kwarg_re = re.compile(r"(?:(.+)=)?(.+)") context_processor_error_msg = ( "{%% %s %%} requires django.core.context_processors.request " "to be in your settings.TEMPLATE_CONTEXT_PROCESSORS in order for " "the included template tags to function correctly." ) def token_kwargs(bits, parser): """ Based on Django's `~django.template.defaulttags.token_kwargs`, but with a few changes: - No legacy mode. - Both keys and values are compiled as a filter """ if not bits: return {} kwargs = OrderedDict() while bits: match = kwarg_re.match(bits[0]) if not match or not match.group(1): return kwargs key, value = match.groups() del bits[:1] kwargs[parser.compile_filter(key)] = parser.compile_filter(value) return kwargs class SetUrlParamNode(Node): def __init__(self, changes): super(SetUrlParamNode, self).__init__() self.changes = changes def render(self, context): if not 'request' in context: raise ImproperlyConfigured(context_processor_error_msg % 'set_url_param') params = dict(context['request'].GET) for key, newvalue in self.changes.items(): newvalue = newvalue.resolve(context) if newvalue == '' or newvalue is None: params.pop(key, False) else: params[key] = six.text_type(newvalue) return "?" + urlencode(params, doseq=True) @register.tag def set_url_param(parser, token): """ Creates a URL (containing only the querystring [including "?"]) based on the current URL, but updated with the provided keyword arguments. Example:: {% set_url_param name="help" age=20 %} ?name=help&age=20 **Deprecated** as of 0.7.0, use `querystring`. """ bits = token.contents.split() qschanges = {} for i in bits[1:]: try: key, value = i.split('=', 1) key = key.strip() value = value.strip() key_line_iter = six.StringIO(key).readline keys = list(tokenize.generate_tokens(key_line_iter)) if keys[0][0] == tokenize.NAME: # workaround bug #5270 value = Variable(value) if value == '""' else parser.compile_filter(value) qschanges[str(key)] = value else: raise ValueError except ValueError: raise TemplateSyntaxError("Argument syntax wrong: should be" "key=value") return SetUrlParamNode(qschanges) class QuerystringNode(Node): def __init__(self, updates, removals): super(QuerystringNode, self).__init__() self.updates = updates self.removals = removals def render(self, context): if not 'request' in context: raise ImproperlyConfigured(context_processor_error_msg % 'querystring') params = dict(context['request'].GET) for key, value in self.updates.items(): key = key.resolve(context) value = value.resolve(context) if key not in ("", None): params[key] = value for removal in self.removals: params.pop(removal.resolve(context), None) return escape("?" + urlencode(params, doseq=True)) # {% querystring "name"="abc" "age"=15 %} @register.tag def querystring(parser, token): """ Creates a URL (containing only the querystring [including "?"]) derived from the current URL's querystring, by updating it with the provided keyword arguments. Example (imagine URL is ``/abc/?gender=male&name=Brad``):: {% querystring "name"="Ayers" "age"=20 %} ?name=Ayers&gender=male&age=20 {% querystring "name"="Ayers" without "gender" %} ?name=Ayers """ bits = token.split_contents() tag = bits.pop(0) updates = token_kwargs(bits, parser) # ``bits`` should now be empty of a=b pairs, it should either be empty, or # have ``without`` arguments. if bits and bits.pop(0) != "without": raise TemplateSyntaxError("Malformed arguments to '%s'" % tag) removals = [parser.compile_filter(bit) for bit in bits] return QuerystringNode(updates, removals) class RenderTableNode(Node): """ :param table: the table to render :type table: Table object :param template: Name[s] of template to render :type template: unicode or list """ def __init__(self, table, template=None): super(RenderTableNode, self).__init__() self.table = table self.template = template def render(self, context): table = self.table.resolve(context) if isinstance(table, tables.Table): pass elif hasattr(table, "model"): queryset = table # We've been given a queryset, create a table using its model and # render that. class OnTheFlyTable(tables.Table): class Meta: model = queryset.model attrs = {"class": "paleblue"} table = OnTheFlyTable(queryset) request = context.get('request') if request: RequestConfig(request).configure(table) else: raise ValueError("Expected table or queryset, not '%s'." % type(table).__name__) if self.template: template = self.template.resolve(context) else: template = table.template if isinstance(template, six.string_types): template = get_template(template) else: # assume some iterable was given template = select_template(template) # Contexts are basically a `MergeDict`, when you `update()`, it # internally just adds a dict to the list to attempt lookups from. This # is why we're able to `pop()` later. context.update({"table": table}) try: # HACK: # TemplateColumn benefits from being able to use the context # that the table is rendered in. The current way this is # achieved is to temporarily attach the context to the table, # which TemplateColumn then looks for and uses. table.context = context return template.render(context) finally: del table.context context.pop() @register.tag def render_table(parser, token): """ Render a HTML table. The tag can be given either a `.Table` object, or a queryset. An optional second argument can specify the template to use. Example:: {% render_table table %} {% render_table table "custom.html" %} {% render_table user_queryset %} When given a queryset, a `.Table` class is generated dynamically as follows:: class OnTheFlyTable(tables.Table): class Meta: model = queryset.model attrs = {"class": "paleblue"} For configuration beyond this, a `.Table` class must be manually defined, instantiated, and passed to this tag. The context should include a *request* variable containing the current request. This allows pagination URLs to be created without clobbering the existing querystring. """ bits = token.split_contents() try: bits.pop(0) table = parser.compile_filter(bits.pop(0)) except ValueError: raise TemplateSyntaxError("'%s' must be given a table or queryset." % bits[0]) template = parser.compile_filter(bits.pop(0)) if bits else None return RenderTableNode(table, template) class NoSpacelessNode(Node): def __init__(self, nodelist): self.nodelist = nodelist super(NoSpacelessNode, self).__init__() def render(self, context): return mark_safe(re.sub(r'>\s+<', '> <', self.nodelist.render(context))) @register.tag def nospaceless(parser, token): nodelist = parser.parse(('endnospaceless',)) parser.delete_first_token() return NoSpacelessNode(nodelist) RE_UPPERCASE = re.compile('[A-Z]') @register.filter @stringfilter def title(value): """ A slightly better title template filter. Same as Django's builtin `~django.template.defaultfilters.title` filter, but operates on individual words and leaves words unchanged if they already have a capital letter. """ title_word = lambda w: w if RE_UPPERCASE.search(w) else old_title(w) return re.sub('(\S+)', lambda m: title_word(m.group(0)), value) title.is_safe = True # Django 1.2 doesn't include the l10n template tag library (and it's non- # trivial to implement) so for Django 1.2 the localize functionality is # disabled. try: from django.templatetags.l10n import register as l10n_register except ImportError: localize = unlocalize = lambda x: x # no-op else: localize = l10n_register.filters['localize'] unlocalize = l10n_register.filters['unlocalize'] register.filter('localize', localize) register.filter('unlocalize', unlocalize) django-tables2-1.0.7/django_tables2/utils.py000066400000000000000000000435421264227116200207160ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals import inspect import warnings from itertools import chain import six from django.utils.html import escape from django.utils.safestring import mark_safe def python_2_unicode_compatible(klass): """ A decorator that defines __unicode__ and __str__ methods under Python 2. Under Python 3 it does nothing. To support Python 2 and 3 with a single code base, define a __str__ method returning text and apply this decorator to the class. Taken directly from Django. """ if not six.PY3: klass.__unicode__ = klass.__str__ klass.__str__ = lambda self: self.__unicode__().encode('utf-8') return klass class Sequence(list): """ Represents a column sequence, e.g. ``("first_name", "...", "last_name")`` This is used to represent `.Table.Meta.sequence` or the `.Table` constructors's *sequence* keyword argument. The sequence must be a list of column names and is used to specify the order of the columns on a table. Optionally a "..." item can be inserted, which is treated as a *catch-all* for column names that aren't explicitly specified. """ def expand(self, columns): """ Expands the ``"..."`` item in the sequence into the appropriate column names that should be placed there. :raises: `ValueError` if the sequence is invalid for the columns. """ ellipses = self.count("...") if ellipses > 1: raise ValueError("'...' must be used at most once in a sequence.") elif ellipses == 0: self.append("...") # everything looks good, let's expand the "..." item columns = list(columns) # take a copy and exhaust the generator head = [] tail = [] target = head # start by adding things to the head for name in self: if name == "...": # now we'll start adding elements to the tail target = tail continue target.append(name) if name in columns: columns.pop(columns.index(name)) self[:] = chain(head, columns, tail) class OrderBy(six.text_type): """ A single item in an `.OrderByTuple` object. This class is essentially just a `str` with some extra properties. """ @property def bare(self): """ Return the bare form. The *bare form* is the non-prefixed form. Typically the bare form is just the ascending form. Example: ``age`` is the bare form of ``-age`` :rtype: `.OrderBy` object """ return OrderBy(self[1:]) if self[:1] == '-' else self @property def opposite(self): """ Return an `.OrderBy` object with an opposite sort influence. Example: .. code-block:: python >>> order_by = OrderBy('name') >>> order_by.opposite '-name' :rtype: `.OrderBy` object """ return OrderBy(self[1:]) if self.is_descending else OrderBy('-' + self) @property def is_descending(self): """ Return `True` if this object induces *descending* ordering :rtype: `bool` """ return self.startswith('-') @property def is_ascending(self): """ Return `True` if this object induces *ascending* ordering. :returns: `bool` """ return not self.is_descending @python_2_unicode_compatible class OrderByTuple(tuple): """Stores ordering as (as `.OrderBy` objects). The `~django_tables2.tables.Table.order_by` property is always converted to an `.OrderByTuple` object. This class is essentially just a `tuple` with some useful extras. Example: .. code-block:: python >>> x = OrderByTuple(('name', '-age')) >>> x['age'] '-age' >>> x['age'].is_descending True >>> x['age'].opposite 'age' """ def __new__(cls, iterable): transformed = [] for item in iterable: if not isinstance(item, OrderBy): item = OrderBy(item) transformed.append(item) return super(OrderByTuple, cls).__new__(cls, transformed) def __str__(self): return ','.join(self) def __contains__(self, name): """ Determine if a column has an influence on ordering. Example: .. code-block:: python >>> ordering = >>> x = OrderByTuple(('name', )) >>> 'name' in x True >>> '-name' in x True :param name: The name of a column. (optionally prefixed) :returns: `bool` """ name = OrderBy(name).bare for order_by in self: if order_by.bare == name: return True return False def __getitem__(self, index): """ Allows an `.OrderBy` object to be extracted via named or integer based indexing. When using named based indexing, it's fine to used a prefixed named. .. code-block:: python >>> x = OrderByTuple(('name', '-age')) >>> x[0] 'name' >>> x['age'] '-age' >>> x['-age'] '-age' :rtype: `.OrderBy` object """ if isinstance(index, six.string_types): for order_by in self: if order_by == index or order_by.bare == index: return order_by raise KeyError return super(OrderByTuple, self).__getitem__(index) @property def key(self): accessors = [] reversing = [] for order_by in self: accessors.append(Accessor(order_by.bare)) reversing.append(order_by.is_descending) @total_ordering class Comparator(object): def __init__(self, obj): self.obj = obj def __eq__(self, other): for accessor in accessors: a = accessor.resolve(self.obj, quiet=True) b = accessor.resolve(other.obj, quiet=True) if not a == b: return False return True def __lt__(self, other): for accessor, reverse in six.moves.zip(accessors, reversing): a = accessor.resolve(self.obj, quiet=True) b = accessor.resolve(other.obj, quiet=True) if a == b: continue if reverse: a, b = b, a # The rest of this should be refactored out into a util # function 'compare' that handles different types. try: return a < b except TypeError: # If the truth values differ, it's a good way to # determine ordering. if bool(a) is not bool(b): return bool(a) < bool(b) # Handle comparing different types, by falling back to # the string and id of the type. This at least groups # different types together. a_type = type(a) b_type = type(b) return (repr(a_type), id(a_type)) < (repr(b_type), id(b_type)) return False return Comparator @property def cmp(self): """ Return a function for use with `list.sort` that implements this object's ordering. This is used to sort non-`.QuerySet` based :term:`table data`. :rtype: function """ warnings.warn('`cmp` is deprecated, use `key` instead.', DeprecationWarning) # pylint: disable=C0103 def _cmp(a, b): for accessor, reverse in instructions: x = accessor.resolve(a) y = accessor.resolve(b) try: res = cmp(x, y) except TypeError: res = cmp((repr(type(x)), id(type(x)), x), (repr(type(y)), id(type(y)), y)) if res != 0: return -res if reverse else res return 0 instructions = [] for order_by in self: if order_by.startswith('-'): instructions.append((Accessor(order_by[1:]), True)) else: instructions.append((Accessor(order_by), False)) return _cmp def get(self, key, fallback): """ Identical to __getitem__, but supports fallback value. """ try: return self[key] except (KeyError, IndexError): return fallback @property def opposite(self): """ Return version with each `.OrderBy` prefix toggled. Example: .. code-block:: python >>> order_by = OrderByTuple(('name', '-age')) >>> order_by.opposite ('-name', 'age') """ return type(self)((o.opposite for o in self)) class Accessor(str): """ A string describing a path from one object to another via attribute/index accesses. For convenience, the class has an alias `.A` to allow for more concise code. Relations are separated by a ``.`` character. """ SEPARATOR = '.' def resolve(self, context, safe=True, quiet=False): """ Return an object described by the accessor by traversing the attributes of *context*. Example: .. code-block:: python >>> x = Accessor('__len__') >>> x.resolve('brad') 4 >>> x = Accessor('0.upper') >>> x.resolve('brad') 'B' :type context: `object` :param context: The root/first object to traverse. :type safe: `bool` :param safe: Don't call anything with ``alters_data = True`` :type quiet: bool :param quiet: Smother all exceptions and instead return `None` :returns: target object :raises: anything ``getattr(a, "b")`` raises, e.g. `TypeError`, `AttributeError`, `KeyError`, `ValueError` (unless *quiet* == `True`) `~.Accessor.resolve` attempts lookups in the following order: - dictionary (e.g. ``obj[related]``) - attribute (e.g. ``obj.related``) - list-index lookup (e.g. ``obj[int(related)]``) Callable objects are called, and their result is used, before proceeding with the resolving. """ try: current = context for bit in self.bits: try: # dictionary lookup current = current[bit] except (TypeError, AttributeError, KeyError): try: # attribute lookup current = getattr(current, bit) except (TypeError, AttributeError): try: # list-index lookup current = current[int(bit)] except (IndexError, # list index out of range ValueError, # invalid literal for int() KeyError, # dict without `int(bit)` key TypeError, # unsubscriptable object ): raise ValueError('Failed lookup for key [%s] in %r' ', when resolving the accessor %s' % (bit, current, self)) if callable(current): if safe and getattr(current, 'alters_data', False): raise ValueError('refusing to call %s() because `.alters_data = True`' % repr(current)) if not getattr(current, 'do_not_call_in_templates', False): current = current() # important that we break in None case, or a relationship # spanning across a null-key will raise an exception in the # next iteration, instead of defaulting. if current is None: break return current except: if not quiet: raise @property def bits(self): if self == '': return () return self.split(self.SEPARATOR) A = Accessor # alias class AttributeDict(dict): """ A wrapper around `dict` that knows how to render itself as HTML style tag attributes. The returned string is marked safe, so it can be used safely in a template. See `.as_html` for a usage example. """ def as_html(self): """ Render to HTML tag attributes. Example: .. code-block:: python >>> from django_tables2.utils import AttributeDict >>> attrs = AttributeDict({'class': 'mytable', 'id': 'someid'}) >>> attrs.as_html() 'class="mytable" id="someid"' :rtype: `~django.utils.safestring.SafeUnicode` object """ return mark_safe(' '.join(['%s="%s"' % (k, escape(v if not callable(v) else v())) for k, v in six.iteritems(self)])) def segment(sequence, aliases): """ Translates a flat sequence of items into a set of prefixed aliases. This allows the value set by `.QuerySet.order_by` to be translated into a list of columns that would have the same result. These are called "order by aliases" which are optionally prefixed column names. e.g. >>> list(segment(("a", "-b", "c"), ... {"x": ("a"), ... "y": ("b", "-c"), ... "z": ("-b", "c")})) [("x", "-y"), ("x", "z")] """ if not (sequence or aliases): return for alias, parts in aliases.items(): variants = { # alias: order by tuple alias: OrderByTuple(parts), OrderBy(alias).opposite: OrderByTuple(parts).opposite, } for valias, vparts in variants.items(): if list(sequence[:len(vparts)]) == list(vparts): tail_aliases = dict(aliases) del tail_aliases[alias] tail_sequence = sequence[len(vparts):] if tail_sequence: for tail in segment(tail_sequence, tail_aliases): yield tuple(chain([valias], tail)) else: continue else: yield tuple([valias]) class cached_property(object): # pylint: disable=C0103 """ Decorator that creates converts a method with a single self argument into a property cached on the instance. Taken directly from Django 1.4. """ def __init__(self, func): from functools import wraps wraps(func)(self) self.func = func def __get__(self, instance, cls): res = instance.__dict__[self.func.__name__] = self.func(instance) return res funcs = (name for name in ('getfullargspec', 'getargspec') if hasattr(inspect, name)) getargspec = getattr(inspect, next(funcs)) del funcs def total_ordering(cls): """Class decorator that fills in missing ordering methods""" convert = { '__lt__': [('__gt__', lambda self, other: not (self < other or self == other)), ('__le__', lambda self, other: self < other or self == other), ('__ge__', lambda self, other: not self < other)], '__le__': [('__ge__', lambda self, other: not self <= other or self == other), ('__lt__', lambda self, other: self <= other and not self == other), ('__gt__', lambda self, other: not self <= other)], '__gt__': [('__lt__', lambda self, other: not (self > other or self == other)), ('__ge__', lambda self, other: self > other or self == other), ('__le__', lambda self, other: not self > other)], '__ge__': [('__le__', lambda self, other: (not self >= other) or self == other), ('__gt__', lambda self, other: self >= other and not self == other), ('__lt__', lambda self, other: not self >= other)] } roots = set(dir(cls)) & set(convert) if not roots: raise ValueError('must define at least one ordering operation: < > <= >=') root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__ for opname, opfunc in convert[root]: if opname not in roots: opfunc.__name__ = str(opname) # Py2 requires non-unicode, Py3 requires unicode. opfunc.__doc__ = getattr(int, opname).__doc__ setattr(cls, opname, opfunc) return cls def computed_values(d): """ Computes a new `dict` that has callable values replaced with the return values. Simple example: >>> compute_values({"foo": lambda: "bar"}) {"foo": "bar"} Arbitrarily deep structures are supported. The logic is as follows: 1. If the value is callable, call it and make that the new value. 2. If the value is an instance of dict, use ComputableDict to compute its keys. Example: >>> def parents(): ... return { ... "father": lambda: "Foo", ... "mother": "Bar" ... } ... >>> a = { ... "name": "Brad", ... "parents": parents ... } ... >>> computed_values(a) {"name": "Brad", "parents": {"father": "Foo", "mother": "Bar"}} :rtype: dict """ result = {} for k, v in six.iteritems(d): if callable(v): v = v() if isinstance(v, dict): v = computed_values(v) result[k] = v return result django-tables2-1.0.7/django_tables2/views.py000066400000000000000000000070441264227116200207100ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.views.generic.list import ListView from .config import RequestConfig class SingleTableMixin(object): """ Adds a Table object to the context. Typically used with `.TemplateResponseMixin`. :param table_class: table class :type table_class: subclass of `.Table` :param table_data: data used to populate the table :type table_data: any compatible data source :param context_table_name: name of the table's template variable (default: "table") :type context_table_name: `unicode` :param table_pagination: controls table pagination. If a `dict`, passed as the *paginate* keyword argument to `.RequestConfig`. As such, any non-`False` value enables pagination. This mixin plays nice with the Django's`.MultipleObjectMixin` by using `.get_queryset`` as a fallback for the table data source. """ table_class = None table_data = None context_table_name = None table_pagination = None def get_table(self, **kwargs): """ Return a table object to use. The table has automatic support for sorting and pagination. """ options = {} table_class = self.get_table_class() table = table_class(self.get_table_data(), **kwargs) paginate = self.get_table_pagination() # pylint: disable=E1102 if paginate is not None: options['paginate'] = paginate RequestConfig(self.request, **options).configure(table) return table def get_table_class(self): """ Return the class to use for the table. """ if self.table_class: return self.table_class raise ImproperlyConfigured("A table class was not specified. Define " "%(cls)s.table_class" % {"cls": type(self).__name__}) def get_context_table_name(self, table): """ Get the name to use for the table's template variable. """ return self.context_table_name or "table" def get_table_data(self): """ Return the table data that should be used to populate the rows. """ if self.table_data: return self.table_data elif hasattr(self, "object_list"): return self.object_list elif hasattr(self, "get_queryset"): return self.get_queryset() raise ImproperlyConfigured("Table data was not specified. Define " "%(cls)s.table_data" % {"cls": type(self).__name__}) def get_table_pagination(self): """ Returns pagination options: True for standard pagination (default), False for no pagination, and a dictionary for custom pagination. """ return self.table_pagination def get_context_data(self, **kwargs): """ Overriden version of `.TemplateResponseMixin` to inject the table into the template's context. """ context = super(SingleTableMixin, self).get_context_data(**kwargs) table = self.get_table() context[self.get_context_table_name(table)] = table return context class SingleTableView(SingleTableMixin, ListView): """ Generic view that renders a template and passes in a `.Table` object. """ django-tables2-1.0.7/docs/000077500000000000000000000000001264227116200152465ustar00rootroot00000000000000django-tables2-1.0.7/docs/Makefile000066400000000000000000000110211264227116200167010ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -W SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-tables2.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-tables2.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-tables2" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-tables2" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." django-tables2-1.0.7/docs/_static/000077500000000000000000000000001264227116200166745ustar00rootroot00000000000000django-tables2-1.0.7/docs/_static/example.png000066400000000000000000000444121264227116200210420ustar00rootroot00000000000000PNG  IHDRL'MiCCPICC ProfilexYgTMͻ,a9眓%'AX% HQQbD@QEA0""(PDPQEDPߝsyN}3Gw0<`F>?FQ8"a ?06 q7&EoCf#"v78 T*=YD.!3DҢ`B~@(5n?#c#X_vBTlR!mA4MiԤȈx6g|~Y Jir{DoE0&H_f'?0 ȨbP?0FNތsa注{?x.naؠX3g#>|qt 93L3B0R/l968Nx!80(@ "CS wh K '@,eb`~R.GxSտٿ??(WZb kĚcJ~ Қ  BϿ[}ഩ0hneO Z6F@́h5mEʴ/7*apqA;XItZHhZ YE( ()+g75xYX ȸY(]/FLb2, ' i@)*@C`V9,C+ B,iAd 9AEA.h7BI:uB=!z _0 f`~XV`#v}8N 6?' H(0J2AmEyQtTj?U:BB&P%4͊B#D14t>]nCGЯs5 Ç`0L"&Sôb11SX,+DƯ'6 Va[ױǍ⨸8\ׄM~IxA gKn0~L`&t[ $AB-0EX&%zDb1XF'~#H"$m#F Αn^( 2 &  3dRj-o p  t d  ^ .d$!T&'4''l)/|RxPxYDRU$KE(QTK4XXWtNLPNlgq-PR[{%%f$$$%H>"KHHUK=JkIKWI2222daY YlC9\\yy#38l>+)z)VT4LQVRRWQ PTyJV5WMWP&vLmLUN}z]{M1M?ͣOشnkcӵh/h\+ۨ;ErKЖ-z"zTzB~'' o E  Œ>+Ӎ[MtLRML-LQ\*^1PHn $tfB} ;5tz{DK$>/3շC`es'btbmuPOlGr:A?2g[ŝ,;v$$&M''JAޕuQ4(?7]4=;}*">y?K)0n]ٓ{,a̡<٫>>ھ\ܵ)=|zApAaE=lp0pȮXx#G/%ƗNٖu*_x\i\rhŪc?w ډ'۪%Kj5 5jjo:PSWz:DS}_fCC#_33M;ʟ=ђw? 6z/j]<{IVmP[R\{hDgN.ݮ O_Rynbwvk ףLϱoߦM7nݺv[;:w:jmqm@}A:ny=l03b:rգ{?u{db,pliӯ-g<F\gϗ~J>j{^'%[M/'VVWl֞GGSͳ y̟FrO$;7dQP! 0 OP<f[ Û$$Efaeb``'ȟ+UG) 6 i{yFŋJ*j4^k.i3l3շ72 5J01)172krjmlg/UAkv_7?'y~zL`k(.t9u[Ȭ<*@Lk9'4qN$Ѥ')-i3"srfaVnl=9E{Cm]Ch>u"b# %JejG5t:QvrO]ÍgN54w2|їڤ#::ۺ\qM[OaLe^ߞ'>`q?p0AĐCaңG=ڳ-/xe<=1zMdʩCL'D`Qu2ϵ_r¾ϫ}.RW+ܫk/rJtB&v@a^8 GE~a18PEL%2x=Y(Cl?9$9}lŽ"EIf Rue1K))j+i+H 1C5vNnŖ<0mFƚ& bf<Xe9)1ns[c.n\=o{]v;'vE@O`mоPE [𶈲Ȥ(є1EqфS;͒ؒ&/dOK֕~ #,):{4rϝG'@X-qKEb#R%e*B*GS+<~5KOշ6{8EcnM,Q85?-7ëYO~M~H\}\F"rRpBahփOȨ=h# QpwDACc9S!ABJ)6/9)<|T]gzL~CNMcRҧdvz)C JBʋ*ujirVv!DkCAS&inf ,[Ykؐl&l[r}Ա)Em p]Oיmޖ>>oMrR'$ׅĄдp鈯Q;̣ 1tX|l\V~rB{bNj}SxR*HLӺ2,33&v>2'{o>\y伅n4,>y8^] JbJcʢˣ*h~G9t):ӶN Τ4]j^n8WtEK =β+W#^g wsĝԻdg N= x0oiїvozxӍ#2gm ^[p  `r]?0Bݠ#Y?pԂzaNXw Ń2B:Po$>DOc1fx$2`سw /şïl '?fEfb8I&ȓ=L*L̇X,{(0%ÚV.~Ôcsۄ-^^y|r MWEE1ĎKIR$?HIWdk+((**+DFV: [^p227icb}Ħ6nݾgkC#)[7wgρmdoGc~LTU3!34H)d=ѤwOhJęb35=3u?[cO^}iyFZ &1/FƍH'V7mlmiyυm}~A!2ua Q1c؄FIRvb 22wd7)>=\Y$P|WX6]U9RqZܩu?=Zϰ55h;vs16N+bW+1^Opýʭw(w-ޏ|;'.On[7b\uyQMuO>GNsW.3ϯ.WsZ0Y-Zʈ>xwOcM8X#CQ>;jm_y Bh|>E ϯ.Vq5N8j48^q&<+ʸh1y¥0`UsB(el|}_2t2)wۧa.dInpE7 w43huQqqn5t &&6cB]X?v4R׷3^>% {ϱ_Dxv-X<q`ңܐ&NͶ 'NPen*߻H>ʯU 裏 MbCnZ쭺}p]#f,g56r1 )e89{ 2[g׎}. ^d>(ת#6vǙ3V<᠚A\EW~!ã \!؆%@\_C9d~𬉣⭇\1m<̤-btbY8Q*>ރlO=8=5Vƅ?8B gܾvR)'hŪ/i? ly%lx-6M#kC#)l氢;%ƺ'רG[} {Ťz+JSj frV>#n }‹A! ~Pav 8A5NQ0Q}klckkhe Y.!ҤÇ}||СC~~~rE*Q_mOYIe)laSaxB_6f &Y˜D i GlC-hdf xL]7q6u| oec m$a=uM;&9ՖtQ(k+h6z6"T~G,vԣK*(`ܣI227,GuL[X~כg|ԾGKB,))G<䢮a0Lb[+φMvcO7#T!Gu_ G5]ǜŬc}&]Rxia9n" &3 ^q@*/a&[ Ҟ ,`UkٗCWMq9y",]6Sq ,z&Q!:eŋn;)oKjy:P-ܪ\݇48s;{9}dɯtjI 7)lX(Iͧa #a5Jn2=#Ng,F Wyz_2?tF%P*w' cdއ?ô]/hs 6tl-!x%,Vda,_hѡy!>9% -KXՇc,R.Tqqa@h@_zz}KC?!u T?),c)=,1VqVfI#Ψjf\USF"3etz]чy%S\2Sn\?чڪtp5g1wUoGĕ?_L n"5R*W*Ohs\mڱT6r XK-U[~`L>&bz]GaJcmkWۅgNzҫ_N2 (8)k»TЖRxlLyB!5IS%tps)`=QA:1#JfہBdʺ](42ZHǍ5]{9bEN0  Xiǟ\BD9Nz&u5Ñ40w>ԊB՝c?YkyF&,skScR8(q_؄5>"t,Nm]p }d>83k9lzt̬:#k3kвjf s0}l f6f u?GnǫβwZotchl!QFB&ARBD29ޤV1EWFrھhmd\ۘ$0ߋV\snӻ~#7@h Xƍfó{nBc=PE{k'(}ݦo) c@-$*4![TKa= *jh3=؛1dڻضg|HU7-b#RaчSc{["L,ě)No!ޛmB'%m)#x.UƩ3 - :>v( /ZW>+'fK 4˥-p LXbj!Q)&*O|neVĬ/Ox3f J~2M%7ffKP±'xgѺNHMyQ'i+sQ́O":Q*pT[mxXu<Øuh'> 蛽Ԏ^k8{LM,MlMYqwr7e-^w[/D2Nq %Lo:{?l:X smSk'▏=-$ ׺v'|^'ލF1Mmw8`<5tmAֱY@u1,&/p n6_DTLT 0S H/ZHTh&BBE@lLf> I (|vYiln oP9 ɒz(%I5!ZHl yBW#r0Xff.-WM/5fbA $U"$b * @\+A-"@- 2*+U4 H A.xMch0xac1Ǫi6$/dt[R@GyoBAiH7)%yH&$FyR@GyoBAi rneiaii%[v~3|A+'~e)w]ZX(;?w]!qPZۓ۔++:[%8jJ x ކP%WMF(WR@.( wAi5i\# #deBν GSg %@ZTȪ R0BVi%%,dO$&-K]\_^pkGՕd''aMK=$J]PrAA.$S\s/ ?7)&..ߪ9㑴2(IV+ܸ0*D"QA1롃#I5wC6/7WuĜT+`WuCU@Ee )5ɠ>,:[* % d\Ȅv]!@ˬ.i4of'($,jT bZKXHq ؈DIFʦT:I ).¥y38Cd2I˰v%ᦻfB;;hqIIZPJ 6d_= kAnCSC6Qr[pV@}f/J"{5+#&+OZu5~?/ jj)]gTQ.fRQ7 ?#nV\ :B%3۾pl-C7gjxph[%h3ZmNaS_6:{n멮ZǢ:jo7`{%Ͷ3N6Jj6]-`vkoG<^y1މ] ,G 3 ]ؤ)M!>%bA}V+z TS5yzo~# bGf󎌽6Qh$V슌_vu6RItQ l{=.2.ycn~B ڂbu upd1SlҪZg;l"N zjg͚IcSg \ׯGp?-g(!J:Y:HhfHf]WHyZ )ܡ#z,#tuj$Qy:]<ܮG/TLG9 ab3`qxJYNi%ej5 #!lfo^LEr\\neܻ; K!ɒ;ju j` Gxٗ`'W&m y ;C('sKMdI%%Ia%=A1+ v=`FކB N6ɪ\0;"" 7ҭ!Ud'%6ѥ5y%aN ڥ%AÇs)PIDhJ?"of ULA [JqBKPlmմNhgB[EZV2ƛf+E סFn=BـƛR2T0AuKzT]Lns}HBR6ˤObAIso 7)(ZUHw/+dЛC /#X4)DvM䑟1/shDeW O6a 視l6b 6jel 6e"SEK*Uqs#֕<'] ߹SCLsUD5Ʃ o@HO6rl!eb-4_Iʼ#Eq$#U=L'[kel fΣMZJKμپ0!(ܼ3ƴ0X h7gu iwVFGhҬ$O EHV#6T@ `V@[Dy[l5b3A@Ha%Edsb  >YOP{+R}t+,uGFr\WW r r mbA@] WDA@C A C" B!h($5aYu!)A^{3uy%'SUZVI|N"3[)+}Q&B}mU[P!^#/)4OsB IDAT9RǦkgwey/0)[LY[ԥpKۮ=,&{* !֖νM-'lZȽi]KFޛ$+"FhX rR.'})K2Q[HݦmR:#3V7җZQD+/:Pzh$}g͠tfS g]MfdOϧ>c)g3k ,,uȤjL4\#5xJȴPEoʋZq+WDT/jhZ >8;ݓ?>*XFqmѡ/yzMϸ:boE}Fq_2A^yW WYާamnF{Q$9EhG Nn1{GJeE!Ccܒw_َ/cҽ *M6nQC~.<5:0n {9bEN`pM;"ĦO=/8># zXcH7v޽1.N{iu?m* ˑ/LXS8|g[ 'S!XOn)$kMkAӄpFטm[L9/veHڽ7y? w׵I+p('cmoc :MyMSX}G9W̄7< },J/9P5jwA~|4qZg>r, cwGCӽy@pf] pR1}"6Ե \Y9_޿xa@._p 'k#]υB>5*Tpa{̠=#:3Q+5uMkGl,ՄV{~<8.NA4qAmr?/t/ff3tһ %B5<@A"23spঐw7wǸSZO3 R35qsAιVNr6ןmۏP`9?MWIWO=9Ux7/wXg'QkGftSvA0)JHˮ =Gp1;֏aۅHc v4Dw]]WHlx55K/H= z=zggv=yݻ]Ẽg0v:rV?:[{SfB=<K*!nyExC"iMUؗ!2Fq*paK%.gDŇrK i s*,E*l_FG*oJ׍dco "|}]7)*$K:xr wLQR?~z'[`@ܪ* 9:\U9lw3fd={miAehLM7ZPt^0gdPH6\kTIJ*$l6Aν7 ,:{jr{5rnxRpܚ8kp_c٢SOMe^X.o_BWǶ.>߶EA b+Z)$sa߁DžE2Q50yfXS jx yJ=K/lk2]OGN2S#*9f܂e8arBO96φ9WxI\}|WӅWi8:QQZPxkkB6Fw<{tf?+yE[wN§?nۘx!dWdV "6n=C]&1 >ن'3u*wlM,<}tK<(! ;[xG@Hn&v' y,=mǗyD.koɞ3FprE`n\{X ^u;H[&ۺu؆XkJ1bxwթIO^-׳UlAI{X)Qd2ZfGwa;'16'_퉶]W4FHı~e[Tņ9rˆ˶zIEx_Y~))K/Vy[»Xϥr'p3FzƽLs?+S=ĂI"&JFX%U5$HCщ..((z0#ny Z&ϘaMr3,Uρ2>~褏BC}ɂ/DEJO͆ضAVFM<=:b )W}P"A}"S\v&!`ꇦra0B+Wx@"k?2ɥ;KJ= &T.DC OG& ?7{ `(xCe)J9QR ӝጇ(*u4 W<#NX-q>wj:a/qO> A:g'EKk/\ܼ})"'l˚;_Fzi6r/z<ڗ!\gѲp[ 0fF͊N(/"|ӰOgxi^ro1ô皏a5N-EX]C鉪#^^吣e_Wa`r _;#T#Ơ8ģYr~aI$/2I3 |.XLgz,&KmxrNM9`ս걤.mjyYÔv'&~ 1dzA@UY}dA̠Bdn]љI1dTUM77u/.G$N\$OFRs:: ;.d&R E@>ՂL6[-2Ar2A@c AMC #H!h,$5ia @\=8)E6 1 HG"  ئ!ԃ rH4 ~;RjT!R R!F#o˃ĽIfP H2,s$X9Z# \z8Ahyeo7h aڕgAB@3n@Byj/b-A@eH @h[ o[E%i6N8IENDB`django-tables2-1.0.7/docs/_static/tutorial.png000066400000000000000000000111701264227116200212450ustar00rootroot00000000000000PNG  IHDRoMgtEXtSoftwareAdobe ImageReadyqe<IDATx]\G=pGUTHA(h @ أbbkHҬDEDAEwpmf/ѹٙ7o޼H=zV@!///sUڃr H>H||> @O:|w MDN:5kVk{MqFע6sT %T#V ߻ݞE8OAv<-MZ; KX2@J%tuu%VPto3n7Gp\c??G~ectemğWLӽ&r繷 OH%̻S55J>@(lc6s&!?^].9!5Rځx#ԖPȓQ7fffL1`~les:W܉IdÚwwU,U 2Lo$ICCzq"*Z3a=wӆ_#Ke̠()D>H aw-'ndߴa֐wO/-Rt-F 5PGXBɺq!΋RB^+H eD 9RUs4=s#+ZnXkc"X.6cl. (LO,b= / dmZA${PU C``CzX Uquumb=iJ]&Pk**NKqQe)~ =mcãXcQG?ڻY2’q\^QI98l)D"W\Z,-W=sF5Ck$yGd%=IWCcҠ}\--.5ٷ98PPx_ hPj uSxCXS´[bMfs$BP ֓+7 8jO3] os;4TTVin..zamm*uldw3CECGJ͔$$$H>r>0w.;=·͵cO$e/~2"3Ecxny972<@l_?kQ_:PKlU on.Tdw:R@ s 0qGB~RFFz' .-48msS=9U?%+7Gy}0í|@t}Kf:z5h lQάI1OeļX1xu؉/OΘw4..iWvJs'L. 7nzzW PC]N>}~g85qI>h^p*߬z`X~(h***8)/aL}6*a S[\2 a:e|NuCXG37LøaO&6vD.9αӭgMɍ6/4./>#ω2ћPK@d+%Pl dD<6y+yVvp['娺jMI`,/x&~ݥYnNWQ7ڨ('LS;d8%`>&{Xu#zԡN%nQWU빐dtɴ~Vs?-Dv݁ekj8陵My*{O |04%!?Mk,E<>x>`*`4&Xe[JCSky)zn9Oo pEO\E4yi쀂 Ɣh2*~ιzaG뛍Xǯ~=&@I8?Ё =Lzh~ ATzdUUUTl(--d?H|| O!o(RӀ#.i`9 }te-3:ЬAU&G'sEsߓf 7R-B n[#KcfymGն'~4T<ήFHm@\~]}rD$[U>4TiL:6--~+HXud侎oBԘ.8EߨMDqQf4Cő`S\80lλLLE)=8!*X]ijK@;U %, +.² q8NǿyvjP})h9rLZ@Uɚۙc@.坰\t+J16(+cm7<`AcwLiIGʿ͐qBInrWc%滑0t (MBqx MV6Q,*r *_};zmɼҌBDtʴ/E\@_ AAATOA&}.v2$H>H>H|| AAA _ A,KnzLDz~[4A(|QY 85mu>E=A3ôҁ_>y>:v+<BS6@sWfg9+H/d+%$35'Qf9I vCFf'6J70ڨy?JNroqp 2E^'8A)|HO#ͫx\] sw }Ba=&r}}UIl>!34]G xE̡΅_cLE=8oWޑ?J[|GHdЉywàYh $S>.Jlٰxtÿ{e{']PTUoz~qߵ죠/kgmjrk€OwrZQl#jUq PUfP3|'mnR \@ eU&D{M0xaIݡL/">dԃ64,DžF~(S R.g?!PCq>7?|RUW!xn)]V:DCjՙam͎[7Б ՘tyaoh\%q"i .~|˖Ua6¸J8éײ}O8֞'BY;lTe2QqOA|8;JJ "| &4h$$$H>H>H| AD-C.K @vi.z5W$>'*/GM_UIENDB`django-tables2-1.0.7/docs/conf.py000066400000000000000000000015371264227116200165530ustar00rootroot00000000000000# coding: utf-8 import os from os.path import abspath, dirname, join import re import sys os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings" # import project sys.path.insert(0, abspath('..')) import example import django_tables2 sys.path.pop(0) project = 'django-tables2' with open('../django_tables2/__init__.py', 'rb') as f: release = re.search('__version__ = "(.+?)"', f.read()).group(1) version = release.rpartition('.')[0] default_role = "py:obj" extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'sphinx.ext.doctest', ] intersphinx_mapping = { 'python': ('http://docs.python.org/dev/', None), 'django': ('http://docs.djangoproject.com/en/dev/', 'http://docs.djangoproject.com/en/dev/_objects/'), } master_doc = 'index' html_theme = 'default' html_static_path = ['_static'] django-tables2-1.0.7/docs/index.rst000066400000000000000000000016721264227116200171150ustar00rootroot00000000000000.. default-domain:: py ================================================ django-tables2 - An app for creating HTML tables ================================================ django-tables2 turns data into HTML tables. Features: - Pagination - Ordering - Extendable - Class based view - Supports for queryset and list data - Themes Report bugs at http://github.com/bradleyayers/django-tables2/issues .. toctree:: :hidden: pages/tutorial pages/table-data pages/accessors pages/order-by-accessors pages/swapping-columns pages/column-headers pages/pagination pages/custom-rendering pages/query-string-fields pages/column-attributes pages/builtin-columns pages/template-tags pages/template-filters pages/generic-mixins pages/table-mixins pages/tables-for-models pages/localization-control pages/api-reference pages/upgrading-from-v1 pages/glossary pages/internal django-tables2-1.0.7/docs/make.bat000066400000000000000000000106571264227116200166640ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-tables2.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-tables2.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end django-tables2-1.0.7/docs/pages/000077500000000000000000000000001264227116200163455ustar00rootroot00000000000000django-tables2-1.0.7/docs/pages/accessors.rst000066400000000000000000000017171264227116200210720ustar00rootroot00000000000000.. _accessors: Specifying alternative data for a column ======================================== Each column has a "key" that describes which value to pull from each record to populate the column's cells. By default, this key is just the name given to the column, but it can be changed to allow foreign key traversal or other complex cases. To reduce ambiguity, rather than calling it a "key", it's been given the special name "accessor". Accessors are just dotted paths that describe how an object should be traversed to reach a specific value. To demonstrate how they work we'll use them directly:: >>> from django_tables2 import A >>> data = {"abc": {"one": {"two": "three"}}} >>> A("abc.one.two").resolve(data) "three" Dots represent a relationships, and are attempted in this order: 1. Dictionary lookup ``a[b]`` 2. Attribute lookup ``a.b`` 3. List index lookup ``a[int(b)]`` Then, if the value is callable, it is called and the result is used. django-tables2-1.0.7/docs/pages/api-reference.rst000066400000000000000000000214571264227116200216150ustar00rootroot00000000000000API Reference ============= `.Accessor` (`.A`) ------------------ .. autoclass:: django_tables2.utils.Accessor `.RequestConfig` ---------------- .. autoclass:: django_tables2.config.RequestConfig `.Table` -------- .. autoclass:: django_tables2.tables.Table :members: paginate, as_html `.Table.Meta` ------------- .. class:: Table.Meta Provides a way to define *global* settings for table, as opposed to defining them for each instance. .. attribute:: attrs Allows custom HTML attributes to be specified which will be added to the ```` tag of any table rendered via :meth:`.Table.as_html` or the :ref:`template-tags.render_table` template tag. :type: `dict` :default: ``{}`` This is typically used to enable a theme for a table (which is done by adding a CSS class to the ``
`` element). i.e.:: class SimpleTable(tables.Table): name = tables.Column() class Meta: attrs = {"class": "paleblue"} .. versionadded:: 0.15.0 It's possible to use callables to create *dynamic* values. A few caveats: - It's not supported for ``dict`` keys, i.e. only values. - All values will be resolved on table instantiation. Consider this example where a unique ``id`` is given to each instance of the table:: import itertools counter = itertools.count() class UniqueIdTable(tables.Table): name = tables.Column() class Meta: attrs = {"id": lambda: "table_%d" % next(counter)} .. note:: This functionality is also available via the ``attrs`` keyword argument to a table's constructor. .. attribute:: empty_text Defines the text to display when the table has no rows. :type: `unicode` :default: `None` If the table is empty and ``bool(empty_text)`` is `True`, a row is displayed containing ``empty_text``. This is allows a message such as *There are currently no FOO.* to be displayed. .. note:: This functionality is also available via the ``empty_text`` keyword argument to a table's constructor. .. attribute:: show_header Defines whether the table header (````) should be displayed or not. :type: `bool` :default: `True` .. note:: This functionality is also available via the ``show_header`` keyword argument to a table's constructor. .. attribute:: exclude Defines which columns should be excluded from the table. This is useful in subclasses to exclude columns in a parent. :type: tuple of `unicode` :default: ``()`` Example:: >>> class Person(tables.Table): ... first_name = tables.Column() ... last_name = tables.Column() ... >>> Person.base_columns {'first_name': , 'last_name': } >>> class ForgetfulPerson(Person): ... class Meta: ... exclude = ("last_name", ) ... >>> ForgetfulPerson.base_columns {'first_name': } .. note:: This functionality is also available via the ``exclude`` keyword argument to a table's constructor. However, unlike some of the other `.Table.Meta` options, providing the ``exclude`` keyword to a table's constructor **won't override** the `.Meta.exclude`. Instead, it will be effectively be *added* to it. i.e. you can't use the constructor's ``exclude`` argument to *undo* an exclusion. .. attribute:: fields Used in conjunction with `~.Table.Meta.model`, specifies which fields should have columns in the table. :type: tuple of `unicode` or `None` :default: `None` If `None`, all fields are used, otherwise only those named. Example:: # models.py class Person(models.Model): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200) # tables.py class PersonTable(tables.Table): class Meta: model = Person fields = ("first_name", ) .. attribute:: model A model to inspect and automatically create corresponding columns. :type: Django model :default: `None` This option allows a Django model to be specified to cause the table to automatically generate columns that correspond to the fields in a model. .. attribute:: order_by The default ordering. e.g. ``('name', '-age')``. A hyphen ``-`` can be used to prefix a column name to indicate *descending* order. :type: `tuple` :default: ``()`` .. note:: This functionality is also available via the ``order_by`` keyword argument to a table's constructor. .. attribute:: sequence The sequence of the table columns. This allows the default order of columns (the order they were defined in the Table) to be overridden. :type: any iterable (e.g. `tuple` or `list`) :default: ``()`` The special item ``"..."`` can be used as a placeholder that will be replaced with all the columns that weren't explicitly listed. This allows you to add columns to the front or back when using inheritence. Example:: >>> class Person(tables.Table): ... first_name = tables.Column() ... last_name = tables.Column() ... ... class Meta: ... sequence = ("last_name", "...") ... >>> Person.base_columns.keys() ['last_name', 'first_name'] The ``"..."`` item can be used at most once in the sequence value. If it's not used, every column *must* be explicitly included. e.g. in the above example, ``sequence = ("last_name", )`` would be **invalid** because neither ``"..."`` or ``"first_name"`` were included. .. note:: This functionality is also available via the ``sequence`` keyword argument to a table's constructor. .. attribute:: orderable Default value for column's *orderable* attribute. :type: `bool` :default: `True` If the table and column don't specify a value, a column's ``orderable`` value will fallback to this. object specify. This provides an easy mechanism to disable ordering on an entire table, without adding ``orderable=False`` to each column in a table. .. note:: This functionality is also available via the ``orderable`` keyword argument to a table's constructor. .. attribute:: template The default template to use when rendering the table. :type: `unicode` :default: ``"django_tables2/table.html"`` .. note:: This functionality is also available via the *template* keyword argument to a table's constructor. .. attribute:: localize Specifies which fields should be localized in the table. Read :ref:`localization-control` for more information. :type: tuple of `unicode` :default: empty tuple .. attribute:: unlocalize Specifies which fields should be unlocalized in the table. Read :ref:`localization-control` for more information. :type: tuple of `unicode` :default: empty tuple `.BooleanColumn` ---------------- .. autoclass:: django_tables2.columns.BooleanColumn `.Column` --------- .. autoclass:: django_tables2.columns.Column `.CheckBoxColumn` ----------------- .. autoclass:: django_tables2.columns.CheckBoxColumn :members: `.DateColumn` ------------- .. autoclass:: django_tables2.columns.DateColumn :members: `.DateTimeColumn` ----------------- .. autoclass:: django_tables2.columns.DateTimeColumn :members: `.EmailColumn` -------------- .. autoclass:: django_tables2.columns.EmailColumn :members: `.FileColumn` ------------- .. autoclass:: django_tables2.columns.FileColumn :members: `.LinkColumn` ------------- .. autoclass:: django_tables2.columns.LinkColumn :members: `.TemplateColumn` ----------------- .. autoclass:: django_tables2.columns.TemplateColumn :members: `.URLColumn` ------------ .. autoclass:: django_tables2.columns.URLColumn :members: See :doc:`internal` for internal classes. django-tables2-1.0.7/docs/pages/builtin-columns.rst000066400000000000000000000011641264227116200222250ustar00rootroot00000000000000.. _builtin-columns: Built-in columns ================ For common use-cases the following columns are included: - `.BooleanColumn` -- renders boolean values - `.Column` -- generic column - `.CheckBoxColumn` -- renders checkbox form inputs - `.DateColumn` -- date formatting - `.DateTimeColumn` -- datetime formatting in the local timezone - `.FileColumn` -- renders files as links - `.EmailColumn` -- renders ```` tags - `.LinkColumn` -- renders ```` tags (compose a django url) - `.TemplateColumn` -- renders template code - `.URLColumn` -- renders ```` tags (absolute url) django-tables2-1.0.7/docs/pages/column-attributes.rst000066400000000000000000000015671264227116200225710ustar00rootroot00000000000000.. _column-attributes: Column attributes ================= Column attributes can be specified using the `dict` with specific keys. The dict defines HTML attributes for one of more elements within the column. Depending on the column, different elements are supported, however ``th``, ``td``, and ``cell`` are supported universally. e.g. .. sourcecode:: python >>> import django_tables2 as tables >>> >>> class SimpleTable(tables.Table): ... name = tables.Column(attrs={"th": {"id": "foo"}}) ... >>> SimpleTable(data).as_html() "{snip}
{snip}
{snip}" ``th`` and ``td`` are special cases because they're extended during rendering to add the column name as a class. This is done to make writing CSS easier. Have a look at each column's API reference to find which elements are supported. django-tables2-1.0.7/docs/pages/column-headers.rst000066400000000000000000000026171264227116200220130ustar00rootroot00000000000000.. _column-headers: Customising column headings =========================== The header cell for each column comes from `~.Column.header`. By default this method returns `~.Column.verbose_name`, falling back to the titlised attribute name of the column in the table class. When using queryset data and a verbose name hasn't been explicitly defined for a column, the corresponding model field's verbose name will be used. Consider the following: >>> class Person(models.Model): ... first_name = models.CharField(verbose_name='model verbose name', max_length=200) ... last_name = models.CharField(max_length=200) ... region = models.ForeignKey('Region') ... >>> class Region(models.Model): ... name = models.CharField(max_length=200) ... >>> class PersonTable(tables.Table): ... first_name = tables.Column() ... ln = tables.Column(accessor='last_name') ... region_name = tables.Column(accessor='region.name') ... >>> table = PersonTable(Person.objects.all()) >>> table.columns['first_name'].header u'Model Verbose Name' >>> table.columns['ln'].header u'Last Name' >>> table.columns['region_name'].header u'Name' As you can see in the last example (region name), the results are not always desirable when an accessor is used to cross relationships. To get around this be careful to define `.Column.verbose_name`. django-tables2-1.0.7/docs/pages/custom-rendering.rst000066400000000000000000000106171264227116200223710ustar00rootroot00000000000000.. _custom-rendering: Custom rendering ================ Various options are available for changing the way the table is :term:`rendered `. Each approach has a different balance of ease-of-use and flexibility. .. _table.render_foo: :meth:`Table.render_FOO` methods -------------------------------- To change how a column is rendered, implement a ``render_FOO`` method on the table (where ``FOO`` is the :term:`column name`). This approach is suitable if you have a one-off change that you don't want to use in multiple tables. Supported keyword arguments include: - ``record`` -- the entire record for the row from the :term:`table data` - ``value`` -- the value for the cell retrieved from the :term:`table data` - ``column`` -- the `.Column` object - ``bound_column`` -- the `.BoundColumn` object - ``bound_row`` -- the `.BoundRow` object - ``table`` -- alias for ``self`` Here's an example where the first column displays the current row number:: >>> import django_tables2 as tables >>> import itertools >>> class SimpleTable(tables.Table): ... row_number = tables.Column(empty_values=()) ... id = tables.Column() ... age = tables.Column() ... ... def __init__(self, *args, **kwargs): ... super(SimpleTable, self).__init__(*args, **kwargs) ... self.counter = itertools.count() ... ... def render_row_number(self): ... return 'Row %d' % next(self.counter) ... ... def render_id(self, value): ... return '<%s>' % value ... >>> table = SimpleTable([{'age': 31, 'id': 10}, {'age': 34, 'id': 11}]) >>> for cell in table.rows[0]: ... print cell ... Row 0 <10> 31 Python's `inspect.getargspec` is used to only pass the arguments declared by the function. This means it's not necessary to add a catch all (``**``) keyword argument. .. important:: `render` methods are *only* called if the value for a cell is determined to be not an :term:`empty value`. When a value is in `.Column.empty_values`, a default value is rendered instead (both `.Column.render` and ``Table.render_FOO`` are skipped). .. _subclassing-column: Subclassing `.Column` --------------------- Defining a column subclass allows functionality to be reused across tables. Columns have a `render` method that behaves the same as :ref:`table.render_foo` methods on tables:: >>> import django_tables2 as tables >>> >>> class UpperColumn(tables.Column): ... def render(self, value): ... return value.upper() ... >>> class Example(tables.Table): ... normal = tables.Column() ... upper = UpperColumn() ... >>> data = [{'normal': 'Hi there!', ... 'upper': 'Hi there!'}] ... >>> table = Example(data) >>> table.as_html() u'
NormalUpper
Hi there!HI THERE!
\n' See :ref:`table.render_foo` for a list of arguments that can be accepted. For complicated columns, you may want to return HTML from the :meth:`~Column.render` method. This is fine, but be sure to mark the string as safe to avoid it being escaped:: >>> from django.utils.safestring import mark_safe >>> from django.utils.html import escape >>> >>> class ImageColumn(tables.Column): ... def render(self, value): ... return mark_safe('' ... % escape(value)) ... .. _css: CSS --- In order to use CSS to style a table, you'll probably want to add a ``class`` or ``id`` attribute to the ```` element. django-tables2 has a hook that allows abitrary attributes to be added to the ``
`` tag. .. sourcecode:: python >>> import django_tables2 as tables >>> class SimpleTable(tables.Table): ... id = tables.Column() ... age = tables.Column() ... ... class Meta: ... attrs = {'class': 'mytable'} ... >>> table = SimpleTable() >>> table.as_html() '
...' .. _custom-template: Custom Template --------------- And of course if you want full control over the way the table is rendered, ignore the built-in generation tools, and instead pass an instance of your `.Table` subclass into your own template, and render it yourself. Have a look at the ``django_tables2/table.html`` template for an example. django-tables2-1.0.7/docs/pages/generic-mixins.rst000066400000000000000000000034731264227116200220270ustar00rootroot00000000000000Class Based Generic Mixins ========================== Django 1.3 introduced `class based views`__ as a mechanism to reduce the repetition in view code. django-tables2 comes with a single class based view mixin: `.SingleTableMixin`. It makes it trivial to incorporate a table into a view/template. The following view parameters are supported: - ``table_class`` –- the table class to use, e.g. ``SimpleTable`` - ``table_data`` (or ``get_table_data()``) -- the data used to populate the table - ``context_table_name`` -- the name of template variable containing the table object - ``table_pagination`` (or ``get_table_pagination``) -- pagination options to pass to `.RequestConfig`. Set ``table_pagination=False`` to disable pagination. .. __: https://docs.djangoproject.com/en/1.3/topics/class-based-views/ For example: .. sourcecode:: python from django_tables2 import SingleTableView class Person(models.Model): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200) class PersonTable(tables.Table): class Meta: model = Simple class PersonList(SingleTableView): model = Person table_class = PersonTable The template could then be as simple as: .. sourcecode:: django {% load render_table from django_tables2 %} {% render_table table %} Such little code is possible due to the example above taking advantage of default values and `.SimpleTableMixin`'s eagarness at finding data sources when one isn't explicitly defined. .. note:: If you need more than one table on a page, use `.SingleTableView` and use `.get_context_data` to initialise the other tables and add them to the context. .. note:: You don't have to base your view on `ListView`, you're able to mix `SingleTableMixin` directly. django-tables2-1.0.7/docs/pages/glossary.rst000066400000000000000000000031231264227116200207410ustar00rootroot00000000000000Glossary ======== .. glossary:: accessor Refers to an `.Accessor` object column name The name given to a column. In the follow example, the *column name* is ``age``. .. sourcecode:: python class SimpleTable(tables.Table): age = tables.Column() empty value An empty value is synonymous with "no value". Columns have an ``empty_values`` attribute that contains values that are considered empty. It's a way to declare which values from the database correspond to *null*/*blank*/*missing* etc. order by alias A prefixed column name that describes how a column should impact the order of data within the table. This allows the implementation of how a column affects ordering to be abstracted, which is useful (e.g. in querystrings). .. sourcecode:: python class ExampleTable(tables.Table): name = tables.Column(order_by=('first_name', 'last_name')) In this example ``-name`` and ``name`` are valid order by aliases. In a querystring you might then have ``?order=-name``. table The traditional concept of a table. i.e. a grid of rows and columns containing data. view A Django view. record A single Python object used as the data for a single row. render The act of serialising a `.Table` into HTML. template A Django template. table data An interable of :term:`records ` that `.Table` uses to populate its rows. django-tables2-1.0.7/docs/pages/internal.rst000066400000000000000000000014611264227116200207150ustar00rootroot00000000000000============= Internal APIs ============= The items documented here are internal and subject to change. It exists here for convenience. `.BoundColumns` --------------- .. autoclass:: django_tables2.columns.BoundColumns :members: :private-members: :special-members: `.BoundColumn` -------------- .. autoclass:: django_tables2.columns.BoundColumn :members: :private-members: :special-members: `.BoundRows` ------------ .. autoclass:: django_tables2.rows.BoundRows :members: :private-members: :special-members: `.BoundRow` ----------- .. autoclass:: django_tables2.rows.BoundRow :members: :private-members: :special-members: `.TableData` ------------ .. autoclass:: django_tables2.tables.TableData :members: :private-members: :special-members: django-tables2-1.0.7/docs/pages/localization-control.rst000066400000000000000000000036371264227116200232560ustar00rootroot00000000000000.. _localization-control: Controlling localization ======================== .. note:: This functionality doesn't work in Django prior to version 1.3 Django_tables2 allows you to define which column of a table should or should not be localized. For example you may want to use this feature in following use cases: * You want to format some columns representing for example numeric values in the given locales even if you don't enable `USE_L10N` in your settings file. * You don't want to format primary key values in your table even if you enabled `USE_L10N` in your settings file. This control is done by using two filter functions in Django's `l10n` library named `localize` and `unlocalize`. Check out Django docs about :ref:`localization ` for more information about them. There are two ways of controling localization in your columns. First one is setting the `~.Column.localize` attribute in your column definition to `True` or `False`. Like so:: class PersonTable(tables.Table): id = tables.Column(name="id", accessor="pk", localize=False) class Meta: model = Person .. note:: The default value of the `localize` attribute is `None` which means the formatting of columns is dependant from the `USE_L10N` setting. The second way is to define a `~.Table.Meta.localize` and/or `~.Table.Meta.unlocalize` tuples in your tables Meta class (jutst like with `~.Table.Meta.fields` or `~.Table.Meta.exclude`). You can do this like so:: class PersonTable(tables.Table): id = tables.Column(accessor="pk") value = tables.Column(accessor="some_numerical_field") class Meta: model = Person unlocalize = ('id',) localize = ('value',) If you define the same column in both `localize` and `unlocalize` then the value of this column will be "unlocalized" which means that `unlocalize` has higher precedence. django-tables2-1.0.7/docs/pages/order-by-accessors.rst000066400000000000000000000026541264227116200226140ustar00rootroot00000000000000.. _order-by-accessors: Specifying alternative ordering for a column ============================================ When using queryset data, it's possible for a column to present a computed value that doesn't correspond to a column in the database. In this situation attempting to order the column will cause a database exception. Example:: # models.py class Person(models.Model): first_name = models.CharField(max_length=200) family_name = models.CharField(max_length=200) @property def name(self): return u"%s %s" % (self.first_name, self.family_name) # tables.py class PersonTable(tables.Table): name = tables.Column() :: >>> table = PersonTable(Person.objects.all()) >>> table.order_by = "name" >>> table.as_html() ... FieldError: Cannot resolve keyword u'name' into field. Choices are: first_name, family_name The solution is to declare which fields should be used when ordering on via the ``order_by`` argument:: # tables.py class PersonTable(tables.Table): name = tables.Column(order_by=("first_name", "family_name")) Accessor syntax can be used for the values, but they must terminate on a model field. If ordering doesn't make sense for a particular column, it can be disabled via the ``orderable`` argument:: class SimpleTable(tables.Table): name = tables.Column() actions = tables.Column(orderable=False) django-tables2-1.0.7/docs/pages/pagination.rst000066400000000000000000000013251264227116200212310ustar00rootroot00000000000000.. _pagination: Pagination ========== Pagination is easy, just call :meth:`.Table.paginate` and pass in the current page number, e.g. .. sourcecode:: python def people_listing(request): table = PeopleTable(Person.objects.all()) table.paginate(page=request.GET.get('page', 1), per_page=25) return render(request, 'people_listing.html', {'table': table}) If you're using `.RequestConfig`, pass pagination options to the constructor, e.g.: .. sourcecode:: python def people_listing(request): table = PeopleTable(Person.objects.all()) RequestConfig(request, paginate={"per_page": 25}).configure(table) return render(request, 'people_listing.html', {'table': table}) django-tables2-1.0.7/docs/pages/query-string-fields.rst000066400000000000000000000021361264227116200230160ustar00rootroot00000000000000.. _query-string-fields: Querystring fields ================== Tables pass data via the querystring to indicate ordering and pagination preferences. The names of the querystring variables are configurable via the options: - ``order_by_field`` -- default: ``"sort"`` - ``page_field`` -- default: ``"page"`` - ``per_page_field`` -- default: ``"per_page"``, **note:** this field currently isn't used by ``{% render_table %}`` Each of these can be specified in three places: - ``Table.Meta.foo`` - ``Table(..., foo=...)`` - ``Table(...).foo = ...`` If you're using multiple tables on a single page, you'll want to prefix these fields with a table-specific name. e.g. .. sourcecode:: python def people_listing(request): config = RequestConfig(request) table1 = PeopleTable(Person.objects.all(), prefix="1-") # prefix specified table2 = PeopleTable(Person.objects.all(), prefix="2-") # prefix specified config.configure(table1) config.configure(table2) return render(request, "people_listing.html", {"table1": table1, "table2": table2}) django-tables2-1.0.7/docs/pages/swapping-columns.rst000066400000000000000000000014101264227116200224010ustar00rootroot00000000000000.. _swapping-columns: Swapping the position of columns ================================ By default columns are positioned in the same order as they are declared, however when mixing auto-generated columns (via `Table.Meta.model`) with manually declared columns, the column sequence becomes ambiguous. To resolve the ambiguity, columns sequence can be declared via the `.Table.Meta.sequence` option:: class PersonTable(tables.Table): selection = tables.CheckBoxColumn(accessor="pk", orderable=False) class Meta: model = Person sequence = ("selection", "first_name", "last_name") The special value ``"..."`` can be used to indicate that any omitted columns should inserted at that location. As such it can be used at most once. django-tables2-1.0.7/docs/pages/table-data.rst000066400000000000000000000024471264227116200211040ustar00rootroot00000000000000.. _table-data: Populating a table with data ============================ Tables are compatible with a range of input data structures. If you've seen the tutorial you'll have seen a queryset being used, however any iterable that supports :func:`len` and contains items that expose key-based accessed to column values is fine. An an example we'll demonstrate using list of dicts. When defining a table it's necessary to declare each column. If your data matches the fields in a model, columns can be declared automatically for you via the `Table.Meta.model` option, but for non-queryset data you'll probably want to declare them manually:: import django_tables2 as tables data = [ {"name": "Bradley"}, {"name": "Stevie"}, ] class NameTable(tables.Table): name = tables.Column() table = NameTable(data) You can use this technique to override columns that were automatically created via `Table.Meta.model` too:: # models.py from django.db import models class Person(models.Model): name = models.CharField(max_length=200) # tables.py import django_tables2 as tables from .models import Person class PersonTable(tables.Table): name = tables.Column(verbose_name="full name") class Meta: model = Person django-tables2-1.0.7/docs/pages/table-mixins.rst000066400000000000000000000016121264227116200214730ustar00rootroot00000000000000Table Mixins ============ It's possible to create a mixin for a table that overrides something, however unless it itself is a subclass of `.Table` class variable instances of `.Column` will **not** be added to the class which is using the mixin. Example:: >>> class UselessMixin(object): ... extra = tables.Column() ... >>> class TestTable(UselessMixin, tables.Table): ... name = tables.Column() ... >>> TestTable.base_columns.keys() ['name'] To have a mixin contribute a column, it needs to be a subclass of `~django_tables2.tables.Table`. With this in mind the previous example *should* have been written as follows:: >>> class UsefulMixin(tables.Table): ... extra = tables.Column() ... >>> class TestTable(UsefulMixin, tables.Table): ... name = tables.Column() ... >>> TestTable.base_columns.keys() ['extra', 'name'] django-tables2-1.0.7/docs/pages/tables-for-models.rst000066400000000000000000000020321264227116200224130ustar00rootroot00000000000000.. _tables-for-models: Tables for models ================= If you build use tables to display `.QuerySet` data, rather than defining each column manually in the table, the `.Table.Meta.model` option allows tables to be dynamically created based on a model:: # models.py class Person(models.Model): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200) user = models.ForeignKey("auth.User") dob = models.DateField() # tables.py class PersonTable(tables.Table): class Meta: model = Person This has a number of benefits: - Less code, easier to write, more DRY - Columns use the field's `~.models.Field.verbose_name` - Specialised columns are used where possible (e.g. `.DateColumn` for a `~.models.DateField`) When using this approach, the following options are useful: - `~.Table.Meta.sequence` -- reorder columns - `~.Table.Meta.fields` -- specify model fields to *include* - `~.Table.Meta.exclude` -- specify model fields to *exclude* django-tables2-1.0.7/docs/pages/template-filters.rst000066400000000000000000000007261264227116200223650ustar00rootroot00000000000000Template filters ================ title ----- String filter that performs title case conversion on a per-word basis, leaving words containing upper-case letters alone. .. sourcecode:: django {{ "start 6PM"|title }} # Start 6PM {{ "sTart 6pm"|title }} # sTart 6pm .. warning:: Be careful when loading the ``django_tables2`` template library to not in advertantly load ``title``. You should always use the ``{% load ... from ... %}`` syntax. django-tables2-1.0.7/docs/pages/template-tags.rst000066400000000000000000000042641264227116200216540ustar00rootroot00000000000000.. _template_tags: Template tags ============= .. _template-tags.render_table: render_table ------------ Renders a `~django_tables2.tables.Table` object to HTML and enables as many features in the output as possible. .. sourcecode:: django {% load django_tables2 %} {% render_table table %} {# Alternatively a specific template can be used #} {% render_table table "path/to/custom_table_template.html" %} If the second argument (template path) is given, the template will be rendered with a `.RequestContext` and the table will be in the variable ``table``. .. note:: This tag temporarily modifies the `.Table` object during rendering. A ``context`` attribute is added to the table, providing columns with access to the current context for their own rendering (e.g. `.TemplateColumn`). This tag requires that the template in which it's rendered contains the `~.http.HttpRequest` inside a ``request`` variable. This can be achieved by ensuring the ``TEMPLATE_CONTEXT_PROCESSORS`` setting contains ``"django.core.context_processors.request"``. By default it is not included, and the setting itself is not even defined within your project's ``settings.py``. To resolve this add the following to your ``settings.py``: .. sourcecode:: python from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS TEMPLATE_CONTEXT_PROCESSORS += ('django.core.context_processors.request',) .. _template-tags.querystring: querystring ----------- A utility that allows you to update a portion of the query-string without overwriting the entire thing. Let's assume we have the querystring ``?search=pirates&sort=name&page=5`` and we want to update the ``sort`` parameter: .. sourcecode:: django {% querystring "sort"="dob" %} # ?search=pirates&sort=dob&page=5 {% querystring "sort"="" %} # ?search=pirates&page=5 {% querystring "sort"="" "search"="" %} # ?page=5 {% with "search" as key %} # supports variables as keys {% querystring key="robots" %} # ?search=robots&page=5 {% endwith %} This tag requires the ``django.core.context_processors.request`` context processor, see :ref:`template-tags.render_table`. django-tables2-1.0.7/docs/pages/tutorial.rst000066400000000000000000000057371264227116200207560ustar00rootroot00000000000000Tutorial ======== 1. ``pip install django-tables2`` 2. Add ``'django_tables2'`` to ``INSTALLED_APPS`` 3. Add ``'django.core.context_processors.request'`` to ``TEMPLATE_CONTEXT_PROCESSORS`` We're going to run through creating a tutorial app. Let's start with a simple model:: # tutorial/models.py class Person(models.Model): name = models.CharField(verbose_name="full name") Add some data so you have something to display in the table. Now write a view to pass a ``Person`` queryset into a template:: # tutorial/views.py from django.shortcuts import render def people(request): return render(request, "people.html", {"people": Person.objects.all()}) Finally, implement the template: .. sourcecode:: django {# tutorial/templates/people.html #} {% load render_table from django_tables2 %} {% render_table people %} Hook the view up in your URLs, and load the page, you should see: .. figure:: /_static/tutorial.png :align: center :alt: An example table rendered using django-tables2 While simple, passing a queryset directly to ``{% render_table %}`` doesn't allow for any customisation. For that, you must define a `.Table` class. :: # tutorial/tables.py import django_tables2 as tables from tutorial.models import Person class PersonTable(tables.Table): class Meta: model = Person # add class="paleblue" to
tag attrs = {"class": "paleblue"} You'll then need to instantiate and configure the table in the view, before adding it to the context. :: # tutorial/views.py from django.shortcuts import render from django_tables2 import RequestConfig from tutorial.models import Person from tutorial.tables import PersonTable def people(request): table = PersonTable(Person.objects.all()) RequestConfig(request).configure(table) return render(request, 'people.html', {'table': table}) Using `.RequestConfig` automatically pulls values from ``request.GET`` and updates the table accordingly. This enables data ordering and pagination. Rather than passing a queryset to ``{% render_table %}``, instead pass the table. .. sourcecode:: django {% render_table table %} .. note:: ``{% render_table %}`` works best when it's used in a template that contains the current request in the context as ``request``. The easiest way to enable this, is to ensure that the ``TEMPLATE_CONTEXT_PROCESSORS`` setting contains ``"django.core.context_processors.request"``. At this point you haven't actually customised anything, you've merely added the boilerplate code that ``{% render_table %}`` does for you when given a queryset. The remaining sections in this document describe how to change various aspects of the table. django-tables2-1.0.7/docs/pages/upgrading-from-v1.rst000066400000000000000000000032361264227116200223500ustar00rootroot00000000000000Upgrading from django-tables Version 1 ====================================== - Change your ``INSTALLLED_APPS`` entry from ``"django_tables.app"`` to ``"django_tables2"``. - Change all your import references from ``django_tables`` to ``django_tables2``. - Replace all references to the old ``MemoryTable`` and ``ModelTable`` classes with simply ``Table``. - In your templates, load the ``django_tables2`` template library; ``{% load django_tables2 %}`` instead of ``{% load tables %}``. - A table object is no longer iterable; rather than ``for row in table``, instead you now do explicitly: ``for row in table.rows``. - If you were using ``row.data`` to access a row's underlying data, replace it with ``row.record`` instead. - When declaring columns, replace the use of:: name_in_dataset = tables.Column(name="wanted_column_name") with:: wanted_column_name = tables.Column(accessor="name_in_dataset") - When declaring columns, replace the use of:: column_to_override = tables.Column(name="wanted_column_name", data="name_in_dataset") with:: wanted_column_name = tables.Column(accessor="name_in_dataset") and exclude ``column_to_override`` via the table meta data. - When generating the link to order the column, instead of: .. sourcecode:: django {% set_url_param sort=column.name_toggled %} use: .. sourcecode:: django {% querystring table.order_by_field=column.order_by_alias.next %} - Replace: .. sourcecode:: django {{ column.is_ordered_reverse }} and {{ column.is_ordered_straight }} with: .. sourcecode:: django {{ column.order_by.is_descending }} and {{ column.order_by.is_ascending }} django-tables2-1.0.7/example/000077500000000000000000000000001264227116200157515ustar00rootroot00000000000000django-tables2-1.0.7/example/README.rst000066400000000000000000000003701264227116200174400ustar00rootroot00000000000000The example project only supports the latest version of Django. To get it up and running: pip install -r requirements.pip python manage.py migrate python manage.py loaddata app/fixtures/initial_data.json python manage.py runserver django-tables2-1.0.7/example/__init__.py000066400000000000000000000000001264227116200200500ustar00rootroot00000000000000django-tables2-1.0.7/example/app/000077500000000000000000000000001264227116200165315ustar00rootroot00000000000000django-tables2-1.0.7/example/app/__init__.py000066400000000000000000000000001264227116200206300ustar00rootroot00000000000000django-tables2-1.0.7/example/app/admin.py000066400000000000000000000002661264227116200201770ustar00rootroot00000000000000# coding: utf-8 from django.contrib import admin from .models import Country class CountryAdmin(admin.ModelAdmin): list_per_page = 2 admin.site.register(Country, CountryAdmin) django-tables2-1.0.7/example/app/fixtures/000077500000000000000000000000001264227116200204025ustar00rootroot00000000000000django-tables2-1.0.7/example/app/fixtures/initial_data.json000066400000000000000000000020521264227116200237160ustar00rootroot00000000000000[ { "pk": 1, "model": "app.country", "fields": { "tz": "Australia/Brisbane", "name": "Australia", "visits": 2, "population": 20000000, "flag": "country/flags/australia.svg" } }, { "pk": 2, "model": "app.country", "fields": { "tz": "NZST", "name": "New Zealand", "visits": 1, "population": 12000000, "flag": "country/flags/new_zealand.svg" } }, { "pk": 3, "model": "app.country", "fields": { "tz": "CAT", "name": "Africa", "visits": 0, "population": 1000010000, "flag": "country/flags/africa.svg" } }, { "pk": 4, "model": "app.country", "fields": { "tz": "UTC\u22123.5", "name": "Canada", "visits": 1, "population": 34447000, "flag": "country/flags/canada.svg" } } ] django-tables2-1.0.7/example/app/migrations/000077500000000000000000000000001264227116200207055ustar00rootroot00000000000000django-tables2-1.0.7/example/app/migrations/0001_initial.py000066400000000000000000000025321264227116200233520ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.9 on 2015-12-29 04:12 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='Country', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100)), ('population', models.PositiveIntegerField(verbose_name='poblaci\xf3n')), ('tz', models.CharField(max_length=50)), ('visits', models.PositiveIntegerField()), ('commonwealth', models.NullBooleanField()), ('flag', models.FileField(upload_to='country/flags/')), ], options={ 'verbose_name_plural': 'countries', }, ), migrations.CreateModel( name='Person', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=200, verbose_name='full name')), ], options={ 'verbose_name_plural': 'people', }, ), ] django-tables2-1.0.7/example/app/migrations/__init__.py000066400000000000000000000000001264227116200230040ustar00rootroot00000000000000django-tables2-1.0.7/example/app/models.py000066400000000000000000000016541264227116200203740ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ class Country(models.Model): """Represents a geographical Country""" name = models.CharField(max_length=100) population = models.PositiveIntegerField(verbose_name="población") tz = models.CharField(max_length=50) visits = models.PositiveIntegerField() commonwealth = models.NullBooleanField() flag = models.FileField(upload_to="country/flags/") class Meta: verbose_name_plural = _("countries") def __unicode__(self): return self.name @property def summary(self): return "%s (pop. %s)" % (self.name, self.population) class Person(models.Model): name = models.CharField(max_length=200, verbose_name="full name") class Meta: verbose_name_plural = "people" def __unicode__(self): return self.name django-tables2-1.0.7/example/app/tables.py000066400000000000000000000007001264227116200203520ustar00rootroot00000000000000# coding: utf-8 import django_tables2 as tables from .models import Country class CountryTable(tables.Table): name = tables.Column() population = tables.Column() tz = tables.Column(verbose_name='time zone') visits = tables.Column() summary = tables.Column(order_by=("name", "population")) class Meta: model = Country class ThemedCountryTable(CountryTable): class Meta: attrs = {'class': 'paleblue'} django-tables2-1.0.7/example/app/tests.py000066400000000000000000000006171264227116200202510ustar00rootroot00000000000000# coding: utf-8 """ This file demonstrates writing tests using the unittest module. These will pass when you run "manage.py test". Replace this with more appropriate tests for your application. """ from django.test import TestCase class SimpleTest(TestCase): def test_basic_addition(self): """ Tests that 1 + 1 always equals 2. """ self.assertEqual(1 + 1, 2) django-tables2-1.0.7/example/app/views.py000066400000000000000000000025671264227116200202520ustar00rootroot00000000000000# coding: utf-8 from django.shortcuts import render from django_tables2 import RequestConfig, SingleTableView from .tables import CountryTable, ThemedCountryTable from .models import Country, Person def multiple(request): qs = Country.objects.all() example1 = CountryTable(qs, prefix="1-") RequestConfig(request, paginate=False).configure(example1) example2 = CountryTable(qs, prefix="2-") RequestConfig(request, paginate={"per_page": 2}).configure(example2) example3 = ThemedCountryTable(qs, prefix="3-") RequestConfig(request, paginate={"per_page": 3}).configure(example3) example4 = ThemedCountryTable(qs, prefix="4-") RequestConfig(request, paginate={"per_page": 3}).configure(example4) example5 = ThemedCountryTable(qs, prefix="5-") example5.template = "extended_table.html" RequestConfig(request, paginate={"per_page": 3}).configure(example5) return render(request, 'multiple.html', { 'example1': example1, 'example2': example2, 'example3': example3, 'example4': example4, 'example5': example5, }) class ClassBased(SingleTableView): table_class = ThemedCountryTable queryset = Country.objects.all() template_name = "class_based.html" class_based = ClassBased.as_view() def tutorial(request): return render(request, "tutorial.html", {"people": Person.objects.all()}) django-tables2-1.0.7/example/manage.py000077500000000000000000000003621264227116200175570ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-tables2-1.0.7/example/media/000077500000000000000000000000001264227116200170305ustar00rootroot00000000000000django-tables2-1.0.7/example/media/country/000077500000000000000000000000001264227116200205335ustar00rootroot00000000000000django-tables2-1.0.7/example/media/country/flags/000077500000000000000000000000001264227116200216275ustar00rootroot00000000000000django-tables2-1.0.7/example/media/country/flags/australia.svg000066400000000000000000000045051264227116200243410ustar00rootroot00000000000000 django-tables2-1.0.7/example/media/country/flags/canada.svg000066400000000000000000000026171264227116200235650ustar00rootroot00000000000000django-tables2-1.0.7/example/media/country/flags/new_zealand.svg000066400000000000000000000031741264227116200246440ustar00rootroot00000000000000 Flag of New Zealand django-tables2-1.0.7/example/requirements.pip000066400000000000000000000000331264227116200212020ustar00rootroot00000000000000-e .. django-debug-toolbar django-tables2-1.0.7/example/settings.py000066400000000000000000000127441264227116200201730ustar00rootroot00000000000000# coding: utf-8 # import django_tables2 import sys from os.path import abspath, dirname, join from django import VERSION ROOT = dirname(abspath(__file__)) DEBUG = True ADMINS = ( # ('Your Name', 'your_email@example.com'), ) MANAGERS = ADMINS DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 'NAME': join(ROOT, 'database.sqlite'), # Or path to database file if using sqlite3. 'USER': '', # Not used with sqlite3. 'PASSWORD': '', # Not used with sqlite3. 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 'PORT': '', # Set to empty string for default. Not used with sqlite3. } } # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. # On Unix systems, a value of None will cause Django to use the same # timezone as the operating system. # If running in a Windows environment this must be set to the same as your # system time zone. TIME_ZONE = 'America/Chicago' # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' SITE_ID = 1 # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = True # If you set this to False, Django will not format dates, numbers and # calendars according to the current locale USE_L10N = True # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/home/media/media.lawrence.com/media/" MEDIA_ROOT = join(ROOT, 'media') # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash. # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" MEDIA_URL = '/media/' # Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files # in apps' "static/" subdirectories and in STATICFILES_DIRS. # Example: "/home/media/media.lawrence.com/static/" STATIC_ROOT = '' # URL prefix for static files. # Example: "http://media.lawrence.com/static/" STATIC_URL = '/static/' # URL prefix for admin static files -- CSS, JavaScript and images. # Make sure to use a trailing slash. # Examples: "http://foo.com/static/admin/", "/static/admin/". ADMIN_MEDIA_PREFIX = '/static/admin/' # Additional locations of static files STATICFILES_DIRS = ( # Put strings here, like "/home/html/static" or "C:/www/django/static". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. ) # List of finder classes that know how to find static files in # various locations. STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 'django.contrib.staticfiles.finders.DefaultStorageFinder', ) # Make this unique, and don't share it with anybody. SECRET_KEY = '=nzw@mkqk)tz+_#vf%li&8sn7yn8z7!2-4njuyf1rxs*^muhvh' if VERSION < (1, 8): TEMPLATE_DEBUG = DEBUG # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ) TEMPLATE_CONTEXT_PROCESSORS = [ 'django.core.context_processors.request' ] + list(global_settings.TEMPLATE_CONTEXT_PROCESSORS) TEMPLATE_DIRS = ( join(ROOT, 'templates'), ) else: TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': ['templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.templates.context_processors.request', 'django.contrib.auth.context_processors.auth', ], } } ] MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', "debug_toolbar.middleware.DebugToolbarMiddleware", ) ROOT_URLCONF = 'example.urls' INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'example.app', 'django_tables2', 'debug_toolbar', ) INTERNAL_IPS = ( "127.0.0.1", ) # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error. # See http://docs.djangoproject.com/en/dev/topics/logging for # more details on how to customize your logging configuration. LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'mail_admins': { 'level': 'ERROR', 'class': 'django.utils.log.AdminEmailHandler' } }, 'loggers': { 'django.request': { 'handlers': ['mail_admins'], 'level': 'ERROR', 'propagate': True, }, } } django-tables2-1.0.7/example/templates/000077500000000000000000000000001264227116200177475ustar00rootroot00000000000000django-tables2-1.0.7/example/templates/base.html000066400000000000000000000007511264227116200215520ustar00rootroot00000000000000django-tables2 examples {% block body %} {% endblock %} django-tables2-1.0.7/example/templates/class_based.html000066400000000000000000000003721264227116200231020ustar00rootroot00000000000000{% extends "base.html" %} {% block body %}

class based view via render_table

{{ "{%" }} load django_tables2 {{ "%}" }}
{{ "{%" }} render_table example3 {{ "%}" }}
{% load django_tables2 %} {% render_table table %} {% endblock %} django-tables2-1.0.7/example/templates/extended_table.html000066400000000000000000000002751264227116200236100ustar00rootroot00000000000000{% extends "django_tables2/table.html" %} {% block table.tfoot %} {% endblock %} django-tables2-1.0.7/example/templates/multiple.html000066400000000000000000000046131264227116200224740ustar00rootroot00000000000000{% extends "base.html" %} {% block body %}

django-tables2 examples

This page demonstrates various types of tables being rendered via django-tables2.

Example 1 — QuerySet

via as_html()

{% templatetag openvariable %} example1.as_html {% templatetag closevariable %}
{{ example1.as_html }}

via template tag

{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
{% templatetag openblock %} render_table example1 {% templatetag closeblock %}
{% load django_tables2 %} {% render_table example1 %}

Example 2 — QuerySet + pagination

via as_html()

{% templatetag openvariable %} example2.as_html {% templatetag closevariable %}
{{ example2.as_html }}

via template tag

{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
{% templatetag openblock %} render_table example2 {% templatetag closeblock %}
{% load django_tables2 %} {% render_table example2 %}

Example 3 — QuerySet + paleblue theme

via as_html()

{% templatetag openvariable %} example3.as_html {% templatetag closevariable %}
{{ example3.as_html }}

via template tag

{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
{% templatetag openblock %} render_table example3 {% templatetag closeblock %}
{% load django_tables2 %} {% render_table example3 %}

Example 4 — QuerySet + pagination + paleblue theme

via as_html()

{% templatetag openvariable %} example4.as_html {% templatetag closevariable %}
{{ example4.as_html }}

via template tag

{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
{% templatetag openblock %} render_table example4 {% templatetag closeblock %}
{% load django_tables2 %} {% render_table example4 %}

Example 5 – QuerySet + pagination + paleblue theme + custom template

via as_html()

{% templatetag openvariable %} example5.as_html {% templatetag closevariable %}
{{ example5.as_html }}

via template tag

{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
{% templatetag openblock %} render_table example5 {% templatetag closeblock %}
{% load django_tables2 %} {% render_table example5 %} {% endblock %} django-tables2-1.0.7/example/templates/tutorial.html000066400000000000000000000004021264227116200224740ustar00rootroot00000000000000{% load render_table from django_tables2 %} {% render_table people %} django-tables2-1.0.7/example/urls.py000066400000000000000000000011501264227116200173050ustar00rootroot00000000000000# coding: utf-8 from django.conf import settings from django.conf.urls import include, url from django.conf.urls.static import static from django.contrib import admin from django.views import static from app.views import class_based, multiple, tutorial admin.autodiscover() urlpatterns = [ url(r'^$', multiple), url(r'^class-based/$', class_based), url(r'^tutorial/$', tutorial), url(r'^admin/doc/', include('django.contrib.admindocs.urls')), url(r'^admin/', include(admin.site.urls)), url(r'^media/(?P.*)$', static.serve, { 'document_root': settings.MEDIA_ROOT, }), ] django-tables2-1.0.7/requirements/000077500000000000000000000000001264227116200170415ustar00rootroot00000000000000django-tables2-1.0.7/requirements/common.pip000066400000000000000000000001201264227116200210340ustar00rootroot00000000000000django-haystack>=2.4.1,<2.5.0 fudge lxml pylint pytz>0 six pytest pytest-django django-tables2-1.0.7/requirements/django-dev.pip000066400000000000000000000000751264227116200215730ustar00rootroot00000000000000-r common.pip http://github.com/django/django/tarball/master django-tables2-1.0.7/setup.py000077500000000000000000000032421264227116200160340ustar00rootroot00000000000000#!/usr/bin/env python import re from setuptools import find_packages, setup with open('django_tables2/__init__.py', 'rb') as f: version = str(re.search('__version__ = "(.+?)"', f.read().decode('utf-8')).group(1)) try: # Verify output using # ./setup.py --long-description | rst2html.py > output.html import pypandoc long_description = pypandoc.convert('README.md', 'rst') except ImportError: print('PyPI needs .rst and README is in .md.') print('Use `pip install pypandoc` to enable conversion') long_description = open('README.md').read() setup( name='django-tables2', version=version, description='Table/data-grid framework for Django', long_description=long_description, author='Bradley Ayers', author_email='bradley.ayers@gmail.com', license='Simplified BSD', url='https://github.com/bradleyayers/django-tables2/', packages=find_packages(exclude=['tests.*', 'tests', 'example.*', 'example']), include_package_data=True, # declarations in MANIFEST.in install_requires=['Django >=1.7', 'six'], classifiers=[ 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Software Development :: Libraries', ], ) django-tables2-1.0.7/tests/000077500000000000000000000000001264227116200154605ustar00rootroot00000000000000django-tables2-1.0.7/tests/__init__.py000066400000000000000000000000001264227116200175570ustar00rootroot00000000000000django-tables2-1.0.7/tests/app/000077500000000000000000000000001264227116200162405ustar00rootroot00000000000000django-tables2-1.0.7/tests/app/__init__.py000066400000000000000000000000001264227116200203370ustar00rootroot00000000000000django-tables2-1.0.7/tests/app/models.py000066400000000000000000000043761264227116200201070ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import six from django.core.urlresolvers import reverse from django.db import models from django.utils.safestring import mark_safe from django.utils.translation import ugettext, ugettext_lazy class Person(models.Model): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200, verbose_name='surname') occupation = models.ForeignKey( 'Occupation', related_name='people', null=True, verbose_name='occupation') trans_test = models.CharField( max_length=200, blank=True, verbose_name=ugettext("translation test")) trans_test_lazy = models.CharField( max_length=200, blank=True, verbose_name=ugettext_lazy("translation test lazy")) safe = models.CharField( max_length=200, blank=True, verbose_name=mark_safe("Safe")) class Meta: verbose_name = "person" verbose_name_plural = "people" def __unicode__(self): return self.first_name @property def name(self): return "%s %s" % (self.first_name, self.last_name) def get_absolute_url(self): return reverse('person', args=(self.pk, )) class PersonProxy(Person): class Meta: proxy = True ordering = ('last_name',) class Occupation(models.Model): name = models.CharField(max_length=200) region = models.ForeignKey('Region', null=True) def __unicode__(self): return self.name class Region(models.Model): name = models.CharField(max_length=200) mayor = models.OneToOneField(Person, null=True) def __unicode__(self): return self.name class PersonInformation(models.Model): person = models.ForeignKey( Person, related_name='info_list', verbose_name='Information') # -- haystack ----------------------------------------------------------------- if not six.PY3: # Haystack isn't compatible with Python 3 from haystack import indexes class PersonIndex(indexes.SearchIndex, indexes.Indexable): first_name = indexes.CharField(document=True) def get_model(self): return Person def index_queryset(self, using=None): return self.get_model().objects.all() django-tables2-1.0.7/tests/app/settings.py000066400000000000000000000022301264227116200204470ustar00rootroot00000000000000import os import six from django import VERSION from django.conf import global_settings DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', } } INSTALLED_APPS = [ 'tests.app', 'django.contrib.contenttypes', 'django.contrib.auth', 'django_tables2', ] ROOT_URLCONF = 'tests.app.urls' SECRET_KEY = "this is super secret" if VERSION < (1, 8): TEMPLATE_CONTEXT_PROCESSORS = [ 'django.core.context_processors.request' ] + list(global_settings.TEMPLATE_CONTEXT_PROCESSORS) else: TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request' ], } } ] TIME_ZONE = "Australia/Brisbane" USE_TZ = True if not six.PY3: # Haystack isn't compatible with Python 3 INSTALLED_APPS += [ 'haystack', ] HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', } } django-tables2-1.0.7/tests/app/templates/000077500000000000000000000000001264227116200202365ustar00rootroot00000000000000django-tables2-1.0.7/tests/app/templates/child/000077500000000000000000000000001264227116200213215ustar00rootroot00000000000000django-tables2-1.0.7/tests/app/templates/child/foo.html000066400000000000000000000000041264227116200227640ustar00rootroot00000000000000bar django-tables2-1.0.7/tests/app/templates/dummy.html000066400000000000000000000000301264227116200222500ustar00rootroot00000000000000dummy template contents django-tables2-1.0.7/tests/app/templates/test_template_column.html000066400000000000000000000000451264227116200253520ustar00rootroot00000000000000name:{{ record.col }}{{ STATIC_URL }}django-tables2-1.0.7/tests/app/urls.py000066400000000000000000000005641264227116200176040ustar00rootroot00000000000000# coding: utf-8 try: from django.conf.urls import url except ImportError: from django.conf.urls.defaults import url from . import views urlpatterns = [ url(r'^people/(?P\d+)/$', views.person, name='person'), url(r'^occupations/(?P\d+)/$', views.occupation, name='occupation'), url(r'^&\'"/(?P\d+)/$', lambda req: None, name='escaping'), ] django-tables2-1.0.7/tests/app/views.py000066400000000000000000000010731264227116200177500ustar00rootroot00000000000000# coding: utf-8 from django.shortcuts import get_object_or_404 from django.http import HttpResponse from .models import Person, Occupation def person(request, pk): """A really simple view to provide an endpoint for the 'person' URL.""" person = get_object_or_404(Person, pk=pk) return HttpResponse('Person: %s' % person) def occupation(request, pk): """ Another really simple view to provide an endpoint for the 'occupation' URL. """ occupation = get_object_or_404(Occupation, pk=pk) return HttpResponse('Occupation: %s' % occupation) django-tables2-1.0.7/tests/columns/000077500000000000000000000000001264227116200171405ustar00rootroot00000000000000django-tables2-1.0.7/tests/columns/__init__.py000066400000000000000000000000001264227116200212370ustar00rootroot00000000000000django-tables2-1.0.7/tests/columns/test_booleancolumn.py000066400000000000000000000031601264227116200234060ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=R0912,E0102 from __future__ import unicode_literals from django.db import models import django_tables2 as tables from ..utils import attrs def test_should_be_used_for_booleanfield(): class BoolModel(models.Model): field = models.BooleanField() class Meta: app_label = 'django_tables2_test' class Table(tables.Table): class Meta: model = BoolModel column = Table.base_columns["field"] assert type(column) == tables.BooleanColumn assert column.empty_values != () def test_should_be_used_for_nullbooleanfield(): class NullBoolModel(models.Model): field = models.NullBooleanField() class Meta: app_label = 'django_tables2_test' class Table(tables.Table): class Meta: model = NullBoolModel column = Table.base_columns["field"] assert type(column) == tables.BooleanColumn assert column.empty_values == () def test_treat_none_different_from_false(): class Table(tables.Table): col = tables.BooleanColumn(null=False, default="---") table = Table([{"col": None}]) assert table.rows[0]["col"] == "---" def test_treat_none_as_false(): class Table(tables.Table): col = tables.BooleanColumn(null=True) table = Table([{"col": None}]) assert table.rows[0]["col"] == '' def test_span_attrs(): class Table(tables.Table): col = tables.BooleanColumn(attrs={"span": {"key": "value"}}) table = Table([{"col": True}]) assert attrs(table.rows[0]["col"]) == {"class": "true", "key": "value"} django-tables2-1.0.7/tests/columns/test_checkboxcolumn.py000066400000000000000000000027731264227116200235660ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=R0912,E0102 from __future__ import unicode_literals import django_tables2 as tables from ..utils import attrs, warns def test_attrs_should_be_translated_for_backwards_compatibility(): with warns(DeprecationWarning): class TestTable(tables.Table): col = tables.CheckBoxColumn(header_attrs={"th_key": "th_value"}, attrs={"td_key": "td_value"}) table = TestTable([{"col": "data"}]) assert attrs(table.columns["col"].header) == {"type": "checkbox", "th_key": "th_value"} assert attrs(table.rows[0]["col"]) == {"type": "checkbox", "td_key": "td_value", "value": "data", "name": "col"} def new_attrs_should_be_supported(): with warns(DeprecationWarning): class TestTable(tables.Table): col1 = tables.CheckBoxColumn(attrs=dict(th__input={"th_key": "th_value"}, td__input={"td_key": "td_value"})) col2 = tables.CheckBoxColumn(attrs=dict(input={"key": "value"})) table = TestTable([{"col1": "data", "col2": "data"}]) assert attrs(table.columns["col1"].header) == {"type": "checkbox", "th_key": "th_value"} assert attrs(table.rows[0]["col1"]) == {"type": "checkbox", "td_key": "td_value", "value": "data", "name": "col1"} assert attrs(table.columns["col2"].header) == {"type": "checkbox", "key": "value"} assert attrs(table.rows[0]["col2"]) == {"type": "checkbox", "key": "value", "value": "data", "name": "col2"} django-tables2-1.0.7/tests/columns/test_datecolumn.py000066400000000000000000000035761264227116200227170ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=R0912,E0102 from __future__ import unicode_literals from datetime import date from django.db import models import django_tables2 as tables # Format string: https://docs.djangoproject.com/en/1.4/ref/templates/builtins/#date # D -- Day of the week, textual, 3 letters -- 'Fri' # b -- Month, textual, 3 letters, lowercase -- 'jan' # Y -- Year, 4 digits. -- '1999' def test_should_handle_explicit_format(): class TestTable(tables.Table): date = tables.DateColumn(format="D b Y") class Meta: default = "—" table = TestTable([{"date": date(2012, 9, 11)}, {"date": None}]) assert table.rows[0]["date"] == "Tue sep 2012" assert table.rows[1]["date"] == "—" def test_should_handle_long_format(settings): settings.DATE_FORMAT = "D Y b" class TestTable(tables.Table): date = tables.DateColumn(short=False) class Meta: default = "—" table = TestTable([{"date": date(2012, 9, 11)}, {"date": None}]) assert table.rows[0]["date"] == "Tue 2012 sep" assert table.rows[1]["date"] == "—" def test_should_handle_short_format(settings): settings.SHORT_DATE_FORMAT = "b Y D" class TestTable(tables.Table): date = tables.DateColumn(short=True) class Meta: default = "—" table = TestTable([{"date": date(2012, 9, 11)}, {"date": None}]) assert table.rows[0]["date"] == "sep 2012 Tue" assert table.rows[1]["date"] == "—" def test_should_be_used_for_datefields(): class DateModel(models.Model): field = models.DateField() class Meta: app_label = 'django_tables2_test' class Table(tables.Table): class Meta: model = DateModel assert type(Table.base_columns["field"]) == tables.DateColumn django-tables2-1.0.7/tests/columns/test_datetimecolumn.py000066400000000000000000000046551264227116200235750ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=R0912,E0102 from __future__ import unicode_literals from datetime import datetime import pytz from django.db import models import django_tables2 as tables import pytest try: from django.utils import timezone except ImportError: timezone = None # Format string: https://docs.djangoproject.com/en/1.4/ref/templates/builtins/#date # D -- Day of the week, textual, 3 letters -- 'Fri' # b -- Month, textual, 3 letters, lowercase -- 'jan' # Y -- Year, 4 digits. -- '1999' # A -- 'AM' or 'PM'. -- 'AM' # f -- Time, in 12-hour hours[:minutes] -- '1', '1:30' @pytest.yield_fixture def dt(): dt = datetime(2012, 9, 11, 12, 30, 0) if timezone: # If the version of Django has timezone support, convert from naive to # UTC, the test project uses Australia/Brisbane so regardless the # output from the column should be the same. dt = pytz.timezone("Australia/Brisbane").localize(dt) yield dt def test_should_handle_explicit_format(dt): class TestTable(tables.Table): date = tables.DateTimeColumn(format="D b Y") class Meta: default = "—" table = TestTable([{"date": dt}, {"date": None}]) assert table.rows[0]["date"] == "Tue sep 2012" assert table.rows[1]["date"] == "—" def test_should_handle_long_format(dt, settings): class TestTable(tables.Table): date = tables.DateTimeColumn(short=False) class Meta: default = "—" settings.DATETIME_FORMAT = "D Y b A f" table = TestTable([{"date": dt}, {"date": None}]) assert table.rows[0]["date"] == "Tue 2012 sep PM 12:30" assert table.rows[1]["date"] == "—" def test_should_handle_short_format(dt, settings): class TestTable(tables.Table): date = tables.DateTimeColumn(short=True) class Meta: default = "—" settings.SHORT_DATETIME_FORMAT = "b Y D A f" table = TestTable([{"date": dt}, {"date": None}]) assert table.rows[0]["date"] == "sep 2012 Tue PM 12:30" assert table.rows[1]["date"] == "—" def test_should_be_used_for_datetimefields(): class DateTimeModel(models.Model): field = models.DateTimeField() class Meta: app_label = 'django_tables2_test' class Table(tables.Table): class Meta: model = DateTimeModel assert type(Table.base_columns["field"]) == tables.DateTimeColumn django-tables2-1.0.7/tests/columns/test_emailcolumn.py000066400000000000000000000020251264227116200230550ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=R0912,E0102 from __future__ import unicode_literals from django.db import models import django_tables2 as tables try: from django.utils import timezone except ImportError: timezone = None def test_should_turn_email_address_into_hyperlink(): class Table(tables.Table): email = tables.EmailColumn() table = Table([{"email": "test@example.com"}]) assert table.rows[0]["email"] == 'test@example.com' def test_should_render_default_for_blank(): class Table(tables.Table): email = tables.EmailColumn(default="---") table = Table([{"email": ""}]) assert table.rows[0]["email"] == '---' def test_should_be_used_for_datetimefields(): class EmailModel(models.Model): field = models.EmailField() class Meta: app_label = 'django_tables2_test' class Table(tables.Table): class Meta: model = EmailModel assert type(Table.base_columns["field"]) == tables.EmailColumn django-tables2-1.0.7/tests/columns/test_filecolumn.py000066400000000000000000000051001264227116200227020ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=R0912,E0102 from __future__ import unicode_literals from os.path import dirname, join from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage from django.db import models from django.db.models.fields.files import FieldFile import django_tables2 as tables import pytest from ..utils import parse @pytest.yield_fixture def storage(): """Provide a storage that exposes the test templates""" root = join(dirname(__file__), "..", "app", "templates") yield FileSystemStorage(location=root, base_url="/baseurl/") @pytest.yield_fixture def column(): yield tables.FileColumn(attrs={"span": {"class": "span"}, "a": {"class": "a"}}) def test_should_be_used_for_filefields(): class FileModel(models.Model): field = models.FileField() class Meta: app_label = 'django_tables2_test' class Table(tables.Table): class Meta: model = FileModel assert type(Table.base_columns["field"]) == tables.FileColumn def test_filecolumn_supports_storage_file(column, storage): file_ = storage.open("child/foo.html") try: root = parse(column.render(value=file_)) finally: file_.close() path = file_.name assert root.tag == "span" assert root.attrib == {"class": "span exists", "title": path} assert root.text == "foo.html" def test_filecolumn_supports_contentfile(column): name = "foobar.html" file_ = ContentFile('') file_.name = name # Django <1.4 compatible root = parse(column.render(value=file_)) assert root.tag == "span" assert root.attrib == {"title": name, "class": "span"} assert root.text == "foobar.html" def test_filecolumn_supports_fieldfile(column, storage): field = models.FileField(storage=storage) name = "child/foo.html" fieldfile = FieldFile(instance=None, field=field, name=name) root = parse(column.render(value=fieldfile)) assert root.tag == "a" assert root.attrib == {"class": "a exists", "title": name, "href": "/baseurl/child/foo.html"} assert root.text == "foo.html" # Now try a file that doesn't exist name = "child/does_not_exist.html" fieldfile = FieldFile(instance=None, field=field, name=name) html = column.render(value=fieldfile) root = parse(html) assert root.tag == "a" assert root.attrib == {"class": "a missing", "title": name, "href": "/baseurl/child/does_not_exist.html"} assert root.text == "does_not_exist.html" django-tables2-1.0.7/tests/columns/test_general.py000066400000000000000000000254101264227116200221700ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=R0912,E0102 from __future__ import unicode_literals import pytest from django.utils.safestring import SafeData, mark_safe from django.utils.translation import ugettext_lazy import django_tables2 as tables from ..app.models import Person from ..utils import build_request, parse, warns request = build_request('/') def test_column_render_supports_kwargs(): class TestColumn(tables.Column): def render(self, **kwargs): expected = {"record", "value", "column", "bound_column", "bound_row", "table"} actual = set(kwargs.keys()) assert actual == expected return "success" class TestTable(tables.Table): foo = TestColumn() table = TestTable([{"foo": "bar"}]) assert table.rows[0]["foo"] == "success" def test_column_header_should_use_titlised_verbose_name_unless_given_explicitly(): class SimpleTable(tables.Table): basic = tables.Column() acronym = tables.Column(verbose_name="has FBI help") table = SimpleTable([]) assert table.columns["basic"].header == "Basic" assert table.columns["acronym"].header == "has FBI help" def test_should_support_safe_verbose_name(): class SimpleTable(tables.Table): safe = tables.Column(verbose_name=mark_safe("Safe")) table = SimpleTable([]) assert isinstance(table.columns["safe"].header, SafeData) def test_should_support_safe_verbose_name_via_model(): class PersonTable(tables.Table): safe = tables.Column() table = PersonTable(Person.objects.all()) assert isinstance(table.columns["safe"].header, SafeData) def test_should_support_empty_string_as_explicit_verbose_name(): class SimpleTable(tables.Table): acronym = tables.Column(verbose_name="") table = SimpleTable([]) assert table.columns["acronym"].header == "" @pytest.mark.django_db def test_handle_verbose_name_of_many2onerel(): class Table(tables.Table): count = tables.Column(accessor='info_list.count') Person.objects.create(first_name='bradley', last_name='ayers') table = Table(Person.objects.all()) assert table.columns['count'].verbose_name == 'Information' def test_sortable_backwards_compatibility(): # Table.Meta.sortable (not set) class SimpleTable(tables.Table): name = tables.Column() table = SimpleTable([]) with warns(DeprecationWarning): assert table.columns['name'].sortable is True # Table.Meta.sortable = False with warns(DeprecationWarning): class SimpleTable(tables.Table): name = tables.Column() class Meta: sortable = False table = SimpleTable([]) with warns(DeprecationWarning): assert table.columns['name'].sortable is False # backwards compatible assert table.columns['name'].orderable is False # Table.Meta.sortable = True with warns(DeprecationWarning): class SimpleTable(tables.Table): name = tables.Column() class Meta: sortable = True table = SimpleTable([]) with warns(DeprecationWarning): assert table.columns['name'].sortable is True # backwards compatible assert table.columns['name'].orderable is True def test_orderable(): # Table.Meta.orderable = False class SimpleTable(tables.Table): name = tables.Column() table = SimpleTable([]) assert table.columns['name'].orderable is True # Table.Meta.orderable = False class SimpleTable(tables.Table): name = tables.Column() class Meta: orderable = False table = SimpleTable([]) assert table.columns['name'].orderable is False with warns(DeprecationWarning): assert table.columns['name'].sortable is False # backwards compatible # Table.Meta.orderable = True class SimpleTable(tables.Table): name = tables.Column() class Meta: orderable = True table = SimpleTable([]) with warns(DeprecationWarning): assert table.columns['name'].sortable is True # backwards compatible assert table.columns['name'].orderable is True def test_order_by_defaults_to_accessor(): class SimpleTable(tables.Table): foo = tables.Column(accessor="bar") table = SimpleTable([]) assert table.columns["foo"].order_by == ("bar", ) def test_supports_order_by(): class SimpleTable(tables.Table): name = tables.Column(order_by=("last_name", "-first_name")) age = tables.Column() table = SimpleTable([], order_by=("-age", )) # alias assert table.columns["name"].order_by_alias == "name" assert table.columns["age"].order_by_alias == "-age" # order by assert table.columns["name"].order_by == ("last_name", "-first_name") assert table.columns["age"].order_by == ("-age", ) # now try with name ordered table = SimpleTable([], order_by=("-name", )) # alias assert table.columns["name"].order_by_alias == "-name" assert table.columns["age"].order_by_alias == "age" # alias next assert table.columns["name"].order_by_alias.next == "name" assert table.columns["age"].order_by_alias.next == "age" # order by assert table.columns["name"].order_by == ("-last_name", "first_name") assert table.columns["age"].order_by == ("age", ) def test_supports_is_ordered(): class SimpleTable(tables.Table): name = tables.Column() # sorted table = SimpleTable([], order_by='name') assert table.columns["name"].is_ordered # unsorted table = SimpleTable([]) assert not table.columns["name"].is_ordered def test_translation(): """ Tests different types of values for the ``verbose_name`` property of a column. """ class TranslationTable(tables.Table): text = tables.Column(verbose_name=ugettext_lazy("Text")) table = TranslationTable([]) assert "Text" == table.columns["text"].header def test_sequence(): """ Ensures that the sequence of columns is configurable. """ class TestTable(tables.Table): a = tables.Column() b = tables.Column() c = tables.Column() assert ["a", "b", "c"] == TestTable([]).columns.names() assert ["b", "a", "c"] == TestTable([], sequence=("b", "a", "c")).columns.names() class TestTable2(TestTable): class Meta: sequence = ("b", "a", "c") assert ["b", "a", "c"] == TestTable2([]).columns.names() assert ["a", "b", "c"] == TestTable2([], sequence=("a", "b", "c")).columns.names() class TestTable3(TestTable): class Meta: sequence = ("c", ) assert ["c", "a", "b"] == TestTable3([]).columns.names() assert ["c", "a", "b"] == TestTable([], sequence=("c", )).columns.names() class TestTable4(TestTable): class Meta: sequence = ("...", ) assert ["a", "b", "c"] == TestTable4([]).columns.names() assert ["a", "b", "c"] == TestTable([], sequence=("...", )).columns.names() class TestTable5(TestTable): class Meta: sequence = ("b", "...") assert ["b", "a", "c"] == TestTable5([]).columns.names() assert ["b", "a", "c"] == TestTable([], sequence=("b", "...")).columns.names() class TestTable6(TestTable): class Meta: sequence = ("...", "b") assert ["a", "c", "b"] == TestTable6([]).columns.names() assert ["a", "c", "b"] == TestTable([], sequence=("...", "b")).columns.names() class TestTable7(TestTable): class Meta: sequence = ("b", "...", "a") assert ["b", "c", "a"] == TestTable7([]).columns.names() assert ["b", "c", "a"] == TestTable([], sequence=("b", "...", "a")).columns.names() # Let's test inheritence class TestTable8(TestTable): d = tables.Column() e = tables.Column() f = tables.Column() class Meta: sequence = ("d", "...") class TestTable9(TestTable): d = tables.Column() e = tables.Column() f = tables.Column() assert ["d", "a", "b", "c", "e", "f"] == TestTable8([]).columns.names() assert ["d", "a", "b", "c", "e", "f"] == TestTable9([], sequence=("d", "...")).columns.names() def test_should_support_both_meta_sequence_and_constructor_exclude(): """ Issue #32 describes a problem when both ``Meta.sequence`` and ``Table(..., exclude=...)`` are used on a single table. The bug caused an exception to be raised when the table was iterated. """ class SequencedTable(tables.Table): a = tables.Column() b = tables.Column() c = tables.Column() class Meta: sequence = ('a', '...') table = SequencedTable([], exclude=('c', )) table.as_html(request) def test_bound_columns_should_support_indexing(): class SimpleTable(tables.Table): a = tables.Column() b = tables.Column() table = SimpleTable([]) assert 'b' == table.columns[1].name assert 'b' == table.columns['b'].name def test_cell_attrs_applies_to_td_and_th(): class SimpleTable(tables.Table): a = tables.Column(attrs={"cell": {"key": "value"}}) # providing data ensures 1 row is rendered table = SimpleTable([{"a": "value"}]) root = parse(table.as_html(request)) assert root.findall('.//thead/tr/th')[0].attrib == {"key": "value", "class": "a orderable sortable"} assert root.findall('.//tbody/tr/td')[0].attrib == {"key": "value", "class": "a"} def test_cells_are_automatically_given_column_name_as_class(): class SimpleTable(tables.Table): a = tables.Column() table = SimpleTable([{"a": "value"}]) root = parse(table.as_html(request)) assert root.findall('.//thead/tr/th')[0].attrib == {"class": "a orderable sortable"} assert root.findall('.//tbody/tr/td')[0].attrib == {"class": "a"} def test_th_are_given_sortable_class_if_column_is_orderable(): class SimpleTable(tables.Table): a = tables.Column() b = tables.Column(orderable=False) table = SimpleTable([{"a": "value"}]) root = parse(table.as_html(request)) # return classes of an element as a set classes = lambda x: set(x.attrib["class"].split()) assert "sortable" in classes(root.findall('.//thead/tr/th')[0]) assert "sortable" not in classes(root.findall('.//thead/tr/th')[1]) # Now try with an ordered table table = SimpleTable([], order_by="a") root = parse(table.as_html(request)) # return classes of an element as a set assert "sortable" in classes(root.findall('.//thead/tr/th')[0]) assert "asc" in classes(root.findall('.//thead/tr/th')[0]) assert "sortable" not in classes(root.findall('.//thead/tr/th')[1]) def test_empty_values_triggers_default(): class Table(tables.Table): a = tables.Column(empty_values=(1, 2), default="--") table = Table([{"a": 1}, {"a": 2}, {"a": 3}, {"a": 4}]) assert [x["a"] for x in table.rows] == ["--", "--", 3, 4] django-tables2-1.0.7/tests/columns/test_linkcolumn.py000066400000000000000000000123011264227116200227210ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=R0912,E0102 from __future__ import unicode_literals import django_tables2 as tables from django.core.urlresolvers import reverse from django.template import Context, Template from django.utils.html import mark_safe from django_tables2 import A import pytest from ..app.models import Person from ..utils import attrs, build_request, warns def test_unicode(): """Test LinkColumn""" # test unicode values + headings class UnicodeTable(tables.Table): first_name = tables.LinkColumn('person', args=[A('pk')]) last_name = tables.LinkColumn('person', args=[A('pk')], verbose_name='äÚ¨´ˆÁ˜¨ˆ˜˘Ú…Ò˚ˆπ∆ˆ´') dataset = [ {'pk': 1, 'first_name': 'Brädley', 'last_name': '∆yers'}, {'pk': 2, 'first_name': 'Chr…s', 'last_name': 'DÒble'}, ] table = UnicodeTable(dataset) request = build_request('/some-url/') template = Template('{% load django_tables2 %}{% render_table table %}') html = template.render(Context({'request': request, 'table': table})) assert 'Brädley' in html assert '∆yers' in html assert 'Chr…s' in html assert 'DÒble' in html def test_link_text_custom_value(): class CustomLinkTable(tables.Table): first_name = tables.LinkColumn('person', text='foo::bar', args=[A('pk')]) last_name = tables.LinkColumn('person', text=lambda row: '%s %s' % (row['last_name'], row['first_name']), args=[A('pk')]) dataset = [ {'pk': 1, 'first_name': 'John', 'last_name': 'Doe'} ] table = CustomLinkTable(dataset) request = build_request('/some-url/') template = Template('{% load django_tables2 %}{% render_table table %}') html = template.render(Context({'request': request, 'table': table})) assert 'foo::bar' in html assert 'Doe John' in html def test_link_text_excaping(): class CustomLinkTable(tables.Table): last_name = tables.LinkColumn('person', text=mark_safe(''), args=[A('pk')]) dataset = [ {'pk': 1, 'first_name': 'John', 'last_name': 'Doe'} ] table = CustomLinkTable(dataset) request = build_request('/some-url/') template = Template('{% load django_tables2 %}{% render_table table %}') html = template.render(Context({'request': request, 'table': table})) assert '>' in html @pytest.mark.django_db def test_null_foreign_key(): class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() occupation = tables.LinkColumn('occupation', args=[A('occupation.pk')]) Person.objects.create(first_name='bradley', last_name='ayers') request = build_request() table = PersonTable(Person.objects.all()) table.as_html(request) def test_kwargs(): class PersonTable(tables.Table): a = tables.LinkColumn('occupation', kwargs={"pk": A('a')}) request = build_request('/') html = PersonTable([{"a": 0}, {"a": 1}]).as_html(request) assert reverse("occupation", kwargs={"pk": 0}) in html assert reverse("occupation", kwargs={"pk": 1}) in html def test_html_escape_value(): class PersonTable(tables.Table): name = tables.LinkColumn("escaping", kwargs={"pk": A("pk")}) table = PersonTable([{"name": "", "pk": 1}]) assert table.rows[0]["name"] == '<brad>' def test_old_style_attrs_should_still_work(): with warns(DeprecationWarning): class TestTable(tables.Table): col = tables.LinkColumn('occupation', kwargs={"pk": A('col')}, attrs={"title": "Occupation Title"}) table = TestTable([{"col": 0}]) assert attrs(table.rows[0]["col"]) == {"href": reverse("occupation", kwargs={"pk": 0}), "title": "Occupation Title"} def test_a_attrs_should_be_supported(): class TestTable(tables.Table): col = tables.LinkColumn('occupation', kwargs={'pk': A('col')}, attrs={"a": {"title": "Occupation Title"}}) table = TestTable([{'col': 0}]) assert attrs(table.rows[0]['col']) == {'href': reverse('occupation', kwargs={'pk': 0}), 'title': 'Occupation Title'} def test_defaults(): class Table(tables.Table): link = tables.LinkColumn('occupation', kwargs={'pk': 1}, default='xyz') table = Table([{}]) assert table.rows[0]['link'] == 'xyz' @pytest.mark.django_db def test_get_absolute_url(): class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.LinkColumn() person = Person.objects.create(first_name='Jan Pieter', last_name='Waagmeester', ) table = PersonTable(Person.objects.all()) expected = 'Waagmeester' % person.pk assert table.rows[0]['last_name'] == expected def test_get_absolute_url_not_defined(): class Table(tables.Table): first_name = tables.Column() last_name = tables.LinkColumn() table = Table([ dict(first_name='Jan Pieter', last_name='Waagmeester') ]) with pytest.raises(TypeError): table.as_html(build_request('/')) django-tables2-1.0.7/tests/columns/test_templatecolumn.py000066400000000000000000000022131264227116200236000ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=R0912,E0102 from __future__ import unicode_literals from django.template import Context import django_tables2 as tables def test_should_handle_context_on_table(): class TestTable(tables.Table): col_code = tables.TemplateColumn(template_code="code:{{ record.col }}{{ STATIC_URL }}") col_name = tables.TemplateColumn(template_name="test_template_column.html") table = TestTable([{"col": "brad"}]) assert table.rows[0]["col_code"] == "code:brad" assert table.rows[0]["col_name"] == "name:brad" table.context = Context({"STATIC_URL": "/static/"}) assert table.rows[0]["col_code"] == "code:brad/static/" assert table.rows[0]["col_name"] == "name:brad/static/" def test_should_support_default(): class Table(tables.Table): foo = tables.TemplateColumn("default={{ default }}", default="bar") table = Table([{}]) assert table.rows[0]["foo"] == "default=bar" def test_should_support_value(): class Table(tables.Table): foo = tables.TemplateColumn("value={{ value }}") table = Table([{"foo": "bar"}]) assert table.rows[0]["foo"] == "value=bar" django-tables2-1.0.7/tests/columns/test_urlcolumn.py000066400000000000000000000013331264227116200225710ustar00rootroot00000000000000# coding: utf-8 # pylint: disable=R0912,E0102 from __future__ import unicode_literals from django.db import models import django_tables2 as tables def test_should_turn_url_into_hyperlink(): class TestTable(tables.Table): url = tables.URLColumn() table = TestTable([{"url": "http://example.com"}]) assert table.rows[0]["url"] == 'http://example.com' def test_should_be_used_for_urlfields(): class URLModel(models.Model): field = models.URLField() class Meta: app_label = 'django_tables2_test' class Table(tables.Table): class Meta: model = URLModel assert type(Table.base_columns["field"]) == tables.URLColumn django-tables2-1.0.7/tests/test_config.py000066400000000000000000000041511264227116200203370ustar00rootroot00000000000000# coding: utf-8 import pytest from django.core.paginator import EmptyPage, PageNotAnInteger from fudge import Fake from django_tables2 import RequestConfig from .utils import build_request NOTSET = object() # unique value @pytest.yield_fixture def table(): yield (Fake("Table") .has_attr(prefixed_page_field="page", prefixed_per_page_field="per_page", prefixed_order_by_field="sort")) def test_no_querystring(table): request = build_request("/") table = table.has_attr(order_by=NOTSET).expects("paginate") RequestConfig(request).configure(table) assert table.order_by is NOTSET def test_full_querystring(table): request = build_request("/?page=1&per_page=5&sort=abc") table = (table .expects("paginate").with_args(page=1, per_page=5) .expects("order_by").with_args("abc")) RequestConfig(request).configure(table) def test_partial_querystring(table): request = build_request("/?page=1&sort=abc") table = (table .expects("paginate").with_args(page=1, per_page=5) .expects("order_by").with_args("abc")) RequestConfig(request, paginate={"per_page": 5}).configure(table) def test_silent_page_not_an_integer_error(table): request = build_request("/") paginator = (Fake("Paginator") .expects("page").with_args(1)) table = (table .has_attr(paginator=paginator) .expects("paginate").with_args(page="abc") .raises(PageNotAnInteger)) RequestConfig(request, paginate={"page": "abc", "silent": True}).configure(table) def test_silent_empty_page_error(table): request = build_request("/") paginator = (Fake("Paginator") .has_attr(num_pages=987) .expects("page").with_args(987)) table = (table .has_attr(paginator=paginator) .expects("paginate").with_args(page=123) .raises(EmptyPage)) RequestConfig(request, paginate={"page": 123, "silent": True}).configure(table) django-tables2-1.0.7/tests/test_core.py000066400000000000000000000421641264227116200200300ustar00rootroot00000000000000# coding: utf-8 """Test the core table functionality.""" from __future__ import absolute_import, unicode_literals import copy import itertools import pytest import six from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator import django_tables2 as tables from django_tables2.tables import DeclarativeColumnsMetaclass from .utils import build_request, warns request = build_request('/') class UnorderedTable(tables.Table): i = tables.Column() alpha = tables.Column() beta = tables.Column() class OrderedTable(UnorderedTable): class Meta: order_by = 'alpha' MEMORY_DATA = [ {'i': 2, 'alpha': 'b', 'beta': 'b'}, {'i': 1, 'alpha': 'a', 'beta': 'c'}, {'i': 3, 'alpha': 'c', 'beta': 'a'}, ] def test_declarations(): """Test defining tables by declaration.""" class GeoAreaTable(tables.Table): name = tables.Column() population = tables.Column() assert len(GeoAreaTable.base_columns) == 2 assert 'name' in GeoAreaTable.base_columns assert not hasattr(GeoAreaTable, 'name') class CountryTable(GeoAreaTable): capital = tables.Column() assert len(CountryTable.base_columns) == 3 assert 'capital' in CountryTable.base_columns # multiple inheritance class AddedMixin(tables.Table): added = tables.Column() class CityTable(GeoAreaTable, AddedMixin): mayor = tables.Column() assert len(CityTable.base_columns) == 4 assert 'added' in CityTable.base_columns def test_metaclass_inheritance(): class Tweaker(type): """Adds an attribute "tweaked" to all classes""" def __new__(cls, name, bases, attrs): attrs['tweaked'] = True return super(Tweaker, cls).__new__(cls, name, bases, attrs) class Meta(Tweaker, DeclarativeColumnsMetaclass): pass class TweakedTableBase(tables.Table): __metaclass__ = Meta name = tables.Column() # Python 2/3 compatible way to enable the metaclass TweakedTable = Meta(str('TweakedTable'), (TweakedTableBase, ), {}) table = TweakedTable([]) assert 'name' in table.columns assert table.tweaked # now flip the order class FlippedMeta(DeclarativeColumnsMetaclass, Tweaker): pass class FlippedTweakedTableBase(tables.Table): name = tables.Column() # Python 2/3 compatible way to enable the metaclass FlippedTweakedTable = FlippedMeta(str('FlippedTweakedTable'), (FlippedTweakedTableBase, ), {}) table = FlippedTweakedTable([]) assert 'name' in table.columns assert table.tweaked def test_attrs(): class TestTable(tables.Table): class Meta: attrs = {} assert {} == TestTable([]).attrs class TestTable2(tables.Table): class Meta: attrs = {"a": "b"} assert {"a": "b"} == TestTable2([]).attrs class TestTable3(tables.Table): pass assert {} == TestTable3([]).attrs assert {"a": "b"} == TestTable3([], attrs={"a": "b"}).attrs class TestTable4(tables.Table): class Meta: attrs = {"a": "b"} assert {"c": "d"} == TestTable4([], attrs={"c": "d"}).attrs def test_attrs_support_computed_values(): counter = itertools.count() class TestTable(tables.Table): class Meta: attrs = {"id": lambda: "test_table_%d" % next(counter)} assert {"id": "test_table_0"} == TestTable([]).attrs assert {"id": "test_table_1"} == TestTable([]).attrs def test_data_knows_its_name(): table = tables.Table([{}]) assert table.data.verbose_name == "item" assert table.data.verbose_name_plural == "items" def test_datasource_untouched(): """Ensure that data that is provided to the table (the datasource) is not modified by table operations. """ original_data = copy.deepcopy(MEMORY_DATA) table = UnorderedTable(MEMORY_DATA) table.order_by = 'i' list(table.rows) assert MEMORY_DATA == original_data table = UnorderedTable(MEMORY_DATA) table.order_by = 'beta' list(table.rows) assert MEMORY_DATA == original_data def test_should_support_tuple_data_source(): class SimpleTable(tables.Table): name = tables.Column() table = SimpleTable(( {'name': 'brad'}, {'name': 'davina'}, )) assert len(table.rows) == 2 @pytest.mark.skipif(six.PY3, reason="Haystack requires Python 2.x") def test_should_support_haystack_data_source(): from haystack.query import SearchQuerySet class PersonTable(tables.Table): first_name = tables.Column() table = PersonTable(SearchQuerySet().all()) table.as_html(request) def test_data_validation(): with pytest.raises(ValueError): table = OrderedTable(None) class Bad: def __len__(self): pass with pytest.raises(ValueError): table = OrderedTable(Bad()) class Ok: def __len__(self): return 1 def __getitem__(self, pos): if pos != 0: raise IndexError() return {'a': 1} table = OrderedTable(Ok()) assert len(table.rows) == 1 def test_ordering(): # fallback to Table.Meta assert ('alpha', ) == OrderedTable([], order_by=None).order_by == OrderedTable([]).order_by # values of order_by are wrapped in tuples before being returned assert OrderedTable([], order_by='alpha').order_by == ('alpha', ) assert OrderedTable([], order_by=('beta',)).order_by == ('beta', ) table = OrderedTable([]) table.order_by = [] assert () == table.order_by == OrderedTable([], order_by=[]).order_by table = OrderedTable([]) table.order_by = () assert () == table.order_by == OrderedTable([], order_by=()).order_by table = OrderedTable([]) table.order_by = '' assert () == table.order_by == OrderedTable([], order_by='').order_by # apply an ordering table = UnorderedTable([]) table.order_by = 'alpha' assert ('alpha', ) == UnorderedTable([], order_by='alpha').order_by == table.order_by table = OrderedTable([]) table.order_by = 'alpha' assert ('alpha', ) == OrderedTable([], order_by='alpha').order_by == table.order_by # let's check the data table = OrderedTable(MEMORY_DATA, order_by='beta') assert 3 == table.rows[0]['i'] table = OrderedTable(MEMORY_DATA, order_by='-beta') assert 1 == table.rows[0]['i'] # allow fallback to Table.Meta.order_by table = OrderedTable(MEMORY_DATA) assert 1 == table.rows[0]['i'] # column's can't be ordered if they're not allowed to be class TestTable2(tables.Table): a = tables.Column(orderable=False) b = tables.Column() table = TestTable2([], order_by='a') assert table.order_by == () table = TestTable2([], order_by='b') assert table.order_by == ('b', ) # ordering disabled by default class TestTable3(tables.Table): a = tables.Column(orderable=True) b = tables.Column() class Meta: orderable = False table = TestTable3([], order_by='a') assert table.order_by == ('a', ) table = TestTable3([], order_by='b') assert table.order_by == () table = TestTable3([], orderable=True, order_by='b') assert table.order_by == ('b', ) with warns(DeprecationWarning) as captured: tables.Column(sortable=True) tables.Column(sortable=False) class TestTable4(tables.Table): class Meta: sortable = True class TestTable4(tables.Table): class Meta: sortable = False assert len(captured) == 4 def test_ordering_different_types(): from datetime import datetime data = [ {'i': 1, 'alpha': datetime.now(), 'beta': [1]}, {'i': {}, 'alpha': None, 'beta': ''}, {'i': 2, 'alpha': None, 'beta': []}, ] table = OrderedTable(data) assert "—" == table.rows[0]['alpha'] table = OrderedTable(data, order_by='i') if six.PY3: assert {} == table.rows[0]['i'] else: assert 1 == table.rows[0]['i'] table = OrderedTable(data, order_by='beta') assert [] == table.rows[0]['beta'] def test_multi_column_ordering(): brad = {"first_name": "Bradley", "last_name": "Ayers"} brad2 = {"first_name": "Bradley", "last_name": "Fake"} chris = {"first_name": "Chris", "last_name": "Doble"} davina = {"first_name": "Davina", "last_name": "Adisusila"} ross = {"first_name": "Ross", "last_name": "Ayers"} people = [brad, brad2, chris, davina, ross] class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() table = PersonTable(people, order_by=("first_name", "last_name")) assert [brad, brad2, chris, davina, ross] == [r.record for r in table.rows] table = PersonTable(people, order_by=("first_name", "-last_name")) assert [brad2, brad, chris, davina, ross] == [r.record for r in table.rows] # let's try column order_by using multiple keys class PersonTable(tables.Table): name = tables.Column(order_by=("first_name", "last_name")) # add 'name' key for each person. for person in people: person['name'] = "{p[first_name]} {p[last_name]}".format(p=person) assert brad['name'] == "Bradley Ayers" table = PersonTable(people, order_by="name") assert [brad, brad2, chris, davina, ross] == [r.record for r in table.rows] table = PersonTable(people, order_by="-name") assert [ross, davina, chris, brad2, brad] == [r.record for r in table.rows] def test_column_count(): class SimpleTable(tables.Table): visible = tables.Column(visible=True) hidden = tables.Column(visible=False) # The columns container supports the len() builtin assert len(SimpleTable([]).columns) == 1 def test_column_accessor(): class SimpleTable(UnorderedTable): col1 = tables.Column(accessor='alpha.upper.isupper') col2 = tables.Column(accessor='alpha.upper') table = SimpleTable(MEMORY_DATA) row = table.rows[0] assert row['col1'] is True assert row['col2'] == 'B' def test_exclude_columns(): """ Defining ``Table.Meta.exclude`` or providing an ``exclude`` argument when instantiating a table should have the same effect -- exclude those columns from the table. It should have the same effect as not defining the columns originally. """ # Table(..., exclude=...) table = UnorderedTable([], exclude=("i")) assert [c.name for c in table.columns] == ["alpha", "beta"] # Table.Meta: exclude=... class PartialTable(UnorderedTable): class Meta: exclude = ("alpha", ) table = PartialTable([]) assert [c.name for c in table.columns] == ["i", "beta"] # Inheritence -- exclude in parent, add in child class AddonTable(PartialTable): added = tables.Column() table = AddonTable([]) assert [c.name for c in table.columns] == ["i", "beta", "added"] # Inheritence -- exclude in child class ExcludeTable(UnorderedTable): added = tables.Column() class Meta: exclude = ("beta", ) table = ExcludeTable([]) assert [c.name for c in table.columns] == ["i", "alpha", "added"] def test_table_exclude_property_should_override_constructor_argument(): class SimpleTable(tables.Table): a = tables.Column() b = tables.Column() table = SimpleTable([], exclude=('b', )) assert [c.name for c in table.columns] == ['a'] table.exclude = ('a', ) assert [c.name for c in table.columns] == ['b'] def test_pagination(): class BookTable(tables.Table): name = tables.Column() # create some sample data data = [] for i in range(100): data.append({"name": "Book No. %d" % i}) books = BookTable(data) # external paginator paginator = Paginator(books.rows, 10) assert paginator.num_pages == 10 page = paginator.page(1) assert page.has_previous() is False assert page.has_next() is True # integrated paginator books.paginate(page=1) assert hasattr(books, "page") is True books.paginate(page=1, per_page=10) assert len(list(books.page.object_list)) == 10 # new attributes assert books.paginator.num_pages == 10 assert books.page.has_previous() is False assert books.page.has_next() is True # accessing a non-existant page raises 404 with pytest.raises(EmptyPage): books.paginate(Paginator, page=9999, per_page=10) with pytest.raises(PageNotAnInteger): books.paginate(Paginator, page='abc', per_page=10) def test_pagination_shouldnt_prevent_multiple_rendering(): class SimpleTable(tables.Table): name = tables.Column() table = SimpleTable([{'name': 'brad'}]) table.paginate() assert table.as_html(request) == table.as_html(request) def test_empty_text(): class TestTable(tables.Table): a = tables.Column() table = TestTable([]) assert table.empty_text is None class TestTable2(tables.Table): a = tables.Column() class Meta: empty_text = 'nothing here' table = TestTable2([]) assert table.empty_text == 'nothing here' table = TestTable2([], empty_text='still nothing') assert table.empty_text == 'still nothing' def test_prefix(): """Test that table prefixes affect the names of querystring parameters""" class TableA(tables.Table): name = tables.Column() class Meta: prefix = "x" assert "x" == TableA([]).prefix class TableB(tables.Table): name = tables.Column() assert "" == TableB([]).prefix assert "x" == TableB([], prefix="x").prefix table = TableB([]) table.prefix = "x" assert "x" == table.prefix def test_field_names(): class TableA(tables.Table): class Meta: order_by_field = "abc" page_field = "def" per_page_field = "ghi" table = TableA([]) assert "abc" == table.order_by_field assert "def" == table.page_field assert "ghi" == table.per_page_field def test_field_names_with_prefix(): class TableA(tables.Table): class Meta: order_by_field = "sort" page_field = "page" per_page_field = "per_page" prefix = "1-" table = TableA([]) assert "1-sort" == table.prefixed_order_by_field assert "1-page" == table.prefixed_page_field assert "1-per_page" == table.prefixed_per_page_field class TableB(tables.Table): class Meta: order_by_field = "sort" page_field = "page" per_page_field = "per_page" table = TableB([], prefix="1-") assert "1-sort" == table.prefixed_order_by_field assert "1-page" == table.prefixed_page_field assert "1-per_page" == table.prefixed_per_page_field table = TableB([]) table.prefix = "1-" assert "1-sort" == table.prefixed_order_by_field assert "1-page" == table.prefixed_page_field assert "1-per_page" == table.prefixed_per_page_field def test_should_support_a_template_to_be_specified(): class ConstructorSpecifiedTemplateTable(tables.Table): name = tables.Column() table = ConstructorSpecifiedTemplateTable([], template="dummy.html") assert table.template == "dummy.html" class PropertySpecifiedTemplateTable(tables.Table): name = tables.Column() table = PropertySpecifiedTemplateTable([]) table.template = "dummy.html" assert table.template == "dummy.html" class DefaultTable(tables.Table): pass table = DefaultTable([]) assert table.template == "django_tables2/table.html" def test_template_in_meta_class_declaration_should_be_honored(): class MetaDeclarationSpecifiedTemplateTable(tables.Table): name = tables.Column() class Meta: template = "dummy.html" table = MetaDeclarationSpecifiedTemplateTable([]) assert table.template == "dummy.html" assert table.as_html(request) == "dummy template contents\n" def test_should_support_rendering_multiple_times(): class MultiRenderTable(tables.Table): name = tables.Column() # test list data table = MultiRenderTable([{'name': 'brad'}]) assert table.as_html(request) == table.as_html(request) def test_column_defaults_are_honored(): class Table(tables.Table): name = tables.Column(default="abcd") class Meta: default = "efgh" table = Table([{}], default="ijkl") assert table.rows[0]['name'] == "abcd" def test_table_meta_defaults_are_honored(): class Table(tables.Table): name = tables.Column() class Meta: default = "abcd" table = Table([{}]) assert table.rows[0]['name'] == "abcd" def test_table_defaults_are_honored(): class Table(tables.Table): name = tables.Column() table = Table([{}], default="abcd") assert table.rows[0]['name'] == "abcd" table = Table([{}], default="abcd") table.default = "efgh" assert table.rows[0]['name'] == "efgh" def test_list_table_data_supports_ordering(): class Table(tables.Table): name = tables.Column() data = [ {"name": "Bradley"}, {"name": "Davina"}, ] table = Table(data) assert table.rows[0]["name"] == "Bradley" table.order_by = "-name" assert table.rows[0]["name"] == "Davina" django-tables2-1.0.7/tests/test_models.py000066400000000000000000000246201264227116200203600ustar00rootroot00000000000000# coding: utf-8 import pytest import six import django_tables2 as tables from .app.models import Occupation, Person, PersonProxy from .utils import build_request pytestmark = pytest.mark.django_db request = build_request('/') class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() occupation = tables.Column() def test_boundrows_iteration(): occupation = Occupation.objects.create(name='Programmer') Person.objects.create(first_name='Bradley', last_name='Ayers', occupation=occupation) Person.objects.create(first_name='Chris', last_name='Doble', occupation=occupation) table = PersonTable(Person.objects.all()) records = [row.record for row in table.rows] expecteds = Person.objects.all() for expected, actual in six.moves.zip(expecteds, records): assert expected == actual def test_model_table(): """ The ``model`` option on a table causes the table to dynamically add columns based on the fields. """ class OccupationTable(tables.Table): class Meta: model = Occupation assert ["id", "name", "region"] == list(OccupationTable.base_columns.keys()) class OccupationTable2(tables.Table): extra = tables.Column() class Meta: model = Occupation assert ["id", "name", "region", "extra"] == list(OccupationTable2.base_columns.keys()) # be aware here, we already have *models* variable, but we're importing # over the top from django.db import models class ComplexModel(models.Model): char = models.CharField(max_length=200) fk = models.ForeignKey("self") m2m = models.ManyToManyField("self") class Meta: app_label = 'django_tables2_test' class ComplexTable(tables.Table): class Meta: model = ComplexModel assert ["id", "char", "fk"] == list(ComplexTable.base_columns.keys()) def test_mixins(): class TableMixin(tables.Table): extra = tables.Column() class OccupationTable(TableMixin, tables.Table): extra2 = tables.Column() class Meta: model = Occupation assert ["extra", "id", "name", "region", "extra2"] == list(OccupationTable.base_columns.keys()) def test_column_verbose_name(): """ When using queryset data as input for a table, default to using model field verbose names rather than an autogenerated string based on the column name. However if a column does explicitly describe a verbose name, it should be used. """ class PersonTable(tables.Table): """ The test_colX columns are to test that the accessor is used to determine the field on the model, rather than the column name. """ first_name = tables.Column() fn1 = tables.Column(accessor='first_name') fn2 = tables.Column(accessor='first_name.upper') fn3 = tables.Column(accessor='last_name', verbose_name='OVERRIDE') last_name = tables.Column() ln1 = tables.Column(accessor='last_name') ln2 = tables.Column(accessor='last_name.upper') ln3 = tables.Column(accessor='last_name', verbose_name='OVERRIDE') region = tables.Column(accessor='occupation.region.name') r1 = tables.Column(accessor='occupation.region.name') r2 = tables.Column(accessor='occupation.region.name.upper') r3 = tables.Column(accessor='occupation.region.name', verbose_name='OVERRIDE') trans_test = tables.Column() trans_test_lazy = tables.Column() # The Person model has a ``first_name`` and ``last_name`` field, but only # the ``last_name`` field has an explicit ``verbose_name`` set. This means # that we should expect that the two columns that use the ``last_name`` # field should both use the model's ``last_name`` field's ``verbose_name``, # however both fields that use the ``first_name`` field should just use a # capitalized version of the column name as the column header. table = PersonTable(Person.objects.all()) # Should be generated (capitalized column name) assert 'first name' == table.columns['first_name'].verbose_name assert 'first name' == table.columns['fn1'].verbose_name assert 'first name' == table.columns['fn2'].verbose_name assert 'OVERRIDE' == table.columns['fn3'].verbose_name # Should use the model field's verbose_name assert 'surname' == table.columns['last_name'].verbose_name assert 'surname' == table.columns['ln1'].verbose_name assert 'surname' == table.columns['ln2'].verbose_name assert 'OVERRIDE' == table.columns['ln3'].verbose_name assert 'name' == table.columns['region'].verbose_name assert 'name' == table.columns['r1'].verbose_name assert 'name' == table.columns['r2'].verbose_name assert 'OVERRIDE' == table.columns['r3'].verbose_name assert "translation test" == table.columns["trans_test"].verbose_name assert "translation test lazy" == table.columns["trans_test_lazy"].verbose_name # ------------------------------------------------------------------------- # Now we'll try using a table with Meta.model class PersonTable(tables.Table): class Meta: model = Person # Issue #16 table = PersonTable([]) assert "translation test" == table.columns["trans_test"].verbose_name assert "translation test lazy" == table.columns["trans_test_lazy"].verbose_name def test_data_verbose_name(): table = tables.Table(Person.objects.all()) assert table.data.verbose_name == "person" assert table.data.verbose_name_plural == "people" def test_field_choices_used_to_translated_value(): """ When a model field uses the ``choices`` option, a table should render the "pretty" value rather than the database value. See issue #30 for details. """ LANGUAGES = ( ('en', 'English'), ('ru', 'Russian'), ) from django.db import models class Article(models.Model): name = models.CharField(max_length=200) language = models.CharField(max_length=200, choices=LANGUAGES) class Meta: app_label = 'django_tables2_test' def __unicode__(self): return self.name class ArticleTable(tables.Table): class Meta: model = Article table = ArticleTable([Article(name='English article', language='en'), Article(name='Russian article', language='ru')]) assert 'English' == table.rows[0]['language'] assert 'Russian' == table.rows[1]['language'] def test_column_mapped_to_nonexistant_field(): """ Issue #9 describes how if a Table has a column that has an accessor that targets a non-existent field, a FieldDoesNotExist error is raised. """ class FaultyPersonTable(PersonTable): missing = tables.Column() table = FaultyPersonTable(Person.objects.all()) table.as_html(request) # the bug would cause this to raise FieldDoesNotExist def test_should_support_rendering_multiple_times(): class MultiRenderTable(tables.Table): name = tables.Column() # test queryset data table = MultiRenderTable(Person.objects.all()) assert table.as_html(request) == table.as_html(request) def test_ordering(): class SimpleTable(tables.Table): name = tables.Column(order_by=("first_name", "last_name")) table = SimpleTable(Person.objects.all(), order_by="name") assert table.as_html(request) def test_default_order(): # 204 table = PersonTable(PersonProxy.objects.all()) Person.objects.create(first_name='Foo', last_name='Bar') Person.objects.create(first_name='Bradley', last_name='Ayers') table.data.order_by([]) assert list(table.rows[0])[1] == 'Ayers' def test_fields_should_implicitly_set_sequence(): class PersonTable(tables.Table): extra = tables.Column() class Meta: model = Person fields = ('last_name', 'first_name') table = PersonTable(Person.objects.all()) assert table.columns.names() == ['last_name', 'first_name', 'extra'] def test_model_properties_should_be_useable_for_columns(): class PersonTable(tables.Table): class Meta: model = Person fields = ('name', 'first_name') Person.objects.create(first_name='Bradley', last_name='Ayers') table = PersonTable(Person.objects.all()) assert list(table.rows[0]) == ['Bradley Ayers', 'Bradley'] def test_column_with_delete_accessor_shouldnt_delete_records(): class PersonTable(tables.Table): delete = tables.Column() Person.objects.create(first_name='Bradley', last_name='Ayers') table = PersonTable(Person.objects.all()) table.as_html(request) assert Person.objects.get(first_name='Bradley') def test_order_by_derived_from_queryset(): queryset = Person.objects.order_by("first_name", "last_name", "-occupation__name") class PersonTable(tables.Table): name = tables.Column(order_by=("first_name", "last_name")) occupation = tables.Column(order_by=("occupation__name",)) assert PersonTable(queryset.order_by("first_name", "last_name", "-occupation__name")).order_by == ("name", "-occupation") class PersonTable(PersonTable): class Meta: order_by = ("occupation", ) assert PersonTable(queryset.all()).order_by == ("occupation", ) def test_queryset_table_data_supports_ordering(): class Table(tables.Table): class Meta: model = Person for name in ("Bradley Ayers", "Stevie Armstrong"): first_name, last_name = name.split() Person.objects.create(first_name=first_name, last_name=last_name) table = Table(Person.objects.all()) assert table.rows[0]["first_name"] == "Bradley" table.order_by = "-first_name" assert table.rows[0]["first_name"] == "Stevie" def test_doesnotexist_from_accessor_should_use_default(): class Table(tables.Table): class Meta: model = Person default = "abc" fields = ("first_name", "last_name", "region") Person.objects.create(first_name="Brad", last_name="Ayers") table = Table(Person.objects.all()) assert table.rows[0]["first_name"] == "Brad" assert table.rows[0]["region"] == "abc" def test_unicode_field_names(): class Table(tables.Table): class Meta: model = Person fields = (six.text_type("first_name"),) Person.objects.create(first_name="Brad") table = Table(Person.objects.all()) assert table.rows[0]["first_name"] == "Brad" django-tables2-1.0.7/tests/test_rows.py000066400000000000000000000021551264227116200200660ustar00rootroot00000000000000# coding: utf-8 import pytest import django_tables2 as tables def test_bound_rows(): class SimpleTable(tables.Table): name = tables.Column() data = [ {'name': 'Bradley'}, {'name': 'Chris'}, {'name': 'Davina'}, ] table = SimpleTable(data) # iteration records = [] for row in table.rows: records.append(row.record) assert records == data def test_bound_row(): class SimpleTable(tables.Table): name = tables.Column() occupation = tables.Column() age = tables.Column() record = {'name': 'Bradley', 'age': 20, 'occupation': 'programmer'} table = SimpleTable([record]) row = table.rows[0] # integer indexing into a row assert row[0] == record['name'] assert row[1] == record['occupation'] assert row[2] == record['age'] with pytest.raises(IndexError): row[3] # column name indexing into a row assert row['name'] == record['name'] assert row['occupation'] == record['occupation'] assert row['age'] == record['age'] with pytest.raises(KeyError): row['gamma'] django-tables2-1.0.7/tests/test_templates.py000066400000000000000000000357671264227116200211110ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import django import lxml.etree import lxml.html import pytest import six from django.core.exceptions import ImproperlyConfigured from django.template import Context, RequestContext, Template from django.test import TransactionTestCase from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy import django_tables2 as tables from django_tables2.config import RequestConfig from .app.models import Person, Region from .utils import build_request, parse, translation try: from urlparse import parse_qs except ImportError: from urllib.parse import parse_qs request = build_request('/') class CountryTable(tables.Table): name = tables.Column() capital = tables.Column(orderable=False, verbose_name=ugettext_lazy("Capital")) population = tables.Column(verbose_name='Population Size') currency = tables.Column(visible=False) tld = tables.Column(visible=False, verbose_name='Domain') calling_code = tables.Column(accessor='cc', verbose_name='Phone Ext.') MEMORY_DATA = [ {'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'cc': 49}, {'name': 'France', 'population': 64, 'currency': 'Euro (€)', 'tld': 'fr', 'cc': 33}, {'name': 'Netherlands', 'capital': 'Amsterdam', 'cc': '31'}, {'name': 'Austria', 'cc': 43, 'currency': 'Euro (€)', 'population': 8} ] def test_as_html(): table = CountryTable(MEMORY_DATA) root = parse(table.as_html(request)) assert len(root.findall('.//thead/tr')) == 1 assert len(root.findall('.//thead/tr/th')) == 4 assert len(root.findall('.//tbody/tr')) == 4 assert len(root.findall('.//tbody/tr/td')) == 16 # no data with no empty_text table = CountryTable([]) root = parse(table.as_html(request)) assert 1 == len(root.findall('.//thead/tr')) assert 4 == len(root.findall('.//thead/tr/th')) assert 0 == len(root.findall('.//tbody/tr')) # no data WITH empty_text table = CountryTable([], empty_text='this table is empty') root = parse(table.as_html(request)) assert 1 == len(root.findall('.//thead/tr')) assert 4 == len(root.findall('.//thead/tr/th')) assert 1 == len(root.findall('.//tbody/tr')) assert 1 == len(root.findall('.//tbody/tr/td')) assert int(root.find('.//tbody/tr/td').attrib['colspan']) == len(root.findall('.//thead/tr/th')) assert root.find('.//tbody/tr/td').text == 'this table is empty' # data without header table = CountryTable(MEMORY_DATA, show_header=False) root = parse(table.as_html(request)) assert len(root.findall('.//thead')) == 0 assert len(root.findall('.//tbody/tr')) == 4 assert len(root.findall('.//tbody/tr/td')) == 16 # with custom template table = CountryTable([], template="django_tables2/table.html") table.as_html(request) def test_custom_rendering(): """For good measure, render some actual templates.""" countries = CountryTable(MEMORY_DATA) context = Context({'countries': countries}) # automatic and manual column verbose names template = Template('{% for column in countries.columns %}{{ column }}/' '{{ column.name }} {% endfor %}') result = ('Name/name Capital/capital Population Size/population ' 'Phone Ext./calling_code ') assert result == template.render(context) # row values template = Template('{% for row in countries.rows %}{% for value in row %}' '{{ value }} {% endfor %}{% endfor %}') result = ('Germany Berlin 83 49 France — 64 33 Netherlands Amsterdam ' '— 31 Austria — 8 43 ') assert result == template.render(context) def test_render_table_templatetag(settings): request = build_request('/') # ensure it works with a multi-order-by table = CountryTable(MEMORY_DATA, order_by=('name', 'population')) RequestConfig(request).configure(table) template = Template('{% load django_tables2 %}{% render_table table %}') html = template.render(Context({'request': request, 'table': table})) root = parse(html) assert len(root.findall('.//thead/tr')) == 1 assert len(root.findall('.//thead/tr/th')) == 4 assert len(root.findall('.//tbody/tr')) == 4 assert len(root.findall('.//tbody/tr/td')) == 16 assert root.find('ul[@class="pagination"]/li[@class="cardinality"]').text == '4 items' # no data with no empty_text table = CountryTable([]) template = Template('{% load django_tables2 %}{% render_table table %}') html = template.render(Context({'request': build_request('/'), 'table': table})) root = parse(html) assert len(root.findall('.//thead/tr')) == 1 assert len(root.findall('.//thead/tr/th')) == 4 assert len(root.findall('.//tbody/tr')) == 0 # no data WITH empty_text request = build_request('/') table = CountryTable([], empty_text='this table is empty') RequestConfig(request).configure(table) template = Template('{% load django_tables2 %}{% render_table table %}') html = template.render(Context({'request': request, 'table': table})) root = parse(html) assert len(root.findall('.//thead/tr')) == 1 assert len(root.findall('.//thead/tr/th')) == 4 assert len(root.findall('.//tbody/tr')) == 1 assert len(root.findall('.//tbody/tr/td')) == 1 assert int(root.find('.//tbody/tr/td').attrib['colspan']) == len(root.findall('.//thead/tr/th')) assert root.find('.//tbody/tr/td').text == 'this table is empty' # variable that doesn't exist (issue #8) template = Template('{% load django_tables2 %}' '{% render_table this_doesnt_exist %}') with pytest.raises(ValueError): settings.DEBUG = True template.render(Context()) # Should still be noisy with debug off with pytest.raises(ValueError): settings.DEBUG = False template.render(Context()) def test_render_table_should_support_template_argument(): table = CountryTable(MEMORY_DATA, order_by=('name', 'population')) template = Template('{% load django_tables2 %}' '{% render_table table "dummy.html" %}') request = build_request('/') context = RequestContext(request, {'table': table}) assert template.render(context) == 'dummy template contents\n' @pytest.mark.django_db def test_render_table_supports_queryset(): for name in ("Mackay", "Brisbane", "Maryborough"): Region.objects.create(name=name) template = Template('{% load django_tables2 %}{% render_table qs %}') html = template.render(Context({'qs': Region.objects.all(), 'request': build_request('/')})) root = parse(html) assert [e.text for e in root.findall('.//thead/tr/th/a')] == ["ID", "name", "mayor"] td = [[td.text for td in tr.findall('td')] for tr in root.findall('.//tbody/tr')] db = [] for region in Region.objects.all(): db.append([six.text_type(region.id), region.name, "—"]) assert td == db def test_querystring_templatetag(): template = Template('{% load django_tables2 %}' '{% querystring "name"="Brad" foo.bar=value %}') # Should be something like: ?name=Brad&a=b&c=5&age=21 xml = template.render(Context({ "request": build_request('/?a=b&name=dog&c=5'), "foo": {"bar": "age"}, "value": 21, })) # Ensure it's valid XML, retrieve the URL url = parse(xml).text qs = parse_qs(url[1:]) # everything after the ? pylint: disable=C0103 assert qs["name"] == ["Brad"] assert qs["age"] == ["21"] assert qs["a"] == ["b"] assert qs["c"] == ["5"] def test_querystring_templatetag_requires_request(): with pytest.raises(ImproperlyConfigured): (Template('{% load django_tables2 %}{% querystring "name"="Brad" %}') .render(Context())) def test_querystring_templatetag_supports_without(): context = Context({ "request": build_request('/?a=b&name=dog&c=5'), "a_var": "a", }) template = Template('{% load django_tables2 %}' '{% querystring "name"="Brad" without a_var %}') url = parse(template.render(context)).text qs = parse_qs(url[1:]) # trim the ? pylint: disable=C0103 assert set(qs.keys()) == set(["name", "c"]) # Try with only exclusions template = Template('{% load django_tables2 %}' '{% querystring without "a" "name" %}') url = parse(template.render(context)).text qs = parse_qs(url[1:]) # trim the ? pylint: disable=C0103 assert set(qs.keys()) == set(["c"]) def test_title_should_only_apply_to_words_without_uppercase_letters(): expectations = { "a brown fox": "A Brown Fox", "a brown foX": "A Brown foX", "black FBI": "Black FBI", "f.b.i": "F.B.I", "start 6pm": "Start 6pm", } for raw, expected in expectations.items(): template = Template("{% load django_tables2 %}{{ x|title }}") assert template.render(Context({"x": raw})) == expected def test_nospaceless_works(): template = Template("{% load django_tables2 %}" "{% spaceless %}a b {% nospaceless %}c d {% endnospaceless %}lic{% endspaceless %}") assert template.render(Context()) == "ab c d lic" def test_whitespace_is_preserved(): class TestTable(tables.Table): name = tables.Column(verbose_name=mark_safe("foo bar")) html = TestTable([{"name": mark_safe("foo bar")}]).as_html(request) tree = parse(html) assert "foo bar" in lxml.etree.tostring(tree.findall('.//thead/tr/th')[0], encoding='unicode') assert "foo bar" in lxml.etree.tostring(tree.findall('.//tbody/tr/td')[0], encoding='unicode') @pytest.mark.django_db def test_as_html_db_queries(transactional_db): class PersonTable(tables.Table): class Meta: model = Person # TODO: check why this is commented out # with queries(count=1): # PersonTable(Person.objects.all()).as_html(request) @pytest.mark.django_db class TestQueries(TransactionTestCase): def test_as_html_db_queries(self): class PersonTable(tables.Table): class Meta: model = Person with self.assertNumQueries(1): PersonTable(Person.objects.all()).as_html(request) def test_render_table_db_queries(self): Person.objects.create(first_name="brad", last_name="ayers") Person.objects.create(first_name="davina", last_name="adisusila") class PersonTable(tables.Table): class Meta: model = Person per_page = 1 with self.assertNumQueries(2): # one query for pagination: .count() # one query for page records: .all()[start:end] request = build_request('/') table = PersonTable(Person.objects.all()) RequestConfig(request).configure(table) # render (Template('{% load django_tables2 %}{% render_table table %}') .render(Context({'table': table, 'request': request}))) @pytest.mark.skipif(django.VERSION < (1, 3), reason="requires Django >= 1.3") def test_localization_check(settings): def get_cond_localized_table(localizeit=None): ''' helper function for defining Table class conditionally ''' class TestTable(tables.Table): name = tables.Column(verbose_name="my column", localize=localizeit) return TestTable simple_test_data = [{'name': 1234.5}] expected_reults = { None: '1234.5', False: '1234.5', True: '1 234,5' # non-breaking space } # no localization html = get_cond_localized_table(None)(simple_test_data).as_html(request) assert ''.format(expected_reults[None]) in html # unlocalize html = get_cond_localized_table(False)(simple_test_data).as_html(request) assert ''.format(expected_reults[False]) in html settings.USE_L10N = True settings.USE_THOUSAND_SEPARATOR = True with translation("pl"): # with default polish locales and enabled thousand separator # 1234.5 is formatted as "1 234,5" with nbsp html = get_cond_localized_table(True)(simple_test_data).as_html(request) assert ''.format(expected_reults[True]) in html # with localize = False there should be no formatting html = get_cond_localized_table(False)(simple_test_data).as_html(request) assert ''.format(expected_reults[False]) in html # with localize = None and USE_L10N = True # there should be the same formatting as with localize = True html = get_cond_localized_table(None)(simple_test_data).as_html(request) assert ''.format(expected_reults[True]) in html @pytest.mark.skipif(django.VERSION < (1, 3), reason="requires Django >= 1.3") def test_localization_check_in_meta(settings): class TableNoLocalize(tables.Table): name = tables.Column(verbose_name="my column") class Meta: default = "---" class TableLocalize(tables.Table): name = tables.Column(verbose_name="my column") class Meta: default = "---" localize = ('name',) class TableUnlocalize(tables.Table): name = tables.Column(verbose_name="my column") class Meta: default = "---" unlocalize = ('name',) class TableLocalizePrecedence(tables.Table): name = tables.Column(verbose_name="my column") class Meta: default = "---" unlocalize = ('name',) localize = ('name',) simple_test_data = [{'name': 1234.5}] expected_reults = { None: '1234.5', False: '1234.5', True: '1{0}234,5'.format(' ') # non-breaking space } # No localize html = TableNoLocalize(simple_test_data).as_html(request) assert ''.format(expected_reults[None]) in html settings.USE_L10N = True settings.USE_THOUSAND_SEPARATOR = True with translation("pl"): # the same as in localization_check. # with localization and polish locale we get formatted output html = TableNoLocalize(simple_test_data).as_html(request) assert ''.format(expected_reults[True]) in html # localize html = TableLocalize(simple_test_data).as_html(request) assert ''.format(expected_reults[True]) in html # unlocalize html = TableUnlocalize(simple_test_data).as_html(request) assert ''.format(expected_reults[False]) in html # test unlocalize higher precedence html = TableLocalizePrecedence(simple_test_data).as_html(request) assert ''.format(expected_reults[False]) in html django-tables2-1.0.7/tests/test_utils.py000066400000000000000000000065011264227116200202330ustar00rootroot00000000000000# coding: utf-8 # from attest import assert_hook, raises, Tests import six from django_tables2.utils import (Accessor, AttributeDict, OrderBy, OrderByTuple, computed_values, segment) import pytest def test_orderbytuple(): obt = OrderByTuple(('a', 'b', 'c')) assert obt == (OrderBy('a'), OrderBy('b'), OrderBy('c')) # indexing assert obt[0] == OrderBy('a') assert obt['b'] == OrderBy('b') with pytest.raises(KeyError): obt['d'] with pytest.raises(TypeError): obt[('tuple', )] # .get sentinel = object() assert obt.get('b', sentinel) is obt['b'] # keying assert obt.get('-', sentinel) is sentinel assert obt.get(0, sentinel) is obt['a'] # indexing assert obt.get(3, sentinel) is sentinel # .opposite assert OrderByTuple(('a', '-b', 'c')).opposite == ('-a', 'b', '-c') # in assert 'a' in obt and '-a' in obt def test_orderbytuple_sort_key_multiple(): obt = OrderByTuple(('a', '-b')) items = [ {"a": 1, "b": 2}, {"a": 1, "b": 3}, ] assert sorted(items, key=obt.key) == [ {"a": 1, "b": 3}, {"a": 1, "b": 2}, ] def test_orderbytuple_sort_key_empty_comes_first(): obt = OrderByTuple(('a')) items = [ {"a": 1}, {"a": ""}, {"a": 2}, ] if six.PY3: assert sorted(items, key=obt.key) == [ {"a": ""}, {"a": 1}, {"a": 2}, ] else: assert sorted(items, key=obt.key) == [ {"a": 1}, {"a": 2}, {"a": ""}, ] def test_orderby(): a = OrderBy('a') assert 'a' == a assert 'a' == a.bare assert '-a' == a.opposite assert True == a.is_ascending assert False == a.is_descending b = OrderBy('-b') assert '-b' == b assert 'b' == b.bare assert 'b' == b.opposite assert True == b.is_descending assert False == b.is_ascending def test_accessor(): x = Accessor('0') assert 'B' == x.resolve('Brad') x = Accessor('1') assert 'r' == x.resolve('Brad') x = Accessor('2.upper') assert 'A' == x.resolve('Brad') x = Accessor('2.upper.__len__') assert 1 == x.resolve('Brad') x = Accessor('') assert 'Brad' == x.resolve('Brad') def test_accessor_wont_honors_alters_data(): class Foo(object): deleted = False def delete(self): self.deleted = True delete.alters_data = True foo = Foo() with pytest.raises(ValueError): Accessor('delete').resolve(foo) assert foo.deleted is False def test_accessor_can_be_quiet(): foo = {} assert Accessor("bar").resolve(foo, quiet=True) is None def test_attribute_dict_handles_escaping(): x = AttributeDict({"x": '"\'x&'}) assert x.as_html() == 'x=""'x&"' def test_compute_values_supports_shallow_structures(): x = computed_values({"foo": lambda: "bar"}) assert x == {"foo": "bar"} def test_compute_values_supports_nested_structures(): x = computed_values({"foo": lambda: {"bar": lambda: "baz"}}) assert x == {"foo": {"bar": "baz"}} def test_segment_should_return_all_candidates(): assert set(segment(("a", "-b", "c"), { "x": "a", "y": ("b", "-c"), "-z": ("b", "-c"), })) == { ("x", "-y"), ("x", "z"), } django-tables2-1.0.7/tests/test_views.py000066400000000000000000000031661264227116200202340ustar00rootroot00000000000000# coding: utf-8 import django_tables2 as tables import pytest from .app.models import Region from .utils import build_request USING_CBV = hasattr(tables, "SingleTableView") class DispatchHookMixin(object): """ Returns a response *and* reference to the view. """ def dispatch(self, *args, **kwargs): return super(DispatchHookMixin, self).dispatch(*args, **kwargs), self class SimpleTable(tables.Table): class Meta: model = Region @pytest.mark.skipif(not USING_CBV, reason="requires class based views") @pytest.mark.django_db def test_view_should_support_pagination_options(): for name in ("Queensland", "New South Wales", "Victoria", "Tasmania"): Region.objects.create(name=name) class SimpleView(DispatchHookMixin, tables.SingleTableView): table_class = SimpleTable table_pagination = {"per_page": 1} model = Region # needed for ListView request = build_request('/') response, view = SimpleView.as_view()(request) assert view.get_table().paginator.num_pages == 4 @pytest.mark.skipif(not USING_CBV, reason="requires class based views") def test_should_support_explicit_table_data(): class SimpleView(DispatchHookMixin, tables.SingleTableView): table_class = SimpleTable table_data = [ {"name": "Queensland"}, {"name": "New South Wales"}, {"name": "Victoria"}, ] table_pagination = {"per_page": 1} model = Region # needed for ListView request = build_request('/') response, view = SimpleView.as_view()(request) assert view.get_table().paginator.num_pages == 3 django-tables2-1.0.7/tests/utils.py000066400000000000000000000047341264227116200172020ustar00rootroot00000000000000import warnings from contextlib import contextmanager import lxml.etree import lxml.html import six from django.core.handlers.wsgi import WSGIRequest from django.template import RequestContext from django.test.client import FakePayload def parse(html): return lxml.etree.fromstring(html) def attrs(xml): """ Helper function that returns a dict of XML attributes, given an element. """ return lxml.html.fromstring(xml).attrib @contextmanager def warns(warning_class): with warnings.catch_warnings(record=True) as ws: warnings.simplefilter("always") yield ws assert any((issubclass(w.category, DeprecationWarning) for w in ws)) @contextmanager def translation(language_code, deactivate=False): """ Port of django.utils.translation.override from Django 1.4 @param language_code: a language code or ``None``. If ``None``, translation is disabled and raw translation strings are used @param deactivate: If ``True``, when leaving the manager revert to the default behaviour (i.e. ``settings.LANGUAGE_CODE``) rather than the translation that was active prior to entering. """ from django.utils import translation original = translation.get_language() if language_code is not None: translation.activate(language_code) else: translation.deactivate_all() try: yield finally: if deactivate: translation.deactivate() else: translation.activate(original) def build_request(uri='/'): """ Return a fresh HTTP GET / request. This is essentially a heavily cutdown version of Django 1.3's `~django.test.client.RequestFactory`. """ path, _, querystring = uri.partition('?') return WSGIRequest({ 'CONTENT_TYPE': 'text/html; charset=utf-8', 'PATH_INFO': path, 'QUERY_STRING': querystring, 'REMOTE_ADDR': '127.0.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'SERVER_NAME': 'testserver', 'SERVER_PORT': '80', 'SERVER_PROTOCOL': 'HTTP/1.1', 'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': FakePayload(b''), 'wsgi.errors': six.StringIO(), 'wsgi.multiprocess': True, 'wsgi.multithread': False, 'wsgi.run_once': False, }) django-tables2-1.0.7/tox.ini000066400000000000000000000013311264227116200156270ustar00rootroot00000000000000[pytest] DJANGO_SETTINGS_MODULE=tests.app.settings [tox] args_are_paths = false envlist = {py27,py33,py34}-{1.7,1.8}, {py27,py34,py35}-{1.9,master} [testenv] basepython = py27: python2.7 py33: python3.3 py34: python3.4 py35: python3.5 usedevelop = true pip_pre = true setenv = PYTHONPATH={toxinidir} commands = py.test --cov=django_tables2 coverage html deps = 1.7: Django>=1.7,<1.8 1.8: Django>=1.8,<1.9 1.9: Django>=1.9,<1.10 master: https://github.com/django/django/archive/master.tar.gz -r{toxinidir}/requirements/common.pip pytest-cov coverage [testenv:docs] changedir = docs commands = make html deps = -r{toxinidir}/requirements/django-dev.pip Sphinx
This is a footer
{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}