Django Fundamentals

Django Fundamentals

Django is a high-level, free and open-source web framework written in Python. It is widely used for building complex web applications and is known for its ability to handle high traffic, its security features and reusable components. It follows the model-view-controller (MVC) architecture and comes with a built-in admin interface, authentication support and an ORM (Object-Relational Mapper) that allows you to define your database schema using Python classes.

In addition, Django is backed by a large and active community that provides a wealth of tutorials, packages, and other resources for developers to use. Many well-known websites, such as Instagram and Pinterest, are built using Django. It is considered one of the most popular and powerful web frameworks, and in this tutorial, we are going to cover the fundamentals of the Django framework and by the end, you will be able to create Django applications with ease.

You can access the source code for this tutorial here 👈.

Setting up your computer for Django development

To get started, there are a few tools that you need to install on your machine. For a web application, you need at least a programming language (Python), a database (SQLite), and a server (Django's built-in dev server) for the application to run successfully.

However, you should know that this stack is only for the dev environment, SQLite and Django's dev server are not designed for the production environment. You can check out this article on how to deploy Django for production.

And of course, you also need an IDE, such as PyCharm, or at least a code editor. We'll use VS Code for this tutorial since it is a free software.

Creating a new Django project

After everything has been properly installed, it is time to initialize a new project. First of all, you need to create a new work directory to keep everything organized:

bash
1mkdir <work_directory>
bash
1cd <work_directory>

Replace <work_directory> with your desired name.

Next, create a new Python virtual environment inside the working directory. This virtual environment will be isolated from your system's Python environment, as well as other Python projects you might have on your machine.

bash
1python -m venv env

Please note that if you are using Linux or macOS, you might have to run python3 instead of python, but we are going to keep using python in this tutorial for simplicity.

This command will create an env directory containing the virtual environment you just created. You can activate the virtual environment using the following command:

bash
1source env/bin/activate

If you are using Windows, use this command instead:

bash
1env/Scripts/activate

If the virtual environment has been successfully activated, your terminal prompt will look like this:

text
1(env) eric@djangoDemo:~/django-demo$

(env) means you are currently working inside a virtual environment named env.

Now it is time for us to initialize a new Django project. You need to install the Django package by running the following command:

bash
1python -m pip install Django

Next, use the django-admin command to create a new Django project:

bash
1django-admin startproject djangoBlog

A new djangoBlog directory will be created:

django new project

Optionally, you can restructure the project a bit so that everything starts with the project root directory.

django new project restructure

Create a new blog app

Right now, the project is still empty, and as you can see, several files are generated. We'll discuss each file in detail later.

Django allows you to create multiple apps under a single project. For example, there could be a blog app, a gallery app, and a forum app inside one single project. These apps could share the same static files, images, videos, and other resources, or they could be completely independent of each other. It depends entirely on your own choice.

In this tutorial, we’ll create only one blog app for demonstration purposes. Go back to the terminal and execute the following command:

bash
1python manage.py startapp blog

You should see a new blog folder generated under the project root directory. Here is a small command-line trick, you can list the content of a directory using the following command:

bash
1ls

If you want to include hidden files as well:

bash
1ls -a

If you want to see the file structure:

bash
1tree

You might have to install tree for the last command to work, depending on your operating system.

From now on, we'll use this command to display the file structure. Right now, your project should have the following structure:

text
1.
2├── blog
3│   ├── admin.py
4│   ├── apps.py
5│   ├── __init__.py
6│   ├── migrations
7│   ├── models.py
8│   ├── tests.py
9│   └── views.py
10├── djangoBlog
11│   ├── asgi.py
12│   ├── __init__.py
13│   ├── __pycache__
14│   ├── settings.py
15│   ├── urls.py
16│   └── wsgi.py
17├── env
18└── manage.py

Next, you need to register this new blog app with Django. Go to settings.py and find INSTALLED_APPS:

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

Now, you can start the development server to test if everything works. Open the terminal, and run the following command:

bash
1python manage.py runserver

You should see the following output:

text
1Watching for file changes with StatReloader
2Performing system checks...
3
4System check identified no issues (0 silenced).
5
6You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
7Run 'python manage.py migrate' to apply them.
8October 19, 2022 - 01:39:33
9Django version 4.1.2, using settings 'djangoBlog.settings'
10Starting development server at http://127.0.0.1:8000/
11Quit the server with CONTROL-C.

Open the browser and go to http://127.0.0.1:8000/

django welcome page

Django's project structure

Now, let's talk about the structure of this new Django application and what each file does.

text
1.
2├── blog
3│   ├── admin.py
4│   ├── apps.py
5│   ├── __init__.py
6│   ├── migrations
7│   ├── models.py
8│   ├── tests.py
9│   └── views.py
10├── djangoBlog
11│   ├── asgi.py
12│   ├── __init__.py
13│   ├── __pycache__
14│   ├── settings.py
15│   ├── urls.py
16│   └── wsgi.py
17├── env
18└── manage.py

Under the root directory, we have:

  • manage.py: A command-line utility that lets you interact with this Django project in various ways.
  • djangoBlog: The main project directory, which contains the settings file and the entry point of the project.
  • blog: The blog app.

And look inside the project directory, djangoBlog, we have:

  • djangoBlog/__init__.py: An empty file that tells Python that this directory should be considered a Python package.
  • djangoBlog/settings.py: As the name suggests, it is the setting/configuration file for this Django project.
  • djangoBlog/urls.py: The URL declarations for this Django project, a table of contents of your Django application. We'll talk more about this later.
  • djangoBlog/asgi.py: An entry-point for ASGI-compatible web servers to serve your project.
  • djangoBlog/wsgi.py: An entry-point for WSGI-compatible web servers to serve your project.

And finally, under the application directory, blog, we have:

  • blog/migrations: This directory contains all the migration files for the blog app. Unlike Laravel, these files are automatically generated by Django based on your models.
  • blog/admin.py: Django also comes with an admin panel, and this file contains all the configurations for it.
  • blog/models.py: Models describe the structure and relation of the database. The migration files are generated based on this file.
  • blog/views.py: This is equivalent to the controllers in Laravel. It contains all the core logic of this app.

Configuring our Django project

Before we delve into our project, there are some changes you need to make to the settings.py file.

  • Allowed hosts

The ALLOWED_HOSTS is a list of domains that the Django site is allowed to serve. This is a security measure to prevent HTTP host header attacks, which are possible even under many seemingly-safe web server configurations.

However, you might notice that even if the ALLOWED_HOSTS is currently empty, we can still access our site using the host 127.0.0.1. That is because when DEBUG is True, and ALLOWED_HOSTS is empty, the host is validated against ['.localhost', '127.0.0.1', '[::1]'].

  • Database

DATABASES is a dictionary containing the database settings that our website needs to use. By default, Django uses SQLite, which is a very lightweight database consisting of only one file. It should be enough for our small demo project, but it won't work for large sites. So, if you want to use other databases, here is an example:

python
1DATABASES = {
2    "default": {
3        "ENGINE": "django.db.backends.postgresql",
4        "NAME": "<database_name>",
5        "USER": "<database_user_name>",
6        "PASSWORD": "<database_user_password>",
7        "HOST": "127.0.0.1",
8        "PORT": "5432",
9    }
10}

As a side note, Django recommends using PostgreSQL as your database in a production environment. We are aware that many tutorials are teaching you how to use Django with MongoDB, but that's actually a bad idea. MongoDB is a great database solution, it just doesn't work so well with Django. You can read this article for details.

  • Static and media files

And finally, we need to take care of the static and media files. Static files are CSS and JavaScript files, and media files are images, videos, and other things the user might upload.

First, we need to specify where these files are stored. We'll create a static directory inside the blog app for our Django site. This is where we store static files for the blog app during development.

text
1blog
2├── admin.py
3├── apps.py
4├── __init__.py
5├── migrations
6├── models.py
7├── static
8├── tests.py
9└── views.py

In the production environment, however, things are a bit different. You need a different folder under the root directory of our project. Let's call it staticfiles.

text
1.
2├── blog
3├── db.sqlite3
4├── djangoBlog
5├── env
6├── manage.py
7└── staticfiles

When you put our Django project to production, the files under blog/static will be automatically copied to staticfiles.

Also remember that you need to specify this staticfiles folder in settings.py.

python
1STATIC_ROOT = "staticfiles/"

Next, you need to tell Django what URL to use when accessing these static files. It does not have to be /static, but do make sure it does not overlap with our URL configurations which we'll talk about later.

python
1STATIC_URL = "static/"

Media files are configured in a similar way. You can create a mediafiles folder in the project root directory:

text
1.
2├── blog
3├── db.sqlite3
4├── djangoBlog
5├── env
6├── manage.py
7├── mediafiles
8└── staticfiles

And then, specify the location and URL in the settings.py file:

python
1# Media files
2
3MEDIA_ROOT = "mediafiles/"
4
5MEDIA_URL = "media/"

URL dispatchers in Django

In the web development field, there is something we call the MVC (Model-View-Controller) architecture. Under this architecture, the model is in charge of interacting with our database, each model should correspond to one database table. The view is the frontend part of the application, it is what the users can see. And finally, the controller is the central command of the app, it is in charge of tasks such as retrieving data from the database through the models, putting them in the corresponding view, and eventually returning the rendered template back to the user.

Django is designed based on this architecture, just with a different terminology. For Django, it is the MTV (Model-Template-View) structure. The template is the frontend, and the view is the backend logic.

In this tutorial, our focus will be understanding each of these layers, but first, we need to start with the entry point of every web application, the URL dispatcher. What it does is, when the user types in a URL and hits Enter, the dispatcher reads that URL and directs the user to the correct page.

Basic URL configurations

The URL configurations are stored in example/urls.py:

text
1djangoBlog
2├── asgi.py
3├── __init__.py
4├── __pycache__
5├── settings.py
6├── urls.py   <===
7└── wsgi.py

The file gives you an example of what an URL dispatcher should look like:

python
1from django.contrib import admin
2from django.urls import path
3
4urlpatterns = [
5    path('admin/', admin.site.urls),
6]

It reads information from a URL and returns a view function.

In this example, if the domain is followed by admin/, the dispatcher will route the user to the admin page.

Passing parameters with Django URL dispatchers

In practice, it is often necessary to take a segment from the URL and pass them to the view as parameters. For example, if you want to display a blog post page, you'll need to pass the post id or the slug to the view so that you can use this extra information to find the blog post you are searching for. This is how we can pass a parameter:

python
1from django.urls import path
2from blog import views
3
4urlpatterns = [
5    path('post/<int:id>', views.post),
6]

The angle bracket (<int:id>) will capture the segment after post/ as a parameter. Look inside the bracket, on the left side, the int is called a path converter, and it only captures integer parameters. On the right side, it is the name of the parameter. We'll need to use it in the view.

The following path converters are available by default:

  • str: Matches any non-empty string, excluding the path separator, '/'. This is the default if a converter isn’t included in the expression.
  • int: Matches zero or any positive integer. Returns an int.
  • slug: Matches any slug string consisting of ASCII letters or numbers, plus the hyphen and underscore characters. For example, building-your-1st-django-site.
  • uuid: Matches a formatted UUID. To prevent multiple URLs from mapping to the same page, dashes must be included, and letters must be lowercase. For example, 075194d3-6885-417e-a8a8-6c931e272f00. Returns a UUID instance.
  • path: Matches any non-empty string, including the path separator, '/'. This allows you to match against a complete URL path rather than a segment of a URL path as with str.

Using regular expressions to match URL patterns

Sometimes the pattern you need to match is more complicated, in which case you can use regular expressions to describe URL patterns. We discussed regular expressions in detail in the linked lesson.

To enable regular expressions, you need to use re_path() instead of path():

python
1from django.urls import path, re_path
2from . import views
3
4urlpatterns = [
5    path('articles/2003/', views.special_case_2003),
6    re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
7    re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
8    re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<slug>[\w-]+)/$', views.article_detail),
9]

Importing other URL configurations

Imagine you have a Django project with multiple different apps in it. If you put all configurations in one file, it will become very messy. When this happens, you can separate the URL configurations and move them to different apps. For example, in our project, we can create a new urls.py inside the blog app.

python
1from django.urls import path, include
2
3urlpatterns = [
4    path('blog/', include('blog.urls'))
5]

This means if the URL has the pattern http://www.example.com/blog/xxxx, Django will go to blog/urls.py and match for the rest of the URL.

Define the URL patterns for the blog app in the exact same way:

python
1from django.urls import path
2from blog import views
3
4urlpatterns = [
5    path('post/<int:id>', views.post),
6]

This pattern will match the URL pattern: http://www.mydomain.com/blog/post/123

Naming URL patterns

One last thing we have to discuss is that you can name URL patterns by giving it a third parameter like this:

python
1urlpatterns = [
2    path('articles/<int:year>/', views.year_archive, name='news-year-archive'),
3]

This little trick is very important, it allows us to reverse resolute URLs from the template. We'll talk about this when we get to the template layer.

That is all we need to cover for the basics of URL dispatcher. However, you may have noticed that something is missing. What if we are using differnet HTTP methods? How can we differentiate them?

In Django, you cannot match different HTTP methods inside the URL dispatcher. All methods match the same URL pattern, and we can only differentiate then inside the view functions. We'll discuss this in detail later.

Let's talk about the models

Django's model layer is one of its best features. For other web frameworks, you need to create both a model and a migration file. The migration file is a schema for the database, which describes the structure (column names and column types) of the database. The model provides an interface that handles the data manipulation based on that schema.

But for Django, you only need a model, and the corresponding migration files can be generated with a simple command, saving you a lot of time.

Each app has one models.py file, and all the models related to the app should be defined inside. Each model corresponds to a migration file, which corresponds to a database table. To differentiate tables for different apps, a prefix will be automatically assigned to each table. For our blog app, the corresponding database table will have the prefix blog_.

Here is an example of a model:

blog/models.py

python
1from django.db import models
2
3class Person(models.Model):
4    first_name = models.CharField(max_length=30)
5    last_name = models.CharField(max_length=30)

You can then generate a migration file using the following command:

bash
1python manage.py makemigrations

And the generated migration file should look like this:

blog/migrations/0001_initial.py

python
1# Generated by Django 4.1.2 on 2022-10-19 23:13
2
3from django.db import migrations, models
4
5
6class Migration(migrations.Migration):
7
8    initial = True
9
10    dependencies = []
11
12    operations = [
13        migrations.CreateModel(
14            name="Person",
15            fields=[
16                (
17                    "id",
18                    models.BigAutoField(
19                        auto_created=True,
20                        primary_key=True,
21                        serialize=False,
22                        verbose_name="ID",
23                    ),
24                ),
25                ("first_name", models.CharField(max_length=30)),
26                ("last_name", models.CharField(max_length=30)),
27            ],
28        ),
29    ]
30

The migration file is a schema that describes how the database table should look, you can use the following command to apply this schema:

bash
1python manage.py migrate

Your database (db.sqlite3) should look like this:

database blog person

As a beginner, you should never try to edit or delete the migration files, just let Django do everything for you unless you absolutely know what you are doing. In most cases, Django can detect the changes you've made in the models and generate the migration files accordingly.

In our example, this model will create a blog_phone table, and inside the table, there will be three columns, each named id, first_name, and last_name. The id column is created automatically, as seen in the migration file. The id column is used as the primary key for indexing by default.

CharField() is called a field type and it defines the type of the corresponding column. max_length is a field option, and it specifies extra information about that column. You can find a reference of all field types and field options here.

Model field types and options

To save us some time, we'll only introduce some of the most commonly used field types.

Field TypeDescription
BigAutoFieldCreates an integer column that automatically increments. Usually used for the id column.
BooleanFieldCreates a Boolean value column, with values True or False.
DateField and DateTimeFieldAs their names suggest, adds dates and times.
FileField and ImageFieldCreates a column that stores the path, which points to the uploaded file or image.
IntegerField and BigIntegerFieldInteger has values from -2147483648 to 2147483647. Big integer has values from -9223372036854775808 to 9223372036854775807
SlugFieldSlug is usually a URL-friendly version of the title/name.
CharField and TextFieldCharField and TextField both create a column for storing strings, except TextField corresponds to a larger text box in the Django admin, which we'll talk about later.

And field options.

Field OptionDescription
blankAllows this field to have an empty entry.
choicesGives this field multiple choices, you'll see how this works after we get to Django Admin.
defaultGives the field a default value.
uniqueMakes sure that every item in the column is unique. Usually used to slug and other fields that are supposed to have unique values.

Meta options

You can also add a Meta class inside the model class, which contains extra information about this model, such as database table name, ordering options, and human-readable singular and plural names.

python
1class Category(models.Model):
2    priority = models.IntegerField()
3
4    class Meta:
5        ordering = ["priority"]
6        verbose_name_plural = "categories"

Model methods

Model methods are functions defined inside the model class. These functions allow us to perform custom actions to the current instance of the model object. For example:

python
1class Person(models.Model):
2    first_name = models.CharField(max_length=50)
3    last_name = models.CharField(max_length=50)
4    birth_date = models.DateField()
5
6    def baby_boomer_status(self):
7        "Returns the person's baby-boomer status."
8        import datetime
9        if self.birth_date < datetime.date(1945, 8, 1):
10            return "Pre-boomer"
11        elif self.birth_date < datetime.date(1965, 1, 1):
12            return "Baby boomer"
13        else:
14            return "Post-boomer"

When the baby_boomer_status() method is called, Django will examine the person's birth date and return the person's baby-boomer status.

Model inheritance

For most web applications, you'll need more than one model, and some of them will have common fields. In this case, you can create a parent model which contains the common fields, and make other models inherit from the parent.

python
1class CommonInfo(models.Model):
2    name = models.CharField(max_length=100)
3    age = models.PositiveIntegerField()
4
5    class Meta:
6        abstract = True
7
8class Student(CommonInfo):
9    home_group = models.CharField(max_length=5)

Notice that CommonInfo is marked as an abstract model, which means this model doesn't really correspond to an individual model, instead, it is used as a parent to other models.

To verify this, generate a new migration file:

blog/migrations/0002_student.py

bash
1python manage.py makemigrations
python
1# Generated by Django 4.1.2 on 2022-10-19 23:28
2
3from django.db import migrations, models
4
5
6class Migration(migrations.Migration):
7
8    dependencies = [
9        ("blog", "0001_initial"),
10    ]
11
12    operations = [
13        migrations.CreateModel(
14            name="Student",
15            fields=[
16                (
17                    "id",
18                    models.BigAutoField(
19                        auto_created=True,
20                        primary_key=True,
21                        serialize=False,
22                        verbose_name="ID",
23                    ),
24                ),
25                ("name", models.CharField(max_length=100)),
26                ("age", models.PositiveIntegerField()),
27                ("home_group", models.CharField(max_length=5)),
28            ],
29            options={
30                "abstract": False,
31            },
32        ),
33    ]
34

As you can see, only a Student table is created.

Database relations

So far, we've only talked about how to create individual tables, however, in practice, these tables aren't entirely independent, and there are usually relations between different tables. For instance, you could have a category that has multiple posts, a post that belongs to a specific user, etc. So how can you describe such relations in Django?

There are primarily three types of database relations, one-to-one, many-to-one, and many-to-many.

One-to-one relation

The one-to-one relationship should be the easiest to understand. For example, each person could have one phone, and each phone could belong to one person. We can describe this relation in the models like this:

python
1class Person(models.Model):
2    name = models.CharField(max_length=100)
3
4class Phone(models.Model):
5    person = models.OneToOneField('Person', on_delete=models.CASCADE)

The OneToOneField works just like a regular field type, except it takes at least two arguments. The first argument is the name of the model with whom this model has a relationship. And the second argument is on_delete, which defines the action Django will take when data is deleted. This has more to do with SQL than Django, so we are not going into this topic in detail, but if you are interested, here are some available values for on_delete.

You can now generate and apply migrations for these models and see what happens. If you run into problems running these commands, simply delete the db.sqlite3 file and the migration files to start over.

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

one to one relation

As you can see, the OneToOneField created a person_id column inside the blog_phone table, and this column will store the id of the person that owns this phone.

Many-to-one relation

Each category has many posts, and each post belongs to one category. This relation is referred to as a many-to-one relation.

python
1class Category(models.Model):
2    name = models.CharField(max_length=100)
3
4class Post(models.Model):
5    category = models.ForeignKey('Category', on_delete=models.CASCADE)

ForeignKey will create a category_id column in the blog_post table, which stores the id of the category that this post belongs to.

Many-to-many relation

Many-to-many relation is slightly more complicated. For example, every article could have multiple tags, and each tag could have multiple articles.

python
1class Tag(models.Model):
2    name = models.CharField(max_length=100)
3
4class Post(models.Model):
5    tags = models.ManyToManyField('Tag')

Instead of creating a new column, this code will create a pivot table called post_tags, which contains two columns, post_id, and tag_id. This allows you to locate all the tags associated with a particular post, and vice versa.

To make things clearer, imagine we have a table like this:

post_idtag_id
11
21
32
12
23

For a post with id=1, there are two tags, each with id=1 and id=2. If you want to do things backward and find posts through a tag, you can see that for a tag with id=2, there are two posts, id=3 and id=1.

Moving to the view layer

The view layer is one of the most important components in a Django application, it is where we write all the backend logic. The most important thing that views are supposed to do is retrieving data from the database through the corresponding model, processing the retrieved data, putting them in the corresponding location in the template, and finally, rendering and returning that template back to the user.

Of course, that is not the only thing we can do with a view function. In most web applications, there are four most basic operations we can do with data, create, read, update and delete them. These operations put together are often referred to as CRUD. We are going to investigate all of them in this tutorial later.

We've briefly discussed models in the previous section, but we still don't know how to retrieve or store data using the model. Django offers us a very simple API to help us with that, which is called the QuerySet.

Suppose that this is your model:

blog/models.py

python
1class Category(models.Model):
2    name = models.CharField(max_length=100)
3
4
5class Tag(models.Model):
6    name = models.CharField(max_length=200)
7
8
9class Post(models.Model):
10    title = models.CharField(max_length=255)
11    content = models.TextField()
12    pub_date = models.DateField()
13    category = models.ForeignKey(Category, on_delete=models.CASCADE)
14    tags = models.ManyToManyField(Tag)

With the help of QuerySet, you can manipulate data through this model inside the view functions, which are located inside the blog/views.py file.

Creating and saving data

Let's say you are trying to create a new category, this is what you can do:

python
1# import the Category model
2from blog.models import Category
3
4# create a new instance of Category
5category = Category(name="New Category")
6
7# save the newly created category to database
8category.save()

In this example, we created a new instance of the Category object and used the save() method to save the information to the database.

Now, what about relations? For example, there is a many-to-one relationship between category and post, which we defined using the ForeignKey() field.

python
1from blog.models import Category, Post
2
3# Post.objects.get(pk=1) is how we retrieve the post with pk=1,
4# where pk stands for primary key, which is usually the id unless otherwise specified.
5post = Post.objects.get(pk=1)
6
7# retrieve the category with name="New Category"
8new_category = Category.objects.get(name="New Category")
9
10# assign new_category to the post's category field and save it
11post.category = new_category
12post.save()

There is also a many-to-many relation between posts and tags.

python
1from blog.models import Tag, Post
2
3post1 = Post.objects.get(pk=1) # Retrieve post 1
4
5tag1 = Tag.objects.get(pk=1) # Retrieve tag 1
6tag2 = Tag.objects.get(pk=2) # Retrieve tag 2
7tag3 = Tag.objects.get(pk=3) # Retrieve tag 3
8tag4 = Tag.objects.get(pk=4) # Retrieve tag 4
9tag5 = Tag.objects.get(pk=5) # Retrieve tag 5
10
11post.tags.add(tag1, tag2, tag3, tag4, tag5) # Add tag 1-5 to post 1

Retrieving data

Retrieving objects is slightly more complicated. Imagine we have thousands of records in our database. How do we find one particular record, if we don't know the id? Or, what if we want a collection of records that fits particular criteria instead of one record?

The QuerySet methods allow you to retrieve data based on certain criteria. And they can be accessed using the attribute objects. We've already seen an example, get(), which is used to retrieve one particular record.

python
1first_tag = Tag.objects.get(pk=1)
2new_category = Category.objects.get(name="New Category")

You can also retrieve all records using the all() method.

python
1Post.objects.all()

The all() method returns what we call a QuerySet, it is a collection of records. And you can further refine that collection by chaining a filter() or exclude() method.

python
1Post.objects.all().filter(pub_date__year=2006)

This will return all the posts that are published in the year 2006. And pub_date__year is called a field lookup.

Or we can exclude the posts that are published in the year 2006.

python
1Post.objects.all().exclude(pub_date__year=2006)

Besides get(), all(), filter() and exclude(), there are lots of other QuerySet methods just like them. We can't talk about all of them here, but if you are interested, here is a full list of all QuerySet methods.

Field lookups are the keyword arguments for QuerySet methods. If you are familiar with SQL, they work just like the SQL WHERE clause. And they take the form fieldname__lookuptype=value. Notice that it is a double underscore in between.

python
1Post.objects.all().filter(pub_date__lte='2006-01-01')

In this example, pub_date is the field name, and lte is the lookup type, which means less than or equal to. This code will return all the posts where the pub_date is less than or equal to 2006-01-01.

Here is a list of all field lookups you can use.

Field lookups can also be used to find records that have a relationship with the current record. For example:

python
1Post.objects.filter(category__name='Django')

This line of code will return all posts that belong to the category whose name is "Django".

This works backward too. For instance, we can return all the categories that have at least one post whose title contains the word "Django".

python
1Category.objects.filter(post__title__contains='Django')

We can go across multiple relations as well.

python
1Category.objects.filter(post__author__name='Admin')

This will return all categories that own posts, which are published by the user Admin. In fact, you can chain as many relationships as you want.

Deleting Objects

The method we use to delete a record is conveniently named delete(). The following code will delete the post that has pk=1.

python
1post = Post.objects.get(pk=1)
2post.delete()

We can also delete multiple records together.

python
1Post.objects.filter(pub_date__year=2005).delete()

This will delete all posts that are published in the year 2005.

However, what if the record we are deleting relates to another record? For example, here we are trying to delete a category with multiple posts.

python
1category = Category.objects.get(pk=1)
2category.delete()

By default, Django emulates the behaviour of the SQL constraint ON DELETE CASCADE, which means all the posts that belong to this category also be deleted. If you wish to change that, you can change the on_delete option to something else. Here is a reference of all available options for on_delete.

The view function

So far, we've only seen some snippets showing what you can do inside a view function, but what does a complete view function look like? Well, here is an example. In Django, all the views are defined inside the views.py file.

python
1from django.shortcuts import render
2from blog.models import Post
3
4# Create your views here.
5def my_view(request):
6    posts = Post.objects.all()
7
8    return render(request, 'blog/index.html', {
9        'posts': posts,
10    })

There are two things you need to pay attention to in this example.

First, notice that this view function takes an input request. This variable request is an HttpRequest object, and it is automatically passed to the view from our URL dispatcher.

The request contains information about the current HTTP request. For example, we can access the HTTP request method and create different logic for different methods.

python
1if request.method == 'GET':
2    do_something()
3elif request.method == 'POST':
4    do_something_else()

Here is a list of all the information you can access from request.

Second, notice that a shortcut called render() is used to pass the variable posts to the template blog/index.html.

This is called a shortcut because, by default, you are supposed to load the template with the loader() method, render that template with the retrieved data, and return an HttpResponse object. Django simplified this process with the render() method. To make life easier for you, we are not going to talk about the complex way here, since we will not use it in this tutorial anyway.

Here is a list of all shortcut functions in Django.

Django's template system

Now, let's talk about templates. The template layer is the frontend part of a Django application, which is why the template files are all HTML code since they are what you see in the browser, but things are slightly more complicated than that. If it contains only HTML code, the entire website would be static, and that is not what we want. So the template would have to tell the view function where to put the retrieved data.

Configurations for the template

Before we start, there is something you need to change in the settings.py, you must tell Django where you are putting the template files.

First, create the templates directory. FOr this tutorial, we choose to put it under the root directory of the project, but you can move it somewhere else if you want.

text
1.
2├── blog
3├── db.sqlite3
4├── djangoBlog
5├── env
6├── manage.py
7├── mediafiles
8├── staticfiles
9└── templates

Go to settings.py and find TEMPLATES.

python
1TEMPLATES = [
2    {
3        'BACKEND': 'django.template.backends.django.DjangoTemplates',
4        'DIRS': [
5            'templates',
6        ],
7        'APP_DIRS': True,
8        'OPTIONS': {
9            'context_processors': [
10                'django.template.context_processors.debug',
11                'django.template.context_processors.request',
12                'django.contrib.auth.context_processors.auth',
13                'django.contrib.messages.context_processors.messages',
14            ],
15        },
16    },
17]

Change the DIRS option, which points to the template folder. Now let's verify that this setting works. Create a new URL pattern that points to a test() view.

djangoBlog/urls.py

python
1from django.urls import path
2from blog import views
3
4urlpatterns = [
5    path('test/', views.test),
6]

Create the test() view:

blog/views.py

python
1def test(request):
2    return render(request, 'test.html')

Go to the templates folder and create a test.html template:

html
1<!doctype html>
2<html lang="en">
3  <head>
4    <meta charset="UTF-8" />
5    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7    <title>Test Page</title>
8  </head>
9  <body>
10    <p>This is a test page.</p>
11  </body>
12</html>

Start the dev server and go to http://127.0.0.1:8000/.

Django template

The Django template language

Now let's discuss Django's template engine in detail. Recall that we can send data from the view to the template like this:

python
1def test(request):
2    return render(request, 'test.html', {
3        'name': 'Jack'
4    })

The string 'Jack' is assigned to the variable name and passed to the template. And inside the template, we can display the name using double curly braces, {{ }}.

django
1<p>Hello, {{ name }}</p>

Refresh the browser, and you will see the output.

Display Data

However, in most cases, the data that is passed to the template is not a simple string. For example:

python
1def test(request):
2    post = Post.objects.get(pk=1)
3
4    return render(request, 'test.html', {
5        'post': post
6    })

In this case, the post variable is, in fact, a dictionary. You can access the items in that dictionary like this:

django
1{{ post.title }}
2{{ post.content }}
3{{ post.pub_date }}

Sometimes, instead of displaying the retrieved data directly, you might need to perform certain transformations first, such as turn then upper or lower cases. You can use filters to achieve this effect. For example, we have a variable django, with the value 'the web framework for perfectionists with deadlines'. If we put a title filter on this variable:

django
1{{ django|title }}

The template will be rendered into:

html
1The Web Framework For Perfectionists With Deadlines

Here is a full list of all built-in filters in Django.

Besides simple transformations, you might also want to add programming language features such as flow control and loops to HTML code. This is called tags in Django template engine. All tags are defined using {% %}.

For example, this is a for loop:

django
1<ul>
2    {% for athlete in athlete_list %}
3        <li>{{ athlete.name }}</li>
4    {% endfor %}
5</ul>

This is an if statement:

django
1{% if somevar == "x" %}
2  This appears if variable somevar equals the string "x"
3{% endif %}

And this is an if-else statement:

django
1{% if athlete_list %}
2    Number of athletes: {{ athlete_list|length }}
3{% elif athlete_in_locker_room_list %}
4    Athletes should be out of the locker room soon!
5{% else %}
6    No athletes.
7{% endif %}

Here is a full list of all built-in tags in Django. There are lots of other useful filters and tags in Django template system, we'll talk about them as we encounter specific problems.

The inheritance system

The primary benefit of using the Django template is that you do not need to write the same code over and over again. For example, in a typical web application, there is usually a navigation bar and a footer, which will appear on every page. Repeating all of these code on every page will make it very difficult for maintenance. Django offers us a easy way to solve this problem.

Let’s create a layout.html file in the templates folder. As the name suggests, this is the place where we define the layout of our template. To make this example easier to read, we skipped the code for the footer and navbar.

layout.html

django
1<!DOCTYPE html>
2<html>
3<head>
4    {% block meta %} {% endblock %}
5    <!-- Import CSS here -->
6</head>
7<body>
8
9<div class="container">
10    <!-- Put the navbar here -->
11
12    {% block content %} {% endblock %}
13
14    <!-- Put the footer here -->
15</div>
16</body>
17</html>

Notice that in this file, we defined two blocks, meta and content, using the {% block ... %} tag. next, we define a home.html template that uses this layout.

home.html

django
1{% extends 'layout.html' %}
2
3{% block meta %}
4    <title>Page Title</title>
5    <meta charset="UTF-8">
6    <meta name="description" content="Free Web tutorials">
7    <meta name="keywords" content="HTML, CSS, JavaScript">
8    <meta name="author" content="John Doe">
9    <meta name="viewport" content="width=device-width, initial-scale=1.0">
10{% endblock %}
11
12{% block content %}
13<p>This is the content section.</p>
14    {% include 'vendor/sidebar.html' %}
15{% endblock %}

When this template is called, Django will first find the layout.html file, and fill the meta and content blocks with snippets defined in this home.html page.

Also notice there is something else in this template. {% include 'vendor/sidebar.html' %} tells Django to look for the templates/vendor/sidebar.html and place it here.

sidebar.html

html
1<p>This is the sidebar.</p>

It is not exactly a sidebar, but we can use it to demonstrate this inheritance system works. Also make sure your corresponding view is correct.

python
1from django.shortcuts import render
2
3def home(request):
4    return render(request, 'home.html')

And make sure your URL dispatcher points to this view.

python
1path('home/', views.home),

Open your browser and go to http://127.0.0.1:8000/home, and you should see the following page:

template layout

Create your first app

We introduced many new concepts in the previous sections, and you probably feel a bit overwelmed. But don't worry, in this section, we will dig deeper and find out how the URL dispatchers, models, views, and templates can work together to create a functional Django application.

To make things easier to understand, we are not going to create a full-featured blog application with categories, tags and etc. Instead, we will create only a post page that displays a post article, a home page that shows a list of all articles, and a create/update/delete page that modifies the post.

Designing the database structure

Let's start with the model layer. The first thing you need to do is design the database structure. Since we are only dealing with posts right now, you can go ahead to the models.py file, and create a new Post model:

blog/models.py

python
1from django.db import models
2
3
4class Post(models.Model):
5    title = models.CharField(max_length=100)
6    content = models.TextField()

This Post contains only two fields, title, which is a CharField with a maximum of 100 characters, and content, which is a TextField.

Generate the corresponding migration files with the following command:

bash
1python manage.py makemigrations

Apply the migrations:

bash
1python manage.py migrate

The CRUD operations

Now, it is time for us to delve into the application itself. When building real-life applications, it is unlikely that you can create all the controllers first, and then design the templates, and then move on to the routers. Instead, you need to think from the user's perspective, and think about what actions the user might want to take.

In general, the user should have the ability to perform four operations on each resource, which in our case, is the Post.

  • Create: This operation is used to insert new data into the database.
  • Read: This operation is used to retrieve data from the database.
  • Update: This operation is used to modify existing data in the database.
  • Delete: This operation is used to remove data from the database.

Together, they are referred to as the CRUD operations.

  • The create action

First, let's start with the create action. Currently, the database is still empty, so the user must create a new post. To complete this create action, you need a URL dispatcher that points the URL pattern /post/create/ to the post_create() view function.

A flow control should be implemented inside the post_create() function, if the request method is GET, return a template that contains an HTML form, allowing the user to pass information to the backend. When the form is submitted, a POST request should be sent to the same view function, in which case a new Post resource should be created and saved.

Let us start with the URL dispatcher, go to urls.py:

djangoBlog/urls.py

python
1from django.urls import path
2from blog import views
3
4urlpatterns = [
5    path("post/create/", views.post_create, name="create"),
6]

Then you'll need a post_create() view function. Go to views.py and add the following code:

blog/views.py

python
1from django.shortcuts import redirect, render
2from .models import Post
3
4
5def post_create(request):
6    if request.method == "GET":
7        return render(request, "post/create.html")
8    elif request.method == "POST":
9        post = Post(title=request.POST["title"], content=request.POST["content"])
10        post.save()
11        return redirect("home")

The post_create() function first examines the method of the HTTP request, if it is a GET method, return the create.html template, if it is a POST, use the information passed by that POST request to create a new Post instance, and after it is done, redirect to the home page (we'll create this page in the next step).

Next, time to create the create.html template. First of all, you need a layout.html:

templates/layout.html

html
1<!doctype html>
2<html lang="en">
3  <head>
4    <meta charset="UTF-8" />
5    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7    <script src="https://cdn.tailwindcss.com"></script>
8    {% block title %}{% endblock %}
9  </head>
10
11  <body class="container mx-auto font-serif">
12    <div class="bg-white text-black font-serif">
13      <div id="nav">
14        <nav class="flex flex-row justify-between h-16 items-center shadow-md">
15          <div class="px-5 text-2xl">
16            <a href="/">My Blog</a>
17          </div>
18          <div class="hidden lg:flex content-between space-x-10 px-10 text-lg">
19            <a
20              href="{% url 'create' %}"
21              class="hover:underline hover:underline-offset-1"
22              >New Post</a
23            >
24            <a
25              href="https://github.com/thedevspacehq"
26              class="hover:underline hover:underline-offset-1"
27              >GitHub</a
28            >
29          </div>
30        </nav>
31      </div>
32
33      {% block content %}{% endblock %}
34
35      <footer class="bg-gray-700 text-white">
36        <div
37          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">
38          <p class="font-serif text-center mb-3 sm:mb-0">
39            Copyright ©
40            <a href="https://www.thedevspace.io/" class="hover:underline"
41              >TheDevSpace</a
42            >
43          </p>
44
45          <div class="flex justify-center space-x-4">. . .</div>
46        </div>
47      </footer>
48    </div>
49  </body>
50</html>

Notice the {% url 'create' %} on line 20. This is how you can reverse resolute URLs based on their names. The name create matches the name you gave to the post/create/ dispatcher.

We also added TailwindCSS through a CDN on line 7 to make this page look better, but you shouldn't do this in the production environment.

Next, create the create.html template. We choose to create a post directory to make it clear that this template is for creating a post, but you can do this differently as long as it makes sense to you:

templates/post/create.html

django
1{% extends 'layout.html' %}
2
3{% block title %}
4<title>Create</title>
5{% endblock %}
6
7{% block content %}
8<div class="w-96 mx-auto my-8">
9  <h2 class="text-2xl font-semibold underline mb-4">Create new post</h2>
10  <form action="{% url 'create' %}" method="POST">
11    {% csrf_token %}
12    <label for="title">Title:</label><br />
13
14    <input
15      type="text"
16      id="title"
17      name="title"
18      class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" />
19    <br />
20    <br />
21
22    <label for="content">Content:</label><br />
23
24    <textarea
25      type="text"
26      id="content"
27      name="content"
28      rows="15"
29      class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"></textarea>
30    <br />
31    <br />
32
33    <button
34      type="submit"
35      class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">
36      Submit
37    </button>
38  </form>
39</div>
40{% endblock %}

Line 10 specifies the action this form will take when it is submitted, as well as the request method it will use.

Line 11 adds CSRF protection to the form for security purposes.

Also pay attention to the <input> field on line 14 to 18. Its name attribute is very important. When the form is submitted, the user input will be tied to this name attribute, and you can then retrieve that input in the view function like this:

python
1title=request.POST["title"]

Same for the <textarea> on line 24 to 29.

And finally, the button must have type="submit" for it to work.

  • The list action

Now let's create a home page where you can show a list of all posts. You can start with the URL again:

djangoBlog/urls.py

python
1path("", views.post_list, name="home"),

And then the view function:

blog/views.py

python
1def post_list(request):
2    posts = Post.objects.all()
3    return render(request, "post/list.html", {"posts": posts})

The list.html template:

django
1{% extends 'layout.html' %}
2
3{% block title %}
4<title>My Blog</title>
5{% endblock %}
6
7{% block content %}
8<div class="max-w-screen-lg mx-auto my-8">
9  {% for post in posts %}
10  <h2 class="text-2xl font-semibold underline mb-2">
11    <a href="{% url 'show' post.pk %}">{{ post.title }}</a>
12  </h2>
13  <p class="mb-4">{{ post.content | truncatewords:50 }}</p>
14  {% endfor %}
15</div>
16{% endblock %}

The {% for post in posts %} iterates over all posts, and each item is assigned to the variable post.

The {% url 'show' post.pk %} passes the primary key of the post to the show URL dispatcher, which we'll create later.

And finally, {{ post.content | truncatewords:50 }} uses a filter truncatewords to truncate the content, so that only the first 50 words are kept.

  • The show action

Next, the show action should display the content of a particular post, which means its URL should contain something unique that allows Django to locate just one Post instance. That something is usually the primary key.

djangoBlog/urls.py

python
1path("post/<int:id>", views.post_show, name="show"),

The integer following post/ will be assigned to the variable id, and passed to the view function.

blog/views.py

python
1def post_show(request, id):
2    post = Post.objects.get(pk=id)
3    return render(request, "post/show.html", {"post": post})

And again, the corresponding template:

templates/post/show.html

django
1{% extends 'layout.html' %}
2
3{% block title %}
4<title>{{ post.title }}</title>
5{% endblock %}
6
7{% block content %}
8<div class="max-w-screen-lg mx-auto my-8">
9  <h2 class="text-2xl font-semibold underline mb-2">{{ post.title }}</h2>
10  <p class="mb-4">{{ post.content }}</p>
11
12  <a
13    href="{% url 'update' post.pk %}"
14    class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center"
15    >Update</a
16  >
17</div>
18{% endblock %}
  • The update action

The URL dispatcher:

djangoBlog/urls.py

python
1path("post/update/<int:id>", views.post_update, name="update"),

The view function:

blog/views.py

python
1def post_update(request, id):
2    if request.method == "GET":
3        post = Post.objects.get(pk=id)
4        return render(request, "post/update.html", {"post": post})
5    elif request.method == "POST":
6        post = Post.objects.update_or_create(
7            pk=id,
8            defaults={
9                "title": request.POST["title"],
10                "content": request.POST["content"],
11            },
12        )
13        return redirect("home")

The update_or_create() method is a new method added in Django 4.1.

The corresponding template:

templates/post/update.html

django
1{% extends 'layout.html' %}
2
3{% block title %}
4<title>Update</title>
5{% endblock %}
6
7{% block content %}
8<div class="w-96 mx-auto my-8">
9  <h2 class="text-2xl font-semibold underline mb-4">Update post</h2>
10  <form action="{% url 'update' post.pk %}" method="POST">
11    {% csrf_token %}
12    <label for="title">Title:</label><br />
13
14    <input
15      type="text"
16      id="title"
17      name="title"
18      value="{{ post.title }}"
19      class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" />
20    <br />
21    <br />
22
23    <label for="content">Content:</label><br />
24
25    <textarea
26      type="text"
27      id="content"
28      name="content"
29      rows="15"
30      class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300">
31{{ post.content }}</textarea
32    >
33    <br />
34    <br />
35
36    <div class="grid grid-cols-2 gap-x-2">
37      <button
38        type="submit"
39        class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">
40        Submit
41      </button>
42      <a
43        href="{% url 'delete' post.pk %}"
44        class="font-sans text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center"
45        >Delete</a
46      >
47    </div>
48  </form>
49</div>
50{% endblock %}
  • The delete action

Finally, for the delete action:

djangoBlog/urls.py

python
1path("post/delete/<int:id>", views.post_delete, name="delete"),

blog/views.py

python
1def post_delete(request, id):
2    post = Post.objects.get(pk=id)
3    post.delete()
4    return redirect("home")

This action does not require a template, since it just redirects you to the home page after the action is completed.

Finally, let's start the dev server and see the result.

bash
1python manage.py runserver

The home page:

The home page

The create page:

The create page

The show page:

The show page

The update page:

The update page

One step forward

Finally, it is time for us to take one last step forward, and create a fully featured blog application using Django. We explored how the model, view, and template may work together to create a Django application in the previous section, 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 section, 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.

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
keytypeinfo
idintegerauto increment
namestring
emailstringunique
passwordstring

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
keytypeinfo
idintegerauto increment
namestring
slugstringunique
descriptiontext
  • The Tag model
keytypeinfo
idintegerauto increment
namestring
slugstringunique
descriptiontext
  • The Post model
keytypeinfo
idintegerauto increment
titlestring
slugstringunique
contenttext
featured_imagestring
is_publishedboolean
is_featuredboolean
created_atdate
  • 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.

keytypeinfo
namestring
descriptiontext
logostring

And finally, 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

Next, it’s time to implement this design.

First of all, you need a Site model.

python
1class Site(models.Model):
2    name = models.CharField(max_length=200)
3    description = models.TextField()
4    logo = models.ImageField(upload_to="logo/")
5
6    class Meta:
7        verbose_name_plural = "site"
8
9    def __str__(self):
10        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:

bash
1pip install Pillow

The Category model should be easy to understand.

python
1class Category(models.Model):
2    name = models.CharField(max_length=200)
3    slug = models.SlugField(unique=True)
4    description = models.TextField()
5
6    class Meta:
7        verbose_name_plural = "categories"
8
9    def __str__(self):
10        return self.name

However, we do need to talk about 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" in this particular aspect, if you don't 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.

And next, we have the Tag model.

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

As well as the Post model.

python
1from ckeditor.fields import RichTextField
2
3. . .
4
5class Post(models.Model):
6    title = models.CharField(max_length=200)
7    slug = models.SlugField(unique=True)
8    content = RichTextField()
9    featured_image = models.ImageField(upload_to="images/")
10    is_published = models.BooleanField(default=False)
11    is_featured = models.BooleanField(default=False)
12    created_at = models.DateField(auto_now=True)
13
14    # Define relations
15    category = models.ForeignKey(Category, on_delete=models.CASCADE)
16    tag = models.ManyToManyField(Tag)
17    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
18
19    def __str__(self):
20        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 section, you can only add plain text when creating a new post, and that 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, we are using the CKEditor as an example.

CKEditor

To install CKEditor, run the following command:

bash
1pip install django-ckeditor

After that, register ckeditor in settings.py:

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

Finally, you must add relations to the models. For this project, we only need to add three lines of code in the Post model:

python
1category = models.ForeignKey(Category, on_delete=models.CASCADE)
2tag = models.ManyToManyField(Tag)
3user = 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
1from django.conf import settings

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

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

Set up the admin panel

Our next step would be setting 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:

bash
1python 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

python
1from django.contrib import admin
2from .models import Site, Category, Tag, Post
3
4
5# Register your models here.
6class CategoryAdmin(admin.ModelAdmin):
7    prepopulated_fields = {"slug": ("name",)}
8
9
10class TagAdmin(admin.ModelAdmin):
11    prepopulated_fields = {"slug": ("name",)}
12
13
14class PostAdmin(admin.ModelAdmin):
15    prepopulated_fields = {"slug": ("title",)}
16
17
18admin.site.register(Site)
19admin.site.register(Category, CategoryAdmin)
20admin.site.register(Tag, TagAdmin)
21admin.site.register(Post, PostAdmin)

On line 2, import the models you just created, and then register the imported model using admin.site.register(). 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.

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 add a InlineModelAdmin.

blog/admin.py

python
1class PostInlineCategory(admin.StackedInline):
2    model = Post
3    max_num = 2
4
5
6class CategoryAdmin(admin.ModelAdmin):
7    prepopulated_fields = {"slug": ("name",)}
8    inlines = [
9        PostInlineCategory
10    ]

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

python
1class PostInlineTag(admin.TabularInline):
2    model = Post.tag.through
3    max_num = 5
4
5
6class TagAdmin(admin.ModelAdmin):
7    prepopulated_fields = {"slug": ("name",)}
8    inlines = [
9        PostInlineTag
10    ]

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

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
1from .models import Site, Category, Tag, Post
2
3def home(request):
4    site = Site.objects.first()
5    posts = Post.objects.all().filter(is_published=True)
6    categories = Category.objects.all()
7    tags = Tag.objects.all()
8
9    return render(request, 'home.html', {
10        'site': site,
11        'posts': posts,
12        'categories': categories,
13        'tags': tags,
14    })

Line 1, import the necessary models.

Line 4, site contains the basic information of our website, and we 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
1path('', views.home, name='home'),
  • The category view

blog/views.py

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

djangoBlog/urls.py

python
1path('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
1def tag(request, slug):
2    site = Site.objects.first()
3    posts = Post.objects.filter(tag__slug=slug).filter(is_published=True)
4    requested_tag = Tag.objects.get(slug=slug)
5    categories = Category.objects.all()
6    tags = Tag.objects.all()
7
8    return render(request, 'tag.html', {
9        'site': site,
10        'posts': posts,
11        'tag': requested_tag,
12        'categories': categories,
13        'tags': tags,
14    })

djangoBlog/urls.py

python
1path('tag/<slug:slug>', views.tag, name='tag'),
  • The post view

blog/views.py

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

djangoBlog/urls.py

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

Create the template layer

For the templates, instead of writing your own code, you may use the template we've created here.

Blog template

This is the template structure we are going with.

text
1templates
2├── category.html
3├── home.html
4├── layout.html
5├── post.html
6├── search.html
7├── tag.html
8└── vendor
9    ├── list.html
10    └── 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.html

django
1<!DOCTYPE html>
2<html lang="en">
3  <head>
4    <meta charset="UTF-8" />
5    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7    {% load static %}
8    <link rel="stylesheet" href="{% static 'style.css' %}" />
9    {% block title %}{% endblock %}
10  </head>
11
12  <body class="container mx-auto font-serif">
13    <nav class="flex flex-row justify-between h-16 items-center border-b-2">
14      <div class="px-5 text-2xl">
15        <a href="/"> My Blog </a>
16      </div>
17      <div class="hidden lg:flex content-between space-x-10 px-10 text-lg">
18        <a
19          href="https://github.com/thedevspacehq"
20          class="hover:underline hover:underline-offset-1"
21          >GitHub</a
22        >
23        <a href="#" class="hover:underline hover:underline-offset-1">Link</a>
24        <a href="#" class="hover:underline hover:underline-offset-1">Link</a>
25      </div>
26    </nav>
27
28    {% block content %}{% endblock %}
29
30    <footer class="bg-gray-700 text-white">
31      <div
32        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">
33        <p class="font-serif text-center mb-3 sm:mb-0">
34          Copyright ©
35          <a href="https://www.ericsdevblog.com/" class="hover:underline"
36            >Eric Hu</a
37          >
38        </p>
39
40        <div class="flex justify-center space-x-4">. . .</div>
41      </div>
42    </footer>
43  </body>
44</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 covering CSS in this tutorial, but we have 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
1blog
2├── admin.py
3├── apps.py
4├── __init__.py
5├── migrations
6├── models.py
7├── static
8│   ├── input.css
9│   └── style.css
10├── tests.py
11└── views.py

home.html

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

Home page

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.

vendor/list.html

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

From line 3 to 25, 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, remember that we created a URL dispatcher like this:

python
1path('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 17, 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.

vendor/sidebar.html

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

category.html

django
1{% extends 'layout.html' %}
2
3{% block title %}
4<title>Page Title</title>
5{% endblock %}
6
7{% block content %}
8<div class="grid grid-cols-4 gap-4 py-10">
9  <div class="col-span-3 grid grid-cols-1">
10    {% include "vendor/list.html" %}
11  </div>
12  {% include "vendor/sidebar.html" %}
13</div>
14{% endblock %}

tag.html

django
1{% extends 'layout.html' %}
2
3{% block title %}
4<title>Page Title</title>
5{% endblock %}
6
7{% block content %}
8<div class="grid grid-cols-4 gap-4 py-10">
9  <div class="col-span-3 grid grid-cols-1">
10    {% include "vendor/list.html" %}
11  </div>
12  {% include "vendor/sidebar.html" %}
13</div>
14{% endblock %}

post.html

django
1{% extends 'layout.html' %}
2
3{% block title %}
4<title>Page Title</title>
5{% endblock %}
6
7{% block content %}
8<div class="grid grid-cols-4 gap-4 py-10">
9  <div class="col-span-3">
10    <img
11      class="rounded-md object-cover h-96 w-full"
12      src="{{ post.featured_image.url }}"
13      alt="..." />
14    <h2 class="mt-5 mb-2 text-center text-2xl font-bold">{{ post.title }}</h2>
15    <p class="mb-5 text-center text-sm text-slate-500 italic">
16      By {{ post.user|capfirst }} | {{ post.created_at }}
17    </p>
18
19    <div>{{ post.content|safe }}</div>
20
21    <div class="my-5">
22      {% for tag in post.tag.all %}
23      <a
24        href="{% url 'tag' tag.slug %}"
25        class="text-blue-500 hover:underline mr-3"
26        >#{{ tag.name }}</a
27      >
28      {% endfor %}
29    </div>
30  </div>
31  {% include "vendor/sidebar.html" %}
32</div>
33{% endblock %}

post page

One last thing we need to talk about is line 19, notice that 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:

python
1python manage.py runserver

Create pagination in Django

Before we wrap up this article, let's add some advanced features for our Django blog app, including a paginator, related posts, as well as a search feature.

Paginator

As your blog scales, creating a paginator might be a good idea, since you don’t want too many posts on a single page. To do that, you need to add some extra code to the view functions. Let's take the home view as an example.

First, you must import some necessary packages:

python
1from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

Update the home view:

python
1def home(request):
2    site = Site.objects.first()
3    categories = Category.objects.all()
4    tags = Tag.objects.all()
5    featured_post = Post.objects.filter(is_featured=True).first()
6
7    # Add Paginator
8    page = request.GET.get("page", "")  # Get the current page number
9    posts = Post.objects.all().filter(is_published=True)
10    paginator = Paginator(posts, n)  # Showing n post for every page
11
12    try:
13        posts = paginator.page(page)
14    except PageNotAnInteger:
15        posts = paginator.page(1)
16    except EmptyPage:
17        posts = paginator.page(paginator.num_pages)
18
19    return render(
20        request,
21        "home.html",
22        {
23            "site": site,
24            "posts": posts,
25            "categories": categories,
26            "tags": tags,
27            "featured_post":featured_post
28        },
29    )

Line 12 to 17, here you must consider three different conditions. If the page number is an integer, return the requested page; if the page number is not an integer, return page 1; if the page number is larger than the number of pages, return the last page.

Next, you need to put the paginator in the template, along with the list of posts like this:

templates/vendor/list.html

django
1<!-- Paginator -->
2<nav
3  class="isolate inline-flex -space-x-px rounded-md mx-auto my-5 max-h-10"
4  aria-label="Pagination">
5  {% if posts.has_previous %}
6  <a
7    href="?page={{ posts.previous_page_number }}"
8    class="relative inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20">
9    <span class="sr-only">Previous</span>
10    <!-- Heroicon name: mini/chevron-left -->
11    <svg
12      class="h-5 w-5"
13      xmlns="http://www.w3.org/2000/svg"
14      viewBox="0 0 20 20"
15      fill="currentColor"
16      aria-hidden="true">
17      . . .
18    </svg>
19  </a>
20  {% endif %}
21
22  {% for i in posts.paginator.page_range %}
23
24  {% if posts.number == i %}
25  <a
26    href="?page={{ i }}"
27    aria-current="page"
28    class="relative z-10 inline-flex items-center border border-blue-500 bg-blue-50 px-4 py-2 text-sm font-medium text-blue-600 focus:z-20"
29    >{{ i }}</a
30  >
31  {% else %}
32  <a
33    href="?page={{ i }}"
34    aria-current="page"
35    class="relative inline-flex items-center border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20"
36    >{{ i }}</a
37  >
38  {% endif %}
39
40  {% endfor %}
41
42  {% if posts.has_next %}
43  <a
44    href="?page={{ posts.next_page_number }}"
45    class="relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20">
46    <span class="sr-only">Next</span>
47    <!-- Heroicon name: mini/chevron-right -->
48    <svg
49      class="h-5 w-5"
50      xmlns="http://www.w3.org/2000/svg"
51      viewBox="0 0 20 20"
52      fill="currentColor"
53      aria-hidden="true">
54      . . .
55    </svg>
56  </a>
57  {% endif %}
58</nav>

You should do the same for all the pages that contain a list of posts.

related posts

The idea is to get the posts with the same tags.

python
1def post(request, slug):
2    site = Site.objects.first()
3    requested_post = Post.objects.get(slug=slug)
4    categories = Category.objects.all()
5    tags = Tag.objects.all()
6
7    # Related Posts
8    ## Get all the tags related to this article
9    post_tags = requested_post.tag.all()
10    ## Filter all posts that contain tags which are related to the current post, and exclude the current post
11    related_posts_ids = (
12        Post.objects.all()
13        .filter(tag__in=post_tags)
14        .exclude(id=requested_post.id)
15        .values_list("id")
16    )
17
18    related_posts = Post.objects.filter(pk__in=related_posts_ids)
19
20    return render(
21        request,
22        "post.html",
23        {
24            "site": site,
25            "post": requested_post,
26            "categories": categories,
27            "tags": tags,
28            "related_posts": related_posts,
29        },
30    )

This code is a little difficult to understand, but don't worry, let's analyze it line by line.

Line 3, get the requested post using the slug.

Line 9, get all the tags that belongs to the requested post.

Line 11 to 16, this is where things get tricky. First, Post.objects.all() retrieves all posts from the database. And then, filter(tag__in=post_tags) retrieves all posts that have tags which are related to the current post.

However, we have two problems. First, the current post will also be included in the query set, so we use exclude(id=requested_post.id) to exclude the current post.

The second problem, however, is not that easy to understand. Let's consider this scenario. Here we have three posts and three tags.

Tag IDTag Name
1Tag 1
2Tag 2
3Tag 3
Post IDPost Name
1Post 1
2Post 2
3Post 3

And the posts and tags have a many-to-many relationship to each other.

Tag IDPost ID
12
13
11
21
22
23
32
Post IDTag ID
11
12
21
22
23
31
32

Let's say our current post is post 2, that means our related tags will be 1, 2 and 3. Now, when you are using filter(tag__in=post_tags), Django will first go to tag 1 and find its related posts, which is post 2, 3 and 1, and then go to tag 2 to find its related posts, and finally move onto tag 3.

This means filter(tag__in=post_tags) will eventually return [2,3,1,1,2,3,2]. After the exclude() method, it would return [3,1,1,3]. This is still not what we want, we need to find a way to get rid of the duplicates.

This is why we need to use values_list('id') to pass the post ids to the variable related_posts_ids and then use that variable to retrieve the related posts. This way will eliminate the duplicates.

Finally, we can display the related posts in the corresponding template:

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

Implement search in Django

Lastly, you can add a search feature for your app. To create a search feature, you need a search form in the frontend, which will send the search query to the view, and the view function will retrieve the target records from the database, and finally return a search page that will display the result.

First, let's add a search form to your sidebar:

templates/vendor/sidebar.html

django
1<div class="col-span-1">
2  <div class="border rounded-md mb-4">
3    <div class="bg-slate-200 p-4">Search</div>
4    <div class="p-4">
5      <form
6        action="{% url 'search' %}"
7        method="POST"
8        class="grid grid-cols-4 gap-2">
9        {% csrf_token %}
10        <input
11          type="text"
12          name="q"
13          id="search"
14          class="border rounded-md w-full focus:ring p-2 col-span-3"
15          placeholder="Search something..." />
16        <button
17          type="submit"
18          class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1">
19          Search
20        </button>
21      </form>
22    </div>
23  </div>
24  . . .
25</div>

Line 10 to 15, notice the name attribute of the input field, here we’ll call it q. The user input will be tied to the variable q and sent to the backend.

Line 5 to 8, when the button is clicked, the user will be routed to the URL with the name search, so you need to register the corresponding URL pattern.

python
1path('search', views.search, name='search'),

And we also need to search method:

python
1def search(request):
2    site = Site.objects.first()
3    categories = Category.objects.all()
4    tags = Tag.objects.all()
5
6    query = request.POST.get("q", "")
7    if query:
8        posts = Post.objects.filter(is_published=True).filter(title__icontains=query)
9    else:
10        posts = []
11    return render(
12        request,
13        "search.html",
14        {
15            "site": site,
16            "categories": categories,
17            "tags": tags,
18            "posts": posts,
19            "query": query,
20        },
21    )

And a template to display the search result.

templates/search.html

django
1{% extends 'layout.html' %}
2
3{% block title %}
4<title>Page Title</title>
5{% endblock %}
6
7{% block content %}
8<div class="grid grid-cols-4 gap-4 py-10">
9  <div class="col-span-3 grid grid-cols-1">
10    {% include "vendor/list.html" %}
11  </div>
12  {% include "vendor/sidebar.html" %}
13</div>
14{% endblock %}

Now try to search something in the search form, and you should be returned only the posts you are requesting.

Interested in Learning Web Development?

Starting from HTML, CSS to JavaScript, from language syntaxes to frameworks, from the frontend to the backend, we'll guide you through every step of your coding journey.

Get started from here 🎉