├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .readthedocs.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── django_tables2 ├── __init__.py ├── columns │ ├── __init__.py │ ├── base.py │ ├── booleancolumn.py │ ├── checkboxcolumn.py │ ├── datecolumn.py │ ├── datetimecolumn.py │ ├── emailcolumn.py │ ├── filecolumn.py │ ├── jsoncolumn.py │ ├── linkcolumn.py │ ├── manytomanycolumn.py │ ├── templatecolumn.py │ ├── timecolumn.py │ └── urlcolumn.py ├── config.py ├── data.py ├── export │ ├── __init__.py │ ├── export.py │ └── views.py ├── locale │ ├── cs │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── el │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fa │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── hu │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── lt │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nb │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt_PT │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── sv │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── uk │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_Hans │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── paginators.py ├── rows.py ├── static │ └── django_tables2 │ │ ├── bootstrap.css │ │ └── themes │ │ └── paleblue │ │ ├── css │ │ └── screen.css │ │ └── img │ │ ├── arrow-active-down.png │ │ ├── arrow-active-up.png │ │ ├── arrow-inactive-down.png │ │ ├── arrow-inactive-up.png │ │ ├── false.gif │ │ ├── header-bg.png │ │ ├── missing.png │ │ ├── pagination-bg.gif │ │ └── true.gif ├── tables.py ├── templates │ └── django_tables2 │ │ ├── bootstrap-responsive.html │ │ ├── bootstrap.html │ │ ├── bootstrap4-responsive.html │ │ ├── bootstrap4.html │ │ ├── bootstrap5-responsive.html │ │ ├── bootstrap5.html │ │ ├── semantic.html │ │ └── table.html ├── templatetags │ ├── __init__.py │ └── django_tables2.py ├── utils.py └── views.py ├── docs ├── Makefile ├── _static │ ├── example.png │ ├── tutorial-bootstrap.png │ └── tutorial.png ├── conf.py ├── img │ ├── bootstrap.png │ ├── bootstrap4.png │ ├── example.png │ └── semantic.png ├── index.rst ├── make.bat ├── pages │ ├── api-reference.rst │ ├── builtin-columns.rst │ ├── column-attributes.rst │ ├── column-headers-and-footers.rst │ ├── custom-data.rst │ ├── custom-rendering.rst │ ├── export.rst │ ├── faq.rst │ ├── filtering.rst │ ├── generic-mixins.rst │ ├── glossary.rst │ ├── installation.rst │ ├── internal.rst │ ├── localization-control.rst │ ├── ordering.rst │ ├── pagination.rst │ ├── pinned-rows.rst │ ├── query-string-fields.rst │ ├── reference.rst │ ├── swapping-columns.rst │ ├── table-data.rst │ ├── table-mixins.rst │ ├── template-tags.rst │ ├── tutorial.rst │ └── upgrade-changelog.rst ├── requirements.txt └── spelling_wordlist.txt ├── example ├── README.md ├── __init__.py ├── app │ ├── __init__.py │ ├── admin.py │ ├── data.py │ ├── filters.py │ ├── fixtures │ │ └── initial_data.json │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20180416_0959.py │ │ ├── 0003_auto_20180416_1020.py │ │ ├── 0004_auto_fix_deprecation_warnings.py │ │ └── __init__.py │ ├── models.py │ ├── tables.py │ └── views.py ├── manage.py ├── media │ └── country │ │ └── flags │ │ ├── australia.svg │ │ ├── canada.svg │ │ └── new_zealand.svg ├── requirements.txt ├── settings.py ├── templates │ ├── base.html │ ├── bootstrap4_template.html │ ├── bootstrap5_template.html │ ├── bootstrap_template.html │ ├── checkbox_example.html │ ├── class_based.html │ ├── country_detail.html │ ├── extended_table.html │ ├── index.html │ ├── multiTable.html │ ├── multiple.html │ ├── person_detail.html │ ├── semantic_template.html │ └── tutorial.html └── urls.py ├── maintenance.py ├── manage.py ├── pyproject.toml ├── requirements ├── common.pip └── django-dev.pip ├── tests ├── __init__.py ├── app │ ├── __init__.py │ ├── locale │ │ └── ua │ │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── settings.py │ ├── templates │ │ ├── child │ │ │ └── foo.html │ │ ├── csrf.html │ │ ├── dummy.html │ │ ├── minimal.html │ │ ├── multiple.html │ │ └── test_template_column.html │ ├── urls.py │ └── views.py ├── columns │ ├── __init__.py │ ├── test_booleancolumn.py │ ├── test_checkboxcolumn.py │ ├── test_datecolumn.py │ ├── test_datetimecolumn.py │ ├── test_emailcolumn.py │ ├── test_filecolumn.py │ ├── test_general.py │ ├── test_initialsortcolumn.py │ ├── test_jsoncolumn.py │ ├── test_linkcolumn.py │ ├── test_manytomanycolumn.py │ ├── test_templatecolumn.py │ ├── test_timecolumn.py │ └── test_urlcolumn.py ├── test_config.py ├── test_core.py ├── test_export.py ├── test_extra_columns.py ├── test_faq.py ├── test_footer.py ├── test_models.py ├── test_ordering.py ├── test_paginators.py ├── test_pinned_rows.py ├── test_rows.py ├── test_tabledata.py ├── test_templates.py ├── test_templatetags.py ├── test_utils.py ├── test_views.py └── utils.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | django_tables2 4 | tests 5 | branch = true 6 | 7 | 8 | [html] 9 | directory = reports/htmlcov 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | pre-commit: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/setup-python@v5 8 | with: 9 | python-version: "3.11" 10 | - uses: actions/checkout@v4 11 | - run: pip install pre-commit 12 | - run: pre-commit run --show-diff-on-failure --all-files 13 | 14 | tests: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.9, "3.10", 3.11, 3.12] # 3.13 can be supported if lxml supports 3.13 19 | django-version: [4.2, 5.0, 5.1, "5.2a1", "master"] 20 | exclude: 21 | # Django 4.2 22 | - python-version: 3.12 23 | django-version: 4.2 24 | - python-version: 3.13 25 | django-version: 4.2 26 | 27 | # Django 5.0 28 | - python-version: 3.9 29 | django-version: 5.0 30 | - python-version: 3.13 31 | django-version: 5.0 32 | 33 | # Django 5.1 34 | - python-version: 3.9 35 | django-version: 5.1 36 | 37 | # Django 5.2 38 | - python-version: 3.9 39 | django-version: 5.2a1 40 | 41 | # Django master 42 | - python-version: 3.9 43 | django-version: master 44 | - python-version: 3.10 45 | django-version: master 46 | - python-version: 3.11 47 | django-version: master 48 | 49 | steps: 50 | - name: Set up Python ${{ matrix.python-version }} 51 | uses: actions/setup-python@v5 52 | with: 53 | python-version: ${{ matrix.python-version }} 54 | - uses: actions/checkout@v4 55 | - uses: actions/cache@v4.2.3 56 | with: 57 | path: ~/.cache/pip 58 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 59 | restore-keys: | 60 | ${{ runner.os }}-pip- 61 | - run: python -m pip install Django==${{ matrix.django-version }} 62 | if: matrix.django-version != 'master' 63 | - run: python -m pip install https://github.com/django/django/archive/master.tar.gz 64 | if: matrix.django-version == 'master' 65 | - run: | 66 | python -m pip install coverage 67 | python -m pip install -r requirements/common.pip 68 | - run: coverage run --source=django_tables2 manage.py test 69 | 70 | docs: 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v4 74 | - uses: actions/setup-python@v5 75 | with: 76 | python-version: "3.11" 77 | cache: 'pip' 78 | cache-dependency-path: | 79 | docs/requirements.txt 80 | common/requirements.txt 81 | - name: Install and build 82 | run: | 83 | cd docs 84 | python -m pip install -r requirements.txt 85 | make html 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /.env 3 | /reports 4 | /*.sublime-* 5 | /*.komodoproject 6 | /*.tmproj 7 | /*.egg-info/ 8 | /*.egg 9 | /.tox 10 | /.coverage 11 | /MANIFEST 12 | /dist/ 13 | /build/ 14 | /docs/_build/ 15 | /docs/pages/CHANGELOG.md 16 | /example/database.sqlite 17 | /example/.env 18 | /report.pylint 19 | .cache/ 20 | .python-version 21 | .idea 22 | *.sw[po] 23 | pip-wheel-metadata 24 | .vscode -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.8.0 4 | hooks: 5 | - id: ruff 6 | args: [--fix, --exit-non-zero-on-fix] 7 | - id: ruff-format 8 | types_or: [ python, pyi ] 9 | 10 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 11 | rev: v2.14.0 12 | hooks: 13 | - id: pretty-format-toml 14 | args: [--autofix] 15 | 16 | - repo: https://github.com/adamchainz/django-upgrade 17 | rev: "1.22.1" 18 | hooks: 19 | - id: django-upgrade 20 | args: [--target-version, "4.2"] 21 | 22 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | 13 | # Build documentation in the docs/ directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | python: 18 | install: 19 | - requirements: docs/requirements.txt 20 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # Optionally declare the Python requirements required to build your docs 19 | python: 20 | install: 21 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to django-tables2 2 | 3 | You are welcome to contribute to the development of `django-tables2` in various ways: 4 | 5 | - Discover and [report bugs](https://github.com/jieter/django-tables2/issues/new). 6 | Make sure to include a minimal example to show your problem. 7 | - Propose features, add tests or fix bugs by [opening a Pull Request](https://github.com/jieter/django-tables2/compare) 8 | - Fix documentation or translations 9 | 10 | When contributing features or making bug fixes, please add unit tests to verify the expected behaviour. 11 | This helps 12 | 13 | ## Coding style 14 | 15 | We use [black](https://black.readthedocs.io/en/stable/) to format the sources, with a 100 char line length. 16 | 17 | Before committing, run `black .`, or use `pre-commit`: 18 | 19 | ``` 20 | pip install pre-commit 21 | pre-commit install 22 | ``` 23 | 24 | ## Running the tests 25 | 26 | With `tox` installed, you can run the test suite in all supported environments by typing `tox`. 27 | During development, you might not want to wait for the tests to run in all environments, 28 | in that case, use the `-e` argument to specify a specific environment. 29 | For example `tox -e py36-2.0` will run the tests in python 3.6 with Django 2.0. 30 | You can also run the tests only in your current environment, using 31 | `PYTHONPATH=. ./manage.py test` (which is even quicker). 32 | 33 | ## Code coverage 34 | 35 | To generate a html coverage report: 36 | ``` 37 | coverage run --source=django_tables2 manage.py test 38 | coverage html 39 | ``` 40 | 41 | ## Building the documentation 42 | 43 | If you want to build the docs from within a virtualenv, and Sphinx is installed globally, use: 44 | 45 | ``` 46 | cd docs/ 47 | make html SPHINXBUILD="python $(which sphinx-build)" 48 | ``` 49 | 50 | Publishing a release 51 | -------------------- 52 | 53 | 1. Bump the version in `django-tables2/__init__.py`. 54 | 2. Update `CHANGELOG.md`. 55 | 3. Create a tag `./maintenance.py tag`. 56 | 4. Run `./maintenance.py publish` 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All changes made to django-tables2 since forking from django-tables 2 | are Copyright (c) 2011, Bradley Ayers 3 | All rights reserved. 4 | 5 | Redistribution is permitted under the same terms as the original 6 | django-tables license. The original django-tables license is included 7 | below. 8 | 9 | 10 | Copyright (c) 2008, Michael Elsdörfer 11 | All rights reserved. 12 | 13 | Redistribution and use in source and binary forms, with or without 14 | modification, are permitted provided that the following conditions 15 | are met: 16 | 17 | 1. Redistributions of source code must retain the above copyright 18 | notice, this list of conditions and the following disclaimer. 19 | 20 | 2. Redistributions in binary form must reproduce the above 21 | copyright notice, this list of conditions and the following 22 | disclaimer in the documentation and/or other materials 23 | provided with the distribution. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 28 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 29 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 30 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 31 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 32 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 33 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 34 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 35 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 36 | POSSIBILITY OF SUCH DAMAGE. 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-tables2 - An app for creating HTML tables 2 | 3 | [![Latest PyPI version](https://badge.fury.io/py/django-tables2.svg)](https://pypi.python.org/pypi/django-tables2) 4 | [![Any color you like](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 5 | 6 | django-tables2 simplifies the task of turning sets of data into HTML tables. It 7 | has native support for pagination and sorting. It does for HTML tables what 8 | `django.forms` does for HTML forms. e.g. 9 | 10 | - Available on pypi as [django-tables2](https://pypi.python.org/pypi/django-tables2) 11 | - Tested against currently supported versions of Django 12 | [and supported python 3 versions Django supports](https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django). 13 | - [Documentation on readthedocs.org](https://django-tables2.readthedocs.io/en/latest/) 14 | - [Bug tracker](http://github.com/jieter/django-tables2/issues) 15 | 16 | Features: 17 | 18 | - Any iterable can be a data-source, but special support for Django `QuerySets` is included. 19 | - The builtin UI does not rely on JavaScript. 20 | - Support for automatic table generation based on a Django model. 21 | - Supports custom column functionality via subclassing. 22 | - Pagination. 23 | - Column based table sorting. 24 | - Template tag to enable trivial rendering to HTML. 25 | - Generic view mixin. 26 | 27 | ![An example table rendered using django-tables2](https://cdn.rawgit.com/jieter/django-tables2/master/docs/img/example.png) 28 | 29 | ![An example table rendered using django-tables2 and bootstrap theme](https://cdn.rawgit.com/jieter/django-tables2/master/docs/img/bootstrap.png) 30 | 31 | ![An example table rendered using django-tables2 and semantic-ui theme]( 32 | https://cdn.rawgit.com/jieter/django-tables2/master/docs/img/semantic.png) 33 | 34 | ## Example 35 | 36 | Start by adding `django_tables2` to your `INSTALLED_APPS` setting like this: 37 | 38 | ```python 39 | INSTALLED_APPS = ( 40 | ..., 41 | "django_tables2", 42 | ) 43 | ``` 44 | 45 | Creating a table for a model `Simple` is as simple as: 46 | 47 | ```python 48 | import django_tables2 as tables 49 | 50 | class SimpleTable(tables.Table): 51 | class Meta: 52 | model = Simple 53 | ``` 54 | This would then be used in a view: 55 | 56 | ```python 57 | class TableView(tables.SingleTableView): 58 | table_class = SimpleTable 59 | queryset = Simple.objects.all() 60 | template_name = "simple_list.html" 61 | ``` 62 | And finally in the template: 63 | 64 | ``` 65 | {% load django_tables2 %} 66 | {% render_table table %} 67 | ``` 68 | 69 | This example shows one of the simplest cases, but django-tables2 can do a lot more! 70 | Check out the [documentation](https://django-tables2.readthedocs.io/en/latest/) for more details. 71 | -------------------------------------------------------------------------------- /django_tables2/__init__.py: -------------------------------------------------------------------------------- 1 | from .columns import ( 2 | BooleanColumn, 3 | CheckBoxColumn, 4 | Column, 5 | DateColumn, 6 | DateTimeColumn, 7 | EmailColumn, 8 | FileColumn, 9 | JSONColumn, 10 | LinkColumn, 11 | ManyToManyColumn, 12 | RelatedLinkColumn, 13 | TemplateColumn, 14 | TimeColumn, 15 | URLColumn, 16 | ) 17 | from .config import RequestConfig 18 | from .paginators import LazyPaginator 19 | from .tables import Table, table_factory 20 | from .utils import A 21 | from .views import MultiTableMixin, SingleTableMixin, SingleTableView 22 | 23 | __version__ = "2.7.5" 24 | 25 | __all__ = ( 26 | "Table", 27 | "table_factory", 28 | "BooleanColumn", 29 | "Column", 30 | "CheckBoxColumn", 31 | "DateColumn", 32 | "DateTimeColumn", 33 | "EmailColumn", 34 | "FileColumn", 35 | "JSONColumn", 36 | "LinkColumn", 37 | "ManyToManyColumn", 38 | "RelatedLinkColumn", 39 | "TemplateColumn", 40 | "TimeColumn", 41 | "URLColumn", 42 | "RequestConfig", 43 | "A", 44 | "SingleTableMixin", 45 | "SingleTableView", 46 | "MultiTableMixin", 47 | "LazyPaginator", 48 | ) 49 | -------------------------------------------------------------------------------- /django_tables2/columns/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import BoundColumn, BoundColumns, Column, library 2 | from .booleancolumn import BooleanColumn 3 | from .checkboxcolumn import CheckBoxColumn 4 | from .datecolumn import DateColumn 5 | from .datetimecolumn import DateTimeColumn 6 | from .emailcolumn import EmailColumn 7 | from .filecolumn import FileColumn 8 | from .jsoncolumn import JSONColumn 9 | from .linkcolumn import LinkColumn, RelatedLinkColumn 10 | from .manytomanycolumn import ManyToManyColumn 11 | from .templatecolumn import TemplateColumn 12 | from .timecolumn import TimeColumn 13 | from .urlcolumn import URLColumn 14 | 15 | __all__ = ( 16 | "library", 17 | "BoundColumn", 18 | "BoundColumns", 19 | "Column", 20 | "BooleanColumn", 21 | "CheckBoxColumn", 22 | "DateColumn", 23 | "DateTimeColumn", 24 | "EmailColumn", 25 | "FileColumn", 26 | "JSONColumn", 27 | "LinkColumn", 28 | "ManyToManyColumn", 29 | "RelatedLinkColumn", 30 | "TemplateColumn", 31 | "URLColumn", 32 | "TimeColumn", 33 | ) 34 | -------------------------------------------------------------------------------- /django_tables2/columns/booleancolumn.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.html import escape, format_html 3 | 4 | from ..utils import AttributeDict 5 | from .base import Column, library 6 | 7 | 8 | @library.register 9 | class BooleanColumn(Column): 10 | """ 11 | A column suitable for rendering boolean data. 12 | 13 | Arguments: 14 | null (bool): is `None` different from `False`? 15 | yesno (str): comma separated values string or 2-tuple to display for 16 | True/False values. 17 | 18 | Rendered values are wrapped in a ```` to allow customization by using 19 | CSS. By default the span is given the class ``true``, ``false``. 20 | 21 | In addition to *attrs* keys supported by `~.Column`, the following are 22 | available: 23 | 24 | - ``span`` -- adds attributes to the ```` tag 25 | """ 26 | 27 | def __init__(self, null=False, yesno="✔,✘", **kwargs): 28 | self.yesno = yesno.split(",") if isinstance(yesno, str) else tuple(yesno) 29 | if not null: 30 | kwargs["empty_values"] = () 31 | super().__init__(**kwargs) 32 | 33 | def _get_bool_value(self, record, value, bound_column): 34 | # If record is a model, we need to check if it has choices defined. 35 | if hasattr(record, "_meta"): 36 | field = bound_column.accessor.get_field(record) 37 | 38 | # If that's the case, we need to inverse lookup the value to convert 39 | # to a boolean we can use. 40 | if hasattr(field, "choices") and field.choices is not None and len(field.choices) > 0: 41 | value = next(val for val, name in field.choices if name == value) 42 | 43 | value = bool(value) 44 | return value 45 | 46 | def render(self, value, record, bound_column): 47 | value = self._get_bool_value(record, value, bound_column) 48 | text = self.yesno[int(not value)] 49 | attrs = {"class": str(value).lower()} 50 | attrs.update(self.attrs.get("span", {})) 51 | 52 | return format_html("{}", AttributeDict(attrs).as_html(), escape(text)) 53 | 54 | def value(self, record, value, bound_column): 55 | """Return the content for a specific cell similarly to `.render` however without any html content.""" 56 | value = self._get_bool_value(record, value, bound_column) 57 | return str(value) 58 | 59 | @classmethod 60 | def from_field(cls, field, **kwargs): 61 | if isinstance(field, models.NullBooleanField): 62 | return cls(null=True, **kwargs) 63 | 64 | if isinstance(field, models.BooleanField): 65 | return cls(null=getattr(field, "null", False), **kwargs) 66 | -------------------------------------------------------------------------------- /django_tables2/columns/checkboxcolumn.py: -------------------------------------------------------------------------------- 1 | from django.utils.safestring import mark_safe 2 | 3 | from django_tables2.utils import Accessor, AttributeDict, computed_values 4 | 5 | from .base import Column, library 6 | 7 | 8 | @library.register 9 | class CheckBoxColumn(Column): 10 | """ 11 | A subclass of `.Column` that renders as a checkbox form input. 12 | 13 | This column allows a user to *select* a set of rows. The selection 14 | information can then be used to apply some operation (e.g. "delete") onto 15 | the set of objects that correspond to the selected rows. 16 | 17 | The value that is extracted from the :term:`table data` for this column is 18 | used as the value for the checkbox, i.e. ```` 20 | 21 | This class implements some sensible defaults: 22 | 23 | - HTML input's ``name`` attribute is the :term:`column name` (can override 24 | via *attrs* argument). 25 | - ``orderable`` defaults to `False`. 26 | 27 | Arguments: 28 | attrs (dict): In addition to *attrs* keys supported by `~.Column`, the 29 | following are available: 30 | 31 | - ``input`` -- ```` elements in both ```` and ````. 32 | - ``th__input`` -- Replaces ``input`` attrs in header cells. 33 | - ``td__input`` -- Replaces ``input`` attrs in body cells. 34 | 35 | checked (`~.Accessor`, bool, callable): Allow rendering the checkbox as 36 | checked. If it resolves to a truthy value, the checkbox will be 37 | rendered as checked. 38 | 39 | .. note:: 40 | 41 | You might expect that you could select multiple checkboxes in the 42 | rendered table and then *do something* with that. This functionality 43 | is not implemented. If you want something to actually happen, you will 44 | need to implement that yourself. 45 | """ 46 | 47 | def __init__(self, attrs=None, checked=None, **extra): 48 | self.checked = checked 49 | kwargs = {"orderable": False, "attrs": attrs} 50 | kwargs.update(extra) 51 | super().__init__(**kwargs) 52 | 53 | @property 54 | def header(self): 55 | default = {"type": "checkbox"} 56 | general = self.attrs.get("input") 57 | specific = self.attrs.get("th__input") 58 | attrs = AttributeDict(default, **(specific or general or {})) 59 | return mark_safe(f"") 60 | 61 | def render(self, value, bound_column, record): 62 | default = {"type": "checkbox", "name": bound_column.name, "value": value} 63 | if self.is_checked(value, record): 64 | default.update({"checked": "checked"}) 65 | 66 | general = self.attrs.get("input") 67 | specific = self.attrs.get("td__input") 68 | 69 | attrs = dict(default, **(specific or general or {})) 70 | attrs = computed_values(attrs, kwargs={"record": record, "value": value}) 71 | return mark_safe(f"") 72 | 73 | def is_checked(self, value, record): 74 | """Determine if the checkbox should be checked.""" 75 | if self.checked is None: 76 | return False 77 | if self.checked is True: 78 | return True 79 | 80 | if callable(self.checked): 81 | return bool(self.checked(value, record)) 82 | 83 | checked = Accessor(self.checked) 84 | if checked in record: 85 | return bool(record[checked]) 86 | return False 87 | -------------------------------------------------------------------------------- /django_tables2/columns/datecolumn.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .base import library 4 | from .templatecolumn import TemplateColumn 5 | 6 | 7 | @library.register 8 | class DateColumn(TemplateColumn): 9 | """ 10 | A column that renders dates in the local timezone. 11 | 12 | Arguments: 13 | format (str): format string in same format as Django's ``date`` template 14 | filter (optional) 15 | short (bool): if `format` is not specified, use Django's 16 | ``SHORT_DATE_FORMAT`` setting, otherwise use ``DATE_FORMAT`` 17 | """ 18 | 19 | def __init__(self, format=None, short=True, *args, **kwargs): 20 | if format is None: 21 | format = "SHORT_DATE_FORMAT" if short else "DATE_FORMAT" 22 | template = '{{ value|date:"%s"|default:default }}' % format # noqa: UP031 23 | super().__init__(template_code=template, *args, **kwargs) 24 | 25 | @classmethod 26 | def from_field(cls, field, **kwargs): 27 | if isinstance(field, models.DateField): 28 | return cls(**kwargs) 29 | -------------------------------------------------------------------------------- /django_tables2/columns/datetimecolumn.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .base import library 4 | from .templatecolumn import TemplateColumn 5 | 6 | 7 | @library.register 8 | class DateTimeColumn(TemplateColumn): 9 | """ 10 | A column that renders `datetime` instances in the local timezone. 11 | 12 | Arguments: 13 | format (str): format string for datetime (optional). 14 | Note that *format* uses Django's `date` template tag syntax. 15 | short (bool): if `format` is not specified, use Django's 16 | ``SHORT_DATETIME_FORMAT``, else ``DATETIME_FORMAT`` 17 | """ 18 | 19 | def __init__(self, format=None, short=True, *args, **kwargs): 20 | if format is None: 21 | format = "SHORT_DATETIME_FORMAT" if short else "DATETIME_FORMAT" 22 | template = '{{ value|date:"%s"|default:default }}' % format # noqa: UP031 23 | super().__init__(template_code=template, *args, **kwargs) 24 | 25 | @classmethod 26 | def from_field(cls, field, **kwargs): 27 | if isinstance(field, models.DateTimeField): 28 | return cls(**kwargs) 29 | -------------------------------------------------------------------------------- /django_tables2/columns/emailcolumn.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .base import library 4 | from .linkcolumn import BaseLinkColumn 5 | 6 | 7 | @library.register 8 | class EmailColumn(BaseLinkColumn): 9 | """ 10 | Render email addresses to `mailto:`-links. 11 | 12 | Arguments: 13 | attrs (dict): HTML attributes that are added to the rendered 14 | ``...`` tag. 15 | text: Either static text, or a callable. If set, this will be used to 16 | render the text inside link instead of the value. 17 | 18 | Example:: 19 | 20 | # models.py 21 | class Person(models.Model): 22 | name = models.CharField(max_length=200) 23 | email = models.EmailField() 24 | 25 | # tables.py 26 | class PeopleTable(tables.Table): 27 | name = tables.Column() 28 | email = tables.EmailColumn() 29 | 30 | # result 31 | # [...]email@example.com 32 | """ 33 | 34 | def get_url(self, value): 35 | return f"mailto:{value}" 36 | 37 | @classmethod 38 | def from_field(cls, field, **kwargs): 39 | if isinstance(field, models.EmailField): 40 | return cls(**kwargs) 41 | -------------------------------------------------------------------------------- /django_tables2/columns/filecolumn.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.db import models 4 | from django.utils.html import format_html 5 | 6 | from ..utils import AttributeDict 7 | from .base import library 8 | from .linkcolumn import BaseLinkColumn 9 | 10 | 11 | @library.register 12 | class FileColumn(BaseLinkColumn): 13 | """ 14 | Attempts to render `.FieldFile` (or other storage backend `.File`) as a 15 | hyperlink. 16 | 17 | When the file is accessible via a URL, the file is rendered as a 18 | hyperlink. The `.basename` is used as the text, wrapped in a span:: 19 | 20 | receipt.pdf 21 | 22 | When unable to determine the URL, a ``span`` is used instead:: 23 | 24 | receipt.pdf 25 | 26 | `.Column.attrs` keys ``a`` and ``span`` can be used to add additional attributes. 27 | 28 | Arguments: 29 | verify_exists (bool): attempt to determine if the file exists 30 | If *verify_exists*, the HTML class ``exists`` or ``missing`` is 31 | added to the element to indicate the integrity of the storage. 32 | text (str or callable): Either static text, or a callable. If set, this 33 | will be used to render the text inside the link instead of 34 | the file's ``basename`` (default) 35 | """ 36 | 37 | def __init__(self, verify_exists=True, **kwargs): 38 | self.verify_exists = verify_exists 39 | super().__init__(**kwargs) 40 | 41 | def get_url(self, value, record): 42 | storage = getattr(value, "storage", None) 43 | if not storage: 44 | return None 45 | 46 | return storage.url(value.name) 47 | 48 | def text_value(self, record, value): 49 | if self.text is None: 50 | return os.path.basename(value.name) 51 | return super().text_value(record, value) 52 | 53 | def render(self, record, value): 54 | attrs = AttributeDict(self.attrs.get("span", {})) 55 | classes = [c for c in attrs.get("class", "").split(" ") if c] 56 | 57 | exists = None 58 | storage = getattr(value, "storage", None) 59 | if storage: 60 | # we'll assume value is a `django.db.models.fields.files.FieldFile` 61 | if self.verify_exists: 62 | exists = storage.exists(value.name) 63 | else: 64 | if self.verify_exists and hasattr(value, "name"): 65 | # ignore negatives, perhaps the file has a name but it doesn't 66 | # represent a local path... better to stay neutral than give a 67 | # false negative. 68 | exists = os.path.exists(value.name) or exists 69 | 70 | if exists is not None: 71 | classes.append("exists" if exists else "missing") 72 | 73 | attrs["title"] = value.name 74 | attrs["class"] = " ".join(classes) 75 | 76 | return format_html( 77 | "{text}", 78 | attrs=attrs.as_html(), 79 | text=self.text_value(record, value), 80 | ) 81 | 82 | @classmethod 83 | def from_field(cls, field, **kwargs): 84 | if isinstance(field, models.FileField): 85 | return cls(**kwargs) 86 | -------------------------------------------------------------------------------- /django_tables2/columns/jsoncolumn.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.db.models import JSONField 4 | from django.utils.html import format_html 5 | 6 | from ..utils import AttributeDict 7 | from .base import library 8 | from .linkcolumn import BaseLinkColumn 9 | 10 | try: 11 | from django.contrib.postgres.fields import HStoreField 12 | 13 | POSTGRES_AVAILABLE = True 14 | except ImportError: 15 | # psycopg2 is not available, cannot import from django.contrib.postgres. 16 | # JSONColumn might still be useful to add manually. 17 | POSTGRES_AVAILABLE = False 18 | 19 | 20 | @library.register 21 | class JSONColumn(BaseLinkColumn): 22 | """ 23 | Render the contents of `~django.contrib.postgres.fields.JSONField` or 24 | `~django.contrib.postgres.fields.HStoreField` as an indented string. 25 | 26 | .. versionadded :: 1.5.0 27 | 28 | .. note:: 29 | 30 | Automatic rendering of data to this column requires PostgreSQL support 31 | (psycopg2 installed) to import the fields, but this column can also be 32 | used manually without it. 33 | 34 | Arguments: 35 | json_dumps_kwargs: kwargs passed to `json.dumps`, defaults to `{'indent': 2}` 36 | attrs (dict): In addition to *attrs* keys supported by `~.Column`, the 37 | following are available: 38 | 39 | - ``pre`` -- ``
`` around the rendered JSON string in ```` elements.
40 | 
41 |     """
42 | 
43 |     def __init__(self, json_dumps_kwargs=None, **kwargs):
44 |         self.json_dumps_kwargs = (
45 |             json_dumps_kwargs if json_dumps_kwargs is not None else {"indent": 2}
46 |         )
47 | 
48 |         super().__init__(**kwargs)
49 | 
50 |     def render(self, record, value):
51 |         return format_html(
52 |             "
{}
", 53 | AttributeDict(self.attrs.get("pre", {})).as_html(), 54 | json.dumps(value, **self.json_dumps_kwargs), 55 | ) 56 | 57 | @classmethod 58 | def from_field(cls, field, **kwargs): 59 | if POSTGRES_AVAILABLE: 60 | if isinstance(field, (JSONField, HStoreField)): 61 | return cls(**kwargs) 62 | -------------------------------------------------------------------------------- /django_tables2/columns/manytomanycolumn.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.encoding import force_str 3 | from django.utils.html import conditional_escape 4 | from django.utils.safestring import mark_safe 5 | 6 | from .base import Column, LinkTransform, library 7 | 8 | 9 | @library.register 10 | class ManyToManyColumn(Column): 11 | """ 12 | Display the list of objects from a `ManyRelatedManager`. 13 | 14 | Ordering is disabled for this column. 15 | 16 | Arguments: 17 | transform: callable to transform each item to text, it gets an item as argument 18 | and must return a string-like representation of the item. 19 | By default, it calls `~django.utils.force_str` on each item. 20 | filter: callable to filter, limit or order the QuerySet, it gets the 21 | `ManyRelatedManager` as first argument and must return a filtered QuerySet. 22 | By default, it returns `all()` 23 | separator: separator string to join the items with. default: ``", "`` 24 | linkify_item: callable, arguments to reverse() or `True` to wrap items in a ```` tag. 25 | For a detailed explanation, see ``linkify`` argument to ``Column``. 26 | 27 | For example, when displaying a list of friends with their full name:: 28 | 29 | # models.py 30 | class Person(models.Model): 31 | first_name = models.CharField(max_length=200) 32 | last_name = models.CharField(max_length=200) 33 | friends = models.ManyToManyField(Person) 34 | is_active = models.BooleanField(default=True) 35 | 36 | @property 37 | def name(self): 38 | return f"{self.first_name} {self.last_name}" 39 | 40 | # tables.py 41 | class PersonTable(tables.Table): 42 | name = tables.Column(order_by=("last_name", "first_name")) 43 | friends = tables.ManyToManyColumn(transform=lambda user: user.name) 44 | 45 | If only the active friends should be displayed, you can use the `filter` argument:: 46 | 47 | friends = tables.ManyToManyColumn(filter=lambda qs: qs.filter(is_active=True)) 48 | 49 | """ 50 | 51 | def __init__( 52 | self, transform=None, filter=None, separator=", ", linkify_item=None, *args, **kwargs 53 | ): 54 | kwargs.setdefault("orderable", False) 55 | super().__init__(*args, **kwargs) 56 | 57 | if transform is not None: 58 | self.transform = transform 59 | if filter is not None: 60 | self.filter = filter 61 | self.separator = separator 62 | 63 | link_kwargs = None 64 | if callable(linkify_item): 65 | link_kwargs = dict(url=linkify_item) 66 | elif isinstance(linkify_item, (dict, tuple)): 67 | link_kwargs = dict(reverse_args=linkify_item) 68 | elif linkify_item is True: 69 | link_kwargs = dict() 70 | 71 | if link_kwargs is not None: 72 | self.linkify_item = LinkTransform(attrs=self.attrs.get("a", {}), **link_kwargs) 73 | 74 | def transform(self, obj): 75 | """Apply to each item of the list of objects from the ManyToMany relation.""" 76 | return force_str(obj) 77 | 78 | def filter(self, qs): 79 | """Call on the ManyRelatedManager to allow ordering, filtering or limiting on the set of related objects.""" 80 | return qs.all() 81 | 82 | def render(self, value): 83 | items = [] 84 | for item in self.filter(value): 85 | content = conditional_escape(self.transform(item)) 86 | if hasattr(self, "linkify_item"): 87 | content = self.linkify_item(content=content, record=item) 88 | 89 | items.append(content) 90 | 91 | return mark_safe(conditional_escape(self.separator).join(items)) 92 | 93 | @classmethod 94 | def from_field(cls, field, **kwargs): 95 | if isinstance(field, models.ManyToManyField): 96 | return cls(**kwargs) 97 | -------------------------------------------------------------------------------- /django_tables2/columns/templatecolumn.py: -------------------------------------------------------------------------------- 1 | from django.template import Context, Template 2 | from django.template.loader import get_template 3 | from django.utils.html import strip_tags 4 | 5 | from .base import Column, library 6 | 7 | 8 | @library.register 9 | class TemplateColumn(Column): 10 | """ 11 | A subclass of `.Column` that renders some template code to use as the cell value. 12 | 13 | Arguments: 14 | template_code (str): template code to render 15 | template_name (str): name of the template to render 16 | extra_context (dict): optional extra template context 17 | 18 | A `~django.template.Template` object is created from the 19 | *template_code* or *template_name* and rendered with a context containing: 20 | 21 | - *record* -- data record for the current row 22 | - *value* -- value from `record` that corresponds to the current column 23 | - *default* -- appropriate default value to use as fallback. 24 | - *row_counter* -- The number of the row this cell is being rendered in. 25 | - any context variables passed using the `extra_context` argument to `TemplateColumn`. 26 | 27 | Example: 28 | 29 | .. code-block:: python 30 | 31 | class ExampleTable(tables.Table): 32 | foo = tables.TemplateColumn("{{ record.bar }}") 33 | # contents of `myapp/bar_column.html` is `{{ label }}: {{ value }}` 34 | bar = tables.TemplateColumn(template_name="myapp/name2_column.html", 35 | extra_context={"label": "Label"}) 36 | 37 | Both columns will have the same output. 38 | """ 39 | 40 | empty_values = () 41 | 42 | def __init__(self, template_code=None, template_name=None, extra_context=None, **extra): 43 | super().__init__(**extra) 44 | self.template_code = template_code 45 | self.template_name = template_name 46 | self.extra_context = extra_context or {} 47 | 48 | if not self.template_code and not self.template_name: 49 | raise ValueError("A template must be provided") 50 | 51 | def render(self, record, table, value, bound_column, **kwargs): 52 | # If the table is being rendered using `render_table`, it hackily 53 | # attaches the context to the table as a gift to `TemplateColumn`. 54 | context = getattr(table, "context", Context()) 55 | additional_context = { 56 | "default": bound_column.default, 57 | "column": bound_column, 58 | "record": record, 59 | "value": value, 60 | "row_counter": kwargs["bound_row"].row_counter, 61 | } 62 | additional_context.update(self.extra_context) 63 | with context.update(additional_context): 64 | if self.template_code: 65 | return Template(self.template_code).render(context) 66 | else: 67 | return get_template(self.template_name).render(context.flatten()) 68 | 69 | def value(self, **kwargs): 70 | """ 71 | Non-HTML value returned from a call to `value()` on a `TemplateColumn`. 72 | 73 | By default this is the rendered template with `django.utils.html.strip_tags` applied. 74 | Leading and trailing whitespace is stripped. 75 | """ 76 | html = super().value(**kwargs) 77 | return strip_tags(html).strip() if isinstance(html, str) else html 78 | -------------------------------------------------------------------------------- /django_tables2/columns/timecolumn.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .base import library 4 | from .templatecolumn import TemplateColumn 5 | 6 | 7 | @library.register 8 | class TimeColumn(TemplateColumn): 9 | """ 10 | A column that renders times in the local timezone. 11 | 12 | Arguments: 13 | format (str): format string in same format as Django's ``time`` template filter (optional). 14 | short (bool): if *format* is not specified, use Django's ``TIME_FORMAT`` setting. 15 | """ 16 | 17 | def __init__(self, format=None, *args, **kwargs): 18 | if format is None: 19 | format = "TIME_FORMAT" 20 | template = '{{ value|date:"%s"|default:default }}' % format # noqa: UP031 21 | super().__init__(template_code=template, *args, **kwargs) 22 | 23 | @classmethod 24 | def from_field(cls, field, **kwargs): 25 | if isinstance(field, models.TimeField): 26 | return cls(**kwargs) 27 | -------------------------------------------------------------------------------- /django_tables2/columns/urlcolumn.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .base import library 4 | from .linkcolumn import BaseLinkColumn 5 | 6 | 7 | @library.register 8 | class URLColumn(BaseLinkColumn): 9 | """ 10 | Renders URL values as hyperlinks. 11 | 12 | Arguments: 13 | text (str or callable): Either static text, or a callable. If set, this 14 | will be used to render the text inside link instead of value (default) 15 | attrs (dict): Additional attributes for the ```` tag 16 | 17 | Example:: 18 | 19 | >>> class CompaniesTable(tables.Table): 20 | ... link = tables.URLColumn() 21 | ... 22 | >>> table = CompaniesTable([{"link": "http://google.com"}]) 23 | >>> table.rows[0].get_cell("link") 24 | 'http://google.com' 25 | """ 26 | 27 | def get_url(self, value): 28 | return value 29 | 30 | @classmethod 31 | def from_field(cls, field, **kwargs): 32 | if isinstance(field, models.URLField): 33 | return cls(**kwargs) 34 | -------------------------------------------------------------------------------- /django_tables2/config.py: -------------------------------------------------------------------------------- 1 | from django.core.paginator import EmptyPage, PageNotAnInteger 2 | 3 | 4 | class RequestConfig: 5 | """ 6 | A configurator that uses request data to setup a table. 7 | 8 | A single RequestConfig can be used for multiple tables in one view. 9 | 10 | Arguments: 11 | paginate (dict or bool): Indicates whether to paginate, and if so, what 12 | default values to use. If the value evaluates to `False`, pagination 13 | will be disabled. A `dict` can be used to specify default values for 14 | the call to `~.tables.Table.paginate` (e.g. to define a default 15 | `per_page` value). 16 | 17 | A special *silent* item can be used to enable automatic handling of 18 | pagination exceptions using the following logic: 19 | 20 | - If `~django.core.paginator.PageNotAnInteger` is raised, show the first page. 21 | - If `~django.core.paginator.EmptyPage` is raised, show the last page. 22 | 23 | For example, to use `~.LazyPaginator`:: 24 | 25 | RequestConfig(paginate={"paginator_class": LazyPaginator}).configure(table) 26 | 27 | """ 28 | 29 | def __init__(self, request, paginate=True): 30 | self.request = request 31 | self.paginate = paginate 32 | 33 | def configure(self, table): 34 | """ 35 | Configure a table using information from the request. 36 | 37 | Arguments: 38 | table (`~.Table`): table to be configured 39 | """ 40 | table.request = self.request 41 | 42 | order_by = self.request.GET.getlist(table.prefixed_order_by_field) 43 | if order_by: 44 | table.order_by = order_by 45 | if self.paginate: 46 | if hasattr(self.paginate, "items"): 47 | kwargs = dict(self.paginate) 48 | else: 49 | kwargs = {} 50 | # extract some options from the request 51 | for arg in ("page", "per_page"): 52 | name = getattr(table, f"prefixed_{arg}_field") 53 | try: 54 | kwargs[arg] = int(self.request.GET[name]) 55 | except (ValueError, KeyError): 56 | pass 57 | 58 | silent = kwargs.pop("silent", True) 59 | if not silent: 60 | table.paginate(**kwargs) 61 | else: 62 | try: 63 | table.paginate(**kwargs) 64 | except PageNotAnInteger: 65 | table.page = table.paginator.page(1) 66 | except EmptyPage: 67 | table.page = table.paginator.page(table.paginator.num_pages) 68 | 69 | return table 70 | -------------------------------------------------------------------------------- /django_tables2/export/__init__.py: -------------------------------------------------------------------------------- 1 | from .export import TableExport 2 | from .views import ExportMixin 3 | 4 | __all__ = ("TableExport", "ExportMixin") 5 | -------------------------------------------------------------------------------- /django_tables2/export/export.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | from django.http import HttpResponse 3 | 4 | try: 5 | from tablib import Dataset 6 | except ImportError: # pragma: no cover 7 | raise ImproperlyConfigured( 8 | "You must have tablib installed in order to use the django-tables2 export functionality" 9 | ) 10 | 11 | 12 | class TableExport: 13 | """ 14 | Export data from a table to the file type specified. 15 | 16 | Arguments: 17 | export_format (str): one of `csv, json, latex, ods, tsv, xls, xlsx, yaml` 18 | 19 | table (`~.Table`): instance of the table to export the data from 20 | 21 | exclude_columns (iterable): list of column names to exclude from the export 22 | 23 | dataset_kwargs (dictionary): passed as `**kwargs` to `tablib.Dataset` constructor 24 | 25 | """ 26 | 27 | CSV = "csv" 28 | JSON = "json" 29 | LATEX = "latex" 30 | ODS = "ods" 31 | TSV = "tsv" 32 | XLS = "xls" 33 | XLSX = "xlsx" 34 | YAML = "yaml" 35 | 36 | FORMATS = { 37 | CSV: "text/csv; charset=utf-8", 38 | JSON: "application/json", 39 | LATEX: "text/plain", 40 | ODS: "application/vnd.oasis.opendocument.spreadsheet", 41 | TSV: "text/tsv; charset=utf-8", 42 | XLS: "application/vnd.ms-excel", 43 | XLSX: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 44 | YAML: "text/yaml; charset=utf-8", 45 | } 46 | 47 | def __init__(self, export_format, table, exclude_columns=None, dataset_kwargs=None): 48 | if not self.is_valid_format(export_format): 49 | raise TypeError(f'Export format "{export_format}" is not supported.') 50 | 51 | self.format = export_format 52 | self.dataset = self.table_to_dataset(table, exclude_columns, dataset_kwargs) 53 | 54 | def table_to_dataset(self, table, exclude_columns, dataset_kwargs=None): 55 | """Transform a table to a tablib dataset.""" 56 | 57 | def default_dataset_title(): 58 | try: 59 | return table.Meta.model._meta.verbose_name_plural.title() 60 | except AttributeError: 61 | return "Export Data" 62 | 63 | kwargs = {"title": default_dataset_title()} 64 | kwargs.update(dataset_kwargs or {}) 65 | dataset = Dataset(**kwargs) 66 | for i, row in enumerate(table.as_values(exclude_columns=exclude_columns)): 67 | if i == 0: 68 | dataset.headers = row 69 | else: 70 | dataset.append(row) 71 | return dataset 72 | 73 | @classmethod 74 | def is_valid_format(self, export_format): 75 | """Return True if `export_format` is one of the supported export formats.""" 76 | return export_format is not None and export_format in TableExport.FORMATS.keys() 77 | 78 | def content_type(self): 79 | """Return the content type for the current export format.""" 80 | return self.FORMATS[self.format] 81 | 82 | def export(self): 83 | """Return the string/bytes for the current export format.""" 84 | return self.dataset.export(self.format) 85 | 86 | def response(self, filename=None): 87 | """ 88 | Build and return a `HttpResponse` containing the exported data. 89 | 90 | Arguments: 91 | filename (str): if not `None`, the filename is attached to the 92 | `Content-Disposition` header of the response. 93 | """ 94 | response = HttpResponse(content_type=self.content_type()) 95 | if filename is not None: 96 | response["Content-Disposition"] = f'attachment; filename="{filename}"' 97 | 98 | response.write(self.export()) 99 | return response 100 | -------------------------------------------------------------------------------- /django_tables2/export/views.py: -------------------------------------------------------------------------------- 1 | from .export import TableExport 2 | 3 | 4 | class ExportMixin: 5 | """ 6 | Support various export formats for the table data. 7 | 8 | `ExportMixin` looks for some attributes on the class to change it's behavior: 9 | 10 | Attributes: 11 | export_class (TableExport): Allows using a custom implementation of `TableExport`. 12 | export_name (str): is the name of file that will be exported, without extension. 13 | export_trigger_param (str): is the name of the GET attribute used to trigger 14 | the export. It's value decides the export format, refer to 15 | `TableExport` for a list of available formats. 16 | exclude_columns (iterable): column names excluded from the export. 17 | For example, one might want to exclude columns containing buttons from 18 | the export. Excluding columns from the export is also possible using the 19 | `exclude_from_export` argument to the `.Column` constructor:: 20 | 21 | class Table(tables.Table): 22 | name = tables.Column() 23 | buttons = tables.TemplateColumn(exclude_from_export=True, template_name=...) 24 | export_formats (iterable): export formats to render a set of buttons in the template. 25 | dataset_kwargs (dictionary): passed as `**kwargs` to `tablib.Dataset` constructor:: 26 | 27 | dataset_kwargs = {"tite": "My custom tab title"} 28 | """ 29 | 30 | export_class = TableExport 31 | export_name = "table" 32 | export_trigger_param = "_export" 33 | exclude_columns = () 34 | dataset_kwargs = None 35 | 36 | export_formats = (TableExport.CSV,) 37 | 38 | def get_export_filename(self, export_format): 39 | return f"{self.export_name}.{export_format}" 40 | 41 | def get_dataset_kwargs(self): 42 | return self.dataset_kwargs 43 | 44 | def create_export(self, export_format): 45 | exporter = self.export_class( 46 | export_format=export_format, 47 | table=self.get_table(**self.get_table_kwargs()), 48 | exclude_columns=self.exclude_columns, 49 | dataset_kwargs=self.get_dataset_kwargs(), 50 | ) 51 | 52 | return exporter.response(filename=self.get_export_filename(export_format)) 53 | 54 | def render_to_response(self, context, **kwargs): 55 | export_format = self.request.GET.get(self.export_trigger_param, None) 56 | if self.export_class.is_valid_format(export_format): 57 | return self.create_export(export_format) 58 | 59 | return super().render_to_response(context, **kwargs) 60 | -------------------------------------------------------------------------------- /django_tables2/locale/cs/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/cs/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/cs/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PACKAGE VERSION\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 11 | "PO-Revision-Date: 2018-01-22 08:21+0100\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | "Language: cs\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" 19 | 20 | #: templates/django_tables2/bootstrap.html:64 21 | #: templates/django_tables2/bootstrap4.html:64 22 | #: templates/django_tables2/table.html:61 23 | msgid "previous" 24 | msgstr "předchozí" 25 | 26 | #: templates/django_tables2/bootstrap.html:89 27 | #: templates/django_tables2/bootstrap4.html:82 28 | #: templates/django_tables2/table.html:82 29 | msgid "next" 30 | msgstr "další" 31 | -------------------------------------------------------------------------------- /django_tables2/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 11 | "PO-Revision-Date: 2015-04-09 12:45+0100\n" 12 | "Last-Translator: Tim Schneider \n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 1.7.5\n" 20 | 21 | #: templates/django_tables2/bootstrap.html:64 22 | #: templates/django_tables2/bootstrap4.html:64 23 | #: templates/django_tables2/table.html:61 24 | msgid "previous" 25 | msgstr "zurück" 26 | 27 | #: templates/django_tables2/bootstrap.html:89 28 | #: templates/django_tables2/bootstrap4.html:82 29 | #: templates/django_tables2/table.html:82 30 | msgid "next" 31 | msgstr "weiter" 32 | -------------------------------------------------------------------------------- /django_tables2/locale/el/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/el/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/el/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | # 3 | #, fuzzy 4 | msgid "" 5 | msgstr "" 6 | "Project-Id-Version: django-tables2\n" 7 | "Report-Msgid-Bugs-To: \n" 8 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 9 | "PO-Revision-Date: 2013-03-19 21:56+0200\n" 10 | "Last-Translator: Serafeim Papastefanos \n" 11 | "Language-Team: el \n" 12 | "Language: el\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | 17 | #: templates/django_tables2/bootstrap.html:64 18 | #: templates/django_tables2/bootstrap4.html:64 19 | #: templates/django_tables2/table.html:61 20 | msgid "previous" 21 | msgstr "προηγούμενη" 22 | 23 | #: templates/django_tables2/bootstrap.html:89 24 | #: templates/django_tables2/bootstrap4.html:82 25 | #: templates/django_tables2/table.html:82 26 | msgid "next" 27 | msgstr "επόμενη" 28 | -------------------------------------------------------------------------------- /django_tables2/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | # 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: django-tables2\n" 6 | "Report-Msgid-Bugs-To: \n" 7 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 8 | "PO-Revision-Date: 2011-11-06 10:41+1000\n" 9 | "Last-Translator: Bradley Ayers \n" 10 | "Language-Team: English \n" 11 | "Language: en_US\n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | 16 | #: templates/django_tables2/bootstrap.html:64 17 | #: templates/django_tables2/bootstrap4.html:64 18 | #: templates/django_tables2/table.html:61 19 | msgid "previous" 20 | msgstr "previous" 21 | 22 | #: templates/django_tables2/bootstrap.html:89 23 | #: templates/django_tables2/bootstrap4.html:82 24 | #: templates/django_tables2/table.html:82 25 | msgid "next" 26 | msgstr "next" 27 | -------------------------------------------------------------------------------- /django_tables2/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | # 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: django-tables2\n" 6 | "Report-Msgid-Bugs-To: \n" 7 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 8 | "PO-Revision-Date: 2013-08-21 07:06-0500\n" 9 | "Last-Translator: Pablo Martín \n" 10 | "Language-Team: LANGUAGE \n" 11 | "Language: es\n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | 16 | #: templates/django_tables2/bootstrap.html:64 17 | #: templates/django_tables2/bootstrap4.html:64 18 | #: templates/django_tables2/table.html:61 19 | msgid "previous" 20 | msgstr "anterior" 21 | 22 | #: templates/django_tables2/bootstrap.html:89 23 | #: templates/django_tables2/bootstrap4.html:82 24 | #: templates/django_tables2/table.html:82 25 | msgid "next" 26 | msgstr "siguiente" 27 | -------------------------------------------------------------------------------- /django_tables2/locale/fa/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/fa/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/fa/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | # 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: \n" 6 | "POT-Creation-Date: 2021-05-24 14:23+0430\n" 7 | "PO-Revision-Date: 2021-05-24 14:31+0430\n" 8 | "Last-Translator: \n" 9 | "Language-Team: \n" 10 | "Language: fa\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "X-Generator: Poedit 2.4.1\n" 15 | 16 | #: paginators.py:72 17 | msgid "That page number is not an integer" 18 | msgstr "شماره صفحه عددی صحیح نیست" 19 | 20 | #: paginators.py:74 21 | msgid "That page number is less than 1" 22 | msgstr "شماره صفحه عددی کمتر از 1 است" 23 | 24 | #: paginators.py:93 25 | msgid "That page contains no results" 26 | msgstr "صفحه شامل هیچ نتیجه ای نیست" 27 | 28 | #: templates/django_tables2/bootstrap.html:66 29 | #: templates/django_tables2/bootstrap4.html:66 30 | #: templates/django_tables2/table.html:64 31 | msgid "previous" 32 | msgstr "قبلی" 33 | 34 | #: templates/django_tables2/bootstrap.html:91 35 | #: templates/django_tables2/bootstrap4.html:86 36 | #: templates/django_tables2/table.html:88 37 | msgid "next" 38 | msgstr "بعدی" 39 | -------------------------------------------------------------------------------- /django_tables2/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: fr \n" 15 | "Language: fr\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" 20 | 21 | #: templates/django_tables2/bootstrap.html:64 22 | #: templates/django_tables2/bootstrap4.html:64 23 | #: templates/django_tables2/table.html:61 24 | msgid "previous" 25 | msgstr "précédent" 26 | 27 | #: templates/django_tables2/bootstrap.html:89 28 | #: templates/django_tables2/bootstrap4.html:82 29 | #: templates/django_tables2/table.html:82 30 | msgid "next" 31 | msgstr "suivant" 32 | -------------------------------------------------------------------------------- /django_tables2/locale/hu/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/hu/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/hu/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | # 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: django-tables2\n" 6 | "Report-Msgid-Bugs-To: \n" 7 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 8 | "PO-Revision-Date: 2017-08-13 14:19+0200\n" 9 | "Last-Translator: Miklos Horvath \n" 10 | "Language-Team: Hungarian \n" 11 | "Language: hu\n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "X-Generator: Poedit 1.8.12\n" 16 | 17 | #: templates/django_tables2/bootstrap.html:64 18 | #: templates/django_tables2/bootstrap4.html:64 19 | #: templates/django_tables2/table.html:61 20 | msgid "previous" 21 | msgstr "előző" 22 | 23 | #: templates/django_tables2/bootstrap.html:89 24 | #: templates/django_tables2/bootstrap4.html:82 25 | #: templates/django_tables2/table.html:82 26 | msgid "next" 27 | msgstr "következő" 28 | -------------------------------------------------------------------------------- /django_tables2/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: django-tables2\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 7 | "PO-Revision-Date: 2016-04-14 11:21+0200\n" 8 | "Last-Translator: Paolo Dina \n" 9 | "Language-Team: Italian \n" 10 | "Language: it\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 15 | 16 | #: templates/django_tables2/bootstrap.html:64 17 | #: templates/django_tables2/bootstrap4.html:64 18 | #: templates/django_tables2/table.html:61 19 | msgid "previous" 20 | msgstr "indietro" 21 | 22 | #: templates/django_tables2/bootstrap.html:89 23 | #: templates/django_tables2/bootstrap4.html:82 24 | #: templates/django_tables2/table.html:82 25 | msgid "next" 26 | msgstr "avanti" 27 | -------------------------------------------------------------------------------- /django_tables2/locale/lt/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/lt/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/lt/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-tables2\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2020-07-28 12:40+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Robertas Murnikovas \n" 14 | "Language-Team: Lithuanian \n" 15 | "Language: lt\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < " 20 | "11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? " 21 | "1 : n % 1 != 0 ? 2: 3);\n" 22 | 23 | #: paginators.py:72 24 | msgid "That page number is not an integer" 25 | msgstr "Puslapio numeris nėra sveikasis skaičius" 26 | 27 | #: paginators.py:74 28 | msgid "That page number is less than 1" 29 | msgstr "Puslapio numeris yra mažesnis už 1" 30 | 31 | #: paginators.py:93 32 | msgid "That page contains no results" 33 | msgstr "Puslapyje nerasta jokių rezultatų" 34 | 35 | #: templates/django_tables2/bootstrap.html:66 36 | #: templates/django_tables2/bootstrap4.html:66 37 | #: templates/django_tables2/table.html:64 38 | msgid "previous" 39 | msgstr "ankstesnis" 40 | 41 | #: templates/django_tables2/bootstrap.html:91 42 | #: templates/django_tables2/bootstrap4.html:86 43 | #: templates/django_tables2/table.html:88 44 | msgid "next" 45 | msgstr "kitas" 46 | -------------------------------------------------------------------------------- /django_tables2/locale/nb/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/nb/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/nb/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | # 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: django-tables2\n" 6 | "Report-Msgid-Bugs-To: \n" 7 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 8 | "PO-Revision-Date: 2016-07-22 11:11+0200\n" 9 | "Last-Translator: Andreas Tollånes\n" 10 | "Language-Team: Norwegian Bokmal \n" 11 | "Language: nb\n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | 16 | #: templates/django_tables2/bootstrap.html:64 17 | #: templates/django_tables2/bootstrap4.html:64 18 | #: templates/django_tables2/table.html:61 19 | msgid "previous" 20 | msgstr "forrige" 21 | 22 | #: templates/django_tables2/bootstrap.html:89 23 | #: templates/django_tables2/bootstrap4.html:82 24 | #: templates/django_tables2/table.html:82 25 | msgid "next" 26 | msgstr "neste" 27 | -------------------------------------------------------------------------------- /django_tables2/locale/nl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/nl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/nl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: django-tables2\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 7 | "PO-Revision-Date: 2016-04-19 10:21+0200\n" 8 | "Last-Translator: Jan Pieter Waagmeester \n" 9 | "Language-Team: Dutch \n" 10 | "Language: nl\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 15 | 16 | #: templates/django_tables2/bootstrap.html:64 17 | #: templates/django_tables2/bootstrap4.html:64 18 | #: templates/django_tables2/table.html:61 19 | msgid "previous" 20 | msgstr "vorige" 21 | 22 | #: templates/django_tables2/bootstrap.html:89 23 | #: templates/django_tables2/bootstrap4.html:82 24 | #: templates/django_tables2/table.html:82 25 | msgid "next" 26 | msgstr "volgende" 27 | -------------------------------------------------------------------------------- /django_tables2/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/pl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 0.1\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 11 | "PO-Revision-Date: 2013-08-22 09:57+0100\n" 12 | "Last-Translator: Michał Pasternak \n" 13 | "Language-Team: Polish \n" 14 | "Language: pl\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " 19 | "|| n%100>=20) ? 1 : 2);\n" 20 | "X-Generator: Poedit 1.5.5\n" 21 | 22 | #: templates/django_tables2/bootstrap.html:64 23 | #: templates/django_tables2/bootstrap4.html:64 24 | #: templates/django_tables2/table.html:61 25 | msgid "previous" 26 | msgstr "poprzednia" 27 | 28 | #: templates/django_tables2/bootstrap.html:89 29 | #: templates/django_tables2/bootstrap4.html:82 30 | #: templates/django_tables2/table.html:82 31 | msgid "next" 32 | msgstr "następna" 33 | -------------------------------------------------------------------------------- /django_tables2/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/pt_BR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | # Fabio C. Barrionuevo da Luz , 2014. 3 | # 4 | #, fuzzy 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: 0.14.0\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 10 | "PO-Revision-Date: 2014-02-02 00:44-0300\n" 11 | "Last-Translator: Fabio C. Barrionuevo da Luz \n" 12 | "Language-Team: Portuguese (Brazil) \n" 13 | "Language: pt_BR\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 18 | 19 | #: templates/django_tables2/bootstrap.html:64 20 | #: templates/django_tables2/bootstrap4.html:64 21 | #: templates/django_tables2/table.html:61 22 | msgid "previous" 23 | msgstr "anterior" 24 | 25 | #: templates/django_tables2/bootstrap.html:89 26 | #: templates/django_tables2/bootstrap4.html:82 27 | #: templates/django_tables2/table.html:82 28 | msgid "next" 29 | msgstr "próximo" 30 | -------------------------------------------------------------------------------- /django_tables2/locale/pt_PT/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/pt_PT/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/pt_PT/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | # 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: django-tables2\n" 6 | "Report-Msgid-Bugs-To: \n" 7 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 8 | "PO-Revision-Date: 2011-11-06 10:41+1000\n" 9 | "Last-Translator: Bradley Ayers \n" 10 | "Language-Team: European Portuguese \n" 11 | "Language: pt_PT\n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | 16 | #: templates/django_tables2/bootstrap.html:64 17 | #: templates/django_tables2/bootstrap4.html:64 18 | #: templates/django_tables2/table.html:61 19 | msgid "previous" 20 | msgstr "Anterior" 21 | 22 | #: templates/django_tables2/bootstrap.html:89 23 | #: templates/django_tables2/bootstrap4.html:82 24 | #: templates/django_tables2/table.html:82 25 | msgid "next" 26 | msgstr "seguinte" 27 | -------------------------------------------------------------------------------- /django_tables2/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 11 | "PO-Revision-Date: 2020-09-18 12:51+0600\n" 12 | "Last-Translator: Andrii Pryz \n" 13 | "Language-Team: Russian \n" 14 | "Language: ru\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 20 | "X-Generator: Poedit 2.4.1\n" 21 | 22 | #: templates/django_tables2/bootstrap.html:64 23 | #: templates/django_tables2/bootstrap4.html:64 24 | #: templates/django_tables2/table.html:61 25 | msgid "previous" 26 | msgstr "предыдущая" 27 | 28 | #: templates/django_tables2/bootstrap.html:89 29 | #: templates/django_tables2/bootstrap4.html:82 30 | #: templates/django_tables2/table.html:82 31 | msgid "next" 32 | msgstr "следующая" 33 | -------------------------------------------------------------------------------- /django_tables2/locale/sv/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/sv/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/sv/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-tables2 package 2 | # 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: django-tables2\n" 6 | "Report-Msgid-Bugs-To: \n" 7 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 8 | "PO-Revision-Date: 2014-12-04 10:25+0100\n" 9 | "Last-Translator: Petter Jönsson \n" 10 | "Language-Team: Swedish \n" 11 | "Language: sv\n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | 16 | #: templates/django_tables2/bootstrap.html:64 17 | #: templates/django_tables2/bootstrap4.html:64 18 | #: templates/django_tables2/table.html:61 19 | msgid "previous" 20 | msgstr "föregående" 21 | 22 | #: templates/django_tables2/bootstrap.html:89 23 | #: templates/django_tables2/bootstrap4.html:82 24 | #: templates/django_tables2/table.html:82 25 | msgid "next" 26 | msgstr "nästa" 27 | -------------------------------------------------------------------------------- /django_tables2/locale/uk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/uk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/uk/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Andrii Pryz \n" 14 | "Language-Team: Ukrainian \n" 15 | "Language: uk\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 21 | 22 | #: templates/django_tables2/bootstrap.html:64 23 | #: templates/django_tables2/bootstrap4.html:64 24 | #: templates/django_tables2/table.html:61 25 | msgid "previous" 26 | msgstr "попередня" 27 | 28 | #: templates/django_tables2/bootstrap.html:89 29 | #: templates/django_tables2/bootstrap4.html:82 30 | #: templates/django_tables2/table.html:82 31 | msgid "next" 32 | msgstr "наступна" 33 | -------------------------------------------------------------------------------- /django_tables2/locale/zh_Hans/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/locale/zh_Hans/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_tables2/locale/zh_Hans/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: \n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-04-12 10:06+0200\n" 12 | "PO-Revision-Date: 2018-03-19 16:20+0800\n" 13 | "Last-Translator: Zhong Chang<726608501@qq.com>\n" 14 | "Language-Team: Simplified Chinese\n" 15 | "Language: zh_Hans\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: templates/django_tables2/bootstrap.html:64 21 | #: templates/django_tables2/bootstrap4.html:64 22 | #: templates/django_tables2/table.html:61 23 | msgid "previous" 24 | msgstr "上一页" 25 | 26 | #: templates/django_tables2/bootstrap.html:89 27 | #: templates/django_tables2/bootstrap4.html:82 28 | #: templates/django_tables2/table.html:82 29 | msgid "next" 30 | msgstr "下一页" 31 | -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/bootstrap.css: -------------------------------------------------------------------------------- 1 | .table-container th.asc:after { 2 | content: '\0000a0\0025b2'; 3 | float: right; 4 | } 5 | .table-container th.desc:after { 6 | content: '\0000a0\0025bc'; 7 | float: right; 8 | } 9 | -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/themes/paleblue/css/screen.css: -------------------------------------------------------------------------------- 1 | table.paleblue { 2 | border-collapse: collapse; 3 | border-color: #CCC; 4 | border: 1px solid #DDD; 5 | } 6 | 7 | table.paleblue, 8 | table.paleblue + ul.pagination { 9 | font: normal 11px/14px 'Lucida Grande', Verdana, Helvetica, Arial, sans-serif; 10 | } 11 | 12 | table.paleblue a:link, 13 | table.paleblue a:visited, 14 | table.paleblue + ul.pagination > li > a { 15 | color: #5B80B2; 16 | text-decoration: none; 17 | font-weight: bold; 18 | } 19 | 20 | table.paleblue a:hover { 21 | color: #036; 22 | } 23 | 24 | table.paleblue td, 25 | table.paleblue th { 26 | padding: 5px; 27 | line-height: 13px; 28 | border-bottom: 1px solid #EEE; 29 | border-left: 1px solid #DDD; 30 | text-align: left; 31 | } 32 | 33 | table.paleblue thead th:first-child, 34 | table.paleblue thead td:first-child { 35 | border-left: none !important; 36 | } 37 | 38 | table.paleblue thead th, 39 | table.paleblue thead td { 40 | background: #FCFCFC url(../img/header-bg.png) left bottom repeat-x; 41 | border-bottom: 1px solid #DDD; 42 | padding: 2px 5px; 43 | font-size: 11px; 44 | vertical-align: middle; 45 | color: #666; 46 | } 47 | 48 | table.paleblue thead th > a:link, 49 | table.paleblue thead th > a:visited { 50 | color: #666; 51 | } 52 | 53 | table.paleblue thead th.orderable > a { 54 | padding-right: 20px; 55 | background: url(../img/arrow-inactive-up.png) right center no-repeat; 56 | } 57 | table.paleblue thead th.orderable.asc > a { 58 | background-image: url(../img/arrow-active-up.png); 59 | } 60 | table.paleblue thead th.orderable.desc > a { 61 | background-image: url(../img/arrow-active-down.png); 62 | } 63 | 64 | table.paleblue tr.odd { 65 | background-color: #EDF3FE; 66 | } 67 | 68 | table.paleblue tr.even { 69 | background-color: white; 70 | } 71 | 72 | table.paleblue + ul.pagination { 73 | background: white url(../img/pagination-bg.gif) left 180% repeat-x; 74 | overflow: auto; 75 | margin: 0; 76 | padding: 10px; 77 | border: 1px solid #DDD; 78 | list-style: none; 79 | } 80 | 81 | table.paleblue + ul.pagination > li { 82 | float: left; 83 | line-height: 22px; 84 | margin-left: 10px; 85 | } 86 | 87 | table.paleblue + ul.pagination > li:first-child { 88 | margin-left: 0; 89 | } 90 | 91 | table.paleblue + ul.pagination > li.cardinality { 92 | float: right; 93 | color: #8d8d8d; 94 | } 95 | 96 | table.paleblue > tbody > tr > td > span.true, 97 | table.paleblue > tbody > tr > td > span.false { 98 | background-position: top left; 99 | background-repeat: no-repeat; 100 | display: inline-block; 101 | height: 10px; 102 | overflow: hidden; 103 | text-indent: -200px; 104 | width: 10px; 105 | } 106 | 107 | table.paleblue > tbody > tr > td > .missing { 108 | background: transparent url(../img/missing.png) right center no-repeat; 109 | color: #717171; 110 | padding-right: 20px; 111 | } 112 | 113 | table.paleblue > tbody > tr > td > .missing:hover { 114 | color: #333; 115 | } 116 | 117 | table.paleblue > tbody > tr > td > span.true { 118 | background-image: url(../img/true.gif); 119 | } 120 | 121 | table.paleblue > tbody > tr > td > span.false { 122 | background-image: url(../img/false.gif); 123 | } 124 | 125 | div.table-container { 126 | display: inline-block; 127 | } 128 | -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/themes/paleblue/img/arrow-active-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/static/django_tables2/themes/paleblue/img/arrow-active-down.png -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/themes/paleblue/img/arrow-active-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/static/django_tables2/themes/paleblue/img/arrow-active-up.png -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/themes/paleblue/img/arrow-inactive-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/static/django_tables2/themes/paleblue/img/arrow-inactive-down.png -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/themes/paleblue/img/arrow-inactive-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/static/django_tables2/themes/paleblue/img/arrow-inactive-up.png -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/themes/paleblue/img/false.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/static/django_tables2/themes/paleblue/img/false.gif -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/themes/paleblue/img/header-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/static/django_tables2/themes/paleblue/img/header-bg.png -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/themes/paleblue/img/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/static/django_tables2/themes/paleblue/img/missing.png -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/themes/paleblue/img/pagination-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/static/django_tables2/themes/paleblue/img/pagination-bg.gif -------------------------------------------------------------------------------- /django_tables2/static/django_tables2/themes/paleblue/img/true.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/static/django_tables2/themes/paleblue/img/true.gif -------------------------------------------------------------------------------- /django_tables2/templates/django_tables2/bootstrap-responsive.html: -------------------------------------------------------------------------------- 1 | {% extends 'django_tables2/bootstrap.html' %} 2 | 3 | {% block table-wrapper %} 4 |
5 | {% block table %} 6 | {{ block.super }} 7 | {% endblock table %} 8 | 9 | {% block pagination %} 10 | {% if table.page and table.paginator.num_pages > 1 %} 11 | {{ block.super }} 12 | {% endif %} 13 | {% endblock pagination %} 14 |
15 | {% endblock table-wrapper %} 16 | -------------------------------------------------------------------------------- /django_tables2/templates/django_tables2/bootstrap4-responsive.html: -------------------------------------------------------------------------------- 1 | {% extends 'django_tables2/bootstrap4.html' %} 2 | 3 | {% block table-wrapper %} 4 |
5 | {% block table %} 6 | {{ block.super }} 7 | {% endblock table %} 8 | 9 | {% block pagination %} 10 | {% if table.page and table.paginator.num_pages > 1 %} 11 | {{ block.super }} 12 | {% endif %} 13 | {% endblock pagination %} 14 |
15 | {% endblock table-wrapper %} 16 | -------------------------------------------------------------------------------- /django_tables2/templates/django_tables2/bootstrap5-responsive.html: -------------------------------------------------------------------------------- 1 | {% extends 'django_tables2/bootstrap5.html' %} 2 | 3 | {% block table-wrapper %} 4 |
5 | {% block table %} 6 | {{ block.super }} 7 | {% endblock table %} 8 | 9 | {% block pagination %} 10 | {{ block.super }} 11 | {% endblock pagination %} 12 |
13 | {% endblock table-wrapper %} 14 | -------------------------------------------------------------------------------- /django_tables2/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/django_tables2/templatetags/__init__.py -------------------------------------------------------------------------------- /docs/_static/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/docs/_static/example.png -------------------------------------------------------------------------------- /docs/_static/tutorial-bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/docs/_static/tutorial-bootstrap.png -------------------------------------------------------------------------------- /docs/_static/tutorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/docs/_static/tutorial.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | from pathlib import Path 5 | 6 | import sphinx_rtd_theme 7 | 8 | os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings" 9 | 10 | # import project 11 | sys.path.insert(0, str(Path("../").resolve())) 12 | 13 | project = "django-tables2" 14 | with open("../django_tables2/__init__.py", "rb") as f: 15 | release = str(re.search('__version__ = "(.+?)"', f.read().decode()).group(1)) 16 | version = release.rpartition(".")[0] 17 | 18 | 19 | default_role = "py:obj" 20 | 21 | # symlink CHANGELOG.md from repo root to the pages dir. 22 | basedir = Path(__file__).parent.parent 23 | filename = "CHANGELOG.md" 24 | target = basedir / "docs" / "pages" / filename 25 | if not target.is_symlink(): 26 | target.symlink_to(basedir / filename) 27 | 28 | extensions = [ 29 | "sphinx.ext.autodoc", 30 | "sphinx.ext.intersphinx", 31 | "sphinx.ext.napoleon", 32 | "sphinx.ext.viewcode", 33 | "sphinx.ext.doctest", 34 | "sphinxcontrib.jquery", 35 | "sphinxcontrib.spelling", 36 | "myst_parser", 37 | ] 38 | 39 | intersphinx_mapping = { 40 | "python": ("http://docs.python.org/dev/", None), 41 | "django": ( 42 | "http://docs.djangoproject.com/en/stable/", 43 | "http://docs.djangoproject.com/en/stable/_objects/", 44 | ), 45 | } 46 | 47 | 48 | master_doc = "index" 49 | 50 | html_theme = "sphinx_rtd_theme" 51 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 52 | html_static_path = ["_static"] 53 | 54 | # -- Options for Spelling output ------------------------------------------ 55 | 56 | # String specifying the language, as understood by PyEnchant and enchant. 57 | # Defaults to en_US for US English. 58 | spelling_lang = "en_US" 59 | 60 | # String specifying a file containing a list of words known to be spelled 61 | # correctly but that do not appear in the language dictionary selected by 62 | # spelling_lang. The file should contain one word per line. 63 | spelling_word_list_filename = "spelling_wordlist.txt" 64 | 65 | # Boolean controlling whether suggestions for misspelled words are printed. 66 | # Defaults to False. 67 | spelling_show_suggestions = True 68 | 69 | myst_heading_anchors = 3 70 | -------------------------------------------------------------------------------- /docs/img/bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/docs/img/bootstrap.png -------------------------------------------------------------------------------- /docs/img/bootstrap4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/docs/img/bootstrap4.png -------------------------------------------------------------------------------- /docs/img/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/docs/img/example.png -------------------------------------------------------------------------------- /docs/img/semantic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/docs/img/semantic.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. default-domain:: py 2 | 3 | ================================================ 4 | django-tables2 - An app for creating HTML tables 5 | ================================================ 6 | 7 | Its features include: 8 | 9 | - Any iterable can be a data-source, but special support for Django QuerySets is included. 10 | - The built in UI does not rely on JavaScript. 11 | - Support for automatic table generation based on a Django model. 12 | - Supports custom column functionality via subclassing. 13 | - Pagination. 14 | - Column based table sorting. 15 | - Template tag to enable trivial rendering to HTML. 16 | - Generic view mixin. 17 | 18 | 19 | About the app: 20 | 21 | - `Available on pypi `_ 22 | - Tested against currently supported versions of Django 23 | `and the python versions Django supports `_ 24 | - `Documentation on readthedocs.org `_ 25 | - `Bug tracker `_ 26 | 27 | 28 | Table of contents 29 | ----------------- 30 | 31 | .. toctree:: 32 | :maxdepth: 1 33 | :caption: Getting started 34 | 35 | pages/installation 36 | pages/tutorial 37 | pages/table-data 38 | 39 | .. toctree:: 40 | :maxdepth: 1 41 | :caption: Customization 42 | 43 | pages/custom-data 44 | pages/ordering 45 | pages/column-attributes 46 | pages/column-headers-and-footers 47 | pages/swapping-columns 48 | pages/pagination 49 | pages/table-mixins 50 | pages/custom-rendering 51 | pages/query-string-fields 52 | pages/localization-control 53 | pages/generic-mixins 54 | pages/pinned-rows 55 | pages/filtering 56 | pages/export 57 | 58 | .. toctree:: 59 | :maxdepth: 1 60 | :caption: Reference 61 | 62 | pages/reference 63 | pages/faq 64 | pages/upgrade-changelog 65 | pages/glossary 66 | -------------------------------------------------------------------------------- /docs/pages/builtin-columns.rst: -------------------------------------------------------------------------------- 1 | .. _builtin-columns: 2 | 3 | Built-in columns 4 | ================ 5 | 6 | For common use-cases the following columns are included: 7 | 8 | - `.BooleanColumn` -- renders boolean values 9 | - `.CheckBoxColumn` -- renders ``checkbox`` form inputs 10 | - `.Column` -- generic column 11 | - `.DateColumn` -- date formatting 12 | - `.DateTimeColumn` -- ``datetime`` formatting in the local timezone 13 | - `.EmailColumn` -- renders ```` tags 14 | - `.FileColumn` -- renders files as links 15 | - `.JSONColumn` -- renders JSON as an indented string in ``
``
16 | - `.LinkColumn` -- renders ``
`` tags (compose a Django URL) 17 | - `.ManyToManyColumn` -- renders a list objects from a `ManyToManyField` 18 | - `.RelatedLinkColumn` -- renders ```` tags linking related objects 19 | - `.TemplateColumn` -- renders template code 20 | - `.TimeColumn` -- time formatting 21 | - `.URLColumn` -- renders ```` tags (absolute URL) 22 | -------------------------------------------------------------------------------- /docs/pages/column-attributes.rst: -------------------------------------------------------------------------------- 1 | .. _column-attributes: 2 | 3 | Column and row attributes 4 | ========================= 5 | 6 | Column attributes 7 | ~~~~~~~~~~~~~~~~~ 8 | 9 | Column attributes can be specified using the `dict` with specific keys. 10 | The dict defines HTML attributes for one of more elements within the column. 11 | Depending on the column, different elements are supported, however ``th``, 12 | ``td``, and ``cell`` are supported universally:: 13 | 14 | >>> import django_tables2 as tables 15 | >>> 16 | >>> class SimpleTable(tables.Table): 17 | ... name = tables.Column(attrs={"th": {"id": "foo"}}) 18 | ... 19 | >>> # will render something like this: 20 | '{snip}{snip}{snip}' 21 | 22 | 23 | Have a look at each column's API reference to find which elements are supported. 24 | 25 | If you need to add some extra attributes to column's tags rendered in the 26 | footer, use key name ``tf``, as described in section on :ref:`css`. 27 | 28 | Callables passed in this dict will be called, with optional kwargs ``table``, 29 | ``bound_column`` ``record`` and ``value``, with the return value added. For example:: 30 | 31 | class Table(tables.Table): 32 | person = tables.Column(attrs={ 33 | "td": { 34 | "data-length": lambda value: len(value) 35 | } 36 | }) 37 | 38 | will render the ````'s in the tables ```` with a ``data-length`` attribute 39 | containing the number of characters in the value. 40 | 41 | .. note:: 42 | The keyword arguments ``record`` and ``value`` only make sense in the context of a row 43 | containing data. If you supply a callable with one of these keyword arguments, 44 | it will not be executed for the header and footer rows. 45 | 46 | If you also want to customize the attributes of those tags, you must define a 47 | callable with a catchall (``**kwargs``) argument:: 48 | 49 | def data_first_name(**kwargs): 50 | first_name = kwargs.get("value", None) 51 | if first_name is None: 52 | return "header" 53 | else: 54 | return first_name 55 | 56 | class Table(tables.Table): 57 | first_name = tables.Column(attrs={ 58 | "td": { 59 | 'data-first-name': data_first_name 60 | } 61 | }) 62 | 63 | This `attrs` can also be defined when subclassing a column, to allow better reuse:: 64 | 65 | class PersonColumn(tables.Column): 66 | attrs = { 67 | "td": { 68 | "data-first-name": lambda record: record.first_name 69 | "data-last-name": lambda record: record.last_name 70 | } 71 | } 72 | def render(self, record): 73 | return f"{record.first_name} {record.last_name}"" 74 | 75 | class Table(tables.Table): 76 | person = PersonColumn() 77 | 78 | is equivalent to the previous example. 79 | 80 | .. _row-attributes: 81 | 82 | Row attributes 83 | ~~~~~~~~~~~~~~ 84 | 85 | Row attributes can be specified using a dict defining the HTML attributes for 86 | the ```` element on each row. 87 | 88 | By default, class names *odd* and *even* are supplied to the rows, which can be 89 | customized using the ``row_attrs`` `.Table.Meta` attribute or as argument to the 90 | constructor of `.Table`. String-like values will just be added, 91 | callables will be called with optional keyword arguments `record` and `table`, 92 | the return value will be added. For example:: 93 | 94 | class Table(tables.Table): 95 | class Meta: 96 | model = User 97 | row_attrs = { 98 | "data-id": lambda record: record.pk 99 | } 100 | 101 | will render tables with the following ```` tag 102 | 103 | .. sourcecode:: django 104 | 105 | [...] 106 | [...] 107 | -------------------------------------------------------------------------------- /docs/pages/faq.rst: -------------------------------------------------------------------------------- 1 | .. _faq: 2 | 3 | .. 4 | Any code examples in this file should have a corresponding test in 5 | tests/test_faq.py 6 | 7 | FAQ 8 | === 9 | 10 | Some frequently requested questions/examples. All examples assume you 11 | import django-tables2 like this:: 12 | 13 | import django_tables2 as tables 14 | 15 | How should I fix error messages about the request context processor? 16 | -------------------------------------------------------------------- 17 | 18 | The error message looks something like this:: 19 | 20 | Tag {% querystring %} requires django.template.context_processors.request to be 21 | in the template configuration in settings.TEMPLATES[]OPTIONS.context_processors) 22 | in order for the included template tags to function correctly. 23 | 24 | which should be pretty clear, but here is an example template configuration anyway:: 25 | 26 | TEMPLATES = [ 27 | { 28 | "BACKEND": "django.template.backends.django.DjangoTemplates", 29 | "DIRS": ["templates"], 30 | "APP_DIRS": True, 31 | "OPTIONS": { 32 | "context_processors": [ 33 | "django.contrib.auth.context_processors.auth", 34 | "django.template.context_processors.request", 35 | "django.template.context_processors.static", 36 | ], 37 | } 38 | } 39 | ] 40 | 41 | How to create a row counter? 42 | ---------------------------- 43 | 44 | You can use ``itertools.counter`` to add row count to a table. Note that in a 45 | paginated table, every page's counter will start at zero:: 46 | 47 | class CountryTable(tables.Table): 48 | counter = tables.TemplateColumn("{{ row_counter }}") 49 | 50 | 51 | How to add a footer containing a column total? 52 | ---------------------------------------------- 53 | 54 | Using the `footer`-argument to `~.Column`:: 55 | 56 | class CountryTable(tables.Table): 57 | population = tables.Column( 58 | footer=lambda table: sum(x["population"] for x in table.data) 59 | ) 60 | 61 | Or by creating a custom column:: 62 | 63 | class SummingColumn(tables.Column): 64 | def render_footer(self, bound_column, table): 65 | return sum(bound_column.accessor.resolve(row) for row in table.data) 66 | 67 | class Table(tables.Table): 68 | name = tables.Column(footer="Total:") 69 | population = SummingColumn() 70 | 71 | Documentation: :ref:`column-footers` 72 | 73 | .. note :: 74 | Your table template must include a block rendering the table footer! 75 | 76 | 77 | Can I use inheritance to build Tables that share features? 78 | ---------------------------------------------------------- 79 | 80 | Yes, like this:: 81 | 82 | class CountryTable(tables.Table): 83 | name = tables.Column() 84 | language = tables.Column() 85 | 86 | A `CountryTable` will show columns `name` and `language`:: 87 | 88 | class TouristCountryTable(CountryTable): 89 | tourist_info = tables.Column() 90 | 91 | A `TouristCountryTable` will show columns `name`, `language` and `tourist_info`. 92 | 93 | Overwriting a `Column` attribute from the base class with anything that is not a 94 | `Column` will result in removing that Column from the `Table`. For example:: 95 | 96 | class SimpleCountryTable(CountryTable): 97 | language = None 98 | 99 | A `SimpleCountryTable` will only show column `name`. 100 | 101 | 102 | How can I use with Jinja2 template? 103 | ----------------------------------- 104 | 105 | In Jinja2 templates, the ``{% render_table %}`` tag is not available, but you can still use *django-tables2* like this:: 106 | 107 | {{ table.as_html(request) }} 108 | 109 | where ``request`` need to be passed from view, or from *context processors* (which is supported by `django-jinja `_). 110 | -------------------------------------------------------------------------------- /docs/pages/filtering.rst: -------------------------------------------------------------------------------- 1 | .. _filtering: 2 | 3 | Filtering data in your table 4 | ============================ 5 | 6 | When presenting a large amount of data, filtering is often a necessity. 7 | Fortunately, filtering the data in your django-tables2 table is simple with 8 | `django-filter `_. 9 | 10 | The basis of a filtered table is a `SingleTableMixin` combined with a 11 | `FilterView` from django-filter:: 12 | 13 | from django_filters.views import FilterView 14 | from django_tables2.views import SingleTableMixin 15 | 16 | 17 | class FilteredPersonListView(SingleTableMixin, FilterView): 18 | table_class = PersonTable 19 | model = Person 20 | template_name = "template.html" 21 | 22 | filterset_class = PersonFilter 23 | 24 | The ``PersonFilter`` is defined the following way:: 25 | 26 | from django_filters import FilterSet 27 | from .models import Person 28 | 29 | class PersonFilter(FilterSet): 30 | class Meta: 31 | model = Person 32 | fields = {"name": ["exact", "contains"], "country": ["exact"]} 33 | 34 | The ``FilterSet`` is added to the template context in a ``filter`` variable by 35 | default. A basic template rendering the filter (using django-bootstrap3)[https://pypi.org/project/django-bootstrap3/] and 36 | table looks like this:: 37 | 38 | {% load render_table from django_tables2 %} 39 | {% load bootstrap3 %} 40 | 41 | {% if filter %} 42 |
43 | {% bootstrap_form filter.form layout='inline' %} 44 | {% bootstrap_button 'filter' %} 45 |
46 | {% endif %} 47 | {% render_table table 'django_tables2/bootstrap.html' %} 48 | -------------------------------------------------------------------------------- /docs/pages/generic-mixins.rst: -------------------------------------------------------------------------------- 1 | Class Based Generic Mixins 2 | ========================== 3 | 4 | Django-tables2 comes with two class based view mixins: `.SingleTableMixin` and 5 | `.MultiTableMixin`. 6 | 7 | 8 | A single table using `.SingleTableMixin` 9 | ---------------------------------------- 10 | 11 | `.SingleTableMixin` makes it trivial to incorporate a table into a view or 12 | template. 13 | 14 | The following view parameters are supported: 15 | 16 | - ``table_class`` –- the table class to use, e.g. ``SimpleTable``, if not specified 17 | and ``model`` is provided, a default table will be created on-the-fly. 18 | - ``table_data`` (or ``get_table_data()``) -- the data used to populate the table 19 | - ``context_table_name`` -- the name of template variable containing the table object 20 | - ``table_pagination`` (or ``get_table_pagination``) -- pagination 21 | options to pass to `.RequestConfig`. Set ``table_pagination=False`` 22 | to disable pagination. 23 | - ``get_table_kwargs()`` allows the keyword arguments passed to the ``Table`` 24 | constructor. 25 | 26 | For example:: 27 | 28 | from django_tables2 import SingleTableView 29 | 30 | 31 | class Person(models.Model): 32 | first_name = models.CharField(max_length=200) 33 | last_name = models.CharField(max_length=200) 34 | 35 | 36 | class PersonTable(tables.Table): 37 | class Meta: 38 | model = Person 39 | 40 | 41 | class PersonList(SingleTableView): 42 | model = Person 43 | table_class = PersonTable 44 | 45 | 46 | The template could then be as simple as: 47 | 48 | .. sourcecode:: django 49 | 50 | {% load django_tables2 %} 51 | {% render_table table %} 52 | 53 | Such little code is possible due to the example above taking advantage of 54 | default values and `.SingleTableMixin`'s eagerness at finding data sources 55 | when one is not explicitly defined. 56 | 57 | .. note:: 58 | 59 | You don't have to base your view on `ListView`, you're able to mix 60 | `SingleTableMixin` directly. 61 | 62 | 63 | Multiple tables using `.MultiTableMixin` 64 | ---------------------------------------- 65 | 66 | If you need more than one table in a single view you can use `MultiTableMixin`. 67 | It manages multiple tables for you and takes care of adding the appropriate 68 | prefixes for them. Just define a list of tables in the tables attribute:: 69 | 70 | from django_tables2 import MultiTableMixin 71 | from django.views.generic.base import TemplateView 72 | 73 | class PersonTablesView(MultiTableMixin, TemplateView): 74 | template_name = "multiTable.html" 75 | tables = [ 76 | PersonTable(qs), 77 | PersonTable(qs, exclude=("country", )) 78 | ] 79 | 80 | table_pagination = { 81 | "per_page": 10 82 | } 83 | 84 | In the template, you get a variable `tables`, which you can loop over like this: 85 | 86 | .. sourcecode:: django 87 | 88 | {% load django_tables2 %} 89 | {% for table in tables %} 90 | {% render_table table %} 91 | {% endfor %} 92 | -------------------------------------------------------------------------------- /docs/pages/glossary.rst: -------------------------------------------------------------------------------- 1 | Glossary 2 | ======== 3 | 4 | .. glossary:: 5 | 6 | accessor 7 | Refers to an `.Accessor` object 8 | 9 | column name 10 | The name given to a column. In the follow example, the *column name* is 11 | ``age``. 12 | 13 | .. sourcecode:: python 14 | 15 | class SimpleTable(tables.Table): 16 | age = tables.Column() 17 | 18 | empty value 19 | An empty value is synonymous with "no value". Columns have an 20 | ``empty_values`` attribute that contains values that are considered 21 | empty. It's a way to declare which values from the database correspond 22 | to *null*/*blank*/*missing* etc. 23 | 24 | order by alias 25 | A prefixed column name that describes how a column should impact the 26 | order of data within the table. This allows the implementation of how 27 | a column affects ordering to be abstracted, which is useful (e.g. in 28 | query strings). 29 | 30 | .. sourcecode:: python 31 | 32 | class ExampleTable(tables.Table): 33 | name = tables.Column(order_by=("first_name", "last_name")) 34 | 35 | In this example ``-name`` and ``name`` are valid order by aliases. In 36 | a query string you might then have ``?order=-name``. 37 | 38 | table 39 | The traditional concept of a table. i.e. a grid of rows and columns 40 | containing data. 41 | 42 | view 43 | A Django view. 44 | 45 | record 46 | A single Python object used as the data for a single row. 47 | 48 | render 49 | The act of serializing a `.Table` into 50 | HTML. 51 | 52 | template 53 | A Django template. 54 | 55 | table data 56 | An iterable of :term:`records ` that 57 | `.Table` uses to populate its rows. 58 | -------------------------------------------------------------------------------- /docs/pages/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Django-tables2 is `Available on pypi `_ 5 | and can be installed using pip:: 6 | 7 | pip install django-tables2 8 | 9 | After installing, add ``'django_tables2'`` to ``INSTALLED_APPS`` and make sure 10 | that ``"django.template.context_processors.request"`` is added to the 11 | ``context_processors`` in your template setting ``OPTIONS``. 12 | -------------------------------------------------------------------------------- /docs/pages/internal.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Internal APIs 3 | ============= 4 | 5 | The items documented here are internal and subject to change. 6 | 7 | 8 | `.BoundColumns` 9 | --------------- 10 | 11 | .. autoclass:: django_tables2.columns.BoundColumns 12 | :members: 13 | :private-members: 14 | :special-members: 15 | 16 | 17 | `.BoundColumn` 18 | -------------- 19 | 20 | .. autoclass:: django_tables2.columns.BoundColumn 21 | :members: 22 | :private-members: 23 | :special-members: 24 | 25 | 26 | `.BoundRows` 27 | ------------ 28 | 29 | .. autoclass:: django_tables2.rows.BoundRows 30 | :members: 31 | :private-members: 32 | :special-members: 33 | 34 | 35 | `.BoundRow` 36 | ----------- 37 | 38 | .. autoclass:: django_tables2.rows.BoundRow 39 | :members: 40 | :private-members: 41 | :special-members: 42 | 43 | 44 | `.BoundPinnedRow` 45 | ----------------- 46 | 47 | .. autoclass:: django_tables2.rows.BoundPinnedRow 48 | :members: 49 | :private-members: 50 | :special-members: 51 | 52 | 53 | `.TableData` 54 | ------------ 55 | 56 | .. autoclass:: django_tables2.tables.TableData 57 | :members: 58 | :private-members: 59 | :special-members: 60 | 61 | 62 | `.utils` 63 | -------- 64 | 65 | .. autoclass:: django_tables2.utils.Sequence 66 | :members: 67 | :private-members: 68 | :special-members: 69 | 70 | 71 | .. autoclass:: django_tables2.utils.OrderBy 72 | :members: 73 | :private-members: 74 | :special-members: 75 | 76 | 77 | .. autoclass:: django_tables2.utils.OrderByTuple 78 | :members: 79 | :private-members: 80 | :special-members: 81 | 82 | 83 | .. autoclass:: django_tables2.utils.Accessor 84 | :noindex: 85 | :members: 86 | :private-members: 87 | :special-members: 88 | 89 | 90 | .. autoclass:: django_tables2.utils.AttributeDict 91 | :noindex: 92 | :members: 93 | :private-members: 94 | :special-members: 95 | 96 | 97 | .. autofunction:: django_tables2.utils.signature 98 | :noindex: 99 | 100 | 101 | .. autofunction:: django_tables2.utils.call_with_appropriate 102 | :noindex: 103 | 104 | 105 | .. autofunction:: django_tables2.utils.computed_values 106 | :noindex: 107 | -------------------------------------------------------------------------------- /docs/pages/localization-control.rst: -------------------------------------------------------------------------------- 1 | .. _localization-control: 2 | 3 | Controlling localization 4 | ======================== 5 | 6 | Django-tables2 allows you to define which column of a table should or should not 7 | be localized. For example you may want to use this feature in following use cases: 8 | 9 | * You want to format some columns representing for example numeric values in the given locales 10 | even if you don't enable `USE_L10N` in your settings file. 11 | 12 | * You don't want to format primary key values in your table 13 | even if you enabled `USE_L10N` in your settings file. 14 | 15 | This control is done by using two filter functions in Django's `l10n` library 16 | named `localize` and `unlocalize`. Check out Django docs about 17 | `localization ` for more information about them. 18 | 19 | There are two ways of controlling localization in your columns. 20 | 21 | First one is setting the `~.Column.localize` attribute in your column definition 22 | to `True` or `False`. Like so:: 23 | 24 | class PersonTable(tables.Table): 25 | id = tables.Column(accessor="pk", localize=False) 26 | class Meta: 27 | model = Person 28 | 29 | 30 | .. note:: 31 | The default value of the `localize` attribute is `None` which means the formatting 32 | of columns is depending on the `USE_L10N` setting. 33 | 34 | The second way is to define a `~.Table.Meta.localize` and/or `~.Table.Meta.unlocalize` 35 | tuples in your tables Meta class (like with `~.Table.Meta.fields` 36 | or `~.Table.Meta.exclude`). You can do this like so:: 37 | 38 | class PersonTable(tables.Table): 39 | id = tables.Column(accessor='pk') 40 | value = tables.Column(accessor='some_numerical_field') 41 | class Meta: 42 | model = Person 43 | unlocalize = ("id", ) 44 | localize = ("value", ) 45 | 46 | If you define the same column in both `localize` and `unlocalize` then the value 47 | of this column will be 'unlocalized' which means that `unlocalize` has higher precedence. 48 | -------------------------------------------------------------------------------- /docs/pages/pagination.rst: -------------------------------------------------------------------------------- 1 | .. _pagination: 2 | 3 | Pagination 4 | ========== 5 | 6 | Pagination is easy, just call :meth:`.Table.paginate` and pass in the current 7 | page number:: 8 | 9 | def people_listing(request): 10 | table = PeopleTable(Person.objects.all()) 11 | table.paginate(page=request.GET.get("page", 1), per_page=25) 12 | return render(request, "people_listing.html", {"table": table}) 13 | 14 | If you are using `.RequestConfig`, pass pagination options to the constructor:: 15 | 16 | def people_listing(request): 17 | table = PeopleTable(Person.objects.all()) 18 | RequestConfig(request, paginate={"per_page": 25}).configure(table) 19 | return render(request, "people_listing.html", {"table": table}) 20 | 21 | If you are using `SingleTableView`, the table will get paginated by default:: 22 | 23 | class PeopleListView(SingleTableView): 24 | table_class = PeopleTable 25 | 26 | Disabling pagination 27 | ~~~~~~~~~~~~~~~~~~~~ 28 | 29 | If you are using `SingleTableView` and want to disable the default behavior, 30 | set `SingleTableView.table_pagination = False` 31 | 32 | Lazy pagination 33 | ~~~~~~~~~~~~~~~ 34 | 35 | The default `~django.core.paginators.Paginator` wants to count the number of items, 36 | which might be an expensive operation for large QuerySets. 37 | In those cases, you can use `.LazyPaginator`, which does not perform a count, 38 | but also does not know what the total amount of pages will be, until you've hit 39 | the last page. 40 | 41 | The `.LazyPaginator` does this by fetching `n + 1` records where the number of records 42 | per page is `n`. If it receives `n` or less records, it knows it is on the last page, 43 | preventing rendering of the 'next' button and further "..." ellipsis. 44 | Usage with `SingleTableView`:: 45 | 46 | class UserListView(SingleTableView): 47 | table_class = UserTable 48 | table_data = User.objects.all() 49 | paginator_class = LazyPaginator 50 | -------------------------------------------------------------------------------- /docs/pages/pinned-rows.rst: -------------------------------------------------------------------------------- 1 | .. _pinned_rows: 2 | 3 | Pinned rows 4 | =========== 5 | 6 | This feature allows one to pin certain rows to the top or bottom of your table. 7 | Provide an implementation for one or two of these methods, returning an iterable 8 | (QuerySet, list of dicts, list objects) representing the pinned data: 9 | 10 | * `get_top_pinned_data(self)` - Displays the returned rows on top. 11 | * `get_bottom_pinned_data(self)` - Displays the returned rows at the bottom. 12 | 13 | Pinned rows are not affected by sorting and pagination, they will be present on every 14 | page of the table, regardless of ordering. 15 | Values will be rendered just like you are used to for normal rows. 16 | 17 | Example:: 18 | 19 | class Table(tables.Table): 20 | first_name = tables.Column() 21 | last_name = tables.Column() 22 | 23 | def get_top_pinned_data(self): 24 | return [ 25 | {"first_name": "Janet", "last_name": "Crossen"}, 26 | # key "last_name" is None here, so the default value will be rendered. 27 | {"first_name": "Trine", "last_name": None} 28 | ] 29 | 30 | .. note:: If you need very different rendering for the bottom pinned rows, chances are 31 | you actually want to use column footers: :ref:`column-footers` 32 | 33 | .. _pinned_row_attributes: 34 | 35 | Attributes for pinned rows 36 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 37 | 38 | You can override the attributes used to render the ```` tag of the pinned rows using: ``pinned_row_attrs``. 39 | This works exactly like :ref:`row-attributes`. 40 | 41 | .. note:: By default the ```` tags for pinned rows will get the attribute ``class="pinned-row"``. 42 | 43 | .. sourcecode:: django 44 | 45 | [...] 46 | [...] 47 | -------------------------------------------------------------------------------- /docs/pages/query-string-fields.rst: -------------------------------------------------------------------------------- 1 | .. _query-string-fields: 2 | 3 | Query string fields 4 | =================== 5 | 6 | Tables pass data via the query string to indicate ordering and pagination 7 | preferences. 8 | 9 | The names of the query string variables are configurable via the options: 10 | 11 | - ``order_by_field`` -- default: ``'sort'`` 12 | - ``page_field`` -- default: ``"page"`` 13 | - ``per_page_field`` -- default: ``"per_page"``, **note:** this field currently 14 | is not used by ``{% render_table %}`` 15 | 16 | Each of these can be specified in three places: 17 | 18 | - ``Table.Meta.foo`` 19 | - ``Table(..., foo=...)`` 20 | - ``Table(...).foo = ...`` 21 | 22 | If you are using multiple tables on a single page, you will want to prefix these 23 | fields with a table-specific name, in order to prevent links on one table 24 | interfere with those on another table:: 25 | 26 | def people_listing(request): 27 | config = RequestConfig(request) 28 | table1 = PeopleTable(Person.objects.all(), prefix="1-") # prefix specified 29 | table2 = PeopleTable(Person.objects.all(), prefix="2-") # prefix specified 30 | config.configure(table1) 31 | config.configure(table2) 32 | 33 | return render(request, 'people_listing.html', { 34 | 'table1': table1, 35 | 'table2': table2 36 | }) 37 | -------------------------------------------------------------------------------- /docs/pages/reference.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. toctree:: 5 | builtin-columns 6 | template-tags 7 | api-reference 8 | internal 9 | -------------------------------------------------------------------------------- /docs/pages/swapping-columns.rst: -------------------------------------------------------------------------------- 1 | .. _swapping-columns: 2 | 3 | Swapping the position of columns 4 | ================================ 5 | 6 | By default columns are positioned in the same order as they are declared, 7 | however when mixing auto-generated columns (via `Table.Meta.model`) with 8 | manually declared columns, the column sequence becomes ambiguous. 9 | 10 | To resolve the ambiguity, columns sequence can be declared via the 11 | `.Table.Meta.sequence` option:: 12 | 13 | class PersonTable(tables.Table): 14 | selection = tables.CheckBoxColumn(accessor="pk", orderable=False) 15 | 16 | class Meta: 17 | model = Person 18 | sequence = ('selection', 'first_name', 'last_name') 19 | 20 | The special value ``'...'`` can be used to indicate that any omitted columns 21 | should inserted at that location. As such it can be used at most once. 22 | -------------------------------------------------------------------------------- /docs/pages/table-data.rst: -------------------------------------------------------------------------------- 1 | .. _table_data: 2 | 3 | Populating a table with data 4 | ============================ 5 | 6 | Tables can be created from a range of input data structures. If you have seen the 7 | tutorial you will have seen a ``QuerySet`` being used, however any iterable that 8 | supports :func:`len` and contains items that exposes key-based access to column 9 | values is fine. 10 | 11 | 12 | List of dicts 13 | ------------- 14 | 15 | In an example we will demonstrate using list of dicts. When defining a table 16 | it is necessary to declare each column:: 17 | 18 | import django_tables2 as tables 19 | 20 | data = [ 21 | {"name": "Bradley"}, 22 | {"name": "Stevie"}, 23 | ] 24 | 25 | class NameTable(tables.Table): 26 | name = tables.Column() 27 | 28 | table = NameTable(data) 29 | 30 | 31 | QuerySets 32 | --------- 33 | 34 | If your build uses tables to display `~django.db.models.query.QuerySet` data, 35 | rather than defining each column manually in the table, the `.Table.Meta.model` 36 | option allows tables to be dynamically created based on a model:: 37 | 38 | # models.py 39 | from django.contrib.auth import get_user_model 40 | from django.db import models 41 | 42 | class Person(models.Model): 43 | first_name = models.CharField(max_length=200) 44 | last_name = models.CharField(max_length=200) 45 | user = models.ForeignKey(get_user_model(), null=True, on_delete=models.CASCADE) 46 | birth_date = models.DateField() 47 | 48 | # tables.py 49 | import django_tables2 as tables 50 | 51 | class PersonTable(tables.Table): 52 | class Meta: 53 | model = Person 54 | 55 | # views.py 56 | def person_list(request): 57 | table = PersonTable(Person.objects.all()) 58 | 59 | return render(request, "person_list.html", { 60 | "table": table 61 | }) 62 | 63 | This has a number of benefits: 64 | 65 | - Less repetition 66 | - Column headers are defined using the field's `~.models.Field.verbose_name` 67 | - Specialized columns are used where possible (e.g. `.DateColumn` for a 68 | `~.models.DateField`) 69 | 70 | When using this approach, the following options might be useful to customize 71 | what fields to show or hide: 72 | 73 | - `~.Table.Meta.sequence` -- reorder columns (if used alone, columns that are not specified are still rendered in the table after the specified columns) 74 | - `~.Table.Meta.fields` -- specify model fields to *include* 75 | - `~.Table.Meta.exclude` -- specify model fields to *exclude* 76 | 77 | These options can be specified as tuples. In this example we will demonstrate how this can be done:: 78 | 79 | # tables.py 80 | class PersonTable(tables.Table): 81 | class Meta: 82 | model = Person 83 | sequence = ("last_name", "first_name", "birth_date", ) 84 | exclude = ("user", ) 85 | 86 | With these options specified, the columns would be show according to the order defined in the `~.Table.Meta.sequence`, while the ``user`` column will be hidden. 87 | 88 | Performance 89 | ----------- 90 | 91 | Django-tables tries to be efficient in displaying big datasets. It tries to 92 | avoid converting the `~django.db.models.query.QuerySet` instances to lists by 93 | using SQL to slice the data and should be able to handle datasets with 100k 94 | records without a problem. 95 | 96 | However, when performance is degrading, these tips might help: 97 | 98 | 1. For large datasets, try to use `.LazyPaginator`. 99 | 2. Try to strip the table of customizations and check if performance improves. 100 | If so, re-add them one by one, checking for performance after each step. 101 | This should help to narrow down the source of your performance problems. 102 | -------------------------------------------------------------------------------- /docs/pages/table-mixins.rst: -------------------------------------------------------------------------------- 1 | Table Mixins 2 | ============ 3 | 4 | It's possible to create a mixin for a table that overrides something, however 5 | unless it itself is a subclass of `.Table` class variable instances of 6 | `.Column` will **not** be added to the class which is using the mixin. 7 | 8 | Example:: 9 | 10 | >>> class UselessMixin: 11 | ... extra = tables.Column() 12 | ... 13 | >>> class TestTable(UselessMixin, tables.Table): 14 | ... name = tables.Column() 15 | ... 16 | >>> TestTable.base_columns.keys() 17 | ["name"] 18 | 19 | To have a mixin contribute a column, it needs to be a subclass of 20 | `~django_tables2.tables.Table`. With this in mind the previous example 21 | *should* have been written as follows:: 22 | 23 | >>> class UsefulMixin(tables.Table): 24 | ... extra = tables.Column() 25 | ... 26 | >>> class TestTable(UsefulMixin, tables.Table): 27 | ... name = tables.Column() 28 | ... 29 | >>> TestTable.base_columns.keys() 30 | ["extra", "name"] 31 | -------------------------------------------------------------------------------- /docs/pages/template-tags.rst: -------------------------------------------------------------------------------- 1 | .. _template_tags: 2 | 3 | Template tags 4 | ============= 5 | 6 | .. _template-tags.render_table: 7 | 8 | render_table 9 | ------------ 10 | 11 | Renders a `~django_tables2.tables.Table` object to HTML and enables as 12 | many features in the output as possible. 13 | 14 | .. sourcecode:: django 15 | 16 | {% load django_tables2 %} 17 | {% render_table table %} 18 | 19 | {# Alternatively a specific template can be used #} 20 | {% render_table table "path/to/custom_table_template.html" %} 21 | 22 | If the second argument (template path) is given, the template will be rendered 23 | with a `.RequestContext` and the table will be in the variable ``table``. 24 | 25 | .. note:: 26 | 27 | This tag temporarily modifies the `.Table` object during rendering. A 28 | ``context`` attribute is added to the table, providing columns with access 29 | to the current context for their own rendering (e.g. `.TemplateColumn`). 30 | 31 | This tag requires that the template in which it's rendered contains the 32 | `~.http.HttpRequest` inside a ``request`` variable. This can be achieved by 33 | ensuring the ``TEMPLATES[]['OPTIONS']['context_processors']`` setting contains 34 | ``django.template.context_processors.request``. 35 | Please refer to the Django documentation for the TEMPLATES-setting_. 36 | 37 | .. _TEMPLATES-setting: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-TEMPLATES 38 | 39 | .. _template-tags.querystring: 40 | 41 | ``querystring`` 42 | --------------- 43 | 44 | A utility that allows you to update a portion of the query-string without 45 | overwriting the entire thing. 46 | 47 | Let's assume we have the query string ``?search=pirates&sort=name&page=5`` and 48 | we want to update the ``sort`` parameter: 49 | 50 | .. sourcecode:: django 51 | 52 | {% querystring "sort"="dob" %} # ?search=pirates&sort=dob&page=5 53 | {% querystring "sort"="" %} # ?search=pirates&page=5 54 | {% querystring "sort"="" "search"="" %} # ?page=5 55 | 56 | {% with "search" as key %} # supports variables as keys 57 | {% querystring key="robots" %} # ?search=robots&page=5 58 | {% endwith %} 59 | 60 | This tag requires the ``django.template.context_processors.request`` context 61 | processor, see :ref:`template-tags.render_table`. 62 | -------------------------------------------------------------------------------- /docs/pages/upgrade-changelog.rst: -------------------------------------------------------------------------------- 1 | Upgrading and change log 2 | ======================== 3 | 4 | Recent versions of django-tables2 have a corresponding git tag for each version 5 | released to `pypi `_. 6 | 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | CHANGELOG.md 12 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | -r ../requirements/common.pip 2 | Sphinx==7.2.6 3 | sphinx_rtd_theme==1.3.0 4 | django 5 | sphinxcontrib-spelling==8.0.0 6 | pyenchant==3.2.2 7 | myst-parser==2.0.0 8 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | accessor 2 | accessors 3 | app 4 | arg 5 | args 6 | attrs 7 | backend 8 | blocktrans 9 | boolean 10 | brianmay 11 | callables 12 | cardinality 13 | checkbox 14 | checkboxes 15 | computable 16 | configurator 17 | css 18 | csv 19 | customizations 20 | dataset 21 | datasets 22 | dicts 23 | distutils 24 | django 25 | django_tables2 26 | django-tables2 27 | docstring 28 | dramon 29 | fallback 30 | filename 31 | getter 32 | github 33 | html 34 | hyperlink 35 | hyperlinked 36 | hyperlinks 37 | instantiation 38 | iterable 39 | jinja 40 | koledennix 41 | kwarg 42 | kwargs 43 | linkification 44 | lookup 45 | lookups 46 | mixin 47 | mixins 48 | ods 49 | orderable 50 | paginator 51 | paginators 52 | py 53 | pylint 54 | pypi 55 | pytest 56 | QuerySet 57 | QuerySets 58 | readthedocs 59 | refactor 60 | rst 61 | Sapovits 62 | screenshots 63 | sortable 64 | sortability 65 | str 66 | subclasses 67 | subclassing 68 | th 69 | truthy 70 | tsv 71 | tuple 72 | tuples 73 | unicode 74 | unlocalize 75 | unlocalized 76 | uppercased 77 | viewname 78 | webkit 79 | whitespace 80 | xls 81 | xlsx 82 | yaml 83 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Django-tables2 example project 2 | 3 | This example project only supports the latest version of Django. 4 | 5 | # To get it up and running: 6 | 7 | ``` 8 | git clone https://github.com/jieter/django-tables2.git 9 | 10 | cd django-tables2/example 11 | pip install -r requirements.txt 12 | python manage.py migrate 13 | python manage.py loaddata app/fixtures/initial_data.json 14 | python manage.py runserver 15 | ``` 16 | 17 | Server should be live at http://127.0.0.1:8000/ now. 18 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/example/__init__.py -------------------------------------------------------------------------------- /example/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/example/app/__init__.py -------------------------------------------------------------------------------- /example/app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Continent, Country 4 | 5 | 6 | @admin.register(Country) 7 | class CountryAdmin(admin.ModelAdmin): 8 | list_per_page = 20 9 | 10 | list_display = ("name", "continent") 11 | 12 | 13 | admin.site.register(Continent) 14 | -------------------------------------------------------------------------------- /example/app/data.py: -------------------------------------------------------------------------------- 1 | COUNTRIES = """Aruba;104822 2 | Afghanistan;34656032 3 | Angola;28813463 4 | Albania;2876101 5 | Andorra;77281 6 | Arab World;406452690 7 | United Arab Emirates;9269612 8 | Argentina;43847430 9 | Armenia;2924816 10 | American Samoa;55599 11 | Antigua and Barbuda;100963 12 | Australia;24127159 13 | Austria;8747358 14 | Azerbaijan;9762274 15 | Burundi;10524117 16 | Belgium;11348159 17 | Benin;10872298 18 | Burkina Faso;18646433 19 | Bangladesh;162951560 20 | Bulgaria;7127822 21 | Bahrain;1425171 22 | Bahamas, The;391232 23 | Bosnia and Herzegovina;3516816 24 | Belarus;9507120 25 | Belize;366954 26 | Bermuda;65331 27 | Bolivia;10887882 28 | Brazil;207652865 29 | Barbados;284996 30 | Brunei Darussalam;423196 31 | Bhutan;797765 32 | Botswana;2250260 33 | Central African Republic;4594621 34 | Canada;36286425 35 | Switzerland;8372098 36 | Channel Islands;164541 37 | Chile;17909754 38 | China;1378665000 39 | Cote d'Ivoire;23695919 40 | Cameroon;23439189 41 | Congo, Dem. Rep.;78736153 42 | Congo, Rep.;5125821 43 | Colombia;48653419 44 | Comoros;795601 45 | Cabo Verde;539560 46 | """ 47 | -------------------------------------------------------------------------------- /example/app/filters.py: -------------------------------------------------------------------------------- 1 | from django_filters import FilterSet 2 | 3 | from .models import Person 4 | 5 | 6 | class PersonFilter(FilterSet): 7 | class Meta: 8 | model = Person 9 | fields = {"name": ["exact", "contains"], "country": ["exact"]} 10 | -------------------------------------------------------------------------------- /example/app/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "app.country", 5 | "fields": { 6 | "tz": "Australia/Brisbane", 7 | "name": "Australia", 8 | "visits": 2, 9 | "population": 20000000, 10 | "flag": "country/flags/australia.svg" 11 | } 12 | }, 13 | { 14 | "pk": 2, 15 | "model": "app.country", 16 | "fields": { 17 | "tz": "NZST", 18 | "name": "New Zealand", 19 | "visits": 1, 20 | "population": 12000000, 21 | "flag": "country/flags/new_zealand.svg" 22 | } 23 | }, 24 | { 25 | "pk": 4, 26 | "model": "app.country", 27 | "fields": { 28 | "tz": "UTC\u22123.5", 29 | "name": "Canada", 30 | "visits": 1, 31 | "population": 34447000, 32 | "flag": "country/flags/canada.svg" 33 | } 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /example/app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.5 on 2017-09-22 13:23 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Country", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 20 | ), 21 | ), 22 | ("name", models.CharField(max_length=100)), 23 | ("population", models.PositiveIntegerField(verbose_name="population")), 24 | ("tz", models.CharField(max_length=50)), 25 | ("visits", models.PositiveIntegerField()), 26 | ("commonwealth", models.NullBooleanField()), 27 | ("flag", models.FileField(upload_to="country/flags/")), 28 | ], 29 | options={"verbose_name_plural": "countries"}, 30 | ), 31 | migrations.CreateModel( 32 | name="Person", 33 | fields=[ 34 | ( 35 | "id", 36 | models.AutoField( 37 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 38 | ), 39 | ), 40 | ("name", models.CharField(max_length=200, verbose_name="full name")), 41 | ("friendly", models.BooleanField(default=True)), 42 | ( 43 | "country", 44 | models.ForeignKey( 45 | null=True, on_delete=django.db.models.deletion.CASCADE, to="app.Country" 46 | ), 47 | ), 48 | ], 49 | options={"verbose_name_plural": "people"}, 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /example/app/migrations/0002_auto_20180416_0959.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-04-16 09:59 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [("app", "0001_initial")] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Continent", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 18 | ), 19 | ), 20 | ("name", models.CharField(max_length=100)), 21 | ], 22 | ), 23 | migrations.AddField( 24 | model_name="country", 25 | name="continent", 26 | field=models.ForeignKey( 27 | null=True, on_delete=django.db.models.deletion.CASCADE, to="app.Continent" 28 | ), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /example/app/migrations/0003_auto_20180416_1020.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-04-16 10:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [("app", "0002_auto_20180416_0959")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="country", 12 | name="flag", 13 | field=models.FileField(blank=True, upload_to="country/flags/"), 14 | ), 15 | migrations.AlterField( 16 | model_name="country", name="tz", field=models.CharField(blank=True, max_length=50) 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /example/app/migrations/0004_auto_fix_deprecation_warnings.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-09 08:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("app", "0003_auto_20180416_1020"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="continent", 14 | name="id", 15 | field=models.BigAutoField( 16 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 17 | ), 18 | ), 19 | migrations.AlterField( 20 | model_name="country", 21 | name="commonwealth", 22 | field=models.BooleanField(null=True), 23 | ), 24 | migrations.AlterField( 25 | model_name="country", 26 | name="id", 27 | field=models.BigAutoField( 28 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 29 | ), 30 | ), 31 | migrations.AlterField( 32 | model_name="person", 33 | name="id", 34 | field=models.BigAutoField( 35 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 36 | ), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /example/app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/example/app/migrations/__init__.py -------------------------------------------------------------------------------- /example/app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | class Continent(models.Model): 7 | name = models.CharField(max_length=100) 8 | 9 | def __str__(self): 10 | return self.name 11 | 12 | 13 | class Country(models.Model): 14 | """Represents a geographical Country.""" 15 | 16 | name = models.CharField(max_length=100) 17 | population = models.PositiveIntegerField(verbose_name=_("population")) 18 | tz = models.CharField(max_length=50, blank=True) 19 | visits = models.PositiveIntegerField() 20 | commonwealth = models.BooleanField(null=True) 21 | flag = models.FileField(upload_to="country/flags/", blank=True) 22 | 23 | continent = models.ForeignKey(Continent, null=True, on_delete=models.CASCADE) 24 | 25 | class Meta: 26 | verbose_name_plural = _("countries") 27 | 28 | def __str__(self): 29 | return self.name 30 | 31 | def get_absolute_url(self): 32 | return reverse("country_detail", args=(self.pk,)) 33 | 34 | @property 35 | def summary(self): 36 | return f"{self.name} (pop. {self.population})" 37 | 38 | 39 | class Person(models.Model): 40 | name = models.CharField(max_length=200, verbose_name="full name") 41 | friendly = models.BooleanField(default=True) 42 | 43 | country = models.ForeignKey(Country, null=True, on_delete=models.CASCADE) 44 | 45 | class Meta: 46 | verbose_name_plural = "people" 47 | 48 | def __str__(self): 49 | return self.name 50 | 51 | def get_absolute_url(self): 52 | return reverse("person_detail", args=(self.pk,)) 53 | -------------------------------------------------------------------------------- /example/app/tables.py: -------------------------------------------------------------------------------- 1 | import django_tables2 as tables 2 | 3 | from .models import Country, Person 4 | 5 | 6 | class CountryTable(tables.Table): 7 | name = tables.Column() 8 | population = tables.Column() 9 | tz = tables.Column(verbose_name="time zone") 10 | visits = tables.Column() 11 | summary = tables.Column(order_by=("name", "population")) 12 | 13 | class Meta: 14 | model = Country 15 | 16 | 17 | class ThemedCountryTable(CountryTable): 18 | class Meta: 19 | attrs = {"class": "paleblue"} 20 | 21 | 22 | class CheckboxTable(tables.Table): 23 | select = tables.CheckBoxColumn(empty_values=(), footer="") 24 | 25 | population = tables.Column(attrs={"cell": {"class": "population"}}) 26 | 27 | class Meta: 28 | model = Country 29 | template_name = "django_tables2/bootstrap.html" 30 | fields = ("select", "name", "population") 31 | 32 | 33 | class BootstrapTable(tables.Table): 34 | class Meta: 35 | model = Person 36 | template_name = "django_tables2/bootstrap.html" 37 | fields = ("id", "country", "country__continent__name") 38 | linkify = ("country", "country__continent__name") 39 | 40 | 41 | class BootstrapTablePinnedRows(BootstrapTable): 42 | class Meta(BootstrapTable.Meta): 43 | pinned_row_attrs = {"class": "info"} 44 | 45 | def get_top_pinned_data(self): 46 | return [ 47 | { 48 | "name": "Most used country: ", 49 | "country": Country.objects.filter(name="Cameroon").first(), 50 | } 51 | ] 52 | 53 | 54 | class Bootstrap4Table(tables.Table): 55 | country = tables.Column(linkify=True) 56 | continent = tables.Column(accessor="country__continent", linkify=True) 57 | 58 | class Meta: 59 | model = Person 60 | template_name = "django_tables2/bootstrap4.html" 61 | attrs = {"class": "table table-hover"} 62 | exclude = ("friendly",) 63 | 64 | 65 | class Bootstrap5Table(tables.Table): 66 | country = tables.Column(linkify=True) 67 | continent = tables.Column(accessor="country__continent", linkify=True) 68 | 69 | class Meta: 70 | model = Person 71 | template_name = "django_tables2/bootstrap5.html" 72 | exclude = ("friendly",) 73 | 74 | 75 | class SemanticTable(tables.Table): 76 | country = tables.RelatedLinkColumn() 77 | 78 | class Meta: 79 | model = Person 80 | template_name = "django_tables2/semantic.html" 81 | exclude = ("friendly",) 82 | 83 | 84 | class PersonTable(tables.Table): 85 | id = tables.Column(linkify=True) 86 | country = tables.Column(linkify=True) 87 | 88 | class Meta: 89 | model = Person 90 | template_name = "django_tables2/bootstrap.html" 91 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/media/country/flags/australia.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/media/country/flags/canada.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/media/country/flags/new_zealand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flag of New Zealand 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | -e .. 2 | django-bootstrap3==22.2 3 | django-bootstrap4==22.3 4 | django-bootstrap5==22.2 5 | django-debug-toolbar==3.8.1 6 | django-filter==22.1 7 | tablib==3.3.0 8 | -------------------------------------------------------------------------------- /example/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | django-tables2 examples 6 | 7 | 14 | {% block extrahead %}{% endblock %} 15 | 16 | 17 | 18 | {% block body %} 19 | {% load django_tables2 %} 20 | {% render_table table %} 21 | {% endblock %} 22 | 23 | 24 | -------------------------------------------------------------------------------- /example/templates/bootstrap4_template.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load render_table from django_tables2 %} 3 | {% load bootstrap4 %} 4 | 5 | 6 | 7 | django_tables2 with bootstrap 4 template example 8 | {% bootstrap_css %} 9 | 10 | 11 | 12 |
13 | {% block body %} 14 | 15 | Bootstrap 4 - tables docs | 16 | Bootstrap 4 - pagination docs 17 | 18 |

django_tables2 with Bootstrap 4 template example

19 | 20 | 21 |
22 | {% if filter %} 23 |
24 |
25 | {% bootstrap_form filter.form layout='inline' %} 26 | {% bootstrap_button 'filter' %} 27 |
28 |
29 | {% endif %} 30 |
31 | {% render_table table %} 32 |
33 |
34 | {% endblock %} 35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /example/templates/bootstrap5_template.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load render_table from django_tables2 %} 3 | {% load django_bootstrap5 %} 4 | 5 | 6 | 7 | django_tables2 with bootstrap 5 template example 8 | {% bootstrap_css %} 9 | 10 | 11 | 12 |
13 | {% block body %} 14 | 15 | Bootstrap 5 - tables docs | 16 | Bootstrap 5 - pagination docs 17 | 18 |

django_tables2 with Bootstrap 5 template example

19 | 20 |
21 | {% if filter %} 22 |
23 |
24 | {% bootstrap_form filter.form layout='inline' %} 25 | {% bootstrap_button 'filter' %} 26 |
27 |
28 | {% endif %} 29 |
30 | {% render_table table %} 31 |
32 |
33 | {% endblock %} 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /example/templates/bootstrap_template.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load django_tables2 %} 3 | {% load bootstrap3 %} 4 | 5 | 6 | 7 | 8 | django_tables2 with bootstrap 3 template example 9 | {% bootstrap_css %} 10 | 11 | 12 | 13 | 14 |
15 | {% block body %} 16 | 17 | Bootstrap 3 - table docs | 18 | Bootstrap 3 - pagination docs
19 | 20 |

{% block title %}django_tables2 with Bootstrap 3 template example{% endblock %}

21 | 22 |
23 |
24 |
25 | {% if view.export_formats %} 26 | {% for format in view.export_formats %} 27 | 28 | download .{{ format }} 29 | 30 | {% endfor %} 31 | {% endif %} 32 |
33 | {% if filter %} 34 |
35 | {% bootstrap_form filter.form layout='inline' %} 36 | {% bootstrap_button 'filter' %} 37 |
38 | {% endif %} 39 |
40 |
41 | {% render_table table %} 42 |
43 |
44 | {% endblock %} 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /example/templates/checkbox_example.html: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap_template.html' %} 2 | {% load bootstrap3 %} 3 | 4 | {% block title %}django-tables2 CheckBoxColumn example{% endblock %} 5 | {% block body %} 6 | {{ block.super }} 7 | 8 | {% bootstrap_javascript jquery=True %} 9 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /example/templates/class_based.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load django_tables2 %} 3 | 4 | {% block body %} 5 |

class based view via render_table

6 |
 7 | {{ "{%" }} load django_tables2 {{ "%}" }}
 8 | {{ "{%" }} render_table example {{ "%}" }}
 9 | 
10 | 11 | {% render_table table %} 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /example/templates/country_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'semantic_template.html' %} 2 | {% load django_tables2 %} 3 | 4 | {% block body %} 5 |

{{ country }}

6 | 7 |

Persons in this country

8 | {% render_table table 'django_tables2/semantic.html' %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example/templates/extended_table.html: -------------------------------------------------------------------------------- 1 | {% extends "django_tables2/table.html" %} 2 | 3 | {% block table.tfoot %} 4 | 5 | 6 | This is a footer 7 | 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load django_tables2 %} 3 | {% load static %} 4 | {% load i18n %} 5 | {% block extrahead %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 | 45 | 46 |
47 |

Some examples of using django-tables2

48 | 49 |

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

53 | 54 |
    55 | {% for url, text in urls %} 56 |
  • {{ text }}
  • 57 | {% endfor %} 58 |
59 | 60 |

Basic example of a table

61 | {% render_table table "django_tables2/bootstrap.html" %} 62 | 63 |
64 |

Same table, but responsive (.table-responsive)

65 |
66 | {% render_table table "django_tables2/bootstrap-responsive.html" %} 67 |
68 |
69 |
70 | {% endblock %} 71 | -------------------------------------------------------------------------------- /example/templates/multiTable.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load django_tables2 %} 3 | {% load static %} 4 | 5 | {% block extrahead %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 |
12 | 13 |

Multiple tables on a single page using MultiTableMixin

14 | 15 |

16 | Pagination should work independently for each table, 17 | the second table excludes the column 'country'. 18 |

19 |
20 | {% for table in tables %} 21 |
{% render_table table %}
22 | {% endfor %} 23 |
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /example/templates/multiple.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block body %} 5 |

django-tables2 examples

6 |

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

8 | 9 |

Example 1 — QuerySet

10 |

via as_html()

11 |
{% templatetag openvariable %} example1.as_html {% templatetag closevariable %}
12 | {{ example1.as_html }} 13 | 14 |

via template tag

15 |
{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
16 | {% templatetag openblock %} render_table example1 {% templatetag closeblock %}
17 | {% load django_tables2 %} 18 | {% render_table example1 %} 19 | 20 |

Example 2 — QuerySet + pagination

21 |

via as_html()

22 |
{% templatetag openvariable %} example2.as_html {% templatetag closevariable %}
23 | {{ example2.as_html }} 24 | 25 |

via template tag

26 |
{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
27 | {% templatetag openblock %} render_table example2 {% templatetag closeblock %}
28 | {% load django_tables2 %} 29 | {% render_table example2 %} 30 | 31 |

Example 3 — QuerySet + paleblue theme

32 |

via as_html()

33 |
{% templatetag openvariable %} example3.as_html {% templatetag closevariable %}
34 | {{ example3.as_html }} 35 | 36 |

via template tag

37 |
{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
38 | {% templatetag openblock %} render_table example3 {% templatetag closeblock %}
39 | {% load django_tables2 %} 40 | {% render_table example3 %} 41 | 42 |

Example 4 — QuerySet + pagination + paleblue theme

43 |

via as_html()

44 |
{% templatetag openvariable %} example4.as_html {% templatetag closevariable %}
45 | {{ example4.as_html }} 46 | 47 |

via template tag

48 |
{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
49 | {% templatetag openblock %} render_table example4 {% templatetag closeblock %}
50 | {% load django_tables2 %} 51 | {% render_table example4 %} 52 | 53 |

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

54 |

via as_html()

55 |
{% templatetag openvariable %} example5.as_html {% templatetag closevariable %}
56 | {{ example5.as_html }} 57 | 58 |

via template tag

59 |
{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
60 | {% templatetag openblock %} render_table example5 {% templatetag closeblock %}
61 | {% load django_tables2 %} 62 | {% render_table example5 %} 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /example/templates/person_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'semantic_template.html' %} 2 | {% load django_tables2 %} 3 | 4 | {% block body %} 5 |

{{ person }}

6 | 7 | country: {{ person.country }} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /example/templates/semantic_template.html: -------------------------------------------------------------------------------- 1 | {% load render_table from django_tables2 %} 2 | 3 | 4 | 5 | django_tables2 with semantic template example 6 | 7 | 8 | 9 | 10 | 11 |
12 | {% block body %} 13 |

django_tables2 with Semantic UI template example

14 | {% render_table table %} 15 | {% endblock %} 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /example/templates/tutorial.html: -------------------------------------------------------------------------------- 1 | {% load django_tables2 %} 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | 8 | 9 |

django_tables2 with the default template example

10 | 11 | {% render_table table %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from app.views import ( 2 | ClassBased, 3 | FilteredPersonListView, 4 | MultipleTables, 5 | checkbox, 6 | country_detail, 7 | index, 8 | multiple, 9 | person_detail, 10 | template_example, 11 | tutorial, 12 | ) 13 | from django.conf import settings 14 | from django.contrib import admin 15 | from django.urls import include, path 16 | from django.views import static 17 | 18 | urlpatterns = [ 19 | path("", index), 20 | path("multiple/", multiple, name="multiple"), 21 | path("class-based/", ClassBased.as_view(), name="singletableview"), 22 | path("class-based-multiple/", MultipleTables.as_view(), name="multitableview"), 23 | path("class-based-filtered/", FilteredPersonListView.as_view(), name="filtertableview"), 24 | path("checkbox/", checkbox, name="checkbox"), 25 | path("tutorial/", tutorial, name="tutorial"), 26 | path("template//", template_example, name="template_example"), 27 | path("admin/doc/", include("django.contrib.admindocs.urls")), 28 | path("admin/", admin.site.urls), 29 | path("country//", country_detail, name="country_detail"), 30 | path("person//", person_detail, name="person_detail"), 31 | path("media/", static.serve, {"document_root": settings.MEDIA_ROOT}), 32 | path("i18n/", include("django.conf.urls.i18n")), 33 | ] 34 | 35 | if settings.DEBUG: 36 | import debug_toolbar 37 | 38 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns 39 | -------------------------------------------------------------------------------- /maintenance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import subprocess 4 | import sys 5 | import time 6 | 7 | try: 8 | import hatch # noqa 9 | except ImportError: 10 | print("Package `hatch` is required: pip install hatch") 11 | sys.exit() 12 | 13 | VERSION = subprocess.check_output(["hatch version"], shell=True).decode().strip() 14 | 15 | if sys.argv[-1] == "bump": 16 | os.system("hatch version patch") 17 | 18 | print("\n- Commit CHANGELOG and django_tables2/__init__.py") 19 | print("- Run `./maintenance.py tag` to tag the new version") 20 | 21 | elif sys.argv[-1] == "publish": 22 | os.system("hatch publish") 23 | os.system("rm -f dist/django_tables2-2.7.4*") 24 | 25 | elif sys.argv[-1] == "tag": 26 | os.system("hatch build") 27 | tag_cmd = f"git tag -a v{VERSION} -m 'tagging v{VERSION}'" 28 | if exitcode := os.system(tag_cmd): 29 | print(f"Failed tagging with command: {tag_cmd}") 30 | else: 31 | os.system("git push --tags && git push origin master") 32 | print("\nTag created, run `./maintenance.py publish` to publish it") 33 | 34 | elif sys.argv[-1] == "screenshots": 35 | 36 | def screenshot(url, filename="screenshot.png", delay=2): 37 | print(f"Making screenshot of url: {url}") 38 | chrome = subprocess.Popen( 39 | ["chromium-browser", "--incognito", "--headless", "--screenshot", url], close_fds=False 40 | ) 41 | print(f"Starting to sleep for {delay}s...") 42 | time.sleep(delay) 43 | chrome.kill() 44 | os.system(f"convert screenshot.png -trim -bordercolor White -border 10x10 {dest}") 45 | os.remove("screenshot.png") 46 | print(f"Saved file to {dest}") 47 | 48 | images = { 49 | "{url}/tutorial/": "docs/img/example.png", 50 | "{url}/bootstrap/": "docs/img/bootstrap.png", 51 | "{url}/bootstrap4/": "docs/img/bootstrap4.png", 52 | "{url}/semantic/": "docs/img/semantic.png", 53 | } 54 | 55 | print( 56 | "Make sure the devserver is running: \n cd example/\n PYTHONPATH=.. ./manage.py runserver --insecure\n\n" 57 | ) 58 | 59 | for url, dest in images.items(): 60 | screenshot(url.format(url="http://localhost:8000"), dest) 61 | else: 62 | print(f"Current version: {VERSION}") 63 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.app.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling"] 4 | 5 | [project] 6 | authors = [ 7 | {name = "Bradley Ayers", email = "bradley.ayers@gmail.com"}, 8 | {name = "Jan Pieter Waagmeester", email = "jieter@jieter.nl"} 9 | ] 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Environment :: Web Environment", 13 | "Framework :: Django", 14 | "Framework :: Django :: 4.2", 15 | "Framework :: Django :: 5.0", 16 | "Framework :: Django :: 5.1", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: BSD License", 19 | "Operating System :: OS Independent", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3 :: Only", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Topic :: Internet :: WWW/HTTP", 28 | "Topic :: Software Development :: Libraries" 29 | ] 30 | dependencies = ["Django>=4.2"] 31 | description = "Table/data-grid framework for Django" 32 | dynamic = ["version"] 33 | license = {file = "LICENSE"} 34 | name = "django-tables2" 35 | readme = "README.md" 36 | requires-python = ">=3.9" 37 | 38 | [project.optional-dependencies] 39 | tablib = ["tablib"] 40 | 41 | [project.urls] 42 | Changelog = "https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md" 43 | Documentation = "https://django-tables2.readthedocs.io/en/latest/" 44 | Homepage = "https://github.com/jieter/django-tables2/" 45 | Readme = "https://github.com/jieter/django-tables2/blob/master/README.md" 46 | 47 | [tool.hatch.build.targets.sdist] 48 | exclude = ["docs"] 49 | 50 | [tool.hatch.build.targets.wheel] 51 | packages = ["django_tables2"] 52 | 53 | [tool.hatch.version] 54 | path = "django_tables2/__init__.py" 55 | 56 | [tool.ruff] 57 | line-length = 100 58 | target-version = "py39" 59 | 60 | [tool.ruff.lint] 61 | fixable = [ 62 | # "D", 63 | "D200", 64 | "D202", 65 | "D401", 66 | "D400", 67 | "D415", 68 | "E", 69 | "F", 70 | "I", 71 | "UP" 72 | ] 73 | ignore = [ 74 | "D1", # Missing docstring error codes (because not every function and class has a docstring) 75 | "D203", # 1 blank line required before class docstring (conflicts with D211 and should be disabled, see https://github.com/PyCQA/pydocstyle/pull/91) 76 | "D205", 77 | "D406", # Section name should end with a newline 78 | "D407", # Missing dashed underline after section 79 | "D413", # Missing blank line after last section ... 80 | "D212", # Multi-line docstring summary should start at the first line 81 | "E501" # Line too long 82 | ] 83 | select = [ 84 | "D", # pydocstyle 85 | "E", # pycodestyle 86 | "F", # flake8 87 | "I", # isort 88 | "UP" # pyupgrade 89 | ] 90 | unfixable = [ 91 | "F8" # names in flake8, such as defined but unused variables 92 | ] 93 | 94 | [tool.ruff.lint.per-file-ignores] 95 | "*/migrations/*" = ["D417", "E501"] 96 | 97 | [tool.setuptools.dynamic] 98 | version = {attr = "django_tables2.__version__"} 99 | -------------------------------------------------------------------------------- /requirements/common.pip: -------------------------------------------------------------------------------- 1 | # xml parsing 2 | lxml==5.3.0 3 | pytz==2024.2 4 | tablib[xls,yaml]==3.7.0 5 | openpyxl==3.1.5 6 | psycopg2-binary==2.9.10 7 | django-filter==24.3 8 | -------------------------------------------------------------------------------- /requirements/django-dev.pip: -------------------------------------------------------------------------------- 1 | -r common.pip 2 | Django==4.2.6 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/tests/__init__.py -------------------------------------------------------------------------------- /tests/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/tests/app/__init__.py -------------------------------------------------------------------------------- /tests/app/locale/ua/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/tests/app/locale/ua/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /tests/app/locale/ua/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Report-Msgid-Bugs-To: \n" 4 | "MIME-Version: 1.0\n" 5 | "Content-Type: text/plain; charset=UTF-8\n" 6 | 7 | msgid "translation test" 8 | msgstr "тест перекладу" 9 | 10 | msgid "translation test lazy" 11 | msgstr "тест ленивого перекладу" 12 | -------------------------------------------------------------------------------- /tests/app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/tests/app/migrations/__init__.py -------------------------------------------------------------------------------- /tests/app/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.fields import GenericForeignKey 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.db import models 4 | from django.urls import reverse 5 | from django.utils.safestring import mark_safe 6 | from django.utils.translation import gettext, gettext_lazy 7 | 8 | 9 | class Person(models.Model): 10 | first_name = models.CharField(max_length=200) 11 | 12 | last_name = models.CharField(max_length=200, verbose_name="surname") 13 | 14 | occupation = models.ForeignKey( 15 | "Occupation", 16 | related_name="people", 17 | null=True, 18 | verbose_name="occupation of the person", 19 | on_delete=models.CASCADE, 20 | ) 21 | 22 | trans_test = models.CharField( 23 | max_length=200, blank=True, verbose_name=gettext("translation test") 24 | ) 25 | 26 | trans_test_lazy = models.CharField( 27 | max_length=200, blank=True, verbose_name=gettext_lazy("translation test lazy") 28 | ) 29 | 30 | safe = models.CharField(max_length=200, blank=True, verbose_name=mark_safe("Safe")) 31 | 32 | website = models.URLField(max_length=200, null=True, blank=True, verbose_name="web site") 33 | 34 | birthdate = models.DateField(null=True) 35 | 36 | content_type = models.ForeignKey(ContentType, null=True, blank=True, on_delete=models.CASCADE) 37 | object_id = models.PositiveIntegerField(null=True, blank=True) 38 | foreign_key = GenericForeignKey() 39 | 40 | friends = models.ManyToManyField("Person") 41 | 42 | class Meta: 43 | verbose_name = "person" 44 | verbose_name_plural = "people" 45 | 46 | def __str__(self): 47 | return self.first_name 48 | 49 | @property 50 | def name(self): 51 | return f"{self.first_name} {self.last_name}" 52 | 53 | def get_absolute_url(self): 54 | return reverse("person", args=(self.pk,)) 55 | 56 | 57 | class PersonProxy(Person): 58 | class Meta: 59 | proxy = True 60 | ordering = ("last_name",) 61 | 62 | 63 | class Group(models.Model): 64 | name = models.CharField(max_length=200) 65 | members = models.ManyToManyField("Person") 66 | 67 | def __str__(self): 68 | return self.name 69 | 70 | def get_absolute_url(self): 71 | return f"/group/{self.pk}/" 72 | 73 | 74 | class Occupation(models.Model): 75 | name = models.CharField(max_length=200) 76 | region = models.ForeignKey("Region", null=True, on_delete=models.CASCADE) 77 | boolean = models.BooleanField(null=True) 78 | boolean_with_choices = models.BooleanField(null=True, choices=((True, "Yes"), (False, "No"))) 79 | 80 | def get_absolute_url(self): 81 | return reverse("occupation", args=(self.pk,)) 82 | 83 | def __str__(self): 84 | return self.name 85 | 86 | 87 | class Region(models.Model): 88 | name = models.CharField(max_length=200) 89 | mayor = models.OneToOneField(Person, null=True, on_delete=models.CASCADE) 90 | 91 | class Meta: 92 | ordering = ["name"] 93 | 94 | def __str__(self): 95 | return self.name 96 | 97 | 98 | class PersonInformation(models.Model): 99 | person = models.ForeignKey( 100 | Person, related_name="info_list", verbose_name="Information", on_delete=models.CASCADE 101 | ) 102 | -------------------------------------------------------------------------------- /tests/app/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 2 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 3 | 4 | INSTALLED_APPS = [ 5 | "django.contrib.auth", 6 | "django.contrib.contenttypes", 7 | "django_tables2", 8 | "tests.app", 9 | ] 10 | 11 | ROOT_URLCONF = "tests.app.urls" 12 | 13 | SECRET_KEY = "this is super secret" 14 | 15 | TEMPLATES = [ 16 | { 17 | "BACKEND": "django.template.backends.django.DjangoTemplates", 18 | "APP_DIRS": True, 19 | "OPTIONS": {"context_processors": ["django.template.context_processors.request"]}, 20 | } 21 | ] 22 | 23 | TIME_ZONE = "Europe/Amsterdam" 24 | 25 | USE_TZ = True 26 | -------------------------------------------------------------------------------- /tests/app/templates/child/foo.html: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /tests/app/templates/csrf.html: -------------------------------------------------------------------------------- 1 | {% csrf_token %} 2 | -------------------------------------------------------------------------------- /tests/app/templates/dummy.html: -------------------------------------------------------------------------------- 1 | dummy template contents 2 | -------------------------------------------------------------------------------- /tests/app/templates/minimal.html: -------------------------------------------------------------------------------- 1 | {% load django_tables2 %} 2 | 3 | 4 | {% for column in table.columns %} 5 | 6 | {% endfor %} 7 | 8 | {% for row in table.paginated_rows %} 9 | 10 | {% for column, cell in row.items %} 11 | 12 | {% endfor %} 13 | 14 | {% endfor %} 15 | {% if table.page.has_next %} 16 | 17 | next 18 | 19 | {% endif %} 20 |
{{ column.header }}
{{ cell }}
21 | -------------------------------------------------------------------------------- /tests/app/templates/multiple.html: -------------------------------------------------------------------------------- 1 | {% load django_tables2 %} 2 |

Multiple tables using MultiTableMixin

3 | 4 | {% for table in tables %} 5 | {% render_table table %} 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /tests/app/templates/test_template_column.html: -------------------------------------------------------------------------------- 1 | name:{{ record.col }}-{{ foo|default:"empty" }} 2 | -------------------------------------------------------------------------------- /tests/app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path("people/delete//", views.person, name="person_delete"), 7 | path("people/edit//", views.person, name="person_edit"), 8 | path("people//", views.person, name="person"), 9 | path("occupations//", views.occupation, name="occupation"), 10 | re_path(r'^&\'"/(?P\d+)/$', lambda req: None, name="escaping"), 11 | ] 12 | -------------------------------------------------------------------------------- /tests/app/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.shortcuts import get_object_or_404 3 | 4 | from .models import Occupation, Person 5 | 6 | 7 | def person(request, pk): 8 | """Endpoint for the 'person' URL.""" 9 | person = get_object_or_404(Person, pk=pk) 10 | return HttpResponse(f"Person: {person}") 11 | 12 | 13 | def occupation(request, pk): 14 | """Endpoint for the 'occupation' URL.""" 15 | occupation = get_object_or_404(Occupation, pk=pk) 16 | return HttpResponse(f"Occupation: {occupation}") 17 | -------------------------------------------------------------------------------- /tests/columns/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieter/django-tables2/3c158ffc22ea9cb7c770de7e44a829de587202be/tests/columns/__init__.py -------------------------------------------------------------------------------- /tests/columns/test_checkboxcolumn.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase 2 | 3 | import django_tables2 as tables 4 | 5 | from ..utils import attrs 6 | 7 | 8 | class CheckBoxColumnTest(SimpleTestCase): 9 | def test_new_attrs_should_be_supported(self): 10 | class TestTable(tables.Table): 11 | col1 = tables.CheckBoxColumn( 12 | attrs=dict(th__input={"th_key": "th_value"}, td__input={"td_key": "td_value"}) 13 | ) 14 | col2 = tables.CheckBoxColumn(attrs=dict(input={"key": "value"})) 15 | 16 | table = TestTable([{"col1": "data", "col2": "data"}]) 17 | assert attrs(table.columns["col1"].header) == {"type": "checkbox", "th_key": "th_value"} 18 | assert attrs(table.rows[0].get_cell("col1")) == { 19 | "type": "checkbox", 20 | "td_key": "td_value", 21 | "value": "data", 22 | "name": "col1", 23 | } 24 | assert attrs(table.columns["col2"].header) == {"type": "checkbox", "key": "value"} 25 | assert attrs(table.rows[0].get_cell("col2")) == { 26 | "type": "checkbox", 27 | "key": "value", 28 | "value": "data", 29 | "name": "col2", 30 | } 31 | 32 | def test_column_is_checked(self): 33 | class TestTable(tables.Table): 34 | col = tables.CheckBoxColumn(attrs={"name": "col"}, checked="is_selected") 35 | 36 | table = TestTable([{"col": "1", "is_selected": True}, {"col": "2", "is_selected": False}]) 37 | assert attrs(table.rows[0].get_cell("col")) == { 38 | "type": "checkbox", 39 | "value": "1", 40 | "name": "col", 41 | "checked": "checked", 42 | } 43 | assert attrs(table.rows[1].get_cell("col")) == { 44 | "type": "checkbox", 45 | "value": "2", 46 | "name": "col", 47 | } 48 | 49 | def test_column_is_not_checked_for_non_existing_column(self): 50 | class TestTable(tables.Table): 51 | col = tables.CheckBoxColumn(checked="does_not_exist") 52 | 53 | table = TestTable([{"col": "1", "is_selected": True}, {"col": "2", "is_selected": False}]) 54 | assert attrs(table.rows[0].get_cell("col")) == { 55 | "type": "checkbox", 56 | "value": "1", 57 | "name": "col", 58 | } 59 | assert attrs(table.rows[1].get_cell("col")) == { 60 | "type": "checkbox", 61 | "value": "2", 62 | "name": "col", 63 | } 64 | 65 | def test_column_is_alway_checked(self): 66 | class TestTable(tables.Table): 67 | col = tables.CheckBoxColumn(checked=True) 68 | 69 | table = TestTable([{"col": 1, "foo": "bar"}, {"col": 2, "foo": "baz"}]) 70 | assert attrs(table.rows[0].get_cell("col"))["checked"] == "checked" 71 | assert attrs(table.rows[1].get_cell("col"))["checked"] == "checked" 72 | 73 | def test_column_is_checked_callback(self): 74 | def is_selected(value, record): 75 | return value == "1" 76 | 77 | class TestTable(tables.Table): 78 | col = tables.CheckBoxColumn(attrs={"name": "col"}, checked=is_selected) 79 | 80 | table = TestTable([{"col": "1"}, {"col": "2"}]) 81 | assert attrs(table.rows[0].get_cell("col")) == { 82 | "type": "checkbox", 83 | "value": "1", 84 | "name": "col", 85 | "checked": "checked", 86 | } 87 | assert attrs(table.rows[1].get_cell("col")) == { 88 | "type": "checkbox", 89 | "value": "2", 90 | "name": "col", 91 | } 92 | 93 | def test_column_callable_attrs(self): 94 | class TestTable(tables.Table): 95 | col = tables.CheckBoxColumn( 96 | attrs={"input": {"data-source": lambda record: record["col"]}} 97 | ) 98 | 99 | table = TestTable([{"col": "1"}]) 100 | assert attrs(table.rows[0].get_cell("col")) == { 101 | "type": "checkbox", 102 | "value": "1", 103 | "name": "col", 104 | "data-source": "1", 105 | } 106 | -------------------------------------------------------------------------------- /tests/columns/test_datecolumn.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.db import models 4 | from django.test import SimpleTestCase 5 | 6 | import django_tables2 as tables 7 | 8 | 9 | def isoformat_link(value): 10 | return f"/test/{value.isoformat()}/" 11 | 12 | 13 | class DateColumnTest(SimpleTestCase): 14 | """ 15 | Date formatting test case. 16 | 17 | Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date 18 | D -- Day of the week, textual, 3 letters -- 'Fri' 19 | b -- Month, textual, 3 letters, lowercase -- 'jan' 20 | Y -- Year, 4 digits. -- '1999' 21 | """ 22 | 23 | def test_should_handle_explicit_format(self): 24 | class TestTable(tables.Table): 25 | date = tables.DateColumn(format="D b Y") 26 | date_linkify = tables.DateColumn( 27 | accessor="date", format="D b Y", linkify=isoformat_link 28 | ) 29 | 30 | class Meta: 31 | default = "—" 32 | 33 | table = TestTable([{"date": date(2012, 9, 11)}, {"date": None}]) 34 | self.assertEqual(table.rows[0].get_cell("date"), "Tue sep 2012") 35 | self.assertEqual( 36 | table.rows[0].get_cell("date_linkify"), 'Tue sep 2012' 37 | ) 38 | self.assertEqual(table.rows[1].get_cell("date"), "—") 39 | 40 | def test_should_handle_long_format(self): 41 | class TestTable(tables.Table): 42 | date = tables.DateColumn(short=False) 43 | 44 | class Meta: 45 | default = "—" 46 | 47 | table = TestTable([{"date": date(2012, 9, 11)}, {"date": None}]) 48 | self.assertEqual(table.rows[0].get_cell("date"), "Sept. 11, 2012") 49 | self.assertEqual(table.rows[1].get_cell("date"), "—") 50 | 51 | def test_should_handle_short_format(self): 52 | class TestTable(tables.Table): 53 | date = tables.DateColumn(short=True) 54 | 55 | class Meta: 56 | default = "—" 57 | 58 | table = TestTable([{"date": date(2012, 9, 11)}, {"date": None}]) 59 | self.assertEqual(table.rows[0].get_cell("date"), "09/11/2012") 60 | self.assertEqual(table.rows[1].get_cell("date"), "—") 61 | 62 | def test_should_be_used_for_datefields(self): 63 | class DateModel(models.Model): 64 | field = models.DateField() 65 | 66 | class Meta: 67 | app_label = "django_tables2_test" 68 | 69 | class Table(tables.Table): 70 | class Meta: 71 | model = DateModel 72 | 73 | self.assertEqual(type(Table.base_columns["field"]), tables.DateColumn) 74 | 75 | def test_value_returns_a_raw_value_without_html(self): 76 | class Table(tables.Table): 77 | date = tables.DateColumn() 78 | date_linkify = tables.DateColumn(accessor="date", linkify=isoformat_link) 79 | 80 | table = Table([{"date": date(2012, 9, 12)}]) 81 | self.assertEqual(table.rows[0].get_cell_value("date"), "09/12/2012") 82 | self.assertEqual( 83 | table.rows[0].get_cell("date_linkify"), '09/12/2012' 84 | ) 85 | -------------------------------------------------------------------------------- /tests/columns/test_datetimecolumn.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytz 4 | from django.conf import settings 5 | from django.db import models 6 | from django.test import SimpleTestCase 7 | 8 | import django_tables2 as tables 9 | 10 | 11 | def isoformat_link(value): 12 | return f"/test/{value.isoformat()}/" 13 | 14 | 15 | class DateTimeColumnTest(SimpleTestCase): 16 | """ 17 | Date time formatting test case. 18 | 19 | Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date 20 | D -- Day of the week, textual, 3 letters -- 'Fri' 21 | b -- Month, textual, 3 letters, lowercase -- 'jan' 22 | Y -- Year, 4 digits. -- '1999' 23 | A -- 'AM' or 'PM'. -- 'AM' 24 | f -- Time, in 12-hour hours[:minutes] -- '1', '1:30' 25 | """ 26 | 27 | def dt(self): 28 | dt = datetime(2012, 9, 11, 12, 30, 0) 29 | return pytz.timezone(settings.TIME_ZONE).localize(dt) 30 | 31 | def test_should_handle_explicit_format(self): 32 | class TestTable(tables.Table): 33 | date = tables.DateTimeColumn(format="D b Y") 34 | date_linkify = tables.DateTimeColumn( 35 | format="D b Y", accessor="date", linkify=isoformat_link 36 | ) 37 | 38 | class Meta: 39 | default = "—" 40 | 41 | table = TestTable([{"date": self.dt()}, {"date": None}]) 42 | self.assertEqual(table.rows[0].get_cell("date"), "Tue sep 2012") 43 | self.assertEqual( 44 | table.rows[0].get_cell("date_linkify"), 45 | 'Tue sep 2012', 46 | ) 47 | self.assertEqual(table.rows[1].get_cell("date"), "—") 48 | 49 | def test_should_handle_long_format(self): 50 | class TestTable(tables.Table): 51 | date = tables.DateTimeColumn(short=False) 52 | 53 | class Meta: 54 | default = "—" 55 | 56 | table = TestTable([{"date": self.dt()}, {"date": None}]) 57 | self.assertEqual(table.rows[0].get_cell("date"), "Sept. 11, 2012, 12:30 p.m.") 58 | self.assertEqual(table.rows[1].get_cell("date"), "—") 59 | 60 | def test_should_handle_short_format(self): 61 | class TestTable(tables.Table): 62 | date = tables.DateTimeColumn(short=True) 63 | 64 | class Meta: 65 | default = "—" 66 | 67 | table = TestTable([{"date": self.dt()}, {"date": None}]) 68 | self.assertEqual(table.rows[0].get_cell("date"), "09/11/2012 12:30 p.m.") 69 | self.assertEqual(table.rows[1].get_cell("date"), "—") 70 | 71 | def test_should_be_used_for_datetimefields(self): 72 | class DateTimeModel(models.Model): 73 | field = models.DateTimeField() 74 | 75 | class Meta: 76 | app_label = "django_tables2_test" 77 | 78 | class Table(tables.Table): 79 | class Meta: 80 | model = DateTimeModel 81 | 82 | self.assertIsInstance(Table.base_columns["field"], tables.DateTimeColumn) 83 | 84 | def test_value_returns_a_raw_value_without_html(self): 85 | class Table(tables.Table): 86 | col = tables.DateTimeColumn() 87 | 88 | table = Table([{"col": self.dt()}]) 89 | self.assertEqual(table.rows[0].get_cell_value("col"), "09/11/2012 12:30 p.m.") 90 | -------------------------------------------------------------------------------- /tests/columns/test_emailcolumn.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.test import SimpleTestCase 3 | 4 | import django_tables2 as tables 5 | 6 | 7 | class EmailColumnTest(SimpleTestCase): 8 | def test_should_turn_email_address_into_hyperlink(self): 9 | class Table(tables.Table): 10 | email = tables.EmailColumn() 11 | 12 | table = Table([{"email": "test@example.com"}]) 13 | self.assertEqual( 14 | table.rows[0].get_cell("email"), 15 | 'test@example.com', 16 | ) 17 | 18 | def test_should_render_default_for_blank(self): 19 | class Table(tables.Table): 20 | email = tables.EmailColumn(default="---") 21 | 22 | table = Table([{"email": ""}]) 23 | self.assertEqual(table.rows[0].get_cell("email"), "---") 24 | 25 | def test_should_be_used_for_emailfields(self): 26 | class EmailModel(models.Model): 27 | field = models.EmailField() 28 | 29 | class Meta: 30 | app_label = "test" 31 | 32 | class Table(tables.Table): 33 | class Meta: 34 | model = EmailModel 35 | 36 | self.assertEqual(type(Table.base_columns["field"]), tables.EmailColumn) 37 | 38 | def test_text_should_be_overridable(self): 39 | class Table(tables.Table): 40 | email = tables.EmailColumn(text="@") 41 | 42 | table = Table([{"email": "test@example.com"}]) 43 | self.assertEqual(table.rows[0].get_cell("email"), '@') 44 | 45 | def test_value_returns_a_raw_value_without_html(self): 46 | class Table(tables.Table): 47 | col = tables.EmailColumn() 48 | 49 | table = Table([{"col": "test@example.com"}]) 50 | self.assertEqual(table.rows[0].get_cell_value("col"), "test@example.com") 51 | -------------------------------------------------------------------------------- /tests/columns/test_filecolumn.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.files.base import ContentFile 4 | from django.core.files.storage import FileSystemStorage 5 | from django.db import models 6 | from django.db.models.fields.files import FieldFile 7 | from django.test import SimpleTestCase 8 | 9 | import django_tables2 as tables 10 | 11 | from ..utils import parse 12 | 13 | 14 | def storage(): 15 | """Provide a storage that exposes the test templates.""" 16 | root = os.path.join(os.path.dirname(__file__), "..", "app", "templates") 17 | return FileSystemStorage(location=root, base_url="/baseurl/") 18 | 19 | 20 | def column(): 21 | return tables.FileColumn(attrs={"span": {"class": "span"}, "a": {"class": "a"}}) 22 | 23 | 24 | class FileColumnTest(SimpleTestCase): 25 | def test_should_be_used_for_filefields(self): 26 | class FileModel(models.Model): 27 | field = models.FileField() 28 | 29 | class Meta: 30 | app_label = "django_tables2_test" 31 | 32 | class Table(tables.Table): 33 | class Meta: 34 | model = FileModel 35 | 36 | self.assertEqual(type(Table.base_columns["field"]), tables.FileColumn) 37 | 38 | def test_filecolumn_supports_storage_file(self): 39 | file_ = storage().open("child/foo.html") 40 | try: 41 | root = parse(column().render(value=file_, record=None)) 42 | finally: 43 | file_.close() 44 | 45 | self.assertEqual(root.tag, "span") 46 | self.assertEqual(root.attrib, {"class": "span exists", "title": file_.name}) 47 | self.assertEqual(root.text, "foo.html") 48 | 49 | def test_filecolumn_supports_contentfile(self): 50 | name = "foobar.html" 51 | file_ = ContentFile("") 52 | file_.name = name 53 | 54 | root = parse(column().render(value=file_, record=None)) 55 | self.assertEqual(root.tag, "span") 56 | self.assertEqual(root.attrib, {"title": name, "class": "span"}) 57 | self.assertEqual(root.text, "foobar.html") 58 | 59 | def test_filecolumn_supports_fieldfile(self): 60 | field = models.FileField(storage=storage()) 61 | name = "child/foo.html" 62 | 63 | class Table(tables.Table): 64 | filecolumn = column() 65 | 66 | table = Table([{"filecolumn": FieldFile(instance=None, field=field, name=name)}]) 67 | html = table.rows[0].get_cell("filecolumn") 68 | root = parse(html) 69 | 70 | self.assertEqual(root.tag, "a") 71 | self.assertEqual(root.attrib, {"class": "a", "href": "/baseurl/child/foo.html"}) 72 | span = root.find("span") 73 | self.assertEqual(span.tag, "span") 74 | self.assertEqual(span.text, "foo.html") 75 | 76 | # Now try a file that doesn't exist 77 | name = "child/does_not_exist.html" 78 | fieldfile = FieldFile(instance=None, field=field, name=name) 79 | root = parse(column().render(value=fieldfile, record=None)) 80 | 81 | self.assertEqual(root.tag, "span") 82 | self.assertEqual(root.attrib, {"class": "span missing", "title": name}) 83 | self.assertEqual(root.text, "does_not_exist.html") 84 | 85 | def test_filecolumn_text_custom_value(self): 86 | file_ = ContentFile("") 87 | file_.name = "foobar.html" 88 | 89 | root = parse(tables.FileColumn(text="Download").render(value=file_, record=None)) 90 | self.assertEqual(root.tag, "span") 91 | self.assertEqual(root.attrib, {"title": file_.name, "class": ""}) 92 | self.assertEqual(root.text, "Download") 93 | -------------------------------------------------------------------------------- /tests/columns/test_initialsortcolumn.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.test import TestCase 3 | 4 | import django_tables2 as tables 5 | 6 | 7 | class InitialSortColumnTest(TestCase): 8 | def test_initial_sort_descending_affects_order_by_alias_next(self): 9 | class IntModel(models.Model): 10 | field = models.IntegerField() 11 | 12 | class Meta: 13 | app_label = "django_tables2_test" 14 | 15 | class Table(tables.Table): 16 | class Meta: 17 | model = IntModel 18 | 19 | class TableDescOrd(tables.Table): 20 | field = tables.Column(initial_sort_descending=True) 21 | 22 | class Meta: 23 | model = IntModel 24 | 25 | data = [{"field": 1}, {"field": 5}, {"field": 3}] 26 | 27 | # no initial ordering 28 | 29 | table = Table(data) 30 | table_desc = TableDescOrd(data) 31 | 32 | self.assertEqual(table.columns[1].order_by_alias.next, "field") 33 | self.assertEqual(table_desc.columns[1].order_by_alias.next, "-field") 34 | 35 | # with ascending ordering 36 | 37 | table = Table(data, order_by=("field",)) 38 | table_desc = TableDescOrd(data, order_by=("field",)) 39 | 40 | self.assertEqual(table.columns[1].order_by_alias.next, "-field") 41 | self.assertEqual(table_desc.columns[1].order_by_alias.next, "-field") 42 | self.assertEqual(table.rows[0].get_cell("field"), 1) 43 | self.assertEqual(table.rows[1].get_cell("field"), 3) 44 | self.assertEqual(table.rows[2].get_cell("field"), 5) 45 | self.assertEqual(table_desc.rows[0].get_cell("field"), 1) 46 | self.assertEqual(table_desc.rows[1].get_cell("field"), 3) 47 | self.assertEqual(table_desc.rows[2].get_cell("field"), 5) 48 | 49 | # with initial descending ordering 50 | 51 | table = Table(data, order_by=("-field",)) 52 | table_desc = TableDescOrd(data, order_by=("-field",)) 53 | 54 | self.assertEqual(table.columns[1].order_by_alias.next, "field") 55 | self.assertEqual(table_desc.columns[1].order_by_alias.next, "field") 56 | self.assertEqual(table.rows[0].get_cell("field"), 5) 57 | self.assertEqual(table.rows[1].get_cell("field"), 3) 58 | self.assertEqual(table.rows[2].get_cell("field"), 1) 59 | self.assertEqual(table_desc.rows[0].get_cell("field"), 5) 60 | self.assertEqual(table_desc.rows[1].get_cell("field"), 3) 61 | self.assertEqual(table_desc.rows[2].get_cell("field"), 1) 62 | -------------------------------------------------------------------------------- /tests/columns/test_jsoncolumn.py: -------------------------------------------------------------------------------- 1 | from django.contrib.postgres.fields import HStoreField 2 | from django.db import models 3 | from django.db.models import JSONField 4 | from django.test import SimpleTestCase 5 | 6 | import django_tables2 as tables 7 | 8 | 9 | class JsonColumnTestCase(SimpleTestCase): 10 | def test_should_be_used_for_json_and_hstore_fields(self): 11 | class Model(models.Model): 12 | json = JSONField() 13 | hstore = HStoreField() 14 | 15 | class Meta: 16 | app_label = "django_tables2_test" 17 | 18 | class Table(tables.Table): 19 | class Meta: 20 | model = Model 21 | 22 | self.assertIsInstance(Table.base_columns["json"], tables.JSONColumn) 23 | self.assertIsInstance(Table.base_columns["hstore"], tables.JSONColumn) 24 | 25 | def test_jsoncolumn_attrs(self): 26 | column = tables.JSONColumn(attrs={"pre": {"class": "json"}}) 27 | 28 | record = {"json": "foo"} 29 | html = column.render(value=record["json"], record=record) 30 | self.assertEqual(html, '
"foo"
') 31 | 32 | def test_jsoncolumn_dict(self): 33 | column = tables.JSONColumn() 34 | 35 | record = {"json": {"species": "Falcon"}} 36 | html = column.render(value=record["json"], record=record) 37 | self.assertEqual(html, "
{\n  "species": "Falcon"\n}
") 38 | 39 | def test_jsoncolumn_string(self): 40 | column = tables.JSONColumn() 41 | 42 | record = {"json": "really?"} 43 | html = column.render(value=record["json"], record=record) 44 | self.assertEqual(html, "
"really?"
") 45 | 46 | def test_jsoncolumn_number(self): 47 | column = tables.JSONColumn() 48 | 49 | record = {"json": 3.14} 50 | html = column.render(value=record["json"], record=record) 51 | self.assertEqual(html, "
3.14
") 52 | -------------------------------------------------------------------------------- /tests/columns/test_timecolumn.py: -------------------------------------------------------------------------------- 1 | from datetime import time 2 | 3 | from django.db import models 4 | from django.test import SimpleTestCase 5 | 6 | import django_tables2 as tables 7 | 8 | 9 | class TimeColumnTest(SimpleTestCase): 10 | def test_should_handle_explicit_format(self): 11 | class TestTable(tables.Table): 12 | time = tables.TimeColumn(format="H:i:s") 13 | 14 | class Meta: 15 | default = "—" 16 | 17 | table = TestTable([{"time": time(11, 11, 11)}, {"time": None}]) 18 | assert table.rows[0].get_cell("time") == "11:11:11" 19 | assert table.rows[1].get_cell("time") == "—" 20 | 21 | def test_should_be_used_for_timefields(self): 22 | class TimeModel(models.Model): 23 | field = models.TimeField() 24 | 25 | class Meta: 26 | app_label = "django_tables2_test" 27 | 28 | class Table(tables.Table): 29 | class Meta: 30 | model = TimeModel 31 | 32 | assert type(Table.base_columns["field"]) is tables.TimeColumn 33 | 34 | def test_value_returns_a_raw_value_without_html(self): 35 | class Table(tables.Table): 36 | col = tables.TimeColumn(format="H:i:s") 37 | 38 | table = Table([{"col": time(11, 11, 11)}]) 39 | assert table.rows[0].get_cell_value("col") == "11:11:11" 40 | -------------------------------------------------------------------------------- /tests/columns/test_urlcolumn.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.test import SimpleTestCase 3 | 4 | import django_tables2 as tables 5 | 6 | MEMORY_DATA = [ 7 | {"url": "http://example.com", "name": "Example"}, 8 | {"url": "https://example.com", "name": "Example (https)"}, 9 | {"url": "ftp://example.com", "name": "Example (ftp)"}, 10 | ] 11 | 12 | 13 | class UrlColumnTest(SimpleTestCase): 14 | def test_should_turn_url_into_hyperlink(self): 15 | class TestTable(tables.Table): 16 | url = tables.URLColumn() 17 | 18 | table = TestTable(MEMORY_DATA) 19 | 20 | self.assertEqual( 21 | table.rows[0].get_cell("url"), 'http://example.com' 22 | ) 23 | self.assertEqual( 24 | table.rows[1].get_cell("url"), 'https://example.com' 25 | ) 26 | 27 | def test_should_be_used_for_urlfields(self): 28 | class URLModel(models.Model): 29 | field = models.URLField() 30 | 31 | class Meta: 32 | app_label = "django_tables2_test" 33 | 34 | class Table(tables.Table): 35 | class Meta: 36 | model = URLModel 37 | 38 | assert type(Table.base_columns["field"]) is tables.URLColumn 39 | 40 | def test_text_can_be_overridden(self): 41 | class Table(tables.Table): 42 | url = tables.URLColumn(text="link") 43 | 44 | table = Table(MEMORY_DATA) 45 | 46 | assert table.rows[0].get_cell("url") == 'link' 47 | 48 | def test_text_can_be_overridden_with_callable(self): 49 | class Table(tables.Table): 50 | url = tables.URLColumn(text=lambda record: record["name"]) 51 | 52 | table = Table(MEMORY_DATA) 53 | 54 | assert table.rows[0].get_cell("url") == 'Example' 55 | assert table.rows[1].get_cell("url") == 'Example (https)' 56 | 57 | def test_value_returns_a_raw_value_without_html(self): 58 | class TestTable(tables.Table): 59 | col = tables.URLColumn() 60 | 61 | table = TestTable([{"col": "http://example.com"}]) 62 | 63 | assert table.rows[0].get_cell_value("col") == "http://example.com" 64 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock, Mock 2 | 3 | from django.core.paginator import EmptyPage, PageNotAnInteger 4 | from django.test import SimpleTestCase, TestCase 5 | 6 | from django_tables2 import Column, RequestConfig, Table 7 | 8 | from .app.models import Person 9 | from .utils import build_request 10 | 11 | NOTSET = object() # unique value 12 | 13 | 14 | def MockTable(**kwargs): 15 | return MagicMock( 16 | prefixed_page_field="page", 17 | prefixed_per_page_field="per_page", 18 | prefixed_order_by_field="sort", 19 | **kwargs, 20 | ) 21 | 22 | 23 | class ConfigTest(SimpleTestCase): 24 | def test_no_querystring(self): 25 | table = MockTable(order_by=NOTSET) 26 | 27 | RequestConfig(build_request("/")).configure(table) 28 | 29 | table.paginate.assert_called() 30 | self.assertEqual(table.order_by, NOTSET) 31 | 32 | def test_full_querystring(self): 33 | request = build_request("/?page=1&per_page=5&sort=abc") 34 | table = MockTable() 35 | 36 | RequestConfig(request).configure(table) 37 | 38 | table.paginate.assert_called_with(page=1, per_page=5) 39 | self.assertEqual(table.order_by, ["abc"]) 40 | 41 | def test_partial_querystring(self): 42 | table = MockTable() 43 | request = build_request("/?page=1&sort=abc") 44 | 45 | RequestConfig(request, paginate={"per_page": 5}).configure(table) 46 | 47 | table.paginate.assert_called_with(page=1, per_page=5) 48 | self.assertEqual(table.order_by, ["abc"]) 49 | 50 | def test_silent_page_not_an_integer_error(self): 51 | request = build_request("/") 52 | table = MockTable(paginate=Mock(side_effect=PageNotAnInteger), paginator=MagicMock()) 53 | 54 | RequestConfig(request, paginate={"page": "abc", "silent": True}).configure(table) 55 | 56 | table.paginate.assert_called_with(page="abc") 57 | table.paginator.page.assert_called_with(1) 58 | 59 | def test_silent_empty_page_error(self): 60 | request = build_request("/") 61 | table = MockTable(paginate=Mock(side_effect=EmptyPage), paginator=MagicMock(num_pages=987)) 62 | 63 | RequestConfig(request, paginate={"page": 123, "silent": True}).configure(table) 64 | 65 | table.paginator.page.assert_called_with(987) 66 | 67 | def test_passing_request_to_constructor(self): 68 | """Table constructor should call RequestConfig if a request is passed.""" 69 | request = build_request("/?page=1&sort=abc") 70 | 71 | class SimpleTable(Table): 72 | abc = Column() 73 | 74 | table = SimpleTable([{}], request=request) 75 | self.assertTrue(table.columns["abc"].is_ordered) 76 | 77 | def test_request_is_added_to_the_table(self): 78 | table = MockTable() 79 | request = build_request("/") 80 | 81 | RequestConfig(request, paginate=False).configure(table) 82 | 83 | self.assertEqual(table.request, request) 84 | 85 | 86 | class NoPaginationQueriesTest(TestCase): 87 | def test_should_not_count_with_paginate_False(self): 88 | """ 89 | No extra queries with pagination turned off. 90 | 91 | https://github.com/jieter/django-tables2/issues/551 92 | """ 93 | 94 | class MyTable(Table): 95 | first_name = Column() 96 | 97 | class Meta: 98 | template_name = "minimal.html" 99 | 100 | request = build_request() 101 | 102 | Person.objects.create(first_name="Talip", last_name="Molenschot") 103 | 104 | table = MyTable(Person.objects.all()) 105 | RequestConfig(request, paginate=False).configure(table) 106 | 107 | with self.assertNumQueries(1): 108 | html = table.as_html(request) 109 | 110 | self.assertIn("", html) 111 | -------------------------------------------------------------------------------- /tests/test_faq.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase 2 | 3 | import django_tables2 as tables 4 | 5 | from .utils import build_request, parse 6 | 7 | TEST_DATA = [ 8 | {"name": "Belgium", "population": 11200000}, 9 | {"name": "Luxembourgh", "population": 540000}, 10 | {"name": "France", "population": 66000000}, 11 | ] 12 | 13 | 14 | class FaqTest(SimpleTestCase): 15 | def test_row_counter_using_templateColumn(self): 16 | class CountryTable(tables.Table): 17 | counter = tables.TemplateColumn("{{ row_counter }}") 18 | name = tables.Column() 19 | 20 | expected = "" 21 | 22 | table = CountryTable(TEST_DATA) 23 | html = table.as_html(build_request()) 24 | self.assertIn(expected, html) 25 | 26 | # the counter should start at zero the second time too 27 | table = CountryTable(TEST_DATA) 28 | html = table.as_html(build_request()) 29 | self.assertIn(expected, html) 30 | 31 | def test_row_footer_total(self): 32 | class CountryTable(tables.Table): 33 | name = tables.Column() 34 | population = tables.Column( 35 | footer=lambda table: f'Total: {sum(x["population"] for x in table.data)}' 36 | ) 37 | 38 | table = CountryTable(TEST_DATA) 39 | html = table.as_html(build_request()) 40 | 41 | columns = parse(html).findall(".//tfoot/tr")[-1].findall("td") 42 | self.assertEqual(columns[1].text, "Total: 77740000") 43 | -------------------------------------------------------------------------------- /tests/test_footer.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase 2 | 3 | import django_tables2 as tables 4 | 5 | from .utils import build_request, parse 6 | 7 | MEMORY_DATA = [ 8 | {"name": "Queensland", "country": "Australia", "population": 4750500}, 9 | {"name": "New South Wales", "country": "Australia", "population": 7565500}, 10 | {"name": "Victoria", "country": "Australia", "population": 6000000}, 11 | {"name": "Tasmania", "country": "Australia", "population": 517000}, 12 | ] 13 | 14 | 15 | class FooterTest(SimpleTestCase): 16 | def test_has_footer_is_False_without_footer(self): 17 | class Table(tables.Table): 18 | name = tables.Column() 19 | country = tables.Column() 20 | population = tables.Column() 21 | 22 | table = Table(MEMORY_DATA) 23 | self.assertFalse(table.has_footer()) 24 | 25 | def test_footer(self): 26 | class Table(tables.Table): 27 | name = tables.Column() 28 | country = tables.Column(footer="Total:") 29 | population = tables.Column( 30 | footer=lambda table: sum(x["population"] for x in table.data) 31 | ) 32 | 33 | table = Table(MEMORY_DATA) 34 | self.assertTrue(table.has_footer()) 35 | html = table.as_html(build_request("/")) 36 | 37 | columns = parse(html).findall(".//tfoot/tr/td") 38 | self.assertEqual(columns[1].text, "Total:") 39 | self.assertEqual(columns[2].text, "18833000") 40 | 41 | def test_footer_disable_on_table(self): 42 | """Showing the footer can be disabled using show_footer argument to the Table constructor.""" 43 | 44 | class Table(tables.Table): 45 | name = tables.Column() 46 | country = tables.Column(footer="Total:") 47 | 48 | table = Table(MEMORY_DATA, show_footer=False) 49 | self.assertFalse(table.has_footer()) 50 | 51 | def test_footer_column_method(self): 52 | class SummingColumn(tables.Column): 53 | def render_footer(self, bound_column, table): 54 | return sum(bound_column.accessor.resolve(row) for row in table.data) 55 | 56 | class TestTable(tables.Table): 57 | name = tables.Column() 58 | country = tables.Column(footer="Total:") 59 | population = SummingColumn() 60 | 61 | table = TestTable(MEMORY_DATA) 62 | html = table.as_html(build_request("/")) 63 | 64 | columns = parse(html).findall(".//tfoot/tr/td") 65 | self.assertEqual(columns[1].text, "Total:") 66 | self.assertEqual(columns[2].text, "18833000") 67 | 68 | def test_footer_has_class(self): 69 | class SummingColumn(tables.Column): 70 | def render_footer(self, bound_column, table): 71 | return sum(bound_column.accessor.resolve(row) for row in table.data) 72 | 73 | class TestTable(tables.Table): 74 | name = tables.Column() 75 | country = tables.Column(footer="Total:") 76 | population = SummingColumn(attrs={"tf": {"class": "population_sum"}}) 77 | 78 | table = TestTable(MEMORY_DATA) 79 | html = table.as_html(build_request("/")) 80 | 81 | columns = parse(html).findall(".//tfoot/tr/td") 82 | self.assertEqual(columns[2].attrib, {"class": "population_sum"}) 83 | 84 | def test_footer_custom_attriubtes(self): 85 | class SummingColumn(tables.Column): 86 | def render_footer(self, bound_column, table): 87 | return sum(bound_column.accessor.resolve(row) for row in table.data) 88 | 89 | class TestTable(tables.Table): 90 | name = tables.Column() 91 | country = tables.Column(footer="Total:", attrs={"tf": {"align": "right"}}) 92 | population = SummingColumn() 93 | 94 | table = TestTable(MEMORY_DATA) 95 | table.columns["country"].attrs["tf"] = {"align": "right"} 96 | html = table.as_html(build_request("/")) 97 | 98 | columns = parse(html).findall(".//tfoot/tr/td") 99 | assert "align" in columns[1].attrib 100 | -------------------------------------------------------------------------------- /tests/test_paginators.py: -------------------------------------------------------------------------------- 1 | from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator 2 | from django.test import TestCase 3 | 4 | from django_tables2 import LazyPaginator 5 | 6 | 7 | class FakeQuerySet: 8 | objects = range(1, 10**6) 9 | 10 | def count(self): 11 | raise AssertionError("LazyPaginator should not call QuerySet.count()") 12 | 13 | def __getitem__(self, key): 14 | return self.objects[key] 15 | 16 | def __iter__(self): 17 | yield next(self.objects) 18 | 19 | 20 | class LazyPaginatorTest(TestCase): 21 | def test_compare_to_default_paginator(self): 22 | objects = list(range(1, 1000)) 23 | 24 | paginator = Paginator(objects, 10) 25 | lazy_paginator = LazyPaginator(objects, 10) 26 | self.assertEqual(paginator.page(1).object_list, lazy_paginator.page(1).object_list) 27 | self.assertEqual(paginator.page(10).object_list, lazy_paginator.page(10).object_list) 28 | self.assertEqual(paginator.page(100).object_list, lazy_paginator.page(100).object_list) 29 | 30 | def test_no_count_call(self): 31 | paginator = LazyPaginator(FakeQuerySet(), 10) 32 | # num_pages initially is None, but is page_number + 1 after requesting a page. 33 | self.assertEqual(paginator.num_pages, None) 34 | 35 | paginator.page(1) 36 | self.assertEqual(paginator.num_pages, 2) 37 | paginator.page(3) 38 | self.assertEqual(paginator.num_pages, 4) 39 | 40 | paginator.page(1.0) 41 | # and again decreases when a lower page nu 42 | self.assertEqual(paginator.num_pages, 2) 43 | 44 | with self.assertRaisesMessage(PageNotAnInteger, "That page number is not an integer"): 45 | paginator.page(1.5) 46 | 47 | with self.assertRaisesMessage(EmptyPage, "That page number is less than 1"): 48 | paginator.page(-1) 49 | 50 | with self.assertRaises(NotImplementedError): 51 | paginator.count() 52 | 53 | with self.assertRaises(NotImplementedError): 54 | paginator.page_range() 55 | 56 | # last page 57 | last_page_number = 10**5 58 | paginator.page(last_page_number) 59 | 60 | with self.assertRaisesMessage(EmptyPage, "That page contains no results"): 61 | paginator.page(last_page_number + 1) 62 | 63 | def test_lookahead(self): 64 | objects = list(range(1, 1000)) 65 | paginator = LazyPaginator(objects, 10, look_ahead=3) 66 | 67 | self.assertEqual(paginator.look_ahead, 3) 68 | self.assertEqual(paginator.page(1).object_list, list(range(1, 11))) 69 | self.assertEqual(paginator.num_pages, 4) 70 | 71 | self.assertEqual(paginator.page(98).object_list, list(range(971, 981))) 72 | self.assertEqual(paginator.num_pages, 100) 73 | 74 | def test_number_is_none(self): 75 | """When number=None is supplied, the paginator should serve its first page.""" 76 | objects = list(range(1, 1000)) 77 | paginator = LazyPaginator(objects, 10, look_ahead=3) 78 | self.assertEqual(paginator.page(None).object_list, list(range(1, 11))) 79 | self.assertEqual(paginator.num_pages, 4) 80 | 81 | objects = list(range(1, 20)) 82 | paginator = LazyPaginator(objects, 10, look_ahead=3) 83 | self.assertEqual(paginator.page(None).object_list, list(range(1, 11))) 84 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | 3 | import lxml.etree 4 | import lxml.html 5 | from django.core.handlers.wsgi import WSGIRequest 6 | from django.test.client import FakePayload 7 | 8 | 9 | def parse(html): 10 | # We use html instead of etree. Because etree can't parse html entities. 11 | return lxml.html.fromstring(html) 12 | 13 | 14 | def attrs(xml: str) -> dict: 15 | """Return a dict of XML attributes, given an element.""" 16 | return lxml.html.fromstring(xml).attrib 17 | 18 | 19 | def build_request(uri="/", user=None): 20 | """ 21 | Return a fresh HTTP GET / request. 22 | 23 | This is essentially a heavily cutdown version of Django's 24 | `~django.test.client.RequestFactory`. 25 | """ 26 | path, _, querystring = uri.partition("?") 27 | request = WSGIRequest( 28 | { 29 | "CONTENT_TYPE": "text/html; charset=utf-8", 30 | "PATH_INFO": path, 31 | "QUERY_STRING": querystring, 32 | "REMOTE_ADDR": "127.0.0.1", 33 | "REQUEST_METHOD": "GET", 34 | "SCRIPT_NAME": "", 35 | "SERVER_NAME": "testserver", 36 | "SERVER_PORT": "80", 37 | "SERVER_PROTOCOL": "HTTP/1.1", 38 | "wsgi.version": (1, 0), 39 | "wsgi.url_scheme": "http", 40 | "wsgi.input": FakePayload(b""), 41 | "wsgi.errors": StringIO(), 42 | "wsgi.multiprocess": True, 43 | "wsgi.multithread": False, 44 | "wsgi.run_once": False, 45 | } 46 | ) 47 | if user is not None: 48 | request.user = user 49 | return request 50 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | args_are_paths = false 3 | envlist = 4 | py39-{4.2}, 5 | py310{4.2,5.0,5.1,master}, 6 | py311{4.2,5.0,5.1,master}, 7 | py312{5.0,5.1,master}, 8 | ; py313{master}, 9 | docs, 10 | flake8, 11 | isort, 12 | 13 | [testenv] 14 | basepython = 15 | py39: python3.9 16 | py310: python3.10 17 | py311: python3.11 18 | py312: python3.12 19 | py313: python3.13 20 | usedevelop = true 21 | pip_pre = true 22 | setenv = 23 | PYTHONPATH={toxinidir} 24 | PYTHONWARNINGS=all 25 | commands = 26 | coverage run --source=django_tables2 manage.py test {posargs} 27 | deps = 28 | 4.2: Django==4.2.* 29 | 5.0: Django==5.0.* 30 | 5.1: Django==5.1.* 31 | master: https://github.com/django/django/archive/master.tar.gz 32 | coverage 33 | -r{toxinidir}/requirements/common.pip 34 | 35 | [testenv:docs] 36 | basepython = python3.11 37 | whitelist_externals = make 38 | changedir = docs 39 | setenv = 40 | PYTHONWARNINGS=default 41 | commands = 42 | make html 43 | make spelling 44 | deps = 45 | -r{toxinidir}/docs/requirements.txt 46 | --------------------------------------------------------------------------------
0