Book Image

Django 4 By Example - Fourth Edition

By : Antonio Melé
4.8 (4)
Book Image

Django 4 By Example - Fourth Edition

4.8 (4)
By: Antonio Melé

Overview of this book

Django 4 By Example is the 4th edition of the best-selling franchise that helps you build web apps. This book will walk you through the creation of real-world applications, solving common problems, and implementing best practices using a step-by-step approach. You'll cover a wide range of web app development topics as you build four different apps: A blog application: Create data models, views, and URLs and implement an admin site for your blog. Create sitemaps and RSS feeds and implement a full-text search engine with PostgreSQL. A social website: Implement authentication with Facebook, Twitter, and Google. Create user profiles, image thumbnails, a bookmarklet, and an activity stream. Implement a user follower system and add infinite scroll pagination to your website. An e-commerce application: Build a product catalog, a shopping cart, and asynchronous tasks with Celery and RabbitMQ. Process payments with Stripe and manage payment notifications via webhooks. Build a product recommendation engine with Redis. Create PDF invoices and export orders to CSV. An e-learning platform: Create a content management system to manage polymorphic content. Cache content with Memcached and Redis. Build and consume a RESTful API. Implement a real-time chat using WebSockets with ASGI. Create a production environment using NGINX, uWSGI and Daphne with Docker Compose. This is a practical book that will have you creating web apps quickly.
Table of Contents (20 chapters)
18
Other Books You May Enjoy
19
Index

Creating the blog data models

Remember that a Python object is a collection of data and methods. Classes are the blueprint for bundling data and functionality together. Creating a new class creates a new type of object, allowing you to create instances of that type.

A Django model is a source of information and behaviors of your data. It consists of a Python class that subclasses django.db.models.Model. Each model maps to a single database table, where each attribute of the class represents a database field. When you create a model, Django will provide you with a practical API to query objects in the database easily.

We will define the database models for our blog application. Then, we will generate the database migrations for the models to create the corresponding database tables. When applying the migrations, Django will create a table for each model defined in the models.py file of the application.

Creating the Post model

First, we will define a Post model that will allow us to store blog posts in the database.

Add the following lines to the models.py file of the blog application. The new lines are highlighted in bold:

from django.db import models
class Post(models.Model):
    title = models.CharField(max_length=250)
    slug = models.SlugField(max_length=250)
    body = models.TextField()
    def __str__(self):
        return self.title

This is the data model for blog posts. Posts will have a title, a short label called slug, and a body. Let’s take a look at the fields of this model:

  • title: This is the field for the post title. This is a CharField field that translates into a VARCHAR column in the SQL database.
  • slug: This is a SlugField field that translates into a VARCHAR column in the SQL database. A slug is a short label that contains only letters, numbers, underscores, or hyphens. A post with the title Django Reinhardt: A legend of Jazz could have a slug like django-reinhardt-legend-jazz. We will use the slug field to build beautiful, SEO-friendly URLs for blog posts in Chapter 2, Enhancing Your Blog with Advanced Features.
  • body: This is the field for storing the body of the post. This is a TextField field that translates into a TEXT column in the SQL database.

We have also added a __str__() method to the model class. This is the default Python method to return a string with the human-readable representation of the object. Django will use this method to display the name of the object in many places, such as the Django administration site.

If you have been using Python 2.x, note that in Python 3, all strings are natively considered Unicode; therefore, we only use the __str__() method. The __unicode__() method from Python 2.x is obsolete.

Let’s take a look at how the model and its fields will be translated into a database table and columns. The following diagram shows the Post model and the corresponding database table that Django will create when we synchronize the model to the database:

Figure 1.4: Initial Post model and database table correspondence

Django will create a database column for each of the model fields: title, slug, and body. You can see how each field type corresponds to a database data type.

By default, Django adds an auto-incrementing primary key field to each model. The field type for this field is specified in each application configuration or globally in the DEFAULT_AUTO_FIELD setting. When creating an application with the startapp command, the default value for DEFAULT_AUTO_FIELD is BigAutoField. This is a 64-bit integer that automatically increments according to available IDs. If you don’t specify a primary key for your model, Django adds this field automatically. You can also define one of the model fields to be the primary key by setting primary_key=True on it.

We will expand the Post model with additional fields and behaviors. Once complete, we will synchronize it to the database by creating a database migration and applying it.

Adding datetime fields

We will continue by adding different datetime fields to the Post model. Each post will be published at a specific date and time. Therefore, we need a field to store the publication date and time. We also want to store the date and time when the Post object was created, and when it was last modified.

Edit the models.py file of the blog application to make it look like this. The new lines are highlighted in bold:

from django.db import models
from django.utils import timezone
class Post(models.Model):
    title = models.CharField(max_length=250)
    slug = models.SlugField(max_length=250)
    body = models.TextField()
    publish = models.DateTimeField(default=timezone.now)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    def __str__(self):
        return self.title

We have added the following fields to the Post model:

  • publish: This is a DateTimeField field that translates into a DATETIME column in the SQL database. We will use it to store the date and time when the post was published. We use Django’s timezone.now method as the default value for the field. Note that we imported the timezone module to use this method. timezone.now returns the current datetime in a timezone-aware format. You can think of it as a timezone-aware version of the standard Python datetime.now method.
  • created: This is a DateTimeField field. We will use it to store the date and time when the post was created. By using auto_now_add, the date will be saved automatically when creating an object.
  • updated: This is a DateTimeField field. We will use it to store the last date and time when the post was updated. By using auto_now, the date will be updated automatically when saving an object.

Defining a default sort order

Blog posts are usually displayed in reverse chronological order (from newest to oldest). We will define a default ordering for our model. The default order will apply when obtaining objects from the database when no order is specified in the query.

Edit the models.py file of the blog application to make it look like this. The new lines are highlighted in bold:

from django.db import models
from django.utils import timezone
class Post(models.Model):
    title = models.CharField(max_length=250)
    slug = models.SlugField(max_length=250)
    body = models.TextField()
    publish = models.DateTimeField(default=timezone.now)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    class Meta:
        ordering = ['-publish']
    def __str__(self):
        return self.title

We have added a Meta class inside the model. This class defines metadata for the model. We use the ordering attribute to tell Django that it should sort results by the publish field. This ordering will apply by default for database queries when no specific order is provided in the query. We indicate descending order by using a hyphen before the field name, -publish. Posts will be returned in reverse chronological order by default.

Adding a database index

Let’s define a database index for the publish field. This will improve performance for queries filtering or ordering results by this field. We expect many queries to take advantage of this index since we are using the publish field to order results by default.

Edit the models.py file of the blog application and make it look like this. The new lines are highlighted in bold:

from django.db import models
from django.utils import timezone
class Post(models.Model):
    title = models.CharField(max_length=250)
    slug = models.SlugField(max_length=250)
    body = models.TextField()
    publish = models.DateTimeField(default=timezone.now)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    class Meta:
        ordering = ['-publish']
        indexes = [
            models.Index(fields=['-publish']),
        ]
    def __str__(self):
        return self.title

We have added the indexes option to the model’s Meta class. This option allows you to define database indexes for your model, which could comprise one or multiple fields, in ascending or descending order, or functional expressions and database functions. We have added an index for the publish field. We use a hyphen before the field name to define the index in descending order. The creation of this index will be included in the database migrations that we will generate later for our blog models.

Index ordering is not supported on MySQL. If you use MySQL for the database, a descending index will be created as a normal index.

You can read more information about how to define indexes for models at https://docs.djangoproject.com/en/4.1/ref/models/indexes/.

Activating the application

We need to activate the blog application in the project, for Django to keep track of the application and be able to create database tables for its models.

Edit the settings.py file and add blog.apps.BlogConfig to the INSTALLED_APPS setting. It should look like this. The new lines are highlighted in bold:

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

The BlogConfig class is the application configuration. Now Django knows that the application is active for this project and will be able to load the application models.

Adding a status field

A common functionality for blogs is to save posts as a draft until ready for publication. We will add a status field to our model that will allow us to manage the status of blog posts. We will be using Draft and Published statuses for posts.

Edit the models.py file of the blog application to make it look as follows. The new lines are highlighted in bold:

from django.db import models
from django.utils import timezone
class Post(models.Model):
    class Status(models.TextChoices):
        DRAFT = 'DF', 'Draft'
        PUBLISHED = 'PB', 'Published'
    title = models.CharField(max_length=250)
    slug = models.SlugField(max_length=250)
    body = models.TextField()
    publish = models.DateTimeField(default=timezone.now)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=2,
                              choices=Status.choices,
                              default=Status.DRAFT)
    class Meta:
        ordering = ['-publish']
        indexes = [
            models.Index(fields=['-publish']),
        ]
    def __str__(self):
        return self.title

We have defined the enumeration class Status by subclassing models.TextChoices. The available choices for the post status are DRAFT and PUBLISHED. Their respective values are DF and PB, and their labels or readable names are Draft and Published.

Django provides enumeration types that you can subclass to define choices simply. These are based on the enum object of Python’s standard library. You can read more about enum at https://docs.python.org/3/library/enum.html.

Django enumeration types present some modifications over enum. You can learn about those differences at https://docs.djangoproject.com/en/4.1/ref/models/fields/#enumeration-types.

We can access Post.Status.choices to obtain the available choices, Post.Status.labels to obtain the human-readable names, and Post.Status.values to obtain the actual values of the choices.

We have also added a new status field to the model that is an instance of CharField. It includes a choices parameter to limit the value of the field to the choices in Status.choices. We have also set a default value for the field using the default parameter. We use DRAFT as the default choice for this field.

It’s a good practice to define choices inside the model class and use the enumeration types. This will allow you to easily reference choice labels, values, or names from anywhere in your code. You can import the Post model and use Post.Status.DRAFT as a reference for the Draft status anywhere in your code.

Let’s take a look at how to interact with the status choices.

Run the following command in the shell prompt to open the Python shell:

python manage.py shell

Then, type the following lines:

>>> from blog.models import Post
>>> Post.Status.choices

You will obtain the enum choices with value-label pairs like this:

[('DF', 'Draft'), ('PB', 'Published')]

Type the following line:

>>> Post.Status.labels

You will get the human-readable names of the enum members as follows:

['Draft', 'Published']

Type the following line:

>>> Post.Status.values

You will get the values of the enum members as follows. These are the values that can be stored in the database for the status field:

['DF', 'PB']

Type the following line:

>>> Post.Status.names

You will get the names of the choices like this:

['DRAFT', 'PUBLISHED']

You can access a specific lookup enumeration member with Post.Status.PUBLISHED and you can access its .name and .value properties as well.

Adding a many-to-one relationship

Posts are always written by an author. We will create a relationship between users and posts that will indicate which user wrote which posts. Django comes with an authentication framework that handles user accounts. The Django authentication framework comes in the django.contrib.auth package and contains a User model. We will use the User model from the Django authentication framework to create a relationship between users and posts.

Edit the models.py file of the blog application to make it look like this. The new lines are highlighted in bold:

from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
class Post(models.Model):
    class Status(models.TextChoices):
        DRAFT = 'DF', 'Draft'
        PUBLISHED = 'PB', 'Published'
    title = models.CharField(max_length=250)
    slug = models.SlugField(max_length=250)
    author = models.ForeignKey(User,
                               on_delete=models.CASCADE,
                               related_name='blog_posts')
    body = models.TextField()
    publish = models.DateTimeField(default=timezone.now)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=2,
                              choices=Status.choices,
                              default=Status.DRAFT)
    class Meta:
        ordering = ['-publish']
        indexes = [
            models.Index(fields=['-publish']),
        ]
    def __str__(self):
        return self.title

We have imported the User model from the django.contrib.auth.models module and we have added an author field to the Post model. This field defines a many-to-one relationship, meaning that each post is written by a user, and a user can write any number of posts. For this field, Django will create a foreign key in the database using the primary key of the related model.

The on_delete parameter specifies the behavior to adopt when the referenced object is deleted. This is not specific to Django; it is an SQL standard. Using CASCADE, you specify that when the referenced user is deleted, the database will also delete all related blog posts. You can take a look at all the possible options at https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.ForeignKey.on_delete.

We use related_name to specify the name of the reverse relationship, from User to Post. This will allow us to access related objects easily from a user object by using the user.blog_posts notation. We will learn more about this later.

Django comes with different types of fields that you can use to define your models. You can find all field types at https://docs.djangoproject.com/en/4.1/ref/models/fields/.

The Post model is now complete, and we can now synchronize it to the database. But before that, we need to activate the blog application in our Django project.

Creating and applying migrations

Now that we have a data model for blog posts, we need to create the corresponding database table. Django comes with a migration system that tracks the changes made to models and enables them to propagate into the database.

The migrate command applies migrations for all applications listed in INSTALLED_APPS. It synchronizes the database with the current models and existing migrations.

First, we will need to create an initial migration for our Post model.

Run the following command in the shell prompt from the root directory of your project:

python manage.py makemigrations blog

You should get an output similar to the following one:

Migrations for 'blog':
    blog/migrations/0001_initial.py
        - Create model Post
        - Create index blog_post_publish_bb7600_idx on field(s)
          -publish of model post

Django just created the 0001_initial.py file inside the migrations directory of the blog application. This migration contains the SQL statements to create the database table for the Post model and the definition of the database index for the publish field.

You can take a look at the file contents to see how the migration is defined. A migration specifies dependencies on other migrations and operations to perform in the database to synchronize it with model changes.

Let’s take a look at the SQL code that Django will execute in the database to create the table for your model. The sqlmigrate command takes the migration names and returns their SQL without executing it.

Run the following command from the shell prompt to inspect the SQL output of your first migration:

python manage.py sqlmigrate blog 0001

The output should look as follows:

BEGIN;
--
-- Create model Post
--
CREATE TABLE "blog_post" (
  "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
  "title" varchar(250) NOT NULL,
  "slug" varchar(250) NOT NULL,
  "body" text NOT NULL,
  "publish" datetime NOT NULL,
  "created" datetime NOT NULL,
  "updated" datetime NOT NULL,
  "status" varchar(10) NOT NULL,
  "author_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED);
--
-- Create blog_post_publish_bb7600_idx on field(s) -publish of model post
--
CREATE INDEX "blog_post_publish_bb7600_idx" ON "blog_post" ("publish" DESC);
CREATE INDEX "blog_post_slug_b95473f2" ON "blog_post" ("slug");
CREATE INDEX "blog_post_author_id_dd7a8485" ON "blog_post" ("author_id");
COMMIT;

The exact output depends on the database you are using. The preceding output is generated for SQLite. As you can see in the output, Django generates the table names by combining the application name and the lowercase name of the model (blog_post), but you can also specify a custom database name for your model in the Meta class of the model using the db_table attribute.

Django creates an auto-incremental id column used as the primary key for each model, but you can also override this by specifying primary_key=True on one of your model fields. The default id column consists of an integer that is incremented automatically. This column corresponds to the id field that is automatically added to your model.

The following three database indexes are created:

  • An index with descending order on the publish column. This is the index we explicitly defined with the indexes option of the model’s Meta class.
  • An index on the slug column because SlugField fields imply an index by default.
  • An index on the author_id column because ForeignKey fields imply an index by default.

Let’s compare the Post model with its corresponding database blog_post table:

Figure 1.5: Complete Post model and database table correspondence

Figure 1.5 shows how the model fields correspond to database table columns.

Let’s sync the database with the new model.

Execute the following command in the shell prompt to apply existing migrations:

python manage.py migrate

You will get an output that ends with the following line:

Applying blog.0001_initial... OK

We just applied migrations for the applications listed in INSTALLED_APPS, including the blog application. After applying the migrations, the database reflects the current status of the models.

If you edit the models.py file in order to add, remove, or change the fields of existing models, or if you add new models, you will have to create a new migration using the makemigrations command. Each migration allows Django to keep track of model changes. Then, you will have to apply the migration using the migrate command to keep the database in sync with your models.