Today’s goal
Move the posts out of views.py and save them in Django’s SQLite database. You will create and edit posts through Django admin, then display them on the homepage.
The change is:
Before: views.py → temporary list → template
After: SQLite → Post objects → view → template
Step 1 — Create the Post model
Replace blog/models.py with:
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ("-created_at",)
def __str__(self):
return self.title
Each Post object becomes one database row:
| Field | What it stores |
|---|---|
title | Short text, up to 200 characters |
body | The full article text |
created_at | The date and time Django creates the post |
Django also creates an integer id automatically.
Step 2 — Create and apply the migration
The model is Python code. A migration turns that code into a database change.
python manage.py makemigrations
python manage.py migrate
Remember the order:
- Change
models.py. - Run
makemigrationsto create a recipe. - Run
migrateto apply that recipe.
Step 3 — Register Post in admin
Replace blog/admin.py with:
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ("title", "created_at")
search_fields = ("title", "body")
Step 4 — Create an admin account
python manage.py createsuperuser
Choose a username and password. The terminal does not display password characters while you type; that is normal.
Run the server:
python manage.py runserver
Open http://127.0.0.1:8000/admin/, sign in, open Posts, and create at least three posts.
You can reuse these ideas:
- Learning Python one step at a time
- My first Django page
- Why HTML matters
Step 5 — Query the database in the view
Replace blog/views.py with:
from django.db.models import Q
from django.shortcuts import render
from .models import Post
def home(request):
query = request.GET.get("q", "").strip()
posts = Post.objects.all()
if query:
posts = posts.filter(
Q(title__icontains=query) | Q(body__icontains=query)
)
context = {
"site_title": "My First Blog",
"posts": posts,
"query": query,
}
return render(request, "blog/index.html", context)
Post.objects.all() asks for every post. Django returns a QuerySet, which behaves like a list in the template.
icontains means “contains this text, ignoring case.” Q(...) | Q(...) means the title or body can match.
Step 6 — Update the post cards
Your template already loops over posts. Change the inside of each card so it uses model fields:
<article class="post-card">
<h3>{{ post.title }}</h3>
<p>Published {{ post.created_at|date:"j F Y" }}</p>
<p>{{ post.body|truncatewords:25 }}</p>
</article>
truncatewords:25 shows a preview instead of the whole body.
Step 7 — Prove the database is now the source
- Create a new post in
/admin/. - Return to
/and refresh. - The new post should appear without changing Python.
- Edit its title in admin and refresh again.
- Search for a word inside its body.
Delete the old hardcoded list from views.py. There should now be only one source of posts: the database.
Checkpoint
-
makemigrationsandmigratefinish without errors. - Posts appear in
/admin/. - Creating a post in admin makes it appear on
/. - Newest posts appear first.
- Search matches both the title and body.
- No temporary post dictionaries remain in
views.py.
If it does not work
| What you see | Check this |
|---|---|
no such table: blog_post | Run python manage.py migrate. |
| Post is missing from admin | Register Post in blog/admin.py. |
NameError: Q is not defined | Import Q from django.db.models. |
| Homepage shows nothing | Create posts in admin and confirm context uses "posts". |
| Search only matches titles | Include Q(body__icontains=query). |
Stop here
Today is complete when admin can create a post and the homepage displays it. Tomorrow each title will open its own page with the complete article.
