Alex K.
Posted on July 19, 2019
Nowadays there exist a ton of solutions for easily creating and hosting blogs. However, sometimes you have an already existing website and just want to add a blog to it, without using other tools than the ones already at hand. In this article we'll go through the process of setting up a blog with Django and see how easy and straightforward it is. This post assumes that you already have a Django app/site. If not, one can be easily created by following the official instructions. Django version used is 2.2.
First off, we start by running a handy startapp
command, which will scaffold our blog app. This should be run from the same folder where manage.py
is:
$ python manage.py startapp blog
This will add a blog
directory with the following structure:
blog/
__init__.py
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
views.py
The first step would be to add the newly created app to the list of INSTALLED_APPS
in the settings.py
:
INSTALLED_APPS = [
# other apps ...
'mywebsite.blog'
]
Now we can start setting up the blog. We begin by opening models.py
and adding a BlogPost
model there.
from datetime import datetime
from django.db import models
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from autoslug.fields import AutoSlugField
from ckeditor.fields import RichTextField
class BlogPost(models.Model):
title = models.CharField(_('title'), max_length=255)
slug = AutoSlugField(_('slug'), populate_from='title', unique=True)
image = models.ImageField(_('image'), blank=True, null=True, upload_to='blog')
text = RichTextField(_('text'))
description = models.TextField(_('description'), blank=True, null=True)
published = models.BooleanField(_('published'), default=False)
created = models.DateTimeField(_('created'), auto_now_add=True)
modified = models.DateTimeField(_('modified'), auto_now=True)
pub_date = models.DateTimeField(_('publish date'), blank=True, null=True)
class Meta:
verbose_name = _('blog post')
verbose_name_plural = _('blog posts')
ordering = ['pub_date']
Apart from the expected title
, image
, description
and text
, we have a few extra fields, which will be used when publishing and navigating to a blog post:
-
slug
- is a uniquely identifiable part of the bog post URL. We are using django-autoslug package, which will make preserving the field's uniqueness and auto populating a breeze. In this case we're populating slug from title by usingpopulate_from
param. -
published
- to indicate when a post should be visible on the website. -
pub_date
- a date when a post was published, also used to order the posts.
So far so good. However, it would make sense to set the pub_date
automatically when a post is set to published
. It can be done by extending Django model's save
method:
def save(self, *args, **kwargs):
"""
Set publish date to the date when post's published status is switched to True,
reset the date if post is unpublished
"""
if self.published and self.pub_date is None:
self.pub_date = datetime.now()
elif not self.published and self.pub_date is not None:
self.pub_date = None
super().save(*args, **kwargs)
To finish up with the model setup, we are going to add two more utility methods to it: __str__()
, which will be used to represent the model throughout the whole app (particularly in the admin), and get_absolute_url()
, which will make retrieving the URL to a blog post in the views easier.
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:detail', kwargs={'slug': self.slug})
With the model out of the way, we can go on to create the migrations and run them.
# Create migrations
$ python manage.py makemigrations blog
# Apply them
$ python manage.py migrate blog
At this point it would be a good idea to add newly created migrations to version control (I always seem to forget that) and commit/push the progress.
Now that we have a proper and functioning BlogPost
model, it's time to add it to Django's admin. Considering how powerful the admin is, enabling a new model there is quite a quick job:
from django.contrib import admin
from .models import BlogPost
@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
list_display = ('title', 'image', 'text', 'published', 'pub_date')
And that's it for the admin setup! After adding just these few lines of code we can now navigate to http://localhost:8000/admin/blog/ (by default Django runs development sever on port 8000) and start creating blog posts.
The next step, after adding some posts via admin, naturally would be to display them in the views. Our blog is going to have a paginated list view with all the posts in chronological order and a separate detail view for each post. We are going to use Class Based Views since they allow great code reuse and enable writing the most common views with minimal amount of code. We can set them up in the views.py
like so:
from django.views.generic import ListView, DetailView
from .models import BlogPost
class BlogPostListView(ListView):
model = BlogPost
queryset = BlogPost.objects.filter(published=True)
template_name = 'blog/list.html'
context_object_name = 'blog_posts'
paginate_by = 15 # that is all it takes to add pagination in a Class Based View
class BlogPostDetailView(DetailView):
model = BlogPost
queryset = BlogPost.objects.filter(published=True)
template_name = 'blog/detail.html'
context_object_name = 'blog_post'
Pretty straightforward. Note that it takes one line of code to enable pagination in the view. That will give us is_paginated
and a few other useful context variables to render pagination in templates.
Note that we are applying the same filter to a queryset in two places. This is not a big deal here, however in some cases we would like to abstract this kind of functionality away. In the current case we can add an instance of Manager with custom QuerySet
methods to our model.
# models.py
class BlogPostQueryset(models.QuerySet):
def published(self):
return self.filter(published=True)
def draft(self):
return self.filter(published=False)
class BlogPost(models.Model):
title = models.CharField(_('title'), max_length=255)
...
objects = BlogPostQueryset.as_manager()
Now in the views we can use BlogPost.objects.published()
to get all the published blog posts. At this point we already got the main bulk of functionality out of the way and it only took a few lines of code in a bunch of files.
Finally we can setup the list and detail templates. Per Django convention we will put them into mywebsite/blog/templates/blog/
. Firstly we'll extend the main base template and also will add a reusable navbar template, setting "blog" as an active tab:
{% extends "base.html" %}{% load i18n staticfiles %}
{% block header %}
{% include "includes/navbar.html" with active="blog" %}
{% endblock %}
For reference, navbar.html
could look something like this:
{% load i18n staticfiles %}
<nav class="navbar">
<div class="navbar__header">
<div class="navbar--left">
<a class="no-underline {% if active == 'home' %}active{% endif %}" href="/">{% trans "Home" %}</a>
</div>
</div>
<div class="navbar__inner">
<ul class="navbar--right">
<li class="navbar__tab">
<a class="no-underline {% if active == 'portfolio' %}active{% endif %}"
href="{% url 'portfolio:list' %}">
{% trans "Portfolio" %}
</a>
</li>
<li class="navbar__tab">
<a class="no-underline {% if active == 'blog' %}active{% endif %}"
href="{% url 'blog:list' %}">
{% trans "Blog" %}
</a>
</li>
<li class="navbar__tab">
<a class="no-underline {% if active == 'about' %}active{% endif %}" href="{% url 'about' %}">
{% trans "About" %}
</a>
</li>
</ul>
</div>
</nav>
Since the HTML structure and its styling is not the main purpose of this post, we'll discuss those only in brief. A simple list of posts can be setup with this HTML:
<!-- Assuming you have overridable 'content' block in you master.html, otherwise leave this out-->
{% block content %}
<div class="blog__container">
<h3 class="blog__header">{% trans "Latest posts" %}</h3>
<ul class="blog__list">
{% for post in blog_posts %}
<li class="blog__item">
<div class="blog__inner">
{% if post.image %}
<div class="blog__image" style="background-image: url({{ post.image.url }})"></div>
{% endif %}
<div class="blog__info">
<a href="{{ post.get_absolute_url }}" class="blog__title">{{ post.title }}</a>
<p class="blog__description">{{ post.description }}</p>
<p class="blog__footer">{{ post.pub_date|date }}</p>
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
{% if is_paginated %}
<div class="blog__pagination">
{% if page_obj.has_previous %}
<a href="{% url 'blog:list' %}?page={{ page_obj.previous_page_number }}"><<</a>
{% endif %}
<span class="page-current">
{% blocktrans with num=page_obj.number num_pages=page_obj.paginator.num_pages %}
Page {{ num }} of {{ num_pages }}
{% endblocktrans %}
</span>
{% if page_obj.has_next %}
<a href="{% url 'blog:list' %}?page={{ page_obj.next_page_number }}">>></a>
{% endif %}
</div>
{% endif %}
{% endblock %}
A few things worth noting here:
-
background-image
is used instead of<img/>
because it's easier to make responsive in most cases. - We're using
get_absolute_url
, added to the model, in the post's titlehref
to easily redirect to a post's detail view without needing to explicitly pass post's id in the template. - As mentioned before, enabling pagination in the views exposes a bunch of useful context variables in template, namely
is_paginated
, used to check if pagination has to be rendered andpage_obj
, containing the information about page numbering.
Talking about URLs, if we try to navigate to our blog posts list page, we are going to receive a NoReverseMatch
error since we haven't setup the URLs for the blog. To fix that, let's add urls.py
to our blog app (same level as models.py
and views.py
) and enable the necessary routes. Note that Django starting from version 2 introduced a new path
function for declaring URLs.
from django.urls import path
from .views import BlogPostDetailView, BlogPostListView
urlpatterns = [
path('', BlogPostListView.as_view(), name='list'),
path('<slug>', BlogPostDetailView.as_view(), name='detail'),
]
After that we need to go to the root urls.py
and add the blog URLs there:
from django.urls import include, path
urlpatterns = [
# .... other urls
path('blog/', include(('clarityv2.blog.urls', 'blog'), namespace='blog')),
]
And with that we can navigate to localhost:8000/blog and see the list of blogs we created. Clicking on the post title should redirect us to the post detail page, however since we have not set it up, we see a blank page. To fix this let's add some simple HTML to display a post:
{% extends "base.html" %}{% load i18n staticfiles %}
{% block header %}
{% include "includes/navbar.html" with active="blog" %}
{% endblock %}
{% block content %}
<article class="blog__container">
<section class="blog__header">
<h3 class="blog__title blog__title--large">{{ blog_post.title }}</h3>
<p class="blog__footer">
{{ blog_post.pub_date|date }}
</p>
</section>
<section class="blog__text">{{ blog_post.text|safe }}</section>
</article>
{% endblock %}
Here we use safe
template filter to escape HTML markdown from the editor.
That's about it! Now we have a fully functional, albeit simple, personal blog with all necessary CRUD functionality. There are several ways it can be styled and extended, but that is left as an exercise for the reader :)
Posted on July 19, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.