Capstone: A Blog App
This capstone brings together everything you've learned by building a complete Django blog application — models for posts and comments, the admin, URL routing, class-based and generic views, templates with inheritance, forms, authentication, and tests — assembled into one working project from start to finish.
Learn Capstone: A Blog App in our free Django course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Django course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Instead of introducing a brand-new idea, this final lesson is a guided build. We'll wire each piece together in the order you'd build a real site, so by the end you'll have a blog you understand top to bottom and a template you can reuse for your own projects.
Every blog starts with its data. The Post model is a Python class that maps to a database table. It links each post to its author with a ForeignKey , stores a URL-friendly slug , the body text, and created / published timestamps. The __str__ method gives each post a readable name, and get_absolute_url tells Django where a single post lives.
Once the model exists, you run python manage.py makemigrations and migrate to create the table, then register the model so it appears in Django's admin:
Here is the shape of that model in plain Python, so you can see how a post object behaves before a database is involved.
Now we expose the posts to the web. Django's generic views handle the common cases for us: ListView for the post list, DetailView for one post, and CreateView for a new post. We protect CreateView with LoginRequiredMixin so only signed-in users can post.
Each view is connected to a URL in the app's urls.py . The names ( post_list , post_detail ) are what get_absolute_url and the templates refer to.
Templates use inheritance . A shared base.html defines the page shell with a {' '} hole; each page fills that hole with {' '} and its own {' '} .
A blog needs conversation. The Comment model has a ForeignKey to Post , so one post can have many comments. The related_name="comments" lets a template read post.comments.all .
A ModelForm turns that model into an HTML form automatically — you just list the fields you want users to fill in.
Finally, a test proves the list view works. Django's TestCase uses a temporary database and a test client to request the page and check the response. The example below is plain unittest so it runs right here in the editor; the comment shows the equivalent Django test.
A ListView needs to know which model to list. Replace ___ so the view lists Post objects, then print the configured attributes.
❌ TypeError: __init__() missing 1 required positional argument: 'on_delete'
You declared a ForeignKey without telling Django what to do when the parent row is deleted.
✅ Fix: add the argument, e.g. models.ForeignKey(Post, on_delete=models.CASCADE) .
❌ Anyone can reach the "new post" page without logging in
You forgot to protect the create view, so unauthenticated users can post.
✅ Fix: add LoginRequiredMixin as the first base class: class PostCreateView(LoginRequiredMixin, CreateView) .
Django can't find the template because the path or folder nesting is wrong.
✅ Fix: put templates in blog/templates/blog/ and reference them as "blog/post_list.html" .
Extend the blog two ways. In Django you'd add a tags = models.ManyToManyField(Tag) to Post and override get_queryset on the list view to show only published posts. Practice the filtering logic below in plain Python.
🎉 You finished the entire Django course!
This is the final lesson — and you didn't just read about Django, you built a real blog with it. You created models for posts and comments, registered them in the admin, routed URLs to generic class-based views, protected actions with authentication, shared layout through template inheritance, accepted input with a ModelForm, and proved the whole thing works with a test.
That means you now understand the full Django stack — the ORM, the admin, URLs, views, templates, forms, auth, and testing — and how the pieces fit together into one working project. Every database-driven site you build from here is a variation on what you just made.
🏆 Congratulations — keep this blog as your starter project, extend it with your own ideas, and go build something real. Well done!
Practice quiz
Which generic view shows a single object?
- ListView
- DetailView
- CreateView
- TemplateView
Answer: DetailView. DetailView renders one object; ListView shows a list and CreateView a form.
Which generic view displays a list of objects?
- ListView
- DetailView
- FormView
- RedirectView
Answer: ListView. ListView is the pre-built CBV for showing a list of model instances.
Which mixin restricts the create view to logged-in users?
- PermissionRequiredMixin
- ContextMixin
- UserPassesTestMixin
- LoginRequiredMixin
Answer: LoginRequiredMixin. LoginRequiredMixin, listed before the base view, blocks anonymous users.
What relationship does a Comment with a ForeignKey to Post create?
- One-to-one
- Many-to-many
- Many-to-one (one post, many comments)
- No relationship
Answer: Many-to-one (one post, many comments). A ForeignKey is many-to-one: many comments belong to one post.
What does on_delete=models.CASCADE do when a post is deleted?
- Deletes the post's comments too
- Sets comments to null
- Blocks the deletion
- Archives the post
Answer: Deletes the post's comments too. CASCADE deletes the related comments when the parent post is deleted.
What lets you read a post's comments as post.comments.all()?
- on_delete
- related_name on the ForeignKey
- context_object_name
- get_absolute_url
Answer: related_name on the ForeignKey. related_name='comments' names the reverse accessor from Post to its comments.
What does a ModelForm give you?
- A database migration
- A URL route
- An HTML form built automatically from a model
- A test client
Answer: An HTML form built automatically from a model. A ModelForm derives form fields from the model so you list the fields you want.
Which base class spins up a temporary database and test client?
- unittest.TestCase only
- models.Model
- django.test.TestCase
- ListView
Answer: django.test.TestCase. Django's TestCase provides a temp database and self.client to exercise views.
What does get_absolute_url typically call to build a URL?
- reverse()
- redirect()
- render()
- include()
Answer: reverse(). get_absolute_url uses reverse('blog:post_detail', args=[...]) to build the URL.
Where must templates live to be found as 'blog/post_list.html'?
- The project root
- blog/templates/blog/
- static/blog/
- blog/html/
Answer: blog/templates/blog/. App templates go in app/templates/app/ so they are namespaced and found correctly.