Modifying my blog to support StreamFields in blog posts

July 19, 2018, 11:29 p.m.   wschaub  


I wanted to write some articles about configuring Jenkins but I quickly found out that I was quite limited with the blog software out of the box. I wanted to be able to post inline screenshots, source code and other things and that really wasn't easy to do with just a plain markdown body on the PostPage.

That's a bummer but everything I'm doing on this domain is a learning exercise anyway so let's get cracking on modifying it to my purposes. After a lot of head scratching I came up with the following patch that adds a MarkDownBlock class to the blog.blocks module and does some minor(possibly incorrect) modifications to the templates and then we modify the models for LandingPage and PostPage.

Finally at the very end I had to figure out how to manage migrations so that my current blog posts did not get wiped out. First time I've written a custom migration in Django (with lots of help from the Wagtail docs)

This post is implemented with the new changes. The text at the top is in RichText and the diff is our new MarkDownBlock added to the stream. If you're interested in playing with my changes yourself you can find the unadulterated version of this blog software at https://github.com/michael-yin/wagtail-bootstrap-blog and an article on integrating it into an existing Django project at https://wagtail.io/blog/add-wagtail-blog-to-django-app/

diff --git a/longearsforlife/blog/blocks.py b/longearsforlife/blog/blocks.py
index 959d44a..f1d563a 100644
--- a/longearsforlife/blog/blocks.py
+++ b/longearsforlife/blog/blocks.py
@@ -4,7 +4,7 @@
 #
 # Copyright © 2017-12-23 michael_yin
 #
-
+from django.utils.safestring import mark_safe
 from wagtail.core.fields import StreamField
 from wagtail.core import blocks

@@ -14,6 +14,8 @@ from wagtail.embeds.blocks import EmbedBlock
 from wagtail.admin.edit_handlers import FieldPanel, FieldRowPanel,MultiFieldPanel, \
     InlinePanel, PageChooserPanel, StreamFieldPanel

+from wagtailmd.templatetags.wagtailmd import markdown_filter
+
 class ColumnBlock(blocks.StreamBlock):
     heading = blocks.CharBlock(classname="full title")
     paragraph = blocks.RichTextBlock()
@@ -32,3 +34,10 @@ class TwoColumnBlock(blocks.StructBlock):
         icon = 'placeholder'
         label = 'Two Columns'

+class MarkDownBlock(blocks.TextBlock):
+    def render_basic(self,value, context=None):
+        if value:
+            return mark_safe(markdown_filter(value))
+        else:
+            return ''
+
diff --git a/longearsforlife/blog/migrations/0003_auto_20180719_1551.py b/longearsforlife/blog/migrations/0003_auto_20180719_1551.py
new file mode 100644
index 0000000..e927737
--- /dev/null
+++ b/longearsforlife/blog/migrations/0003_auto_20180719_1551.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.14 on 2018-07-19 15:51
+from __future__ import unicode_literals
+
+import blog.blocks
+from django.db import migrations
+import wagtail.core.blocks
+import wagtail.core.fields
+import wagtail.embeds.blocks
+import wagtail.images.blocks
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('blog', '0002_auto_20180614_0223'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='landingpage',
+            name='body',
+            field=wagtail.core.fields.StreamField([('heading', wagtail.core.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.core.blocks.RichTextBlock()), ('markdown', blog.blocks.MarkDownBlock()), ('image', wagtail.images.blocks.ImageChooserBlock(icon='image')), ('two_columns', wagtail.core.blocks.StructBlock([('left_column', wagtail.core.blocks.StreamBlock([('heading', wagtail.core.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())], icon='arrow-right', label='Left column content')), ('right_column', wagtail.core.blocks.StreamBlock([('heading', wagtail.core.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())], icon='arrow-right', label='Right column content'))])), ('embedded_video', wagtail.embeds.blocks.EmbedBlock(icon='media'))], blank=True, null=True),
+        ),
+    ]
diff --git a/longearsforlife/blog/migrations/0004_change_postpage_body_to_streamfield.py b/longearsforlife/blog/migrations/0004_change_postpage_body_to_streamfield.py
new file mode 100644
index 0000000..0a92614
--- /dev/null
+++ b/longearsforlife/blog/migrations/0004_change_postpage_body_to_streamfield.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.14 on 2018-07-19 17:59
+from __future__ import unicode_literals
+
+import blog.blocks
+from django.db import migrations
+import wagtail.core.blocks
+import wagtail.core.fields
+import wagtail.embeds.blocks
+import wagtail.images.blocks
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('blog', '0003_auto_20180719_1551'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='postpage',
+            name='body',
+            field=wagtail.core.fields.StreamField([('heading', wagtail.core.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.core.blocks.RichTextBlock()), ('markdown', blog.blocks.MarkDownBlock()), ('image', wagtail.images.blocks.ImageChooserBlock(icon='image')), ('two_columns', wagtail.core.blocks.StructBlock([('left_column', wagtail.core.blocks.StreamBlock([('heading', wagtail.core.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())], icon='arrow-right', label='Left column content')), ('right_column', wagtail.core.blocks.StreamBlock([('heading', wagtail.core.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())], icon='arrow-right', label='Right column content'))])), ('embedded_video', wagtail.embeds.blocks.EmbedBlock(icon='media'))], blank=True, null=True),
+        ),
+    ]
diff --git a/longearsforlife/blog/migrations/0005_migrate_markdown_to_streamfield.py b/longearsforlife/blog/migrations/0005_migrate_markdown_to_streamfield.py
new file mode 100644
index 0000000..7d2c13c
--- /dev/null
+++ b/longearsforlife/blog/migrations/0005_migrate_markdown_to_streamfield.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.14 on 2018-07-19 17:59
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+def convert_to_streamfield(apps, schema_editor):
+    PostPage = apps.get_model("blog", "PostPage")
+    for page in PostPage.objects.all():
+        if page.body.raw_text and not page.body:
+            page.body = [('markdown', page.body.raw_text)]
+            page.save()
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('blog', '0004_change_postpage_body_to_streamfield'),
+    ]
+
+    operations = [
+        migrations.RunPython(convert_to_streamfield)
+    ]
diff --git a/longearsforlife/blog/models.py b/longearsforlife/blog/models.py
index 5f7fd47..c23adfe 100644
--- a/longearsforlife/blog/models.py
+++ b/longearsforlife/blog/models.py
@@ -40,7 +40,7 @@ from wagtail.contrib.routable_page.models import RoutablePageMixin, route

 from wagtailmd.utils import MarkdownField, MarkdownPanel

-from blog.blocks import TwoColumnBlock
+from blog.blocks import TwoColumnBlock, MarkDownBlock

 class BlogPage(RoutablePageMixin, Page):
     description = models.CharField(max_length=255, blank=True,)
@@ -113,7 +113,15 @@ class BlogPage(RoutablePageMixin, Page):
         return Page.serve(self, request, *args, **kwargs)

 class PostPage(Page):
-    body = MarkdownField()
+    body = StreamField([
+        ('heading', blocks.CharBlock(classname="full title")),
+        ('paragraph', blocks.RichTextBlock()),
+        ('markdown', MarkDownBlock()),
+        ('image', ImageChooserBlock(icon="image")),
+        ('two_columns', TwoColumnBlock()),
+        ('embedded_video', EmbedBlock(icon="media")),
+    ],null=True,blank=True)
+
     date = models.DateTimeField(verbose_name="Post date", default=datetime.datetime.today)
     excerpt = MarkdownField(
         verbose_name='excerpt', blank=True,
@@ -131,7 +139,7 @@ class PostPage(Page):

     content_panels = Page.content_panels + [
         ImageChooserPanel('header_image'),
-        MarkdownPanel("body"),
+        StreamFieldPanel("body"),
         MarkdownPanel("excerpt"),
         FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
         FieldPanel('tags'),
@@ -155,6 +163,7 @@ class LandingPage(Page):
     body = StreamField([
         ('heading', blocks.CharBlock(classname="full title")),
         ('paragraph', blocks.RichTextBlock()),
+        ('markdown', MarkDownBlock()),
         ('image', ImageChooserBlock(icon="image")),
         ('two_columns', TwoColumnBlock()),
         ('embedded_video', EmbedBlock(icon="media")),
diff --git a/longearsforlife/blog/templates/blog/blog_page.html b/longearsforlife/blog/templates/blog/blog_page.html
index ace5a97..f79c48a 100644
--- a/longearsforlife/blog/templates/blog/blog_page.html
+++ b/longearsforlife/blog/templates/blog/blog_page.html
@@ -30,7 +30,7 @@
             {% if post.excerpt %}
                 {{ post.excerpt|markdown|safe }}
             {% else %}
-                {{ post.body|markdown|safe|truncatewords_html:70 }}
+                {{ post.body|safe|truncatewords_html:70 }}
             {% endif %}
             <hr>
         </p>
diff --git a/longearsforlife/blog/templates/blog/post_page.html b/longearsforlife/blog/templates/blog/post_page.html
index a618208..1169a9d 100644
--- a/longearsforlife/blog/templates/blog/post_page.html
+++ b/longearsforlife/blog/templates/blog/post_page.html
@@ -19,7 +19,7 @@
         {% post_categories %}
     </p>
     <hr>
-    {{ post.body|markdown|safe }}
+    {{ post.body|safe }}
     <hr>
     {% post_tags_list %}

bootstrap-blog wagtail