Adding a blog to your Django website
Tags: blog, django, python
Published: 2019-07-19 11:17:47

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](https://docs.djangoproject.com/en/2.2/intro/tutorial01/). 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: 

``` bash
$ 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`:

```python
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.

```python
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](https://django-autoslug.readthedocs.io/en/latest/) package, which will make preserving the field's uniqueness and auto populating a breeze. In this case we're populating slug from title by using `populate_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:

```python
    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.

```python
 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.

```bash
# 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:

```python
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] (https://docs.djangoproject.com/en/2.2/topics/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:

```python
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](https://docs.djangoproject.com/en/2.2/topics/db/managers/#calling-custom-queryset-methods-from-the-manager) with custom `QuerySet` methods to our model. 

```python
# 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:

```python
{% 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:
```html
{% 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:
```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 title `href` 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 and `page_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. 

```python
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:
```python
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:

```html
{% 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 :)