[Python-modules-commits] [django-tables] 01/03: New upstream version 1.10.0
Neil Williams
codehelp at moszumanska.debian.org
Tue Aug 8 17:45:57 UTC 2017
This is an automated email from the git hooks/post-receive script.
codehelp pushed a commit to branch codehelp
in repository django-tables.
commit 7ea133fb37fdb03c535d833fd1514661e9aed9e9
Author: Neil Williams <codehelp at debian.org>
Date: Tue Aug 8 13:29:00 2017 -0400
New upstream version 1.10.0
---
.coveragerc | 1 +
CHANGELOG.md | 10 +++-
django_tables2/__init__.py | 8 +--
django_tables2/columns/__init__.py | 3 +-
django_tables2/columns/base.py | 25 ++++++---
django_tables2/columns/manytomanycolumn.py | 73 +++++++++++++++++++++++++
django_tables2/export/TODO.md | 10 ----
django_tables2/export/export.py | 5 +-
django_tables2/export/views.py | 19 ++++++-
django_tables2/rows.py | 2 +-
django_tables2/tables.py | 48 ++++++++++++++--
docs/index.rst | 1 -
docs/pages/api-reference.rst | 9 ++-
docs/pages/builtin-columns.rst | 1 +
docs/pages/custom-data.rst | 11 +++-
docs/pages/export.rst | 26 +++++++++
docs/pages/performance.rst | 12 ----
docs/pages/table-data.rst | 13 +++++
requirements/common.pip | 2 +-
tests/app/models.py | 2 +
tests/columns/test_manytomanycolumn.py | 80 +++++++++++++++++++++++++++
tests/export/test_export.py | 88 +++++++++++++++++++++++++-----
tests/test_core.py | 68 ++++++++++++++++++++---
tox.ini | 21 ++++---
24 files changed, 458 insertions(+), 80 deletions(-)
diff --git a/.coveragerc b/.coveragerc
index bb3a108..7e64496 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,6 +2,7 @@
source =
django_tables2
tests
+branch = true
[html]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 338c286..f66c223 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,16 @@
## master (unreleased)
+## 1.10.0 (2017-06-30)
+ - Added `ManyToManyColumn` automatically added for `ManyToManyField`s.
+
+## 1.9.1 (2017-06-29)
+ - Allow customizing the value used in `Table.as_values()` (when using a `render_<name>` method) using a `value_<name>` method. (fixes [#458](https://github.com/bradleyayers/django-tables2/issues/458))
+ - Allow excluding columns from the `Table.as_values()` output. (fixes [#459](https://github.com/bradleyayers/django-tables2/issues/459))
+ - Fixed unicode handling for columhn headers in `Table.as_values()`
+
## 1.9.0 (2017-06-22)
-Allow computable attrs for `<td>`-tags from `Table.attrs` ([#457](https://github.com/bradleyayers/django-tables2/pull/457), fixes [#451](https://github.com/bradleyayers/django-tables2/issues/451))
+- Allow computable attrs for `<td>`-tags from `Table.attrs` ([#457](https://github.com/bradleyayers/django-tables2/pull/457), fixes [#451](https://github.com/bradleyayers/django-tables2/issues/451))
## 1.8.0 (2017-06-17)
- Feature: Added an `ExportMixin` to export table data in various export formats (CSV, XLS, etc.) using [tablib](http://docs.python-tablib.org/en/latest/).
diff --git a/django_tables2/__init__.py b/django_tables2/__init__.py
index b62443c..841365c 100644
--- a/django_tables2/__init__.py
+++ b/django_tables2/__init__.py
@@ -2,20 +2,20 @@
from .tables import Table, TableBase
from .columns import (BooleanColumn, Column, CheckBoxColumn, DateColumn,
DateTimeColumn, EmailColumn, FileColumn, JSONColumn,
- LinkColumn, RelatedLinkColumn, TemplateColumn,
+ LinkColumn, ManyToManyColumn, RelatedLinkColumn, TemplateColumn,
TimeColumn, URLColumn)
from .config import RequestConfig
from .utils import A
from .views import SingleTableMixin, SingleTableView, MultiTableMixin
-__version__ = '1.9.0'
+__version__ = '1.10.0'
__all__ = (
'Table', 'TableBase',
'BooleanColumn', 'Column', 'CheckBoxColumn', 'DateColumn', 'DateTimeColumn',
- 'EmailColumn', 'FileColumn', 'JSONColumn', 'LinkColumn', 'RelatedLinkColumn',
- 'TemplateColumn', 'TimeColumn', 'URLColumn',
+ 'EmailColumn', 'FileColumn', 'JSONColumn', 'LinkColumn', 'ManyToManyColumn',
+ 'RelatedLinkColumn', 'TemplateColumn', 'TimeColumn', 'URLColumn',
'RequestConfig',
'A',
diff --git a/django_tables2/columns/__init__.py b/django_tables2/columns/__init__.py
index a08d968..7e62870 100644
--- a/django_tables2/columns/__init__.py
+++ b/django_tables2/columns/__init__.py
@@ -7,6 +7,7 @@ from .emailcolumn import EmailColumn
from .filecolumn import FileColumn
from .jsoncolumn import JSONColumn
from .linkcolumn import LinkColumn, RelatedLinkColumn
+from .manytomanycolumn import ManyToManyColumn
from .templatecolumn import TemplateColumn
from .urlcolumn import URLColumn
from .timecolumn import TimeColumn
@@ -14,6 +15,6 @@ from .timecolumn import TimeColumn
__all__ = (
'library', 'BoundColumn', 'BoundColumns', 'Column',
'BooleanColumn', 'CheckBoxColumn', 'DateColumn', 'DateTimeColumn',
- 'EmailColumn', 'FileColumn', 'JSONColumn', 'LinkColumn',
+ 'EmailColumn', 'FileColumn', 'JSONColumn', 'LinkColumn', 'ManyToManyColumn',
'RelatedLinkColumn', 'TemplateColumn', 'URLColumn', 'TimeColumn'
)
diff --git a/django_tables2/columns/base.py b/django_tables2/columns/base.py
index b09aee2..93f6dba 100644
--- a/django_tables2/columns/base.py
+++ b/django_tables2/columns/base.py
@@ -4,7 +4,6 @@ from __future__ import absolute_import, unicode_literals
from collections import OrderedDict
from itertools import islice
-from django.db import models
from django.utils import six
from django.utils.safestring import SafeData
@@ -77,6 +76,14 @@ class Column(object):
the table is using. The only case where ordering is not affected is
when a `.QuerySet` is used as the table data (since sorting is
performed by the database).
+ empty_values (iterable): list of values considered as a missing value,
+ for which the column will render the default value. Defaults to
+ `(None, '')`
+ exclude_from_export (bool): If `True`, this column will not be added to
+ the data iterator returned from as_values().
+ footer (str, callable): Defines the footer of this column. If a callable
+ is passed, it can take optional keyword argumetns `column`,
+ `bound_colun` and `table`.
order_by (str, tuple or `.Accessor`): Allows one or more accessors to be
used for ordering rather than *accessor*.
orderable (bool): If `False`, this column will not be allowed to
@@ -103,7 +110,8 @@ class Column(object):
def __init__(self, verbose_name=None, accessor=None, default=None,
visible=True, orderable=None, attrs=None, order_by=None,
- empty_values=None, localize=None, footer=None):
+ empty_values=None, localize=None, footer=None,
+ exclude_from_export=False):
if not (accessor is None or isinstance(accessor, six.string_types) or
callable(accessor)):
raise TypeError('accessor must be a string or callable, not %s' %
@@ -129,6 +137,8 @@ class Column(object):
self._footer = footer
+ self.exclude_from_export = exclude_from_export
+
@property
def default(self):
# handle callables
@@ -155,6 +165,9 @@ class Column(object):
return self.verbose_name
def footer(self, bound_column, table):
+ '''
+ Returns the content of the footer, if specified.
+ '''
footer_kwargs = {
'column': self,
'bound_column': bound_column,
@@ -179,8 +192,6 @@ class Column(object):
This method can be overridden by :ref:`table.render_FOO` methods on the
table or by subclassing `.Column`.
- :returns: `unicode`
-
If the value for this cell is in `.empty_values`, this method is
skipped and an appropriate default value is rendered instead.
Subclasses should set `.empty_values` to ``()`` if they want to handle
@@ -203,10 +214,6 @@ class Column(object):
'''
value = call_with_appropriate(self.render, kwargs)
- # convert model instances to string, otherwise exporting to xls fails.
- if isinstance(value, models.Model):
- value = str(value)
-
return value
def order(self, queryset, is_descending):
@@ -547,6 +554,8 @@ class BoundColumns(object):
for name, column in six.iteritems(base_columns):
self.columns[name] = bc = BoundColumn(table, column, name)
bc.render = getattr(table, 'render_' + name, column.render)
+ # How the value is defined: 1. value_<name> 2. render_<name> 3. column.value.
+ bc.value = getattr(table, 'value_' + name, getattr(table, 'render_' + name, column.value))
bc.order = getattr(table, 'order_' + name, column.order)
def iternames(self):
diff --git a/django_tables2/columns/manytomanycolumn.py b/django_tables2/columns/manytomanycolumn.py
new file mode 100644
index 0000000..505b05c
--- /dev/null
+++ b/django_tables2/columns/manytomanycolumn.py
@@ -0,0 +1,73 @@
+# coding: utf-8
+from __future__ import absolute_import, unicode_literals
+
+from django.db import models
+from django.utils.encoding import force_text
+
+from django_tables2.templatetags.django_tables2 import title
+
+from .base import Column, library
+
+
+ at library.register
+class ManyToManyColumn(Column):
+ '''
+ Display the list of objects from a `ManyRelatedManager`
+
+ Arguments:
+ transform: callable to transform each item to text, it gets an item as argument
+ and must return a string-like representation of the item.
+ By default, it calls `~django.utils.force_text` on each item.
+ filter: callable to filter, limit or order the QuerySet, it gets the
+ `ManyRelatedManager` as first argument and must return.
+ By default, it returns `all()``
+
+ For example, when displaying a list of friends with their full name::
+
+ # models.py
+ class Person(models.Model):
+ first_name = models.CharField(max_length=200)
+ last_name = models.CharField(max_length=200)
+ friends = models.ManyToManyField(Person)
+
+ @property
+ def name(self):
+ return '{} {}'.format(self.first_name, self.last_name)
+
+ # tables.py
+ class PersonTable(tables.Table):
+ name = tables.Column(order_by=('last_name', 'first_name'))
+ friends = tables.ManyToManyColumn(transform=lamda user: u.name)
+
+ '''
+ def __init__(self, transform=None, filter=None, *args, **kwargs):
+ if transform is not None:
+ self.transform = transform
+ if filter is not None:
+ self.filter = filter
+
+ super(ManyToManyColumn, self).__init__(*args, **kwargs)
+
+ def transform(self, obj):
+ '''
+ Transform is applied to each item of the list of objects from the ManyToMany relation.
+ '''
+ return force_text(obj)
+
+ def filter(self, qs):
+ '''
+ Filter is called on the ManyRelatedManager to allow ordering, filtering or limiting
+ on the set of related objects.
+ '''
+ return qs.all()
+
+ def render(self, value):
+ if not value.exists():
+ return '-'
+
+ return ', '.join(map(self.transform, self.filter(value)))
+
+ @classmethod
+ def from_field(cls, field):
+ if isinstance(field, models.ManyToManyField):
+ return cls(verbose_name=title(field.verbose_name))
diff --git a/django_tables2/export/TODO.md b/django_tables2/export/TODO.md
deleted file mode 100644
index 6e50e1b..0000000
--- a/django_tables2/export/TODO.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# django_tables2 csv export
-
-Try to make a simple and clean implementation of table exports based on `Table.as_values()`.
-
-## TODO:
- - [ ] exclude columns from export
- - [x] example how to use in function views.
- - [x] docs
- - [ ] test using a template column
- - [ ] test using a render_foo() method
diff --git a/django_tables2/export/export.py b/django_tables2/export/export.py
index 4a6ebfe..ec37cbb 100644
--- a/django_tables2/export/export.py
+++ b/django_tables2/export/export.py
@@ -18,6 +18,7 @@ class TableExport(object):
Argumenents:
export_format (str): one of `csv, json, latex, ods, tsv, xls, xlsx, yml`
table (`~.Table`): instance of the table to export the data from
+ exclude_columns (iterable): list of column names to exclude from the export
'''
CSV = 'csv'
JSON = 'json'
@@ -39,14 +40,14 @@ class TableExport(object):
YAML: 'text/yml; charset=utf-8',
}
- def __init__(self, export_format, table):
+ def __init__(self, export_format, table, exclude_columns=None):
if not self.is_valid_format(export_format):
raise TypeError('Export format "{}" is not supported.'.format(export_format))
self.format = export_format
self.dataset = Dataset()
- for i, row in enumerate(table.as_values()):
+ for i, row in enumerate(table.as_values(exclude_columns=exclude_columns)):
if i == 0:
self.dataset.headers = row
else:
diff --git a/django_tables2/export/views.py b/django_tables2/export/views.py
index bb05b81..841079e 100644
--- a/django_tables2/export/views.py
+++ b/django_tables2/export/views.py
@@ -6,8 +6,24 @@ from .export import TableExport
class ExportMixin(object):
'''
Support various export formats for the table data.
+
+ `ExportMixin` looks for some attributes on the class to change it's behaviour:
+
+ Attributes:
+ export_trigger_param (str): is the name of the GET attribute used to trigger
+ the export. It's value decides the export format, refer to
+ `TableExport` for a list of available formats.
+ excude_columns (iterable): column names excluded from the export.
+ For example, one might want to exclude columns containing buttons from
+ the export. Excluding columns from the export is also possible using the
+ `exclude_from_export` argument to the `.Column` constructor::
+
+ class Table(tables.Table):
+ name = tables.Column()
+ buttons = tables.TemplateColumn(exclude_from_export=True, template_name=...)
'''
export_trigger_param = '_export'
+ exclude_columns = ()
def get_export_filename(self, export_format):
return 'table.{}'.format(export_format)
@@ -15,7 +31,8 @@ class ExportMixin(object):
def create_export(self, export_format):
exporter = TableExport(
export_format=export_format,
- table=self.get_table(**self.get_table_kwargs())
+ table=self.get_table(**self.get_table_kwargs()),
+ exclude_columns=self.exclude_columns
)
return exporter.response(filename=self.get_export_filename(export_format))
diff --git a/django_tables2/rows.py b/django_tables2/rows.py
index 5401e05..823462a 100644
--- a/django_tables2/rows.py
+++ b/django_tables2/rows.py
@@ -205,7 +205,7 @@ class BoundRow(object):
Call the column's value method with appropriate kwargs
'''
return call_with_appropriate(
- bound_column.column.value,
+ bound_column.value,
self._optional_cell_arguments(bound_column, value)
)
diff --git a/django_tables2/tables.py b/django_tables2/tables.py
index 0d6f472..569aba3 100644
--- a/django_tables2/tables.py
+++ b/django_tables2/tables.py
@@ -10,12 +10,13 @@ from django.core.paginator import Paginator
from django.db.models.fields import FieldDoesNotExist
from django.template.loader import get_template
from django.utils import six
+from django.utils.encoding import force_text
from . import columns
from .config import RequestConfig
from .data import TableData
from .rows import BoundRows
-from .utils import AttributeDict, OrderBy, OrderByTuple, Sequence, computed_values
+from .utils import AttributeDict, OrderBy, OrderByTuple, Sequence
class DeclarativeColumnsMetaclass(type):
@@ -380,15 +381,50 @@ class TableBase(object):
self.before_render(request)
return template.render(context)
- def as_values(self):
+ def as_values(self, exclude_columns=None):
'''
- Return a row iterator of the data which would be shown in the table where the first row is the table headers.
+ Return a row iterator of the data which would be shown in the table where
+ the first row is the table headers.
- This can be used to output the table data as CSV, excel, etc
+ arguments:
+ exclude_columns (iterable): columns to exclude in the data iterator.
+
+ This can be used to output the table data as CSV, excel, for example using the
+ `~.export.ExportMixin`.
+
+ If a column is defined using a :ref:`table.render_FOO`, the returned value from
+ that method is used. If you want to differentiate between the rendered cell
+ and a value, use a `value_Foo`-method::
+
+ class Table(tables.Table):
+ name = tables.Column()
+
+ def render_name(self, value):
+ return format_html('<span class="name">{}</span>', value)
+
+ def value_name(self, value):
+ return value
+
+ will have a value wrapped in `<span>` in the rendered HTML, and just returns
+ the value when `as_values()` is called.
'''
- yield [str(c.header) for c in self.columns]
+ if exclude_columns is None:
+ exclude_columns = ()
+
+ def excluded(column):
+ if column.column.exclude_from_export:
+ return True
+ return column.name in exclude_columns
+
+ yield [
+ force_text(column.header, strings_only=True)
+ for column in self.columns if not excluded(column)
+ ]
for r in self.rows:
- yield [r.get_cell_value(column.name) for column in r.table.columns]
+ yield [
+ force_text(r.get_cell_value(column.name), strings_only=True)
+ for column in r.table.columns if not excluded(column)
+ ]
def has_footer(self):
'''
diff --git a/docs/index.rst b/docs/index.rst
index e313192..ee90b3f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -34,7 +34,6 @@ Table of contents
pages/installation
pages/tutorial
pages/table-data
- pages/performance
.. toctree::
:maxdepth: 1
diff --git a/docs/pages/api-reference.rst b/docs/pages/api-reference.rst
index b6d9a1d..072b0c8 100644
--- a/docs/pages/api-reference.rst
+++ b/docs/pages/api-reference.rst
@@ -200,7 +200,7 @@ API Reference
This functionality is also available via the ``order_by`` keyword
argument to a table's constructor.
- sequence (iteralbe): The sequence of the table columns.
+ sequence (iterable): The sequence of the table columns.
This allows the default order of columns (the order they were defined
in the Table) to be overridden.
@@ -260,6 +260,7 @@ API Reference
---------
.. autoclass:: django_tables2.columns.Column
+ :members: render, value, order
`.BooleanColumn`
@@ -315,6 +316,12 @@ API Reference
.. autoclass:: django_tables2.columns.LinkColumn
:members:
+`.ManyToManyColumn`
+-------------------
+
+.. autoclass:: django_tables2.columns.ManyToManyColumn
+ :members:
+
`.RelatedLinkColumn`
--------------------
diff --git a/docs/pages/builtin-columns.rst b/docs/pages/builtin-columns.rst
index ffac4aa..dcf137c 100644
--- a/docs/pages/builtin-columns.rst
+++ b/docs/pages/builtin-columns.rst
@@ -14,6 +14,7 @@ For common use-cases the following columns are included:
- `.FileColumn` -- renders files as links
- `.JSONColumn` -- renders JSON as an indented string in ``<pre></pre>``
- `.LinkColumn` -- renders ``<a href="...">`` tags (compose a django url)
+- `.ManyToManyColumn` -- renders a list objects from a `ManyToManyField`
- `.RelatedLinkColumn` -- renders ``<a href="...">`` tags linking related objects
- `.TemplateColumn` -- renders template code
- `.URLColumn` -- renders ``<a href="...">`` tags (absolute url)
diff --git a/docs/pages/custom-data.rst b/docs/pages/custom-data.rst
index b36c928..a02035b 100644
--- a/docs/pages/custom-data.rst
+++ b/docs/pages/custom-data.rst
@@ -37,7 +37,7 @@ If the resulting value is callable, it is called and the return value is used.
.. _table.render_foo:
`Table.render_foo` methods
-------------------------------------
+--------------------------
To change how a column is rendered, define a ``render_foo`` method on
the table for example: `render_row_number()` for a column named `row_number`.
@@ -87,6 +87,15 @@ argument.
a default value is rendered instead (both `.Column.render` and
``Table.render_FOO`` are skipped).
+`Table.value_foo` methods
+-------------------------
+
+If you want to use `Table.as_values` to export your data, you might want to define
+a method ``value_foo``, which is analogous to ``render_foo``, but used to render the
+values rather than the HTML output.
+
+Please refer to `~.Table.as_values` for an example.
+
.. _subclassing-column:
Subclassing `.Column`
diff --git a/docs/pages/export.rst b/docs/pages/export.rst
index f10cb2c..90a639b 100644
--- a/docs/pages/export.rst
+++ b/docs/pages/export.rst
@@ -51,3 +51,29 @@ If you must use a function view, you might use someting like this::
return render(request, 'table.html', {
'table': table
})
+
+
+Excluding columns
+-----------------
+
+Certain columns do not make sense while exporting data: you might show images or
+have a column with buttons you want to exclude from the export.
+You can define the columns you want to exclude in several ways::
+
+ # exclude a column while defining Columns on a table:
+ class Table(tables.Table):
+ name = columns.Column()
+ buttons = columns.TemplateColumn(template_name='...', exclude_from_export=True)
+
+
+ # exclude columns while creating the TableExport instance:
+ exporter = TableExport('csv', table, exclude_columns=('image', 'buttons'))
+
+
+If you use the ``~.ExportMixin``, add an ``exclude_columns`` attribute to your class::
+
+ class TableView(ExportMixin, tables.SingleTableView):
+ table_class = MyTable
+ model = Person
+ template_name = 'django_tables2/bootstrap.html'
+ exclude_column = ('buttons', )
diff --git a/docs/pages/performance.rst b/docs/pages/performance.rst
deleted file mode 100644
index 660abfd..0000000
--- a/docs/pages/performance.rst
+++ /dev/null
@@ -1,12 +0,0 @@
-Performance
------------
-
-Django-tables tries to be efficient in displaying big datasets. It tries to
-avoid converting the `~django.db.models.query.QuerySet` instances to lists by
-using SQL to slice the data and should be able to handle datasets with 100k
-records without a problem.
-
-However, when using one of the customisation methods described in this
-documentation, there is lot's of oppurtunity to introduce slowness.
-If you experience that, try to strip the table of customisations and re-add them
-one by one, checking for performance after each step.
diff --git a/docs/pages/table-data.rst b/docs/pages/table-data.rst
index 2d0fb78..f293efb 100644
--- a/docs/pages/table-data.rst
+++ b/docs/pages/table-data.rst
@@ -70,3 +70,16 @@ what fields to show or hide:
- `~.Table.Meta.sequence` -- reorder columns
- `~.Table.Meta.fields` -- specify model fields to *include*
- `~.Table.Meta.exclude` -- specify model fields to *exclude*
+
+Performance
+-----------
+
+Django-tables tries to be efficient in displaying big datasets. It tries to
+avoid converting the `~django.db.models.query.QuerySet` instances to lists by
+using SQL to slice the data and should be able to handle datasets with 100k
+records without a problem.
+
+However, when using one of the customisation methods described in this
+documentation, there is lot's of oppurtunity to introduce slowness.
+If you experience that, try to strip the table of customisations and re-add them
+one by one, checking for performance after each step.
diff --git a/requirements/common.pip b/requirements/common.pip
index cd79631..2086e6d 100644
--- a/requirements/common.pip
+++ b/requirements/common.pip
@@ -1,4 +1,4 @@
-django-haystack>=2.4.1,<2.5.0
+django-haystack>=2.6.1
fudge
lxml
pylint
diff --git a/tests/app/models.py b/tests/app/models.py
index 077e245..7b99f53 100644
--- a/tests/app/models.py
+++ b/tests/app/models.py
@@ -57,6 +57,8 @@ class Person(models.Model):
object_id = models.PositiveIntegerField(null=True, blank=True)
foreign_key = GenericForeignKey()
+ friends = models.ManyToManyField('Person')
+
class Meta:
verbose_name = "person"
verbose_name_plural = "people"
diff --git a/tests/columns/test_manytomanycolumn.py b/tests/columns/test_manytomanycolumn.py
new file mode 100644
index 0000000..9657e3c
--- /dev/null
+++ b/tests/columns/test_manytomanycolumn.py
@@ -0,0 +1,80 @@
+# coding: utf-8
+from __future__ import unicode_literals
+
+from random import randint, sample
+
+import pytest
+
+import django_tables2 as tables
+from tests.app.models import Person
+
+FAKE_NAMES = (
+ ('Kyle', 'Strader'),
+ ('Francis', 'Fisher'),
+ ('James', 'Jury'),
+ ('Florentina', 'Floyd'),
+ ('Mark', 'Boyd'),
+ ('Simone', 'Fong'),
+)
+
+
+def create_Persons():
+ for first, last in FAKE_NAMES:
+ Person.objects.create(first_name=first, last_name=last)
+
+ persons = list(Person.objects.all())
+
+ # give everyone 1 to 3 friends
+ for person in persons:
+ person.friends.add(*sample(persons, randint(1, 3)))
+ person.save()
+
+
+ at pytest.mark.django_db
+def test_ManyToManyColumn_from_model():
+ '''
+ Automaticcally uses the ManyToManyColumn for a ManyToManyField, and calls the
+ Models's `__str__` method to transform the model instace to string.
+ '''
+ create_Persons()
+
+ class Table(tables.Table):
+ name = tables.Column(accessor='name', order_by=('last_name', 'first_name'))
+
+ class Meta:
+ model = Person
+ fields = ('name', 'friends')
+
+ table = Table(Person.objects.all())
+
+ for row in table.rows:
+ friends = row.get_cell('friends').split(', ')
+
+ for friend in friends:
+ assert Person.objects.filter(first_name=friend).exists()
+
+
+ at pytest.mark.django_db
+def test_ManyToManyColumn_complete_exmplae():
+ create_Persons()
+
+ # add a friendless person
+ remi = Person.objects.create(first_name='Remi', last_name='Barberin')
+
+ class Table(tables.Table):
+ name = tables.Column(accessor='name', order_by=('last_name', 'first_name'))
+ friends = tables.ManyToManyColumn(
+ transform=lambda o: o.name,
+ filter=lambda o: o.order_by('-last_name')
+ )
+
+ table = Table(Person.objects.all().order_by('last_name'))
+ for row in table.rows:
+ friends = row.get_cell('friends')
+ if friends == '-':
+ assert row.get_cell('name') == remi.name
+ continue
+
+ friends = list(map(lambda o: o.split(' '), friends.split(', ')))
+
+ assert friends == sorted(friends, key=lambda item: item[1], reverse=True)
diff --git a/tests/export/test_export.py b/tests/export/test_export.py
index 7a63368..ec064a0 100644
--- a/tests/export/test_export.py
+++ b/tests/export/test_export.py
@@ -32,7 +32,7 @@ EXPECTED_JSON = list([
])
-def create_test_data():
+def create_test_persons():
for first_name, last_name in NAMES:
Person.objects.create(first_name=first_name, last_name=last_name)
@@ -51,7 +51,7 @@ class View(DispatchHookMixin, ExportMixin, tables.SingleTableView):
@pytest.mark.django_db
def test_view_should_support_csv_export():
- create_test_data()
+ create_test_persons()
response, view = View.as_view()(build_request('/?_export=csv'))
assert response.getvalue().decode('utf8') == EXPECTED_CSV
@@ -73,7 +73,7 @@ def test_exporter_should_raise_error_for_unsupported_file_type():
@pytest.mark.django_db
def test_view_should_support_json_export():
- create_test_data()
+ create_test_persons()
response, view = View.as_view()(build_request('/?_export=json'))
assert json.loads(response.getvalue().decode('utf8')) == EXPECTED_JSON
@@ -84,7 +84,7 @@ def test_function_view():
'''
Test the code used in the docs
'''
- create_test_data()
+ create_test_persons()
def table_view(request):
table = Table(Person.objects.all())
@@ -110,22 +110,28 @@ def test_function_view():
assert 'Lindy' not in html
- at pytest.mark.django_db
-def test_exporting_should_work_with_foreign_keys():
+def create_test_occupations():
vlaanderen = Region.objects.create(name='Vlaanderen')
Occupation.objects.create(name='Timmerman', boolean=True, region=vlaanderen)
Occupation.objects.create(name='Ecoloog', boolean=False, region=vlaanderen)
- class OccupationTable(tables.Table):
- name = tables.Column()
- boolean = tables.Column()
- region = tables.Column()
- class OccupationView(DispatchHookMixin, ExportMixin, tables.SingleTableView):
- table_class = OccupationTable
- table_pagination = {'per_page': 1}
- model = Occupation
- template_name = 'django_tables2/bootstrap.html'
+class OccupationTable(tables.Table):
+ name = tables.Column()
+ boolean = tables.Column()
+ region = tables.Column()
+
+
+class OccupationView(DispatchHookMixin, ExportMixin, tables.SingleTableView):
+ table_class = OccupationTable
+ table_pagination = {'per_page': 1}
+ model = Occupation
+ template_name = 'django_tables2/bootstrap.html'
+
+
+ at pytest.mark.django_db
+def test_exporting_should_work_with_foreign_keys():
+ create_test_occupations()
response, view = OccupationView.as_view()(build_request('/?_export=xls'))
data = response.content
@@ -133,3 +139,55 @@ def test_exporting_should_work_with_foreign_keys():
assert data.find('Vlaanderen'.encode())
assert data.find('Ecoloog'.encode())
assert data.find('Timmerman'.encode())
+
+
+ at pytest.mark.django_db
+def test_exporting_exclude_columns():
+ create_test_occupations()
+
+ class OccupationExcludingView(DispatchHookMixin, ExportMixin, tables.SingleTableView):
+ table_class = OccupationTable
+ table_pagination = {'per_page': 1}
+ model = Occupation
+ template_name = 'django_tables2/bootstrap.html'
+ exclude_columns = ('boolean', )
+
+ response, view = OccupationExcludingView.as_view()(build_request('/?_export=csv'))
+ data = response.getvalue().decode('utf8')
+
+ assert data.splitlines()[0] == 'Name,Region'
+
+
+ at pytest.mark.django_db
+def test_exporting_unicode_data():
+ unicode_name = '木匠'
+ Occupation.objects.create(name=unicode_name)
+
+ expected_csv = 'Name,Boolean,Region\r\n{},,\r\n'.format(unicode_name)
+
+ response, view = OccupationView.as_view()(build_request('/?_export=csv'))
+ assert response.getvalue().decode('utf8') == expected_csv
+
+ # smoke tests, hard to test this binary format for string containment
+ response, view = OccupationView.as_view()(build_request('/?_export=xls'))
+ data = response.content
+ assert len(data) > len(expected_csv)
+
+ response, view = OccupationView.as_view()(build_request('/?_export=xlsx'))
+ data = response.content
+ assert len(data) > len(expected_csv)
+
+
+def test_exporting_unicode_header():
+ unicode_header = 'hé'
+
+ class Table(tables.Table):
+ name = tables.Column(verbose_name=unicode_header)
+
+ exporter = TableExport('csv', Table([]))
+ response = exporter.response()
+ assert response.getvalue().decode('utf8') == unicode_header + '\r\n'
+
+ exporter = TableExport('xls', Table([]))
+ # this would fail if the header contains unicode and string converstion is attempted.
+ exporter.export()
diff --git a/tests/test_core.py b/tests/test_core.py
index 3caeddc..e21cde5 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -520,23 +520,44 @@ def test_table_defaults_are_honored():
assert table.rows[0].get_cell('name') == 'efgh'
+AS_VALUES_DATA = [
+ {'name': 'Adrian', 'country': 'Australia'},
+ {'name': 'Adrian', 'country': 'Brazil'},
+ {'name': 'Audrey', 'country': 'Chile'},
+ {'name': 'Bassie', 'country': 'Belgium'},
+]
+
+
def test_as_values():
class Table(tables.Table):
name = tables.Column()
country = tables.Column()
- data = [
- {'name': 'Adrian', 'country': 'Australia'},
- {'name': 'Adrian', 'country': 'Brazil'},
- {'name': 'Audrey', 'country': 'Chile'},
- {'name': 'Bassie', 'country': 'Belgium'},
- ]
- expected = [['Name', 'Country']] + [[r['name'], r['country']] for r in data]
- table = Table(data)
+ expected = [['Name', 'Country']] + [[r['name'], r['country']] for r in AS_VALUES_DATA]
+ table = Table(AS_VALUES_DATA)
assert list(table.as_values()) == expected
+def test_as_values_exclude():
+ class Table(tables.Table):
+ name = tables.Column()
+ country = tables.Column()
+
+ expected = [['Name']] + [[r['name']] for r in AS_VALUES_DATA]
+ table = Table(AS_VALUES_DATA)
+
+ assert list(table.as_values(exclude_columns=('country', ))) == expected
+
+
+def test_as_values_exclude_from_export():
+ class Table(tables.Table):
+ name = tables.Column()
+ buttons = tables.Column(exclude_from_export=True)
+
+ assert list(Table([]).as_values()) == [['Name'], ]
+
+
def test_as_values_empty_values():
'''
Table's as_values() method returns `None` for missing values
@@ -557,6 +578,37 @@ def test_as_values_empty_values():
assert list(table.as_values()) == expected
+def test_as_values_render_FOO():
+
+ class Table(tables.Table):
+ name = tables.Column()
+ country = tables.Column()
+
+ def render_country(self, value):
+ return value + ' test'
+
+ expected = [['Name', 'Country']] + [[r['name'], r['country'] + ' test'] for r in AS_VALUES_DATA]
+
+ assert list(Table(AS_VALUES_DATA).as_values()) == expected
+
+
+def test_as_values_value_FOO():
+
+ class Table(tables.Table):
+ name = tables.Column()
+ country = tables.Column()
+
+ def render_country(self, value):
+ return value + ' test'
+
+ def value_country(self, value):
+ return value + ' different'
+
+ expected = [['Name', 'Country']] + [[r['name'], r['country'] + ' different'] for r in AS_VALUES_DATA]
+
+ assert list(Table(AS_VALUES_DATA).as_values()) == expected
+
+
def test_row_attrs():
class Table(tables.Table):
alpha = tables.Column()
diff --git a/tox.ini b/tox.ini
index c8b1a88..457b8ee 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,7 +8,14 @@ envlist =
{py27,py33,py34}-{1.8},
{py27,py34,py35}-{1.9,1.10,1.11},
{py34,py35,py36}-{master},
- py27-{docs,flake8,isort}
+ docs,
+ flake8,
+ isort
+
+[travis]
+python:
+ 2.7: py27, docs
+ 3.6: py36, flake8, isort
[testenv]
basepython =
@@ -32,16 +39,16 @@ deps =
psycopg2
-r{toxinidir}/requirements/common.pip
-[testenv:py27-docs]
+[testenv:docs]
+basepython = python2.7
whitelist_externals = make
changedir = docs
commands = make html
-basepython = python2.7
deps =
-r{toxinidir}/docs/requirements.txt
-[testenv:py27-flake8]
-basepython = python2.7
+[testenv:flake8]
+basepython = python3.6
deps = flake8
commands = flake8
@@ -49,7 +56,7 @@ commands = flake8
ignore = F401,E731
max-line-length = 120
-[testenv:py27-isort]
+[testenv:isort]
+basepython = python3.6
deps = isort==4.2.15
... 2 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/django-tables.git
More information about the Python-modules-commits
mailing list