Django for Beginners #4 - The Blog App

You can download the source code here.

Finally, it is time for us to create a complete blog application using Django. In the previous article, we explored how the model, view, and template may work together to create a Django application, but frankly, it is a tedious process since you have to write at least 5 actions for each feature, and most of the code feels repetitive.

So in this article, we are going to utilize one of the best features of Django, it’s built-in admin panel. For most features you wish to create for your application, you only need to write the show/list action, and Django will automatically take care of the rest for you.

Create the model layer

Again, let’s start by designing the database structure.

Design the database structure

For a basic blogging system, you need at least 4 models: UserCategoryTag, and Post. In the next article, we will add some advanced features, but for now, these four models are all you need.

The User model

key type info
id integer auto increment
name string
email string unique
password string

The User model is already included in Django, and you don’t need to do anything about it. The built-in User model provides some basic features, such as password hashing, and user authentication, as well as a built-in permission system integrated with the Django admin. You’ll see how this works later.

The Category model

key type info
id integer auto increment
name string
slug string unique
description text

The Tag model

key type info
id integer auto increment
name string
slug string unique
description text

The Post model

key type info
id integer auto increment
title string
slug string unique
content text
featured_image string
is_published boolean
is_featured boolean
created_at date

The Site model

And of course, you need another table that stores the basic information of this entire website, such as name, description and logo.

key type info
name string
description text
logo string

The relations

For this blog application, there are six relations you need to take care of.

  • Each user has multiple posts
  • Each category has many posts
  • Each tag belongs to many posts
  • Each post belongs to one user
  • Each post belongs to one category
  • Each post belongs to many tags

Implement the design

Next, it’s time to implement this design.

The Site model

First of all, you need a Site model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Site(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()
    logo = models.ImageField(upload_to="logo/")

    class Meta:
        verbose_name_plural = "site"

    def __str__(self):
        return self.name

Notice the ImageField(), this field is, in fact, a string type. Since databases can’t really store images, instead, the images are stored in your server’s file system, and this field will keep the path that points to the image’s location.

In this example, the images will be uploaded to mediafiles/logo/ directory. Recall that we defined MEDIA_ROOT = "mediafiles/" in settings.py file.

For this ImageField() to work, you need to install Pillow on your machine:

1
pip install Pillow

The Category model

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Category(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    description = models.TextField()

    class Meta:
        verbose_name_plural = "categories"

    def __str__(self):
        return self.name

The Category model should be easy to understand. What I want to talk about is the Meta class. This is how you add metadata to your models.

Recall that model’s metadata is anything that’s not a field, such as ordering options, database table name, etc. In this case, we use verbose_name_plural to define the plural form of the word category. Unfortunately, Django is not as “smart” as Laravel in this particular aspect, if we do not give Django the correct plural form, it will use categorys instead.

And the __str__(self) function defines what field Django will use when referring to a particular category, in our case, we are using the name field. It will become clear why this is necessary when you get to the Django Admin section.

The Tag model

1
2
3
4
5
6
7
class Tag(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    description = models.TextField()

    def __str__(self):
        return self.name

The Post model

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from ckeditor.fields import RichTextField

. . .

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    content = RichTextField()
    featured_image = models.ImageField(upload_to="images/")
    is_published = models.BooleanField(default=False)
    is_featured = models.BooleanField(default=False)
    created_at = models.DateField(auto_now=True)

    # Define relations
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    tag = models.ManyToManyField(Tag)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

    def __str__(self):
        return self.title

Line 1, if you just copy and paste this code, your editor will tell you that it cannot find the RichTextField and ckeditor. That is because it is a third-party package, and it is not included in the Django framework.

Recall that in the previous article, when you create a post, you can only add plain text, which is not ideal for a blog article. The rich text editor or WYSIWYG HTML editor allows you to edit HTML pages directly without writing the code. In this tutorial, I am using the CKEditor as an example.

CKEditor

To install CKEditor, run the following command:

1
pip install django-ckeditor

After that, register ckeditor in settings.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
INSTALLED_APPS = [
    "blog",
    "ckeditor",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

Define relations

Finally, you can add relations to the models. You only need to add three lines of code in the Post model:

1
2
3
category = models.ForeignKey(Category, on_delete=models.CASCADE)
tag = models.ManyToManyField(Tag)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

And since we are using the built-in User model (settings.AUTH_USER_MODEL), remember to import the settings module.

1
from django.conf import settings

Last but not least, generate the migration files and apply them to the database.

1
python manage.py makemigrations
1
python manage.py migrate

Set up the admin panel

Our next step would be to set up the admin panel. Django comes with a built-in admin system, and to use it, all you need to do is just register a superuser by running the following command:

1
python manage.py createsuperuser

django create superuser

And then, you can access the admin panel by going to http://127.0.0.1:8000/admin/.

django admin panel login

Django admin panel

Right now, the admin panel is still empty, there is only an authentication tab, which you can use to assign different roles to different users. This is a rather complicated topic requiring another tutorial article, so we will not cover that right now. Instead, we focus on how to connect your blog app to the admin system.

Inside the blog app, you should find a file called admin.py. Add the following code to it.

blog/admin.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from django.contrib import admin
from .models import Site, Category, Tag, Post


# Register your models here.
class CategoryAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug": ("name",)}


class TagAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug": ("name",)}


class PostAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug": ("title",)}


admin.site.register(Site)
admin.site.register(Category, CategoryAdmin)
admin.site.register(Tag, TagAdmin)
admin.site.register(Post, PostAdmin)

On line 2, import the models you just created, and then register the imported model using admin.site.register(). However, notice that when you register the Category model, there is something extra called CategoryAdmin, which is a class that is defined on line 6. This is how you can pass some extra information to the Django admin system.

Here you can use prepopulated_fields to generate slugs for all categories, tags, and posts. The value of the slug will be depended on the name. Let’s test it by creating a new category.

Go to http://127.0.0.1:8000/admin/. Click on Categories, and add a new category. Remember we defined the plural form of Category in our model? This is why it is necessary, if we don’t do that, Django will use Categorys instead.

django admin homepage

category page

Notice that the slug will be automatically generated as you type in the name. Try adding some dummy data, everything should work smoothly.

Optional configurations

However, our work is not done yet. Open the category panel, you will notice that you can access categories from the post page, but there is no way to access corresponding posts from the category page. If you don’t think that’s necessary, you can jump to the next section. But if you want to solve this problem, you must use InlineModelAdmin.

blog/admin.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class PostInlineCategory(admin.StackedInline):
    model = Post
    max_num = 2


class CategoryAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug": ("name",)}
    inlines = [
        PostInlineCategory
    ]

First, create a PostInlineCategory class, and then use it in the CategoryAdminmax_num = 2 means only two posts will be shown on the category page. This is how it looks:

category inline

Next, you can do the same for the TagAdmin.

blog/admin.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class PostInlineTag(admin.TabularInline):
    model = Post.tag.through
    max_num = 5


class TagAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug": ("name",)}
    inlines = [
        PostInlineTag
    ]

The code is very similar, but notice the model is not just Post, it is Post.tag.through. That is because the relationship between Post and Tag is a many-to-many relationship. This is the final result.

tag inline

Build the view layer

In the previous sections, we mainly focused on the backend and admin part of our Django application. Now, it is time for us to focus on the frontend, the part that the users can see. We’ll start with the view functions.

Since we have the admin panel set up for our blog application, you don’t need to build the full CRUD operations on your own. Instead, you only need to worry about how to retrieve information from the database. You need four pages, home, category, tag, and post, and you’ll need one view function for each of them.

The home view

blog/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from .models import Site, Category, Tag, Post

def home(request):
    site = Site.objects.first()
    posts = Post.objects.all().filter(is_published=True)
    categories = Category.objects.all()
    tags = Tag.objects.all()

    return render(request, 'home.html', {
        'site': site,
        'posts': posts,
        'categories': categories,
        'tags': tags,
    })

Line 1, here, we import the models we created in the previous article.

Line 4, site contains the basic information of your website, and you are always retrieving the first record in the database.

Line 5, filter(is_published=True)ensures that only published articles will be displayed.

Next, don’t forget the corresponding URL dispatcher.

djangoBlog/urls.py

1
path('', views.home, name='home'),

The category view

blog/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def category(request, slug):
    site = Site.objects.first()
    posts = Post.objects.filter(category__slug=slug).filter(is_published=True)
    requested_category = Category.objects.get(slug=slug)
    categories = Category.objects.all()
    tags = Tag.objects.all()

    return render(request, 'category.html', {
        'site': site,
        'posts': posts,
        'category': requested_category,
        'categories': categories,
        'tags': tags,
    })

djangoBlog/urls.py

1
path('category/<slug:slug>', views.category, name='category'),

Here we passed an extra variable, slug, from the URL to the view function, and on lines 3 and 4, we used that variable to find the correct category and posts.

The tag view

blog/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def tag(request, slug):
    site = Site.objects.first()
    posts = Post.objects.filter(tag__slug=slug).filter(is_published=True)
    requested_tag = Tag.objects.get(slug=slug)
    categories = Category.objects.all()
    tags = Tag.objects.all()

    return render(request, 'tag.html', {
        'site': site,
        'posts': posts,
        'tag': requested_tag,
        'categories': categories,
        'tags': tags,
    })

djangoBlog/urls.py

1
path('tag/<slug:slug>', views.tag, name='tag'),

The post view

blog/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def post(request, slug):
    site = Site.objects.first()
    requested_post = Post.objects.get(slug=slug)
    categories = Category.objects.all()
    tags = Tag.objects.all()

    return render(request, 'post.html', {
        'site': site,
        'post': requested_post,
        'categories': categories,
        'tags': tags,
    })

djangoBlog/urls.py

1
path('post/<slug:slug>', views.post, name='post'),

Create the template layer

For the templates, instead of writing your own HTML and CSS code, you may use the template I’ve created here, since HTML and CSS are not really the focus of this tutorial.

Blog template

This is the template structure I’m going with.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
templates
├── category.html
├── home.html
├── layout.html
├── post.html
├── search.html
├── tag.html
└── vendor
    ├── list.html
    └── sidebar.html

The layout.html contains the header and the footer, and it is where you import the CSS and JavaScript files. The homecategorytag and post are the templates that the view functions point to, and they all extends to the layout. And finally, inside the vendor directory are the components that will appear multiple times in different templates, and you can import them with the include tag.

Layout

layout.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    {% load static %}
    <link rel="stylesheet" href="{% static 'style.css' %}" />
    {% block title %}{% endblock %}
  </head>

  <body class="container mx-auto font-serif">
    <nav class="flex flex-row justify-between h-16 items-center border-b-2">
      <div class="px-5 text-2xl">
        <a href="/"> My Blog </a>
      </div>
      <div class="hidden lg:flex content-between space-x-10 px-10 text-lg">
        <a
          href="https://github.com/ericnanhu"
          class="hover:underline hover:underline-offset-1"
          >GitHub</a
        >
        <a href="#" class="hover:underline hover:underline-offset-1">Link</a>
        <a href="#" class="hover:underline hover:underline-offset-1">Link</a>
      </div>
    </nav>

    {% block content %}{% endblock %}

    <footer class="bg-gray-700 text-white">
      <div
        class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10"
      >
        <p class="font-serif text-center mb-3 sm:mb-0">
          Copyright ©
          <a href="https://www.ericsdevblog.com/" class="hover:underline"
            >Eric Hu</a
          >
        </p>

        <div class="flex justify-center space-x-4">
          . . .
        </div>
      </div>
    </footer>
  </body>
</html>

There is one thing we need to talk about in this file. Notice from line 7 to 8, this is how you can import static files (CSS and JavaScript files) in Django. Of course, we are not discussing CSS in this tutorial, but I’d like to talk about how it can be done if you do need to import extra CSS files.

By default, Django will search for static files in individual app folders. For the blog app, Django will go to /blog and search for a folder called static, and then inside that static folder, Django will look for the style.css file, as defined in the template.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
blog
├── admin.py
├── apps.py
├── __init__.py
├── migrations
├── models.py
├── static
│   ├── input.css
│   └── style.css
├── tests.py
└── views.py

Home

Home page

home.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{% extends 'layout.html' %} {% block title %}
<title>Page Title</title>
{% endblock %} {% block content %}
<div class="grid grid-cols-4 gap-4 py-10">
  <div class="col-span-3 grid grid-cols-1">
    <!-- Featured post -->
    <div class="mb-4 ring-1 ring-slate-200 rounded-md hover:shadow-md">
      <a href="{% url 'post' featured_post.slug %}"
        ><img
          class="float-left mr-4 rounded-l-md object-cover h-full w-1/3"
          src="{{ featured_post.featured_image.url }}"
          alt="..."
      /></a>
      <div class="my-4 mr-4 grid gap-2">
        <div class="text-sm text-gray-500">
          {{ featured_post.created_at|date:"F j, o" }}
        </div>
        <h2 class="text-lg font-bold">{{ featured_post.title }}</h2>
        <p class="text-base">
          {{ featured_post.content|striptags|truncatewords:80 }}
        </p>
        <a
          class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring"
          href="{% url 'post' featured_post.slug %}"
          >Read more →</a
        >
      </div>
    </div>

    {% include "vendor/list.html" %}
  </div>
  {% include "vendor/sidebar.html" %}
</div>
{% endblock %}

Notice that instead of hardcoding the sidebar and the list of posts, we separated them and placed them in the vendor directory, since we are going to use the same components in the category and the tag page.

List of posts

vendor/list.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- List of posts -->
<div class="grid grid-cols-3 gap-4">
  {% for post in posts %}
  <!-- post -->
  <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md">
    <a href="{% url 'post' post.slug %}"
      ><img
        class="rounded-t-md object-cover h-60 w-full"
        src="{{ post.featured_image.url }}"
        alt="..."
    /></a>
    <div class="m-4 grid gap-2">
      <div class="text-sm text-gray-500">
        {{ post.created_at|date:"F j, o" }}
      </div>
      <h2 class="text-lg font-bold">{{ post.title }}</h2>
      <p class="text-base">
        {{ post.content|striptags|truncatewords:30 }}
      </p>
      <a
        class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring"
        href="{% url 'post' post.slug %}"
        >Read more →</a
      >
    </div>
  </div>
  {% endfor %}
</div>

From line 3 to 27, recall that we passed a variable posts from the view to the template. The posts contains a collection of posts, and here, inside the template, we iterate over every item in that collection using a for loop.

Line 6, recall that we created a URL dispatcher like this:

1
path('post/<slug:slug>', views.post, name='post'),

In our template, {% url 'post' post.slug %} will find the URL dispatcher with the name 'posts', and assign the value of post.slug to the variable <slug:slug>, which will then be passed to the corresponding view function.

Line 14, the date filter will format the date data that is passed to the template since the default value is not user-friendly. You can find other date formats here.

Line 18, here we chained two filters to post.content. The first one removes the HTML tags, and the second one takes the first 30 words and slices the rest.

Sidebar

vendor/sidebar.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<div class="col-span-1">
  <div class="border rounded-md mb-4">
    <div class="bg-slate-200 p-4">Search</div>
    <div class="p-4">
      <form action="" method="get">
        <input type="text" name="search" id="search" class="border rounded-md w-44 focus:ring p-2" placeholder="Search something...">
        <button type="submit" class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-fit focus:ring">Search</button>
      </form>
    </div>
  </div>
  <div class="border rounded-md mb-4">
    <div class="bg-slate-200 p-4">Categories</div>
    <div class="p-4">
      <ul class="list-none list-inside">
        {% for category in categories %}
        <li>
          <a
            href="{% url 'category' category.slug %}"
            class="text-blue-500 hover:underline"
            >{{ category.name }}</a
          >
        </li>
        {% endfor %}
      </ul>
    </div>
  </div>
  <div class="border rounded-md mb-4">
    <div class="bg-slate-200 p-4">Tags</div>
    <div class="p-4">
      {% for tag in tags %}
      <span class="mr-2"
        ><a
          href="{% url 'tag' tag.slug %}"
          class="text-blue-500 hover:underline"
          >{{ tag.name }}</a
        ></span
      >
      {% endfor %}
    </div>
  </div>
  <div class="border rounded-md mb-4">
    <div class="bg-slate-200 p-4">More Card</div>
    <div class="p-4">
      <p>
        . . .
      </p>
    </div>
  </div>
</div>

Category

category.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{% extends 'layout.html' %}

{% block title %}
<title>Page Title</title>
{% endblock %}

{% block content %}
<div class="grid grid-cols-4 gap-4 py-10">
  <div class="col-span-3 grid grid-cols-1">

    {% include "vendor/list.html" %}

  </div>
  {% include "vendor/sidebar.html" %}
</div>
{% endblock %}

Tag

tag.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{% extends 'layout.html' %}

{% block title %}
<title>Page Title</title>
{% endblock %}

{% block content %}
<div class="grid grid-cols-4 gap-4 py-10">
  <div class="col-span-3 grid grid-cols-1">

    {% include "vendor/list.html" %}

  </div>
  {% include "vendor/sidebar.html" %}
</div>
{% endblock %}

Post

post page

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{% extends 'layout.html' %}

{% block title %}
<title>Page Title</title>
{% endblock %}

{% block content %}
<div class="grid grid-cols-4 gap-4 py-10">
  <div class="col-span-3">

    <img
        class="rounded-md object-cover h-96 w-full"
        src="{{ post.featured_image.url }}"
        alt="..."
    />
    <h2 class="mt-5 mb-2 text-center text-2xl font-bold">{{ post.title }}</h2>
    <p class="mb-5 text-center text-sm text-slate-500 italic">By {{ post.user|capfirst }} | {{ post.created_at }}</p>

    <div>{{ post.content|safe }}</div>

    <div class="my-5">
        {% for tag in post.tag.all %}
        <a href="{% url 'tag' tag.slug %}" class="text-blue-500 hover:underline" mr-3">#{{ tag.name }}</a>
        {% endfor %}
    </div>

  </div>
  {% include "vendor/sidebar.html" %}
</div>
{% endblock %}

One last thing we need to talk about is line 19, notice we added a safe filter. That is because, by default, Django will render HTML code as plain text for security reasons, we have to tell Django that it is OK to render HTML codes as HTML.

Lastly, start the dev server and explore your first Django app.

1
python manage.py runserver
comments powered by Disqus