7 Aug 2020

Upgrading from Wagtail 1.9 to 2.9

In this blog post I'll go over the steps I took to upgrade a Wagtail 1.9 and Django 1.8 website up to Wagtail 2.9 and Django 3.0

Kalob Taulien

Kalob Taulien

Wagtail Core Team Member
Upgrading-Wagtail-Cover-Image.min.png

Original photo by Kamil Feczk

A few things before we started with a pretty big upgrade path. First, the site I'm upgrading isn't very complex. There will be some edge cases that I'll have caught and address in this blog post, but that doesn't mean I'll be able to catch every edge case in every project. Second, I'm upgrading slowly — one Django and Wagtail version at a time. There's a lot of versions you can jump right over, but to create a helpful story I'll be taking the long route in hopes that this can help more people. Lastly, I'll be starting with Python 3.4 on Wagtail 1.9 and during the jump from Django 1 to Django 2 to Django 3, and Wagtail 1 to Wagtail 2 we'll be doing a lot of updates.

The best way to upgrade your Wagtail website is to go version-by-version. If you're looking at this blog post thinking "Wow, this is going to be a lot of work!" it's not that much work and most of these upgrade steps can be completed within a couple of minutes. And if you ever get stuck along the way you can read through the well-documented release notes of your version.

Let's get started!

Starting at Wagtail 1.9

At this time of writing, it's currently May 2020 and this project started with Wagtail 1.9 and Django 1.8.8. Currently, we're on Django 3.0.* and Wagtail 2.9. As you can tell, this website I'm working on is a little behind. And by a little, I mean quite far behind.

Wagtail-1.9-Dashboard.png

Upgrade to Wagtail 1.10 and Django 1.9

pip install wagtail==1.10
pip install Django==1.9.*

A couple of things came up. I strange logging error appeared. It was one I've never seen before but was common enough that a simple Google search find the solution in 12 seconds flat.

ValueError: Unable to configure handler 'null': Cannot resolve 'django.utils.log.NullHandler': No module named 'django.utils.log.NullHandler'; 'django.utils.log' is not a package

The issue was in the settings.py file. Below is what was there and what is now there.

LOGGING = {
    'version': 1,
    'handlers': {
        'null': {
            # 'class': 'django.utils.log.NullHandler',  # This is what was here. 
            'class': 'logging.NullHandler',  # This is what is it now.
        },
    },
}

After that, the runserver command was working fine. But I had to run migrations. And when python manage.py migrate was executed, this is what I saw (and could be alarming for some people):

# python manage.py migrate

    wagtailimages | filter

Any objects related to these content types by a foreign key will also
be deleted. Are you sure you want to delete these content types?
If you're unsure, answer 'no'.

Modern Wagtail no longer uses the filter in question, so I was able to safely type 'YES' and that problem is now solved. However, if you have a complex Wagtail website you may want to do some digging here. I only suggest that because I'd rather you be safe than sorry. Though you should be running this on a local database before running anything on production, so if you accidentally broke something you could always repopulate your database.

If there are migrations at this point, run python manage.py migrate.

Upgrade to Django 1.10.8

The upgrade from Django 1.9.13 to Django 1.10.8 was relatively painless. I had one small thing to tweak in the urls.py file.

It was giving me the following error:

url(r'^404/$', 'django.views.defaults.page_not_found'),
...
TypeError: view must be a callable or a list/tuple in the case of include().

What it was looking for a callable instead of a string path to a function to execute.

# urls.py 
# Before 
urlpatterns += (
    url(r'^404/$', 'django.views.defaults.page_not_found'),
    url(r'^500/$', 'django.views.defaults.server_error'),
)

# After
from django.views.defaults import page_not_found
from django.views.defaults import server_error
urlpatterns += (
    url(r'^404/$', page_not_found),
    url(r'^500/$', server_error),
)

All that was needed was to properly import the function and use said function instead of providing a URL string as a path.
If there are migrations at this point, run python manage.py migrate. It never hurts to try to run migrations — either new migrations from Wagtail and Django will be applied, or nothing will happen.

To read more about the amazing work people have done in Wagtail 1.10, read the release notes.

Wagtail 1.10 to Wagtail 1.11

Wagtail-1.10-Dashboard.png

Right, let's keep this train movin'.

Next, we need to upgrade Wagtail to v1.11.1. Whenever you see the 3rd number in a version, it's likely a minor bug fix or security patch. So instead of upgrading directly to Wagtail 1.11, we'll jump to it's latest minor release.

pip install wagtail==1.11.*

That * in the 1.11.* means the latest version. We know for sure what the latest version is going to be because we're way passed Wagtail 1.11. But for future versions, it might just be easier to tell pip to get the latest minor release without us having to look it up.

So now that you have Wagtail 1.11 installed, you'll either see an error or some type in your terminal or a bunch of red text letting you know you have database migrations.

If you're getting complaints in your terminal saying something along these lines:

File "/usr/local/lib/python3.4/site-packages/admin_honeypot/urls.py", line 7, in <module>
    from django.conf.urls.defaults import patterns, include, url
ImportError: No module named 'django.conf.urls.defaults' # <- Most this line

Then you're likely running into an issue where Django starts to upgrade how it works with URLs. This is a common issue we see when upgrading Django projects. And in this case, I had django-admin-honeypot installed. All I did was upgrade the package to a newer version and it worked for me.

"One thing to keep in mind when upgrading 3rd party packages throughout this article: we're still using Django 1.11 so upgrading to the latest version of a package may end up removing support for Django 1. I try to upgrade a couple version numbers at a time to get this to work."

If you didn't see any errors, or you've fixed any problems that have sprung up, you should see red text in your terminal saying you have 6 new migrations. It's strange that red text can be so comforting to see. (You'll 100% relate to this after we upgrade to Django 2 and Wagtail 2 😉). Let's run those migrations, and maybe take a look at the release notes.

python manage.py migrate

Now you can run your localhost server and use Wagtail 1.11. But while we're at it let's upgrade Django to its last version before moving into Django 2. The BIG visual feature that came with Wagtail 1.11 was a new page explorer.

Wagtail-1-11-1-Explorer.png

Upgrading to Django 1.11.29

This one is a piece of cake! We'll see plenty more updates that run smoothly as well.

pip install Django==1.11.*

Run your server. Apply migrations (if there are any for you). And we should now successfully be running the latest version of Django 1 and Wagtail 1.11

Also, I like to read over the release notes for each version of Wagtail. It'll help us detect if there are going to be any breaking changes, and give us information on the latest improvements. Here are the 1.11 release notes. It's also good to read the release notes for Django as well, albeit in the Wagtail word it's generally not considered required reading.

Wagtail 1.11 to Wagtail 1.12

Wagtail-1-11-1-Dashboard.png

Wagtail 1.12 has 6 minor versions. I know that by referring to the release note docs. But double-checking every single thing every time we do a thing gets a bit exhausting. From now on we'll skip straight to installing the latest minor version.

pip install wagtail==1.12.*

One migration to apply. Easy!

python manage.py migrate

We’re now at the Wagtail LTS (long term support) version which mean this was supported for 8 months. That was quite a while ago, so we're long passed that date.

And because in the previous set we upgraded to the latest version of Django 1 (Django 1.11.29), we don’t need to work with Django 2 until Wagtail 2 is installed. The nice folks working on the big Wagtail 1 to Wagtail 2 upgrade at to be thanked for their foresight here because they released Wagtail 2 when Django 2 came out which makes this upgrade easy to remember (thank you!)

Got a hankerin' for release notes? Probably not, but here they are just in case you want to take a look at all the tremendous updates.

Wagtail 1.12 to Wagtail 1.13 (The last Wagtail 1 Upgrade)

Wagtail-1-12-6-Dashboard.png

This is our last upgrade before moving into the big beautiful world of Wagtail 2 (and Django 2). Let's install the latest version of Wagtail 1.13 and deal with anything that comes up!

pip install wagtail==1.13.*
python manage.py runserver 0.0.0.0:8000

Huh. No migrations. No errors (at least not from the packages I'm using). Nice and easy! We'll see more upgrades like this later, too. Thankfully Wagtail contributors and the other Wagtail Core Members try very hard to make sure these updates are as smooth as possible for you. Personally, as someone who fears updating his MacBook because it'll break my programs.. I literally could not be a happier person when I upgrade Wagtail sites.

Wagtail-1-13-4-Dashboard.png

☝️If you can't see that, there's a little yellow notification bar saying we're using Wagtail 1.13.4 and that Wagtail 2.9 is available. At this point, we're as far as we can go without entering the world of Django 2 and Wagtail 2 🎉

Wagtail 2.0 and Django 2.0

When upgrading to Django 2 and Wagtail 2, I find it's best to upgrade them at the same time. They were purposely released around the same time, so in my mind, they should be upgraded together. But first... if you have the ability now is the time to upgrade to Python 3.6 if you're using an older version. If you're still using Python 3.4, feel free to skip passed Python 3.5 and head directly to Python 3.6. By skipping Python 3.5 you're saving yourself the pain of updating your environment in production two times.

This is a BIG upgrade

Just a warning that upgrading to Django 2.0 and Wagtail 2.0 is a very big update. It can become quite involved and become a lot of work. If you're tight on time right now I would suggest postponing this update until you have more time. This update isn't hard, it's just big, and big means time-consuming. Naturally, if you have a smaller website it'll be faster for you, but if you have a large application this could take some time. And we'll be changing quite a few files — so if you're using some version control software, you'll end up with a commit that could be between 10 and 100 files changed. It's mostly small updates, thankfully, and doesn't involve refactoring entire chunks of our codebase.

There are also some pretty big changes that come with 2.0 such as Wagtail's new richtext editor: Draftail and over 35 other features were added or changed and a plethora of bug fixes.

Let's get started.

Installing the new versions

First, we need to install Wagtail 2.0 and Django 2.0. As always, we'll skip right to the latest minor versions.

pip install wagtail==2.0.*
pip install Django==2.0.*
python manage.py runserver 0.0.0.0:8000

Django and Wagtail should have installed perfectly fine. But as soon as you ran your localhost server, you'll see a ModuleNotFoundError.

ModuleNotFoundError: No module named 'wagtail.wagtailcore'

What changed here? Pathing. Wagtail used this major version change as an opportunity to refactor where a lot of code lives. So instead of "wagtail.wagtailcore" we'll now have "wagtail.core". Read better and it's less typing. It's got my vote!

But you might be thinking, "How do I know which paths to update and to where?"

The first time I upgraded a Wagtail site to 2.0 I did this the long way.. I went through the single file and updated every import by hand. The second time, I did a find and replace for the different paths (because I wrote docs for myself the first time).

And then one day I was Googling Wagtail stuff (Because that's what I do for fun. Yes, you can judge me 🤣) and noticed this little command called updatemodulepaths. It's actually built into Wagtail, so it's a Wagtail command. Try this:

wagtail updatemodulepaths

On the website I was upgrading I saw: Checked 105 .py files, 46 files updated.

105 .py files... not a huge site by any means.
46 files updated... that was 46 files I didn't have to manually open, fix import paths, save and close.

If this command saved you a tonne of time made your life easier, you have Matt Westcott to thank! 👏

On Delete

If you run your localhost server again, you'll be greeted with a less-friendly problem.

TypeError: __init__() missing 1 required positional argument: 'on_delete'

My first error was from a dunder init method. Yours might be from somewhere else. Regardless, it'll complain about this 'on_delete' argument. And it will complain hard.

Here’s the painful part... We now need to open every file and look for a ForeignKey field and determine how the on_delete argument should work. Luckily, in most cases, we can assume the same behaviour from prior Django versions: CASCADE. CASCADE was the default prior to Django 2.0, and we can likely use it everywhere it asks for an on_delete argument. But don't take my word on that.. you know your application best, you should be the deciding factor on how an on_delete argument works, not some guy in an article.

According to the nice little nice in the Django docs here it's been deprecated since Django 1.9 and defaults to CASCADE. So if you want your models to all work the exact same as they did in Django 1, you can set on_delete=models.CASCADE. Here's an example:

class YourModel(models.Model):
    image = models.ForeignKey(
        "Image",
        related_name='+',
        blank=True,
        null=True,
    )

👆The problem with that is the missing on_delete argument. Below is what it should look like 👇

class YourModel(models.Model):
    image = models.ForeignKey(
        "Image",
        related_name='+',
        blank=True,
        null=True,
        on_delete=models.CASCADE,  # <- This. This is what Django is saying you need. CASCADE, SET_NULL, etc. You choose what's best for your application.
    )

What I do to get through this is keep Django's runserver open and fix them as they appear. Django will complain about a specific ForeignKey in a specific file on a specific line. Read what the terminal outputs for you, and it'll guide you to each one, one by one until they are all resolved.

That might take a few minutes, so feel free to pause here for a little bit and power through those changes. Once you see a different type of error, continue on.

URLs

Earlier I had mentioned URLs being updated in Django. They continue to do this. Honestly, I don't know why I've never looked into the reasons. And the problem is never painful enough for me to want to look into it. I just know Django has a new way to work with URLs and we need to play by this frameworks' rules.

You might see an error similar to this:

File "/usr/src/app/your_site/your_site/urls.py", line 23, in <module>
    url(r'^admin/‘, include(admin.site.urls)),
File "/usr/local/lib/python3.6/site-packages/django/urls/conf.py", line 27, in include
    'provide the namespace argument to include() instead.' % len(arg)
django.core.exceptions.ImproperlyConfigured: Passing a 3-tuple to include() is not supported. Pass a 2-tuple containing the list of patterns and app_name, and provide the namespace argument to include() instead.

Not sure where to start? In your terminal error it actually tells us. It’s in your_site/urls.py on line 23. It doesn't like the way we're using the include() function anymore.

The fix here is pretty simple. But if Im being honest, I Google this one every time it comes up because I can’t seem to commit this to my memory. Probably just get buried with the previous pain points from above and this problem only seems to come up when upgrading websites, which isn't an everyday task.

If you have:

url(r'^admin/', include(admin.site.urls)),

Make it look like:

url(r'^admin/', admin.site.urls),

Problem solved!

Now for the fun part! And by fun, I mean really messy and sometimes scary part. Working with migration files.

On Delete (Migration Edition) 😱

Ok.. so here's the thing... we can't get around this very easily. Luckily, we only have to go through this once. And there are two ways to do this.

  1. If possible, it’s going to be easier to delete all your migration files and re-create them. That means getting your hands dirty with a database backup (standard operation, you should have backups anyway) or migrating your site to a new database schema. Personally I like this option as it simplifies and reduces historical migrations and speeds them up in the future, but for people who don’t have access to Postgres, MariaDB or whichever database you’re using in production that might not be an option.
    We won't be taking this option, we'll be taking the painful route.
  2. The second option is to “patch” your migration files, one by one. This isn’t hard, its just... erm.. boring. But it needs to get done, so let’s do it!

Ok. Big breath. Ready? Let's get this bread!

File "/usr/src/app/your_website/pages/migrations/0003_auto_20151126_1457.py", line 8, in <module>
    class Migration(migrations.Migration):
File "/usr/src/app/your_website/pages/migrations/0003_auto_20151126_1457.py", line 74, in Migration
    field=models.ForeignKey(related_name='+', to='pages.OtherPage'),
TypeError: __init__() missing 1 required positional argument: 'on_delete'

Now we’re dealing with migrations. Maybe it’s just me, but I really dislike dealing with historical migrations. On the bright side.. it’s asking for an on_delete parameter for a foreign key. We’ve dealt with those, we know how to deal with those, now we just need to deal with them one last time.

You will likely see an error like this. Actually, you'll likely see this one (in our application migrations) and eventually, you'll see another one that refers to Wagtail's migration files.

To be honest, I oversold the painful parts of this, it's really just the same step we did with the original on_delete work, but now we're mimicking this in our migration files. If you have 872 migration files this could take a while (I've actually seen that before!)

"Remember the default value for on_delete in a ForeignKey is is `models.CASCADE`"

Likewise in your migration files if you see any models.OneToOneField’s you can use on_delete=django.db.models.deletion.CASCADE (or on_delete=models.CASCADE if you have imported `models` directly). OneToOneFields are very similar to a standard ForeignKey.

Remember: it's your application. Don't just randomly trust this guy speaking to you over the internet. I don't know your code or your business logic. Perhaps CASCADE isn't the right action.

This might take you a little while. Feel free to take your time through this as it's important to get it right, but it's also very boring work.

You've got m..igrations!

If you see that beautifully alarming red text in your terminal saying: You have 1 unapplied migration(s). Congrats! You've gotten through the hardest part of the entire upgrade path. From here on out it's going to be pretty easy.

python manage.py migrate
python manage.py runserver 0.0.0.0:8000
...
May 21, 2020 - 16:39:17
Django version 2.0.13, using settings 'your_site.settings.dev'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.

Above we ran the migration, started Django, and it's giving us signs of hope!

Now if you start up your website in your browser you should see your website!

Django-2-WSGI-Heart-Breaking-To-The-Max.png

Ok, that was kind of mean to get your hope up like that. But we're super close to being done with the massive 2.0 upgrade. The problem above is easy to fix. And from here on out the issues tend to be pretty easy! This particular issue is likely caused by MIDDLEWARE_CLASSES being renamed to MIDDLEWARE.

If you do a quick find-and-replace you'll solve this problem. Find: MIDDLEWARE_CLASSES and replace it with just MIDDLEWARE.

Next, you might see one of two problems.. or maybe no problems and you're done with the 2.0 upgrade!

(Optional) Handling Sentry in 2.0

If you see this scary error: ValueError: Unable to configure handler 'sentry' then you're likely using Sentry and Raven. The solution here is just a package update. Try this: pip install raven==6.4.0. That tends to solve this problem for me and hopefully, it'll solve the problem for you too.

But of course, if you're not using Sentry just skip this part... is what I should have said about 3 sentences earlier!

(Optional but likely) Handling Template Tags in 2.0

If you see an error that says, 'Library' object has no attribute 'assignment_tag', then you'll need to update your template tags. Otherwise, if you don't see this error feel free to skip this part! (Yes, I got the timing right this time!)

In Django 1.9 the assignment_tag became deprecated. We could have dealt with it then, but apparently I like making a single version upgrade as hard as possible and put it off until 2.0. This is what procrastination does, folks, it causes a lot of heartbreak. If you want to read more about the Django 1.9 assignment tag the Django docs are well written and informative.

The simplest fix here is to do a global find and replace for @register.assignment_tag() and replace it with @register.simple_tag(). You’ll likely notice these are all in the templatetags/ directory, too. But if you see this in a 3rd party package.. you likely need to upgrade that package.

But since Django 2.0 is a bit old as of May 2020, you might not want to upgrade to the latest version of EVERY package as there are older features that won’t be supported in newer releases. of all packages. I usually look for a CHANGELOG (which Wagtail beautiful provides), README notes saying which versions are supporter, GitHub issues, and PyPi package versions. But with some packages, you might have to guess the best version to use because there are no other indicators for Wagtail 2.0 or Django 2.0 support.

Alright.. once your done touching up template tags, let's carry on.

Final 2.0 steps

If you like to pin your package dependencies now might be the time to unpin Pillow and Django Rest Framework (pillow and djangorestframework). Why? Because Wagtail will guide us with its own dependencies.

But sometimes I make rules like that and then immediately break them 🙊 which is what I'm about to do with djangorestframework (but you don't have to do this part). For me, because I tend to use Django Rest Framework quite a bit, I like to know which version I have and I don't want to leave my text editor if I don't have to. So I tend to pin DRF to 3.7.* at this point with djangorestframework==3.7.*. Or just keep djangorestframework unpinned and take the easier route.

Lastly, make sure you have wagtail and Django pinned in your requirement files to 2.0.*.

The Wagtail 2.0 release notes can be found here: https://docs.wagtail.io/en/stable/releases/2.0.html

Wagtail-2-0-Dashboard.png

The REAL last step for 2.0

Just kidding, there's nothing left for the 2.0 upgrade. Your site should be up and running now! If it's not working it's likely something we covered earlier or a third party package that needs an upgrade (or possibly a downgrade if it was over-upgraded).

Now that we're done with the big 2.0 upgrade, you can take a big deep breath and get yourself a treat — you earned it! Maybe have a relaxing bath, find some zen, and let the stresses of the 2.0 upgrade fade away.

All jokes aside, I always take a well-deserved break here. And you should too! It's important to reward yourself for good work. You have powered through the hardest part of this entire article. When you're ready, in your own time, let's get started on Wagtail 2.1.

Wagtail 2.1, 2.2 and 2.3

Now that we're in the clear we can start upgrading Wagtail pretty quickly! In this section, we'll jump 3 versions in total, but we'll still take this one step at a time.

I also won't keep spamming you with links to the release notes though I highly recommend reading through them after each version upgrade. If anything, you'll get to see all the cool new features people have been adding for quite a while. Click here for all the release notes in one place.

Hello, 2.1

After the big 2.0 upgrade, any upgrade could seem easy. But in truth this is just a standard upgrade at this point. For the time being we'll continue to use Django 2.0, as well.

pip install wagtail==2.1.* 
python manage.py migrate

Done! Isn't that the most beautiful thing you've ever seen? Brings a giant tear of happiness to my eye.

2 can be as easy as 1 (Wagtail 2.2)

Just as we did with Wagtail 2.1 in the last step, we'll do the same here.

pip install wagtail==2.2.* 
python manage.py migrate

Done.. again! We'll stick with Django 2.0 again for the time being. We'll give Django a version bump in the next step.

2.3 - Wagtail LTS

If you're like me you might think of upgrade paths as travelling across long distances, like the stars, or between two far away cities. And LTS, Long Term Support, is our safe spot to take a break. Here we'll also upgrade to Django 2.1, which is the second last version before moving to Django 3.0.

pip install wagtail==2.3.* 
pip install Django==2.1.*
python manage.py migrate

With this upgrade, Wagtail doesn't have any migrations. But Django does; one migration is included. And if you take a look at your Wagtail dashboard you'll see some nice visual improvements, too. In Wagtail 2.3 we have bolder colours that are easier on the eyes. It's also WCAG 2 level AA compliant which sounds like boring compliance but is actually very important. Plus, your eyes will thank you for it.

Wagtail-2-3-Dashboard.png

Wagtail 2.4 welcomes Python 3.7

With this version bump, we get to upgrade Python. If you're using some virtualization system like Docker, it's as easy as changing 3.6 to 3.7. But that's for another day because I don't know which virtual environment you're using (ie. venv, virtualenv, Docker, Vagrant, pipenv, etc).

First, we'll upgrade Wagtail and apply the migrations.

pip install wagtail==2.4 
python manage.py migrate

You should have seen 6 migrations from Wagtail being applied. We're not going to upgrade Django again... not yet, at least.

And if you haven't, you can safely upgrade your Python version to 3.7 (I'm now using Python 3.7.7 for this upgrade path).

wagtail-2-4-dashboard.png

Wagtail 2.4 to 2.5

From here on out it's almost the exact same steps, one at a time, until we get to Django 3.0. And the Django 3.0 upgrade is not nearly as tough as the Django 2.0 upgrade was.

pip install wagtail==2.5.2
python manage.py migrate

With the Wagtail 2.5 upgrade comes the capability to support Django 2.2, which is Django 2's last major release prior to upgrading to Django 3.0.

pip install Django==2.2
python manage.py migrate

Nice and easy! The Wagtail 2.6 and 2.7 will be even faster. In fact you could jump straight to 2.7 and skip 2.6. But I said we'd upgrade our Wagtail site, one-by-one, so that's what we'll do.

Wagtail-2-5-2-Dashboard.png

Wagtail 2.5 to 2.6

pip install wagtail==2.6.3

Done! Next?

Wagtail-2-6-3-Dashboard.png

Wagtail 2.7 comes with LTS

Wagtail 2.7 comes with long term support, or LTS for short. That means it was given one year of full support before considering it to be out of date.

pip install wagtail==2.7.3 
python manage.py migrate

Up until now, I haven't mentioned too many big improvements in Wagtail - that's not to say there aren't any. In fact, there are so many I can't list them all on this page. I can only redirect you to each release note document.

But one thing that came with Wagtail 2.7 that was a big game-changer, at least for me, was the ability to support .webp images. .webp images are highly minified lightweight images with hardly any image quality loss. That means you can serve the same images (on Chrome that supports .webp, that is) but use less bandwidth. That also means faster load times, and happier users. Images are a huge slowdown for a lot of websites, and this is a huge and quick win for most websites.

Once again, that's not to downplay the importance of all the other work which I wish I could talk about - this was just a big one for me and the sites I've worked on.

Lastly, let's go ahead and upgrade Django to its final version of Django 2.2.

pip install Django==2.2.*  # 2.2.13 is the latest version as of writing this article
Wagtail-2-7-3-Dashboard.png

Wagtail 2.8 and Django 3.0

If you've gone through this entire article, you remember the agony we went through upgrading to Django 2.0 from 1.11. Well, I have news for you...

This is not that!

The Django 3.0 upgrade does have some caveats, such as certain modules being removed. It also means that some packages simply won't support Django 3.0 yet as it's still very new.

First, we'll upgrade Wagtail, then Django.

pip install wagtail==2.8.*
python manage.py migrate  # Should be 5 migrations, it's a rather large upgrade

pip install Django==3.0

Here's where the fun begins. And by fun, I mean it might be a little more work than we're used to.

Much like the Django 1.11 to 2.0 upgrade, I can't possibly cover every scenario that will come up in your project, but I'll provide some examples that came up in mine and how I dealt with them.

ModuleNotFoundError: No module named 'django.utils.six'

This module was removed in Django 3.0. Along with several others. For more details on which modules were removed take a look at the Django 3.0 release notes - but don't forget to come back here to finish off your upgrade. Simply open your code and remove references to django.utils.six.

ModuleNotFoundError: No module named 'django.contrib.admin.templatetags.admin_static' 

django-suit is a popular package with about 2,000 stars on GitHub. It's likely someone will run into this issue or a similar issue.

Originally I was using version 0.2.28 so I tried to upgrade to a newer version, but there were only 0.3 alpha versions out. I tried 0.3a1 and 0.3a3 but nothing worked. So at the time of writing this, I had to remove it from the project. Though by the time you're reading this there may have been a proper release that supports Django 3.0.

With new big released of frameworks such as Django 3, you'll likely run into issues like this. And because v3.0 is relatively new still, it's unlikely every Django or Wagtail package will be able to support Django 3. So just keep this in mind as you try to upgrade to 3.0.

If the Django 3.0 upgrade is too painful and you can't use the packages you absolutely need to feel free to skip this part. We can continue to use Wagtail 2.8 and 2.9 on Django 2.2.

  File "/usr/local/lib/python3.7/site-packages/wagtail/documents/api/v2/views.py", line 1, in <module>
    from wagtail.api.v2.filters import FieldsFilter, OrderingFilter, SearchFilter
  File "/usr/local/lib/python3.7/site-packages/wagtail/api/v2/filters.py", line 3, in <module>
    from rest_framework.filters import BaseFilterBackend
  File "/usr/local/lib/python3.7/site-packages/rest_framework/filters.py", line 15, in <module>
    from django.utils import six
  ImportError: cannot import name 'six' from 'django.utils' (/usr/local/lib/python3.7/site-packages/django/utils/__init__.py)

Make sure you read through the stack traces too. The one above says it cannot import six from django.util. We know why already, but why is it complaining about six again?

Well if you read the 3rd line from the bottom, you'll see this is coming from Django Rest Framework.

Now I happen to know Django Rest Framework, or DRF for short, is a very well maintained package and it's extremely unlikely it wouldn't have immediate Django 3.0 support. So my fix was to run an upgrade to the latest version.

pip install djangorestframework --upgrade

You might encounter more issues. If you have a large project, you'll likely be using more third party packages. You'll have to tackle each one on a case-by-case basis. But if you do do it one at a time (and remember to upgrade your requirements.txt, Pipfile, etc) it's not so bad.

Wagtail-2-8-2-Dashboard-Django-3.png

Wagtail 2.9

Well.. we're here! We're at the last step. And this upgrade is an easy one, too, just like all the Wagtail version updates.

pip install wagtail==2.9.2
python manage.py migrate

pip install Django==3.0.8  # Latest version as of writing

Above we updated Wagtail to the latest point release of 2.9, ran migrations to keep our database updated, and then upgraded Django to it's latest point release (as of writing; you may want a newer version if it's available.)

Wagtail-2-9-1-Dashboard.png

Notice something different about that last image? No yellow bar telling us there's a newer version out there!

And as of Wagtail 2.7, you can use Python 3.8. So if you had a great time following this article you can keep up the great upgrade work by upgrading to Python 3.8, Django 3.1, Wagtail 2.10, and so on.

Just make sure you quickly read through the Wagtail release notes.

For more support...

I covered a lot of ground in this article. You may have gotten confused at some point and that's OK if you did! That's why we have a dedicated Slack channel.

Come join us in the #support channel on the Wagtail CMS Slack at https://wagtail.io/slack