Django Table Exporter
May 5, 2019
3 minute read

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.


🐋 hello there! If you enjoy this, a "Thank you" is enough.

Or you can also ...

Buy me a teaBuy me a tea

comments powered by Disqus