Add CSV Export to Wagtail's Modeladmin

All blog posts

Introduction

Wagtail is a modern open source CMS written in Python and based on Django. It is easy to integrate with existing Django projects. Apart from traditional CMS features, it provides a nice UI for managing any Django database model via the modeladmin module.

The modeladmin IndexView lists entries for a specific model in tabular form. It is easy to define which columns should be included. Starting from here, there are buttons for editing, creating and deleting entries.

One feature is missing though: Data Export
As the data is already presented in a table, CSV is an obvious export format.

We will add an additional button to the modeladmin IndexView

We will add an additional button to the modeladmin IndexView

This idea is not entirely original. This blog post and this StackOverflow question discuss the same thing and my code is heavily influenced by them. However, the solutions given at these sources are not quite ready to copy and paste, as they require some customization of the CSV exporting code for each model you want to export.

The code given in this blog post can be used with any Django model. Per default, all columns are exported, but this can easily be customized on a per-model basis.

Implementation

I will cover the different implementation parts in detail. If you just want to copy-paste and get on with your life, that’s fine too. Just make sure you copy all the given code snippets. It is fine to put everything into wagtail_hooks.py, except the HTML template.

ButtonHelper

The first thing you need whenever you want to add custom functionality to Wagtail’s modeladmin is usually a ButtonHelper:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class ExportButtonHelper(ButtonHelper):
    export_button_classnames = ['icon', 'icon-download']

    def export_button(self, classnames_add=None, classnames_exclude=None):
        if classnames_add is None:
            classnames_add = []
        if classnames_exclude is None:
            classnames_exclude = []

        classnames = self.export_button_classnames + classnames_add
        cn = self.finalise_classname(classnames, classnames_exclude)
        text = _('Export {} to CSV'.format(self.verbose_name_plural.title()))

        return {
            'url': self.url_helper.get_action_url('export',
                                            query_params=self.request.GET),
            'label': text,
            'classname': cn,
            'title': text,
        }

Most of this code is just to get the CSS classes for the button right. The CSS classes icon and icon-download will ensure a simple button with a download icon and some text.

AdminURLHelper

Next, we need an AdminURLHelper that helps with generation, naming, and referencing of our new export URL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ExportAdminURLHelper(AdminURLHelper):
    non_object_specific_actions = ('create', 'choose_parent', 'index',
                                    'export')

    def get_action_url(self, action, *args, **kwargs):
        query_params = kwargs.pop('query_params', None)

        url_name = self.get_action_url_name(action)
        if action in self.non_object_specific_actions:
            url = reverse(url_name)
        else:
            url = reverse(url_name, args=args, kwargs=kwargs)

        if query_params:
            url += '?{params}'.format(params=query_params.urlencode())

        return url

        def get_action_url_pattern(self, action):
        if action in self.non_object_specific_actions:
            return self._get_action_url_pattern(action)

        return self._get_object_specific_action_url_pattern(action)

Once again, this looks a little more complicated than it is. We just need to add the export action to the non_object_specific_actions, because Wagtail treats actions as object-specific per default and will attempt to add the an object’s PK to the URL. Additionally, the URL helper appends the modeladmin filters to the action so that only the filtered data is exported.

ExportView

Finally, we need an ExportView that implements the CSV export. For this, we will use some help from django-queryset-csv.

Install via

pip install django-queryset-csv

Using this, our view is very simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class ExportView(IndexView):
    model_admin = None
    
    def export_csv(self):
        if (self.model_admin is None) or not hasattr(self.model_admin,
                                                        'csv_export_fields'):
            data = self.queryset.all().values()
        else:
            data = self.queryset.all().values(
                *self.model_admin.csv_export_fields)
        return render_to_csv_response(data)

    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        super().dispatch(request, *args, **kwargs)
        return self.export_csv()

It is worth to note the model_admin field. We will use this for specifying a custom list of exported fields. Lines 5 and 6 make sure that whenever model_admin is set and the csv_export_fields attribute is given, the custom field list is used instead of the default behavior that just exports all fields.

Mixin

Making use of Python’s Multiple-Inheritance system, we can create a Mixin that we will later use to override button_helper_class, url_helper_class, export_view_class and get_admin_urls_for_registration all at once:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class ExportModelAdminMixin(object):
    button_helper_class = ExportButtonHelper
    url_helper_class = ExportAdminURLHelper
    export_view_class = ExportView

    def get_admin_urls_for_registration(self):
        urls = super().get_admin_urls_for_registration()
        urls += (url(self.url_helper.get_action_url_pattern('export'),
                        self.export_view,
                        name=self.url_helper.get_action_url_name('export')), )
        return urls

    def export_view(self, request):
        kwargs = {'model_admin': self}
        view_class = self.export_view_class
        return view_class.as_view(**kwargs)(request)d

HTML Template

Now, we need to create an HTML template that includes our new button plus the original modeladmin buttons:

1
2
3
4
5
6
{% extends "modeladmin/index.html" %}

{% block header_extra %}
    {% include 'modeladmin/includes/button.html' with button=view.button_helper.export_button %}
    {{ block.super }}{% comment %}Display the original buttons {% endcomment %}
{% endblock %}

Enabling CSV Export for Models

To make a modeladmin table exportable, just add the mixin to your ModelAdmin definitions in wagtail_hooks.py and set the index_template_name:

class FooModelAdmin(ExportModelAdminMixin, ModelAdmin):
    index_template_name = "wagtailadmin/export_csv.html"

If you want to customize the CSV columns, you can use our new optionl csv_export_fields attribute. It even allows to export attributes of related tables using regular Django ORM syntax (__):

class FooModelAdmin(ExportModelAdminMixin, ModelAdmin):
    index_template_name = "wagtailadmin/export_admin.html"
        csv_export_fields = [
        'bar', 'foobar', 'other_model__attribute'
    ]

Conclusion

In 2020, Wagtail is almost certainly the best option to add CMS functionality to a Django project. This post illustrates its extensibility. In a couple of minutes, you can enable CSV export for all your Django models.


comments powered by Disqus
Tired of ads? Concerned about privacy? The Chromium-based Brave browser tackles both issues and is available on all platforms.