diff -Nru django-tables-1.14.2/CHANGELOG.md django-tables-1.21.2/CHANGELOG.md --- django-tables-1.14.2/CHANGELOG.md 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/CHANGELOG.md 2018-03-26 06:43:09.000000000 +0000 @@ -1,5 +1,55 @@ # Change log + +## 1.21.2 (2018-03-26) + - Moved table instantiation from `get_context_data` to `get_tables` [#554](https://github.com/jieter/django-tables2/pull/554) by [@sdolemelipone](https://github.com/sdolemelipone) + - Pass request as kwarg to template.render, rather than as part of context. + (fixes [#552](https://github.com/jieter/django-tables2/issues/552)) + +## 1.21.1 (2018-03-12) + - Do not perform extra `COUNT()` queries for non-paginated tables. Fixes [#551](https://github.com/jieter/django-tables2/issues/551) + +## 1.21.0 (2018-03-12) + - Add new method `paginated_rows` to `Table` to replace fallback to non-paginated rows in templates. + - Prevent mutation of the template context `{% render_table %}` is called from (fixes [#547](https://github.com/jieter/django-tables2/issues/547)) + +## 1.20.0 (2018-03-08) + - Define and use `get_table_data` in `MultiTableMixin` [#538]https://github.com/jieter/django-tables2/pull/538) by [@vCra](https://github.com/vCra) (fixes [#528](https://github.com/jieter/django-tables2/issues/528)) + - Added `{% export_url %}` template tag. + - Allow passing a `TableData`-derived class to the data argument of the `Table` constructor, instead of a QuerySet or list of dicts. + +## 1.19.0 (2018-02-02) + - `BoundColumn.attrs` does not evaluate `current_value` as `bool` [#536](https://github.com/jieter/django-tables2/pull/536) by [@pachewise](https://github.com/pachewise) (fixes [#534](https://github.com/jieter/django-tables2/issues/534)) + - Allow more flexible access to cell values (especially useful for django templates) (fixes [#485](https://github.com/jieter/django-tables2/issues/485)) + +## 1.18.0 (2018-01-27) + - Follow relations when detecting column type for fields in `Table.Meta.fields` (fixes [#498](https://github.com/jieter/django-tables2/issues/498)) + - Renamed `Table.Meta.template` to `template_name` (with deprecation warning for the former) [#542](https://github.com/jieter/django-tables2/pull/524) (fixes [#520](https://github.com/jieter/django-tables2/issues/520)) + - Added Czech translation [#533](https://github.com/jieter/django-tables2/pull/533) by [@OndraRehounek](https://github.com/OndraRehounek) + - Added `table_factory` [#532](https://github.com/jieter/django-tables2/pull/532) by [@ZuluPro](https://github.com/ZuluPro) + +## 1.17.1 (2017-12-14) + - Fix typo in setup.py for `extras_require`. + +## 1.17.0 (2017-12-14) + - Dropped support for Django 1.8, 1.9 and 1.10. + - Add `extra_context` argument to `TemplateColumn` [#509](https://github.com/jieter/django-tables2/pull/509) by [@ad-m](https://github.com/ad-m) + - Remove unnecessary cast of record to `str` [#514](https://github.com/jieter/django-tables2/pull/514), fixes [#511](https://github.com/jieter/django-tables2/issues/511) + - Use `django.test.TestCase` for all tests, and remove dependency on pytest and reorganized some tests [#515](https://github.com/jieter/django-tables2/pull/515) + - Remove traces of django-haystack tests from the tests, there were no actual tests. + +## 1.16.0 (2017-11-27) +This is the last version supporting Django 1.8, 1.9 and 1.10. Django 1.8 is only supported until april 2018, so consider upgrading to Django 1.11! + - Added `tf` dictionary to `Column.attrs` with default values for the footer, so footers now have `class` attribute by default [#501](https://github.com/jieter/django-tables2/pull/501) by [@mpasternak](https://github.com/mpasternak) + +## 1.15.0 (2017-11-23) + - Added `as=varname` keyword argument to the `{% querystring %}` template tag, + fixes [#481](https://github.com/jieter/django-tables2/issues/481) + - Updated the tutorial to reflect current state of Django a bit better. + - Used `OrderedDict` rather than `dict` as the parent for `utils.AttributeDict` to make the rendered html more consistant accross python versions. + - Allow reading column `attrs` from a column's attribute, allowing easier reuse of custom column attributes (fixes [#241](https://github.com/jieter/django-tables2/issues/241)) + - `value` and `record` are optionally passed to the column attrs callables for data rows. [#503](https://github.com/jieter/django-tables2/pull/503), fixes [#500](https://github.com/jieter/django-tables2/issues/500) + ## 1.14.2 (2017-10-30) - Added a `row_counter` variable to the template context in `TemplateColumn` (fixes [#448](https://github.com/jieter/django-tables2/issues/488)) diff -Nru django-tables-1.14.2/CONTRIBUTING.md django-tables-1.21.2/CONTRIBUTING.md --- django-tables-1.14.2/CONTRIBUTING.md 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/CONTRIBUTING.md 2018-03-26 06:43:09.000000000 +0000 @@ -15,7 +15,7 @@ It will take care of installing the correct dependencies. During development, you might not want to wait for the tests to run in all environments. In that case, use the `-e` argument to specify an environment: -`tox -e py27-1.9` to run the tests in python 2.7 with Django 1.9, +`tox -e py27-1.11` to run the tests in python 2.7 with Django 1.11, or `PYTHONPATH=. py.test` to run the tests against your current environment (which is even quicker). ## Code coverage @@ -39,6 +39,6 @@ -------------------- 1. Bump the version in `django-tables2/__init__.py`. -2. Update CHANGELOG.md`. -3. Create a tag `./setup.py tag` or `git tag -a v1.0.6 -m 'tagging v1.0.6'` -4. Run `./setup.py publish` or `python setup.py sdist upload --sign --identity=`. +2. Update `CHANGELOG.md`. +3. Create a tag `./setup.py tag`. +4. Run `./setup.py publish` diff -Nru django-tables-1.14.2/debian/changelog django-tables-1.21.2/debian/changelog --- django-tables-1.14.2/debian/changelog 2017-10-30 21:31:32.000000000 +0000 +++ django-tables-1.21.2/debian/changelog 2018-05-08 21:56:52.000000000 +0000 @@ -1,3 +1,32 @@ +django-tables (1.21.2-1) unstable; urgency=low + + [ Ondřej Nový ] + * d/control: Set Vcs-* to salsa.debian.org + * d/copyright: Use https protocol in Format field + + [ Joseph Herlant ] + * New upstream version 1.21.2 + * d/rules: new test management system + * wrap-n-sort + * d/compat, d/control: upgrade standards to 4.1.4 and compat to 11 + * d/control: update build dependencies + * Remove patch now integrated upstream + * d/rules: re-enable export module + * d/control: add explicitly autopkgtest testsuite (Closes: #895009) + * d/control: update the dependencies now that we have python3-tablib + * Fix debian-pycompat-is-obsolete + * d/control: update package descriptions to avoid duplicates + (Closes: #896396, #896429) + * Add autopkgtests with a real use case too + * Adding the examples in the doc package + * Move static content to usr/share + * d/control: adding myself as Uploaders to avoid nmu warnings + * Fix privacy-breach lintian issues + * d/rules: fix package-installs-python-pycache-dir + * Adapt doc-base to match new rules about location from policy 4.1.4/dh11 + + -- Brian May Wed, 09 May 2018 07:56:52 +1000 + django-tables (1.14.2-1) unstable; urgency=medium * New upstream version. diff -Nru django-tables-1.14.2/debian/compat django-tables-1.21.2/debian/compat --- django-tables-1.14.2/debian/compat 2017-10-30 21:01:53.000000000 +0000 +++ django-tables-1.21.2/debian/compat 2018-05-08 21:30:11.000000000 +0000 @@ -1 +1 @@ -9 +11 diff -Nru django-tables-1.14.2/debian/control django-tables-1.21.2/debian/control --- django-tables-1.14.2/debian/control 2017-10-30 21:26:18.000000000 +0000 +++ django-tables-1.21.2/debian/control 2018-05-08 21:30:11.000000000 +0000 @@ -2,45 +2,74 @@ Section: python Priority: optional Maintainer: Debian Python Modules Team -Uploaders: Brian May -Build-Depends: debhelper (>=9), dh-python, - python-all (>= 2.6.6-3~), python-django (>= 1.8), python-setuptools, python-six, - python-pytest-django, python-fudge, python-lxml, python-tz, - python-django-haystack, python-sphinx-rtd-theme, python-recommonmark, - python3-all, python3-django (>= 1.8), python3-setuptools, python3-six, - python3-pytest-django, python3-fudge, python3-lxml, python3-tz, - python3-django-haystack, python3-sphinx-rtd-theme, python3-recommonmark, -Build-Depends-Indep: python3-sphinx (>= 1.0.7+dfsg-1~), python-doc, python-django-doc -Standards-Version: 4.1.1 +Uploaders: Brian May , Joseph Herlant +Build-Depends: debhelper (>=11), + dh-python, + python-all (>= 2.6.6-3~), + python-setuptools, + python3-all, + python3-setuptools +Build-Depends-Indep: python-django (>= 1.8), + python-doc, + python-fudge, + python-lxml, + python-mock, + python-psycopg2, + python-recommonmark, + python-six, + python-sphinx-rtd-theme, + python-tablib, + python-tz, + python3-django (>= 1.8), + python3-fudge, + python3-lxml, + python3-mock, + python3-psycopg2, + python3-recommonmark, + python3-six, + python3-sphinx (>= 1.0.7+dfsg-1~), + python3-sphinx-rtd-theme, + python3-tablib, + python3-tz +Standards-Version: 4.1.4 X-Python-Version: >= 2.6 Homepage: https://github.com/bradleyayers/django-tables2/ -Vcs-Git: https://anonscm.debian.org/git/python-modules/packages/django-tables.git -Vcs-Browser: https://anonscm.debian.org/cgit/python-modules/packages/django-tables.git +Vcs-Git: https://salsa.debian.org/python-team/modules/django-tables.git +Vcs-Browser: https://salsa.debian.org/python-team/modules/django-tables Package: python-django-tables2 Architecture: all -Depends: ${misc:Depends}, ${python:Depends}, python-django (>= 1.8) +Depends: python-django (>= 1.8), ${misc:Depends}, ${python:Depends} Suggests: python-django-tables2-doc Replaces: django-tables (<< 0.13.0-2) Breaks: django-tables (<< 0.13.0-2) -Description: Table/data-grid framework for Django +Description: Table/data-grid framework for Django (Python 2.7) django-tables2 simplifies the task of turning sets of data into HTML tables. It has native support for pagination and sorting. It does for HTML tables what ``django.forms`` does for HTML forms. + . + This package provides the Python 2.7 module. Package: python3-django-tables2 Architecture: all -Depends: ${misc:Depends}, ${python3:Depends}, python3-django (>= 1.8) +Depends: python3-django (>= 1.8), ${misc:Depends}, ${python3:Depends} Suggests: python-django-tables2-doc -Description: Table/data-grid framework for Django +Description: Table/data-grid framework for Django (Python 3) django-tables2 simplifies the task of turning sets of data into HTML tables. It has native support for pagination and sorting. It does for HTML tables what ``django.forms`` does for HTML forms. + . + This package provides the Python 3 module. Package: python-django-tables2-doc Section: doc Architecture: all -Depends: ${misc:Depends}, ${sphinxdoc:Depends}, python-doc, python-django-doc +Depends: libjs-bootstrap, + libjs-jquery, + python-django-doc, + python-doc, + ${misc:Depends}, + ${sphinxdoc:Depends} Description: Table/data-grid framework for Django (Documentation) django-tables2 simplifies the task of turning sets of data into HTML tables. It has native support for pagination and sorting. It does for HTML tables what diff -Nru django-tables-1.14.2/debian/copyright django-tables-1.21.2/debian/copyright --- django-tables-1.14.2/debian/copyright 2016-05-03 01:04:05.000000000 +0000 +++ django-tables-1.21.2/debian/copyright 2018-05-08 21:30:11.000000000 +0000 @@ -1,4 +1,4 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: django-tables Upstream-Contact: Alex Gaynor Source: https://github.com/bradleyayers/django-tables2/ diff -Nru django-tables-1.14.2/debian/patches/0002-Fix-python2-only-code.patch django-tables-1.21.2/debian/patches/0002-Fix-python2-only-code.patch --- django-tables-1.14.2/debian/patches/0002-Fix-python2-only-code.patch 2017-10-30 21:26:18.000000000 +0000 +++ django-tables-1.21.2/debian/patches/0002-Fix-python2-only-code.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -From: Brian May -Date: Tue, 31 Oct 2017 08:25:49 +1100 -Subject: Fix python2 only code - ---- - docs/conf.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/docs/conf.py b/docs/conf.py -index 150e362..d656c39 100644 ---- a/docs/conf.py -+++ b/docs/conf.py -@@ -14,7 +14,7 @@ sys.path.insert(0, os.path.abspath('../')) - - project = 'django-tables2' - with open('../django_tables2/__init__.py', 'rb') as f: -- release = re.search('__version__ = \'(.+?)\'', f.read()).group(1) -+ release = re.search('__version__ = \'(.+?)\'', f.read().decode('UTF8')).group(1) - version = release.rpartition('.')[0] - - diff -Nru django-tables-1.14.2/debian/patches/privacy_breach_fix.patch django-tables-1.21.2/debian/patches/privacy_breach_fix.patch --- django-tables-1.14.2/debian/patches/privacy_breach_fix.patch 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/debian/patches/privacy_breach_fix.patch 2018-05-08 21:30:11.000000000 +0000 @@ -0,0 +1,51 @@ +Description: Replace privacy breach links by packaged copies +Author: Joseph Herlant +Last-Update: 2018-05-04 +--- a/docs/pages/tutorial.rst ++++ b/docs/pages/tutorial.rst +@@ -107,7 +107,7 @@ + + + List of persons +- ++ + + + {% render_table table %} +--- a/example/templates/index.html ++++ b/example/templates/index.html +@@ -3,7 +3,7 @@ + {% load static %} + + {% block extrahead %} +- ++ + + {% endblock %} + +--- a/example/templates/multiTable.html ++++ b/example/templates/multiTable.html +@@ -3,7 +3,7 @@ + {% load static %} + + {% block extrahead %} +- ++ + + {% endblock %} + +--- a/example/templates/semantic_template.html ++++ b/example/templates/semantic_template.html +@@ -3,9 +3,9 @@ + + + django_tables2 with semantic template example +- +- +- ++ ++ ++ + + +
diff -Nru django-tables-1.14.2/debian/patches/series django-tables-1.21.2/debian/patches/series --- django-tables-1.14.2/debian/patches/series 2017-10-30 21:26:18.000000000 +0000 +++ django-tables-1.21.2/debian/patches/series 2018-05-08 21:30:11.000000000 +0000 @@ -1,2 +1,2 @@ local_inventory -0002-Fix-python2-only-code.patch +privacy_breach_fix.patch diff -Nru django-tables-1.14.2/debian/pycompat django-tables-1.21.2/debian/pycompat --- django-tables-1.14.2/debian/pycompat 2016-05-03 01:04:05.000000000 +0000 +++ django-tables-1.21.2/debian/pycompat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -2 diff -Nru django-tables-1.14.2/debian/python3-django-tables2.install django-tables-1.21.2/debian/python3-django-tables2.install --- django-tables-1.14.2/debian/python3-django-tables2.install 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/debian/python3-django-tables2.install 2018-05-08 21:30:11.000000000 +0000 @@ -0,0 +1 @@ +django_tables2/static/django_tables2/* usr/share/python3-django-tables2/ diff -Nru django-tables-1.14.2/debian/python-django-tables2-doc.doc-base django-tables-1.21.2/debian/python-django-tables2-doc.doc-base --- django-tables-1.14.2/debian/python-django-tables2-doc.doc-base 2016-05-03 01:04:05.000000000 +0000 +++ django-tables-1.21.2/debian/python-django-tables2-doc.doc-base 2018-05-08 21:30:11.000000000 +0000 @@ -5,5 +5,5 @@ Section: Programming/Python Format: HTML -Index: /usr/share/doc/python-django-tables2-doc/html/index.html -Files: /usr/share/doc/python-django-tables2-doc/html/*.html +Index: /usr/share/doc/python-django-tables2/html/index.html +Files: /usr/share/doc/python-django-tables2/html/*.html diff -Nru django-tables-1.14.2/debian/python-django-tables2-doc.examples django-tables-1.21.2/debian/python-django-tables2-doc.examples --- django-tables-1.14.2/debian/python-django-tables2-doc.examples 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/debian/python-django-tables2-doc.examples 2018-05-08 21:30:11.000000000 +0000 @@ -0,0 +1 @@ +example/* diff -Nru django-tables-1.14.2/debian/python-django-tables2.install django-tables-1.21.2/debian/python-django-tables2.install --- django-tables-1.14.2/debian/python-django-tables2.install 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/debian/python-django-tables2.install 2018-05-08 21:30:11.000000000 +0000 @@ -0,0 +1 @@ +django_tables2/static/django_tables2/* usr/share/python-django-tables2/ diff -Nru django-tables-1.14.2/debian/rules django-tables-1.21.2/debian/rules --- django-tables-1.14.2/debian/rules 2017-10-30 21:01:53.000000000 +0000 +++ django-tables-1.21.2/debian/rules 2018-05-08 21:30:11.000000000 +0000 @@ -2,6 +2,7 @@ # -*- makefile -*- export PYBUILD_NAME=django-tables2 +export PYBUILD_AFTER_INSTALL=rm -rf {destdir}/usr/lib/python*/dist-packages/django_tables2/static/django_tables2 %: dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild @@ -11,16 +12,15 @@ PYTHONPATH=. sphinx-build -b html -d docs/.build/.doctrees -N docs docs/.build/html dh_auto_build +override_dh_installexamples: + dh_installexamples -A -X__pycache__ + .PHONY: override_dh_auto_clean override_dh_auto_clean: dh_auto_clean - ${RM} django_tables2/export/export.py django_tables2/export/__init__.py - ${RM} django_tables2/export/views.py docs/pages/export.rst - ${RM} tests/export/__init__.py tests/export/test_export.py ${RM} -r .cache django_tables2.egg-info $(RM) -r docs/.build .PHONY: override_dh_auto_test override_dh_auto_test: - PYTHONPATH=. \ - dh_auto_test -- --system=custom --test-args="{interpreter} /usr/bin/py.test" + dh_auto_test -- --system=custom --test-args="{interpreter} -Wd manage.py test" diff -Nru django-tables-1.14.2/debian/tests/control django-tables-1.21.2/debian/tests/control --- django-tables-1.14.2/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/debian/tests/control 2018-05-08 21:30:11.000000000 +0000 @@ -0,0 +1,11 @@ +Test-Command: python -Wd manage.py test +Depends: @, @builddeps@ +Restrictions: allow-stderr + +Test-Command: python3 -Wd manage.py test +Depends: @, @builddeps@ +Restrictions: allow-stderr + +Tests: test-run-py3 +Depends: @ +Restrictions: allow-stderr diff -Nru django-tables-1.14.2/debian/tests/test-run-py3 django-tables-1.21.2/debian/tests/test-run-py3 --- django-tables-1.14.2/debian/tests/test-run-py3 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/debian/tests/test-run-py3 2018-05-08 21:30:11.000000000 +0000 @@ -0,0 +1,99 @@ +#!/bin/sh + +set -e + +run() { + echo "Running “$@”" + $@ +} + +run django-admin startproject testproject +cd testproject +# python3 manage.py startapp tutorial +run python3 manage.py startapp tutorial + +# Create necessary configuration +sed "s/INSTALLED_APPS = \[/INSTALLED_APPS = [\n 'django_tables2',\n 'tutorial',/" -i testproject/settings.py +sed "s/ALLOWED_HOSTS = \[/ALLOWED_HOSTS = ['testserver'/" -i testproject/settings.py + +cat > tutorial/models.py << __EOF__ +from django.db import models + +class Person(models.Model): + name = models.CharField(max_length=100, verbose_name='full name') +__EOF__ + +run python3 manage.py makemigrations tutorial +run python3 manage.py migrate tutorial + +run python3 manage.py shell << __EOF__ +from tutorial.models import Person +Person.objects.bulk_create([Person(name='Jieter'), Person(name='Bradley')]) +__EOF__ + +cat > tutorial/views.py << __EOF__ +from django.shortcuts import render +from django_tables2 import RequestConfig +from .models import Person +from .tables import PersonTable + +def people(request): + table = PersonTable(Person.objects.all()) + RequestConfig(request).configure(table) + return render(request, 'tutorial/people.html', {'table': table}) +__EOF__ + + +cat > testproject/urls.py << __EOF__ +from django.conf.urls import url +from django.contrib import admin + +from tutorial.views import people + +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^people/', people) +] +__EOF__ + +mkdir -p tutorial/templates/tutorial +cat > tutorial/templates/tutorial/people.html << __EOF__ +{% load render_table from django_tables2 %} + + + + List of persons + + + + {% render_table table %} + + +__EOF__ + + + +cat > tutorial/tables.py << __EOF__ +import django_tables2 as tables +from .models import Person + +class PersonTable(tables.Table): + class Meta: + model = Person + template_name = 'django_tables2/bootstrap.html' +__EOF__ + +cat > verify.py << __EOF__ +from django.test import Client +client = Client() +response = client.get('/people/') +assert response.status_code == 200, "Response code is not 200" +body = response.content.decode('utf-8') +assert 'Jieter' in body, "The string 'Jieter' should be in the response" +assert 'Bradley' in body, "The string 'Bradley' should be in the response" +__EOF__ + +run python3 manage.py shell < verify.py + +cd .. +rm -rf testproject diff -Nru django-tables-1.14.2/django_tables2/columns/base.py django-tables-1.21.2/django_tables2/columns/base.py --- django-tables-1.14.2/django_tables2/columns/base.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/columns/base.py 2018-03-26 06:43:09.000000000 +0000 @@ -29,12 +29,15 @@ Returns: `.Column` object or `None` ''' + if field is None: + return self.columns[0]() + # iterate in reverse order as columns are registered in order # of least to most specialised (i.e. Column is registered # first). This also allows user-registered columns to be # favoured. for candidate in reversed(self.columns): - if not hasattr(candidate, "from_field"): + if not hasattr(candidate, 'from_field'): continue column = candidate.from_field(field) if column is None: @@ -83,7 +86,7 @@ the data iterator returned from as_values(). footer (str, callable): Defines the footer of this column. If a callable is passed, it can take optional keyword argumetns `column`, - `bound_colun` and `table`. + `bound_column` and `table`. order_by (str, tuple or `.Accessor`): Allows one or more accessors to be used for ordering rather than *accessor*. orderable (bool): If `False`, this column will not be allowed to @@ -123,7 +126,8 @@ self.verbose_name = verbose_name self.visible = visible self.orderable = orderable - self.attrs = attrs or {} + self.attrs = attrs or getattr(self, 'attrs', {}) + # massage order_by into an OrderByTuple or None order_by = (order_by, ) if isinstance(order_by, six.string_types) else order_by self.order_by = OrderByTuple(order_by) if order_by is not None else None @@ -264,8 +268,6 @@ means that a `.BoundColumn` knows the *"variable name"* given to the `.Column` when it was declared on the `.Table`. - For convenience, all `.Column` properties are available from this class. - arguments: table (`~.Table`): The table in which this column exists column (`~.Column`): The type of column @@ -281,6 +283,8 @@ self.column = column self.name = name + self.current_value = None + def __str__(self): return six.text_type(self.header) @@ -297,10 +301,25 @@ ''' Proxy to `.Column.attrs` but injects some values of our own. - A ``th`` and ``td`` are guaranteed to be defined (irrespective of - what's actually defined in the column attrs. This makes writing - templates easier. + A ``th``, ``td`` and ``tf`` are guaranteed to be defined (irrespective + of what's actually defined in the column attrs. This makes writing + templates easier. ``tf`` is not actually a HTML tag, but this key name + will be used for attributes for column's footer, if the column has one. ''' + + # prepare kwargs for computed_values() + kwargs = { + 'table': self._table, + 'bound_column': self, + } + # BoundRow.items() sets current_record and current_value when iterating over + # the records in a table. + if getattr(self, 'current_record', None) is not None and getattr(self, 'current_value', None) is not None: + kwargs.update({ + 'record': self.current_record, + 'value': self.current_value + }) + # Start with table's attrs; Only 'th' and 'td' attributes will be used attrs = dict(self._table.attrs) @@ -310,37 +329,43 @@ # we take the value for 'cell' as the basis for both the th and td attrs cell_attrs = attrs.get('cell', {}) # override with attrs defined specifically for th and td respectively. - kwargs = { - 'table': self._table, - 'column': self - } attrs['th'] = computed_values(attrs.get('th', cell_attrs), kwargs=kwargs) attrs['td'] = computed_values(attrs.get('td', cell_attrs), kwargs=kwargs) + attrs['tf'] = computed_values(attrs.get('tf', cell_attrs), kwargs=kwargs) # wrap in AttributeDict attrs['th'] = AttributeDict(attrs['th']) attrs['td'] = AttributeDict(attrs['td']) + attrs['tf'] = AttributeDict(attrs['tf']) # Override/add classes attrs['th']['class'] = self.get_th_class(attrs['th']) attrs['td']['class'] = self.get_td_class(attrs['td']) + attrs['tf']['class'] = self.get_td_class(attrs['tf']) return attrs + def _get_cell_class(self, attrs): + ''' + return a set of the classes from the class key in ``attrs``, augmented + with the column name (or anything else added by Table.get_column_class_names()). + ''' + classes = attrs.get('class', None) + classes = set() if classes is None else {c for c in classes.split(' ') if c} + + return self._table.get_column_class_names(classes, self) + def get_td_class(self, td_attrs): ''' Returns the HTML class attribute for a data cell in this column ''' - classes = set((c for c in td_attrs.get('class', '').split(' ') if c)) - classes = self._table.get_column_class_names(classes, self) - return ' '.join(sorted(classes)) + return ' '.join(sorted(self._get_cell_class(td_attrs))) def get_th_class(self, th_attrs): ''' Returns the HTML class attribute for a header cell in this column ''' - classes = set((c for c in th_attrs.get('class', '').split(' ') if c)) - classes = self._table.get_column_class_names(classes, self) + classes = self._get_cell_class(th_attrs) # add classes for ordering ordering_class = th_attrs.get('_ordering', {}) @@ -643,7 +668,7 @@ def __contains__(self, item): ''' - Check if a column is contained within a `Columns` object. + Check if a column is contained within a `BoundColumns` object. *item* can either be a `~.BoundColumn` object, or the name of a column. ''' diff -Nru django-tables-1.14.2/django_tables2/columns/linkcolumn.py django-tables-1.21.2/django_tables2/columns/linkcolumn.py --- django-tables-1.14.2/django_tables2/columns/linkcolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/columns/linkcolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,18 +1,13 @@ # coding: utf-8 from __future__ import absolute_import, unicode_literals +from django.urls import reverse from django.utils.html import format_html from django_tables2.utils import Accessor, AttributeDict from .base import Column, library -try: - from django.urls import reverse -except ImportError: - # to keep backward (Django <= 1.9) compatibility - from django.core.urlresolvers import reverse - class BaseLinkColumn(Column): ''' @@ -82,7 +77,7 @@ rendered ```` tag. Arguments: - viewname (str): See `~django.urls.reverse`, or use `None` + viewname (str or None): See `~django.urls.reverse`, or use `None` to use the model's `get_absolute_url` urlconf (str): See `~django.urls.reverse`. args (list): See `~django.urls.reverse`. [2]_ diff -Nru django-tables-1.14.2/django_tables2/columns/manytomanycolumn.py django-tables-1.21.2/django_tables2/columns/manytomanycolumn.py --- django-tables-1.14.2/django_tables2/columns/manytomanycolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/columns/manytomanycolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -41,7 +41,7 @@ # tables.py class PersonTable(tables.Table): name = tables.Column(order_by=('last_name', 'first_name')) - friends = tables.ManyToManyColumn(transform=lamda user: u.name) + friends = tables.ManyToManyColumn(transform=lambda user: u.name) ''' def __init__(self, transform=None, filter=None, separator=', ', *args, **kwargs): diff -Nru django-tables-1.14.2/django_tables2/columns/templatecolumn.py django-tables-1.21.2/django_tables2/columns/templatecolumn.py --- django-tables-1.14.2/django_tables2/columns/templatecolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/columns/templatecolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -18,6 +18,7 @@ Arguments: template_code (str): template code to render template_name (str): name of the template to render + extra_context (dict): optional extra template context A `~django.template.Template` object is created from the *template_code* or *template_name* and rendered with a context containing: @@ -26,6 +27,7 @@ - *value* -- value from `record` that corresponds to the current column - *default* -- appropriate default value to use as fallback - *row_counter* -- The number of the row this cell is being rendered in. + - any context variables passed using the `extra_context` argument to `TemplateColumn`. Example: @@ -33,17 +35,19 @@ class ExampleTable(tables.Table): foo = tables.TemplateColumn('{{ record.bar }}') - # contents of `myapp/bar_column.html` is `{{ value }}` - bar = tables.TemplateColumn(template_name='myapp/name2_column.html') + # contents of `myapp/bar_column.html` is `{{ label }}: {{ value }}` + bar = tables.TemplateColumn(template_name='myapp/name2_column.html', + extra_context={'label': 'Label'}) Both columns will have the same output. ''' empty_values = () - def __init__(self, template_code=None, template_name=None, **extra): + def __init__(self, template_code=None, template_name=None, extra_context=None, **extra): super(TemplateColumn, self).__init__(**extra) self.template_code = template_code self.template_name = template_name + self.extra_context = extra_context or {} if not self.template_code and not self.template_name: raise ValueError('A template must be provided') @@ -52,6 +56,7 @@ # If the table is being rendered using `render_table`, it hackily # attaches the context to the table as a gift to `TemplateColumn`. context = getattr(table, 'context', Context()) + context.update(self.extra_context) context.update({ 'default': bound_column.default, 'column': bound_column, diff -Nru django-tables-1.14.2/django_tables2/config.py django-tables-1.21.2/django_tables2/config.py --- django-tables-1.14.2/django_tables2/config.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/config.py 2018-03-26 06:43:09.000000000 +0000 @@ -20,10 +20,8 @@ A special *silent* item can be used to enable automatic handling of pagination exceptions using the following logic: - - If `~django.core.paginator.PageNotAnInteger` is raised, show the - first page. - - If `~django.core.paginator.EmptyPage` is raised, show the last - page. + - If `~django.core.paginator.PageNotAnInteger` is raised, show the first page. + - If `~django.core.paginator.EmptyPage` is raised, show the last page. ''' def __init__(self, request, paginate=True): @@ -63,3 +61,5 @@ table.page = table.paginator.page(1) except EmptyPage: table.page = table.paginator.page(table.paginator.num_pages) + + return table diff -Nru django-tables-1.14.2/django_tables2/data.py django-tables-1.21.2/django_tables2/data.py --- django-tables-1.14.2/django_tables2/data.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/data.py 2018-03-26 06:43:09.000000000 +0000 @@ -7,11 +7,8 @@ ''' Base class for table data containers. ''' - def __init__(self, data, table): + def __init__(self, data): self.data = data - self.table = table - - super(TableData, self).__init__() def __getitem__(self, key): ''' @@ -44,11 +41,14 @@ return 'items' @staticmethod - def from_data(data, table): + def from_data(data): + # allow explicit child classes of TableData to be passed to Table() + if isinstance(data, TableData): + return data if TableQuerysetData.validate(data): - return TableQuerysetData(data, table) + return TableQuerysetData(data) elif TableListData.validate(data): - return TableListData(list(data), table) + return TableListData(list(data)) raise ValueError( 'data must be QuerySet-like (have count() and order_by()) or support' @@ -133,11 +133,14 @@ ) def __len__(self): - if not hasattr(self, '_length'): - # Use the queryset count() method to get the length, instead of - # loading all results into memory. This allows, for example, - # smart paginators that use len() to perform better. - self._length = self.data.count() + '''Cached data length''' + if not hasattr(self, '_length') or self._length is None: + if hasattr(self.table, 'paginator'): + # for paginated tables, use QuerySet.count() as we are interested in total number of records. + self._length = self.data.count() + else: + # for non-paginated tables, use the length of the QuerySet + self._length = len(self.data) return self._length diff -Nru django-tables-1.14.2/django_tables2/export/export.py django-tables-1.21.2/django_tables2/export/export.py --- django-tables-1.14.2/django_tables2/export/export.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/export/export.py 2018-03-26 06:43:09.000000000 +0000 @@ -17,7 +17,9 @@ Argumenents: export_format (str): one of `csv, json, latex, ods, tsv, xls, xlsx, yml` + table (`~.Table`): instance of the table to export the data from + exclude_columns (iterable): list of column names to exclude from the export ''' CSV = 'csv' @@ -80,7 +82,8 @@ Builds and returns a `HttpResponse` containing the exported data Arguments: - filename (str): if not `None`, + filename (str): if not `None`, the filename is attached to the + `Content-Disposition` header of the response. ''' response = HttpResponse(content_type=self.content_type()) if filename is not None: diff -Nru django-tables-1.14.2/django_tables2/__init__.py django-tables-1.21.2/django_tables2/__init__.py --- django-tables-1.14.2/django_tables2/__init__.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/__init__.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,5 +1,5 @@ # coding: utf-8 -from .tables import Table, TableBase +from .tables import Table, TableBase, table_factory from .columns import (BooleanColumn, Column, CheckBoxColumn, DateColumn, DateTimeColumn, EmailColumn, FileColumn, JSONColumn, LinkColumn, ManyToManyColumn, RelatedLinkColumn, TemplateColumn, @@ -9,10 +9,10 @@ from .views import SingleTableMixin, SingleTableView, MultiTableMixin -__version__ = '1.14.2' +__version__ = '1.21.2' __all__ = ( - 'Table', 'TableBase', + 'Table', 'TableBase', 'table_factory', 'BooleanColumn', 'Column', 'CheckBoxColumn', 'DateColumn', 'DateTimeColumn', 'EmailColumn', 'FileColumn', 'JSONColumn', 'LinkColumn', 'ManyToManyColumn', 'RelatedLinkColumn', 'TemplateColumn', 'TimeColumn', 'URLColumn', Binary files /tmp/tmpsQnb2r/mHVP3TqTGW/django-tables-1.14.2/django_tables2/locale/cs/LC_MESSAGES/django.mo and /tmp/tmpsQnb2r/d2pAR_hTFC/django-tables-1.21.2/django_tables2/locale/cs/LC_MESSAGES/django.mo differ diff -Nru django-tables-1.14.2/django_tables2/locale/cs/LC_MESSAGES/django.po django-tables-1.21.2/django_tables2/locale/cs/LC_MESSAGES/django.po --- django-tables-1.14.2/django_tables2/locale/cs/LC_MESSAGES/django.po 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/django_tables2/locale/cs/LC_MESSAGES/django.po 2018-03-26 06:43:09.000000000 +0000 @@ -0,0 +1,44 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-22 08:20+0100\n" +"PO-Revision-Date: 2018-01-22 08:21+0100\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: templates/django_tables2/bootstrap.html:61 +#: templates/django_tables2/semantic.html:57 +msgid "previous" +msgstr "předchozí" + +#: templates/django_tables2/bootstrap.html:69 +#: templates/django_tables2/semantic.html:63 +#: templates/django_tables2/table.html:71 +#, python-format +msgid "Page %(current)s of %(total)s" +msgstr "Strana %(current)s z %(total)s" + +#: templates/django_tables2/bootstrap.html:77 +#: templates/django_tables2/semantic.html:69 +msgid "next" +msgstr "další" + +#: templates/django_tables2/table.html:63 +msgid "Previous" +msgstr "Předchozí" + +#: templates/django_tables2/table.html:79 +msgid "Next" +msgstr "Další" + Binary files /tmp/tmpsQnb2r/mHVP3TqTGW/django-tables-1.14.2/django_tables2/locale/zh_Hans/LC_MESSAGES/django.mo and /tmp/tmpsQnb2r/d2pAR_hTFC/django-tables-1.21.2/django_tables2/locale/zh_Hans/LC_MESSAGES/django.mo differ diff -Nru django-tables-1.14.2/django_tables2/locale/zh_Hans/LC_MESSAGES/django.po django-tables-1.21.2/django_tables2/locale/zh_Hans/LC_MESSAGES/django.po --- django-tables-1.14.2/django_tables2/locale/zh_Hans/LC_MESSAGES/django.po 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/django_tables2/locale/zh_Hans/LC_MESSAGES/django.po 2018-03-26 06:43:09.000000000 +0000 @@ -0,0 +1,43 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-03-19 15:20+0800\n" +"PO-Revision-Date: 2018-03-19 16:20+0800\n" +"Last-Translator: Zhong Chang<726608501@qq.com>\n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: templates/django_tables2/bootstrap.html:61 +#: templates/django_tables2/semantic.html:57 +msgid "previous" +msgstr "上一页" + +#: templates/django_tables2/bootstrap.html:69 +#: templates/django_tables2/semantic.html:63 +#: templates/django_tables2/table.html:71 +#, python-format +msgid "Page %(current)s of %(total)s" +msgstr "第 %(current)s 页,共 %(total)s 页" + +#: templates/django_tables2/bootstrap.html:77 +#: templates/django_tables2/semantic.html:69 +msgid "next" +msgstr "下一页" + +#: templates/django_tables2/table.html:63 +msgid "Previous" +msgstr "上一页" + +#: templates/django_tables2/table.html:79 +msgid "Next" +msgstr "下一页" diff -Nru django-tables-1.14.2/django_tables2/rows.py django-tables-1.21.2/django_tables2/rows.py --- django-tables-1.14.2/django_tables2/rows.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/rows.py 2018-03-26 06:43:09.000000000 +0000 @@ -9,6 +9,20 @@ from .utils import A, AttributeDict, call_with_appropriate, computed_values +class CellAccessor(object): + ''' + Allows accessing cell contents on a row object (see `BoundRow`) + ''' + def __init__(self, row): + self.row = row + + def __getitem__(self, key): + return self.row.get_cell(key) + + def __getattr__(self, name): + return self.row.get_cell(name) + + class BoundRow(object): ''' Represents a *specific* row in a table. @@ -33,25 +47,32 @@ 1 - Alternatively you can use row.get_cell() to retrieve a specific cell:: + Alternatively you can use row.cells[0] to retrieve a specific cell:: - >>> row.get_cell(0) + >>> row.cells[0] 1 - >>> row.get_cell(1) - u'' - >>> row.get_cell(2) + >>> row.cells[1] + '' + >>> row.cells[2] ... IndexError: list index out of range Finally you can also use the column names to retrieve a specific cell:: - >>> row.get_cell('a') + >>> row.cells.a 1 - >>> row.get_cell('b') - u'' - >>> row.get_cell('c') + >>> row.cells.b + '' + >>> row.cells.c ... - KeyError: 'c' + KeyError: "Column with name 'c' does not exist; choices are: ['a', 'b']" + + If you have the column name in a variable, you can also treat the `cells` + property like a `dict`:: + + >>> key = 'a' + >>> row.cells[key] + 1 Arguments: table: The `.Table` in which this row exists. @@ -66,6 +87,8 @@ self.row_counter = next(table._counter) + self.cells = CellAccessor(self) + @property def table(self): ''' @@ -228,7 +251,12 @@ ``rendered within ````. ''' for column in self.table.columns: - yield (column, self.get_cell(column.name)) + # column gets some attributes relevant only relevant in this iteration, + # used to allow passing the value/record to a callable Column.attrs / + # Table.attrs item. + column.current_value = self.get_cell(column.name) + column.current_record = self.record + yield (column, column.current_value) class BoundPinnedRow(BoundRow): @@ -324,9 +352,9 @@ ''' if isinstance(key, slice): return BoundRows( - self.data[key], + data=self.data[key], table=self.table, pinned_data=self.pinned_data ) else: - return BoundRow(self.data[key], table=self.table) + return BoundRow(record=self.data[key], table=self.table) diff -Nru django-tables-1.14.2/django_tables2/tables.py django-tables-1.21.2/django_tables2/tables.py --- django-tables-1.14.2/django_tables2/tables.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/tables.py 2018-03-26 06:43:09.000000000 +0000 @@ -2,12 +2,12 @@ from __future__ import unicode_literals import copy +import warnings from collections import OrderedDict from itertools import count from django.conf import settings from django.core.paginator import Paginator -from django.db.models.fields import FieldDoesNotExist from django.template.loader import get_template from django.utils import six from django.utils.encoding import force_text @@ -16,7 +16,7 @@ from .config import RequestConfig from .data import TableData from .rows import BoundRows -from .utils import AttributeDict, OrderBy, OrderByTuple, Sequence +from .utils import Accessor, AttributeDict, OrderBy, OrderByTuple, Sequence class DeclarativeColumnsMetaclass(type): @@ -59,13 +59,8 @@ # Each item in opts.fields is the name of a model field or a # normal attribute on the model for field_name in opts.fields: - try: - field = opts.model._meta.get_field(field_name) - except FieldDoesNotExist: - extra[field_name] = columns.Column() - else: - extra[field_name] = columns.library.column_for_field(field) - + field = Accessor(field_name).get_field(opts.model) + extra[field_name] = columns.library.column_for_field(field) else: for field in opts.model._meta.fields: extra[field.name] = columns.library.column_for_field(field) @@ -141,7 +136,11 @@ self.sequence = Sequence(getattr(options, 'sequence', ())) self.orderable = getattr(options, 'orderable', True) self.model = getattr(options, 'model', None) - self.template = getattr(options, 'template', DJANGO_TABLES2_TEMPLATE) + if hasattr(options, 'template'): + self.template_name = getattr(options, 'template', DJANGO_TABLES2_TEMPLATE) + warnings.warn('Table.Meta.template is deprecated. Use template_name instead.', DeprecationWarning) + else: + self.template_name = getattr(options, 'template_name', DJANGO_TABLES2_TEMPLATE) self.localize = getattr(options, 'localize', ()) self.unlocalize = getattr(options, 'unlocalize', ()) @@ -195,8 +194,9 @@ per_page_field (str): If not `None`, defines the name of the *per page* querystring field. - template (str): The template to render when using ``{% render_table %}`` - (default ``'django_tables2/table.html'``) + template_name (str): The template to render when using ``{% render_table %}`` + (defaults to DJANGO_TABLES2_TEMPLATE, which is ``'django_tables2/table.html'`` + by default). default (str): Text to render in empty cells (determined by `.Column.empty_values`, default `.Table.Meta.default`) @@ -215,7 +215,7 @@ def __init__(self, data=None, order_by=None, orderable=None, empty_text=None, exclude=None, attrs=None, row_attrs=None, pinned_row_attrs=None, sequence=None, prefix=None, order_by_field=None, page_field=None, - per_page_field=None, template=None, default=None, request=None, + per_page_field=None, template=None, template_name=None, default=None, request=None, show_header=None, show_footer=True, extra_columns=None): super(TableBase, self).__init__() @@ -226,7 +226,8 @@ self.exclude = exclude or self._meta.exclude self.sequence = sequence - self.data = TableData.from_data(data=data, table=self) + self.data = TableData.from_data(data=data) + self.data.table = self if default is None: default = self._meta.default self.default = default @@ -280,7 +281,6 @@ base_columns = OrderedDict(( (x, base_columns[x]) for x in sequence if x in base_columns )) - self.columns = columns.BoundColumns(self, base_columns) # `None` value for order_by means no order is specified. This means we # `shouldn't touch our data's ordering in any way. *However* @@ -297,7 +297,11 @@ self.order_by = order_by else: self.order_by = order_by - self.template = template + if template is not None: + self.template_name = template + warnings.warn('template argument to Table is deprecated. Use template_name instead.', DeprecationWarning) + else: + self.template_name = template_name # If a request is passed, configure for request if request: RequestConfig(request).configure(self) @@ -383,7 +387,7 @@ ''' # reset counter for new rendering self._counter = count() - template = get_template(self.template) + template = get_template(self.template_name) context = { 'table': self, @@ -574,16 +578,25 @@ self._orderable = value @property - def template(self): + def template_name(self): if self._template is not None: return self._template else: - return self._meta.template + return self._meta.template_name - @template.setter - def template(self, value): + @template_name.setter + def template_name(self, value): self._template = value + @property + def paginated_rows(self): + ''' + Return the rows for the current page if the table is paginated, else all rows. + ''' + if hasattr(self, 'page'): + return self.page.object_list + return self.rows + def get_column_class_names(self, classes_set, bound_column): ''' Returns a set of HTML class names for cells (both td and th) of a @@ -617,3 +630,41 @@ __doc__ = TableBase.__doc__ # Table = DeclarativeColumnsMetaclass(str('Table'), (TableBase, ), {}) + + +def table_factory(model, table=Table, fields=None, exclude=None, + localize=None): + """ + Returns Table class for given `model`, equivalent to defining a custom table class:: + + class MyTable(tables.Table): + class Meta: + model = model + + Arguments: + model (`~django.db.models.Model`): Model associated with the new table + table (`.Table`): Base Table class used to create the new one + fields (list of str): Fields displayed in tables + exclude (list of str): Fields exclude in tables + localize (list of str): Fields to localize + """ + attrs = {'model': model} + if fields is not None: + attrs['fields'] = fields + if exclude is not None: + attrs['exclude'] = exclude + if localize is not None: + attrs['localize'] = localize + # If parent form class already has an inner Meta, the Meta we're + # creating needs to inherit from the parent's inner meta. + parent = (object,) + if hasattr(table, 'Meta'): + parent = (table.Meta, object) + Meta = type(str('Meta'), parent, attrs) + # Give this new table class a reasonable name. + class_name = model.__name__ + str('Table') + # Class attributes for the new table class. + table_class_attrs = { + 'Meta': Meta, + } + return type(table)(class_name, (table,), table_class_attrs) diff -Nru django-tables-1.14.2/django_tables2/templates/django_tables2/bootstrap.html django-tables-1.21.2/django_tables2/templates/django_tables2/bootstrap.html --- django-tables-1.14.2/django_tables2/templates/django_tables2/bootstrap.html 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/templates/django_tables2/bootstrap.html 2018-03-26 06:43:09.000000000 +0000 @@ -21,7 +21,7 @@ {% endblock table.thead %} {% block table.tbody %} - {% for row in table.page.object_list|default:table.rows %} {# support pagination #} + {% for row in table.paginated_rows %} {% block table.tbody.row %} {% for column, cell in row.items %} @@ -43,7 +43,7 @@ {% for column in table.columns %} - {{ column.footer }} + {{ column.footer }} {% endfor %} diff -Nru django-tables-1.14.2/django_tables2/templates/django_tables2/semantic.html django-tables-1.21.2/django_tables2/templates/django_tables2/semantic.html --- django-tables-1.14.2/django_tables2/templates/django_tables2/semantic.html 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/templates/django_tables2/semantic.html 2018-03-26 06:43:09.000000000 +0000 @@ -21,7 +21,7 @@ {% endblock table.thead %} {% block table.tbody %} - {% for row in table.page.object_list|default:table.rows %} {# support pagination #} + {% for row in table.paginated_rows %} {% block table.tbody.row %} {% for column, cell in row.items %} @@ -43,7 +43,7 @@ {% if table.has_footer %} {% for column in table.columns %} - {{ column.footer }} + {{ column.footer }} {% endfor %} {% endif %} @@ -60,7 +60,7 @@ {% if table.page.has_previous or table.page.has_next %} {% block pagination.current %} -
{% blocktrans with table.page.number as current and table.paginator.num_pages as total %}Page {{ current }} of {{ total }}{% endblocktrans %}
+
{% blocktrans with table.page.number as current and table.paginator.num_pages as total %}Page {{ current }} of {{ total }}{% endblocktrans %}
{% endblock pagination.current %} {% endif %} diff -Nru django-tables-1.14.2/django_tables2/templates/django_tables2/table.html django-tables-1.21.2/django_tables2/templates/django_tables2/table.html --- django-tables-1.14.2/django_tables2/templates/django_tables2/table.html 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/templates/django_tables2/table.html 2018-03-26 06:43:09.000000000 +0000 @@ -21,7 +21,7 @@ {% endblock table.thead %} {% block table.tbody %} - {% for row in table.page.object_list|default:table.rows %} {# support pagination #} + {% for row in table.paginated_rows %} {% block table.tbody.row %} {% for column, cell in row.items %} @@ -43,7 +43,7 @@ {% for column in table.columns %} - {{ column.footer }} + {{ column.footer }} {% endfor %} diff -Nru django-tables-1.14.2/django_tables2/templatetags/django_tables2.py django-tables-1.21.2/django_tables2/templatetags/django_tables2.py --- django-tables-1.14.2/django_tables2/templatetags/django_tables2.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/templatetags/django_tables2.py 2018-03-26 06:43:09.000000000 +0000 @@ -16,7 +16,6 @@ from django.utils.http import urlencode import django_tables2 as tables -from django_tables2.config import RequestConfig register = template.Library() kwarg_re = re.compile(r"(?:(.+)=)?(.+)") @@ -50,10 +49,11 @@ class QuerystringNode(Node): - def __init__(self, updates, removals): + def __init__(self, updates, removals, asvar=None): super(QuerystringNode, self).__init__() self.updates = updates self.removals = removals + self.asvar = asvar def render(self, context): if 'request' not in context: @@ -61,16 +61,26 @@ params = dict(context['request'].GET) for key, value in self.updates.items(): + if isinstance(key, six.string_types): + params[key] = value + continue key = key.resolve(context) value = value.resolve(context) if key not in ('', None): params[key] = value for removal in self.removals: params.pop(removal.resolve(context), None) - return escape('?' + urlencode(params, doseq=True)) + + value = escape('?' + urlencode(params, doseq=True)) + + if self.asvar: + context[str(self.asvar)] = value + return '' + else: + return value -# {% querystring "name"="abc" "age"=15 %} +# {% querystring "name"="abc" "age"=15 as=qs %} @register.tag def querystring(parser, token): ''' @@ -80,21 +90,34 @@ Example (imagine URL is ``/abc/?gender=male&name=Brad``):: + # {% querystring "name"="abc" "age"=15 %} + ?name=abc&gender=male&age=15 {% querystring "name"="Ayers" "age"=20 %} ?name=Ayers&gender=male&age=20 {% querystring "name"="Ayers" without "gender" %} ?name=Ayers - ''' bits = token.split_contents() tag = bits.pop(0) updates = token_kwargs(bits, parser) + + asvar_key = None + for key in updates: + if str(key) == 'as': + asvar_key = key + + if asvar_key is not None: + asvar = updates[asvar_key] + del updates[asvar_key] + else: + asvar = None + # ``bits`` should now be empty of a=b pairs, it should either be empty, or # have ``without`` arguments. if bits and bits.pop(0) != 'without': raise TemplateSyntaxError("Malformed arguments to '%s'" % tag) removals = [parser.compile_filter(bit) for bit in bits] - return QuerystringNode(updates, removals) + return QuerystringNode(updates, removals, asvar=asvar) class RenderTableNode(Node): @@ -103,10 +126,10 @@ table (~.Table): the table to render template (str or list): Name[s] of template to render ''' - def __init__(self, table, template=None): + def __init__(self, table, template_name=None): super(RenderTableNode, self).__init__() self.table = table - self.template = template + self.template_name = template_name def render(self, context): table = self.table.resolve(context) @@ -118,34 +141,22 @@ elif hasattr(table, 'model'): queryset = table - # We've been given a queryset, create a table using its model and - # render that. - class OnTheFlyTable(tables.Table): - class Meta: - model = queryset.model - - table = OnTheFlyTable(queryset) - if request: - RequestConfig(request).configure(table) + table = tables.table_factory(model=queryset.model)(queryset, request=request) else: klass = type(table).__name__ raise ValueError('Expected table or queryset, not {}'.format(klass)) - if self.template: - template = self.template.resolve(context) + if self.template_name: + template_name = self.template_name.resolve(context) else: - template = table.template + template_name = table.template_name - if isinstance(template, six.string_types): - template = get_template(template) + if isinstance(template_name, six.string_types): + template = get_template(template_name) else: # assume some iterable was given - template = select_template(template) + template = select_template(template_name) - # Contexts are basically a `MergeDict`, when you `update()`, it - # internally just adds a dict to the list to attempt lookups from. This - # is why we're able to `pop()` later. - context.update({'table': table}) try: # HACK: # TemplateColumn benefits from being able to use the context @@ -154,10 +165,10 @@ # which TemplateColumn then looks for and uses. table.context = context table.before_render(request) - return template.render(context.flatten()) + + return template.render(context={'table': table}, request=request) finally: del table.context - context.pop() @register.tag @@ -228,3 +239,20 @@ register.filter('localize', l10n_register.filters['localize']) register.filter('unlocalize', l10n_register.filters['unlocalize']) + + +@register.simple_tag(takes_context=True) +def export_url(context, export_format, export_trigger_param='_export'): + ''' + Returns an export url for the given file `export_format`, preserving current + query string parameters. + + Example for a page requested with querystring ``?q=blue``:: + + {% export_url "csv" %} + + It will return:: + + ?q=blue&_export=csv + ''' + return QuerystringNode(updates={export_trigger_param: export_format}, removals=[]).render(context) diff -Nru django-tables-1.14.2/django_tables2/utils.py django-tables-1.21.2/django_tables2/utils.py --- django-tables-1.14.2/django_tables2/utils.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/utils.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,9 +1,11 @@ # coding: utf-8 from __future__ import absolute_import, unicode_literals +from collections import OrderedDict from functools import total_ordering from itertools import chain +from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils import six from django.utils.html import format_html_join @@ -278,6 +280,9 @@ ''' SEPARATOR = '.' + ALTERS_DATA_ERROR_FMT = 'Refusing to call {method}() because `.alters_data = True`' + LOOKUP_ERROR_FMT = 'Failed lookup for key [{key}] in {context}, when resolving the accessor {accessor}' + def resolve(self, context, safe=True, quiet=False): ''' Return an object described by the accessor by traversing the attributes @@ -313,6 +318,7 @@ TypeError`, `AttributeError`, `KeyError`, `ValueError` (unless `quiet` == `True`) ''' + try: current = context for bit in self.bits: @@ -329,13 +335,16 @@ KeyError, # dict without `int(bit)` key TypeError, # unsubscriptable object ): - raise ValueError('Failed lookup for key [%s] in %r' - ', when resolving the accessor %s' % (bit, current, self) - ) + current_context = type(current) if isinstance(current, models.Model) else current + + raise ValueError( + self.LOOKUP_ERROR_FMT.format(key=bit, context=current_context, accessor=self) + ) if callable(current): if safe and getattr(current, 'alters_data', False): - raise ValueError('refusing to call %s() because `.alters_data = True`' - % repr(current)) + raise ValueError( + self.ALTERS_DATA_ERROR_FMT.format(method=repr(current)) + ) if not getattr(current, 'do_not_call_in_templates', False): current = current() # important that we break in None case, or a relationship @@ -372,12 +381,6 @@ rel = getattr(field, 'remote_field', None) model = getattr(rel, 'model', model) - # !!! Support only for Django <= 1.8 - # Remove this when support for Django 1.8 is over - else: - rel = getattr(field, 'rel', None) - model = getattr(rel, 'to', model) - return field def penultimate(self, context, quiet=True): @@ -399,10 +402,12 @@ A = Accessor # alias -class AttributeDict(dict): +class AttributeDict(OrderedDict): ''' - A wrapper around `dict` that knows how to render itself as HTML - style tag attributes. + A wrapper around `collections.OrderedDict` that knows how to render itself + as HTML style tag attributes. + + Any key with ``value is None`` will be skipped. The returned string is marked safe, so it can be used safely in a template. See `.as_html` for a usage example. @@ -411,8 +416,9 @@ def _iteritems(self): for k, v in six.iteritems(self): - if k not in self.blacklist: - yield (k, v() if callable(v) else v) + value = v() if callable(v) else v + if k not in self.blacklist and value is not None: + yield (k, value) def as_html(self): ''' @@ -427,7 +433,7 @@ >>> attrs.as_html() 'class="mytable" id="someid"' - :rtype: `~django.utils.safestring.SafeUnicode` object + returns: `~django.utils.safestring.SafeUnicode` object ''' return format_html_join(' ', '{}="{}"', self._iteritems()) @@ -513,11 +519,19 @@ If the kwargs argument is defined, pass all arguments, else provide exactly the arguments wanted. + + If one of the arguments of ``fn`` are not contained in kwargs, ``fn`` will not + be called and ``None`` will be returned. ''' - args, keyword = signature(fn) - if not keyword: + args, kwargs_name = signature(fn) + # no catch-all defined, we need to exactly pass the arguments specified. + if not kwargs_name: kwargs = {key: kwargs[key] for key in kwargs if key in args} + # if any argument of fn is not in kwargs, just return None + if any(arg not in kwargs for arg in args): + return None + return fn(**kwargs) diff -Nru django-tables-1.14.2/django_tables2/views.py django-tables-1.21.2/django_tables2/views.py --- django-tables-1.14.2/django_tables2/views.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/django_tables2/views.py 2018-03-26 06:43:09.000000000 +0000 @@ -6,28 +6,17 @@ from django.core.exceptions import ImproperlyConfigured from django.views.generic.list import ListView +from . import tables from .config import RequestConfig class TableMixinBase(object): ''' - Base mixin for table-related class based views. + Base mixin for the Single- and MultiTable class based views ''' - context_table_name = 'table' table_pagination = None - def get_table_class(self): - ''' - Return the class to use for the table. - ''' - if self.table_class: - return self.table_class - klass = type(self).__name__ - raise ImproperlyConfigured( - 'A table class was not specified. Define {}.table_class'.format(klass) - ) - def get_context_table_name(self, table): ''' Get the name to use for the table's template variable. @@ -65,14 +54,33 @@ 'table') table_pagination (dict): controls table pagination. If a `dict`, passed as the *paginate* keyword argument to `.RequestConfig`. As such, any - Truthy value enables pagination. (default: enable pagination) + Truthy value enables pagination. (default: enable pagination). + + The `dict` can be used to specify values for arguments for the call to + `~.tables.Table.paginate`. + + If you want to use a non-standard paginator for example, you can add a key + `klass` to the dict, containing a custom `Pagintor` class. - This mixin plays nice with the Django's`.MultipleObjectMixin` by using - `.get_queryset`` as a fallback for the table data source. + This mixin plays nice with the Django's ``.MultipleObjectMixin`` by using + ``.get_queryset`` as a fallback for the table data source. ''' table_class = None table_data = None + def get_table_class(self): + ''' + Return the class to use for the table. + ''' + if self.table_class: + return self.table_class + if self.model: + return tables.table_factory(self.model) + + raise ImproperlyConfigured( + 'You must either specify {0}.table_class or {0}.model'.format(type(self).__name__) + ) + def get_table(self, **kwargs): ''' Return a table object to use. The table has automatic support for @@ -80,8 +88,7 @@ ''' table_class = self.get_table_class() table = table_class(data=self.get_table_data(), **kwargs) - RequestConfig(self.request, paginate=self.get_table_pagination(table)).configure(table) - return table + return RequestConfig(self.request, paginate=self.get_table_pagination(table)).configure(table) def get_table_data(self): ''' @@ -103,8 +110,8 @@ ''' Return the keyword arguments for instantiating the table. - Allows passing customized arguments to the table constructor, for example, to remove the buttons column, - you could define this method in your View:: + Allows passing customized arguments to the table constructor, for example, + to remove the buttons column, you could define this method in your View:: def get_table_kwargs(self): return { @@ -127,18 +134,21 @@ class SingleTableView(SingleTableMixin, ListView): ''' Generic view that renders a template and passes in a `.Table` instances. + + Mixes ``.SingleTableMixin`` with ``django.views.generic.list.ListView``. ''' class MultiTableMixin(TableMixinBase): ''' - Adds a Table object to the context. Typically used with + Add a list with multiple Table object's to the context. Typically used with `.TemplateResponseMixin`. - the `tables` attribute must be either a list of `.Table` instances or + The `tables` attribute must be either a list of `.Table` instances or classes extended from `.Table` which are not already instantiated. In that - case, tables_data must be defined, having an entry containing the data for - each table in `tables`. + case, `get_tables_data` must be able to return the tables data, either by + having an entry containing the data for each table in `tables`, or by + overriding this method in order to return this data. Attributes: tables: list of `.Table` instances or list of `.Table` child objects. @@ -162,27 +172,36 @@ context_table_name = 'tables' def get_tables(self): - if not self.tables: - klass = type(self).__name__ - raise ImproperlyConfigured( - 'No tables were specified. Define {}.tables'.format(klass) - ) - - return self.tables + ''' + Return an array of table instances containing data. + ''' + data = self.get_tables_data() - def get_context_data(self, **kwargs): - context = super(MultiTableMixin, self).get_context_data(**kwargs) + if data is None: + if not self.tables: + klass = type(self).__name__ + raise ImproperlyConfigured( + 'No tables were specified. Define {}.tables'.format(klass) + ) - if self.tables_data is None: - tables = self.get_tables() + return self.tables else: - data = self.tables_data - if len(data) != len(self.get_tables()): + if len(data) != len(self.tables): klass = type(self).__name__ raise ImproperlyConfigured( 'len({}.tables_data) != len({}.tables)'.format(klass, klass) ) - tables = list(Table(data[i]) for i, Table in enumerate(self.tables)) + return list(Table(data[i]) for i, Table in enumerate(self.tables)) + + def get_tables_data(self): + ''' + Return an array of table_data that should be used to populate each table + ''' + return self.tables_data + + def get_context_data(self, **kwargs): + context = super(MultiTableMixin, self).get_context_data(**kwargs) + tables = self.get_tables() # apply prefixes and execute requestConfig for each table table_counter = count() diff -Nru django-tables-1.14.2/docs/conf.py django-tables-1.21.2/docs/conf.py --- django-tables-1.14.2/docs/conf.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/conf.py 2018-03-26 06:43:09.000000000 +0000 @@ -14,7 +14,7 @@ project = 'django-tables2' with open('../django_tables2/__init__.py', 'rb') as f: - release = re.search('__version__ = \'(.+?)\'', f.read()).group(1) + release = str(re.search('__version__ = \'(.+?)\'', f.read().decode('utf-8')).group(1)) version = release.rpartition('.')[0] diff -Nru django-tables-1.14.2/docs/index.rst django-tables-1.21.2/docs/index.rst --- django-tables-1.14.2/docs/index.rst 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/index.rst 2018-03-26 06:43:09.000000000 +0000 @@ -19,9 +19,9 @@ About the app: - `Available on pypi `_ -- Tested with python 2.7, 3.3, 3.4, 3.5 and Django 1.8, 1.9, `Travis CI `_ +- Tested with python 2.7, 3.4, 3.5, 3.6 and Django 1.11, `Travis CI `_ - `Documentation on readthedocs.org `_ -- `Bug tracker `_ +- `Bug tracker `_ Table of contents diff -Nru django-tables-1.14.2/docs/pages/api-reference.rst django-tables-1.21.2/docs/pages/api-reference.rst --- django-tables-1.14.2/docs/pages/api-reference.rst 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/pages/api-reference.rst 2018-03-26 06:43:09.000000000 +0000 @@ -1,3 +1,5 @@ +.. _api-public: + API Reference ============= @@ -241,11 +243,11 @@ This functionality is also available via the ``orderable`` keyword argument to a table's constructor. - template (str): The default template to use when rendering the table. + template_name (str): The name of template to use when rendering the table. .. note:: - This functionality is also available via the *template* keyword + This functionality is also available via the *template_name* keyword argument to a table's constructor. diff -Nru django-tables-1.14.2/docs/pages/column-attributes.rst django-tables-1.21.2/docs/pages/column-attributes.rst --- django-tables-1.14.2/docs/pages/column-attributes.rst 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/pages/column-attributes.rst 2018-03-26 06:43:09.000000000 +0000 @@ -21,9 +21,63 @@ For ``th`` and ``td``, the column name will be added as a class name. This makes -selecting the row for styling easier. -Have a look at each column's API reference to find which elements are supported. +selecting the row for styling easier. Have a look at each column's API +reference to find which elements are supported. +If you need to add some extra attributes to column's tags rendered in the +footer, use key name ``tf``, as described in section on :ref:`css`. + +Callables passed in this dict will be called, with optional kwargs ``table``, +``bound_column`` ``record`` and ``value``, with the return value added. For example:: + + class Table(tables.Table): + person = tables.Column(attrs={ + 'td': { + 'data-length': lambda value: len(value) + } + }) + +will render the ````'s in the tables ```` with a ``data-length`` attribute +containing the number of characters in the value. + +.. note:: + The kwargs ``record`` and ``value`` only make sense in the context of a row + containing data. If you supply a callable with one of these keyword arguments, + it will not be executed for the header and footer rows. + + If you also want to customize the attributes of those tags, you must define a + callable with a catchall (``**kwargs``) argument:: + + def data_first_name(**kwargs): + first_name = kwargs.get('first_name', None) + if first_name is None: + return 'header' + else: + return first_name + + class Table(tables.Table): + first_name = tables.Column(attrs={ + 'td': { + 'data-first-name': data_first_name + } + }) + +This `attrs` can also be defined when subclassing a column, to allow better reuse:: + + class PersonColumn(tables.Column): + attrs = { + 'td': { + 'data-first-name': lambda record: record.first_name + 'data-last-name': lambda record: record.last_name + } + } + def render(self, record): + return '{} {}'.format(record.first_name, record.last_name) + + class Table(tables.Table): + person = PersonColumn() + +is equivalent to the previous example. .. _row-attributes: @@ -35,7 +89,9 @@ By default, class names *odd* and *even* are supplied to the rows, wich can be customized using the ``row_attrs`` `.Table.Meta` attribute or as argument to the -constructor of `.Table`, for example:: +constructor of `.Table`. String-like values will just be added, +callables will be called with optional keyword argument `record`, the return value +will be added. For example:: class Table(tables.Table): class Meta: diff -Nru django-tables-1.14.2/docs/pages/custom-rendering.rst django-tables-1.21.2/docs/pages/custom-rendering.rst --- django-tables-1.14.2/docs/pages/custom-rendering.rst 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/pages/custom-rendering.rst 2018-03-26 06:43:09.000000000 +0000 @@ -25,6 +25,85 @@ >>> # renders to something like this: '...' +Also every column gets a class attribute, which by default is the same as the +column's label. Also, by default, odd rows' class is ``odd`` and even rows' +class is ``even``. So rows of the ``SimpleTable()`` from previous example +in django-tables2 default configuration will look like: + +.. sourcecode:: html + + + + + + + + + + +You can also specify ``attrs`` attribute when creating a column. ``attrs`` +is a dictionary which contains attributes which by default get rendered +on various tags involved with rendering a column. You can read more about +them in :ref:`column-attributes`. django-tables2 supports 3 different +dictionaries, this way you can give different attributes +to column tags in table header (``th``), rows (``td``) or footer (``tf``) + +.. sourcecode:: python + + >>> import django_tables2 as tables + >>> + >>> class SimpleTable(tables.Table): + ... id = tables.Column(attrs={'td': {'class': 'my-class'}}) + ... age = tables.Column(attrs={'tf': {'bgcolor': 'red'}}) + ... + >>> table = SimpleTable() + >>> # renders to something like this: + '' + >>> # and the footer will look like this: + ' ... '' + + +.. _available-templates: + +Available templates +------------------- + +We ship a couple of different templates: + +======================================== ====================================================== +Template name Description +======================================== ====================================================== +django_tables2/table.html Basic table template (default). +django_tables2/bootstrap.html Template using bootstrap 3 structure/classes +django_tables2/bootstrap-responsive.html Same as boostrap, but wrapped in ``.table-responsive`` +django_tables2/semantic.html Template using semantic UI +======================================== ====================================================== + +By default, django-tables2 looks for the ``DJANGO_TABLES2_TEMPLATE`` setting +which is ``django_tables2/table.html`` by default. + +If you use bootstrap 3 for your site, it makes sense to set the default to +the bootstrap 3 template:: + + DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap.html' + +If you want to specify a custom template for selected tables in your project, +you can set a ``template_name`` attribute to your custom ``Table.Meta`` class:: + + class PersonTable(tables.Table): + + class Meta: + model = Person + template_name = 'django_tables2/semantic.html' + +You can also use the ``template_name`` argument to the ``Table`` constructor to +override the template for a certain instance:: + + table = PersonTable(data, template_name='django_tables2/bootstrap-responsive.html') + +For none of the templates any CSS file is added to the HTML. You are responsible for +including the relevant style sheets for a template. + .. _custom-template: Custom Template @@ -34,7 +113,4 @@ ignore the built-in generation tools, and instead pass an instance of your `.Table` subclass into your own template, and render it yourself. -Have a look at the ``django_tables2/table.html`` template for an example. - -You can set `DJANGO_TABLES2_TEMPLATE` in your django settings to change the -default template django-tables2 looks for. +You should use one of the provided templates as a basis. diff -Nru django-tables-1.14.2/docs/pages/export.rst django-tables-1.21.2/docs/pages/export.rst --- django-tables-1.14.2/docs/pages/export.rst 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/pages/export.rst 2018-03-26 06:43:09.000000000 +0000 @@ -100,7 +100,7 @@ table_class = MyTable model = Person template_name = 'django_tables2/bootstrap.html' - exclude_column = ('buttons', ) + exclude_columns = ('buttons', ) Generating export urls @@ -109,7 +109,7 @@ You can use the ``querystring`` template tag included with django_tables2 to render a link to export the data as ``csv``:: - {% querystring '_export'='csv' %} + {% export_url "csv" %} This will make sure any other query string parameters will be preserved, for example in combination when filtering table items. @@ -117,7 +117,7 @@ If you want to render more than one button, you could use something like this:: {% for format in table.export_formats %} - + download .{{ format }} {% endfor %} diff -Nru django-tables-1.14.2/docs/pages/filtering.rst django-tables-1.21.2/docs/pages/filtering.rst --- django-tables-1.14.2/docs/pages/filtering.rst 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/pages/filtering.rst 2018-03-26 06:43:09.000000000 +0000 @@ -1,3 +1,5 @@ +.. _filtering: + Filtering data in your table ============================ diff -Nru django-tables-1.14.2/docs/pages/generic-mixins.rst django-tables-1.21.2/docs/pages/generic-mixins.rst --- django-tables-1.14.2/docs/pages/generic-mixins.rst 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/pages/generic-mixins.rst 2018-03-26 06:43:09.000000000 +0000 @@ -13,7 +13,8 @@ The following view parameters are supported: -- ``table_class`` –- the table class to use, e.g. ``SimpleTable`` +- ``table_class`` –- the table class to use, e.g. ``SimpleTable``, if not specfied + and ``model`` is provided, a default table will be created on-the-fly. - ``table_data`` (or ``get_table_data()``) -- the data used to populate the table - ``context_table_name`` -- the name of template variable containing the table object - ``table_pagination`` (or ``get_table_pagination``) -- pagination @@ -46,7 +47,7 @@ .. sourcecode:: django - {% load render_table from django_tables2 %} + {% load django_tables2 %} {% render_table table %} Such little code is possible due to the example above taking advantage of @@ -60,7 +61,7 @@ Multiple tables using `.MultiTableMixin` --------------------------------------------- +---------------------------------------- If you need more than one table in a single view you can use `MultiTableMixin`. It manages multiple tables for you and takes care of adding the appropriate @@ -84,6 +85,7 @@ .. sourcecode:: django + {% load django_tables2 %} {% for table in tables %} {% render_table table %} {% endfor %} diff -Nru django-tables-1.14.2/docs/pages/table-data.rst django-tables-1.21.2/docs/pages/table-data.rst --- django-tables-1.14.2/docs/pages/table-data.rst 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/pages/table-data.rst 2018-03-26 06:43:09.000000000 +0000 @@ -1,4 +1,4 @@ -.. _table-data: +.. _table_data: Populating a table with data ============================ diff -Nru django-tables-1.14.2/docs/pages/tutorial.rst django-tables-1.21.2/docs/pages/tutorial.rst --- django-tables-1.14.2/docs/pages/tutorial.rst 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/pages/tutorial.rst 2018-03-26 06:43:09.000000000 +0000 @@ -1,50 +1,74 @@ Tutorial ~~~~~~~~ -This is a step-by-step guide to learn how to install and use django-tables2. +This is a step-by-step guide to learn how to install and use django-tables2 using Django 1.11. 1. ``pip install django-tables2`` -2. Add ``'django_tables2'`` to ``INSTALLED_APPS`` -3. Add ``'django.template.context_processors.request'`` to the ``context_processors`` in your template setting ``OPTIONS``. +2. Start a new Django app using `python manage.py startapp tutorial` +3. Add both ``'django_tables2'`` and ``'tutorial'`` to your ``INSTALLED_APPS`` setting in ``settings.py``. - -We are going to run through creating a tutorial app. Let's start with a simple model:: +Now, add a model to your ``tutorial/models.py``:: # tutorial/models.py class Person(models.Model): - name = models.CharField(verbose_name="full name") + name = models.CharField(max_length=100, verbose_name='full name') + +Create the database tables for the newly added model:: + + $ python manage.py makemigrations tutorial + $ python manage.py migrate tutorial -Add some data so you have something to display in the table. Now write a view -to pass a ``Person`` queryset into a template:: +Add some data so you have something to display in the table:: + + $ python manage.py shell + >>> from tutorial.models import Person + >>> Person.objects.bulk_create([Person(name='Jieter'), Person(name='Bradley')]) + [, ] + +Now write a view to pass a ``Person`` queryset into a template:: # tutorial/views.py from django.shortcuts import render + from .models import Person def people(request): - return render(request, 'people.html', {'people': Person.objects.all()}) + return render(request, 'tutorial/people.html', {'people': Person.objects.all()}) -Finally, implement the template:: +Add the view to your ``urls.py``:: - {# tutorial/templates/people.html #} + # urls.py + from django.conf.urls import url + from django.contrib import admin + + from tutorial.views import people + + urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^people/', people) + ] + +Finally, create the template:: + + {# tutorial/templates/tutorial/people.html #} {% load render_table from django_tables2 %} - {% load static %} - + List of persons {% render_table people %} -Hook the view up in your URLs, and load the page, you should see: +You should be able to load the page in the browser (http://localhost:8000/people/ by default), +you should see: .. figure:: /_static/tutorial.png :align: center :alt: An example table rendered using django-tables2 -While simple, passing a queryset directly to ``{% render_table %}`` doesn't +While simple, passing a queryset directly to ``{% render_table %}`` does not allow for any customisation. For that, you must define a custom `.Table` class:: # tutorial/tables.py @@ -54,11 +78,10 @@ class PersonTable(tables.Table): class Meta: model = Person - # add class="paleblue" to
......
......
...
tag - attrs = {'class': 'paleblue'} + template_name = 'django_tables2/bootstrap.html' -You'll then need to instantiate and configure the table in the view, before +You will then need to instantiate and configure the table in the view, before adding it to the context:: # tutorial/views.py @@ -76,15 +99,37 @@ updates the table accordingly. This enables data ordering and pagination. Rather than passing a queryset to ``{% render_table %}``, instead pass the -table instance: +table instance:: -.. sourcecode:: django + {# tutorial/templates/tutorial/people.html #} + {% load render_table from django_tables2 %} + + + + List of persons + + + + {% render_table table %} + + + +This results in a table rendered with the bootstrap3 stylesheet: + +.. figure:: /_static/tutorial-bootstrap.png + :align: center + :alt: An example table rendered using django-tables2 with the bootstrap template - {% render_table table %} +At this point you have not actually customised anything but the template. +There are several topic you can read into to futher customize the table: -At this point you haven't actually customised anything, you've merely added the -boilerplate code that ``{% render_table %}`` does for you when given a -``QuerySet``. The remaining sections in this document describe how to change -various aspects of the table. +- Table data + - :ref:`Populating the table with data `, + - :ref:`Filtering table data ` +- Customizing the rendered table + - :ref:`Headers and footers ` + - :ref:`pinned_rows` +- :ref:`api-public` -TODO: insert links to various customisation options here. +If you think you don't have a lot customization to do and don't want to make +a full class declaration use ``django_tables2.tables.table_factory``. diff -Nru django-tables-1.14.2/docs/requirements.txt django-tables-1.21.2/docs/requirements.txt --- django-tables-1.14.2/docs/requirements.txt 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/docs/requirements.txt 2018-03-26 06:43:09.000000000 +0000 @@ -1,4 +1,4 @@ -r ../requirements/common.pip -Sphinx==1.6.2 +Sphinx==1.6.5 sphinx_rtd_theme recommonmark Binary files /tmp/tmpsQnb2r/mHVP3TqTGW/django-tables-1.14.2/docs/_static/tutorial-bootstrap.png and /tmp/tmpsQnb2r/d2pAR_hTFC/django-tables-1.21.2/docs/_static/tutorial-bootstrap.png differ Binary files /tmp/tmpsQnb2r/mHVP3TqTGW/django-tables-1.14.2/docs/_static/tutorial.png and /tmp/tmpsQnb2r/d2pAR_hTFC/django-tables-1.21.2/docs/_static/tutorial.png differ diff -Nru django-tables-1.14.2/example/app/models.py django-tables-1.21.2/example/app/models.py --- django-tables-1.14.2/example/app/models.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/example/app/models.py 2018-03-26 06:43:09.000000000 +0000 @@ -2,9 +2,12 @@ from __future__ import unicode_literals from django.db import models +from django.urls import reverse +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +@python_2_unicode_compatible class Country(models.Model): ''' Represents a geographical Country @@ -19,25 +22,26 @@ class Meta: verbose_name_plural = _('countries') - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): - return 'country/%d' % self.pk + return reverse('country_detail', args=(self.pk, )) @property def summary(self): return '%s (pop. %s)' % (self.name, self.population) +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=200, verbose_name='full name') friendly = models.BooleanField(default=True) - country = models.ForeignKey(Country, null=True) + country = models.ForeignKey(Country, null=True, on_delete=models.CASCADE) class Meta: verbose_name_plural = 'people' - def __unicode__(self): + def __str__(self): return self.name diff -Nru django-tables-1.14.2/example/app/tables.py django-tables-1.21.2/example/app/tables.py --- django-tables-1.14.2/example/app/tables.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/example/app/tables.py 2018-03-26 06:43:09.000000000 +0000 @@ -26,7 +26,7 @@ class Meta: model = Person - template = 'django_tables2/bootstrap.html' + template_name = 'django_tables2/bootstrap.html' attrs = {'class': 'table table-bordered table-striped table-hover'} exclude = ('friendly', ) @@ -37,7 +37,7 @@ class Meta: model = Person - template = 'django_tables2/semantic.html' + template_name = 'django_tables2/semantic.html' # attrs = {'class': 'ui table table-bordered table-striped table-hover'} exclude = ('friendly', ) @@ -46,4 +46,3 @@ class Meta: model = Person - # template = 'django_tables2/bootstrap.html' diff -Nru django-tables-1.14.2/example/app/views.py django-tables-1.21.2/example/app/views.py --- django-tables-1.14.2/example/app/views.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/example/app/views.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,25 +1,19 @@ # coding: utf-8 from random import choice -from django.shortcuts import render +from django.shortcuts import get_object_or_404, render +from django.urls import reverse from django.utils.lorem_ipsum import words from django.views.generic.base import TemplateView -from django_filters.views import FilterMixin, FilterView +from django_filters.views import FilterView from django_tables2 import MultiTableMixin, RequestConfig, SingleTableMixin, SingleTableView -from django_tables2.export.views import ExportMixin from .data import COUNTRIES from .filters import PersonFilter from .models import Country, Person from .tables import BootstrapTable, CountryTable, PersonTable, SemanticTable, ThemedCountryTable -try: - from django.urls import reverse -except ImportError: - # to keep backward (Django <= 1.9) compatibility - from django.core.urlresolvers import reverse - def create_fake_data(): # create some fake data to make sure we need to paginate @@ -142,3 +136,10 @@ template_name = 'bootstrap_template.html' filterset_class = PersonFilter + + +def country_detail(request, pk): + country = get_object_or_404(Country, pk=pk) + return render(request, 'country_detail.html', { + 'country': country + }) diff -Nru django-tables-1.14.2/example/requirements.pip django-tables-1.21.2/example/requirements.pip --- django-tables-1.14.2/example/requirements.pip 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/example/requirements.pip 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ --e .. -django-bootstrap3==8.2.3 -django-debug-toolbar<1.6 -django-filter==1.0.2 -tablib<0.11.99 diff -Nru django-tables-1.14.2/example/requirements.txt django-tables-1.21.2/example/requirements.txt --- django-tables-1.14.2/example/requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/example/requirements.txt 2018-03-26 06:43:09.000000000 +0000 @@ -0,0 +1,5 @@ +-e .. +django-bootstrap3==9.1.0 +django-debug-toolbar==1.9.1 +django-filter==1.1.0 +tablib<0.11.99 diff -Nru django-tables-1.14.2/example/settings.py django-tables-1.21.2/example/settings.py --- django-tables-1.14.2/example/settings.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/example/settings.py 2018-03-26 06:43:09.000000000 +0000 @@ -99,13 +99,13 @@ ] -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( + 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - "debug_toolbar.middleware.DebugToolbarMiddleware", ) ROOT_URLCONF = 'urls' @@ -119,12 +119,13 @@ 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', - 'app', 'django_filters', 'bootstrap3', 'django_tables2', 'debug_toolbar', + + 'app', ) INTERNAL_IPS = ( diff -Nru django-tables-1.14.2/example/templates/bootstrap_template.html django-tables-1.21.2/example/templates/bootstrap_template.html --- django-tables-1.14.2/example/templates/bootstrap_template.html 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/example/templates/bootstrap_template.html 2018-03-26 06:43:09.000000000 +0000 @@ -11,6 +11,7 @@
+ {% block body %}

django_tables2 with bootstrap template example

@@ -27,6 +28,7 @@ {% render_table table 'django_tables2/bootstrap.html' %}
+ {% endblock %} diff -Nru django-tables-1.14.2/example/templates/country_detail.html django-tables-1.21.2/example/templates/country_detail.html --- django-tables-1.14.2/example/templates/country_detail.html 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/example/templates/country_detail.html 2018-03-26 06:43:09.000000000 +0000 @@ -0,0 +1,7 @@ +{% extends 'semantic_template.html' %} +{% load django_tables2 %} + +{% block body %} +

{{ country }}

+{% render_table country.person_set.all 'django_tables2/semantic.html' %} +{% endblock %} diff -Nru django-tables-1.14.2/example/templates/semantic_template.html django-tables-1.21.2/example/templates/semantic_template.html --- django-tables-1.14.2/example/templates/semantic_template.html 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/example/templates/semantic_template.html 2018-03-26 06:43:09.000000000 +0000 @@ -9,8 +9,10 @@
+ {% block body %}

django_tables2 with Semantic UI template example

{% render_table table %} + {% endblock %}
diff -Nru django-tables-1.14.2/example/urls.py django-tables-1.21.2/example/urls.py --- django-tables-1.14.2/example/urls.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/example/urls.py 2018-03-26 06:43:09.000000000 +0000 @@ -4,9 +4,8 @@ from django.contrib import admin from django.views import static -from app.views import ClassBased, FilteredPersonListView, MultipleTables, bootstrap, index, multiple, semantic, tutorial - -admin.autodiscover() +from app.views import (ClassBased, FilteredPersonListView, MultipleTables, bootstrap, country_detail, index, multiple, + semantic, tutorial) urlpatterns = [ url(r'^$', index), @@ -20,9 +19,17 @@ url(r'^semantic/$', semantic, name='semantic'), url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', admin.site.urls), + + url(r'^country/(?P[0-9]+)/$', country_detail, name='country_detail'), url(r'^media/(?P.*)$', static.serve, { 'document_root': settings.MEDIA_ROOT, }), ] + +if settings.DEBUG: + import debug_toolbar + urlpatterns = [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] + urlpatterns diff -Nru django-tables-1.14.2/.gitignore django-tables-1.21.2/.gitignore --- django-tables-1.14.2/.gitignore 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/.gitignore 2018-03-26 06:43:09.000000000 +0000 @@ -19,3 +19,4 @@ .cache/ .python-version .idea +*.sw[po] diff -Nru django-tables-1.14.2/manage.py django-tables-1.21.2/manage.py --- django-tables-1.14.2/manage.py 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/manage.py 2018-03-26 06:43:09.000000000 +0000 @@ -0,0 +1,9 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.app.settings") + + from django.core.management import execute_from_command_line + execute_from_command_line(sys.argv) diff -Nru django-tables-1.14.2/README.rst django-tables-1.21.2/README.rst --- django-tables-1.14.2/README.rst 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/README.rst 2018-03-26 06:43:09.000000000 +0000 @@ -1,3 +1,5 @@ + + django-tables2 - An app for creating HTML tables ------------------------------------------------ @@ -5,10 +7,14 @@ :target: https://pypi.python.org/pypi/django-tables2 :alt: Latest PyPI version -.. image:: https://travis-ci.org/bradleyayers/django-tables2.svg?branch=master - :target: https://travis-ci.org/bradleyayers/django-tables2 +.. image:: https://travis-ci.org/jieter/django-tables2.svg?branch=master + :target: https://travis-ci.org/jieter/django-tables2 :alt: Travis CI +.. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjieter%2Fdjango-tables2.svg + :target: https://app.fossa.io/projects/git%2Bgithub.com%2Fjieter%2Fdjango-tables2?ref=badge_shield + :alt: FOSSA Status + django-tables2 simplifies the task of turning sets of data into HTML tables. It has native support for pagination and sorting. It does for HTML tables what `django.forms` does for HTML forms. e.g. @@ -16,9 +22,9 @@ - `Available on pypi `_ - Tested against currently supported versions of Django `and the python versions Django supports `_ - (see `Travis CI `_) + (see `Travis CI `_) - `Documentation on readthedocs.org `_ -- `Bug tracker `_ +- `Bug tracker `_ Features: @@ -31,13 +37,13 @@ - Template tag to enable trivial rendering to HTML. - Generic view mixin. -.. image:: https://cdn.rawgit.com/bradleyayers/django-tables2/1044316e/docs/img/example.png +.. image:: https://cdn.rawgit.com/jieter/django-tables2/1044316e/docs/img/example.png :alt: An example table rendered using django-tables2 -.. image:: https://cdn.rawgit.com/bradleyayers/django-tables2/1044316e/docs/img/bootstrap.png +.. image:: https://cdn.rawgit.com/jieter/django-tables2/1044316e/docs/img/bootstrap.png :alt: An example table rendered using django-tables2 and bootstrap theme -.. image:: https://cdn.rawgit.com/bradleyayers/django-tables2/1044316e/docs/img/semantic.png +.. image:: https://cdn.rawgit.com/jieter/django-tables2/1044316e/docs/img/semantic.png :alt: An example table rendered using django-tables2 and semantic-ui theme Example diff -Nru django-tables-1.14.2/requirements/common.pip django-tables-1.21.2/requirements/common.pip --- django-tables-1.14.2/requirements/common.pip 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/requirements/common.pip 2018-03-26 06:43:09.000000000 +0000 @@ -1,9 +1,8 @@ -django-haystack>=2.6.1 +# mocks/stubs for tests fudge +# xml parsing lxml -pylint pytz>0 -pytest -pytest-django -pytest-cov tablib==0.11.4 +mock +psycopg2 diff -Nru django-tables-1.14.2/requirements/django-dev.pip django-tables-1.21.2/requirements/django-dev.pip --- django-tables-1.14.2/requirements/django-dev.pip 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/requirements/django-dev.pip 2018-03-26 06:43:09.000000000 +0000 @@ -1,2 +1,2 @@ -r common.pip -Django==1.11.6 +Django==1.11.8 diff -Nru django-tables-1.14.2/setup.py django-tables-1.21.2/setup.py --- django-tables-1.14.2/setup.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/setup.py 2018-03-26 06:43:09.000000000 +0000 @@ -33,7 +33,10 @@ packages=find_packages(exclude=['tests.*', 'tests', 'example.*', 'example']), include_package_data=True, # declarations in MANIFEST.in - install_requires=['Django>=1.8'], + install_requires=['Django>=1.11'], + extras_require={ + 'tablib': ['tablib'] + }, classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -46,9 +49,9 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Software Development :: Libraries', ], diff -Nru django-tables-1.14.2/tests/app/models.py django-tables-1.21.2/tests/app/models.py --- django-tables-1.14.2/tests/app/models.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/app/models.py 2018-03-26 06:43:09.000000000 +0000 @@ -4,18 +4,11 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models +from django.urls import reverse from django.utils import six from django.utils.safestring import mark_safe from django.utils.translation import ugettext, ugettext_lazy -from haystack import indexes - -try: - from django.urls import reverse -except ImportError: - # to keep backward (Django <= 1.9) compatibility - from django.core.urlresolvers import reverse - @six.python_2_unicode_compatible class Person(models.Model): @@ -84,8 +77,8 @@ class Occupation(models.Model): name = models.CharField(max_length=200) region = models.ForeignKey('Region', null=True, on_delete=models.CASCADE) - boolean = models.BooleanField(null=True) - boolean_with_choices = models.BooleanField(null=True, choices=( + boolean = models.NullBooleanField(null=True) + boolean_with_choices = models.NullBooleanField(null=True, choices=( (True, 'Yes'), (False, 'No') )) @@ -113,15 +106,3 @@ verbose_name='Information', on_delete=models.CASCADE ) - - -# -- haystack ----------------------------------------------------------------- - -class PersonIndex(indexes.SearchIndex, indexes.Indexable): - first_name = indexes.CharField(document=True) - - def get_model(self): - return Person - - def index_queryset(self, using=None): - return self.get_model().objects.all() diff -Nru django-tables-1.14.2/tests/app/settings.py django-tables-1.21.2/tests/app/settings.py --- django-tables-1.14.2/tests/app/settings.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/app/settings.py 2018-03-26 06:43:09.000000000 +0000 @@ -10,7 +10,6 @@ 'django.contrib.contenttypes', 'django.contrib.auth', 'django_tables2', - 'haystack', ] ROOT_URLCONF = 'tests.app.urls' @@ -32,9 +31,3 @@ TIME_ZONE = "Australia/Brisbane" USE_TZ = True - -HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', - } -} diff -Nru django-tables-1.14.2/tests/app/templates/csrf.html django-tables-1.21.2/tests/app/templates/csrf.html --- django-tables-1.14.2/tests/app/templates/csrf.html 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/tests/app/templates/csrf.html 2018-03-26 06:43:09.000000000 +0000 @@ -0,0 +1 @@ +{% csrf_token %} diff -Nru django-tables-1.14.2/tests/app/templates/minimal.html django-tables-1.21.2/tests/app/templates/minimal.html --- django-tables-1.14.2/tests/app/templates/minimal.html 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/tests/app/templates/minimal.html 2018-03-26 06:43:09.000000000 +0000 @@ -0,0 +1,15 @@ +{% load django_tables2 %} +
+ + {% for column in table.columns %} + + {% endfor %} + + {% for row in table.paginated_rows %} + + {% for column, cell in row.items %} + + {% endfor %} + + {% endfor %} +
{{ column.header }}
{{ cell }}
diff -Nru django-tables-1.14.2/tests/columns/test_booleancolumn.py django-tables-1.21.2/tests/columns/test_booleancolumn.py --- django-tables-1.14.2/tests/columns/test_booleancolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_booleancolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,147 +1,139 @@ # coding: utf-8 from __future__ import unicode_literals -import pytest from django.db import models +from django.test import TestCase import django_tables2 as tables from ..app.models import Occupation, Person -from ..utils import attrs, build_request +from ..utils import attrs, build_request, parse -def test_should_be_used_for_booleanfield(): - class BoolModel(models.Model): - field = models.BooleanField() +class BooleanColumnTest(TestCase): + def test_should_be_used_for_booleanfield(self): + class BoolModel(models.Model): + field = models.BooleanField() - class Meta: - app_label = 'django_tables2_test' + class Meta: + app_label = 'django_tables2_test' - class Table(tables.Table): - class Meta: - model = BoolModel + class Table(tables.Table): + class Meta: + model = BoolModel - column = Table.base_columns['field'] - assert type(column) == tables.BooleanColumn - assert column.empty_values != () + column = Table.base_columns['field'] + assert type(column) == tables.BooleanColumn + assert column.empty_values != () + def test_should_be_used_for_nullbooleanfield(self): + class NullBoolModel(models.Model): + field = models.NullBooleanField() -def test_should_be_used_for_nullbooleanfield(): - class NullBoolModel(models.Model): - field = models.NullBooleanField() + class Meta: + app_label = 'django_tables2_test' - class Meta: - app_label = 'django_tables2_test' + class Table(tables.Table): + class Meta: + model = NullBoolModel - class Table(tables.Table): - class Meta: - model = NullBoolModel + column = Table.base_columns['field'] + assert type(column) == tables.BooleanColumn + assert column.empty_values == () - column = Table.base_columns['field'] - assert type(column) == tables.BooleanColumn - assert column.empty_values == () + def test_treat_none_different_from_false(self): + class Table(tables.Table): + col = tables.BooleanColumn(null=False, default='---') + table = Table([{'col': None}]) + assert table.rows[0].get_cell('col') == '---' -def test_treat_none_different_from_false(): - class Table(tables.Table): - col = tables.BooleanColumn(null=False, default='---') + def test_treat_none_as_false(self): + class Table(tables.Table): + col = tables.BooleanColumn(null=True) - table = Table([{'col': None}]) - assert table.rows[0].get_cell('col') == '---' + table = Table([{'col': None}]) + assert table.rows[0].get_cell('col') == '' + def test_value_returns_a_raw_value_without_html(self): + class Table(tables.Table): + col = tables.BooleanColumn() -def test_treat_none_as_false(): - class Table(tables.Table): - col = tables.BooleanColumn(null=True) + table = Table([{'col': True}, {'col': False}]) + assert table.rows[0].get_cell_value('col') == 'True' + assert table.rows[1].get_cell_value('col') == 'False' - table = Table([{'col': None}]) - assert table.rows[0].get_cell('col') == '' + def test_span_attrs(self): + class Table(tables.Table): + col = tables.BooleanColumn(attrs={'span': {'key': 'value'}}) + table = Table([{'col': True}]) + assert attrs(table.rows[0].get_cell('col')) == {'class': 'true', 'key': 'value'} -def test_value_returns_a_raw_value_without_html(): - class Table(tables.Table): - col = tables.BooleanColumn() + def test_boolean_field_choices_with_real_model_instances(self): + ''' + If a booleanField has choices defined, the value argument passed to + BooleanColumn.render() is the rendered value, not a bool. + ''' + class BoolModelChoices(models.Model): + field = models.BooleanField(choices=( + (True, 'Yes'), + (False, 'No')) + ) - table = Table([{'col': True}, {'col': False}]) - assert table.rows[0].get_cell_value('col') == 'True' - assert table.rows[1].get_cell_value('col') == 'False' + class Meta: + app_label = 'django_tables2_test' + class Table(tables.Table): + class Meta: + model = BoolModelChoices -def test_span_attrs(): - class Table(tables.Table): - col = tables.BooleanColumn(attrs={'span': {'key': 'value'}}) + table = Table([BoolModelChoices(field=True), BoolModelChoices(field=False)]) - table = Table([{'col': True}]) - assert attrs(table.rows[0].get_cell('col')) == {'class': 'true', 'key': 'value'} + assert table.rows[0].get_cell('field') == '' + assert table.rows[1].get_cell('field') == '' + def test_boolean_field_choices_spanning_relations(self): + 'The inverse lookup voor boolean choices should also work on related models' -def test_boolean_field_choices_with_real_model_instances(): - ''' - If a booleanField has choices defined, the value argument passed to - BooleanColumn.render() is the rendered value, not a bool. - ''' - class BoolModelChoices(models.Model): - field = models.BooleanField(choices=( - (True, 'Yes'), - (False, 'No')) - ) - - class Meta: - app_label = 'django_tables2_test' - - class Table(tables.Table): - class Meta: - model = BoolModelChoices - - table = Table([BoolModelChoices(field=True), BoolModelChoices(field=False)]) - - assert table.rows[0].get_cell('field') == '' - assert table.rows[1].get_cell('field') == '' - - -@pytest.mark.django_db -def test_boolean_field_choices_spanning_relations(): - 'The inverse lookup voor boolean choices should also work on related models' - - class Table(tables.Table): - boolean = tables.BooleanColumn(accessor='occupation.boolean_with_choices') + class Table(tables.Table): + boolean = tables.BooleanColumn(accessor='occupation.boolean_with_choices') - class Meta: - model = Person + class Meta: + model = Person - model_true = Occupation.objects.create( - name='true-name', - boolean_with_choices=True - ) - model_false = Occupation.objects.create( - name='false-name', - boolean_with_choices=False - ) - - table = Table([ - Person(first_name='True', last_name='False', occupation=model_true), - Person(first_name='True', last_name='False', occupation=model_false) - ]) - - assert table.rows[0].get_cell('boolean') == '' - assert table.rows[1].get_cell('boolean') == '' - - -@pytest.mark.django_db -def test_boolean_should_not_prevent_rendering_of_other_columns(): - '''Test for issue 360''' - class Table(tables.Table): - boolean = tables.BooleanColumn(yesno='waar,onwaar') - - class Meta: - model = Person - fields = ('boolean', 'name') + model_true = Occupation.objects.create( + name='true-name', + boolean_with_choices=True + ) + model_false = Occupation.objects.create( + name='false-name', + boolean_with_choices=False + ) - Occupation.objects.create(name='Waar', boolean=True), - Occupation.objects.create(name='Onwaar', boolean=False), - Occupation.objects.create(name='Onduidelijk') + table = Table([ + Person(first_name='True', last_name='False', occupation=model_true), + Person(first_name='True', last_name='False', occupation=model_false) + ]) + + assert table.rows[0].get_cell('boolean') == '' + assert table.rows[1].get_cell('boolean') == '' + + def test_boolean_should_not_prevent_rendering_of_other_columns(self): + '''Test for issue 360''' + class Table(tables.Table): + boolean = tables.BooleanColumn(yesno='waar,onwaar') + + class Meta: + model = Occupation + fields = ('boolean', 'name') + + Occupation.objects.create(name='Waar', boolean=True), + Occupation.objects.create(name='Onwaar', boolean=False), + Occupation.objects.create(name='Onduidelijk') - html = Table(Occupation.objects.all()).as_html(build_request()) + html = Table(Occupation.objects.all()).as_html(build_request()) + root = parse(html) - assert 'Waar' in html - assert 'Onwaar' in html + self.assertEqual(root.findall('.//tbody/tr[1]/td')[1].text, 'Waar') + self.assertEqual(root.findall('.//tbody/tr[2]/td')[1].text, 'Onwaar') diff -Nru django-tables-1.14.2/tests/columns/test_checkboxcolumn.py django-tables-1.21.2/tests/columns/test_checkboxcolumn.py --- django-tables-1.14.2/tests/columns/test_checkboxcolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_checkboxcolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,87 +1,86 @@ # coding: utf-8 from __future__ import unicode_literals +from django.test import SimpleTestCase + import django_tables2 as tables from ..utils import attrs -def test_new_attrs_should_be_supported(): - class TestTable(tables.Table): - col1 = tables.CheckBoxColumn(attrs=dict(th__input={'th_key': 'th_value'}, - td__input={'td_key': 'td_value'})) - col2 = tables.CheckBoxColumn(attrs=dict(input={'key': 'value'})) - - table = TestTable([{'col1': 'data', 'col2': 'data'}]) - assert attrs(table.columns['col1'].header) == {'type': 'checkbox', 'th_key': 'th_value'} - assert attrs(table.rows[0].get_cell('col1')) == { - 'type': 'checkbox', - 'td_key': 'td_value', - 'value': 'data', - 'name': 'col1' - } - assert attrs(table.columns['col2'].header) == {'type': 'checkbox', 'key': 'value'} - assert attrs(table.rows[0].get_cell('col2')) == { - 'type': 'checkbox', - 'key': 'value', - 'value': 'data', - 'name': 'col2' - } - - -def test_column_is_checked(): - class TestTable(tables.Table): - col = tables.CheckBoxColumn(attrs={'name': 'col'}, checked='is_selected') - - table = TestTable([ - {'col': '1', 'is_selected': True}, - {'col': '2', 'is_selected': False} - ]) - assert attrs(table.rows[0].get_cell('col')) == { - 'type': 'checkbox', - 'value': '1', - 'name': 'col', - 'checked': 'checked' - } - assert attrs(table.rows[1].get_cell('col')) == {'type': 'checkbox', 'value': '2', 'name': 'col'} - - -def test_column_is_not_checked_for_non_existing_column(): - class TestTable(tables.Table): - col = tables.CheckBoxColumn(checked='does_not_exist') - - table = TestTable([ - {'col': '1', 'is_selected': True}, - {'col': '2', 'is_selected': False} - ]) - assert attrs(table.rows[0].get_cell('col')) == {'type': 'checkbox', 'value': '1', 'name': 'col'} - assert attrs(table.rows[1].get_cell('col')) == {'type': 'checkbox', 'value': '2', 'name': 'col'} - - -def test_column_is_alway_checked(): - class TestTable(tables.Table): - col = tables.CheckBoxColumn(checked=True) - - table = TestTable([ - {'col': 1, 'foo': 'bar'}, - {'col': 2, 'foo': 'baz'} - ]) - assert attrs(table.rows[0].get_cell('col'))['checked'] == 'checked' - assert attrs(table.rows[1].get_cell('col'))['checked'] == 'checked' - - -def test_column_is_checked_callback(): - def is_selected(value, record): - return value == '1' - - class TestTable(tables.Table): - col = tables.CheckBoxColumn(attrs={'name': 'col'}, checked=is_selected) - - table = TestTable([{'col': '1'}, {'col': '2'}]) - assert attrs(table.rows[0].get_cell('col')) == { - 'type': 'checkbox', - 'value': '1', - 'name': 'col', - 'checked': 'checked' - } - assert attrs(table.rows[1].get_cell('col')) == {'type': 'checkbox', 'value': '2', 'name': 'col'} +class CheckBoxColumnTest(SimpleTestCase): + def test_new_attrs_should_be_supported(self): + class TestTable(tables.Table): + col1 = tables.CheckBoxColumn(attrs=dict(th__input={'th_key': 'th_value'}, + td__input={'td_key': 'td_value'})) + col2 = tables.CheckBoxColumn(attrs=dict(input={'key': 'value'})) + + table = TestTable([{'col1': 'data', 'col2': 'data'}]) + assert attrs(table.columns['col1'].header) == {'type': 'checkbox', 'th_key': 'th_value'} + assert attrs(table.rows[0].get_cell('col1')) == { + 'type': 'checkbox', + 'td_key': 'td_value', + 'value': 'data', + 'name': 'col1' + } + assert attrs(table.columns['col2'].header) == {'type': 'checkbox', 'key': 'value'} + assert attrs(table.rows[0].get_cell('col2')) == { + 'type': 'checkbox', + 'key': 'value', + 'value': 'data', + 'name': 'col2' + } + + def test_column_is_checked(self): + class TestTable(tables.Table): + col = tables.CheckBoxColumn(attrs={'name': 'col'}, checked='is_selected') + + table = TestTable([ + {'col': '1', 'is_selected': True}, + {'col': '2', 'is_selected': False} + ]) + assert attrs(table.rows[0].get_cell('col')) == { + 'type': 'checkbox', + 'value': '1', + 'name': 'col', + 'checked': 'checked' + } + assert attrs(table.rows[1].get_cell('col')) == {'type': 'checkbox', 'value': '2', 'name': 'col'} + + def test_column_is_not_checked_for_non_existing_column(self): + class TestTable(tables.Table): + col = tables.CheckBoxColumn(checked='does_not_exist') + + table = TestTable([ + {'col': '1', 'is_selected': True}, + {'col': '2', 'is_selected': False} + ]) + assert attrs(table.rows[0].get_cell('col')) == {'type': 'checkbox', 'value': '1', 'name': 'col'} + assert attrs(table.rows[1].get_cell('col')) == {'type': 'checkbox', 'value': '2', 'name': 'col'} + + def test_column_is_alway_checked(self): + class TestTable(tables.Table): + col = tables.CheckBoxColumn(checked=True) + + table = TestTable([ + {'col': 1, 'foo': 'bar'}, + {'col': 2, 'foo': 'baz'} + ]) + assert attrs(table.rows[0].get_cell('col'))['checked'] == 'checked' + assert attrs(table.rows[1].get_cell('col'))['checked'] == 'checked' + + def test_column_is_checked_callback(self): + def is_selected(value, record): + return value == '1' + + class TestTable(tables.Table): + col = tables.CheckBoxColumn(attrs={'name': 'col'}, checked=is_selected) + + table = TestTable([{'col': '1'}, {'col': '2'}]) + assert attrs(table.rows[0].get_cell('col')) == { + 'type': 'checkbox', + 'value': '1', + 'name': 'col', + 'checked': 'checked' + } + assert attrs(table.rows[1].get_cell('col')) == {'type': 'checkbox', 'value': '2', 'name': 'col'} diff -Nru django-tables-1.14.2/tests/columns/test_datecolumn.py django-tables-1.21.2/tests/columns/test_datecolumn.py --- django-tables-1.14.2/tests/columns/test_datecolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_datecolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -4,80 +4,74 @@ from datetime import date from django.db import models +from django.test import SimpleTestCase, override_settings import django_tables2 as tables -''' -Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date -D -- Day of the week, textual, 3 letters -- 'Fri' -b -- Month, textual, 3 letters, lowercase -- 'jan' -Y -- Year, 4 digits. -- '1999' -''' +class DateColumnTest(SimpleTestCase): + ''' + Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date + D -- Day of the week, textual, 3 letters -- 'Fri' + b -- Month, textual, 3 letters, lowercase -- 'jan' + Y -- Year, 4 digits. -- '1999' + ''' + + def test_should_handle_explicit_format(self): + class TestTable(tables.Table): + date = tables.DateColumn(format='D b Y') + + class Meta: + default = '—' + + table = TestTable([{'date': date(2012, 9, 11)}, + {'date': None}]) + assert table.rows[0].get_cell('date') == 'Tue sep 2012' + assert table.rows[1].get_cell('date') == '—' + + @override_settings(DATE_FORMAT='D Y b') + def test_should_handle_long_format(self): + class TestTable(tables.Table): + date = tables.DateColumn(short=False) + + class Meta: + default = '—' + + table = TestTable([{'date': date(2012, 9, 11)}, + {'date': None}]) + assert table.rows[0].get_cell('date') == 'Tue 2012 sep' + assert table.rows[1].get_cell('date') == '—' + + @override_settings(SHORT_DATE_FORMAT='b Y D') + def test_should_handle_short_format(self): + class TestTable(tables.Table): + date = tables.DateColumn(short=True) + + class Meta: + default = '—' + + table = TestTable([{'date': date(2012, 9, 11)}, + {'date': None}]) + assert table.rows[0].get_cell('date') == 'sep 2012 Tue' + assert table.rows[1].get_cell('date') == '—' + + def test_should_be_used_for_datefields(self): + class DateModel(models.Model): + field = models.DateField() + + class Meta: + app_label = 'django_tables2_test' + + class Table(tables.Table): + class Meta: + model = DateModel + + assert type(Table.base_columns['field']) == tables.DateColumn + + @override_settings(SHORT_DATE_FORMAT='b Y D') + def test_value_returns_a_raw_value_without_html(self): + class Table(tables.Table): + col = tables.DateColumn() - -def test_should_handle_explicit_format(): - class TestTable(tables.Table): - date = tables.DateColumn(format='D b Y') - - class Meta: - default = '—' - - table = TestTable([{'date': date(2012, 9, 11)}, - {'date': None}]) - assert table.rows[0].get_cell('date') == 'Tue sep 2012' - assert table.rows[1].get_cell('date') == '—' - - -def test_should_handle_long_format(settings): - settings.DATE_FORMAT = 'D Y b' - - class TestTable(tables.Table): - date = tables.DateColumn(short=False) - - class Meta: - default = '—' - - table = TestTable([{'date': date(2012, 9, 11)}, - {'date': None}]) - assert table.rows[0].get_cell('date') == 'Tue 2012 sep' - assert table.rows[1].get_cell('date') == '—' - - -def test_should_handle_short_format(settings): - settings.SHORT_DATE_FORMAT = 'b Y D' - - class TestTable(tables.Table): - date = tables.DateColumn(short=True) - - class Meta: - default = '—' - - table = TestTable([{'date': date(2012, 9, 11)}, - {'date': None}]) - assert table.rows[0].get_cell('date') == 'sep 2012 Tue' - assert table.rows[1].get_cell('date') == '—' - - -def test_should_be_used_for_datefields(): - class DateModel(models.Model): - field = models.DateField() - - class Meta: - app_label = 'django_tables2_test' - - class Table(tables.Table): - class Meta: - model = DateModel - - assert type(Table.base_columns['field']) == tables.DateColumn - - -def test_value_returns_a_raw_value_without_html(settings): - settings.SHORT_DATE_FORMAT = 'b Y D' - - class Table(tables.Table): - col = tables.DateColumn() - - table = Table([{'col': date(2012, 9, 11)}]) - assert table.rows[0].get_cell_value('col') == 'sep 2012 Tue' + table = Table([{'col': date(2012, 9, 11)}]) + assert table.rows[0].get_cell_value('col') == 'sep 2012 Tue' diff -Nru django-tables-1.14.2/tests/columns/test_datetimecolumn.py django-tables-1.21.2/tests/columns/test_datetimecolumn.py --- django-tables-1.14.2/tests/columns/test_datetimecolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_datetimecolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -3,86 +3,78 @@ from datetime import datetime -import pytest import pytz from django.db import models +from django.test import SimpleTestCase, override_settings import django_tables2 as tables -''' -Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date -D -- Day of the week, textual, 3 letters -- 'Fri' -b -- Month, textual, 3 letters, lowercase -- 'jan' -Y -- Year, 4 digits. -- '1999' -A -- 'AM' or 'PM'. -- 'AM' -f -- Time, in 12-hour hours[:minutes] -- '1', '1:30' -''' +class DateTimeColumnTest(SimpleTestCase): + ''' + Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date + D -- Day of the week, textual, 3 letters -- 'Fri' + b -- Month, textual, 3 letters, lowercase -- 'jan' + Y -- Year, 4 digits. -- '1999' + A -- 'AM' or 'PM'. -- 'AM' + f -- Time, in 12-hour hours[:minutes] -- '1', '1:30' + ''' + def dt(self): + dt = datetime(2012, 9, 11, 12, 30, 0) + return pytz.timezone('Australia/Brisbane').localize(dt) + + def test_should_handle_explicit_format(self): + class TestTable(tables.Table): + date = tables.DateTimeColumn(format='D b Y') + + class Meta: + default = '—' + + table = TestTable([{'date': self.dt()}, {'date': None}]) + assert table.rows[0].get_cell('date') == 'Tue sep 2012' + assert table.rows[1].get_cell('date') == '—' + + @override_settings(DATETIME_FORMAT='D Y b A f') + def test_should_handle_long_format(self): + class TestTable(tables.Table): + date = tables.DateTimeColumn(short=False) + + class Meta: + default = '—' + + table = TestTable([{'date': self.dt()}, {'date': None}]) + assert table.rows[0].get_cell('date') == 'Tue 2012 sep PM 12:30' + assert table.rows[1].get_cell('date') == '—' + + @override_settings(SHORT_DATETIME_FORMAT='b Y D A f') + def test_should_handle_short_format(self): + class TestTable(tables.Table): + date = tables.DateTimeColumn(short=True) + + class Meta: + default = '—' + + table = TestTable([{'date': self.dt()}, {'date': None}]) + assert table.rows[0].get_cell('date') == 'sep 2012 Tue PM 12:30' + assert table.rows[1].get_cell('date') == '—' + + def test_should_be_used_for_datetimefields(self): + class DateTimeModel(models.Model): + field = models.DateTimeField() + + class Meta: + app_label = 'django_tables2_test' + + class Table(tables.Table): + class Meta: + model = DateTimeModel + + assert type(Table.base_columns['field']) == tables.DateTimeColumn + + @override_settings(SHORT_DATETIME_FORMAT='b Y D A f') + def test_value_returns_a_raw_value_without_html(self): + class Table(tables.Table): + col = tables.DateTimeColumn() - -@pytest.yield_fixture -def dt(): - dt = datetime(2012, 9, 11, 12, 30, 0) - yield pytz.timezone('Australia/Brisbane').localize(dt) - - -def test_should_handle_explicit_format(dt): - class TestTable(tables.Table): - date = tables.DateTimeColumn(format='D b Y') - - class Meta: - default = '—' - - table = TestTable([{'date': dt}, {'date': None}]) - assert table.rows[0].get_cell('date') == 'Tue sep 2012' - assert table.rows[1].get_cell('date') == '—' - - -def test_should_handle_long_format(dt, settings): - class TestTable(tables.Table): - date = tables.DateTimeColumn(short=False) - - class Meta: - default = '—' - - settings.DATETIME_FORMAT = 'D Y b A f' - table = TestTable([{'date': dt}, {'date': None}]) - assert table.rows[0].get_cell('date') == 'Tue 2012 sep PM 12:30' - assert table.rows[1].get_cell('date') == '—' - - -def test_should_handle_short_format(dt, settings): - class TestTable(tables.Table): - date = tables.DateTimeColumn(short=True) - - class Meta: - default = '—' - - settings.SHORT_DATETIME_FORMAT = 'b Y D A f' - table = TestTable([{'date': dt}, {'date': None}]) - assert table.rows[0].get_cell('date') == 'sep 2012 Tue PM 12:30' - assert table.rows[1].get_cell('date') == '—' - - -def test_should_be_used_for_datetimefields(): - class DateTimeModel(models.Model): - field = models.DateTimeField() - - class Meta: - app_label = 'django_tables2_test' - - class Table(tables.Table): - class Meta: - model = DateTimeModel - - assert type(Table.base_columns['field']) == tables.DateTimeColumn - - -def test_value_returns_a_raw_value_without_html(dt, settings): - settings.SHORT_DATETIME_FORMAT = 'b Y D A f' - - class Table(tables.Table): - col = tables.DateTimeColumn() - - table = Table([{'col': dt}]) - assert table.rows[0].get_cell_value('col') == 'sep 2012 Tue PM 12:30' + table = Table([{'col': self.dt()}]) + assert table.rows[0].get_cell_value('col') == 'sep 2012 Tue PM 12:30' diff -Nru django-tables-1.14.2/tests/columns/test_emailcolumn.py django-tables-1.21.2/tests/columns/test_emailcolumn.py --- django-tables-1.14.2/tests/columns/test_emailcolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_emailcolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -2,51 +2,49 @@ from __future__ import unicode_literals from django.db import models +from django.test import SimpleTestCase import django_tables2 as tables -def test_should_turn_email_address_into_hyperlink(): - class Table(tables.Table): - email = tables.EmailColumn() +class EmailColumnTest(SimpleTestCase): + def test_should_turn_email_address_into_hyperlink(self): + class Table(tables.Table): + email = tables.EmailColumn() - table = Table([{'email': 'test@example.com'}]) - assert table.rows[0].get_cell('email') == 'test@example.com' + table = Table([{'email': 'test@example.com'}]) + assert table.rows[0].get_cell('email') == 'test@example.com' + def test_should_render_default_for_blank(self): + class Table(tables.Table): + email = tables.EmailColumn(default='---') -def test_should_render_default_for_blank(): - class Table(tables.Table): - email = tables.EmailColumn(default='---') + table = Table([{'email': ''}]) + assert table.rows[0].get_cell('email') == '---' - table = Table([{'email': ''}]) - assert table.rows[0].get_cell('email') == '---' + def test_should_be_used_for_emailfields(self): + class EmailModel(models.Model): + field = models.EmailField() + class Meta: + app_label = 'test' -def test_should_be_used_for_emailfields(): - class EmailModel(models.Model): - field = models.EmailField() + class Table(tables.Table): + class Meta: + model = EmailModel - class Meta: - app_label = 'django_tables2_test' + assert type(Table.base_columns['field']) == tables.EmailColumn - class Table(tables.Table): - class Meta: - model = EmailModel + def test_text_should_be_overridable(self): + class Table(tables.Table): + email = tables.EmailColumn(text='@') - assert type(Table.base_columns['field']) == tables.EmailColumn + table = Table([{'email': 'test@example.com'}]) + assert table.rows[0].get_cell('email') == '@' + def test_value_returns_a_raw_value_without_html(self): + class Table(tables.Table): + col = tables.EmailColumn() -def test_text_should_be_overridable(): - class Table(tables.Table): - email = tables.EmailColumn(text='@') - - table = Table([{'email': 'test@example.com'}]) - assert table.rows[0].get_cell('email') == '@' - - -def test_value_returns_a_raw_value_without_html(): - class Table(tables.Table): - col = tables.EmailColumn() - - table = Table([{'col': 'test@example.com'}]) - assert table.rows[0].get_cell_value('col') == 'test@example.com' + table = Table([{'col': 'test@example.com'}]) + assert table.rows[0].get_cell_value('col') == 'test@example.com' diff -Nru django-tables-1.14.2/tests/columns/test_filecolumn.py django-tables-1.21.2/tests/columns/test_filecolumn.py --- django-tables-1.14.2/tests/columns/test_filecolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_filecolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,107 +1,97 @@ # coding: utf-8 from __future__ import unicode_literals -from os.path import dirname, join +import os -import pytest from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage from django.db import models from django.db.models.fields.files import FieldFile +from django.test import SimpleTestCase import django_tables2 as tables from ..utils import parse -@pytest.yield_fixture def storage(): '''Provide a storage that exposes the test templates''' - root = join(dirname(__file__), '..', 'app', 'templates') - yield FileSystemStorage(location=root, base_url='/baseurl/') + root = os.path.join(os.path.dirname(__file__), '..', 'app', 'templates') + return FileSystemStorage(location=root, base_url='/baseurl/') -@pytest.yield_fixture def column(): - yield tables.FileColumn(attrs={ + return tables.FileColumn(attrs={ 'span': {'class': 'span'}, 'a': {'class': 'a'} }) -@pytest.yield_fixture -def column_with_text(): - yield tables.FileColumn(text='Download') - - -def test_should_be_used_for_filefields(): - class FileModel(models.Model): - field = models.FileField() - - class Meta: - app_label = 'django_tables2_test' - - class Table(tables.Table): - class Meta: - model = FileModel - - assert type(Table.base_columns['field']) == tables.FileColumn - - -def test_filecolumn_supports_storage_file(column, storage): - file_ = storage.open('child/foo.html') - try: - root = parse(column.render(value=file_, record=None)) - finally: - file_.close() - path = file_.name - assert root.tag == 'span' - assert root.attrib == {'class': 'span exists', 'title': path} - assert root.text == 'foo.html' - - -def test_filecolumn_supports_contentfile(column): - name = 'foobar.html' - file_ = ContentFile('') - file_.name = name - root = parse(column.render(value=file_, record=None)) - assert root.tag == 'span' - assert root.attrib == {'title': name, 'class': 'span'} - assert root.text == 'foobar.html' - - -def test_filecolumn_supports_fieldfile(column, storage): - field = models.FileField(storage=storage) - name = 'child/foo.html' - fieldfile = FieldFile(instance=None, field=field, name=name) - root = parse(column.render(value=fieldfile, record=None)) - assert root.tag == 'a' - assert root.attrib == { - 'class': 'a exists', - 'title': name, - 'href': '/baseurl/child/foo.html' - } - assert root.text == 'foo.html' - - # Now try a file that doesn't exist - name = 'child/does_not_exist.html' - fieldfile = FieldFile(instance=None, field=field, name=name) - html = column.render(value=fieldfile, record=None) - root = parse(html) - assert root.tag == 'a' - assert root.attrib == { - 'class': 'a missing', - 'title': name, - 'href': '/baseurl/child/does_not_exist.html' - } - assert root.text == 'does_not_exist.html' - - -def test_filecolumn_text_custom_value(column_with_text, storage): - name = 'foobar.html' - file_ = ContentFile('') - file_.name = name - root = parse(column_with_text.render(value=file_, record=None)) - assert root.tag == 'span' - assert root.attrib == {'title': name, 'class': ''} - assert root.text == 'Download' +class FileColumnTest(SimpleTestCase): + def test_should_be_used_for_filefields(self): + class FileModel(models.Model): + field = models.FileField() + + class Meta: + app_label = 'django_tables2_test' + + class Table(tables.Table): + class Meta: + model = FileModel + + assert type(Table.base_columns['field']) == tables.FileColumn + + def test_filecolumn_supports_storage_file(self): + file_ = storage().open('child/foo.html') + try: + root = parse(column().render(value=file_, record=None)) + finally: + file_.close() + path = file_.name + assert root.tag == 'span' + assert root.attrib == {'class': 'span exists', 'title': path} + assert root.text == 'foo.html' + + def test_filecolumn_supports_contentfile(self): + name = 'foobar.html' + file_ = ContentFile('') + file_.name = name + root = parse(column().render(value=file_, record=None)) + assert root.tag == 'span' + assert root.attrib == {'title': name, 'class': 'span'} + assert root.text == 'foobar.html' + + def test_filecolumn_supports_fieldfile(self): + field = models.FileField(storage=storage()) + name = 'child/foo.html' + fieldfile = FieldFile(instance=None, field=field, name=name) + root = parse(column().render(value=fieldfile, record=None)) + assert root.tag == 'a' + assert root.attrib == { + 'class': 'a exists', + 'title': name, + 'href': '/baseurl/child/foo.html' + } + assert root.text == 'foo.html' + + # Now try a file that doesn't exist + name = 'child/does_not_exist.html' + fieldfile = FieldFile(instance=None, field=field, name=name) + html = column().render(value=fieldfile, record=None) + root = parse(html) + assert root.tag == 'a' + assert root.attrib == { + 'class': 'a missing', + 'title': name, + 'href': '/baseurl/child/does_not_exist.html' + } + assert root.text == 'does_not_exist.html' + + def test_filecolumn_text_custom_value(self): + name = 'foobar.html' + file_ = ContentFile('') + file_.name = name + root = parse(tables.FileColumn(text='Download').render(value=file_, record=None)) + assert root.tag == 'span' + assert root.attrib == {'title': name, 'class': ''} + assert root.text == 'Download' diff -Nru django-tables-1.14.2/tests/columns/test_general.py django-tables-1.21.2/tests/columns/test_general.py --- django-tables-1.14.2/tests/columns/test_general.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_general.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,8 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals -import pytest from django.db import models +from django.test import TestCase from django.utils.safestring import SafeData, mark_safe from django.utils.translation import ugettext_lazy @@ -14,328 +14,325 @@ request = build_request('/') -def test_column_render_supports_kwargs(): - class TestColumn(tables.Column): - def render(self, **kwargs): - expected = {'record', 'value', 'column', 'bound_column', 'bound_row', 'table'} - actual = set(kwargs.keys()) - assert actual == expected - return 'success' +class ColumnGeneralTest(TestCase): + def test_column_render_supports_kwargs(self): + class TestColumn(tables.Column): + def render(self, **kwargs): + expected = {'record', 'value', 'column', 'bound_column', 'bound_row', 'table'} + actual = set(kwargs.keys()) + assert actual == expected + return 'success' - class TestTable(tables.Table): - foo = TestColumn() + class TestTable(tables.Table): + foo = TestColumn() - table = TestTable([{'foo': 'bar'}]) - assert table.rows[0].get_cell('foo') == 'success' + table = TestTable([{'foo': 'bar'}]) + assert table.rows[0].get_cell('foo') == 'success' - -def test_column_header_should_use_titlised_verbose_name_unless_given_explicitly(): - class SimpleTable(tables.Table): - basic = tables.Column() - acronym = tables.Column(verbose_name='has FBI help') - - table = SimpleTable([]) - assert table.columns['basic'].header == 'Basic' - assert table.columns['acronym'].header == 'has FBI help' - - -def test_should_support_safe_verbose_name(): - class SimpleTable(tables.Table): - safe = tables.Column(verbose_name=mark_safe('Safe')) - - table = SimpleTable([]) - assert isinstance(table.columns['safe'].header, SafeData) - - -def test_should_raise_on_invalid_accessor(): - with pytest.raises(TypeError): + def test_column_header_should_use_titlised_verbose_name_unless_given_explicitly(self): class SimpleTable(tables.Table): - column = tables.Column(accessor={}) + basic = tables.Column() + acronym = tables.Column(verbose_name='has FBI help') + table = SimpleTable([]) + assert table.columns['basic'].header == 'Basic' + assert table.columns['acronym'].header == 'has FBI help' -def test_column_with_callable_accessor_should_not_have_default(): - with pytest.raises(TypeError): + def test_should_support_safe_verbose_name(self): class SimpleTable(tables.Table): - column = tables.Column(accessor=lambda: 'foo', default='') - - -def test_should_support_safe_verbose_name_via_model(): - class PersonTable(tables.Table): - safe = tables.Column() - - table = PersonTable(Person.objects.all()) - assert isinstance(table.columns['safe'].header, SafeData) - - -def test_should_support_empty_string_as_explicit_verbose_name(): - class SimpleTable(tables.Table): - acronym = tables.Column(verbose_name='') - - table = SimpleTable([]) - assert table.columns['acronym'].header == '' - - -@pytest.mark.django_db -def test_handle_verbose_name_of_many2onerel(): - - class Table(tables.Table): - count = tables.Column(accessor='info_list.count') - - Person.objects.create(first_name='bradley', last_name='ayers') - table = Table(Person.objects.all()) - assert table.columns['count'].verbose_name == 'Information' - - -def test_orderable(): - class SimpleTable(tables.Table): - name = tables.Column() - - table = SimpleTable([]) - assert table.columns['name'].orderable is True - - class SimpleTable(tables.Table): - name = tables.Column() - - class Meta: - orderable = False - table = SimpleTable([]) - assert table.columns['name'].orderable is False - - class SimpleTable(tables.Table): - name = tables.Column() - - class Meta: - orderable = True - - table = SimpleTable([]) - assert table.columns['name'].orderable is True - - -def test_order_by_defaults_to_accessor(): - class SimpleTable(tables.Table): - foo = tables.Column(accessor='bar') - - table = SimpleTable([]) - assert table.columns['foo'].order_by == ('bar', ) - - -def test_supports_order_by(): - class SimpleTable(tables.Table): - name = tables.Column(order_by=('last_name', '-first_name')) - age = tables.Column() - - table = SimpleTable([], order_by=('-age', )) - # alias - assert table.columns['name'].order_by_alias == 'name' - assert table.columns['age'].order_by_alias == '-age' - # order by - assert table.columns['name'].order_by == ('last_name', '-first_name') - assert table.columns['age'].order_by == ('-age', ) - - # now try with name ordered - table = SimpleTable([], order_by=('-name', )) - # alias - assert table.columns['name'].order_by_alias == '-name' - assert table.columns['age'].order_by_alias == 'age' - # alias next - assert table.columns['name'].order_by_alias.next == 'name' - assert table.columns['age'].order_by_alias.next == 'age' - # order by - assert table.columns['name'].order_by == ('-last_name', 'first_name') - assert table.columns['age'].order_by == ('age', ) - - -def test_supports_is_ordered(): - class SimpleTable(tables.Table): - name = tables.Column() + safe = tables.Column(verbose_name=mark_safe('Safe')) - # sorted - table = SimpleTable([], order_by='name') - assert table.columns['name'].is_ordered - # unsorted - table = SimpleTable([]) - assert not table.columns['name'].is_ordered + table = SimpleTable([]) + assert isinstance(table.columns['safe'].header, SafeData) + def test_should_raise_on_invalid_accessor(self): + with self.assertRaises(TypeError): + class SimpleTable(tables.Table): + column = tables.Column(accessor={}) -def test_translation(): - ''' - Tests different types of values for the ``verbose_name`` property of a - column. - ''' - class TranslationTable(tables.Table): - text = tables.Column(verbose_name=ugettext_lazy('Text')) + def test_column_with_callable_accessor_should_not_have_default(self): + with self.assertRaises(TypeError): + class SimpleTable(tables.Table): + column = tables.Column(accessor=lambda: 'foo', default='') - table = TranslationTable([]) - assert 'Text' == table.columns['text'].header + def test_should_support_safe_verbose_name_via_model(self): + class PersonTable(tables.Table): + safe = tables.Column() + table = PersonTable(Person.objects.all()) + assert isinstance(table.columns['safe'].header, SafeData) -def test_sequence(): - ''' - Ensures that the sequence of columns is configurable. - ''' - class TestTable(tables.Table): - a = tables.Column() - b = tables.Column() - c = tables.Column() - assert ['a', 'b', 'c'] == TestTable([]).columns.names() - assert ['b', 'a', 'c'] == TestTable([], sequence=('b', 'a', 'c')).columns.names() - - class TestTable2(TestTable): - class Meta: - sequence = ('b', 'a', 'c') - assert ['b', 'a', 'c'] == TestTable2([]).columns.names() - assert ['a', 'b', 'c'] == TestTable2([], sequence=('a', 'b', 'c')).columns.names() + def test_should_support_empty_string_as_explicit_verbose_name(self): + class SimpleTable(tables.Table): + acronym = tables.Column(verbose_name='') - class TestTable3(TestTable): - class Meta: - sequence = ('c', ) - assert ['c', 'a', 'b'] == TestTable3([]).columns.names() - assert ['c', 'a', 'b'] == TestTable([], sequence=('c', )).columns.names() + table = SimpleTable([]) + assert table.columns['acronym'].header == '' - class TestTable4(TestTable): - class Meta: - sequence = ('...', ) - assert ['a', 'b', 'c'] == TestTable4([]).columns.names() - assert ['a', 'b', 'c'] == TestTable([], sequence=('...', )).columns.names() + def test_handle_verbose_name_of_many2onerel(self): + class Table(tables.Table): + count = tables.Column(accessor='info_list.count') - class TestTable5(TestTable): - class Meta: - sequence = ('b', '...') - assert ['b', 'a', 'c'] == TestTable5([]).columns.names() - assert ['b', 'a', 'c'] == TestTable([], sequence=('b', '...')).columns.names() + Person.objects.create(first_name='bradley', last_name='ayers') + table = Table(Person.objects.all()) + assert table.columns['count'].verbose_name == 'Information' - class TestTable6(TestTable): - class Meta: - sequence = ('...', 'b') - assert ['a', 'c', 'b'] == TestTable6([]).columns.names() - assert ['a', 'c', 'b'] == TestTable([], sequence=('...', 'b')).columns.names() + def test_orderable(self): + class SimpleTable(tables.Table): + name = tables.Column() - class TestTable7(TestTable): - class Meta: - sequence = ('b', '...', 'a') - assert ['b', 'c', 'a'] == TestTable7([]).columns.names() - assert ['b', 'c', 'a'] == TestTable([], sequence=('b', '...', 'a')).columns.names() + table = SimpleTable([]) + assert table.columns['name'].orderable is True - # Let's test inheritence - class TestTable8(TestTable): - d = tables.Column() - e = tables.Column() - f = tables.Column() + class SimpleTable(tables.Table): + name = tables.Column() - class Meta: - sequence = ('d', '...') + class Meta: + orderable = False + table = SimpleTable([]) + assert table.columns['name'].orderable is False - class TestTable9(TestTable): - d = tables.Column() - e = tables.Column() - f = tables.Column() + class SimpleTable(tables.Table): + name = tables.Column() - assert ['d', 'a', 'b', 'c', 'e', 'f'] == TestTable8([]).columns.names() - assert ['d', 'a', 'b', 'c', 'e', 'f'] == TestTable9([], sequence=('d', '...')).columns.names() + class Meta: + orderable = True + table = SimpleTable([]) + assert table.columns['name'].orderable is True -def test_should_support_both_meta_sequence_and_constructor_exclude(): - ''' - Issue #32 describes a problem when both ``Meta.sequence`` and - ``Table(..., exclude=...)`` are used on a single table. The bug caused an - exception to be raised when the table was iterated. - ''' - class SequencedTable(tables.Table): - a = tables.Column() - b = tables.Column() - c = tables.Column() + def test_order_by_defaults_to_accessor(self): + class SimpleTable(tables.Table): + foo = tables.Column(accessor='bar') - class Meta: - sequence = ('a', '...') + table = SimpleTable([]) + assert table.columns['foo'].order_by == ('bar', ) - table = SequencedTable([], exclude=('c', )) - table.as_html(request) + def test_supports_order_by(self): + class SimpleTable(tables.Table): + name = tables.Column(order_by=('last_name', '-first_name')) + age = tables.Column() + table = SimpleTable([], order_by=('-age', )) + # alias + assert table.columns['name'].order_by_alias == 'name' + assert table.columns['age'].order_by_alias == '-age' + # order by + assert table.columns['name'].order_by == ('last_name', '-first_name') + assert table.columns['age'].order_by == ('-age', ) + + # now try with name ordered + table = SimpleTable([], order_by=('-name', )) + # alias + assert table.columns['name'].order_by_alias == '-name' + assert table.columns['age'].order_by_alias == 'age' + # alias next + assert table.columns['name'].order_by_alias.next == 'name' + assert table.columns['age'].order_by_alias.next == 'age' + # order by + assert table.columns['name'].order_by == ('-last_name', 'first_name') + assert table.columns['age'].order_by == ('age', ) -def test_bound_columns_should_support_indexing(): - class SimpleTable(tables.Table): - a = tables.Column() - b = tables.Column() + def test_supports_is_ordered(self): + class SimpleTable(tables.Table): + name = tables.Column() - table = SimpleTable([]) - assert 'b' == table.columns[1].name - assert 'b' == table.columns['b'].name + # sorted + table = SimpleTable([], order_by='name') + assert table.columns['name'].is_ordered + # unsorted + table = SimpleTable([]) + assert not table.columns['name'].is_ordered + def test_translation(self): + ''' + Tests different types of values for the ``verbose_name`` property of a + column. + ''' + class TranslationTable(tables.Table): + text = tables.Column(verbose_name=ugettext_lazy('Text')) -def test_cell_attrs_applies_to_td_and_th(): - class SimpleTable(tables.Table): - a = tables.Column(attrs={'cell': {'key': 'value'}}) + table = TranslationTable([]) + assert 'Text' == table.columns['text'].header - # providing data ensures 1 row is rendered - table = SimpleTable([{'a': 'value'}]) - root = parse(table.as_html(request)) + def test_sequence(self): + ''' + Ensures that the sequence of columns is configurable. + ''' + class TestTable(tables.Table): + a = tables.Column() + b = tables.Column() + c = tables.Column() + assert ['a', 'b', 'c'] == TestTable([]).columns.names() + assert ['b', 'a', 'c'] == TestTable([], sequence=('b', 'a', 'c')).columns.names() + + class TestTable2(TestTable): + class Meta: + sequence = ('b', 'a', 'c') + assert ['b', 'a', 'c'] == TestTable2([]).columns.names() + assert ['a', 'b', 'c'] == TestTable2([], sequence=('a', 'b', 'c')).columns.names() + + class TestTable3(TestTable): + class Meta: + sequence = ('c', ) + assert ['c', 'a', 'b'] == TestTable3([]).columns.names() + assert ['c', 'a', 'b'] == TestTable([], sequence=('c', )).columns.names() + + class TestTable4(TestTable): + class Meta: + sequence = ('...', ) + assert ['a', 'b', 'c'] == TestTable4([]).columns.names() + assert ['a', 'b', 'c'] == TestTable([], sequence=('...', )).columns.names() + + class TestTable5(TestTable): + class Meta: + sequence = ('b', '...') + assert ['b', 'a', 'c'] == TestTable5([]).columns.names() + assert ['b', 'a', 'c'] == TestTable([], sequence=('b', '...')).columns.names() + + class TestTable6(TestTable): + class Meta: + sequence = ('...', 'b') + assert ['a', 'c', 'b'] == TestTable6([]).columns.names() + assert ['a', 'c', 'b'] == TestTable([], sequence=('...', 'b')).columns.names() + + class TestTable7(TestTable): + class Meta: + sequence = ('b', '...', 'a') + assert ['b', 'c', 'a'] == TestTable7([]).columns.names() + assert ['b', 'c', 'a'] == TestTable([], sequence=('b', '...', 'a')).columns.names() + + # Let's test inheritence + class TestTable8(TestTable): + d = tables.Column() + e = tables.Column() + f = tables.Column() + + class Meta: + sequence = ('d', '...') + + class TestTable9(TestTable): + d = tables.Column() + e = tables.Column() + f = tables.Column() - assert root.findall('.//thead/tr/th')[0].attrib == {'key': 'value', 'class': 'a orderable'} - assert root.findall('.//tbody/tr/td')[0].attrib == {'key': 'value', 'class': 'a'} + assert ['d', 'a', 'b', 'c', 'e', 'f'] == TestTable8([]).columns.names() + assert ['d', 'a', 'b', 'c', 'e', 'f'] == TestTable9([], sequence=('d', '...')).columns.names() + def test_should_support_both_meta_sequence_and_constructor_exclude(self): + ''' + Issue #32 describes a problem when both ``Meta.sequence`` and + ``Table(..., exclude=...)`` are used on a single table. The bug caused an + exception to be raised when the table was iterated. + ''' + class SequencedTable(tables.Table): + a = tables.Column() + b = tables.Column() + c = tables.Column() -def test_cells_are_automatically_given_column_name_as_class(): - class SimpleTable(tables.Table): - a = tables.Column() + class Meta: + sequence = ('a', '...') - table = SimpleTable([{'a': 'value'}]) - root = parse(table.as_html(request)) - assert root.findall('.//thead/tr/th')[0].attrib == {'class': 'a orderable'} - assert root.findall('.//tbody/tr/td')[0].attrib == {'class': 'a'} + table = SequencedTable([], exclude=('c', )) + table.as_html(request) + def test_bound_columns_should_support_indexing(self): + class SimpleTable(tables.Table): + a = tables.Column() + b = tables.Column() -def test_th_are_given_orderable_class_if_column_is_orderable(): - class SimpleTable(tables.Table): - a = tables.Column() - b = tables.Column(orderable=False) + table = SimpleTable([]) + assert 'b' == table.columns[1].name + assert 'b' == table.columns['b'].name - table = SimpleTable([{'a': 'value'}]) - root = parse(table.as_html(request)) - # return classes of an element as a set - classes = lambda x: set(x.attrib['class'].split()) - assert 'orderable' in classes(root.findall('.//thead/tr/th')[0]) - assert 'orderable' not in classes(root.findall('.//thead/tr/th')[1]) + def test_cell_attrs_applies_to_td_and_th_and_footer_td(self): + class SimpleTable(tables.Table): + a = tables.Column( + attrs={'cell': {'key': 'value'}}, + footer=lambda table: len(table.data) + ) + + # providing data ensures 1 row is rendered + table = SimpleTable([{'a': 'value'}]) + root = parse(table.as_html(request)) + + assert root.findall('.//thead/tr/th')[0].attrib == {'key': 'value', 'class': 'a orderable'} + assert root.findall('.//tbody/tr/td')[0].attrib == {'key': 'value', 'class': 'a'} + assert root.findall('.//tfoot/tr/td')[0].attrib == {'key': 'value', 'class': 'a'} - # Now try with an ordered table - table = SimpleTable([], order_by='a') - root = parse(table.as_html(request)) - # return classes of an element as a set - assert 'orderable' in classes(root.findall('.//thead/tr/th')[0]) - assert 'asc' in classes(root.findall('.//thead/tr/th')[0]) - assert 'orderable' not in classes(root.findall('.//thead/tr/th')[1]) + def test_cells_are_automatically_given_column_name_as_class(self): + class SimpleTable(tables.Table): + a = tables.Column() + table = SimpleTable([{'a': 'value'}]) + root = parse(table.as_html(request)) + assert root.findall('.//thead/tr/th')[0].attrib == {'class': 'a orderable'} + assert root.findall('.//tbody/tr/td')[0].attrib == {'class': 'a'} -def test_empty_values_triggers_default(): - class Table(tables.Table): - a = tables.Column(empty_values=(1, 2), default='--') + def test_th_are_given_orderable_class_if_column_is_orderable(self): + class SimpleTable(tables.Table): + a = tables.Column() + b = tables.Column(orderable=False) - table = Table([{'a': 1}, {'a': 2}, {'a': 3}, {'a': 4}]) - assert [row.get_cell('a') for row in table.rows] == ['--', '--', 3, 4] + table = SimpleTable([{'a': 'value'}]) + root = parse(table.as_html(request)) + # return classes of an element as a set + classes = lambda x: set(x.attrib['class'].split()) + assert 'orderable' in classes(root.findall('.//thead/tr/th')[0]) + assert 'orderable' not in classes(root.findall('.//thead/tr/th')[1]) + + # Now try with an ordered table + table = SimpleTable([], order_by='a') + root = parse(table.as_html(request)) + # return classes of an element as a set + assert 'orderable' in classes(root.findall('.//thead/tr/th')[0]) + assert 'asc' in classes(root.findall('.//thead/tr/th')[0]) + assert 'orderable' not in classes(root.findall('.//thead/tr/th')[1]) + + def test_empty_values_triggers_default(self): + class Table(tables.Table): + a = tables.Column(empty_values=(1, 2), default='--') + table = Table([{'a': 1}, {'a': 2}, {'a': 3}, {'a': 4}]) + assert [row.get_cell('a') for row in table.rows] == ['--', '--', 3, 4] -def test_register_skips_non_columns(): - from django_tables2.columns.base import library + def test_register_skips_non_columns(self): + from django_tables2.columns.base import library - @library.register - class Klass(object): - pass + @library.register + class Klass(object): + pass - class Table(tables.Table): - class Meta: - model = Person + class Table(tables.Table): + class Meta: + model = Person - Table([]) + Table([]) + def test_raises_when_using_non_supported_index(self): + class Table(tables.Table): + column = tables.Column() -def test_raises_when_using_non_supported_index(): - class Table(tables.Table): - column = tables.Column() + table = Table([{'column': 'foo'}]) - table = Table([{'column': 'foo'}]) + row = table.rows[0] + with self.assertRaises(TypeError): + row[table] - row = table.rows[0] - with pytest.raises(TypeError): - row[table] + def test_related_fields_get_correct_type(self): + ''' + Types of related fields should also lead to the correct type of column. + ''' + class PersonTable(tables.Table): + class Meta: + model = Person + fields = ['first_name', 'occupation.boolean'] + + table = PersonTable([]) + self.assertEqual( + [type(column).__name__ for column in table.base_columns.values()], + ['Column', 'BooleanColumn'] + ) class MyModel(models.Model): @@ -353,91 +350,188 @@ fields = ('item1', ) -def test_column_params_should_be_preserved_under_inheritance(): - ''' - Github issue #337 - - Columns explicitly defined on MyTable get overridden by columns implicitly - defined on it's child. - If the column is not redefined, the explicit definition of MyTable is used, - preserving the specialized verbose_name defined on it. - ''' - - class MyTableA(MyTable): +class ColumnInheritanceTest(TestCase): + def test_column_params_should_be_preserved_under_inheritance(self): ''' - having an empty `class Meta` should not undo the explicit definition - of column item1 in MyTable. - ''' - class Meta(MyTable.Meta): - pass + Github issue #337 - class MyTableB(MyTable): + Columns explicitly defined on MyTable get overridden by columns implicitly + defined on it's child. + If the column is not redefined, the explicit definition of MyTable is used, + preserving the specialized verbose_name defined on it. ''' - having a non-empty `class Meta` should not undo the explicit definition - of column item1 in MyTable. - ''' - class Meta(MyTable.Meta): - per_page = 22 - table = MyTable(MyModel.objects.all()) - tableA = MyTableA(MyModel.objects.all()) - tableB = MyTableB(MyModel.objects.all()) + class MyTableA(MyTable): + ''' + having an empty `class Meta` should not undo the explicit definition + of column item1 in MyTable. + ''' + class Meta(MyTable.Meta): + pass + + class MyTableB(MyTable): + ''' + having a non-empty `class Meta` should not undo the explicit definition + of column item1 in MyTable. + ''' + class Meta(MyTable.Meta): + per_page = 22 + + table = MyTable(MyModel.objects.all()) + tableA = MyTableA(MyModel.objects.all()) + tableB = MyTableB(MyModel.objects.all()) + + assert table.columns['item1'].verbose_name == 'Nice column name' + assert tableA.columns['item1'].verbose_name == 'Nice column name' + assert tableB.columns['item1'].verbose_name == 'Nice column name' + + def test_explicit_column_can_be_overridden_by_other_explicit_column(self): + class MyTableC(MyTable): + ''' + If we define a new explict item1 column, that one should be used. + ''' + item1 = tables.Column(verbose_name='New nice column name') - assert table.columns['item1'].verbose_name == 'Nice column name' - assert tableA.columns['item1'].verbose_name == 'Nice column name' - assert tableB.columns['item1'].verbose_name == 'Nice column name' + table = MyTable(MyModel.objects.all()) + tableC = MyTableC(MyModel.objects.all()) + assert table.columns['item1'].verbose_name == 'Nice column name' + assert tableC.columns['item1'].verbose_name == 'New nice column name' -def test_explicit_column_can_be_overridden_by_other_explicit_column(): - class MyTableC(MyTable): + def test_override_column_class_names(self): ''' - If we define a new explict item1 column, that one should be used. + We control the output of CSS class names for a column by overriding + get_column_class_names ''' - item1 = tables.Column(verbose_name='New nice column name') - - table = MyTable(MyModel.objects.all()) - tableC = MyTableC(MyModel.objects.all()) + class MyTable(tables.Table): + population = tables.Column(verbose_name='Population') - assert table.columns['item1'].verbose_name == 'Nice column name' - assert tableC.columns['item1'].verbose_name == 'New nice column name' - - -def test_override_column_class_names(): - ''' - We control the output of CSS class names for a column by overriding - get_column_class_names - ''' - class MyTable(tables.Table): - population = tables.Column(verbose_name='Population') - - def get_column_class_names(self, classes_set, bound_column): - classes_set.add('prefix-%s' % bound_column.name) - return classes_set + def get_column_class_names(self, classes_set, bound_column): + classes_set.add('prefix-%s' % bound_column.name) + return classes_set + + TEST_DATA = [ + {'name': 'Belgium', 'population': 11200000}, + {'name': 'Luxembourgh', 'population': 540000}, + {'name': 'France', 'population': 66000000}, + ] + + html = MyTable(TEST_DATA).as_html(build_request()) + + assert '11200000' in html + + +class ColumnAttrsTest(TestCase): + def setUp(self): + Person.objects.create(first_name='Jan', last_name='Pietersz.') + Person.objects.create(first_name='Sjon', last_name='Jansen') + + def test_computable_td_attrs(self): + '''Computable attrs for columns, using table argument''' + class Table(tables.Table): + person = tables.Column(attrs={ + 'cell': { + 'data-length': lambda table: len(table.data) + } + }) + first_name = tables.Column(attrs={ + 'td': { + 'class': lambda table: 'status-{}'.format(len(table.data)) + } + }) + + table = Table(Person.objects.all()) + html = table.as_html(request) + # cell should affect both and + assert '' in html + assert '' in html + # td should only affect + assert '' in html + + def test_computable_td_attrs_defined_in_column_class_attribute(self): + '''Computable attrs for columns, using custom Column''' + + class MyColumn(tables.Column): + attrs = { + 'td': { + 'data-test': lambda table: len(table.data) + } + } - TEST_DATA = [ - {'name': 'Belgium', 'population': 11200000}, - {'name': 'Luxembourgh', 'population': 540000}, - {'name': 'France', 'population': 66000000}, - ] + class Table(tables.Table): + last_name = MyColumn() - html = MyTable(TEST_DATA).as_html(build_request()) + table = Table(Person.objects.all()) + html = table.as_html(request) + root = parse(html) + + assert root.findall('.//tbody/tr/td')[0].attrib == { + 'data-test': '2', + 'class': 'last_name' + } + assert root.findall('.//tbody/tr/td')[1].attrib == { + 'data-test': '2', + 'class': 'last_name' + } + + def test_computable_td_attrs_defined_in_column_class_attribute_record(self): + '''Computable attrs for columns, using custom column''' + + class PersonColumn(tables.Column): + attrs = { + 'td': { + 'data-first-name': lambda record: record.first_name, + 'data-last-name': lambda record: record.last_name, + } + } - assert '11200000' in html + def render(self, record): + return '{} {}'.format(record.first_name, record.last_name) + class Table(tables.Table): + person = PersonColumn(empty_values=()) -@pytest.mark.django_db -def test_computable_td_attrs(): - '''Computable attrs for columns, using table argument''' - Person.objects.create(first_name='Jan', last_name='Pietersz.') - Person.objects.create(first_name='Sjon', last_name='Jansen') + table = Table(Person.objects.all()) + html = table.as_html(request) + root = parse(html) + + assert root.findall('.//tbody/tr/td')[0].attrib == { + 'data-first-name': 'Jan', + 'data-last-name': 'Pietersz.', + 'class': 'person' + } - class Table(tables.Table): - person = tables.Column(attrs={ - 'cell': { - 'data-length': lambda table: len(table.data) - } - }) + def test_computable_column_td_attrs_record_header(self): + ''' + Computable attrs for columns, using custom column with a callable containing + a catch-all argument. + ''' - table = Table(Person.objects.all()) - html = table.as_html(request) - assert 'data-length="2"' in html + def data_first_name(**kwargs): + record = kwargs.get('record', None) + return 'header' if not record else record.first_name + + class Table(tables.Table): + first_name = tables.Column(attrs={ + 'cell': { + 'data-first-name': data_first_name, + 'class': lambda value: 'status-{}'.format(value) + } + }) + + table = Table(Person.objects.all()) + html = table.as_html(request) + root = parse(html) + + assert root.findall('.//thead/tr/th')[0].attrib == { + 'class': 'first_name orderable', + 'data-first-name': 'header', + } + assert root.findall('.//tbody/tr/td')[0].attrib == { + 'class': 'first_name status-Jan', + 'data-first-name': 'Jan', + } + assert root.findall('.//tbody/tr/td')[1].attrib == { + 'class': 'first_name status-Sjon', + 'data-first-name': 'Sjon', + } diff -Nru django-tables-1.14.2/tests/columns/test_jsoncolumn.py django-tables-1.21.2/tests/columns/test_jsoncolumn.py --- django-tables-1.14.2/tests/columns/test_jsoncolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_jsoncolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,56 +1,46 @@ # coding: utf-8 from __future__ import unicode_literals -import pytest +from django.contrib.postgres.fields import HStoreField, JSONField from django.db import models +from django.test import SimpleTestCase import django_tables2 as tables -try: - from django.contrib.postgres.fields import HStoreField, JSONField - JSONFIELD_AVAILABLE = True -except ImportError: - # fields are introduced in django == 1.9 - # remove shim wen we drop support for django 1.8 - JSONFIELD_AVAILABLE = False - -@pytest.mark.skipif(not JSONFIELD_AVAILABLE, reason='JSONField added in django 1.9') -def test_should_be_used_for_json_and_hstore_fields(): - class Model(models.Model): - json = JSONField() - hstore = HStoreField() - - class Meta: - app_label = 'django_tables2_test' - - class Table(tables.Table): - class Meta: - model = Model - - assert isinstance(Table.base_columns['json'], tables.JSONColumn) - assert isinstance(Table.base_columns['hstore'], tables.JSONColumn) - - -def test_jsoncolumn_dict(): - column = tables.JSONColumn() - - record = {'json': {'species': 'Falcon'}} - html = column.render(value=record['json'], record=record) - assert html == '
{\n  "species": "Falcon"\n}
' - - -def test_jsoncolumn_string(): - column = tables.JSONColumn() - - record = {'json': "really?"} - html = column.render(value=record['json'], record=record) - assert html == '
"really?"
' - - -def test_jsoncolumn_number(): - column = tables.JSONColumn() - - record = {'json': 3.14} - html = column.render(value=record['json'], record=record) - assert html == '
3.14
' +class JsonColumnTestCase(SimpleTestCase): + def test_should_be_used_for_json_and_hstore_fields(self): + class Model(models.Model): + json = JSONField() + hstore = HStoreField() + + class Meta: + app_label = 'django_tables2_test' + + class Table(tables.Table): + class Meta: + model = Model + + assert isinstance(Table.base_columns['json'], tables.JSONColumn) + assert isinstance(Table.base_columns['hstore'], tables.JSONColumn) + + def test_jsoncolumn_dict(self): + column = tables.JSONColumn() + + record = {'json': {'species': 'Falcon'}} + html = column.render(value=record['json'], record=record) + assert html == '
{\n  "species": "Falcon"\n}
' + + def test_jsoncolumn_string(self): + column = tables.JSONColumn() + + record = {'json': "really?"} + html = column.render(value=record['json'], record=record) + assert html == '
"really?"
' + + def test_jsoncolumn_number(self): + column = tables.JSONColumn() + + record = {'json': 3.14} + html = column.render(value=record['json'], record=record) + assert html == '
3.14
' diff -Nru django-tables-1.14.2/tests/columns/test_linkcolumn.py django-tables-1.21.2/tests/columns/test_linkcolumn.py --- django-tables-1.14.2/tests/columns/test_linkcolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_linkcolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,8 +1,9 @@ # coding: utf-8 from __future__ import unicode_literals -import pytest from django.template import Context, Template +from django.test import TestCase +from django.urls import reverse from django.utils.html import mark_safe import django_tables2 as tables @@ -11,224 +12,201 @@ from ..app.models import Occupation, Person from ..utils import attrs, build_request -try: - from django.urls import reverse -except ImportError: - # to keep backward (Django <= 1.9) compatibility - from django.core.urlresolvers import reverse - - -def test_unicode(): - '''Test LinkColumn for unicode values + headings''' - class UnicodeTable(tables.Table): - first_name = tables.LinkColumn('person', args=[A('pk')]) - last_name = tables.LinkColumn('person', args=[A('pk')], verbose_name='äÚ¨´ˆÁ˜¨ˆ˜˘Ú…Ò˚ˆπ∆ˆ´') - - dataset = [ - {'pk': 1, 'first_name': 'Brädley', 'last_name': '∆yers'}, - {'pk': 2, 'first_name': 'Chr…s', 'last_name': 'DÒble'}, - ] - - template = Template('{% load django_tables2 %}{% render_table table %}') - html = template.render(Context({ - 'request': build_request(), - 'table': UnicodeTable(dataset) - })) - - assert 'Brädley' in html - assert '∆yers' in html - assert 'Chr…s' in html - assert 'DÒble' in html - - -def test_link_text_custom_value(): - class CustomLinkTable(tables.Table): - first_name = tables.LinkColumn('person', text='foo::bar', args=[A('pk')]) - last_name = tables.LinkColumn( - 'person', - text=lambda row: '%s %s' % (row['last_name'], row['first_name']), - args=[A('pk')] - ) - - dataset = [ - {'pk': 1, 'first_name': 'John', 'last_name': 'Doe'} - ] - - html = CustomLinkTable(dataset).as_html(build_request()) - - assert 'foo::bar' in html - assert 'Doe John' in html - - -def test_link_text_escaping(): - class CustomLinkTable(tables.Table): - editlink = tables.LinkColumn( - 'person', - text=mark_safe('edit'), - args=[A('pk')] - ) - - dataset = [ - {'pk': 1, 'first_name': 'John', 'last_name': 'Doe'} - ] - - html = CustomLinkTable(dataset).as_html(build_request()) - - expected = 'edit'.format( - reverse('person', args=(1, )) - ) - assert expected in html - - -@pytest.mark.django_db -def test_null_foreign_key(): - class PersonTable(tables.Table): - first_name = tables.Column() - last_name = tables.Column() - occupation = tables.LinkColumn('occupation', args=[A('occupation.pk')]) - - Person.objects.create(first_name='bradley', last_name='ayers') - - table = PersonTable(Person.objects.all()) - html = table.as_html(build_request()) - - assert '—' in html - -@pytest.mark.django_db -def test_linkcolumn_non_field_based(): - '''Test for issue 257, non-field based columns''' - class Table(tables.Table): - first_name = tables.Column() - delete_link = tables.LinkColumn('person_delete', text='delete', kwargs={'pk': tables.A('id')}) +class LinkColumnTest(TestCase): + def test_unicode(self): + '''Test LinkColumn for unicode values + headings''' + class UnicodeTable(tables.Table): + first_name = tables.LinkColumn('person', args=[A('pk')]) + last_name = tables.LinkColumn('person', args=[A('pk')], verbose_name='äÚ¨´ˆÁ˜¨ˆ˜˘Ú…Ò˚ˆπ∆ˆ´') + + dataset = [ + {'pk': 1, 'first_name': 'Brädley', 'last_name': '∆yers'}, + {'pk': 2, 'first_name': 'Chr…s', 'last_name': 'DÒble'}, + ] + + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({ + 'request': build_request(), + 'table': UnicodeTable(dataset) + })) + + assert 'Brädley' in html + assert '∆yers' in html + assert 'Chr…s' in html + assert 'DÒble' in html + + def test_link_text_custom_value(self): + class CustomLinkTable(tables.Table): + first_name = tables.LinkColumn('person', text='foo::bar', args=[A('pk')]) + last_name = tables.LinkColumn( + 'person', + text=lambda row: '%s %s' % (row['last_name'], row['first_name']), + args=[A('pk')] + ) + + dataset = [ + {'pk': 1, 'first_name': 'John', 'last_name': 'Doe'} + ] + + html = CustomLinkTable(dataset).as_html(build_request()) + + assert 'foo::bar' in html + assert 'Doe John' in html + + def test_link_text_escaping(self): + class CustomLinkTable(tables.Table): + editlink = tables.LinkColumn( + 'person', + text=mark_safe('edit'), + args=[A('pk')] + ) + + dataset = [ + {'pk': 1, 'first_name': 'John', 'last_name': 'Doe'} + ] - willem = Person.objects.create(first_name='Willem', last_name='Wever') - - html = Table(Person.objects.all()).as_html(build_request()) - - expected = 'delete'.format( - reverse('person_delete', kwargs={'pk': willem.pk}) - ) - assert expected in html - - -def test_kwargs(): - class PersonTable(tables.Table): - a = tables.LinkColumn('occupation', kwargs={'pk': A('a')}) - - table = PersonTable([{'a': 0}, {'a': 1}]) - - assert reverse('occupation', kwargs={'pk': 0}) in table.rows[0].get_cell('a') - assert reverse('occupation', kwargs={'pk': 1}) in table.rows[1].get_cell('a') - - -def test_html_escape_value(): - class PersonTable(tables.Table): - name = tables.LinkColumn('escaping', kwargs={'pk': A('pk')}) - - table = PersonTable([{'name': '', 'pk': 1}]) - assert table.rows[0].get_cell('name') == '<brad>' + html = CustomLinkTable(dataset).as_html(build_request()) + expected = 'edit'.format( + reverse('person', args=(1, )) + ) + assert expected in html -def test_a_attrs_should_be_supported(): - class TestTable(tables.Table): - col = tables.LinkColumn('occupation', kwargs={'pk': A('col')}, - attrs={'a': {'title': 'Occupation Title'}}) + def test_null_foreign_key(self): + class PersonTable(tables.Table): + first_name = tables.Column() + last_name = tables.Column() + occupation = tables.LinkColumn('occupation', args=[A('occupation.pk')]) - table = TestTable([{'col': 0}]) - assert attrs(table.rows[0].get_cell('col')) == { - 'href': reverse('occupation', kwargs={'pk': 0}), - 'title': 'Occupation Title' - } + Person.objects.create(first_name='bradley', last_name='ayers') + table = PersonTable(Person.objects.all()) + html = table.as_html(build_request()) -@pytest.mark.django_db -def test_td_attrs_should_be_supported(): - '''LinkColumn should support both and attrs''' + assert '—' in html - person = Person.objects.create(first_name='Bob', last_name='Builder') + def test_linkcolumn_non_field_based(self): + '''Test for issue 257, non-field based columns''' + class Table(tables.Table): + first_name = tables.Column() + delete_link = tables.LinkColumn('person_delete', text='delete', kwargs={'pk': tables.A('id')}) - class Table(tables.Table): - first_name = tables.LinkColumn(attrs={ - 'a': {'style': 'color: red;'}, - 'td': {'style': 'background-color: #ddd;'} - }) - last_name = tables.Column() + willem = Person.objects.create(first_name='Willem', last_name='Wever') - table = Table(Person.objects.all()) + html = Table(Person.objects.all()).as_html(build_request()) - a_tag = table.rows[0].get_cell('first_name') - assert 'href="{}"'.format(reverse('person', args=(person.pk, ))) in a_tag - assert 'style="color: red;"' in a_tag - assert person.first_name in a_tag + expected = 'delete'.format( + reverse('person_delete', kwargs={'pk': willem.pk}) + ) + assert expected in html - html = table.as_html(build_request()) + def test_kwargs(self): + class PersonTable(tables.Table): + a = tables.LinkColumn('occupation', kwargs={'pk': A('a')}) - td_tag_1 = '' - td_tag_2 = '' + table = PersonTable([{'a': 0}, {'a': 1}]) - assert td_tag_1 in html or td_tag_2 in html + assert reverse('occupation', kwargs={'pk': 0}) in table.rows[0].get_cell('a') + assert reverse('occupation', kwargs={'pk': 1}) in table.rows[1].get_cell('a') + def test_html_escape_value(self): + class PersonTable(tables.Table): + name = tables.LinkColumn('escaping', kwargs={'pk': A('pk')}) -def test_defaults(): - class Table(tables.Table): - link = tables.LinkColumn('occupation', kwargs={'pk': 1}, default='xyz') + table = PersonTable([{'name': '', 'pk': 1}]) + assert table.rows[0].get_cell('name') == '<brad>' - table = Table([{}]) - assert table.rows[0].get_cell('link') == 'xyz' + def test_a_attrs_should_be_supported(self): + class TestTable(tables.Table): + col = tables.LinkColumn('occupation', kwargs={'pk': A('col')}, + attrs={'a': {'title': 'Occupation Title'}}) + table = TestTable([{'col': 0}]) + assert attrs(table.rows[0].get_cell('col')) == { + 'href': reverse('occupation', kwargs={'pk': 0}), + 'title': 'Occupation Title' + } -@pytest.mark.django_db -def test_get_absolute_url(): - class PersonTable(tables.Table): - first_name = tables.Column() - last_name = tables.LinkColumn() + def test_td_attrs_should_be_supported(self): + '''LinkColumn should support both and attrs''' - person = Person.objects.create(first_name='Jan Pieter', last_name='Waagmeester', ) - table = PersonTable(Person.objects.all()) + person = Person.objects.create(first_name='Bob', last_name='Builder') - expected = '{}'.format( - reverse('person', args=(person.pk, )), - person.last_name - ) - assert table.rows[0].get_cell('last_name') == expected + class Table(tables.Table): + first_name = tables.LinkColumn(attrs={ + 'a': {'style': 'color: red;'}, + 'td': {'style': 'background-color: #ddd;'} + }) + last_name = tables.Column() + table = Table(Person.objects.all()) -def test_get_absolute_url_not_defined(): - ''' - The dict doesn't have a get_absolute_url(), so creating the table should - raise a TypeError - ''' - class Table(tables.Table): - first_name = tables.Column() - last_name = tables.LinkColumn() + a_tag = table.rows[0].get_cell('first_name') + assert 'href="{}"'.format(reverse('person', args=(person.pk, ))) in a_tag + assert 'style="color: red;"' in a_tag + assert person.first_name in a_tag - table = Table([ - dict(first_name='Jan Pieter', last_name='Waagmeester') - ]) + html = table.as_html(build_request()) - with pytest.raises(TypeError): - table.as_html(build_request()) + td_tag_1 = '' + td_tag_2 = '' + assert td_tag_1 in html or td_tag_2 in html -@pytest.mark.django_db -def test_RelatedLinkColumn(): - carpenter = Occupation.objects.create(name='Carpenter') - Person.objects.create(first_name='Bob', last_name='Builder', occupation=carpenter) + def test_defaults(self): + class Table(tables.Table): + link = tables.LinkColumn('occupation', kwargs={'pk': 1}, default='xyz') - class Table(tables.Table): - first_name = tables.LinkColumn() - last_name = tables.Column() - occupation = tables.RelatedLinkColumn() + table = Table([{}]) + assert table.rows[0].get_cell('link') == 'xyz' - table = Table(Person.objects.all()) + def test_get_absolute_url(self): + class PersonTable(tables.Table): + first_name = tables.Column() + last_name = tables.LinkColumn() - assert table.rows[0].get_cell('occupation') == 'Carpenter' % carpenter.pk + person = Person.objects.create(first_name='Jan Pieter', last_name='Waagmeester', ) + table = PersonTable(Person.objects.all()) + expected = '{}'.format( + reverse('person', args=(person.pk, )), + person.last_name + ) + assert table.rows[0].get_cell('last_name') == expected -def test_value_returns_a_raw_value_without_html(): - class Table(tables.Table): - col = tables.LinkColumn('occupation', args=(A('id'), )) + def test_get_absolute_url_not_defined(self): + ''' + The dict doesn't have a get_absolute_url(), so creating the table should + raise a TypeError + ''' + class Table(tables.Table): + first_name = tables.Column() + last_name = tables.LinkColumn() + + table = Table([ + dict(first_name='Jan Pieter', last_name='Waagmeester') + ]) + + with self.assertRaises(TypeError): + table.as_html(build_request()) + + def test_RelatedLinkColumn(self): + carpenter = Occupation.objects.create(name='Carpenter') + Person.objects.create(first_name='Bob', last_name='Builder', occupation=carpenter) + + class Table(tables.Table): + first_name = tables.LinkColumn() + last_name = tables.Column() + occupation = tables.RelatedLinkColumn() + + table = Table(Person.objects.all()) + + assert table.rows[0].get_cell('occupation') == 'Carpenter' % carpenter.pk + + def test_value_returns_a_raw_value_without_html(self): + class Table(tables.Table): + col = tables.LinkColumn('occupation', args=(A('id'), )) - table = Table([{'col': 'link-text', 'id': 1}]) - assert table.rows[0].get_cell_value('col') == 'link-text' + table = Table([{'col': 'link-text', 'id': 1}]) + assert table.rows[0].get_cell_value('col') == 'link-text' diff -Nru django-tables-1.14.2/tests/columns/test_manytomanycolumn.py django-tables-1.21.2/tests/columns/test_manytomanycolumn.py --- django-tables-1.14.2/tests/columns/test_manytomanycolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_manytomanycolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -3,122 +3,110 @@ from random import randint, sample -import pytest +from django.test import TestCase from django.utils.html import format_html, mark_safe, strip_tags import django_tables2 as tables from tests.app.models import Person -pytestmark = pytest.mark.django_db -FAKE_NAMES = ( - ('Kyle', 'Strader'), - ('Francis', 'Fisher'), - ('James', 'Jury'), - ('Florentina', 'Floyd'), - ('Mark', 'Boyd'), - ('Simone', 'Fong'), -) +class ManyToManyColumnTest(TestCase): + FAKE_NAMES = ( + ('Kyle', 'Strader'), + ('Francis', 'Fisher'), + ('James', 'Jury'), + ('Florentina', 'Floyd'), + ('Mark', 'Boyd'), + ('Simone', 'Fong'), + ) + + def setUp(self): + for first, last in self.FAKE_NAMES: + Person.objects.create(first_name=first, last_name=last) + + persons = list(Person.objects.all()) + + # give everyone 1 to 3 friends + for person in persons: + person.friends.add(*sample(persons, randint(1, 3))) + person.save() + + def test_ManyToManyColumn_from_model(self): + ''' + Automatically uses the ManyToManyColumn for a ManyToManyField, and calls the + Models's `__str__` method to transform the model instace to string. + ''' + class Table(tables.Table): + name = tables.Column(accessor='name', order_by=('last_name', 'first_name')) -def create_Persons(): - for first, last in FAKE_NAMES: - Person.objects.create(first_name=first, last_name=last) - - persons = list(Person.objects.all()) - - # give everyone 1 to 3 friends - for person in persons: - person.friends.add(*sample(persons, randint(1, 3))) - person.save() - - -def test_ManyToManyColumn_from_model(): - ''' - Automaticcally uses the ManyToManyColumn for a ManyToManyField, and calls the - Models's `__str__` method to transform the model instace to string. - ''' - create_Persons() - - class Table(tables.Table): - name = tables.Column(accessor='name', order_by=('last_name', 'first_name')) - - class Meta: - model = Person - fields = ('name', 'friends') - - table = Table(Person.objects.all()) + class Meta: + model = Person + fields = ('name', 'friends') - for row in table.rows: - friends = row.get_cell('friends').split(', ') + table = Table(Person.objects.all()) - for friend in friends: - assert Person.objects.filter(first_name=friend).exists() + for row in table.rows: + friends = row.get_cell('friends').split(', ') + for friend in friends: + assert Person.objects.filter(first_name=friend).exists() -def test_custom_separator(): - create_Persons() + def test_custom_separator(self): + def assert_sep(sep): + class Table(tables.Table): + friends = tables.ManyToManyColumn(separator=sep) + + table = Table(Person.objects.all().order_by('last_name')) + for row in table.rows: + friends = row.get_cell('friends').split(sep) + + for friend in friends: + assert Person.objects.filter(first_name=friend).exists() + + # normal string, will not be escaped + assert_sep('|') + + # html tag, would normally be escaped, but should not be escaped because + # it is mark_safe()'ed + assert_sep(mark_safe('
')) - def assert_sep(sep): + def test_transform_returns_html(self): class Table(tables.Table): - friends = tables.ManyToManyColumn(separator=sep) + friends = tables.ManyToManyColumn(transform=lambda m: format_html('{}', m.first_name)) table = Table(Person.objects.all().order_by('last_name')) for row in table.rows: - friends = row.get_cell('friends').split(sep) - + friends = row.get_cell('friends').split(', ') for friend in friends: - assert Person.objects.filter(first_name=friend).exists() - - # normal string, will not be escaped - assert_sep('|') - - # html tag, would normally be escaped, but should not be escaped because - # it is mark_safe()'ed - assert_sep(mark_safe('
')) - - -def test_transform_returns_html(): - create_Persons() - - class Table(tables.Table): - friends = tables.ManyToManyColumn(transform=lambda m: format_html('{}', m.first_name)) - - table = Table(Person.objects.all().order_by('last_name')) - for row in table.rows: - friends = row.get_cell('friends').split(', ') - for friend in friends: - stripped = strip_tags(friend) - assert Person.objects.filter(first_name=stripped).exists() - - -def test_orderable_is_false(): - class Table(tables.Table): - friends = tables.ManyToManyColumn(orderable=False) - - Table([]) + stripped = strip_tags(friend) + assert Person.objects.filter(first_name=stripped).exists() + def test_orderable_is_false(self): + class Table(tables.Table): + friends = tables.ManyToManyColumn(orderable=False) -def test_ManyToManyColumn_complete_example(): - create_Persons() + Table([]) + # TODO: assertions - # add a friendless person - remi = Person.objects.create(first_name='Remi', last_name='Barberin') + def test_ManyToManyColumn_complete_example(self): + # add a friendless person + remi = Person.objects.create(first_name='Remi', last_name='Barberin') - class Table(tables.Table): - name = tables.Column(accessor='name', order_by=('last_name', 'first_name')) - friends = tables.ManyToManyColumn( - transform=lambda o: o.name, - filter=lambda o: o.order_by('-last_name') - ) + class Table(tables.Table): + name = tables.Column(accessor='name', order_by=('last_name', 'first_name')) + friends = tables.ManyToManyColumn( + transform=lambda o: o.name, + filter=lambda o: o.order_by('-last_name') + ) - table = Table(Person.objects.all().order_by('last_name')) - for row in table.rows: - friends = row.get_cell('friends') - if friends == '-': - assert row.get_cell('name') == remi.name - continue + table = Table(Person.objects.all().order_by('last_name')) + for row in table.rows: + friends = row.get_cell('friends') + if friends == '-': + assert row.get_cell('name') == remi.name + continue - friends = list(map(lambda o: o.split(' '), friends.split(', '))) + friends = list(map(lambda o: o.split(' '), friends.split(', '))) - assert friends == sorted(friends, key=lambda item: item[1], reverse=True) + assert friends == sorted(friends, key=lambda item: item[1], reverse=True) diff -Nru django-tables-1.14.2/tests/columns/test_templatecolumn.py django-tables-1.21.2/tests/columns/test_templatecolumn.py --- django-tables-1.14.2/tests/columns/test_templatecolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_templatecolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,108 +1,106 @@ # coding: utf-8 from __future__ import unicode_literals -import pytest from django.template import Context, Template +from django.test import SimpleTestCase import django_tables2 as tables from ..utils import build_request -def test_should_render_in_pinned_row(): - class TestOnlyPinnedTable(tables.Table): - foo = tables.TemplateColumn('value={{ value }}') +class TemplateColumnTest(SimpleTestCase): + def test_should_render_in_pinned_row(self): + class TestOnlyPinnedTable(tables.Table): + foo = tables.TemplateColumn('value={{ value }}') + + def __init__(self, data): + self.pinned = data + revised_data = [] + + super(TestOnlyPinnedTable, self).__init__(revised_data) + + def get_top_pinned_data(self): + return self.pinned + + table = TestOnlyPinnedTable([{'foo': 'bar'}]) + for row in table.rows: + assert row.get_cell('foo') == 'value=bar' + + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({'request': build_request(), 'table': table})) + + assert 'value=bar' in html + + def test_should_handle_context_on_table(self): + class TestTable(tables.Table): + col_code = tables.TemplateColumn(template_code='code:{{ record.col }}-{{ foo }}') + col_name = tables.TemplateColumn(template_name='test_template_column.html') + col_context = tables.TemplateColumn(template_code="{{ label }}:{{ record.col }}-{{ foo }}", + extra_context={'label': 'label'}) + + table = TestTable([{'col': 'brad'}]) + assert table.rows[0].get_cell('col_code') == 'code:brad-' + assert table.rows[0].get_cell('col_name') == 'name:brad-empty\n' + assert table.rows[0].get_cell("col_context") == "label:brad-" + + table.context = Context({'foo': 'author'}) + assert table.rows[0].get_cell('col_code') == 'code:brad-author' + assert table.rows[0].get_cell('col_name') == 'name:brad-author\n' + assert table.rows[0].get_cell("col_context") == "label:brad-author" + + # new table and render using the 'render_table' template tag. + table = TestTable([{'col': 'brad'}]) + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({'request': build_request(), 'table': table, 'foo': 'author'})) - def __init__(self, data): - self.pinned = data - revised_data = [] + assert 'name:brad-author\n' in html - super(TestOnlyPinnedTable, self).__init__(revised_data) - - def get_top_pinned_data(self): - return self.pinned - - table = TestOnlyPinnedTable([{'foo': 'bar'}]) - for row in table.rows: - assert row.get_cell('foo') == 'value=bar' - - template = Template('{% load django_tables2 %}{% render_table table %}') - html = template.render(Context({'request': build_request(), 'table': table})) - - assert 'value=bar' in html - - -def test_should_handle_context_on_table(): - class TestTable(tables.Table): - col_code = tables.TemplateColumn(template_code='code:{{ record.col }}-{{ foo }}') - col_name = tables.TemplateColumn(template_name='test_template_column.html') - - table = TestTable([{'col': 'brad'}]) - assert table.rows[0].get_cell('col_code') == 'code:brad-' - assert table.rows[0].get_cell('col_name') == 'name:brad-empty\n' - - table.context = Context({'foo': 'author'}) - assert table.rows[0].get_cell('col_code') == 'code:brad-author' - assert table.rows[0].get_cell('col_name') == 'name:brad-author\n' - - # new table and render using the 'render_table' template tag. - table = TestTable([{'col': 'brad'}]) - template = Template('{% load django_tables2 %}{% render_table table %}') - html = template.render(Context({'request': build_request(), 'table': table, 'foo': 'author'})) - - assert 'name:brad-author\n' in html - - -def test_should_support_default(): - class Table(tables.Table): - foo = tables.TemplateColumn('default={{ default }}', default='bar') - - table = Table([{}]) - assert table.rows[0].get_cell('foo') == 'default=bar' - - -def test_should_support_value(): - class Table(tables.Table): - foo = tables.TemplateColumn('value={{ value }}') - - table = Table([{'foo': 'bar'}]) - assert table.rows[0].get_cell('foo') == 'value=bar' - - template = Template('{% load django_tables2 %}{% render_table table %}') - html = template.render(Context({'request': build_request(), 'table': table})) + def test_should_support_default(self): + class Table(tables.Table): + foo = tables.TemplateColumn('default={{ default }}', default='bar') - assert 'value=bar' in html + table = Table([{}]) + assert table.rows[0].get_cell('foo') == 'default=bar' + def test_should_support_value(self): + class Table(tables.Table): + foo = tables.TemplateColumn('value={{ value }}') -def test_should_support_column(): - class Table(tables.Table): - tcol = tables.TemplateColumn('column={{ column.name }}') + table = Table([{'foo': 'bar'}]) + assert table.rows[0].get_cell('foo') == 'value=bar' - table = Table([{'foo': 'bar'}]) - assert table.rows[0].get_cell('tcol') == 'column=tcol' + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({'request': build_request(), 'table': table})) + assert 'value=bar' in html -def test_should_raise_when_called_without_template(): - with pytest.raises(ValueError): + def test_should_support_column(self): class Table(tables.Table): - col = tables.TemplateColumn() - + tcol = tables.TemplateColumn('column={{ column.name }}') -def test_should_support_value_with_curly_braces(): - ''' - https://github.com/bradleyayers/django-tables2/issues/441 - ''' - class Table(tables.Table): - track = tables.TemplateColumn('track: {{ value }}') + table = Table([{'foo': 'bar'}]) + assert table.rows[0].get_cell('tcol') == 'column=tcol' - table = Table([{'track': 'Beat it {Freestyle}'}]) - assert table.rows[0].get_cell('track') == 'track: Beat it {Freestyle}' + def test_should_raise_when_called_without_template(self): + with self.assertRaises(ValueError): + class Table(tables.Table): + col = tables.TemplateColumn() + + def test_should_support_value_with_curly_braces(self): + ''' + https://github.com/bradleyayers/django-tables2/issues/441 + ''' + class Table(tables.Table): + track = tables.TemplateColumn('track: {{ value }}') + table = Table([{'track': 'Beat it {Freestyle}'}]) + assert table.rows[0].get_cell('track') == 'track: Beat it {Freestyle}' -def test_should_strip_tags_for_value(): - class Table(tables.Table): - track = tables.TemplateColumn('{{ value }}') + def test_should_strip_tags_for_value(self): + class Table(tables.Table): + track = tables.TemplateColumn('{{ value }}') - table = Table([{'track': 'Space Oddity'}]) + table = Table([{'track': 'Space Oddity'}]) - assert list(table.as_values()) == [['Track'], ['Space Oddity']] + assert list(table.as_values()) == [['Track'], ['Space Oddity']] diff -Nru django-tables-1.14.2/tests/columns/test_timecolumn.py django-tables-1.21.2/tests/columns/test_timecolumn.py --- django-tables-1.14.2/tests/columns/test_timecolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_timecolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -4,45 +4,44 @@ from datetime import time from django.db import models +from django.test import SimpleTestCase import django_tables2 as tables -''' -Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date -''' +class TimeColumnTest(SimpleTestCase): + ''' + Format string for TimeColumn: + https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date + ''' + def test_should_handle_explicit_format(self): + class TestTable(tables.Table): + time = tables.TimeColumn(format='H:i:s') + + class Meta: + default = '—' + + table = TestTable([{'time': time(11, 11, 11)}, + {'time': None}]) + assert table.rows[0].get_cell('time') == "11:11:11" + assert table.rows[1].get_cell('time') == "—" + + def test_should_be_used_for_timefields(self): + class TimeModel(models.Model): + field = models.TimeField() + + class Meta: + app_label = 'django_tables2_test' + + class Table(tables.Table): + class Meta: + model = TimeModel + + assert type(Table.base_columns['field']) == tables.TimeColumn + + def test_value_returns_a_raw_value_without_html(self): + class Table(tables.Table): + col = tables.TimeColumn(format='H:i:s') - -def test_should_handle_explicit_format(): - class TestTable(tables.Table): - time = tables.TimeColumn(format='H:i:s') - - class Meta: - default = '—' - - table = TestTable([{'time': time(11, 11, 11)}, - {'time': None}]) - assert table.rows[0].get_cell('time') == "11:11:11" - assert table.rows[1].get_cell('time') == "—" - - -def test_should_be_used_for_timefields(): - class TimeModel(models.Model): - field = models.TimeField() - - class Meta: - app_label = 'django_tables2_test' - - class Table(tables.Table): - class Meta: - model = TimeModel - - assert type(Table.base_columns['field']) == tables.TimeColumn - - -def test_value_returns_a_raw_value_without_html(): - class Table(tables.Table): - col = tables.TimeColumn(format='H:i:s') - - table = Table([{'col': time(11, 11, 11)}]) - assert table.rows[0].get_cell_value('col') == '11:11:11' + table = Table([{'col': time(11, 11, 11)}]) + assert table.rows[0].get_cell_value('col') == '11:11:11' diff -Nru django-tables-1.14.2/tests/columns/test_urlcolumn.py django-tables-1.21.2/tests/columns/test_urlcolumn.py --- django-tables-1.14.2/tests/columns/test_urlcolumn.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/columns/test_urlcolumn.py 2018-03-26 06:43:09.000000000 +0000 @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.db import models +from django.test import SimpleTestCase import django_tables2 as tables @@ -12,53 +13,50 @@ ] -def test_should_turn_url_into_hyperlink(): - class TestTable(tables.Table): - url = tables.URLColumn() +class UrlColumnTest(SimpleTestCase): + def test_should_turn_url_into_hyperlink(self): + class TestTable(tables.Table): + url = tables.URLColumn() - table = TestTable(MEMORY_DATA) + table = TestTable(MEMORY_DATA) - assert table.rows[0].get_cell('url') == 'http://example.com' - assert table.rows[1].get_cell('url') == 'https://example.com' + assert table.rows[0].get_cell('url') == 'http://example.com' + assert table.rows[1].get_cell('url') == 'https://example.com' + def test_should_be_used_for_urlfields(self): + class URLModel(models.Model): + field = models.URLField() -def test_should_be_used_for_urlfields(): - class URLModel(models.Model): - field = models.URLField() + class Meta: + app_label = 'django_tables2_test' - class Meta: - app_label = 'django_tables2_test' + class Table(tables.Table): + class Meta: + model = URLModel - class Table(tables.Table): - class Meta: - model = URLModel + assert type(Table.base_columns['field']) == tables.URLColumn - assert type(Table.base_columns['field']) == tables.URLColumn + def test_text_can_be_overridden(self): + class Table(tables.Table): + url = tables.URLColumn(text='link') + table = Table(MEMORY_DATA) -def test_text_can_be_overridden(): - class Table(tables.Table): - url = tables.URLColumn(text='link') + assert table.rows[0].get_cell('url') == 'link' - table = Table(MEMORY_DATA) + def test_text_can_be_overridden_with_callable(self): + class Table(tables.Table): + url = tables.URLColumn(text=lambda record: record['name']) - assert table.rows[0].get_cell('url') == 'link' + table = Table(MEMORY_DATA) + assert table.rows[0].get_cell('url') == 'Example' + assert table.rows[1].get_cell('url') == 'Example (https)' -def test_text_can_be_overridden_with_callable(): - class Table(tables.Table): - url = tables.URLColumn(text=lambda record: record['name']) + def test_value_returns_a_raw_value_without_html(self): + class TestTable(tables.Table): + col = tables.URLColumn() - table = Table(MEMORY_DATA) + table = TestTable([{'col': 'http://example.com'}]) - assert table.rows[0].get_cell('url') == 'Example' - assert table.rows[1].get_cell('url') == 'Example (https)' - - -def test_value_returns_a_raw_value_without_html(): - class TestTable(tables.Table): - col = tables.URLColumn() - - table = TestTable([{'col': 'http://example.com'}]) - - assert table.rows[0].get_cell_value('col') == 'http://example.com' + assert table.rows[0].get_cell_value('col') == 'http://example.com' diff -Nru django-tables-1.14.2/tests/export/test_export.py django-tables-1.21.2/tests/export/test_export.py --- django-tables-1.14.2/tests/export/test_export.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/export/test_export.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,258 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals - -import json - -import pytest -from django.core.exceptions import ImproperlyConfigured -from django.shortcuts import render - -import django_tables2 as tables -from django_tables2.config import RequestConfig - -from ..app.models import Occupation, Person, Region -from ..test_views import DispatchHookMixin -from ..utils import build_request - -# Skip if tablib is not installed (required for debian packaging) -pytest.importorskip('tablib') - -try: - from django_tables2.export.export import TableExport - from django_tables2.export.views import ExportMixin -except ImproperlyConfigured: - pass - - -NAMES = [ - ('Yildiz', 'van der Kuil'), - ('Lindi', 'Hakvoort'), - ('Gerardo', 'Castelein'), -] - -EXPECTED_CSV = '\r\n'.join( - ('First Name,Surname', ) + tuple(','.join(name) for name in NAMES) -) + '\r\n' - -EXPECTED_JSON = list([ - {'First Name': first_name, 'Surname': last_name} - for first_name, last_name in NAMES -]) - - -def create_test_persons(): - for first_name, last_name in NAMES: - Person.objects.create(first_name=first_name, last_name=last_name) - - -class Table(tables.Table): - first_name = tables.Column() - last_name = tables.Column() - - -class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): - table_class = Table - table_pagination = {'per_page': 1} - model = Person # required for ListView - template_name = 'django_tables2/bootstrap.html' - - -@pytest.mark.django_db -def test_view_should_support_csv_export(): - create_test_persons() - - response, view = View.as_view()(build_request('/?_export=csv')) - assert response.getvalue().decode('utf8') == EXPECTED_CSV - - # should just render the normal table without the _export query - response, view = View.as_view()(build_request('/')) - html = response.render().rendered_content - - assert 'Yildiz' in html - assert 'Lindy' not in html - - -def test_exporter_should_raise_error_for_unsupported_file_type(): - table = Table([]) - - with pytest.raises(TypeError): - TableExport(table=table, export_format='exe') - - -@pytest.mark.django_db -def test_view_should_support_json_export(): - create_test_persons() - - response, view = View.as_view()(build_request('/?_export=json')) - assert json.loads(response.getvalue().decode('utf8')) == EXPECTED_JSON - - -@pytest.mark.django_db -def test_view_should_support_custom_trigger_param(): - - class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): - table_class = Table - export_trigger_param = 'export_to' - model = Person # required for ListView - - create_test_persons() - - response, view = View.as_view()(build_request('/?export_to=json')) - assert json.loads(response.getvalue().decode('utf8')) == EXPECTED_JSON - - -@pytest.mark.django_db -def test_view_should_support_custom_filename(): - - class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): - table_class = Table - export_name = 'people' - model = Person # required for ListView - - create_test_persons() - - response, view = View.as_view()(build_request('/?_export=json')) - assert response['Content-Disposition'] == 'attachment; filename="people.json"' - - -@pytest.mark.django_db -def test_function_view(): - ''' - Test the code used in the docs - ''' - create_test_persons() - - def table_view(request): - table = Table(Person.objects.all()) - RequestConfig(request).configure(table) - - export_format = request.GET.get('_export', None) - if TableExport.is_valid_format(export_format): - exporter = TableExport(export_format, table) - return exporter.response('table.{}'.format(export_format)) - - return render(request, 'django_tables2/table.html', { - 'table': table - }) - - response = table_view(build_request('/?_export=csv')) - assert response.getvalue().decode('utf8') == EXPECTED_CSV - - # must also support the normal html table. - response = table_view(build_request('/')) - html = response.content.decode('utf8') - - assert 'Yildiz' in html - assert 'Lindy' not in html - - -def create_test_occupations(): - richard = Person.objects.create(first_name='Richard', last_name='Queener') - - vlaanderen = Region.objects.create(name='Vlaanderen', mayor=richard) - Occupation.objects.create(name='Timmerman', boolean=True, region=vlaanderen) - Occupation.objects.create(name='Ecoloog', boolean=False, region=vlaanderen) - - -class OccupationTable(tables.Table): - name = tables.Column() - boolean = tables.Column() - region = tables.Column() - - -class OccupationView(DispatchHookMixin, ExportMixin, tables.SingleTableView): - table_class = OccupationTable - table_pagination = {'per_page': 1} - model = Occupation - template_name = 'django_tables2/bootstrap.html' - - -@pytest.mark.django_db -def test_exporting_should_work_with_foreign_keys(): - create_test_occupations() - - response, view = OccupationView.as_view()(build_request('/?_export=xls')) - data = response.content - # binary data, so not possible to compare to an exact expectation - assert data.find('Vlaanderen'.encode()) - assert data.find('Ecoloog'.encode()) - assert data.find('Timmerman'.encode()) - - -@pytest.mark.django_db -def test_exporting_should_work_with_foreign_key_fields(): - create_test_occupations() - - class OccupationWithForeignKeyFieldsTable(tables.Table): - name = tables.Column() - boolean = tables.Column() - region = tables.Column() - mayor = tables.Column(accessor='region.mayor.first_name') - - class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): - table_class = OccupationWithForeignKeyFieldsTable - table_pagination = {'per_page': 1} - model = Occupation - template_name = 'django_tables2/bootstrap.html' - - response, view = View.as_view()(build_request('/?_export=csv')) - data = response.getvalue().decode('utf8') - - expected_csv = '\r\n'.join(( - 'Name,Boolean,Region,First Name', - 'Timmerman,True,Vlaanderen,Richard', - 'Ecoloog,False,Vlaanderen,Richard\r\n' - )) - assert data == expected_csv - - -@pytest.mark.django_db -def test_exporting_exclude_columns(): - create_test_occupations() - - class OccupationExcludingView(DispatchHookMixin, ExportMixin, tables.SingleTableView): - table_class = OccupationTable - table_pagination = {'per_page': 1} - model = Occupation - template_name = 'django_tables2/bootstrap.html' - exclude_columns = ('boolean', ) - - response, view = OccupationExcludingView.as_view()(build_request('/?_export=csv')) - data = response.getvalue().decode('utf8') - - assert data.splitlines()[0] == 'Name,Region' - - -@pytest.mark.django_db -def test_exporting_unicode_data(): - unicode_name = '木匠' - Occupation.objects.create(name=unicode_name) - - expected_csv = 'Name,Boolean,Region\r\n{},,\r\n'.format(unicode_name) - - response, view = OccupationView.as_view()(build_request('/?_export=csv')) - assert response.getvalue().decode('utf8') == expected_csv - - # smoke tests, hard to test this binary format for string containment - response, view = OccupationView.as_view()(build_request('/?_export=xls')) - data = response.content - assert len(data) > len(expected_csv) - - response, view = OccupationView.as_view()(build_request('/?_export=xlsx')) - data = response.content - assert len(data) > len(expected_csv) - - -def test_exporting_unicode_header(): - unicode_header = 'hé' - - class Table(tables.Table): - name = tables.Column(verbose_name=unicode_header) - - exporter = TableExport('csv', Table([])) - response = exporter.response() - assert response.getvalue().decode('utf8') == unicode_header + '\r\n' - - exporter = TableExport('xls', Table([])) - # this would fail if the header contains unicode and string converstion is attempted. - exporter.export() diff -Nru django-tables-1.14.2/tests/test_config.py django-tables-1.21.2/tests/test_config.py --- django-tables-1.14.2/tests/test_config.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_config.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,84 +1,105 @@ # coding: utf-8 -import pytest from django.core.paginator import EmptyPage, PageNotAnInteger +from django.test import SimpleTestCase, TestCase from fudge import Fake from django_tables2 import Column, RequestConfig, Table +from .app.models import Person from .utils import build_request NOTSET = object() # unique value -@pytest.yield_fixture -def table(): - yield (Fake('Table') - .has_attr(prefixed_page_field='page', - prefixed_per_page_field='per_page', - prefixed_order_by_field='sort')) - - -def test_no_querystring(table): - request = build_request('/') - table = table.has_attr(order_by=NOTSET).expects('paginate') - RequestConfig(request).configure(table) - assert table.order_by is NOTSET - - -def test_full_querystring(table): - request = build_request('/?page=1&per_page=5&sort=abc') - table = (table - .expects('paginate').with_args(page=1, per_page=5) - .expects('order_by').with_args('abc')) - RequestConfig(request).configure(table) - - -def test_partial_querystring(table): - request = build_request('/?page=1&sort=abc') - table = (table - .expects('paginate').with_args(page=1, per_page=5) - .expects('order_by').with_args('abc')) - RequestConfig(request, paginate={'per_page': 5}).configure(table) - - -def test_silent_page_not_an_integer_error(table): - request = build_request('/') - paginator = (Fake('Paginator') - .expects('page').with_args(1)) - table = (table - .has_attr(paginator=paginator) - .expects('paginate').with_args(page='abc') - .raises(PageNotAnInteger)) - - RequestConfig(request, paginate={'page': 'abc', - 'silent': True}).configure(table) - - -def test_silent_empty_page_error(table): - request = build_request('/') - paginator = (Fake('Paginator') - .has_attr(num_pages=987) - .expects('page').with_args(987)) - table = (table - .has_attr(paginator=paginator) - .expects('paginate').with_args(page=123) - .raises(EmptyPage)) - - RequestConfig(request, paginate={'page': 123, - 'silent': True}).configure(table) - - -def test_passing_request_to_constructor(): - '''Table constructor should call RequestConfig if a request is passed.''' - - request = build_request('/?page=1&sort=abc') - - class SimpleTable(Table): - abc = Column() - - table = SimpleTable([ - {'abc': 'bar'}, - {'abc': 'rab'} - ], request=request) +class ConfigTest(SimpleTestCase): + def table(self): + return Fake('Table').has_attr(prefixed_page_field='page', + prefixed_per_page_field='per_page', + prefixed_order_by_field='sort') + + def test_no_querystring(self): + table = self.table().has_attr(order_by=NOTSET).expects('paginate') + RequestConfig(build_request('/')).configure(table) + + self.assertEqual(table.order_by, NOTSET) + + def test_full_querystring(self): + table = self.table() + request = build_request('/?page=1&per_page=5&sort=abc') + table = (table + .expects('paginate').with_args(page=1, per_page=5) + .expects('order_by').with_args('abc')) + RequestConfig(request).configure(table) + + def test_partial_querystring(self): + table = self.table() + request = build_request('/?page=1&sort=abc') + table = (table + .expects('paginate').with_args(page=1, per_page=5) + .expects('order_by').with_args('abc')) + RequestConfig(request, paginate={'per_page': 5}).configure(table) + + def test_silent_page_not_an_integer_error(self): + table = self.table() + request = build_request('/') + paginator = (Fake('Paginator') + .expects('page').with_args(1)) + table = (table + .has_attr(paginator=paginator) + .expects('paginate').with_args(page='abc') + .raises(PageNotAnInteger)) + + RequestConfig(request, paginate={'page': 'abc', + 'silent': True}).configure(table) + + def test_silent_empty_page_error(self): + table = self.table() + request = build_request('/') + paginator = (Fake('Paginator') + .has_attr(num_pages=987) + .expects('page').with_args(987)) + table = (table + .has_attr(paginator=paginator) + .expects('paginate').with_args(page=123) + .raises(EmptyPage)) + + RequestConfig(request, paginate={'page': 123, + 'silent': True}).configure(table) + + def test_passing_request_to_constructor(self): + '''Table constructor should call RequestConfig if a request is passed.''' + + request = build_request('/?page=1&sort=abc') + + class SimpleTable(Table): + abc = Column() + + table = SimpleTable([{}], request=request) + assert table.columns['abc'].is_ordered + + +class NoPaginationQueriesTest(TestCase): + + def test_should_not_count_with_paginate_False(self): + ''' + No extra queries with pagination turned off. + + https://github.com/jieter/django-tables2/issues/551 + ''' + class MyTable(Table): + first_name = Column() + + class Meta: + template_name = 'minimal.html' + + request = build_request() + + Person.objects.create(first_name='Talip', last_name='Molenschot') - assert table.columns['abc'].is_ordered + table = MyTable(Person.objects.all()) + RequestConfig(request, paginate=False).configure(table) + + with self.assertNumQueries(1): + html = table.as_html(request) + + self.assertIn('', html) diff -Nru django-tables-1.14.2/tests/test_core.py django-tables-1.21.2/tests/test_core.py --- django-tables-1.14.2/tests/test_core.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_core.py 2018-03-26 06:43:09.000000000 +0000 @@ -4,9 +4,10 @@ import copy import itertools +import warnings -import pytest from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.test import SimpleTestCase, override_settings import django_tables2 as tables from django_tables2.tables import DeclarativeColumnsMetaclass @@ -33,628 +34,629 @@ order_by = 'alpha' -def test_omitting_data(): - with pytest.raises(TypeError): - UnorderedTable() - - -def test_column_named_items(): - ''' - A column named items must not make the table fail - https://github.com/bradleyayers/django-tables2/issues/316 - ''' - class ItemsTable(tables.Table): - items = tables.Column() - - table = ItemsTable([{'items': 123}, {'items': 2345}]) - - html = table.as_html(request) - assert '123' in html - assert '2345' in html - - -def test_declarations(): - '''Test defining tables by declaration.''' - class GeoAreaTable(tables.Table): - name = tables.Column() - population = tables.Column() - - assert len(GeoAreaTable.base_columns) == 2 - assert 'name' in GeoAreaTable.base_columns - assert not hasattr(GeoAreaTable, 'name') - - class CountryTable(GeoAreaTable): - capital = tables.Column() - - assert len(CountryTable.base_columns) == 3 - assert 'capital' in CountryTable.base_columns - - # multiple inheritance - class AddedMixin(tables.Table): - added = tables.Column() - - class CityTable(GeoAreaTable, AddedMixin): - mayor = tables.Column() - - assert len(CityTable.base_columns) == 4 - assert 'added' in CityTable.base_columns - - # overwrite a column with a non-column - class MayorlessCityTable(CityTable): - mayor = None - - assert len(MayorlessCityTable.base_columns) == 3 - - -def test_metaclass_inheritance(): - class Tweaker(type): - '''Adds an attribute "tweaked" to all classes''' - def __new__(cls, name, bases, attrs): - attrs['tweaked'] = True - return super(Tweaker, cls).__new__(cls, name, bases, attrs) - - class Meta(Tweaker, DeclarativeColumnsMetaclass): - pass - - class TweakedTableBase(tables.Table): - __metaclass__ = Meta - name = tables.Column() - - # Python 2/3 compatible way to enable the metaclass - TweakedTable = Meta(str('TweakedTable'), (TweakedTableBase, ), {}) - - table = TweakedTable([]) - assert 'name' in table.columns - assert table.tweaked - - # now flip the order - class FlippedMeta(DeclarativeColumnsMetaclass, Tweaker): - pass - - class FlippedTweakedTableBase(tables.Table): - name = tables.Column() - - # Python 2/3 compatible way to enable the metaclass - FlippedTweakedTable = FlippedMeta(str('FlippedTweakedTable'), (FlippedTweakedTableBase, ), {}) - - table = FlippedTweakedTable([]) - assert 'name' in table.columns - assert table.tweaked - - -def test_attrs(): - class TestTable(tables.Table): - class Meta: - attrs = {} - assert {} == TestTable([]).attrs - - class TestTable2(tables.Table): - class Meta: - attrs = {'a': 'b'} - assert {'a': 'b'} == TestTable2([]).attrs - - class TestTable3(tables.Table): - pass - assert {} == TestTable3([]).attrs - assert {'a': 'b'} == TestTable3([], attrs={'a': 'b'}).attrs - - class TestTable4(tables.Table): - class Meta: - attrs = {'a': 'b'} - assert {'c': 'd'} == TestTable4([], attrs={'c': 'd'}).attrs - - -def test_attrs_support_computed_values(): - counter = itertools.count() - - class TestTable(tables.Table): - class Meta: - attrs = {'id': lambda: 'test_table_%d' % next(counter)} - - assert 'id="test_table_0"' == TestTable([]).attrs.as_html() - assert 'id="test_table_1"' == TestTable([]).attrs.as_html() - - -def test_attrs_from_settings(settings): - settings.DJANGO_TABLES2_TABLE_ATTRS = { - 'class': 'table-compact' - } - - class Table(tables.Table): - column = tables.Column() - - table = Table({}) - assert table.attrs == {'class': 'table-compact'} - - -def test_datasource_untouched(): - ''' - Ensure that data that is provided to the table (the datasource) is not - modified by table operations. - ''' - original_data = copy.deepcopy(MEMORY_DATA) - - table = UnorderedTable(MEMORY_DATA) - table.order_by = 'i' - list(table.rows) - assert MEMORY_DATA == original_data - - table = UnorderedTable(MEMORY_DATA) - table.order_by = 'beta' - list(table.rows) - assert MEMORY_DATA == original_data - - -def test_should_support_tuple_data_source(): - class SimpleTable(tables.Table): - name = tables.Column() - - table = SimpleTable(( - {'name': 'brad'}, - {'name': 'davina'}, - )) - - assert len(table.rows) == 2 - - -# @pytest.mark.django_db -# def test_should_support_haystack_data_source(): -# Person.objects.create(first_name='Foo', last_name='Bar') -# Person.objects.create(first_name='Brad', last_name='Pitt') -# -# from haystack.query import SearchQuerySet -# from haystack.management.commands import update_index -# -# update_index.Command().handle(interactive=False) -# -# class PersonTable(tables.Table): -# first_name = tables.Column() -# -# table = PersonTable(SearchQuerySet().all()) -# html = table.as_html(request) -# -# # TODO: assert that a person is actually in the produced html. -# assert 'Brad' in html -# - -def test_column_count(): - class SimpleTable(tables.Table): - visible = tables.Column(visible=True) - hidden = tables.Column(visible=False) - - # The columns container supports the len() builtin - assert len(SimpleTable([]).columns) == 1 - - -def test_column_accessor(): - class SimpleTable(UnorderedTable): - col1 = tables.Column(accessor='alpha.upper.isupper') - col2 = tables.Column(accessor='alpha.upper') - table = SimpleTable(MEMORY_DATA) - - assert table.rows[0].get_cell('col1') is True - assert table.rows[0].get_cell('col2') == 'B' - - -def test_exclude_columns(): - ''' - Defining ``Table.Meta.exclude`` or providing an ``exclude`` argument when - instantiating a table should have the same effect -- exclude those columns - from the table. It should have the same effect as not defining the - columns originally. - ''' - table = UnorderedTable([], exclude=('i')) - assert table.columns.names() == ['alpha', 'beta'] - - # Table.Meta: exclude=... - class PartialTable(UnorderedTable): - class Meta: - exclude = ('alpha', ) - table = PartialTable([]) - assert table.columns.names() == ['i', 'beta'] - - # Inheritence -- exclude in parent, add in child - class AddonTable(PartialTable): - added = tables.Column() - table = AddonTable([]) - assert table.columns.names() == ['i', 'beta', 'added'] - - # Inheritence -- exclude in child - class ExcludeTable(UnorderedTable): - added = tables.Column() - - class Meta: - exclude = ('beta', ) - - table = ExcludeTable([]) - assert table.columns.names() == ['i', 'alpha', 'added'] - - -def test_table_exclude_property_should_override_constructor_argument(): - class SimpleTable(tables.Table): - a = tables.Column() - b = tables.Column() - - table = SimpleTable([], exclude=('b', )) - assert table.columns.names() == ['a'] - table.exclude = ('a', ) - assert table.columns.names() == ['b'] - - -def test_exclude_should_work_on_sequence_too(): - ''' - It should be possible to define a sequence on a table - and exclude it in a child of that table. - ''' - class PersonTable(tables.Table): - first_name = tables.Column() - last_name = tables.Column() - occupation = tables.Column() - - class Meta: - sequence = ('first_name', 'last_name', 'occupation') - - class AnotherPersonTable(PersonTable): - class Meta(PersonTable.Meta): - exclude = ('first_name', 'last_name') - - tableA = PersonTable([]) - assert tableA.columns.names() == ['first_name', 'last_name', 'occupation'] - - tableB = AnotherPersonTable([]) - assert tableB.columns.names() == ['occupation'] - - tableC = PersonTable([], exclude=('first_name')) - assert tableC.columns.names() == ['last_name', 'occupation'] - - -def test_pagination(): - class BookTable(tables.Table): - name = tables.Column() - - # create some sample data - data = [] - for i in range(100): - data.append({'name': 'Book No. %d' % i}) - books = BookTable(data) - - # external paginator - paginator = Paginator(books.rows, 10) - assert paginator.num_pages == 10 - page = paginator.page(1) - assert page.has_previous() is False - assert page.has_next() is True - - # integrated paginator - books.paginate(page=1) - assert hasattr(books, 'page') is True - - books.paginate(page=1, per_page=10) - assert len(list(books.page.object_list)) == 10 - - # new attributes - assert books.paginator.num_pages == 10 - assert books.page.has_previous() is False - assert books.page.has_next() is True - - # accessing a non-existant page raises 404 - with pytest.raises(EmptyPage): - books.paginate(Paginator, page=9999, per_page=10) - - with pytest.raises(PageNotAnInteger): - books.paginate(Paginator, page='abc', per_page=10) - - -def test_pagination_shouldnt_prevent_multiple_rendering(): - class SimpleTable(tables.Table): - name = tables.Column() - - table = SimpleTable([{'name': 'brad'}]) - table.paginate() - - assert table.as_html(request) == table.as_html(request) - - -def test_empty_text(): - class TestTable(tables.Table): - a = tables.Column() - - table = TestTable([]) - assert table.empty_text is None - - class TestTable2(tables.Table): - a = tables.Column() - - class Meta: - empty_text = 'nothing here' - - table = TestTable2([]) - assert table.empty_text == 'nothing here' - - table = TestTable2([], empty_text='still nothing') - assert table.empty_text == 'still nothing' - - -def test_prefix(): - ''' - Test that table prefixes affect the names of querystring parameters - ''' - class TableA(tables.Table): - name = tables.Column() - - class Meta: - prefix = 'x' - - table = TableA([]) - html = table.as_html(build_request('/')) - - assert 'x' == table.prefix - assert 'xsort=name' in html - - class TableB(tables.Table): - last_name = tables.Column() - - assert '' == TableB([]).prefix - assert 'x' == TableB([], prefix='x').prefix - - table = TableB([]) - table.prefix = 'x-' - html = table.as_html(build_request('/')) - - assert 'x-' == table.prefix - assert 'x-sort=last_name' in html - - -def test_field_names(): - class TableA(tables.Table): - class Meta: - order_by_field = 'abc' - page_field = 'def' - per_page_field = 'ghi' - - table = TableA([]) - assert 'abc' == table.order_by_field - assert 'def' == table.page_field - assert 'ghi' == table.per_page_field - - -def test_field_names_with_prefix(): - class TableA(tables.Table): - class Meta: - order_by_field = 'sort' - page_field = 'page' - per_page_field = 'per_page' - prefix = '1-' - - table = TableA([]) - assert '1-sort' == table.prefixed_order_by_field - assert '1-page' == table.prefixed_page_field - assert '1-per_page' == table.prefixed_per_page_field - - class TableB(tables.Table): - class Meta: - order_by_field = 'sort' - page_field = 'page' - per_page_field = 'per_page' - - table = TableB([], prefix='1-') - assert '1-sort' == table.prefixed_order_by_field - assert '1-page' == table.prefixed_page_field - assert '1-per_page' == table.prefixed_per_page_field - - table = TableB([]) - table.prefix = '1-' - assert '1-sort' == table.prefixed_order_by_field - assert '1-page' == table.prefixed_page_field - assert '1-per_page' == table.prefixed_per_page_field - - -def test_should_support_a_template_to_be_specified(): - class ConstructorSpecifiedTemplateTable(tables.Table): - name = tables.Column() - - table = ConstructorSpecifiedTemplateTable([], template='dummy.html') - assert table.template == 'dummy.html' - - class PropertySpecifiedTemplateTable(tables.Table): - name = tables.Column() - - table = PropertySpecifiedTemplateTable([]) - table.template = 'dummy.html' - assert table.template == 'dummy.html' - - class DefaultTable(tables.Table): - pass - - table = DefaultTable([]) - assert table.template == 'django_tables2/table.html' - - -def test_template_in_meta_class_declaration_should_be_honored(): - class MetaDeclarationSpecifiedTemplateTable(tables.Table): - name = tables.Column() - - class Meta: - template = 'dummy.html' - - table = MetaDeclarationSpecifiedTemplateTable([]) - assert table.template == 'dummy.html' - assert table.as_html(request) == 'dummy template contents\n' - - -def test_should_support_rendering_multiple_times(): - class MultiRenderTable(tables.Table): - name = tables.Column() - - # test list data - table = MultiRenderTable([{'name': 'brad'}]) - assert table.as_html(request) == table.as_html(request) - - -def test_column_defaults_are_honored(): - class Table(tables.Table): - name = tables.Column(default='abcd') - - class Meta: - default = 'efgh' - - table = Table([{}], default='ijkl') - assert table.rows[0].get_cell('name') == 'abcd' - - -def test_table_meta_defaults_are_honored(): - class Table(tables.Table): - name = tables.Column() - - class Meta: - default = 'abcd' - - table = Table([{}]) - assert table.rows[0].get_cell('name') == 'abcd' - - -def test_table_defaults_are_honored(): - class Table(tables.Table): - name = tables.Column() - - table = Table([{}], default='abcd') - assert table.rows[0].get_cell('name') == 'abcd' - - table = Table([{}], default='abcd') - table.default = 'efgh' - assert table.rows[0].get_cell('name') == 'efgh' - - -AS_VALUES_DATA = [ - {'name': 'Adrian', 'country': 'Australia'}, - {'name': 'Adrian', 'country': 'Brazil'}, - {'name': 'Audrey', 'country': 'Chile'}, - {'name': 'Bassie', 'country': 'Belgium'}, -] - - -def test_as_values(): - class Table(tables.Table): - name = tables.Column() - country = tables.Column() - - expected = [['Name', 'Country']] + [[r['name'], r['country']] for r in AS_VALUES_DATA] - table = Table(AS_VALUES_DATA) - - assert list(table.as_values()) == expected - - -def test_as_values_exclude(): - class Table(tables.Table): - name = tables.Column() - country = tables.Column() - - expected = [['Name']] + [[r['name']] for r in AS_VALUES_DATA] - table = Table(AS_VALUES_DATA) - - assert list(table.as_values(exclude_columns=('country', ))) == expected - - -def test_as_values_exclude_from_export(): - class Table(tables.Table): - name = tables.Column() - buttons = tables.Column(exclude_from_export=True) +class CoreTest(SimpleTestCase): + def test_omitting_data(self): + with self.assertRaises(TypeError): + UnorderedTable() + + def test_column_named_items(self): + ''' + A column named items must not make the table fail + https://github.com/bradleyayers/django-tables2/issues/316 + ''' + class ItemsTable(tables.Table): + items = tables.Column() + + table = ItemsTable([{'items': 123}, {'items': 2345}]) + + html = table.as_html(request) + self.assertIn('123', html) + self.assertIn('2345', html) + + def test_declarations(self): + '''Test defining tables by declaration.''' + class GeoAreaTable(tables.Table): + name = tables.Column() + population = tables.Column() + + self.assertEqual(len(GeoAreaTable.base_columns), 2) + self.assertIn('name', GeoAreaTable.base_columns) + self.assertFalse(hasattr(GeoAreaTable, 'name')) + + class CountryTable(GeoAreaTable): + capital = tables.Column() + + self.assertEqual(len(CountryTable.base_columns), 3) + self.assertIn('capital', CountryTable.base_columns) + + # multiple inheritance + class AddedMixin(tables.Table): + added = tables.Column() + + class CityTable(GeoAreaTable, AddedMixin): + mayor = tables.Column() + + self.assertEqual(len(CityTable.base_columns), 4) + self.assertIn('added', CityTable.base_columns) + + # overwrite a column with a non-column + class MayorlessCityTable(CityTable): + mayor = None + + self.assertEqual(len(MayorlessCityTable.base_columns), 3) + + def test_metaclass_inheritance(self): + class Tweaker(type): + '''Adds an attribute "tweaked" to all classes''' + def __new__(cls, name, bases, attrs): + attrs['tweaked'] = True + return super(Tweaker, cls).__new__(cls, name, bases, attrs) + + class Meta(Tweaker, DeclarativeColumnsMetaclass): + pass + + class TweakedTableBase(tables.Table): + __metaclass__ = Meta + name = tables.Column() + + # Python 2/3 compatible way to enable the metaclass + TweakedTable = Meta(str('TweakedTable'), (TweakedTableBase, ), {}) + + table = TweakedTable([]) + self.assertIn('name', table.columns) + self.assertTrue(table.tweaked) + + # now flip the order + class FlippedMeta(DeclarativeColumnsMetaclass, Tweaker): + pass + + class FlippedTweakedTableBase(tables.Table): + name = tables.Column() + + # Python 2/3 compatible way to enable the metaclass + FlippedTweakedTable = FlippedMeta(str('FlippedTweakedTable'), (FlippedTweakedTableBase, ), {}) + + table = FlippedTweakedTable([]) + self.assertIn('name', table.columns) + self.assertTrue(table.tweaked) + + def test_attrs(self): + class TestTable(tables.Table): + class Meta: + attrs = {} + self.assertEqual({}, TestTable([]).attrs) + + class TestTable2(tables.Table): + class Meta: + attrs = {'a': 'b'} + self.assertEqual({'a': 'b'}, TestTable2([]).attrs) + + class TestTable3(tables.Table): + pass + self.assertEqual({}, TestTable3([]).attrs) + self.assertEqual({'a': 'b'}, TestTable3([], attrs={'a': 'b'}).attrs) + + class TestTable4(tables.Table): + class Meta: + attrs = {'a': 'b'} + self.assertEqual({'c': 'd'}, TestTable4([], attrs={'c': 'd'}).attrs) + + def test_attrs_support_computed_values(self): + counter = itertools.count() + + class TestTable(tables.Table): + class Meta: + attrs = {'id': lambda: 'test_table_%d' % next(counter)} + + self.assertEqual('id="test_table_0"', TestTable([]).attrs.as_html()) + self.assertEqual('id="test_table_1"', TestTable([]).attrs.as_html()) + + @override_settings(DJANGO_TABLES2_TABLE_ATTRS={'class': 'table-compact'}) + def test_attrs_from_settings(self): + + class Table(tables.Table): + column = tables.Column() + + table = Table({}) + self.assertEqual(table.attrs, {'class': 'table-compact'}) + + def test_datasource_untouched(self): + ''' + Ensure that data that is provided to the table (the datasource) is not + modified by table operations. + ''' + original_data = copy.deepcopy(MEMORY_DATA) + + table = UnorderedTable(MEMORY_DATA) + table.order_by = 'i' + list(table.rows) + self.assertEqual(MEMORY_DATA, original_data) + + table = UnorderedTable(MEMORY_DATA) + table.order_by = 'beta' + list(table.rows) + self.assertEqual(MEMORY_DATA, original_data) + + def test_should_support_tuple_data_source(self): + class SimpleTable(tables.Table): + name = tables.Column() + + table = SimpleTable(( + {'name': 'brad'}, + {'name': 'davina'}, + )) + + self.assertEqual(len(table.rows), 2) + + def test_column_count(self): + class SimpleTable(tables.Table): + visible = tables.Column(visible=True) + hidden = tables.Column(visible=False) + + # The columns container supports the len() builtin + self.assertEqual(len(SimpleTable([]).columns), 1) + + def test_column_accessor(self): + class SimpleTable(UnorderedTable): + col1 = tables.Column(accessor='alpha.upper.isupper') + col2 = tables.Column(accessor='alpha.upper') + table = SimpleTable(MEMORY_DATA) + + self.assertTrue(table.rows[0].get_cell('col1')) + self.assertEqual(table.rows[0].get_cell('col2'), 'B') + + def test_exclude_columns(self): + ''' + Defining ``Table.Meta.exclude`` or providing an ``exclude`` argument when + instantiating a table should have the same effect -- exclude those columns + from the table. It should have the same effect as not defining the + columns originally. + ''' + table = UnorderedTable([], exclude=('i')) + self.assertEqual(table.columns.names(), ['alpha', 'beta']) + + # Table.Meta: exclude=... + class PartialTable(UnorderedTable): + class Meta: + exclude = ('alpha', ) + table = PartialTable([]) + self.assertEqual(table.columns.names(), ['i', 'beta']) + + # Inheritence -- exclude in parent, add in child + class AddonTable(PartialTable): + added = tables.Column() + table = AddonTable([]) + self.assertEqual(table.columns.names(), ['i', 'beta', 'added']) + + # Inheritence -- exclude in child + class ExcludeTable(UnorderedTable): + added = tables.Column() + + class Meta: + exclude = ('beta', ) + + table = ExcludeTable([]) + self.assertEqual(table.columns.names(), ['i', 'alpha', 'added']) + + def test_table_exclude_property_should_override_constructor_argument(self): + class SimpleTable(tables.Table): + a = tables.Column() + b = tables.Column() + + table = SimpleTable([], exclude=('b', )) + self.assertEqual(table.columns.names(), ['a']) + table.exclude = ('a', ) + self.assertEqual(table.columns.names(), ['b']) + + def test_exclude_should_work_on_sequence_too(self): + ''' + It should be possible to define a sequence on a table + and exclude it in a child of that table. + ''' + class PersonTable(tables.Table): + first_name = tables.Column() + last_name = tables.Column() + occupation = tables.Column() + + class Meta: + sequence = ('first_name', 'last_name', 'occupation') + + class AnotherPersonTable(PersonTable): + class Meta(PersonTable.Meta): + exclude = ('first_name', 'last_name') + + tableA = PersonTable([]) + self.assertEqual(tableA.columns.names(), ['first_name', 'last_name', 'occupation']) + + tableB = AnotherPersonTable([]) + self.assertEqual(tableB.columns.names(), ['occupation']) + + tableC = PersonTable([], exclude=('first_name')) + self.assertEqual(tableC.columns.names(), ['last_name', 'occupation']) + + def test_pagination(self): + class BookTable(tables.Table): + name = tables.Column() + + # create some sample data + data = list([{'name': 'Book No. %d' % i} for i in range(100)]) + books = BookTable(data) + + # external paginator + paginator = Paginator(books.rows, 10) + self.assertEqual(paginator.num_pages, 10) + page = paginator.page(1) + self.assertFalse(page.has_previous()) + self.assertTrue(page.has_next()) + + # integrated paginator + books.paginate(page=1) + self.assertTrue(hasattr(books, 'page')) + + books.paginate(page=1, per_page=10) + self.assertEqual(len(list(books.page.object_list)), 10) + + # new attributes + self.assertEqual(books.paginator.num_pages, 10) + self.assertFalse(books.page.has_previous()) + self.assertTrue(books.page.has_next()) + + # accessing a non-existant page raises 404 + with self.assertRaises(EmptyPage): + books.paginate(Paginator, page=9999, per_page=10) + + with self.assertRaises(PageNotAnInteger): + books.paginate(Paginator, page='abc', per_page=10) + + def test_pagination_shouldnt_prevent_multiple_rendering(self): + class SimpleTable(tables.Table): + name = tables.Column() + + table = SimpleTable([{'name': 'brad'}]) + table.paginate() + + self.assertEqual(table.as_html(request), table.as_html(request)) + + def test_empty_text(self): + class TestTable(tables.Table): + a = tables.Column() + + table = TestTable([]) + self.assertEqual(table.empty_text, None) + + class TestTable2(tables.Table): + a = tables.Column() + + class Meta: + empty_text = 'nothing here' + + table = TestTable2([]) + self.assertEqual(table.empty_text, 'nothing here') + + table = TestTable2([], empty_text='still nothing') + self.assertEqual(table.empty_text, 'still nothing') + + def test_prefix(self): + ''' + Test that table prefixes affect the names of querystring parameters + ''' + class TableA(tables.Table): + name = tables.Column() + + class Meta: + prefix = 'x' + + table = TableA([]) + html = table.as_html(build_request('/')) + + self.assertEqual('x', table.prefix) + self.assertIn('xsort=name', html) + + class TableB(tables.Table): + last_name = tables.Column() + + self.assertEqual('', TableB([]).prefix) + self.assertEqual('x', TableB([], prefix='x').prefix) + + table = TableB([]) + table.prefix = 'x-' + html = table.as_html(build_request('/')) + + self.assertEqual('x-', table.prefix) + self.assertIn('x-sort=last_name', html) + + def test_field_names(self): + class TableA(tables.Table): + class Meta: + order_by_field = 'abc' + page_field = 'def' + per_page_field = 'ghi' + + table = TableA([]) + self.assertEqual('abc', table.order_by_field) + self.assertEqual('def', table.page_field) + self.assertEqual('ghi', table.per_page_field) + + def test_field_names_with_prefix(self): + class TableA(tables.Table): + class Meta: + order_by_field = 'sort' + page_field = 'page' + per_page_field = 'per_page' + prefix = '1-' + + table = TableA([]) + self.assertEqual('1-sort', table.prefixed_order_by_field) + self.assertEqual('1-page', table.prefixed_page_field) + self.assertEqual('1-per_page', table.prefixed_per_page_field) + + class TableB(tables.Table): + class Meta: + order_by_field = 'sort' + page_field = 'page' + per_page_field = 'per_page' + + table = TableB([], prefix='1-') + self.assertEqual('1-sort', table.prefixed_order_by_field) + self.assertEqual('1-page', table.prefixed_page_field) + self.assertEqual('1-per_page', table.prefixed_per_page_field) + + table = TableB([]) + table.prefix = '1-' + self.assertEqual('1-sort', table.prefixed_order_by_field) + self.assertEqual('1-page', table.prefixed_page_field) + self.assertEqual('1-per_page', table.prefixed_per_page_field) + + def test_should_support_a_template_name_to_be_specified(self): + class ConstructorSpecifiedTemplateTable(tables.Table): + name = tables.Column() + + table = ConstructorSpecifiedTemplateTable([], template_name='dummy.html') + self.assertEqual(table.template_name, 'dummy.html') + + class PropertySpecifiedTemplateTable(tables.Table): + name = tables.Column() + + table = PropertySpecifiedTemplateTable([]) + table.template_name = 'dummy.html' + self.assertEqual(table.template_name, 'dummy.html') + + class DefaultTable(tables.Table): + pass + + table = DefaultTable([]) + self.assertEqual(table.template_name, 'django_tables2/table.html') + + def test_template_name_in_meta_class_declaration_should_be_honored(self): + class MetaDeclarationSpecifiedTemplateTable(tables.Table): + name = tables.Column() + + class Meta: + template_name = 'dummy.html' + + table = MetaDeclarationSpecifiedTemplateTable([]) + self.assertEqual(table.template_name, 'dummy.html') + self.assertEqual(table.as_html(request), 'dummy template contents\n') + + def test_warns_for_legacy_template(self): + ''' + Test for DepricationWarning and fallback to current table.template_name + attribute. + ''' + with warnings.catch_warnings(record=True) as w: + + class MetaDeclarationSpecifiedTemplateTable(tables.Table): + name = tables.Column() + + class Meta: + template = 'dummy.html' + + table = MetaDeclarationSpecifiedTemplateTable([], template='dummy2.html') + + self.assertEqual(len(w), 2) + self.assertTrue(issubclass(w[0].category, DeprecationWarning)) + self.assertTrue(issubclass(w[1].category, DeprecationWarning)) + self.assertEqual(table.template_name, 'dummy2.html') + + def test_should_support_rendering_multiple_times(self): + class MultiRenderTable(tables.Table): + name = tables.Column() + + # test list data + table = MultiRenderTable([{'name': 'brad'}]) + self.assertEqual(table.as_html(request), table.as_html(request)) + + def test_column_defaults_are_honored(self): + class Table(tables.Table): + name = tables.Column(default='abcd') + + class Meta: + default = 'efgh' + + table = Table([{}], default='ijkl') + self.assertEqual(table.rows[0].get_cell('name'), 'abcd') + + def test_table_meta_defaults_are_honored(self): + class Table(tables.Table): + name = tables.Column() + + class Meta: + default = 'abcd' + + table = Table([{}]) + self.assertEqual(table.rows[0].get_cell('name'), 'abcd') + + def test_table_defaults_are_honored(self): + class Table(tables.Table): + name = tables.Column() + + table = Table([{}], default='abcd') + self.assertEqual(table.rows[0].get_cell('name'), 'abcd') + + table = Table([{}], default='abcd') + table.default = 'efgh' + self.assertEqual(table.rows[0].get_cell('name'), 'efgh') + + +class BoundColumnTest(SimpleTestCase): + + def test_attrs_bool_error(self): + class Table(tables.Table): + c_element = tables.Column() + + class ErrorObject(object): + def __bool__(self): + raise NotImplementedError + + table = Table([{'c_element': ErrorObject()}]) + list(table.rows[0].items()) + try: + table.columns[0].attrs + except NotImplementedError: + self.fail('__bool__ should not be evaluated!') + + def test_attrs_falsy_object(self): + """Computed attrs in BoundColumn should be passed the column value, even if its __bool__ returns False. """ + class Table(tables.Table): + c_element = tables.Column() + + class Meta: + attrs = { + 'td': {'data-column-name': lambda value: value.name} + } - assert list(Table([]).as_values()) == [['Name'], ] + class FalsyObject(object): + name = 'FalsyObject1' + def __bool__(self): + return False -def test_as_values_empty_values(): - ''' - Table's as_values() method returns `None` for missing values - ''' + table = Table([{'c_element': FalsyObject()}]) + list(table.rows[0].items()) + self.assertEqual('FalsyObject1', table.columns[0].attrs['td']['data-column-name']) - class Table(tables.Table): - name = tables.Column() - country = tables.Column() - data = [ +class AsValuesTest(SimpleTestCase): + AS_VALUES_DATA = [ + {'name': 'Adrian', 'country': 'Australia'}, {'name': 'Adrian', 'country': 'Brazil'}, - {'name': 'Audrey'}, + {'name': 'Audrey', 'country': 'Chile'}, {'name': 'Bassie', 'country': 'Belgium'}, - {'country': 'France'}, ] - expected = [['Name', 'Country']] + [[r.get('name'), r.get('country')] for r in data] - table = Table(data) - assert list(table.as_values()) == expected + def test_as_values(self): + class Table(tables.Table): + name = tables.Column() + country = tables.Column() -def test_as_values_render_FOO(): + expected = [['Name', 'Country']] + [[r['name'], r['country']] for r in self.AS_VALUES_DATA] + table = Table(self.AS_VALUES_DATA) - class Table(tables.Table): - name = tables.Column() - country = tables.Column() + self.assertEqual(list(table.as_values()), expected) - def render_country(self, value): - return value + ' test' + def test_as_values_exclude(self): + class Table(tables.Table): + name = tables.Column() + country = tables.Column() - expected = [['Name', 'Country']] + [[r['name'], r['country'] + ' test'] for r in AS_VALUES_DATA] + expected = [['Name']] + [[r['name']] for r in self.AS_VALUES_DATA] + table = Table(self.AS_VALUES_DATA) - assert list(Table(AS_VALUES_DATA).as_values()) == expected + self.assertEqual(list(table.as_values(exclude_columns=('country', ))), expected) + def test_as_values_exclude_from_export(self): + class Table(tables.Table): + name = tables.Column() + buttons = tables.Column(exclude_from_export=True) -def test_as_values_value_FOO(): + self.assertEqual(list(Table([]).as_values()), [['Name'], ]) - class Table(tables.Table): - name = tables.Column() - country = tables.Column() + def test_as_values_empty_values(self): + ''' + Table's as_values() method returns `None` for missing values + ''' + class Table(tables.Table): + name = tables.Column() + country = tables.Column() - def render_country(self, value): - return value + ' test' + data = [ + {'name': 'Adrian', 'country': 'Brazil'}, + {'name': 'Audrey'}, + {'name': 'Bassie', 'country': 'Belgium'}, + {'country': 'France'}, + ] + expected = [['Name', 'Country']] + [[r.get('name'), r.get('country')] for r in data] + table = Table(data) + self.assertEqual(list(table.as_values()), expected) - def value_country(self, value): - return value + ' different' + def test_as_values_render_FOO(self): + class Table(tables.Table): + name = tables.Column() + country = tables.Column() - expected = [['Name', 'Country']] + [[r['name'], r['country'] + ' different'] for r in AS_VALUES_DATA] + def render_country(self, value): + return value + ' test' - assert list(Table(AS_VALUES_DATA).as_values()) == expected + expected = [['Name', 'Country']] + [[r['name'], r['country'] + ' test'] for r in self.AS_VALUES_DATA] + self.assertEqual(list(Table(self.AS_VALUES_DATA).as_values()), expected) -def test_row_attrs(): - class Table(tables.Table): - alpha = tables.Column() - beta = tables.Column() + def test_as_values_value_FOO(self): + class Table(tables.Table): + name = tables.Column() + country = tables.Column() - table = Table(MEMORY_DATA, row_attrs={ - 'class': lambda record: 'row-id-{}'.format(record['i']), - }) + def render_country(self, value): + return value + ' test' - assert table.rows[0].attrs == {'class': 'row-id-2 even'} + def value_country(self, value): + return value + ' different' + expected = [['Name', 'Country']] + [[r['name'], r['country'] + ' different'] for r in self.AS_VALUES_DATA] -def test_row_attrs_in_meta(): - class Table(tables.Table): - alpha = tables.Column() - beta = tables.Column() + self.assertEqual(list(Table(self.AS_VALUES_DATA).as_values()), expected) - class Meta: - row_attrs = { - 'class': lambda record: 'row-id-{}'.format(record['i']), - } - table = Table(MEMORY_DATA) - assert table.rows[0].attrs == {'class': 'row-id-2 even'} +class RowAttrsTest(SimpleTestCase): + def test_row_attrs(self): + class Table(tables.Table): + alpha = tables.Column() + beta = tables.Column() + table = Table(MEMORY_DATA, row_attrs={ + 'class': lambda record: 'row-id-{}'.format(record['i']), + }) + + self.assertEqual(table.rows[0].attrs, {'class': 'row-id-2 even'}) + + def test_row_attrs_in_meta(self): + class Table(tables.Table): + alpha = tables.Column() + beta = tables.Column() + + class Meta: + row_attrs = { + 'class': lambda record: 'row-id-{}'.format(record['i']), + } -def test_td_attrs_from_table(): - class Table(tables.Table): - alpha = tables.Column() - beta = tables.Column() + table = Table(MEMORY_DATA) + self.assertEqual(table.rows[0].attrs, {'class': 'row-id-2 even'}) - class Meta: - attrs = { - 'td': { - 'data-column-name': lambda column: column.name + def test_td_attrs_from_table(self): + class Table(tables.Table): + alpha = tables.Column() + beta = tables.Column() + + class Meta: + attrs = { + 'td': { + 'data-column-name': lambda bound_column: bound_column.name + } } - } - table = Table(MEMORY_DATA) - html = table.as_html(request) - td = parse(html).find('.//tbody/tr[1]/td[1]') - assert td.attrib == { - 'data-column-name': 'alpha', - 'class': 'alpha' - } + table = Table(MEMORY_DATA) + html = table.as_html(request) + td = parse(html).find('.//tbody/tr[1]/td[1]') + self.assertEqual(td.attrib, { + 'data-column-name': 'alpha', + 'class': 'alpha' + }) diff -Nru django-tables-1.14.2/tests/test_data.py django-tables-1.21.2/tests/test_data.py --- django-tables-1.14.2/tests/test_data.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_data.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,38 +0,0 @@ - -from django_tables2.data import TableListData - - -def generator(max_value): - for i in range(max_value): - yield { - 'foo': i, - 'bar': chr(i), - 'baz': hex(i), - 'inv': max_value - i - } - - -def test_TableListData_basic_list(): - list_data = list(generator(100)) - data = TableListData(list_data, object()) - - assert len(list_data) == len(data) - assert data.verbose_name == 'item' - assert data.verbose_name_plural == 'items' - - -def test_TableListData_with_verbose_name(): - ''' - TableListData uses the attributes on the listlike object to generate - it's verbose_name. - ''' - class listlike(list): - verbose_name = 'unit' - verbose_name_plural = 'units' - - list_data = listlike(generator(100)) - data = TableListData(list_data, object()) - - assert len(list_data) == len(data) - assert data.verbose_name == 'unit' - assert data.verbose_name_plural == 'units' diff -Nru django-tables-1.14.2/tests/test_dynamically_add_show_hide_columns.py django-tables-1.21.2/tests/test_dynamically_add_show_hide_columns.py --- django-tables-1.14.2/tests/test_dynamically_add_show_hide_columns.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_dynamically_add_show_hide_columns.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,9 +1,9 @@ # coding: utf-8 from __future__ import absolute_import, unicode_literals -import pytest from django.contrib.auth import get_user_model from django.template import Context, Template +from django.test import TestCase from django.utils.translation import ugettext_lazy as _ import django_tables2 as tables @@ -21,111 +21,106 @@ ] -def test_dynamically_adding_columns(): - ''' - When adding columns to self.base_columns, they were actually added to - the class attribute `Table.base_columns`, and not to the instance - attribute, `table.base_columns` - - issue #403 - ''' - class MyTable(tables.Table): - name = tables.Column() - - # this is obvious: - assert list(MyTable(data).columns.columns.keys()) == ['name'] - - assert list(MyTable(data, extra_columns=[ - ('country', tables.Column()) - ]).columns.columns.keys()) == ['name', 'country'] - - # this new instance should not have the extra columns added to the first instance. - assert list(MyTable(data).columns.columns.keys()) == ['name'] - - -def test_sorting_on_dynamically_added_columns(): - class MyTable(tables.Table): - name = tables.Column() - - table = MyTable(data, order_by='-country', extra_columns=[ - ('country', tables.Column(verbose_name=_('country'))) - ]) - - root = parse(table.as_html(build_request())) - assert root.find('.//tbody/tr/td[2]').text == 'Chile' - assert root.find('.//tbody/tr[4]/td[2]').text == 'Australia' - - -@pytest.mark.django_db -def test_dynamically_override_auto_generated_columns(): - for name, country in data: - Person.objects.create(first_name=name, last_name=country) - queryset = Person.objects.all() - - class MyTable(tables.Table): - class Meta: - model = Person - fields = ('first_name', 'last_name') - - assert list(MyTable(queryset).columns.columns.keys()) == ['first_name', 'last_name'] - - table = MyTable(queryset, extra_columns=[ - ('first_name', tables.Column(attrs={'td': {'style': 'color: red;'}})) - ]) - # we still should have two columns - assert list(table.columns.columns.keys()) == ['first_name', 'last_name'] - # the attrs should be applied to the `first_name` column - assert table.columns['first_name'].attrs['td'] == {'class': 'first_name', 'style': 'color: red;'} - - -def test_dynamically_add_column_with_sequence(): - class MyTable(tables.Table): - name = tables.Column() - - class Meta: - sequence = ('...', 'name') - - assert list(MyTable(data, extra_columns=[ - ('country', tables.Column()) - ]).columns.columns.keys()) == ['country', 'name'] - - # override sequence with an argument. - assert list(MyTable( - data, - extra_columns=[('country', tables.Column())], - sequence=('name', '...') - ).columns.columns.keys()) == ['name', 'country'] - - -@pytest.mark.django_db -def test_dynamically_hide_columns(): - class MyTable(tables.Table): - name = tables.Column(orderable=False) - country = tables.Column(orderable=False) - - def before_render(self, request): - if request.user.username == 'Bob': - self.columns.hide('country') - else: - self.columns.show('country') - - template = Template('{% load django_tables2 %}{% render_table table %}') - - table = MyTable(data) - request = build_request(user=User.objects.create(username='Bob')) - html = table.as_html(request) - assert '' in html - assert '' not in html - - html = template.render(Context({'request': request, 'table': table})) - assert '' in html - assert '' not in html - - request = build_request(user=User.objects.create(username='Alice')) - html = table.as_html(request) - assert '' in html - assert '' in html - - html = template.render(Context({'request': request, 'table': table})) - assert '' in html - assert '' in html +class DynamicColumnsTest(TestCase): + def test_dynamically_adding_columns(self): + ''' + When adding columns to self.base_columns, they were actually added to + the class attribute `Table.base_columns`, and not to the instance + attribute, `table.base_columns` + + issue #403 + ''' + class MyTable(tables.Table): + name = tables.Column() + + # this is obvious: + assert list(MyTable(data).columns.columns.keys()) == ['name'] + + assert list(MyTable(data, extra_columns=[ + ('country', tables.Column()) + ]).columns.columns.keys()) == ['name', 'country'] + + # this new instance should not have the extra columns added to the first instance. + assert list(MyTable(data).columns.columns.keys()) == ['name'] + + def test_sorting_on_dynamically_added_columns(self): + class MyTable(tables.Table): + name = tables.Column() + + table = MyTable(data, order_by='-country', extra_columns=[ + ('country', tables.Column(verbose_name=_('country'))) + ]) + + root = parse(table.as_html(build_request())) + assert root.find('.//tbody/tr/td[2]').text == 'Chile' + assert root.find('.//tbody/tr[4]/td[2]').text == 'Australia' + + def test_dynamically_override_auto_generated_columns(self): + for name, country in data: + Person.objects.create(first_name=name, last_name=country) + queryset = Person.objects.all() + + class MyTable(tables.Table): + class Meta: + model = Person + fields = ('first_name', 'last_name') + + assert list(MyTable(queryset).columns.columns.keys()) == ['first_name', 'last_name'] + + table = MyTable(queryset, extra_columns=[ + ('first_name', tables.Column(attrs={'td': {'style': 'color: red;'}})) + ]) + # we still should have two columns + assert list(table.columns.columns.keys()) == ['first_name', 'last_name'] + # the attrs should be applied to the `first_name` column + assert table.columns['first_name'].attrs['td'] == {'class': 'first_name', 'style': 'color: red;'} + + def test_dynamically_add_column_with_sequence(self): + class MyTable(tables.Table): + name = tables.Column() + + class Meta: + sequence = ('...', 'name') + + assert list(MyTable(data, extra_columns=[ + ('country', tables.Column()) + ]).columns.columns.keys()) == ['country', 'name'] + + # override sequence with an argument. + assert list(MyTable( + data, + extra_columns=[('country', tables.Column())], + sequence=('name', '...') + ).columns.columns.keys()) == ['name', 'country'] + + def test_dynamically_hide_columns(self): + class MyTable(tables.Table): + name = tables.Column(orderable=False) + country = tables.Column(orderable=False) + + def before_render(self, request): + if request.user.username == 'Bob': + self.columns.hide('country') + else: + self.columns.show('country') + + template = Template('{% load django_tables2 %}{% render_table table %}') + + table = MyTable(data) + request = build_request(user=User.objects.create(username='Bob')) + html = table.as_html(request) + assert '' in html + assert '' not in html + + html = template.render(Context({'request': request, 'table': table})) + assert '' in html + assert '' not in html + + request = build_request(user=User.objects.create(username='Alice')) + html = table.as_html(request) + assert '' in html + assert '' in html + + html = template.render(Context({'request': request, 'table': table})) + assert '' in html + assert '' in html diff -Nru django-tables-1.14.2/tests/test_export.py django-tables-1.21.2/tests/test_export.py --- django-tables-1.14.2/tests/test_export.py 1970-01-01 00:00:00.000000000 +0000 +++ django-tables-1.21.2/tests/test_export.py 2018-03-26 06:43:09.000000000 +0000 @@ -0,0 +1,263 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import json +from unittest import skipIf + +from django.core.exceptions import ImproperlyConfigured +from django.shortcuts import render +from django.test import TestCase + +import django_tables2 as tables +from django_tables2.config import RequestConfig + +from .app.models import Occupation, Person, Region +from .test_views import DispatchHookMixin +from .utils import build_request + +try: + from django_tables2.export.export import TableExport + from django_tables2.export.views import ExportMixin +except ImproperlyConfigured: + TableExport = None + + +NAMES = [ + ('Yildiz', 'van der Kuil'), + ('Lindi', 'Hakvoort'), + ('Gerardo', 'Castelein'), +] + +CSV_SEP = '\r\n' + +EXPECTED_CSV = CSV_SEP.join( + ('First Name,Surname', ) + tuple(','.join(name) for name in NAMES) +) + CSV_SEP + +EXPECTED_JSON = list([ + {'First Name': first_name, 'Surname': last_name} + for first_name, last_name in NAMES +]) + + +class Table(tables.Table): + first_name = tables.Column() + last_name = tables.Column() + + +class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): + table_class = Table + table_pagination = {'per_page': 1} + model = Person # required for ListView + template_name = 'django_tables2/bootstrap.html' + + +@skipIf(TableExport is None, 'Tablib is required to run the export tests') +class TableExportTest(TestCase): + ''' + github issue #474: null/None values in exports + ''' + def test_None_values(self): + table = Table([ + {'first_name': 'Yildiz', 'last_name': 'van der Kuil'}, + {'first_name': 'Jan', 'last_name': None} + ]) + + exporter = TableExport('csv', table) + expected = ( + 'First Name,Last Name', + 'Yildiz,van der Kuil', + 'Jan,' + ) + self.assertEqual(exporter.export(), CSV_SEP.join(expected) + CSV_SEP) + + def test_null_values(self): + Person.objects.create(first_name='Jan', last_name='Coen') + + class Table(tables.Table): + first_name = tables.Column() + last_name = tables.Column(verbose_name='Last Name') + occupation = tables.Column(verbose_name='Occupation') + + table = Table(Person.objects.all()) + exporter = TableExport('csv', table) + expected = ( + 'First Name,Last Name,Occupation', + 'Jan,Coen,' + ) + self.assertEqual(exporter.export(), CSV_SEP.join(expected) + CSV_SEP) + + +@skipIf(TableExport is None, 'Tablib is required to run the export tests') +class ExportViewTest(TestCase): + def setUp(self): + for first_name, last_name in NAMES: + Person.objects.create(first_name=first_name, last_name=last_name) + + def test_view_should_support_csv_export(self): + response, view = View.as_view()(build_request('/?_export=csv')) + assert response.getvalue().decode('utf8') == EXPECTED_CSV + + # should just render the normal table without the _export query + response, view = View.as_view()(build_request('/')) + html = response.render().rendered_content + + assert 'Yildiz' in html + assert 'Lindy' not in html + + def test_should_raise_error_for_unsupported_file_type(self): + table = Table([]) + + with self.assertRaises(TypeError): + TableExport(table=table, export_format='exe') + + def test_should_support_json_export(self): + response, view = View.as_view()(build_request('/?_export=json')) + assert json.loads(response.getvalue().decode('utf8')) == EXPECTED_JSON + + def test_should_support_custom_trigger_param(self): + class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): + table_class = Table + export_trigger_param = 'export_to' + model = Person # required for ListView + + response, view = View.as_view()(build_request('/?export_to=json')) + assert json.loads(response.getvalue().decode('utf8')) == EXPECTED_JSON + + def test_should_support_custom_filename(self): + class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): + table_class = Table + export_name = 'people' + model = Person # required for ListView + + response, view = View.as_view()(build_request('/?_export=json')) + assert response['Content-Disposition'] == 'attachment; filename="people.json"' + + def test_function_view(self): + ''' + Test the code used in the docs + ''' + def table_view(request): + table = Table(Person.objects.all()) + RequestConfig(request).configure(table) + + export_format = request.GET.get('_export', None) + if TableExport.is_valid_format(export_format): + exporter = TableExport(export_format, table) + return exporter.response('table.{}'.format(export_format)) + + return render(request, 'django_tables2/table.html', { + 'table': table + }) + + response = table_view(build_request('/?_export=csv')) + assert response.getvalue().decode('utf8') == EXPECTED_CSV + + # must also support the normal html table. + response = table_view(build_request('/')) + html = response.content.decode('utf8') + + assert 'Yildiz' in html + assert 'Lindy' not in html + + +class OccupationTable(tables.Table): + name = tables.Column() + boolean = tables.Column() + region = tables.Column() + + +class OccupationView(DispatchHookMixin, ExportMixin, tables.SingleTableView): + table_class = OccupationTable + table_pagination = {'per_page': 1} + model = Occupation + template_name = 'django_tables2/bootstrap.html' + + +@skipIf(TableExport is None, 'Tablib is required to run the export tests') +class AdvancedExportViewTest(TestCase): + def setUp(self): + richard = Person.objects.create(first_name='Richard', last_name='Queener') + + vlaanderen = Region.objects.create(name='Vlaanderen', mayor=richard) + Occupation.objects.create(name='Timmerman', boolean=True, region=vlaanderen) + Occupation.objects.create(name='Ecoloog', boolean=False, region=vlaanderen) + + def test_should_work_with_foreign_keys(self): + response, view = OccupationView.as_view()(build_request('/?_export=xls')) + data = response.content + # binary data, so not possible to compare to an exact expectation + assert data.find('Vlaanderen'.encode()) + assert data.find('Ecoloog'.encode()) + assert data.find('Timmerman'.encode()) + + def test_should_work_with_foreign_key_fields(self): + class OccupationWithForeignKeyFieldsTable(tables.Table): + name = tables.Column() + boolean = tables.Column() + region = tables.Column() + mayor = tables.Column(accessor='region.mayor.first_name') + + class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): + table_class = OccupationWithForeignKeyFieldsTable + table_pagination = {'per_page': 1} + model = Occupation + template_name = 'django_tables2/bootstrap.html' + + response, view = View.as_view()(build_request('/?_export=csv')) + data = response.getvalue().decode('utf8') + + expected_csv = '\r\n'.join(( + 'Name,Boolean,Region,First Name', + 'Timmerman,True,Vlaanderen,Richard', + 'Ecoloog,False,Vlaanderen,Richard\r\n' + )) + assert data == expected_csv + + def test_should_allow_exclude_columns(self): + class OccupationExcludingView(DispatchHookMixin, ExportMixin, tables.SingleTableView): + table_class = OccupationTable + table_pagination = {'per_page': 1} + model = Occupation + template_name = 'django_tables2/bootstrap.html' + exclude_columns = ('boolean', ) + + response, view = OccupationExcludingView.as_view()(build_request('/?_export=csv')) + data = response.getvalue().decode('utf8') + + assert data.splitlines()[0] == 'Name,Region' + + +@skipIf(TableExport is None, 'Tablib is required to run the export tests') +class UnicodeExportViewTest(TestCase): + def test_exporting_unicode_data(self): + unicode_name = '木匠' + Occupation.objects.create(name=unicode_name) + + expected_csv = 'Name,Boolean,Region\r\n{},,\r\n'.format(unicode_name) + + response, view = OccupationView.as_view()(build_request('/?_export=csv')) + assert response.getvalue().decode('utf8') == expected_csv + + # smoke tests, hard to test this binary format for string containment + response, view = OccupationView.as_view()(build_request('/?_export=xls')) + data = response.content + assert len(data) > len(expected_csv) + + response, view = OccupationView.as_view()(build_request('/?_export=xlsx')) + data = response.content + assert len(data) > len(expected_csv) + + def test_exporting_unicode_header(self): + unicode_header = 'hé' + + class Table(tables.Table): + name = tables.Column(verbose_name=unicode_header) + + exporter = TableExport('csv', Table([])) + response = exporter.response() + assert response.getvalue().decode('utf8') == unicode_header + '\r\n' + + exporter = TableExport('xls', Table([])) + # this would fail if the header contains unicode and string converstion is attempted. + exporter.export() diff -Nru django-tables-1.14.2/tests/test_faq.py django-tables-1.21.2/tests/test_faq.py --- django-tables-1.14.2/tests/test_faq.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_faq.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,8 +1,8 @@ -import itertools +from django.test import SimpleTestCase import django_tables2 as tables -from .utils import build_request +from .utils import build_request, parse TEST_DATA = [ {'name': 'Belgium', 'population': 11200000}, @@ -11,37 +11,34 @@ ] -def _test_counter(Table, expected=''): - table = Table(TEST_DATA) - html = table.as_html(build_request()) - assert expected in html - - # the counter should start at zero the second time too - table = Table(TEST_DATA) - html = table.as_html(build_request()) - assert expected in html - - return html - - -def test_row_counter_using_templateColumn(): - class CountryTable(tables.Table): - counter = tables.TemplateColumn('{{ row_counter }}') - name = tables.Column() - - _test_counter(CountryTable) - - -def test_row_footer_total(): - class CountryTable(tables.Table): - name = tables.Column() - population = tables.Column( - footer=lambda table: 'Total: {}'.format( - sum(x['population'] for x in table.data) +class FaqTest(SimpleTestCase): + def test_row_counter_using_templateColumn(self): + class CountryTable(tables.Table): + counter = tables.TemplateColumn('{{ row_counter }}') + name = tables.Column() + + expected = '' + + table = CountryTable(TEST_DATA) + html = table.as_html(build_request()) + assert expected in html + + # the counter should start at zero the second time too + table = CountryTable(TEST_DATA) + html = table.as_html(build_request()) + assert expected in html + + def test_row_footer_total(self): + class CountryTable(tables.Table): + name = tables.Column() + population = tables.Column( + footer=lambda table: 'Total: {}'.format( + sum(x['population'] for x in table.data) + ) ) - ) - table = CountryTable(TEST_DATA) - html = table.as_html(build_request()) + table = CountryTable(TEST_DATA) + html = table.as_html(build_request()) - assert '' in html + columns = parse(html).findall('.//tfoot/tr')[-1].findall('td') + assert columns[1].text == 'Total: 77740000' diff -Nru django-tables-1.14.2/tests/test_footer.py django-tables-1.21.2/tests/test_footer.py --- django-tables-1.14.2/tests/test_footer.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_footer.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,7 +1,9 @@ # coding: utf-8 +from django.test import SimpleTestCase + import django_tables2 as tables -from .utils import build_request +from .utils import build_request, parse MEMORY_DATA = [ {'name': 'Queensland', 'country': 'Australia', 'population': 4750500}, @@ -11,58 +13,93 @@ ] -def test_has_footer_is_False_without_footer(): - class Table(tables.Table): - name = tables.Column() - country = tables.Column() - population = tables.Column() - - table = Table(MEMORY_DATA) - assert table.has_footer() is False - - -def test_footer(): - class Table(tables.Table): - name = tables.Column() - country = tables.Column(footer='Total:') - population = tables.Column( - footer=lambda table: sum(x['population'] for x in table.data) - ) - - table = Table(MEMORY_DATA) - assert table.has_footer() is True - - html = table.as_html(build_request('/')) - - assert '' in html - assert '' in html - - -def test_footer_disable_on_table(): - ''' - Showing the footer can be disabled using show_footer argument to the Table - constructor - ''' - class Table(tables.Table): - name = tables.Column() - country = tables.Column(footer='Total:') - - table = Table(MEMORY_DATA, show_footer=False) - assert table.has_footer() is False - - -def test_footer_column_method(): - - class SummingColumn(tables.Column): - def render_footer(self, bound_column, table): - return sum(bound_column.accessor.resolve(row) for row in table.data) - - class Table(tables.Table): - name = tables.Column() - country = tables.Column(footer='Total:') - population = SummingColumn() +class FooterTest(SimpleTestCase): + def test_has_footer_is_False_without_footer(self): + class Table(tables.Table): + name = tables.Column() + country = tables.Column() + population = tables.Column() + + table = Table(MEMORY_DATA) + assert table.has_footer() is False + + def test_footer(self): + class Table(tables.Table): + name = tables.Column() + country = tables.Column(footer='Total:') + population = tables.Column( + footer=lambda table: sum(x['population'] for x in table.data) + ) + + table = Table(MEMORY_DATA) + assert table.has_footer() is True + html = table.as_html(build_request('/')) + + columns = parse(html).findall('.//tfoot/tr/td') + assert columns[1].text == 'Total:' + assert columns[2].text == '18833000' + + def test_footer_disable_on_table(self): + ''' + Showing the footer can be disabled using show_footer argument to the Table + constructor + ''' + class Table(tables.Table): + name = tables.Column() + country = tables.Column(footer='Total:') + + table = Table(MEMORY_DATA, show_footer=False) + assert table.has_footer() is False + + def test_footer_column_method(self): + class SummingColumn(tables.Column): + def render_footer(self, bound_column, table): + return sum( + bound_column.accessor.resolve(row) for row in table.data) + + class TestTable(tables.Table): + name = tables.Column() + country = tables.Column(footer='Total:') + population = SummingColumn() + + table = TestTable(MEMORY_DATA) + html = table.as_html(build_request('/')) + + columns = parse(html).findall('.//tfoot/tr/td') + assert columns[1].text == 'Total:' + assert columns[2].text == '18833000' + + def test_footer_has_class(self): + class SummingColumn(tables.Column): + def render_footer(self, bound_column, table): + return sum( + bound_column.accessor.resolve(row) for row in table.data) + + class TestTable(tables.Table): + name = tables.Column() + country = tables.Column(footer='Total:') + population = SummingColumn() + + table = TestTable(MEMORY_DATA) + html = table.as_html(build_request('/')) + + columns = parse(html).findall('.//tfoot/tr/td') + assert 'class' in columns[1].attrib + + def test_footer_custom_attriubtes(self): + class SummingColumn(tables.Column): + def render_footer(self, bound_column, table): + return sum( + bound_column.accessor.resolve(row) for row in table.data) + + class TestTable(tables.Table): + name = tables.Column() + country = tables.Column(footer='Total:', attrs={'tf': {'align': 'right'}}) + population = SummingColumn() + + table = TestTable(MEMORY_DATA) + table.columns['country'].attrs['tf'] = {'align': 'right'} + html = table.as_html(build_request('/')) - table = Table(MEMORY_DATA) - html = table.as_html(build_request('/')) - assert '' in html - assert '' in html + columns = parse(html).findall('.//tfoot/tr/td') + assert 'align' in columns[1].attrib diff -Nru django-tables-1.14.2/tests/test_models.py django-tables-1.21.2/tests/test_models.py --- django-tables-1.14.2/tests/test_models.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_models.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,18 +1,21 @@ # coding: utf-8 from __future__ import unicode_literals -import pytest +from collections import defaultdict + from django.db.models.functions import Length +from django.template import Context, Template +from django.test import TestCase from django.utils import six from django.utils.translation import override as translation_override import django_tables2 as tables +import mock from .app.models import Occupation, Person, PersonProxy -from .utils import assertNumQueries, build_request +from .utils import build_request, parse -pytestmark = pytest.mark.django_db -request = build_request('/') +request = build_request() class PersonTable(tables.Table): @@ -21,418 +24,520 @@ occupation = tables.Column() -def test_boundrows_iteration(): - occupation = Occupation.objects.create(name='Programmer') - Person.objects.create(first_name='Bradley', last_name='Ayers', occupation=occupation) - Person.objects.create(first_name='Chris', last_name='Doble', occupation=occupation) - - table = PersonTable(Person.objects.all()) - records = [row.record for row in table.rows] - expecteds = Person.objects.all() - for expected, actual in six.moves.zip(expecteds, records): - assert expected == actual - - -def test_model_table(): - ''' - The ``model`` option on a table causes the table to dynamically add columns - based on the fields. - ''' - class OccupationTable(tables.Table): - class Meta: - model = Occupation - - expected = ['id', 'name', 'region', 'boolean', 'boolean_with_choices'] - assert expected == list(OccupationTable.base_columns.keys()) - - class OccupationTable2(tables.Table): - extra = tables.Column() - - class Meta: - model = Occupation - - expected.append('extra') - assert expected == list(OccupationTable2.base_columns.keys()) - - # be aware here, we already have *models* variable, but we're importing - # over the top - from django.db import models - - class ComplexModel(models.Model): - char = models.CharField(max_length=200) - fk = models.ForeignKey('self', on_delete=models.CASCADE) - m2m = models.ManyToManyField('self') +class ModelsTest(TestCase): + def setUp(self): + occupation = Occupation.objects.create(name='Programmer') + Person.objects.create(first_name='Bradley', last_name='Ayers', occupation=occupation) + Person.objects.create(first_name='Chris', last_name='Doble', occupation=occupation) + + def test_boundrows_iteration(self): + table = PersonTable(Person.objects.all()) + expected = list(Person.objects.all()) + for i, actual in enumerate([row.record for row in table.rows]): + assert expected[i] == actual + + def test_should_support_rendering_multiple_times(self): + class MultiRenderTable(tables.Table): + name = tables.Column() + + # test queryset data + table = MultiRenderTable(Person.objects.all()) + assert table.as_html(request) == table.as_html(request) + + def test_doesnotexist_from_accessor_should_use_default(self): + class Table(tables.Table): + class Meta: + model = Person + default = 'abc' + fields = ('first_name', 'last_name', 'region') + + table = Table(Person.objects.all()) + assert table.rows[0].get_cell('first_name') == 'Bradley' + assert table.rows[0].get_cell('region') == 'abc' + + def test_unicode_field_names(self): + class Table(tables.Table): + class Meta: + model = Person + fields = (six.text_type('first_name'), ) - class Meta: - app_label = 'django_tables2_test' + table = Table(Person.objects.all()) + assert table.rows[0].get_cell('first_name') == 'Bradley' - class ComplexTable(tables.Table): - class Meta: - model = ComplexModel - assert ['id', 'char', 'fk'] == list(ComplexTable.base_columns.keys()) - - -def test_mixins(): - class TableMixin(tables.Table): - extra = tables.Column() - - class OccupationTable(TableMixin, tables.Table): - extra2 = tables.Column() - - class Meta: - model = Occupation - - expected = ['extra', 'id', 'name', 'region', 'boolean', 'boolean_with_choices', 'extra2'] - assert expected == list(OccupationTable.base_columns.keys()) - - -def test_column_verbose_name(): - ''' - When using queryset data as input for a table, default to using model field - verbose names rather than an autogenerated string based on the column name. - - However if a column does explicitly describe a verbose name, it should be - used. - ''' - class PersonTable(tables.Table): + def test_Meta_option_model_table(self): ''' - The test_colX columns are to test that the accessor is used to - determine the field on the model, rather than the column name. + The ``model`` option on a table causes the table to dynamically add columns + based on the fields. ''' - first_name = tables.Column() - fn1 = tables.Column(accessor='first_name') - fn2 = tables.Column(accessor='first_name.upper') - fn3 = tables.Column(accessor='last_name', verbose_name='OVERRIDE') - fn4 = tables.Column(accessor='last_name', verbose_name='override') - last_name = tables.Column() - ln1 = tables.Column(accessor='last_name') - ln2 = tables.Column(accessor='last_name.upper') - ln3 = tables.Column(accessor='last_name', verbose_name='OVERRIDE') - region = tables.Column(accessor='occupation.region.name') - r1 = tables.Column(accessor='occupation.region.name') - r2 = tables.Column(accessor='occupation.region.name.upper') - r3 = tables.Column(accessor='occupation.region.name', verbose_name='OVERRIDE') - trans_test = tables.Column() - trans_test_lazy = tables.Column() - - # The Person model has a ``first_name`` and ``last_name`` field, but only - # the ``last_name`` field has an explicit ``verbose_name`` set. This means - # that we should expect that the two columns that use the ``last_name`` - # field should both use the model's ``last_name`` field's ``verbose_name``, - # however both fields that use the ``first_name`` field should just use a - # titlised version of the column name as the column header. - table = PersonTable(Person.objects.all()) - - # Should be generated (capitalized column name) - assert 'First Name' == table.columns['first_name'].verbose_name - assert 'First Name' == table.columns['fn1'].verbose_name - assert 'First Name' == table.columns['fn2'].verbose_name - assert 'OVERRIDE' == table.columns['fn3'].verbose_name - assert 'override' == table.columns['fn4'].verbose_name - # Should use the titlised model field's verbose_name - assert 'Surname' == table.columns['last_name'].verbose_name - assert 'Surname' == table.columns['ln1'].verbose_name - assert 'Surname' == table.columns['ln2'].verbose_name - assert 'OVERRIDE' == table.columns['ln3'].verbose_name - assert 'Name' == table.columns['region'].verbose_name - assert 'Name' == table.columns['r1'].verbose_name - assert 'Name' == table.columns['r2'].verbose_name - assert 'OVERRIDE' == table.columns['r3'].verbose_name - assert 'Translation Test' == table.columns['trans_test'].verbose_name - assert 'Translation Test Lazy' == table.columns['trans_test_lazy'].verbose_name + class OccupationTable(tables.Table): + class Meta: + model = Occupation - # ------------------------------------------------------------------------- + expected = ['id', 'name', 'region', 'boolean', 'boolean_with_choices'] + assert expected == list(OccupationTable.base_columns.keys()) - # Now we'll try using a table with Meta.model - class PersonTable(tables.Table): - first_name = tables.Column(verbose_name='OVERRIDE') + class OccupationTable2(tables.Table): + extra = tables.Column() - class Meta: - model = Person + class Meta: + model = Occupation - # Issue #16 - table = PersonTable(Person.objects.all()) - assert 'Translation Test' == table.columns['trans_test'].verbose_name - assert 'Translation Test Lazy' == table.columns['trans_test_lazy'].verbose_name - assert 'Web Site' == table.columns['website'].verbose_name - assert 'Birthdate' == table.columns['birthdate'].verbose_name - assert 'OVERRIDE' == table.columns['first_name'].verbose_name + expected.append('extra') + assert expected == list(OccupationTable2.base_columns.keys()) - # Verbose name should be lazy if it comes from the model field and - # the column was not declared explicitly - class PersonTable(tables.Table): - class Meta: - model = Person + # be aware here, we already have *models* variable, but we're importing + # over the top + from django.db import models - table = PersonTable(Person.objects.all()) - assert type(table.columns['trans_test_lazy'].verbose_name) is not six.text_type - with translation_override('ua'): - assert 'Тест Ленивого Перекладу' == table.columns['trans_test_lazy'].verbose_name + class ComplexModel(models.Model): + char = models.CharField(max_length=200) + fk = models.ForeignKey('self', on_delete=models.CASCADE) + m2m = models.ManyToManyField('self') + class Meta: + app_label = 'django_tables2_test' -def test_data_verbose_name(): - table = tables.Table(Person.objects.all()) - assert table.data.verbose_name == 'person' - assert table.data.verbose_name_plural == 'people' + class ComplexTable(tables.Table): + class Meta: + model = ComplexModel + assert ['id', 'char', 'fk'] == list(ComplexTable.base_columns.keys()) + def test_mixins(self): + class TableMixin(tables.Table): + extra = tables.Column() -def test_field_choices_used_to_translated_value(): - ''' - When a model field uses the ``choices`` option, a table should render the - 'pretty' value rather than the database value. + class OccupationTable(TableMixin, tables.Table): + extra2 = tables.Column() - See issue #30 for details. - ''' - LANGUAGES = ( - ('en', 'English'), - ('ru', 'Russian'), - ) + class Meta: + model = Occupation - from django.db import models + expected = ['extra', 'id', 'name', 'region', 'boolean', 'boolean_with_choices', 'extra2'] + assert expected == list(OccupationTable.base_columns.keys()) - class Article(models.Model): - name = models.CharField(max_length=200) - language = models.CharField(max_length=200, choices=LANGUAGES) + def test_fields_empty_list_means_no_fields(self): + class Table(tables.Table): + class Meta: + model = Person + fields = () - class Meta: - app_label = 'django_tables2_test' + table = Table(Person.objects.all()) + assert len(table.columns.names()) == 0 - def __unicode__(self): - return self.name + def test_compound_ordering(self): + class SimpleTable(tables.Table): + name = tables.Column(order_by=('first_name', 'last_name')) - class ArticleTable(tables.Table): - class Meta: - model = Article - - table = ArticleTable([Article(name='English article', language='en'), - Article(name='Russian article', language='ru')]) - - assert 'English' == table.rows[0].get_cell('language') - assert 'Russian' == table.rows[1].get_cell('language') - - -def test_column_mapped_to_nonexistant_field(): - ''' - Issue #9 describes how if a Table has a column that has an accessor that - targets a non-existent field, a FieldDoesNotExist error is raised. - ''' - class FaultyPersonTable(PersonTable): - missing = tables.Column() - - table = FaultyPersonTable(Person.objects.all()) - table.as_html(request) # the bug would cause this to raise FieldDoesNotExist + table = SimpleTable(Person.objects.all(), order_by='name') + html = table.as_html(request) + self.assertEqual( + parse(html).findall('.//thead/tr/th/a')[0].attrib, + {'href': '?sort=-name'} + ) + def test_default_order(self): + ''' + If orderable=False, do not sort queryset. + https://github.com/bradleyayers/django-tables2/issues/204 + ''' + class PersonTable(tables.Table): + first_name = tables.Column() + last_name = tables.Column() + + table = PersonTable(PersonProxy.objects.all()) + table.data.order_by([]) + assert list(table.rows[0]) == ['Bradley', 'Ayers'] + + def test_fields_should_implicitly_set_sequence(self): + class PersonTable(tables.Table): + extra = tables.Column() + + class Meta: + model = Person + fields = ('last_name', 'first_name') + table = PersonTable(Person.objects.all()) + assert table.columns.names() == ['last_name', 'first_name', 'extra'] + + def test_model_properties_should_be_useable_for_columns(self): + class PersonTable(tables.Table): + class Meta: + model = Person + fields = ('name', 'first_name') + + table = PersonTable(Person.objects.all()) + assert list(table.rows[0]) == ['Bradley Ayers', 'Bradley'] + + def test_meta_fields_may_be_list(self): + class PersonTable(tables.Table): + class Meta: + model = Person + fields = ['name', 'first_name'] + + table = PersonTable(Person.objects.all()) + assert list(table.rows[0]) == ['Bradley Ayers', 'Bradley'] + + +class ColumnNameTest(TestCase): + def setUp(self): + for i in range(10): + Person.objects.create(first_name='Bob %d' % i, last_name='Builder') -def test_should_support_rendering_multiple_times(): - class MultiRenderTable(tables.Table): - name = tables.Column() + def test_column_verbose_name(self): + ''' + When using queryset data as input for a table, default to using model field + verbose names rather than an autogenerated string based on the column name. - # test queryset data - table = MultiRenderTable(Person.objects.all()) - assert table.as_html(request) == table.as_html(request) + However if a column does explicitly describe a verbose name, it should be + used. + ''' + class PersonTable(tables.Table): + ''' + The test_colX columns are to test that the accessor is used to + determine the field on the model, rather than the column name. + ''' + first_name = tables.Column() + fn1 = tables.Column(accessor='first_name') + fn2 = tables.Column(accessor='first_name.upper') + fn3 = tables.Column(accessor='last_name', verbose_name='OVERRIDE') + fn4 = tables.Column(accessor='last_name', verbose_name='override') + last_name = tables.Column() + ln1 = tables.Column(accessor='last_name') + ln2 = tables.Column(accessor='last_name.upper') + ln3 = tables.Column(accessor='last_name', verbose_name='OVERRIDE') + region = tables.Column(accessor='occupation.region.name') + r1 = tables.Column(accessor='occupation.region.name') + r2 = tables.Column(accessor='occupation.region.name.upper') + r3 = tables.Column(accessor='occupation.region.name', verbose_name='OVERRIDE') + trans_test = tables.Column() + trans_test_lazy = tables.Column() + + # The Person model has a ``first_name`` and ``last_name`` field, but only + # the ``last_name`` field has an explicit ``verbose_name`` set. This means + # that we should expect that the two columns that use the ``last_name`` + # field should both use the model's ``last_name`` field's ``verbose_name``, + # however both fields that use the ``first_name`` field should just use a + # titlised version of the column name as the column header. + table = PersonTable(Person.objects.all()) + + # Should be generated (capitalized column name) + assert 'First Name' == table.columns['first_name'].verbose_name + assert 'First Name' == table.columns['fn1'].verbose_name + assert 'First Name' == table.columns['fn2'].verbose_name + assert 'OVERRIDE' == table.columns['fn3'].verbose_name + assert 'override' == table.columns['fn4'].verbose_name + # Should use the titlised model field's verbose_name + assert 'Surname' == table.columns['last_name'].verbose_name + assert 'Surname' == table.columns['ln1'].verbose_name + assert 'Surname' == table.columns['ln2'].verbose_name + assert 'OVERRIDE' == table.columns['ln3'].verbose_name + assert 'Name' == table.columns['region'].verbose_name + assert 'Name' == table.columns['r1'].verbose_name + assert 'Name' == table.columns['r2'].verbose_name + assert 'OVERRIDE' == table.columns['r3'].verbose_name + assert 'Translation Test' == table.columns['trans_test'].verbose_name + assert 'Translation Test Lazy' == table.columns['trans_test_lazy'].verbose_name + + def test_using_Meta_model(self): + # Now we'll try using a table with Meta.model + class PersonTable(tables.Table): + first_name = tables.Column(verbose_name='OVERRIDE') + + class Meta: + model = Person + + # Issue #16 + table = PersonTable(Person.objects.all()) + assert 'Translation Test' == table.columns['trans_test'].verbose_name + assert 'Translation Test Lazy' == table.columns['trans_test_lazy'].verbose_name + assert 'Web Site' == table.columns['website'].verbose_name + assert 'Birthdate' == table.columns['birthdate'].verbose_name + assert 'OVERRIDE' == table.columns['first_name'].verbose_name + + # Verbose name should be lazy if it comes from the model field and + # the column was not declared explicitly + class PersonTable(tables.Table): + class Meta: + model = Person + + table = PersonTable(Person.objects.all()) + assert type(table.columns['trans_test_lazy'].verbose_name) is not six.text_type + with translation_override('ua'): + assert 'Тест Ленивого Перекладу' == table.columns['trans_test_lazy'].verbose_name + + def test_data_verbose_name(self): + table = tables.Table(Person.objects.all()) + assert table.data.verbose_name == 'person' + assert table.data.verbose_name_plural == 'people' + + def test_column_named_delete(self): + class DeleteTable(tables.Table): + delete = tables.TemplateColumn('[delete button]', verbose_name='') + + class Meta: + model = Person + fields = ('name', 'delete') + + person1 = Person.objects.create(first_name='Jan', last_name='Pieter') + person2 = Person.objects.create(first_name='John', last_name='Peter') + DeleteTable(Person.objects.all()).as_html(build_request()) -def test_ordering(): - class SimpleTable(tables.Table): - name = tables.Column(order_by=('first_name', 'last_name')) + assert Person.objects.get(pk=person1.pk) == person1 + assert Person.objects.get(pk=person2.pk) == person2 - table = SimpleTable(Person.objects.all(), order_by='name') - assert table.as_html(request) +class ModelFieldTest(TestCase): + def test_use_to_translated_value(self): + ''' + When a model field uses the ``choices`` option, a table should render the + 'pretty' value rather than the database value. -def test_default_order(): - ''' - If orderable=False, do not sort queryset. - https://github.com/bradleyayers/django-tables2/issues/204 - ''' - table = PersonTable(PersonProxy.objects.all()) - Person.objects.create(first_name='Foo', last_name='Bar') - Person.objects.create(first_name='Bradley', last_name='Ayers') - table.data.order_by([]) + See issue #30 for details. + ''' + LANGUAGES = ( + ('en', 'English'), + ('ru', 'Russian'), + ) - assert list(table.rows[0])[1] == 'Ayers' + from django.db import models + class Article(models.Model): + name = models.CharField(max_length=200) + language = models.CharField(max_length=200, choices=LANGUAGES) -def test_fields_should_implicitly_set_sequence(): - class PersonTable(tables.Table): - extra = tables.Column() + class Meta: + app_label = 'tests' - class Meta: - model = Person - fields = ('last_name', 'first_name') - table = PersonTable(Person.objects.all()) - assert table.columns.names() == ['last_name', 'first_name', 'extra'] + def __unicode__(self): + return self.name + class ArticleTable(tables.Table): + class Meta: + model = Article -def test_model_properties_should_be_useable_for_columns(): - class PersonTable(tables.Table): - class Meta: - model = Person - fields = ('name', 'first_name') + table = ArticleTable([Article(name='English article', language='en'), + Article(name='Russian article', language='ru')]) - Person.objects.create(first_name='Bradley', last_name='Ayers') - table = PersonTable(Person.objects.all()) - assert list(table.rows[0]) == ['Bradley Ayers', 'Bradley'] + assert 'English' == table.rows[0].get_cell('language') + assert 'Russian' == table.rows[1].get_cell('language') + def test_column_mapped_to_nonexistant_field(self): + ''' + Issue #9 describes how if a Table has a column that has an accessor that + targets a non-existent field, a FieldDoesNotExist error is raised. + ''' + class FaultyPersonTable(PersonTable): + missing = tables.Column() -def test_meta_fields_may_be_list(): - class PersonTable(tables.Table): - class Meta: - model = Person - fields = ['name', 'first_name'] + table = FaultyPersonTable(Person.objects.all()) + table.as_html(request) # the bug would cause this to raise FieldDoesNotExist - Person.objects.create(first_name='Bradley', last_name='Ayers') - table = PersonTable(Person.objects.all()) - assert list(table.rows[0]) == ['Bradley Ayers', 'Bradley'] +class OrderingDataTest(TestCase): + NAMES = ('Bradley Ayers', 'Stevie Armstrong', 'VeryLongFirstName VeryLongLastName') -def test_column_with_delete_accessor_shouldnt_delete_records(): - class PersonTable(tables.Table): - delete = tables.Column() + def setUp(self): + for name in self.NAMES: + first_name, last_name = name.split() + Person.objects.create(first_name=first_name, last_name=last_name) - Person.objects.create(first_name='Bradley', last_name='Ayers') - table = PersonTable(Person.objects.all()) - table.as_html(request) - assert Person.objects.get(first_name='Bradley') + def test_order_by_derived_from_queryset(self): + queryset = Person.objects.order_by('first_name', 'last_name', '-occupation__name') + class PersonTable(tables.Table): + name = tables.Column(order_by=('first_name', 'last_name')) + occupation = tables.Column(order_by=('occupation__name',)) -def test_order_by_derived_from_queryset(): - queryset = Person.objects.order_by('first_name', 'last_name', '-occupation__name') + assert PersonTable( + queryset.order_by('first_name', 'last_name', '-occupation__name') + ).order_by == ('name', '-occupation') - class PersonTable(tables.Table): - name = tables.Column(order_by=('first_name', 'last_name')) - occupation = tables.Column(order_by=('occupation__name',)) + class PersonTable(PersonTable): + class Meta: + order_by = ('occupation', ) - assert PersonTable( - queryset.order_by('first_name', 'last_name', '-occupation__name') - ).order_by == ('name', '-occupation') + assert PersonTable(queryset.all()).order_by == ('occupation', ) - class PersonTable(PersonTable): - class Meta: - order_by = ('occupation', ) + def test_queryset_table_data_supports_ordering(self): + class Table(tables.Table): + class Meta: + model = Person - assert PersonTable(queryset.all()).order_by == ('occupation', ) + table = Table(Person.objects.all()) + assert table.rows[0].get_cell('first_name') == 'Bradley' + table.order_by = '-first_name' + assert table.rows[0].get_cell('first_name') == 'VeryLongFirstName' + def test_queryset_table_data_supports_custom_ordering(self): + class Table(tables.Table): + class Meta: + model = Person + order_by = 'first_name' -def test_queryset_table_data_supports_ordering(): - class Table(tables.Table): - class Meta: - model = Person + def order_first_name(self, queryset, is_descending): + # annotate to order by length of first_name + last_name + queryset = queryset.annotate( + length=Length('first_name') + Length('last_name') + ).order_by(('-' if is_descending else '') + 'length') + return (queryset, True) - for name in ('Bradley Ayers', 'Stevie Armstrong'): - first_name, last_name = name.split() - Person.objects.create(first_name=first_name, last_name=last_name) + table = Table(Person.objects.all()) - table = Table(Person.objects.all()) - assert table.rows[0].get_cell('first_name') == 'Bradley' - table.order_by = '-first_name' - assert table.rows[0].get_cell('first_name') == 'Stevie' + # Shortest full names first + assert table.rows[0].get_cell('first_name') == 'Bradley' + # Longest full names first + table.order_by = '-first_name' + assert table.rows[0].get_cell('first_name') == 'VeryLongFirstName' -def test_queryset_table_data_supports_custom_ordering(): - class Table(tables.Table): - class Meta: - model = Person - order_by = 'first_name' - def order_first_name(self, queryset, is_descending): - # annotate to order by length of first_name + last_name - queryset = queryset.annotate( - length=Length('first_name') + Length('last_name') - ).order_by(('-' if is_descending else '') + 'length') - return (queryset, True) +class ModelSanityTest(TestCase): + def setUp(self): + for i in range(10): + Person.objects.create(first_name='Bob %d' % i, last_name='Builder') - for name in ('Bradley Ayers', 'Stevie Armstrong', 'VeryLongFirstName VeryLongLastName'): - first_name, last_name = name.split() - Person.objects.create(first_name=first_name, last_name=last_name) + def test_column_with_delete_accessor_shouldnt_delete_records(self): + class PersonTable(tables.Table): + delete = tables.Column() - table = Table(Person.objects.all()) + table = PersonTable(Person.objects.all()) + table.as_html(request) - # Shortest full names first - assert table.rows[0].get_cell('first_name') == 'Bradley' + self.assertEqual(Person.objects.all().count(), 10) - # Longest full names first - table.order_by = '-first_name' - assert table.rows[0].get_cell('first_name') == 'VeryLongFirstName' + def test_model__str__calls(self): + ''' + Model.__str__ should not be called when not necessary. + ''' + calls = defaultdict(int) + def counting__str__(self): + calls[self.pk] += 1 + return self.first_name -def test_doesnotexist_from_accessor_should_use_default(): - class Table(tables.Table): - class Meta: - model = Person - default = 'abc' - fields = ('first_name', 'last_name', 'region') + with mock.patch('tests.app.models.Person.__str__', counting__str__): + for i in range(1, 4): + Person.objects.create(first_name='Bob %d' % i, last_name='Builder') - Person.objects.create(first_name='Brad', last_name='Ayers') + class PersonTable(tables.Table): + edit = tables.Column() - table = Table(Person.objects.all()) - assert table.rows[0].get_cell('first_name') == 'Brad' - assert table.rows[0].get_cell('region') == 'abc' + class Meta: + model = Person + fields = ['first_name', 'last_name'] + assert calls == {} -def test_unicode_field_names(): - class Table(tables.Table): - class Meta: - model = Person - fields = (six.text_type('first_name'), ) + table = PersonTable(Person.objects.all()) + table.as_html(build_request()) - Person.objects.create(first_name='Brad') + assert calls == {} - table = Table(Person.objects.all()) - assert table.rows[0].get_cell('first_name') == 'Brad' + def test_render_table_template_tag_numqueries(self): + class PersonTable(tables.Table): + class Meta: + model = Person + per_page = 1 + request = build_request('/') -def test_foreign_key(): - class PersonTable(tables.Table): - class Meta: - model = Person - fields = ('foreign_key', ) + with self.assertNumQueries(0): + table = PersonTable(Person.objects.all()) - # TODO: implement + with self.assertNumQueries(1): + # one query for pagination: .count() + tables.RequestConfig(request).configure(table) + template = Template('{% load django_tables2 %}{% render_table table %}') + context = Context({'table': table, 'request': request}) -def test_fields_empty_list_means_no_fields(): - class Table(tables.Table): - class Meta: - model = Person - fields = () + with self.assertNumQueries(1): + # one query for page records + template.render(context) - table = Table(Person.objects.all()) - assert len(table.columns.names()) == 0 + with self.assertNumQueries(0): + # re-render should not produce extra queries + template.render(context) + # second page + request = build_request('/?page=2') + context = Context({'table': table, 'request': request}) -def test_column_named_delete(): - class DeleteTable(tables.Table): - delete = tables.TemplateColumn('[delete button]', verbose_name='') + with self.assertNumQueries(0): + # count is already done, not needed anymore + tables.RequestConfig(request).configure(table) - class Meta: - model = Person - fields = ('name', 'delete') + with self.assertNumQueries(1): + # one query for page records + template.render(context) - person1 = Person.objects.create(first_name='Jan', last_name='Pieter') - person2 = Person.objects.create(first_name='John', last_name='Peter') + def test_single_query_for_non_paginated_table(self): + ''' + A non-paginated table should not generate a query for each row, but only + one query fetch the rows. + ''' - DeleteTable(Person.objects.all()).as_html(build_request()) + class PersonTable(tables.Table): + class Meta: + model = Person + fields = ('first_name', 'last_name') + order_by = ('last_name', 'first_name') - assert Person.objects.get(pk=person1.pk) == person1 - assert Person.objects.get(pk=person2.pk) == person2 + table = PersonTable(Person.objects.all()) + with self.assertNumQueries(1): + list(table.as_values()) -def test_single_query_for_non_paginated_table(): - ''' - A non-paginated table should not generate a query for each row, but only - one query fetch the rows. - ''' - for i in range(10): - Person.objects.create(first_name='Bob %d' % i, last_name='Builder') + def test_as_html_db_queries_nonpaginated(self): + ''' + Basic tables without pagination should NOT result in a COUNT(*) being done, + but only fetch the rows. + ''' + class PersonTable(tables.Table): + class Meta: + model = Person + + with self.assertNumQueries(1): + html = PersonTable(Person.objects.all()).as_html(build_request()) + self.assertIn('Bob 0', html) + + +class TableFactoryTest(TestCase): + def test_factory(self): + occupation = Occupation.objects.create(name='Programmer') + Person.objects.create(first_name='Bradley', last_name='Ayers', occupation=occupation) + persons = Person.objects.all() + Table = tables.table_factory(Person) + table = Table(persons) + self.assertIsInstance(table, tables.Table) + self.assertEqual(Table.__name__, 'PersonTable') + + def test_factory_fields_argument(self): + fields = ('username',) + Table = tables.table_factory(Person, fields=fields) + self.assertEqual(Table.Meta.fields, fields) + self.assertEqual(Table._meta.fields, fields) + + def test_factory_exclude_argument(self): + exclude = ('username',) + Table = tables.table_factory(Person, exclude=exclude) + self.assertEqual(Table.Meta.exclude, exclude) + self.assertEqual(Table._meta.exclude, exclude) + + def test_factory_localize_argument(self): + localize = ('username',) + Table = tables.table_factory(Person, localize=localize) + self.assertEqual(Table.Meta.localize, localize) + self.assertEqual(Table._meta.localize, localize) + + def test_factory_with_meta(self): + fields = ('first_name',) - class PersonTable(tables.Table): - class Meta: - model = Person - fields = ('first_name', 'last_name') - order_by = ('last_name', 'first_name') + class TableWithMeta(tables.Table): + first_name = tables.Column() - table = PersonTable(Person.objects.all()) + class Meta: + fields = ('first_name',) - with assertNumQueries(1): - list(table.as_values()) + Table = tables.table_factory(Person, table=TableWithMeta) + self.assertEqual(Table.Meta.fields, fields) diff -Nru django-tables-1.14.2/tests/test_ordering.py django-tables-1.21.2/tests/test_ordering.py --- django-tables-1.14.2/tests/test_ordering.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_ordering.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,7 +1,9 @@ # coding: utf-8 from __future__ import absolute_import, unicode_literals -import pytest +from datetime import datetime + +from django.test import TestCase from django.utils import six import django_tables2 as tables @@ -31,283 +33,277 @@ order_by = 'alpha' -def test_ordering(): - # fallback to Table.Meta - assert ('alpha', ) == OrderedTable([], order_by=None).order_by == OrderedTable([]).order_by - - # values of order_by are wrapped in tuples before being returned - assert OrderedTable([], order_by='alpha').order_by == ('alpha', ) - assert OrderedTable([], order_by=('beta', )).order_by == ('beta', ) - - table = OrderedTable([]) - table.order_by = [] - assert () == table.order_by == OrderedTable([], order_by=[]).order_by - - table = OrderedTable([]) - table.order_by = () - assert () == table.order_by == OrderedTable([], order_by=()).order_by - - table = OrderedTable([]) - table.order_by = '' - assert () == table.order_by == OrderedTable([], order_by='').order_by - - # apply an ordering - table = UnorderedTable([]) - table.order_by = 'alpha' - assert ('alpha', ) == UnorderedTable([], order_by='alpha').order_by == table.order_by - - table = OrderedTable([]) - table.order_by = 'alpha' - assert ('alpha', ) == OrderedTable([], order_by='alpha').order_by == table.order_by - - # let's check the data - table = OrderedTable(MEMORY_DATA, order_by='beta') - assert 3 == table.rows[0].get_cell('i') - - table = OrderedTable(MEMORY_DATA, order_by='-beta') - assert 1 == table.rows[0].get_cell('i') - - # allow fallback to Table.Meta.order_by - table = OrderedTable(MEMORY_DATA) - assert 1 == table.rows[0].get_cell('i') - - # column's can't be ordered if they're not allowed to be - class TestTable2(tables.Table): - a = tables.Column(orderable=False) - b = tables.Column() - - table = TestTable2([], order_by='a') - assert table.order_by == () - - table = TestTable2([], order_by='b') - assert table.order_by == ('b', ) - - # ordering disabled by default - class TestTable3(tables.Table): - a = tables.Column(orderable=True) - b = tables.Column() - - class Meta: - orderable = False - - table = TestTable3([], order_by='a') - assert table.order_by == ('a', ) - - table = TestTable3([], order_by='b') - assert table.order_by == () - - table = TestTable3([], orderable=True, order_by='b') - assert table.order_by == ('b', ) - +class OrderingTest(TestCase): + def test_ordering(self): + # fallback to Table.Meta + assert ('alpha', ) == OrderedTable([], order_by=None).order_by == OrderedTable([]).order_by + + # values of order_by are wrapped in tuples before being returned + assert OrderedTable([], order_by='alpha').order_by == ('alpha', ) + assert OrderedTable([], order_by=('beta', )).order_by == ('beta', ) + + table = OrderedTable([]) + table.order_by = [] + assert () == table.order_by == OrderedTable([], order_by=[]).order_by + + table = OrderedTable([]) + table.order_by = () + assert () == table.order_by == OrderedTable([], order_by=()).order_by + + table = OrderedTable([]) + table.order_by = '' + assert () == table.order_by == OrderedTable([], order_by='').order_by + + # apply an ordering + table = UnorderedTable([]) + table.order_by = 'alpha' + assert ('alpha', ) == UnorderedTable([], order_by='alpha').order_by == table.order_by + + table = OrderedTable([]) + table.order_by = 'alpha' + assert ('alpha', ) == OrderedTable([], order_by='alpha').order_by == table.order_by + + # let's check the data + table = OrderedTable(MEMORY_DATA, order_by='beta') + assert 3 == table.rows[0].get_cell('i') -def test_ordering_different_types(): - from datetime import datetime - - data = [ - {'i': 1, 'alpha': datetime.now(), 'beta': [1]}, - {'i': {}, 'alpha': None, 'beta': ''}, - {'i': 2, 'alpha': None, 'beta': []}, - ] - - table = OrderedTable(data) - assert '—' == table.rows[0].get_cell('alpha') - - table = OrderedTable(data, order_by='i') - if six.PY3: - assert {} == table.rows[0].get_cell('i') - else: + table = OrderedTable(MEMORY_DATA, order_by='-beta') assert 1 == table.rows[0].get_cell('i') - table = OrderedTable(data, order_by='beta') - assert [] == table.rows[0].get_cell('beta') - - -brad = {'first_name': 'Bradley', 'last_name': 'Ayers'} -brad2 = {'first_name': 'Bradley', 'last_name': 'Fake'} -chris = {'first_name': 'Chris', 'last_name': 'Doble'} -davina = {'first_name': 'Davina', 'last_name': 'Adisusila'} -ross = {'first_name': 'Ross', 'last_name': 'Ayers'} - -people = [brad, brad2, chris, davina, ross] - - -def test_multi_column_ordering_by_table(): - - class PersonTable(tables.Table): - first_name = tables.Column() - last_name = tables.Column() - - table = PersonTable(people, order_by=('first_name', 'last_name')) - assert [brad, brad2, chris, davina, ross] == [r.record for r in table.rows] - - table = PersonTable(people, order_by=('first_name', '-last_name')) - assert [brad2, brad, chris, davina, ross] == [r.record for r in table.rows] - - -def test_multi_column_ordering_by_column(): - # let's try column order_by using multiple keys - class PersonTable(tables.Table): - name = tables.Column(order_by=('first_name', 'last_name')) - - # add 'name' key for each person. - for person in people: - person['name'] = '{p[first_name]} {p[last_name]}'.format(p=person) - assert brad['name'] == 'Bradley Ayers' - - table = PersonTable(people, order_by='name') - assert [brad, brad2, chris, davina, ross] == [r.record for r in table.rows] - - table = PersonTable(people, order_by='-name') - assert [ross, davina, chris, brad2, brad] == [r.record for r in table.rows] - - -@pytest.mark.django_db -def test_ordering_by_custom_field(): - ''' - When defining a custom field in a table, as name=tables.Column() with - methods to render and order render_name and order_name, sorting by this - column causes an error if the custom field is not in last position. - (issue #413) - ''' - - Person.objects.create(first_name='Alice', last_name='Beta') - Person.objects.create(first_name='Bob', last_name='Alpha') - - from django.db.models import F, Value - from django.db.models.functions import Concat - - class PersonTable(tables.Table): - first_name = tables.Column() - last_name = tables.Column() - full_name = tables.Column() - - def render_full_name(self, record): - return record.last_name + ' ' + record.first_name - - def order_full_name(self, queryset, is_descending): - queryset = queryset.annotate( - full_name=Concat(F('last_name'), Value(' '), F('first_name')) - ).order_by(('-' if is_descending else '') + 'full_name') - return queryset, True - - class Meta: - model = Person - fields = ('first_name', 'last_name', 'full_name') - - table = PersonTable(Person.objects.all()) - request = build_request('/?sort=full_name&sort=first_name') - RequestConfig(request).configure(table) - - assert table.rows[0].record.first_name == 'Bob' - - -def test_list_table_data_supports_ordering(): - class Table(tables.Table): - name = tables.Column() - - data = [ - {'name': 'Bradley'}, - {'name': 'Davina'}, - ] - - table = Table(data) - assert table.rows[0].get_cell('name') == 'Bradley' - table.order_by = '-name' - assert table.rows[0].get_cell('name') == 'Davina' - - -def test_ordering_non_database_data(): - class Table(tables.Table): - name = tables.Column() - country = tables.Column() - - data = [ - {'name': 'Adrian', 'country': 'Australia'}, - {'name': 'Adrian', 'country': 'Brazil'}, - {'name': 'Audrey', 'country': 'Chile'}, - {'name': 'Bassie', 'country': 'Belgium'}, - ] - table = Table(data, order_by=('-name', '-country')) - - assert table.rows[0].get_cell('name') == 'Bassie' - assert table.rows[1].get_cell('name') == 'Audrey' - assert table.rows[2].get_cell('name') == 'Adrian' - assert table.rows[2].get_cell('country') == 'Brazil' - assert table.rows[3].get_cell('name') == 'Adrian' - assert table.rows[3].get_cell('country') == 'Australia' - + # allow fallback to Table.Meta.order_by + table = OrderedTable(MEMORY_DATA) + assert 1 == table.rows[0].get_cell('i') -def test_table_ordering_attributes(): - class Table(tables.Table): - alpha = tables.Column() - beta = tables.Column() + # column's can't be ordered if they're not allowed to be + class TestTable2(tables.Table): + a = tables.Column(orderable=False) + b = tables.Column() + + table = TestTable2([], order_by='a') + assert table.order_by == () + + table = TestTable2([], order_by='b') + assert table.order_by == ('b', ) + + # ordering disabled by default + class TestTable3(tables.Table): + a = tables.Column(orderable=True) + b = tables.Column() + + class Meta: + orderable = False + + table = TestTable3([], order_by='a') + assert table.order_by == ('a', ) + + table = TestTable3([], order_by='b') + assert table.order_by == () + + table = TestTable3([], orderable=True, order_by='b') + assert table.order_by == ('b', ) + + def test_ordering_different_types(self): + data = [ + {'i': 1, 'alpha': datetime.now(), 'beta': [1]}, + {'i': {}, 'alpha': None, 'beta': ''}, + {'i': 2, 'alpha': None, 'beta': []}, + ] + + table = OrderedTable(data) + assert '—' == table.rows[0].get_cell('alpha') + + table = OrderedTable(data, order_by='i') + if six.PY3: + assert {} == table.rows[0].get_cell('i') + else: + assert 1 == table.rows[0].get_cell('i') + + table = OrderedTable(data, order_by='beta') + assert [] == table.rows[0].get_cell('beta') + + def get_people(self): + brad = {'first_name': 'Bradley', 'last_name': 'Ayers'} + brad2 = {'first_name': 'Bradley', 'last_name': 'Fake'} + chris = {'first_name': 'Chris', 'last_name': 'Doble'} + davina = {'first_name': 'Davina', 'last_name': 'Adisusila'} + ross = {'first_name': 'Ross', 'last_name': 'Ayers'} + + return [brad, brad2, chris, davina, ross] + + def test_multi_column_ordering_by_table(self): + class PersonTable(tables.Table): + first_name = tables.Column() + last_name = tables.Column() + + people = self.get_people() + brad, brad2, chris, davina, ross = people + + table = PersonTable(people, order_by=('first_name', 'last_name')) + assert [brad, brad2, chris, davina, ross] == [r.record for r in table.rows] + + table = PersonTable(people, order_by=('first_name', '-last_name')) + assert [brad2, brad, chris, davina, ross] == [r.record for r in table.rows] + + def test_multi_column_ordering_by_column(self): + # let's try column order_by using multiple keys + class PersonTable(tables.Table): + name = tables.Column(order_by=('first_name', 'last_name')) + + people = self.get_people() + brad, brad2, chris, davina, ross = people + + # add 'name' key for each person. + for person in people: + person['name'] = '{p[first_name]} {p[last_name]}'.format(p=person) + assert brad['name'] == 'Bradley Ayers' + + table = PersonTable(people, order_by='name') + assert [brad, brad2, chris, davina, ross] == [r.record for r in table.rows] + + table = PersonTable(people, order_by='-name') + assert [ross, davina, chris, brad2, brad] == [r.record for r in table.rows] + + def test_ordering_by_custom_field(self): + ''' + When defining a custom field in a table, as name=tables.Column() with + methods to render and order render_name and order_name, sorting by this + column causes an error if the custom field is not in last position. + (issue #413) + ''' + + Person.objects.create(first_name='Alice', last_name='Beta') + Person.objects.create(first_name='Bob', last_name='Alpha') + + from django.db.models import F, Value + from django.db.models.functions import Concat + + class PersonTable(tables.Table): + first_name = tables.Column() + last_name = tables.Column() + full_name = tables.Column() + + def render_full_name(self, record): + return record.last_name + ' ' + record.first_name + + def order_full_name(self, queryset, is_descending): + queryset = queryset.annotate( + full_name=Concat(F('last_name'), Value(' '), F('first_name')) + ).order_by(('-' if is_descending else '') + 'full_name') + return queryset, True + + class Meta: + model = Person + fields = ('first_name', 'last_name', 'full_name') + + table = PersonTable(Person.objects.all()) + request = build_request('/?sort=full_name&sort=first_name') + RequestConfig(request).configure(table) + + assert table.rows[0].record.first_name == 'Bob' + + def test_list_table_data_supports_ordering(self): + class Table(tables.Table): + name = tables.Column() + + data = [ + {'name': 'Bradley'}, + {'name': 'Davina'}, + ] + + table = Table(data) + assert table.rows[0].get_cell('name') == 'Bradley' + table.order_by = '-name' + assert table.rows[0].get_cell('name') == 'Davina' + + def test_ordering_non_database_data(self): + class Table(tables.Table): + name = tables.Column() + country = tables.Column() + + data = [ + {'name': 'Adrian', 'country': 'Australia'}, + {'name': 'Adrian', 'country': 'Brazil'}, + {'name': 'Audrey', 'country': 'Chile'}, + {'name': 'Bassie', 'country': 'Belgium'}, + ] + table = Table(data, order_by=('-name', '-country')) + + assert table.rows[0].get_cell('name') == 'Bassie' + assert table.rows[1].get_cell('name') == 'Audrey' + assert table.rows[2].get_cell('name') == 'Adrian' + assert table.rows[2].get_cell('country') == 'Brazil' + assert table.rows[3].get_cell('name') == 'Adrian' + assert table.rows[3].get_cell('country') == 'Australia' + + def test_table_ordering_attributes(self): + class Table(tables.Table): + alpha = tables.Column() + beta = tables.Column() - table = Table(MEMORY_DATA, attrs={ - 'th': { - 'class': 'custom-header-class', - '_ordering': { - 'orderable': 'sortable', - 'ascending': 'ascend', - 'descending': 'descend', + table = Table(MEMORY_DATA, attrs={ + 'th': { + 'class': 'custom-header-class', + '_ordering': { + 'orderable': 'sortable', + 'ascending': 'ascend', + 'descending': 'descend', + }, }, - }, - }, order_by='alpha') - - assert 'sortable' in table.columns[0].attrs['th']['class'] - assert 'ascend' in table.columns[0].attrs['th']['class'] - assert 'custom-header-class' in table.columns[1].attrs['th']['class'] + }, order_by='alpha') + assert 'sortable' in table.columns[0].attrs['th']['class'] + assert 'ascend' in table.columns[0].attrs['th']['class'] + assert 'custom-header-class' in table.columns[1].attrs['th']['class'] + + def test_table_ordering_attributes_in_meta(self): + class Table(tables.Table): + alpha = tables.Column() + beta = tables.Column() + + class Meta(OrderedTable.Meta): + attrs = { + 'th': { + 'class': 'custom-header-class-in-meta', + '_ordering': { + 'orderable': 'sortable', + 'ascending': 'ascend', + 'descending': 'descend', + }, + } + } -def test_table_ordering_attributes_in_meta(): - class Table(tables.Table): - alpha = tables.Column() - beta = tables.Column() + table = Table(MEMORY_DATA) - class Meta(OrderedTable.Meta): - attrs = { + assert 'sortable' in table.columns[0].attrs['th']['class'] + assert 'ascend' in table.columns[0].attrs['th']['class'] + assert 'custom-header-class-in-meta' in table.columns[1].attrs['th']['class'] + + def test_column_ordering_attributes(self): + class Table(tables.Table): + alpha = tables.Column(attrs={ 'th': { - 'class': 'custom-header-class-in-meta', + 'class': 'custom-header-class', '_ordering': { - 'orderable': 'sortable', - 'ascending': 'ascend', - 'descending': 'descend', - }, + 'orderable': 'sort', + 'ascending': 'ascending' + } } - } - - table = Table(MEMORY_DATA) - - assert 'sortable' in table.columns[0].attrs['th']['class'] - assert 'ascend' in table.columns[0].attrs['th']['class'] - assert 'custom-header-class-in-meta' in table.columns[1].attrs['th']['class'] + }) + beta = tables.Column(attrs={ + 'th': { + '_ordering': { + 'orderable': 'canOrder', + } + }, + 'td': { + 'class': 'cell-2' + } + }) + table = Table(MEMORY_DATA, attrs={'class': 'only-on-table'}, order_by='alpha') -def test_column_ordering_attributes(): - class Table(tables.Table): - alpha = tables.Column(attrs={ - 'th': { - 'class': 'custom-header-class', - '_ordering': { - 'orderable': 'sort', - 'ascending': 'ascending' - } - } - }) - beta = tables.Column(attrs={ - 'th': { - '_ordering': { - 'orderable': 'canOrder', - } - }, - 'td': { - 'class': 'cell-2' - } - }) - - table = Table(MEMORY_DATA, attrs={'class': 'only-on-table'}, order_by='alpha') - - assert 'only-on-table' not in table.columns[0].attrs['th']['class'] - assert 'custom-header-class' in table.columns[0].attrs['th']['class'] - assert 'ascending' in table.columns[0].attrs['th']['class'] - assert 'sort' in table.columns[0].attrs['th']['class'] - assert 'canOrder' in table.columns[1].attrs['th']['class'] + assert 'only-on-table' not in table.columns[0].attrs['th']['class'] + assert 'custom-header-class' in table.columns[0].attrs['th']['class'] + assert 'ascending' in table.columns[0].attrs['th']['class'] + assert 'sort' in table.columns[0].attrs['th']['class'] + assert 'canOrder' in table.columns[1].attrs['th']['class'] diff -Nru django-tables-1.14.2/tests/test_pinned_rows.py django-tables-1.21.2/tests/test_pinned_rows.py --- django-tables-1.14.2/tests/test_pinned_rows.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_pinned_rows.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,6 +1,5 @@ # encoding: utf-8 -import pytest -from django.db import models +from django.test import SimpleTestCase import django_tables2 as tables from django_tables2.rows import BoundRow, BoundRows @@ -9,7 +8,6 @@ class PinnedObj(object): - def __init__(self, name, age): self.name = name self.age = age @@ -30,154 +28,149 @@ return [{'occupation': 'Sum age', 'age': 130}] -def test_bound_rows_with_pinned_data(): - record = {'name': 'Grzegorz', 'age': 30, 'occupation': 'programmer'} - table = SimpleTable([record]) - row = table.rows[0] - - with pytest.raises(IndexError): - table.rows[1] - - with pytest.raises(IndexError): - row.get_cell(3) - - assert row.get_cell('name') == record['name'] - assert row.get_cell('occupation') == record['occupation'] - assert row.get_cell('age') == record['age'] - - with pytest.raises(KeyError): - row.get_cell('gamma') - - assert 'name' in row - assert 'occupation' in row - assert 'gamma' not in row - - -def test_as_html(): - ''' - Ensure that html render correctly. - ''' - request = build_request('/') - table = SimpleTable([{'name': 'Grzegorz', 'age': 30, 'occupation': 'programmer'}]) - root = parse(table.as_html(request)) - - # One row for header - assert len(root.findall('.//thead/tr')) == 1 - - # In the header should be 3 cell. - assert len(root.findall('.//thead/tr/th')) == 3 - - # In the body, should be one original record and 3 pinned rows. - assert len(root.findall('.//tbody/tr')) == 4 - assert len(root.findall('.//tbody/tr/td')) == 12 - - # First top pinned row. - tr = root.findall('.//tbody/tr') - td = tr[0].findall('td') - assert td[0].text == "Ron" - assert td[1].text == table.default - assert td[2].text == "90" - - # Second top pinned row. - td = tr[1].findall('td') - assert td[0].text == "Jon" - assert td[1].text == table.default - assert td[2].text == '10' - - # Original row - td = tr[2].findall('td') - assert td[0].text == "Grzegorz" - assert td[1].text == 'programmer' - assert td[2].text == '30' - - # First bottom pinned row. - td = tr[3].findall('td') - assert td[0].text == table.default - assert td[1].text == 'Sum age' - assert td[2].text == '130' - - -def test_pinned_row_attrs(): - ''' - Testing attrs for pinned rows. - ''' - - pinned_row_attrs = { - 'class': 'super-mega-row', - 'data-foo': 'bar' - } - - request = build_request('/') - record = {'name': 'Grzegorz', 'age': 30, 'occupation': 'programmer'} - table = SimpleTable([record], pinned_row_attrs=pinned_row_attrs) - html = table.as_html(request) - - assert 'pinned-row' in html - assert 'super-mega-row' in html - assert 'data-foo' in html - - -def test_ordering(): - ''' - Change sorting should not change ordering pinned rows. - ''' - request = build_request('/') - records = [ - {'name': 'Alex', 'age': 42, 'occupation': 'programmer'}, - {'name': 'Greg', 'age': 30, 'occupation': 'programmer'}, - ] - - table = SimpleTable(records, order_by='age') - root = parse(table.as_html(request)) - tr = root.findall('.//tbody/tr') - assert tr[0].findall('td')[2].text == '90' - assert tr[1].findall('td')[2].text == '10' - assert tr[2].findall('td')[2].text == '30' - assert tr[3].findall('td')[2].text == '42' - assert tr[4].findall('td')[2].text == '130' - - table = SimpleTable(records, order_by='-age') - root = parse(table.as_html(request)) - tr = root.findall('.//tbody/tr') - assert tr[0].findall('td')[2].text == '90' - assert tr[1].findall('td')[2].text == '10' - assert tr[2].findall('td')[2].text == '42' - assert tr[3].findall('td')[2].text == '30' - assert tr[4].findall('td')[2].text == '130' - - -def test_bound_rows_getitem(): - ''' - Testing BoundRows.__getitem__() method. - Checking the return class for simple value and for slice. - Ensure that inside of BoundRows pinned rows are included in length. - ''' - records = [ - {'name': 'Greg', 'age': 30, 'occupation': 'policeman'}, - {'name': 'Alex', 'age': 42, 'occupation': 'programmer'}, - {'name': 'John', 'age': 72, 'occupation': 'official'}, - ] - - table = SimpleTable(records, order_by='age') - assert isinstance(table.rows[0], BoundRow) is True - assert isinstance(table.rows[0:2], BoundRows) is True - assert table.rows[0:2][0].get_cell('name') == 'Greg' - assert len(table.rows[:]) == 6 - - -def test_uniterable_pinned_data(): - ''' - Ensure that, when data for pinned rows are not iterable, - the ValueError exception will be raised. - ''' - class FooTable(tables.Table): - col = tables.Column() - - def get_top_pinned_data(self): - return 1 - - tab = FooTable([1, 2, 3]) - - with pytest.raises(ValueError): - for row in tab.rows: - pass +class PinnedRowsTest(SimpleTestCase): + def test_bound_rows_with_pinned_data(self): + record = {'name': 'Grzegorz', 'age': 30, 'occupation': 'programmer'} + table = SimpleTable([record]) + row = table.rows[0] + + with self.assertRaises(IndexError): + table.rows[1] + + with self.assertRaises(IndexError): + row.get_cell(3) + + assert row.get_cell('name') == record['name'] + assert row.get_cell('occupation') == record['occupation'] + assert row.get_cell('age') == record['age'] + + with self.assertRaises(KeyError): + row.get_cell('gamma') + + assert 'name' in row + assert 'occupation' in row + assert 'gamma' not in row + + def test_as_html(self): + ''' + Ensure that html render correctly. + ''' + request = build_request('/') + table = SimpleTable([{'name': 'Grzegorz', 'age': 30, 'occupation': 'programmer'}]) + root = parse(table.as_html(request)) + + # One row for header + assert len(root.findall('.//thead/tr')) == 1 + + # In the header should be 3 cell. + assert len(root.findall('.//thead/tr/th')) == 3 + + # In the body, should be one original record and 3 pinned rows. + assert len(root.findall('.//tbody/tr')) == 4 + assert len(root.findall('.//tbody/tr/td')) == 12 + + # First top pinned row. + tr = root.findall('.//tbody/tr') + td = tr[0].findall('td') + assert td[0].text == "Ron" + assert td[1].text == table.default + assert td[2].text == "90" + + # Second top pinned row. + td = tr[1].findall('td') + assert td[0].text == "Jon" + assert td[1].text == table.default + assert td[2].text == '10' + + # Original row + td = tr[2].findall('td') + assert td[0].text == "Grzegorz" + assert td[1].text == 'programmer' + assert td[2].text == '30' + + # First bottom pinned row. + td = tr[3].findall('td') + assert td[0].text == table.default + assert td[1].text == 'Sum age' + assert td[2].text == '130' + + def test_pinned_row_attrs(self): + ''' + Testing attrs for pinned rows. + ''' + pinned_row_attrs = { + 'class': 'super-mega-row', + 'data-foo': 'bar' + } + + request = build_request('/') + record = {'name': 'Grzegorz', 'age': 30, 'occupation': 'programmer'} + table = SimpleTable([record], pinned_row_attrs=pinned_row_attrs) + html = table.as_html(request) + + assert 'pinned-row' in html + assert 'super-mega-row' in html + assert 'data-foo' in html + + def test_ordering(self): + ''' + Change sorting should not change ordering pinned rows. + ''' + request = build_request('/') + records = [ + {'name': 'Alex', 'age': 42, 'occupation': 'programmer'}, + {'name': 'Greg', 'age': 30, 'occupation': 'programmer'}, + ] + + table = SimpleTable(records, order_by='age') + root = parse(table.as_html(request)) + tr = root.findall('.//tbody/tr') + assert tr[0].findall('td')[2].text == '90' + assert tr[1].findall('td')[2].text == '10' + assert tr[2].findall('td')[2].text == '30' + assert tr[3].findall('td')[2].text == '42' + assert tr[4].findall('td')[2].text == '130' + + table = SimpleTable(records, order_by='-age') + root = parse(table.as_html(request)) + tr = root.findall('.//tbody/tr') + assert tr[0].findall('td')[2].text == '90' + assert tr[1].findall('td')[2].text == '10' + assert tr[2].findall('td')[2].text == '42' + assert tr[3].findall('td')[2].text == '30' + assert tr[4].findall('td')[2].text == '130' + + def test_bound_rows_getitem(self): + ''' + Testing BoundRows.__getitem__() method. + Checking the return class for simple value and for slice. + Ensure that inside of BoundRows pinned rows are included in length. + ''' + records = [ + {'name': 'Greg', 'age': 30, 'occupation': 'policeman'}, + {'name': 'Alex', 'age': 42, 'occupation': 'programmer'}, + {'name': 'John', 'age': 72, 'occupation': 'official'}, + ] + + table = SimpleTable(records, order_by='age') + assert isinstance(table.rows[0], BoundRow) is True + assert isinstance(table.rows[0:2], BoundRows) is True + assert table.rows[0:2][0].get_cell('name') == 'Greg' + assert len(table.rows[:]) == 6 + + def test_uniterable_pinned_data(self): + ''' + Ensure that, when data for pinned rows are not iterable, + the ValueError exception will be raised. + ''' + class FooTable(tables.Table): + col = tables.Column() + + def get_top_pinned_data(self): + return 1 + + tab = FooTable([1, 2, 3]) + + with self.assertRaises(ValueError): + for row in tab.rows: + pass diff -Nru django-tables-1.14.2/tests/test_rows.py django-tables-1.21.2/tests/test_rows.py --- django-tables-1.14.2/tests/test_rows.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_rows.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,167 +1,187 @@ # coding: utf-8 -import pytest +from itertools import count + from django.db import models +from django.test import SimpleTestCase import django_tables2 as tables -def test_bound_rows(): - class SimpleTable(tables.Table): - name = tables.Column() - - data = [ - {'name': 'Bradley'}, - {'name': 'Chris'}, - {'name': 'Davina'}, - ] - - table = SimpleTable(data) - - # iteration - records = [] - for row in table.rows: - records.append(row.record) - assert records == data - - -def test_bound_row(): - class SimpleTable(tables.Table): - name = tables.Column() - occupation = tables.Column() - age = tables.Column() - - record = {'name': 'Bradley', 'age': 20, 'occupation': 'programmer'} - - table = SimpleTable([record]) - row = table.rows[0] - - # integer indexing into a row - assert row.get_cell(0) == record['name'] - assert row.get_cell(1) == record['occupation'] - assert row.get_cell(2) == record['age'] - - with pytest.raises(IndexError): - row.get_cell(3) - - # column name indexing into a row - assert row.get_cell('name') == record['name'] - assert row.get_cell('occupation') == record['occupation'] - assert row.get_cell('age') == record['age'] - - with pytest.raises(KeyError): - row.get_cell('gamma') - - # row should support contains check - assert 'name' in row - assert 'occupation' in row - assert 'gamma' not in row - - -def test_row_attrs(): - ''' - If a callable returns an empty string, do not add a space to the CSS class - attribute. (#416) - ''' - from itertools import count - counter = count() - - class Table(tables.Table): - name = tables.Column() - - class Meta(object): - row_attrs = { - 'class': lambda: '' if next(counter) % 2 == 0 else 'bla' - } - - table = Table([ - {'name': 'Brian'}, - {'name': 'Thomas'}, - {'name': 'John'} - ]) - - assert table.rows[0].attrs['class'] == 'even' - assert table.rows[1].attrs['class'] == 'bla odd' - assert table.rows[1].attrs['class'] == 'even' - - -def test_get_cell_display(): - - class A(models.Model): - foo = models.CharField( - max_length=1, - choices=( - ('a', 'valA'), - ('b', 'valB'), +class RowsTest(SimpleTestCase): + def test_bound_rows(self): + class SimpleTable(tables.Table): + name = tables.Column() + + data = [ + {'name': 'Bradley'}, + {'name': 'Chris'}, + {'name': 'Davina'}, + ] + + table = SimpleTable(data) + + # iteration + records = [] + for row in table.rows: + records.append(row.record) + assert records == data + + def test_bound_row(self): + class SimpleTable(tables.Table): + name = tables.Column() + occupation = tables.Column() + age = tables.Column() + + record = {'name': 'Bradley', 'age': 20, 'occupation': 'programmer'} + + table = SimpleTable([record]) + row = table.rows[0] + + # integer indexing into a row + assert row.get_cell(0) == record['name'] + assert row.get_cell(1) == record['occupation'] + assert row.get_cell(2) == record['age'] + + with self.assertRaises(IndexError): + row.get_cell(3) + + # column name indexing into a row + assert row.get_cell('name') == record['name'] + assert row.get_cell('occupation') == record['occupation'] + assert row.get_cell('age') == record['age'] + + with self.assertRaises(KeyError): + row.get_cell('gamma') + + # row should support contains check + assert 'name' in row + assert 'occupation' in row + assert 'gamma' not in row + + def test_boud_row_cells(self): + class SimpleTable(tables.Table): + name = tables.Column() + occupation = tables.Column() + age = tables.Column() + + record = {'name': 'Bradley', 'age': 20, 'occupation': 'programmer'} + + table = SimpleTable([record]) + row = table.rows[0] + self.assertEqual(row.cells.name, record['name']) + self.assertEqual(row.cells.age, record['age']) + self.assertEqual(row.cells.name, row.get_cell('name')) + self.assertEqual(row.cells[0], record['name']) + self.assertEqual(row.cells[0], row.get_cell(0)) + + with self.assertRaises(IndexError): + row.cells[3] + + with self.assertRaises(KeyError): + row.cells['gamma'] + + def test_row_attrs(self): + ''' + If a callable returns an empty string, do not add a space to the CSS class + attribute. (#416) + ''' + counter = count() + + class Table(tables.Table): + name = tables.Column() + + class Meta(object): + row_attrs = { + 'class': lambda: '' if next(counter) % 2 == 0 else 'bla' + } + + table = Table([ + {'name': 'Brian'}, + {'name': 'Thomas'}, + {'name': 'John'} + ]) + + assert table.rows[0].attrs['class'] == 'even' + assert table.rows[1].attrs['class'] == 'bla odd' + assert table.rows[1].attrs['class'] == 'even' + + def test_get_cell_display(self): + + class A(models.Model): + foo = models.CharField( + max_length=1, + choices=( + ('a', 'valA'), + ('b', 'valB'), + ) ) - ) - class Meta: - app_label = 'django_tables2_test' + class Meta: + app_label = 'tests' + + class B(models.Model): + a = models.ForeignKey(A, on_delete=models.CASCADE) - class B(models.Model): - a = models.ForeignKey(A, on_delete=models.CASCADE) + class Meta: + app_label = 'tests' - class Meta: - app_label = 'django_tables2_test' - - class C(models.Model): - b = models.ForeignKey(B, on_delete=models.CASCADE) - - class Meta: - app_label = 'django_tables2_test' - - class Tab(tables.Table): - a = tables.Column(accessor="b.a.foo") - - class Meta: - model = C - - a = A(foo='a') - b = B(a=a) - c = C(b=b) - - tab = Tab([c]) - row = tab.rows[0] - assert row.get_cell('a') == 'valA' - - -def test_even_odd_css_class(): - ''' - Test for BoundRow.get_even_odd_css_class() method - ''' - class SimpleTable(tables.Table): - foo = tables.Column() - - def get_top_pinned_data(self): - return [{'foo': 'top-pinned'}] - - def get_bottom_pinned_data(self): - return [{'foo': 'bottom-pinned'}] - - data = [ - {'foo', 'bar'}, - {'foo', 'bas'}, - {'foo', 'baz'}, - ] - - simple_table = SimpleTable(data) - - count = 0 - prev = None - for row in simple_table.rows: - if prev: - assert row.get_even_odd_css_class() != prev.get_even_odd_css_class() - prev = row - count += 1 - - # count should be 5 because: - # First row is a top pinned row. - # Three defaults rows with data. - # Last row is a bottom pinned row. - assert count == 5 - - # Important! - # Length of data is five because pinned rows are added to data list. - # If pinned rows are added only in the iteration on BoundRows, - # then nothing will display if there are *only* pinned rows - assert len(simple_table.rows) == 5 + class C(models.Model): + b = models.ForeignKey(B, on_delete=models.CASCADE) + + class Meta: + app_label = 'tests' + + class Tab(tables.Table): + a = tables.Column(accessor="b.a.foo") + + class Meta: + model = C + + a = A(foo='a') + b = B(a=a) + c = C(b=b) + + tab = Tab([c]) + row = tab.rows[0] + assert row.get_cell('a') == 'valA' + + def test_even_odd_css_class(self): + ''' + Test for BoundRow.get_even_odd_css_class() method + ''' + class SimpleTable(tables.Table): + foo = tables.Column() + + def get_top_pinned_data(self): + return [{'foo': 'top-pinned'}] + + def get_bottom_pinned_data(self): + return [{'foo': 'bottom-pinned'}] + + data = [ + {'foo', 'bar'}, + {'foo', 'bas'}, + {'foo', 'baz'}, + ] + + simple_table = SimpleTable(data) + + count = 0 + prev = None + for row in simple_table.rows: + if prev: + assert row.get_even_odd_css_class() != prev.get_even_odd_css_class() + prev = row + count += 1 + + # count should be 5 because: + # First row is a top pinned row. + # Three defaults rows with data. + # Last row is a bottom pinned row. + assert count == 5 + + # Important! + # Length of data is five because pinned rows are added to data list. + # If pinned rows are added only in the iteration on BoundRows, + # then nothing will display if there are *only* pinned rows + assert len(simple_table.rows) == 5 diff -Nru django-tables-1.14.2/tests/test_tabledata.py django-tables-1.21.2/tests/test_tabledata.py --- django-tables-1.14.2/tests/test_tabledata.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_tabledata.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,108 +1,122 @@ # coding: utf-8 from __future__ import unicode_literals -import pytest +from django.test import TestCase +from django_tables2 import Table from django_tables2.data import TableData, TableListData, TableQuerysetData from .app.models import Person -def test_TableData_factory_invalid_data_None(): - with pytest.raises(ValueError): - TableData.from_data(None, table={}) +class TableDataFactoryTest(TestCase): + def test_invalid_data_None(self): + with self.assertRaises(ValueError): + TableData.from_data(None) + + def test_invalid_data_int(self): + with self.assertRaises(ValueError): + TableData.from_data(1) - -def test_TableData_factory_invalid_data_int(): - with pytest.raises(ValueError): - TableData.from_data(1, table={}) - - -def test_TableData_factory_invalid_data_classes(): - class Klass(object): - pass - - with pytest.raises(ValueError): - TableData.from_data(Klass(), table={}) - - class Bad(object): - def __len__(self): + def test_invalid_data_classes(self): + class Klass(object): pass - with pytest.raises(ValueError): - TableData.from_data(Bad(), table={}) + with self.assertRaises(ValueError): + TableData.from_data(Klass()) + + class Bad(object): + def __len__(self): + pass + + with self.assertRaises(ValueError): + TableData.from_data(Bad()) + + def test_valid_QuerySet(self): + data = TableData.from_data(Person.objects.all()) + assert isinstance(data, TableQuerysetData) + + def test_valid_list_of_dicts(self): + data = TableData.from_data([{'name': 'John'}, {'name': 'Pete'}]) + assert isinstance(data, TableListData) + assert len(data) == 2 + + def test_valid_tuple_of_dicts(self): + data = TableData.from_data(({'name': 'John'}, {'name': 'Pete'})) + assert isinstance(data, TableListData) + assert len(data) == 2 + + def test_valid_class(self): + class Datasource(object): + def __len__(self): + return 1 + + def __getitem__(self, pos): + if pos != 0: + raise IndexError() + return {'a': 1} + + data = TableData.from_data(Datasource()) + assert len(data) == 1 + + +class TableDataTest(TestCase): + def test_knows_its_default_name(self): + data = TableData.from_data([{}]) + assert data.verbose_name == 'item' + assert data.verbose_name_plural == 'items' + + def test_knows_its_name(self): + data = TableData.from_data(Person.objects.all()) + + assert data.verbose_name == 'person' + assert data.verbose_name_plural == 'people' + + +def generator(max_value): + for i in range(max_value): + yield { + 'foo': i, + 'bar': chr(i), + 'baz': hex(i), + 'inv': max_value - i + } + + +class TableListsDataTest(TestCase): + def test_TableListData_basic_list(self): + list_data = list(generator(100)) + data = TableListData(list_data) + + assert len(list_data) == len(data) + assert data.verbose_name == 'item' + assert data.verbose_name_plural == 'items' + + def test_TableListData_with_verbose_name(self): + ''' + TableListData uses the attributes on the listlike object to generate + it's verbose_name. + ''' + class listlike(list): + verbose_name = 'unit' + verbose_name_plural = 'units' + + list_data = listlike(generator(100)) + data = TableListData(list_data) + + assert len(list_data) == len(data) + assert data.verbose_name == 'unit' + assert data.verbose_name_plural == 'units' + + +class CustomTableQuerysetData(TestCase): + def test_custom_TableData(self): + '''If TableQuerysetData._length is set, no count() query will be performed''' + for i in range(20): + Person.objects.create(first_name='first {}'.format(i)) + data = TableQuerysetData(Person.objects.all()) + data._length = 10 -@pytest.mark.django_db -def test_TableData_factory_valid_QuerySet(): - data = TableData.from_data(Person.objects.all(), table={}) - assert isinstance(data, TableQuerysetData) - - -def test_TableData_factory_valid_list_of_dicts(): - data = TableData.from_data([{'name': 'John'}, {'name': 'Pete'}], table={}) - assert isinstance(data, TableListData) - assert len(data) == 2 - - -def test_TableData_factory_valid_tuple_of_dicts(): - data = TableData.from_data(({'name': 'John'}, {'name': 'Pete'}), table={}) - assert isinstance(data, TableListData) - assert len(data) == 2 - - -def test_TableData_factory_valid_class(): - class Datasource(object): - def __len__(self): - return 1 - - def __getitem__(self, pos): - if pos != 0: - raise IndexError() - return {'a': 1} - - data = TableData.from_data(Datasource(), table={}) - assert len(data) == 1 - - -def test_tabledata_knows_its_default_name(): - data = TableData.from_data([{}], table={}) - assert data.verbose_name == 'item' - assert data.verbose_name_plural == 'items' - - -def test_tabledata_knows_its_name(): - data = TableData.from_data(Person.objects.all(), table={}) - - assert data.verbose_name == 'person' - assert data.verbose_name_plural == 'people' - - -# def test_tabledata_is_untouched(): -# ''' -# Ensure that data that is provided to the table (the datasource) is not -# modified by table operations. -# ''' -# -# MEMORY_DATA = [ -# {'i': 2, 'alpha': 'b', 'beta': 'b'}, -# {'i': 1, 'alpha': 'a', 'beta': 'c'}, -# {'i': 3, 'alpha': 'c', 'beta': 'a'}, -# ] -# -# class Table(tables.Table): -# i = tables.Column() -# alpha = tables.Column() -# beta = tables.Column() -# -# original_data = deepcopy(MEMORY_DATA) -# -# table = Table(MEMORY_DATA) -# table.order_by = 'i' -# list(table.rows) -# assert MEMORY_DATA == original_data -# -# table = Table(MEMORY_DATA) -# table.order_by = 'beta' -# list(table.rows) -# assert MEMORY_DATA == original_data + table = Table(data=data) + self.assertEqual(len(table.data), 10) diff -Nru django-tables-1.14.2/tests/test_templates.py django-tables-1.21.2/tests/test_templates.py --- django-tables-1.14.2/tests/test_templates.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_templates.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,9 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals -import pytest from django.template import Context, Template -from django.test import TransactionTestCase +from django.test import SimpleTestCase, TestCase, override_settings from django.utils.translation import override as translation_override from django.utils.translation import ugettext_lazy @@ -14,16 +13,6 @@ from .utils import build_request, parse -def test_template_override_in_settings(settings): - settings.DJANGO_TABLES2_TEMPLATE = 'foo/bar.html' - - class Table(tables.Table): - column = tables.Column() - - table = Table({}) - assert table.template == 'foo/bar.html' - - class CountryTable(tables.Table): name = tables.Column() capital = tables.Column(orderable=False, @@ -46,66 +35,73 @@ ] -def test_as_html(): - request = build_request('/') - table = CountryTable(MEMORY_DATA) - root = parse(table.as_html(request)) - assert len(root.findall('.//thead/tr')) == 1 - assert len(root.findall('.//thead/tr/th')) == 4 - assert len(root.findall('.//tbody/tr')) == 4 - assert len(root.findall('.//tbody/tr/td')) == 16 - - # no data with no empty_text - table = CountryTable([]) - root = parse(table.as_html(request)) - assert 1 == len(root.findall('.//thead/tr')) - assert 4 == len(root.findall('.//thead/tr/th')) - assert 0 == len(root.findall('.//tbody/tr')) - - # no data WITH empty_text - table = CountryTable([], empty_text='this table is empty') - root = parse(table.as_html(request)) - assert 1 == len(root.findall('.//thead/tr')) - assert 4 == len(root.findall('.//thead/tr/th')) - assert 1 == len(root.findall('.//tbody/tr')) - assert 1 == len(root.findall('.//tbody/tr/td')) - assert int(root.find('.//tbody/tr/td').get('colspan')) == len(root.findall('.//thead/tr/th')) - assert root.find('.//tbody/tr/td').text == 'this table is empty' - - # data without header - table = CountryTable(MEMORY_DATA, show_header=False) - root = parse(table.as_html(request)) - assert len(root.findall('.//thead')) == 0 - assert len(root.findall('.//tbody/tr')) == 4 - assert len(root.findall('.//tbody/tr/td')) == 16 - - # with custom template - table = CountryTable([], template='django_tables2/table.html') - table.as_html(request) - - -def test_custom_rendering(): - '''For good measure, render some actual templates.''' - countries = CountryTable(MEMORY_DATA) - context = Context({'countries': countries}) - - # automatic and manual column verbose names - template = Template('{% for column in countries.columns %}{{ column }}/' - '{{ column.name }} {% endfor %}') - result = ('Name/name Capital/capital Population Size/population ' - 'Phone Ext./calling_code ') - assert result == template.render(context) - - # row values - template = Template('{% for row in countries.rows %}{% for value in row %}' - '{{ value }} {% endfor %}{% endfor %}') - result = ('Germany Berlin 83 49 France — 64 33 Netherlands Amsterdam ' - '— 31 Austria — 8 43 ') - assert result == template.render(context) +class TemplateTest(TestCase): + @override_settings(DJANGO_TABLES2_TEMPLATE='foo/bar.html') + def test_template_override_in_settings(self): + class Table(tables.Table): + column = tables.Column() + + table = Table({}) + assert table.template_name == 'foo/bar.html' + def test_as_html(self): + request = build_request('/') + table = CountryTable(MEMORY_DATA) + root = parse(table.as_html(request)) + assert len(root.findall('.//thead/tr')) == 1 + assert len(root.findall('.//thead/tr/th')) == 4 + assert len(root.findall('.//tbody/tr')) == 4 + assert len(root.findall('.//tbody/tr/td')) == 16 + + # no data with no empty_text + table = CountryTable([]) + root = parse(table.as_html(request)) + assert 1 == len(root.findall('.//thead/tr')) + assert 4 == len(root.findall('.//thead/tr/th')) + assert 0 == len(root.findall('.//tbody/tr')) + + # no data WITH empty_text + table = CountryTable([], empty_text='this table is empty') + root = parse(table.as_html(request)) + assert 1 == len(root.findall('.//thead/tr')) + assert 4 == len(root.findall('.//thead/tr/th')) + assert 1 == len(root.findall('.//tbody/tr')) + assert 1 == len(root.findall('.//tbody/tr/td')) + assert int(root.find('.//tbody/tr/td').get('colspan')) == len(root.findall('.//thead/tr/th')) + assert root.find('.//tbody/tr/td').text == 'this table is empty' + + # data without header + table = CountryTable(MEMORY_DATA, show_header=False) + root = parse(table.as_html(request)) + assert len(root.findall('.//thead')) == 0 + assert len(root.findall('.//tbody/tr')) == 4 + assert len(root.findall('.//tbody/tr/td')) == 16 + + # with custom template + table = CountryTable([], template_name='django_tables2/table.html') + table.as_html(request) + + def test_custom_rendering(self): + '''For good measure, render some actual templates.''' + countries = CountryTable(MEMORY_DATA) + context = Context({'countries': countries}) + + # automatic and manual column verbose names + template = Template('{% for column in countries.columns %}{{ column }}/' + '{{ column.name }} {% endfor %}') + result = ('Name/name Capital/capital Population Size/population ' + 'Phone Ext./calling_code ') + assert result == template.render(context) + + # row values + template = Template('{% for row in countries.rows %}{% for value in row %}' + '{{ value }} {% endfor %}{% endfor %}') + result = ('Germany Berlin 83 49 France — 64 33 Netherlands Amsterdam ' + '— 31 Austria — 8 43 ') + assert result == template.render(context) -@pytest.mark.django_db -class TestQueries(TransactionTestCase): + +class TestQueries(TestCase): def test_as_html_db_queries(self): class PersonTable(tables.Table): class Meta: @@ -117,6 +113,11 @@ PersonTable(Person.objects.all()).as_html(request) def test_render_table_db_queries(self): + ''' + Paginated tables should result in two queries: + - one query for pagination: .count() + - one query for records on the current page: .all()[start:end] + ''' Person.objects.create(first_name='brad', last_name='ayers') Person.objects.create(first_name='davina', last_name='adisusila') @@ -127,215 +128,211 @@ request = build_request('/') with self.assertNumQueries(2): - # one query for pagination: .count() - # one query for page records: .all()[start:end] request = build_request('/') table = PersonTable(Person.objects.all()) RequestConfig(request).configure(table) - # render - (Template('{% load django_tables2 %}{% render_table table %}') - .render(Context({'table': table, 'request': request}))) + html = Template('{% load django_tables2 %}{% render_table table %}') \ + .render(Context({'table': table, 'request': request})) + self.assertIn('brad', html) + self.assertIn('ayers', html) -def test_localization_check(settings): - def get_cond_localized_table(localizeit=None): - ''' - helper function for defining Table class conditionally - ''' - class TestTable(tables.Table): - name = tables.Column(verbose_name="my column", localize=localizeit) - return TestTable +class TemplateLocalizeTest(TestCase): simple_test_data = [{'name': 1234.5}] expected_results = { None: '1234.5', False: '1234.5', True: '1 234,5' # non-breaking space } - request = build_request('/') - - # no localization - html = get_cond_localized_table(None)(simple_test_data).as_html(request) - assert ''.format(expected_results[None]) in html - - # unlocalize - html = get_cond_localized_table(False)(simple_test_data).as_html(request) - assert ''.format(expected_results[False]) in html - - settings.USE_L10N = True - settings.USE_THOUSAND_SEPARATOR = True - with translation_override('pl'): - # with default polish locales and enabled thousand separator - # 1234.5 is formatted as "1 234,5" with nbsp - html = get_cond_localized_table(True)(simple_test_data).as_html(request) - assert ''.format(expected_results[True]) in html - - # with localize = False there should be no formatting - html = get_cond_localized_table(False)(simple_test_data).as_html(request) - assert ''.format(expected_results[False]) in html - - # with localize = None and USE_L10N = True - # there should be the same formatting as with localize = True - html = get_cond_localized_table(None)(simple_test_data).as_html(request) - assert ''.format(expected_results[True]) in html + def assert_cond_localized_table(self, localizeit=None, expected=None): + ''' + helper function for defining Table class conditionally + ''' + class TestTable(tables.Table): + name = tables.Column(verbose_name="my column", localize=localizeit) + self.assert_table_localization(TestTable, expected) -def test_localization_check_in_meta(settings): - class TableNoLocalize(tables.Table): - name = tables.Column(verbose_name='my column') + def assert_table_localization(self, TestTable, expected): + html = TestTable(self.simple_test_data).as_html(build_request()) + self.assertIn( + ''.format(self.expected_results[expected]), + html + ) - class Meta: - default = '---' + def test_localization_check(self): + self.assert_cond_localized_table(None, None) + # unlocalize + self.assert_cond_localized_table(False, False) - class TableLocalize(tables.Table): - name = tables.Column(verbose_name='my column') + @override_settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True) + def test_localization_different_locale(self): + with translation_override('pl'): + # with default polish locales and enabled thousand separator + # 1234.5 is formatted as "1 234,5" with nbsp + self.assert_cond_localized_table(True, True) + + # with localize = False there should be no formatting + self.assert_cond_localized_table(False, False) + + # with localize = None and USE_L10N = True + # there should be the same formatting as with localize = True + self.assert_cond_localized_table(None, True) + + def test_localization_check_in_meta(self): + class TableNoLocalize(tables.Table): + name = tables.Column(verbose_name='my column') - class Meta: - default = '---' - localize = ('name', ) + class Meta: + default = '---' - class TableUnlocalize(tables.Table): - name = tables.Column(verbose_name='my column') + self.assert_table_localization(TableNoLocalize, None) - class Meta: - default = '---' - unlocalize = ('name', ) + @override_settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True) + def test_localization_check_in_meta_different_locale(self): + class TableNoLocalize(tables.Table): + name = tables.Column(verbose_name='my column') - class TableLocalizePrecedence(tables.Table): - name = tables.Column(verbose_name='my column') + class Meta: + default = '---' - class Meta: - default = '---' - unlocalize = ('name', ) - localize = ('name', ) + class TableLocalize(tables.Table): + name = tables.Column(verbose_name='my column') - simple_test_data = [{'name': 1234.5}] - expected_results = { - None: '1234.5', - False: '1234.5', - True: '1{0}234,5'.format(' ') # non-breaking space - } - request = build_request('/') - # No localize - html = TableNoLocalize(simple_test_data).as_html(request) - assert ''.format(expected_results[None]) in html - - settings.USE_L10N = True - settings.USE_THOUSAND_SEPARATOR = True - - with translation_override('pl'): - # the same as in localization_check. - # with localization and polish locale we get formatted output - html = TableNoLocalize(simple_test_data).as_html(request) - assert ''.format(expected_results[True]) in html - - # localize - html = TableLocalize(simple_test_data).as_html(request) - assert ''.format(expected_results[True]) in html + class Meta: + default = '---' + localize = ('name', ) - # unlocalize - html = TableUnlocalize(simple_test_data).as_html(request) - assert ''.format(expected_results[False]) in html + class TableUnlocalize(tables.Table): + name = tables.Column(verbose_name='my column') - # test unlocalize higher precedence - html = TableLocalizePrecedence(simple_test_data).as_html(request) - assert ''.format(expected_results[False]) in html + class Meta: + default = '---' + unlocalize = ('name', ) + class TableLocalizePrecedence(tables.Table): + name = tables.Column(verbose_name='my column') -def test_localization_of_pagination_string(): - class Table(tables.Table): - foo = tables.Column(verbose_name='my column') - bar = tables.Column() + class Meta: + default = '---' + unlocalize = ('name', ) + localize = ('name', ) + + with translation_override('pl'): + # the same as in localization_check. + # with localization and polish locale we get formatted output + self.assert_table_localization(TableNoLocalize, True) + + # localize + self.assert_table_localization(TableLocalize, True) + + # unlocalize + self.assert_table_localization(TableUnlocalize, False) + + # test unlocalize has higher precedence + self.assert_table_localization(TableLocalizePrecedence, False) + + def test_localization_of_pagination_string(self): + class Table(tables.Table): + foo = tables.Column(verbose_name='my column') + bar = tables.Column() - class Meta: - default = '---' + class Meta: + default = '---' - table = Table(map(lambda x: [x, x + 100], range(40))) - request = build_request('/') - RequestConfig(request, paginate={'per_page': 10}).configure(table) + table = Table(map(lambda x: [x, x + 100], range(40))) + request = build_request('/') + RequestConfig(request, paginate={'per_page': 10}).configure(table) - with translation_override('en'): - assert 'Page 1 of 4' in table.as_html(request) + with translation_override('en'): + assert 'Page 1 of 4' in table.as_html(request) - with translation_override('nl'): - assert 'Pagina 1 van 4' in table.as_html(request) + with translation_override('nl'): + assert 'Pagina 1 van 4' in table.as_html(request) - with translation_override('it'): - assert 'Pagina 1 di 4' in table.as_html(request) + with translation_override('it'): + assert 'Pagina 1 di 4' in table.as_html(request) - with translation_override('nb'): - assert 'Side 1 av 4' in table.as_html(request) + with translation_override('nb'): + assert 'Side 1 av 4' in table.as_html(request) class BootstrapTable(CountryTable): class Meta: - template = 'django_tables2/bootstrap.html' + template_name = 'django_tables2/bootstrap.html' prefix = 'bootstrap-' per_page = 2 -def test_boostrap_template(): - table = BootstrapTable(MEMORY_DATA) - request = build_request('/') - RequestConfig(request).configure(table) - - template = Template('{% load django_tables2 %}{% render_table table %}') - html = template.render(Context({'request': request, 'table': table})) - root = parse(html) - assert len(root.findall('.//thead/tr')) == 1 - assert len(root.findall('.//thead/tr/th')) == 4 - assert len(root.findall('.//tbody/tr')) == 2 - assert len(root.findall('.//tbody/tr/td')) == 8 - - assert root.find('./ul[@class="pager list-inline"]/li[@class="cardinality"]/small').text.strip() == 'Page 1 of 2' - # make sure the link is prefixed - assert root.find('./ul[@class="pager list-inline"]/li[@class="next"]/a').get('href') == '?bootstrap-page=2' +class BootstrapTemplateTest(SimpleTestCase): + def test_boostrap_template(self): + table = BootstrapTable(MEMORY_DATA) + request = build_request('/') + RequestConfig(request).configure(table) + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({'request': request, 'table': table})) + root = parse(html) + assert len(root.findall('.//thead/tr')) == 1 + assert len(root.findall('.//thead/tr/th')) == 4 + assert len(root.findall('.//tbody/tr')) == 2 + assert len(root.findall('.//tbody/tr/td')) == 8 + + self.assertEqual( + root.find('./ul[@class="pager list-inline"]/li[@class="cardinality"]/small').text.strip(), + 'Page 1 of 2' + ) + # make sure the link is prefixed + self.assertEqual( + root.find('./ul[@class="pager list-inline"]/li[@class="next"]/a').get('href'), + '?bootstrap-page=2' + ) + + def test_bootstrap_responsive_template(self): + class BootstrapResponsiveTable(BootstrapTable): + class Meta(BootstrapTable.Meta): + template_name = 'django_tables2/bootstrap-responsive.html' -class SemanticTable(CountryTable): - class Meta: - template = 'django_tables2/semantic.html' - prefix = 'semantic-' - per_page = 2 + table = BootstrapResponsiveTable(MEMORY_DATA) + request = build_request('/') + RequestConfig(request).configure(table) + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({'request': request, 'table': table})) + root = parse(html) + assert len(root.findall('.//thead/tr')) == 1 + assert len(root.findall('.//thead/tr/th')) == 4 + assert len(root.findall('.//tbody/tr')) == 2 + assert len(root.findall('.//tbody/tr/td')) == 8 + + pager = './/ul/li[@class="cardinality"]/small' + assert root.find(pager).text.strip() == 'Page 1 of 2' + + +class SemanticTemplateTest(SimpleTestCase): + def test_semantic_template(self): + class SemanticTable(CountryTable): + class Meta: + template_name = 'django_tables2/semantic.html' + prefix = 'semantic-' + per_page = 2 -def test_semantic_template(): - table = SemanticTable(MEMORY_DATA) - request = build_request('/') - RequestConfig(request).configure(table) - - template = Template('{% load django_tables2 %}{% render_table table %}') - html = template.render(Context({'request': request, 'table': table})) - root = parse(html) - assert len(root.findall('.//thead/tr')) == 1 - assert len(root.findall('.//thead/tr/th')) == 4 - assert len(root.findall('.//tbody/tr')) == 2 - assert len(root.findall('.//tbody/tr/td')) == 8 - - pager = './/tfoot/tr/th/div[@class="ui right floated pagination menu"]/div[@class="item"]' - assert root.find(pager).text.strip() == 'Page 1 of 2' - # make sure the link is prefixed - next_page = './/tfoot/tr/th/div[@class="ui right floated pagination menu"]/a[@class="icon item"]' - assert root.find(next_page).get('href') == '?semantic-page=2' - - -def test_bootstrap_responsive_template(): - class BootstrapResponsiveTable(BootstrapTable): - class Meta(BootstrapTable.Meta): - template = 'django_tables2/bootstrap-responsive.html' - - table = BootstrapResponsiveTable(MEMORY_DATA) - request = build_request('/') - RequestConfig(request).configure(table) - - template = Template('{% load django_tables2 %}{% render_table table %}') - html = template.render(Context({'request': request, 'table': table})) - root = parse(html) - assert len(root.findall('.//thead/tr')) == 1 - assert len(root.findall('.//thead/tr/th')) == 4 - assert len(root.findall('.//tbody/tr')) == 2 - assert len(root.findall('.//tbody/tr/td')) == 8 + table = SemanticTable(MEMORY_DATA) + request = build_request('/') + RequestConfig(request).configure(table) - pager = './/ul/li[@class="cardinality"]/small' - assert root.find(pager).text.strip() == 'Page 1 of 2' + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({'request': request, 'table': table})) + root = parse(html) + assert len(root.findall('.//thead/tr')) == 1 + assert len(root.findall('.//thead/tr/th')) == 4 + assert len(root.findall('.//tbody/tr')) == 2 + assert len(root.findall('.//tbody/tr/td')) == 8 + + pager = './/tfoot/tr/th/div[@class="ui right floated pagination menu"]/div[@class="item"]' + assert root.find(pager).text.strip() == 'Page 1 of 2' + # make sure the link is prefixed + next_page = './/tfoot/tr/th/div[@class="ui right floated pagination menu"]/a[@class="icon item"]' + assert root.find(next_page).get('href') == '?semantic-page=2' diff -Nru django-tables-1.14.2/tests/test_templatetags.py django-tables-1.21.2/tests/test_templatetags.py --- django-tables-1.14.2/tests/test_templatetags.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_templatetags.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,198 +1,264 @@ # coding: utf-8 from __future__ import unicode_literals -import pytest from django.core.exceptions import ImproperlyConfigured from django.template import Context, RequestContext, Template, TemplateSyntaxError +from django.test import SimpleTestCase, TestCase, override_settings from django.utils import six from django.utils.six.moves.urllib.parse import parse_qs -import django_tables2 as tables -from django_tables2.config import RequestConfig +from django_tables2 import RequestConfig, Table, TemplateColumn -from .app.models import Person, Region +from .app.models import Region from .test_templates import MEMORY_DATA, CountryTable -from .utils import assertNumQueries, build_request, parse +from .utils import build_request, parse -def test_render_table_templatetag_invalid_type(): - template = Template('{% load django_tables2 %}{% render_table table %}') +class RenderTableTagTest(TestCase): - with pytest.raises(ValueError): - template.render(Context({ + def test_invalid_type(self): + template = Template('{% load django_tables2 %}{% render_table table %}') + + with self.assertRaises(ValueError): + template.render(Context({ + 'request': build_request(), + 'table': dict() + })) + + def test_basic(self): + request = build_request('/') + # ensure it works with a multi-order-by + table = CountryTable(MEMORY_DATA, order_by=('name', 'population')) + RequestConfig(request).configure(table) + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({'request': request, 'table': table})) + + root = parse(html) + self.assertEqual(len(root.findall('.//thead/tr')), 1) + self.assertEqual(len(root.findall('.//thead/tr/th')), 4) + self.assertEqual(len(root.findall('.//tbody/tr')), 4) + self.assertEqual(len(root.findall('.//tbody/tr/td')), 16) + + def test_does_not_mutate_context(self): + ''' + Make sure the tag does not change the context of the template the tag is called from + https://github.com/jieter/django-tables2/issues/547 + ''' + class MyTable(Table): + col = TemplateColumn(template_code='{{ value }}') + + table = MyTable([{'col': 'foo'}, {'col': 'bar'}], template_name='minimal.html') + template = Template( + '{% load django_tables2 %}' + '{% with "foo" as table %}{{ table }}{% render_table mytable %}\n{{ table }}{% endwith %}' + ) + + html = template.render(Context({ 'request': build_request(), - 'table': dict() + 'mytable': table, })) + lines = html.splitlines() + self.assertEqual(lines[0], 'foo') + self.assertEqual(lines[-1], 'foo') + + def test_table_context_is_RequestContext(self): + class MyTable(Table): + col = TemplateColumn(template_code='{{ value }}') + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({ + 'request': build_request(), + 'table': MyTable([], template_name='csrf.html') + })) + input_tag = parse(html) + self.assertEqual(input_tag.get('type'), 'hidden') + self.assertEqual(input_tag.get('name'), 'csrfmiddlewaretoken') + self.assertEqual(len(input_tag.get('value')), 64) + + def test_no_data_without_empty_text(self): + table = CountryTable([]) + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({'request': build_request('/'), 'table': table})) + root = parse(html) + self.assertEqual(len(root.findall('.//thead/tr')), 1) + self.assertEqual(len(root.findall('.//thead/tr/th')), 4) + self.assertEqual(len(root.findall('.//tbody/tr')), 0) + + def test_no_data_with_empty_text(self): + # no data WITH empty_text + request = build_request('/') + table = CountryTable([], empty_text='this table is empty') + RequestConfig(request).configure(table) + template = Template('{% load django_tables2 %}{% render_table table %}') + html = template.render(Context({'request': request, 'table': table})) + + root = parse(html) + self.assertEqual(len(root.findall('.//thead/tr')), 1) + self.assertEqual(len(root.findall('.//thead/tr/th')), 4) + self.assertEqual(len(root.findall('.//tbody/tr')), 1) + self.assertEqual(len(root.findall('.//tbody/tr/td')), 1) + self.assertEqual(int(root.find('.//tbody/tr/td').get('colspan')), len(root.findall('.//thead/tr/th'))) + self.assertEqual(root.find('.//tbody/tr/td').text, 'this table is empty') + + @override_settings(DEBUG=True) + def test_missing_variable(self): + # variable that doesn't exist (issue #8) + template = Template('{% load django_tables2 %}{% render_table this_doesnt_exist %}') + with self.assertRaises(ValueError): + template.render(Context()) + + @override_settings(DEBUG=False) + def test_missing_variable_debug_False(self): + template = Template('{% load django_tables2 %}{% render_table this_doesnt_exist %}') + # Should still be noisy with debug off + with self.assertRaises(ValueError): + template.render(Context()) + + def test_should_support_template_argument(self): + table = CountryTable(MEMORY_DATA, order_by=('name', 'population')) + template = Template('{% load django_tables2 %}' + '{% render_table table "dummy.html" %}') + + context = RequestContext(build_request(), {'table': table}) + self.assertEqual(template.render(context), 'dummy template contents\n') + + def test_template_argument_list(self): + template = Template('{% load django_tables2 %}' + '{% render_table table template_list %}') + + context = RequestContext(build_request(), { + 'table': CountryTable(MEMORY_DATA, order_by=('name', 'population')), + 'template_list': ('dummy.html', 'child/foo.html') + }) + self.assertEqual(template.render(context), 'dummy template contents\n') + + def test_render_table_supports_queryset(self): + for name in ('Mackay', 'Brisbane', 'Maryborough'): + Region.objects.create(name=name) + template = Template('{% load django_tables2 %}{% render_table qs %}') + html = template.render(Context({ + 'qs': Region.objects.all(), + 'request': build_request('/') + })) -def test_render_table_templatetag(settings): - request = build_request('/') - # ensure it works with a multi-order-by - table = CountryTable(MEMORY_DATA, order_by=('name', 'population')) - RequestConfig(request).configure(table) - template = Template('{% load django_tables2 %}{% render_table table %}') - html = template.render(Context({'request': request, 'table': table})) - - root = parse(html) - assert len(root.findall('.//thead/tr')) == 1 - assert len(root.findall('.//thead/tr/th')) == 4 - assert len(root.findall('.//tbody/tr')) == 4 - assert len(root.findall('.//tbody/tr/td')) == 16 - - # no data with no empty_text - table = CountryTable([]) - template = Template('{% load django_tables2 %}{% render_table table %}') - html = template.render(Context({'request': build_request('/'), 'table': table})) - root = parse(html) - assert len(root.findall('.//thead/tr')) == 1 - assert len(root.findall('.//thead/tr/th')) == 4 - assert len(root.findall('.//tbody/tr')) == 0 - - # no data WITH empty_text - request = build_request('/') - table = CountryTable([], empty_text='this table is empty') - RequestConfig(request).configure(table) - template = Template('{% load django_tables2 %}{% render_table table %}') - html = template.render(Context({'request': request, 'table': table})) - root = parse(html) - assert len(root.findall('.//thead/tr')) == 1 - assert len(root.findall('.//thead/tr/th')) == 4 - assert len(root.findall('.//tbody/tr')) == 1 - assert len(root.findall('.//tbody/tr/td')) == 1 - assert int(root.find('.//tbody/tr/td').get('colspan')) == len(root.findall('.//thead/tr/th')) - assert root.find('.//tbody/tr/td').text == 'this table is empty' - - # variable that doesn't exist (issue #8) - template = Template('{% load django_tables2 %}' - '{% render_table this_doesnt_exist %}') - with pytest.raises(ValueError): - settings.DEBUG = True - template.render(Context()) - - # Should still be noisy with debug off - with pytest.raises(ValueError): - settings.DEBUG = False - template.render(Context()) - - -def test_render_table_should_support_template_argument(): - table = CountryTable(MEMORY_DATA, order_by=('name', 'population')) - template = Template('{% load django_tables2 %}' - '{% render_table table "dummy.html" %}') - - context = RequestContext(build_request(), {'table': table}) - assert template.render(context) == 'dummy template contents\n' - - -def test_render_table_template_argument_list(): - template = Template('{% load django_tables2 %}' - '{% render_table table template_list %}') - - context = RequestContext(build_request(), { - 'table': CountryTable(MEMORY_DATA, order_by=('name', 'population')), - 'template_list': ('dummy.html', 'child/foo.html') - }) - assert template.render(context) == 'dummy template contents\n' - - -@pytest.mark.django_db -def test_render_table_supports_queryset(): - for name in ('Mackay', 'Brisbane', 'Maryborough'): - Region.objects.create(name=name) - template = Template('{% load django_tables2 %}{% render_table qs %}') - html = template.render(Context({'qs': Region.objects.all(), - 'request': build_request('/')})) - - root = parse(html) - assert [e.text for e in root.findall('.//thead/tr/th/a')] == ['ID', 'Name', 'Mayor'] - td = [[td.text for td in tr.findall('td')] for tr in root.findall('.//tbody/tr')] - db = [] - for region in Region.objects.all(): - db.append([six.text_type(region.id), region.name, "—"]) - assert td == db - - -def test_querystring_templatetag(): - template = Template('{% load django_tables2 %}' - '{% querystring "name"="Brad" foo.bar=value %}') - - # Should be something like: ?name=Brad&a=b&c=5&age=21 - xml = template.render(Context({ - 'request': build_request('/?a=b&name=dog&c=5'), - 'foo': {'bar': 'age'}, - 'value': 21, - })) - - # Ensure it's valid XML, retrieve the URL - url = parse(xml).text - - qs = parse_qs(url[1:]) # everything after the ? - assert qs['name'] == ['Brad'] - assert qs['age'] == ['21'] - assert qs['a'] == ['b'] - assert qs['c'] == ['5'] - - -def test_querystring_templatetag_requires_request(): - template = Template('{% load django_tables2 %}{% querystring "name"="Brad" %}') - with pytest.raises(ImproperlyConfigured): - template.render(Context()) - - -def test_querystring_templatetag_supports_without(): - context = Context({ - 'request': build_request('/?a=b&name=dog&c=5'), - 'a_var': 'a', - }) - - template = Template('{% load django_tables2 %}' - '{% querystring "name"="Brad" without a_var %}') - url = parse(template.render(context)).text - qs = parse_qs(url[1:]) # trim the ? - assert set(qs.keys()) == set(['name', 'c']) - - # Try with only exclusions - template = Template('{% load django_tables2 %}' - '{% querystring without "a" "name" %}') - url = parse(template.render(context)).text - qs = parse_qs(url[1:]) # trim the ? - assert set(qs.keys()) == set(["c"]) - - -def test_querystring_syntax_error(): - with pytest.raises(TemplateSyntaxError): - Template('{% load django_tables2 %}{% querystring foo= %}') - - -def test_title_should_only_apply_to_words_without_uppercase_letters(): - expectations = { - 'a brown fox': 'A Brown Fox', - 'a brown foX': 'A Brown foX', - 'black FBI': 'Black FBI', - 'f.b.i': 'F.B.I', - 'start 6pm': 'Start 6pm', - - # Some cyrillic samples - 'руда лисиця': 'Руда Лисиця', - 'руда лисицЯ': 'Руда лисицЯ', - 'діяльність СБУ': 'Діяльність СБУ', - 'а.б.в': 'А.Б.В', - 'вага 6кг': 'Вага 6кг', - 'у 80-их роках': 'У 80-их Роках', - } - - for raw, expected in expectations.items(): - template = Template('{% load django_tables2 %}{{ x|title }}') - assert template.render(Context({'x': raw})) == expected - - -@pytest.mark.django_db -def test_as_html_db_queries(transactional_db): - class PersonTable(tables.Table): - class Meta: - model = Person + root = parse(html) + self.assertEqual([e.text for e in root.findall('.//thead/tr/th/a')], ['ID', 'Name', 'Mayor']) + td = [[td.text for td in tr.findall('td')] for tr in root.findall('.//tbody/tr')] + db = [] + for region in Region.objects.all(): + db.append([six.text_type(region.id), region.name, "—"]) + self.assertEqual(td, db) + + +class QuerystringTagTest(SimpleTestCase): + def test_basic(self): + template = Template('{% load django_tables2 %}' + '{% querystring "name"="Brad" foo.bar=value %}') + + # Should be something like: ?name=Brad&a=b&c=5&age=21 + xml = template.render(Context({ + 'request': build_request('/?a=b&name=dog&c=5'), + 'foo': {'bar': 'age'}, + 'value': 21, + })) - Person.objects.create(first_name='John', last_name='Doo') + # Ensure it's valid XML, retrieve the URL + url = parse(xml).text - with assertNumQueries(count=2): - PersonTable(Person.objects.all()).as_html(build_request()) + qs = parse_qs(url[1:]) # everything after the ? + self.assertEqual(qs['name'], ['Brad']) + self.assertEqual(qs['age'], ['21']) + self.assertEqual(qs['a'], ['b']) + self.assertEqual(qs['c'], ['5']) + + def test_requires_request(self): + template = Template('{% load django_tables2 %}{% querystring "name"="Brad" %}') + with self.assertRaises(ImproperlyConfigured): + template.render(Context()) + + def test_supports_without(self): + context = Context({ + 'request': build_request('/?a=b&name=dog&c=5'), + 'a_var': 'a', + }) + + template = Template('{% load django_tables2 %}' + '{% querystring "name"="Brad" without a_var %}') + url = parse(template.render(context)).text + qs = parse_qs(url[1:]) # trim the ? + self.assertEqual(set(qs.keys()), set(['name', 'c'])) + + def test_only_without(self): + context = Context({ + 'request': build_request('/?a=b&name=dog&c=5'), + 'a_var': 'a', + }) + template = Template('{% load django_tables2 %}' + '{% querystring without "a" "name" %}') + url = parse(template.render(context)).text + qs = parse_qs(url[1:]) # trim the ? + self.assertEqual(set(qs.keys()), set(["c"])) + + def test_querystring_syntax_error(self): + with self.assertRaises(TemplateSyntaxError): + Template('{% load django_tables2 %}{% querystring foo= %}') + + def test_querystring_as_var(self): + def assert_querystring_asvar(template_code, expected): + template = Template( + '{% load django_tables2 %}' + + '{% querystring ' + template_code + ' %}' + + '{{ varname }}' + ) + + # Should be something like: ?name=Brad&a=b&c=5&age=21 + xml = template.render(Context({ + 'request': build_request('/?a=b'), + 'foo': {'bar': 'age'}, + 'value': 21, + })) + self.assertIn('', xml) + qs = parse(xml).xpath('.//strong')[0].text[1:] + self.assertEqual(parse_qs(qs), expected) + + tests = ( + ('"name"="Brad" as=varname', dict(name=['Brad'], a=['b'])), + ('as=varname "name"="Brad"', dict(name=['Brad'], a=['b'])), + ('"name"="Brad" as=varname without "a" ', dict(name=['Brad'])) + ) + + for argstr, expected in tests: + assert_querystring_asvar(argstr, expected) + + def test_export_url_tag(self): + template = Template('{% load django_tables2 %}{% export_url "csv" %}') + html = template.render(Context({'request': build_request('?q=foo')})) + self.assertEqual(dict(parse_qs(html[1:])), dict(parse_qs('q=foo&_export=csv'))) + + # using a template context variable + template = Template('{% load django_tables2 %}{% export_url format %}') + html = template.render(Context({'request': build_request('?q=foo'), 'format': 'xls'})) + self.assertEqual(dict(parse_qs(html[1:])), dict(parse_qs('q=foo&_export=xls'))) + + +class TitleTagTest(SimpleTestCase): + def test_should_only_apply_to_words_without_uppercase_letters(self): + expectations = { + 'a brown fox': 'A Brown Fox', + 'a brown foX': 'A Brown foX', + 'black FBI': 'Black FBI', + 'f.b.i': 'F.B.I', + 'start 6pm': 'Start 6pm', + + # Some cyrillic samples + 'руда лисиця': 'Руда Лисиця', + 'руда лисицЯ': 'Руда лисицЯ', + 'діяльність СБУ': 'Діяльність СБУ', + 'а.б.в': 'А.Б.В', + 'вага 6кг': 'Вага 6кг', + 'у 80-их роках': 'У 80-их Роках', + } + + for raw, expected in expectations.items(): + template = Template('{% load django_tables2 %}{{ x|title }}') + assert template.render(Context({'x': raw})), expected diff -Nru django-tables-1.14.2/tests/test_utils.py django-tables-1.21.2/tests/test_utils.py --- django-tables-1.14.2/tests/test_utils.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_utils.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,5 +1,6 @@ # coding: utf-8 -import pytest +from unittest import TestCase + from django.db import models from django.utils import six @@ -7,251 +8,262 @@ computed_values, segment, signature) -def test_orderbytuple(): - obt = OrderByTuple(('a', 'b', 'c')) - assert obt == (OrderBy('a'), OrderBy('b'), OrderBy('c')) - - # indexing - assert obt[0] == OrderBy('a') - assert obt['b'] == OrderBy('b') - with pytest.raises(KeyError): - obt['d'] - with pytest.raises(TypeError): - obt[('tuple', )] - - # .get - sentinel = object() - assert obt.get('b', sentinel) is obt['b'] # keying - assert obt.get('-', sentinel) is sentinel - assert obt.get(0, sentinel) is obt['a'] # indexing - assert obt.get(3, sentinel) is sentinel - - # .opposite - assert OrderByTuple(('a', '-b', 'c')).opposite == ('-a', 'b', '-c') - - # in - assert 'a' in obt and '-a' in obt - - -def test_orderbytuple_sort_key_multiple(): - obt = OrderByTuple(('a', '-b')) - items = [ - {'a': 1, 'b': 2}, - {'a': 1, 'b': 3}, - ] - assert sorted(items, key=obt.key) == [ - {'a': 1, 'b': 3}, - {'a': 1, 'b': 2}, - ] - - -def test_orderbytuple_sort_key_empty_comes_first(): - obt = OrderByTuple(('a')) - items = [ - {'a': 1}, - {'a': ''}, - {'a': 2}, - ] - if six.PY3: - assert sorted(items, key=obt.key) == [ - {'a': ''}, - {'a': 1}, - {'a': 2}, +class OrderByTupleTest(TestCase): + def test_basic(self): + obt = OrderByTuple(('a', 'b', 'c')) + assert obt == (OrderBy('a'), OrderBy('b'), OrderBy('c')) + + def test_intexing(self): + obt = OrderByTuple(('a', 'b', 'c')) + assert obt[0] == OrderBy('a') + assert obt['b'] == OrderBy('b') + with self.assertRaises(KeyError): + obt['d'] + with self.assertRaises(TypeError): + obt[('tuple', )] + + def test_get(self): + obt = OrderByTuple(('a', 'b', 'c')) + sentinel = object() + assert obt.get('b', sentinel) is obt['b'] # keying + assert obt.get('-', sentinel) is sentinel + assert obt.get(0, sentinel) is obt['a'] # indexing + assert obt.get(3, sentinel) is sentinel + + def test_opposite(self): + assert OrderByTuple(('a', '-b', 'c')).opposite == ('-a', 'b', '-c') + + def test_in(self): + obt = OrderByTuple(('a', 'b', 'c')) + assert 'a' in obt and '-a' in obt + + def test_sort_key_multiple(self): + obt = OrderByTuple(('a', '-b')) + items = [ + {'a': 1, 'b': 2}, + {'a': 1, 'b': 3}, ] - else: assert sorted(items, key=obt.key) == [ + {'a': 1, 'b': 3}, + {'a': 1, 'b': 2}, + ] + + def test_sort_key_empty_comes_first(self): + obt = OrderByTuple(('a')) + items = [ {'a': 1}, - {'a': 2}, {'a': ''}, + {'a': 2}, ] - - -def test_orderby(): - a = OrderBy('a') - assert 'a' == a - assert 'a' == a.bare - assert '-a' == a.opposite - assert True is a.is_ascending - assert False is a.is_descending - - b = OrderBy('-b') - assert '-b' == b - assert 'b' == b.bare - assert 'b' == b.opposite - assert True is b.is_descending - assert False is b.is_ascending - - -def test_accessor(): - x = Accessor('0') - assert 'B' == x.resolve('Brad') - - x = Accessor('1') - assert 'r' == x.resolve('Brad') - - x = Accessor('2.upper') - assert 'A' == x.resolve('Brad') - - x = Accessor('2.upper.__len__') - assert 1 == x.resolve('Brad') - - x = Accessor('') - assert 'Brad' == x.resolve('Brad') - - -def test_accessor_wont_honors_alters_data(): - class Foo(object): - deleted = False - - def delete(self): - self.deleted = True - delete.alters_data = True - - foo = Foo() - with pytest.raises(ValueError): - Accessor('delete').resolve(foo) - assert foo.deleted is False - - -def test_accessor_can_be_quiet(): - foo = {} - assert Accessor('bar').resolve(foo, quiet=True) is None + if six.PY3: + assert sorted(items, key=obt.key) == [ + {'a': ''}, + {'a': 1}, + {'a': 2}, + ] + else: + assert sorted(items, key=obt.key) == [ + {'a': 1}, + {'a': 2}, + {'a': ''}, + ] + + +class OrderByTest(TestCase): + def test_orderby_ascending(self): + a = OrderBy('a') + assert 'a' == a + assert 'a' == a.bare + assert '-a' == a.opposite + assert True is a.is_ascending + assert False is a.is_descending + + def test_orderby_descending(self): + b = OrderBy('-b') + assert '-b' == b + assert 'b' == b.bare + assert 'b' == b.opposite + assert True is b.is_descending + assert False is b.is_ascending + + +class AccessorTest(TestCase): + def test_bare(self): + assert 'Brad' == Accessor('').resolve('Brad') + assert {'Brad'} == Accessor('').resolve({'Brad'}) + assert {'Brad': 'author'} == Accessor('').resolve({'Brad': 'author'}) + + def test_index_lookup(self): + x = Accessor('0') + assert 'B' == x.resolve('Brad') + + x = Accessor('1') + assert 'r' == x.resolve('Brad') + + def test_calling_methods(self): + x = Accessor('2.upper') + assert 'A' == x.resolve('Brad') + + x = Accessor('2.upper.__len__') + assert 1 == x.resolve('Brad') + + def test_honors_alters_data(self): + class Foo(object): + deleted = False + + def delete(self): + self.deleted = True + delete.alters_data = True + + foo = Foo() + with self.assertRaises(ValueError): + Accessor('delete').resolve(foo) + assert foo.deleted is False + + def test_accessor_can_be_quiet(self): + foo = {} + assert Accessor('bar').resolve(foo, quiet=True) is None + + def test_penultimate(self): + context = { + 'a': { + 'a': 1, + 'b': { + 'c': 2, + 'd': 4 + } + } + } + assert Accessor('a.b.c').penultimate(context) == (context['a']['b'], 'c') + assert Accessor('a.b.c.d.e').penultimate(context) == (None, 'e') class AccessorTestModel(models.Model): foo = models.CharField(max_length=20) class Meta: - app_label = 'django_tables2_test' - - -def test_accessor_can_return_field(): - context = AccessorTestModel(foo='bar') - assert type(Accessor('foo').get_field(context)) == models.CharField - - -def test_accessor_returns_None_when_doesnt_exist(): - context = AccessorTestModel(foo='bar') - assert Accessor('bar').get_field(context) is None + app_label = 'tests' -def test_accessor_returns_None_if_not_a_model(): - context = {'bar': 234} - assert Accessor('bar').get_field(context) is None +class AccessorModelTests(TestCase): + def test_can_return_field(self): + context = AccessorTestModel(foo='bar') + assert type(Accessor('foo').get_field(context)) == models.CharField + def test_returns_None_when_doesnt_exist(self): + context = AccessorTestModel(foo='bar') + assert Accessor('bar').get_field(context) is None -def test_accessor_penultimate(): - context = { - 'a': { - 'a': 1, - 'b': { - 'c': 2, - 'd': 4 - } - } - } - - assert Accessor('a.b.c').penultimate(context) == (context['a']['b'], 'c') + def test_returns_None_if_not_a_model(self): + context = {'bar': 234} + assert Accessor('bar').get_field(context) is None - assert Accessor('a.b.c.d.e').penultimate(context) == (None, 'e') +class AttributeDictTest(TestCase): + def test_handles_escaping(self): + x = AttributeDict({'x': '"\'x&'}) + assert x.as_html() == 'x=""'x&"' -def test_attribute_dict_handles_escaping(): - x = AttributeDict({'x': '"\'x&'}) - assert x.as_html() == 'x=""'x&"' + def test_omits_None(self): + x = AttributeDict({'x': None}) + assert x.as_html() == '' -def test_computed_values_supports_shallow_structures(): - x = computed_values({'foo': lambda: 'bar'}) - assert x == {'foo': 'bar'} +class ComputedValuesTest(TestCase): + def test_supports_shallow_structures(self): + x = computed_values({'foo': lambda: 'bar'}) + assert x == {'foo': 'bar'} + def test_supports_nested_structures(self): + x = computed_values({'foo': lambda: {'bar': lambda: 'baz'}}) + assert x == {'foo': {'bar': 'baz'}} -def test_computed_values_supports_nested_structures(): - x = computed_values({'foo': lambda: {'bar': lambda: 'baz'}}) - assert x == {'foo': {'bar': 'baz'}} - + def test_with_argument(self): + x = computed_values({ + 'foo': lambda y: { + 'bar': lambda y: 'baz-{}'.format(y) + } + }, kwargs=dict(y=2)) + assert x == {'foo': {'bar': 'baz-2'}} -def test_computed_values_with_argument(): - x = computed_values({ - 'foo': lambda y: { - 'bar': lambda y: 'baz-{}'.format(y) + def test_returns_None_if_not_enough_kwargs(self): + x = computed_values({'foo': lambda x: 'bar'}) + assert x == {'foo': None} + + +class SegmentTest(TestCase): + def test_should_return_all_candidates(self): + assert set(segment(('a', '-b', 'c'), { + 'x': 'a', + 'y': ('b', '-c'), + '-z': ('b', '-c'), + })) == { + ('x', '-y'), + ('x', 'z'), } - }, kwargs=dict(y=2)) - assert x == {'foo': {'bar': 'baz-2'}} - -def test_segment_should_return_all_candidates(): - assert set(segment(("a", "-b", "c"), { - 'x': 'a', - 'y': ('b', '-c'), - '-z': ('b', '-c'), - })) == { - ('x', '-y'), - ('x', 'z'), - } +class SequenceTest(TestCase): + def test_multiple_ellipsis(self): + sequence = Sequence(['foo', '...', 'bar', '...']) -def test_sequence_multiple_ellipsis(): - sequence = Sequence(['foo', '...', 'bar', '...']) + with self.assertRaises(ValueError): + sequence.expand(['foo']) - with pytest.raises(ValueError): - sequence.expand(['foo']) - -def test_signature(): - def foo(bar, baz): - pass - - args, keywords = signature(foo) - assert args == ('bar', 'baz') - assert keywords is None - - -def test_signature_method(): - class Foo(object): - def foo(self): +class SignatureTest(TestCase): + def test_basic(self): + def foo(bar, baz): pass - def bar(self, bar, baz): - pass + args, keywords = signature(foo) + assert args == ('bar', 'baz') + assert keywords is None + + def test_signature_method(self): + class Foo(object): + def foo(self): + pass + + def bar(self, bar, baz): + pass + + def baz(self, bar, *bla, **boo): + pass + + obj = Foo() + args, keywords = signature(obj.foo) + assert args == () + assert keywords is None + + args, keywords = signature(obj.bar) + assert args == ('bar', 'baz') + assert keywords is None + + args, keywords = signature(obj.baz) + assert args == ('bar', ) + assert keywords == 'boo' - def baz(self, bar, *bla, **boo): + def test_catch_all_kwargs(self): + def foo(bar, baz, **kwargs): pass - obj = Foo() - args, keywords = signature(obj.foo) - assert args == () - assert keywords is None - - args, keywords = signature(obj.bar) - assert args == ('bar', 'baz') - assert keywords is None - - args, keywords = signature(obj.baz) - assert args == ('bar', ) - assert keywords == 'boo' - - -def test_signature_catch_all_kwargs(): - def foo(bar, baz, **kwargs): - pass - - args, keywords = signature(foo) - assert args == ('bar', 'baz') - assert keywords == 'kwargs' - + args, keywords = signature(foo) + assert args == ('bar', 'baz') + assert keywords == 'kwargs' -def test_call_with_appropriate(): - def foo(): - return 'bar' +class CallWithAppropriateTest(TestCase): + def test_basic(self): + def foo(): + return 'bar' - assert call_with_appropriate(foo, { - 'a': 'd', - 'c': 'e' - }) == 'bar' + assert call_with_appropriate(foo, { + 'a': 'd', + 'c': 'e' + }) == 'bar' - def bar(baz): - return baz + def bar(baz): + return baz - assert call_with_appropriate(bar, dict(baz=23)) == 23 + assert call_with_appropriate(bar, dict(baz=23)) == 23 diff -Nru django-tables-1.14.2/tests/test_views.py django-tables-1.21.2/tests/test_views.py --- django-tables-1.14.2/tests/test_views.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/test_views.py 2018-03-26 06:43:09.000000000 +0000 @@ -1,6 +1,8 @@ # coding: utf-8 -import pytest + from django.core.exceptions import ImproperlyConfigured +from django.core.paginator import Paginator +from django.test import TestCase from django.views.generic.base import TemplateView import django_tables2 as tables @@ -20,7 +22,6 @@ ''' Returns a response *and* reference to the view. ''' - def dispatch(self, *args, **kwargs): return super(DispatchHookMixin, self).dispatch(*args, **kwargs), self @@ -41,173 +42,217 @@ model = Region # required for ListView -@pytest.mark.django_db -def test_view_should_support_pagination_options(): - for region in MEMORY_DATA: - Region.objects.create(name=region['name']) - - response, view = SimplePaginatedView.as_view()(build_request('/')) - assert view.get_table().paginator.num_pages == len(MEMORY_DATA) - assert view.get_table().paginator.per_page == 1 - - -@pytest.mark.django_db -def test_view_should_support_default_pagination(): - class PaginateDefault(DispatchHookMixin, tables.SingleTableView): - table_class = SimpleTable - model = Region - table_data = MEMORY_DATA - - response, view = PaginateDefault.as_view()(build_request('/')) - table = view.get_table() - assert table.paginator.per_page == 25 - assert len(table.page) == 4 - - -@pytest.mark.django_db -def test_view_should_support_default_pagination_with_table_options(): - class Table(tables.Table): - class Meta: +class SingleTableViewTest(TestCase): + def test_should_support_pagination_options(self): + for region in MEMORY_DATA: + Region.objects.create(name=region['name']) + + response, view = SimplePaginatedView.as_view()(build_request('/')) + self.assertEqual(view.get_table().paginator.num_pages, len(MEMORY_DATA)) + self.assertEqual(view.get_table().paginator.per_page, 1) + + def test_should_support_default_pagination(self): + class PaginateDefault(DispatchHookMixin, tables.SingleTableView): + table_class = SimpleTable model = Region - per_page = 2 - - class PaginateByDefinedOnView(DispatchHookMixin, tables.SingleTableView): - table_class = Table - model = Region - table_data = MEMORY_DATA + table_data = MEMORY_DATA - response, view = PaginateByDefinedOnView.as_view()(build_request('/')) - table = view.get_table() - assert table.paginator.per_page == 2 - assert len(table.page) == 2 + response, view = PaginateDefault.as_view()(build_request('/')) + table = view.get_table() + self.assertEqual(table.paginator.per_page, 25) + self.assertEqual(len(table.page), 4) + + def test_should_support_default_pagination_with_table_options(self): + class Table(tables.Table): + class Meta: + model = Region + per_page = 2 + class PaginateByDefinedOnView(DispatchHookMixin, tables.SingleTableView): + table_class = Table + model = Region + table_data = MEMORY_DATA -@pytest.mark.django_db -def test_view_should_support_disabling_pagination_options(): - class SimpleNotPaginatedView(DispatchHookMixin, tables.SingleTableView): - table_class = SimpleTable - table_data = MEMORY_DATA - table_pagination = False - model = Region # required for ListView + response, view = PaginateByDefinedOnView.as_view()(build_request('/')) + table = view.get_table() + self.assertEqual(table.paginator.per_page, 2) + self.assertEqual(len(table.page), 2) + + def test_should_support_disabling_pagination_options(self): + class SimpleNotPaginatedView(DispatchHookMixin, tables.SingleTableView): + table_class = SimpleTable + table_data = MEMORY_DATA + table_pagination = False + model = Region # required for ListView + + response, view = SimpleNotPaginatedView.as_view()(build_request('/')) + table = view.get_table() + self.assertFalse(hasattr(table, 'page')) + + def test_data_from_get_queryset(self): + for region in MEMORY_DATA: + Region.objects.create(name=region['name']) + + class GetQuerysetView(SimpleView): + def get_queryset(self): + return Region.objects.filter(name__startswith='Q') + + response, view = GetQuerysetView.as_view()(build_request('/')) + table = view.get_table() + + self.assertEqual(len(table.rows), 1) + self.assertEqual(table.rows[0].get_cell('name'), 'Queensland') + + def test_should_support_explicit_table_data(self): + class ExplicitDataView(SimplePaginatedView): + table_data = MEMORY_DATA + + response, view = ExplicitDataView.as_view()(build_request('/')) + self.assertEqual(view.get_table().paginator.num_pages, len(MEMORY_DATA)) + + def test_paginate_by_on_view_class(self): + Region.objects.create(name='Friesland') + + class Table(tables.Table): + class Meta: + model = Region - response, view = SimpleNotPaginatedView.as_view()(build_request('/')) - table = view.get_table() - assert not hasattr(table, 'page') + class PaginateByDefinedOnView(DispatchHookMixin, tables.SingleTableView): + table_class = Table + model = Region + paginate_by = 2 + table_data = MEMORY_DATA + def get_queryset(self): + return Region.objects.all().order_by('name') -@pytest.mark.django_db -def test_view_from_get_queryset(): - for region in MEMORY_DATA: - Region.objects.create(name=region['name']) + response, view = PaginateByDefinedOnView.as_view()(build_request('/')) + assert view.get_table().paginator.per_page == 2 - class GetQuerysetView(SimpleView): - def get_queryset(self): - return Region.objects.filter(name__startswith='Q') + def test_should_pass_kwargs_to_table_constructor(self): + class PassKwargsView(SimpleView): + table_data = [] - response, view = GetQuerysetView.as_view()(build_request('/')) - table = view.get_table() + def get_table(self, **kwargs): + kwargs.update({'orderable': False}) + return super(PassKwargsView, self).get_table(**kwargs) - assert len(table.rows) == 1 - assert table.rows[0].get_cell('name') == 'Queensland' + response, view = SimpleView.as_view()(build_request('/')) + assert view.get_table().orderable is True + response, view = PassKwargsView.as_view()(build_request('/')) + assert view.get_table().orderable is False -def test_should_raise_without_tableclass(): - class WithoutTableclassView(tables.SingleTableView): - model = Region + def test_should_override_table_pagination(self): + class PrefixedTable(SimpleTable): + class Meta(SimpleTable.Meta): + prefix = 'p_' - with pytest.raises(ImproperlyConfigured): - WithoutTableclassView.as_view()(build_request('/')) + class PrefixedView(SimpleView): + table_class = PrefixedTable + class PaginationOverrideView(PrefixedView): + table_data = MEMORY_DATA -def test_should_support_explicit_table_data(): - class ExplicitDataView(SimplePaginatedView): - table_data = MEMORY_DATA + def get_table_pagination(self, table): + assert isinstance(table, tables.Table) - response, view = ExplicitDataView.as_view()(build_request('/')) - assert view.get_table().paginator.num_pages == len(MEMORY_DATA) + per_page = self.request.GET.get('%s_override' % table.prefixed_per_page_field) + if per_page is not None: + return {'per_page': per_page} + return super(PaginationOverrideView, self).get_table_pagination(table) + response, view = PaginationOverrideView.as_view()(build_request('/?p_per_page_override=2')) + assert view.get_table().paginator.per_page == 2 -@pytest.mark.django_db -def test_paginate_by_on_view_class(): - Region.objects.create(name='Friesland') + def test_using_get_queryset(self): + ''' + Should not raise a value-error for a View using View.get_queryset() + (test for reverting regressing in #423) + ''' + Person.objects.create(first_name='Anton', last_name='Sam') - class Table(tables.Table): - class Meta: - model = Region + class Table(tables.Table): + class Meta(object): + model = Person + fields = ('first_name', 'last_name') - class PaginateByDefinedOnView(DispatchHookMixin, tables.SingleTableView): - table_class = Table - model = Region - paginate_by = 2 - table_data = MEMORY_DATA + class TestView(tables.SingleTableView): + model = Person + table_class = Table - def get_queryset(self): - return Region.objects.all().order_by('name') + def get(self, request, *args, **kwargs): + self.get_table() + from django.http import HttpResponse + return HttpResponse() - response, view = PaginateByDefinedOnView.as_view()(build_request('/')) - assert view.get_table().paginator.per_page == 2 + def get_queryset(self): + return Person.objects.all() + TestView.as_view()(build_request()) -@pytest.mark.django_db -def test_should_pass_kwargs_to_table_constructor(): + def test_get_tables_class(self): + view = SimpleView() + table_class = view.get_table_class() + self.assertEqual(table_class, view.table_class) - class PassKwargsView(SimpleView): - table_data = [] + def test_get_tables_class_auto(self): + class SimpleNoTableClassView(tables.SingleTableView): + model = Region - def get_table(self, **kwargs): - kwargs.update({'orderable': False}) - return super(PassKwargsView, self).get_table(**kwargs) + view = SimpleNoTableClassView() + table_class = view.get_table_class() + self.assertEqual(table_class.__name__, 'RegionTable') - response, view = SimpleView.as_view()(build_request('/')) - assert view.get_table().orderable is True + def test_get_tables_class_raises_no_model(self): + class SimpleNoTableClassNoModelView(tables.SingleTableView): + model = None + table_class = None - response, view = PassKwargsView.as_view()(build_request('/')) - assert view.get_table().orderable is False + view = SimpleNoTableClassNoModelView() + with self.assertRaises(ImproperlyConfigured): + view.get_table_class() -@pytest.mark.django_db -def test_should_override_table_pagination(): +class SingleTableMixinTest(TestCase): + def test_with_non_paginated_view(self): + ''' + SingleTableMixin should not assume it is mixed with a ListView - class PrefixedTable(SimpleTable): - class Meta(SimpleTable.Meta): - prefix = 'p_' + Github issue #326 + ''' - class PrefixedView(SimpleView): - table_class = PrefixedTable + class Table(tables.Table): + class Meta: + model = Region - class PaginationOverrideView(PrefixedView): - table_data = MEMORY_DATA + class View(tables.SingleTableMixin, TemplateView): + table_class = Table + table_data = MEMORY_DATA - def get_table_pagination(self, table): - assert isinstance(table, tables.Table) + template_name = 'dummy.html' - per_page = self.request.GET.get('%s_override' % table.prefixed_per_page_field) - if per_page is not None: - return {'per_page': per_page} - return super(PaginationOverrideView, self).get_table_pagination(table) + View.as_view()(build_request()) - response, view = PaginationOverrideView.as_view()(build_request('/?p_per_page_override=2')) - assert view.get_table().paginator.per_page == 2 + def test_with_custom_paginator(self): + class Table(tables.Table): + class Meta: + model = Region + class MyPaginator(Paginator): + pass -def test_singletablemixin_with_non_paginated_view(): - ''' - SingleTableMixin should not assume it is mixed with a ListView + class View(DispatchHookMixin, tables.SingleTableView): + table_class = Table + queryset = Region.objects.all() + table_pagination = { + 'klass': MyPaginator + } - Github issue #326 - ''' + response, view = View.as_view()(build_request()) - class Table(tables.Table): - class Meta: - model = Region - - class View(tables.SingleTableMixin, TemplateView): - table_class = Table - table_data = MEMORY_DATA - - template_name = 'dummy.html' - - View.as_view()(build_request('/')) + context = view.get_context_data() + self.assertIsInstance(context['table'].paginator, MyPaginator) class TableA(tables.Table): @@ -221,142 +266,114 @@ exclude = ('id', ) -@pytest.mark.django_db -def test_multiTableMixin_basic(): - Person.objects.create(first_name='Jan Pieter', last_name='W') - - Region.objects.create(name='Zuid-Holland') - Region.objects.create(name='Noord-Holland') - - class View(tables.MultiTableMixin, TemplateView): - tables = (TableA, TableB) - tables_data = (Person.objects.all(), Region.objects.all()) - template_name = 'multiple.html' - - response = View.as_view()(build_request('/')) - response.render() - - html = response.rendered_content - - assert 'table_0-sort=first_name' in html - assert 'table_1-sort=name' in html - - assert '' in html - assert '' in html - - -@pytest.mark.django_db -def test_multiTableMixin_basic_alternative(): - Person.objects.create(first_name='Jan Pieter', last_name='W') - - Region.objects.create(name='Zuid-Holland') - Region.objects.create(name='Noord-Holland') - - class View(tables.MultiTableMixin, TemplateView): - tables = ( - TableA(Person.objects.all()), - TableB(Region.objects.all()) +class MultiTableMixinTest(TestCase): + def setUp(self): + Person.objects.create(first_name='Jan Pieter', last_name='W') + + NL_PROVICES = ( + 'Flevoland', 'Friesland', 'Gelderland', 'Groningen', 'Limburg', + 'Noord-Brabant', 'Noord-Holland', 'Overijssel', 'Utrecht', + 'Zeeland', 'Zuid-Holland', ) - template_name = 'multiple.html' + for name in NL_PROVICES: + Region.objects.create(name=name) - response = View.as_view()(build_request('/')) - response.render() + def test_basic(self): + class View(tables.MultiTableMixin, TemplateView): + tables = (TableA, TableB) + tables_data = (Person.objects.all(), Region.objects.all()) + template_name = 'multiple.html' - html = response.rendered_content + response = View.as_view()(build_request('/')) + response.render() - assert 'table_0-sort=first_name' in html - assert 'table_1-sort=name' in html + html = response.rendered_content - assert '' in html - assert '' in html + assert 'table_0-sort=first_name' in html + assert 'table_1-sort=name' in html + assert '' in html + assert '' in html -def test_multiTableMixin_without_tables(): - class View(tables.MultiTableMixin, TemplateView): - template_name = 'multiple.html' + def test_supplying_instances(self): + class View(tables.MultiTableMixin, TemplateView): + tables = ( + TableA(Person.objects.all()), + TableB(Region.objects.all()) + ) + template_name = 'multiple.html' - with pytest.raises(ImproperlyConfigured): - View.as_view()(build_request('/')) + response = View.as_view()(build_request('/')) + response.render() + html = response.rendered_content -def test_multiTableMixin_with_empty_get_tables_list(): - class View(tables.MultiTableMixin, TemplateView): - template_name = 'multiple.html' + assert 'table_0-sort=first_name' in html + assert 'table_1-sort=name' in html - def get_tables(self): - return [] + assert '' in html + assert '' in html - response = View.as_view()(build_request('/')) - response.render() + def test_without_tables(self): + class View(tables.MultiTableMixin, TemplateView): + template_name = 'multiple.html' - html = response.rendered_content - assert '

Multiple tables using MultiTableMixin

' in html + with self.assertRaises(ImproperlyConfigured): + View.as_view()(build_request('/')) + def test_with_empty_get_tables_list(self): + class View(tables.MultiTableMixin, TemplateView): + template_name = 'multiple.html' -def test_multiTableMixin_incorrect_len(): + def get_tables(self): + return [] - class View(tables.MultiTableMixin, TemplateView): - tables = (TableA, TableB) - tables_data = (Person.objects.all(), ) - template_name = 'multiple.html' + response = View.as_view()(build_request('/')) + response.render() - with pytest.raises(ImproperlyConfigured): - View.as_view()(build_request('/')) + html = response.rendered_content + assert '

Multiple tables using MultiTableMixin

' in html + def test_length_mismatch(self): + class View(tables.MultiTableMixin, TemplateView): + tables = (TableA, TableB) + tables_data = (Person.objects.all(), ) + template_name = 'multiple.html' -@pytest.mark.django_db -def test_multiTableMixin_pagination(): - NL_PROVICES = ( - 'Flevoland', 'Friesland', 'Gelderland', 'Groningen', 'Limburg', - 'Noord-Brabant', 'Noord-Holland', 'Overijssel', 'Utrecht', - 'Zeeland', 'Zuid-Holland', - ) - for name in NL_PROVICES: - Region.objects.create(name=name) + with self.assertRaises(ImproperlyConfigured): + View.as_view()(build_request('/')) - class View(DispatchHookMixin, tables.MultiTableMixin, TemplateView): - tables = ( - TableB(Region.objects.all()), - TableB(Region.objects.all()) - ) - template_name = 'multiple.html' + def test_pagination(self): + class View(DispatchHookMixin, tables.MultiTableMixin, TemplateView): + tables = ( + TableB(Region.objects.all()), + TableB(Region.objects.all()) + ) + template_name = 'multiple.html' - table_pagination = { - 'per_page': 5 - } + table_pagination = { + 'per_page': 5 + } - response, view = View.as_view()(build_request('/?table_1-page=3')) + response, view = View.as_view()(build_request('/?table_1-page=3')) - tableA, tableB = view.get_tables() + tableA, tableB = view.get_tables() - assert tableA.page.number == 1 - assert tableB.page.number == 3 + assert tableA.page.number == 1 + assert tableB.page.number == 3 + def test_get_tables_data(self): + class View(tables.MultiTableMixin, TemplateView): + tables = (TableA, TableB) + template_name = 'multiple.html' -@pytest.mark.django_db -def test_View_using_get_queryset(): - ''' - Should not raise a value-error for a View using View.get_queryset() - (test for reverting regressing in #423) - ''' - Person.objects.create(first_name='Anton', last_name='Sam') - - class Table(tables.Table): - class Meta(object): - model = Person - fields = ('first_name', 'last_name') + def get_tables_data(self): + return [Person.objects.all(), Region.objects.all()] - class TestView(tables.SingleTableView): - model = Person - table_class = Table + response = View.as_view()(build_request('/')) + response.render() - def get(self, request, *args, **kwargs): - self.get_table() - from django.http import HttpResponse - return HttpResponse() - - def get_queryset(self): - '''get_queryset should be called''' - return Person.objects.all() + html = response.rendered_content - TestView.as_view()(build_request()) + assert '' in html + assert '' in html diff -Nru django-tables-1.14.2/tests/utils.py django-tables-1.21.2/tests/utils.py --- django-tables-1.14.2/tests/utils.py 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tests/utils.py 2018-03-26 06:43:09.000000000 +0000 @@ -14,30 +14,6 @@ return lxml.html.fromstring(html) -class assertNumQueries(object): - ''' - Assert the number of queries made through the django ORM in a with-block - ''' - def __init__(self, count=1): - self.count = count - from django.conf import settings - settings.DEBUG = True - - def query_count(self): - from django.db import connection - return len(connection.queries) - - def __enter__(self): - self.original = self.query_count() - - def __exit__(self, exc_type, exc_value, traceback): - expected = self.original + self.count - count = self.query_count() - assert expected == count, 'Expected {} queries, but got {}.'.format( - self.count, self.query_count() - self.original - ) - - def attrs(xml): ''' Helper function that returns a dict of XML attributes, given an element. diff -Nru django-tables-1.14.2/tox.ini django-tables-1.21.2/tox.ini --- django-tables-1.14.2/tox.ini 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/tox.ini 2018-03-26 06:43:09.000000000 +0000 @@ -1,14 +1,9 @@ -[pytest] -DJANGO_SETTINGS_MODULE=tests.app.settings -norecursedirs = docs *.egg-info .git example .tox - [tox] args_are_paths = false envlist = - py27-{1.8,1.9,1.10,1.11}, - py33-{1.8}, - py34-{1.8,1.9,1.10,1.11,2.0}, - py35-{1.9,1.10,1.11,2.0,master}, + py27-{1.11}, + py34-{1.11,2.0}, + py35-{1.11,2.0,master}, py36-{2.0,master}, docs, flake8, @@ -16,13 +11,12 @@ [travis] python: - 2.7: py27, docs - 3.6: py36, flake8, isort + 2.7: py27 + 3.6: py36, docs, flake8, isort [testenv] basepython = py27: python2.7 - py33: python3.3 py34: python3.4 py35: python3.5 py36: python3.6 @@ -31,19 +25,15 @@ pip_pre = true setenv = PYTHONPATH={toxinidir} commands = - py.test -rw --cov=django_tables2 --cov-report term-missing + python -Wd manage.py test deps = - 1.8: Django>=1.8,<1.9 - 1.9: Django>=1.9,<1.10 - 1.10: Django>=1.10,<1.11 1.11: Django>=1.11,<2.0 - 2.0: Django==2.0b1 + 2.0: Django>=2.0,<2.1 master: https://github.com/django/django/archive/master.tar.gz - psycopg2 -r{toxinidir}/requirements/common.pip [testenv:docs] -basepython = python2.7 +basepython = python3.6 whitelist_externals = make changedir = docs commands = make html @@ -56,7 +46,7 @@ commands = flake8 [flake8] -ignore = F401,E731 +ignore = E731 exclude = .git,__pycache__,.tox,example/app/migrations max-line-length = 120 diff -Nru django-tables-1.14.2/.travis.yml django-tables-1.21.2/.travis.yml --- django-tables-1.14.2/.travis.yml 2017-10-30 09:57:27.000000000 +0000 +++ django-tables-1.21.2/.travis.yml 2018-03-26 06:43:09.000000000 +0000 @@ -2,7 +2,6 @@ python: - "2.7" - - "3.3" - "3.4" - "3.5" - "3.6"
NameCountryNameCountryNameCountryNameCountryNameCountryNameCountryNameCountryNameCountry00Total: 77740000Total:18833000Total:18833000{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}Jan PieterZuid-HollandJan PieterZuid-HollandJan PieterZuid-HollandJan PieterZuid-HollandJan PieterZuid-Holland