Django for Beginners #4 - The Blog App
Tags: django, python, webdev, beginners
Published: 2023-03-28 14:35:00

> [Download source code here.](https://www.ericsdevblog.com/posts/django-for-beginners-1?ref=Dev_Community/) ⬅️

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: `User`, `Category`, `Tag`, 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.

```python
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](https://pillow.readthedocs.io/en/stable/) on your machine:

```cmd
pip install Pillow
```

**The `Category` model**

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

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

```python
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](https://ckeditor.com/) as an example.

![CKEditor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ea5hw8ld4cabedx1klit.png)

To install CKEditor, run the following command:

```cmd
pip install django-ckeditor
```

After that, register `ckeditor` in `settings.py`:

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

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

```python
from django.conf import settings
```

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

```cmd
python manage.py makemigrations
```

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

```cmd
python manage.py createsuperuser
```

![django create superuser](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wj7mriwmnf2i0gtqhnje.png)

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

![django admin panel login](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sk6n5046czuevhowxikk.png)

![Django admin panel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4222ilevho7ydjljtpnf.png)

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`

```python
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d6pyf6n6c7lbeasktbam.png)

![category page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qgqstqxkf1a8tkdndoos.png)

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`

```python
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 `CategoryAdmin`. `max_num = 2` means only two posts will be shown on the category page. This is how it looks:

![category inline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ze9yw57703vj9dqpuznr.png)

Next, you can do the same for the `TagAdmin`.

`blog/admin.py`

```python
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/97c5gnxzdv40clppq62o.png)

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

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

```python
path('', views.home, name='home'),
```

**The `category` view**

`blog/views.py`

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

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

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

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

**The `post` view**

`blog/views.py`

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

```python
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](https://github.com/ericsdevblog/blog-template), since HTML and CSS are not really the focus of this tutorial.

![Blog template](https://www.ericsdevblog.com/posts/django-for-beginners-4/images/blog-template.png)

This is the template structure I'm going with.

```text
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 `home`, `category`, `tag` 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`

```django
<!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.

```text
blog
├── admin.py
├── apps.py
├── __init__.py
├── migrations
├── models.py
├── static
│   ├── input.css
│   └── style.css
├── tests.py
└── views.py
```

**Home**

![Home page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rxdr4ltlg0yuneygna2o.png)

`home.html`

```django
{% 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`

```django
<!-- 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:

```python
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](https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#date).

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`

```django
<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`

```django
{% 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`

```django
{% 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wgxhh00unv48vjl3u4g4.png)

```django
{% 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.

```python
python manage.py runserver
```

> If you liked this article, please also check out my other tutorials:
> - [Laravel for Beginners](https://www.ericsdevblog.com/posts/laravel-for-beginners-1?ref=Dev_Community/)
> - [Vue.js for Beginners](https://www.ericsdevblog.com/posts/vuejs-for-beginners-1?ref=Dev_Community/)
> - [Create a Modern Application with Django and Vue](https://www.ericsdevblog.com/posts/create-a-modern-application-with-django-and-vue-1?ref=Dev_Community/)
> - [Beginner’s Roadmap to Web Development](https://www.ericsdevblog.com/series/beginners-roadmap-to-web-development?ref=Dev_Community/)