Skip to the resource

Day 7 of 7 · 90–120 minutes

Create posts with a Django form

Build a ModelForm, handle GET and POST, include CSRF protection, save, and redirect to the new post.

Today you finish withA local form that validates and saves a new blog post.

Today’s goal

Create a page at /posts/new/ where you can enter a title and body. Django will validate the form, save a Post, and redirect to its detail page.

This form is for learning on your local computer. Do not deploy a public create-post form until you add login and permission checks.

Step 1 — Create a ModelForm

Create a new file named blog/forms.py:

from django import forms

from .models import Post


class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "body"]
        widgets = {
            "title": forms.TextInput(
                attrs={"class": "form-input", "placeholder": "Post title"}
            ),
            "body": forms.Textarea(
                attrs={
                    "class": "form-input",
                    "rows": 10,
                    "placeholder": "Write your post here...",
                }
            ),
        }

A ModelForm builds form fields from a model and knows how to save a valid Post.

List the fields explicitly. created_at is not included because Django fills it automatically.

Step 2 — Add the form URL

Update blog/urls.py:

urlpatterns = [
    path("", views.home, name="home"),
    path("posts/new/", views.post_create, name="post_create"),
    path("posts/<int:pk>/", views.post_detail, name="post_detail"),
]

Place posts/new/ before the route containing <int:pk>. The exact route is easier to recognize first.

Step 3 — Build the create view

Update the imports in blog/views.py:

from django.shortcuts import get_object_or_404, redirect, render

from .forms import PostForm
from .models import Post

Keep your existing views and add:

def post_create(request):
    if request.method == "POST":
        form = PostForm(request.POST)

        if form.is_valid():
            post = form.save()
            return redirect("post_detail", pk=post.pk)
    else:
        form = PostForm()

    context = {
        "form": form,
    }

    return render(request, "blog/post_create.html", context)

The same URL has two jobs:

GET  /posts/new/ → show an empty form
POST /posts/new/ → validate
                     ├─ valid → save → redirect to the new post
                     └─ invalid → show the form and its errors again

The redirect prevents a browser refresh from submitting the same post twice.

Step 4 — Create the form template

Create blog/templates/blog/post_create.html:

{% load static %}
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>New post · My First Blog</title>
    <link rel="stylesheet" href="{% static 'blog/style.css' %}">
</head>
<body>
    <header class="site-header">
        <p><a href="{% url 'home' %}">My First Blog</a></p>
        <h1>Write a new post</h1>
    </header>

    <main class="content">
        <form class="post-form" method="post">
            {% csrf_token %}

            {{ form.non_field_errors }}

            <p class="field">
                <label for="{{ form.title.id_for_label }}">Title</label>
                {{ form.title }}
                {{ form.title.errors }}
            </p>

            <p class="field">
                <label for="{{ form.body.id_for_label }}">Body</label>
                {{ form.body }}
                {{ form.body.errors }}
            </p>

            <button type="submit">Publish</button>
        </form>
    </main>
</body>
</html>

Every Django POST form needs {% csrf_token %}. Django checks this secret value so another website cannot easily submit the form as if it were you.

In blog/templates/blog/index.html, add this above the search form:

<p>
    <a href="{% url 'post_create' %}">Write a new post</a>
</p>

Step 6 — Style the form

Add to blog/static/blog/style.css:

.post-form {
    padding: 1.5rem;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--surface);
}

.field {
    margin: 0 0 1rem;
}

.field label {
    display: block;
    margin-bottom: 0.4rem;
    font-weight: 700;
}

.form-input {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    font: inherit;
}

.post-form button {
    padding: 0.75rem 1.1rem;
    border: 0;
    border-radius: 8px;
    background: var(--accent);
    color: white;
    cursor: pointer;
    font: inherit;
    font-weight: 700;
}

.errorlist {
    padding: 0;
    color: #a31818;
    list-style: none;
}

Step 7 — Test the complete flow

  1. Open /posts/new/ — an empty form appears.
  2. Enter a title and body.
  3. Click Publish.
  4. You land on the new post’s detail page.
  5. Return to / — the post appears at the top.
  6. Open /admin/ — the same post appears there.

To test validation, try submitting without a title. The browser or Django should stop the save and show an error instead of crashing.

Final project checkpoint

  • / lists posts from the database.
  • Search filters titles and bodies.
  • Each post opens at /posts/<id>/.
  • /posts/new/ validates and saves a post.
  • Saving redirects to the new post.
  • /admin/ can still create, edit, and delete posts.
  • You can trace URL → view → template for every page.

If it does not work

What you seeCheck this
403 CSRF verification failedPut {% csrf_token %} inside the form.
PostForm is not definedImport it with from .forms import PostForm.
Save returns to a blank formDisplay form.title.errors and form.body.errors.
Refresh creates another postRedirect after form.save().
/posts/new/ returns 404Check the path and the post_create function name.

You finished

You now have a complete first Django project—not a production platform, but a small system you can explain:

  1. URLs choose views.
  2. Views prepare data.
  3. Templates display data.
  4. Static files style the pages.
  5. GET reads and searches.
  6. Models save records.
  7. POST validates and creates.

Your best next step is to rebuild the same project once without copying every line. After that, add login before allowing public visitors to create posts, then learn editing, deleting, tests, and deployment one feature at a time.