In this lesson, we will build a practical Django Blog Project. A blog is an excellent project for understanding how models, forms, templates, views, URLs, CRUD operations, authentication, search, and pagination work together in a real Django application.
Our basic blog system will allow users to create posts, view posts, edit posts, delete posts, search posts, and display posts with pagination.
A blog is a website where users can publish and read articles or posts.
A basic blog can contain:
Our Django blog project can contain the following features:
Create a new Django project:
django-admin startproject blog_project
Move into the project:
cd blog_project
Start the development server:
python manage.py runserver
Create an application named blog.
python manage.py startapp blog
Add it to INSTALLED_APPS:
INSTALLED_APPS = [
...
"blog",
]
The Post model will store blog information.
from django.db import models
class Post(models.Model):
title = models.CharField(
max_length=200
)
content = models.TextField()
author = models.CharField(
max_length=100
)
created_at = models.DateTimeField(
auto_now_add=True
)
updated_at = models.DateTimeField(
auto_now=True
)
def __str__(self):
return self.title
Create the migration:
python manage.py makemigrations
Apply the migration:
python manage.py migrate
Django will create the database table for blog posts.
Create blog/forms.py.
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = [
"title",
"content",
"author"
]
The ModelForm generates form fields from the Post model.
The list view retrieves all blog posts.
from django.shortcuts import render
from .models import Post
def post_list(request):
posts = Post.objects.all().order_by(
"-created_at"
)
return render(
request,
"blog/post_list.html",
{
"posts": posts
}
)
The newest posts are displayed first.
Create post_list.html.
<h1>Latest Blog Posts</h1>
{% for post in posts %}
<article>
<h2>
{{ post.title }}
</h2>
<p>
By {{ post.author }}
</p>
<p>
{{ post.content|truncatewords:30 }}
</p>
<a href="{% url 'post_detail' post.id %}">
Read More
</a>
</article>
{% empty %}
<p>
No blog posts available.
</p>
{% endfor %}
The detail view displays one complete blog post.
from django.shortcuts import get_object_or_404
def post_detail(request, id):
post = get_object_or_404(
Post,
id=id
)
return render(
request,
"blog/post_detail.html",
{
"post": post
}
)
<article>
<h1>
{{ post.title }}
</h1>
<p>
By {{ post.author }}
</p>
<p>
Published:
{{ post.created_at }}
</p>
<div>
{{ post.content|linebreaks }}
</div>
</article>
The create view allows a user to add a new blog post.
from django.shortcuts import redirect
from .forms import PostForm
def post_create(request):
if request.method == "POST":
form = PostForm(
request.POST
)
if form.is_valid():
form.save()
return redirect(
"post_list"
)
else:
form = PostForm()
return render(
request,
"blog/post_form.html",
{
"form": form
}
)
<h2>
Create Blog Post
</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button
type="submit"
>
Publish Post
</button>
</form>
The CSRF token should be included in POST forms.
The same ModelForm can be used to edit an existing post.
def post_update(request, id):
post = get_object_or_404(
Post,
id=id
)
if request.method == "POST":
form = PostForm(
request.POST,
instance=post
)
if form.is_valid():
form.save()
return redirect(
"post_detail",
id=post.id
)
else:
form = PostForm(
instance=post
)
return render(
request,
"blog/post_form.html",
{
"form": form,
"post": post
}
)
The delete view removes an existing blog post.
def post_delete(request, id):
post = get_object_or_404(
Post,
id=id
)
if request.method == "POST":
post.delete()
return redirect(
"post_list"
)
return render(
request,
"blog/post_confirm_delete.html",
{
"post": post
}
)
<h2>
Delete Blog Post
</h2>
<p>
Are you sure you want to delete
"{{ post.title }}"?
</p>
<form method="post">
{% csrf_token %}
<button
type="submit"
>
Yes, Delete
</button>
</form>
<a href="{% url 'post_detail' post.id %}">
Cancel
</a>
Create blog/urls.py.
from django.urls import path
from . import views
urlpatterns = [
path(
"",
views.post_list,
name="post_list"
),
path(
"create/",
views.post_create,
name="post_create"
),
path(
"<int:id>/",
views.post_detail,
name="post_detail"
),
path(
"<int:id>/edit/",
views.post_update,
name="post_update"
),
path(
"<int:id>/delete/",
views.post_delete,
name="post_delete"
),
]
Include the blog URLs in the project's main
urls.py.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path(
"admin/",
admin.site.urls
),
path(
"",
include("blog.urls")
),
]
The blog homepage can now be accessed from the root URL.
Users can search blog posts by title.
query = request.GET.get(
"q",
""
).strip()
posts = Post.objects.all()
if query:
posts = posts.filter(
title__icontains=query
)
posts = posts.order_by(
"-created_at"
)
<form method="get">
<input
type="text"
name="q"
value="{{ query }}"
placeholder="Search posts"
>
<button
type="submit"
>
Search
</button>
</form>
The search value is sent as a GET parameter.
Pagination is useful when a blog contains many posts.
from django.core.paginator import Paginator
paginator = Paginator(
posts,
5
)
page_number = request.GET.get(
"page"
)
page_obj = paginator.get_page(
page_number
)
Here, five blog posts are displayed per page.
{% if page_obj.has_previous %}
<a href="?q={{ query }}&page={{
page_obj.previous_page_number
}}">
Previous
</a>
{% endif %}
<span>
Page {{ page_obj.number }}
of
{{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?q={{ query }}&page={{
page_obj.next_page_number
}}">
Next
</a>
{% endif %}
Messages can inform users when posts are created, updated, or deleted.
from django.contrib import messages
form.save()
messages.success(
request,
"Blog post published successfully."
)
return redirect(
"post_list"
)
Messages can also be added after update and delete operations.
Register the Post model in blog/admin.py.
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = (
"title",
"author",
"created_at",
"updated_at"
)
search_fields = (
"title",
"content",
"author"
)
ordering = (
"-created_at",
)
Only authenticated users may be allowed to create, edit, or delete posts.
from django.contrib.auth.decorators import login_required
@login_required
def post_create(request):
if request.method == "POST":
form = PostForm(
request.POST
)
if form.is_valid():
form.save()
return redirect(
"post_list"
)
else:
form = PostForm()
return render(
request,
"blog/post_form.html",
{
"form": form
}
)
blog_project/
│
├── manage.py
│
├── blog_project/
│ ├── settings.py
│ ├── urls.py
│ ├── asgi.py
│ └── wsgi.py
│
└── blog/
├── migrations/
├── templates/
│ └── blog/
│ ├── post_list.html
│ ├── post_detail.html
│ ├── post_form.html
│ └── post_confirm_delete.html
│
├── __init__.py
├── admin.py
├── apps.py
├── forms.py
├── models.py
├── urls.py
├── views.py
└── tests.py
from django.shortcuts import render
from django.core.paginator import Paginator
from .models import Post
def post_list(request):
query = request.GET.get(
"q",
""
).strip()
posts = Post.objects.all()
if query:
posts = posts.filter(
title__icontains=query
)
posts = posts.order_by(
"-created_at"
)
paginator = Paginator(
posts,
5
)
page_number = request.GET.get(
"page"
)
page_obj = paginator.get_page(
page_number
)
return render(
request,
"blog/post_list.html",
{
"page_obj": page_obj,
"query": query
}
)
Blog Model
↓
Database Migration
↓
ModelForm
↓
Create Post
↓
List Posts
↓
View Post
↓
Edit Post
↓
Delete Post
↓
Search Posts
↓
Pagination
↓
Authentication
↓
Admin Panel
These components work together to create a complete basic blog application.
The basic blog can be extended with many advanced features.
The Django Blog Project combines many concepts learned throughout the Django tutorial into one practical application.
Question: Which Django component is commonly used to create a form directly from the Blog Post model?