Project: Full-Stack Blog (MTV)
Introduction
This capstone assembles everything from the server-rendered half of the Django track into one blog application: models with categories and tags, Django Admin for editors, public list/detail pages with pagination, and authenticated users creating posts through forms. You practice the MTV pattern end to end—SQLite locally, MySQL optional on deploy.
Prerequisites
- Models and ORM Basics
- QuerySets and Relationships
- Templates and Static Files
- Django Admin
- Forms and CSRF
- User Authentication
Project Goals
| Feature | URL / area |
|---|---|
| Post list with pagination | / or /posts/ |
| Post detail | /posts/<id>/ |
| Category filter | /category/<slug>/ |
| Create/edit post (logged in) | /posts/new/, /posts/<id>/edit/ |
| Admin backend | /admin/ |
| Login / signup | /accounts/ |
Models: Post, Category, Tag (M2M).
Step 1: Project Layout
Create fresh or extend hello-django from earlier chapters.
Step 2: Models
blog/models.py:
python manage.py makemigrations blog
python manage.py migrate
python manage.py createsuperuserStep 3: Admin
blog/admin.py:
Seed categories/tags in admin before public launch.
Step 4: Forms and Views
blog/forms.py:
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "slug", "excerpt", "body", "category", "tags", "published"]
widgets = {"body": forms.Textarea(attrs={"rows": 14})}blog/views.py (key views):
Wire auth URLs from chapter 14.
Step 5: URLs
blog/urls.py:
from django.urls import path
from . import views
urlpatterns = [
path("", views.post_list, name="home"),
path("posts/", views.post_list, name="post-list"),
path("posts/new/", views.post_create, name="post-create"),
path("posts/<int:pk>/", views.post_detail, name="post-detail"),
path("category/<slug:slug>/", views.category_posts, name="category"),
]Step 6: Templates
blog/templates/blog/post_list.html — extend base.html, loop page.object_list, pagination:
{% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}">Previous</a>
{% endif %}
<span>Page {{ page.number }} of {{ page.paginator.num_pages }}</span>
{% if page.has_next %}
<a href="?page={{ page.next_page_number }}">Next</a>
{% endif %}post_detail.html — title, author, category link, tags, body with linebreaks.
Optional: minimal CSS in blog/static/blog/css/style.css.
Step 7: Acceptance Checklist
Run python manage.py runserver.
| Step | Action | Expected |
|---|---|---|
| 1 | Create category + posts in /admin/ | Published posts visible |
| 2 | Visit / | Paginated list |
| 3 | Click post | Detail page |
| 4 | Visit /category/<slug>/ | Filtered list |
| 5 | Log in, /posts/new/ | Create post as author |
| 6 | Log out, hit create URL | Redirect to login |
Step 8: Tests (Sample)
blog/tests/test_blog_project.py:
Run python manage.py test.
Step 9: Deploy Preview
Swap to MySQL in production.py, then follow Gunicorn deploy:
python manage.py migrate
python manage.py collectstatic --noinputCompare with Flask server-rendered blog.
Extensions (Optional)
UpdateView/DeleteViewfor authors editing own posts- Comments model with moderation
- Full-text search with
icontainsor PostgreSQL search - RSS feed with
django.contrib.syndication
Common Problems
| Issue | Fix |
|---|---|
| Empty homepage | Mark posts published in admin |
NoReverseMatch | Match name= in urls and templates |
| Tags not saved | form.save_m2m() after commit=False |
| Pagination ignores filters | Pass query string in pagination links |
Post-Project Checklist
- Models migrated; admin usable
- Public list, detail, category pages work
- Authenticated create flow with CSRF
- Pagination on list view
- Core tests pass
FAQ## FAQ
Slug in URL instead of pk?
Change path to posts/<slug:slug>/ and get_object_or_404(Post, slug=slug).
Markdown body?
Use markdown library in template filter or store HTML from admin.
Only staff can publish?
Set published=False by default; staff toggles in admin; authors submit drafts.
Bootstrap styling?
Add CDN link in base.html or static CSS—HTML/CSS tracks.
Duplicate hello-django project?
Rename folder to hello-blog or start from django-admin startproject.