In this chapter we will use a database for the first time to build a basic Message Board application where users can post and read short messages. We’ll explore Django’s powerful built-in admin interface which provides a visual way to make changes to our data. And after adding tests we will push our code to Bitbucket and deploy the app on Heroku.

Django provides built-in support for several types of database backends. With just a few lines in our file it can support PostgreSQL, MySQL, Oracle, or SQLite. But the simplest–by far–to use is SQLite because it runs off a single file and requires no complex installation. By contrast, the other options require a process to be running in the background and can be quite complex to properly configure. Django uses SQLite by default for this reason and it’s a perfect choice for small projects.

Initial Setup

Since we’ve already set up several Django projects at this point in the book, we can quickly run through our commands to begin a new one. We need to do the following:

  • create a new directory for our code on the Desktop called mb
  • install Django in a new virtual environment
  • create a new project called mb_project
  • create a new app call posts
  • update

In a new command line console, enter the following commands. Note that I’m using (mb) here to represent the virtual environment name even though it’s actually (mb-XXX) where XXX represents random characters.

$ cd ~/Desktop
$ mkdir mb
$ cd mb
$ pipenv install django
$ pipenv shell
(mb) $ django-admin startproject mb_project .
(mb) $ python startapp posts

Tell Django about the new app posts by adding it to the bottom of the INSTALLED_APPS section of our file. Open it with your text editor of choice.

# mb_project/
    'posts', # new

Then execute the migrate command to create an initial database based on Django’s default settings.

(mb) $ python migrate

If you look inside our directory with the ls command, you’ll see there’s now a db.sqlite3 file representing our SQLite database.

(mb) $ ls
db.sqlite3 mb_project

Aside: Technically a db.sqlite3 file is created the first time you run either migrate or runserver. Using runserver configures a database using Django’s default settings, however migrate will sync the database with the current state of any database models contained in the project and listed in INSTALLED_APPS. In other words, to make sure the database reflects the current state of your project you’ll need to run migrate (and also makemigrations) each time you update a model. More on this shortly.

To confirm everything works correctly, spin up our local server.

(mb) $ python runserver

And navigate to to see the familiar Django installed correctly page.

Django welcome page

Create a database model

Our first task is to create a database model where we can store and display posts from our users. Django will turn this model into a database table for us. In real-world Django projects, it’s often the case that there will be many complex, interconnected database models but in our simple message board app we only need one.

I won’t cover database design in this book but I have written a short guide which you can find here if this is all new to you.

Open the posts/ file and look at the default code which Django provides:

# posts/
from django.db import models

# Create your models here

Django imports a module models to help us build new database models, which will “model” the characteristics of the data in our database. We want to create a model to store the textual content of a message board post, which we can do so as follows:

# posts/
from django.db import models

class Post(models.Model):
    text = models.TextField()

Note that we’ve created a new database model called Post which has the database field text. We’ve also specified the type of content it will hold, TextField(). Django provides many model fields supporting common types of content such as characters, dates, integers, emails, and so on.

Activating models

Now that our new model is created we need to activate it. Going forward, whenever we create or modify an existing model we’ll need to update Django in a two-step process.

  1. First we create a migration file with the makemigrations command which generate the SQL commands for preinstalled apps in our INSTALLED_APPS setting. Migration files do not execute those commands on our database file, rather they are a reference of all new changes to our models. This approach means that we have a record of the changes to our models over time.

  2. Second we build the actual database with migrate which does execute the instructions in our migrations file.

(mb) $ python makemigrations posts
(mb) $ python migrate posts

Note that you don’t have to include a name after either makemigrations or migrate. If you simply run the commands then they will apply to all available changes. But it’s a good habit to be specific. If we had two separate apps in our project, and updated the models in both, and then ran makemigrations it would generate a migrations file containing data on both changes. This makes debugging harder in the future. You want each migration file to be as small and isolated as possible. That way if you need to look at past migrations, there is only one change per migration rather than one that applies to multiple apps.

Django Admin

Django provides us with a robust admin interface for interacting with our database. This is a truly killer feature that few web frameworks offer. It has its routes in Django’s origin as a project at a newspaper. The developers wanted a CMS (Content Management System) so that journalists could write and edit their stories without needing to touch “code.” Over time the built-in admin app has evolved into a fantastic, out-of-the-box tool for managing all aspects of a Django project.

To use the Django admin, we first need to create a superuser who can login. In your command line console, type python createsuperuser and respond to the prompts for a username, email, and password:

(mb) $ python createsuperuser
Username (leave blank to use 'wsv'): wsv
Password (again):
Superuser created successfully.

Note: When you type your password, it will not appear visible in the command line console for security reasons.

Restart the Django server with python runserver and in your browser go to You should see the admin’s login screen:

Admin login page

Login by entering the username and password you just created. You will see the Django admin homepage next:

Admin homepage

But where’s our posts app? It’s not displayed on the main admin page!

We need to explicitly tell Django what to display in the admin. Fortunately we can change fix this easily by opening the posts/ file and editing it to look like this:

# posts/
from django.contrib import admin

from .models import Post

Django now knows that it should display our posts app and its database model Post on the admin page. If you refresh your browser you’ll see that it now appears:

Admin homepage updated

Now let’s create our first message board post for our database. Click on the + Add button opposite Posts. Enter your own text in the Text form field.

Admin new entry

Then click the “Save” button, which will redirect you to the main Post page. However if you look closely, there’s a problem: our new entry is called “Post object”, which isn’t very helpful.

Admin new entry

Let’s change that. Within the posts/ file, add a new function __str__ as follows:

# posts/
from django.db import models

class Post(models.Model):
    text = models.TextField()

    def __str__(self):
        """A string representation of the model."""
        return self.text[:50]

If you refresh your Admin page in the browser, you’ll see it’s changed to a much more descriptive and helpful representation of our database entry.

Admin new entry

Much better! It’s a best practice to add str() methods to all of your models to improve their readability.


In order to display our database content on our homepage, we have to wire up our views, templates, and URLConfs. This pattern should start to feel familiar now.

Let’s begin with the view. Earlier in the book we used the built-in generic TemplateView to display a template file on our homepage. Now we want to list the contents of our database model. Fortunately this is also a common task in web development and Django comes equipped with the generic class-based ListView.

In the posts/ file enter the Python code below:

# posts/
from django.views.generic import ListView
from .models import Post

class HomePageView(ListView):
    model = Post
    template_name = 'home.html'

In the first line we’re importing ListView and in the second line we need to explicitly define which model we’re using. In the view, we subclass ListView, specify our model name and specify our template reference. Internally ListView returns an object called object_list that we want to display in our template.

Our view is complete which means we still need to configure our URLs and make our template. Let’s start with the template. Create a project-level directory called templates and a home.html template file.

(mb) $ mkdir templates
(mb) $ touch templates/home.html

Then update the DIRS field in our file so that Django knows to look in this templates folder.

        'DIRS': [os.path.join(BASE_DIR, 'templates')],

In our templates file home.html we can use the Django Templating Language’s for loop to list all the objects in object_list. Remember that object_list is what ListView returns to us.

<!-- templates/home.html -->
<h1>Message board homepage</h1>
  {% for post in object_list %}
    <li>{{ post }}</li>
  {% endfor %}

The last step is to set up our URLConfs. Let’s start with the project-level file where we simply include our posts and add include on the second line.

# mb_project/
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('posts.urls')),

Then create an app-level file.

(mb) $ touch posts/

And update it like so:

# posts/
from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),

Restart the server with python runserver and navigate to our homepage which now lists out our message board posts.

Homepage with posts

We’re basically done at this point, but let’s create a few more message board posts in the Django admin to confirm that they will display correctly on the homepage.

Adding new posts

To add new posts to our message board, go back into the Admin at and create two more posts. Here’s what mine look like:

Admin entry

Admin entry

Updated admin entries section

If you return to the homepage you’ll see it automatically displays our formatted posts. Woohoo!

Homepage with three entries

Everything works so it’s a good time to initialize our directory, add the new code, and include our first git commit.

(mb) $ git init
(mb) $ git add -A
(mb) $ git commit -m 'initial commit'


The best time to write tests is either before you’ve written any actual code (Test Driven Development) or right after when the new features are fresh in your mind. Write some tests, check that they work, and then feel confident that if you inadvertently break something down the road–and rest assured, you will–the tests will catch any errors for you.

Previously we were only testing static pages so we used SimpleTestCase. But now that our homepage works with a database we need to use TestCase which will let us create a “test” database we can check against. In other words, we don’t need to run tests on our actual database but instead can make a separate test database, fill it with sample data, and then test against it.

Let’s start by adding a sample post to the text database field and then check that it is stored correctly in the database. It’s important that all our test methods start with test_ so Django knows to test them! The code will look like this:

# posts/
from django.test import TestCase
from .models import Post

class PostModelTest(TestCase):

    def setUp(self):
        Post.objects.create(text='just a test')

    def test_text_content(self):
        expected_object_name = f'{post.text}'
        self.assertEqual(expected_object_name, 'just a test')

At the top we import the TestCase module which lets us create a sample database, then import our Post model. We create a new class PostModelTest and add a method setUp to create a new database that has just one entry: a post with a text field containing the string ‘just a test’.

Then we run our first test, test_text_content, to check that the database field actually contains just a test. We create a variable called post that represents the first id on our Post model. Remember that Django automatically sets this id for us. If we created another entry it would have an id of 2, the next one would be 3, and so on.

The following line uses f strings which are a very cool addition to Python. They let us put variables directly in our strings as long as the variables are surrounded by brackets {}. Here we’re setting expected_object_name to be the string of the value in post.text, which should be just a test.

On the final line we use assertEqual to check that our newly created entry does in fact match what we input at the top. Go ahead and run the test on the command line with python test.

(mb) $ python test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Ran 1 test in 0.001s

Destroying test database for alias 'default'...

It passed!

Don’t worry if the previous explanation felt like information overload. That’s natural the first time you start writing tests, but you’ll soon find that most tests that you write are actually quite repetitive.

Time for our second test. The first test was on the model but now we want test our one and only page: the homepage. Specifically, we want to test that it exists (throws an HTTP 200 response), uses the home view, and uses the home.html template.

We’ll need to add one more import at the top for reverse and a brand new class HomePageViewTest for our test.

from django.test import TestCase
from django.urls import reverse
from .models import Post

class PostModelTest(TestCase):

    def setUp(self):
        Post.objects.create(text='just a test')

    def test_text_content(self):
        expected_object_name = f'{post.text}'
        self.assertEqual(expected_object_name, 'just a test')

class HomePageViewTest(TestCase):

    def setUp(self):
        Post.objects.create(text='this is another test')

    def test_view_url_exists_at_proper_location(self):
        resp = self.client.get('/')
        self.assertEqual(resp.status_code, 200)

    def test_view_url_by_name(self):
        resp = self.client.get(reverse('home'))
        self.assertEqual(resp.status_code, 200)

    def test_view_uses_correct_template(self):
        resp = self.client.get(reverse('home'))
        self.assertEqual(resp.status_code, 200)
        self.assertTemplateUsed(resp, 'home.html')

If you run our tests again you should see that they pass.

(mb) $ python test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Ran 4 tests in 0.036s

Destroying test database for alias 'default'...

Why does it say four tests? Remember that our setUp methods are not actually tests, they merely let us run subsequent tests. Our four actual tests are test_text_content, test_view_url_exists_at_proper_location, test_view_url_by_name, and test_view_uses_correct_template.

Any function that has the word test* at the beginning and exists in a file will be run when we execute the command python test.

We’re done adding code for our testing so it’s time to commit the changes to git.

(mb) $ git add -A
(mb) $ git commit -m 'added tests'


We also need to store our code on Bitbucket. This is a good habit to get into in case anything happens to your local computer and it also allows you to share and collaborate with other developers.

You should already have a Bitbucket account from Chapter 3 so go ahead and create a new repo which we’ll call mb-app.

Bitbucket create app

On the next page click on the bottom link for “I have an existing project”. Copy the two commands to connect and then push the repository to Bitbucket.

It should look like this, replacing wsvincent (my username) with your Bitbucket username:

(mb) $ git remote add origin
(mb) $ git push -u origin master

Heroku configuration

You should also already have a Heroku account setup and installed from Chapter 3. We need to make the following changes to our Message Board project to deploy it online:

  • update Pipfile.lock
  • new Procfile
  • install gunicorn
  • update

Within your Pipfile specify the version of Python we’re using, which is 3.7. Add these two lines at the bottom of the file.

# Pipfile
python_version = "3.7"

Run pipenv lock to generate the appropriate Pipfile.lock.

(mb) $ pipenv lock

Then create a Procfile which tells Heroku how to run the remote server where our code will live.

(mb) $ touch Procfile

For now we’re telling Heroku to use gunicorn as our production server and look in our mb_project.wsgi file for further instructions.

web: gunicorn mb_project.wsgi --log-file -

Next install gunicorn which we’ll use in production while still using Django’s internal server for local development use.

(mb) $ pipenv install gunicorn

Finally update ALLOWED_HOSTS in our file.

# mb_project/

We’re all done! Add and commit our new changes to git and then push them up to Bitbucket.

(mb) $ git status
(mb) $ git add -A
(mb) $ git commit -m 'New updates for Heroku deployment'
(mb) $ git push -u origin master

Heroku deployment

Make sure you’re logged into your correct Heroku account.

(mb) $ heroku login

Then run the create command and Heroku will randomly generate an app name for you. You can customize this later if desired.

(mb) $ heroku create
Creating app... done, ⬢ agile-inlet-25811 |

Set git to use the name of your new app when you push code to Heroku. My Heroku-generated name is agile-inlet-25811 so the command looks like this.

(mb) $ heroku git:remote -a agile-inlet-25811

Tell Heroku to ignore static files which we’ll cover in-depth when deploying our Blog app later in the book.

(mb) $ heroku config:set DISABLE_COLLECTSTATIC=1

Push the code to Heroku and add free scaling so it’s actually running online, otherwise the code is just sitting there.

(mb) $ git push heroku master
(mb) $ heroku ps:scale web=1

If you open the new project with heroku open it will automatically launch a new browser window with the URL of your app. Mine is live at

Live site


We’ve now built, tested, and deployed our first database-driven app. While it’s deliberately quite basic, now we know how to create a database model, update it with the admin panel, and then display the contents on a web page. But something is missing, no?

In the real-world, users need forms to interact with our site. After all, not everyone should have access to the admin panel. In the next chapter we’ll build a blog application that uses forms so that users can create, edit, and delete posts. We’ll also add styling via CSS.

Purchase the full-length book as an eBook (free updates), Paperback, or Kindle.



Chapter 1: Initial Setup

Chapter 2: Hello World app

Chapter 3: Pages app

Chapter 4: Message Board app

Chapter 5: Blog app

Chapter 6: Forms

Chapter 7: User Accounts

Chapter 8: Custom User Model

Chapter 9: User Authentication

Chapter 10: Bootstrap

Chapter 11: Password Change and Reset

Chapter 12: Email

Chapter 13: Newspaper app

Chapter 14: Permissions and Authorization

Chapter 15: Comments


I also write a monthly newsletter with up-to-date tutorials on web development with Django, React, and more. Sign up below or view all posts on my personal site