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.