The Python Geek

Markdown in Django

Creating blogs using markdown syntax

Markdown in Django

Created: Fri 30 November 2018

Tags: Django , Python


Introduction

Markdown is a very convenient and lightweight markup language that let's you format and mildly style text. Text written using markdown syntax is converted to HTML using a markdown tool. This blog will show you how I implemented the use of markdown in my blogs.

Blog Model

Below is the blog model called Post that I am using in my Django project. I am not going to cover every part of this as this is out of the scope of this blog. If you want more detail on Django models please refer to the django documentation. What I can say is that we have some pretty basic fields in here like title and body for example. The body is the text for the meat of the blog. In the below situation, if we were to add a blog via the admin page the result to the end user would just be the text we typed. This could be just very plain. We wouldn't have bold text, lists, code sections, links, etc.

import requests
from django.db import models
from django.utils import timezone
from django.template.defaultfilters import slugify
from django.contrib.auth.models import User
from django.urls import reverse
from taggit.managers import TaggableManager


class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status='published')

class Post(models.Model):
    STATUS_CHOICES = (
        ('draft', 'Draft'),
        ('published', 'Published'),
    )
    title = models.CharField(max_length=250)
    subtitle = models.CharField(max_length=100)
    slug = models.SlugField(max_length=250, unique_for_date='publish')
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
    body = models.TextField()
    publish = models.DateTimeField(default=timezone.now)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    objects = models.Manager()  # The default manager.
    published = PublishedManager()  # Our custom manager.
    tags = TaggableManager()

    def __str__(self):
        return self.title

    class Meta:
        ordering = ('-publish',)

    def get_absolute_url(self):
        return reverse('blog:blog_detail_url',
                       args=[self.publish.year,
                             self.publish.month,
                             self.publish.day,
                             self.slug])

    def save(self, *args, **kwargs):
        self.slug = slugify(self.title)
        super(Post, self).save(*args, **kwargs)

Using Markdown

There are probably dozens of ways to fix this and there are probably some third party libraries to use as well. I decided to fix this using the Github Markdown API. First, let's add a new field in our model called body_markdown. We will expose this to the admin page later on so that we can write markdown syntax for our blog posts using this newly created field instead of writing plain text or HTML manually in the blog field.

class Post(models.Model):
    ...
    body_markdown = models.TextField()
    ...

Now let's add a method called githubify() and also update our save method. The save method now calls the githubify method before doing a save on the instance. The githubify method uses the requests module to access the Github API to convert the markdown written in body_markdown to HTML. We return the HTML code to self.body. So we have a body_markdown with markdown text and self.body with the HTML code. Once this is done, the instance is saved. So body_markdown is for the blog developer and body is to be rendered in the template.

class Post(models.Model):
    ...   
    def save(self, *args, **kwargs):
        self.slug = slugify(self.title)
        self.body = self.githubify()
        super(Post, self).save(*args, **kwargs)

    def githubify(self):
        headers = {'Content-Type': 'text/plain'}
        r = requests.post('https://api.github.com/markdown/raw', headers=headers, data=self.body_markdown)
        return r.text

Admin Update

To exclude the body field when creating blogs in admin we just type the below below the rest of our admin registration logic. We do not want to show body as all we need is the markdown section.

from django.contrib import admin
from .models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    ...
    exclude = ['body']

Rendering

Assuming we have a simple view that has a blog post in context, we can render the generated html code using the safe filter. Below we see that we have the body attribute of post and we apply the safe filter.

    <div class="container" id="blog_body">
            <h1>{{ post.title }}</h1>
            <p>Created: {{ post.created|date:"D j F Y" }}</p>
            ...
            
          {{ post.body|safe }}
         
           ...
    </div>

Example

So let's pretend we go into our Django admin and create a new blog. Below shows that we have typed markdown into our body_markdown textbox, which we saw was added to our Post model

App Loaded

When the instance is saved, the HTML code is written to the body field via the Github API. When we render it to the page it looks like this.

App Loaded

Conculsuon

As I said earlier, there are probably dozens of ways to do this. Some are better than others and I am sure most are better than mine. But what I've done works well enough for my needs. The only problem I see is that you are at the mercy of the API. If it goes down then you will encounter an error. This is probably something I need to account for.

To learn more about writing markdown syntax please visit markdown guide.