diff --git a/README.rst b/README.rst
index 6f9857e..649a548 100644
--- a/README.rst
+++ b/README.rst
@@ -83,6 +83,7 @@ v1.0.0
- Added Python 3.4 support.
- Added Django 1.7 and Django 1.8 support.
- Dropped Python 2.6 and 3.2 support.
+- Convert tests to using py.test.
v0.16.0
-------
diff --git a/django_tables2/columns/base.py b/django_tables2/columns/base.py
index 9d29174..e59b41d 100644
--- a/django_tables2/columns/base.py
+++ b/django_tables2/columns/base.py
@@ -242,10 +242,11 @@ class Column(object): # pylint: disable=R0902
# Since this method is inherited by every subclass, only provide a
# column if this class was asked directly.
if cls is Column:
- # django 1.8 fix, but maintain compatibility
- if 'django.db.models.fields.related.ManyToOneRel' in str(field.__class__):
- return cls(verbose_name=field.get_related_field().verbose_name)
- return cls(verbose_name=field.verbose_name)
+ if hasattr(field, "get_related_field"):
+ verbose_name = field.get_related_field().verbose_name
+ else:
+ verbose_name = field.verbose_name
+ return cls(verbose_name=verbose_name)
class BoundColumn(object):
@@ -456,8 +457,8 @@ class BoundColumn(object):
# in anything useful.
name = title(self.name.replace('_', ' '))
- # Try to use a tmodel field's verbose_name
- if hasattr(self.table.data, 'queryset'):
+ # Try to use a model field's verbose_name
+ if hasattr(self.table.data, 'queryset') and hasattr(self.table.data.queryset, 'model'):
model = self.table.data.queryset.model
parts = self.accessor.split('.')
field = None
diff --git a/django_tables2/rows.py b/django_tables2/rows.py
index d1125e2..bd2dd0f 100644
--- a/django_tables2/rows.py
+++ b/django_tables2/rows.py
@@ -112,7 +112,7 @@ class BoundRow(object):
try:
field = penultimate._meta.get_field(remainder)
display = getattr(penultimate, 'get_%s_display' % remainder, None)
- if field.choices and display:
+ if getattr(field, "choices", ()) and display:
value = display()
remainder = None
except FieldDoesNotExist:
diff --git a/requirements/common.pip b/requirements/common.pip
new file mode 100644
index 0000000..ceb312a
--- /dev/null
+++ b/requirements/common.pip
@@ -0,0 +1,8 @@
+django-haystack>=2.0.0,<2.1.0
+fudge
+lxml
+pylint
+pytz>0
+six
+pytest
+pytest-django
diff --git a/requirements/django-1.2.x.pip b/requirements/django-1.2.x.pip
new file mode 100644
index 0000000..9817b15
--- /dev/null
+++ b/requirements/django-1.2.x.pip
@@ -0,0 +1,2 @@
+-r common.pip
+Django>=1.2,<1.3
diff --git a/requirements/django-1.3.x.pip b/requirements/django-1.3.x.pip
new file mode 100644
index 0000000..5bac7cb
--- /dev/null
+++ b/requirements/django-1.3.x.pip
@@ -0,0 +1,2 @@
+-r common.pip
+Django>=1.3,<1.4
diff --git a/requirements/django-1.4.x.pip b/requirements/django-1.4.x.pip
new file mode 100644
index 0000000..9a05ed5
--- /dev/null
+++ b/requirements/django-1.4.x.pip
@@ -0,0 +1,2 @@
+-r common.pip
+Django>=1.4,<1.5
diff --git a/requirements/django-1.5.x.pip b/requirements/django-1.5.x.pip
new file mode 100644
index 0000000..658a0d6
--- /dev/null
+++ b/requirements/django-1.5.x.pip
@@ -0,0 +1,2 @@
+-r common.pip
+Django>=1.5,<1.6
diff --git a/requirements/django-1.6.x.pip b/requirements/django-1.6.x.pip
new file mode 100644
index 0000000..b1edda1
--- /dev/null
+++ b/requirements/django-1.6.x.pip
@@ -0,0 +1,2 @@
+-r common.pip
+Django>=1.6,<1.7
diff --git a/requirements/django-1.7.x.pip b/requirements/django-1.7.x.pip
new file mode 100644
index 0000000..70897e2
--- /dev/null
+++ b/requirements/django-1.7.x.pip
@@ -0,0 +1,2 @@
+-r common.pip
+Django>=1.7,<1.8
diff --git a/requirements/django-1.8.x.pip b/requirements/django-1.8.x.pip
new file mode 100644
index 0000000..af4e25c
--- /dev/null
+++ b/requirements/django-1.8.x.pip
@@ -0,0 +1,2 @@
+-r common.pip
+Django>=1.8,<1.9
diff --git a/requirements/django-dev.pip b/requirements/django-dev.pip
new file mode 100644
index 0000000..66d17b5
--- /dev/null
+++ b/requirements/django-dev.pip
@@ -0,0 +1,2 @@
+-r common.pip
+http://github.com/django/django/tarball/master
diff --git a/setup.py b/setup.py
index 73d8701..c109467 100755
--- a/setup.py
+++ b/setup.py
@@ -22,9 +22,6 @@ setup(
install_requires=['Django >=1.2', 'six'],
- test_loader='tests:loader',
- test_suite='tests.everything',
-
classifiers=[
'Environment :: Web Environment',
'Framework :: Django',
diff --git a/tests/__init__.py b/tests/__init__.py
index 1482687..e69de29 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,49 +0,0 @@
-# coding: utf-8
-from __future__ import absolute_import, unicode_literals
-import os
-os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.app.settings'
-
-from attest import Tests
-import django_attest
-from .columns import columns
-from .config import config
-from .core import core
-from .models import models
-from .rows import rows
-from .templates import templates
-from .utils import utils
-from .views import views
-
-
-loader = django_attest.FancyReporter.test_loader
-everything = Tests([columns, config, core, models, rows, templates, utils,
- views])
-
-
-# -----------------------------------------------------------------------------
-
-
-junit = Tests()
-
-@junit.test
-def make_junit_output():
- import xmlrunner
- runner = xmlrunner.XMLTestRunner(output=b'reports')
- runner.run(everything.test_suite())
-
-
-# -----------------------------------------------------------------------------
-
-
-pylint = Tests()
-
-@pylint.test
-def make_pylint_output():
- from os.path import expanduser
- from pylint.lint import Run
- from pylint.reporters.text import ParseableTextReporter
- if not os.path.exists('reports'):
- os.mkdir('reports')
- with open('reports/pylint.report', 'wb') as handle:
- args = ['django_tables2', 'example', 'tests']
- Run(args, reporter=ParseableTextReporter(output=handle), exit=False)
diff --git a/tests/app/models.py b/tests/app/models.py
index da26a4b..335f225 100644
--- a/tests/app/models.py
+++ b/tests/app/models.py
@@ -2,8 +2,7 @@
from __future__ import unicode_literals
from django.db import models
from django.utils.safestring import mark_safe
-from django.utils.translation import ugettext_lazy
-from django.utils.translation import ugettext
+from django.utils.translation import ugettext, ugettext_lazy
import six
diff --git a/tests/columns.py b/tests/columns.py
deleted file mode 100644
index f468358..0000000
--- a/tests/columns.py
+++ /dev/null
@@ -1,838 +0,0 @@
-# coding: utf-8
-# pylint: disable=R0912,E0102
-from __future__ import unicode_literals
-from attest import assert_hook, Tests, warns # pylint: disable=W0611
-from datetime import date, datetime
-from django_attest import settings, TestContext
-import django_tables2 as tables
-from django_tables2.utils import build_request
-from django_tables2 import A, Attrs
-from django.db import models
-from django.db.models.fields.files import FieldFile
-from django.core.urlresolvers import reverse
-from django.core.files.base import ContentFile
-from django.core.files.storage import FileSystemStorage
-from django.template import Context, Template
-from django.utils.translation import ugettext
-from django.utils.safestring import mark_safe, SafeData
-try:
- from django.utils import timezone
-except ImportError:
- timezone = None
-from os.path import dirname, join
-import pytz
-from .app.models import Person
-from .templates import attrs, parse
-
-
-booleancolumn = Tests()
-
-
-@booleancolumn.test
-def should_be_used_for_booleanfield():
- class BoolModel(models.Model):
- field = models.BooleanField()
-
- class Table(tables.Table):
- class Meta:
- model = BoolModel
-
- column = Table.base_columns["field"]
- assert type(column) == tables.BooleanColumn
- assert column.empty_values != ()
-
-
-@booleancolumn.test
-def should_be_used_for_nullbooleanfield():
- class NullBoolModel(models.Model):
- field = models.NullBooleanField()
-
- class Table(tables.Table):
- class Meta:
- model = NullBoolModel
-
- column = Table.base_columns["field"]
- assert type(column) == tables.BooleanColumn
- assert column.empty_values == ()
-
-
-@booleancolumn.test
-def treat_none_different_from_false():
- class Table(tables.Table):
- col = tables.BooleanColumn(null=False, default="---")
-
- table = Table([{"col": None}])
- assert table.rows[0]["col"] == "---"
-
-
-@booleancolumn.test
-def treat_none_as_false():
- class Table(tables.Table):
- col = tables.BooleanColumn(null=True)
-
- table = Table([{"col": None}])
- assert table.rows[0]["col"] == '✘'
-
-
-@booleancolumn.test
-def span_attrs():
- class Table(tables.Table):
- col = tables.BooleanColumn(attrs={"span": {"key": "value"}})
-
- table = Table([{"col": True}])
- assert attrs(table.rows[0]["col"]) == {"class": "true", "key": "value"}
-
-
-checkboxcolumn = Tests()
-
-
-@checkboxcolumn.test
-def attrs_should_be_translated_for_backwards_compatibility():
- with warns(DeprecationWarning):
- class TestTable(tables.Table):
- col = tables.CheckBoxColumn(header_attrs={"th_key": "th_value"},
- attrs={"td_key": "td_value"})
-
- table = TestTable([{"col": "data"}])
- assert attrs(table.columns["col"].header) == {"type": "checkbox", "th_key": "th_value"}
- assert attrs(table.rows[0]["col"]) == {"type": "checkbox", "td_key": "td_value", "value": "data", "name": "col"}
-
-
-@checkboxcolumn.test
-def new_attrs_should_be_supported():
- with warns(DeprecationWarning):
- class TestTable(tables.Table):
- col1 = tables.CheckBoxColumn(attrs=Attrs(th__input={"th_key": "th_value"},
- td__input={"td_key": "td_value"}))
- col2 = tables.CheckBoxColumn(attrs=Attrs(input={"key": "value"}))
-
- table = TestTable([{"col1": "data", "col2": "data"}])
- assert attrs(table.columns["col1"].header) == {"type": "checkbox", "th_key": "th_value"}
- assert attrs(table.rows[0]["col1"]) == {"type": "checkbox", "td_key": "td_value", "value": "data", "name": "col1"}
- assert attrs(table.columns["col2"].header) == {"type": "checkbox", "key": "value"}
- assert attrs(table.rows[0]["col2"]) == {"type": "checkbox", "key": "value", "value": "data", "name": "col2"}
-
-
-general = Tests()
-
-
-@general.test
-def column_render_supports_kwargs():
- class TestColumn(tables.Column):
- def render(self, **kwargs):
- expected = set(("record", "value", "column", "bound_column",
- "bound_row", "table"))
- actual = set(kwargs.keys())
- assert actual == expected
- return "success"
-
- class TestTable(tables.Table):
- foo = TestColumn()
-
- table = TestTable([{"foo": "bar"}])
- assert table.rows[0]["foo"] == "success"
-
-
-@general.test
-def 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"
-
-
-@general.test
-def 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)
-
-
-@general.test
-def 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)
-
-
-@general.test
-def sortable_backwards_compatibility():
- # Table.Meta.sortable (not set)
- class SimpleTable(tables.Table):
- name = tables.Column()
- table = SimpleTable([])
- with warns(DeprecationWarning):
- assert table.columns['name'].sortable is True
-
- # Table.Meta.sortable = False
- with warns(DeprecationWarning):
- class SimpleTable(tables.Table):
- name = tables.Column()
-
- class Meta:
- sortable = False
- table = SimpleTable([])
- with warns(DeprecationWarning):
- assert table.columns['name'].sortable is False # backwards compatible
- assert table.columns['name'].orderable is False
-
- # Table.Meta.sortable = True
- with warns(DeprecationWarning):
- class SimpleTable(tables.Table):
- name = tables.Column()
-
- class Meta:
- sortable = True
- table = SimpleTable([])
- with warns(DeprecationWarning):
- assert table.columns['name'].sortable is True # backwards compatible
- assert table.columns['name'].orderable is True
-
-
-@general.test
-def orderable():
- # Table.Meta.orderable = False
- class SimpleTable(tables.Table):
- name = tables.Column()
- table = SimpleTable([])
- assert table.columns['name'].orderable is True
-
- # Table.Meta.orderable = False
- class SimpleTable(tables.Table):
- name = tables.Column()
-
- class Meta:
- orderable = False
- table = SimpleTable([])
- assert table.columns['name'].orderable is False
- with warns(DeprecationWarning):
- assert table.columns['name'].sortable is False # backwards compatible
-
- # Table.Meta.orderable = True
- class SimpleTable(tables.Table):
- name = tables.Column()
-
- class Meta:
- orderable = True
- table = SimpleTable([])
- with warns(DeprecationWarning):
- assert table.columns['name'].sortable is True # backwards compatible
- assert table.columns['name'].orderable is True
-
-
-@general.test
-def order_by_defaults_to_accessor():
- class SimpleTable(tables.Table):
- foo = tables.Column(accessor="bar")
-
- table = SimpleTable([])
- assert table.columns["foo"].order_by == ("bar", )
-
-
-@general.test
-def 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", )
-
-
-@general.test
-def supports_is_ordered():
- class SimpleTable(tables.Table):
- name = tables.Column()
-
- # sorted
- table = SimpleTable([], order_by='name')
- assert table.columns["name"].is_ordered
- # unsorted
- table = SimpleTable([])
- assert not table.columns["name"].is_ordered
-
-
-@general.test
-def translation():
- """
- Tests different types of values for the ``verbose_name`` property of a
- column.
- """
- class TranslationTable(tables.Table):
- normal = tables.Column(verbose_name=ugettext("Normal"))
- lazy = tables.Column(verbose_name=ugettext("Lazy"))
-
- table = TranslationTable([])
- assert "Normal" == table.columns["normal"].header
- assert "Lazy" == table.columns["lazy"].header
-
-
-@general.test
-def sequence():
- """
- Ensures that the sequence of columns is configurable.
- """
- class TestTable(tables.Table):
- a = tables.Column()
- b = tables.Column()
- c = tables.Column()
- assert ["a", "b", "c"] == TestTable([]).columns.names()
- assert ["b", "a", "c"] == TestTable([], sequence=("b", "a", "c")).columns.names()
-
- class TestTable2(TestTable):
- class Meta:
- sequence = ("b", "a", "c")
- assert ["b", "a", "c"] == TestTable2([]).columns.names()
- assert ["a", "b", "c"] == TestTable2([], sequence=("a", "b", "c")).columns.names()
-
- class TestTable3(TestTable):
- class Meta:
- sequence = ("c", )
- assert ["c", "a", "b"] == TestTable3([]).columns.names()
- assert ["c", "a", "b"] == TestTable([], sequence=("c", )).columns.names()
-
- class TestTable4(TestTable):
- class Meta:
- sequence = ("...", )
- assert ["a", "b", "c"] == TestTable4([]).columns.names()
- assert ["a", "b", "c"] == TestTable([], sequence=("...", )).columns.names()
-
- class TestTable5(TestTable):
- class Meta:
- sequence = ("b", "...")
- assert ["b", "a", "c"] == TestTable5([]).columns.names()
- assert ["b", "a", "c"] == TestTable([], sequence=("b", "...")).columns.names()
-
- class TestTable6(TestTable):
- class Meta:
- sequence = ("...", "b")
- assert ["a", "c", "b"] == TestTable6([]).columns.names()
- assert ["a", "c", "b"] == TestTable([], sequence=("...", "b")).columns.names()
-
- class TestTable7(TestTable):
- class Meta:
- sequence = ("b", "...", "a")
- assert ["b", "c", "a"] == TestTable7([]).columns.names()
- assert ["b", "c", "a"] == TestTable([], sequence=("b", "...", "a")).columns.names()
-
- # Let's test inheritence
- class TestTable8(TestTable):
- d = tables.Column()
- e = tables.Column()
- f = tables.Column()
-
- class Meta:
- sequence = ("d", "...")
-
- class TestTable9(TestTable):
- d = tables.Column()
- e = tables.Column()
- f = tables.Column()
-
- assert ["d", "a", "b", "c", "e", "f"] == TestTable8([]).columns.names()
- assert ["d", "a", "b", "c", "e", "f"] == TestTable9([], sequence=("d", "...")).columns.names()
-
-
-@general.test
-def should_support_both_meta_sequence_and_constructor_exclude():
- """
- Issue #32 describes a problem when both ``Meta.sequence`` and
- ``Table(..., exclude=...)`` are used on a single table. The bug caused an
- exception to be raised when the table was iterated.
- """
- class SequencedTable(tables.Table):
- a = tables.Column()
- b = tables.Column()
- c = tables.Column()
-
- class Meta:
- sequence = ('a', '...')
-
- table = SequencedTable([], exclude=('c', ))
- table.as_html()
-
-
-@general.test
-def bound_columns_should_support_indexing():
- class SimpleTable(tables.Table):
- a = tables.Column()
- b = tables.Column()
-
- table = SimpleTable([])
- assert 'b' == table.columns[1].name
- assert 'b' == table.columns['b'].name
-
-
-@general.test
-def cell_attrs_applies_to_td_and_th():
- class SimpleTable(tables.Table):
- a = tables.Column(attrs={"cell": {"key": "value"}})
-
- # providing data ensures 1 row is rendered
- table = SimpleTable([{"a": "value"}])
- root = parse(table.as_html())
-
- assert root.findall('.//thead/tr/th')[0].attrib == {"key": "value", "class": "a orderable sortable"}
- assert root.findall('.//tbody/tr/td')[0].attrib == {"key": "value", "class": "a"}
-
-
-@general.test
-def cells_are_automatically_given_column_name_as_class():
- class SimpleTable(tables.Table):
- a = tables.Column()
-
- table = SimpleTable([{"a": "value"}])
- root = parse(table.as_html())
- assert root.findall('.//thead/tr/th')[0].attrib == {"class": "a orderable sortable"}
- assert root.findall('.//tbody/tr/td')[0].attrib == {"class": "a"}
-
-
-@general.test
-def th_are_given_sortable_class_if_column_is_orderable():
- class SimpleTable(tables.Table):
- a = tables.Column()
- b = tables.Column(orderable=False)
-
- table = SimpleTable([{"a": "value"}])
- root = parse(table.as_html())
- # return classes of an element as a set
- classes = lambda x: set(x.attrib["class"].split())
- assert "sortable" in classes(root.findall('.//thead/tr/th')[0])
- assert "sortable" not in classes(root.findall('.//thead/tr/th')[1])
-
- # Now try with an ordered table
- table = SimpleTable([], order_by="a")
- root = parse(table.as_html())
- # return classes of an element as a set
- assert "sortable" in classes(root.findall('.//thead/tr/th')[0])
- assert "asc" in classes(root.findall('.//thead/tr/th')[0])
- assert "sortable" not in classes(root.findall('.//thead/tr/th')[1])
-
-
-@general.test
-def empty_values_triggers_default():
- class Table(tables.Table):
- a = tables.Column(empty_values=(1, 2), default="--")
-
- table = Table([{"a": 1}, {"a": 2}, {"a": 3}, {"a": 4}])
- assert [x["a"] for x in table.rows] == ["--", "--", 3, 4]
-
-
-linkcolumn = Tests()
-linkcolumn.context(TestContext())
-
-@linkcolumn.test
-def unicode():
- """Test LinkColumn"""
- # test unicode values + headings
- class UnicodeTable(tables.Table):
- first_name = tables.LinkColumn('person', args=[A('pk')])
- last_name = tables.LinkColumn('person', args=[A('pk')], verbose_name='äÚ¨´ˆÁ˜¨ˆ˜˘Ú…Ò˚ˆπ∆ˆ´')
-
- dataset = [
- {'pk': 1, 'first_name': 'Brädley', 'last_name': '∆yers'},
- {'pk': 2, 'first_name': 'Chr…s', 'last_name': 'DÒble'},
- ]
-
- table = UnicodeTable(dataset)
- request = build_request('/some-url/')
- template = Template('{% load django_tables2 %}{% render_table table %}')
- html = template.render(Context({'request': request, 'table': table}))
-
- assert 'Brädley' in html
- assert '∆yers' in html
- assert 'Chr…s' in html
- assert 'DÒble' in html
-
-
-@linkcolumn.test
-def 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())
- table.as_html()
-
-
-@linkcolumn.test
-def kwargs():
- class PersonTable(tables.Table):
- a = tables.LinkColumn('occupation', kwargs={"pk": A('a')})
-
- html = PersonTable([{"a": 0}, {"a": 1}]).as_html()
- assert reverse("occupation", kwargs={"pk": 0}) in html
- assert reverse("occupation", kwargs={"pk": 1}) in html
-
-
-@linkcolumn.test
-def html_escape_value():
- class PersonTable(tables.Table):
- name = tables.LinkColumn("escaping", kwargs={"pk": A("pk")})
-
- table = PersonTable([{"name": "", "pk": 1}])
- assert table.rows[0]["name"] == '<brad>'
-
-
-@linkcolumn.test
-def old_style_attrs_should_still_work():
- with warns(DeprecationWarning):
- class TestTable(tables.Table):
- col = tables.LinkColumn('occupation', kwargs={"pk": A('col')},
- attrs={"title": "Occupation Title"})
-
- table = TestTable([{"col": 0}])
- assert attrs(table.rows[0]["col"]) == {"href": reverse("occupation", kwargs={"pk": 0}),
- "title": "Occupation Title"}
-
-
-@linkcolumn.test
-def a_attrs_should_be_supported():
- class TestTable(tables.Table):
- col = tables.LinkColumn('occupation', kwargs={"pk": A('col')},
- attrs={"a": {"title": "Occupation Title"}})
-
- table = TestTable([{"col": 0}])
- assert attrs(table.rows[0]["col"]) == {"href": reverse("occupation", kwargs={"pk": 0}),
- "title": "Occupation Title"}
-
-
-@linkcolumn.test
-def defaults():
- class Table(tables.Table):
- link = tables.LinkColumn('occupation', kwargs={"pk": 1}, default="xyz")
-
- table = Table([{}])
- assert table.rows[0]['link'] == 'xyz'
-
-
-templatecolumn = Tests()
-
-
-@templatecolumn.test
-def should_handle_context_on_table():
- class TestTable(tables.Table):
- col_code = tables.TemplateColumn(template_code="code:{{ record.col }}{{ STATIC_URL }}")
- col_name = tables.TemplateColumn(template_name="test_template_column.html")
-
- table = TestTable([{"col": "brad"}])
- assert table.rows[0]["col_code"] == "code:brad"
- assert table.rows[0]["col_name"] == "name:brad"
- table.context = Context({"STATIC_URL": "/static/"})
- assert table.rows[0]["col_code"] == "code:brad/static/"
- assert table.rows[0]["col_name"] == "name:brad/static/"
-
-
-@templatecolumn.test
-def should_support_default():
- class Table(tables.Table):
- foo = tables.TemplateColumn("default={{ default }}", default="bar")
-
- table = Table([{}])
- assert table.rows[0]["foo"] == "default=bar"
-
-
-@templatecolumn.test
-def should_support_value():
- class Table(tables.Table):
- foo = tables.TemplateColumn("value={{ value }}")
-
- table = Table([{"foo": "bar"}])
- assert table.rows[0]["foo"] == "value=bar"
-
-
-urlcolumn = Tests()
-
-
-@urlcolumn.test
-def should_turn_url_into_hyperlink():
- class TestTable(tables.Table):
- url = tables.URLColumn()
-
- table = TestTable([{"url": "http://example.com"}])
- assert table.rows[0]["url"] == 'http://example.com'
-
-
-@urlcolumn.test
-def should_be_used_for_urlfields():
- class URLModel(models.Model):
- field = models.URLField()
-
- class Table(tables.Table):
- class Meta:
- model = URLModel
-
- assert type(Table.base_columns["field"]) == tables.URLColumn
-
-
-emailcolumn = Tests()
-
-
-@emailcolumn.test
-def should_turn_email_address_into_hyperlink():
- class Table(tables.Table):
- email = tables.EmailColumn()
-
- table = Table([{"email": "test@example.com"}])
- assert table.rows[0]["email"] == 'test@example.com'
-
-
-@emailcolumn.test
-def should_render_default_for_blank():
- class Table(tables.Table):
- email = tables.EmailColumn(default="---")
-
- table = Table([{"email": ""}])
- assert table.rows[0]["email"] == '---'
-
-
-@emailcolumn.test
-def should_be_used_for_datetimefields():
- class EmailModel(models.Model):
- field = models.EmailField()
-
- class Table(tables.Table):
- class Meta:
- model = EmailModel
-
- assert type(Table.base_columns["field"]) == tables.EmailColumn
-
-
-datecolumn = Tests()
-
-# Format string: https://docs.djangoproject.com/en/1.4/ref/templates/builtins/#date
-# D -- Day of the week, textual, 3 letters -- 'Fri'
-# b -- Month, textual, 3 letters, lowercase -- 'jan'
-# Y -- Year, 4 digits. -- '1999'
-
-@datecolumn.test
-def should_handle_explicit_format():
- class TestTable(tables.Table):
- date = tables.DateColumn(format="D b Y")
-
- class Meta:
- default = "—"
-
- table = TestTable([{"date": date(2012, 9, 11)},
- {"date": None}])
- assert table.rows[0]["date"] == "Tue sep 2012"
- assert table.rows[1]["date"] == "—"
-
-
-@datecolumn.test
-def should_handle_long_format():
- with settings(DATE_FORMAT="D Y b"):
- class TestTable(tables.Table):
- date = tables.DateColumn(short=False)
-
- class Meta:
- default = "—"
-
- table = TestTable([{"date": date(2012, 9, 11)},
- {"date": None}])
- assert table.rows[0]["date"] == "Tue 2012 sep"
- assert table.rows[1]["date"] == "—"
-
-
-@datecolumn.test
-def should_handle_short_format():
- with settings(SHORT_DATE_FORMAT="b Y D"):
- class TestTable(tables.Table):
- date = tables.DateColumn(short=True)
-
- class Meta:
- default = "—"
-
- table = TestTable([{"date": date(2012, 9, 11)},
- {"date": None}])
- assert table.rows[0]["date"] == "sep 2012 Tue"
- assert table.rows[1]["date"] == "—"
-
-
-@datecolumn.test
-def should_be_used_for_datefields():
- class DateModel(models.Model):
- field = models.DateField()
-
- class Table(tables.Table):
- class Meta:
- model = DateModel
-
- assert type(Table.base_columns["field"]) == tables.DateColumn
-
-
-datetimecolumn = Tests()
-
-# Format string: https://docs.djangoproject.com/en/1.4/ref/templates/builtins/#date
-# D -- Day of the week, textual, 3 letters -- 'Fri'
-# b -- Month, textual, 3 letters, lowercase -- 'jan'
-# Y -- Year, 4 digits. -- '1999'
-# A -- 'AM' or 'PM'. -- 'AM'
-# f -- Time, in 12-hour hours[:minutes] -- '1', '1:30'
-
-
-@datetimecolumn.context
-def dt():
- dt = datetime(2012, 9, 11, 12, 30, 0)
- if timezone:
- # If the version of Django has timezone support, convert from naive to
- # UTC, the test project uses Australia/Brisbane so regardless the
- # output from the column should be the same.
- dt = pytz.timezone("Australia/Brisbane").localize(dt)
- yield dt
-
-
-@datetimecolumn.test
-def should_handle_explicit_format(dt):
- class TestTable(tables.Table):
- date = tables.DateTimeColumn(format="D b Y")
-
- class Meta:
- default = "—"
-
- table = TestTable([{"date": dt}, {"date": None}])
- assert table.rows[0]["date"] == "Tue sep 2012"
- assert table.rows[1]["date"] == "—"
-
-
-@datetimecolumn.test
-def should_handle_long_format(dt):
- class TestTable(tables.Table):
- date = tables.DateTimeColumn(short=False)
-
- class Meta:
- default = "—"
-
- with settings(DATETIME_FORMAT="D Y b A f"):
- table = TestTable([{"date": dt}, {"date": None}])
- assert table.rows[0]["date"] == "Tue 2012 sep PM 12:30"
- assert table.rows[1]["date"] == "—"
-
-
-@datetimecolumn.test
-def should_handle_short_format(dt):
- class TestTable(tables.Table):
- date = tables.DateTimeColumn(short=True)
-
- class Meta:
- default = "—"
-
- with settings(SHORT_DATETIME_FORMAT="b Y D A f"):
- table = TestTable([{"date": dt}, {"date": None}])
- assert table.rows[0]["date"] == "sep 2012 Tue PM 12:30"
- assert table.rows[1]["date"] == "—"
-
-
-@datetimecolumn.test
-def should_be_used_for_datetimefields():
- class DateTimeModel(models.Model):
- field = models.DateTimeField()
-
- class Table(tables.Table):
- class Meta:
- model = DateTimeModel
-
- assert type(Table.base_columns["field"]) == tables.DateTimeColumn
-
-
-filecolumn = Tests()
-
-
-@filecolumn.context
-def template_storage_and_column():
- """Provide a storage that exposes the test templates"""
- root = join(dirname(__file__), "app", "templates")
- storage = FileSystemStorage(location=root, base_url="/baseurl/")
- column = tables.FileColumn(attrs={"span": {"class": "span"},
- "a": {"class": "a"}})
- yield column, storage
-
-
-@filecolumn.test
-def should_be_used_for_filefields():
- class FileModel(models.Model):
- field = models.FileField()
-
- class Table(tables.Table):
- class Meta:
- model = FileModel
-
- assert type(Table.base_columns["field"]) == tables.FileColumn
-
-
-@filecolumn.test
-def filecolumn_supports_storage_file(column, storage):
- file_ = storage.open("child/foo.html")
- try:
- root = parse(column.render(value=file_))
- finally:
- file_.close()
- path = file_.name
- assert root.tag == "span"
- assert root.attrib == {"class": "span exists", "title": path}
- assert root.text == "foo.html"
-
-
-@filecolumn.test
-def filecolumn_supports_contentfile(column):
- name = "foobar.html"
- file_ = ContentFile('')
- file_.name = name # Django <1.4 compatible
- root = parse(column.render(value=file_))
- assert root.tag == "span"
- assert root.attrib == {"title": name, "class": "span"}
- assert root.text == "foobar.html"
-
-
-@filecolumn.test
-def filecolumn_supports_fieldfile(column, storage):
- field = models.FileField(storage=storage)
- name = "child/foo.html"
- fieldfile = FieldFile(instance=None, field=field, name=name)
- root = parse(column.render(value=fieldfile))
- assert root.tag == "a"
- assert root.attrib == {"class": "a exists", "title": name,
- "href": "/baseurl/child/foo.html"}
- assert root.text == "foo.html"
-
- # Now try a file that doesn't exist
- name = "child/does_not_exist.html"
- fieldfile = FieldFile(instance=None, field=field, name=name)
- html = column.render(value=fieldfile)
- root = parse(html)
- assert root.tag == "a"
- assert root.attrib == {"class": "a missing", "title": name,
- "href": "/baseurl/child/does_not_exist.html"}
- assert root.text == "does_not_exist.html"
-
-
-columns = Tests([booleancolumn, checkboxcolumn, datecolumn, datetimecolumn,
- emailcolumn, filecolumn, general, linkcolumn, templatecolumn,
- urlcolumn])
diff --git a/tests/columns/__init__.py b/tests/columns/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/columns/test_booleancolumn.py b/tests/columns/test_booleancolumn.py
new file mode 100644
index 0000000..f3dc9f9
--- /dev/null
+++ b/tests/columns/test_booleancolumn.py
@@ -0,0 +1,56 @@
+# coding: utf-8
+# pylint: disable=R0912,E0102
+from __future__ import unicode_literals
+import django_tables2 as tables
+from django.db import models
+from ..utils import attrs
+
+
+def test_should_be_used_for_booleanfield():
+ class BoolModel(models.Model):
+ field = models.BooleanField()
+
+ class Table(tables.Table):
+ class Meta:
+ model = BoolModel
+
+ column = Table.base_columns["field"]
+ assert type(column) == tables.BooleanColumn
+ assert column.empty_values != ()
+
+
+def test_should_be_used_for_nullbooleanfield():
+ class NullBoolModel(models.Model):
+ field = models.NullBooleanField()
+
+ class Table(tables.Table):
+ class Meta:
+ model = NullBoolModel
+
+ column = Table.base_columns["field"]
+ assert type(column) == tables.BooleanColumn
+ assert column.empty_values == ()
+
+
+def test_treat_none_different_from_false():
+ class Table(tables.Table):
+ col = tables.BooleanColumn(null=False, default="---")
+
+ table = Table([{"col": None}])
+ assert table.rows[0]["col"] == "---"
+
+
+def test_treat_none_as_false():
+ class Table(tables.Table):
+ col = tables.BooleanColumn(null=True)
+
+ table = Table([{"col": None}])
+ assert table.rows[0]["col"] == '✘'
+
+
+def test_span_attrs():
+ class Table(tables.Table):
+ col = tables.BooleanColumn(attrs={"span": {"key": "value"}})
+
+ table = Table([{"col": True}])
+ assert attrs(table.rows[0]["col"]) == {"class": "true", "key": "value"}
diff --git a/tests/columns/test_checkboxcolumn.py b/tests/columns/test_checkboxcolumn.py
new file mode 100644
index 0000000..a1d0e9c
--- /dev/null
+++ b/tests/columns/test_checkboxcolumn.py
@@ -0,0 +1,31 @@
+# coding: utf-8
+# pylint: disable=R0912,E0102
+from __future__ import unicode_literals
+import django_tables2 as tables
+from django_tables2 import Attrs
+from ..utils import attrs, warns
+
+
+def test_attrs_should_be_translated_for_backwards_compatibility():
+ with warns(DeprecationWarning):
+ class TestTable(tables.Table):
+ col = tables.CheckBoxColumn(header_attrs={"th_key": "th_value"},
+ attrs={"td_key": "td_value"})
+
+ table = TestTable([{"col": "data"}])
+ assert attrs(table.columns["col"].header) == {"type": "checkbox", "th_key": "th_value"}
+ assert attrs(table.rows[0]["col"]) == {"type": "checkbox", "td_key": "td_value", "value": "data", "name": "col"}
+
+
+def new_attrs_should_be_supported():
+ with warns(DeprecationWarning):
+ class TestTable(tables.Table):
+ col1 = tables.CheckBoxColumn(attrs=Attrs(th__input={"th_key": "th_value"},
+ td__input={"td_key": "td_value"}))
+ col2 = tables.CheckBoxColumn(attrs=Attrs(input={"key": "value"}))
+
+ table = TestTable([{"col1": "data", "col2": "data"}])
+ assert attrs(table.columns["col1"].header) == {"type": "checkbox", "th_key": "th_value"}
+ assert attrs(table.rows[0]["col1"]) == {"type": "checkbox", "td_key": "td_value", "value": "data", "name": "col1"}
+ assert attrs(table.columns["col2"].header) == {"type": "checkbox", "key": "value"}
+ assert attrs(table.rows[0]["col2"]) == {"type": "checkbox", "key": "value", "value": "data", "name": "col2"}
diff --git a/tests/columns/test_datecolumn.py b/tests/columns/test_datecolumn.py
new file mode 100644
index 0000000..bf3f019
--- /dev/null
+++ b/tests/columns/test_datecolumn.py
@@ -0,0 +1,66 @@
+# coding: utf-8
+# pylint: disable=R0912,E0102
+from __future__ import unicode_literals
+from datetime import date
+
+from django.db import models
+
+import django_tables2 as tables
+
+# Format string: https://docs.djangoproject.com/en/1.4/ref/templates/builtins/#date
+# D -- Day of the week, textual, 3 letters -- 'Fri'
+# b -- Month, textual, 3 letters, lowercase -- 'jan'
+# Y -- Year, 4 digits. -- '1999'
+
+def test_should_handle_explicit_format():
+ class TestTable(tables.Table):
+ date = tables.DateColumn(format="D b Y")
+
+ class Meta:
+ default = "—"
+
+ table = TestTable([{"date": date(2012, 9, 11)},
+ {"date": None}])
+ assert table.rows[0]["date"] == "Tue sep 2012"
+ assert table.rows[1]["date"] == "—"
+
+
+def test_should_handle_long_format(settings):
+ settings.DATE_FORMAT = "D Y b"
+
+ class TestTable(tables.Table):
+ date = tables.DateColumn(short=False)
+
+ class Meta:
+ default = "—"
+
+ table = TestTable([{"date": date(2012, 9, 11)},
+ {"date": None}])
+ assert table.rows[0]["date"] == "Tue 2012 sep"
+ assert table.rows[1]["date"] == "—"
+
+
+def test_should_handle_short_format(settings):
+ settings.SHORT_DATE_FORMAT = "b Y D"
+
+ class TestTable(tables.Table):
+ date = tables.DateColumn(short=True)
+
+ class Meta:
+ default = "—"
+
+ table = TestTable([{"date": date(2012, 9, 11)},
+ {"date": None}])
+ assert table.rows[0]["date"] == "sep 2012 Tue"
+ assert table.rows[1]["date"] == "—"
+
+
+def test_should_be_used_for_datefields():
+ class DateModel(models.Model):
+ field = models.DateField()
+
+ class Table(tables.Table):
+ class Meta:
+ model = DateModel
+
+ assert type(Table.base_columns["field"]) == tables.DateColumn
diff --git a/tests/columns/test_datetimecolumn.py b/tests/columns/test_datetimecolumn.py
new file mode 100644
index 0000000..ff41e7d
--- /dev/null
+++ b/tests/columns/test_datetimecolumn.py
@@ -0,0 +1,81 @@
+# coding: utf-8
+# pylint: disable=R0912,E0102
+from __future__ import unicode_literals
+from datetime import datetime
+
+from django.db import models
+
+import django_tables2 as tables
+
+try:
+ from django.utils import timezone
+except ImportError:
+ timezone = None
+import pytz
+import pytest
+
+# Format string: https://docs.djangoproject.com/en/1.4/ref/templates/builtins/#date
+# D -- Day of the week, textual, 3 letters -- 'Fri'
+# b -- Month, textual, 3 letters, lowercase -- 'jan'
+# Y -- Year, 4 digits. -- '1999'
+# A -- 'AM' or 'PM'. -- 'AM'
+# f -- Time, in 12-hour hours[:minutes] -- '1', '1:30'
+
+@pytest.yield_fixture
+def dt():
+ dt = datetime(2012, 9, 11, 12, 30, 0)
+ if timezone:
+ # If the version of Django has timezone support, convert from naive to
+ # UTC, the test project uses Australia/Brisbane so regardless the
+ # output from the column should be the same.
+ dt = pytz.timezone("Australia/Brisbane").localize(dt)
+ yield dt
+
+
+def test_should_handle_explicit_format(dt):
+ class TestTable(tables.Table):
+ date = tables.DateTimeColumn(format="D b Y")
+
+ class Meta:
+ default = "—"
+
+ table = TestTable([{"date": dt}, {"date": None}])
+ assert table.rows[0]["date"] == "Tue sep 2012"
+ assert table.rows[1]["date"] == "—"
+
+
+def test_should_handle_long_format(dt, settings):
+ class TestTable(tables.Table):
+ date = tables.DateTimeColumn(short=False)
+
+ class Meta:
+ default = "—"
+
+ settings.DATETIME_FORMAT = "D Y b A f"
+ table = TestTable([{"date": dt}, {"date": None}])
+ assert table.rows[0]["date"] == "Tue 2012 sep PM 12:30"
+ assert table.rows[1]["date"] == "—"
+
+
+def test_should_handle_short_format(dt, settings):
+ class TestTable(tables.Table):
+ date = tables.DateTimeColumn(short=True)
+
+ class Meta:
+ default = "—"
+
+ settings.SHORT_DATETIME_FORMAT = "b Y D A f"
+ table = TestTable([{"date": dt}, {"date": None}])
+ assert table.rows[0]["date"] == "sep 2012 Tue PM 12:30"
+ assert table.rows[1]["date"] == "—"
+
+
+def test_should_be_used_for_datetimefields():
+ class DateTimeModel(models.Model):
+ field = models.DateTimeField()
+
+ class Table(tables.Table):
+ class Meta:
+ model = DateTimeModel
+
+ assert type(Table.base_columns["field"]) == tables.DateTimeColumn
diff --git a/tests/columns/test_emailcolumn.py b/tests/columns/test_emailcolumn.py
new file mode 100644
index 0000000..426ed8e
--- /dev/null
+++ b/tests/columns/test_emailcolumn.py
@@ -0,0 +1,39 @@
+# coding: utf-8
+# pylint: disable=R0912,E0102
+from __future__ import unicode_literals
+
+from django.db import models
+
+import django_tables2 as tables
+
+try:
+ from django.utils import timezone
+except ImportError:
+ timezone = None
+
+
+def test_should_turn_email_address_into_hyperlink():
+ class Table(tables.Table):
+ email = tables.EmailColumn()
+
+ table = Table([{"email": "test@example.com"}])
+ assert table.rows[0]["email"] == 'test@example.com'
+
+
+def test_should_render_default_for_blank():
+ class Table(tables.Table):
+ email = tables.EmailColumn(default="---")
+
+ table = Table([{"email": ""}])
+ assert table.rows[0]["email"] == '---'
+
+
+def test_should_be_used_for_datetimefields():
+ class EmailModel(models.Model):
+ field = models.EmailField()
+
+ class Table(tables.Table):
+ class Meta:
+ model = EmailModel
+
+ assert type(Table.base_columns["field"]) == tables.EmailColumn
diff --git a/tests/columns/test_filecolumn.py b/tests/columns/test_filecolumn.py
new file mode 100644
index 0000000..56272f2
--- /dev/null
+++ b/tests/columns/test_filecolumn.py
@@ -0,0 +1,81 @@
+# coding: utf-8
+# pylint: disable=R0912,E0102
+from __future__ import unicode_literals
+
+from os.path import dirname, join
+
+from django.db import models
+from django.db.models.fields.files import FieldFile
+from django.core.files.base import ContentFile
+from django.core.files.storage import FileSystemStorage
+import pytest
+
+import django_tables2 as tables
+from ..utils import parse
+
+
+@pytest.yield_fixture
+def storage():
+ """Provide a storage that exposes the test templates"""
+ root = join(dirname(__file__), "..", "app", "templates")
+ yield FileSystemStorage(location=root, base_url="/baseurl/")
+
+
+@pytest.yield_fixture
+def column():
+ yield tables.FileColumn(attrs={"span": {"class": "span"},
+ "a": {"class": "a"}})
+
+
+def test_should_be_used_for_filefields():
+ class FileModel(models.Model):
+ field = models.FileField()
+
+ class Table(tables.Table):
+ class Meta:
+ model = FileModel
+
+ assert type(Table.base_columns["field"]) == tables.FileColumn
+
+
+def test_filecolumn_supports_storage_file(column, storage):
+ file_ = storage.open("child/foo.html")
+ try:
+ root = parse(column.render(value=file_))
+ finally:
+ file_.close()
+ path = file_.name
+ assert root.tag == "span"
+ assert root.attrib == {"class": "span exists", "title": path}
+ assert root.text == "foo.html"
+
+
+def test_filecolumn_supports_contentfile(column):
+ name = "foobar.html"
+ file_ = ContentFile('')
+ file_.name = name # Django <1.4 compatible
+ root = parse(column.render(value=file_))
+ assert root.tag == "span"
+ assert root.attrib == {"title": name, "class": "span"}
+ assert root.text == "foobar.html"
+
+
+def test_filecolumn_supports_fieldfile(column, storage):
+ field = models.FileField(storage=storage)
+ name = "child/foo.html"
+ fieldfile = FieldFile(instance=None, field=field, name=name)
+ root = parse(column.render(value=fieldfile))
+ assert root.tag == "a"
+ assert root.attrib == {"class": "a exists", "title": name,
+ "href": "/baseurl/child/foo.html"}
+ assert root.text == "foo.html"
+
+ # Now try a file that doesn't exist
+ name = "child/does_not_exist.html"
+ fieldfile = FieldFile(instance=None, field=field, name=name)
+ html = column.render(value=fieldfile)
+ root = parse(html)
+ assert root.tag == "a"
+ assert root.attrib == {"class": "a missing", "title": name,
+ "href": "/baseurl/child/does_not_exist.html"}
+ assert root.text == "does_not_exist.html"
diff --git a/tests/columns/test_general.py b/tests/columns/test_general.py
new file mode 100644
index 0000000..b5a2443
--- /dev/null
+++ b/tests/columns/test_general.py
@@ -0,0 +1,317 @@
+# coding: utf-8
+# pylint: disable=R0912,E0102
+from __future__ import unicode_literals
+
+from django.utils.translation import ugettext_lazy
+from django.utils.safestring import mark_safe, SafeData
+
+import django_tables2 as tables
+from ..app.models import Person
+from ..utils import parse, warns
+
+
+def test_column_render_supports_kwargs():
+ class TestColumn(tables.Column):
+ def render(self, **kwargs):
+ expected = {"record", "value", "column", "bound_column", "bound_row", "table"}
+ actual = set(kwargs.keys())
+ assert actual == expected
+ return "success"
+
+ class TestTable(tables.Table):
+ foo = TestColumn()
+
+ table = TestTable([{"foo": "bar"}])
+ assert table.rows[0]["foo"] == "success"
+
+
+def test_column_header_should_use_titlised_verbose_name_unless_given_explicitly():
+ class SimpleTable(tables.Table):
+ basic = tables.Column()
+ acronym = tables.Column(verbose_name="has FBI help")
+
+ table = SimpleTable([])
+ assert table.columns["basic"].header == "Basic"
+ assert table.columns["acronym"].header == "has FBI help"
+
+
+def test_should_support_safe_verbose_name():
+ class SimpleTable(tables.Table):
+ safe = tables.Column(verbose_name=mark_safe("Safe"))
+
+ table = SimpleTable([])
+ assert isinstance(table.columns["safe"].header, SafeData)
+
+
+def test_should_support_safe_verbose_name_via_model():
+ class PersonTable(tables.Table):
+ safe = tables.Column()
+
+ table = PersonTable(Person.objects.all())
+ assert isinstance(table.columns["safe"].header, SafeData)
+
+
+def test_sortable_backwards_compatibility():
+ # Table.Meta.sortable (not set)
+ class SimpleTable(tables.Table):
+ name = tables.Column()
+ table = SimpleTable([])
+ with warns(DeprecationWarning):
+ assert table.columns['name'].sortable is True
+
+ # Table.Meta.sortable = False
+ with warns(DeprecationWarning):
+ class SimpleTable(tables.Table):
+ name = tables.Column()
+
+ class Meta:
+ sortable = False
+ table = SimpleTable([])
+ with warns(DeprecationWarning):
+ assert table.columns['name'].sortable is False # backwards compatible
+ assert table.columns['name'].orderable is False
+
+ # Table.Meta.sortable = True
+ with warns(DeprecationWarning):
+ class SimpleTable(tables.Table):
+ name = tables.Column()
+
+ class Meta:
+ sortable = True
+ table = SimpleTable([])
+ with warns(DeprecationWarning):
+ assert table.columns['name'].sortable is True # backwards compatible
+ assert table.columns['name'].orderable is True
+
+
+def test_orderable():
+ # Table.Meta.orderable = False
+ class SimpleTable(tables.Table):
+ name = tables.Column()
+ table = SimpleTable([])
+ assert table.columns['name'].orderable is True
+
+ # Table.Meta.orderable = False
+ class SimpleTable(tables.Table):
+ name = tables.Column()
+
+ class Meta:
+ orderable = False
+ table = SimpleTable([])
+ assert table.columns['name'].orderable is False
+
+ with warns(DeprecationWarning):
+ assert table.columns['name'].sortable is False # backwards compatible
+
+ # Table.Meta.orderable = True
+ class SimpleTable(tables.Table):
+ name = tables.Column()
+
+ class Meta:
+ orderable = True
+ table = SimpleTable([])
+ with warns(DeprecationWarning):
+ assert table.columns['name'].sortable is True # backwards compatible
+ assert table.columns['name'].orderable is True
+
+
+def test_order_by_defaults_to_accessor():
+ class SimpleTable(tables.Table):
+ foo = tables.Column(accessor="bar")
+
+ table = SimpleTable([])
+ assert table.columns["foo"].order_by == ("bar", )
+
+
+def test_supports_order_by():
+ class SimpleTable(tables.Table):
+ name = tables.Column(order_by=("last_name", "-first_name"))
+ age = tables.Column()
+
+ table = SimpleTable([], order_by=("-age", ))
+ # alias
+ assert table.columns["name"].order_by_alias == "name"
+ assert table.columns["age"].order_by_alias == "-age"
+ # order by
+ assert table.columns["name"].order_by == ("last_name", "-first_name")
+ assert table.columns["age"].order_by == ("-age", )
+
+ # now try with name ordered
+ table = SimpleTable([], order_by=("-name", ))
+ # alias
+ assert table.columns["name"].order_by_alias == "-name"
+ assert table.columns["age"].order_by_alias == "age"
+ # alias next
+ assert table.columns["name"].order_by_alias.next == "name"
+ assert table.columns["age"].order_by_alias.next == "age"
+ # order by
+ assert table.columns["name"].order_by == ("-last_name", "first_name")
+ assert table.columns["age"].order_by == ("age", )
+
+
+def test_supports_is_ordered():
+ class SimpleTable(tables.Table):
+ name = tables.Column()
+
+ # sorted
+ table = SimpleTable([], order_by='name')
+ assert table.columns["name"].is_ordered
+ # unsorted
+ table = SimpleTable([])
+ assert not table.columns["name"].is_ordered
+
+
+def test_translation():
+ """
+ Tests different types of values for the ``verbose_name`` property of a
+ column.
+ """
+ class TranslationTable(tables.Table):
+ text = tables.Column(verbose_name=ugettext_lazy("Text"))
+
+ table = TranslationTable([])
+ assert "Text" == table.columns["text"].header
+
+
+def test_sequence():
+ """
+ Ensures that the sequence of columns is configurable.
+ """
+ class TestTable(tables.Table):
+ a = tables.Column()
+ b = tables.Column()
+ c = tables.Column()
+ assert ["a", "b", "c"] == TestTable([]).columns.names()
+ assert ["b", "a", "c"] == TestTable([], sequence=("b", "a", "c")).columns.names()
+
+ class TestTable2(TestTable):
+ class Meta:
+ sequence = ("b", "a", "c")
+ assert ["b", "a", "c"] == TestTable2([]).columns.names()
+ assert ["a", "b", "c"] == TestTable2([], sequence=("a", "b", "c")).columns.names()
+
+ class TestTable3(TestTable):
+ class Meta:
+ sequence = ("c", )
+ assert ["c", "a", "b"] == TestTable3([]).columns.names()
+ assert ["c", "a", "b"] == TestTable([], sequence=("c", )).columns.names()
+
+ class TestTable4(TestTable):
+ class Meta:
+ sequence = ("...", )
+ assert ["a", "b", "c"] == TestTable4([]).columns.names()
+ assert ["a", "b", "c"] == TestTable([], sequence=("...", )).columns.names()
+
+ class TestTable5(TestTable):
+ class Meta:
+ sequence = ("b", "...")
+ assert ["b", "a", "c"] == TestTable5([]).columns.names()
+ assert ["b", "a", "c"] == TestTable([], sequence=("b", "...")).columns.names()
+
+ class TestTable6(TestTable):
+ class Meta:
+ sequence = ("...", "b")
+ assert ["a", "c", "b"] == TestTable6([]).columns.names()
+ assert ["a", "c", "b"] == TestTable([], sequence=("...", "b")).columns.names()
+
+ class TestTable7(TestTable):
+ class Meta:
+ sequence = ("b", "...", "a")
+ assert ["b", "c", "a"] == TestTable7([]).columns.names()
+ assert ["b", "c", "a"] == TestTable([], sequence=("b", "...", "a")).columns.names()
+
+ # Let's test inheritence
+ class TestTable8(TestTable):
+ d = tables.Column()
+ e = tables.Column()
+ f = tables.Column()
+
+ class Meta:
+ sequence = ("d", "...")
+
+ class TestTable9(TestTable):
+ d = tables.Column()
+ e = tables.Column()
+ f = tables.Column()
+
+ assert ["d", "a", "b", "c", "e", "f"] == TestTable8([]).columns.names()
+ assert ["d", "a", "b", "c", "e", "f"] == TestTable9([], sequence=("d", "...")).columns.names()
+
+
+def test_should_support_both_meta_sequence_and_constructor_exclude():
+ """
+ Issue #32 describes a problem when both ``Meta.sequence`` and
+ ``Table(..., exclude=...)`` are used on a single table. The bug caused an
+ exception to be raised when the table was iterated.
+ """
+ class SequencedTable(tables.Table):
+ a = tables.Column()
+ b = tables.Column()
+ c = tables.Column()
+
+ class Meta:
+ sequence = ('a', '...')
+
+ table = SequencedTable([], exclude=('c', ))
+ table.as_html()
+
+
+def test_bound_columns_should_support_indexing():
+ class SimpleTable(tables.Table):
+ a = tables.Column()
+ b = tables.Column()
+
+ table = SimpleTable([])
+ assert 'b' == table.columns[1].name
+ assert 'b' == table.columns['b'].name
+
+
+def test_cell_attrs_applies_to_td_and_th():
+ class SimpleTable(tables.Table):
+ a = tables.Column(attrs={"cell": {"key": "value"}})
+
+ # providing data ensures 1 row is rendered
+ table = SimpleTable([{"a": "value"}])
+ root = parse(table.as_html())
+
+ assert root.findall('.//thead/tr/th')[0].attrib == {"key": "value", "class": "a orderable sortable"}
+ assert root.findall('.//tbody/tr/td')[0].attrib == {"key": "value", "class": "a"}
+
+
+def test_cells_are_automatically_given_column_name_as_class():
+ class SimpleTable(tables.Table):
+ a = tables.Column()
+
+ table = SimpleTable([{"a": "value"}])
+ root = parse(table.as_html())
+ assert root.findall('.//thead/tr/th')[0].attrib == {"class": "a orderable sortable"}
+ assert root.findall('.//tbody/tr/td')[0].attrib == {"class": "a"}
+
+
+def test_th_are_given_sortable_class_if_column_is_orderable():
+ class SimpleTable(tables.Table):
+ a = tables.Column()
+ b = tables.Column(orderable=False)
+
+ table = SimpleTable([{"a": "value"}])
+ root = parse(table.as_html())
+ # return classes of an element as a set
+ classes = lambda x: set(x.attrib["class"].split())
+ assert "sortable" in classes(root.findall('.//thead/tr/th')[0])
+ assert "sortable" not in classes(root.findall('.//thead/tr/th')[1])
+
+ # Now try with an ordered table
+ table = SimpleTable([], order_by="a")
+ root = parse(table.as_html())
+ # return classes of an element as a set
+ assert "sortable" in classes(root.findall('.//thead/tr/th')[0])
+ assert "asc" in classes(root.findall('.//thead/tr/th')[0])
+ assert "sortable" not in classes(root.findall('.//thead/tr/th')[1])
+
+
+def test_empty_values_triggers_default():
+ class Table(tables.Table):
+ a = tables.Column(empty_values=(1, 2), default="--")
+
+ table = Table([{"a": 1}, {"a": 2}, {"a": 3}, {"a": 4}])
+ assert [x["a"] for x in table.rows] == ["--", "--", 3, 4]
diff --git a/tests/columns/test_linkcolumn.py b/tests/columns/test_linkcolumn.py
new file mode 100644
index 0000000..c3ccd1c
--- /dev/null
+++ b/tests/columns/test_linkcolumn.py
@@ -0,0 +1,95 @@
+# coding: utf-8
+# pylint: disable=R0912,E0102
+from __future__ import unicode_literals
+
+from django.core.urlresolvers import reverse
+from django.template import Context, Template
+import pytest
+
+import django_tables2 as tables
+from django_tables2.utils import build_request
+from django_tables2 import A
+from ..app.models import Person
+from ..utils import attrs, warns
+
+
+def test_unicode():
+ """Test LinkColumn"""
+ # test unicode values + headings
+ class UnicodeTable(tables.Table):
+ first_name = tables.LinkColumn('person', args=[A('pk')])
+ last_name = tables.LinkColumn('person', args=[A('pk')], verbose_name='äÚ¨´ˆÁ˜¨ˆ˜˘Ú…Ò˚ˆπ∆ˆ´')
+
+ dataset = [
+ {'pk': 1, 'first_name': 'Brädley', 'last_name': '∆yers'},
+ {'pk': 2, 'first_name': 'Chr…s', 'last_name': 'DÒble'},
+ ]
+
+ table = UnicodeTable(dataset)
+ request = build_request('/some-url/')
+ template = Template('{% load django_tables2 %}{% render_table table %}')
+ html = template.render(Context({'request': request, 'table': table}))
+
+ assert 'Brädley' in html
+ assert '∆yers' in html
+ assert 'Chr…s' in html
+ assert 'DÒble' in html
+
+
+@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())
+ table.as_html()
+
+
+def test_kwargs():
+ class PersonTable(tables.Table):
+ a = tables.LinkColumn('occupation', kwargs={"pk": A('a')})
+
+ html = PersonTable([{"a": 0}, {"a": 1}]).as_html()
+ assert reverse("occupation", kwargs={"pk": 0}) in html
+ assert reverse("occupation", kwargs={"pk": 1}) in html
+
+
+def test_html_escape_value():
+ class PersonTable(tables.Table):
+ name = tables.LinkColumn("escaping", kwargs={"pk": A("pk")})
+
+ table = PersonTable([{"name": "", "pk": 1}])
+ assert table.rows[0]["name"] == '<brad>'
+
+
+def test_old_style_attrs_should_still_work():
+ with warns(DeprecationWarning):
+ class TestTable(tables.Table):
+ col = tables.LinkColumn('occupation', kwargs={"pk": A('col')},
+ attrs={"title": "Occupation Title"})
+
+ table = TestTable([{"col": 0}])
+ assert attrs(table.rows[0]["col"]) == {"href": reverse("occupation", kwargs={"pk": 0}),
+ "title": "Occupation Title"}
+
+
+def test_a_attrs_should_be_supported():
+ class TestTable(tables.Table):
+ col = tables.LinkColumn('occupation', kwargs={"pk": A('col')},
+ attrs={"a": {"title": "Occupation Title"}})
+
+ table = TestTable([{"col": 0}])
+ assert attrs(table.rows[0]["col"]) == {"href": reverse("occupation", kwargs={"pk": 0}),
+ "title": "Occupation Title"}
+
+
+def test_defaults():
+ class Table(tables.Table):
+ link = tables.LinkColumn('occupation', kwargs={"pk": 1}, default="xyz")
+
+ table = Table([{}])
+ assert table.rows[0]['link'] == 'xyz'
diff --git a/tests/columns/test_templatecolumn.py b/tests/columns/test_templatecolumn.py
new file mode 100644
index 0000000..a346279
--- /dev/null
+++ b/tests/columns/test_templatecolumn.py
@@ -0,0 +1,36 @@
+# coding: utf-8
+# pylint: disable=R0912,E0102
+from __future__ import unicode_literals
+
+from django.template import Context
+
+import django_tables2 as tables
+
+
+def test_should_handle_context_on_table():
+ class TestTable(tables.Table):
+ col_code = tables.TemplateColumn(template_code="code:{{ record.col }}{{ STATIC_URL }}")
+ col_name = tables.TemplateColumn(template_name="test_template_column.html")
+
+ table = TestTable([{"col": "brad"}])
+ assert table.rows[0]["col_code"] == "code:brad"
+ assert table.rows[0]["col_name"] == "name:brad"
+ table.context = Context({"STATIC_URL": "/static/"})
+ assert table.rows[0]["col_code"] == "code:brad/static/"
+ assert table.rows[0]["col_name"] == "name:brad/static/"
+
+
+def test_should_support_default():
+ class Table(tables.Table):
+ foo = tables.TemplateColumn("default={{ default }}", default="bar")
+
+ table = Table([{}])
+ assert table.rows[0]["foo"] == "default=bar"
+
+
+def test_should_support_value():
+ class Table(tables.Table):
+ foo = tables.TemplateColumn("value={{ value }}")
+
+ table = Table([{"foo": "bar"}])
+ assert table.rows[0]["foo"] == "value=bar"
diff --git a/tests/columns/test_urlcolumn.py b/tests/columns/test_urlcolumn.py
new file mode 100644
index 0000000..4ff4cb3
--- /dev/null
+++ b/tests/columns/test_urlcolumn.py
@@ -0,0 +1,26 @@
+# coding: utf-8
+# pylint: disable=R0912,E0102
+from __future__ import unicode_literals
+
+from django.db import models
+
+import django_tables2 as tables
+
+
+def test_should_turn_url_into_hyperlink():
+ class TestTable(tables.Table):
+ url = tables.URLColumn()
+
+ table = TestTable([{"url": "http://example.com"}])
+ assert table.rows[0]["url"] == 'http://example.com'
+
+
+def test_should_be_used_for_urlfields():
+ class URLModel(models.Model):
+ field = models.URLField()
+
+ class Table(tables.Table):
+ class Meta:
+ model = URLModel
+
+ assert type(Table.base_columns["field"]) == tables.URLColumn
diff --git a/tests/config.py b/tests/test_config.py
similarity index 83%
rename from tests/config.py
rename to tests/test_config.py
index 450c26b..3599a2a 100644
--- a/tests/config.py
+++ b/tests/test_config.py
@@ -1,33 +1,30 @@
# coding: utf-8
-from attest import Tests
from django_tables2 import RequestConfig
from django_tables2.utils import build_request
from django.core.paginator import EmptyPage, PageNotAnInteger
from fudge import Fake
+import pytest
NOTSET = object() # unique value
-requestconfig = Tests()
-@requestconfig.context
-def faketable():
+@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"))
-@requestconfig.test
-def no_querystring(table):
+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
-@requestconfig.test
-def full_querystring(table):
+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)
@@ -35,8 +32,7 @@ def full_querystring(table):
RequestConfig(request).configure(table)
-@requestconfig.test
-def partial_querystring(table):
+def test_partial_querystring(table):
request = build_request("/?page=1&sort=abc")
table = (table
.expects("paginate").with_args(page=1, per_page=5)
@@ -44,8 +40,7 @@ def partial_querystring(table):
RequestConfig(request, paginate={"per_page": 5}).configure(table)
-@requestconfig.test
-def silent_page_not_an_integer_error(table):
+def test_silent_page_not_an_integer_error(table):
request = build_request("/")
paginator = (Fake("Paginator")
.expects("page").with_args(1))
@@ -58,8 +53,7 @@ def silent_page_not_an_integer_error(table):
"silent": True}).configure(table)
-@requestconfig.test
-def silent_empty_page_error(table):
+def test_silent_empty_page_error(table):
request = build_request("/")
paginator = (Fake("Paginator")
.has_attr(num_pages=987)
@@ -71,6 +65,3 @@ def silent_empty_page_error(table):
RequestConfig(request, paginate={"page": 123,
"silent": True}).configure(table)
-
-
-config = Tests([requestconfig])
diff --git a/tests/core.py b/tests/test_core.py
similarity index 89%
rename from tests/core.py
rename to tests/test_core.py
index 66351e2..9a33bbb 100644
--- a/tests/core.py
+++ b/tests/test_core.py
@@ -1,16 +1,14 @@
# coding: utf-8
"""Test the core table functionality."""
from __future__ import absolute_import, unicode_literals
-from attest import assert_hook, raises, Tests, warns
import copy
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
import django_tables2 as tables
from django_tables2.tables import DeclarativeColumnsMetaclass
import six
import itertools
-
-
-core = Tests()
+import pytest
+from .utils import warns
class UnorderedTable(tables.Table):
@@ -31,8 +29,7 @@ MEMORY_DATA = [
]
-@core.test
-def declarations():
+def test_declarations():
"""Test defining tables by declaration."""
class GeoAreaTable(tables.Table):
name = tables.Column()
@@ -59,8 +56,7 @@ def declarations():
assert 'added' in CityTable.base_columns
-@core.test
-def metaclass_inheritance():
+def test_metaclass_inheritance():
class Tweaker(type):
"""Adds an attribute "tweaked" to all classes"""
def __new__(cls, name, bases, attrs):
@@ -96,8 +92,7 @@ def metaclass_inheritance():
assert table.tweaked
-@core.test
-def attrs():
+def test_attrs():
class TestTable(tables.Table):
class Meta:
attrs = {}
@@ -119,8 +114,7 @@ def attrs():
assert {"c": "d"} == TestTable4([], attrs={"c": "d"}).attrs
-@core.test
-def attrs_support_computed_values():
+def test_attrs_support_computed_values():
counter = itertools.count()
class TestTable(tables.Table):
@@ -131,15 +125,13 @@ def attrs_support_computed_values():
assert {"id": "test_table_1"} == TestTable([]).attrs
-@core.test
-def data_knows_its_name():
+def test_data_knows_its_name():
table = tables.Table([{}])
assert table.data.verbose_name == "item"
assert table.data.verbose_name_plural == "items"
-@core.test
-def datasource_untouched():
+def test_datasource_untouched():
"""Ensure that data that is provided to the table (the datasource) is not
modified by table operations.
"""
@@ -156,21 +148,20 @@ def datasource_untouched():
assert MEMORY_DATA == original_data
-@core.test
-def should_support_tuple_data_source():
+def test_should_support_tuple_data_source():
class SimpleTable(tables.Table):
name = tables.Column()
table = SimpleTable((
{'name': 'brad'},
- {'name': 'stevie'},
+ {'name': 'davina'},
))
assert len(table.rows) == 2
-@core.test_if(not six.PY3) # Haystack isn't compatible with Python 3
-def should_support_haystack_data_source():
+@pytest.mark.skipif(six.PY3, reason="Haystack requires Python 2.x")
+def test_should_support_haystack_data_source():
from haystack.query import SearchQuerySet
class PersonTable(tables.Table):
@@ -178,33 +169,32 @@ def should_support_haystack_data_source():
table = PersonTable(SearchQuerySet().all())
table.as_html()
-
-
-@core.test
-def data_validation():
- with raises(ValueError):
- table = OrderedTable(None)
-
- class Bad:
- def __len__(self):
- pass
-
- with raises(ValueError):
- table = OrderedTable(Bad())
- class Ok:
- def __len__(self):
- return 1
- def __getitem__(self, pos):
- if pos != 0:
- raise IndexError()
- return {'a': 1}
-
- table = OrderedTable(Ok())
- assert len(table.rows) == 1
-@core.test
-def ordering():
+def test_data_validation():
+ with pytest.raises(ValueError):
+ table = OrderedTable(None)
+
+ class Bad:
+ def __len__(self):
+ pass
+
+ with pytest.raises(ValueError):
+ table = OrderedTable(Bad())
+
+ class Ok:
+ def __len__(self):
+ return 1
+ def __getitem__(self, pos):
+ if pos != 0:
+ raise IndexError()
+ return {'a': 1}
+
+ table = OrderedTable(Ok())
+ assert len(table.rows) == 1
+
+
+def test_ordering():
# fallback to Table.Meta
assert ('alpha', ) == OrderedTable([], order_by=None).order_by == OrderedTable([]).order_by
@@ -287,8 +277,7 @@ def ordering():
assert len(captured) == 4
-@core.test
-def ordering_different_types():
+def test_ordering_different_types():
from datetime import datetime
data = [
@@ -311,25 +300,24 @@ def ordering_different_types():
assert [] == table.rows[0]['beta']
-@core.test
-def multi_column_ordering():
+def test_multi_column_ordering():
brad = {"first_name": "Bradley", "last_name": "Ayers"}
brad2 = {"first_name": "Bradley", "last_name": "Fake"}
chris = {"first_name": "Chris", "last_name": "Doble"}
- stevie = {"first_name": "Stevie", "last_name": "Armstrong"}
+ davina = {"first_name": "Davina", "last_name": "Adisusila"}
ross = {"first_name": "Ross", "last_name": "Ayers"}
- people = [brad, brad2, chris, stevie, ross]
+ people = [brad, brad2, chris, davina, ross]
class PersonTable(tables.Table):
first_name = tables.Column()
last_name = tables.Column()
table = PersonTable(people, order_by=("first_name", "last_name"))
- assert [brad, brad2, chris, ross, stevie] == [r.record for r in table.rows]
+ 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, ross, stevie] == [r.record for r in table.rows]
+ assert [brad2, brad, chris, davina, ross] == [r.record for r in table.rows]
# let's try column order_by using multiple keys
class PersonTable(tables.Table):
@@ -341,14 +329,13 @@ def multi_column_ordering():
assert brad['name'] == "Bradley Ayers"
table = PersonTable(people, order_by="name")
- assert [brad, brad2, chris, ross, stevie] == [r.record for r in table.rows]
+ assert [brad, brad2, chris, davina, ross] == [r.record for r in table.rows]
table = PersonTable(people, order_by="-name")
- assert [stevie, ross, chris, brad2, brad] == [r.record for r in table.rows]
+ assert [ross, davina, chris, brad2, brad] == [r.record for r in table.rows]
-@core.test
-def column_count():
+def test_column_count():
class SimpleTable(tables.Table):
visible = tables.Column(visible=True)
hidden = tables.Column(visible=False)
@@ -357,8 +344,7 @@ def column_count():
assert len(SimpleTable([]).columns) == 1
-@core.test
-def column_accessor():
+def test_column_accessor():
class SimpleTable(UnorderedTable):
col1 = tables.Column(accessor='alpha.upper.isupper')
col2 = tables.Column(accessor='alpha.upper')
@@ -368,8 +354,7 @@ def column_accessor():
assert row['col2'] == 'B'
-@core.test
-def exclude_columns():
+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
@@ -402,8 +387,7 @@ def exclude_columns():
assert [c.name for c in table.columns] == ["i", "alpha", "added"]
-@core.test
-def table_exclude_property_should_override_constructor_argument():
+def test_table_exclude_property_should_override_constructor_argument():
class SimpleTable(tables.Table):
a = tables.Column()
b = tables.Column()
@@ -414,8 +398,7 @@ def table_exclude_property_should_override_constructor_argument():
assert [c.name for c in table.columns] == ['b']
-@core.test
-def pagination():
+def test_pagination():
class BookTable(tables.Table):
name = tables.Column()
@@ -445,15 +428,14 @@ def pagination():
assert books.page.has_next() is True
# accessing a non-existant page raises 404
- with raises(EmptyPage):
+ with pytest.raises(EmptyPage):
books.paginate(Paginator, page=9999, per_page=10)
- with raises(PageNotAnInteger):
+ with pytest.raises(PageNotAnInteger):
books.paginate(Paginator, page='abc', per_page=10)
-@core.test
-def pagination_shouldnt_prevent_multiple_rendering():
+def test_pagination_shouldnt_prevent_multiple_rendering():
class SimpleTable(tables.Table):
name = tables.Column()
@@ -463,8 +445,7 @@ def pagination_shouldnt_prevent_multiple_rendering():
assert table.as_html() == table.as_html()
-@core.test
-def empty_text():
+def test_empty_text():
class TestTable(tables.Table):
a = tables.Column()
@@ -484,8 +465,7 @@ def empty_text():
assert table.empty_text == 'still nothing'
-@core.test
-def prefix():
+def test_prefix():
"""Test that table prefixes affect the names of querystring parameters"""
class TableA(tables.Table):
name = tables.Column()
@@ -506,8 +486,7 @@ def prefix():
assert "x" == table.prefix
-@core.test
-def field_names():
+def test_field_names():
class TableA(tables.Table):
class Meta:
order_by_field = "abc"
@@ -520,8 +499,7 @@ def field_names():
assert "ghi" == table.per_page_field
-@core.test
-def field_names_with_prefix():
+def test_field_names_with_prefix():
class TableA(tables.Table):
class Meta:
order_by_field = "sort"
@@ -552,8 +530,7 @@ def field_names_with_prefix():
assert "1-per_page" == table.prefixed_per_page_field
-@core.test
-def should_support_a_template_to_be_specified():
+def test_should_support_a_template_to_be_specified():
class MetaDeclarationSpecifiedTemplateTable(tables.Table):
name = tables.Column()
@@ -583,8 +560,7 @@ def should_support_a_template_to_be_specified():
assert table.template == "django_tables2/table.html"
-@core.test
-def should_support_rendering_multiple_times():
+def test_should_support_rendering_multiple_times():
class MultiRenderTable(tables.Table):
name = tables.Column()
@@ -593,8 +569,7 @@ def should_support_rendering_multiple_times():
assert table.as_html() == table.as_html()
-@core.test
-def column_defaults_are_honored():
+def test_column_defaults_are_honored():
class Table(tables.Table):
name = tables.Column(default="abcd")
@@ -605,8 +580,7 @@ def column_defaults_are_honored():
assert table.rows[0]['name'] == "abcd"
-@core.test
-def table_meta_defaults_are_honored():
+def test_table_meta_defaults_are_honored():
class Table(tables.Table):
name = tables.Column()
@@ -617,8 +591,7 @@ def table_meta_defaults_are_honored():
assert table.rows[0]['name'] == "abcd"
-@core.test
-def table_defaults_are_honored():
+def test_table_defaults_are_honored():
class Table(tables.Table):
name = tables.Column()
@@ -630,17 +603,16 @@ def table_defaults_are_honored():
assert table.rows[0]['name'] == "efgh"
-@core.test
-def list_table_data_supports_ordering():
+def test_list_table_data_supports_ordering():
class Table(tables.Table):
name = tables.Column()
data = [
{"name": "Bradley"},
- {"name": "Stevie"},
+ {"name": "Davina"},
]
table = Table(data)
assert table.rows[0]["name"] == "Bradley"
table.order_by = "-name"
- assert table.rows[0]["name"] == "Stevie"
+ assert table.rows[0]["name"] == "Davina"
diff --git a/tests/models.py b/tests/test_models.py
similarity index 90%
rename from tests/models.py
rename to tests/test_models.py
index fcb3b7c..049e71a 100644
--- a/tests/models.py
+++ b/tests/test_models.py
@@ -1,14 +1,12 @@
# coding: utf-8
-from .app.models import Person, Occupation
-from attest import assert_hook, Tests # pylint: disable=W0611
-import itertools
-from django_attest import TestContext
-import django_tables2 as tables
import six
+import pytest
+
+from .app.models import Person, Occupation
+import django_tables2 as tables
-models = Tests()
-models.context(TestContext())
+pytestmark = pytest.mark.django_db
class PersonTable(tables.Table):
@@ -17,8 +15,7 @@ class PersonTable(tables.Table):
occupation = tables.Column()
-@models.test
-def boundrows_iteration():
+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)
@@ -30,8 +27,7 @@ def boundrows_iteration():
assert expected == actual
-@models.test
-def model_table():
+def test_model_table():
"""
The ``model`` option on a table causes the table to dynamically add columns
based on the fields.
@@ -63,8 +59,7 @@ def model_table():
assert ["id", "char", "fk"] == list(ComplexTable.base_columns.keys())
-@models.test
-def mixins():
+def test_mixins():
class TableMixin(tables.Table):
extra = tables.Column()
@@ -76,8 +71,7 @@ def mixins():
assert ["extra", "id", "name", "region", "extra2"] == list(OccupationTable.base_columns.keys())
-@models.test
-def column_verbose_name():
+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.
@@ -141,15 +135,13 @@ def column_verbose_name():
assert "translation test lazy" == table.columns["trans_test_lazy"].verbose_name
-@models.test
-def data_verbose_name():
+def test_data_verbose_name():
table = tables.Table(Person.objects.all())
assert table.data.verbose_name == "person"
assert table.data.verbose_name_plural == "people"
-@models.test
-def field_choices_used_to_translated_value():
+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.
@@ -181,8 +173,7 @@ def field_choices_used_to_translated_value():
assert 'Russian' == table.rows[1]['language']
-@models.test
-def column_mapped_to_nonexistant_field():
+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.
@@ -194,8 +185,7 @@ def column_mapped_to_nonexistant_field():
table.as_html() # the bug would cause this to raise FieldDoesNotExist
-@models.test
-def should_support_rendering_multiple_times():
+def test_should_support_rendering_multiple_times():
class MultiRenderTable(tables.Table):
name = tables.Column()
@@ -204,8 +194,7 @@ def should_support_rendering_multiple_times():
assert table.as_html() == table.as_html()
-@models.test
-def ordering():
+def test_ordering():
class SimpleTable(tables.Table):
name = tables.Column(order_by=("first_name", "last_name"))
@@ -213,8 +202,7 @@ def ordering():
assert table.as_html()
-@models.test
-def fields_should_implicitly_set_sequence():
+def test_fields_should_implicitly_set_sequence():
class PersonTable(tables.Table):
extra = tables.Column()
@@ -225,8 +213,7 @@ def fields_should_implicitly_set_sequence():
assert table.columns.names() == ['last_name', 'first_name', 'extra']
-@models.test
-def model_properties_should_be_useable_for_columns():
+def test_model_properties_should_be_useable_for_columns():
class PersonTable(tables.Table):
class Meta:
model = Person
@@ -237,8 +224,7 @@ def model_properties_should_be_useable_for_columns():
assert list(table.rows[0]) == ['Bradley Ayers', 'Bradley']
-@models.test
-def column_with_delete_accessor_shouldnt_delete_records():
+def test_column_with_delete_accessor_shouldnt_delete_records():
class PersonTable(tables.Table):
delete = tables.Column()
@@ -248,8 +234,7 @@ def column_with_delete_accessor_shouldnt_delete_records():
assert Person.objects.get(first_name='Bradley')
-@models.test
-def order_by_derived_from_queryset():
+def test_order_by_derived_from_queryset():
queryset = Person.objects.order_by("first_name", "last_name", "-occupation__name")
class PersonTable(tables.Table):
@@ -265,8 +250,7 @@ def order_by_derived_from_queryset():
assert PersonTable(queryset.all()).order_by == ("occupation", )
-@models.test
-def queryset_table_data_supports_ordering():
+def test_queryset_table_data_supports_ordering():
class Table(tables.Table):
class Meta:
model = Person
@@ -281,8 +265,7 @@ def queryset_table_data_supports_ordering():
assert table.rows[0]["first_name"] == "Stevie"
-@models.test
-def doesnotexist_from_accessor_should_use_default():
+def test_doesnotexist_from_accessor_should_use_default():
class Table(tables.Table):
class Meta:
model = Person
@@ -296,8 +279,7 @@ def doesnotexist_from_accessor_should_use_default():
assert table.rows[0]["region"] == "abc"
-@models.test
-def unicode_field_names():
+def test_unicode_field_names():
class Table(tables.Table):
class Meta:
model = Person
diff --git a/tests/rows.py b/tests/test_rows.py
similarity index 75%
rename from tests/rows.py
rename to tests/test_rows.py
index 0e29a25..1ab0b65 100644
--- a/tests/rows.py
+++ b/tests/test_rows.py
@@ -1,20 +1,17 @@
# coding: utf-8
-from attest import assert_hook, raises, Tests
+import pytest
+
import django_tables2 as tables
-rows = Tests()
-
-
-@rows.test
-def bound_rows():
+def test_bound_rows():
class SimpleTable(tables.Table):
name = tables.Column()
data = [
{'name': 'Bradley'},
{'name': 'Chris'},
- {'name': 'Peter'},
+ {'name': 'Davina'},
]
table = SimpleTable(data)
@@ -26,8 +23,7 @@ def bound_rows():
assert records == data
-@rows.test
-def bound_row():
+def test_bound_row():
class SimpleTable(tables.Table):
name = tables.Column()
occupation = tables.Column()
@@ -43,13 +39,13 @@ def bound_row():
assert row[1] == record['occupation']
assert row[2] == record['age']
- with raises(IndexError):
+ with pytest.raises(IndexError):
row[3]
# column name indexing into a row
- assert row['name'] == record['name']
+ assert row['name'] == record['name']
assert row['occupation'] == record['occupation']
- assert row['age'] == record['age']
+ assert row['age'] == record['age']
- with raises(KeyError):
+ with pytest.raises(KeyError):
row['gamma']
diff --git a/tests/templates.py b/tests/test_templates.py
similarity index 72%
rename from tests/templates.py
rename to tests/test_templates.py
index 6af0b3b..a65a1b0 100644
--- a/tests/templates.py
+++ b/tests/test_templates.py
@@ -1,17 +1,18 @@
# coding: utf-8
from __future__ import unicode_literals
-from .app.models import Person, Region
-from attest import assert_hook, raises, Tests # pylint: disable=W0611
-from contextlib import contextmanager
-from django_attest import queries, settings, TestContext, translation
-import django_tables2 as tables
-from django_tables2.config import RequestConfig
-from django_tables2.utils import build_request
+
+from django.test import TransactionTestCase
import django
from django.core.exceptions import ImproperlyConfigured
from django.template import Template, RequestContext, Context
from django.utils.translation import ugettext_lazy
from django.utils.safestring import mark_safe
+
+from .app.models import Person, Region
+import django_tables2 as tables
+from django_tables2.config import RequestConfig
+from django_tables2.utils import build_request
+
try:
from urlparse import parse_qs
except ImportError:
@@ -19,21 +20,8 @@ except ImportError:
import lxml.etree
import lxml.html
import six
-
-
-def parse(html):
- return lxml.etree.fromstring(html)
-
-
-def attrs(xml):
- """
- Helper function that returns a dict of XML attributes, given an element.
- """
- return lxml.html.fromstring(xml).attrib
-
-
-database = contextmanager(TestContext())
-templates = Tests()
+from .utils import parse, translation
+import pytest
class CountryTable(tables.Table):
@@ -58,8 +46,7 @@ MEMORY_DATA = [
]
-@templates.test
-def as_html():
+def test_as_html():
table = CountryTable(MEMORY_DATA)
root = parse(table.as_html())
assert len(root.findall('.//thead/tr')) == 1
@@ -89,8 +76,7 @@ def as_html():
table.as_html()
-@templates.test
-def custom_rendering():
+def test_custom_rendering():
"""For good measure, render some actual templates."""
countries = CountryTable(MEMORY_DATA)
context = Context({'countries': countries})
@@ -110,8 +96,7 @@ def custom_rendering():
assert result == template.render(context)
-@templates.test
-def render_table_templatetag():
+def test_render_table_templatetag(settings):
# ensure it works with a multi-order-by
request = build_request('/')
table = CountryTable(MEMORY_DATA, order_by=('name', 'population'))
@@ -152,18 +137,17 @@ def render_table_templatetag():
# variable that doesn't exist (issue #8)
template = Template('{% load django_tables2 %}'
'{% render_table this_doesnt_exist %}')
- with raises(ValueError):
- with settings(DEBUG=True):
- template.render(Context())
+ with pytest.raises(ValueError):
+ settings.DEBUG = True
+ template.render(Context())
# Should still be noisy with debug off
- with raises(ValueError):
- with settings(DEBUG=False):
- template.render(Context())
+ with pytest.raises(ValueError):
+ settings.DEBUG = False
+ template.render(Context())
-@templates.test
-def render_table_should_support_template_argument():
+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" %}')
@@ -172,26 +156,24 @@ def render_table_should_support_template_argument():
assert template.render(context) == 'dummy template contents\n'
-@templates.test
-def render_table_supports_queryset():
- with database():
- 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('/')}))
+@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
+ 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
-@templates.test
-def querystring_templatetag():
+def test_querystring_templatetag():
template = Template('{% load django_tables2 %}'
'{% querystring "name"="Brad" foo.bar=value %}')
@@ -212,15 +194,13 @@ def querystring_templatetag():
assert qs["c"] == ["5"]
-@templates.test
-def querystring_templatetag_requires_request():
- with raises(ImproperlyConfigured):
+def test_querystring_templatetag_requires_request():
+ with pytest.raises(ImproperlyConfigured):
(Template('{% load django_tables2 %}{% querystring "name"="Brad" %}')
.render(Context()))
-@templates.test
-def querystring_templatetag_supports_without():
+def test_querystring_templatetag_supports_without():
context = Context({
"request": build_request('/?a=b&name=dog&c=5'),
"a_var": "a",
@@ -240,8 +220,7 @@ def querystring_templatetag_supports_without():
assert set(qs.keys()) == set(["c"])
-@templates.test
-def title_should_only_apply_to_words_without_uppercase_letters():
+def test_title_should_only_apply_to_words_without_uppercase_letters():
expectations = {
"a brown fox": "A Brown Fox",
"a brown foX": "A Brown foX",
@@ -255,15 +234,13 @@ def title_should_only_apply_to_words_without_uppercase_letters():
assert template.render(Context({"x": raw})) == expected
-@templates.test
-def nospaceless_works():
+def test_nospaceless_works():
template = Template("{% load django_tables2 %}"
"{% spaceless %}a b {% nospaceless %}c d {% endnospaceless %}lic{% endspaceless %}")
assert template.render(Context()) == "ab c d lic"
-@templates.test
-def whitespace_is_preserved():
+def test_whitespace_is_preserved():
class TestTable(tables.Table):
name = tables.Column(verbose_name=mark_safe("foo bar"))
@@ -275,29 +252,35 @@ def whitespace_is_preserved():
assert "foo bar" in lxml.etree.tostring(tree.findall('.//tbody/tr/td')[0], encoding='unicode')
-@templates.test
-def as_html_db_queries():
- with database():
+@pytest.mark.django_db
+def test_as_html_db_queries(transactional_db):
+ class PersonTable(tables.Table):
+ class Meta:
+ model = Person
+
+ # with queries(count=1):
+ # PersonTable(Person.objects.all()).as_html()
+
+
+class TestQueries(TransactionTestCase):
+ def test_as_html_db_queries(self):
class PersonTable(tables.Table):
class Meta:
model = Person
- with queries(count=1):
+ with self.assertNumQueries(1):
PersonTable(Person.objects.all()).as_html()
-
-@templates.test
-def render_table_db_queries():
- with database():
+ def test_render_table_db_queries(self):
Person.objects.create(first_name="brad", last_name="ayers")
- Person.objects.create(first_name="stevie", last_name="armstrong")
+ Person.objects.create(first_name="davina", last_name="adisusila")
class PersonTable(tables.Table):
class Meta:
model = Person
per_page = 1
- with queries(count=2):
+ with self.assertNumQueries(2):
# one query for pagination: .count()
# one query for page records: .all()[start:end]
request = build_request('/')
@@ -308,8 +291,8 @@ def render_table_db_queries():
.render(Context({'table': table, 'request': request})))
-@templates.test_if(django.VERSION >= (1, 3))
-def localization_check():
+@pytest.mark.skipif(django.VERSION < (1, 3), reason="requires Django >= 1.3")
+def test_localization_check(settings):
def get_cond_localized_table(localizeit=None):
'''
helper function for defining Table class conditionally
@@ -333,25 +316,27 @@ def localization_check():
html = get_cond_localized_table(False)(simple_test_data).as_html()
assert '{0} | '.format(expected_reults[False]) in html
- with settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True):
- with translation("pl"):
- # with default polish locales and enabled thousand separator
- # 1234.5 is formatted as "1 234,5" with nbsp
- html = get_cond_localized_table(True)(simple_test_data).as_html()
- assert '{0} | '.format(expected_reults[True]) in html
+ settings.USE_L10N = True
+ settings.USE_THOUSAND_SEPARATOR = True
- # with localize = False there should be no formatting
- html = get_cond_localized_table(False)(simple_test_data).as_html()
- assert '{0} | '.format(expected_reults[False]) in html
+ with translation("pl"):
+ # with default polish locales and enabled thousand separator
+ # 1234.5 is formatted as "1 234,5" with nbsp
+ html = get_cond_localized_table(True)(simple_test_data).as_html()
+ assert '{0} | '.format(expected_reults[True]) 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()
- assert '{0} | '.format(expected_reults[True]) in html
+ # with localize = False there should be no formatting
+ html = get_cond_localized_table(False)(simple_test_data).as_html()
+ assert '{0} | '.format(expected_reults[False]) in html
+
+ # with localize = None and USE_L10N = True
+ # there should be the same formatting as with localize = True
+ html = get_cond_localized_table(None)(simple_test_data).as_html()
+ assert '{0} | '.format(expected_reults[True]) in html
-@templates.test_if(django.VERSION >= (1, 3))
-def localization_check_in_meta():
+@pytest.mark.skipif(django.VERSION < (1, 3), reason="requires Django >= 1.3")
+def test_localization_check_in_meta(settings):
class TableNoLocalize(tables.Table):
name = tables.Column(verbose_name="my column")
@@ -391,21 +376,23 @@ def localization_check_in_meta():
html = TableNoLocalize(simple_test_data).as_html()
assert '{0} | '.format(expected_reults[None]) in html
- with settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True):
- with translation("pl"):
- # the same as in localization_check.
- # with localization and polish locale we get formatted output
- html = TableNoLocalize(simple_test_data).as_html()
- assert '{0} | '.format(expected_reults[True]) in html
+ settings.USE_L10N = True
+ settings.USE_THOUSAND_SEPARATOR = True
- # localize
- html = TableLocalize(simple_test_data).as_html()
- assert '{0} | '.format(expected_reults[True]) in html
+ with translation("pl"):
+ # the same as in localization_check.
+ # with localization and polish locale we get formatted output
+ html = TableNoLocalize(simple_test_data).as_html()
+ assert '{0} | '.format(expected_reults[True]) in html
- # unlocalize
- html = TableUnlocalize(simple_test_data).as_html()
- assert '{0} | '.format(expected_reults[False]) in html
+ # localize
+ html = TableLocalize(simple_test_data).as_html()
+ assert '{0} | '.format(expected_reults[True]) in html
- # test unlocalize higher precedence
- html = TableLocalizePrecedence(simple_test_data).as_html()
- assert '{0} | '.format(expected_reults[False]) in html
+ # unlocalize
+ html = TableUnlocalize(simple_test_data).as_html()
+ assert '{0} | '.format(expected_reults[False]) in html
+
+ # test unlocalize higher precedence
+ html = TableLocalizePrecedence(simple_test_data).as_html()
+ assert '{0} | '.format(expected_reults[False]) in html
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..bf7ef55
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,144 @@
+# coding: utf-8
+# from attest import assert_hook, raises, Tests
+import six
+import pytest
+
+from django_tables2.utils import (Accessor, AttributeDict, computed_values,
+ OrderByTuple, OrderBy, segment)
+
+
+def test_orderbytuple():
+ obt = OrderByTuple(('a', 'b', 'c'))
+ assert obt == (OrderBy('a'), OrderBy('b'), OrderBy('c'))
+
+ # indexing
+ assert obt[0] == OrderBy('a')
+ assert obt['b'] == OrderBy('b')
+ with pytest.raises(KeyError):
+ obt['d']
+ with pytest.raises(TypeError):
+ obt[('tuple', )]
+
+ # .get
+ sentinel = object()
+ assert obt.get('b', sentinel) is obt['b'] # keying
+ assert obt.get('-', sentinel) is sentinel
+ assert obt.get(0, sentinel) is obt['a'] # indexing
+ assert obt.get(3, sentinel) is sentinel
+
+ # .opposite
+ assert OrderByTuple(('a', '-b', 'c')).opposite == ('-a', 'b', '-c')
+
+ # in
+ assert 'a' in obt and '-a' in obt
+
+
+def test_orderbytuple_sort_key_multiple():
+ obt = OrderByTuple(('a', '-b'))
+ items = [
+ {"a": 1, "b": 2},
+ {"a": 1, "b": 3},
+ ]
+ assert sorted(items, key=obt.key) == [
+ {"a": 1, "b": 3},
+ {"a": 1, "b": 2},
+ ]
+
+
+def test_orderbytuple_sort_key_empty_comes_first():
+ obt = OrderByTuple(('a'))
+ items = [
+ {"a": 1},
+ {"a": ""},
+ {"a": 2},
+ ]
+ if six.PY3:
+ assert sorted(items, key=obt.key) == [
+ {"a": ""},
+ {"a": 1},
+ {"a": 2},
+ ]
+ else:
+ assert sorted(items, key=obt.key) == [
+ {"a": 1},
+ {"a": 2},
+ {"a": ""},
+ ]
+
+
+def test_orderby():
+ a = OrderBy('a')
+ assert 'a' == a
+ assert 'a' == a.bare
+ assert '-a' == a.opposite
+ assert True == a.is_ascending
+ assert False == a.is_descending
+
+ b = OrderBy('-b')
+ assert '-b' == b
+ assert 'b' == b.bare
+ assert 'b' == b.opposite
+ assert True == b.is_descending
+ assert False == b.is_ascending
+
+
+def test_accessor():
+ x = Accessor('0')
+ assert 'B' == x.resolve('Brad')
+
+ x = Accessor('1')
+ assert 'r' == x.resolve('Brad')
+
+ x = Accessor('2.upper')
+ assert 'A' == x.resolve('Brad')
+
+ x = Accessor('2.upper.__len__')
+ assert 1 == x.resolve('Brad')
+
+ x = Accessor('')
+ assert 'Brad' == x.resolve('Brad')
+
+
+def test_accessor_wont_honors_alters_data():
+ class Foo(object):
+ deleted = False
+
+ def delete(self):
+ self.deleted = True
+ delete.alters_data = True
+
+ foo = Foo()
+ with pytest.raises(ValueError):
+ Accessor('delete').resolve(foo)
+ assert foo.deleted is False
+
+
+def test_accessor_can_be_quiet():
+ foo = {}
+ assert Accessor("bar").resolve(foo, quiet=True) is None
+
+
+def test_attribute_dict_handles_escaping():
+ x = AttributeDict({"x": '"\'x&'})
+ assert x.as_html() == 'x=""'x&"'
+
+
+def test_compute_values_supports_shallow_structures():
+ x = computed_values({"foo": lambda: "bar"})
+ assert x == {"foo": "bar"}
+
+
+def test_compute_values_supports_shallow_structures():
+ x = computed_values({"foo": lambda: {"bar": lambda: "baz"}})
+ assert x == {"foo": {"bar": "baz"}}
+
+
+def test_segment_should_return_all_candidates():
+ assert set(segment(("a", "-b", "c"), {
+ "x": "a",
+ "y": ("b", "-c"),
+ "-z": ("b", "-c"),
+ })) == {
+ ("x", "-y"),
+ ("x", "z"),
+ }
diff --git a/tests/views.py b/tests/test_views.py
similarity index 83%
rename from tests/views.py
rename to tests/test_views.py
index 00340f1..d141547 100644
--- a/tests/views.py
+++ b/tests/test_views.py
@@ -1,13 +1,10 @@
# coding: utf-8
from .app.models import Region
-from attest import assert_hook, Tests
-from django_attest import TestContext
import django_tables2 as tables
from django_tables2.utils import build_request
+import pytest
-views = Tests()
-views.context(TestContext())
USING_CBV = hasattr(tables, "SingleTableView")
@@ -24,8 +21,9 @@ class SimpleTable(tables.Table):
model = Region
-@views.test_if(USING_CBV)
-def view_should_support_pagination_options():
+@pytest.mark.skipif(not USING_CBV, reason="requires class based views")
+@pytest.mark.django_db
+def test_view_should_support_pagination_options():
for name in ("Queensland", "New South Wales", "Victoria", "Tasmania"):
Region.objects.create(name=name)
@@ -39,8 +37,8 @@ def view_should_support_pagination_options():
assert view.get_table().paginator.num_pages == 4
-@views.test_if(USING_CBV)
-def should_support_explicit_table_data():
+@pytest.mark.skipif(not USING_CBV, reason="requires class based views")
+def test_should_support_explicit_table_data():
class SimpleView(DispatchHookMixin, tables.SingleTableView):
table_class = SimpleTable
table_data = [
diff --git a/tests/utils.py b/tests/utils.py
index 9e182a3..990d868 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,156 +1,50 @@
-# coding: utf-8
-from attest import assert_hook, raises, Tests
-from django_tables2.utils import (Accessor, AttributeDict, computed_values,
- OrderByTuple, OrderBy, segment)
-import itertools
-import six
+from contextlib import contextmanager
+import lxml.etree
+import lxml.html
+import warnings
-utils = Tests()
+def parse(html):
+ return lxml.etree.fromstring(html)
-@utils.test
-def 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 raises(KeyError):
- obt['d']
- with 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 attrs(xml):
+ """
+ Helper function that returns a dict of XML attributes, given an element.
+ """
+ return lxml.html.fromstring(xml).attrib
-@utils.test
-def 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},
- ]
+@contextmanager
+def warns(warning_class):
+ with warnings.catch_warnings(record=True) as ws:
+ warnings.simplefilter("always")
+ yield ws
+ assert any((issubclass(w.category, DeprecationWarning) for w in ws))
-@utils.test
-def 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},
- ]
+@contextmanager
+def translation(language_code, deactivate=False):
+ """
+ Port of django.utils.translation.override from Django 1.4
+
+ @param language_code: a language code or ``None``. If ``None``, translation
+ is disabled and raw translation strings are used
+ @param deactivate: If ``True``, when leaving the manager revert to the
+ default behaviour (i.e. ``settings.LANGUAGE_CODE``)
+ rather than the translation that was active prior to
+ entering.
+ """
+ from django.utils import translation
+ original = translation.get_language()
+ if language_code is not None:
+ translation.activate(language_code)
else:
- assert sorted(items, key=obt.key) == [
- {"a": 1},
- {"a": 2},
- {"a": ""},
- ]
-
-@utils.test
-def orderby():
- a = OrderBy('a')
- assert 'a' == a
- assert 'a' == a.bare
- assert '-a' == a.opposite
- assert True == a.is_ascending
- assert False == a.is_descending
-
- b = OrderBy('-b')
- assert '-b' == b
- assert 'b' == b.bare
- assert 'b' == b.opposite
- assert True == b.is_descending
- assert False == b.is_ascending
-
-
-@utils.test
-def 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')
-
-
-@utils.test
-def accessor_wont_honors_alters_data():
- class Foo(object):
- deleted = False
-
- def delete(self):
- self.deleted = True
- delete.alters_data = True
-
- foo = Foo()
- with raises(ValueError):
- Accessor('delete').resolve(foo)
- assert foo.deleted is False
-
-
-@utils.test
-def accessor_can_be_quiet():
- foo = {}
- assert Accessor("bar").resolve(foo, quiet=True) is None
-
-
-@utils.test
-def attribute_dict_handles_escaping():
- x = AttributeDict({"x": '"\'x&'})
- assert x.as_html() == 'x=""'x&"'
-
-
-@utils.test
-def compute_values_supports_shallow_structures():
- x = computed_values({"foo": lambda: "bar"})
- assert x == {"foo": "bar"}
-
-
-@utils.test
-def compute_values_supports_shallow_structures():
- x = computed_values({"foo": lambda: {"bar": lambda: "baz"}})
- assert x == {"foo": {"bar": "baz"}}
-
-
-@utils.test
-def segment_should_return_all_candidates():
- assert set(segment(("a", "-b", "c"), {
- "x": ("a"),
- "y": ("b", "-c"),
- "-z": ("b", "-c"),
- })) == set((
- ("x", "-y"),
- ("x", "z"),
- ))
+ translation.deactivate_all()
+ try:
+ yield
+ finally:
+ if deactivate:
+ translation.deactivate()
+ else:
+ translation.activate(original)
diff --git a/tox.ini b/tox.ini
index 9d51f0f..b7016c6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,176 +1,95 @@
+[pytest]
+DJANGO_SETTINGS_MODULE=tests.app.settings
+
[testenv]
-commands =
- python -W error -W ignore::PendingDeprecationWarning {envbindir}/coverage run setup.py test
-
-[libs]
-common =
- http://github.com/bradleyayers/python-progressbar/tarball/master
- http://github.com/dag/attest/tarball/master
- coverage
- django-attest
- fudge
- lxml
- pylint
- pytz>0
- six
- unittest-xml-reporting
-for-dj =
- django-haystack
-for-dj18 =
- django-haystack
-for-dj17 =
- django-haystack
-for-dj16 =
- django-haystack
-for-dj15 =
- django-haystack
-for-dj14 =
- django-haystack
-for-dj13 =
- django-haystack>=2.0.0,<2.1.0
-for-dj12 =
- django-haystack>=2.0.0,<2.1.0
-
-[django]
-latest = http://github.com/django/django/tarball/master
-1.8.x = Django>=1.8,<1.9
-1.7.x = Django>=1.7,<1.8
-1.6.x = Django>=1.6,<1.7
-1.5.x = Django>=1.5,<1.6
-1.4.x = Django>=1.4,<1.5
-1.3.x = Django>=1.3,<1.4
-1.2.x = Django>=1.2,<1.3
+setenv = PYTHONPATH={toxinidir}
+commands = py.test
[testenv:docs]
changedir = docs
commands = make html
deps =
- {[libs]common}
- {[django]latest}
+ -r{toxinidir}/requirements/django-dev.pip
Sphinx
; -- python 3.4 ---------------------------------------------------------------
[testenv:py34-dj]
basepython = python3.4
-commands =
- python {envbindir}/coverage run setup.py test []
-deps =
- {[libs]common}
- {[libs]for-dj}
- {[django]latest}
+commands = python -W error {envbindir}/coverage run setup.py test []
+deps = -r{toxinidir}/requirements/django-dev.pip
[testenv:py34-dj18]
basepython = python3.4
-deps =
- {[libs]common}
- {[libs]for-dj18}
- {[django]1.8.x}
+deps = -r{toxinidir}/requirements/django-1.8.x.pip
[testenv:py34-dj17]
basepython = python3.4
-deps =
- {[libs]common}
- {[libs]for-dj17}
- {[django]1.7.x}
+deps = -r{toxinidir}/requirements/django-1.7.x.pip
[testenv:py34-dj16]
basepython = python3.4
-deps =
- {[libs]common}
- {[libs]for-dj16}
- {[django]1.6.x}
+deps = -r{toxinidir}/requirements/django-1.6.x.pip
[testenv:py34-dj15]
basepython = python3.4
-deps =
- {[libs]common}
- {[libs]for-dj15}
- {[django]1.5.x}
+deps = -r{toxinidir}/requirements/django-1.5.x.pip
; -- python 3.3 ---------------------------------------------------------------
[testenv:py33-dj]
basepython = python3.3
-commands =
- python {envbindir}/coverage run setup.py test []
-deps =
- {[libs]common}
- {[libs]for-dj}
- {[django]latest}
+commands = python -W error {envbindir}/coverage run setup.py test []
+deps = -r{toxinidir}/requirements/django-dev.pip
[testenv:py33-dj18]
basepython = python3.3
-deps =
- {[libs]common}
- {[libs]for-dj18}
- {[django]1.8.x}
+deps = -r{toxinidir}/requirements/django-1.8.x.pip
[testenv:py33-dj17]
basepython = python3.3
-deps =
- {[libs]common}
- {[libs]for-dj17}
- {[django]1.7.x}
+deps = -r{toxinidir}/requirements/django-1.7.x.pip
[testenv:py33-dj16]
basepython = python3.3
-deps =
- {[libs]common}
- {[libs]for-dj16}
- {[django]1.6.x}
+deps = -r{toxinidir}/requirements/django-1.6.x.pip
[testenv:py33-dj15]
basepython = python3.3
-deps =
- {[libs]common}
- {[libs]for-dj15}
- {[django]1.5.x}
+deps = -r{toxinidir}/requirements/django-1.5.x.pip
; -- python 2.7 ---------------------------------------------------------------
[testenv:py27-dj]
basepython = python2.7
-commands =
- python {envbindir}/coverage run setup.py test []
-deps =
- {[libs]common}
- {[libs]for-dj}
- {[django]latest}
+commands = python -W error {envbindir}/coverage run setup.py test []
+deps = -r{toxinidir}/requirements/django-dev.pip
+
+[testenv:py27-dj18]
+basepython = python2.7
+deps = -r{toxinidir}/requirements/django-1.8.x.pip
+
+[testenv:py27-dj17]
+basepython = python2.7
+deps = -r{toxinidir}/requirements/django-1.7.x.pip
[testenv:py27-dj16]
basepython = python2.7
-deps =
- {[libs]common}
- {[libs]for-dj16}
- {[django]1.6.x}
+deps = -r{toxinidir}/requirements/django-1.6.x.pip
[testenv:py27-dj15]
basepython = python2.7
-deps =
- {[libs]common}
- {[libs]for-dj15}
- {[django]1.5.x}
+deps = -r{toxinidir}/requirements/django-1.5.x.pip
[testenv:py27-dj14]
basepython = python2.7
-deps =
- {[libs]common}
- {[libs]for-dj14}
- {[django]1.4.x}
+deps = -r{toxinidir}/requirements/django-1.4.x.pip
[testenv:py27-dj13]
basepython = python2.7
-deps =
- {[libs]common}
- {[libs]for-dj13}
- {[django]1.3.x}
+deps = -r{toxinidir}/requirements/django-1.3.x.pip
[testenv:py27-dj12]
basepython = python2.7
-commands =
- python {envbindir}/coverage run setup.py test []
-deps =
- {[libs]common}
- {[libs]for-dj12}
- {[django]1.2.x}
+commands = python -W error {envbindir}/coverage run setup.py test []
+deps = -r{toxinidir}/requirements/django-1.2.x.pip