In this chapter, we will build, test, and deploy a Pages app containing a homepage and an about page. We are still not using the database yet–that comes in the next chapter–but we’ll learn about class-based views and templates which are the building blocks for the more complex web applications built later on in the book.

Initial Set Up

Our initial setup is similar to that in the previous chapter and contains the following steps:

  • make a new directory for our code called pages and navigate into it
  • create a new virtual environment called .venv and activate it
  • install Django and Black
  • create a new Django project called django_project
  • create a new app called pages

On the command line, ensure you’re not working in an existing virtual environment. If there is text before the command line prompt–either > on Windows or % on macOS–then you are! Make sure to type deactivate to leave it.

Within a new command line shell, navigate to the code folder on the desktop, create a new folder called pages, change directories into it, and activate a new Python virtual environment called .venv.

# Windows
$ cd onedrive\desktop\code
$ mkdir pages
$ cd pages
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $

# macOS
$ cd ~/desktop/code
$ mkdir pages
$ cd pages
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $

Next, install Django and Black, create a new project called django_project, and make a new app called pages.

(.venv) $ python -m pip install django~=4.2.0
(.venv) $ python -m pip install black
(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py startapp pages

Remember that even though we added a new app, Django will not recognize it until it is added to the INSTALLED_APPS setting within django_project/settings.py. Open your text editor and add it to the bottom now:

# django_project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "pages",  # new
]

Initialize the database with migrate and start the local web server with runserver.

(.venv) $ python manage.py migrate
(.venv) $ python manage.py runserver

And then navigate to http://127.0.0.1:8000/.

Django welcome page

Templates

Every web framework needs a convenient way to generate HTML files, and in Django, the approach is to use templates: individual HTML files that can be linked together and include basic logic.

Recall that our “Hello, World” site had the phrase hardcoded into a views.py file in the previous chapter. That technically works but does not scale well! A better approach is to link a view to a template, thereby separating the information contained in each.

In this chapter, we’ll learn how to use templates to create our desired homepage and about page. And in future chapters, the use of templates will support building websites that can support hundreds, thousands, or even millions of webpages with a minimal amount of code.

The first consideration is where to place templates within the structure of a Django project. There are two options. By default, Django’s template loader will look within each app for related templates. However, the structure is somewhat confusing: each app needs a new templates directory, another directory with the same name as the app, and then the template file.

Therefore, in our pages app, Django would expect the following layout:

└── pages
    ├── templates
        ├── pages
            ├── home.html

Why this seemingly repetitive approach? The short answer is that the Django template loader wants to be sure it finds the correct template! What happens if there are home.html files within two separate apps? This structure makes sure there are no such conflicts.

However, another approach is to instead create a single project-level templates directory and place all templates within it. By tweaking our django_project/settings.py file, we can tell Django to also look in this directory for templates. That is the approach we’ll use.

First, quit the running server with the Control+c command. Then create a directory called templates.

(.venv) $ mkdir templates

Next, we need to update django_project/settings.py to tell Django the location of our new templates directory. Doing so requires a one-line change to the setting "DIRS" under TEMPLATES.

# django_project/settings.py
TEMPLATES = [
    {
        ...
        "DIRS": [BASE_DIR / "templates"],  # new
        ...
    },
]

Create a new file called home.html within the templates directory. You can do this within your text editor: in Visual Studio Code, go to the top left of your screen, click “File,” and then “New File.” Make sure to name and save the file in the correct location.

The home.html file will have a simple headline for now.

<!-- templates/home.html -->
<h1>Homepage</h1>

Ok, our template is complete! The next step is to configure our URL and view files.

Class-Based Views

Early versions of Django only shipped with function-based views, but developers soon found themselves repeating the same patterns over and over. Write a view that lists all objects in a model. Write a view that displays only one detailed item from a model. And so on. Generic function-based views were introduced to abstract these patterns and streamline the development of common patterns. The problem with generic function-based views was that there was no easy way to extend or customize them. As projects grow in size, this became more and more of an issue and, as a result, generic function-based views were deprecated in Django 1.3 and removed completely in version 1.5.

To help with code reusability, Django added class-based views and generic class-based views while still retaining function-based views. Classes are a fundamental part of Python, but a thorough discussion of them is beyond the scope of this book. If you need an introduction or refresher, I suggest reviewing the official Python docs, which have an excellent tutorial on classes and their usage.

There are, therefore, three different ways to write a view in Django–function-based, class-based, or generic class-based–which is very confusing to beginners. Function-based views are simpler to understand because they mimic the HTTP request/response cycle, and they are a good place to start which is what we’ve done in this book. Class-based views are a little harder to understand because their inheritance structure means you have to dive into the code to see everything happening; with a function-based view, it is all there. And generic class-based views are the hardest yet to understand. An entire website, Classy Class-Based Views, is dedicated to helping developers decipher them.

So why bother with generic class-based views? Once you have used them for a while, they become elegant and efficient ways to write code. You can often modify a single method on one to do custom behavior rather than rewriting everything from scratch, which makes it easier to understand someone else’s code. This does, however, come at the cost of complexity and requires a leap of faith because it takes a long time to understand how they work under the hood.

That said, most Django developers first reach for generic class-based views when trying to solve a problem. It is incredible how many issues can be solved by off-the-shelf GCVBs like TemplateView, ListView, or DetailView. When a generic class-based view is not enough, modify it to suit your needs. And if that still isn’t enough, revert to a function-based or class-based view. That is the recommended approach for handling the three different ways to write views in Django.

In our specific use case, we will use TemplateView to display our template. Replace the default text in the pages/views.py file with the following:

# pages/views.py
from django.views.generic import TemplateView


class HomePageView(TemplateView):
    template_name = "home.html"

Note that we’ve capitalized our view, HomePageView since it’s now a Python class–classes, unlike functions, must always be capitalized. The TemplateView already contains all the logic needed to display our template; we just need to specify the template’s name.

URLs

The last step is to update our URLs. Recall from Chapter 2 that we need to make updates in two locations. First, we update the django_project/urls.py file to point at our pages app, and then within pages, we match views to URL routes.

Let’s start with the django_project/urls.py file.

# django_project/urls.py
from django.contrib import admin
from django.urls import path, include  # new

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("pages.urls")),  # new
]

The code here should look familiar at this point. We add include on the second line to point the existing URL to the pages app. Next, create a pages/urls.py file and add the code below. This pattern is almost identical to what we did in the last chapter with one significant difference: when using Class-Based Views, you always add as_view() at the end of the view name.

# pages/urls.py
from django.urls import path
from .views import HomePageView

urlpatterns = [
    path("", HomePageView.as_view(), name="home"),
]

And we’re done! Start the local web server with the command python manage.py runserver and navigate to http://127.0.0.1:8000/ to see our new homepage.

Homepage

About Page

The process for adding an About page is very similar to what we just did. We’ll create a new template file, a new view, and a new URL route. Start by making a new template file called templates/about.html and populate it with a short HTML headline.

<!-- templates/about.html -->
<h1>About page</h1>

Then add a new view for the page.

# pages/views.py
from django.views.generic import TemplateView


class HomePageView(TemplateView):
    template_name = "home.html"


class AboutPageView(TemplateView):  # new
    template_name = "about.html"

And finally, import the view name and connect it to a URL at about/.

# pages/urls.py
from django.urls import path
from .views import HomePageView, AboutPageView  # new

urlpatterns = [
    path("about/", AboutPageView.as_view(), name="about"),  # new
    path("", HomePageView.as_view(), name="home"),
]

Start up the web server with runserver and navigate to http://127.0.0.1:8000/about. The new About page is visible.

About page

Extending Templates

The real power of templates is that they can be extended. If you think about most websites, the same content appears on every page (header, footer, etc.). Wouldn’t it be nice if we, as developers, could have one canonical place for our header code that all other templates would inherit?

Well, we can! Let’s create a base.html file containing a header with links to our two pages. We could name this file anything, but using base.html is a standard convention. In your text editor, make this new file called templates/base.html.

Django has a minimal templating language for adding links and basic logic in our templates. You can see the complete list of built-in template tags here in the official docs. Template tags take the form of {% something %} where the “something” is the template tag itself. You can even create custom template tags, though we won’t do that in this book.

To add URL links in our project, we can use the built-in URL template tag which takes the URL pattern name as an argument. Remember how we added optional URL names to our two routes in pages/urls.py? The url tag uses these names to create links for us automatically.

The URL route for our homepage is called home. To configure a link to it, we use the following syntax: {% url 'home' %}.

<!-- templates/base.html -->
<header>
  <a href="{% url 'home' %}">Home</a> |
  <a href="{% url 'about' %}">About</a>
</header>

{% block content %} {% endblock content %}

At the bottom, we’ve added a block tag called content. While it’s optional to name our closing endblock–you can write {% endblock %} if you prefer–doing so helps with readability, especially in larger template files.

Let’s update our home.html and about.html files to extend the base.html template. That means we can reuse the same code from one template in another. The Django templating language comes with an extends method that we can use for this.

<!-- templates/home.html -->
{% extends "base.html" %}

{% block content %}
<h1>Homepage</h1>
{% endblock content %}
<!-- templates/about.html -->
{% extends "base.html" %}

{% block content %}
<h1>About page</h1>
{% endblock content %}

Now if you start up the server with python manage.py runserver and open up our webpages again at http://127.0.0.1:8000/, the header is included.

Homepage with header

And it is also present on the about page at http://127.0.0.1:8000/about/.

About page with header

There’s a lot more we can do with templates, and in practice, you’ll typically create a base.html file and then add additional templates on top of it in a robust Django project. We’ll do this later on in the book.

Tests

It’s important to add automated tests and run them whenever a codebase changes. Tests require a small amount of upfront time to write but more than pay off later on. In the words of Django co-creator Jacob Kaplan-Moss, “Code without tests is broken as designed.”

Testing can be divided into two main categories: unit and integration. Unit tests check a piece of functionality in isolation, while Integration tests check multiple linked pieces. Unit tests run faster and are easier to maintain since they focus on only a tiny amount of code. Integration tests are slower and harder to maintain since a failure doesn’t point you in the specific direction of the cause. Most developers focus on writing many unit tests and a small number of integration tests.

The next question is, What to test? Anytime you have created new functionality, a test is necessary to confirm that it works as intended. For example, in our project, we have a home page and an about page, and we should test that both exist at the expected URLs.It may seem unnecessary now but as a project grows in size, tests make sure that you don’t inadvertently break something you did previously. By writing tests and regularly running them, you can ensure this doesn’t happen.

The Python standard library contains a built-in testing framework called unittest that uses TestCase instances and a long list of assert methods to check for and report failures. Django’s testing framework provides several extensions on top of Python’s unittest.TestCase base class. These include a test client for making dummy Web browser requests, several Django-specific additional assertions, and four test case classes: SimpleTestCase, TestCase, TransactionTestCase, and LiveServerTestCase.

Generally speaking, SimpleTestCase is used when a database is unnecessary while TestCase is used when you want to test the database. TransactionTestCase is helpful to directly test database transactions while LiveServerTestCase launches a live server thread for testing with browser-based tools like Selenium.

Before we proceed: you may notice that the naming of methods in unittest and django.test are written in camelCase rather than the more Pythonic snake_case pattern. The reason is that unittest is based on the jUnit testing framework from Java, which does use camelCase, so when Python added unittest it came along with camelCase naming.

If you look within our pages app, Django already provided a tests.py file we can use. Since no database is involved in our project, we will import SimpleTestCase at the top of the file. For our first tests, we’ll check that the two URLs for our website, the homepage and about page, both return HTTP status codes of 200, the standard response for a successful HTTP request.

# pages/tests.py
from django.test import SimpleTestCase


class HomepageTests(SimpleTestCase):
    def test_url_exists_at_correct_location(self):
        response = self.client.get("/")
        self.assertEqual(response.status_code, 200)


class AboutpageTests(SimpleTestCase):
    def test_url_exists_at_correct_location(self):
        response = self.client.get("/about/")
        self.assertEqual(response.status_code, 200)

To run the tests, quit the server with Control+c and type python manage.py test on the command line to run them.

(.venv) $ python manage.py test
Found 2 test(s).
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.003s

OK

If you see an error such as AssertionError: 301 != 200, you likely forgot to add the trailing slash to "/about" above. The web browser knows to add a slash if it’s not provided automatically, but that causes a 301 redirect, not a 200 success response!

What else can we test? How about the URL name for each page? Recall that in pages/urls.py, we added the name "home" for the homepage path and "about" for the about page. To check both, we can use the very handy Django utility function reverse. Don’t forget to import reverse at the top of the file and add a new unit test for each below.

# pages/tests.py
from django.test import SimpleTestCase
from django.urls import reverse  # new


class HomepageTests(SimpleTestCase):
    def test_url_exists_at_correct_location(self):
        response = self.client.get("/")
        self.assertEqual(response.status_code, 200)

    def test_url_available_by_name(self):  # new
        response = self.client.get(reverse("home"))
        self.assertEqual(response.status_code, 200)


class AboutpageTests(SimpleTestCase):
    def test_url_exists_at_correct_location(self):
        response = self.client.get("/about/")
        self.assertEqual(response.status_code, 200)

    def test_url_available_by_name(self):  # new
        response = self.client.get(reverse("about"))
        self.assertEqual(response.status_code, 200)

Run the tests again to confirm that they work correctly.

(.venv) $ python manage.py test
Found 4 test(s).
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 4 tests in 0.005s

OK

We have tested our URL locations and names so far but not our templates. Let’s make sure that the correct templates–home.html and about.html–are used on each page and that they display the expected content of "<h1>Homepage</h1>" and "<h1>About page</h1>" respectively.

We can use assertTemplateUsed and assertContains to achieve this.

# pages/tests.py
from django.test import SimpleTestCase
from django.urls import reverse  


class HomepageTests(SimpleTestCase):
    def test_url_exists_at_correct_location(self):
        response = self.client.get("/")
        self.assertEqual(response.status_code, 200)

    def test_url_available_by_name(self):  
        response = self.client.get(reverse("home"))
        self.assertEqual(response.status_code, 200)

    def test_template_name_correct(self):  # new
        response = self.client.get(reverse("home"))
        self.assertTemplateUsed(response, "home.html")

    def test_template_content(self):  # new
        response = self.client.get(reverse("home"))
        self.assertContains(response, "<h1>Homepage</h1>")


class AboutpageTests(SimpleTestCase):
    def test_url_exists_at_correct_location(self):
        response = self.client.get("/about/")
        self.assertEqual(response.status_code, 200)

    def test_url_available_by_name(self):  
        response = self.client.get(reverse("about"))
        self.assertEqual(response.status_code, 200)

    def test_template_name_correct(self):  # new
        response = self.client.get(reverse("about"))
        self.assertTemplateUsed(response, "about.html")

    def test_template_content(self):  # new
        response = self.client.get(reverse("about"))
        self.assertContains(response, "<h1>About page</h1>")

Run the tests one last time to check our new work. Everything should pass.

(.venv) $ python manage.py test
Found 8 test(s).
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 8 tests in 0.006s

OK

Experienced programmers may look at our testing code and note that there is a lot of repetition. For example, we set the response each time for all eight tests. Generally, it is a good idea to abide by DRY (Don’t Repeat Yourself) coding, but unit tests work best when they are self-contained and highly verbose. As a test suite expands, for performance reasons, it might make more sense to combine multiple assertions into a smaller number of tests, but that is an advanced–and often subjective–topic beyond the scope of this book.

We’ll do much more with testing in the future, especially once we start working with databases. For now, it’s important to see how easy and important it is to add tests every time we add new functionality to our Django project.

Git and GitHub

It’s time to track our changes with Git and push them to GitHub. We’ll start by initializing our directory and checking the status of our changes.

(.venv) $ git init
(.venv) $ git status

Use git status to see all our code changes. Notice that the .venv directory with our virtual environment is included? We don’t want that. In your text editor create a new hidden file, .gitignore, so we can specify what Git will not track.

.venv/

Run git status again to confirm these two files are being ignored. You may have noticed we did not yet create a requirements.txt file as we did in the “Hello, World!” project in the last chapter. Hang tight; we will be adding this shortly as part of the deployment process. Use git add -A to add the intended files/directories and include an initial commit message.

(.venv) > git status
(.venv) > git add -A
(.venv) > git commit -m "initial commit"

Over on GitHub create a new repo called pages and make sure to select the “Private” radio button. Then click on the “Create repository” button.

On the next page, scroll down to where it says “…or push an existing repository from the command line.” Copy and paste the two commands there into your terminal.

It should look like the below, albeit instead of wsvincent as the username, it will be your GitHub username.

(.venv) $ git remote add origin https://github.com/wsvincent/pages.git
(.venv) $ git branch -M main
(.venv) $ git push -u origin main

Hosting Options

To make our site available online where everyone can see it, we need to deploy our code to an external server and database. Local code lives only on our computer; production code lives on an external server available to everyone. Therefore, deploying code online is also called, putting our code into production.

There are three main ways to host a website:

  • 1. Dedicated Server: a physical server sitting in a data center that belongs exclusively to you. Generally, only the largest companies adopt this approach since it requires a lot of technical expertise to configure and maintain.
  • 2. Virtual Private Server (VPS): a server can be divided into multiple virtual machines that use the same hardware, which is much more affordable than a dedicated server. This approach also means you don’t have to worry about maintaining the hardware yourself.
  • 3. Platform as a Service (PaaS): a managed VPS solution that is pre-configured and maintained, making it the fastest option to deploy and scale a website. The downside is that a developer does not have access to the same degree of customization that a VPS or a dedicated server provides.

These days most individual developers and small companies rely on PaaS’s for their hosting needs since it abstracts away a challenging part of website development. That is the approach we will use in this book, too.

Deployment Checklist

Django defaults to a local development configuration with the startproject command, which makes it easier to start building apps locally, but many settings are a security risk in production.

Since deployment requires many discrete steps, it is common to have a “checklist” for these since there quickly become too many to remember. At this stage, we are intentionally keeping things basic however, this list will grow in future projects as we add additional security and performance features.

Here is the current deployment checklist:

  • install Gunicorn as a WSGI server
  • generate a requirements.txt file
  • update ALLOWED_HOSTS in django_project/settings.py
  • add a .dockerignore file

The first step is to install Gunicorn, a production-ready WSGI server for our project. Gunicorn replaces the Django local development server and–as with all Python packages–is installable via Pip.

(.venv) $ python -m pip install gunicorn==20.1.0

Step two is to create a requirements.txt file containing every Python package currently installed in our virtual environment. This file is necessary so that the project can be recreated on production servers and other team members (if any) can recreate the repository from scratch in the future.

We can use the command python -m pip freeze to output all the installed packages to the terminal. Then we could create a root project-level requirements.txt file and copy/paste the output over. Or we can take a more elegant approach by redirecting the output of pip freeze over to the requirements.txt file automatically. The greater-than symbol (>) lets us redirect the output to a file. Here is what the full command looks like. Run it.

(.venv) $ python -m pip freeze > requirements.txt

If you look in your text editor, a new requirements.txt file has been created listing each package in our virtual environment. You do not need to focus too much on the contents of this file, just remember that whenever you install a new Python package in your virtual environment, you should update the requirements.txt file to reflect it. There are more complicated tools that automate this process, but since we are using venv and keeping things as simple as possible, it must be done manually.

The third step is to look within django_project/settings.py for the ALLOWED_HOSTS setting, which represents that host/domain names our Django site can serve. This is a security measure to prevent HTTP Host header attacks. For now, we’ll use the wildcard asterisk, *, so that all domains are acceptable. Later in the book, we’ll adopt a more secure approach of explicitly listing allowed domains.

# django_project/settings.py
ALLOWED_HOSTS = ["*"]

The fourth and final step is to create a .dockerignore file in the project root directory, next to the existing.gitignore file. Docker is used by most modern hosting companies–more on this shortly–and it is essential to tell it what to ignore. The .dockerignore file does this for us. At a minimum, we want to ignore our local virtual environment, SQLite database, and Git repo.

# .dockerignore
.venv/
*.sqlite3
.git

That’s it! Remember that we’ve taken several security shortcuts here, but the goal is to push our project into production in as few steps as possible. In future chapters, we will cover proper security around deployments in depth.

Use git status to check our changes, add the new files, and then commit them:

(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "New updates for deployment"

Push the code to GitHub to have an online backup of all our code changes.

(.venv) $ git push -u origin main

Fly.io

Until recently, Heroku was the default choice for simple Django deployments because it was free for small projects and widely used. Unfortunately, Heroku announced that as of November 2022, it would cease offering free tiers. While many newer platforms offer some degree of free hosting, none is as generous as Heroku. Running a hosting company requires actual costs and efforts to prevent fraud, and it is not unreasonable to expect to pay something to host a website.

For this book, we will use Fly.io. Sign up for an account on their website using your email address or GitHub account. Adding a payment method is now required.

Deploy to Fly.io

Fly has its own Command Line Interface (CLI) to help manage deployments. Follow the official instructions to install it. Then from the command line sign in with the command flyctl auth login, which will likely open up a web browser window for you to sign into Fly.

(.venv) $ flyctl auth login
Opening https://fly.io/app/auth/cli/606daf31a9c91b62e5528e27ee891e4e ...

Waiting for session... Done
successfully logged in as [email protected]

To configure and launch our site run the fly launch command and follow the wizard.

  • Choose an app name: this will be your dedicated fly.dev subdomain
  • Choose the region for deployment: select the one closest to you or another region if you prefer
  • Decline overwriting our .dockerignore file: our choices are already optimized for the project
  • Decline to setup a Postgres or Redis database: we will tackle this in the next project
(.venv) $ fly launch
Creating app in ~/desktop/code/pages
Scanning source code
Detected a Django app
? Choose an app name (leave blank to generate one): dfb-ch3-pages
automatically selected personal organization: Will Vincent
? Choose a region for deployment: Boston, Massachusetts (US) (bos)
App will use 'bos' region as primary
Created app dfb-ch3-pages in organization 'personal'
Admin URL: https://fly.io/apps/dfb-ch3-pages
Hostname: dfb-ch3-pages.fly.dev
? Overwrite "/Users/wsv/Desktop/dfb_42/ch3-pages/.dockerignore"? No
Set secrets on dfb-ch3-pages: SECRET_KEY
? Would you like to set up a Postgresql database now? No
? Would you like to set up an Upstash Redis database now? No
Wrote config file fly.toml
Your app is ready! Deploy with `flyctl deploy`

In your project directory, Fly created two new files: fly.toml and Dockerfile. The fly.toml is a Fly-specific configuration file while the Dockerfile contains instructions for creating a Docker image on Fly servers.

Now that everything is configured, run the command flyctl deploy to deploy our project on Fly servers.

(.venv) $ flyctl deploy
...
 1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
--> v0 deployed successfully

This initial deploy will take a few seconds to run as it uploads your application, verifies the app configuration, builds the image, and then monitors to ensure it starts successfully. Once complete, visit your app with the following command to open your custom URL in a web browser:

(.venv) $ fly open

Note: If you are wondering why some commands start with flyctl and others with fly, you are not alone. Initially, all Fly commands required flyctl, but many work just as well with simply fly at this point. When in doubt, use flyctl, but often fly will suffice.

If your application didn’t boot on the first deployment, run fly logs to see what is happening. This command shows the past few log file entries and tails your production log files. Additional flags are available for filtering.

As a brief recap of the Fly deployment process:

  • our Django website is running on a Docker virtual machine created via the Dockerfile image
  • the fly.toml file controls our website configuration and can be modified as needed
  • fly dashboard opens a web browser-based view to monitor and adjust our deployment as needed

Conclusion

Congratulations on building and deploying your second Django project! This time we used templates, class-based views, explored URLs more fully, added basic tests, and used Fly for deployment. If you feel overwhelmed by the deployment process, don’t worry. Deployment is hard even with a tool like Fly. The good news is the steps required are the same for most projects, so you can refer to a deployment checklist to guide you each time you launch a new project.

The complete source code for this chapter is available on GitHub if you need a reference. In the next chapter, we’ll move on to our first database-backed project, a Message Board website, and see where Django truly shines.