Chapter 18: Getting Started with Django

Source: Python Crash Course, 3rd Edition by Eric Matthes

As the internet has evolved, the line between websites and mobile apps has blurred. Websites and apps both help users interact with data in a variety of ways. Fortunately, you can use Django to build a single project that serves a dynamic website as well as a set of mobile apps.

Django is Python’s most popular web framework, a set of tools designed for building interactive web applications. In this chapter, you’ll learn how to use Django to build a project called Learning Log, an online journal system that lets you keep track of information you’ve learned about different topics.

We’ll write a specification for this project, and then define models for the data the app will work with. We’ll use Django’s admin system to enter some initial data, and then write views and templates so Django can build the site’s pages.

Django can respond to page requests and make it easier to read and write to a database, manage users, and much more. In Chapters 19 and 20, you’ll refine the Learning Log project, and then deploy it to a live server so you (and everyone else in the world) can use it.

Setting Up a Project

When starting work on something as significant as a web app, you first need to describe the project’s goals in a specification, or spec. Once you have a clear set of goals, you can start to identify manageable tasks to achieve those goals.

Writing a Spec

A full spec details the project goals, describes the project’s functionality, and discusses its appearance and user interface. Like any good project or business plan, a spec should keep you focused and help keep your project on track. We won’t write a full project spec here, but we’ll lay out a few clear goals to keep the development process focused.

Here’s the spec we’ll use:

We’ll write a web app called Learning Log that allows users to log the topics they’re interested in and make journal entries as they learn about each topic. The Learning Log home page will describe the site and invite users to either register or log in. Once logged in, a user can create new topics, add new entries, and read and edit existing entries.

When you’re researching a new topic, maintaining a journal of what you’ve learned can help you keep track of new information and information you’ve already found. This is especially true when studying technical subjects. A good app, like the one we’ll be creating, can help make this process more efficient.

Creating a Virtual Environment

To work with Django, we’ll first set up a virtual environment. A virtual environment is a place on your system where you can install packages and isolate them from all other Python packages. Separating one project’s libraries from other projects is beneficial and will be necessary when we deploy Learning Log to a server in Chapter 20.

Create a new directory for your project called learning_log, switch to that directory in a terminal, and enter the following code to create a virtual environment:

learning_log$ python -m venv ll_env
learning_log$

Here we’re running the venv virtual environment module and using it to create an environment named ll_env (note that this name starts with two lowercase Ls, not two ones). If you use a command such as python3 when running programs or installing packages, make sure to use that command here.

Activating the Virtual Environment

Now we need to activate the virtual environment, using the following command:

learning_log$ source ll_env/bin/activate
(ll_env)learning_log$

This command runs the script activate in ll_env/bin/. When the environment is active, you’ll see the name of the environment in parentheses. This indicates that you can install new packages to the environment and use packages that have already been installed. Packages you install in ll_env will not be available when the environment is inactive.

If you’re using Windows, use the command ll_env\Scripts\activate (without the word source) to activate the virtual environment. If you’re using PowerShell, you might need to capitalize Activate.

To stop using a virtual environment, enter deactivate:

(ll_env)learning_log$ deactivate
learning_log$

The environment will also become inactive when you close the terminal it’s running in.

Installing Django

With the virtual environment activated, enter the following to update pip and install Django:

(ll_env)learning_log$ pip install --upgrade pip
(ll_env)learning_log$ pip install django
Collecting django
--snip--
Installing collected packages: sqlparse, asgiref, django
Successfully installed asgiref-3.5.2 django-4.1 sqlparse-0.4.2
(ll_env)learning_log$

Because it downloads resources from a variety of sources, pip is upgraded fairly often. It’s a good idea to upgrade pip whenever you make a new virtual environment.

We’re working in a virtual environment now, so the command to install Django is the same on all systems. There’s no need to use longer commands, such as python -m pip install package_name, or to include the --user flag. Keep in mind that Django will be available only when the ll_env environment is active.

Django releases a new version about every eight months, so you may see a newer version when you install Django. This project will most likely work as it’s written here, even on newer versions of Django. If you want to make sure to use the same version of Django you see here, use the command pip install django==4.1.*. This will install the latest release of Django 4.1. If you have any issues related to the version you’re using, see the online resources for this book at ehmatthes.github.io/pcc_3e.

Creating a Project in Django

Without leaving the active virtual environment (remember to look for ll_env in parentheses in the terminal prompt), enter the following commands to create a new project:

(ll_env)learning_log$ django-admin startproject ll_project .  (1)
(ll_env)learning_log$ ls                                       (2)
ll_env  ll_project  manage.py
(ll_env)learning_log$ ls ll_project                           (3)
__init__.py  asgi.py  settings.py  urls.py  wsgi.py
1 The startproject command tells Django to set up a new project called ll_project. The dot (.) at the end of the command creates the new project with a directory structure that will make it easy to deploy the app to a server when we’re finished developing it.
2 Running the ls command (dir on Windows) shows that Django has created a new directory called ll_project. It also created a manage.py file, which is a short program that takes in commands and feeds them to the relevant part of Django. We’ll use these commands to manage tasks, such as working with databases and running servers.
3 The ll_project directory contains four files; the most important are settings.py, urls.py, and wsgi.py. The settings.py file controls how Django interacts with your system and manages your project. The urls.py file tells Django which pages to build in response to browser requests. The wsgi.py file helps Django serve the files it creates. The filename is an acronym for "web server gateway interface."

Don’t forget this dot, or you might run into some configuration issues when you deploy the app. If you forget the dot, delete the files and folders that were created (except ll_env) and run the command again.

Creating the Database

Django stores most of the information for a project in a database, so next we need to create a database that Django can work with. Enter the following command (still in an active environment):

(ll_env)learning_log$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK    (1)
  Applying auth.0001_initial... OK
  --snip--
  Applying sessions.0001_initial... OK
(ll_env)learning_log$ ls                      (2)
db.sqlite3  ll_env  ll_project  manage.py
1 Anytime we modify a database, we say we’re migrating the database. Issuing the migrate command for the first time tells Django to make sure the database matches the current state of the project. The first time we run this command in a new project using SQLite, Django will create a new database for us. Here, Django reports that it will prepare the database to store information it needs to handle administrative and authentication tasks.
2 Running the ls command shows that Django created another file called db.sqlite3. SQLite is a database that runs off a single file; it’s ideal for writing simple apps because you won’t have to pay much attention to managing the database.

In an active virtual environment, use the command python to run manage.py commands, even if you use something different, like python3, to run other programs. In a virtual environment, the command python refers to the version of Python that was used to create the virtual environment.

Viewing the Project

Let’s make sure that Django has set up the project properly. Enter the runserver command to view the project in its current state:

(ll_env)learning_log$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).  (1)
May 19, 2022 - 21:52:35
Django version 4.1, using settings 'll_project.settings'  (2)
Starting development server at http://127.0.0.1:8000/  (3)
Quit the server with CONTROL-C.
1 Django first checks to make sure the project is set up properly.
2 It then reports the version of Django in use and the name of the settings file in use.
3 Finally, it reports the URL where the project is being served. The URL 127.0.0.1:8000/ indicates that the project is listening for requests on port 8000 on your computer, which is called a localhost. The term localhost refers to a server that only processes requests on your system; it doesn’t allow anyone else to see the pages you’re developing.

Open a web browser and enter the URL localhost:8000/, or 127.0.0.1:8000/ if the first one doesn’t work. You should see a page that Django creates to let you know everything is working properly so far. Keep the server running for now, but when you want to stop the server, press Ctrl+C in the terminal where the runserver command was issued.

If you receive the error message "That port is already in use," tell Django to use a different port by entering python manage.py runserver 8001 and then cycling through higher numbers until you find an open port.

Try It Yourself

18-1. New Projects: To get a better idea of what Django does, build a couple empty projects and look at what Django creates. Make a new folder with a simple name, like tik_gram or insta_tok (outside of your learning_log directory), navigate to that folder in a terminal, and create a virtual environment. Install Django and run the command django-admin startproject tg_project . (making sure to include the dot at the end of the command). Look at the files and folders this command creates, and compare them to Learning Log.

Starting an App

A Django project is organized as a group of individual apps that work together to make the project work as a whole. For now, we’ll create one app to do most of our project’s work. We’ll add another app in Chapter 19 to manage user accounts.

You should leave the development server running in the terminal window you opened earlier. Open a new terminal window (or tab) and navigate to the directory that contains manage.py. Activate the virtual environment, and then run the startapp command:

learning_log$ source ll_env/bin/activate
(ll_env)learning_log$ python manage.py startapp learning_logs
(ll_env)learning_log$ ls                                        (1)
db.sqlite3  learning_logs  ll_env  ll_project  manage.py
(ll_env)learning_log$ ls learning_logs/                         (2)
__init__.py  admin.py  apps.py  migrations  models.py  tests.py  views.py
1 The command startapp appname tells Django to create the infrastructure needed to build an app. When you look in the project directory now, you’ll see a new folder called learning_logs.
2 The most important files are models.py, admin.py, and views.py. We’ll use models.py to define the data we want to manage in our app. We’ll look at admin.py and views.py a little later.

Defining Models

Let’s think about our data for a moment. Each user will need to create a number of topics in their learning log. Each entry they make will be tied to a topic, and these entries will be displayed as text. We’ll also need to store the timestamp of each entry so we can show users when they made each one.

Open the file models.py and look at its existing content:

models.py
from django.db import models

# Create your models here.

A module called models is being imported, and we’re being invited to create models of our own. A model tells Django how to work with the data that will be stored in the app. A model is a class; it has attributes and methods, just like every class we’ve discussed. Here’s the model for the topics users will store:

models.py
from django.db import models

class Topic(models.Model):
    """A topic the user is learning about."""
    text = models.CharField(max_length=200)        (1)
    date_added = models.DateTimeField(auto_now_add=True)  (2)

    def __str__(self):                             (3)
        """Return a string representation of the model."""
        return self.text
1 The text attribute is a CharField, a piece of data that’s made up of characters or text. You use CharField when you want to store a small amount of text, such as a name, a title, or a city. When we define a CharField attribute, we have to tell Django how much space it should reserve in the database. Here we give it a max_length of 200 characters, which should be enough to hold most topic names.
2 The date_added attribute is a DateTimeField, a piece of data that will record a date and time. We pass the argument auto_now_add=True, which tells Django to automatically set this attribute to the current date and time whenever the user creates a new topic.
3 It’s a good idea to tell Django how you want it to represent an instance of a model. If a model has a str() method, Django calls that method whenever it needs to generate output referring to an instance of that model. Here we’ve written a str() method that returns the value assigned to the text attribute.

To see the different kinds of fields you can use in a model, see the Model Field Reference page at docs.djangoproject.com/en/4.1/ref/models/fields. You won’t need all the information right now, but it will be extremely useful when you’re developing your own Django projects.

Activating Models

To use our models, we have to tell Django to include our app in the overall project. Open settings.py (in the ll_project directory); you’ll see a section that tells Django which apps are installed in the project:

settings.py
# --snip--
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
# --snip--

Add our app to this list by modifying INSTALLED_APPS so it looks like this:

settings.py
# --snip--
INSTALLED_APPS = [
    # My apps.
    'learning_logs',

    # Default django apps.
    'django.contrib.admin',
    # --snip--
]
# --snip--

Grouping apps together in a project helps keep track of them as the project grows to include more apps. Here we start a section called My apps, which includes only 'learning_logs' for now. It’s important to place your own apps before the default apps, in case you need to override any behavior of the default apps with your own custom behavior.

Next, we need to tell Django to modify the database so it can store information related to the model Topic. From the terminal, run the following command:

(ll_env)learning_log$ python manage.py makemigrations learning_logs
Migrations for 'learning_logs':
  learning_logs/migrations/0001_initial.py
    - Create model Topic
(ll_env)learning_log$

The command makemigrations tells Django to figure out how to modify the database so it can store the data associated with any new models we’ve defined. The output here shows that Django has created a migration file called 0001_initial.py. This migration will create a table for the model Topic in the database.

Now we’ll apply this migration and have Django modify the database for us:

(ll_env)learning_log$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, learning_logs, sessions
Running migrations:
  Applying learning_logs.0001_initial... OK

Most of the output from this command is identical to the output from the first time we issued the migrate command. We need to check the last line in this output, where Django confirms that the migration for learning_logs worked OK.

Whenever we want to modify the data that Learning Log manages, we’ll follow these three steps: modify models.py, call makemigrations on learning_logs, and tell Django to migrate the project.

The Django Admin Site

Django makes it easy to work with your models through its admin site. Django’s admin site is only meant to be used by the site’s administrators; it’s not meant for regular users. In this section, we’ll set up the admin site and use it to add some topics through the Topic model.

Setting Up a Superuser

Django allows you to create a superuser, a user who has all privileges available on the site. A user’s privileges control the actions they can take. To create a superuser in Django, enter the following command and respond to the prompts:

(ll_env)learning_log$ python manage.py createsuperuser
Username (leave blank to use 'eric'): ll_admin   (1)
Email address:                                    (2)
Password:                                         (3)
Password (again):
Superuser created successfully.
(ll_env)learning_log$
1 When you issue the command createsuperuser, Django prompts you to enter a username for the superuser. Here I’m using ll_admin, but you can enter any username you want.
2 You can enter an email address or just leave this field blank.
3 You’ll need to enter your password twice.

Some sensitive information can be hidden from a site’s administrators. For example, Django doesn’t store the password you enter; instead, it stores a string derived from the password, called a hash. Each time you enter your password, Django hashes your entry and compares it to the stored hash. If the two hashes match, you’re authenticated. By requiring hashes to match, Django ensures that if an attacker gains access to a site’s database, they’ll be able to read the stored hashes but not the passwords. When a site is set up properly, it’s almost impossible to get the original passwords from the hashes.

Registering a Model with the Admin Site

Django includes some models in the admin site automatically, such as User and Group, but the models we create need to be added manually. When we started the learning_logs app, Django created an admin.py file in the same directory as models.py. Open the admin.py file:

admin.py
from django.contrib import admin

# Register your models here.

To register Topic with the admin site, enter the following:

admin.py
from django.contrib import admin

from .models import Topic

admin.site.register(Topic)

This code first imports the model we want to register, Topic. The dot in front of models tells Django to look for models.py in the same directory as admin.py. The code admin.site.register() tells Django to manage our model through the admin site.

Now use the superuser account to access the admin site. Go to localhost:8000/admin/ and enter the username and password for the superuser you just created. You should see a page that allows you to add new users and groups, and change existing ones. You can also work with data related to the Topic model that we just defined.

If you see a message in your browser that the web page is not available, make sure you still have the Django server running in a terminal window. If you don’t, activate a virtual environment and reissue the command python manage.py runserver. If you’re having trouble viewing your project at any point in the development process, closing any open terminals and reissuing the runserver command is a good first troubleshooting step.

Adding Topics

Now that Topic has been registered with the admin site, let’s add our first topic. Click Topics to go to the Topics page, which is mostly empty, because we have no topics to manage yet. Click Add Topic, and a form for adding a new topic appears. Enter Chess in the first box and click Save. You’ll be sent back to the Topics admin page, and you’ll see the topic you just created.

Let’s create a second topic so we’ll have more data to work with. Click Add Topic again, and enter Rock Climbing. Click Save, and you’ll be sent back to the main Topics page again. Now you’ll see Chess and Rock Climbing listed.

Defining the Entry Model

For a user to record what they’ve been learning about chess and rock climbing, we need to define a model for the kinds of entries users can make in their learning logs. Each entry needs to be associated with a particular topic. This relationship is called a many-to-one relationship, meaning many entries can be associated with one topic.

Here’s the code for the Entry model. Place it in your models.py file:

models.py
from django.db import models

class Topic(models.Model):
    # --snip--

class Entry(models.Model):                                     (1)
    """Something specific learned about a topic."""
    topic = models.ForeignKey(Topic, on_delete=models.CASCADE)  (2)
    text = models.TextField()                                  (3)
    date_added = models.DateTimeField(auto_now_add=True)

    class Meta:                                                (4)
        verbose_name_plural = 'entries'

    def __str__(self):
        """Return a simple string representing the entry."""
        return f"{self.text[:50]}..."                          (5)
1 The Entry class inherits from Django’s base Model class, just as Topic did.
2 The first attribute, topic, is a ForeignKey instance. A foreign key is a database term; it’s a reference to another record in the database. This is the code that connects each entry to a specific topic. The on_delete=models.CASCADE argument tells Django that when a topic is deleted, all the entries associated with that topic should be deleted as well. This is known as a cascading delete.
3 Next is an attribute called text, which is an instance of TextField. This kind of field doesn’t need a size limit, because we don’t want to limit the size of individual entries. The date_added attribute allows us to present entries in the order they were created, and to place a timestamp next to each entry.
4 The Meta class is nested inside the Entry class. The Meta class holds extra information for managing a model; here, it lets us set a special attribute telling Django to use Entries when it needs to refer to more than one entry. Without this, Django would refer to multiple entries as Entrys.
5 The str() method tells Django which information to show when it refers to individual entries. Because an entry can be a long body of text, str() returns just the first 50 characters of text. We also add an ellipsis to clarify that we’re not always displaying the entire entry.

Migrating the Entry Model

Because we’ve added a new model, we need to migrate the database again. This process will become quite familiar: you modify models.py, run the command python manage.py makemigrations app_name, and then run the command python manage.py migrate.

Migrate the database and check the output by entering the following commands:

(ll_env)learning_log$ python manage.py makemigrations learning_logs
Migrations for 'learning_logs':
  learning_logs/migrations/0002_entry.py   (1)
    - Create model Entry
(ll_env)learning_log$ python manage.py migrate
Operations to perform:
  --snip--
  Applying learning_logs.0002_entry... OK  (2)
1 A new migration called 0002_entry.py is generated, which tells Django how to modify the database to store information related to the model Entry.
2 When we issue the migrate command, we see that Django applied this migration and everything worked properly.

Registering Entry with the Admin Site

We also need to register the Entry model. Here’s what admin.py should look like now:

admin.py
from django.contrib import admin

from .models import Topic, Entry

admin.site.register(Topic)
admin.site.register(Entry)

Go back to localhost/admin/, and you should see Entries listed under Learning_Logs. Click the Add link for Entries, or click Entries and then choose Add entry. You should see a drop-down list to select the topic you’re creating an entry for and a text box for adding an entry.

Select Chess from the drop-down list, and add some entries. Make a second entry for Chess and one entry for Rock Climbing so we have some initial data to work with.

The Django Shell

Now that we’ve entered some data, we can examine it programmatically through an interactive terminal session. This interactive environment is called the Django shell, and it’s a great environment for testing and troubleshooting your project. Here’s an example of an interactive shell session:

(ll_env)learning_log$ python manage.py shell
>>> from learning_logs.models import Topic       (1)
>>> Topic.objects.all()
<QuerySet [<Topic: Chess>, <Topic: Rock Climbing>]>
1 The command python manage.py shell, run in an active virtual environment, launches a Python interpreter that you can use to explore the data stored in your project’s database. Here, we import the model Topic from the learning_logs.models module. We then use the method Topic.objects.all() to get all instances of the model Topic; the list that’s returned is called a queryset.

We can loop over a queryset just as we’d loop over a list. Here’s how you can see the ID that’s been assigned to each topic object:

>>> topics = Topic.objects.all()
>>> for topic in topics:
...     print(topic.id, topic)
...
1 Chess
2 Rock Climbing

We assign the queryset to topics and then print each topic’s id attribute and the string representation of each topic. We can see that Chess has an ID of 1 and Rock Climbing has an ID of 2.

If you know the ID of a particular object, you can use the method Topic.objects.get() to retrieve that object and examine any attribute the object has. Let’s look at the text and date_added values for Chess:

>>> t = Topic.objects.get(id=1)
>>> t.text
'Chess'
>>> t.date_added
datetime.datetime(2022, 5, 20, 3, 33, 36, 928759, tzinfo=datetime.timezone.utc)

We can also look at the entries related to a certain topic. Earlier, we defined the topic attribute for the Entry model. This was a ForeignKey, a connection between each entry and a topic. Django can use this connection to get every entry related to a certain topic, like this:

>>> t.entry_set.all()                           (1)
<QuerySet [<Entry: The opening is the first part of the game, roughly...>,
 <Entry: In the opening phase of the game, it's important t...>]>
1 To get data through a foreign key relationship, you use the lowercase name of the related model followed by an underscore and the word set. For example, say you have the models Pizza and Topping, and Topping is related to Pizza through a foreign key. If your object is called my_pizza, representing a single pizza, you can get all of the pizza’s toppings using the code my_pizza.topping_set.all().

We’ll use this syntax when we begin to code the pages users can request. The shell is really useful for making sure your code retrieves the data you want it to. If your code works as you expect it to in the shell, it should also work properly in the files within your project. If your code generates errors or doesn’t retrieve the data you expect it to, it’s much easier to troubleshoot your code in the simple shell environment than within the files that generate web pages.

Each time you modify your models, you’ll need to restart the shell to see the effects of those changes. To exit a shell session, press Ctrl+D; on Windows, press Ctrl+Z and then press Enter.

Try It Yourself

18-2. Short Entries: The str() method in the Entry model currently appends an ellipsis to every instance of Entry when Django shows it in the admin site or the shell. Add an if statement to the str() method that adds an ellipsis only if the entry is longer than 50 characters. Use the admin site to add an entry that’s fewer than 50 characters in length, and check that it doesn’t have an ellipsis when viewed.

18-3. The Django API: When you write code to access the data in your project, you’re writing a query. Skim through the documentation for querying your data at docs.djangoproject.com/en/4.1/topics/db/queries. Much of what you see will look new to you, but it will be quite useful as you start to work on your own projects.

18-4. Pizzeria: Start a new project called pizzeria_project with an app called pizzas. Define a model Pizza with a field called name, which will hold name values, such as Hawaiian and Meat Lovers. Define a model called Topping with fields called pizza and name. The pizza field should be a foreign key to Pizza, and name should be able to hold values such as pineapple, Canadian bacon, and sausage. Register both models with the admin site, and use the site to enter some pizza names and toppings. Use the shell to explore the data you entered.

Making Pages: The Learning Log Home Page

Making web pages with Django consists of three stages: defining URLs, writing views, and writing templates. You can do these in any order, but in this project we’ll always start by defining the URL pattern. A URL pattern describes the way the URL is laid out. It also tells Django what to look for when matching a browser request with a site URL, so it knows which page to return.

Each URL then maps to a particular view. The view function retrieves and processes the data needed for that page. The view function often renders the page using a template, which contains the overall structure of the page.

Because we just want to ensure that Learning Log works as it’s supposed to, we’ll make a simple page for now. For now, the home page will display only a title and a brief description.

Mapping a URL

Users request pages by entering URLs into a browser and clicking links, so we’ll need to decide what URLs are needed. The home page URL is first: it’s the base URL people use to access the project. At the moment, the base URL, localhost:8000/, returns the default Django site that lets us know the project was set up correctly. We’ll change this by mapping the base URL to Learning Log’s home page.

In the main ll_project folder, open the file urls.py. You should see the following code:

ll_project/urls.py
from django.contrib import admin
from django.urls import path          (1)

urlpatterns = [                       (2)
    path('admin/', admin.site.urls),  (3)
]
1 The first two lines import the admin module and a function to build URL paths.
2 The body of the file defines the urlpatterns variable. In this urls.py file, which defines URLs for the project as a whole, the urlpatterns variable includes sets of URLs from the apps in the project.
3 The list includes the module admin.site.urls, which defines all the URLs that can be requested from the admin site.

We need to include the URLs for learning_logs, so add the following:

ll_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('learning_logs.urls')),
]

We’ve imported the include() function, and we’ve also added a line to include the module learning_logs.urls.

The default urls.py is in the ll_project folder; now we need to make a second urls.py file in the learning_logs folder. Create a new Python file, save it as urls.py in learning_logs, and enter this code into it:

learning_logs/urls.py
"""Defines URL patterns for learning_logs."""   (1)

from django.urls import path                    (2)

from . import views                             (3)

app_name = 'learning_logs'                      (4)
urlpatterns = [                                 (5)
    # Home page
    path('', views.index, name='index'),        (6)
]
1 To make it clear which urls.py we’re working in, we add a docstring at the beginning of the file.
2 We then import the path function, which is needed when mapping URLs to views.
3 We also import the views module; the dot tells Python to import the views.py module from the same directory as the current urls.py module.
4 The variable app_name helps Django distinguish this urls.py file from files of the same name in other apps within the project.
5 The variable urlpatterns in this module is a list of individual pages that can be requested from the learning_logs app.
6 The actual URL pattern is a call to the path() function, which takes three arguments. The first argument is a string that helps Django route the current request properly. Django receives the requested URL and tries to route the request to a view. It does this by searching all the URL patterns we’ve defined to find one that matches the current request. Django ignores the base URL for the project (localhost:8000/), so the empty string ('') matches the base URL. The second argument specifies which function to call in views.py. The third argument provides the name index for this URL pattern so we can refer to it more easily in other files throughout the project.

Writing a View

A view function takes in information from a request, prepares the data needed to generate a page, and then sends the data back to the browser, often using a template that defines what the page will look like.

The file views.py in learning_logs was generated automatically when we ran the command python manage.py startapp. Here’s what’s in views.py right now:

views.py
from django.shortcuts import render

# Create your views here.

Currently, this file just imports the render() function, which renders the response based on the data provided by views. Open views.py and add the following code for the home page:

views.py
from django.shortcuts import render

def index(request):
    """The home page for Learning Log."""
    return render(request, 'learning_logs/index.html')

When a URL request matches the pattern we just defined, Django looks for a function called index() in the views.py file. Django then passes the request object to this view function. In this case, we don’t need to process any data for the page, so the only code in the function is a call to render(). The render() function here passes two arguments: the original request object and a template it can use to build the page.

Writing a Template

The template defines what the page should look like, and Django fills in the relevant data each time the page is requested. A template allows you to access any data provided by the view.

Inside the learning_logs folder, make a new folder called templates. Inside the templates folder, make another folder called learning_logs. This might seem a little redundant (we have a folder named learning_logs inside a folder named templates inside a folder named learning_logs), but it sets up a structure that Django can interpret unambiguously, even in the context of a large project containing many individual apps.

Inside the inner learning_logs folder, make a new file called index.html. The path to the file will be learning_log/learning_logs/templates/learning_logs/index.html. Enter the following code into that file:

index.html
<p>Learning Log</p>

<p>Learning Log helps you keep track of your learning, for any topic
you're interested in.</p>

This is a very simple file. If you’re not familiar with HTML, the <p> tags signify paragraphs. The <p> tag opens a paragraph, and the </p> tag closes a paragraph. We have two paragraphs: the first acts as a title, and the second describes what users can do with Learning Log.

Now when you request the project’s base URL, localhost:8000/, you should see the page we just built instead of the default Django page. Django will take the requested URL, and that URL will match the pattern ''; then Django will call the function views.index(), which will render the page using the template contained in index.html.

Although it might seem like a complicated process for creating one page, this separation between URLs, views, and templates works quite well. It allows you to think about each aspect of a project separately. In larger projects, it allows individuals working on the project to focus on the areas in which they’re strongest.

You might see the following error message:

ModuleNotFoundError: No module named 'learning_logs.urls'

If you do, stop the development server by pressing Ctrl+C in the terminal window where you issued the runserver command. Then reissue the command python manage.py runserver. Anytime you run into an error like this, try stopping and restarting the server.

Try It Yourself

18-5. Meal Planner: Consider an app that helps people plan their meals throughout the week. Make a new folder called meal_planner, and start a new Django project inside this folder. Then make a new app called meal_plans. Make a simple home page for this project.

18-6. Pizzeria Home Page: Add a home page to the Pizzeria project you started in Exercise 18-4.

Building Additional Pages

Now that we’ve established a routine for building a page, we can start to build out the Learning Log project. We’ll build two pages that display data: a page that lists all topics and a page that shows all the entries for a particular topic. For each page, we’ll specify a URL pattern, write a view function, and write a template. But before we do this, we’ll create a base template that all templates in the project can inherit from.

Template Inheritance

When building a website, some elements will need to be repeated on each page. Rather than writing these elements directly into each page, you can write a base template containing the repeated elements and then have each page inherit from the base. This approach lets you focus on developing the unique aspects of each page, and makes it much easier to change the overall look and feel of the project.

The Parent Template

We’ll create a template called base.html in the same directory as index.html. This file will contain elements common to all pages; every other template will inherit from base.html. The only element we want to repeat on each page right now is the title at the top. Because we’ll include this template on every page, let’s make the title a link to the home page:

base.html
<p>
  <a href="{% url 'learning_logs:index' %}">Learning Log</a>  (1)
</p>

{% block content %}{% endblock content %}  (2)
1 The first part of this file creates a paragraph containing the name of the project, which also acts as a home page link. To generate a link, we use a template tag, which is indicated by braces and percent signs ({% %}). The template tag {% url 'learning_logs:index' %} generates a URL matching the URL pattern defined in learning_logs/urls.py with the name 'index'. In this example, learning_logs is the namespace and index is a uniquely named URL pattern in that namespace. The namespace comes from the value we assigned to app_name in the learning_logs/urls.py file. Having the template tag generate the URL makes it much easier to keep our links up to date.
2 On the last line, we insert a pair of block tags. This block, named content, is a placeholder; the child template will define the kind of information that goes in the content block. A child template doesn’t have to define every block from its parent, so you can reserve space in parent templates for as many blocks as you like.

In Python code, we almost always use four spaces when we indent. Template files tend to have more levels of nesting than Python files, so it’s common to use only two spaces for each indentation level.

The Child Template

Now we need to rewrite index.html to inherit from base.html. Add the following code to index.html:

index.html
{% extends 'learning_logs/base.html' %}  (1)

{% block content %}                      (2)
  <p>Learning Log helps you keep track of your learning, for any topic
  you're interested in.</p>
{% endblock content %}                   (3)
1 A child template must have an {% extends %} tag on the first line to tell Django which parent template to inherit from. The file base.html is part of learning_logs, so we include learning_logs in the path to the parent template. This line pulls in everything contained in the base.html template and allows index.html to define what goes in the space reserved by the content block.
2 We define the content block by inserting a {% block %} tag with the name content. Everything that we aren’t inheriting from the parent template goes inside the content block. Here, that’s the paragraph describing the Learning Log project.
3 We indicate that we’re finished defining the content by using an {% endblock content %} tag.

You can start to see the benefit of template inheritance: in a child template, we only need to include content that’s unique to that page. This not only simplifies each template, but also makes it much easier to modify the site. To modify an element common to many pages, you only need to modify the parent template.

The Topics Page

Now that we have an efficient approach to building pages, we can focus on our next two pages: the general topics page and the page to display entries for a single topic.

The Topics URL Pattern

First, we define the URL for the topics page. We’ll use the word topics, so the URL localhost:8000/topics/ will return this page. Here’s how we modify learning_logs/urls.py:

learning_logs/urls.py
"""Defines URL patterns for learning_logs."""
# --snip--
urlpatterns = [
    # Home page
    path('', views.index, name='index'),
    # Page that shows all topics.
    path('topics/', views.topics, name='topics'),
]

The Topics View

The topics() function needs to retrieve some data from the database and send it to the template. Add the following to views.py:

views.py
from django.shortcuts import render

from .models import Topic              (1)

def index(request):
    # --snip--

def topics(request):                   (2)
    """Show all topics."""
    topics = Topic.objects.order_by('date_added')   (3)
    context = {'topics': topics}                    (4)
    return render(request, 'learning_logs/topics.html', context)  (5)
1 We first import the model associated with the data we need.
2 The topics() function needs one parameter: the request object Django received from the server.
3 We query the database by asking for the Topic objects, sorted by the date_added attribute. We assign the resulting queryset to topics.
4 We then define a context that we’ll send to the template. A context is a dictionary in which the keys are names we’ll use in the template to access the data we want, and the values are the data we need to send to the template.
5 When building a page that uses data, we call render() with the request object, the template we want to use, and the context dictionary.

The Topics Template

The template for the topics page receives the context dictionary, so the template can use the data that topics() provides. Make a file called topics.html in the same directory as index.html. Here’s how we can display the topics in the template:

topics.html
{% extends 'learning_logs/base.html' %}

{% block content %}

  <p>Topics</p>

  <ul>                           (1)
    {% for topic in topics %}    (2)
      <li>                       (3)
        <a href="">{{ topic.text }}</a>
      </li>
    {% empty %}                  (4)
      <li>No topics have been added yet.</li>
    {% endfor %}                 (5)
  </ul>                         (6)

{% endblock content %}
1 In standard HTML, a bulleted list is called an unordered list and is indicated by the <ul></ul> tags. The opening tag <ul> begins the bulleted list of topics.
2 Next we use a template tag that’s equivalent to a for loop, which loops through the list topics from the context dictionary. In a template, every for loop needs an explicit {% endfor %} tag indicating where the end of the loop occurs.
3 Inside the loop, we want to turn each topic into an item in the bulleted list. To print a variable in a template, wrap the variable name in double braces. The code {{ topic.text }} will be replaced by the value of the current topic’s text attribute on each pass through the loop. The HTML tag <li> indicates a list item.
4 We also use the {% empty %} template tag, which tells Django what to do if there are no items in the list.
5 The {% endfor %} tag closes the for loop.
6 The </ul> tag closes the bulleted list.

Now we need to modify the base template to include a link to the topics page. Add the following code to base.html:

base.html
<p>
  <a href="{% url 'learning_logs:index' %}">Learning Log</a> -  (1)
  <a href="{% url 'learning_logs:topics' %}">Topics</a>         (2)
</p>

{% block content %}{% endblock content %}
1 We add a dash after the link to the home page.
2 And then add a link to the topics page using the {% url %} template tag again. This line tells Django to generate a link matching the URL pattern with the name 'topics' in learning_logs/urls.py.

Individual Topic Pages

Next, we need to create a page that can focus on a single topic, showing the topic name and all the entries for that topic.

The Topic URL Pattern

The URL pattern for the topic page is a little different from the prior URL patterns because it will use the topic’s id attribute to indicate which topic was requested. For example, if the user wants to see the detail page for the Chess topic (where the id is 1), the URL will be localhost:8000/topics/1/. Here’s a pattern to match this URL, which you should place in learning_logs/urls.py:

learning_logs/urls.py
# --snip--
urlpatterns = [
    # --snip--
    # Detail page for a single topic.
    path('topics/<int:topic_id>/', views.topic, name='topic'),
]

The string 'topics/<int:topic_id>/' in this URL pattern tells Django to look for URLs that have the word topics after the base URL. The second part of the string, <int:topic_id>, matches an integer between two forward slashes and assigns the integer value to an argument called topic_id. When Django finds a URL that matches this pattern, it calls the view function topic() with the value assigned to topic_id as an argument.

The Topic View

The topic() function needs to get the topic and all associated entries from the database, much like what we did earlier in the Django shell:

views.py
# --snip--
def topic(request, topic_id):                                    (1)
    """Show a single topic and all its entries."""
    topic = Topic.objects.get(id=topic_id)                       (2)
    entries = topic.entry_set.order_by('-date_added')            (3)
    context = {'topic': topic, 'entries': entries}               (4)
    return render(request, 'learning_logs/topic.html', context)  (5)
1 This is the first view function that requires a parameter other than the request object. The function accepts the value captured by the expression <int:topic_id> and assigns it to topic_id.
2 We use get() to retrieve the topic, just as we did in the Django shell.
3 We get all of the entries associated with this topic and order them according to date_added. The minus sign in front of date_added sorts the results in reverse order, which will display the most recent entries first.
4 We store the topic and entries in the context dictionary.
5 We call render() with the request object, the topic.html template, and the context dictionary.

The code phrases at (2) and (3) are called queries, because they query the database for specific information. When you’re writing queries like these in your own projects, it’s helpful to try them out in the Django shell first. You’ll get much quicker feedback in the shell than you would by writing a view and template and then checking the results in a browser.

The Topic Template

The template needs to display the name of the topic and the entries. We also need to inform the user if no entries have been made yet for this topic.

topic.html
{% extends 'learning_logs/base.html' %}

{% block content %}

  <p>Topic: {{ topic.text }}</p>          (1)

  <p>Entries:</p>
  <ul>                                    (2)
    {% for entry in entries %}            (3)
      <li>
        <p>{{ entry.date_added|date:'M d, Y H:i' }}</p>  (4)
        <p>{{ entry.text|linebreaks }}</p>                (5)
      </li>
    {% empty %}                           (6)
      <li>There are no entries for this topic yet.</li>
    {% endfor %}
  </ul>

{% endblock content %}
1 We extend base.html, as we’ll do for all pages in the project. Next, we show the text attribute of the topic that’s been requested. The variable topic is available because it’s included in the context dictionary.
2 We then start a bulleted list to show each of the entries.
3 We loop through the entries, as we did with the topics earlier.
4 For the timestamp, we display the value of the attribute date_added. In Django templates, a vertical line (|) represents a template filter — a function that modifies the value in a template variable during the rendering process. The filter date:'M d, Y H:i' displays timestamps in the format January 1, 2022 23:00.
5 The filter linebreaks ensures that long text entries include line breaks in a format understood by browsers, rather than showing a block of uninterrupted text.
6 We again use the {% empty %} template tag to print a message informing the user that no entries have been made.

Before we look at the topic page in a browser, we need to modify the topics template so each topic links to the appropriate page. Here’s the change you need to make to topics.html:

topics.html
# --snip--
{% for topic in topics %}
  <li>
    <a href="{% url 'learning_logs:topic' topic.id %}">{{ topic.text }}</a>
  </li>
{% empty %}
# --snip--

We use the URL template tag to generate the proper link, based on the URL pattern in learning_logs with the name 'topic'. This URL pattern requires a topic_id argument, so we add the attribute topic.id to the URL template tag. Now each topic in the list of topics is a link to a topic page, such as localhost:8000/topics/1/.

There’s a subtle but important difference between topic.id and topic_id. The expression topic.id examines a topic and retrieves the value of the corresponding ID. The variable topic_id is a reference to that ID in the code. If you run into errors when working with IDs, make sure you’re using these expressions in the appropriate ways.

Try It Yourself

18-7. Template Documentation: Skim the Django template documentation at docs.djangoproject.com/en/4.1/ref/templates. You can refer back to it when you’re working on your own projects.

18-8. Pizzeria Pages: Add a page to the Pizzeria project from Exercise 18-6 that shows the names of available pizzas. Then link each pizza name to a page displaying the pizza’s toppings. Make sure you use template inheritance to build your pages efficiently.

Summary

In this chapter, you learned how to start building a simple web app using the Django framework. You saw a brief project specification, installed Django to a virtual environment, set up a project, and checked that the project was set up correctly. You set up an app and defined models to represent the data for your app. You learned about databases and how Django helps you migrate your database after you make a change to your models. You created a superuser for the admin site, and you used the admin site to enter some initial data.

You also explored the Django shell, which allows you to work with your project’s data in a terminal session. You learned how to define URLs, create view functions, and write templates to make pages for your site. You also used template inheritance to simplify the structure of individual templates and make it easier to modify the site as the project evolves.

In Chapter 19, you’ll make intuitive, user-friendly pages that allow users to add new topics and entries and edit existing entries without going through the admin site. You’ll also add a user registration system, allowing users to create an account and make their own learning log. This is the heart of a web app — the ability to create something that any number of users can interact with.

Applied Exercises: Ch 18 — Getting Started with Django

These exercises apply the chapter’s patterns — virtual environments, Django project/app setup, model definition, migrations, admin registration, Django shell queries, URL patterns, views, and template inheritance — in infrastructure, security, and language learning contexts. Where a live Django install is available in your environment, implement the full project; otherwise, implement the model definitions, shell query patterns, and URL/view/template structure as Python code with inline comments.

Domus Digitalis / Homelab

D18-1. Domus Inventory Project: Create a new Django project called domus_project with an app called inventory. Define a model called Node with fields hostname (CharField, max_length=100), role (CharField, max_length=50), vlan_id (IntegerField), and date_added (DateTimeField, auto_now_add=True). Add a str() method that returns the hostname. Register Node with the admin site. Use the admin to add at least 4 nodes (kvm-01, kvm-02, ise-02, bind-01).

D18-2. Service Entry Model: Add a ServiceEntry model to the inventory app with a ForeignKey to Node and fields service_name (CharField), status (CharField), and checked_at (DateTimeField, auto_now_add=True). Add a Meta class with verbose_name_plural = 'service entries' and a str() that returns the first 50 chars of service_name. Run makemigrations and migrate. Register with admin.

D18-3. Domus Shell Queries: Using the Django shell, write and test the following queries for your Domus inventory app: (a) get all nodes, (b) get nodes ordered by date_added, (c) retrieve the node with id=1, (d) get all service entries for that node using entry_set.all(). Print the results of each query.

D18-4. Domus Home Page: Define a URL pattern, view function, and template for a home page that displays a brief description of the Domus inventory system. Use template inheritance with a base.html that includes the project name as a link. Verify the page renders at localhost:8000/.

D18-5. Node and Service Pages: Add a nodes/ URL pattern and view that queries all nodes ordered by date_added and passes them to a nodes.html template. Add a nodes/<int:node_id>/ URL pattern and view that retrieves a single node and all its service entries, ordered by -checked_at. Add links between the nodes list and individual node pages using {% url %} template tags.

CHLA / ISE / Network Security

C18-1. ISE Policy Log Project: Create a Django project called ise_project with an app called policy_logs. Define a model called PolicySet with fields name (CharField), protocol (CharField), result (CharField), and date_added (DateTimeField, auto_now_add=True). Add a str() returning name. Register with admin. Add 4 policy sets via the admin interface.

C18-2. Auth Event Model: Add an AuthEvent model with a ForeignKey to PolicySet and fields username (CharField), mac_address (CharField), outcome (CharField), and timestamp (DateTimeField, auto_now_add=True). Add a Meta class with verbose_name_plural = 'auth events' and a str() returning the first 50 chars of username. Run migrations and register with admin.

C18-3. ISE Shell Queries: Using the Django shell, write and test: (a) get all PolicySet objects, (b) get the policy set with id=1, (c) get the value of its protocol attribute, (d) get all auth events for that policy set using authingevent_set.all() (or equivalent entry_set pattern). Print each result.

C18-4. ISE Home Page: Define a URL, view, and template for a home page describing the ISE policy log system. Use base.html template inheritance with the project name as a link. Verify at localhost:8000/.

C18-5. Policy Set and Auth Event Pages: Add a policy_sets/ URL/view that lists all policy sets. Add a policy_sets/<int:ps_id>/ URL/view that shows a single policy set and all its auth events ordered by -timestamp. Add a filter date:'M d, Y H:i' to the event timestamp in the template. Link policy set names on the list page to their detail pages using {% url %}.

General Sysadmin / Linux

L18-1. Service Monitor Project: Create a Django project called sysmon_project with an app called services. Define a model called Server with fields hostname (CharField), environment (CharField, e.g. 'prod'/'staging'/'lab'), and date_added (DateTimeField, auto_now_add=True). Add a str() returning hostname. Register with admin. Add 4 servers via the admin interface.

L18-2. Service Status Model: Add a ServiceStatus model with a ForeignKey to Server and fields service_name (CharField), status (CharField), and checked_at (DateTimeField, auto_now_add=True). Add verbose_name_plural = 'service statuses' and a str() that returns the first 50 chars of service_name. Run migrations and register with admin.

L18-3. Sysmon Shell Queries: Using the Django shell, write and test: (a) get all servers, (b) get the server with id=1, (c) get its environment value, (d) get all service statuses for that server. Print each result.

L18-4. Sysmon Home Page: Define a URL, view, and template for a home page describing the service monitoring system. Use base.html template inheritance. Verify at localhost:8000/.

L18-5. Server and Service Pages: Add a servers/ URL/view that lists all servers ordered by date_added. Add a servers/<int:server_id>/ URL/view that shows a single server and all its service statuses ordered by -checked_at. Apply a date: filter to checked_at in the template. Link server names on the list page to their detail pages.

Spanish / DELE C2

E18-1. Vocab Journal Project: Create a Django project called vocab_project with an app called journal. Define a model called Topic with fields nombre (CharField, max_length=200, e.g. 'Don Quijote Ch. 30') and date_added (DateTimeField, auto_now_add=True). Add a str() returning nombre. Register with admin. Add 4 topics via the admin interface.

E18-2. Vocabulary Entry Model: Add a VocabEntry model with a ForeignKey to Topic and fields palabra (CharField), definicion (TextField), and date_added (DateTimeField, auto_now_add=True). Add verbose_name_plural = 'vocab entries' and a str() returning the first 50 chars of palabra. Run migrations and register with admin.

E18-3. Vocab Shell Queries: Using the Django shell, write and test: (a) get all topics, (b) get the topic with id=1, (c) get the value of its nombre attribute, (d) get all vocab entries for that topic using vocabentry_set.all(). Print each result.

E18-4. Vocab Home Page: Define a URL, view, and template for a home page describing the vocabulary journal. Use base.html template inheritance with the project name as a link. Verify at localhost:8000/.

E18-5. Topic and Entry Pages: Add a topics/ URL/view that lists all topics ordered by date_added. Add a topics/<int:topic_id>/ URL/view that shows a single topic and all its vocab entries ordered by -date_added. Apply linebreaks to definicion in the entry template. Link topic names on the list page to their detail pages using {% url %}.