Migrating RichTextFields to StreamField in Wagtail

Apr 3, 2019

I recently converted RichTextFields to RichTextBlocks inside StreamFields in a Django blog built with Wagtail CMS. I wanted to make this change so that I could have more complex content within blog posts. The rich text editor is a nice quick solution to get a blog up and running, but it is also limiting. You can’t have links over images, Google Maps, videos with captions, or multi-column layouts. Luckily, that’s what StreamFields were designed for. Additionally, I wanted to preserve my already published posts and be able to improve upon them. To do that, I needed to convert them all to StreamFields.

I found the Wagtail documentation to be skimpy, which turned this task into a much longer struggle than it should have been. If you’re struggling too, perhaps this tutorial will help you on your mission.

Requirements

I am using Wagtail version 2.1.3 and Django 1.11.

Models

To start, you should have a models.py that looks something like this:

from django.db import models
from wagtail.core import blocks
from wagtail.core.fields import StreamField, RichTextField
rom wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel


class BlogPost(Page):
  body = RichTextField()

  content_panels = Page.content_panels + [
          FieldPanel('body'))
          ),
      ]

Our first step is to convert the body = RichTextField() into a StreamField. We also need to change the type of panel that it uses in the Wagtail UI. And so, our models.py turns into:

from django.db import models
from wagtail.core import blocks
from wagtail.core.fields import StreamField, RichTextField
rom wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel


class BlogPost(Page):
  body = StreamField([
  ])

  content_panels = Page.content_panels + [
          StreamFieldPanel('body'))
          ),
      ]

Note: we don’t actually have to make a migration to change the type of panel. That should update when you refresh your panel. But we’ll bundle it into this commit because we can.

Make Migrations

Next we will make the migrations. In your terminal, navigate to the directory with manage.py. We’ll enter the command to create a new migration:

./manage.py makemigrations --name convert_richtextfield_to_streamfield

So then we’ll head on over to the migration file generated in your app’s /migrations folder. For me, that’s blog/migrations and it’s called 0020_convert_richtextfield_to_streamfield.py. This file should look like:

from __future__ import unicode_literals

from django.db import models, migrations
import wagtail.core.blocks
import wagtail.core.fields


class Migration(migrations.Migration):

    dependencies = [
        ('blogs', '0019_auto_20190402_1126'),
    ]

    operations = [
        migrations.AlterField(
            model_name='blogpost',
            name='body',
            field=wagtail.core.fields.StreamField([('rich_text', wagtail.core.blocks.RichTextBlock())]),
        ),
    ]

We’re going to change this up. You’ll want to add the RichText import and these additional classes. Also make sure to swap out my app’s name for yours:

// -*- coding: utf-8 -*-
// Generated by Django 1.11.13 on 2019-04-03 16:48
from __future__ import unicode_literals

from django.db import models, migrations
import wagtail.core.blocks
import wagtail.core.fields

from wagtail.core.rich_text import RichText


def convert_to_streamfield(apps, schema_editor):
    BlogPage = apps.get_model("blog", "BlogPost")
    for page in BlogPage.objects.all():
        if page.body.raw_text and not page.body:
            page.body = [('rich_text', RichText(page.body.raw_text))]
            page.save()


def convert_to_richtext(apps, schema_editor):
    BlogPage = apps.get_model("blog", "BlogPost")
    for page in BlogPage.objects.all():
        if page.body.raw_text is None:
            raw_text = ''.join([
                child.value.source for child in page.body
                if child.block_type == 'rich_text'
            ])
            page.body = raw_text
            page.save()


class Migration(migrations.Migration):

    dependencies = [
        // don't change this, Django knows what it's doing
        ('blogs', '0019_auto_20190402_1126'),
    ]

    operations = [
        // leave this AlterField in place
        migrations.AlterField(
            model_name='blogpost',
            name='body',
            field=wagtail.core.fields.StreamField([('rich_text', wagtail.core.blocks.RichTextBlock())]),
        ),
        migrations.RunPython(
            convert_to_streamfield,
            convert_to_richtext,
        ),
    ]

Note: you can also check out Wagtail’s official documentation on this topic to see where the inspiration for this code came from.

Migrate

Finally, we’re going to migrate. Back in your terminal, enter

./manage.py migrate

You’ll then want to restart the Django server. When you return to your Wagtail admin panel you’ll notice that every previously published post’s RichTextField has been converted to a StreamField RichTextBlock. Tada!

Cassidy Arden