More Tutorials on Web Development
Table of Contents
Design Database Structure
For a simple blogging system, we need at least 4 database tables: Users
, Categories
, Tags
, and Posts
. If you want other functions for your blog, comments, for example, you can add other tables yourself. To keep this tutorial short and easy to understand, these four are all we need.
Users Table
id | integer | auto increment |
name | string | cannot be empty |
string | unique, cannot be empty | |
password | string |
The users table is already included in Django and we don’t need to do anything about it.
Categories Table
name | string | cannot be empty |
slug | string | unique, cannot be empty |
description | text | can be empty |
Tags Table
name | string | cannot be empty |
slug | string | unique, cannot be empty |
description | text | can be empty |
Posts Table
title | string | cannot be empty |
slug | string | unique, cannot be empty |
featured image | string or text | can be empty |
content | text | cannot be empty |
published | boolean | |
featured | boolean |
Relations
This is the most important part of this tutorial, but unfortunately it is also the most confusing part. Here I would like to introduce three different types of relationships, and they are sufficient to create any kind of website you want.
One-to-one
This is the most basic relation. For example, each User
is associated with one Phone
.
from django.conf import settings
from django.db import models
class Phone(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
To do that, we create a Phone
model, and inside that model, we use OneToOneField
to define this relationship. This will create a user_id
column in this table, and this column will be used to store the id
of the user that this phone belongs to.
Many-to-one
The many-to-one relationships are more complicated. For instance, we have a Category
model and a Post
model, and each category has multiple posts. This is an example of a many-to-one relationship.
from django.db import models
class Category(models.Model):
# …
pass
class Post(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE)
# …
ForeignKey
is how we can define this relationship, and this will create a category_id
column in the post
table, which will be used to store the id
of the category that this post belongs to.
Many-to-many
The many-to-many relationships are the most complicated. Again, as an example, we have a Post
model and a Tag
model. Each post can have multiple tags and each tag can belong to multiple posts.
from django.db import models
class Post(models.Model):
# …
pass
class Tag(models.Model):
# …
posts = models.ManyToManyField(Post)
This is how we can define this relationship. Django will create a pivot table in the form of post_tag_xxxx
, the xxxx
is a random string to make sure this table is unique. And in this table, there will be two columns post_id
and tag_id
. This way Django will be able to find any associated tags of a post and vice versa.
To make thing clearer, imaging we have a table like this:
post_id | tag_id |
---|---|
1 | 1 |
2 | 1 |
3 | 2 |
1 | 2 |
2 | 3 |
For a post with id=1
, there are two tags, each with id=1
and id=2
. If we want to do things backward, find posts through a tag, we can see that for a tag with id=2
, there are two posts, each with id=3
and id=1
.
Another point worth mentioning is that it does not matter which model has the ManyToManyField
, which means the following code will work just the same.
from django.db import models
class Post(models.Model):
# …
tags = models.ManyToManyField(Tag)
class Tag(models.Model):
# …
pass
Design Relationships
For our blog website project. There are six relationships we 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
Create Models
Now, it’s time for us to implement that design.
Category Model
class Category(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
description = models.TextField()
class Meta:
verbose_name_plural = "categories"
def __str__(self):
return self.name
The first four lines of code should be easy to understand. What I want to talk about is the Meta
class inside our Category
model. This is how we add metadata to our models.
Model 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 “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”. You can test this in the admin panel.
Here is a list of all Meta
options that you can use: https://docs.djangoproject.com/en/3.1/topics/db/models/#meta-options
Tag Model
class Tag(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
description = models.TextField()
def __str__(self):
return self.name
This model should be quite easy to understand for you.
Post Model
from ckeditor.fields import RichTextField
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField()
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)
def __str__(self):
return self.title
Line 1, if you just copy and pasted this code, you editor will tell you that it cannot find the RichTextField
and ckeditor
. That is because it is not included in the Django framework.
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.

To install CKEditor, run the following code:
pip install django-ckeditor
After that, register ckeditor
in settings.py
:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog',
'ckeditor',
]
Relationship
Now, we add relationships to our models. We only need to add three lines of code in the Post
model, which is a lot easier compared to Laravel.
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, remember the import the settings
module.
from django.conf import settings
Last but not least, we need to generate the migration files and apply them.
python manage.py makemigrations
python manage.py migrate
Setup Admin Panel
Our next step would be to setup our admin panel:
from django.contrib import admin
from .models import General, 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(General)
admin.site.register(Category, CategoryAdmin)
admin.site.register(Tag, TagAdmin)
admin.site.register(Post, PostAdmin)
Here we use prepopulated_fields
to generate slugs for all categories, tags and posts. Test this by creating a post, as well as some categories and tags in the admin.

Everything should work smoothly. However, our work is not done here. Open the category panel, you will notice that we can access categories from the post page but there is no way to access corresponding posts from the category page. To solve this problem, we need to use InlineModelAdmin
.
class PostInlineCategory(admin.StackedInline):
model = Post
max_num = 2
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)}
inlines = [
PostInlineCategory
]
We first create a PostInlineCategory
, 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:

Next, we do something similar to the TagAdmin.
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
. This is because the relation between Post
and Tag
is a many-to-many relationship. This is the final result.
