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

Project Goals

FeatureURL / 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:

bash
python manage.py makemigrations blog
python manage.py migrate
python manage.py createsuperuser

Step 3: Admin

blog/admin.py:

Seed categories/tags in admin before public launch.

Step 4: Forms and Views

blog/forms.py:

python
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:

python
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:

django
{% 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.

StepActionExpected
1Create category + posts in /admin/Published posts visible
2Visit /Paginated list
3Click postDetail page
4Visit /category/<slug>/Filtered list
5Log in, /posts/new/Create post as author
6Log out, hit create URLRedirect 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:

bash
python manage.py migrate
python manage.py collectstatic --noinput

Compare with Flask server-rendered blog.

Extensions (Optional)

  • UpdateView / DeleteView for authors editing own posts
  • Comments model with moderation
  • Full-text search with icontains or PostgreSQL search
  • RSS feed with django.contrib.syndication

Common Problems

IssueFix
Empty homepageMark posts published in admin
NoReverseMatchMatch name= in urls and templates
Tags not savedform.save_m2m() after commit=False
Pagination ignores filtersPass 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.