How to Use Django-Markdownx for Your Blog


This is my first crack at using Django-Markdownx, which is  a comprehensive Markdown plugin built for Django that has raw editing and a live preview inside of the Django admin. I looked into many other ways to implement markdown (django-markdown, django-markdown2, django-markdown-duex), but the ability to drag & drop image uploads won me over. Implementing it is very straight forward, so let's begin there.

Most of this comes straight from the GitHub instructions at Django Markdownx, so take a look if you need more detail. First, install django-markdownx with pip install django-markdownx. Next, add markdownx to your INSTALLED_APPS in settings.py

INSTALLED_APPS = [
...
'markdownx',
...
]

And add markdownx URL patterns to your urls.py:

urlpatterns = [
...
path('markdownx/', include('markdownx.urls')),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)


MarkdownX has a two static files it uses for the ability to preview the markdown, markdownx.css and markdownx.js, so we need to run collectstatic to collect MarkdownX assets to STATIC_ROOT with python manage.py collectstatic. Now we can start using MarkdownX in our models. In my example, I used the MarkdownxField for the body of my blog post:

class BlogPost(DateCreateModMixin):
title = models.CharField(max_length=100)
background_image = models.ImageField(default='img/header.jpg', upload_to=datetime.now()
.strftime('backgrounds/%Y/%m/%d'))
tag = models.ManyToManyField(Tag, blank=True)
body = MarkdownxField()
subject = models.ForeignKey(Subject, null=True, blank=True, on_delete=models.SET_NULL)
published = models.BooleanField(default=True)

Note: background_image is just for my header background, not MarkdownX

Where DateCreateModMixin is just a mixin I use for any model where I want to keep track of the created and modified date-times:

class DateCreateModMixin(models.Model):
class Meta:
abstract = True

created_date = models.DateTimeField(default=timezone.now)
mod_date = models.DateTimeField(blank=True, null=True)

Now that we have our model, we can add it to admin:

@admin.register(BlogPost)
class BlogPostAdmin(MarkdownxModelAdmin):
list_display = ('title', 'created_date', 'mod_date', 'published', )
list_editable = ['published', ]
list_filter = ('created_date', 'mod_date')
search_fields = ('title',)
advanced_filter_fields = (
'title', 'created_date', 'mod_date')
filter_horizontal = ('tag',)
fields = ('title', 'background_image', 'subject', 'tag', 'body', 'created_date', 'mod_date', 'published', )
readonly_fields = ('created_date', 'mod_date', )

I'm doing several things here if you aren't that familiar with Django:

@admin.register(BlogPost) adds your model to the admin panel. 

class BlogPostAdmin(MarkdownxModelAdmin) allows you to edit what is displayed when you are viewing the entries in admin and it inherits from MarkdownxModelAdmin so we can see the live preview. 

The rest of it isn't MarkdownX specific, but let's go over it anyway (I know I hate see extra unexplained code in examples):

list_display = ('title', 'created_date', 'mod_date') shows the Title, Created Date, and Modified Date in the list of Blog Posts. 

list_filter = ('created_date', 'mod_date') allows you to filter by the fields specified.

Finally, search_fields = ('title',) allows you to search the specified field.  

The admin list of blog posts now looks like this:

And the blog entry page looks like this:

Since I'm trying to do this in admin, the live preview is below the entry area, so it's not as easy as I'd like. I will be looking into modifying admin to put them side by side or making a separate edit page in the future because it seems to jump around when it resizes.  Another issue I've found is code blocks, but I ended up using four spaces for code blocks.

Now, let's add the add the post(s) to views.py:

from .models import BlogPost


def blog_posts(request):
"""Display all blog posts"""
posts = BlogPost.objects.all().order_by('-created_date')
return render(request, 'blog.html', {'posts': posts})

def post(request, pk):
"""Display specific blog posts"""
post_detail = get_object_or_404(BlogPost, pk=pk)
return render(request, 'post.html', {'post_detail': post_detail})

Note that this still needs pagination, but you can find that in another post here.

Next, urls.py needs to be updated:

urlpatterns = [
path('blog/<int:pk>/', views.post, name='post_detail'),
path('blog/', views.blog_posts, name='blog'),
path('markdownx/', include('markdownx.urls')),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

This adds the paths to our pages and and media files needed for MarkdownX. Don't forget to add MEDIA_ROOT to settings.py like I did: 

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')

Finally, let's add the fields to the templates (I'm using bootstrap, so ignore the styling):

<!--blog.html-->

{% extends 'base.html' %}{% load staticfiles %}

{% block content %}

{% csrf_token %}

{% for post in posts %}

<div class="card shadow bg-white">
<div class="card-body">
<h1 class="card-title"> <a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
<p class="card-text"> {{ post.body_summary|striptags }} </p>
<div> {{ post.created_date }}</div>
</div>
</div>

{% endfor %}

{% endblock %}
<!--post.html-->

<header class="masthead text-center text-white d-flex" id="mastheadPost" style="background-image: url(/media/{{ post_detail.background_image }})">
<div class="container my-auto">
<div class="row">
<div class="col-lg-10 mx-auto">
<h1 style="font-family: 'Fira Code', Impact, sans-serif">
{{ post_detail.title }}
</h1>
</div>
</div>
</div>
</header>

<div id="blog">
<p class="card-text" id="post"> {{ post_detail.formatted_markdown|safe }} </p>
<div class="text-muted text-right" style="margin-top: 4rem"> {{ post_detail.created_date }}</div>
</div>

You can see above that I added a couple of methods:

.formatted_markdown formats the markdown in the MarkdownxField() into HTML for the template using markdownify.

.body_summary grabs the first 300 characters for the summary.

So, the updated model in models.py looks like:

class BlogPost(DateCreateModMixin):
title = models.CharField(max_length=50)
body = MarkdownxField()
background_image = models.ImageField(default='img/header.jpg', upload_to=datetime.now().strftime('backgrounds/%Y/%m/%d'))

def formatted_markdown(self):
return markdownify(self.body)

def body_summary(self):
return markdownify(self.body[:300] + "...")

That should be it! Feel free to ask any questions below!

Comments

Popular posts from this blog

Django Pagination with Bootstrap