Django Table Exporter
django-tables2 comes with a built-in export feature which supports CSV, JSON, XLSX, and many others. The feature is very handy for a quick and easy way to enable “Download this table” functionality to your website.
# Normal usage
from django_tables2.views import SingleTableMixin
from django_tables2.views.mixins import ExportMixin
class PizzaShopsView(
ExportMixin,
SingleTableMixin,
ListView,
):
table_class = PizzaTable
# Inspection
# django_tables2.export.views.ExportMixin
class ExportMixin:
# `render_to_response` calls `create_export` see https://github.com/jieter/django-tables2/blob/master/django_tables2/export/views.py#L43-L48
def create_export(self, export_format):
exporter = TableExport(
export_format=export_format,
table=self.get_table(**self.get_table_kwargs()),
exclude_columns=self.exclude_columns,
)
return exporter.response(filename=self.get_export_filename(export_format))
# django_tables2.export.export.TableExport
class TableExport:
def __init__(self, export_format, table, exclude_columns=None):
...
# This is the part where items to be exported are added
self.dataset = Dataset()
for i, row in enumerate(table.as_values(exclude_columns=exclude_columns)):
if i == 0:
self.dataset.headers = row
else:
self.dataset.append(row)
The default behaviour of the TableExport
is to get data from table.as_values
, a row iterator of the data
that shows up in the table. From this, the string representation of the table gets served as an attachment and … voila!
You got your exported file ready to be downloaded! Pretty neat.
What is that? You want to show more details on the exported file? Oh, you already have Django Rest Framework support?
In some cases, you only use the table to show an overview/summary. To do this, we have the following options:
- Define a separate table
- Build support for DRF Serializer to wrap your exported data
Of course, there could be a lot more ways but let’s focus on these two.
Separate table for export
You could have used SingleTableMixin
or MultiTableMixin
. Either
way you ultimately get a table
(or tables
) in your view’s context. We just need to exploit get_table_class
and supply
the download_table
.
class PizzaShopsView(
ExportMixin,
SingleTableMixin,
ListView,
):
table_class = PizzaTable
table_class_for_download = PizzaDetailsTable
def get_table_class(self):
if self.request.GET.get(self.export_trigger_param, None):
return self.table_class_for_download
return super().get_table_class()
From here, you can control what to display by defining columns to the PizzaDetailsTable
.
Build DRF Serializer support
Another way is to integrate Django Rest Framework serializers if your application is supporting it already. One advantage is that your API serialization is uniform with that of the download feature. If you decide to update serialization of your data then you only have to look for one place instead of two (serializer and table).
PLUS you only get to change the
export mixin from ExportMixin
to SerializerExportMixin
and it would work for both SingleTableMixin
and MultiTableMixin
from django_tables2.export import ExportMixin
from django_tables2.export.export import TableExport
class SerializerTableExport(TableExport):
def __init__(self, export_format, table, serializer=None, 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
if serializer is None:
raise TypeError("Serializer should be provided for table {}".format(table))
self.dataset = Dataset()
serializer_data = serializer([x for x in table.data], many=True).data
if len(serializer_data) > 0:
self.dataset.headers = serializer_data[0].keys()
for row in serializer_data:
self.dataset.append(row.values())
class SerializerExportMixin(ExportMixin):
def create_export(self, export_format):
exporter = SerializerTableExport(
export_format=export_format,
table=self.get_table(**self.get_table_kwargs()),
serializer=self.serializer_class,
exclude_columns=self.exclude_columns,
)
return exporter.response(filename=self.get_export_filename(export_format))
def get_serializer(self, table):
if self.serializer_class is not None:
return self.serializer_class
else:
return getattr(
self, "{}Serializer".format(self.get_table().__class__.__name__), None
)
def get_table_data(self):
selected_column_ids = self.request.GET.get("_selected_column_ids", None)
if selected_column_ids:
selected_column_ids = map(int, selected_column_ids.split(","))
return super().get_table_data().filter(id__in=selected_column_ids)
return super().get_table_data()
class PizzaShopsView(
SerializerExportMixin,
SingleTableMixin,
ListView,
):
table_class = PizzaTable
serializer_class = PizzaSerializer
That’s it!
Takeaway
It’s a matter of preference whichever approach you choose, or not choose :D.
I once created a project that requires a download feature. I already have an API and a table so I decided to integrate both in a neat way. From that time I chose the second approach to showcase to my groupmates the idea of overriding methods. It did not really took much time since Django Tables is a well-written Django app and extensible enough to build on top of.