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:
- Add a post model.
- Add post administration capabilities.
- Add a custom management command to seed posts.
- 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.
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
Lastly, we activate the virtual environment.
$ . venv/bin/activate
Start a new Django app
(venv) $ cd src (venv) $ python manage.py startapp posts
Note: Read here to learn more about
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
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
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.
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.
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
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:
- Create a migration. And,
- 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
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.
(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>]>
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.