[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