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.
First, let's make a directory for our project:
$ mkdir MyMDB
$ cd MyMDBAll our future commands and paths will be relative to this project directory.
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.
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.txtWith 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.pyThe 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 filesLet'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 aDJANGO_SETTINGSenvironment variable, this is where Django looks for settings by default.urls.py: This is the rootURLConffor 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.pyreference).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.
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 calleddefault.'default': {: This is the default database connection configuration. You should always have adefaultset 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 usespsycopg2, 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.
Django apps follow a Model View Template (MVT) pattern; in this pattern, we will note the following things:
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.
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 filesLet'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 usingmanage.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.
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',
]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 avarcharcolumn with a length of 140. Databases generally require a maximum size forvarchar columns, so Django does too.plot = models.TextField(): This will become atextcolumn 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Âintegercolumn, and Django will validate the value before saving it to ensure that it is0or 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 aninteger column. The optional argumentchoices(which is available for allFields, not justIntegerField) 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 calledget_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 inchoiceswill be aValidationErroron save. Thedefaultargument provides a default value if one is not provided when creating the model.runtime = models.PositiveIntegerField(): This is the same as theyear 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. AURLFieldis avarchar(200)field by default (this can be set by providing amax_lengthargument).URLFieldalso comes with validation, checking whether its value is a valid web (http/https/ftp/ftps) URL. Theblankargument is used by theadminapp 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.
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 MovieA 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.
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... OKNow, 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=> \qWe 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... OKThis creates tables to keep track of users, sessions, permissions, and the administrative backend.
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:Â
objectsis the model's default manager. Managers are an interface for querying the model's table. It also offers acreate()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.idis 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 theratingfield was given a tuple ofchoices. We didn't have to provideÂrating with a value in ourÂcreate()call because theratingfield has adefault value (0). Theget_rating_display()method looks up the value and returns the corresponding display value. Django will generate a method like this for eachField attribute with aÂchoicesargument.
Next, let's create a backend for managing movies using the Django Admin app.
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:
- Register our model
- Create a super user who can access the backend
- Run the development server
- 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.
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 = MovieListView 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.
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.
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).
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 runserverIn 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.
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:
- Create a
MovieDetail view - Create
movie_detail.html template - Reference to our
MovieDetail view in ourURLConf
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 = MovieA 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.
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/templatesDjango 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' %}": Theurltag will produce a URL path for the namedpath. URL names should be referenced as<app_namespace>:<name>; in our case,Âcoreis the namespace of the core app (perdjango/core/urls.py), andMovieListis the name of theMovieListview'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 anextendstag. Django will look for the base template (which canÂextendanother template) and execute it first, then replace the blocks. A template that extends another cannot have content outside ofblocks because it's ambiguous where to put that content.{{ object.title }} - {{ block.super }}: We referenceblock.superinside theÂtitletemplateblock.block.superreturns the contents of thetitletemplateblockin 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.
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:

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.
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:

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.
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_objis of theÂPagetype, which knows information about this page of results. We use it to check whether there is a next/previous page usinghas_next()/has_previous()(we don't need to put()in the Django template language, buthas_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,ÂPagethrows anÂEmptyPageexception.object_listcontinues to be available and hold the correct values. Even thoughpage_objencapsulates the results for this page inpage_obj.object_list,ÂListViewdoes the convenient work of ensuring that we can continue to useobject_listand our template doesn't break.
We now have the pagination working!
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:

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Â
pageGETparameters, consider the following things:- The
page_is_lastcontext variable should beFalse - The
page_is_firstcontext variable should beTrue - The first item in the pagination should be marked as active
- The
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):ÂTestCaseis 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'sTestCaseclass offers asetUp()hook that is run before each test. AtearDown() 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 withtest.movie_list_path = reverse('core:MovieList'):Âreverse()was mentioned before and is the Python equivalent of theurlDjango template tag. It will resolve the name into a path.request = RequestFactory().get(path=movie_list_path):ÂRequestFactoryis a convenient factory for creating fake HTTP requests. ARequestFactory has convenience methods for creatingGET,ÂPOST, andÂPUT requests by its convenience methods named after the verb (for example,Âget()forGETrequests). In our case, thepath 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'sstatus_codeto check success or failure (200being 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 toTrue.responseexposes 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(:ÂassertInHTMLis one of the many convenient methods that Django provides as part of its Batteries Included philosophy. Given a valid HTML stringneedleand valid HTML stringhaystack, it will assert thatneedleis inhaystack. 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.
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:
- Create a
Personmodel - Add a
ForeignKeyfield fromMovietoPersonto track the director - Add a
ManyToManyFieldfromMovietoPersonto track the writers - Add a
ManyToManyFieldwith athroughclass (Actor) to track who performed and in what role in a Movie - Create the migration
- Add the director, writer, and actors to the movie details template
- Add a
PersonDetailview to the list that indicates what movies a Person has directed, written, and performed in
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.
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.
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_NULLwill set thedirector field of all theMovie model instances directed by the deletedPersontoNULL. If we wanted to cascade deletes we would use themodels.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 aPersondirected). Ifrelated_namewere not provided, thenPersonwould get an attribute calledmovie_set(following theÂ<model with FK>_set pattern). In our case, we will have multiple different relationships betweenMovieandPerson(writer, director, and actors), somovie_setwould become ambiguous, and we must provide arelated_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.
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).
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.actorswill be a related manager toPersonperson.acting_creditswill be a related manager toMoviemovie.role_setwill be a related manager toRoleperson.role_setwill be a related manager toRole
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').
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... OKNext, let's make our movie pages link to the people in the movies.
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:
- Create a manager to limit the number of database queries
- Create our view
- Create our template
- Create a URL that references our view
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 brevityOur 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.
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.
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 brevityThe 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.
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.
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.
Â
Â
Download code from GitHub