Build a blog with Django: Add a post model

The main reason for having a blog is to be able write and publish posts. If you can't do that with your blog then I'm not so sure we can call it one. To that end, we turn our attention to adding the functionality to deal with posts.

Let's breakdown the functionality we want into manageable tasks as follows:

  1. Add a post model.
  2. Add post administration capabilities.
  3. Add a custom management command to seed posts.
  4. Display published posts.

You'd learn more about what each task entails as we build them out in this and future articles.

Let's get started.

Prepare the workspace

Firstly, move the Trello card tracking the "Add posts" feature from Todo to In Progress.

Note: We talked about how we'd be using Trello here.

The

Secondly, set up a new feature branch.

Note: We did a similar thing here so I won't be going into the details again. Check the link to learn more.

$ cd /path/to/yaba
$ git checkout -b add-posts dev

Note: I created a pull request so you can follow along as I complete the tasks related to this feature. Learn about pull requests (a.k.a. PRs) here.

Lastly, we activate the virtual environment.

$ . venv/bin/activate

Start a new Django app

AFAIK, models must belong to a Django app so we'd begin by creating a new app called posts.

(venv) $ cd src
(venv) $ python manage.py startapp posts

Note: Read here to learn more about startapp.

This creates a posts directory, in the current directory, which is simply a Python package containing the following:

posts  
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│   ├── __init__.py
├── models.py
├── tests.py
└── views.py

Make it a Django app by adding the package to Django's INSTALLED_APPS setting.

INSTALLED_APPS = [  
    ...
    'posts.apps.PostsConfig',
]

Learn everything you need to know about applications, right here.

Add a post model

In Django, models are used for structuring and manipulating the data of your web application. We will use a model to represent our post. Open posts/models.py and edit it to contain the following:

from django.db import models


class Post(models.Model):  
    is_published = models.BooleanField(default=False)

    title = models.CharField(max_length=100)

    slug = models.SlugField(max_length=100, unique=True)

    excerpt = models.TextField()

    body = models.TextField()

    created_at = models.DateTimeField('date created', auto_now_add=True)

    updated_at = models.DateTimeField('last modified', auto_now=True)

    published_at = models.DateTimeField('date published', null=True, blank=True)

    def __str__(self):
        return self.title

I tend to keep my models (and everything else) lean and mean.

Note: Some people (his resource is helpful in other ways nonetheless) litter their models with business logic and as a result their models get "fat" but I don't think the model is the appropriate place for business logic. However, I won't dive deeper into this issue now. Just know that's it's been a point of contention in all MVC based web application frameworks and in Django it's no different. Here's a great rant on the issue and see here for how to begin thinking about your application's architecture.

For a given post, if is_published is False then it's a draft otherwise it's considered published. A published post will have its published_at field set to the date and time it was published. We'd have to write custom code, which will be an example of business logic code, to ensure this happens. You'll see where I decided to put that code when I work on the post administration task in my next article.

The title field is a CharField with a maximum length set to 100, which I settled on after reading "What Really Is The Best Headline Length?".

The slug field is a SlugField. It's maximum length needs to be at least as long as the title field for obvious reasons. I also specified that it should be unique throughout the table since we'd be using this field to identify posts in URLs.

The excerpt and body fields are TextFields. The body (as well as the excerpt) can be arbitrarily long but I expect the excerpt to always be about one or two sentences in length. Furthermore, I'll be allowing Markdown in the body but not in the excerpt.

The created_at and updated_at fields are DateTimeFields. I've set them up to automatically update when appropriate. See how auto_now_add and auto_now work by reading this and this respectively.

Finally, I provide a __str__ method so that Post objects will display nicely in the interactive console and admin.

Update the database

We've defined our model but we haven't made any changes to the database. In order to do that we need to do two things:

  1. Create a migration. And,
  2. Apply the migration.

Step 1: Creating a migration.

(venv) $ python manage.py makemigrations posts
Migrations for 'posts':  
  posts/migrations/0001_initial.py:
    - Create model Post

Here's the contents of the migration file that was created.

# -*- coding: utf-8 -*-
# Generated by Django 1.10.3 on 2016-12-20 14:38
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Post',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('is_published', models.BooleanField(default=False)),
                ('title', models.CharField(max_length=100)),
                ('slug', models.SlugField(max_length=100, unique=True)),
                ('excerpt', models.TextField()),
                ('body', models.TextField()),
                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='date created')),
                ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modified')),
                ('published_at', models.DateTimeField(blank=True, null=True, verbose_name='date published')),
            ],
        ),
    ]

Note: Learn more about the makemigrations command here.

Step 2: Applying the migration.

A migration tells Django how to propagate the changes you make to your models into your database schema. So the last step in the process is to actually apply those changes. But before we do, let's see the corresponding SQL for the migration.

(venv) $ python manage.py sqlmigrate posts 0001
BEGIN;  
--
-- Create model Post
--
CREATE TABLE "posts_post" (  
  "id" serial NOT NULL PRIMARY KEY,
  "is_published" boolean NOT NULL,
  "title" varchar(100) NOT NULL,
  "slug" varchar(100) NOT NULL UNIQUE,
  "excerpt" text NOT NULL,
  "body" text NOT NULL,
  "created_at" timestamp with time zone NOT NULL, 
  "updated_at" timestamp with time zone NOT NULL, 
  "published_at" timestamp with time zone NULL
);

CREATE INDEX "posts_post_slug_6e9097e5_like" ON "posts_post" ("slug" varchar_pattern_ops);

COMMIT;  

Notice how it generated SQL appropriate for a PostgreSQL database since that's the type of database I applied it against. If I did it against a SQLite database then the SQL would be relevant to that database and so on.

Note: Learn more about sqlmigrate here.

Enough already, let's apply it.

(venv) $ python manage.py migrate posts
Operations to perform:  
  Apply all migrations: posts
Running migrations:  
  Applying posts.0001_initial... OK

And just like that, our table is in the database.

Let's play

(venv) $ python manage.py shell
...
>>> from posts.models import Post

>>> Post.objects.all()
<QuerySet []>

>>> Post.objects.create(title='Hello, world!')
<Post: Hello, world!>  
>>> Post.objects.all()
<QuerySet [<Post: Hello, world!>]>  
>>> Post.objects.get(pk=1)
<Post: Hello, world!>

>>> Post.objects.create(title='Another post title', slug='another-post-title')
<Post: Another post title>

>>> Post.objects.all()
<QuerySet [<Post: Hello, world!>, <Post: Another post title>]>  

Wrap up

That's enough for today. Let's wrap up.

(venv) $ git add .
(venv) $ git commit -m "Add a post model"

Finally, check off the first task on your Trello card and bask in the joy of being 25% complete in your first Django feature.

Here's a link to all the code we wrote/generated in this article.

P.S. Let me know in the comments if you get into any problems. I'd be happy to help.

P.S.S. Subscribe to my newsletter if you're interested in getting exclusive Django content.