Wagtail, Wagtail, Smite Me a Blow

Okay, that title is a bit of an overstatement, but if you’ve ever dealt with upgrading a third-party open-source app in a Django project, you may understand the sentiment.

If you follow our blog, you will have noticed several posts about the Django CMS Wagtail. Wagtail 1.0 was recently released and adds some great features that I cannot wait to get into our sites.

One of the things we like about Wagtail is the ability to override or extend its features to suit your needs. That probably means a “wagtailoverrides” app and a directory full of files. The downside is when you do upgrade Wagtail, some of the files you overrode may have changed or been removed altogether. Since there is no time like the present, I decided now would be good time to attempt the upgrade on one of our projects and share some observations.

First off, your upgrade will be easier if YOU did all of the customizations and overrides yourself. Whether you did or not, you should review any documentation or comments you might have regarding your customizations and/or read through your wagtailoverrides directory to refresh yourself on what was done. Next, read the release docs for 1.0 and be sure not to miss the Upgrade Considerations section.

To give you a sense of the types of customization we did, here is a partial list:

  • Created a custom image model to add additional fields, some of them required.
  • Modified the drag-and-drop upload for the new image model, including automated IPTC data extraction of the new fields.
  • Created a custom page chooser for 2 content types, with paginated results.
  • Added an entirely new page in Wagtail admin, using as many of the Wagtail templates as possible.
  • Replaced the Wagtail logo and branding with our own.

For safety’s sake, instead of just starting a new git branch, I created a whole new project on its own virtual box (we use Vagrant and Ansible, so the setup is not much work at all). You could probably just create a new branch, but I wanted to keep an environment that mirrored what is running in production.

The first step was to change my requirements.txt to the latest version of Wagtail and provision my virtualbox, upgrading the Wagtail package. You can take this time to blissfully imagine that everything will run without modification. If you don’t have a wagtailoverrides app, that might just be the case. When provisioning a virtutalbox, our Ansible playbook will upgrade any packages, then move onto other tasks, including starting the app.

BOOM! First problem. In our overrides app, we import tasks from wagtailadmin. Wagtail 1.0 has moved away from using Celery to send email notifications, so tasks are gone (see issue here: https://github.com/torchbox/wagtail/issues/543). You can refer to the corresponding commits to see what you have to do to get yourself out of this one. The short answer is:

# This:
# from wagtail.wagtailadmin import tasks 
# Becomes this:
from wagtail.wagtailadmin.utils import send_notification

# and this:
# tasks.send_notification.delay()
# becomes:
send_notification()

The next hurdle was a result of our custom image model and the drag-and-drop upload. The custom model allowed us to add some custom fields to images, a couple of which are required. This created a problem with the beautifully executed drag-and-drop multiple image upload, because there is no way to enter that required data when uploading, so the validation fails. We recently added IPTC data extraction when files are uploaded that fills in those required fields. With version 0.8.7, we had to override wagtailimage.views.multiple.py. The overridden multiple.py had many changes in it for the IPTC reading, so for now I made a note that I would have to go back and re-implement what I had done, and copied the 1.0 version of muitiple.py into the override directory.

With that done I got all the way through provisioning of my virtualbox. Now I launch my browser and THERE IT IS, my newly-upgraded site. Since Wagtail leaves all of the look & feel of the front end pages to you, there really aren’t any conflicts there so the site appears to function exactly like it did prior to 1.0. Now let’s go to the admin section and see the glory of the new admin features! The snippets are there, the images are there, the pages are there, let’s click on a page to edit, and… TemplateDoesNotExist!

We overrode the template wagtailadmin/pages/edit.html to change the options at the bottom of the page where you can publish, save as draft, etc. (There was quite a lot of refactoring in the wagtailadmin/pages templates, and this refactoring reared its head several times during the upgrade. If you overrode any of the templates in wagtailadmin/pages you will want to take some time to understand how they work now.) _privacy_indicator.html became _privacy_switch.html but instead of just changing that, I copied my modification into the new edit.html template, and now I can edit pages again.

When viewing your list of pages in the admin section, as you mouse over a page title the action buttons are revealed. We overrode the template wagtailadmin/pages/list.html to add a custom button. The button was not appearing after the upgrade because list.html was refactored right out of existence. The buttons now exist in the template wagtailadmin/pages/listing/_page_title_explore.html, so I just had to re-add the button there. I also had to make the corresponding change on _parent_page_title_explore.html so the button would appear when the page is displaying as the parent, with the button at the top of the page.

Even in 1.0, Wagtail doesn’t have any way to make a Django Admin page look like a Wagtail admin page. To do so, as I did with my custom admin page, you need to create your own templates, borrowing/inheriting liberally from the existing Wagtail templates. Here I was lucky because I copied several wagtail templates to my app’s templates directory, and those that included Wagtail templates still worked.

I should point out that during the process of upgrading is a great time to add comments about customizations that may have been left out the first time around, or add detail where comments may have be lacking.

If you read the release notes you will come to this: http://docs.wagtail.io/en/v1.0b1/releases/1.0.html#edithandler-internal-api-has-changed. We created a page chooser panel that allows admins to choose pages of 2 different content types. When I attempted to edit a page that used our custom chooser, I encountered the error "type object '_PageChooserPanel' has no attribute 'bind_to_model'. Basically the panel changed from a function to an object.

# As implemented in Wagtail 0.8.7

def MultiTypePageChooserPanel(field_name, page_types=None):
    return type(str('_PageChooserPanel'), (BaseMultiTypePageChooserPanel,), {
        'field_name': field_name,
        'page_types': page_types,
    })

# As updated for Wagtail 1.0

class MultiTypePageChooserPanel(object):
    def __init__(self, field_name, page_types=None):
        self.field_name = field_name
        self.page_types = page_types
 
    def bind_to_model(self, model):
        return type(str('_PageChooserPanel'), (BaseMultiTypePageChooserPanel,), {
            'model': model,
            'field_name': self.field_name,
          'page_types': self.page_types,
        })

But our page chooser still wasn’t right. You notice the "BaseMultiTypePageChooserPanel in the code snippet above? That is a custom class that extends BaseChooserPanel. Wagtail 1.0 introduces the new concept of widgets, and each edit handler has a widget attached. In the case of BaseChooser it was only the basic field widget that was used, displaying the name of the page. Inheriting from BasePageChooserPanel was very tempting as it uses the AdminPageChooser widget that will display buttons to clear the page, select a new page, or edit the selected page (a nice addition in 1.0). The AdminPageChooser’s default behavior is to allow you to select a page of any content type, or specify a single content type. We needed to specify 2 content types, so it was time to create a custom widget. Here I chose to extend the AdminPageChooser widget, then use the new “widget_overrides” class method of BaseChooserPanel to specify my new widget.

Next I tried to edit one of our article pages, which make up the bulk of our site, and had to refer to the note about when the javascript files are loaded on the admin pages: http://docs.wagtail.io/en/v1.0b1/releases/1.0.html#javascript-includes-in-admin-backend-have-been-moved. We overrode wagtailadmin/base.html, admin_base.html, and skeleton.html. If your richtext field or tags aren’t displaying correctly, it could be that you too overrode the skeleton template. Here the js block was moved above the “furniture” block. Just moving the “javascript” block above the “furniture” block in our template fixes it.

Finally, if you are like me, and I know you are, you customized wagtailadmin/base.html to replace the Wagtail logo with either your logo or your customer’s logo. With 1.0, Wagtail has put the logo in a block, branding_logo, which you can override, or “overextend.” To do this you will need to install django-overextends. With that installed, your overextended base.html will simply be:

{% overextends "wagtailadmin/base.html" %}

{% block branding_logo %}
    MyCompany
{% endblock %}

That is a very clean way to add a custom logo.

That wraps up the functional part of the upgrade. Now all that remains are more subjective issues that I will need to work out with the customer. Things like on the edit page, help text has been relocated from just below the corresponding input field to a location to the right that only appears when mousing over the input element. Also, how to introduce the new StreamField and Blocks. But that’s another blog post.