pax_global_header00006660000000000000000000000064131755736070014530gustar00rootroot0000000000000052 comment=a1b6e47fe00e0093081ec2e319a728c9c6f5240e django-tables2-1.14.2/000077500000000000000000000000001317557360700144115ustar00rootroot00000000000000django-tables2-1.14.2/.coveragerc000066400000000000000000000001371317557360700165330ustar00rootroot00000000000000[run] source = django_tables2 tests branch = true [html] directory = reports/htmlcov django-tables2-1.14.2/.gitignore000066400000000000000000000003661317557360700164060ustar00rootroot00000000000000*.pyc /.env /reports /*.sublime-* /*.komodoproject /*.tmproj /*.egg-info/ /*.egg /.tox /.coverage /MANIFEST /dist/ /build/ /docs/_build/ /docs/pages/CHANGELOG.md /example/database.sqlite /example/.env /report.pylint .cache/ .python-version .idea django-tables2-1.14.2/.isort.cfg000066400000000000000000000002071317557360700163070ustar00rootroot00000000000000[settings] line_length = 120 known_third_party=django,django_filter,pytest,fudge,lxml known_first_party=django_tables2 skip=migrations django-tables2-1.14.2/.travis.yml000066400000000000000000000002731317557360700165240ustar00rootroot00000000000000language: python python: - "2.7" - "3.3" - "3.4" - "3.5" - "3.6" install: - pip install tox-travis python-coveralls script: - tox after_success: coveralls django-tables2-1.14.2/CHANGELOG.md000066400000000000000000000570411317557360700162310ustar00rootroot00000000000000# Change log ## 1.14.2 (2017-10-30) - Added a `row_counter` variable to the template context in `TemplateColumn` (fixes [#448](https://github.com/jieter/django-tables2/issues/488)) ## 1.14.1 (2017-10-30) - Do not fail if `orderable=False` is passed to `ManyToManyColumn()` ## 1.14.0 (2017-10-30) - Added `separator` argument to `ManyToManyColumn`. - Allow `mark_safe()`'d strings from `ManyToManyColumn.tranform()` - Disabled ordering on `ManyToManyColumns` by default. ## 1.13.0 (2017-10-17) - Made positional `data` argument to the table `__init__()` a keyword argument to make inheritance easier. Will raise a `TypeError` if ommitted. ## 1.12.0 (2017-10-10) - Allow export filename customization [#484](https://github.com/bradleyayers/django-tables2/pull/484) by [@federicobond](https://github.com/federicobond) - Fixed a bug where template columns were not rendered for pinned rows ([#483](https://github.com/bradleyayers/django-tables2/pull/483) by [@khirstinova](https://github.com/khirstinova), fixes [#482](https://github.com/bradleyayers/django-tables2/issues/482)) ## 1.11.0 (2017-09-15) - Added Hungarian translation [#471](https://github.com/bradleyayers/django-tables2/pull/471) by [@hmikihth](https://github.com/hmikihth). - Added TemplateColumn.value() and enhanced export docs (fixes [#470](https://github.com/bradleyayers/django-tables2/issues/470)) - Fixed display of pinned rows if table has no data. [#477](https://github.com/bradleyayers/django-tables2/pull/477) by [@khirstinova](https://github.com/khirstinova) ## 1.10.0 (2017-06-30) - Added `ManyToManyColumn` automatically added for `ManyToManyField`s. ## 1.9.1 (2017-06-29) - Allow customizing the value used in `Table.as_values()` (when using a `render_` method) using a `value_` method. (fixes [#458](https://github.com/bradleyayers/django-tables2/issues/458)) - Allow excluding columns from the `Table.as_values()` output. (fixes [#459](https://github.com/bradleyayers/django-tables2/issues/459)) - Fixed unicode handling for columhn headers in `Table.as_values()` ## 1.9.0 (2017-06-22) - Allow computable attrs for ``-tags from `Table.attrs` ([#457](https://github.com/bradleyayers/django-tables2/pull/457), fixes [#451](https://github.com/bradleyayers/django-tables2/issues/451)) ## 1.8.0 (2017-06-17) - Feature: Added an `ExportMixin` to export table data in various export formats (CSV, XLS, etc.) using [tablib](http://docs.python-tablib.org/en/latest/). - Defer expanding `Meta.sequence` to `Table.__init__`, to make sequence work in combination with `extra_columns` (fixes [#450](https://github.com/bradleyayers/django-tables2/issues/450)) - Fixed a crash when `MultiTableMixin.get_tables()` returned an empty array ([#454](https://github.com/bradleyayers/django-tables2/pull/455) by [@pypetey](https://github.com/pypetey) ## 1.7.1 (2017-06-02) - Call before_render when rendering with the render_table template tag (fixes [#447](https://github.com/bradleyayers/django-tables2/issues/447)) ## 1.7.0 (2017-06-01) - Make `title()` lazy ([#443](https://github.com/bradleyayers/django-tables2/pull/443) by [@ygwain](https://github.com/ygwain), fixes [#438](https://github.com/bradleyayers/django-tables2/issues/438)) - Fix `__all__` by populating them with the names of the items to export instead of the items themself. - Allow adding extra columns to an instance using the `extra_columns` argument. Fixes [#403](https://github.com/bradleyayers/django-tables2/issues/403), [#70](https://github.com/bradleyayers/django-tables2/issues/70) - Added a hook `before_render` to allow last-minute changes to the table before rendering. - Added `BoundColumns.show()` and `BoundColumns.hide()` to show/hide columns on an instance of a `Table`. - Use `.verbose_name`/`.verbose_name_plural` if it exists to name the items in the list. (fixes [#166](https://github.com/bradleyayers/django-tables2/issues/166)) ## 1.6.1 (2017-05-08) - Add missing pagination to the responsive bootstrap template ([#440](https://github.com/bradleyayers/django-tables2/pull/440) by [@tobiasmcnulty](https://github.com/tobiasmcnulty)) ## 1.6.0 (2017-05-01) - Add new template `bootstrap-responsive.html` to generate a responsive bootstrap table. (Fixes [#436](https://github.com/bradleyayers/django-tables2/issues/436)) ## 1.5.0 (2017-04-18) _Full disclosure: as of april 1st, 2017, I am an employee of [Zostera](http://zostera.nl/), as such I will continue to maintain and improve django-tables2._ - Made `TableBase.as_values()` an interator ([#432](https://github.com/bradleyayers/django-tables2/pull/432) by [@pziarsolo](https://github.com/pziarsolo)) - Added `JSONField` for data in JSON format. - Added `__all__` in `django_tables2/__init__.py` and `django_tables2/columns/__init__.py` - Added a setting `DJANGO_TABLES2_TEMPLATE` to allow project-wide overriding of the template used to render tables (fixes [#434](https://github.com/bradleyayers/django-tables2/issues/434)). ## 1.4.2 (2017-03-06) - Feature: Pinned rows ([#411](https://github.com/bradleyayers/django-tables2/pull/411) by [@djk2](https://github.com/djk2), fixes [#406](https://github.com/bradleyayers/django-tables2/issues/406)) - Fix an issue where `ValueError` was raised while using a view with a `get_queryset()` method defined. (fix with [#423](https://github.com/bradleyayers/django-tables2/pull/423) by [@desecho](https://github.com/desecho)) ## 1.4.1 (2017-02-27) - Fix urls to screenshots in on pypi description (fixes [ #398](https://github.com/bradleyayers/django-tables2/issues/398)) - Prevent superfluous spaces when a callable `row_attrs['class']` returns an empty string ([#417](https://github.com/bradleyayers/django-tables2/pull/417 by [@Superman8218](https://github.com/Superman8218)), fixes [#416](https://github.com/bradleyayers/django-tables2/issues/416)) ## 1.4.0 (2017-02-27) - Return `None` from `Table.as_values()` for missing values. [#419](https://github.com/bradleyayers/django-tables2/pull/419) - Fix ordering by custom fields, and refactor `TableData` [#424](https://github.com/bradleyayers/django-tables2/pull/424), fixes [#413](https://github.com/bradleyayers/django-tables2/issues/413) - Revert removing `TableData.__iter__()` (removed in [this commit](https://github.com/bradleyayers/django-tables2/commit/8fe9826429e6945a9258bc181fcbd711b282dba9)), fixes [#427](https://github.com/bradleyayers/django-tables2/issues/427), [#361](https://github.com/bradleyayers/django-tables2/issues/361) and [#421](https://github.com/bradleyayers/django-tables2/issues/421). ## 1.3.0 (2017-01-20) - Implement method `Table.as_values()` to get it's raw values. [#394](https://github.com/bradleyayers/django-tables2/pull/394) by [@intiocean](https://github.com/intiocean) - Fix some compatibility issues with django 2.0 [#408](https://github.com/bradleyayers/django-tables2/pull/409) by [djk2](https://github.com/djk2) ## 1.2.9 (2016-12-21) - Documentation for `None`-column attributes [#401](https://github.com/bradleyayers/django-tables2/pull/401) by [@dyve](https://github.com/dyve) ## 1.2.8 (2016-12-21) - `None`-column attributes on child class overwrite column attributes of parent class [#400](https://github.com/bradleyayers/django-tables2/pull/400) by [@dyve](https://github.com/dyve) ## 1.2.7 (2016-12-12) - Apply `title` to a column's `verbose_name` when it is derived from a model, fixes [#249](https://github.com/bradleyayers/django-tables2/issues/249). ([#382](https://github.com/bradleyayers/django-tables2/pull/382) by [@shawnnapora](https://github.com/shawnnapora)) - Update documentation after deprecation of `STATIC_URL` in django ([#384](https://github.com/bradleyayers/django-tables2/pull/384), by [@velaia](https://github.com/velaia)) - Cleanup of the templates, making the output more equal ([#381](https://github.com/bradleyayers/django-tables2/pull/381) by [@ralgozino](https://github.com/ralgozino)) - Use new location for `urlresolvers` in Django and add backwards compatible import ([#388](https://github.com/bradleyayers/django-tables2/pull/388) by [@felixxm](https://github.com/felixxm)) - Fix a bug where using `sequence` and then `exclude` in a child table would result in a `KeyError` - Some documentation fixes and cleanups. ## 1.2.6 (2016-09-06) - Added `get_table_kwargs()` method to `SingleTableMixin` to allow passing custom keyword arguments to the `Table` constructor. ([#366](https://github.com/bradleyayers/django-tables2/pull/366) by [@fritz-k](https://github.com/fritz-k)) - Allow the children of `TableBase` render in the `{% render_table %}` template tag. ([#377](https://github.com/bradleyayers/django-tables2/pull/377) by [@shawnnapora](https://github.com/shawnnapora)) - Refactor `BoundColumn` attributes to allow override of CSS class names, fixes [#349](https://github.com/bradleyayers/django-tables2/issues/349) ([#370](https://github.com/bradleyayers/django-tables2/pull/370) by [@graup](https://github.com/graup)). Current behaviour should be intact, we will change the default in the future so it will **not** add the column name to the list of CSS classes. ## 1.2.5 (2016-07-30) - Fixed an issue preventing the rest of the row being rendered if a `BooleanColumn` was in the table for a model without custom choices defined on the model field. ([#360](https://github.com/bradleyayers/django-tables2/issues/360)) ## 1.2.4 (2016-07-28) - Added Norwegian Locale ([#356](https://github.com/bradleyayers/django-tables2/issues/356) by [@fanzypantz](https://github.com/fanzypantz)) - Restore default pagination for `SingleTableMixin`, fixes [#354](https://github.com/bradleyayers/django-tables2/issues/354) ([#395](https://github.com/bradleyayers/django-tables2/pull/359) by [@graup](https://github.com/graup)) ## 1.2.3 (2016-07-05) - Accept `text` parameter in `FileColumn`, analogous to `LinkColumn` ([#343](https://github.com/bradleyayers/django-tables2/pull/343) by [@graup](https://github.com/graup)) - Fix TemplateColumn RemovedInDjango110Warning fixes [#346](https://github.com/bradleyayers/django-tables2/issues/346). - Use field name in RelatedColumnLink ([#350](https://github.com/bradleyayers/django-tables2/pull/350), fixes [#347](https://github.com/bradleyayers/django-tables2/issues/347)) ## v1.2.2 (2016-06-04) - Allow use of custom class names for ordered columns through `attrs`. ( [#329](https://github.com/bradleyayers/django-tables2/pull/329) by [@theTarkus](https://github.com/theTarkus)) - Column ordering queryset passthrough ([#330](https://github.com/bradleyayers/django-tables2/pull/330) by [@theTarkus](https://github.com/theTarkus)) - Cleanup/restructuring of [documentation](http://django-tables2.readthedocs.io/), ([#325](https://github.com/bradleyayers/django-tables2/pull/325)) - Fixed an issue where explicitly defined column options where not preserved over inheritance ([#339](https://github.com/bradleyayers/django-tables2/pull/339), [issue #337](https://github.com/bradleyayers/django-tables2/issues/337)) - Fixed an issue where `exclude` in combination with `sequence` raised a KeyError ([#341](https://github.com/bradleyayers/django-tables2/pull/341), [issue #205](https://github.com/bradleyayers/django-tables2/issues/205)) ## v1.2.1 (2016-05-09) - table footers (#323) - Non-field based `LinkColumn` only renders default value if lookup fails. (#322) - Accept `text` parameter in `BaseLinkColumn`-based columns. (#322) - Pass the table instance into SingleTableMixin's `get_table_pagination` (#320 by [@georgema1982](https://github.com/georgema1982), fixes #319) - Check if the view has `paginate_by` before before trying to access it. (fixes #326) ## v1.2.0 (2016-05-02) - Allow custom attributes for rows (fixes #47) ## v1.1.8 (2016-05-02) - Ability to change the body of the ``-tag, by passing `text` kwarg to the columns inheriting from BaseLinkColumn (#318 by [@desecho](https://github.com/desecho), #322) - Non-field based LinkColumn only renders default value if lookup fails and text is not set. (#322, fixes #257) ## v1.1.7 (2016-04-26) - Added Italian translation (#315 by [@paolodina](https://github.com/paolodina) - Added Dutch translation. - Fixed {% blocktrans %} template whitespace issues - Fixed errors when using a column named `items` (#316) - Obey `paginate_by` (from `MultipleObjectMixin`) if no later pagination is defined (#242) ## v1.1.6 (2016-04-02) - Correct error message about request context processors for current Django (#314) - Skipped 1.1.5 due to an error while creating the tag. ## v1.1.4 (2016-03-22) - Fix broken `setup.py` if Django is not installed before django-tables2 (fixes #312) ## v1.1.3 (2016-03-21) - Drop support for Django 1.7 - Add argument to CheckBoxColumn to render it as checked (original PR: #208) ## v1.1.2 (2016-02-16) - Fix `BooleanColumn` with choices set will always render as if `True` (#301) - Fix a bug with `TemplateColumn` while using cached template loader (#75) ## v1.1.1 (2016-01-26) - Allow Meta.fields to be a list as well as a tuple (#250) - Call template.render with a dict in Django >= 1.8. (#298) - Added `RelatedLinkColumn()` to render links to related objects (#297) - Remove default value from request param to table.as_html() ## v1.1.0 (2016-01-19) - Add tests for `TimeColumn` - Remove `sortable` argument for `Table` and Column constructors and its associated methods. Deprecated since 2012. - Remove deprecated aliases for `attrs` in `CheckboxColumn`. - Remove deprecated `OrderByTuple` `cmp` method (deprecated since 2013). - Add bootstrap template and (#293, fixes #141, #285) - Fix different html for tables with and without pagination (#293, fixes #149, #285) - Remove `{% nospaceless %}` template tag and remove wrapping template in `{% spaceless %}` **Possible breaking change**, if you use custom templates. ## 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. - 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.14.2/CONTRIBUTING.md000066400000000000000000000031351317557360700166440ustar00rootroot00000000000000# Contributing to django-tables2 You are welcome to contribute to the development of `django-tables2` in various ways: - Discover and [report bugs](https://github.com/bradleyayers/django-tables2/issues/new). Make sure to include a minimal example to show your problem. - Propose features, add tests or fix bugs by [opening a Pull Request](https://github.com/bradleyayers/django-tables2/compare) - Fix documenation or translations When contributing code or making bug fixes, we need to have unit tests to verify the expected behaviour. ## Running the tests With `tox` installed, you can run the test suite by typing `tox`. It will take care of installing the correct dependencies. During development, you might not want to wait for the tests to run in all environments. In that case, use the `-e` argument to specify an environment: `tox -e py27-1.9` to run the tests in python 2.7 with Django 1.9, or `PYTHONPATH=. py.test` to run the tests against your current environment (which is even quicker). ## Code coverage To generate a html coverage report: ``` PYTHONPATH=. py.test -s --cov=django_tables2 --cov-report html ``` ## Building the documentation If you want to build the docs from within a virtualenv, and Sphinx is installed globally, use: ``` cd docs/ 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 `./setup.py tag` or `git tag -a v1.0.6 -m 'tagging v1.0.6'` 4. Run `./setup.py publish` or `python setup.py sdist upload --sign --identity=`. django-tables2-1.14.2/LICENSE000066400000000000000000000031671317557360700154250ustar00rootroot00000000000000All 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.14.2/MANIFEST.in000066400000000000000000000004401317557360700161450ustar00rootroot00000000000000include 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.14.2/README.rst000066400000000000000000000055261317557360700161100ustar00rootroot00000000000000django-tables2 - An app for creating HTML tables ------------------------------------------------ .. image:: https://badge.fury.io/py/django-tables2.svg :target: https://pypi.python.org/pypi/django-tables2 :alt: Latest PyPI version .. image:: https://travis-ci.org/bradleyayers/django-tables2.svg?branch=master :target: https://travis-ci.org/bradleyayers/django-tables2 :alt: Travis CI 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. - `Available on pypi `_ - Tested against currently supported versions of Django `and the python versions Django supports `_ (see `Travis CI `_) - `Documentation on readthedocs.org `_ - `Bug tracker `_ Features: - 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. .. image:: https://cdn.rawgit.com/bradleyayers/django-tables2/1044316e/docs/img/example.png :alt: An example table rendered using django-tables2 .. image:: https://cdn.rawgit.com/bradleyayers/django-tables2/1044316e/docs/img/bootstrap.png :alt: An example table rendered using django-tables2 and bootstrap theme .. image:: https://cdn.rawgit.com/bradleyayers/django-tables2/1044316e/docs/img/semantic.png :alt: An example table rendered using django-tables2 and semantic-ui theme Example ------- Start by adding ``django_tables2`` to your ``INSTALLED_APPS`` setting like this: .. code:: python INSTALLED_APPS = ( ..., 'django_tables2', ) Creating a table for a model `Simple` is as simple as: .. code:: python import django_tables2 as tables class SimpleTable(tables.Table): class Meta: model = Simple This would then be used in a view: .. code:: python def simple_list(request): queryset = Simple.objects.all() table = SimpleTable(queryset) return render(request, 'simple_list.html', {'table': table}) And finally in the template: .. code:: {% 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 `_ for more details. django-tables2-1.14.2/django_tables2/000077500000000000000000000000001317557360700172675ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/__init__.py000066400000000000000000000015201317557360700213760ustar00rootroot00000000000000# coding: utf-8 from .tables import Table, TableBase from .columns import (BooleanColumn, Column, CheckBoxColumn, DateColumn, DateTimeColumn, EmailColumn, FileColumn, JSONColumn, LinkColumn, ManyToManyColumn, RelatedLinkColumn, TemplateColumn, TimeColumn, URLColumn) from .config import RequestConfig from .utils import A from .views import SingleTableMixin, SingleTableView, MultiTableMixin __version__ = '1.14.2' __all__ = ( 'Table', 'TableBase', 'BooleanColumn', 'Column', 'CheckBoxColumn', 'DateColumn', 'DateTimeColumn', 'EmailColumn', 'FileColumn', 'JSONColumn', 'LinkColumn', 'ManyToManyColumn', 'RelatedLinkColumn', 'TemplateColumn', 'TimeColumn', 'URLColumn', 'RequestConfig', 'A', 'SingleTableMixin', 'SingleTableView', 'MultiTableMixin' ) django-tables2-1.14.2/django_tables2/columns/000077500000000000000000000000001317557360700207475ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/columns/__init__.py000066400000000000000000000015021317557360700230560ustar00rootroot00000000000000from .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 .jsoncolumn import JSONColumn from .linkcolumn import LinkColumn, RelatedLinkColumn from .manytomanycolumn import ManyToManyColumn from .templatecolumn import TemplateColumn from .urlcolumn import URLColumn from .timecolumn import TimeColumn __all__ = ( 'library', 'BoundColumn', 'BoundColumns', 'Column', 'BooleanColumn', 'CheckBoxColumn', 'DateColumn', 'DateTimeColumn', 'EmailColumn', 'FileColumn', 'JSONColumn', 'LinkColumn', 'ManyToManyColumn', 'RelatedLinkColumn', 'TemplateColumn', 'URLColumn', 'TimeColumn' ) django-tables2-1.14.2/django_tables2/columns/base.py000066400000000000000000000604021317557360700222350ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from collections import OrderedDict from itertools import islice from django.utils import six from django.utils.safestring import SafeData from django_tables2.templatetags.django_tables2 import title from django_tables2.utils import Accessor, AttributeDict, OrderBy, OrderByTuple, call_with_appropriate, computed_values 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 or `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): ''' Represents a single column of a table. `.Column` objects control the way a column (including the cells that fall within it) are rendered. Arguments: attrs (dict): HTML attributes for elements that make up the column. This API is extended by subclasses to allow arbitrary HTML attributes to be added to the output. By default `.Column` supports: - *th* -- ``table/thead/tr/th`` elements - *td* -- ``table/tbody/tr/td`` elements - *cell* -- fallback if *th* or *td* isn't defined accessor (str or `~.Accessor`): An accessor that describes how to extract values for this column from the :term:`table data`. default (str or callable): 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). empty_values (iterable): list of values considered as a missing value, for which the column will render the default value. Defaults to `(None, '')` exclude_from_export (bool): If `True`, this column will not be added to the data iterator returned from as_values(). footer (str, callable): Defines the footer of this column. If a callable is passed, it can take optional keyword argumetns `column`, `bound_colun` and `table`. order_by (str, tuple or `.Accessor`): Allows one or more accessors to be used for ordering rather than *accessor*. orderable (bool): If `False`, this column will not be allowed to influence row ordering/sorting. verbose_name (str): A human readable version of the column name. visible (bool): If `True`, this column will be rendered. localize: If the cells in this column will be localized by the `localize` filter: - If `True`, force localization - If `False`, values are not localized - If `None` (default), localization depends on the ``USE_L10N`` setting. .. [1] The provided callable object must not expect to receive any arguments. ''' # Tracks each time a Column instance is created. Used to retain order. creation_counter = 0 empty_values = (None, '') # Explicit is set to True if the column is defined as an attribute of a # class, used to give explicit columns precedence. _explicit = False def __init__(self, verbose_name=None, accessor=None, default=None, visible=True, orderable=None, attrs=None, order_by=None, empty_values=None, localize=None, footer=None, exclude_from_export=False): 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 = Accessor(accessor) if accessor else None self._default = default self.verbose_name = verbose_name self.visible = visible 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 self._footer = footer self.exclude_from_export = exclude_from_export @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 footer(self, bound_column, table): ''' Returns the content of the footer, if specified. ''' footer_kwargs = { 'column': self, 'bound_column': bound_column, 'table': table } if self._footer is not None: if callable(self._footer): return call_with_appropriate(self._footer, footer_kwargs) else: return self._footer if hasattr(self, 'render_footer'): return call_with_appropriate(self.render_footer, footer_kwargs) return '' 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`. 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 def value(self, **kwargs): ''' Returns the content for a specific cell similarly to `.render` however without any html content. This can be used to get the data in the formatted as it is presented but in a form that could be added to a csv file. The default implementation just calls the `render` function but any subclasses where `render` returns html content should override this method. See `LinkColumn` for an example. ''' value = call_with_appropriate(self.render, kwargs) return value def order(self, queryset, is_descending): ''' Returns the queryset of the table. This method can be overridden by :ref:`table.order_FOO` methods on the table or by subclassing `.Column`; but only overrides if second element in return tuple is True. returns: Tuple (queryset, boolean) ''' return (queryset, False) @classmethod def from_field(cls, field): ''' Return a specialised column for the model field or `None`. Arguments: field (Model Field instance): the field that needs a suitable column 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=title(verbose_name)) @six.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 this class. arguments: table (`~.Table`): The table in which this column exists column (`~.Column`): The type of column name (str): The variable name of the column used when defining the `.Table`. In this example the name is ``age``:: 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 Accessor(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. ''' # Start with table's attrs; Only 'th' and 'td' attributes will be used attrs = dict(self._table.attrs) # Update attrs to prefer column's attrs rather than table's attrs.update(dict(self.column.attrs)) # we take the value for 'cell' as the basis for both the th and td attrs cell_attrs = attrs.get('cell', {}) # override with attrs defined specifically for th and td respectively. kwargs = { 'table': self._table, 'column': self } attrs['th'] = computed_values(attrs.get('th', cell_attrs), kwargs=kwargs) attrs['td'] = computed_values(attrs.get('td', cell_attrs), kwargs=kwargs) # wrap in AttributeDict attrs['th'] = AttributeDict(attrs['th']) attrs['td'] = AttributeDict(attrs['td']) # Override/add classes attrs['th']['class'] = self.get_th_class(attrs['th']) attrs['td']['class'] = self.get_td_class(attrs['td']) return attrs def get_td_class(self, td_attrs): ''' Returns the HTML class attribute for a data cell in this column ''' classes = set((c for c in td_attrs.get('class', '').split(' ') if c)) classes = self._table.get_column_class_names(classes, self) return ' '.join(sorted(classes)) def get_th_class(self, th_attrs): ''' Returns the HTML class attribute for a header cell in this column ''' classes = set((c for c in th_attrs.get('class', '').split(' ') if c)) classes = self._table.get_column_class_names(classes, self) # add classes for ordering ordering_class = th_attrs.get('_ordering', {}) if self.orderable: classes.add(ordering_class.get('orderable', 'orderable')) if self.is_ordered: classes.add(ordering_class.get('descending', 'desc') if self.order_by_alias.is_descending else ordering_class.get('ascending', 'asc')) return ' '.join(sorted(classes)) @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 footer(self): return call_with_appropriate(self.column.footer, { 'bound_column': self, 'table': self._table }) def has_footer(self): return self.column._footer is not None or hasattr(self.column, 'render_footer') @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 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. In order of preference, this will return: 1) The column's explicitly defined `verbose_name` 2) The titlised model's `verbose_name` (if applicable) 3) Fallback to the titlised column name. Any `verbose_name` that was not passed explicitly in the column definition is returned titlised in keeping with the Django convention of `verbose_name` being defined in lowercase and uppercased/titlised as needed by the application. 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 = self.name.replace('_', ' ') # Try to use a model field's verbose_name model = self._table.data.get_model() if model: field = Accessor(self.accessor).get_field(model) if field: if hasattr(field, 'field'): name = field.field.verbose_name else: name = getattr(field, 'verbose_name', field.name) # If verbose_name was mark_safe()'d, return intact to keep safety if isinstance(name, SafeData): return name return title(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. Arguments: table (`.Table`): the table containing the columns ''' def __init__(self, table, base_columns): self._table = table self.columns = OrderedDict() for name, column in six.iteritems(base_columns): self.columns[name] = bc = BoundColumn(table, column, name) bc.render = getattr(table, 'render_' + name, column.render) # How the value is defined: 1. value_ 2. render_ 3. column.value. bc.value = getattr(table, 'value_' + name, getattr(table, 'render_' + name, column.value)) bc.order = getattr(table, 'order_' + name, column.order) 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.ordarable %}`` 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 orderable(self): return list(self.iterorderable()) 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 hide(self, name): ''' Hide a column. Arguments: name(str): name of the column ''' self.columns[name].column.visible = False def show(self, name): ''' Show a column otherwise hidden. Arguments: name(str): name of the column ''' self.columns[name].column.visible = True 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 `~.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('Column indices must be integers or str, not %s' % type(index).__name__) django-tables2-1.14.2/django_tables2/columns/booleancolumn.py000066400000000000000000000052711317557360700241630ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from django.utils import six from django.utils.html import escape, format_html from django_tables2.templatetags.django_tables2 import title from django_tables2.utils import AttributeDict from .base import Column, library @library.register class BooleanColumn(Column): ''' A column suitable for rendering boolean data. Arguments: null (bool): is `None` different from `False`? yesno (str): text to display for True/False values, comma separated 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 _get_bool_value(self, record, value, bound_column): # If record is a model, we need to check if it has choices defined. if hasattr(record, '_meta'): field = bound_column.accessor.get_field(record) # If that's the case, we need to inverse lookup the value to convert # to a boolean we can use. if hasattr(field, 'choices') and field.choices is not None and len(field.choices) > 0: value = next(val for val, name in field.choices if name == value) value = bool(value) return value def render(self, value, record, bound_column): value = self._get_bool_value(record, value, bound_column) text = self.yesno[int(not value)] attrs = {'class': six.text_type(value).lower()} attrs.update(self.attrs.get('span', {})) return format_html( '{}', AttributeDict(attrs).as_html(), escape(text) ) def value(self, record, value, bound_column): ''' Returns the content for a specific cell similarly to `.render` however without any html content. ''' value = self._get_bool_value(record, value, bound_column) return str(value) @classmethod def from_field(cls, field): if isinstance(field, models.BooleanField): return cls(verbose_name=title(field.verbose_name), null=False) if isinstance(field, models.NullBooleanField): return cls(verbose_name=title(field.verbose_name), null=True) django-tables2-1.14.2/django_tables2/columns/checkboxcolumn.py000066400000000000000000000062711317557360700243330ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.utils.safestring import mark_safe from django_tables2.utils import Accessor, AttributeDict 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`. Arguments: attrs (dict): 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. checked (`~.Accessor`, bool, callable): Allow rendering the checkbox as checked. If it resolves to a truthy value, the checkbox will be rendered as checked. .. note:: You might expect that you could select multiple checkboxes in the rendered table and then *do something* with that. This functionality is not implemented. If you want something to actually happen, you will need to implement that yourself. ''' def __init__(self, attrs=None, checked=None, **extra): self.checked = checked 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, record): default = { 'type': 'checkbox', 'name': bound_column.name, 'value': value } if self.is_checked(value, record): default.update({ 'checked': 'checked', }) general = self.attrs.get('input') specific = self.attrs.get('td__input') attrs = AttributeDict(default, **(specific or general or {})) return mark_safe('' % attrs.as_html()) def is_checked(self, value, record): ''' Determine if the checkbox should be checked ''' if self.checked is None: return False if self.checked is True: return True if callable(self.checked): return bool(self.checked(value, record)) checked = Accessor(self.checked) if checked in record: return bool(record[checked]) return False django-tables2-1.14.2/django_tables2/columns/datecolumn.py000066400000000000000000000021361317557360700234560ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from django_tables2.templatetags.django_tables2 import title from .base import library from .templatecolumn import TemplateColumn @library.register class DateColumn(TemplateColumn): ''' A column that renders dates in the local timezone. Arguments: format (str): format string in same format as Django's ``date`` template filter (optional) short (bool): if `format` is not specified, use Django's ``SHORT_DATE_FORMAT`` setting, otherwise use ``DATE_FORMAT`` ''' def __init__(self, format=None, short=True, *args, **kwargs): 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=title(field.verbose_name)) django-tables2-1.14.2/django_tables2/columns/datetimecolumn.py000066400000000000000000000022041317557360700243310ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from django_tables2.templatetags.django_tables2 import title from .base import library from .templatecolumn import TemplateColumn @library.register class DateTimeColumn(TemplateColumn): ''' A column that renders datetimes in the local timezone. Arguments: format (str): format string for datetime (optional). Note that *format* uses Django's `date` template tag syntax. short (bool): if `format` is not specified, use Django's ``SHORT_DATETIME_FORMAT``, else ``DATETIME_FORMAT`` ''' def __init__(self, format=None, short=True, *args, **kwargs): 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=title(field.verbose_name)) django-tables2-1.14.2/django_tables2/columns/emailcolumn.py000066400000000000000000000024751317557360700236360ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from django_tables2.templatetags.django_tables2 import title from .base import library from .linkcolumn import BaseLinkColumn @library.register class EmailColumn(BaseLinkColumn): ''' Render email addresses to mailto-links. Arguments: attrs (dict): HTML attributes that are added to the rendered ``...`` tag text: Either static text, or a callable. If set, this will be used to render the text inside link instead of the value Example:: # 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() # result # [...]email@example.com ''' def render(self, record, value): return self.render_link( uri='mailto:{}'.format(value), record=record, value=value ) @classmethod def from_field(cls, field): if isinstance(field, models.EmailField): return cls(verbose_name=title(field.verbose_name)) django-tables2-1.14.2/django_tables2/columns/filecolumn.py000066400000000000000000000061401317557360700234570ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals import os from django.db import models from django.utils.html import format_html from django_tables2.templatetags.django_tables2 import title from django_tables2.utils import AttributeDict from .base import library from .linkcolumn import BaseLinkColumn @library.register class FileColumn(BaseLinkColumn): ''' 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. Arguments: verify_exists (bool): 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. text (str or callable): Either static text, or a callable. If set, this will be used to render the text inside the link instead of the file's basename (default) ''' def __init__(self, verify_exists=True, **kwargs): self.verify_exists = verify_exists super(FileColumn, self).__init__(**kwargs) def text_value(self, record, value): if self.text is None: return os.path.basename(value.name) return super(FileColumn, self).text_value(record, value) def render(self, record, 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 classes = [c for c in attrs.get('class', '').split(' ') if c] if exists is not None: classes.append('exists' if exists else 'missing') attrs['class'] = ' '.join(classes) if url: return self.render_link(url, record=record, value=value, attrs=attrs) else: return format_html( '{text}', attrs=attrs.as_html(), text=self.text_value(record, value) ) @classmethod def from_field(cls, field): if isinstance(field, models.FileField): return cls(verbose_name=title(field.verbose_name)) django-tables2-1.14.2/django_tables2/columns/jsoncolumn.py000066400000000000000000000037511317557360700235160ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals import json from django.utils.html import format_html from django_tables2.templatetags.django_tables2 import title from django_tables2.utils import AttributeDict from .base import library from .linkcolumn import BaseLinkColumn try: from django.contrib.postgres.fields import HStoreField, JSONField POSTGRES_AVAILABLE = True except ImportError: # psycopg2 is not available, cannot import from django.contrib.postgres. # JSONColumn might still be useful to add manually. POSTGRES_AVAILABLE = False @library.register class JSONColumn(BaseLinkColumn): ''' Render the contents of `~django.contrib.postgres.fields.JSONField` or `~django.contrib.postgres.fields.HStoreField` as an indented string. .. versionadded :: 1.5.0 .. note:: Automatic rendering of data to this column requires PostgreSQL support (psycopg2 installed) to import the fields, but this column can also be used manually without it. Arguments: json_dumps_kwargs: kwargs passed to `json.dumps`, defaults to `{'indent': 2}` attrs (dict): In addition to *attrs* keys supported by `~.Column`, the following are available: - *pre* -- ``
`` around the rendered JSON string in ```` elements.

    '''
    def __init__(self, json_dumps_kwargs=None, **kwargs):
        self.json_dumps_kwargs = json_dumps_kwargs if json_dumps_kwargs is not None else {'indent': 2}

        super(JSONColumn, self).__init__(**kwargs)

    def render(self, record, value):
        return format_html(
            '
{}
', AttributeDict(self.attrs.get('pre', {})).as_html(), json.dumps(value, **self.json_dumps_kwargs) ) @classmethod def from_field(cls, field): if POSTGRES_AVAILABLE: if isinstance(field, JSONField) or isinstance(field, HStoreField): return cls(verbose_name=title(field.verbose_name)) django-tables2-1.14.2/django_tables2/columns/linkcolumn.py000066400000000000000000000157711317557360700235070ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.utils.html import format_html from django_tables2.utils import Accessor, AttributeDict from .base import Column, library try: from django.urls import reverse except ImportError: # to keep backward (Django <= 1.9) compatibility from django.core.urlresolvers import reverse class BaseLinkColumn(Column): ''' The base for other columns that render links. Arguments: text (str or callable): If set, this value will be used to render the text inside link instead of value. The callable gets the record being rendered as argument. attrs (dict): In addition to *attrs* keys supported by `~.Column`, the following are available: - *a* -- ```` in ```` elements. ''' def __init__(self, attrs=None, text=None, *args, **kwargs): kwargs['attrs'] = attrs self.text = text super(BaseLinkColumn, self).__init__(*args, **kwargs) def text_value(self, record, value): if self.text is None: return value return self.text(record) if callable(self.text) else self.text def render_link(self, uri, record, value, attrs=None): ''' Render a hyperlink. Arguments: uri (str): URI for the hyperlink record: record currently being rendered value (str): value to be wrapped in ````, might be overridden by ``self.text`` attrs (dict): ```` 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=self.text_value(record, value) ) def value(self, record, value): ''' Returns the content for a specific cell similarly to `.render` however without any html content. ''' return self.text_value(record, value) @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.urls.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. Arguments: viewname (str): See `~django.urls.reverse`, or use `None` to use the model's `get_absolute_url` urlconf (str): See `~django.urls.reverse`. args (list): See `~django.urls.reverse`. [2]_ kwargs (dict): See `~django.urls.reverse`. [2]_ current_app (str): See `~django.urls.reverse`. attrs (dict): HTML attributes that are added to the rendered ``...`` tag. text (str or callable): Either static text, or a callable. If set, this will be used to render the text inside link instead of value (default). The callable gets the record being rendered as argument. .. [2] 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.urls.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 ````. Adding attributes to the ````-tag looks like this:: class PeopleTable(tables.Table): first_name = tables.LinkColumn(attrs={ 'a': {'style': 'color: red;'} }) ''' def __init__(self, viewname=None, urlconf=None, args=None, kwargs=None, current_app=None, attrs=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 def compose_url(self, record, *args, **kwargs): '''Compose the url if the column is constructed with a viewname.''' if self.viewname is None: if not hasattr(record, 'get_absolute_url'): raise TypeError('if viewname=None, record must define a get_absolute_url') return record.get_absolute_url() def resolve_if_accessor(val): return val.resolve(record) if isinstance(val, Accessor) else val viewname = resolve_if_accessor(self.viewname) # Collect the optional arguments for django's reverse() params = {} if self.urlconf: params['urlconf'] = resolve_if_accessor(self.urlconf) if self.args: params['args'] = [resolve_if_accessor(a) for a in self.args] if self.kwargs: params['kwargs'] = {key: resolve_if_accessor(val) for key, val in self.kwargs.items()} if self.current_app: params['current_app'] = resolve_if_accessor(self.current_app) return reverse(viewname, **params) def render(self, value, record, bound_column): return self.render_link( self.compose_url(record, bound_column), record=record, value=value ) @library.register class RelatedLinkColumn(LinkColumn): ''' Render a link to a related object using related object's ``get_absolute_url``, same parameters as ``~.LinkColumn`` ''' def compose_url(self, record, bound_column): accessor = self.accessor if self.accessor else Accessor(bound_column.name) return accessor.resolve(record).get_absolute_url() django-tables2-1.14.2/django_tables2/columns/manytomanycolumn.py000066400000000000000000000054561317557360700247450ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from django.utils.encoding import force_text from django.utils.html import conditional_escape, mark_safe from django_tables2.templatetags.django_tables2 import title from .base import Column, library @library.register class ManyToManyColumn(Column): ''' Display the list of objects from a `ManyRelatedManager` Ordering is disabled for this column. Arguments: transform: callable to transform each item to text, it gets an item as argument and must return a string-like representation of the item. By default, it calls `~django.utils.force_text` on each item. filter: callable to filter, limit or order the QuerySet, it gets the `ManyRelatedManager` as first argument and must return. By default, it returns `all()`` separator: separator string to join the items with. default: ', ' For example, when displaying a list of friends with their full name:: # models.py class Person(models.Model): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200) friends = models.ManyToManyField(Person) @property def name(self): return '{} {}'.format(self.first_name, self.last_name) # tables.py class PersonTable(tables.Table): name = tables.Column(order_by=('last_name', 'first_name')) friends = tables.ManyToManyColumn(transform=lamda user: u.name) ''' def __init__(self, transform=None, filter=None, separator=', ', *args, **kwargs): if transform is not None: self.transform = transform if filter is not None: self.filter = filter self.separator = separator kwargs.setdefault('orderable', False) super(ManyToManyColumn, self).__init__(*args, **kwargs) def transform(self, obj): ''' Transform is applied to each item of the list of objects from the ManyToMany relation. ''' return force_text(obj) def filter(self, qs): ''' Filter is called on the ManyRelatedManager to allow ordering, filtering or limiting on the set of related objects. ''' return qs.all() def render(self, value): # if value is None or not value.exists(): if not value.exists(): return '-' return mark_safe( conditional_escape(self.separator).join( map(conditional_escape, map(self.transform, self.filter(value))) ) ) @classmethod def from_field(cls, field): if isinstance(field, models.ManyToManyField): return cls(verbose_name=title(field.verbose_name)) django-tables2-1.14.2/django_tables2/columns/templatecolumn.py000066400000000000000000000054221317557360700243550ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.template import Context, Template from django.template.loader import get_template from django.utils import six from django.utils.html import strip_tags 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. Arguments: template_code (str): template code to render template_name (str): 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 - *row_counter* -- The number of the row this cell is being rendered in. 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. ''' 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`. context = getattr(table, 'context', Context()) context.update({ 'default': bound_column.default, 'column': bound_column, 'record': record, 'value': value, 'row_counter': kwargs['bound_row'].row_counter }) try: if self.template_code: return Template(self.template_code).render(context) else: return get_template(self.template_name).render(context.flatten()) finally: context.pop() def value(self, **kwargs): ''' The value returned from a call to `value()` on a `TemplateColumn` is the rendered tamplate with `django.utils.html.strip_tags` applied. ''' html = super(TemplateColumn, self).value(**kwargs) if isinstance(html, six.string_types): return strip_tags(html) else: return html django-tables2-1.14.2/django_tables2/columns/timecolumn.py000066400000000000000000000020371317557360700234770ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.conf import settings from django.db import models from django_tables2.templatetags.django_tables2 import title from .base import library from .templatecolumn import TemplateColumn @library.register class TimeColumn(TemplateColumn): ''' A column that renders times in the local timezone. Arguments: format (str): format string in same format as Django's ``time`` template filter (optional) short (bool): 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=title(field.verbose_name)) django-tables2-1.14.2/django_tables2/columns/urlcolumn.py000066400000000000000000000021531317557360700233420ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from django_tables2.templatetags.django_tables2 import title from .base import library from .linkcolumn import BaseLinkColumn @library.register class URLColumn(BaseLinkColumn): ''' Renders URL values as hyperlinks. Arguments: text (str or callable): Either static text, or a callable. If set, this will be used to render the text inside link instead of value (default) attrs (dict): Additional attributes for the ```` tag Example:: >>> class CompaniesTable(tables.Table): ... www = tables.URLColumn() ... >>> table = CompaniesTable([{'www': 'http://google.com'}]) >>> table.rows[0].get_cell('www') 'http://google.com' ''' def render(self, record, value): return self.render_link(value, record=record, value=value) @classmethod def from_field(cls, field): if isinstance(field, models.URLField): return cls(verbose_name=title(field.verbose_name)) django-tables2-1.14.2/django_tables2/config.py000066400000000000000000000044551317557360700211160ustar00rootroot00000000000000# 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. A single RequestConfig can be used for multiple tables in one view. Arguments: paginate (dict or bool): 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 logic: - 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. Arguments: table (`~.Table`): table to be configured ''' 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.14.2/django_tables2/data.py000066400000000000000000000150561317557360700205610ustar00rootroot00000000000000from django.utils.functional import cached_property from .utils import OrderBy, OrderByTuple, segment class TableData(object): ''' Base class for table data containers. ''' def __init__(self, data, table): self.data = data self.table = table super(TableData, self).__init__() def __getitem__(self, key): ''' Slicing returns a new `.TableData` instance, indexing returns a single record. ''' return self.data[key] def __iter__(self): ''' for ... in ... default to using this. There's a bug in Django 1.3 with indexing into querysets, so this side-steps that problem (as well as just being a better way to iterate). ''' return iter(self.data) def get_model(self): return getattr(self.data, 'model', None) @property def ordering(self): return None @property def verbose_name(self): return 'item' @property def verbose_name_plural(self): return 'items' @staticmethod def from_data(data, table): if TableQuerysetData.validate(data): return TableQuerysetData(data, table) elif TableListData.validate(data): return TableListData(list(data), table) raise ValueError( 'data must be QuerySet-like (have count() and order_by()) or support' ' list(data) -- {} has neither'.format(type(data).__name__) ) class TableListData(TableData): ''' Table data container for a list of dicts, for example:: [ {'name': 'John', 'age': 20}, {'name': 'Brian', 'age': 25} ] .. note:: Other structures might have worked in the past, but are not explicitly supported or tested. ''' @staticmethod def validate(data): ''' Validates `data` for use in this container ''' return ( hasattr(data, '__iter__') or (hasattr(data, '__len__') and hasattr(data, '__getitem__')) ) def __len__(self): return len(self.data) @property def verbose_name(self): return getattr(self.data, 'verbose_name', super(TableListData, self).verbose_name) @property def verbose_name_plural(self): return getattr(self.data, 'verbose_name_plural', super(TableListData, self).verbose_name_plural) def order_by(self, aliases): ''' Order the data based on order by aliases (prefixed column names) in the table. Arguments: aliases (`~.utils.OrderByTuple`): optionally prefixed names of columns ('-' indicates descending order) in order of significance with regard to data ordering. ''' accessors = [] for alias in aliases: bound_column = self.table.columns[OrderBy(alias).bare] # bound_column.order_by reflects the current ordering applied to # the table. As such we need to check the current ordering on the # column and use the opposite if it doesn't match the alias prefix. if alias[0] != bound_column.order_by_alias[0]: accessors += bound_column.order_by.opposite else: accessors += bound_column.order_by self.data.sort(key=OrderByTuple(accessors).key) class TableQuerysetData(TableData): ''' Table data container for a queryset. ''' @staticmethod def validate(data): ''' Validates `data` for use in this container ''' return ( hasattr(data, 'count') and callable(data.count) and hasattr(data, 'order_by') and callable(data.order_by) ) def __len__(self): if not hasattr(self, '_length'): # Use the queryset count() method to get the length, instead of # loading all results into memory. This allows, for example, # smart paginators that use len() to perform better. self._length = self.data.count() return self._length @property def ordering(self): ''' Returns the list of order by aliases that are enforcing ordering on the data. If the data is unordered, an empty sequence is returned. If the ordering can not be determined, `None` is returned. This works by inspecting the actual underlying data. As such it's only supported for querysets. ''' aliases = {} for bound_column in self.table.columns: aliases[bound_column.order_by_alias] = bound_column.order_by try: return next(segment(self.data.query.order_by, aliases)) except StopIteration: pass def order_by(self, aliases): ''' Order the data based on order by aliases (prefixed column names) in the table. Arguments: aliases (`~.utils.OrderByTuple`): optionally prefixed names of columns ('-' indicates descending order) in order of significance with regard to data ordering. ''' modified_any = False accessors = [] for alias in aliases: bound_column = self.table.columns[OrderBy(alias).bare] # bound_column.order_by reflects the current ordering applied to # the table. As such we need to check the current ordering on the # column and use the opposite if it doesn't match the alias prefix. if alias[0] != bound_column.order_by_alias[0]: accessors += bound_column.order_by.opposite else: accessors += bound_column.order_by if bound_column: queryset, modified = bound_column.order(self.data, alias[0] == '-') if modified: self.data = queryset modified_any = True # custom ordering if modified_any: return # Traditional ordering if accessors: order_by_accessors = (a.for_queryset() for a in accessors) self.data = self.data.order_by(*order_by_accessors) @cached_property def verbose_name(self): ''' The full (singular) name for the data. Model's `~django.db.Model.Meta.verbose_name` is honored. ''' return self.data.model._meta.verbose_name @cached_property def verbose_name_plural(self): ''' The full (plural) name for the data. Model's `~django.db.Model.Meta.verbose_name` is honored. ''' return self.data.model._meta.verbose_name_plural django-tables2-1.14.2/django_tables2/export/000077500000000000000000000000001317557360700206105ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/export/__init__.py000066400000000000000000000001521317557360700227170ustar00rootroot00000000000000 from .export import TableExport from .views import ExportMixin __all__ = ('TableExport', 'ExportMixin') django-tables2-1.14.2/django_tables2/export/export.py000066400000000000000000000053271317557360700225120ustar00rootroot00000000000000from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse try: from tablib import Dataset except ImportError: # pragma: no cover raise ImproperlyConfigured( 'You must have tablib installed in order to use the django-tables2 export functionality' ) class TableExport(object): ''' Export data from a table to the filetype specified. Argumenents: export_format (str): one of `csv, json, latex, ods, tsv, xls, xlsx, yml` table (`~.Table`): instance of the table to export the data from exclude_columns (iterable): list of column names to exclude from the export ''' CSV = 'csv' JSON = 'json' LATEX = 'latex' ODS = 'ods' TSV = 'tsv' XLS = 'xls' XLSX = 'xlsx' YAML = 'yml' FORMATS = { CSV: 'text/csv; charset=utf-8', JSON: 'application/json', LATEX: 'text/plain', ODS: 'application/vnd.oasis.opendocument.spreadsheet', TSV: 'text/tsv; charset=utf-8', XLS: 'application/vnd.ms-excel', XLSX: 'application/vnd.ms-excel', YAML: 'text/yml; charset=utf-8', } def __init__(self, export_format, table, exclude_columns=None): if not self.is_valid_format(export_format): raise TypeError('Export format "{}" is not supported.'.format(export_format)) self.format = export_format self.dataset = Dataset() for i, row in enumerate(table.as_values(exclude_columns=exclude_columns)): if i == 0: self.dataset.headers = row else: self.dataset.append(row) @classmethod def is_valid_format(self, export_format): ''' Returns true if `export_format` is one of the supported export formats ''' return ( export_format is not None and export_format in TableExport.FORMATS.keys() ) def content_type(self): ''' Returns the content type for the current export format ''' return self.FORMATS[self.format] def export(self): ''' Returns the string/bytes for the current export format ''' return getattr(self.dataset, self.format) def response(self, filename=None): ''' Builds and returns a `HttpResponse` containing the exported data Arguments: filename (str): if not `None`, ''' response = HttpResponse(content_type=self.content_type()) if filename is not None: response['Content-Disposition'] = 'attachment; filename="{}"'.format( filename ) response.write(self.export()) return response django-tables2-1.14.2/django_tables2/export/views.py000066400000000000000000000036021317557360700223200ustar00rootroot00000000000000from __future__ import unicode_literals from .export import TableExport class ExportMixin(object): ''' Support various export formats for the table data. `ExportMixin` looks for some attributes on the class to change it's behaviour: Attributes: export_name (str): is the name of file that will be exported, without extension. export_trigger_param (str): is the name of the GET attribute used to trigger the export. It's value decides the export format, refer to `TableExport` for a list of available formats. exclude_columns (iterable): column names excluded from the export. For example, one might want to exclude columns containing buttons from the export. Excluding columns from the export is also possible using the `exclude_from_export` argument to the `.Column` constructor:: class Table(tables.Table): name = tables.Column() buttons = tables.TemplateColumn(exclude_from_export=True, template_name=...) ''' export_name = 'table' export_trigger_param = '_export' exclude_columns = () def get_export_filename(self, export_format): return '{}.{}'.format(self.export_name, export_format) def create_export(self, export_format): exporter = TableExport( export_format=export_format, table=self.get_table(**self.get_table_kwargs()), exclude_columns=self.exclude_columns ) return exporter.response(filename=self.get_export_filename(export_format)) def render_to_response(self, context, **kwargs): export_format = self.request.GET.get(self.export_trigger_param, None) if TableExport.is_valid_format(export_format): return self.create_export(export_format) return super(ExportMixin, self).render_to_response(context, **kwargs) django-tables2-1.14.2/django_tables2/locale/000077500000000000000000000000001317557360700205265ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/de/000077500000000000000000000000001317557360700211165ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/de/LC_MESSAGES/000077500000000000000000000000001317557360700227035ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/de/LC_MESSAGES/django.mo000066400000000000000000000012061317557360700245010ustar00rootroot00000000000000Dlr?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.14.2/django_tables2/locale/de/LC_MESSAGES/django.po000066400000000000000000000020211317557360700245000ustar00rootroot00000000000000# 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.14.2/django_tables2/locale/el/000077500000000000000000000000001317557360700211265ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/el/LC_MESSAGES/000077500000000000000000000000001317557360700227135ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/el/LC_MESSAGES/django.mo000066400000000000000000000011771317557360700245200ustar00rootroot00000000000000DlG/)>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.14.2/django_tables2/locale/el/LC_MESSAGES/django.po000066400000000000000000000016551317557360700245240ustar00rootroot00000000000000# 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.14.2/django_tables2/locale/en/000077500000000000000000000000001317557360700211305ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/en/LC_MESSAGES/000077500000000000000000000000001317557360700227155ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/en/LC_MESSAGES/django.mo000066400000000000000000000011361317557360700245150ustar00rootroot00000000000000DlN27U%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2016-04-19 10:22+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 %(count)s of %(total)sNextPage %(current)s of %(total)sPreviousdjango-tables2-1.14.2/django_tables2/locale/en/LC_MESSAGES/django.po000066400000000000000000000022421317557360700245170ustar00rootroot00000000000000# 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: 2016-04-19 10:22+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/bootstrap.html:35 msgid "no results" msgstr "" #: templates/django_tables2/bootstrap.html:55 #, fuzzy #| msgid "Previous" msgid "previous" msgstr "Previous" #: templates/django_tables2/bootstrap.html:59 #: templates/django_tables2/table.html:64 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Page %(current)s of %(total)s" #: templates/django_tables2/bootstrap.html:64 msgid "next" msgstr "" #: templates/django_tables2/table.html:56 msgid "Previous" msgstr "Previous" #: templates/django_tables2/table.html:72 msgid "Next" msgstr "Next" #: templates/django_tables2/table.html:78 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s of %(total)s" django-tables2-1.14.2/django_tables2/locale/es/000077500000000000000000000000001317557360700211355ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/es/LC_MESSAGES/000077500000000000000000000000001317557360700227225ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/es/LC_MESSAGES/django.mo000066400000000000000000000011441317557360700245210ustar00rootroot00000000000000DlL 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.14.2/django_tables2/locale/es/LC_MESSAGES/django.po000066400000000000000000000015711317557360700245300ustar00rootroot00000000000000# 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.14.2/django_tables2/locale/fr/000077500000000000000000000000001317557360700211355ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/fr/LC_MESSAGES/000077500000000000000000000000001317557360700227225ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/fr/LC_MESSAGES/django.mo000066400000000000000000000012011317557360700245130ustar00rootroot00000000000000Dli6NV 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.14.2/django_tables2/locale/fr/LC_MESSAGES/django.po000066400000000000000000000020211317557360700245170ustar00rootroot00000000000000# 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.14.2/django_tables2/locale/hu/000077500000000000000000000000001317557360700211425ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/hu/LC_MESSAGES/000077500000000000000000000000001317557360700227275ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/hu/LC_MESSAGES/django.mo000066400000000000000000000013771317557360700245360ustar00rootroot00000000000000\   l%  %(count)s of %(total)sNextPage %(current)s of %(total)sPreviousnextno resultspreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2016-04-19 10:22+0200 PO-Revision-Date: 2017-08-13 14:19+0200 Last-Translator: Miklos Horvath Language-Team: Hungarian Language: en MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Generator: Poedit 1.8.12 %(count)s / %(total)sKövetkezőOldal: %(current)s / %(total)sElőzőkövetkezőnincs eredményelőződjango-tables2-1.14.2/django_tables2/locale/hu/LC_MESSAGES/django.po000066400000000000000000000023061317557360700245320ustar00rootroot00000000000000# 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: 2016-04-19 10:22+0200\n" "PO-Revision-Date: 2017-08-13 14:19+0200\n" "Last-Translator: Miklos Horvath \n" "Language-Team: Hungarian \n" "Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.8.12\n" #: templates/django_tables2/bootstrap.html:35 msgid "no results" msgstr "nincs eredmény" #: templates/django_tables2/bootstrap.html:55 msgid "previous" msgstr "előző" #: templates/django_tables2/bootstrap.html:59 #: templates/django_tables2/table.html:64 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Oldal: %(current)s / %(total)s" #: templates/django_tables2/bootstrap.html:64 msgid "next" msgstr "következő" #: templates/django_tables2/table.html:56 msgid "Previous" msgstr "Előző" #: templates/django_tables2/table.html:72 msgid "Next" msgstr "Következő" #: templates/django_tables2/table.html:78 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s / %(total)s" django-tables2-1.14.2/django_tables2/locale/it/000077500000000000000000000000001317557360700211425ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/it/LC_MESSAGES/000077500000000000000000000000001317557360700227275ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/it/LC_MESSAGES/django.mo000066400000000000000000000013721317557360700245310ustar00rootroot00000000000000\   l%%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousnextno resultspreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2016-04-19 10:23+0200 PO-Revision-Date: 2016-04-14 11:21+0200 Last-Translator: Paolo Dina Language-Team: Italian Language: it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); %(count)s di %(total)sAvantiPagina %(current)s di %(total)sIndietroavantinessun risultatoindietrodjango-tables2-1.14.2/django_tables2/locale/it/LC_MESSAGES/django.po000066400000000000000000000022771317557360700245410ustar00rootroot00000000000000# 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: 2016-04-19 10:23+0200\n" "PO-Revision-Date: 2016-04-14 11:21+0200\n" "Last-Translator: Paolo Dina \n" "Language-Team: Italian \n" "Language: it\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/bootstrap.html:35 msgid "no results" msgstr "nessun risultato" #: templates/django_tables2/bootstrap.html:55 msgid "previous" msgstr "indietro" #: templates/django_tables2/bootstrap.html:59 #: templates/django_tables2/table.html:64 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Pagina %(current)s di %(total)s" #: templates/django_tables2/bootstrap.html:64 msgid "next" msgstr "avanti" #: templates/django_tables2/table.html:56 msgid "Previous" msgstr "Indietro" #: templates/django_tables2/table.html:72 msgid "Next" msgstr "Avanti" #: templates/django_tables2/table.html:78 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s di %(total)s" django-tables2-1.14.2/django_tables2/locale/nb/000077500000000000000000000000001317557360700211255ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/nb/LC_MESSAGES/000077500000000000000000000000001317557360700227125ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/nb/LC_MESSAGES/django.mo000066400000000000000000000012471317557360700245150ustar00rootroot00000000000000T  @ Mdj%(count)s of %(total)sNextPage %(current)s of %(total)sPreviousnextno resultsProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2016-07-22 11:11+0200 PO-Revision-Date: 2016-07-22 11:11+0200 Last-Translator: Andreas TollånesLanguage-Team: Norwegian Bokmal Language: nb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit %(count)s av %(total)sNesteSide %(current)s av %(total)sForrigenesteingen resultaterdjango-tables2-1.14.2/django_tables2/locale/nb/LC_MESSAGES/django.po000066400000000000000000000022471317557360700245210ustar00rootroot00000000000000# 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: 2016-07-22 11:11+0200\n" "PO-Revision-Date: 2016-07-22 11:11+0200\n" "Last-Translator: Andreas Tollånes" "Language-Team: Norwegian Bokmal \n" "Language: nb\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/bootstrap.html:35 msgid "no results" msgstr "ingen resultater" #: templates/django_tables2/bootstrap.html:55 #, fuzzy #| msgid "Previous" msgid "previous" msgstr "Forrige" #: templates/django_tables2/bootstrap.html:59 #: templates/django_tables2/table.html:64 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Side %(current)s av %(total)s" #: templates/django_tables2/bootstrap.html:64 msgid "next" msgstr "neste" #: templates/django_tables2/table.html:56 msgid "Previous" msgstr "Forrige" #: templates/django_tables2/table.html:72 msgid "Next" msgstr "Neste" #: templates/django_tables2/table.html:78 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s av %(total)s" django-tables2-1.14.2/django_tables2/locale/nl/000077500000000000000000000000001317557360700211375ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/nl/LC_MESSAGES/000077500000000000000000000000001317557360700227245ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/nl/LC_MESSAGES/django.mo000066400000000000000000000014101317557360700245170ustar00rootroot00000000000000\   y% %(count)s of %(total)sNextPage %(current)s of %(total)sPreviousnextno resultspreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2016-04-19 10:23+0200 PO-Revision-Date: 2016-04-19 10:21+0200 Last-Translator: Jan Pieter Waagmeester Language-Team: Dutch Language: nl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); %(count)s van %(total)sVolgendePagina %(current)s van %(total)sVorigevolgendegeen resultatenvorigedjango-tables2-1.14.2/django_tables2/locale/nl/LC_MESSAGES/django.po000066400000000000000000000023151317557360700245270ustar00rootroot00000000000000# 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: 2016-04-19 10:23+0200\n" "PO-Revision-Date: 2016-04-19 10:21+0200\n" "Last-Translator: Jan Pieter Waagmeester \n" "Language-Team: Dutch \n" "Language: nl\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/bootstrap.html:35 msgid "no results" msgstr "geen resultaten" #: templates/django_tables2/bootstrap.html:55 msgid "previous" msgstr "vorige" #: templates/django_tables2/bootstrap.html:59 #: templates/django_tables2/table.html:64 #, python-format msgid "Page %(current)s of %(total)s" msgstr "Pagina %(current)s van %(total)s" #: templates/django_tables2/bootstrap.html:64 msgid "next" msgstr "volgende" #: templates/django_tables2/table.html:56 msgid "Previous" msgstr "Vorige" #: templates/django_tables2/table.html:72 msgid "Next" msgstr "Volgende" #: templates/django_tables2/table.html:78 #, python-format msgid "%(count)s of %(total)s" msgstr "%(count)s van %(total)s" django-tables2-1.14.2/django_tables2/locale/pl/000077500000000000000000000000001317557360700211415ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/pl/LC_MESSAGES/000077500000000000000000000000001317557360700227265ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/pl/LC_MESSAGES/django.mo000066400000000000000000000013441317557360700245270ustar00rootroot00000000000000Dl  %(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.14.2/django_tables2/locale/pl/LC_MESSAGES/django.po000066400000000000000000000022061317557360700245300ustar00rootroot00000000000000# 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.14.2/django_tables2/locale/pt_BR/000077500000000000000000000000001317557360700215345ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/pt_BR/LC_MESSAGES/000077500000000000000000000000001317557360700233215ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/pt_BR/LC_MESSAGES/django.mo000066400000000000000000000012441317557360700251210ustar00rootroot00000000000000DlZq 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.14.2/django_tables2/locale/pt_BR/LC_MESSAGES/django.po000066400000000000000000000020051317557360700251200ustar00rootroot00000000000000# 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.14.2/django_tables2/locale/pt_PT/000077500000000000000000000000001317557360700215545ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/pt_PT/LC_MESSAGES/000077500000000000000000000000001317557360700233415ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/pt_PT/LC_MESSAGES/django.mo000066400000000000000000000011451317557360700251410ustar00rootroot00000000000000DlN2 ;\%(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.14.2/django_tables2/locale/pt_PT/LC_MESSAGES/django.po000066400000000000000000000015721317557360700251500ustar00rootroot00000000000000# 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.14.2/django_tables2/locale/ru/000077500000000000000000000000001317557360700211545ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/ru/LC_MESSAGES/000077500000000000000000000000001317557360700227415ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/ru/LC_MESSAGES/django.mo000066400000000000000000000014021317557360700245350ustar00rootroot00000000000000Dl+%(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.14.2/django_tables2/locale/ru/LC_MESSAGES/django.po000066400000000000000000000022251317557360700245440ustar00rootroot00000000000000# 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.14.2/django_tables2/locale/sv/000077500000000000000000000000001317557360700211565ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/sv/LC_MESSAGES/000077500000000000000000000000001317557360700227435ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/sv/LC_MESSAGES/django.mo000066400000000000000000000011361317557360700245430ustar00rootroot00000000000000DlH,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.14.2/django_tables2/locale/sv/LC_MESSAGES/django.po000066400000000000000000000015631317557360700245520ustar00rootroot00000000000000# 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.14.2/django_tables2/locale/uk/000077500000000000000000000000001317557360700211455ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/uk/LC_MESSAGES/000077500000000000000000000000001317557360700227325ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/locale/uk/LC_MESSAGES/django.mo000066400000000000000000000013721317557360700245340ustar00rootroot00000000000000Dl)%(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.14.2/django_tables2/locale/uk/LC_MESSAGES/django.po000066400000000000000000000022151317557360700245340ustar00rootroot00000000000000# 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.14.2/django_tables2/rows.py000066400000000000000000000246431317557360700206440ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils import six from .columns.linkcolumn import BaseLinkColumn from .utils import A, AttributeDict, call_with_appropriate, computed_values 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:: >>> 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 use row.get_cell() to retrieve a specific cell:: >>> row.get_cell(0) 1 >>> row.get_cell(1) u'' >>> row.get_cell(2) ... IndexError: list index out of range Finally you can also use the column names to retrieve a specific cell:: >>> row.get_cell('a') 1 >>> row.get_cell('b') u'' >>> row.get_cell('c') ... KeyError: 'c' Arguments: table: The `.Table` in which this row exists. 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 self.row_counter = next(table._counter) @property def table(self): ''' The associated `.Table` object. ''' return self._table def get_even_odd_css_class(self): ''' Return css class, alternating for odd and even records. Return: string: `even` for even records, `odd` otherwise. ''' return 'odd' if self.row_counter % 2 else 'even' @property def attrs(self): ''' Return the attributes for a certain row. ''' cssClass = self.get_even_odd_css_class() row_attrs = computed_values(self._table.row_attrs, kwargs={'record': self._record}) if 'class' in row_attrs and row_attrs['class']: row_attrs['class'] += ' ' + cssClass else: row_attrs['class'] = cssClass return AttributeDict(row_attrs) @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 _get_and_render_with(self, name, render_func, default): bound_column = self.table.columns[name] value = None accessor = A(bound_column.accessor) # We need to take special care here to allow get_FOO_display() # methods on a model to be used if available. See issue #30. penultimate, remainder = accessor.penultimate(self.record) # 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 = accessor.get_field(self.record) display_fn = getattr(penultimate, 'get_%s_display' % remainder, None) if getattr(field, 'choices', ()) and display_fn: value = display_fn() remainder = None except FieldDoesNotExist: pass # Fall back to just using the original accessor if remainder: try: value = accessor.resolve(self.record) except Exception: # we need to account for non-field based columns (issue #257) is_linkcolumn = isinstance(bound_column.column, BaseLinkColumn) if is_linkcolumn and bound_column.column.text is not None: return render_func(bound_column) if value in bound_column.column.empty_values: return default return render_func(bound_column, value) def _optional_cell_arguments(self, bound_column, value): ''' Defines the arguments that will optionally be passed while calling the cell's rendering or value getter if that function has one of these as a keyword argument. ''' return { 'value': value, 'record': self.record, 'column': bound_column.column, 'bound_column': bound_column, 'bound_row': self, 'table': self._table, } def get_cell(self, name): ''' Returns the final rendered html for a cell in the row, given the name of a column. ''' return self._get_and_render_with( name, render_func=self._call_render, default=self.table.columns[name].default ) def _call_render(self, bound_column, value=None): ''' Call the column's render method with appropriate kwargs ''' return call_with_appropriate( bound_column.render, self._optional_cell_arguments(bound_column, value) ) def get_cell_value(self, name): ''' Returns the final rendered value (excluding any html) for a cell in the row, given the name of a column. ''' return self._get_and_render_with( name, render_func=self._call_value, default=None ) def _call_value(self, bound_column, value=None): ''' Call the column's value method with appropriate kwargs ''' return call_with_appropriate( bound_column.value, self._optional_cell_arguments(bound_column, value) ) 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.get_cell(column.name)) class BoundPinnedRow(BoundRow): ''' Represents a *pinned* row in a table. Inherited from BoundRow. ''' @property def attrs(self): ''' Return the attributes for a certain pinned row. Add css clases `pinned-row` to `class` attribute. Return: AttributeDict: Attributes for pinned rows. ''' row_attrs = computed_values(self._table.pinned_row_attrs, kwargs={'record': self._record}) css_class = ' '.join([ self.get_even_odd_css_class(), 'pinned-row', row_attrs.get('class', '') ]) row_attrs['class'] = css_class return AttributeDict(row_attrs) class BoundRows(object): ''' Container for spawning `.BoundRow` objects. Arguments: data: iterable of records table: the `~.Table` in which the rows exist pinned_data: dictionary with iterable of records for top and/or bottom pinned rows. Example: >>> pinned_data = { ... 'top': iterable, # or None value ... 'bottom': iterable, # or None value ... } This is used for `~.Table.rows`. ''' def __init__(self, data, table, pinned_data=None): self.data = data self.table = table self.pinned_data = pinned_data or {} def generator_pinned_row(self, data): ''' Top and bottom pinned rows generator. Arguments: data: Iterable datas for all records for top or bottom pinned rows. Yields: BoundPinnedRow: Top or bottom BoundPinnedRow object for single pinned record. ''' if data is not None: if hasattr(data, '__iter__') is False: raise ValueError('The data for pinned rows must be iterable') else: # If pinned data is iterable for pinned_record in data: yield BoundPinnedRow(pinned_record, table=self.table) def __iter__(self): # Top pinned rows for pinned_record in self.generator_pinned_row(self.pinned_data.get('top')): yield pinned_record for record in self.data: yield BoundRow(record, table=self.table) # Bottom pinned rows for pinned_record in self.generator_pinned_row(self.pinned_data.get('bottom')): yield pinned_record def __len__(self): length = len(self.data) pinned_top = self.pinned_data.get('top') pinned_bottom = self.pinned_data.get('bottom') length += 0 if pinned_top is None else len(pinned_top) length += 0 if pinned_bottom is None else len(pinned_bottom) return length def __getitem__(self, key): ''' Slicing returns a new `~.BoundRows` instance, indexing returns a single `~.BoundRow` instance. ''' if isinstance(key, slice): return BoundRows( self.data[key], table=self.table, pinned_data=self.pinned_data ) else: return BoundRow(self.data[key], table=self.table) django-tables2-1.14.2/django_tables2/static/000077500000000000000000000000001317557360700205565ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/static/django_tables2/000077500000000000000000000000001317557360700234345ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/static/django_tables2/bootstrap.css000066400000000000000000000002471317557360700261660ustar00rootroot00000000000000.table-container th.asc:after { content: '\0000a0\0025b2'; float: right; } .table-container th.desc:after { content: '\0000a0\0025bc'; float: right; } django-tables2-1.14.2/django_tables2/static/django_tables2/themes/000077500000000000000000000000001317557360700247215ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/static/django_tables2/themes/paleblue/000077500000000000000000000000001317557360700265125ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/static/django_tables2/themes/paleblue/css/000077500000000000000000000000001317557360700273025ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/static/django_tables2/themes/paleblue/css/screen.css000066400000000000000000000053261317557360700313010ustar00rootroot00000000000000table.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.14.2/django_tables2/static/django_tables2/themes/paleblue/img/000077500000000000000000000000001317557360700272665ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/static/django_tables2/themes/paleblue/img/arrow-active-down.png000066400000000000000000000003301317557360700333400ustar00rootroot00000000000000PNG  IHDR tEXtSoftwareAdobe ImageReadyqe<zIDATxbk?R8 ^UKx|A }0X\ r>22t:dx R #)vfVbc()Ȋ@ .F`"LIENDB`django-tables2-1.14.2/django_tables2/static/django_tables2/themes/paleblue/img/arrow-active-up.png000066400000000000000000000003121317557360700330150ustar00rootroot00000000000000PNG  IHDR tEXtSoftwareAdobe ImageReadyqe<lIDATxbd@z-K5 Ç`q&tEn@A@@U!H'_0aAX@T?H###H/ @[ ` @=.E!cIENDB`arrow-inactive-down.png000066400000000000000000000003661317557360700336210ustar00rootroot00000000000000django-tables2-1.14.2/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.14.2/django_tables2/static/django_tables2/themes/paleblue/img/arrow-inactive-up.png000066400000000000000000000003221317557360700333450ustar00rootroot00000000000000PNG  IHDR tEXtSoftwareAdobe ImageReadyqe<tIDATx| ( {Х`(1#ARDKg?PJi} P'd> 􅬵hIZ*+m9'@}UUVJA!=s5ˍ=Ӡ.>6WIENDB`django-tables2-1.14.2/django_tables2/static/django_tables2/themes/paleblue/img/false.gif000066400000000000000000000002601317557360700310450ustar00rootroot00000000000000GIF89a !!33..HH``bbXXZZuurr!, - @4(D@:@Ю1A;p cM0ITa;django-tables2-1.14.2/django_tables2/static/django_tables2/themes/paleblue/img/header-bg.png000066400000000000000000000002021317557360700316040ustar00rootroot00000000000000PNG  IHDRStEXtSoftwareAdobe ImageReadyqe<$IDATxb?t0NaM=z`> IENDB`django-tables2-1.14.2/django_tables2/static/django_tables2/themes/paleblue/img/missing.png000066400000000000000000000007751317557360700314560ustar00rootroot00000000000000PNG  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. When accessing the attribute, the value is always returned as an `.AttributeDict` to allow easily conversion to HTML. row_attrs: Add custom html attributes to the table rows. Allows custom HTML attributes to be specified which will be added to the ```` tag of the rendered table. pinned_row_attrs: Same as row_attrs but for pinned rows. sequence (iterable): The sequence/order of columns the columns (from left to right). 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. prefix (str): A prefix for querystring fields. To avoid name-clashes when using multiple tables on single page. order_by_field (str): If not `None`, defines the name of the *order by* querystring field in the url. page_field (str): If not `None`, defines the name of the *current page* querystring field. per_page_field (str): If not `None`, defines the name of the *per page* querystring field. template (str): The template to render when using ``{% render_table %}`` (default ``'django_tables2/table.html'``) default (str): Text to render in empty cells (determined by `.Column.empty_values`, default `.Table.Meta.default`) request: Django's request to avoid using `RequestConfig` show_header (bool): If `False`, the table will not have a header (``), defaults to `True` show_footer (bool): If `False`, the table footer will not be rendered, even if some columns have a footer, defaults to `True`. extra_columns (str, `.Column`): list of `(name, column)`-tuples containing extra columns to add to the instance. ''' def __init__(self, data=None, order_by=None, orderable=None, empty_text=None, exclude=None, attrs=None, row_attrs=None, pinned_row_attrs=None, sequence=None, prefix=None, order_by_field=None, page_field=None, per_page_field=None, template=None, default=None, request=None, show_header=None, show_footer=True, extra_columns=None): super(TableBase, self).__init__() # note that although data is a keyword argument, it used to be positional # so it is assumed to be the first argument to this method. if data is None: raise TypeError('Argument data to {} is required'.format(type(self).__name__)) self.exclude = exclude or self._meta.exclude self.sequence = sequence self.data = TableData.from_data(data=data, table=self) if default is None: default = self._meta.default self.default = default # Pinned rows #406 self.pinned_row_attrs = AttributeDict(pinned_row_attrs or self._meta.pinned_row_attrs) self.pinned_data = { 'top': self.get_top_pinned_data(), 'bottom': self.get_bottom_pinned_data() } self.rows = BoundRows(data=self.data, table=self, pinned_data=self.pinned_data) self.attrs = AttributeDict(attrs if attrs is not None else self._meta.attrs) self.row_attrs = AttributeDict(row_attrs or self._meta.row_attrs) self.empty_text = empty_text if empty_text is not None else self._meta.empty_text 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 self.show_footer = show_footer # 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. base_columns = copy.deepcopy(type(self).base_columns) if extra_columns is not None: for name, column in extra_columns: base_columns[name] = column # 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: sequence = Sequence(sequence) elif self._meta.sequence: sequence = self._meta.sequence else: if self._meta.fields is not None: sequence = Sequence(tuple(self._meta.fields) + ('...', )) else: sequence = Sequence(('...', )) self._sequence = sequence.expand(base_columns.keys()) # reorder columns based on sequence. base_columns = OrderedDict(( (x, base_columns[x]) for x in sequence if x in base_columns )) self.columns = columns.BoundColumns(self, base_columns) # `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) self._counter = count() def get_top_pinned_data(self): ''' Return data for top pinned rows containing data for each row. Iterable type like: queryset, list of dicts, list of objects. Having a non-zero number of pinned rows will not result in an empty resultset message being rendered, even if there are no regular data rows Returns: `None` (default) no pinned rows at the top, iterable, data for pinned rows at the top. Note: To show pinned row this method should be overridden. Example: >>> class TableWithTopPinnedRows(Table): ... def get_top_pinned_data(self): ... return [{ ... 'column_a' : 'some value', ... 'column_c' : 'other value', ... }] ''' return None def get_bottom_pinned_data(self): ''' Return data for bottom pinned rows containing data for each row. Iterable type like: queryset, list of dicts, list of objects. Having a non-zero number of pinned rows will not result in an empty resultset message being rendered, even if there are no regular data rows Returns: `None` (default) no pinned rows at the bottom, iterable, data for pinned rows at the bottom. Note: To show pinned row this method should be overridden. Example: >>> class TableWithBottomPinnedRows(Table): ... def get_bottom_pinned_data(self): ... return [{ ... 'column_a' : 'some value', ... 'column_c' : 'other value', ... }] ''' return None def before_render(self, request): ''' A way to hook into the moment just before rendering the template. Can be used to hide a column. Arguments: request: contains the `WGSIRequest` instance, containing a `user` attribute if `.django.contrib.auth.middleware.AuthenticationMiddleware` is added to your `MIDDLEWARE_CLASSES`. Example:: class Table(tables.Table): name = tables.Column(orderable=False) country = tables.Column(orderable=False) def before_render(self, request): if request.user.has_perm('foo.delete_bar'): self.columns.hide('country') else: self.columns.show('country') ''' return def as_html(self, request): ''' Render the table to an HTML table, adding `request` to the context. ''' # reset counter for new rendering self._counter = count() template = get_template(self.template) context = { 'table': self, 'request': request } self.before_render(request) return template.render(context) def as_values(self, exclude_columns=None): ''' Return a row iterator of the data which would be shown in the table where the first row is the table headers. arguments: exclude_columns (iterable): columns to exclude in the data iterator. This can be used to output the table data as CSV, excel, for example using the `~.export.ExportMixin`. If a column is defined using a :ref:`table.render_FOO`, the returned value from that method is used. If you want to differentiate between the rendered cell and a value, use a `value_Foo`-method:: class Table(tables.Table): name = tables.Column() def render_name(self, value): return format_html('{}', value) def value_name(self, value): return value will have a value wrapped in `` in the rendered HTML, and just returns the value when `as_values()` is called. ''' if exclude_columns is None: exclude_columns = () def excluded(column): if column.column.exclude_from_export: return True return column.name in exclude_columns yield [ force_text(column.header, strings_only=True) for column in self.columns if not excluded(column) ] for row in self.rows: yield [ force_text(row.get_cell_value(column.name), strings_only=True) for column in row.table.columns if not excluded(column) ] def has_footer(self): ''' Returns True if any of the columns define a ``_footer`` attribute or a ``render_footer()`` method ''' return self.show_footer and any(column.has_footer() for column in self.columns) @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 order_by(self): return self._order_by @order_by.setter def order_by(self, value): ''' Order the rows of the table based on columns. Arguments: value: iterable or comma separated string 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. Arguments: klass (`~django.core.paginator.Paginator`): A paginator class to paginate the results. per_page (int): Number of records to display on each page. page (int): Page to display. 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): if self._orderable is not None: return self._orderable else: return self._meta.orderable @orderable.setter def orderable(self, value): self._orderable = value @property def template(self): if self._template is not None: return self._template else: return self._meta.template @template.setter def template(self, value): self._template = value def get_column_class_names(self, classes_set, bound_column): ''' Returns a set of HTML class names for cells (both td and th) of a **bound column** in this table. By default this returns the column class names defined in the table's attributes, and additionally the bound column's name. This method can be overridden to change the default behavior, for example to simply `return classes_set`. Arguments: classes_set(set of string): a set of class names to be added to the cell, retrieved from the column's attributes. In the case of a header cell (th), this also includes ordering classes. To set the classes for a column, see `.Column`. To configure ordering classes, see :ref:`ordering-class-name` bound_column(`.BoundColumn`): the bound column the class names are determined for. Useful for accessing `bound_column.name`. Returns: A set of class names to be added to cells of this column ''' classes_set.add(bound_column.name) return classes_set # Python 2/3 compatible way to enable the metaclass @six.add_metaclass(DeclarativeColumnsMetaclass) class Table(TableBase): # ensure the Table class has the right class docstring __doc__ = TableBase.__doc__ # Table = DeclarativeColumnsMetaclass(str('Table'), (TableBase, ), {}) django-tables2-1.14.2/django_tables2/templates/000077500000000000000000000000001317557360700212655ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/templates/django_tables2/000077500000000000000000000000001317557360700241435ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/templates/django_tables2/bootstrap-responsive.html000066400000000000000000000005731317557360700312460ustar00rootroot00000000000000{% extends 'django_tables2/bootstrap.html' %} {% block table-wrapper %}
{% block table %} {{ block.super }} {% endblock table %} {% if table.page and table.paginator.num_pages > 1 %} {% block pagination %} {{ block.super }} {% endblock pagination %} {% endif %}
{% endblock table-wrapper %} django-tables2-1.14.2/django_tables2/templates/django_tables2/bootstrap.html000066400000000000000000000074021317557360700270510ustar00rootroot00000000000000{% load django_tables2 %} {% load i18n %} {% block table-wrapper %}
{% block table %} {% block table.thead %} {% if table.show_header %} {% for column in table.columns %} {% if column.orderable %} {% else %} {% 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 %} {% for column, cell in row.items %} {% endfor %} {% endblock table.tbody.row %} {% empty %} {% if table.empty_text %} {% block table.tbody.empty_text %} {% endblock table.tbody.empty_text %} {% endif %} {% endfor %} {% endblock table.tbody %} {% block table.tfoot %} {% if table.has_footer %} {% for column in table.columns %} {% endfor %} {% endif %} {% endblock table.tfoot %}
{{ column.header }}{{ column.header }}
{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
{{ table.empty_text }}
{{ column.footer }}
{% endblock table %} {% if table.page and table.paginator.num_pages > 1 %} {% block pagination %}
    {% if table.page.has_previous %} {% block pagination.previous %} {% endblock pagination.previous %} {% endif %} {% if table.page.has_previous or table.page.has_next %} {% 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 %} {% endif %} {% if table.page.has_next %} {% block pagination.next %} {% endblock pagination.next %} {% endif %}
{% endblock pagination %} {% endif %}
{% endblock table-wrapper %} django-tables2-1.14.2/django_tables2/templates/django_tables2/semantic.html000066400000000000000000000076741317557360700266520ustar00rootroot00000000000000{% load django_tables2 %} {% load i18n %}
{% block table %} {% block table.thead %} {% if table.show_header %} {% for column in table.columns %} {% if column.orderable %} {% else %} {% 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 %} {% for column, cell in row.items %} {% endfor %} {% endblock table.tbody.row %} {% empty %} {% if table.empty_text %} {% block table.tbody.empty_text %} {% endblock table.tbody.empty_text %} {% endif %} {% endfor %} {% endblock table.tbody %} {% block table.tfoot %} {% if table.has_footer %} {% for column in table.columns %} {% endfor %} {% endif %} {% if table.page and table.paginator.num_pages > 1 %} {% block pagination %} {% endblock pagination %} {% endif %} {% endblock table.tfoot %}
{{ column.header }}{{ column.header }}
{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
{{ table.empty_text }}
{{ column.footer }}
{% endblock table %}
django-tables2-1.14.2/django_tables2/templates/django_tables2/table.html000066400000000000000000000057731317557360700261340ustar00rootroot00000000000000{% load django_tables2 %} {% load i18n %}
{% block table %} {% 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 %} {% 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 %} {% if table.has_footer %} {% for column in table.columns %} {{ column.footer }} {% endfor %} {% endif %} {% endblock table.tfoot %} {% 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 %} {% block pagination.previous %} {% endblock pagination.previous %} {% endif %} {% if table.page.has_previous or table.page.has_next %} {% block pagination.cardinality %}
  • {% blocktrans with table.page.number as current and table.paginator.num_pages as total %}Page {{ current }} of {{ total }}{% endblocktrans %}
  • {% endblock pagination.cardinality %} {% endif %} {% if table.page.has_next %} {% block pagination.next %} {% endblock pagination.next %} {% endif %}
{% endblock pagination %} {% endwith %} {% endwith %} {% endif %}
django-tables2-1.14.2/django_tables2/templatetags/000077500000000000000000000000001317557360700217615ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/templatetags/__init__.py000066400000000000000000000000001317557360700240600ustar00rootroot00000000000000django-tables2-1.14.2/django_tables2/templatetags/django_tables2.py000066400000000000000000000165371317557360700252250ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals import re from collections import OrderedDict from django import template from django.core.exceptions import ImproperlyConfigured from django.template import Node, TemplateSyntaxError 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.templatetags.l10n import register as l10n_register from django.utils import six from django.utils.html import escape from django.utils.http import urlencode import django_tables2 as tables from django_tables2.config import RequestConfig register = template.Library() kwarg_re = re.compile(r"(?:(.+)=)?(.+)") context_processor_error_msg = ( 'Tag {%% %s %%} requires django.template.context_processors.request to be ' 'in the template configuration in ' 'settings.TEMPLATES[]OPTIONS.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 QuerystringNode(Node): def __init__(self, updates, removals): super(QuerystringNode, self).__init__() self.updates = updates self.removals = removals def render(self, context): if 'request' not 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): ''' parameters: table (~.Table): the table to render template (str or list): Name[s] of template to render ''' 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) request = context.get('request') if isinstance(table, tables.TableBase): 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 table = OnTheFlyTable(queryset) if request: RequestConfig(request).configure(table) else: klass = type(table).__name__ raise ValueError('Expected table or queryset, not {}'.format(klass)) 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 table.before_render(request) return template.render(context.flatten()) 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() bits.pop(0) table = parser.compile_filter(bits.pop(0)) template = parser.compile_filter(bits.pop(0)) if bits else None return RenderTableNode(table, template) @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 or a digit. Actually Django's filter also skips words with digits but only for latin letters (or at least not for cyrillic ones). ''' return ' '.join([ any([c.isupper() or c.isdigit() for c in w]) and w or old_title(w) for w in value.split() ]) title.is_safe = True try: from django.utils.functional import keep_lazy_text title = keep_lazy_text(title) except ImportError: # to keep backward (Django < 1.10) compatibility from django.utils.functional import lazy title = lazy(title, six.text_type) register.filter('localize', l10n_register.filters['localize']) register.filter('unlocalize', l10n_register.filters['unlocalize']) django-tables2-1.14.2/django_tables2/utils.py000066400000000000000000000425431317557360700210110ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals from functools import total_ordering from itertools import chain from django.db.models.fields import FieldDoesNotExist from django.utils import six from django.utils.html import format_html_join 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. arguments: columns (list): list of column names. returns: The current instance. 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) return self class OrderBy(str): ''' A single item in an `.OrderByTuple` object. This class is essentially just a `str` with some extra properties. ''' QUERYSET_SEPARATOR = '__' @property def bare(self): ''' Returns: `.OrderBy`: 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`` ''' return OrderBy(self[1:]) if self[:1] == '-' else self @property def opposite(self): ''' Provides the opposite of the current sorting directon. Returns: `.OrderBy`: object with an opposite sort influence. Example:: >>> order_by = OrderBy('name') >>> order_by.opposite '-name' ''' return OrderBy(self[1:]) if self.is_descending else OrderBy('-' + self) @property def is_descending(self): ''' Returns `True` if this object induces *descending* ordering. ''' return self.startswith('-') @property def is_ascending(self): ''' Returns `True` if this object induces *ascending* ordering. ''' return not self.is_descending def for_queryset(self): ''' Returns the current instance usable in Django QuerySet's order_by arguments. ''' return self.replace(Accessor.SEPARATOR, OrderBy.QUERYSET_SEPARATOR) @six.python_2_unicode_compatible class OrderByTuple(tuple): ''' Stores ordering as (as `.OrderBy` objects). The `~.Table.order_by` property is always converted to an `.OrderByTuple` object. This class is essentially just a `tuple` with some useful extras. Example:: >>> 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:: >>> x = OrderByTuple(('name', )) >>> 'name' in x True >>> '-name' in x True Arguments: name (str): The name of a column. (optionally prefixed) Returns: bool: `True` if the column with `name` influences the ordering. ''' 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:: >>> x = OrderByTuple(('name', '-age')) >>> x[0] 'name' >>> x['age'] '-age' >>> x['-age'] '-age' Arguments: index (int): Index to query the ordering for. Returns: `.OrderBy`: for the ordering at the index. ''' 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 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:: >>> 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*. Lookups are attempted 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. Example:: >>> x = Accessor('__len__') >>> x.resolve('brad') 4 >>> x = Accessor('0.upper') >>> x.resolve('brad') 'B' Arguments: context (object): The root/first object to traverse. safe (bool): Don't call anything with `alters_data = True` quiet (bool): Smother all exceptions and instead return `None` Returns: target object Raises: TypeError`, `AttributeError`, `KeyError`, `ValueError` (unless `quiet` == `True`) ''' 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 Exception: if not quiet: raise @property def bits(self): if self == '': return () return self.split(self.SEPARATOR) def get_field(self, model): ''' Return the django model field for model in context, following relations. ''' if not hasattr(model, '_meta'): return field = None for bit in self.bits: try: field = model._meta.get_field(bit) except FieldDoesNotExist: break if hasattr(field, 'remote_field'): rel = getattr(field, 'remote_field', None) model = getattr(rel, 'model', model) # !!! Support only for Django <= 1.8 # Remove this when support for Django 1.8 is over else: rel = getattr(field, 'rel', None) model = getattr(rel, 'to', model) return field def penultimate(self, context, quiet=True): ''' Split the accessor on the right-most dot '.', return a tuple with: - the resolved left part. - the remainder Example:: >>> Accessor('a.b.c').penultimate({'a': {'a': 1, 'b': {'c': 2, 'd': 4}}}) ({'c': 2, 'd': 4}, 'c') ''' path, _, remainder = self.rpartition('.') return A(path).resolve(context, quiet=quiet), remainder 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. ''' blacklist = ('th', 'td', '_ordering') def _iteritems(self): for k, v in six.iteritems(self): if k not in self.blacklist: yield (k, v() if callable(v) else v) 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 format_html_join(' ', '{}="{}"', self._iteritems()) 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:: >>> 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]) def signature(fn): ''' Returns: tuple: Returns a (arguments, kwarg_name)-tuple: - the arguments (positional or keyword) - the name of the ** kwarg catch all. The self-argument for methods is always removed. ''' import inspect # getargspec is Deprecated since version 3.0, so if not PY2, use the new # inspect api. if six.PY2: argspec = inspect.getargspec(fn) args = argspec.args if len(args) > 0: args = tuple(args[1:] if args[0] == 'self' else args) return (args, argspec.keywords) # python 3 version: signature = inspect.signature(fn) args = [] keywords = None for arg in signature.parameters.values(): if arg.kind == arg.VAR_KEYWORD: keywords = arg.name elif arg.kind == arg.VAR_POSITIONAL: continue # skip *args catch-all else: args.append(arg.name) return tuple(args), keywords def call_with_appropriate(fn, kwargs): ''' Calls the function ``fn`` with the keyword arguments from ``kwargs`` it expects If the kwargs argument is defined, pass all arguments, else provide exactly the arguments wanted. ''' args, keyword = signature(fn) if not keyword: kwargs = {key: kwargs[key] for key in kwargs if key in args} return fn(**kwargs) def computed_values(d, kwargs=None): ''' Returns a new `dict` that has callable values replaced with the return values. 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'}} Arguments: d (dict): The original dictionary. kwargs: any extra keyword arguments will be passed to the callables, if the callable takes an argument with such a name. Returns: dict: with callable values replaced. ''' kwargs = kwargs or {} result = {} for k, v in six.iteritems(d): if callable(v): v = call_with_appropriate(v, kwargs=kwargs) if isinstance(v, dict): v = computed_values(v, kwargs=kwargs) result[k] = v return result django-tables2-1.14.2/django_tables2/views.py000066400000000000000000000150371317557360700210040ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from itertools import count from django.core.exceptions import ImproperlyConfigured from django.views.generic.list import ListView from .config import RequestConfig class TableMixinBase(object): ''' Base mixin for table-related class based views. ''' context_table_name = 'table' table_pagination = None def get_table_class(self): ''' Return the class to use for the table. ''' if self.table_class: return self.table_class klass = type(self).__name__ raise ImproperlyConfigured( 'A table class was not specified. Define {}.table_class'.format(klass) ) def get_context_table_name(self, table): ''' Get the name to use for the table's template variable. ''' return self.context_table_name def get_table_pagination(self, table): ''' Returns pagination options: True for standard pagination (default), False for no pagination, and a dictionary for custom pagination. ''' paginate = self.table_pagination if hasattr(self, 'paginate_by') and self.paginate_by is not None: # Since ListView knows the concept paginate_by, we use that if no # other pagination is configured. paginate = paginate or {} paginate['per_page'] = self.paginate_by if paginate is None: return True return paginate class SingleTableMixin(TableMixinBase): ''' Adds a Table object to the context. Typically used with `.TemplateResponseMixin`. Attributes: table_class: subclass of `.Table` table_data: data used to populate the table, any compatible data source. context_table_name(str): name of the table's template variable (default: 'table') table_pagination (dict): controls table pagination. If a `dict`, passed as the *paginate* keyword argument to `.RequestConfig`. As such, any Truthy value enables pagination. (default: enable 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 def get_table(self, **kwargs): ''' Return a table object to use. The table has automatic support for sorting and pagination. ''' table_class = self.get_table_class() table = table_class(data=self.get_table_data(), **kwargs) RequestConfig(self.request, paginate=self.get_table_pagination(table)).configure(table) return table def get_table_data(self): ''' Return the table data that should be used to populate the rows. ''' if self.table_data is not None: return self.table_data elif hasattr(self, 'object_list'): return self.object_list elif hasattr(self, 'get_queryset'): return self.get_queryset() klass = type(self).__name__ raise ImproperlyConfigured( 'Table data was not specified. Define {}.table_data'.format(klass) ) def get_table_kwargs(self): ''' Return the keyword arguments for instantiating the table. Allows passing customized arguments to the table constructor, for example, to remove the buttons column, you could define this method in your View:: def get_table_kwargs(self): return { 'exclude': ('buttons', ) } ''' return {} 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(**self.get_table_kwargs()) 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` instances. ''' class MultiTableMixin(TableMixinBase): ''' Adds a Table object to the context. Typically used with `.TemplateResponseMixin`. the `tables` attribute must be either a list of `.Table` instances or classes extended from `.Table` which are not already instantiated. In that case, tables_data must be defined, having an entry containing the data for each table in `tables`. Attributes: tables: list of `.Table` instances or list of `.Table` child objects. tables_data: if defined, `tables` is assumed to be a list of table classes which will be instatiated with the corresponding item from this list of `.TableData` instances. table_prefix(str): Prefix to be used for each table. The string must contain one instance of `{}`, which will be replaced by an integer different for each table in the view. Default is 'table_{}-'. context_table_name(str): name of the table's template variable (default: 'tables') .. versionadded:: 1.2.3 ''' tables = None tables_data = None table_prefix = 'table_{}-' # override context table name to make sense in a multiple table context context_table_name = 'tables' def get_tables(self): if not self.tables: klass = type(self).__name__ raise ImproperlyConfigured( 'No tables were specified. Define {}.tables'.format(klass) ) return self.tables def get_context_data(self, **kwargs): context = super(MultiTableMixin, self).get_context_data(**kwargs) if self.tables_data is None: tables = self.get_tables() else: data = self.tables_data if len(data) != len(self.get_tables()): klass = type(self).__name__ raise ImproperlyConfigured( 'len({}.tables_data) != len({}.tables)'.format(klass, klass) ) tables = list(Table(data[i]) for i, Table in enumerate(self.tables)) # apply prefixes and execute requestConfig for each table table_counter = count() for table in tables: table.prefix = self.table_prefix.format(next(table_counter)) RequestConfig(self.request, paginate=self.get_table_pagination(table)).configure(table) context[self.get_context_table_name(table)] = list(tables) return context django-tables2-1.14.2/docs/000077500000000000000000000000001317557360700153415ustar00rootroot00000000000000django-tables2-1.14.2/docs/Makefile000066400000000000000000000110211317557360700167740ustar00rootroot00000000000000# 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.14.2/docs/_static/000077500000000000000000000000001317557360700167675ustar00rootroot00000000000000django-tables2-1.14.2/docs/_static/example.png000066400000000000000000000444121317557360700211350ustar00rootroot00000000000000PNG  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.14.2/docs/_static/tutorial.png000066400000000000000000000111701317557360700213400ustar00rootroot00000000000000PNG  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.14.2/docs/conf.py000066400000000000000000000025541317557360700166460ustar00rootroot00000000000000# coding: utf-8 import os import re import sys import sphinx_rtd_theme from recommonmark.parser import CommonMarkParser os.environ['DJANGO_SETTINGS_MODULE'] = 'example.settings' # import project sys.path.insert(0, os.path.abspath('../')) 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' # allow markdown to be able to include the CHANGELOG.md source_parsers = { '.md': CommonMarkParser, } source_suffix = ['.rst', '.md'] # symlink CHANGELOG.md from repo root to the pages dir. basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) filename = 'CHANGELOG.md' target = os.path.join(basedir, 'docs', 'pages', filename) if not os.path.islink(target): os.symlink(os.path.join(basedir, filename), target) extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', 'sphinx.ext.viewcode', 'sphinx.ext.doctest', ] intersphinx_mapping = { 'python': ('http://docs.python.org/dev/', None), 'django': ('http://docs.djangoproject.com/en/stable/', 'http://docs.djangoproject.com/en/stable/_objects/'), } master_doc = 'index' html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_static_path = ['_static'] django-tables2-1.14.2/docs/img/000077500000000000000000000000001317557360700161155ustar00rootroot00000000000000django-tables2-1.14.2/docs/img/bootstrap.png000066400000000000000000001500321317557360700206410ustar00rootroot00000000000000PNG  IHDRsBITO pHYs+ IDATxw\S'!a/e4( ( E-TulPpbŁ**Zl  ,3$>ߏɹ'4P!B! B!Ø!B!˜!B!˜!B!˜!B!˜!B!˜!B!˜!B!˜!B!˜!B!˜!B!ꘘȑ#VVV[n%l+++{{k# 322Q*))l/tRzquuugϞ;wwڕ'֮]keeu)Ī>PZٳS~:ӎR>k돨 ++׷|\_++ѣG sMZZstGک?deeuttrrrΝ;7cƌЁN2%>>3%>6m_Zk;tB}sG_֕+W޽{{~~~~GpիWJKKW\Y[[۱͍5j}R=*9)&&ʕ+BkOG^ !з!JUTT,X ѧ 6XZZDuu;wN4)''Ç:BBo- MWRR"!PB!g~Sjkk{ vqqwXSSsҥwrÇO:URR믿v>q?ѣGEEEʢTTT߸q#''GAA}͏=:t萕Çqqq***3gq %)+..f0666sQSSkPP(믿Ο?r O?zvvvPPPTTT~~\=\\\(G \xڵk|>_UUo߾3g֭н{4$h&[Լr XYY9lذO0J?}ڵkoݺtٳgGFFYYbxzzΘ1*뺺:== &L2No§O9r$11QRRW^gfX >} h̘1&Mb0>…ׯkkk͞=:N VVVy7w$%%͜9s+W\~}RR%K-߿믿^z___ҧOYfQvpp(--}ٳg/^:`󹲲288822t###'' &CUUUTTTxx_&++kcctRx㙘~Ѿ>-ɧWs>Zŋ;vW^T[^|YOOٳ222ЖŋGuС'OTVVϙ3gԨQBܹs.\TTT]dhٟq߾}_'O;vls)_>s۷o;udmm=o<B9>y޽{@UUUGG˗nnn={lTEE{bb"FQSS͍}پ}D3͜9@NNNNN.'''888**$OaaEl6y+//pǏBpdUUUcc̫W޸qۻM73aÆꚚZXXlQaa!p8n n%7o(++p8˗/߼yo DEEZIJJVVV>~[x1m˖-$&ҥ,˽v͛7ĶuKJJWSS&;bsss322tB_zU__O܏?!C4DCCcذaeee={TWWٳG &''޽;))iƍ 񇔔=zѣ+VPy=<<%$$ g*X)//_hSWWWZZ:--ĉW^ݻw/(**yF^^$p_|9 3229eP[YYy}͛^kmmtҥ;wݻGMZ ^hQff&Fc2qqqorrrW^MHH^p!+㽼 5cQ@kN4Ν;O>ݾ}3gH,˗/KHHlݺ*mfffΚ5..QQQW^/,, KKK;rh3gӧݻw6mzMZ({р711ͽr͛7u+*LmbBCCP(,((pwwgX, deggGcX'N$)|>̙3`LL I !)'N|%IfX.\*\r%Ś1c%)w4h) x)5hРjkk븸6P(ܻw/ڽ{7DTTŋ!CX *qذa{ ^^^,kر$ۢEX,VXX3++֖bر$>{lС,$%>>b988lR]]yf5mڴgX#GxY|9 R|||;JaXG&Yb|Xp!9̊ cBl7>Y,V~ ٳ@@PPP@ǏE(fddL>Uu^Bv˖-@jjjZ3f`X?sVVI̜9s&ڶm[풳,X# KJJ\]]ǏXu֑?[?&r\lNNP(,..1b:|0~555+V kccse>/ oܸ\YgggrѠ-޽cXd֜}66^x.gΜ4hPFFF Bem[M6Z?00Mߤ(6e9<+++''&m ߶mfnnnddd:urWTO$f@޽53H߳gO>\ϟ?-L <?NxBZZ4777;;$>y(MMM---pwwo߶Ν;NOԾaaawV{}>|H꾾ڹaaaOiӦ͍FEGG|ssyQ\]]ޞ&vZ}}}.{͏w&%%tuuw`0._+MJJ ,--yBojjjҏ?M->uL>WVSSscǎ=zOTmmر$%%ϟ?%$$ё7]vQу\4;ܑ:t>3f7ĉD!Qmy $E^^~Ĉ- M I`:CCCCCCSQQA| 4SbccKKKeddtfΜ Ϟ=VZɓƋѡɓE9r$DEE5Y?97Hwpp@Y=ztɂ\n]sU#""/͘1ѱ4hDGG (**bٽ{&7_zdkw=dшNeǍӂ3x?ÇlkpIKKO4Ф9ɓ'[~QG+AN&ݿ L޽;u4Xo@ơo߾rrrTTVV~b:&r\.ׯ_˵5YCQӏ iAAAUU,=&ݻ?}*-d#֤v(rmD:.''r322lvJJ 4u^u޽A J>ѣ듪 PEEE4|6μ}nӘ|q}JO2-^^^TLiŰR2UUU&G2.]ƗqptOOO{y<BkCMM37}é/M-KPw7}6y闕Y6 d2uuu fA(9sĉ$FuU]]=&&TWW7LM M^__s]|9%%%99999ĉWn ߿?((Oٚpl6d yoVVV6Nn-hS\]]-++K3$xiLD>\*:KMaњM^E#LB 6Wgee%!>mUܡSMn?'˗ӦM377744400`0f>\|[V[[[WWͿÇi4S߾}dee9dX:Cv`0f̘1cƌϟ?}\.wgϞmF~gŭ=|޼y222ݻwg0'77,i’O8^tvVTT4y&' #9Vz˅BСCTTT^~d|Xעrk֬iC綎O9 hBpӦMgΜڱ4&{BFs+++:ƾ|N=ztҥ#F011!_R+_ۄ/~|藼$HMMM%(r\_ pV^iӦуM! D%%%!PPP  <<##,mm &ر#<<\WWޠᅦKHHlٲ7SQQ۷\.ZZZlv CJKWWLÿdNٚo%ًc@@iӬȂΫ+tɁ4XQd&Ϸ862>uLD}ql^^Ȉ.@a /r@.**j<^j6SBՆFŋEkjjnܸBArWPPhV&&&lznR<ի j8wXK[FҥMuԉJrcCYF7EQHӧTu {Λ7,(++"K.]v3!P:DDDD$''w޽"۶~z!;P?ÿ2iiid sd{E555/_k'D϶r?-";SIΡCJAqRPP`dCϭ=6ݺukEEӺu̙3o޼!vŰ &|~n,'HzI=:+WvTB?m{L&ڵkǏ' 6l {!'nݢ߽{G~Y܂]UWWXzyΝ;E1L2smXWWw _~M¿kQbbby<2r… T\b 2WȏPhxʕfffLcs̑LLLܶm ͛7[lqƑpK"""PӧdGIB~~>ōٳg۷$ ܭ\XBŋ]JIII۶m#O>#:]f ˗/NG0332d@ LNN&يVZr544LBɷd15Οv1\WҥK!!!鲲kѣWqFQl^^^Ն 6msǤcež>}*--vZݻ?  7oL+WR3kGܹs!!! /Ο? g!P~R__{ӦMLMM%o{M2dÇ׮]{155#%%գG|5;~x##l@PYYIm6s̴˗/^sjjjL&_mC6K1bD=CCC>|h``PVV(?|Р{{;v=zTUU555VSSsǎvfdde˖ 6߼yаӇa3g֯_hhh$3g$?rJn:qD[N2e-i4ϗ6`!I4ɁҵkgϞΝ;ãaoo~]==RII۷S }}},YѣGTWWi^zy{{SUy{{{xx$$$L>@ZZf|uuݻwSk%%%NJ>|ؚΟٳg{|~bb"dz)++ݺ'((H]]=55FWWw۶m-ظ?dkŋM8,BHOO'fOv4oKԴcұʲ999~~~pBj"_b8.p8&L011 ̟?9%%ٳ>>>:::EEE0gΜE!Բ6}5Æ T???t vijjK>?qijg.\=zԚoE 077믿f͚ZTTԻw_â)++=z"//뻹f۾};MEE,9CSOszu̙Cɽz*%%l&/Alܸq…RRR)))nnn'Olj?FFFnnndK--ӧ={I[1hYPe˖*sss< y%bcǎY[[gffVTT 8WuZ~;;;:TWWgmmk׮%KtHB?QwQl6EAAݻ^y BM]]ݵkȏD spp(--=sLkJ|}hz IDAT_wﶳ#ws"tĢ~HEGG;99_A/ꔕ1F!B_}.~mm펭bbbRPPqyDr[gW!Bk=s ;-oR)߿cǎ 2rkJHHh`zA@!BM:8.++QVV3f;rQӧϙ3gY,رc۴BZZZHHHkr:99a̍B!A,P"B!(b!B!D!B!˜!B!˜!B!˜!B!˜!B!˜!B!˜!B!˜!B!jo!B}Q 1wΤ8 B9h\[B!xa̍B!xa̍B!xa̍B!xa̍B!xa̍B!xa̍B!xa̍B!xa̍B!xa̍B!xa̍B!x1:zͷ4xm_!4n.0;7ꅢ)*7kպ% SJe*/ UxX5~b8BCOt:N;c! WoLU#tL$Cu9+k˼NvuHY!ujE&`!!<{,&&ɧdeeg̘y?FNSaT_O֌7owM*N'wF1Ńteyw5ά3M B*Hw^to!5#tСCM>5{OyWx|Ayɽ$$~Xygt(tl} s4do|iFV1WUSA-~H: mt`}ni4zyV z)s=1%FS^3\_MR4w[XO1%o8EڝdW7wMS]'noq/ӊd%%FZ:LBdjjjoo޽siwŕ4zIIYgݕeJL|MVqe𮃻KNDꭵmjoz1K>>}QsF[ﳊz)oKE^jCxQb."rßCf,p0,A 2UeY戞L;l}5zʣ-^|IA h!|MYI 8N~e 򯺮Cꔕe)_OwP^cn}Z[6L՗1Ufu76Q!-AN~]6OwBۜʚ0BϏLu|$7T@N/fS*n+J_n[͓{ p+55/'{㍓z@BVC|bI`nM=BCAA jk5%o !WWLE#Ba O6kq8guB b2K`Hϱ֦K 0TS$y5SO^B;;.,A߆ϱ䯧dS-I)J}X.zٺ[Q͟ggrPa ;@vrwm38w !mԪɓ'$7+J7V \!?xQ](M[&}(w9z&ŤjWѶH_l>7Ye6b% !q:7 PRU_Vǘcoq'6XAնsfQwv2 !XC>E&Z {FgE|Y]WZ4&:!wk׮C;wnTzO=)#)QQEݴwS?b: 0 :5ڬ6$+ Ye>ٷq)}WY0VBg4ٹɹ强^]ؙuvѧEJ u0PCsubގq-;2 ?~]Ik]բS ;tɍ,!uϟ:uOrjr'ڌ%/ ʹL5Fl07ovTs?? 6;w;愄˳zVXqvd޽rO B}nlsW4HݶG柳Wm6I/}D+11w}}իCl~^rɓ'g̘m۶i`mmmoo{ڰaCff&HJJΝ;WIIK! bsu:\}D-(=ޚ !JGRs'''YZZߺuϟ?_WWwƯ^666_!PHHII-X@YY!g)Enɹ *oXEKYF4`2Cj i4ZuuP($)W^8qb|||zzip8v튏ڵ#"">|EgŊFFF .+**ںu+I 2eܹs_~=0lذ۷8{p8***SL>}:TTT޽ѣG***?ե8PثW5khiiݹsٳϟYf[VVVTTdkkk ,//gX^^^Ǐ8~<߸HsdO?|X__O2[n竪N>}ڴi0s̑#G޿?%%E[[{ժU-ؾ}w/^\__rJ*ҽsΎ;Ht{ʕѣGoܸظ" &[n„ AAAϞ=#gϞyƍCBBLw^jxHH[zk.[zʕ+[haDoԭ2!f7c!g6}`Awk(IYVƠHz̛z?6N~ˬz3oM5O[۷}<:ĭ-9pӧ?~ԩST]zyYY5jԈ#Z_`_xĈƍ&q*))QSS6l޽{ JKK޽|r33~͟?dxӧO_tuu-,,DžN/X@RRNfhhhffp۫W/GGGݻիW o޼rJebb흕Eijf۷N2ή" &gK.%k֬љ9s$uG`2&Mp8L&F1 UuB[_V:ض:wW/}.MtMODSæUی M4䭌TtbjDWEBWѱfZ~94>Y[Bٿŋwޭaaa6mcc㌌x;==]OOOF柯SSS[YVCCCMM<޽{ddds9'O`cc3zN:®]RM?f9N||!yJAAtd:Ig0511s8 Q5())q8HGGO=nM[|өB&;;o555Ԋm @ZZZImFE}l9MUmɯ$"/ItLOWYJNAe|V8/bnPs __>}x@k׮>BRRR(hٚ\J!))I= 6WsĉqXXؖ-[:w "7QMjjj <8))Z`qZV[[KuRP(:)%%Ea6.@soa<`8~ӧG=`绹QyZ9B2!GD %Uuob6 !Ao|e,(q=$mUup7b:B_=j{졦0@UUsLp8r 033\AA޿OҙLfe2ʢc)uuu$LLII100QRR"<^ty͛}ΝիWD+++HNN&ݻ' t:>>&LPSS=z4z'ٳYh_յo߾Ͽ~zۇ!Ib{jRwIno(4lZCϕe]/(YN(n KG\YS/'[/ERAqVP[#δ 2.kddԎW B!BH!B-i3 !j \[B!W cnB! cnB! cnB! cnB! cnB! cnB! cnB! cnB!īmC)֮ B!kP2>CߙTB|wC5=kKB!/B!/B!/B!/B!/B!/B!/B!/B!/B!/B! WXakk|*`ʕ3fٳg;9&o.**{9::ڵkVsɓ"G!T\Ip}oͩ{> M/c}^hݺu ĉ7nյ NJIIҥ;QW@__ƌ,8`W9G!;(tUSI&6dODbV1on_k}:&rї/_144qㆥ9Nxx7obbb0̄",ؾWc__XgQB奙 fJ &[wUҽC11vhhS RRR"+Vfiii[\\{]vAjͲeˌ=<<|C"""UUUg͚E&_z뛖2k֬TWW:tƍfII=<4^ \`B5TXQw-U? :f=7N'555W^}رc ++KKKk޽Çwvv Z555:::/^:u#GZԩׯoݺŋ...~~~`ժUGp²eիm[?~/[*rgnذ?sNuuG0,,,000""ѣu! IDATɯ {)&JiҌzh3mŐ%Ct.9|Me {uĚ%UGی-bynfL΍wpYǜ zN\^u?kޡo*jlt¶xu޷^3gٳƍWӧeeedy̘1#Gl9 飣3gIIɴuuu--Ç4X RXXvZ++]n۶FEEg-Z --~ud2@RRN}y _~iڴi'Nhfffƍc0ƍ .L8IOOoŊC-//͜9ֶk׮ޭ@WWW EE1c@G!9U@A䳼Y ;vU顧4R;C sNlf HJ8[Rbn)5¢3g65UST M8[hͰ1.U {ACInܸq偁 6mb0aaaD]K.22lѣGJJJ ͛r8@ //?uu۷oNNN:u-N*'*))u%--tCCú,eeehP5@Ϟ=ϝ;BWuuuJD///"䱶vh B_$VՒ$ KN):}NyZ~EԨ<4y -)QW/: 2*Rk@ $v'Nr@qyO *2K@oPxxx~@]]'766*d!;A8FF-8r䈇GYY,I_jչs&N?޺uKdvB!鰦n\ UCcmd684Q4MH2crݓ!0,/ɠ(MGI5S<&[SYzOƢ{")&攙(gnUQFF"))<`2T6.K,]t͚5&LPTT >>>AAA VZw҅FPZZi``@LHH bcceddtuu  &&F__ۥKߓ|>INccc3d 8vhbzB4>B4j5\{&XWUN\PGxp7ܽsYK}KNU$6PH*%y +j k C½;:VtLmnn.!!}ϟ7NZZdn۶-===""ŋSLNVVV7op8.]"ݻw///?~8Oׯ__jkk\wޜݻs7oޤz{{8<={L4IRRu{JN{a.{kڴim=^/FFFfgg0nݺ>|]uVjŋ111O< j&;B3[حϧd?O) 帹jr22^2^/.[Zo L-Uq-1-^.pq *^!  &W_HFoKǬ疕ݿٳG?w1}tOOO{{iD|||m6}t''DYbǏ?>vXj ;ƌIIIÇ =zt/_ p!)))K4˗/?~:;;߿_BBBSSԠwcǎiiiyyyM0kooh"={. qrr*--ݹs'sqqp8$}ҥҥK-[BwqqikBuMec y;yODR9NWUkl`DO7"S/B0Qr2jBeM݇wt!e#Y)ڬdՊS_R^οy| P^lq$=UŎݶ_M.`o fSSSn$qSNݾ}S}yj B!J.Pk B!xa̍B!xuHٳgϞٚSVVl!BVynB! cnB! cnB! cnB! cn?<.iTҾ""咐tkw%Bv7KJ,!tov.QB%)iҾ񹿹}ۮeZϜ=g3@srnB Z4>U322D WNWW~WWW>3999 C|ځX[ ZȹD 97h!-@srnB ZȹD 97h ?޵kҥK,֮]Kx/_2É̙sy'$o.++Q|85 ~NÖ;?94aw#}Z*]Bι_|y9 _#!w rppÇm;;;###G'^^^uuu_:ZI%w~:;g/*-02K`-.m۶} M,,, Le43eQ$lrjp3|S!TKԻJ~̹{衧c!6maپݻw6lXLLLpppDDġC-tuu,Y񂃃###ONg]v:uC^"""MMM]]]\cǎ'OjiiΞ=Ʀx׮]O>ܹQ,Xd2###Ϟ=re˖M<ҥKnnnzzz~~~eeeׯ'XXX\pA^^u̇x"!$!!?3gIꂃ*++MMMWZplll~Ǐx{{+**R8::?}issgBuuuS^^>|5kֈ5Yf3ݻ骪W&''%%%>}xzzBLn:BHOSQQŇbD+.Zc׃J)ֽXL19)qBHrnEЭ|>)9PENS`ޯvFeTrM4(+AH(}]Z#d 7Rto(.Fɱsr? tTZUf㴲.OKA1vs mmIrr7\\\෈庺M<رc?}͛7׭[6uԀ&//1cƜ9sfٲeAAAqqq-$&;xਨ={̛7ZZZ<֯_k*++߾}[II͘Νkgg7f̘SN5s_nݡC]/_޷o˗9 !eeeC 9y… {]rrrruu+WV\tɠٻwo___BHMM'ʎ=JB9sںiϞ=!!!;w|ӧ;~Z3Y%5FlB1:,FM}xeUqe ]~ϩe!! ozߗ Zk3*02rYjg^/6_L.n7%:ݤ9'x[n]|PF=};vѣ;ٷo_55Yf1L6]SSpTTTFpWW׾}xyyRyի_|kBȋ/֭[ۻwo77 .444B-[6`UUՋ/hhh 6N3 BD{13 :.&&&...͛ׯ} wܹJJJcǎMJJ"\pp޼y#F9s.a…L&sᄐiӦ)((;n8cccqqI&Zŋ[[[gffBv䔕}}}:~Z3U I67: X<>>YYY\.KKKO2eAAAƍҥK%uuu٦NNNV֭!fs\j›!222MZdff X>h36kfggB;w`={$(++S{60rHQ 5TL-D!0]+)) ΅ //?y˗/dα3gn޼9""~DYImV /͜<@ܴjvIurnb'^Ԇ8b*#;y?+:*JGQY\Te.]:1.>]viJBH.Łopr۷ogff .#WGHHH45曚&h+;:nܸA-Xٙ*wuu8qݻwaHKK/^_p!!ccc.\((b?ڼqJIIO?laaaiizQJ{Oݻw>|~UVuD[AOme,ʝY- R+c 1Jgfh!{Y^WѨ^\OZfc2exў-hgm֭[CԩS CBB7DGGݻwUo߾6ū-77_tLMM /((Қ;wnpp55511B EEB߿;w޻wMM͢"&IU:x`7 TjM4ZAi3i+Рhv޿ijjQ|ի Ǹs rrr(--m}jm>MQUUU&MڹsUnݺ% ,qge XB"9j]%.I>~̙Wl}%K >|Æ sp8~~~wtt$899~MZZz…...ޭ#6lآEwjHooשּׁ6c&X[[yyy5ovŊ[z޽{ 1ۤsΤYfرqڴi4eԨQSLqƊ+rss&LtΝ5kHHH,\pܹ 9(˱믧$/*}c/OeͶGXG7^k[^[ml):Jrs<{aJBM_F9ZIBRgx~}ODꢑzL .h 9998O}vppCL_ b@s2̙3gΜ/ @srnB ZȹD 97h!-EX5##C|tuu?ᨏO;q ^>'Acm h!-@srnB ZȹD 97h!-%~[`aBB˗/Bbccmmm---\'#WJ97ݴiS~%.]RRR֞2eʐ!C„#Pܿii:pڴi|>BچG% GͯL-WT;˫I6//]NJ_k$̜={2d$77ؘN}}!-,,Ν2w\Bȵklmmu\\\Kx666O*L6-44bΜ9iiiޖ'Nmѣmbb}{B>lmm=b''7nfkKڌϟz}N,--Ǐ___OYn=}M 6-/*:)FGES^ʋ\쫐7Ef^&;:۶;4֖UpB_ 9v탷Z~ё>7|N;7ZΝt577]t),,|'O~MΟ??c ??<.H7{lss(sss__?xŊWpܹSNY)))IIIO?doo?nܸ-Ǐo۶ɓ'WWתرcѢE0l Ϸ0Tdi{*]kB'rj`Rg!$q}//^ ::aemmf[`24MLLL\ÝFqF!0 :޺ԍ7ٳ rrrǎ۰aڬY>b+V\}.C2XbBC&F'tMWF1 c_fgWoҧ=q IDATy;c) Fm}2kC4:QTEN 24GQVV;vl^zܹs۶m3f̈J_'6))ׂ^zwP~xlvJJ mjjvu֍`2JJJԶxcccccc뤹MMMk%TTT><233\jkk\r%&& kv#!_F4T$RS;̨[]ZdUu*Ŕ{+Si]|m~efqUR{nwIE89[222%fffҌ3&MT\\zb5OU1Fk^M0Ǐ?bNNN:בXWFvIkAAA 7999t:=66VOONB7xM{lK)4DlHK%IZ*M^ŝVɹ}}}\.}ʕ/_]V^^Ç7oQSS#ܻwoĈB뤫]YY)##CIIIūAiNN!!$,,lŊSL!|}BNkkk~zРASPP"iS'N!X,//ݻw1B0[߮Қ׹6FZ1W^zō郵pMN z̤AT>~M(znmmm %%ehh`0455o޼y…;vN8Q(=~̴7lf.^Hѣرciii;w,))edd8Nff7!'k3fOŘl$M 3K%E_YfWe'e+JW6gW6\ͽSkwM#w@u֓'OΘ1#===((s"ˢh;wspp8~}jjjnnnaaaoll0aU~t+VoO^G?)S6l0|99EرcKKKW^ݼ2N߽{.]jddظyfkkk333y޽9܈k*J2u֛F#376`]x!f{+Ws=kl{TsEv_3ddd4o‡ɓ'oݺ"ٳgϴ !\.wĈݻ x"𕋈`2'N֭[_B$ڵ%X|‚  Qwρ% |OK<7h!-@srnB ZȹD 97h}P4ܧ}%jBߙ|g!rrr>@--@srnB ZȹD 97h!-@sM>=11QPݻe˖9r֬YO>nw vڵvs9CLL vUņ?4/,:~>|#w3W,6Dg1|sy{{{NPTTT=jeeYXX(A쌌!w ֮]ijjN> }L,P"˼ J*;LlL3tڝRB˹Ҝ>zM[[{޼y=zr劰z2eJϞ= !|>ZE[[{_6چ'e#tR Uu<ez]U$ח`ԕC @!!^xann>ѣG {)!!A=ӧORRz:l__ݻ6,&&&888""СC B%Kx<^pppdddqq5MhjjꪠТ䠠$>ߧOOOOȳgϪܽ{wٲe'OtRhhhii!n޼9::ZZZzܹ&M"̙3^KKk .tڵHvٳN:9;;/]4;;{˖- kAAA@@@ll,N=zҥK}׮]eeeYYY222򋹹y{cRRRbgg|cǎ 4իl))Çڵ痕=~K.o޼?!͛:uh9ewbQe1 !/O}bɗ]}/Fy, UT}-Yfy' ]U:܊[I9|GSsa{"yiӦ-_E^UZZ}X[[B,,,LLLώѱƂƙLۘ1cTTT bbb"={9` :a,ښS=V\*fHCqZ)!!F:&+d]~lNvI{+Iv]ec(%otxzWIcuY>ʙ_B[[& xX__/)=l!8Ǟ={fddtPJMMr|>???]P.;;TP"//?y˗/dj,iccCmX,j[SSڠh222\.ڋw֭k׮TQZf{njjz]=:>wjn/---BZO<*EEEjz{ХKjľ7#*HF.O1V #g\AuR-J:J3Q_~VX.NέQ$3Ds+((wP['!!|5aLhͫ566Rǎ 7nܠA,XL ZB\JJJ~'eee KK˔jT[I]^$onY&ٺc<ڋzbE!箮Æ 311O8!%&&&,@ 2kM> 6BY,ĉɓ[~ZCVTT$$$TTT\zʕ+m:uK`0 ,$%%9 !mFҷo_==۷geeƞ:uz+bܭ[͛7/_޽{-JiLM['@XMZ|WSNOIzY5f*+)^Qېͩm%.m|D;`0vܹuֹsjjjoݺui_F۶mo6g===jiʕ+O8qĉq xxyyܹ^AA~ȑ#,YBٰaC]]]^Z5jTBB744n|*2lذEr8^zر%111>x`{lٲeΝNNNrrrӧO#L8߿v˖-;vؽ{y󤤤Ǝ`wssĐ]]]jv>e-[̙#%%5`ߍ %UuoeUI7]LN/ i4ejR<>Hîjc?!CUc nVs;Ic"ڇIFFH&|8>}{k.!k`@> ,''GWW@ ZX[|!+@srnB ZȹD 97h!-qC)Pr=LFF3xu<%@srnB ZȹD 97h!-@ssI&zJP;w\KKK{{pv]x޹s篿*N<9w*lڴ[433cلiӦ Nkp/س%w !ߕ֘yEov יyE~f<.'bHFZ`:OOlAI^^ފ+7mڔq.]XYY Sч' 4s;Lt aĉ=zQW{+ eEY־y|KI Ch9wjj7?-"hhhEDD |HHH,YKGqMC e9k- ;I e8\_^0(2{b`1:M|AB{>|yѣ7nؼ[a4㶶cǎݸqc]]!ƍ2dHxx8 5779}4uxCC?z꒒ٳ&L7H5B>8MUXbVj;:~WL{<(;z7u|}ew޵9wҪz?Gn5hh 3K9v;Z .Uvr1Ct>.... wNms8 aպu֙3g.^K&&&2L??!CcVUs !OJ, SGȞJSjP%]{l#z*-:}!d}x|YUVX*lآ*\x.g85GB *=NMyjI㴖aemmf;x!|RRae)Iє3>$?‹̲&>I+W"*s+sޓz JNd3Ԝ:}.<{GlByu 39AF+ufJUK|!\R[[]%uuu6]vUVVfmִ~ѣ?| &*--Mm:uMw^KKK[[[j@,lN0L&S xcccccH_C/r:KS%\^Ֆ[_kO-c*PTLkm2⪄2 MRLBȌZ geT'{Oi߹*ȱ SYTek4F: ase˖UUU+*;#rR%pǏ?bNNNG :999ŋw^v?8vXccrPPP~Z w0Jϵ)ͳ@11upA\5oFt Ǫ9qqw W\8pǏw02\.NwիLr{;wn% IDAT֭Gn͇ KQh7"eXCIY4`jJYb43ݮ/- 5y:Lu[{L+HYlսܫpxnIX,yfm?i\N'W`!ॠ'ߪUV>|?pB^~Mm$&&JJJ [blMM WWW+((PTT {ũSLMM]]]ϟ?/))--"&IUܷo_SS|73*jf}NL{ܫWռBbbb nCYUUCe.]S+++UTTz>h3;wjkkO!!!QQQmh  rbNj)Jc2RRu "6 !dPwe/iw%ޛ54Va-vi)v*Z֐ <,izi\e}TjIZSIy/x6G͹ϝ;ŋӧ'''?|Ç "k痔?iҤ3222qqq'33ۛR__b&O㜜7&%%ijjk׮Ĕ 8POO۷o޼ٸq#koz"))pbbbP?~|Νč7vpxy欬7o^r*6mZbbCrrr_O>OqFNNSRR&Nq_999<߿gϞ}SQQ9rHjj ܹC,))+((~Z며#"".^8cƌzj@@@~~~bbbbbb=, m|_7"'${7y'xM,q:!dpw4B4䥴:II0)*]iM?G%SͲԹs|N{tPAAڧOBݻ}}}ϟ/!!1tP77BN-!idW|,m_sZ eX-%9B[S2xM|ZX2xRc2v^MR~=:B~ptھ)Yn&B[Vjloߵׇ,+X:XBFFF{7u0ٳnPq|}}o޼ZUC|_ - ϿtϨQD 97h!-@srnB ZȹD?jFFHC~Qʟw|O;kKD 97h!-@srnB ZȹD 97h!-k֬8q]]Uݻ+VX[[;99={LX}[nܸ1uTQpn߾ݺ<--իWK}?غ< LJھsNYYǶx|tB{)%J9eöĴov*ös>3Dg|f#-'nll_}||?~~BTVV>tȑ##vȑG.߰aCNNοϗ5`aÆBJKK׭[r?Y&':X-+HrGE %osZZZzzkbb?ߺurWWWmmm'''##W GhTm#GQ܏{,q.U40ҊZ```Ν4'%%IHHP{NJJJ_'cgghѢ .x}iӎ=ze3g_mۖQRR3YYّ#G:;;3۷o 8󄐙3gݻ|СbbbNNN zQee!ѣQQQUUU_rRiiĉ,Yr sss//?y򤦦FKKkҥfff-Y~}D9sLXX˵rqqa0eee>>>'N$L>k̘1W^ -++_lYϞ= !7olhh4hʕ+Ûg)"{Yˬt[*,LVR|dOEL}^p>J !k“ q6%}Sr :̒m90{zJT՗˪YT!Vᅰ$RZjkӥy;mV4=58q?F5 dљE\+n&a湥Cms 8F*((u~sܹs3gٳwwwcǎ;vַo#GL2eÆ eee!!!틉!EFFz{{:ujʔ)ϟ3g'k. wwUVBt|||?~ׯkjj.\/{<,?Ie{BOUdPs3n©itpC*T}Q ~9l:PTBQ>j+vq-ESTw_95'dB ֝KOsIسLPF 9ᯔ!,YBr]L&^=~m~g5Zӧ/X@MMgϞVVVl6h"MMquuu.]200prrRWW>|3 !W611QUUupp`2YYY NNh `ť{yyŋ :NySkL"//chhE5?BΝ[RRWZZڿ}}k=}?s~ٳ̀[ W\Eׂp9JݣG~4--BL&N?sԩSGлw+Wp܈KZJSSxv|ֽL4;Hk..^TegۭS/uYױ/54v~'EU2,+9swm߮7=g 3 wSREj[Hj_tQm*M^ە. w ̘;k%K͔z>s>c׼ys$KpچJ>6$B[h,rlDS٫At3 ڙ2UХt6CMC- : jNm/lddDa2T7*h:wL011UcǎMHH})!$''端3ydBѣG?~ZTT$=wҥK. zeikkګ;Olj_"UO &\, 9u=BҚʞF"8eRi=_pEE^,INyMZ~; /*3! ?+XYB4@WAPd"1!$ߥ,r4>K G ߤb֭[/^ڻwoj%m***4555b%Jrr|իYfeddPFBYfIPmbbbbcc>`3f̝; aaa dgg7dw!tvIV}D˖-wvvvuu7n%k%4yޞm3>DYfK())INK E1R?pe!fP سa3UD-9*Κ"Rz*Q/JD]fl@rr4Ġdّ:ڮ莳6X~4_[5@G'KU{?n* ܄=z<\rݻwֈmVFFɓ'JJJԵk׮∈)SھzZnhhذ"bժUqqqFFFٝ:u244444LII !Ǐ7oŋY,Vmm-2̣ 1 7o޽QӫWJJJ,,, i4ړ'O啕ƍ6|ׄ%rrrzO ~YZF @:%ׯ_qqq ;;wNYY9888$$d̙6l cC Yb@ 1bĜ9sz? +--7FFF߿(((̌5uT 'R\b֭[]]] !NNN.../^駟Ixxxرmܸq۶mL&v-߯Ç۷*88*G\r@ ׯ߶mۘL7:C K,Y$&&fg޼ysFFuɦDG#G_tgEEEllۍ,-- !f{{{4VMիÆ ΥY;|;j޴?m:嬆˫s9e5wm437]bE^^dX,^xq]]݁/^yM)b0ӧOWWWWcv֭V\IUS2118q󩅆g.LH*(nj=A ywm:\jܿB{rrrΜ9fffѣǏKk/mw=vjJffffff2B*f͏R3豨oNj:k6jϔOyٳBkkk555ҥK͛' cmmrU)))ӦM8p3g6lXnݔ)S ٳI& 8Too۷{yyΝ;7??>,,lСդAmɫWVZ5d &ܻw2{ܟyڵ 5jϟϝ;׷Z~ʕq٭]V @ pww0`@*#oo}M:u^^^ :F[reVVgΜYZZJP[\N+f7T>T@-\8zgցJ*߆y)-kڒ*҃I~|eW{~|~I-s,\d6\XPP}v1c9rDZõeÇ CCC ! :zoXXؕ+WƧOvww 277pss;~… #""67n|L+ꅙ/((`)))BvPJ$Too;vۻwرQL&SNl6믿x-Z<2ߞg딾|JaK67PRt&{GKbgtG U\Y'쬠nݺsΝ8qK#aaa)))ƍc0EEE6mz͛7nݪh+UUՄ۷Inw圜.]S?~ֳgOəfvΝBDì.X8{lII_zըJDUU*%%ӧN';6===:::??ΝMYXdfWZE|J;P8X0Tܽy}L:vc;8AvT)z_/h}mm/㣡xbGGG 6lѢE|>]]p///MMiӦ4JYY9,,l۶mǎSVVvss:x>ZOmm sȖl빿4}W^ueݻwojeee>>>Ǐի !1PҹREEW^kX|رy:t^*õqW\ߊnJIMMݰaÔ)S=?^tŋT㄄77~Búu^~y33Bw}7zÇ:t FhhmU'NX|y@@? ''gϞGFFRmN<yDB۷w1wܘ&GpO?t//۷ӻrJ-~GEE%''k !234/V w7Pm.Gsꦭ(_[/\|(+=sDL[r$N'FNsz>f.!$eSOؙoחK.BN?zі\{V ?y]Ïf:*! ?ޞH[/YR[[-ڦ9sX,EEř3g?`,]tvvvzbTc333//À.]jaaabb2~B.+///''Gӛ@$͟?_~) 6p@~oYtɓ'|>!dΛ7ONN`ܸqN"Ǝ?hҤIz:}tXw&Mb0999rrr1̆kkk9!ConjjݗݜyoRӟ>uB[yΝ 5.nkOJB.td=Օe.!N.`klVg]FsuӀΚKGv`<~Q:._$ݷqĉM6I޼yS(:99Iw6K. SSS>_XXhiir6iffF100x//k׮?~|Æ :A^__w…3gxyyB&L￟3?$;;[rޗ,??]ǁ?Ç%O\|y֭ z"9 j $cC+B-Ԗ}n-6 @d @d @d @duϡTڸ{Gqvn-Q[ [ [ [ [ [ [ReeeK,qtt3fL||p8.]@?ʵk׮Yy+WJsI^*Ddܹ׫Wf2nnnR 7vnݺeTTsqqЗ/_޳g:lƦN=5əgΜ#̞=;::[*[R,Kwhwؐ:hSS[RPPCnBŋ/jjj祸8 yӦM<rҥiӦܹ)::ƍW u-[URO"-IJJ4i>ի~~~6m*//,܎?pˆCϘ1رce˖lܸݻ۷o>>ZZZZZZݺuc斖F"5jӦMO\|y3SIluuu:ub27nӧO%055y rrrMn͛7Θ1#::zذaoߊ[MLLJKK Quuudd$dbbe``@OJJ:uwVPPuyqqq&LݻwaadKXQ]]m۶˗_x?Ybzmt2VyymO>}Rbggf6:|p&$$l߾(-----v+))eff&%%"000##ٳgAAAɳOOϢM6|͛[nР:p8ׯ_rݻw?qǏo߾fX/^///ߺu+͖д%F{Ihhhvvv^^---[qdni&6=a„nذo߾R"//m۶ɓ'8::ZJ*=+++%''{zz.Zh'OnիWrrr[neXӧO󳴴.iii^^^7n6m!O>+Və;wٳ%c6cܹ&L8qׯ'OV\z'M$//.e?$;;\,8@O7xSs@d @d @d @d @d @hbMe:6=jEB-dnB-dnB-dnB-dnB-dnB-dnB-dn\uIDATB-dnB-dnْVG<z! sA `2ԋO=#L !bv͛*PpRKm@ h_ NWSSСçg͆Ə>4sBttth4%%%rrr:::z.mge?s֖hkkHk6r]]]eeO=63 xkjj*++p&DŨ3hRxkkk9NN>pvP]]g(#4J%%%%zzz2jG*i/Q*KJJ:ty|4uuup\\\!4MCCモV%%%6l(,, ξy҅ BBB4557qƕR}||5Cu7gy !};Y`X555555(tEyټyvjkk+ٳg[XX6Ibbǎ+Yp***^nv?<7iUU-**B믳mmmn߾M#vٲevvv\.788ѣGbx͚5_ yP(ttt3gN3VVVeee Õmܸ@,1۵kWrr2ͮ8q",55UEEe ,PTT>k/4~`kE=7Kņsrr>ڈ4M 6"|D"QjjjZZZ^^xwޣGΘ1c݄}qǏGEEeddP[_СC8}t> !iiin2dHõWѣѣGŋ sݻy$R[[kmm}ؼH h/>~hp8.]Zq{9>}:0LGUj;F=z Y&111???%%r-???:ޡCQF={L$ݾ}˗G%p& pƍk|Ç7mDQQQ5j͛7UR/((hɒ%28 }\\\d:ʇdVdnd2cOd~A F}}G>; T=ģGVX1qD+++KK]vBtbwꤑX,D[l144$TVV]ro߾=22ԴrXy@mm={R[<`׺Ypcbb]]]\.!ӧO_jݱc|~XXS@@UrUVIڸqc@@@.m6777{{EBJJJ 8@:@ ϟ????{A $c_wgԩ {H$"۟:uJ(rDBN߿LLX,r~~~ΝkϞ={n޼(pB F߾}$$$49۷x<>aÆb]jUhtҴiv2hРKǏ3~iii6mccO&LXfͮ]N8!=|Hk[>|8>>044Z`0좢M6ݻ`,X@$ >͛TP(|rOu]~}ڵ . Ǔ#"":D׭[<7ѣGN:ueee\.wԩ,k;v.d9I 0n8I'555{y䉫}R5k֤?~ʔ) mhĉ]voO.}1==̙3{=իW !n ?ÇΝ[RRbff6}􈈈ꘘdɒ3fxxx;vLVD'n/^1200P(ׯܹCMNN=wUeeX,xߧ4qX,Aꪪlvyyս{8ݍ7bM&l2mڴ*X\ZZڪcФ_~], %K;vS?*]x͛7ۅ nݺU,Ϟ={Νf~~~QQQbsٲe666Tׯ_zu&y .jd<u5''gŊK,k.+ꅙ/(( X,UUUBHaaa}}… %\nnnӕ+W/]ࠨ(477B]DQSS366f]tirC۷GպΝ;__~ֲSUU܃Olvjj|> 3gΤj{PwwYfN8#bN >t|>ȑ#GyW={N6BUT %J|"""455%Pw&>|UVXqao!߾̙37o޼yΝ;O>}t:%-޽{?,ZUҨ%B%˕rrruuuoc͌r|~ !*ai9tʕ+njЪ9=}Ύdhh)YkhhHӋ{Mr?̙3vگ_?v!>qn4---_~ʗ/_RWSJ"Q/N:ձcGo677¢%o - bbbkddDݺu_ZZZO?۷oΝ-"֩33-[7s̟yȑ-҇dVT}l\__?**\ؤq57EEo6<<Ν;AAAƄ:w!C413&$$ѣG٫WֶUWW߻woffɓ'}BHyyyhhhrrrqqqbbA 9T@Zr?I&9sEEEߨҭ[7wwe˖>|ӧ%%̤M>,&YSSӪޕ޿ h">?zh,Xj*.7DDDHVpuukVK, [h@ ٹs'uV^2uT+++__߄Bȴi***VXQUUյkm۶x;|#4իW1117o611 [nBlll nݺ;w. O|c>$-x}}}II ,p8)#B'!u% MDj3^⧦VRR~#}!JKK>,>?0㵢D2{َUWW||b)4~xkynBNYYY;@*D"Qyy97iGQ*ՙWTT!KzzzzmERx-VVVH$***ر @YF)f܄ڒ---UWW5Q/g?sSJKKE"nlD"QiijGϢW,H7}h&TVV &`07zz y<oyyy555?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.14.2/docs/img/semantic.png000066400000000000000000001305141317557360700204320ustar00rootroot00000000000000PNG  IHDRr??i3sBITO pHYs+ IDATxw\SWY $ D"{QqT(ZV[U;Tk"( .,@6$yk[?{s{ (g{GZl2.sNBHCCüylmmޣ.dɒצp\.[QQ!^KpppCC< YZZ/27;v읭}...<vʕO>'@.K.g*SSS.{azq͚5\.wҤIqg;;믿޸qx`H$=7n #yMDҖ^K)++5>R| !իWƍ -..n677]tHKK[|ٳg꼽 _~`{YZZן8q_?~fԜ;wL!'NXv:ZxqJJ !ԔAfܹ?s~~~uuuppp,"ٽ{ׯ-,,"""f͚E}ڱc ?x7'==}yyyΝ!???W\i K͛f͊r}iӚ_ꭝ޽{RHOX\\5b>hҮz: 8G2v]d^/ P8di`E>|XIetH4zhW_'Jz8۶mp8F*++(_))mql~޷?ݭ?!&M211!hjj_~~ &_0 ޼y`0cXqbB3IЭ[e˖Bnݺ% ە1nڴСCw!ZZZSvgϞfff껗ѣGK| ~ SPP.\𨬬e0aBAAAnn&_0x`ӧO" !ƍ;w]8p@ccck׮tH6666vիOOO ͞6mZu:޵M׮]sss ߰ҥ =ؘ~# 6d2ȟ{ڽ{wz-!d鄐W^8;/߿_\\ܥKgg\'''uRO; FxxD"ٸq;Oqn.Bp6}t8xWxޫ?MYI?Kee%=A:" &@?"5cƌ`ooÇ'''gdd IVV!q:G 'ۥV6ʈnY 4_E8xWTT 2%''jiiM4iŊ'H˗/ܹSII駟~իWƍ rss_x/xڵl3Sf(**6>1'N8 ;qÅ+6OO#Ǥƣ!ʤbBm1m^fb1MivH+KmmmI/|~׮][-6}VUUY_kqi4hڴiNzꕕ=I<%AƻŦޫ?UtjxNBBBЪl~~~ǏYjUZhÇ߾};77(ٕvL&*E{)RVVVl6gϞ}ѣG޽{ƨk2RÇk׮_7Xչze˖{JӌƟe:0auuuhhիWNѡ_dff6~M_ BC6ҷoք@xA¦ٳ֭[7FZ?EӅEEE;2we'FJtuudO'u5k֨xxxXZZqݽ*z L'sJJJ?SffɓFh"--˗˨O+((`Xͣ%\rʕK.MIIIII?/[> `ȑ%<<ɓ'=xzzzÆ c0111Ϟ=cَmxw=zSQQ֖F!D$M:599?7008p@'NVnhh(^z%zeeeHHtÇBX,֐!C:B***Z*Dov~!%%%MRy#""{q]vzeJ؟VBΝ;G?ږgϞ(o^^^2Fa5G?kg9::Ϙ1# nܸqt tpeee"1BII+uuu|>N ]VYY~С9s5eee z)S4FlXXCEݺu+ @"/w臠>SP|$Ob111|>1cffWTTdkkn#-[6nܸm۶=y{ꪨ8fBȞ={Quuuaaaݻw?{ҥKnjп#H W=z4!~gٷo_l6=\dd… oݺjժBv";v,|ɯJy֭[;Ʉ2n8mO)vKʪO>[l=@xC'۷7nܸ`\x%)qgUϏpqqӧORRRaBqܘ_~|lhĈgϞeX=СCxyyџPk>7|pz+O65=;D"%%%X*-_n]\\\rrСC{U\\\[[?ydms{VTT,++[f~dddۧ --‚bݺuKSSqٳg={˗'O۷@ MYbERRRH˭7mԱH W\u+++ٳG)P8~}wAW}axIرc….\~6=*''dBJI߽{۷oGI$8w֭[/_,)q455'*CC`WQQQVVm۶/O5k֌7۷On)5IIIIII/^ UPPHNN...VTT_WXѧOz)SΟ?/ @uu{ݛd:88hhh5jŊ***'O|ݻϟd+iӦ|eeǟ8q&yիW31cƜ9st˗/ڵݻw,X@PjjtY/wAN={KydU{-5hggdnnsm۶ttt~ӧkhhjjjΛ7~kWNE p8׮]vڔ)S8 /_ޣGUUU'`fիW_}դÇoniibffq'NtҥÑYXXTTT˩!ZZZJ$;;Ǐ6&nptt2ޕ5@19]Z$EDD6B[n۶mʔ)tIzzѣ544߿/D8ׯ__tV|xPPٳg=zԷo'OBl";>vXBMM%K<ښV|믿wS8uuC.Yw 9Ҥ"@s CZ UrAZ UrAZ UrAZ UrAZ UrAZ UrAZ Ura5Y)//DwZEQ҅@H |mmu|#1zP 궘Y!r]]TREo߾mŸҪ:K/ D-j:eEg@(0 \Vi\Vi\Vi\Vi\Vi\Vi\Vȅ X╋ !c%WuUX鸚iunym\xbŠO7^W {1AǹtWYo4\p5F 12a+]Ƈ dhwZ՜<ބJ8eI׏2*֎)Mzӯr],lھflfgLB$tWUTdg9Udۯ{gD\^ҟ *6KcHO4;/Ϥ|!ا( ]' ֲKj.I/GEwKlbUEI-6։{y[ى锴1eEE}V>򘏴}?р\a}#KVW;9>C{7^ =( ,r=۽F<1:35Cy7%\mcZu?EyUU:@@ #.?."9!A4[sԛ'Y儐An]t^~ȝ=3M-t%L JC9hyLr is n*-(3饶f WOCyޏOʅ{s[r$ک{%T` =p vN쫥A ?BOYVe*025ǩV\X&7Pޠu r2;%WWx.p6X.E+oŹ؋Vڛ&e] T5v'O--USbT5 4|WZQ)Bn' 5|,EnQo!_{tUai(o|!]3>էa< /[=Xr!RW/hHȩdU4+:GXlnX'nsKi(1hW\hB̯O' JDkrG &%odE* !kBNOɯ"Ra*0 9THWzR\wSJ Sg~Ve j$駍4:K:c!uW)A]p3sEc\\]\[V-"~T`0 J"!;/0NRVĬdi(翭{S\GKZ~ҫ_WmR "JLiNE ן "_Z޾|l:9%GBYc`U0Vu0֐.Y BHD' \{'tUV$\H(VlGYz2*~Ʀt)B{ >Zn IP$!̶_wJ:[RP.$h" AN&zoO׉-[uBZ%K~{XH)OWZbXuT IDAT*} 63.ݳkTt9/7k~ !ɤ"3)m <}?ͬz]جoʻ*C2YO+}6o7e“쒚F: ?,&OPԉ\nJ.E5kOl½BYh]TXEu$ u1lz[ CzMif_תSdEUX~'ȪDYQ?$N'?x]6ףc(2&*O۞Z+Bo>2o+HSe3wU :_c t"s:o2FRq75WS ՕY3L9:?>ƸW]y*l)z6N`nν8G"z,S=5B= Xn L}2jCgڸ::/?( _fM?Ӽ I.酜 eru݇w䂴 @.H䂴 @.H䂴 @.H䂴 @.H䂴 @.H䂴 @.H䂴 @.&¡4 q|l:/b9 *  *  *  *  *  *  * KGҪӧ:ȑ# .3-,,w Æ ۿ?!˗\.722~g\.4rfժU=\)m\XY'ԫ | ^AkUX //o…W>x`rr˵&O/"$$dȐ!޽z1bSTTtrrx:۷oGDDs߾}nݺBܹ#|ݻw[+]N,?~*lWt i e=,}¯wrr*&ƦN=$T)Е֭;b{U@U _˘;~K5| u˗3f ??> ?RO>~Ç;FݻgΜ2nd֮Zj˖-qqqvѡw}ԩ;w74ބ#FLJ>ܹs;w/^x߾}111zڽ{wzjDDDXXNY~}FFƵkfϞ]]]Mihhضmے%KΜ9ceeE 8{,jmZEk׮-))s~~~O>]dɚ5kpႿTTҥK׮][__7iҤaÆٳGyj$66vwܱ7yNEaysmg{).ddmm6s&˅ O0> Kֆ?rs+l1n#Ԝ0xZUXXd333BȥK{ر .DDDt^'OxCh)vSe}b* Gch3f̘7o]rq//O>2p@0ztL^^a]N-~{{{GGm۶۷y劊Ǐ߿_( 6I%bBz+AAdKѣV 7r|BHG>++رc\yOvۂdJ#&#T։^ ˏ sZMrEBJY4 En$RKkʅzʝ9G#wg͚5iҤ˗K L&b;!Vqqqr6-}*-rrrTTTlX¤666aaab8 uYYYjEM:t߾}'N055mOoZ===:/*(( %IAA}LoZ-MMMBHyyy܉'?~l>*ן5TO_y?Ē7MIP)Kw^__f4w(Kc*//o̙Ǐׯ!Ǐ?uÇ?;v,??fϞ=</::'O͛7rrr~駙3g>|pAPxݻkhhhiiѻ>uTUUD"% !&==SCC#//͛7TVVVJJ EQQQQ3f=zt```YYH$Y,;jE100011 (:88fk7.A~7ү y4*5W-lHɯr4\W/y':YܼysNNNxxxxx8]3f7Κ5kŝeڵ6l=z͛a???'''}}y?ݺu+--DӦM[b!d̙O>uss֞9sf !uuu111H$VVVlu ֳg˗׏jժe˖ѣ;Щ=zǫ۷" U,[[[wҥ-q"o0 6oٲeׯ_'8::4hĉ܃c߇ 6{?r335k̟?_]] ?BRT俭kV#j_N,t[L*lYm 9rއndK{Uem-1c) F2339܅t]##0z?=[`DDć_i-cpw> |__ܭx/Vi\Vi\Vi\Vi\Vi\Vȅd`|P>E1 >[5Mtuu?H> dffX䂴 @.H䂴 @.H䂴 @.H䂴 @.H䂴 @.H䂴 @.H䂴 @.Hґs̱/Ba))) Ěm*!!嶫)ҵ'+nO>)^br;*--;w[=zKԬXۻSoʔ))))kMMM''={sה+Nl}VWY~Xhɸg:. rssNrJB={&]yfBȎ;:1DHMMիw}ז^SdKII6:ףM5u2ApuTX|ب}bÆ k׮yxxЋO?<֙1~$IPPРA߿Oqppt]CqQQQnnntGggg/\zӦMtW\𰳳[reEEE^^ިQ}||SRR}5!/4hy233q9s˖-sqqquuTWWo޼r|>V徾 9r+WM\@ 80sL5ǻ|ӝNE;wݻwO<ى ]~Th.ͩO<]qs%K+ ׆??dʨ)Yt{+Bee m)+mllƍG),,ܶmҥKw62>}>466s >}dɒ5kTWW֮Zj˖-qqqvjkk##ڵkVVVgϖq/\tҵk-=_p1113fxg#D@O^^g?`oF`a vOK+KŸ[Z^;[6xZu̙7oxb턐0u7n۶m8?F'Ox&MRPxwpp` L&366H___Z922OUUG7nE1 ӧ߼ySNKJJ~u())[nVĈ}! >,//۷W^ݰaCϞ=Lȑ#555|~ddu455rf̘`0 ŋlw {j%nֆ w QÛZѤ2!ĮFeLbRXХKAKmm-@{lUc\.wٲe[n}ӝ !Ϟ=(//@(++رc,B(*))߿q۶mzzzܞ={X &mjjj) 9!d+//һ tz/c;)**6אָjr^ekksK.%XҤKA"jXN"с@v߭ڱcǖ-[l6ҳgOoXD"›NKKy9nhhzk>77wĉ!!!Ǐ؄Ɗ iAQQ:i|;-ZD'TD")((hގ tsU^^ޤbq cy[MtSX&\rQ.{fc.+wWcWֶ]-@ڝV?gΜ)--۷oܹs !^^^gΜsN]]ӧO翇?"ǎϯٳg4xEIg_HKKSVV7nR\\]( /^X__߽{w ---i...jjjD#FNw DLDRUUe0)))MMѣGDb#G (--mhh8{lrrtIHHEQt=E~~߹#55Uc,uI&:SPsqY5J,3-6KQf/hnńu/2*@;BBB7m4o޼KBhccvZoo˗?"k׮p8G0at" 6999͝;Ņs8hР'.^ٳg愐wwwD+m\UU5<<ŋ666Æ ok?~8eʔI&-Zp7.[ɝ޽{-!!aڴiaW):nZ˘:> 9gRZZ_`G[ UrAZ UrAZ UrAZ UrAZ UrAZ Ura5Y.**b0$EQ @/VM*]]dž|(௃-!@  *  *  *  *  *  *  *:eCC@ vZ'BsժUkaժU%%%\\jU"Ġ;1VKXLΣ̲}svvUJJJ (**:99x%1nyܹw rvv>yD"9z333 !{=x۷ׯ_Oٴi/mllqLMM=*H!XfΝ;>JosUWWoٲcvqqG}9n|d蚧O0a'|jժZ4Iײ\@ :p̙3 !UUUW>|_i \~Th.ͩO<]qs%K+ ׆??dʨ)Yt{+Bee @5AKYYï]FQTttQ޹ 4WTT:vppxH$:<˽wEQ/^<|Яw+^+X IDAT O:eaaQUUUSSbcc)JMM͕6^]]w^D]]]###)]`EQ6l( qppΦ(ѣ'OH$>4113gNlllJJ EQaaaOMJJ(-<Ϙ1o[(ŽO[xz~(L$\y\8nϽÒ(+ 'ޯoXVm725&噌2ݭH$V5jԨQSR'Ox&MRPxwpp` L&366H___Z922`0gϞ}eBSRR6nx;wB;6>}B|||233srr!"oqpp066Vd02Bغuk&L];q℣رcY,… X;99Bo``?OMMf/_mф{{cBLLLtuu_|d>>> jjjϟ?@؍gϞl{{oJ%nֆ w QÛZѤ2!ĮFeLbRXХKAKmm-@olUaaaBBǏO'KT`)(BD +iwURd0;[ڗV_n߾k.Lhii7/gGN8q~~~JJJW666aaay@@@PP]qĉoٲ%00pʔ)&&&zzz-l\-!!Ɇ]|ݻ?g yyy5ѫW/BȾ}9dgiKl6"+))]6**O?uttp8 *.W6KLȓ5c-tf)~++wWcW6 Zehߗckjj#Mx<^tt4EQuuumCBHZZq㔔BPxݻkhhhiiIwwwgX b o>>>gS__xB?^__Æ +**/,,hѢ۷o SNyzz輩B|-!!aΜ9vvvBPT=B:{Ç\ydd#N:ظyfBȧ~`e˖Ypɒ% n,Yb ccR|W_xqiiի+<77V[[ۋ/J5008yC~_~jddf݋ʖ<4:~'OΜ93 ŅFB|}}mmmׯ_!ee) >~.7Sua3*;MGdZ:<7ק[UamwmLéaqEkED&z V3tTUUߠ?~|xxTo%&\ʫ=uwip @*URA bTX * VH @*URA bTݶ+++i4ڠ2tD"VUU5؅kOUX>(u 5UUUjjj]>x ڎERA bTX * VH @*URA bTX * VHkҥNNN?f߾}Q!>?3rϟ?Q jk;UM>q w.琟?No?V߿T驧pԩw8p` *פǎw1bd2666} 󝝝%%%4idЂWvϧN0!//~~~VmnnCBB+W%$$hhh !dڴiW&$$ 6]{9{lBBBDD;+١ĉ:::;v ;wNGGgӦM -w裏lْsεkKw^fICE},CU7M?n?~ܩ֛Q/L8ܘ.C.n,ç?1Φ (*0NY᳖9<ԫ*PqFGGǞ+ttt;v…ӧeff]v|||Ledںu+eXnnnT;>|8211Ԥh</++UK2226YYY999g׮]zzz...[hccC-;411111_v P-K'8Oמh &cuTdrn]c`լ,<0YUF G3c TT`X<)k|0dդtj)@ hkk:ufΜI駟r8gg翣⡣l̘1/߿022Rz>`0LxTرc?K>\YYo._\TT.|~vvxyϩdɒÇ?~U+Gkl>~F=7F?X7glz]]m,NlȭGUwsj[:+59r=@Zo޹s믿~dk׮ LCJuuuv}=tii_~'+++X,!qĉ/^400u`tt޽{MMM.]^455UTT׭=00̌dIq07Vȇ&nS܏Y/, uEg!2y%5ir?*n~VLzz={ݫ43A|>e߾}('O͕7oݻwܵ:á7oޤ ԩS,X0e##秧K422233#$%%lvNNNuudP~zllP(lnntRyyybbO?_v o-zBHܓ^i}q#YSE]l~%M\[q)V U555ׯ򲳳i͛7;ҒNS۶m+++Y[[\ÇRޒD|:eʔcǎI\xqiiի^˗_zw߽q9rdڴiP(ܺu]F9r_yMM4O񰸁_-V1dhTWhTO,%M}=hl1e~H-,2+J4EG!]XUUUjjj4dVVVSI/㫯*.. ohhXj OяOxӼ(1+X~9s|eeer. `Jcv w !WX * VH @*URA bTX * VHmF J)CH$hUUU]Vx[uURPSUU6UO@AAAX * VH @*URA bTX * VH @*URA bT:::6l`aa!쬯p(P!!! .پqF//_B`sĒV6%޹C~J+ 8EjӟXU[[啘(٘v˗'&&~g۷oy ӦM;w[{@յQPP`(kj}\Ύ~P!ߤ΋,gh5FEl,GHB={ǎsuu!NNNG3g@ o<999"{@r\.w !*-%촿)gN k+1{Nt(2222G=tŒlw̙3ś3gʪ!K(L:500pΜ9#p+WP}tuuccc'MC5677</hkkE^^ުULbiiB5.]k׮4iұc!EEE>>>'O677߿GG!+((<44} 6B8zOMl2333HH$:}{FFFρMMM;w411144 SNxxxPsss\~}CC3gxӧX;ߺuH \rE|ͭ׳tÇ{{{[YYxxx,\t@}ʢWk:|GEjSnZSI;_+~bv7R-Mr;|"KuzA)ǜ֎;>P.tkinnnhhPWWhjjB***WXXŋϟ?:a„'''eddZ׷WTT{`!iiifJNN>s̱cRSSv*/EEE-YKWW7%%%..N__`B>}u޽GI7|[[ۄ}xzzFEEB"##?~Ѭ]vn !ׯ_ԩS===Cח8q"==]GGgǎT{aaaII?WQQqBٳgȤ$.n:߿KBCBB+W%$$hhhݻg?,ioe*C#_oS[珟8wf"i..73 #2(OƩ2g- +E~bVb2p-շo(}}}&9|xnrY,[ccc~~~uu7l6Zn]tt O4~/_nllаuV6f]]]J&O0 KK1c<~bbb6mDTUU===J"##-[fllLѦLbhhm`MM͵k׶l٢&++a@pmqk.===YYY!BVXA=<<~ƌC)((())!ttt+lmmӣ&c n&N(++쓔t墢^PVVĩLII!KyVu`0L&Ssom2B/Uַ ߑEȭGUwsj[:+59r=@ L"XYY:99QqqqoTTT{XKKK]]]K???YYY+ BHPP&H`[[۠ x^5jTEEEgg8<􃦦&ùpBɩE/Br*QF;DGGݻ788499yҥ=')**΀+%_MKK{{=FqVOͩs,.2RW}.+WRӺy&GL|.`hEk֬rʏ?XSSs֭3gP.~---G)}}8HE5͛7OVVݻT[@@uɓ'[ZZ~wwwwSVVV\\k<OAAMMM'N_۰aÊ ̙`0BBB;w0p3gdggBRSS_mS``;::9`0{422233#$%%Ip-Ph"Bʕ+CCCsss !ϟz=-zBHܓ^i}q#YSE]l~%M\[q)V ĪSNԘ:;;BNZ[[ݻwx6oMĄ'HcPw033? URA bTX * VH @*URA bT]YYIC$h.^+|*uuAcRSS*?[v, bTX * VH @*URA bTX * VH @*U6llLKK0441cɓ'hӦMO>]n{7pknnONN6uuumlllcc3r./L6aA'GeU8N$,n5{^jP(=z.ٞlool2GG؁@r8 ''7%< ofܸq]_ٰa`WFEkshgfi Ꟃ,=z4z?C+*G:tbI7kkk!,$$?((ٙ:tH$B.]zkN4رc;w066mnn'&&:;;[XX̟??%| dzYfͳg!EEE˖-333spprrrr7n^9::B\B--,,ե&$K?RSSfϞrO8x܌>O͛7i$kkSN nvibbbhhPϟ8qbZZZummm;v0661cƥK8M6M>}̙wE%<7oޤN|Nپ};^q+WP?R?)2&ͮ6 YL^zO+! s&5TϏJ\uvо)m2l ~(e#7BBPD.{wڛ죒 [d{'QmxBɞ9~)X~<}ԈmM؃R%7MǏ-Zԟ@µkLMM^w߅_vj 233ZdINNիWcccsrr!YYY}і-[RRRvܹvgϞݽ{ٳ?cBB;ihh&;cƌׯ;woy董nܸPTT4z7nlݺ?nkkuӧ8#maaZWWG?ydŊ֭{ӧϞ={nϿ~NIeˮUV}SN6ۮ<شi8tĉ;wܸqArޣ^Ky[[[7n߽{믿VSS/78βcN=lo?1͖\J.KHk,=%RVV~qPD!Oj,8L:i/%]sLrum[kZqN1!MPm~䂯uQIKL>XErku9az/)ovz3zO7OXj֭[ |F__ƌѣGϚ5֭[TˍMMM7nܨdggA9s&!$..dܽ{RNN.&&CTUU===]L0Ɔ7zǏ(!ӧOohhpvv&L8Q]]]r۷oo޼YNNN]]}T  Ƅ >裈kjj]e555YY 6۷oS~'FFF F͛gnnNݵXbJKK>|bą|^TR/##C?>~QFu2F;QiōI-]li( cjd=rRN-!$Il#5wX!wsjl'tE7 Mqv,:NъzF.फ"bOV}RmyNUcʌSgd;UU0AV![\q/c)ϢLT[#+P^^hѢ?p`g%%'OP?s8q{WW˗o߾][[󵵵 !|>?;;[0ϟwtt?~|nnn|>uխUOO箙Lf{{{WB&)ޔLCCRBHii1cěZZZeee:BFMmt q%%%ի^*uhÆ 4m͚5}: @SSי>{ݼƢuڰ/>q+ۅ"9)QQ NR.jyzO+0;-8fUgrTַBBo!KD,H/_!ʃNY8IbtU턐Q0{zz[nJqqqw>|pZZ!DSSSEEe߾}:/X`~p.\ emT|ūRQQyyѣG7ʺ]{Kyy95Bayy+]D{Qm޼966?;vlSQQ%u;/633 OBX,^bRN o,=`aX B#ۨ[OcmE ê6$ad#29lGIWT_1UrepYBȿޛ(+9q_ vM5w~={p8+++q?ٿMgϞ=g>_222g&={6 Bss-[Q_| 8qℾ˫>^ȑ{ݳg7 N>>߿`ڴiÆ =^:;;[[[;<<|Ĉ^n RVr^ߣ^K*++vBԩS\m۶،5?|2!MiWĤ_x8U(MvJ(CZ ݽvL|bq&G.bɘ•&7b2rO7}4\a=QI,ό1;/>R(k @1!;'~(Y[EZק5pv4yДnUUUg ^OIx̧?^IC V |_0-U * VH @*URA bTX * VH @*nە4mPJ:D"FB§?۪{RWW:*55^| zm"@ VH @*URA bTX * VH @*URA bTҟXahh8cƌ'Occc._rrrꫯ9B%k._<11>۾}͛7owΝul ꏟZyeh3|GC+Ǫd{{e˖8::B;(++;999ro(v999"g{vv/fpqB{^zC<^ܱ^>ä?Yz|3i ~WUAAAZEEEBȽ{fΜ)n9sfVVV}}@9D9;;sCuK;BԩST{QQɓ!Μ9Ob > s_ xQQQ^-[fffIu[tÇ|}}srr<<>>$$$>>_rePPPBB޽{%%%?C\\\EEŁ!'N8vXff͛W^#PPYTTԒ%KzY,''GIIի999aaaT;߿K NNεk>>%%%I111MMM6mhK\.w,DWWWSSFx,+Vh4yyy_~l2CCC:`ggG]#8::._XQQMMM7nܨdggAwttMyqVVVNNNf\\u bjiXtt޽{MMM.]J5jTEEEgg8B455WX_&ùptD=z4)eeeFٿ⋂ݻwkkk>|8-- ԴJJJT)5jTII/%e & 8WbBeAFzk+eh7V5 &i&Ka3-8?-M"5+ՆBuEYαRmdE~Z!"d =vZ>"4 ;}7\xbUVVB֬Ysʕ֭[gΜV^aaavvH$Yh䫙FFFfff$)((1bfC IDATٳl6l6ޓ_`A{{5k***-Z`0zNqssv횭g}&w/|gϞ rB|˖-/\5VXAYtP(\lY]]ݘ1c;fdp{;655e͖掎fff'O9r$((>9rڵk{|#7;1%!7NDS(~lCur5~9P5 $Wj*bTX * VH @*URA bTX FJ6( "FUUU v!Zm=VJCMUU`W>c TX * VH @*URA bTX * VH @*URgذaEϗMMM#""+ ^v[[Çƍj7~{o+15 }?N,illlO\/9䧴S[F?~]zu^^NRKK'|^q\L&F__EM /3Je!55u>SgG?X3{Ie 7Y1{f|X k+*P奯gϞnرҳX222}_>>>}ϐTR3GUZJiS {&F>k1V5SB(NQ|ey3ddd=z!/޸q#44fPyCZHHPP3=tH$"477</hkk:o۶Ty󦓓 !K.=s振ש͚5+22"N>mmmm``Aپ} !BԩSoo_588EEEIΖxb+++GG^wiaaallLlbb2}'$66}ҤI111T~tt͂ THHƍg̘{_~̙ko4Ǒg<Gŗ<*i ]M.(&3m{%)5w~Pʻ1k-ԟ:::=+**W^MO Ҹv횩իWk׮Bbcco߾]YY,\QQW_nܸ?{_FOcccnݺiӦrEFF?~ѣYYYvȳĉ:::ԕslڴ)!!aѢEonnn~޽O8Q򥜜%%W䄅BFDDvuud??UVXWW׍7ƌu˗O2sww[|~J%U7GfICE},CU7M }tNU{!rZ˄ˍ24 1?|jqql Yi>kxK pk׮[n;w.999 `&g̘A=zYnݺU]]} ???6b֭[MuVUUꫯLMM555eddtzbbǏ?j(ܹs!gVUU]ddeˌi4ڔ)S %_p8vӓuqq.b=~PYYH%SSӍ7())QŢ||| Lܹs{޹uV.bElfkkճ]]]*M>Ύbkk[]]]QQQf* UٲtBԑ[{)͟`2VQGUA=9UjXy]Ew*C#1f**0s52Ȋ^=~ÃBƎaÆ[۷o%%'O|Bݻedd!BPNNph?=sÇOnmmmBee嚚ɖ2mm>jHJJ|rQQQSSS{{{۷۷ou۷o|jHqpY*:d2?: jl>~F=7F?X7glz]]m,PDn=S[YY߮ɑU#_{~z'ڐ#Z̏~,l~)H8v c~9mX!3SS'tDX8J?PD>iThv|=_׳۽|wPy=Uiihiii;vBcR ]p tk***233 bii^͆Z^n]```߾}ccc}}}k/bͥ[n[ʕ+SRR>N:mٲ%..NaffWhFZzŋk?;D?]ʶT59]˩2~]6\#WZf2kZmF[8~:**ŋӦMk_X$EFFFDDx{{knرVhzn`e%%%*++kӦJ233Ӷ={޽{۶mS(Æ |ĉwڕB+!!޾_~BӧOMLLrrrrrr*?zh^^K.wԩHbСFO5MCGiddtԩ#qծrKn*!/Wsj8vmP/$fYTXZ~:"#*2͕ V!wBBBȑ#}ݸqƍSRR:tC400066DV;88!V^iӦQFw}޼yX\\V5v+?!DVf̘q kk/E2eJAA̙3{T~wҤIW6m r˗/W\m$)999 cǎ7o<Լy֬YR#Xh֡Csvڵ!ΎFrttρKiWUJpva߾43[xc\@l3C /xҹWhE{)))]Tr+;;[;_M8X'uppXhQc .~jJL|7ݪ߫x<[O d!V,*X@bBYU d!V,*E묬,B$8$IR(M]xWUcyn߾}SW>@JJJ,YU d!V,*X@bBYU d!V,*X@Ī8//^zk;v6N21ojQW%%%nnn[liBgذa{Bܽ{M]MTR6Ԍ\ȒYB) {@ff3,X}?s5333f̶m>|zyJըբ \\\zYK7O2UՈ ԅ>?t3t22Kh_LYN45w:tiӄ...cƌiݺu֭}=z_F.uT*׬YS{DssgSOqFyyySW@#; t .6ӾiD^8nܸy)77WtN:kr{-YDV{zz:99H$~Yz=` mg__߽{Ι3688XTZP?}/((BL2_W``kxx ;va\\СC?boo_93&&f666qqqBOO=[dd㓒v{رٳg?xãr~_wrr?~||||M%IҮ]\]] 4s[n.~;v͛_SgYe*bݟ'\m㢰}2>?>l?{~-Syst#ԃTɍ7|rϞ=/_\1??O>E>۶m?y$I?$I(,,r劝ݿoI|||r…R\Vq7_TWSbjNwܙ5kҥK*tyrٳհaÎ?.0aBflmm.\1}>}Tطo_???333SS!C\x^5n޼ɩGUz<==]\\wGGGGG_~EǧyBS&%%ݼyΏzT*bbbwniiY{6m/*%%%==];̙3 Efͼ~;иjߚb{W:6Sݾ"^Be,M<+ΧrK53t2W4|s' l۷}||yyyy_|E```cԆ055z"??޽gΜ)))INNvssviĉnԩS}O]Ȧ[p֭I&i_啖V޽{uS>h׮][l4hU,,,=֭[?O8++Zm۶w^;(:5Of2ߟt翋J5U:+B MkedhjאXu֭ɓ';vܹUΝ;21joYXXH4a777ZRtɤ+WLII;uesPUVTT)lIKKKg^o~BCCsss.\aZ]3g7n\c͛7-,,j ѻWO%:.K5;y%v\:cH'C}ЏcjmL USFPz/̜4ioyׯgeei/͛ERSSO4EFFFDDx{{^vmܸq**33STjw%wwN:iwq211IJJɱLLLB={-uԩ?cddF)((on߾}+22$??Æ رc-zMLL>|\RRrС6mڨT*33wo߮=6++k޽eeeڷ?hnܸsNoo;иzѧG_gWTZ򝂔?gl?fNr|ʃǩmr%Gq~q廵tP/[|0mv'>OS i```llZvppB͙3̛7o͚5*j11ӗ/_?\ڴi-[tqqi/k׮}ن ,XЬYÇ1BVaa;wUT{ xW=<Sw*++׽400-//oԪ $޽ɓv__-[L2eIII^^^...'NB 2䫯z뭷WTT$䧟~zםƏ;Mo~IWW%K! VXѿWWו+W !׮]dQPP0uTWW6LwFh|lҧOSVqںGDDTwZ[[_~]qYf{AAA_,( tΝ{oذֶrBc σ-Z4{_UVTTTPPPTTԵk}]Zݱcui;ܼyi߾ŋ=-::zږ7oFFF8q"+++00P322^˭[jk{}v튎咏=zΝ;w]r{llŋ/^8k,F6lXll]Ϟ=[qs7oڵkBBZ~!44tڴizB2$<<^ԪU+!czzUwH1uTRibbҿ#+J==;;ݻRCNNαcw3gΔ$''bhrG=ztSWx/444&&ѣX *}effec ޯ_8:IDAT܅ nذA; !ju]H;VTTԽԌQF^zFFF5$i„ nnnjZRM4I^Nj_f͚u֙6nAٹsgrr$IKDD.'TRRrС6mڨT*33hBhVjjjvƎjժ{ !^zᚆs Ebb7mll233wΞ=Udcc#G:u׮]7nJLKK{5nս{͛3dȐF/:fڴi嗃kzjFqpp?~wǎ{Q77ɓ'o[zMF߽{y2rg̘O?6mZ˖-]\\* X-gggGGQFYYY9::j6ߜ9s-,,\]]x|PҥK&ő͖tP5%.h@bBYU d!V,*X@bBYU dѯ:)Z{%gO@_sOHE d!V,*X@bBYU ~SH.]S(!V\dɖ-[FQ===EǏ׾lݺucmܸ|ʔ)վ;ŝ} !m&h{V*jСSN}fj&&&&&&}O?/4V$)..nڵBEjo~`ZZZII_vʪQ{k'IRxxhd"@QLdZ Iw޽s̱ Bܼysڴisww.Ӛ;wҥKuC{zz !-[6|mcLLȑ#mmmCBB4ٲew~!DrrI_?yBT*4MHHСC켼+>!C 0@OT73/ssscBC>|P]tyP={>|e„ ǯ+**/_vppHLL2eʿ@WWWmpւ ?7j@bf*!;TG۷#G8zQmZׯ_xxo]XX8y^z={6<<|Ν'O>tn_hhh W^9sܹs/bݻwﮥe˖gΜٻwo#.ëM6ݿzAzz߹s˗ !8p…1665 ڙz۶mu-N+}v!đ#G[lCzzz^cWOUEEűcv/YFQ/[,777$$t ./;w`h]pւ ?o۶ڵk*!V@!F+h޷oŋk߳g^{Maee5l0s;ӧOӧO֭#""=z`BѮ]ɓ'>|oB\p!==}̘1G޷o~=f̘VK%W\IMMm۶3۷o_萐]T>[nFFFoօ eee$hBOOڙ,AAA@S)={-[>㄄s65MQ](J}& 5]cǮ]65k 4YfurjL%Xʴk7.\PmDI"###""}eĈAAA%%%'OBlrԨQ+VիOCy{{GEE8qBܸq#$$DUϞ=Ϟ=#I´!=ghhhiiiffֈQ_ B>}ZإKb''޽{;88̜9޽{5֭͌[CCC?ׯ߸qTnMSTµkמ9s&777>>>n=֫>`ݺuoժ뵝MLLrrrrrroe Q9Yu2+VP.444ܽ{K^}={iFOZZZkggcǎ{{{&Mwww뭷 v$I&Lpss;x͛ibҤI^^^g~饗 ˗/;wK.]q݃23B/fgg>|Æ ꫯtjj}΋/~7VX4 KC Q*ׯꫯN:%>}#G|jpYu'NlL%PhJIIz $>˕vaaUAAAQQQy&U"7z7xwU*Zm[G &a5Ml‘ҺIsBQŠjqlѼyu5uσM6]6,,LPo?e3Wu59V504k +bsEOOϸY‚lI4xQӋ#%%Kcx'Ja```lLOOa+-4 I$Ijꢀ/RT  @Tk4I*JBk]bFPAQg<[< @bBY*jj~˞I=GR^VjddT[U[ʪ(&UB(/+{JCo+,,|AII Z*ظw*@ BYU d!V,*X?sVSmIENDB`django-tables2-1.14.2/docs/index.rst000066400000000000000000000031671317557360700172110ustar00rootroot00000000000000.. default-domain:: py ================================================ django-tables2 - An app for creating HTML tables ================================================ 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. About the app: - `Available on pypi `_ - Tested with python 2.7, 3.3, 3.4, 3.5 and Django 1.8, 1.9, `Travis CI `_ - `Documentation on readthedocs.org `_ - `Bug tracker `_ Table of contents ----------------- .. toctree:: :maxdepth: 1 :caption: Getting started pages/installation pages/tutorial pages/table-data .. toctree:: :maxdepth: 1 :caption: Customisation pages/custom-data pages/ordering pages/column-attributes pages/column-headers-and-footers pages/swapping-columns pages/pagination pages/table-mixins pages/custom-rendering pages/query-string-fields pages/localization-control pages/generic-mixins pages/pinned-rows pages/filtering pages/export .. toctree:: :maxdepth: 1 :caption: Reference pages/reference pages/faq pages/upgrade-changelog pages/glossary django-tables2-1.14.2/docs/make.bat000066400000000000000000000106571317557360700167570ustar00rootroot00000000000000@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.14.2/docs/pages/000077500000000000000000000000001317557360700164405ustar00rootroot00000000000000django-tables2-1.14.2/docs/pages/api-reference.rst000066400000000000000000000265641317557360700217140ustar00rootroot00000000000000API Reference ============= `.Accessor` (`.A`) ------------------ .. autoclass:: django_tables2.utils.Accessor `.RequestConfig` ---------------- .. autoclass:: django_tables2.config.RequestConfig `.Table` -------- .. autoclass:: django_tables2.tables.Table :members: paginate, as_html, as_values, get_column_class_names, before_render, get_top_pinned_data, get_bottom_pinned_data, get_column_class_names `.Table.Meta` ------------- .. class:: Table.Meta Provides a way to define *global* settings for table, as opposed to defining them for each instance. For example, if you want to create a table of users with their primary key added as a `data-id` attribute on each ``, You can use the following:: class UsersTable(tables.Table): class Meta: row_attrs = {'data-id': lambda record: record.pk} Which adds the desired ``row_attrs`` to every instance of `UsersTable`, in contrast of defining it at construction time:: table = tables.Table(User.objects.all(), row_attrs={'data-id': lambda record: record.pk}) Some settings are only available in `Table.Meta` and not as an argument to the `~.Table` constructor. .. Note:: If you define a `class Meta` on a child of a table already having a `class Meta` defined, you need to specify the parent's `Meta` class as the parent for the `class Meta in the child`:: class PersonTable(table.Table): class Meta: model = Person exclude = ('email', ) class PersonWithEmailTable(PersonTable): class Meta(PersonTable.Meta): exclude = () All attributes are overwritten if defined in the child's `class Meta`, no merging is attempted. Arguments: attrs (`dict`): Add custom HTML attributes to the table. 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. This is typically used to enable a theme for a table (which is done by adding a CSS class to the ``
`` element):: class SimpleTable(tables.Table): name = tables.Column() class Meta: attrs = {'class': 'paleblue'} If you supply a a callable as a value in the dict, it will be called at table instatiation an de returned value will be used: Consider this example where each table gets an unieque ``"id"`` attribute:: 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. row_attrs (`dict`): Add custom html attributes to the table rows. Allows custom HTML attributes to be specified which will be added to the ```` tag of the rendered table. This can be used to add each record's primary key to each row:: class PersonTable(tables.Table): class Meta: model = Person row_attrs = {'data-id': lambda record: record.pk} # will result in '...' .. versionadded:: 1.2.0 .. note:: This functionality is also available via the ``row_attrs`` keyword argument to a table's constructor. empty_text (str): Defines the text to display when the table has no rows. 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. show_header (bool): Wether or not to show the table header. Defines whether the table header should be displayed or not, by default, the header shows the column names. .. note:: This functionality is also available via the ``show_header`` keyword argument to a table's constructor. exclude (typle or str): Exclude columns from the table. This is useful in subclasses to exclude columns in a parent:: >>> 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. fields (`tuple` or `str`): Fields to show in the table. Used in conjunction with `~.Table.Meta.model`, specifies which fields should have columns in the table. If `None`, all fields are used, otherwise only those named:: # 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', ) model (:class:`django.core.db.models.Model`): Create columns from model. A model to inspect and automatically create corresponding columns. 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. order_by (tuple or str): The default ordering tuple or comma separated str. A hyphen `-` can be used to prefix a column name to indicate *descending* order, for example: `('name', '-age')` or `name,-age`. .. note:: This functionality is also available via the ``order_by`` keyword argument to a table's constructor. sequence (iterable): The sequence of the table columns. This allows the default order of columns (the order they were defined in the Table) to be overridden. 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 inheritance. 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. orderable (bool): Default value for column's *orderable* attribute. If the table and column don't specify a value, a column's ``orderable`` value will fallback to this. 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. template (str): The default template to use when rendering the table. .. note:: This functionality is also available via the *template* keyword argument to a table's constructor. localize (str or tuple): Specifies which fields should be localized in the table. Read :ref:`localization-control` for more information. unlocalize (str or tuple): Specifies which fields should be unlocalized in the table. Read :ref:`localization-control` for more information. Columns ------- `.Column` ~~~~~~~~~ .. autoclass:: django_tables2.columns.Column :members: render, value, order `.BooleanColumn` ~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.BooleanColumn `.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: `.JSONColumn` ~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.JSONColumn :members: `.LinkColumn` ~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.LinkColumn :members: `.ManyToManyColumn` ~~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.ManyToManyColumn :members: `.RelatedLinkColumn` ~~~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.RelatedLinkColumn :members: `.TemplateColumn` ~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.TemplateColumn :members: `.URLColumn` ~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.URLColumn :members: Views and view mixins --------------------- `.SingleTableMixin` ~~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.views.SingleTableMixin :members: `.MultiTableMixin` ~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.views.MultiTableMixin :members: `.SingleTableView` ~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.views.SingleTableView :members: get_table, get_table_kwargs `.export.TableExport` ~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.export.TableExport :members: `.export.ExportMixin` ~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.export.ExportMixin :members: See :doc:`internal` for internal classes. django-tables2-1.14.2/docs/pages/builtin-columns.rst000066400000000000000000000015271317557360700223230ustar00rootroot00000000000000.. _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 - `.EmailColumn` -- renders ```` tags - `.FileColumn` -- renders files as links - `.JSONColumn` -- renders JSON as an indented string in ``
``
- `.LinkColumn` -- renders ``
`` tags (compose a django url) - `.ManyToManyColumn` -- renders a list objects from a `ManyToManyField` - `.RelatedLinkColumn` -- renders ```` tags linking related objects - `.TemplateColumn` -- renders template code - `.URLColumn` -- renders ```` tags (absolute url) django-tables2-1.14.2/docs/pages/column-attributes.rst000066400000000000000000000031171317557360700226550ustar00rootroot00000000000000.. _column-attributes: Column and row 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:: >>> import django_tables2 as tables >>> >>> class SimpleTable(tables.Table): ... name = tables.Column(attrs={'th': {'id': 'foo'}}) ... >>> # will render something like this: '{snip}
`` element on each row. The values of the dict may be By default, class names *odd* and *even* are supplied to the rows, wich can be customized using the ``row_attrs`` `.Table.Meta` attribute or as argument to the constructor of `.Table`, for example:: class Table(tables.Table): class Meta: model = User row_attrs = { 'data-id': lambda record: record.pk } will render tables with the following ```` tag .. sourcecode:: django [...] [...] django-tables2-1.14.2/docs/pages/column-headers-and-footers.rst000066400000000000000000000120421317557360700243160ustar00rootroot00000000000000.. _column-headers-and-footers: Customizing headers and footers =============================== By default an header and no footer will be rendered. Adding column headers --------------------- 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 Region(models.Model): ... name = models.CharField(max_length=200) ... >>> 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 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 'Model Verbose Name' >>> table.columns['ln'].header 'Last Name' >>> table.columns['region_name'].header '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`. .. _ordering-class-name: Changing class names for ordered column headers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When a column is ordered in an ascending state there needs to be a way to show it in the interface. django-tables2 does this by adding an `asc` class for ascending or a `desc` class for descending. It should also be known that any orderable column is added with an `orderable` class to the column header. Sometimes there may be a need to change these default classes. On the `attrs` attribute of the table, you can add a `th` key with the value of a dictionary. Within that `th` dictionary, you may add an `_ordering` key also with the value of a dictionary. The `_ordering` element is optional and all elements within it are optional. Inside you can have an `orderable` element, which will change the default `orderable` class name. You can also have `ascending` which will will change the default `asc` class name. And lastly, you can have `descending` which will change the default `desc` class name. Example:: class Table(tables.Table): Meta: attrs = { 'th' : { '_ordering': { 'orderable': 'sortable', # Instead of `orderable` 'ascending': 'ascend', # Instead of `asc` 'descending': 'descend' # Instead of `desc` } } } It can also be specified at initialization using the `attrs` for both: table and column:: ATTRIBUTES = { 'th' : { '_ordering': { 'orderable': 'sortable', # Instead of `orderable` 'ascending': 'ascend', # Instead of `asc` 'descending': 'descend' # Instead of `desc` } } } table = tables.Table(queryset, attrs=ATTRIBUTES) # OR class Table(tables.Table): my_column = tables.Column(attrs=ATTRIBUTES) .. _column-footers: Adding column footers --------------------- By default, no footer will be rendered. If you want to add a footer, define a footer on at least one column. That will make the table render a footer on every view of the table. It's up to you to decide if that makes sense if your table is paginated. Pass `footer`-argument to the `~.Column` constructor. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The simplest case is just passing a `str` as the footer argument to a column:: country = tables.Column(footer='Total:') This will just render the string in the footer. If you need to do more complex things, like showing a sum or an average, you can pass a callable:: population = tables.Column( footer=lambda table: sum(x['population'] for x in table.data) ) You can expect `table`, `column` and `bound_column` as argument. Define `render_footer` on a custom column. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you need the same footer in multiple columns, you can create your own custom column. For example this column that renders the sum of the values in the column:: class SummingColumn(tables.Column): def render_footer(self, bound_column, table): return sum(bound_column.accessor.resolve(row) for row in table.data) Then use this column like so:: class Table(tables.Table): name = tables.Column() country = tables.Column(footer='Total:') population = SummingColumn() .. note:: If you are `sum`\ ming over tables with big datasets, chances are it's going to be slow. You should use some database aggregation function instead. django-tables2-1.14.2/docs/pages/custom-data.rst000066400000000000000000000111021317557360700214060ustar00rootroot00000000000000.. _accessors: Alternative column data ======================= 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. Using `~.Accessors` ------------------- 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', we use the name 'accessor'. Accessors are just dotted paths that describe how an object should be traversed to reach a specific value, for example:: >>> 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)]`` If the resulting value is callable, it is called and the return value is used. .. _table.render_foo: `Table.render_foo` methods -------------------------- To change how a column is rendered, define a ``render_foo`` method on the table for example: `render_row_number()` for a column named `row_number`. This approach is suitable if you have a one-off change that you do not 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`` This example shows how to render the row number in the first row:: >>> 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}]) >>> print ', '.join(map(str, table.rows[0])) 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). .. _table.value_foo: `Table.value_foo` methods ------------------------- If you want to use `Table.as_values` to export your data, you might want to define a method ``value_foo``, which is analogous to ``render_foo``, but used to render the values rather than the HTML output. Please refer to `.Table.as_values` for an example. .. _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) >>> # renders to something like this: '''
{snip}
{snip}' For ``th`` and ``td``, the column name will be added as a class name. This makes selecting the row for styling easier. Have a look at each column's API reference to find which elements are supported. .. _row-attributes: Row attributes ~~~~~~~~~~~~~~ Row attributes can be specified using a dict defining the HTML attributes for the ``
NormalUpper
Hi there!HI THERE!
''' 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. Make sure to use Django's html formatting functions:: >>> from django.utils.html import format_html >>> >>> class ImageColumn(tables.Column): ... def render(self, value): ... return format_html('', value) ... django-tables2-1.14.2/docs/pages/custom-rendering.rst000066400000000000000000000021471317557360700224630ustar00rootroot00000000000000Customizing table style ======================= .. _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 arbitrary 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() >>> # renders to something like this: '
...' .. _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. You can set `DJANGO_TABLES2_TEMPLATE` in your django settings to change the default template django-tables2 looks for. django-tables2-1.14.2/docs/pages/export.rst000066400000000000000000000103221317557360700205110ustar00rootroot00000000000000.. _export: Exporting table data ==================== .. versionadded:: 1.8.0 If you want to allow exporting the data present in your django-tables2 tables to various formats, you must install the `tablib `_ package:: pip install tablib Adding ability to export the table data to a class based views looks like this:: import django_tables2 as tables from django_tables2.export.views import ExportMixin from .models import Person from .tables import MyTable class TableView(ExportMixin, tables.SingleTableView): table_class = MyTable model = Person template_name = 'django_tables2/bootstrap.html' Now, if you append ``_export=csv`` to the querystring, the browser will download a csv file containing your data. Supported export formats are: csv, json, latex, ods, tsv, xls, xlsx, yml To customize the name of the query parameter add an ``export_trigger_param`` attribute to your class. By default, the file will be named ``table.ext``, where ``ext`` is the requested export format extension. To customize this name, add a ``export_name`` attribute to your class. The correct extension will be appended automatically to this value. If you must use a function view, you might use something like this:: from django_tables2.config import RequestConfig from django_tables2.export.export import TableExport from .models import Person from .tables import MyTable def table_view(request): table = MyTable(Person.objects.all()) RequestConfig(request).configure(table) export_format = request.GET.get('_export', None) if TableExport.is_valid_format(export_format): exporter = TableExport(export_format, table) return exporter.response('table.{}'.format(export_format)) return render(request, 'table.html', { 'table': table }) What exacly is exported? ------------------------ The export views use the `.Table.as_values()` method to get the data from the table. Because we often use HTML in our table cells, we need to specify something else for the export to make sense. If you use :ref:`table.render_foo`-methods to customize the output for a column, you should define a :ref:`table.value_foo`-method, returning the value you want to be exported. If you are creating your own custom columns, you should know that each column defines a `value()` method, which is used in `Table.as_values()`. By default, it just calls the `render()` method on that column. If your custom column produces HTML, you should override this method and return the actual value. Excluding columns ----------------- Certain columns do not make sense while exporting data: you might show images or have a column with buttons you want to exclude from the export. You can define the columns you want to exclude in several ways:: # exclude a column while defining Columns on a table: class Table(tables.Table): name = columns.Column() buttons = columns.TemplateColumn(template_name='...', exclude_from_export=True) # exclude columns while creating the TableExport instance: exporter = TableExport('csv', table, exclude_columns=('image', 'buttons')) If you use the ``~.ExportMixin``, add an ``exclude_columns`` attribute to your class:: class TableView(ExportMixin, tables.SingleTableView): table_class = MyTable model = Person template_name = 'django_tables2/bootstrap.html' exclude_column = ('buttons', ) Generating export urls ---------------------- You can use the ``querystring`` template tag included with django_tables2 to render a link to export the data as ``csv``:: {% querystring '_export'='csv' %} This will make sure any other query string parameters will be preserved, for example in combination when filtering table items. If you want to render more than one button, you could use something like this:: {% for format in table.export_formats %} download .{{ format }} {% endfor %} .. note:: This example assumes you define a list of possible export formats on your table instance in attribute ``export_formats`` django-tables2-1.14.2/docs/pages/faq.rst000066400000000000000000000057401317557360700177470ustar00rootroot00000000000000.. _faq: .. Any code examples in this file should have a corresponding test in tests/test_faq.py FAQ === Some frequently requested questions/examples. All examples assume you import django-tables2 like this:: import django_tables2 as tables How should I fix error messages about the request context processor? -------------------------------------------------------------------- The error message looks something like this:: Tag {% querystring %} requires django.template.context_processors.request to be in the template configuration in settings.TEMPLATES[]OPTIONS.context_processors) in order for the included template tags to function correctly. which should be pretty clear, but here is an example template configuration anyway:: TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': ['templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.contrib.auth.context_processors.auth', 'django.template.context_processors.request', 'django.template.context_processors.static', ], } } ] How to create a row counter? ---------------------------- You can use `itertools.counter` to add row count to a table. Note that in a paginated table, every page's counter will start at zero. class CountryTable(tables.Table): counter = tables.TemplateColumn('{{ row_counter }}') How to add a footer containing a column total? ---------------------------------------------- Using the `footer`-argument to `~.Column`:: class CountryTable(tables.Table): population = tables.Column( footer=lambda table: sum(x['population'] for x in table.data) ) Or by creating a custom column:: class SummingColumn(tables.Column): def render_footer(self, bound_column, table): return sum(bound_column.accessor.resolve(row) for row in table.data) class Table(tables.Table): name = tables.Column(footer='Total:') population = SummingColumn() Documentation: :ref:`column-footers` .. note :: Your table template must include a block rendering the table footer! Can I use inheritance to build Tables that share features? ---------------------------------------------------------- Yes, like this:: class CountryTable(tables.Table): name = tables.Column() language = tables.Column() A `CountryTable` will show columns `name` and `language`:: class TouristCountryTable(CountryTable): tourist_info = tables.Column() A `TouristCountryTable` will show columns `name`, `language` and `tourist_info`. Overwriting a `Column` attribute from the base class with anything that is not a `Column` will result in removing that Column from the `Table`. For example:: class SimpleCountryTable(CountryTable): language = None A `SimpleCountryTable` will only show column `name`. django-tables2-1.14.2/docs/pages/filtering.rst000066400000000000000000000023001317557360700211500ustar00rootroot00000000000000Filtering data in your table ============================ When presenting a large amount of data, filtering is often a necessity. Fortunately, filtering the data in your django-tables2 table is simple with `django-filter `_. The basis of a filtered table is a `SingleTableMixin` combined with a `FilterView` from django-filter:: from django_filters.views import FilterView from django_tables2.views import SingleTableMixin class FilteredPersonListView(SingleTableMixin, FilterView): table_class = PersonTable model = Person template_name = 'template.html' filterset_class = PersonFilter The filterset is added to the template context in a ``filter`` variable by default. A basic template rendering the filter (using django-bootstrap3) and table looks like this:: {% load render_table from django_tables2 %} {% load bootstrap3 %} {% if filter %}
{% bootstrap_form filter.form layout='inline' %} {% bootstrap_button 'filter' %} {% endif %} {% render_table table 'django_tables2/bootstrap.html' %} django-tables2-1.14.2/docs/pages/generic-mixins.rst000066400000000000000000000050301317557360700221110ustar00rootroot00000000000000Class Based Generic Mixins ========================== Django-tables2 comes with two class based view mixins: `.SingleTableMixin` and `.MultiTableMixin`. A single table using `.SingleTableMixin` ---------------------------------------- `.SingleTableMixin` makes it trivial to incorporate a table into a view or 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. - ``get_table_kwargs()`` allows the keyword arguments passed to the ``Table`` constructor. For example:: 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 = Person 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 `.SingleTableMixin`'s eagerness at finding data sources when one isn't explicitly defined. .. note:: You don't have to base your view on `ListView`, you're able to mix `SingleTableMixin` directly. Multiple tables using `.MultiTableMixin` -------------------------------------------- If you need more than one table in a single view you can use `MultiTableMixin`. It manages multiple tables for you and takes care of adding the appropriate prefixes for them. Just define a list of tables in the tables attribute:: from django_tables2 import MultiTableMixin from django.views.generic.base import TemplateView class PersonTablesView(MultiTableMixin, TemplateView): template_name = 'multiTable.html' tables = [ PersonTable(qs), PersonTable(qs, exclude=('country', )) ] table_pagination = { 'per_page': 10 } In the template, you get a variable `tables`, which you can loop over like this: .. sourcecode:: django {% for table in tables %} {% render_table table %} {% endfor %} django-tables2-1.14.2/docs/pages/glossary.rst000066400000000000000000000031231317557360700210340ustar00rootroot00000000000000Glossary ======== .. 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 serializing 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.14.2/docs/pages/installation.rst000066400000000000000000000006041317557360700216730ustar00rootroot00000000000000Installation ============ Django-tables2 is `Available on pypi `_ and can be installed using pip:: pip install django-tables2 After installing, add ``'django_tables2'`` to ``INSTALLED_APPS`` and make sure that ``'django.template.context_processors.request'`` is added to the ``context_processors`` in your template setting ``OPTIONS``. django-tables2-1.14.2/docs/pages/internal.rst000066400000000000000000000032751317557360700210150ustar00rootroot00000000000000============= Internal APIs ============= The items documented here are internal and subject to change. `.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: `.BoundPinnedRow` ----------------- .. autoclass:: django_tables2.rows.BoundPinnedRow :members: :private-members: :special-members: `.TableData` ------------ .. autoclass:: django_tables2.tables.TableData :members: :private-members: :special-members: `.utils` -------- .. autoclass:: django_tables2.utils.Sequence :members: :private-members: :special-members: .. autoclass:: django_tables2.utils.OrderBy :members: :private-members: :special-members: .. autoclass:: django_tables2.utils.OrderByTuple :members: :private-members: :special-members: .. autoclass:: django_tables2.utils.Accessor :noindex: :members: :private-members: :special-members: .. autoclass:: django_tables2.utils.AttributeDict :noindex: :members: :private-members: :special-members: .. autofunction:: django_tables2.utils.signature :noindex: .. autofunction:: django_tables2.utils.call_with_appropriate :noindex: .. autofunction:: django_tables2.utils.computed_values :noindex: django-tables2-1.14.2/docs/pages/localization-control.rst000066400000000000000000000035651317557360700233510ustar00rootroot00000000000000.. _localization-control: Controlling localization ======================== 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 `localization ` for more information about them. There are two ways of controlling 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.14.2/docs/pages/ordering.rst000066400000000000000000000112311317557360700210010ustar00rootroot00000000000000Alternative column ordering =========================== When using queryset data, one might want to show a computed value which is not in the database. In this case, attempting to order the column will cause an exception:: # 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 '{} {}'.format(self.first_name, self.family_name) # tables.py class PersonTable(tables.Table): name = tables.Column() :: >>> table = PersonTable(Person.objects.all()) >>> table.order_by = 'name' >>> >>> # will result in: FieldError: Cannot resolve keyword 'name' into field. Choices are: first_name, family_name To prevent this, django-tables2 allows two ways to specify custom ordering: accessors and :meth:`~.order_FOO` methods. .. _order-by-accessors: Ordering by accessors --------------------- You can supply an ``order_by`` argument containing a name or a tuple of the names of the columns the database should use to sort it:: class PersonTable(tables.Table): name = tables.Column(order_by=('first_name', 'family_name')) `~.Accessor` syntax can be used as well, as long as they point to a model field. If ordering does not 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) .. _table.order_foo: :meth:`table.order_FOO` methods -------------------------------- Another solution for alternative ordering is being able to chain functions on to the original queryset. This method allows more complex functionality giving the ability to use all of Django's QuerySet API. Adding a `Table.order_FOO` method (where `FOO` is the name of the column), gives you the ability to chain to, or modify, the original queryset when that column is selected to be ordered. The method takes two arguments: `queryset`, and `is_descending`. The return must be a tuple of two elements. The first being the queryset and the second being a boolean; note that modified queryset will only be used if the boolean is `True`. For example, let's say instead of ordering alphabetically, ordering by amount of characters in the first_name is desired. The implementation would look like this: :: # tables.py from django.db.models.functions import Length class PersonTable(tables.Table): name = tables.Column() def order_name(self, queryset, is_descending): queryset = queryset.annotate( length=Length('first_name') ).order_by(('-' if is_descending else '') + 'length') return (queryset, True) As another example, presume the situation calls for being able to order by a mathematical expression. In this scenario, the table needs to be able to be ordered by the sum of both the shirts and the pants. The custom column will have its value rendered using :ref:`table.render_FOO`. This can be achieved like this:: # models.py class Person(models.Model): first_name = models.CharField(max_length=200) family_name = models.CharField(max_length=200) shirts = models.IntegerField() pants = models.IntegerField() # tables.py from django.db.models import F class PersonTable(tables.Table): clothing = tables.Column() class Meta: model = Person def render_clothing(self, record): return str(record.shirts + record.pants) def order_clothing(self, queryset, is_descending): queryset = queryset.annotate( amount=F('shirts') + F('pants') ).order_by(('-' if is_descending else '') + 'amount') return (queryset, True) Using :meth:`Column.order` on custom columns -------------------------------------------- If you created a custom column, which also requires custom ordering like explained above, you can add the body of your ``order_foo`` method to the order method on your custom column, to allow easier reuse. For example, the `PersonTable` from above could also be defined like this:: class ClothingColumn(tables.Column): def render(self, record): return str(record.shirts + record.pants) def order(self, queryset, is_descending): queryset = queryset.annotate( amount=F('shirts') + F('pants') ).order_by(('-' if is_descending else '') + 'amount') return (queryset, True) class PersonTable(tables.Table): clothing = ClothingColumn() class Meta: model = Person django-tables2-1.14.2/docs/pages/pagination.rst000066400000000000000000000012341317557360700213230ustar00rootroot00000000000000.. _pagination: Pagination ========== Pagination is easy, just call :meth:`.Table.paginate` and pass in the current page number:: 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:: 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.14.2/docs/pages/pinned-rows.rst000066400000000000000000000043341317557360700214430ustar00rootroot00000000000000.. _pinned_rows: Pinned rows =========== By using Pinned Rows, you can pin particular rows to the top or bottom of your table. To add pinned rows to your table, you must override `get_top_pinned_data` and/or `get_bottom_pinned_data` methods in your `.Table` class. * `get_top_pinned_data(self)` - Display the pinned rows on top. * `get_bottom_pinned_data(self)` - Display the pinned rows on bottom. By default both methods return `None` value and pinned rows aren't visible. Return data for pinned rows should be iterable type like: queryset, list of dicts, list of objects. Example:: class Table(tables.Table): def get_top_pinned_data(self): return [ # First top pinned row { 'column_a' : 'value for A column', 'column_b' : 'value for B column' }, # Second top pinned row { 'column_a' : 'extra value for A column' 'column_b' : None } ] def get_top_pinned_data(self): return [{ 'column_c' : 'value for C column', 'column_d' : 'value for D column' }] .. note:: Sorting and pagination for pinned rows not working. Value for cell in pinned row will be shown only when **key** in object has the same name as column. You can decide which columns for pinned rows will visible or not. If you want show value for only one column, use only one column name as key. Non existing keys won't be shown in pinned rows. .. warning:: Pinned rows not exist in ``table.rows``. If table has some pinned rows and one normal row then length of ``table.rows`` is 1. .. _pinned_row_attributes: Attributes for pinned rows ~~~~~~~~~~~~~~~~~~~~~~~~~~ If you want to override HTML attributes for pinned rows you should use: ``pinned_row_attrs``. Pinned row attributes can be specified using a `dict` defining the HTML attributes for the ``
`` element on each row. See more: :ref:`row-attributes`. .. note:: By default pinned rows have ``pinned-row`` css class. .. sourcecode:: django [...] [...] django-tables2-1.14.2/docs/pages/query-string-fields.rst000066400000000000000000000022351317557360700231110ustar00rootroot00000000000000.. _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, in order to prevent links on one table interfere with those on another table:: 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.14.2/docs/pages/reference.rst000066400000000000000000000001331317557360700211250ustar00rootroot00000000000000API === .. toctree:: builtin-columns template-tags api-reference internal django-tables2-1.14.2/docs/pages/swapping-columns.rst000066400000000000000000000014101317557360700224740ustar00rootroot00000000000000.. _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.14.2/docs/pages/table-data.rst000066400000000000000000000047651317557360700212040ustar00rootroot00000000000000.. _table-data: Populating a table with data ============================ Tables can be created from 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 access to column values is fine. List of dicts ------------- An an example we will demonstrate using list of dicts. When defining a table it is necessary to declare each column:: import django_tables2 as tables data = [ {'name': 'Bradley'}, {'name': 'Stevie'}, ] class NameTable(tables.Table): name = tables.Column() table = NameTable(data) Querysets --------- If you build use tables to display `~django.db.models.query.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 import django_tables2 as tables class PersonTable(tables.Table): class Meta: model = Person # views.py def person_list(request): table = PersonTable(Person.objects.all()) return render(request, 'person_list.html', { 'table': table }) This has a number of benefits: - Less repetition - Column headers are defined using the field's `~.models.Field.verbose_name` - Specialized columns are used where possible (e.g. `.DateColumn` for a `~.models.DateField`) When using this approach, the following options might be useful to customize what fields to show or hide: - `~.Table.Meta.sequence` -- reorder columns - `~.Table.Meta.fields` -- specify model fields to *include* - `~.Table.Meta.exclude` -- specify model fields to *exclude* Performance ----------- Django-tables tries to be efficient in displaying big datasets. It tries to avoid converting the `~django.db.models.query.QuerySet` instances to lists by using SQL to slice the data and should be able to handle datasets with 100k records without a problem. However, when using one of the customisation methods described in this documentation, there is lot's of oppurtunity to introduce slowness. If you experience that, try to strip the table of customisations and re-add them one by one, checking for performance after each step. django-tables2-1.14.2/docs/pages/table-mixins.rst000066400000000000000000000016121317557360700215660ustar00rootroot00000000000000Table 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.14.2/docs/pages/template-tags.rst000066400000000000000000000040301317557360700217360ustar00rootroot00000000000000.. _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 ``TEMPLATES[]['OPTIONS']['context_processors']`` setting contains ``django.template.context_processors.request``. Please refer to the Django documentation for the TEMPLATES-setting_. .. _TEMPLATES-setting: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-TEMPLATES .. _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.template.context_processors.request`` context processor, see :ref:`template-tags.render_table`. django-tables2-1.14.2/docs/pages/tutorial.rst000066400000000000000000000055161317557360700210440ustar00rootroot00000000000000Tutorial ~~~~~~~~ This is a step-by-step guide to learn how to install and use django-tables2. 1. ``pip install django-tables2`` 2. Add ``'django_tables2'`` to ``INSTALLED_APPS`` 3. Add ``'django.template.context_processors.request'`` to the ``context_processors`` in your template setting ``OPTIONS``. We are 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:: {# tutorial/templates/people.html #} {% load render_table from django_tables2 %} {% load static %} {% 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 custom `.Table` class:: # tutorial/tables.py import django_tables2 as tables from .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 .models import Person from .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 instance: .. sourcecode:: django {% render_table table %} 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. TODO: insert links to various customisation options here. django-tables2-1.14.2/docs/pages/upgrade-changelog.rst000066400000000000000000000004161317557360700225470ustar00rootroot00000000000000Upgrading and change log ======================== Recent versions of django-tables2 have a corresponding git tag for each version released to `pypi `_. .. toctree:: :maxdepth: 1 CHANGELOG.md upgrading-from-v1 django-tables2-1.14.2/docs/pages/upgrading-from-v1.rst000066400000000000000000000032361317557360700224430ustar00rootroot00000000000000Upgrading 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.14.2/docs/requirements.txt000066400000000000000000000001121317557360700206170ustar00rootroot00000000000000-r ../requirements/common.pip Sphinx==1.6.2 sphinx_rtd_theme recommonmark django-tables2-1.14.2/example/000077500000000000000000000000001317557360700160445ustar00rootroot00000000000000django-tables2-1.14.2/example/README.md000066400000000000000000000006121317557360700173220ustar00rootroot00000000000000# Django-tables2 example project This example project only supports the latest version of Django. # To get it up and running: ``` git clone https://github.com/bradleyayers/django-tables2.git pip install -r requirements.pip python manage.py migrate python manage.py loaddata app/fixtures/initial_data.json python manage.py runserver ``` Server should be live at http://127.0.0.1:8000/ now. django-tables2-1.14.2/example/__init__.py000066400000000000000000000000001317557360700201430ustar00rootroot00000000000000django-tables2-1.14.2/example/app/000077500000000000000000000000001317557360700166245ustar00rootroot00000000000000django-tables2-1.14.2/example/app/__init__.py000066400000000000000000000000001317557360700207230ustar00rootroot00000000000000django-tables2-1.14.2/example/app/admin.py000066400000000000000000000002711317557360700202660ustar00rootroot00000000000000# 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.14.2/example/app/data.py000066400000000000000000000015521317557360700201120ustar00rootroot00000000000000COUNTRIES = '''Aruba;104822 Afghanistan;34656032 Angola;28813463 Albania;2876101 Andorra;77281 Arab World;406452690 United Arab Emirates;9269612 Argentina;43847430 Armenia;2924816 American Samoa;55599 Antigua and Barbuda;100963 Australia;24127159 Austria;8747358 Azerbaijan;9762274 Burundi;10524117 Belgium;11348159 Benin;10872298 Burkina Faso;18646433 Bangladesh;162951560 Bulgaria;7127822 Bahrain;1425171 Bahamas, The;391232 Bosnia and Herzegovina;3516816 Belarus;9507120 Belize;366954 Bermuda;65331 Bolivia;10887882 Brazil;207652865 Barbados;284996 Brunei Darussalam;423196 Bhutan;797765 Botswana;2250260 Central African Republic;4594621 Canada;36286425 Switzerland;8372098 Channel Islands;164541 Chile;17909754 China;1378665000 Cote d'Ivoire;23695919 Cameroon;23439189 Congo, Dem. Rep.;78736153 Congo, Rep.;5125821 Colombia;48653419 Comoros;795601 Cabo Verde;539560 ''' django-tables2-1.14.2/example/app/filters.py000066400000000000000000000003601317557360700206450ustar00rootroot00000000000000from django_filters import FilterSet from .models import Person class PersonFilter(FilterSet): class Meta: model = Person fields = { 'name': ['exact', 'contains'], # 'tz': ['exact'], } django-tables2-1.14.2/example/app/fixtures/000077500000000000000000000000001317557360700204755ustar00rootroot00000000000000django-tables2-1.14.2/example/app/fixtures/initial_data.json000066400000000000000000000014511317557360700240130ustar00rootroot00000000000000[ { "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": 4, "model": "app.country", "fields": { "tz": "UTC\u22123.5", "name": "Canada", "visits": 1, "population": 34447000, "flag": "country/flags/canada.svg" } } ] django-tables2-1.14.2/example/app/migrations/000077500000000000000000000000001317557360700210005ustar00rootroot00000000000000django-tables2-1.14.2/example/app/migrations/0001_initial.py000066400000000000000000000030671317557360700234510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.11.5 on 2017-09-22 13:23 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion 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='population')), ('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')), ('friendly', models.BooleanField(default=True)), ('country', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='app.Country')), ], options={ 'verbose_name_plural': 'people', }, ), ] django-tables2-1.14.2/example/app/migrations/__init__.py000066400000000000000000000000001317557360700230770ustar00rootroot00000000000000django-tables2-1.14.2/example/app/models.py000066400000000000000000000021471317557360700204650ustar00rootroot00000000000000# 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=_('population')) 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 def get_absolute_url(self): return 'country/%d' % self.pk @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') friendly = models.BooleanField(default=True) country = models.ForeignKey(Country, null=True) class Meta: verbose_name_plural = 'people' def __unicode__(self): return self.name django-tables2-1.14.2/example/app/tables.py000066400000000000000000000021741317557360700204540ustar00rootroot00000000000000# coding: utf-8 import django_tables2 as tables from .models import Country, Person 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'} class BootstrapTable(tables.Table): country = tables.RelatedLinkColumn() class Meta: model = Person template = 'django_tables2/bootstrap.html' attrs = {'class': 'table table-bordered table-striped table-hover'} exclude = ('friendly', ) class SemanticTable(tables.Table): country = tables.RelatedLinkColumn() class Meta: model = Person template = 'django_tables2/semantic.html' # attrs = {'class': 'ui table table-bordered table-striped table-hover'} exclude = ('friendly', ) class PersonTable(tables.Table): class Meta: model = Person # template = 'django_tables2/bootstrap.html' django-tables2-1.14.2/example/app/views.py000066400000000000000000000105161317557360700203360ustar00rootroot00000000000000# coding: utf-8 from random import choice from django.shortcuts import render from django.utils.lorem_ipsum import words from django.views.generic.base import TemplateView from django_filters.views import FilterMixin, FilterView from django_tables2 import MultiTableMixin, RequestConfig, SingleTableMixin, SingleTableView from django_tables2.export.views import ExportMixin from .data import COUNTRIES from .filters import PersonFilter from .models import Country, Person from .tables import BootstrapTable, CountryTable, PersonTable, SemanticTable, ThemedCountryTable try: from django.urls import reverse except ImportError: # to keep backward (Django <= 1.9) compatibility from django.core.urlresolvers import reverse def create_fake_data(): # create some fake data to make sure we need to paginate if Country.objects.all().count() < 50: for country in COUNTRIES.splitlines(): name, population = country.split(';') Country.objects.create(name=name, visits=0, population=int(population)) if Person.objects.all().count() < 50: countries = list(Country.objects.all()) + [None] Person.objects.bulk_create([ Person(name=words(3, common=False), country=choice(countries)) for i in range(50) ]) def index(request): create_fake_data() table = PersonTable(Person.objects.all()) RequestConfig(request, paginate={ 'per_page': 5 }).configure(table) return render(request, 'index.html', { 'table': table, 'urls': ( (reverse('tutorial'), 'Tutorial'), (reverse('multiple'), 'Multiple tables'), (reverse('filtertableview'), 'Filtered tables'), (reverse('singletableview'), 'Using SingleTableMixin'), (reverse('multitableview'), 'Using MultiTableMixin'), (reverse('bootstrap'), 'Using the bootstrap template'), (reverse('semantic'), 'Using the Semantic UI template'), ) }) 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, }) def bootstrap(request): '''Demonstrate the use of the bootstrap template''' create_fake_data() table = BootstrapTable(Person.objects.all(), order_by='-name') RequestConfig(request, paginate={'per_page': 10}).configure(table) return render(request, 'bootstrap_template.html', { 'table': table }) def semantic(request): '''Demonstrate the use of the Semantic UI template''' create_fake_data() table = SemanticTable(Person.objects.all(), order_by='-name') RequestConfig(request, paginate={'per_page': 10}).configure(table) return render(request, 'semantic_template.html', { 'table': table }) class ClassBased(SingleTableView): table_class = ThemedCountryTable queryset = Country.objects.all() template_name = 'class_based.html' # note that this is not really the way to go because the queryset is not re # -evaluated after the first time the view is requested. qs = Person.objects.all() class MultipleTables(MultiTableMixin, TemplateView): template_name = 'multiTable.html' tables = [ PersonTable(qs), PersonTable(qs, exclude=('country', )) ] table_pagination = { 'per_page': 10 } def tutorial(request): return render(request, 'tutorial.html', {'people': Person.objects.all()}) class FilteredPersonListView(SingleTableMixin, FilterView): table_class = PersonTable model = Person template_name = 'bootstrap_template.html' filterset_class = PersonFilter django-tables2-1.14.2/example/manage.py000077500000000000000000000003621317557360700176520ustar00rootroot00000000000000#!/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.14.2/example/media/000077500000000000000000000000001317557360700171235ustar00rootroot00000000000000django-tables2-1.14.2/example/media/country/000077500000000000000000000000001317557360700206265ustar00rootroot00000000000000django-tables2-1.14.2/example/media/country/flags/000077500000000000000000000000001317557360700217225ustar00rootroot00000000000000django-tables2-1.14.2/example/media/country/flags/australia.svg000066400000000000000000000045051317557360700244340ustar00rootroot00000000000000 django-tables2-1.14.2/example/media/country/flags/canada.svg000066400000000000000000000026171317557360700236600ustar00rootroot00000000000000django-tables2-1.14.2/example/media/country/flags/new_zealand.svg000066400000000000000000000031741317557360700247370ustar00rootroot00000000000000 Flag of New Zealand django-tables2-1.14.2/example/requirements.pip000066400000000000000000000001341317557360700212770ustar00rootroot00000000000000-e .. django-bootstrap3==8.2.3 django-debug-toolbar<1.6 django-filter==1.0.2 tablib<0.11.99 django-tables2-1.14.2/example/settings.py000066400000000000000000000110351317557360700202560ustar00rootroot00000000000000# coding: utf-8 from os.path import abspath, dirname, join ROOT = dirname(abspath(__file__)) DEBUG = True ADMINS = ( # ('Your Name', 'your_email@example.com'), ) MANAGERS = ADMINS DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': join(ROOT, 'database.sqlite'), } } # 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' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': ['templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.contrib.auth.context_processors.auth', 'django.template.context_processors.request', 'django.template.context_processors.static', ], } } ] 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 = 'urls' INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'app', 'django_filters', 'bootstrap3', '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.14.2/example/templates/000077500000000000000000000000001317557360700200425ustar00rootroot00000000000000django-tables2-1.14.2/example/templates/base.html000066400000000000000000000011511317557360700216400ustar00rootroot00000000000000{% load static %} django-tables2 examples {% block extrahead %}{% endblock %} {% block body %} {% load django_tables2 %} {% render_table table %} {% endblock %} django-tables2-1.14.2/example/templates/bootstrap_template.html000066400000000000000000000016521317557360700246440ustar00rootroot00000000000000{% load static %} {% load render_table from django_tables2 %} {% load bootstrap3 %} django_tables2 with bootstrap template example {% bootstrap_css %}

django_tables2 with bootstrap template example

{% if filter %}
{% bootstrap_form filter.form layout='inline' %} {% bootstrap_button 'filter' %}
{% endif %}
{% render_table table 'django_tables2/bootstrap.html' %}
django-tables2-1.14.2/example/templates/class_based.html000066400000000000000000000003751317557360700232000ustar00rootroot00000000000000{% extends "base.html" %} {% load django_tables2 %} {% block body %}

class based view via render_table

{{ "{%" }} load django_tables2 {{ "%}" }}
{{ "{%" }} render_table example {{ "%}" }}
{% render_table table %} {% endblock %} django-tables2-1.14.2/example/templates/extended_table.html000066400000000000000000000002751317557360700237030ustar00rootroot00000000000000{% extends "django_tables2/table.html" %} {% block table.tfoot %}
{% endblock %} django-tables2-1.14.2/example/templates/index.html000066400000000000000000000021171317557360700220400ustar00rootroot00000000000000{% extends "base.html" %} {% load django_tables2 %} {% load static %} {% block extrahead %} {% endblock %} {% block body %}

Some examples of using django-tables2

Welcome to the django-tables2 example project. Below is a list of different examples using django-tables2.

    {% for url, text in urls %}
  • {{ text }}
  • {% endfor %}

Basic example of a table

{% render_table table "django_tables2/bootstrap.html" %}

Same table, but responsive (.table-responsive)

{% render_table table "django_tables2/bootstrap-responsive.html" %}
{% endblock %} django-tables2-1.14.2/example/templates/multiTable.html000066400000000000000000000013751317557360700230400ustar00rootroot00000000000000{% extends "base.html" %} {% load django_tables2 %} {% load static %} {% block extrahead %} {% endblock %} {% block body %}

Multiple tables on a single page using MultiTableMixin

Pagination should work independently for each table, the second table excludes the column 'country'.

{% for table in tables %}
{% render_table table %}
{% endfor %}
{% endblock %} django-tables2-1.14.2/example/templates/multiple.html000066400000000000000000000046131317557360700225670ustar00rootroot00000000000000{% 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.14.2/example/templates/semantic_template.html000066400000000000000000000012161317557360700244260ustar00rootroot00000000000000{% load render_table from django_tables2 %} django_tables2 with semantic template example

django_tables2 with Semantic UI template example

{% render_table table %}
django-tables2-1.14.2/example/templates/tutorial.html000066400000000000000000000004201317557360700225670ustar00rootroot00000000000000{% load render_table from django_tables2 %} {% load static %} =1.8'], classifiers=[ 'Development Status :: 5 - Production/Stable', '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', 'Programming Language :: Python :: 3.5', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Software Development :: Libraries', ], ) django-tables2-1.14.2/tests/000077500000000000000000000000001317557360700155535ustar00rootroot00000000000000django-tables2-1.14.2/tests/__init__.py000066400000000000000000000000001317557360700176520ustar00rootroot00000000000000django-tables2-1.14.2/tests/app/000077500000000000000000000000001317557360700163335ustar00rootroot00000000000000django-tables2-1.14.2/tests/app/__init__.py000066400000000000000000000000001317557360700204320ustar00rootroot00000000000000django-tables2-1.14.2/tests/app/locale/000077500000000000000000000000001317557360700175725ustar00rootroot00000000000000django-tables2-1.14.2/tests/app/locale/ua/000077500000000000000000000000001317557360700201775ustar00rootroot00000000000000django-tables2-1.14.2/tests/app/locale/ua/LC_MESSAGES/000077500000000000000000000000001317557360700217645ustar00rootroot00000000000000django-tables2-1.14.2/tests/app/locale/ua/LC_MESSAGES/django.mo000066400000000000000000000004431317557360700235640ustar00rootroot000000000000004L`arQ,translation testtranslation test lazyReport-Msgid-Bugs-To: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 тест перекладутест ленивого перекладуdjango-tables2-1.14.2/tests/app/locale/ua/LC_MESSAGES/django.po000066400000000000000000000004041317557360700235640ustar00rootroot00000000000000msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" msgid "translation test" msgstr "тест перекладу" msgid "translation test lazy" msgstr "тест ленивого перекладу" django-tables2-1.14.2/tests/app/models.py000066400000000000000000000065241317557360700201770ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils import six from django.utils.safestring import mark_safe from django.utils.translation import ugettext, ugettext_lazy from haystack import indexes try: from django.urls import reverse except ImportError: # to keep backward (Django <= 1.9) compatibility from django.core.urlresolvers import reverse @six.python_2_unicode_compatible 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 of the person', on_delete=models.CASCADE ) 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")) website = models.URLField( max_length=200, null=True, blank=True, verbose_name="web site") birthdate = models.DateField(null=True) content_type = models.ForeignKey( ContentType, null=True, blank=True, on_delete=models.CASCADE ) object_id = models.PositiveIntegerField(null=True, blank=True) foreign_key = GenericForeignKey() friends = models.ManyToManyField('Person') class Meta: verbose_name = "person" verbose_name_plural = "people" def __str__(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', ) @six.python_2_unicode_compatible class Occupation(models.Model): name = models.CharField(max_length=200) region = models.ForeignKey('Region', null=True, on_delete=models.CASCADE) boolean = models.BooleanField(null=True) boolean_with_choices = models.BooleanField(null=True, choices=( (True, 'Yes'), (False, 'No') )) def get_absolute_url(self): return reverse('occupation', args=(self.pk, )) def __str__(self): return self.name @six.python_2_unicode_compatible class Region(models.Model): name = models.CharField(max_length=200) mayor = models.OneToOneField(Person, null=True, on_delete=models.CASCADE) def __str__(self): return self.name class PersonInformation(models.Model): person = models.ForeignKey( Person, related_name='info_list', verbose_name='Information', on_delete=models.CASCADE ) # -- haystack ----------------------------------------------------------------- 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.14.2/tests/app/settings.py000066400000000000000000000013671317557360700205540ustar00rootroot00000000000000DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', } } INSTALLED_APPS = [ 'tests.app', 'django.contrib.contenttypes', 'django.contrib.auth', 'django_tables2', 'haystack', ] ROOT_URLCONF = 'tests.app.urls' SECRET_KEY = "this is super secret" 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 HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', } } django-tables2-1.14.2/tests/app/templates/000077500000000000000000000000001317557360700203315ustar00rootroot00000000000000django-tables2-1.14.2/tests/app/templates/child/000077500000000000000000000000001317557360700214145ustar00rootroot00000000000000django-tables2-1.14.2/tests/app/templates/child/foo.html000066400000000000000000000000041317557360700230570ustar00rootroot00000000000000bar django-tables2-1.14.2/tests/app/templates/dummy.html000066400000000000000000000000301317557360700223430ustar00rootroot00000000000000dummy template contents django-tables2-1.14.2/tests/app/templates/multiple.html000066400000000000000000000002161317557360700230510ustar00rootroot00000000000000{% load django_tables2 %}

Multiple tables using MultiTableMixin

{% for table in tables %} {% render_table table %} {% endfor %} django-tables2-1.14.2/tests/app/templates/test_template_column.html000066400000000000000000000000601317557360700254420ustar00rootroot00000000000000name:{{ record.col }}-{{ foo|default:"empty" }} django-tables2-1.14.2/tests/app/urls.py000066400000000000000000000006621317557360700176760ustar00rootroot00000000000000from django.conf.urls import url from . import views urlpatterns = [ url(r'^people/delete/(?P\d+)/$', views.person, name='person_delete'), url(r'^people/edit/(?P\d+)/$', views.person, name='person_edit'), 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.14.2/tests/app/views.py000066400000000000000000000010741317557360700200440ustar00rootroot00000000000000# coding: utf-8 from django.http import HttpResponse from django.shortcuts import get_object_or_404 from .models import Occupation, Person 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.14.2/tests/columns/000077500000000000000000000000001317557360700172335ustar00rootroot00000000000000django-tables2-1.14.2/tests/columns/__init__.py000066400000000000000000000000001317557360700213320ustar00rootroot00000000000000django-tables2-1.14.2/tests/columns/test_booleancolumn.py000066400000000000000000000102341317557360700235010ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import pytest from django.db import models import django_tables2 as tables from ..app.models import Occupation, Person from ..utils import attrs, build_request 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].get_cell('col') == '---' def test_treat_none_as_false(): class Table(tables.Table): col = tables.BooleanColumn(null=True) table = Table([{'col': None}]) assert table.rows[0].get_cell('col') == '' def test_value_returns_a_raw_value_without_html(): class Table(tables.Table): col = tables.BooleanColumn() table = Table([{'col': True}, {'col': False}]) assert table.rows[0].get_cell_value('col') == 'True' assert table.rows[1].get_cell_value('col') == 'False' def test_span_attrs(): class Table(tables.Table): col = tables.BooleanColumn(attrs={'span': {'key': 'value'}}) table = Table([{'col': True}]) assert attrs(table.rows[0].get_cell('col')) == {'class': 'true', 'key': 'value'} def test_boolean_field_choices_with_real_model_instances(): ''' If a booleanField has choices defined, the value argument passed to BooleanColumn.render() is the rendered value, not a bool. ''' class BoolModelChoices(models.Model): field = models.BooleanField(choices=( (True, 'Yes'), (False, 'No')) ) class Meta: app_label = 'django_tables2_test' class Table(tables.Table): class Meta: model = BoolModelChoices table = Table([BoolModelChoices(field=True), BoolModelChoices(field=False)]) assert table.rows[0].get_cell('field') == '' assert table.rows[1].get_cell('field') == '' @pytest.mark.django_db def test_boolean_field_choices_spanning_relations(): 'The inverse lookup voor boolean choices should also work on related models' class Table(tables.Table): boolean = tables.BooleanColumn(accessor='occupation.boolean_with_choices') class Meta: model = Person model_true = Occupation.objects.create( name='true-name', boolean_with_choices=True ) model_false = Occupation.objects.create( name='false-name', boolean_with_choices=False ) table = Table([ Person(first_name='True', last_name='False', occupation=model_true), Person(first_name='True', last_name='False', occupation=model_false) ]) assert table.rows[0].get_cell('boolean') == '' assert table.rows[1].get_cell('boolean') == '' @pytest.mark.django_db def test_boolean_should_not_prevent_rendering_of_other_columns(): '''Test for issue 360''' class Table(tables.Table): boolean = tables.BooleanColumn(yesno='waar,onwaar') class Meta: model = Person fields = ('boolean', 'name') Occupation.objects.create(name='Waar', boolean=True), Occupation.objects.create(name='Onwaar', boolean=False), Occupation.objects.create(name='Onduidelijk') html = Table(Occupation.objects.all()).as_html(build_request()) assert 'Waar' in html assert 'Onwaar' in html django-tables2-1.14.2/tests/columns/test_checkboxcolumn.py000066400000000000000000000055321317557360700236550ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import django_tables2 as tables from ..utils import attrs def test_new_attrs_should_be_supported(): 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].get_cell('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].get_cell('col2')) == { 'type': 'checkbox', 'key': 'value', 'value': 'data', 'name': 'col2' } def test_column_is_checked(): class TestTable(tables.Table): col = tables.CheckBoxColumn(attrs={'name': 'col'}, checked='is_selected') table = TestTable([ {'col': '1', 'is_selected': True}, {'col': '2', 'is_selected': False} ]) assert attrs(table.rows[0].get_cell('col')) == { 'type': 'checkbox', 'value': '1', 'name': 'col', 'checked': 'checked' } assert attrs(table.rows[1].get_cell('col')) == {'type': 'checkbox', 'value': '2', 'name': 'col'} def test_column_is_not_checked_for_non_existing_column(): class TestTable(tables.Table): col = tables.CheckBoxColumn(checked='does_not_exist') table = TestTable([ {'col': '1', 'is_selected': True}, {'col': '2', 'is_selected': False} ]) assert attrs(table.rows[0].get_cell('col')) == {'type': 'checkbox', 'value': '1', 'name': 'col'} assert attrs(table.rows[1].get_cell('col')) == {'type': 'checkbox', 'value': '2', 'name': 'col'} def test_column_is_alway_checked(): class TestTable(tables.Table): col = tables.CheckBoxColumn(checked=True) table = TestTable([ {'col': 1, 'foo': 'bar'}, {'col': 2, 'foo': 'baz'} ]) assert attrs(table.rows[0].get_cell('col'))['checked'] == 'checked' assert attrs(table.rows[1].get_cell('col'))['checked'] == 'checked' def test_column_is_checked_callback(): def is_selected(value, record): return value == '1' class TestTable(tables.Table): col = tables.CheckBoxColumn(attrs={'name': 'col'}, checked=is_selected) table = TestTable([{'col': '1'}, {'col': '2'}]) assert attrs(table.rows[0].get_cell('col')) == { 'type': 'checkbox', 'value': '1', 'name': 'col', 'checked': 'checked' } assert attrs(table.rows[1].get_cell('col')) == {'type': 'checkbox', 'value': '2', 'name': 'col'} django-tables2-1.14.2/tests/columns/test_datecolumn.py000066400000000000000000000042661317557360700230070ustar00rootroot00000000000000# coding: utf-8 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/stable/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].get_cell('date') == 'Tue sep 2012' assert table.rows[1].get_cell('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].get_cell('date') == 'Tue 2012 sep' assert table.rows[1].get_cell('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].get_cell('date') == 'sep 2012 Tue' assert table.rows[1].get_cell('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 def test_value_returns_a_raw_value_without_html(settings): settings.SHORT_DATE_FORMAT = 'b Y D' class Table(tables.Table): col = tables.DateColumn() table = Table([{'col': date(2012, 9, 11)}]) assert table.rows[0].get_cell_value('col') == 'sep 2012 Tue' django-tables2-1.14.2/tests/columns/test_datetimecolumn.py000066400000000000000000000046501317557360700236630ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from datetime import datetime import pytest import pytz from django.db import models import django_tables2 as tables ''' Format string: https://docs.djangoproject.com/en/stable/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) yield pytz.timezone('Australia/Brisbane').localize(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].get_cell('date') == 'Tue sep 2012' assert table.rows[1].get_cell('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].get_cell('date') == 'Tue 2012 sep PM 12:30' assert table.rows[1].get_cell('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].get_cell('date') == 'sep 2012 Tue PM 12:30' assert table.rows[1].get_cell('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 def test_value_returns_a_raw_value_without_html(dt, settings): settings.SHORT_DATETIME_FORMAT = 'b Y D A f' class Table(tables.Table): col = tables.DateTimeColumn() table = Table([{'col': dt}]) assert table.rows[0].get_cell_value('col') == 'sep 2012 Tue PM 12:30' django-tables2-1.14.2/tests/columns/test_emailcolumn.py000066400000000000000000000026421317557360700231550ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from django.db import models import django_tables2 as tables 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].get_cell('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].get_cell('email') == '---' def test_should_be_used_for_emailfields(): 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 def test_text_should_be_overridable(): class Table(tables.Table): email = tables.EmailColumn(text='@') table = Table([{'email': 'test@example.com'}]) assert table.rows[0].get_cell('email') == '@' def test_value_returns_a_raw_value_without_html(): class Table(tables.Table): col = tables.EmailColumn() table = Table([{'col': 'test@example.com'}]) assert table.rows[0].get_cell_value('col') == 'test@example.com' django-tables2-1.14.2/tests/columns/test_filecolumn.py000066400000000000000000000057331317557360700230110ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from os.path import dirname, join import pytest 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 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'} }) @pytest.yield_fixture def column_with_text(): yield tables.FileColumn(text='Download') 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_, record=None)) 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 root = parse(column.render(value=file_, record=None)) 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, record=None)) 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, record=None) 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' def test_filecolumn_text_custom_value(column_with_text, storage): name = 'foobar.html' file_ = ContentFile('') file_.name = name root = parse(column_with_text.render(value=file_, record=None)) assert root.tag == 'span' assert root.attrib == {'title': name, 'class': ''} assert root.text == 'Download' django-tables2-1.14.2/tests/columns/test_general.py000066400000000000000000000325701317557360700222700ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import pytest from django.db import models 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 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].get_cell('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_raise_on_invalid_accessor(): with pytest.raises(TypeError): class SimpleTable(tables.Table): column = tables.Column(accessor={}) def test_column_with_callable_accessor_should_not_have_default(): with pytest.raises(TypeError): class SimpleTable(tables.Table): column = tables.Column(accessor=lambda: 'foo', default='') 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_orderable(): class SimpleTable(tables.Table): name = tables.Column() table = SimpleTable([]) assert table.columns['name'].orderable is True class SimpleTable(tables.Table): name = tables.Column() class Meta: orderable = False table = SimpleTable([]) assert table.columns['name'].orderable is False class SimpleTable(tables.Table): name = tables.Column() class Meta: orderable = True table = SimpleTable([]) 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'} 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'} assert root.findall('.//tbody/tr/td')[0].attrib == {'class': 'a'} def test_th_are_given_orderable_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 'orderable' in classes(root.findall('.//thead/tr/th')[0]) assert 'orderable' 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 'orderable' in classes(root.findall('.//thead/tr/th')[0]) assert 'asc' in classes(root.findall('.//thead/tr/th')[0]) assert 'orderable' 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 [row.get_cell('a') for row in table.rows] == ['--', '--', 3, 4] def test_register_skips_non_columns(): from django_tables2.columns.base import library @library.register class Klass(object): pass class Table(tables.Table): class Meta: model = Person Table([]) def test_raises_when_using_non_supported_index(): class Table(tables.Table): column = tables.Column() table = Table([{'column': 'foo'}]) row = table.rows[0] with pytest.raises(TypeError): row[table] class MyModel(models.Model): item1 = models.CharField(max_length=10) class Meta: app_label = 'django_tables2_tests' class MyTable(tables.Table): item1 = tables.Column(verbose_name='Nice column name') class Meta: model = MyModel fields = ('item1', ) def test_column_params_should_be_preserved_under_inheritance(): ''' Github issue #337 Columns explicitly defined on MyTable get overridden by columns implicitly defined on it's child. If the column is not redefined, the explicit definition of MyTable is used, preserving the specialized verbose_name defined on it. ''' class MyTableA(MyTable): ''' having an empty `class Meta` should not undo the explicit definition of column item1 in MyTable. ''' class Meta(MyTable.Meta): pass class MyTableB(MyTable): ''' having a non-empty `class Meta` should not undo the explicit definition of column item1 in MyTable. ''' class Meta(MyTable.Meta): per_page = 22 table = MyTable(MyModel.objects.all()) tableA = MyTableA(MyModel.objects.all()) tableB = MyTableB(MyModel.objects.all()) assert table.columns['item1'].verbose_name == 'Nice column name' assert tableA.columns['item1'].verbose_name == 'Nice column name' assert tableB.columns['item1'].verbose_name == 'Nice column name' def test_explicit_column_can_be_overridden_by_other_explicit_column(): class MyTableC(MyTable): ''' If we define a new explict item1 column, that one should be used. ''' item1 = tables.Column(verbose_name='New nice column name') table = MyTable(MyModel.objects.all()) tableC = MyTableC(MyModel.objects.all()) assert table.columns['item1'].verbose_name == 'Nice column name' assert tableC.columns['item1'].verbose_name == 'New nice column name' def test_override_column_class_names(): ''' We control the output of CSS class names for a column by overriding get_column_class_names ''' class MyTable(tables.Table): population = tables.Column(verbose_name='Population') def get_column_class_names(self, classes_set, bound_column): classes_set.add('prefix-%s' % bound_column.name) return classes_set TEST_DATA = [ {'name': 'Belgium', 'population': 11200000}, {'name': 'Luxembourgh', 'population': 540000}, {'name': 'France', 'population': 66000000}, ] html = MyTable(TEST_DATA).as_html(build_request()) assert '' in html @pytest.mark.django_db def test_computable_td_attrs(): '''Computable attrs for columns, using table argument''' Person.objects.create(first_name='Jan', last_name='Pietersz.') Person.objects.create(first_name='Sjon', last_name='Jansen') class Table(tables.Table): person = tables.Column(attrs={ 'cell': { 'data-length': lambda table: len(table.data) } }) table = Table(Person.objects.all()) html = table.as_html(request) assert 'data-length="2"' in html django-tables2-1.14.2/tests/columns/test_jsoncolumn.py000066400000000000000000000030361317557360700230350ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import pytest from django.db import models import django_tables2 as tables try: from django.contrib.postgres.fields import HStoreField, JSONField JSONFIELD_AVAILABLE = True except ImportError: # fields are introduced in django == 1.9 # remove shim wen we drop support for django 1.8 JSONFIELD_AVAILABLE = False @pytest.mark.skipif(not JSONFIELD_AVAILABLE, reason='JSONField added in django 1.9') def test_should_be_used_for_json_and_hstore_fields(): class Model(models.Model): json = JSONField() hstore = HStoreField() class Meta: app_label = 'django_tables2_test' class Table(tables.Table): class Meta: model = Model assert isinstance(Table.base_columns['json'], tables.JSONColumn) assert isinstance(Table.base_columns['hstore'], tables.JSONColumn) def test_jsoncolumn_dict(): column = tables.JSONColumn() record = {'json': {'species': 'Falcon'}} html = column.render(value=record['json'], record=record) assert html == '
{\n  "species": "Falcon"\n}
' def test_jsoncolumn_string(): column = tables.JSONColumn() record = {'json': "really?"} html = column.render(value=record['json'], record=record) assert html == '
"really?"
' def test_jsoncolumn_number(): column = tables.JSONColumn() record = {'json': 3.14} html = column.render(value=record['json'], record=record) assert html == '
3.14
' django-tables2-1.14.2/tests/columns/test_linkcolumn.py000066400000000000000000000160301317557360700230170ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import pytest from django.template import Context, Template from django.utils.html import mark_safe import django_tables2 as tables from django_tables2 import A from ..app.models import Occupation, Person from ..utils import attrs, build_request try: from django.urls import reverse except ImportError: # to keep backward (Django <= 1.9) compatibility from django.core.urlresolvers import reverse def test_unicode(): '''Test LinkColumn for 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'}, ] template = Template('{% load django_tables2 %}{% render_table table %}') html = template.render(Context({ 'request': build_request(), 'table': UnicodeTable(dataset) })) 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'} ] html = CustomLinkTable(dataset).as_html(build_request()) assert 'foo::bar' in html assert 'Doe John' in html def test_link_text_escaping(): class CustomLinkTable(tables.Table): editlink = tables.LinkColumn( 'person', text=mark_safe('edit'), args=[A('pk')] ) dataset = [ {'pk': 1, 'first_name': 'John', 'last_name': 'Doe'} ] html = CustomLinkTable(dataset).as_html(build_request()) expected = ''.format( reverse('person', args=(1, )) ) assert expected 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') table = PersonTable(Person.objects.all()) html = table.as_html(build_request()) assert '' in html @pytest.mark.django_db def test_linkcolumn_non_field_based(): '''Test for issue 257, non-field based columns''' class Table(tables.Table): first_name = tables.Column() delete_link = tables.LinkColumn('person_delete', text='delete', kwargs={'pk': tables.A('id')}) willem = Person.objects.create(first_name='Willem', last_name='Wever') html = Table(Person.objects.all()).as_html(build_request()) expected = ''.format( reverse('person_delete', kwargs={'pk': willem.pk}) ) assert expected in html def test_kwargs(): class PersonTable(tables.Table): a = tables.LinkColumn('occupation', kwargs={'pk': A('a')}) table = PersonTable([{'a': 0}, {'a': 1}]) assert reverse('occupation', kwargs={'pk': 0}) in table.rows[0].get_cell('a') assert reverse('occupation', kwargs={'pk': 1}) in table.rows[1].get_cell('a') 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].get_cell('name') == '<brad>' 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].get_cell('col')) == { 'href': reverse('occupation', kwargs={'pk': 0}), 'title': 'Occupation Title' } @pytest.mark.django_db def test_td_attrs_should_be_supported(): '''LinkColumn should support both ' in html def test_should_handle_context_on_table(): class TestTable(tables.Table): col_code = tables.TemplateColumn(template_code='code:{{ record.col }}-{{ foo }}') col_name = tables.TemplateColumn(template_name='test_template_column.html') table = TestTable([{'col': 'brad'}]) assert table.rows[0].get_cell('col_code') == 'code:brad-' assert table.rows[0].get_cell('col_name') == 'name:brad-empty\n' table.context = Context({'foo': 'author'}) assert table.rows[0].get_cell('col_code') == 'code:brad-author' assert table.rows[0].get_cell('col_name') == 'name:brad-author\n' # new table and render using the 'render_table' template tag. table = TestTable([{'col': 'brad'}]) template = Template('{% load django_tables2 %}{% render_table table %}') html = template.render(Context({'request': build_request(), 'table': table, 'foo': 'author'})) assert '' in html def test_should_support_default(): class Table(tables.Table): foo = tables.TemplateColumn('default={{ default }}', default='bar') table = Table([{}]) assert table.rows[0].get_cell('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].get_cell('foo') == 'value=bar' template = Template('{% load django_tables2 %}{% render_table table %}') html = template.render(Context({'request': build_request(), 'table': table})) assert '' in html def test_should_support_column(): class Table(tables.Table): tcol = tables.TemplateColumn('column={{ column.name }}') table = Table([{'foo': 'bar'}]) assert table.rows[0].get_cell('tcol') == 'column=tcol' def test_should_raise_when_called_without_template(): with pytest.raises(ValueError): class Table(tables.Table): col = tables.TemplateColumn() def test_should_support_value_with_curly_braces(): ''' https://github.com/bradleyayers/django-tables2/issues/441 ''' class Table(tables.Table): track = tables.TemplateColumn('track: {{ value }}') table = Table([{'track': 'Beat it {Freestyle}'}]) assert table.rows[0].get_cell('track') == 'track: Beat it {Freestyle}' def test_should_strip_tags_for_value(): class Table(tables.Table): track = tables.TemplateColumn('{{ value }}') table = Table([{'track': 'Space Oddity'}]) assert list(table.as_values()) == [['Track'], ['Space Oddity']] django-tables2-1.14.2/tests/columns/test_timecolumn.py000066400000000000000000000022441317557360700230220ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from datetime import time from django.db import models import django_tables2 as tables ''' Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date ''' def test_should_handle_explicit_format(): class TestTable(tables.Table): time = tables.TimeColumn(format='H:i:s') class Meta: default = '—' table = TestTable([{'time': time(11, 11, 11)}, {'time': None}]) assert table.rows[0].get_cell('time') == "11:11:11" assert table.rows[1].get_cell('time') == "—" def test_should_be_used_for_timefields(): class TimeModel(models.Model): field = models.TimeField() class Meta: app_label = 'django_tables2_test' class Table(tables.Table): class Meta: model = TimeModel assert type(Table.base_columns['field']) == tables.TimeColumn def test_value_returns_a_raw_value_without_html(): class Table(tables.Table): col = tables.TimeColumn(format='H:i:s') table = Table([{'col': time(11, 11, 11)}]) assert table.rows[0].get_cell_value('col') == '11:11:11' django-tables2-1.14.2/tests/columns/test_urlcolumn.py000066400000000000000000000034461317557360700226730ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from django.db import models import django_tables2 as tables MEMORY_DATA = [ {'url': 'http://example.com', 'name': 'Example'}, {'url': 'https://example.com', 'name': 'Example (https)'}, {'url': 'ftp://example.com', 'name': 'Example (ftp)'}, ] def test_should_turn_url_into_hyperlink(): class TestTable(tables.Table): url = tables.URLColumn() table = TestTable(MEMORY_DATA) assert table.rows[0].get_cell('url') == 'http://example.com' assert table.rows[1].get_cell('url') == 'https://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 def test_text_can_be_overridden(): class Table(tables.Table): url = tables.URLColumn(text='link') table = Table(MEMORY_DATA) assert table.rows[0].get_cell('url') == 'link' def test_text_can_be_overridden_with_callable(): class Table(tables.Table): url = tables.URLColumn(text=lambda record: record['name']) table = Table(MEMORY_DATA) assert table.rows[0].get_cell('url') == 'Example' assert table.rows[1].get_cell('url') == 'Example (https)' def test_value_returns_a_raw_value_without_html(): class TestTable(tables.Table): col = tables.URLColumn() table = TestTable([{'col': 'http://example.com'}]) assert table.rows[0].get_cell_value('col') == 'http://example.com' django-tables2-1.14.2/tests/export/000077500000000000000000000000001317557360700170745ustar00rootroot00000000000000django-tables2-1.14.2/tests/export/__init__.py000066400000000000000000000000001317557360700211730ustar00rootroot00000000000000django-tables2-1.14.2/tests/export/test_export.py000066400000000000000000000173321317557360700220340ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import json import pytest from django.core.exceptions import ImproperlyConfigured from django.shortcuts import render import django_tables2 as tables from django_tables2.config import RequestConfig from ..app.models import Occupation, Person, Region from ..test_views import DispatchHookMixin from ..utils import build_request # Skip if tablib is not installed (required for debian packaging) pytest.importorskip('tablib') try: from django_tables2.export.export import TableExport from django_tables2.export.views import ExportMixin except ImproperlyConfigured: pass NAMES = [ ('Yildiz', 'van der Kuil'), ('Lindi', 'Hakvoort'), ('Gerardo', 'Castelein'), ] EXPECTED_CSV = '\r\n'.join( ('First Name,Surname', ) + tuple(','.join(name) for name in NAMES) ) + '\r\n' EXPECTED_JSON = list([ {'First Name': first_name, 'Surname': last_name} for first_name, last_name in NAMES ]) def create_test_persons(): for first_name, last_name in NAMES: Person.objects.create(first_name=first_name, last_name=last_name) class Table(tables.Table): first_name = tables.Column() last_name = tables.Column() class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): table_class = Table table_pagination = {'per_page': 1} model = Person # required for ListView template_name = 'django_tables2/bootstrap.html' @pytest.mark.django_db def test_view_should_support_csv_export(): create_test_persons() response, view = View.as_view()(build_request('/?_export=csv')) assert response.getvalue().decode('utf8') == EXPECTED_CSV # should just render the normal table without the _export query response, view = View.as_view()(build_request('/')) html = response.render().rendered_content assert 'Yildiz' in html assert 'Lindy' not in html def test_exporter_should_raise_error_for_unsupported_file_type(): table = Table([]) with pytest.raises(TypeError): TableExport(table=table, export_format='exe') @pytest.mark.django_db def test_view_should_support_json_export(): create_test_persons() response, view = View.as_view()(build_request('/?_export=json')) assert json.loads(response.getvalue().decode('utf8')) == EXPECTED_JSON @pytest.mark.django_db def test_view_should_support_custom_trigger_param(): class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): table_class = Table export_trigger_param = 'export_to' model = Person # required for ListView create_test_persons() response, view = View.as_view()(build_request('/?export_to=json')) assert json.loads(response.getvalue().decode('utf8')) == EXPECTED_JSON @pytest.mark.django_db def test_view_should_support_custom_filename(): class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): table_class = Table export_name = 'people' model = Person # required for ListView create_test_persons() response, view = View.as_view()(build_request('/?_export=json')) assert response['Content-Disposition'] == 'attachment; filename="people.json"' @pytest.mark.django_db def test_function_view(): ''' Test the code used in the docs ''' create_test_persons() def table_view(request): table = Table(Person.objects.all()) RequestConfig(request).configure(table) export_format = request.GET.get('_export', None) if TableExport.is_valid_format(export_format): exporter = TableExport(export_format, table) return exporter.response('table.{}'.format(export_format)) return render(request, 'django_tables2/table.html', { 'table': table }) response = table_view(build_request('/?_export=csv')) assert response.getvalue().decode('utf8') == EXPECTED_CSV # must also support the normal html table. response = table_view(build_request('/')) html = response.content.decode('utf8') assert 'Yildiz' in html assert 'Lindy' not in html def create_test_occupations(): richard = Person.objects.create(first_name='Richard', last_name='Queener') vlaanderen = Region.objects.create(name='Vlaanderen', mayor=richard) Occupation.objects.create(name='Timmerman', boolean=True, region=vlaanderen) Occupation.objects.create(name='Ecoloog', boolean=False, region=vlaanderen) class OccupationTable(tables.Table): name = tables.Column() boolean = tables.Column() region = tables.Column() class OccupationView(DispatchHookMixin, ExportMixin, tables.SingleTableView): table_class = OccupationTable table_pagination = {'per_page': 1} model = Occupation template_name = 'django_tables2/bootstrap.html' @pytest.mark.django_db def test_exporting_should_work_with_foreign_keys(): create_test_occupations() response, view = OccupationView.as_view()(build_request('/?_export=xls')) data = response.content # binary data, so not possible to compare to an exact expectation assert data.find('Vlaanderen'.encode()) assert data.find('Ecoloog'.encode()) assert data.find('Timmerman'.encode()) @pytest.mark.django_db def test_exporting_should_work_with_foreign_key_fields(): create_test_occupations() class OccupationWithForeignKeyFieldsTable(tables.Table): name = tables.Column() boolean = tables.Column() region = tables.Column() mayor = tables.Column(accessor='region.mayor.first_name') class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): table_class = OccupationWithForeignKeyFieldsTable table_pagination = {'per_page': 1} model = Occupation template_name = 'django_tables2/bootstrap.html' response, view = View.as_view()(build_request('/?_export=csv')) data = response.getvalue().decode('utf8') expected_csv = '\r\n'.join(( 'Name,Boolean,Region,First Name', 'Timmerman,True,Vlaanderen,Richard', 'Ecoloog,False,Vlaanderen,Richard\r\n' )) assert data == expected_csv @pytest.mark.django_db def test_exporting_exclude_columns(): create_test_occupations() class OccupationExcludingView(DispatchHookMixin, ExportMixin, tables.SingleTableView): table_class = OccupationTable table_pagination = {'per_page': 1} model = Occupation template_name = 'django_tables2/bootstrap.html' exclude_columns = ('boolean', ) response, view = OccupationExcludingView.as_view()(build_request('/?_export=csv')) data = response.getvalue().decode('utf8') assert data.splitlines()[0] == 'Name,Region' @pytest.mark.django_db def test_exporting_unicode_data(): unicode_name = '木匠' Occupation.objects.create(name=unicode_name) expected_csv = 'Name,Boolean,Region\r\n{},,\r\n'.format(unicode_name) response, view = OccupationView.as_view()(build_request('/?_export=csv')) assert response.getvalue().decode('utf8') == expected_csv # smoke tests, hard to test this binary format for string containment response, view = OccupationView.as_view()(build_request('/?_export=xls')) data = response.content assert len(data) > len(expected_csv) response, view = OccupationView.as_view()(build_request('/?_export=xlsx')) data = response.content assert len(data) > len(expected_csv) def test_exporting_unicode_header(): unicode_header = 'hé' class Table(tables.Table): name = tables.Column(verbose_name=unicode_header) exporter = TableExport('csv', Table([])) response = exporter.response() assert response.getvalue().decode('utf8') == unicode_header + '\r\n' exporter = TableExport('xls', Table([])) # this would fail if the header contains unicode and string converstion is attempted. exporter.export() django-tables2-1.14.2/tests/test_config.py000066400000000000000000000047511317557360700204400ustar00rootroot00000000000000# coding: utf-8 import pytest from django.core.paginator import EmptyPage, PageNotAnInteger from fudge import Fake from django_tables2 import Column, RequestConfig, Table 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) def test_passing_request_to_constructor(): '''Table constructor should call RequestConfig if a request is passed.''' request = build_request('/?page=1&sort=abc') class SimpleTable(Table): abc = Column() table = SimpleTable([ {'abc': 'bar'}, {'abc': 'rab'} ], request=request) assert table.columns['abc'].is_ordered django-tables2-1.14.2/tests/test_core.py000066400000000000000000000427211317557360700201220ustar00rootroot00000000000000# coding: utf-8 '''Test the core table functionality.''' from __future__ import absolute_import, unicode_literals import copy import itertools import pytest from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator import django_tables2 as tables from django_tables2.tables import DeclarativeColumnsMetaclass from .utils import build_request, parse request = build_request('/') MEMORY_DATA = [ {'i': 2, 'alpha': 'b', 'beta': 'b'}, {'i': 1, 'alpha': 'a', 'beta': 'c'}, {'i': 3, 'alpha': 'c', 'beta': 'a'}, ] class UnorderedTable(tables.Table): i = tables.Column() alpha = tables.Column() beta = tables.Column() class OrderedTable(UnorderedTable): class Meta: order_by = 'alpha' def test_omitting_data(): with pytest.raises(TypeError): UnorderedTable() def test_column_named_items(): ''' A column named items must not make the table fail https://github.com/bradleyayers/django-tables2/issues/316 ''' class ItemsTable(tables.Table): items = tables.Column() table = ItemsTable([{'items': 123}, {'items': 2345}]) html = table.as_html(request) assert '123' in html assert '2345' in html 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 # overwrite a column with a non-column class MayorlessCityTable(CityTable): mayor = None assert len(MayorlessCityTable.base_columns) == 3 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.as_html() assert 'id="test_table_1"' == TestTable([]).attrs.as_html() def test_attrs_from_settings(settings): settings.DJANGO_TABLES2_TABLE_ATTRS = { 'class': 'table-compact' } class Table(tables.Table): column = tables.Column() table = Table({}) assert table.attrs == {'class': 'table-compact'} 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.django_db # def test_should_support_haystack_data_source(): # Person.objects.create(first_name='Foo', last_name='Bar') # Person.objects.create(first_name='Brad', last_name='Pitt') # # from haystack.query import SearchQuerySet # from haystack.management.commands import update_index # # update_index.Command().handle(interactive=False) # # class PersonTable(tables.Table): # first_name = tables.Column() # # table = PersonTable(SearchQuerySet().all()) # html = table.as_html(request) # # # TODO: assert that a person is actually in the produced html. # assert 'Brad' in html # 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) assert table.rows[0].get_cell('col1') is True assert table.rows[0].get_cell('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 = UnorderedTable([], exclude=('i')) assert table.columns.names() == ['alpha', 'beta'] # Table.Meta: exclude=... class PartialTable(UnorderedTable): class Meta: exclude = ('alpha', ) table = PartialTable([]) assert table.columns.names() == ['i', 'beta'] # Inheritence -- exclude in parent, add in child class AddonTable(PartialTable): added = tables.Column() table = AddonTable([]) assert table.columns.names() == ['i', 'beta', 'added'] # Inheritence -- exclude in child class ExcludeTable(UnorderedTable): added = tables.Column() class Meta: exclude = ('beta', ) table = ExcludeTable([]) assert table.columns.names() == ['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 table.columns.names() == ['a'] table.exclude = ('a', ) assert table.columns.names() == ['b'] def test_exclude_should_work_on_sequence_too(): ''' It should be possible to define a sequence on a table and exclude it in a child of that table. ''' class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() occupation = tables.Column() class Meta: sequence = ('first_name', 'last_name', 'occupation') class AnotherPersonTable(PersonTable): class Meta(PersonTable.Meta): exclude = ('first_name', 'last_name') tableA = PersonTable([]) assert tableA.columns.names() == ['first_name', 'last_name', 'occupation'] tableB = AnotherPersonTable([]) assert tableB.columns.names() == ['occupation'] tableC = PersonTable([], exclude=('first_name')) assert tableC.columns.names() == ['last_name', 'occupation'] 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' table = TableA([]) html = table.as_html(build_request('/')) assert 'x' == table.prefix assert 'xsort=name' in html class TableB(tables.Table): last_name = tables.Column() assert '' == TableB([]).prefix assert 'x' == TableB([], prefix='x').prefix table = TableB([]) table.prefix = 'x-' html = table.as_html(build_request('/')) assert 'x-' == table.prefix assert 'x-sort=last_name' in html 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].get_cell('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].get_cell('name') == 'abcd' def test_table_defaults_are_honored(): class Table(tables.Table): name = tables.Column() table = Table([{}], default='abcd') assert table.rows[0].get_cell('name') == 'abcd' table = Table([{}], default='abcd') table.default = 'efgh' assert table.rows[0].get_cell('name') == 'efgh' AS_VALUES_DATA = [ {'name': 'Adrian', 'country': 'Australia'}, {'name': 'Adrian', 'country': 'Brazil'}, {'name': 'Audrey', 'country': 'Chile'}, {'name': 'Bassie', 'country': 'Belgium'}, ] def test_as_values(): class Table(tables.Table): name = tables.Column() country = tables.Column() expected = [['Name', 'Country']] + [[r['name'], r['country']] for r in AS_VALUES_DATA] table = Table(AS_VALUES_DATA) assert list(table.as_values()) == expected def test_as_values_exclude(): class Table(tables.Table): name = tables.Column() country = tables.Column() expected = [['Name']] + [[r['name']] for r in AS_VALUES_DATA] table = Table(AS_VALUES_DATA) assert list(table.as_values(exclude_columns=('country', ))) == expected def test_as_values_exclude_from_export(): class Table(tables.Table): name = tables.Column() buttons = tables.Column(exclude_from_export=True) assert list(Table([]).as_values()) == [['Name'], ] def test_as_values_empty_values(): ''' Table's as_values() method returns `None` for missing values ''' class Table(tables.Table): name = tables.Column() country = tables.Column() data = [ {'name': 'Adrian', 'country': 'Brazil'}, {'name': 'Audrey'}, {'name': 'Bassie', 'country': 'Belgium'}, {'country': 'France'}, ] expected = [['Name', 'Country']] + [[r.get('name'), r.get('country')] for r in data] table = Table(data) assert list(table.as_values()) == expected def test_as_values_render_FOO(): class Table(tables.Table): name = tables.Column() country = tables.Column() def render_country(self, value): return value + ' test' expected = [['Name', 'Country']] + [[r['name'], r['country'] + ' test'] for r in AS_VALUES_DATA] assert list(Table(AS_VALUES_DATA).as_values()) == expected def test_as_values_value_FOO(): class Table(tables.Table): name = tables.Column() country = tables.Column() def render_country(self, value): return value + ' test' def value_country(self, value): return value + ' different' expected = [['Name', 'Country']] + [[r['name'], r['country'] + ' different'] for r in AS_VALUES_DATA] assert list(Table(AS_VALUES_DATA).as_values()) == expected def test_row_attrs(): class Table(tables.Table): alpha = tables.Column() beta = tables.Column() table = Table(MEMORY_DATA, row_attrs={ 'class': lambda record: 'row-id-{}'.format(record['i']), }) assert table.rows[0].attrs == {'class': 'row-id-2 even'} def test_row_attrs_in_meta(): class Table(tables.Table): alpha = tables.Column() beta = tables.Column() class Meta: row_attrs = { 'class': lambda record: 'row-id-{}'.format(record['i']), } table = Table(MEMORY_DATA) assert table.rows[0].attrs == {'class': 'row-id-2 even'} def test_td_attrs_from_table(): class Table(tables.Table): alpha = tables.Column() beta = tables.Column() class Meta: attrs = { 'td': { 'data-column-name': lambda column: column.name } } table = Table(MEMORY_DATA) html = table.as_html(request) td = parse(html).find('.//tbody/tr[1]/td[1]') assert td.attrib == { 'data-column-name': 'alpha', 'class': 'alpha' } django-tables2-1.14.2/tests/test_data.py000066400000000000000000000016731317557360700201040ustar00rootroot00000000000000 from django_tables2.data import TableListData def generator(max_value): for i in range(max_value): yield { 'foo': i, 'bar': chr(i), 'baz': hex(i), 'inv': max_value - i } def test_TableListData_basic_list(): list_data = list(generator(100)) data = TableListData(list_data, object()) assert len(list_data) == len(data) assert data.verbose_name == 'item' assert data.verbose_name_plural == 'items' def test_TableListData_with_verbose_name(): ''' TableListData uses the attributes on the listlike object to generate it's verbose_name. ''' class listlike(list): verbose_name = 'unit' verbose_name_plural = 'units' list_data = listlike(generator(100)) data = TableListData(list_data, object()) assert len(list_data) == len(data) assert data.verbose_name == 'unit' assert data.verbose_name_plural == 'units' django-tables2-1.14.2/tests/test_dynamically_add_show_hide_columns.py000066400000000000000000000103511317557360700260730ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals import pytest from django.contrib.auth import get_user_model from django.template import Context, Template from django.utils.translation import ugettext_lazy as _ import django_tables2 as tables from .app.models import Person from .utils import build_request, parse User = get_user_model() data = [ {'name': 'Adrian', 'country': 'Australia'}, {'name': 'Roy', 'country': 'Brazil'}, {'name': 'Audrey', 'country': 'Chile'}, {'name': 'Bassie', 'country': 'Belgium'}, ] def test_dynamically_adding_columns(): ''' When adding columns to self.base_columns, they were actually added to the class attribute `Table.base_columns`, and not to the instance attribute, `table.base_columns` issue #403 ''' class MyTable(tables.Table): name = tables.Column() # this is obvious: assert list(MyTable(data).columns.columns.keys()) == ['name'] assert list(MyTable(data, extra_columns=[ ('country', tables.Column()) ]).columns.columns.keys()) == ['name', 'country'] # this new instance should not have the extra columns added to the first instance. assert list(MyTable(data).columns.columns.keys()) == ['name'] def test_sorting_on_dynamically_added_columns(): class MyTable(tables.Table): name = tables.Column() table = MyTable(data, order_by='-country', extra_columns=[ ('country', tables.Column(verbose_name=_('country'))) ]) root = parse(table.as_html(build_request())) assert root.find('.//tbody/tr/td[2]').text == 'Chile' assert root.find('.//tbody/tr[4]/td[2]').text == 'Australia' @pytest.mark.django_db def test_dynamically_override_auto_generated_columns(): for name, country in data: Person.objects.create(first_name=name, last_name=country) queryset = Person.objects.all() class MyTable(tables.Table): class Meta: model = Person fields = ('first_name', 'last_name') assert list(MyTable(queryset).columns.columns.keys()) == ['first_name', 'last_name'] table = MyTable(queryset, extra_columns=[ ('first_name', tables.Column(attrs={'td': {'style': 'color: red;'}})) ]) # we still should have two columns assert list(table.columns.columns.keys()) == ['first_name', 'last_name'] # the attrs should be applied to the `first_name` column assert table.columns['first_name'].attrs['td'] == {'class': 'first_name', 'style': 'color: red;'} def test_dynamically_add_column_with_sequence(): class MyTable(tables.Table): name = tables.Column() class Meta: sequence = ('...', 'name') assert list(MyTable(data, extra_columns=[ ('country', tables.Column()) ]).columns.columns.keys()) == ['country', 'name'] # override sequence with an argument. assert list(MyTable( data, extra_columns=[('country', tables.Column())], sequence=('name', '...') ).columns.columns.keys()) == ['name', 'country'] @pytest.mark.django_db def test_dynamically_hide_columns(): class MyTable(tables.Table): name = tables.Column(orderable=False) country = tables.Column(orderable=False) def before_render(self, request): if request.user.username == 'Bob': self.columns.hide('country') else: self.columns.show('country') template = Template('{% load django_tables2 %}{% render_table table %}') table = MyTable(data) request = build_request(user=User.objects.create(username='Bob')) html = table.as_html(request) assert '' in html assert '' not in html html = template.render(Context({'request': request, 'table': table})) assert '' in html assert '' not in html request = build_request(user=User.objects.create(username='Alice')) html = table.as_html(request) assert '' in html assert '' in html html = template.render(Context({'request': request, 'table': table})) assert '' in html assert '' in html django-tables2-1.14.2/tests/test_faq.py000066400000000000000000000022551317557360700177370ustar00rootroot00000000000000import itertools import django_tables2 as tables from .utils import build_request TEST_DATA = [ {'name': 'Belgium', 'population': 11200000}, {'name': 'Luxembourgh', 'population': 540000}, {'name': 'France', 'population': 66000000}, ] def _test_counter(Table, expected=''): table = Table(TEST_DATA) html = table.as_html(build_request()) assert expected in html # the counter should start at zero the second time too table = Table(TEST_DATA) html = table.as_html(build_request()) assert expected in html return html def test_row_counter_using_templateColumn(): class CountryTable(tables.Table): counter = tables.TemplateColumn('{{ row_counter }}') name = tables.Column() _test_counter(CountryTable) def test_row_footer_total(): class CountryTable(tables.Table): name = tables.Column() population = tables.Column( footer=lambda table: 'Total: {}'.format( sum(x['population'] for x in table.data) ) ) table = CountryTable(TEST_DATA) html = table.as_html(build_request()) assert '' in html django-tables2-1.14.2/tests/test_footer.py000066400000000000000000000036511317557360700204670ustar00rootroot00000000000000# coding: utf-8 import django_tables2 as tables from .utils import build_request MEMORY_DATA = [ {'name': 'Queensland', 'country': 'Australia', 'population': 4750500}, {'name': 'New South Wales', 'country': 'Australia', 'population': 7565500}, {'name': 'Victoria', 'country': 'Australia', 'population': 6000000}, {'name': 'Tasmania', 'country': 'Australia', 'population': 517000} ] def test_has_footer_is_False_without_footer(): class Table(tables.Table): name = tables.Column() country = tables.Column() population = tables.Column() table = Table(MEMORY_DATA) assert table.has_footer() is False def test_footer(): class Table(tables.Table): name = tables.Column() country = tables.Column(footer='Total:') population = tables.Column( footer=lambda table: sum(x['population'] for x in table.data) ) table = Table(MEMORY_DATA) assert table.has_footer() is True html = table.as_html(build_request('/')) assert '' in html assert '' in html def test_footer_disable_on_table(): ''' Showing the footer can be disabled using show_footer argument to the Table constructor ''' class Table(tables.Table): name = tables.Column() country = tables.Column(footer='Total:') table = Table(MEMORY_DATA, show_footer=False) assert table.has_footer() is False def test_footer_column_method(): class SummingColumn(tables.Column): def render_footer(self, bound_column, table): return sum(bound_column.accessor.resolve(row) for row in table.data) class Table(tables.Table): name = tables.Column() country = tables.Column(footer='Total:') population = SummingColumn() table = Table(MEMORY_DATA) html = table.as_html(build_request('/')) assert '' in html assert '' in html django-tables2-1.14.2/tests/test_models.py000066400000000000000000000351461317557360700204600ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import pytest from django.db.models.functions import Length from django.utils import six from django.utils.translation import override as translation_override import django_tables2 as tables from .app.models import Occupation, Person, PersonProxy from .utils import assertNumQueries, 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 expected = ['id', 'name', 'region', 'boolean', 'boolean_with_choices'] assert expected == list(OccupationTable.base_columns.keys()) class OccupationTable2(tables.Table): extra = tables.Column() class Meta: model = Occupation expected.append('extra') assert expected == 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', on_delete=models.CASCADE) 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 expected = ['extra', 'id', 'name', 'region', 'boolean', 'boolean_with_choices', 'extra2'] assert expected == 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') fn4 = 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 # titlised 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 assert 'override' == table.columns['fn4'].verbose_name # Should use the titlised 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): first_name = tables.Column(verbose_name='OVERRIDE') class Meta: model = Person # Issue #16 table = PersonTable(Person.objects.all()) assert 'Translation Test' == table.columns['trans_test'].verbose_name assert 'Translation Test Lazy' == table.columns['trans_test_lazy'].verbose_name assert 'Web Site' == table.columns['website'].verbose_name assert 'Birthdate' == table.columns['birthdate'].verbose_name assert 'OVERRIDE' == table.columns['first_name'].verbose_name # Verbose name should be lazy if it comes from the model field and # the column was not declared explicitly class PersonTable(tables.Table): class Meta: model = Person table = PersonTable(Person.objects.all()) assert type(table.columns['trans_test_lazy'].verbose_name) is not six.text_type with translation_override('ua'): assert 'Тест Ленивого Перекладу' == 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].get_cell('language') assert 'Russian' == table.rows[1].get_cell('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(): ''' If orderable=False, do not sort queryset. https://github.com/bradleyayers/django-tables2/issues/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_meta_fields_may_be_list(): 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].get_cell('first_name') == 'Bradley' table.order_by = '-first_name' assert table.rows[0].get_cell('first_name') == 'Stevie' def test_queryset_table_data_supports_custom_ordering(): class Table(tables.Table): class Meta: model = Person order_by = 'first_name' def order_first_name(self, queryset, is_descending): # annotate to order by length of first_name + last_name queryset = queryset.annotate( length=Length('first_name') + Length('last_name') ).order_by(('-' if is_descending else '') + 'length') return (queryset, True) for name in ('Bradley Ayers', 'Stevie Armstrong', 'VeryLongFirstName VeryLongLastName'): first_name, last_name = name.split() Person.objects.create(first_name=first_name, last_name=last_name) table = Table(Person.objects.all()) # Shortest full names first assert table.rows[0].get_cell('first_name') == 'Bradley' # Longest full names first table.order_by = '-first_name' assert table.rows[0].get_cell('first_name') == 'VeryLongFirstName' 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].get_cell('first_name') == 'Brad' assert table.rows[0].get_cell('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].get_cell('first_name') == 'Brad' def test_foreign_key(): class PersonTable(tables.Table): class Meta: model = Person fields = ('foreign_key', ) # TODO: implement def test_fields_empty_list_means_no_fields(): class Table(tables.Table): class Meta: model = Person fields = () table = Table(Person.objects.all()) assert len(table.columns.names()) == 0 def test_column_named_delete(): class DeleteTable(tables.Table): delete = tables.TemplateColumn('[delete button]', verbose_name='') class Meta: model = Person fields = ('name', 'delete') person1 = Person.objects.create(first_name='Jan', last_name='Pieter') person2 = Person.objects.create(first_name='John', last_name='Peter') DeleteTable(Person.objects.all()).as_html(build_request()) assert Person.objects.get(pk=person1.pk) == person1 assert Person.objects.get(pk=person2.pk) == person2 def test_single_query_for_non_paginated_table(): ''' A non-paginated table should not generate a query for each row, but only one query fetch the rows. ''' for i in range(10): Person.objects.create(first_name='Bob %d' % i, last_name='Builder') class PersonTable(tables.Table): class Meta: model = Person fields = ('first_name', 'last_name') order_by = ('last_name', 'first_name') table = PersonTable(Person.objects.all()) with assertNumQueries(1): list(table.as_values()) django-tables2-1.14.2/tests/test_ordering.py000066400000000000000000000227701317557360700210050ustar00rootroot00000000000000# coding: utf-8 from __future__ import absolute_import, unicode_literals import pytest from django.utils import six import django_tables2 as tables from django_tables2.tables import RequestConfig from .app.models import Person from .utils import build_request request = build_request('/') MEMORY_DATA = [ {'i': 2, 'alpha': 'b', 'beta': 'b'}, {'i': 1, 'alpha': 'a', 'beta': 'c'}, {'i': 3, 'alpha': 'c', 'beta': 'a'}, ] class UnorderedTable(tables.Table): i = tables.Column() alpha = tables.Column() beta = tables.Column() class OrderedTable(UnorderedTable): class Meta: order_by = 'alpha' 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].get_cell('i') table = OrderedTable(MEMORY_DATA, order_by='-beta') assert 1 == table.rows[0].get_cell('i') # allow fallback to Table.Meta.order_by table = OrderedTable(MEMORY_DATA) assert 1 == table.rows[0].get_cell('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', ) 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].get_cell('alpha') table = OrderedTable(data, order_by='i') if six.PY3: assert {} == table.rows[0].get_cell('i') else: assert 1 == table.rows[0].get_cell('i') table = OrderedTable(data, order_by='beta') assert [] == table.rows[0].get_cell('beta') 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] def test_multi_column_ordering_by_table(): 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] def test_multi_column_ordering_by_column(): # 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] @pytest.mark.django_db def test_ordering_by_custom_field(): ''' When defining a custom field in a table, as name=tables.Column() with methods to render and order render_name and order_name, sorting by this column causes an error if the custom field is not in last position. (issue #413) ''' Person.objects.create(first_name='Alice', last_name='Beta') Person.objects.create(first_name='Bob', last_name='Alpha') from django.db.models import F, Value from django.db.models.functions import Concat class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() full_name = tables.Column() def render_full_name(self, record): return record.last_name + ' ' + record.first_name def order_full_name(self, queryset, is_descending): queryset = queryset.annotate( full_name=Concat(F('last_name'), Value(' '), F('first_name')) ).order_by(('-' if is_descending else '') + 'full_name') return queryset, True class Meta: model = Person fields = ('first_name', 'last_name', 'full_name') table = PersonTable(Person.objects.all()) request = build_request('/?sort=full_name&sort=first_name') RequestConfig(request).configure(table) assert table.rows[0].record.first_name == 'Bob' 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].get_cell('name') == 'Bradley' table.order_by = '-name' assert table.rows[0].get_cell('name') == 'Davina' def test_ordering_non_database_data(): class Table(tables.Table): name = tables.Column() country = tables.Column() data = [ {'name': 'Adrian', 'country': 'Australia'}, {'name': 'Adrian', 'country': 'Brazil'}, {'name': 'Audrey', 'country': 'Chile'}, {'name': 'Bassie', 'country': 'Belgium'}, ] table = Table(data, order_by=('-name', '-country')) assert table.rows[0].get_cell('name') == 'Bassie' assert table.rows[1].get_cell('name') == 'Audrey' assert table.rows[2].get_cell('name') == 'Adrian' assert table.rows[2].get_cell('country') == 'Brazil' assert table.rows[3].get_cell('name') == 'Adrian' assert table.rows[3].get_cell('country') == 'Australia' def test_table_ordering_attributes(): class Table(tables.Table): alpha = tables.Column() beta = tables.Column() table = Table(MEMORY_DATA, attrs={ 'th': { 'class': 'custom-header-class', '_ordering': { 'orderable': 'sortable', 'ascending': 'ascend', 'descending': 'descend', }, }, }, order_by='alpha') assert 'sortable' in table.columns[0].attrs['th']['class'] assert 'ascend' in table.columns[0].attrs['th']['class'] assert 'custom-header-class' in table.columns[1].attrs['th']['class'] def test_table_ordering_attributes_in_meta(): class Table(tables.Table): alpha = tables.Column() beta = tables.Column() class Meta(OrderedTable.Meta): attrs = { 'th': { 'class': 'custom-header-class-in-meta', '_ordering': { 'orderable': 'sortable', 'ascending': 'ascend', 'descending': 'descend', }, } } table = Table(MEMORY_DATA) assert 'sortable' in table.columns[0].attrs['th']['class'] assert 'ascend' in table.columns[0].attrs['th']['class'] assert 'custom-header-class-in-meta' in table.columns[1].attrs['th']['class'] def test_column_ordering_attributes(): class Table(tables.Table): alpha = tables.Column(attrs={ 'th': { 'class': 'custom-header-class', '_ordering': { 'orderable': 'sort', 'ascending': 'ascending' } } }) beta = tables.Column(attrs={ 'th': { '_ordering': { 'orderable': 'canOrder', } }, 'td': { 'class': 'cell-2' } }) table = Table(MEMORY_DATA, attrs={'class': 'only-on-table'}, order_by='alpha') assert 'only-on-table' not in table.columns[0].attrs['th']['class'] assert 'custom-header-class' in table.columns[0].attrs['th']['class'] assert 'ascending' in table.columns[0].attrs['th']['class'] assert 'sort' in table.columns[0].attrs['th']['class'] assert 'canOrder' in table.columns[1].attrs['th']['class'] django-tables2-1.14.2/tests/test_pinned_rows.py000066400000000000000000000117401317557360700215160ustar00rootroot00000000000000# encoding: utf-8 import pytest from django.db import models import django_tables2 as tables from django_tables2.rows import BoundRow, BoundRows from .utils import build_request, parse class PinnedObj(object): def __init__(self, name, age): self.name = name self.age = age class SimpleTable(tables.Table): name = tables.Column() occupation = tables.Column() age = tables.Column() def get_top_pinned_data(self): return [ PinnedObj("Ron", 90), PinnedObj("Jon", 10), ] def get_bottom_pinned_data(self): return [{'occupation': 'Sum age', 'age': 130}] def test_bound_rows_with_pinned_data(): record = {'name': 'Grzegorz', 'age': 30, 'occupation': 'programmer'} table = SimpleTable([record]) row = table.rows[0] with pytest.raises(IndexError): table.rows[1] with pytest.raises(IndexError): row.get_cell(3) assert row.get_cell('name') == record['name'] assert row.get_cell('occupation') == record['occupation'] assert row.get_cell('age') == record['age'] with pytest.raises(KeyError): row.get_cell('gamma') assert 'name' in row assert 'occupation' in row assert 'gamma' not in row def test_as_html(): ''' Ensure that html render correctly. ''' request = build_request('/') table = SimpleTable([{'name': 'Grzegorz', 'age': 30, 'occupation': 'programmer'}]) root = parse(table.as_html(request)) # One row for header assert len(root.findall('.//thead/tr')) == 1 # In the header should be 3 cell. assert len(root.findall('.//thead/tr/th')) == 3 # In the body, should be one original record and 3 pinned rows. assert len(root.findall('.//tbody/tr')) == 4 assert len(root.findall('.//tbody/tr/td')) == 12 # First top pinned row. tr = root.findall('.//tbody/tr') td = tr[0].findall('td') assert td[0].text == "Ron" assert td[1].text == table.default assert td[2].text == "90" # Second top pinned row. td = tr[1].findall('td') assert td[0].text == "Jon" assert td[1].text == table.default assert td[2].text == '10' # Original row td = tr[2].findall('td') assert td[0].text == "Grzegorz" assert td[1].text == 'programmer' assert td[2].text == '30' # First bottom pinned row. td = tr[3].findall('td') assert td[0].text == table.default assert td[1].text == 'Sum age' assert td[2].text == '130' def test_pinned_row_attrs(): ''' Testing attrs for pinned rows. ''' pinned_row_attrs = { 'class': 'super-mega-row', 'data-foo': 'bar' } request = build_request('/') record = {'name': 'Grzegorz', 'age': 30, 'occupation': 'programmer'} table = SimpleTable([record], pinned_row_attrs=pinned_row_attrs) html = table.as_html(request) assert 'pinned-row' in html assert 'super-mega-row' in html assert 'data-foo' in html def test_ordering(): ''' Change sorting should not change ordering pinned rows. ''' request = build_request('/') records = [ {'name': 'Alex', 'age': 42, 'occupation': 'programmer'}, {'name': 'Greg', 'age': 30, 'occupation': 'programmer'}, ] table = SimpleTable(records, order_by='age') root = parse(table.as_html(request)) tr = root.findall('.//tbody/tr') assert tr[0].findall('td')[2].text == '90' assert tr[1].findall('td')[2].text == '10' assert tr[2].findall('td')[2].text == '30' assert tr[3].findall('td')[2].text == '42' assert tr[4].findall('td')[2].text == '130' table = SimpleTable(records, order_by='-age') root = parse(table.as_html(request)) tr = root.findall('.//tbody/tr') assert tr[0].findall('td')[2].text == '90' assert tr[1].findall('td')[2].text == '10' assert tr[2].findall('td')[2].text == '42' assert tr[3].findall('td')[2].text == '30' assert tr[4].findall('td')[2].text == '130' def test_bound_rows_getitem(): ''' Testing BoundRows.__getitem__() method. Checking the return class for simple value and for slice. Ensure that inside of BoundRows pinned rows are included in length. ''' records = [ {'name': 'Greg', 'age': 30, 'occupation': 'policeman'}, {'name': 'Alex', 'age': 42, 'occupation': 'programmer'}, {'name': 'John', 'age': 72, 'occupation': 'official'}, ] table = SimpleTable(records, order_by='age') assert isinstance(table.rows[0], BoundRow) is True assert isinstance(table.rows[0:2], BoundRows) is True assert table.rows[0:2][0].get_cell('name') == 'Greg' assert len(table.rows[:]) == 6 def test_uniterable_pinned_data(): ''' Ensure that, when data for pinned rows are not iterable, the ValueError exception will be raised. ''' class FooTable(tables.Table): col = tables.Column() def get_top_pinned_data(self): return 1 tab = FooTable([1, 2, 3]) with pytest.raises(ValueError): for row in tab.rows: pass django-tables2-1.14.2/tests/test_rows.py000066400000000000000000000076101317557360700201620ustar00rootroot00000000000000# coding: utf-8 import pytest from django.db import models 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.get_cell(0) == record['name'] assert row.get_cell(1) == record['occupation'] assert row.get_cell(2) == record['age'] with pytest.raises(IndexError): row.get_cell(3) # column name indexing into a row assert row.get_cell('name') == record['name'] assert row.get_cell('occupation') == record['occupation'] assert row.get_cell('age') == record['age'] with pytest.raises(KeyError): row.get_cell('gamma') # row should support contains check assert 'name' in row assert 'occupation' in row assert 'gamma' not in row def test_row_attrs(): ''' If a callable returns an empty string, do not add a space to the CSS class attribute. (#416) ''' from itertools import count counter = count() class Table(tables.Table): name = tables.Column() class Meta(object): row_attrs = { 'class': lambda: '' if next(counter) % 2 == 0 else 'bla' } table = Table([ {'name': 'Brian'}, {'name': 'Thomas'}, {'name': 'John'} ]) assert table.rows[0].attrs['class'] == 'even' assert table.rows[1].attrs['class'] == 'bla odd' assert table.rows[1].attrs['class'] == 'even' def test_get_cell_display(): class A(models.Model): foo = models.CharField( max_length=1, choices=( ('a', 'valA'), ('b', 'valB'), ) ) class Meta: app_label = 'django_tables2_test' class B(models.Model): a = models.ForeignKey(A, on_delete=models.CASCADE) class Meta: app_label = 'django_tables2_test' class C(models.Model): b = models.ForeignKey(B, on_delete=models.CASCADE) class Meta: app_label = 'django_tables2_test' class Tab(tables.Table): a = tables.Column(accessor="b.a.foo") class Meta: model = C a = A(foo='a') b = B(a=a) c = C(b=b) tab = Tab([c]) row = tab.rows[0] assert row.get_cell('a') == 'valA' def test_even_odd_css_class(): ''' Test for BoundRow.get_even_odd_css_class() method ''' class SimpleTable(tables.Table): foo = tables.Column() def get_top_pinned_data(self): return [{'foo': 'top-pinned'}] def get_bottom_pinned_data(self): return [{'foo': 'bottom-pinned'}] data = [ {'foo', 'bar'}, {'foo', 'bas'}, {'foo', 'baz'}, ] simple_table = SimpleTable(data) count = 0 prev = None for row in simple_table.rows: if prev: assert row.get_even_odd_css_class() != prev.get_even_odd_css_class() prev = row count += 1 # count should be 5 because: # First row is a top pinned row. # Three defaults rows with data. # Last row is a bottom pinned row. assert count == 5 # Important! # Length of data is five because pinned rows are added to data list. # If pinned rows are added only in the iteration on BoundRows, # then nothing will display if there are *only* pinned rows assert len(simple_table.rows) == 5 django-tables2-1.14.2/tests/test_tabledata.py000066400000000000000000000054071317557360700211130ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import pytest from django_tables2.data import TableData, TableListData, TableQuerysetData from .app.models import Person def test_TableData_factory_invalid_data_None(): with pytest.raises(ValueError): TableData.from_data(None, table={}) def test_TableData_factory_invalid_data_int(): with pytest.raises(ValueError): TableData.from_data(1, table={}) def test_TableData_factory_invalid_data_classes(): class Klass(object): pass with pytest.raises(ValueError): TableData.from_data(Klass(), table={}) class Bad(object): def __len__(self): pass with pytest.raises(ValueError): TableData.from_data(Bad(), table={}) @pytest.mark.django_db def test_TableData_factory_valid_QuerySet(): data = TableData.from_data(Person.objects.all(), table={}) assert isinstance(data, TableQuerysetData) def test_TableData_factory_valid_list_of_dicts(): data = TableData.from_data([{'name': 'John'}, {'name': 'Pete'}], table={}) assert isinstance(data, TableListData) assert len(data) == 2 def test_TableData_factory_valid_tuple_of_dicts(): data = TableData.from_data(({'name': 'John'}, {'name': 'Pete'}), table={}) assert isinstance(data, TableListData) assert len(data) == 2 def test_TableData_factory_valid_class(): class Datasource(object): def __len__(self): return 1 def __getitem__(self, pos): if pos != 0: raise IndexError() return {'a': 1} data = TableData.from_data(Datasource(), table={}) assert len(data) == 1 def test_tabledata_knows_its_default_name(): data = TableData.from_data([{}], table={}) assert data.verbose_name == 'item' assert data.verbose_name_plural == 'items' def test_tabledata_knows_its_name(): data = TableData.from_data(Person.objects.all(), table={}) assert data.verbose_name == 'person' assert data.verbose_name_plural == 'people' # def test_tabledata_is_untouched(): # ''' # Ensure that data that is provided to the table (the datasource) is not # modified by table operations. # ''' # # MEMORY_DATA = [ # {'i': 2, 'alpha': 'b', 'beta': 'b'}, # {'i': 1, 'alpha': 'a', 'beta': 'c'}, # {'i': 3, 'alpha': 'c', 'beta': 'a'}, # ] # # class Table(tables.Table): # i = tables.Column() # alpha = tables.Column() # beta = tables.Column() # # original_data = deepcopy(MEMORY_DATA) # # table = Table(MEMORY_DATA) # table.order_by = 'i' # list(table.rows) # assert MEMORY_DATA == original_data # # table = Table(MEMORY_DATA) # table.order_by = 'beta' # list(table.rows) # assert MEMORY_DATA == original_data django-tables2-1.14.2/tests/test_templates.py000066400000000000000000000301671317557360700211710ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import pytest from django.template import Context, Template from django.test import TransactionTestCase from django.utils.translation import override as translation_override from django.utils.translation import ugettext_lazy import django_tables2 as tables from django_tables2.config import RequestConfig from .app.models import Person from .utils import build_request, parse def test_template_override_in_settings(settings): settings.DJANGO_TABLES2_TEMPLATE = 'foo/bar.html' class Table(tables.Table): column = tables.Column() table = Table({}) assert table.template == 'foo/bar.html' 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(): request = build_request('/') 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').get('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) @pytest.mark.django_db class TestQueries(TransactionTestCase): def test_as_html_db_queries(self): class PersonTable(tables.Table): class Meta: model = Person request = build_request('/') 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 request = build_request('/') 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}))) 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_results = { None: '1234.5', False: '1234.5', True: '1 234,5' # non-breaking space } request = build_request('/') # no localization html = get_cond_localized_table(None)(simple_test_data).as_html(request) assert ''.format(expected_results[None]) in html # unlocalize html = get_cond_localized_table(False)(simple_test_data).as_html(request) assert ''.format(expected_results[False]) in html settings.USE_L10N = True settings.USE_THOUSAND_SEPARATOR = True with translation_override('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_results[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_results[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_results[True]) in html 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_results = { None: '1234.5', False: '1234.5', True: '1{0}234,5'.format(' ') # non-breaking space } request = build_request('/') # No localize html = TableNoLocalize(simple_test_data).as_html(request) assert ''.format(expected_results[None]) in html settings.USE_L10N = True settings.USE_THOUSAND_SEPARATOR = True with translation_override('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_results[True]) in html # localize html = TableLocalize(simple_test_data).as_html(request) assert ''.format(expected_results[True]) in html # unlocalize html = TableUnlocalize(simple_test_data).as_html(request) assert ''.format(expected_results[False]) in html # test unlocalize higher precedence html = TableLocalizePrecedence(simple_test_data).as_html(request) assert ''.format(expected_results[False]) in html def test_localization_of_pagination_string(): class Table(tables.Table): foo = tables.Column(verbose_name='my column') bar = tables.Column() class Meta: default = '---' table = Table(map(lambda x: [x, x + 100], range(40))) request = build_request('/') RequestConfig(request, paginate={'per_page': 10}).configure(table) with translation_override('en'): assert 'Page 1 of 4' in table.as_html(request) with translation_override('nl'): assert 'Pagina 1 van 4' in table.as_html(request) with translation_override('it'): assert 'Pagina 1 di 4' in table.as_html(request) with translation_override('nb'): assert 'Side 1 av 4' in table.as_html(request) class BootstrapTable(CountryTable): class Meta: template = 'django_tables2/bootstrap.html' prefix = 'bootstrap-' per_page = 2 def test_boostrap_template(): table = BootstrapTable(MEMORY_DATA) request = build_request('/') 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')) == 2 assert len(root.findall('.//tbody/tr/td')) == 8 assert root.find('./ul[@class="pager list-inline"]/li[@class="cardinality"]/small').text.strip() == 'Page 1 of 2' # make sure the link is prefixed assert root.find('./ul[@class="pager list-inline"]/li[@class="next"]/a').get('href') == '?bootstrap-page=2' class SemanticTable(CountryTable): class Meta: template = 'django_tables2/semantic.html' prefix = 'semantic-' per_page = 2 def test_semantic_template(): table = SemanticTable(MEMORY_DATA) request = build_request('/') 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')) == 2 assert len(root.findall('.//tbody/tr/td')) == 8 pager = './/tfoot/tr/th/div[@class="ui right floated pagination menu"]/div[@class="item"]' assert root.find(pager).text.strip() == 'Page 1 of 2' # make sure the link is prefixed next_page = './/tfoot/tr/th/div[@class="ui right floated pagination menu"]/a[@class="icon item"]' assert root.find(next_page).get('href') == '?semantic-page=2' def test_bootstrap_responsive_template(): class BootstrapResponsiveTable(BootstrapTable): class Meta(BootstrapTable.Meta): template = 'django_tables2/bootstrap-responsive.html' table = BootstrapResponsiveTable(MEMORY_DATA) request = build_request('/') 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')) == 2 assert len(root.findall('.//tbody/tr/td')) == 8 pager = './/ul/li[@class="cardinality"]/small' assert root.find(pager).text.strip() == 'Page 1 of 2' django-tables2-1.14.2/tests/test_templatetags.py000066400000000000000000000162431317557360700216640ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import pytest from django.core.exceptions import ImproperlyConfigured from django.template import Context, RequestContext, Template, TemplateSyntaxError from django.utils import six from django.utils.six.moves.urllib.parse import parse_qs import django_tables2 as tables from django_tables2.config import RequestConfig from .app.models import Person, Region from .test_templates import MEMORY_DATA, CountryTable from .utils import assertNumQueries, build_request, parse def test_render_table_templatetag_invalid_type(): template = Template('{% load django_tables2 %}{% render_table table %}') with pytest.raises(ValueError): template.render(Context({ 'request': build_request(), 'table': dict() })) 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 # 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').get('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" %}') context = RequestContext(build_request(), {'table': table}) assert template.render(context) == 'dummy template contents\n' def test_render_table_template_argument_list(): template = Template('{% load django_tables2 %}' '{% render_table table template_list %}') context = RequestContext(build_request(), { 'table': CountryTable(MEMORY_DATA, order_by=('name', 'population')), 'template_list': ('dummy.html', 'child/foo.html') }) 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 ? assert qs['name'] == ['Brad'] assert qs['age'] == ['21'] assert qs['a'] == ['b'] assert qs['c'] == ['5'] def test_querystring_templatetag_requires_request(): template = Template('{% load django_tables2 %}{% querystring "name"="Brad" %}') with pytest.raises(ImproperlyConfigured): template.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 ? 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 ? assert set(qs.keys()) == set(["c"]) def test_querystring_syntax_error(): with pytest.raises(TemplateSyntaxError): Template('{% load django_tables2 %}{% querystring foo= %}') 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', # Some cyrillic samples 'руда лисиця': 'Руда Лисиця', 'руда лисицЯ': 'Руда лисицЯ', 'діяльність СБУ': 'Діяльність СБУ', 'а.б.в': 'А.Б.В', 'вага 6кг': 'Вага 6кг', 'у 80-их роках': 'У 80-их Роках', } for raw, expected in expectations.items(): template = Template('{% load django_tables2 %}{{ x|title }}') assert template.render(Context({'x': raw})) == expected @pytest.mark.django_db def test_as_html_db_queries(transactional_db): class PersonTable(tables.Table): class Meta: model = Person Person.objects.create(first_name='John', last_name='Doo') with assertNumQueries(count=2): PersonTable(Person.objects.all()).as_html(build_request()) django-tables2-1.14.2/tests/test_utils.py000066400000000000000000000133401317557360700203250ustar00rootroot00000000000000# coding: utf-8 import pytest from django.db import models from django.utils import six from django_tables2.utils import (Accessor, AttributeDict, OrderBy, OrderByTuple, Sequence, call_with_appropriate, computed_values, segment, signature) 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 is a.is_ascending assert False is a.is_descending b = OrderBy('-b') assert '-b' == b assert 'b' == b.bare assert 'b' == b.opposite assert True is b.is_descending assert False is 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 class AccessorTestModel(models.Model): foo = models.CharField(max_length=20) class Meta: app_label = 'django_tables2_test' def test_accessor_can_return_field(): context = AccessorTestModel(foo='bar') assert type(Accessor('foo').get_field(context)) == models.CharField def test_accessor_returns_None_when_doesnt_exist(): context = AccessorTestModel(foo='bar') assert Accessor('bar').get_field(context) is None def test_accessor_returns_None_if_not_a_model(): context = {'bar': 234} assert Accessor('bar').get_field(context) is None def test_accessor_penultimate(): context = { 'a': { 'a': 1, 'b': { 'c': 2, 'd': 4 } } } assert Accessor('a.b.c').penultimate(context) == (context['a']['b'], 'c') assert Accessor('a.b.c.d.e').penultimate(context) == (None, 'e') def test_attribute_dict_handles_escaping(): x = AttributeDict({'x': '"\'x&'}) assert x.as_html() == 'x=""'x&"' def test_computed_values_supports_shallow_structures(): x = computed_values({'foo': lambda: 'bar'}) assert x == {'foo': 'bar'} def test_computed_values_supports_nested_structures(): x = computed_values({'foo': lambda: {'bar': lambda: 'baz'}}) assert x == {'foo': {'bar': 'baz'}} def test_computed_values_with_argument(): x = computed_values({ 'foo': lambda y: { 'bar': lambda y: 'baz-{}'.format(y) } }, kwargs=dict(y=2)) assert x == {'foo': {'bar': 'baz-2'}} 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'), } def test_sequence_multiple_ellipsis(): sequence = Sequence(['foo', '...', 'bar', '...']) with pytest.raises(ValueError): sequence.expand(['foo']) def test_signature(): def foo(bar, baz): pass args, keywords = signature(foo) assert args == ('bar', 'baz') assert keywords is None def test_signature_method(): class Foo(object): def foo(self): pass def bar(self, bar, baz): pass def baz(self, bar, *bla, **boo): pass obj = Foo() args, keywords = signature(obj.foo) assert args == () assert keywords is None args, keywords = signature(obj.bar) assert args == ('bar', 'baz') assert keywords is None args, keywords = signature(obj.baz) assert args == ('bar', ) assert keywords == 'boo' def test_signature_catch_all_kwargs(): def foo(bar, baz, **kwargs): pass args, keywords = signature(foo) assert args == ('bar', 'baz') assert keywords == 'kwargs' def test_call_with_appropriate(): def foo(): return 'bar' assert call_with_appropriate(foo, { 'a': 'd', 'c': 'e' }) == 'bar' def bar(baz): return baz assert call_with_appropriate(bar, dict(baz=23)) == 23 django-tables2-1.14.2/tests/test_views.py000066400000000000000000000240451317557360700203260ustar00rootroot00000000000000# coding: utf-8 import pytest from django.core.exceptions import ImproperlyConfigured from django.views.generic.base import TemplateView import django_tables2 as tables from .app.models import Person, Region from .utils import build_request MEMORY_DATA = [ {'name': 'Queensland'}, {'name': 'New South Wales'}, {'name': 'Victoria'}, {'name': 'Tasmania'} ] 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 class SimpleView(DispatchHookMixin, tables.SingleTableView): table_class = SimpleTable model = Region # required for ListView class SimplePaginatedView(DispatchHookMixin, tables.SingleTableView): table_class = SimpleTable table_pagination = {'per_page': 1} model = Region # required for ListView @pytest.mark.django_db def test_view_should_support_pagination_options(): for region in MEMORY_DATA: Region.objects.create(name=region['name']) response, view = SimplePaginatedView.as_view()(build_request('/')) assert view.get_table().paginator.num_pages == len(MEMORY_DATA) assert view.get_table().paginator.per_page == 1 @pytest.mark.django_db def test_view_should_support_default_pagination(): class PaginateDefault(DispatchHookMixin, tables.SingleTableView): table_class = SimpleTable model = Region table_data = MEMORY_DATA response, view = PaginateDefault.as_view()(build_request('/')) table = view.get_table() assert table.paginator.per_page == 25 assert len(table.page) == 4 @pytest.mark.django_db def test_view_should_support_default_pagination_with_table_options(): class Table(tables.Table): class Meta: model = Region per_page = 2 class PaginateByDefinedOnView(DispatchHookMixin, tables.SingleTableView): table_class = Table model = Region table_data = MEMORY_DATA response, view = PaginateByDefinedOnView.as_view()(build_request('/')) table = view.get_table() assert table.paginator.per_page == 2 assert len(table.page) == 2 @pytest.mark.django_db def test_view_should_support_disabling_pagination_options(): class SimpleNotPaginatedView(DispatchHookMixin, tables.SingleTableView): table_class = SimpleTable table_data = MEMORY_DATA table_pagination = False model = Region # required for ListView response, view = SimpleNotPaginatedView.as_view()(build_request('/')) table = view.get_table() assert not hasattr(table, 'page') @pytest.mark.django_db def test_view_from_get_queryset(): for region in MEMORY_DATA: Region.objects.create(name=region['name']) class GetQuerysetView(SimpleView): def get_queryset(self): return Region.objects.filter(name__startswith='Q') response, view = GetQuerysetView.as_view()(build_request('/')) table = view.get_table() assert len(table.rows) == 1 assert table.rows[0].get_cell('name') == 'Queensland' def test_should_raise_without_tableclass(): class WithoutTableclassView(tables.SingleTableView): model = Region with pytest.raises(ImproperlyConfigured): WithoutTableclassView.as_view()(build_request('/')) def test_should_support_explicit_table_data(): class ExplicitDataView(SimplePaginatedView): table_data = MEMORY_DATA response, view = ExplicitDataView.as_view()(build_request('/')) assert view.get_table().paginator.num_pages == len(MEMORY_DATA) @pytest.mark.django_db def test_paginate_by_on_view_class(): Region.objects.create(name='Friesland') class Table(tables.Table): class Meta: model = Region class PaginateByDefinedOnView(DispatchHookMixin, tables.SingleTableView): table_class = Table model = Region paginate_by = 2 table_data = MEMORY_DATA def get_queryset(self): return Region.objects.all().order_by('name') response, view = PaginateByDefinedOnView.as_view()(build_request('/')) assert view.get_table().paginator.per_page == 2 @pytest.mark.django_db def test_should_pass_kwargs_to_table_constructor(): class PassKwargsView(SimpleView): table_data = [] def get_table(self, **kwargs): kwargs.update({'orderable': False}) return super(PassKwargsView, self).get_table(**kwargs) response, view = SimpleView.as_view()(build_request('/')) assert view.get_table().orderable is True response, view = PassKwargsView.as_view()(build_request('/')) assert view.get_table().orderable is False @pytest.mark.django_db def test_should_override_table_pagination(): class PrefixedTable(SimpleTable): class Meta(SimpleTable.Meta): prefix = 'p_' class PrefixedView(SimpleView): table_class = PrefixedTable class PaginationOverrideView(PrefixedView): table_data = MEMORY_DATA def get_table_pagination(self, table): assert isinstance(table, tables.Table) per_page = self.request.GET.get('%s_override' % table.prefixed_per_page_field) if per_page is not None: return {'per_page': per_page} return super(PaginationOverrideView, self).get_table_pagination(table) response, view = PaginationOverrideView.as_view()(build_request('/?p_per_page_override=2')) assert view.get_table().paginator.per_page == 2 def test_singletablemixin_with_non_paginated_view(): ''' SingleTableMixin should not assume it is mixed with a ListView Github issue #326 ''' class Table(tables.Table): class Meta: model = Region class View(tables.SingleTableMixin, TemplateView): table_class = Table table_data = MEMORY_DATA template_name = 'dummy.html' View.as_view()(build_request('/')) class TableA(tables.Table): class Meta: model = Person class TableB(tables.Table): class Meta: model = Region exclude = ('id', ) @pytest.mark.django_db def test_multiTableMixin_basic(): Person.objects.create(first_name='Jan Pieter', last_name='W') Region.objects.create(name='Zuid-Holland') Region.objects.create(name='Noord-Holland') class View(tables.MultiTableMixin, TemplateView): tables = (TableA, TableB) tables_data = (Person.objects.all(), Region.objects.all()) template_name = 'multiple.html' response = View.as_view()(build_request('/')) response.render() html = response.rendered_content assert 'table_0-sort=first_name' in html assert 'table_1-sort=name' in html assert '' in html assert '' in html @pytest.mark.django_db def test_multiTableMixin_basic_alternative(): Person.objects.create(first_name='Jan Pieter', last_name='W') Region.objects.create(name='Zuid-Holland') Region.objects.create(name='Noord-Holland') class View(tables.MultiTableMixin, TemplateView): tables = ( TableA(Person.objects.all()), TableB(Region.objects.all()) ) template_name = 'multiple.html' response = View.as_view()(build_request('/')) response.render() html = response.rendered_content assert 'table_0-sort=first_name' in html assert 'table_1-sort=name' in html assert '' in html assert '' in html def test_multiTableMixin_without_tables(): class View(tables.MultiTableMixin, TemplateView): template_name = 'multiple.html' with pytest.raises(ImproperlyConfigured): View.as_view()(build_request('/')) def test_multiTableMixin_with_empty_get_tables_list(): class View(tables.MultiTableMixin, TemplateView): template_name = 'multiple.html' def get_tables(self): return [] response = View.as_view()(build_request('/')) response.render() html = response.rendered_content assert '

Multiple tables using MultiTableMixin

' in html def test_multiTableMixin_incorrect_len(): class View(tables.MultiTableMixin, TemplateView): tables = (TableA, TableB) tables_data = (Person.objects.all(), ) template_name = 'multiple.html' with pytest.raises(ImproperlyConfigured): View.as_view()(build_request('/')) @pytest.mark.django_db def test_multiTableMixin_pagination(): NL_PROVICES = ( 'Flevoland', 'Friesland', 'Gelderland', 'Groningen', 'Limburg', 'Noord-Brabant', 'Noord-Holland', 'Overijssel', 'Utrecht', 'Zeeland', 'Zuid-Holland', ) for name in NL_PROVICES: Region.objects.create(name=name) class View(DispatchHookMixin, tables.MultiTableMixin, TemplateView): tables = ( TableB(Region.objects.all()), TableB(Region.objects.all()) ) template_name = 'multiple.html' table_pagination = { 'per_page': 5 } response, view = View.as_view()(build_request('/?table_1-page=3')) tableA, tableB = view.get_tables() assert tableA.page.number == 1 assert tableB.page.number == 3 @pytest.mark.django_db def test_View_using_get_queryset(): ''' Should not raise a value-error for a View using View.get_queryset() (test for reverting regressing in #423) ''' Person.objects.create(first_name='Anton', last_name='Sam') class Table(tables.Table): class Meta(object): model = Person fields = ('first_name', 'last_name') class TestView(tables.SingleTableView): model = Person table_class = Table def get(self, request, *args, **kwargs): self.get_table() from django.http import HttpResponse return HttpResponse() def get_queryset(self): '''get_queryset should be called''' return Person.objects.all() TestView.as_view()(build_request()) django-tables2-1.14.2/tests/utils.py000066400000000000000000000050051317557360700172650ustar00rootroot00000000000000import re import warnings from contextlib import contextmanager import lxml.etree import lxml.html from django.core.handlers.wsgi import WSGIRequest from django.test.client import FakePayload from django.utils import six def parse(html): # We use html instead of etree. Because etree can't parse html entities. return lxml.html.fromstring(html) class assertNumQueries(object): ''' Assert the number of queries made through the django ORM in a with-block ''' def __init__(self, count=1): self.count = count from django.conf import settings settings.DEBUG = True def query_count(self): from django.db import connection return len(connection.queries) def __enter__(self): self.original = self.query_count() def __exit__(self, exc_type, exc_value, traceback): expected = self.original + self.count count = self.query_count() assert expected == count, 'Expected {} queries, but got {}.'.format( self.count, self.query_count() - self.original ) 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)) def build_request(uri='/', user=None): ''' 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('?') request = 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, }) if user is not None: request.user = user return request def clean_output(s): '''Remove double newlines with whitespace in between and reduce the level of indentation''' return re.sub('\n( *\n)+', '\n', s).replace(' ', ' ') django-tables2-1.14.2/tox.ini000066400000000000000000000025761317557360700157360ustar00rootroot00000000000000[pytest] DJANGO_SETTINGS_MODULE=tests.app.settings norecursedirs = docs *.egg-info .git example .tox [tox] args_are_paths = false envlist = py27-{1.8,1.9,1.10,1.11}, py33-{1.8}, py34-{1.8,1.9,1.10,1.11,2.0}, py35-{1.9,1.10,1.11,2.0,master}, py36-{2.0,master}, docs, flake8, isort [travis] python: 2.7: py27, docs 3.6: py36, flake8, isort [testenv] basepython = py27: python2.7 py33: python3.3 py34: python3.4 py35: python3.5 py36: python3.6 usedevelop = true pip_pre = true setenv = PYTHONPATH={toxinidir} commands = py.test -rw --cov=django_tables2 --cov-report term-missing deps = 1.8: Django>=1.8,<1.9 1.9: Django>=1.9,<1.10 1.10: Django>=1.10,<1.11 1.11: Django>=1.11,<2.0 2.0: Django==2.0b1 master: https://github.com/django/django/archive/master.tar.gz psycopg2 -r{toxinidir}/requirements/common.pip [testenv:docs] basepython = python2.7 whitelist_externals = make changedir = docs commands = make html deps = -r{toxinidir}/docs/requirements.txt [testenv:flake8] basepython = python3.6 deps = flake8 commands = flake8 [flake8] ignore = F401,E731 exclude = .git,__pycache__,.tox,example/app/migrations max-line-length = 120 [testenv:isort] basepython = python3.6 deps = isort==4.2.15 commands = isort --diff --check --recursive {toxinidir}/django_tables2 {toxinidir}/tests {toxinidir}/example
This is a footer
11200000 and attrs''' person = Person.objects.create(first_name='Bob', last_name='Builder') class Table(tables.Table): first_name = tables.LinkColumn(attrs={ 'a': {'style': 'color: red;'}, 'td': {'style': 'background-color: #ddd;'} }) last_name = tables.Column() table = Table(Person.objects.all()) a_tag = table.rows[0].get_cell('first_name') assert 'href="{}"'.format(reverse('person', args=(person.pk, ))) in a_tag assert 'style="color: red;"' in a_tag assert person.first_name in a_tag html = table.as_html(build_request()) td_tag_1 = '' td_tag_2 = '' assert td_tag_1 in html or td_tag_2 in html def test_defaults(): class Table(tables.Table): link = tables.LinkColumn('occupation', kwargs={'pk': 1}, default='xyz') table = Table([{}]) assert table.rows[0].get_cell('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 = '{}'.format( reverse('person', args=(person.pk, )), person.last_name ) assert table.rows[0].get_cell('last_name') == expected def test_get_absolute_url_not_defined(): ''' The dict doesn't have a get_absolute_url(), so creating the table should raise a TypeError ''' 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()) @pytest.mark.django_db def test_RelatedLinkColumn(): carpenter = Occupation.objects.create(name='Carpenter') Person.objects.create(first_name='Bob', last_name='Builder', occupation=carpenter) class Table(tables.Table): first_name = tables.LinkColumn() last_name = tables.Column() occupation = tables.RelatedLinkColumn() table = Table(Person.objects.all()) assert table.rows[0].get_cell('occupation') == 'Carpenter' % carpenter.pk def test_value_returns_a_raw_value_without_html(): class Table(tables.Table): col = tables.LinkColumn('occupation', args=(A('id'), )) table = Table([{'col': 'link-text', 'id': 1}]) assert table.rows[0].get_cell_value('col') == 'link-text' django-tables2-1.14.2/tests/columns/test_manytomanycolumn.py000066400000000000000000000066651317557360700242730ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from random import randint, sample import pytest from django.utils.html import format_html, mark_safe, strip_tags import django_tables2 as tables from tests.app.models import Person pytestmark = pytest.mark.django_db FAKE_NAMES = ( ('Kyle', 'Strader'), ('Francis', 'Fisher'), ('James', 'Jury'), ('Florentina', 'Floyd'), ('Mark', 'Boyd'), ('Simone', 'Fong'), ) def create_Persons(): for first, last in FAKE_NAMES: Person.objects.create(first_name=first, last_name=last) persons = list(Person.objects.all()) # give everyone 1 to 3 friends for person in persons: person.friends.add(*sample(persons, randint(1, 3))) person.save() def test_ManyToManyColumn_from_model(): ''' Automaticcally uses the ManyToManyColumn for a ManyToManyField, and calls the Models's `__str__` method to transform the model instace to string. ''' create_Persons() class Table(tables.Table): name = tables.Column(accessor='name', order_by=('last_name', 'first_name')) class Meta: model = Person fields = ('name', 'friends') table = Table(Person.objects.all()) for row in table.rows: friends = row.get_cell('friends').split(', ') for friend in friends: assert Person.objects.filter(first_name=friend).exists() def test_custom_separator(): create_Persons() def assert_sep(sep): class Table(tables.Table): friends = tables.ManyToManyColumn(separator=sep) table = Table(Person.objects.all().order_by('last_name')) for row in table.rows: friends = row.get_cell('friends').split(sep) for friend in friends: assert Person.objects.filter(first_name=friend).exists() # normal string, will not be escaped assert_sep('|') # html tag, would normally be escaped, but should not be escaped because # it is mark_safe()'ed assert_sep(mark_safe('
')) def test_transform_returns_html(): create_Persons() class Table(tables.Table): friends = tables.ManyToManyColumn(transform=lambda m: format_html('{}', m.first_name)) table = Table(Person.objects.all().order_by('last_name')) for row in table.rows: friends = row.get_cell('friends').split(', ') for friend in friends: stripped = strip_tags(friend) assert Person.objects.filter(first_name=stripped).exists() def test_orderable_is_false(): class Table(tables.Table): friends = tables.ManyToManyColumn(orderable=False) Table([]) def test_ManyToManyColumn_complete_example(): create_Persons() # add a friendless person remi = Person.objects.create(first_name='Remi', last_name='Barberin') class Table(tables.Table): name = tables.Column(accessor='name', order_by=('last_name', 'first_name')) friends = tables.ManyToManyColumn( transform=lambda o: o.name, filter=lambda o: o.order_by('-last_name') ) table = Table(Person.objects.all().order_by('last_name')) for row in table.rows: friends = row.get_cell('friends') if friends == '-': assert row.get_cell('name') == remi.name continue friends = list(map(lambda o: o.split(' '), friends.split(', '))) assert friends == sorted(friends, key=lambda item: item[1], reverse=True) django-tables2-1.14.2/tests/columns/test_templatecolumn.py000066400000000000000000000067131317557360700237040ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import pytest from django.template import Context, Template import django_tables2 as tables from ..utils import build_request def test_should_render_in_pinned_row(): class TestOnlyPinnedTable(tables.Table): foo = tables.TemplateColumn('value={{ value }}') def __init__(self, data): self.pinned = data revised_data = [] super(TestOnlyPinnedTable, self).__init__(revised_data) def get_top_pinned_data(self): return self.pinned table = TestOnlyPinnedTable([{'foo': 'bar'}]) for row in table.rows: assert row.get_cell('foo') == 'value=bar' template = Template('{% load django_tables2 %}{% render_table table %}') html = template.render(Context({'request': build_request(), 'table': table})) assert '
value=barname:brad-author\nvalue=barNameCountryNameCountryNameCountryNameCountry0Total: 77740000Total:18833000Total:18833000{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}Jan PieterZuid-HollandJan PieterZuid-Holland