Building Django 2.0 Web Applications

4.6 (5 reviews total)
By Tom Aratyn
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Starting MyMDB

About this book

This project-based guide will give you a sound understanding of Django 2.0 through three full-featured applications. It starts off by building a basic IMDB clone and adding users who can register, vote on their favorite movies, and upload associated pictures. You will learn how to use the votes that your users have cast to build a list of the top 10 movies. This book will also take you through deploying your app into a production environment using Docker containers hosted on the server in Amazon's Electric Computing Cloud (EC2).

Next, you're going to build a Stack Overflow clone wherein registered users can ask and answer questions. You will learn how to enable a user asking a question to accept answers and mark them as useful. You will also learn how to add search functionality to help users find questions by using ElasticSearch. You'll discover ways to apply the principles of 12 factor apps while deploying Django on the most popular web server, Apache, with mod_wsgi. Lastly, you'll build a clone of MailChimp so users can send and create emails, and deploy it using AWS.

Get set to take your basic Python skills to the next level with this comprehensive guide!

Publication date:
December 2018
Publisher
Packt
Pages
408
ISBN
9781787286214

 

Chapter 1. Starting MyMDB

The first project we will build is a basic Internet Movie Database (IMDB) clone calledMy Movie Database (MyMDB)written in Django 2.0 that we will deploy using Docker. Our IMDB clone will have the following two types of users: users and administrators. The users will be able to rate movies, add images from movies, and view movies and cast. The administrators will be able to add movies, actors, writers, and directors.

In this chapter, we'll do the following things:

  • Create our new Django project MyMDB, an IMDB clone
  • Make a Django app and create our first models, views, and templates
  • Learn about and use a variety of fields in our models and create relationships across models

Note

The code for this project is available online at https://github.com/tomaratyn/MyMDB. 

By the end, we'll be able to add movies, people, and roles into our project and let users view them in easy-to-customize HTML templates.

 

Starting My Movie Database (MyMDB)


First, let's make a directory for our project:

$ mkdir MyMDB
$ cd MyMDB

All our future commands and paths will be relative to this project directory.

Starting the project

A Django project is composed of multiple Django apps. A Django app can come from many different places:

  • Django itself (for example, django.contrib.admin, the admin backend app)
  • Installed Python packages (for example,django-rest-framework, a framework for creating REST APIs from Django models)
  • Written as part of the project (the code we'll be writing)

Usually, a project uses a mix of all of the preceding three options.

Installing Django

We'll install Django using pip, Python's preferred package manager and track which packages we install in a requirements.dev.txt file:

django<2.1
psycopg2<2.8

Now, let's install the packages:

$ pip install -r requirements.dev.txt

Creating the project

With Django installed, we have the django-admin command-line tool with which we can generate our project:

$ django-admin startproject config
$ tree config/
config/
├── config
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

The parent of the settings.py file is called config because we named our project config instead of mymdb. However, letting that top-level directory continue to be called config is confusing, so let's just rename it django (a project may grow to contain lots of different types of code; calling the parent of the Django code django, again, makes it clear):

$ mv config django 
$ tree .
.
├── django
│   ├── config
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── manage.py
└── requirements.dev.txt

2 directories, 6 files

Let's take a closer look at some of these files:

  • settings.py: This is where Django stores all the configuration for your app by default. In the absence of a DJANGO_SETTINGS environment variable, this is where Django looks for settings by default.
  • urls.py: This is the root URLConf for the entire project. Every request that your web app gets will get routed to the first view that matches a path inside this file (or a file urls.py reference).
  • wsgi.py: Web Server Gateway Interface (WSGI) is the interface between Python and a web server. You won't touch this file very much, but it's how your web server and your Python code know how to talk to each other. We'll reference it in Chapter 5, Deploying with Docker.
  • manage.py: This is the command center for making non-code changes. Whether it's creating a database migration, running tests, or starting the development server, we will use this file often.

Note

Note what's missing is that the django directory is not a Python module. There's no __init__.py file in there, and there should not be. If you add one, many things will break because we want the Django apps we add to be top-level Python modules.

Configuring database settings

By default, Django creates a project that will use SQLite, but that's not usable for production, so we'll follow the best practice of using the same database in development as in production.

Let's open up django/config/settings.py and update it to use our Postgres server. Find the line in settings.py that starts with DATABASES. By default, it will look like this:

DATABASES = {
  'default': {
     'ENGINE': 'django.db.backends.sqlite3',
     'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
  }
}

To use Postgres, change the preceding code to the following one:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mymdb',
        'USER': 'mymdb',
        'PASSWORD': 'development',
        'HOST': '127.0.0.1',
        'PORT': '5432',
    }
}

Most of this will seem familiar if you've connected to a database before, but let's review:

  • DATABASES = {: This constant is a dictionary of database connection information and is required by Django. You can have multiple connections to different databases, but, most of the time, you will just need an entry called default.
  • 'default': {: This is the default database connection configuration. You should always have a default set of connections settings. Unless you specify otherwise (and, in this book, we won't), this is the connection you'll be using.
  • 'ENGINE': 'django.db.backends.postgresql ': This tells Django to use the Postgres backend. This in turn uses psycopg2, Python's Postgres library.
  • 'NAME': 'mymdb',: The name of the database you want to connect to.
  • ‘USER': 'mymdb',: The username for your connection.
  • ‘PASSWORD': 'development',: The password for your database user.
  • ‘HOST': '127.0.0.1’,: The address of the database server you want to connect to.
  • ‘PORT': '5432',: The port you want to connect to.
 

The core app


Django apps follow a Model View Template (MVT) pattern; in this pattern, we will note the following things:

  • Models are responsible for saving and retrieving data from the database
  • Views are responsible for processing HTTP Requests, initiating operations on Models, and returning HTTP responses
  • Templates are responsible for the look of the response body

There's no limit on how many apps you can have in your Django project. Ideally, each app should have a tightly scoped and self-contained functionality like any other Python module, but at the beginning of a project, it can be hard to know where the complexity will lie. That's why I find it useful to start off with a core app. Then, when I notice clusters of complexity around particular topics (let's say, in our project, actors could become unexpectedly complex if we're getting traction there), then we can refactor that into its own tightly scoped app. Other times, it's clear that a site has self-contained components (for example, an admin backend), and it's easy to start off with multiple apps.

Making the core app

To make a new Django app, we first have to use manage.py to create the app and then add it to the list of INSTALLED_APPS:

$ cd django
$ python manage.py startapp core
$ ls
config      core        manage.py
$tree core
core
├─  472; __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

1 directory, 7 files

Let's take a closer look at what's inside of the core:

  • core/__init__.py: The core is not just a directory, but also a Python module.
  • admin.py: This is where we will register our models with the built-in admin backend. We'll describe that in the Movie Admin section.
  • apps.py: Most of the time, you'll leave this alone. This is where you would put any code that needs to run when registering your application, which is useful if you're making a reusable Django app (for example, a package you want to upload to PyPi).
  • migrations: This is a Python module with database migrations. Database migrations describe how to migrate the database from one known state to another. With Django, if you add a model, you can just generate and run a migration using manage.py, which you can see later in this chapter in the Migrating the database section.
  • models.py: This is for models.
  • tests.py: This is for tests.
  • views.py: This is for views.

Installing our app

Now that our core app exists, let's make Django aware of it by adding it to the list of installed apps in settings.py file. Your settings.py should have a line that looks like this:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

INSTALLED_APPS is a list of Python paths to Python modules that are Django apps. We already have apps installed to solve common problems, such as managing static files, sessions, and authentication and an admin backend because of Django's Batteries Included philosophy.

Let's add our core app to the top of that list:

INSTALLED_APPS = [
    'core',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Adding our first model – Movie

Now we can add our first model, that is, Movie.

A Django model is a class that is derived from Model and has one or more Fields. In database terms, a Model class corresponds to a database table, Field classes correspond to columns, and instances of a Model correspond to rows. Using an ORM like Django's, let's take advantage of Python and Django to write expressive classes instead of DB writing our models once in Python and again in SQL.

Let's edit django/core/models.py to add a Movie model:

from django.db import models

class Movie(models.Model):
    NOT_RATED = 0
    RATED_G = 1
    RATED_PG = 2
    RATED_R = 3
    RATINGS = (
        (NOT_RATED, 'NR - Not Rated'),
        (RATED_G,
         'G - General Audiences'),
        (RATED_PG,
         'PG - Parental Guidance '
         'Suggested'),
        (RATED_R, 'R - Restricted'),
    )

    title = models.CharField(
        max_length=140)
    plot = models.TextField()
    year = models.PositiveIntegerField()
    rating = models.IntegerField(
        choices=RATINGS,
        default=NOT_RATED)
    runtime = \
        models.PositiveIntegerField()
    website = models.URLField(
        blank=True)

    def __str__(self):
        return '{} ({})'.format(
            self.title, self.year)

Movie is derived from models.Model, which is the base class for all Django models. Next, there's a series of constants that describe ratings; we'll take a look at that when we look at the rating field, but first let's look at the other fields:

  • title = models.CharField(max_length=140): This will become a varchar column with a length of 140. Databases generally require a maximum size for varchar columns, so Django does too.
  • plot = models.TextField(): This will become a text column in our database, which has no maximum length requirement. This makes it more appropriate for a field that can have a paragraph (or even pages) of text.
  • year = models.PositiveIntegerField(): This will become an integer column, and Django will validate the value before saving it to ensure that it is 0 or higher when you save it.
  • rating = models.IntegerField(choices=RATINGS, default=NOT_RATED): This is a more complicated field. Django will know that this is going to be an integer column. The optional argument choices (which is available for all Fields, not just IntegerField) takes an iterable (list or tuple) of value/display pairs. The first element in the pair is a valid value that can be stored in the database and the second is a human-friendly version of the value. Django will also add an instance method to our model called get_rating_display(), which will return the matching second element for the value stored in our model. Anything that doesn't match one of the values in choices will be a ValidationError on save. The default argument provides a default value if one is not provided when creating the model.
  • runtime = models.PositiveIntegerField(): This is the same as the year field.
  • website = models.URLField(blank=True): Most databases don't have a native URL column type, but data-driven web apps often need to store them. A URLField is a varchar(200) field by default (this can be set by providing a max_length argument). URLField also comes with validation, checking whether its value is a valid web (http/https/ftp/ftps) URL. The blank argument is used by the admin app to know whether to require a value (it does not affect the database).

Our model also has a __str__(self) method, which is a best practice that helps Django convert the model to a string. Django does this in the administrative UI and in our own debugging.

Django's ORM automatically adds an autoincrementing id column, so we don't have to repeat that on all our models. It's a simple example of Django's Don't Repeat Yourself(DRY) philosophy. We'll take a look at more examples as we go along.

Migrating the database

Now that we have a model, we will need to create a table in our database that matches it. We will use Django to generate a migration for us and then run the migration to create a table for our movie model.

While Django can create and run migrations for our Django apps, it will not create the database and database user for our Django project. To create the database and user, we have to connect to the server using an administrator's account. Once we've connected we can create the database and user by executing the following SQL:

CREATE DATABASE mymdb;
CREATE USER mymdb;
GRANT ALL ON DATABASE mymdb to "mymdb";
ALTER USER mymdb PASSWORD 'development';
ALTER USER mymdb CREATEDB;

The above SQL statements will create the database and user for our Django project. The GRANT statement ensures that our mymdb user will have access to the database. Then, we set a password on the mymdb user (make sure it's the same as in your settings.py file). Finally, we give the mymdb user permission to create new databases, which will be used by Django to create a test database when running tests.

To generate a migration for our app, we'll need to tell manage.py file to do as follows:

$ cd django
$ python manage.py makemigrations core
Migrations for 'core':
  core/migrations/0001_initial.py
    - Create model Movie

A migration is a Python file in our Django app that describes how to change the database into a desired state. Django migrations are not tied to a particular database system (the same migrations will work across supported databases, unless we add database-specific code). Django generates migration files that use Django's migrations API, which we won't be looking at in this book, but it's useful to know that it exists.

Note

Remember that it's apps not projects that have migrations (since it's apps that have models).

Next, we tell manage.py to migrate our app:

$ python manage.py migrate core 
Operations to perform:
  Apply all migrations: core
Running migrations:
  Applying core.0001_initial... OK

Now, our table exists in our database:

$ python manage.py dbshell
psql (9.6.1, server 9.6.3)
Type "help" for help.

mymdb=> \dt
             List of relations
 Schema |       Name        | Type  | Owner 
--------+-------------------+-------+-------
 public | core_movie        | table | mymdb
 public | django_migrations | table | mymdb
(2 rows)

mymdb=> \q

We can see that our database has two tables. The default naming scheme for Django's model's tables is <app_name>_<model_name>. We can tell core_movie is the table for the Movie model from the core app. django_migrations is for Django's internal use to track the migrations that have been applied. Altering the django_migrations table directly instead of using manage.py is a bad idea, which will lead to problems when you try to apply or roll back migrations.

The migration commands can also run without specifying an app, in which case it will run on all the apps. Let's run the migrate command without an app:

$ python manage.py migrate 
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, core, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying sessions.0001_initial... OK

This creates tables to keep track of users, sessions, permissions, and the administrative backend.

Creating our first movie

Like Python, Django offers an interactive REPL to try things out. The Django shell is fully connected to the database, so we can create, query, update, and delete models from the shell:

$ cd django
$ python manage.py shell
Python 3.4.6 (default, Aug  4 2017, 15:21:32) 
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from core.models import Movie
>>> sleuth = Movie.objects.create(
... title='Sleuth',
... plot='An snobbish writer who loves games'
... ' invites his wife\'s lover for a battle of wits.',
... year=1972,
... runtime=138,
... )
>>> sleuth.id
1
>>> sleuth.get_rating_display()
'NR - Not Rated'

In the preceding Django shell session, note that there are a number of attributes of Movie that we didn't create: 

  • objects is the model's default manager. Managers are an interface for querying the model's table. It also offers a create() method for creating and saving an instance. Every model must have at least one manager, and Django offers a default manager. It's often advisable to create a custom manager; we'll see that later in the Adding Person and model relationships section.
  • id is the primary key of the row for this instance. As mentioned in the preceding step, Django creates it automatically.
  • get_rating_display() is a method that Django added because the rating field was given a tuple of choices. We didn't have to provide rating with a value in our create() call because the rating field has a default value (0). The get_rating_display() method looks up the value and returns the corresponding display value. Django will generate a method like this for each Field attribute with a choices argument.

Next, let's create a backend for managing movies using the Django Admin app.

Creating movie admin

Being able to quickly generate a backend UI lets users to start building the content of the project while the rest of the project is still in development. It's a nice feature that helps parallelize progress and avoid a repetitious and boring task (read/update views share a lot of functionalities). Providing this functionality out of the box is another example of Django's Batteries Included philosophy.

To get Django's admin app working with our models, we will perform the following steps:

  1. Register our model
  2. Create a super user who can access the backend
  3. Run the development server
  4. Access the backend in a browser

Let's register our Movie model with the admin by editing django/core/admin.py, as follows:

from django.contrib import admin

from core.models import Movie

admin.site.register(Movie)

Now our model is registered!

Let's now create a user who can access the backend using manage.py:

$ cd django
$ python manage.py createsuperuser 
Username (leave blank to use 'tomaratyn'): 
Email address: [email protected]
Password: 
Password (again): 
Superuser created successfully.

Django ships with a development server that can serve our app, but is not appropriate for production:

$ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
September 12, 2017 - 20:31:54
Django version 1.11.5, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Also, open it in a browser by navigating to http://localhost:8000/:

To access the admin backend, go to http://localhost:8000/admin:

Once we log in with the credentials, we have to manage users and movies:

Clicking on MOVIES will show us a list of movies:

Note that the title of the link is the result of our Movie.__str__ method. Clicking on it will give you a UI to edit the movie:

On the main admin screen and on the movie list screen, you have links to add a new movie. Let's add a new movie:

Now, our movie list shows both movies:

Now that we have a way of letting our team populate the database with movies, let's start working on the views for our users.

Creating MovieList view

When Django gets a request, it uses the path of the request and the URLConf of the project to match a request to a view, which returns an HTTP response. Django's views can be either functions, often referred to as Function-Based Views (FBVs), or classes, often called Class-Based Views (CBVs). The advantage of CBVs is that Django comes with a rich suite of generic views that you can subclass to easily (almost declaratively) write views to accomplish common tasks.

Let's write a view to list the movies that we have. Open django/core/views.py and change it to the following:

from django.views.generic import ListView

from core.models import Movie


class MovieList(ListView):
    model = Movie

ListView requires at least a model attribute. It will query for all the rows of that model, pass it to the template, and return the rendered template in a response. It also offers a number of hooks that we may use to replace default behavior, which are fully documented.

How does ListView know how to query all the objects in Movie? For that, we will need to discuss manager and QuerySet classes. Every model has a default manager. Manager classes are primarily used to query objects by offering methods, such as all(), that return a QuerySet. A QuerySet class is Django's representation of a query to the database. QuerySet has a number of methods, including filter() (such as a WHERE clause in a SELECT statement) to limit a result. One of the nice features of the QuerySet class is that it is lazy; it is not evaluated until we try to get a model out of the QuerySet. Another nice feature is that methods such as filter() take lookup expressions, which can be field names or span across relationship models. We'll be doing this throughout our projects.

Note

All manager classes have an all() method that should return an unfiltered Queryset, the equivalent of writing SELECT * FROM core_movie;. 

So, how does ListView know that it has to query all the objects in Movie? ListView checks whether it has a model attribute, and, if present, knows that Model classes have a default manager with a all() method, which it calls. ListView also gives us a convention for where to put our template, as follows: <app_name>/<model_name>_list.html.

Adding our first template – movie_list.html

Django ships with its own template language called the Django Template language. Django can also use other template languages (for example, Jinja2), but most Django projects find using the Django Template language to be efficient and convenient.

In the default configuration that is generated in our settings.py file, the Django Template language is configured to use APP_DIRS, meaning that each Django app can have a templates directory, which will be searched to find a template. This can be used to override templates that other apps use without having to modify the third-party apps themselves.

Let's make our first template in django/core/templates/core/movie_list.html:

<!DOCTYPE html>
<html>
  <body>
    <ul>
      {% for movie in object_list %}
        <li>{{ movie }}</li>
      {% empty %}
        <li>
          No movies yet.
        </li>
      {% endfor %}
    </ul>
    <p>
      Using https? 
      {{ request.is_secure|yesno }}
    </p>
  </body>
</html>

Django templates are standard HTML (or whatever text format you wish to use) with variables (for example, object_list in our example) and tags (for example, for in our example). Variables will be evaluated to strings by being surrounded with {{ }}. Filters can be used to help format or modify variables before being printed (for example,yesno). We can also create custom tags and filters.

Note

A full list of filters and tags is provided in the Django docs (https://docs.djangoproject.com/en/2.0/ref/templates/builtins/).

The Django template language is configured in theTEMPLATESvariable ofsettings.py. TheDjangoTemplatesbackend can take a lot ofOPTIONS. In development, it can be helpful to add'string_if_invalid': 'INVALID_VALUE',. Any time Django can't match a variable in a template to a variable or tag, it will print outINVALID_VALUE, which makes it easier to catch typos. Remember that you should not use this setting inProduction. The full list of options is availablein Django's documentation (https://docs.djangoproject.com/en/dev/topics/templates/#django.template.backends.django.DjangoTemplates).

The final step will be to connect our view to a URLConf.

Routing requests to our view with URLConf

Now that we have a model, view, and template, we will need to tell Django which requests it should route to our MovieList View using a URLConf. Each new project has a root URLConf that created by Django (in our case it's the django/config/urls.py file). Django developers have developed the best practice of each app having its own URLConf. Then, the root URLConf of a project will include each app's URLConf using the include() function.

Let's create a URLConf for our core app by creating a  django/core/urls.py file with the following code:

from django.urls import path

from . import views

app_name = 'core'
urlpatterns = [
    path('movies',
         views.MovieList.as_view(),
         name='MovieList'),
]

At its simplest, a URLConf is a module with a urlpatterns attribute, which is a list of paths. A path is composed of a string that describes a string, describing the path in question and a callable. CBVs are not callable, so the base View class has a static as_view() method that returns a callable. FBVs can just be passed in as a callback (without the () operator, which would execute them).

Each path() should be named, which is a helpful best practice for when we have to reference that path in our template. Since a URLConf can be included by another URLConf, we may not know the full path to our view. Django offers a reverse() function and url template tag to go from a name to the full path to a view.

The app_name variable sets the app that this URLConf belongs to. This way, we can reference a named path without Django getting confused about other apps having apath of the same name (for example, index is a very common name, so we can say appA:index and appB:index to distinguish between them).

Finally, let's connect our URLConf to the root URLConf by changing django/config/urls.py to the following:

from django.urls import path, include
from django.contrib import admin

import core.urls

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

This file looks much like our file previous URLConf, except that our path() object isn't taking a view but instead the result of the include() function. The include() function lets us prefix an entire URLConf with a path and give it a custom namespace.

Namespaces let us distinguish between path names like the app_name attribute does, except without modifying the app (for example, a third-party app).

Note

You might wonder why we're using include() but the Django Admin site is using property? Both include() and admin.site.urls return similarly formatted 3-tuple. However, instead of remembering what each portion of the 3-tuple has to have, you should just use include().

Running the development server

Django now knows how to route requests to our View, which knows the Models that need to be shown and which template to render. We can tell manage.py to start our development server and view our result:

$ cd django
$ python manage.py runserver

In our browser, go to http://127.0.0.1:8000/movies:

Good job! We made our first page!

In this section, we created our first model, generated and ran the migration for it, and created a view and template so that users can browse it.

Now, let's add a page for each movie.

 

Individual movie pages


Now that we have our project layout, we can move more quickly. We're already tracking information for each movie. Let's create a view that will show that information.

To add movie details, we'll need to do the following things:

  1. Create a MovieDetail view
  2. Create movie_detail.html template
  3. Reference to our MovieDetail view in our URLConf

Creating the MovieDetail view

Just like Django provides us with a ListView class to do all the common tasks of listing models, Django also provides a DetailView class that we can subclass to create a view showing the details of a single Model.

Let’s create our view in django/core/views.py:

from django.views.generic import (
    ListView, DetailView,
)
from core.models import Movie

class MovieDetail(DetailView):
    model = Movie

class MovieList(ListView):
    model = Movie

A DetailView requires that a path() object include either a pk or slug in the path string so that DetailView can pass that value to the QuerySet to query for a specific model instance. A slug is a short URL-friendly label that is often used in content-heavy sites, as it is SEO friendly.

Creating the movie_detail.html template

Now that we have the View, let's make our template.

Django's Template language supports template inheritance, which means that you can write a template with all the look and feel for your website and mark the block sections that other templates will override. This lets us to create the look and feel of the entire website without having to edit each template. Let's use this to create a base template with MyMDB’s branding and look and feel and then add a Movie Detail template that inherits from the base template.

A base template shouldn't be tied to a particular app, so let's make a general templates directory:

$ mkdir django/templates

Django doesn't know to check our templates directory yet, so we will need to update the configuration in our settings.py file. Find the line that starts with TEMPLATES and change the configuration to list our templates directory in the DIRS list:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            # omittted for brevity
        },
    },
]

The only change we've made is that we added our new templates directory to the list under the DIRS key. We have avoided hardcoding the path to our templates directory using Python's os.path.join() function and the already configured BASE_DIR. BASE_DIR is set at runtime to the path of the project. We don't need to add django/core/templates because the APP_DIRS setting tells Django to check each app for the templates directory.

Note

Although it's very convenient that settings.py is the Python file where we can use os.path.join and all of Python, be careful not to get too clever. settings.py needs to be easy to read and understand. There's nothing worse than having to debug your settings.py.

Let's create a base template in django/templates/base.html that has a main column and sidebar:

<!DOCTYPE html>
<html lang="en" >
<head >
  <meta charset="UTF-8" >
  <meta
    name="viewport"
    content="width=device-width, initial-scale=1, shrink-to-fit=no"
  >
  <link
    href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"
    integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M"
    rel="stylesheet"
    crossorigin="anonymous"
  >
  <title >
    {% block title %}MyMDB{% endblock %}
  </title>
  <style>
    .mymdb-masthead {
      background-color: #EEEEEE;
      margin-bottom: 1em;
    }
  </style>

</head >
<body >
<div class="mymdb-masthead">
  <div class="container">
    <nav class="nav">
      <div class="navbar-brand">MyMDB</div>
      <a
        class="nav-link"
        href="{% url 'core:MovieList' %}"
      >
        Movies
      </a>
    </nav>
  </div>
</div>

<div class="container">
  <div class="row">
    <div class="col-sm-8 mymdb-main">
     {% block main %}{% endblock %}
    </div>
    <div
        class="col-sm-3 offset-sm-1 mymdb-sidebar"
    >
      {% block sidebar %}{% endblock %}
    </div>
  </div>
</div>

</body >
</html >

Most of this HTML is actually bootstrap (HTML/CSS framework) boilerplate, but we do have a few new Django tags:

  • {% block title %}MyMDB{% endblock %}: This creates a block that other templates can replace. If the block is not replaced, the contents from the parent template will be used.
  • href="{% url 'core:MovieList' %}": The url tag will produce a URL path for the named path. URL names should be referenced as <app_namespace>:<name>; in our case, core is the namespace of the core app (per django/core/urls.py), and MovieList is the name of the MovieList view's URL.

This lets us create a simple template in django/core/templates/core/movie_detail.html:

{% extends 'base.html' %}

{% block title %}
  {{ object.title }} - {{ block.super }}
{% endblock %}

{% block main %}
<h1>{{ object }}</h1>
<p class="lead">
{{ object.plot }}
</p>
{% endblock %}

{% block sidebar %}
<div>
This movie is rated:
  <span class="badge badge-primary">
  {{ object.get_rating_display }}
  </span>
</div>
{% endblock %}

This template has a lot less HTML in it because base.html already has that. All MovieDetail.html has to do is provide values to the blocks that base.html defines. Let's take a look at some new tags:

  • {% extends 'base.html' %}: If a template wants to extend another template the first line must be an extends tag. Django will look for the base template (which can extend another template) and execute it first, then replace the blocks. A template that extends another cannot have content outside of blocks because it's ambiguous where to put that content.
  • {{ object.title }} - {{ block.super }}: We reference block.superinside the title template block. block.super returns the contents of the titletemplateblock in the base template.
  • {{ object.get_rating_display }}: The Django Template language doesn't use () to execute the method, just referencing it by name will execute the method.

Adding MovieDetail to core.urls.py

Finally, we add our MovieDetail view to core/urls.py:

from django.urls import path

from . import views

urlpatterns = [
    path('movies',
         views.MovieList.as_view(),
         name='MovieList'),
    path('movie/<int:pk>',
         views.MovieDetail.as_view(),
         name='MovieDetail'),
]

The MovieDetail and MovieListpath()calls both look almost the same, except for the MovieDetail string that has a named parameter. A path route string can include angle brackets to give a parameter a name (for example, <pk>) and even define a type that the parameter's content must conform to (for example, <int:pk> will only match values that parse as an int). These named sections are captured by Django and passed to the view by name. DetailView expects a pk (or slug) argument and uses it to get the correct row from the database.

Let's use python manage.py runserver to start the dev server and take a look at what our new template looks like:

A quick review of the section

In this section, we've created a new view, MovieDetail, learned about template inheritance, and how to pass parameters from a URL path to our view.

Next, we'll add pagination to our MovieList view to prevent it from querying the entire database each time.

 

Pagination and linking movie list to movie details


In this section, we'll update our movie list to provide a link to each movie and to have pagination to prevent our entire database being dumped into one page.

Updating MovieList.html to extend base.html

Our original MovieList.html was a pretty sparse affair. Let's update it to look nicer using our base.html template and the bootstrap CSS it provides:

{% extends 'base.html' %}

{% block title %}
All The Movies
{% endblock %}

{% block main %}
<ul>
  {% for movie in object_list %}
    <li>
      <a href="{% url 'core:MovieDetail' pk=movie.id %}">
        {{ movie }}
      </a>
    </li>
  {% endfor %}
  </ul>
{% endblock %}

We're also seeing the url tag being used with a named argumentpkbecause theMovieDetailURL requires apkargument. If there was no argument provided, then Django would raise aNoReverseMatchexception on rendering, resulting in a500error.

Let's take a look at what it looks like:

Setting the order

Another problem with our current view is that it's not ordered. If the database is returning an unordered query, then pagination won't help navigation. What's more, there's no guarantee that each time the user changes pages that the content will be consistent, as the database may return a differently ordered result set for each time. We need our query to be ordered consistently.

Ordering our model also makes our lives as developers easier too. Whether using a debugger, writing tests, or running a shell ensuring that our models are returned in a consistent order can make troubleshooting simpler.

A Django model may optionally have an inner class called Meta, which lets us specify information about a Model. Let's add a Meta class with an ordering attribute:

class Movie(models.Model):
   # constants and fields omitted for brevity 

    class Meta:
        ordering = ('-year', 'title')

    def __str__(self):
        return '{} ({})'.format(
            self.title, self.year)

ordering takes a list or tuple of, usually, strings that are field names, optionally prefixed by a - character that denotes descending order. ('-year', 'title') is the equivalent of the SQL clause ORDER BY year DESC, title.

Adding ordering to a Model's Meta class will mean that QuerySets from the model's manager will be ordered.

Adding pagination

Now that our movies are always ordered the same way, let's add pagination. A Django ListView already has built-in support for pagination, so all we need to do is take advantage of it. Pagination is controlled by the GET parameter page that controls which page to show.

Let's add pagination to the bottom of our main template block:

{% block main %}
 <ul >
    {% for movie in object_list %}
      <li >
        <a href="{% url 'core:MovieDetail' pk=movie.id %}" >
          {{ movie }}
        </a >
      </li >
    {% endfor %}
  </ul >
  {% if is_paginated %}
    <nav >
      <ul class="pagination" >
        <li class="page-item" >
          <a
            href="{% url 'core:MovieList' %}?page=1"
            class="page-link"
          >
            First
          </a >
        </li >
        {% if page_obj.has_previous %}
          <li class="page-item" >
            <a
              href="{% url 'core:MovieList' %}?page={{ page_obj.previous_page_number }}"
              class="page-link"
            >
              {{ page_obj.previous_page_number }}
            </a >
          </li >
        {% endif %}
        <li class="page-item active" >
          <a
            href="{% url 'core:MovieList' %}?page={{ page_obj.number }}"
            class="page-link"
          >
            {{ page_obj.number }}
          </a >
        </li >
        {% if page_obj.has_next %}
          <li class="page-item" >
            <a
              href="{% url 'core:MovieList' %}?page={{ page_obj.next_page_number }}"
              class="page-link"
            >
              {{ page_obj.next_page_number }}
            </a >
          </li >
        {% endif %}
        <li class="page-item" >
          <a
              href="{% url 'core:MovieList' %}?page=last"
              class="page-link"
          >
            Last
          </a >
        </li >
      </ul >
    </nav >
  {% endif %}
{% endblock %}

Let's take a look at some important points of ourMovieList template:

  • page_obj is of the Pagetype, which knows information about this page of results. We use it to check whether there is a next/previous page using has_next()/has_previous() (we don't need to put () in the Django template language, but has_next() is a method, not a property). We also use it to get the next_page_number()/previous_page_number(). Note that it is important to use the has_*() method to check for the existence of  the next/previous page numbers before retrieving them. If they don't exist when retrieved, Page throws an EmptyPage exception.
  • object_list continues to be available and hold the correct values. Even though page_obj encapsulates the results for this page in page_obj.object_list, ListView does the convenient work of ensuring that we can continue to use object_list and our template doesn't break.

We now have the pagination working!

404 – for when things go missing

We now have a couple of views that can't function if given the wrong value in the URL (the wrong pk will break MovieDetail; the wrong page will break MovieList); let’s plan for that by handling 404 errors. Django offers a hook in the root URLConf to let us use a custom view for 404 errors (also for 403, 400, and 500—all following the same names scheme). In your root urls.py file, add a variable called handler404 whose value is a string Python path to your custom view.

However, we can continue to use the default 404 handler view and just write a custom template. Let's add a 404 template in django/templates/404.html:

{% extends "base.html" %}

{% block title %}
Not Found
{% endblock %}

{% block main %}
<h1>Not Found</h1>
<p>Sorry that reel has gone missing.</p>
{% endblock %}

Even if another app throws a 404 error, this template will be used.

At the moment, if you've got an unused URL such as http://localhost:8000/not-a-real-page, you won't see our custom 404 template because Django's DEBUG settings is True in settings.py. To make our 404 template visible, we will need to change the DEBUG and ALLOWED_HOSTS settings in settings.py:

DEBUG = False

ALLOWED_HOSTS = [
    'localhost',
    '127.0.0.1'
]

ALLOWED_HOSTS is a setting that restricts which HOST values in an HTTP request Django will respond to. If DEBUG is False and a HOST does not match an ALLOWED_HOSTS value, then Django will return a 400 error (you can customize both the view and template for this error as described in the preceding code). This is a security feature that protects us and will be discussed more in our chapter on security.

Now that our project is configured, let's run the Django development server:

$ cd django
$ python manage.py runserver

With it running, we can use our web browser to open http://localhost:8000/not-a-real-page. Our results should look like this:

Testing our view and template

Since we now have some logic in our MoveList template, let's write some tests. We'll talk a lot more about testing in the Chapter 8, Testing Answerly. However, the basics are simple and follow the common XUnit pattern of the TestCase classes holding test methods that make assertions.

For Django's TestRunner to find a test, it must be in the tests module of an installed app. Right now, that means tests.py, but, eventually, you may wish to switch to a directory Python module (in which case, prefix your test filenames with test for the TestRunner to find them).

Let's add a test that performs the following functions:

  • If there's more than 10 movies, then pagination controls should be rendered in the template
  • If there's more than 10 movies and we don't provide pageGET parameters, consider the following things:
    • The page_is_last context variable should be False
    • The page_is_first context variable should be True
    • The first item in the pagination should be marked as active

The following is our tests.py file:

from django.test import TestCase
from django.test.client import \
    RequestFactory
from django.urls.base import reverse

from core.models import Movie
from core.views import MovieList


class MovieListPaginationTestCase(TestCase):

    ACTIVE_PAGINATION_HTML = """
    <li class="page-item active">
      <a href="{}?page={}" class="page-link">{}</a>
    </li>
    """

    def setUp(self):
        for n in range(15):
            Movie.objects.create(
                title='Title {}'.format(n),
                year=1990 + n,
                runtime=100,
            )

    def testFirstPage(self):
        movie_list_path = reverse('core:MovieList')
        request = RequestFactory().get(path=movie_list_path)
        response = MovieList.as_view()(request)
        self.assertEqual(200, response.status_code)
        self.assertTrue(response.context_data['is_paginated'])
        self.assertInHTML(
            self.ACTIVE_PAGINATION_HTML.format(
                movie_list_path, 1, 1),
            response.rendered_content)

Let's take a look at some interesting points:

  • class MovieListPaginationTestCase(TestCase): TestCase is the base class for all Django tests. It has a number of conveniences built in, including a number of convenient assert methods.
  • def setUp(self): Like most XUnit testing frameworks, Django's TestCase class offers a setUp() hook that is run before each test. A tearDown() hook is also available if needed. The database is cleaned up between each test, so we don't need to worry about deleting any models we added.
  • def testFirstPage(self):: A method is a test if its name is prefixed with test.
  • movie_list_path = reverse('core:MovieList'): reverse() was mentioned before and is the Python equivalent of the url Django template tag. It will resolve the name into a path.
  • request = RequestFactory().get(path=movie_list_path): RequestFactory is a convenient factory for creating fake HTTP requests. A RequestFactory has convenience methods for creating GET, POST, and PUT requests by its convenience methods named after the verb (for example, get() for GET requests). In our case, the path object provided doesn't matter, but other views may want to inspect the path of the request.
  • self.assertEqual(200, response.status_code): This asserts that the two arguments are equal. A response's status_code to check success or failure (200 being the status code for success—the one code you never see when you browse the web).
  • self.assertTrue(response.context_data['is_paginated']): This asserts that the argument evaluates to True. response exposes the context that is used in rendering the template. This makes finding bugs much easier as you can quickly check actual values used in rendering.
  • self.assertInHTML(: assertInHTML is one of the many convenient methods that Django provides as part of its Batteries Included philosophy. Given a valid HTML string needle and valid HTML string haystack, it will assert that needle is in haystack. The two strings need to be valid HTML because Django will parse them and examine whether one is inside the other. You don't need to worry about spacing or the order of attributes/classes. It's a very convenient assertion when you try to ensure that templates are working right.

To run tests, we can use manage.py:

$ cd django
$ python manage.py test 
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.035s

OK
Destroying test database for alias 'default'...

Finally, we can be confident that we've got pagination working right.

 

Adding Person and model relationships


In this section, we will add relationships between models to our project. People's relationship to movies can create a complex data model. The same person can be the actor, writer, and director (for example, The Apostle (1997) written, directed, and starring Robert Duvall). Even leaving out the crew and production teams and simplifying a bit, the data model will involve a one-to-many relationship using a ForiengKey field, a many-to-many relationship using a ManyToManyField, and a class that adds extra information about a many-to-many relationship using a through class in a ManyToManyField.

In this section, we will do the following things step by step:

  1. Create a Person model
  2. Add a ForeignKey field from Movie to Person to track the director
  3. Add a ManyToManyField from Movie to Person to track the writers
  4. Add a ManyToManyField with a through class (Actor) to track who performed and in what role in a Movie
  5. Create the migration
  6. Add the director, writer, and actors to the movie details template
  7. Add a PersonDetail view to the list that indicates what movies a Person has directed, written, and performed in

Adding a model with relationships

First, we will need a Person class to describe and store a person involved in a movie:

class Person(models.Model):
    first_name = models.CharField(
        max_length=140)
    last_name = models.CharField(
        max_length=140)
    born = models.DateField()
    died = models.DateField(null=True,
                            blank=True)

    class Meta:
        ordering = (
            'last_name', 'first_name')

    def __str__(self):
        if self.died:
            return '{}, {} ({}-{})'.format(
                self.last_name,
                self.first_name,
                self.born,
                self.died)
        return '{}, {} ({})'.format(
                self.last_name,
                self.first_name,
                self.born)

In Person, we also see a new field (DateField) and a new parameter for fields (null).

DateField is used for tracking date-based data, using the appropriate column type on the database (date on Postgres) and datetime.date in Python. Django also offers a DateTimeField to store the date and time.

All fields support the nullparameter (False by default), which indicates whether the column should accept NULL SQL values (represented by None in Python). We mark died as supporting null so that we can record people as living or dead. Then, in the __str__() method we print out a different string representation if someone is alive or dead.

We now have the Person model that can have various relationships with Movies.

Different types of relationship fields

Django's ORM has support for fields that map relationships between models, including one-to-many, many-to-many, and many-to-many with an intermediary model.

When two models have a one-to-many relationship, we use a ForeignKey field, which will create a column with a Foreign Key (FK) constraint (assuming that there is database support) between the two tables. In the model without the ForeignKey field, Django will automatically add a RelatedManager object as an instance attribute. The RelatedManager class makes it easier to query for objects in a relationship. We'll take a look at examples of this in the following sections.

When two models have a many-to-many relationship, either (but not both) of them can get the ManyToManyField(); Django will create a RelatedManager on the other side for you. As you may know, relational databases cannot actually have a many-to-many relationship between two tables. Rather, relational databases require a bridging table with foreign keys to each of related tables. Assuming that we don’t want to add any attributes describing the relationship, Django will create and manage this bridging table for us automatically.

Sometimes, we want extra fields to describe a many-to-many relationship (for example, when it started or ended); for that, we can provide a ManyToManyField with a through model (sometimes called an association class in UML/OO). This model will have a ForeignKey to each side of the relationship and any extra fields we want.

We'll create an example of each of these, as we go along adding directors, writers, and actors into our Movie model.

Director – ForeignKey

In our model, we will say that each movie can have one director, but each director can have directed many movies. Let's use the ForiengKey field to add a director to our movie:

class Movie(models.Model):
   # constants, methods, Meta class and other fields omitted for brevity.
    director = models.ForeignKey(
        to='Person',
        on_delete=models.SET_NULL,
        related_name='directed',
        null=True,
        blank=True)

Let's take a look at our new field line by line:

  • to='Person': All of Django's relationship fields can take a string reference as well as reference to the related model. This argument is required.
  • on_delete=models.SET_NULL: Django needs instruction on what to do when the referenced model (instance/row) is deleted. SET_NULL will set the director field of all the Movie  model instances directed by the deleted Person to NULL. If we wanted to cascade deletes we would use the models.CASCADE object.
  • related_name='directed': This is an optional argument that indicates the name of theRelatedManager instance on the other model (which lets us query all the Movie model instances a Person directed). If related_name were not provided, then Person would get an attribute called movie_set (following the <model with FK>_set pattern). In our case, we will have multiple different relationships between Movie and Person (writer, director, and actors), so movie_set would become ambiguous, and we must provide a related_name.

This is also the first time we're adding a field to an existing model. When doing so, we have to either add null=True or offer a default value. If we do not, then the migration will force us to. This requirement exists because Django has to assume that there are existing rows in the table (even if there aren't) when the migration is run. When a database adds the new column, it needs to know what it should insert into existing rows. In the case of the director field, we can accept that it may sometimes be NULL.

We have now added a field to Movie and a new attribute to Person instances called directed (of the RelatedManagertype). RelatedManager is a very useful class that is like a model’s default Manager, but automatically manages the relationship across the two models.

Let's take a look at person.directed.create() and compare it to Movie.objects.create(). Both methods will create a new Movie, but person.directed.create() will make sure that the new Movie has person as its director. RelatedManager also offers the add and remove methods so that we can add a Movie to a directed set of Person by calling person.directed.add(movie). There's also a remove() method that works similarly, but removes a model from the relationship.

Writers – ManyToManyField

Two models may also have a many-to-many relationship, for example, a person may write many movies and a movie may be written by many people. Next, we'll add a writers field to our Movie model:

class Movie(models.Model):
   # constants, methods, Meta class and other fields omitted for brevity.
    writers = models.ManyToManyField(
        to='Person',
        related_name='writing_credits',
        blank=True)

A ManyToManyField established a many-to-many relationship and acts like a RelatedManager, permitting users to query and create models. We again use the related_name to avoid giving Person a movie_set attribute and instead give it a writing_credits attribute that will be a RelatedManager.

In the case of a ManyToManyField, both sides of the relationship have RelatedManager s so that person.writing_credits.add(movie) has the same effect as writing movie.writers.add(person).

Role – ManyToManyField with a through class

The last example of a relationship field we'll look at is used when we want to use an intermediary model to describe the relationship between two other models that have a many-to-many relationship. Django lets us do this by creating a model that describes the join table between the two models in a many-to-many relationship.

In our case, we will create a many-to-many relationship between Movie and Person through Role, which will have a name attribute:

class Movie(models.Model):
   # constants, methods, Meta class and other fields omitted for brevity.
    actors = models.ManyToManyField(
        to='Person',
        through='Role',
        related_name='acting_credits',
        blank=True)

class Role(models.Model):
    movie = models.ForeignKey(Movie, on_delete=models.DO_NOTHING)
    person = models.ForeignKey(Person, on_delete=models.DO_NOTHING)
    name = models.CharField(max_length=140)

    def __str__(self):
        return "{} {} {}".format(self.movie_id, self.person_id, self.name)

    class Meta:
        unique_together = ('movie',
                           'person',
                           'name')

This looks like the preceding ManyToManyField, except we have both a to (referencing Person as before) argument and a through (referencing Role) argument.

The Role model looks much like one would design a join table; it has a ForeignKey to each side of the many-to-many relationship. It also has an extra field called name to describe the role.

Role also has a unique constraint on it. It requires that movie, person, and billing all to be unique together; setting the unique_together attribute on the Meta class of Role will prevent duplicate data.

This user of ManyToManyField will create four new RelatedManager instances:

  • movie.actors will be a related manager to Person
  • person.acting_credits will be a related manager to Movie
  • movie.role_set will be a related manager to Role
  • person.role_set will be a related manager to Role

We can use any of the managers to query models but only the role_set managers to create models or modify relationships because of the intermediary class. Django will throw an IntegrityError exception if you try to run movie.actors.add(person) because there’s no way to fill in the value for Role.name. However, you can write movie.role_set.add(person=person, name='Hamlet').

Adding the migration

Now, we can generate a migration for our new models:

$ python manage.py makemigrations core
Migrations for 'core':
  core/migrations/0002_auto_20170926_1650.py
    - Create model Person
    - Create model Role
    - Change Meta options on movie
    - Add field movie to role
    - Add field person to role
    - Add field actors to movie
    - Add field director to movie
    - Add field writers to movie
    - Alter unique_together for role (1 constraint(s))

Then, we can run our migration so that the changes get applied:

$ python manage.py migrate core
Operations to perform:
  Apply all migrations: core
Running migrations:
  Applying core.0002_auto_20170926_1651... OK

Next, let's make our movie pages link to the people in the movies.

Creating a PersonView and updating MovieList

Let's add a PersonDetail view that our movie_detail.html template can link to. To create our view, we'll go through a four-step process:

  1. Create a manager to limit the number of database queries
  2. Create our view
  3. Create our template
  4. Create a URL that references our view

Creating a custom manager – PersonManager

Our PersonDetail view will list all the movies in which a Person is acting, writing, or directing credits. In our template, we will print out the name of each film in each credit (and Role.name for the acting credits). To avoid sending a flood of queries to the database, we will create new managers for our models that will return smarter QuerySet s.

In Django, any time we access a property across a relationship, then Django will query the database to get the related item (in the case of looping over each item person.role_set.all(), one for each related Role). In the case of a Person who is in N movies, this will result in N queries to the database. We can avoid this situation with the prefetch_related() method (later we will look at select_related() method). Using the prefetch_related() method, Django will query all the related data across a single relationship in a single additional query. However, if we don't end up using the prefetched data, querying for it will waste time and memory.

Let's create a PersonManager with a new method, all_with_prefetch_movies(), and make it the default manager for Person:

class PersonManager(models.Manager):
    def all_with_prefetch_movies(self):
        qs = self.get_queryset()
        return qs.prefetch_related(
            'directed',
            'writing_credits',
            'role_set__movie')


class Person(models.Model):
    # fields omitted for brevity

    objects = PersonManager()

    class Meta:
        ordering = (
            'last_name', 'first_name')

    def __str__(self):
        # body omitted for brevity

Our PersonManager will still offer all the same methods as the default because PersonManager inherits from models.Manager. We also define a new method, which uses get_queryset() to get a QuerySet, and tells it to prefetch the related models. QuerySets are lazy, so no communication with the database happens until the query set is evaluated (for example by, iteration, casting to a bool, slicing, or evaluated by an if statement). DetailView won't evaluate the query until it uses get() to get the model by PK.

The prefetch_related() method takes one or more lookups, and after the initial query is done, it automatically queries those related models. When you access a model related to the one from your QuerySet, Django won't have to query it, as you will already have it prefetched in the QuerySet.

A lookup is what a Django QuerySet takes to express a field or RelatedManager in a model. A lookup can even span across relationships by separating the name of the relationship field (or RelatedManager) and the related models field with two underscores:

Movie.objects.all().filter(actors__last_name='Freeman', actors__first_name='Morgan')

The preceding call will return a QuerySet for all the Movie  model instances in which Morgan Freeman has been an actor.

In our PersonManager, we're telling Django to prefetch all the movies that a Person has directed, written, and had a role in as well as prefetch the roles themselves. Using theall_with_prefetch_movies() method will result in a constant number of queries no matter how prolific the Person has been.

Creating a PersonDetail view and template

Now we can write a very thin view in django/core/views.py:

class PersonDetail(DetailView):
    queryset = Person.objects.all_with_prefetch_movies()

This DetailView is different because we're not providing it with a model attribute. Instead, we're giving it a QuerySet object from our PersonManager class. When DetailView uses the filter() of QuerySet and get() methods to retrieve the model instance, DetailView will derive the name of the template from the model instance's class name just as if we had provided model class as an attribute on the view.

Now, let's create our template in django/core/templates/core/person_detail.html:

{% extends 'base.html' %}

{% block title %}
  {{ object.first_name }}
  {{ object.last_name }}
{% endblock %}

{% block main %}

  <h1>{{ object }}</h1>
  <h2>Actor</h2>
  <ul >
    {% for role in object.role_set.all %}
      <li >
        <a href="{% url 'core:MovieDetail' role.movie.id %}" >
          {{ role.movie }}
        </a >:
        {{ role.name }}
      </li >
    {% endfor %}
  </ul >
  <h2>Writer</h2>
  <ul >
    {% for movie in object.writing_credits.all %}
      <li >
        <a href="{% url 'core:MovieDetail' movie.id %}" >
          {{ movie }}
        </a >
      </li >
    {% endfor %}
  </ul >
  <h2>Director</h2>
  <ul >
    {% for movie in object.directed.all %}
      <li >
        <a href="{% url 'core:MovieDetail' movie.id %}" >
          {{ movie }}
        </a >
      </li >
    {% endfor %}
  </ul >

{% endblock %}

Our template doesn't have to do anything special to make use of our prefetching.

Next, we should give the MovieDetail view the same benefit that our PersonDetail view received.

Creating MovieManager

Let's start with a MovieManager in django/core/models.py:

class MovieManager(models.Manager):

    def all_with_related_persons(self):
        qs = self.get_queryset()
        qs = qs.select_related(
            'director')
        qs = qs.prefetch_related(
            'writers', 'actors')
        return qs


class Movie(models.Model):
    # constants and fields omitted for brevity
    objects = MovieManager()

    class Meta:
        ordering = ('-year', 'title')

    def __str__(self):
         # method body omitted for brevity

The MovieManager introduces another new method, called select_related(). The select_related() method is much like the prefetch_related() method but it is used when the relation leads to only one related model (for example, with a ForeignKey field). The select_related() method works by using a JOIN SQL query to retrieve the two models in one query. Use prefetch_related() when the relation may lead to more than one model (for example, either side of a ManyToManyField or a RelatedManager attribute).

Now, we can update our MovieDetail view to use the query set instead of the model directly:

class MovieDetail(DetailView):
    queryset = (
        Movie.objects
            .all_with_related_persons())

The view renders exactly the same, but it won't have to query the database each time a related  Person model instance is required, as they were all prefetched.

A quick review of the section

In this section, we created the Person model and established a variety of relationships between the Movie and Person models. We created a one-to-many relationship with a ForeignKey field class, a many-to-many relationship using the ManyToManyField class,  and used an intermediary (or association) class to add extra information for a many-to-many relationship by providing a through model to a ManyToManyField. We also created a PersonDetail view to show a Person model instance and used a custom model manager to control the number of queries Django sends to the database.

 

Summary


In this chapter, we created our Django project and started our core Django app. We saw how to use Django's Model-View-Template approach to create easy-to-understand code. We created concentrated database logic near the model, pagination in views, and HTML in templates following the Django best practice of fat models, thin views, and dumb templates.

Now we're ready to add users who can register and vote on their favorite movies.

 

 

About the Author

  • Tom Aratyn

    Tom Aratyn is a software developer and the founder of the Boulevard Platform. He has a decade of experience developing web apps for companies of all sizes (from boutiques to large start-ups, such as Snapchat). He loves solving problems using his server-side and client-side development skills and helping other developers grow.

    Browse publications by this author

Latest Reviews

(5 reviews total)
The book is easy to read and full of useful information.
Top. Easy to read. very helpful. Thanks.
Das EBook ist sehr informativ und gut geschrieben.

Recommended For You

Django 2 Web Development Cookbook - Third Edition

Create unbelievably fast, robust and secure web apps with Django Web Framework and Python 3.6

By Jake Kronika and 1 more
Django 2 by Example

Learn Django 2.0 with four end-to-end projects

By Antonio Melé
Django RESTful Web Services

Design, build and test RESTful web services with the Django framework and Python

By Gaston C. Hillar
Django Design Patterns and Best Practices - Second Edition

Build maintainable websites with elegant Django design patterns and modern best practices

By Arun Ravindran