Add download CSV option in Wagtail modeladmin

Wagtail is a free and open source CMS written in Python and built on the Django framework. If you heard about the Wagtail for the first time. You should check it out and you will love it.

The modeladmin module provided by Wagtail allows to create customisable listing pages for any model in your Wagtail project. Suppose you have a model named EventRegistration. Django developers are used to performing CRUD operations on this model using the Django admin. The modeladmin module from Wagtail helps to perform CRUD operations from the Wagtail admin interface. Wagtail admin has a great UI and non developers using your project will love it.

This blog lists the steps to add download CSV option in Wagtail admin listing view of your model. For the Wagtail users, you may have seen the similar button in the form submissions listing page. You can follow the modeladmin documentation to register your model and display the model in Wagtail admin. The simplest example is to add the following code in wagtail_hooks.py file in the app directory.

from wagtail.contrib.modeladmin.options import (
    ModelAdmin, modeladmin_register)
from .models import EventRegistration

class EventRegistrationAdmin(ModelAdmin):
    model = EventRegistration
    menu_label = 'Event registrations'  # ditch this to use verbose_name_plural from model
    menu_icon = 'form'  # change as required
    menu_order = 200  # will put in 3rd place (000 being 1st, 100 2nd)
    add_to_settings_menu = False  # or True to add your model to the Settings sub-menu
    exclude_from_explorer = False # or True to exclude pages of this type from Wagtail's explorer view
    list_display = ('title', 'example_field2', 'example_field3', 'register')
    list_filter = ('register', 'example_field2', 'example_field3')
    search_fields = ('title',)

# Now you just need to register your customised EventRegistrationAdmin class with Wagtail
modeladmin_register(EventRegistrationAdmin)

Now you can see the Event registrations menu item in Wagtail admin. When you click on it, you can see the listing page which lists all the event registration entries. The top of the page will be similar to

listing-page-header

To add Download CSV button next to search input follow the steps:

  • The first step is to override the default IndexView to handle CSV creation at the backend and return CSV file.
import csv
from django.http import HttpResponse
from django.utils.encoding import smart_str
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.core.exceptions import PermissionDenied
from wagtail.contrib.modeladmin.views import IndexView
from wagtail.contrib.modeladmin.options import (
    ModelAdmin, modeladmin_register)
from .models import EventRegistration


class EventRegistrationCustomIndexView(IndexView):

    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        # Only continue if logged in user has list permission
        if not self.permission_helper.user_can_list(request.user):
            raise PermissionDenied

        if request.GET.get('action') == 'CSV':

            # Get all registrations
            registrations = EventRegistration.objects.all()
            data_headings = [field.verbose_name for field
                             in EventRegistration._meta.get_fields()]

            # Get query parameters. If the user has filtered the list view.
            # Suppose there is a boolean field named register in the model.
            registered = request.GET.get('register__exact')

            # Filter by registered or not
            if registered:
                registrations = registrations.filter(
                    register=registered,
                )

            # return a CSV instead
            response = HttpResponse(content_type='text/csv; charset=utf-8')
            response['Content-Disposition'] = 'attachment;filename=' + \
                'registrations.csv'

            # Prevents UnicodeEncodeError for labels with non-ansi symbols
            data_headings = [smart_str(label) for label in data_headings]

            writer = csv.writer(response)
            writer.writerow(data_headings)
            for reg in registrations:
                data_row = []
                data_row.extend([
                    reg.title, reg.example_field2, reg.example_field3
                ])
                writer.writerow(data_row)
            return response

        return super(EventRegistrationCustomIndexView,
                     self).dispatch(request, *args, **kwargs)
  • Now our view can handle CSV download requests. We need to also create the new index view template like event_model_admin_index.html to display the Download CSV button.
{% extends "modeladmin/index.html" %}
{% load i18n modeladmin_tags %}

{% block header_extra %}
    <!-- Download CSV -->
    <a href="{% if request.GET|length == 0 %}?action=CSV{% else %}{{ request.get_full_path }}&amp;action=CSV{% endif %}" class="button bicolor icon icon-download">{% trans 'Download CSV' %}</a>
    {{ block.super }}
{% endblock %}
  • Now we have the custom index view and template for the event registration. You can customize them further according to your model. We need to add the following attributes to EventRegistrationAdmin class.
class EventRegistrationAdmin(ModelAdmin):
    model = EventRegistration
    index_view_class = EventRegistrationCustomIndexView
    index_template_name = 'events/event_model_admin_index.html'
    ....
    ....

That's it. You can now see a Download CSV button and can download CSVs.

listing-page-header-csv

Thanks for reading.

Update:

I got a message from a Linkedin user that he liked this article and also pointed to this stackoverflow question. The answer by Loïc Teixeira uses an alternate approach and better than the above. The difference between the two approaches is: the above approach overrides the same index view to handle CSV logic and the below approach create a new view which is called by a different URL. So, here is the approach which uses the modeladmin helper classes.

import csv
from django.http import HttpResponse
from django.utils.encoding import smart_str
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.conf.urls import url
from django.utils.translation import ugettext as _
from django.utils.decorators import method_decorator
from django.core.exceptions import PermissionDenied
from wagtail.contrib.modeladmin.views import IndexView
from wagtail.contrib.modeladmin.options import (
    ModelAdmin, modeladmin_register)
from wagtail.contrib.modeladmin.helpers import AdminURLHelper, ButtonHelper
from .models import EventRegistration


class ExportButtonHelper(ButtonHelper):
    """
    This helper constructs all the necessary attributes to create a button.

    There is a lot of boilerplate just for the classnames to be right :(
    """

    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 {}'.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,
        }


class ExportAdminURLHelper(AdminURLHelper):
    """
    This helper constructs the different urls.

    This is mostly just to overwrite the default behaviour
    which consider any action other than 'create', 'choose_parent' and 'index'
    as `object specific` and will try to add the object PK to the url
    which is not what we want for the `export` option.

    In addition, it appends the filters to the action.
    """

    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)


class ExportView(IndexView):
    """
    A Class Based View which will generate CSV
    """

    def export_csv(self):
        data = self.queryset.all()
        data_headings = [field.verbose_name for field
                         in EventRegistration._meta.get_fields()]

        # return a CSV instead
        response = HttpResponse(content_type='text/csv; charset=utf-8')
        response['Content-Disposition'] = 'attachment;filename=' + \
            'registrations.csv'

        # Prevents UnicodeEncodeError for labels with non-ansi symbols
        data_headings = [smart_str(label) for label in data_headings]

        writer = csv.writer(response)
        writer.writerow(data_headings)
        for reg in data:
            data_row = []
            data_row.extend([
                reg.title, reg.example_field2, reg.example_field3
            ])
            writer.writerow(data_row)

        return response

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


class ExportModelAdminMixin(object):
    """
    A mixin to add to your model admin which hooks the different helpers, the view and register the new urls.
    """

    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)
  • Now use the ExportModelAdminMixin in EventRegistrationAdmin class.
class EventRegistrationAdmin(ExportModelAdminMixin, ModelAdmin):
    model = EventRegistration
    index_template_name = 'events/event_model_admin_index.html'
    ....
    ....
  • Also update the template to use the button helper.
{% 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 %}
By @Parbhat Puri in
Tags : #wagtail, #django, #opensource, #web, #Programming, #code,

Comments !