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
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 theDownload 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 }}&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.
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 %}
Comments !