Book Image

Web Development with Django - Second Edition

By : Ben Shaw, Saurabh Badhwar, Chris Guest, Bharath Chandra K S
4.7 (3)
Book Image

Web Development with Django - Second Edition

4.7 (3)
By: Ben Shaw, Saurabh Badhwar, Chris Guest, Bharath Chandra K S

Overview of this book

Do you want to develop reliable and secure applications that stand out from the crowd without spending hours on boilerplate code? You’ve made the right choice trusting the Django framework, and this book will tell you why. Often referred to as a “batteries included” web development framework, Django comes with all the core features needed to build a standalone application. Web Development with Django will take you through all the essential concepts and help you explore its power to build real-world applications using Python. Throughout the book, you’ll get the grips with the major features of Django by building a website called Bookr – a repository for book reviews. This end-to-end case study is split into a series of bitesize projects presented as exercises and activities, allowing you to challenge yourself in an enjoyable and attainable way. As you advance, you'll acquire various practical skills, including how to serve static files to add CSS, JavaScript, and images to your application, how to implement forms to accept user input, and how to manage sessions to ensure a reliable user experience. You’ll cover everyday tasks that are part of the development cycle of a real-world web application. By the end of this Django book, you'll have the skills and confidence to creatively develop and deploy your own projects.
Table of Contents (19 chapters)

Creating Django models and migrations

A Django model is essentially a Python class that holds the blueprint for creating a table in a database. The models.py file can have many such models, and each model is transformed into a database table. The attributes of the class form the fields and relationships of the database table as per the model definitions.

For our reviews application, we need to create the following models and their database tables consequently:

  • Book: This should store information about books
  • Contributor: This should store information about the person(s) who contributed to writing the book, such as the author, co-author, or editor
  • Publisher: As the name implies, this refers to the book publisher
  • Review: This should store all the book reviews written by the users of the application

Every book in our application will need to have a publisher, so let’s create Publisher as our first model. Enter the following code in reviews/models.py:

  from django.db import models
  class Publisher(models.Model):
      """A company that publishes books.""" 
      name = models.CharField(
          max_length=50, 
          help_text="The name of the Publisher.")
      website = models.URLField(
          help_text="The Publisher's website.") 
      email = models.EmailField(
          help_text="The Publisher's email address.")

Note

You can take a look at the complete models.py file for the bookr app by clicking the following link: https://github.com/PacktPublishing/Web-Development-with-Django-Second-Edition/blob/main/Chapter02/final/bookr/reviews/models.py.

The first line of code imports the Django models module. While this line will be autogenerated at the time of the creation of the Django app, do make sure you add it if it is not present. Following the import, the rest of the code defines a class named Publisher, a subclass of Django’s models.Model. Furthermore, this class will have attributes or fields such as name, website, and email. The following are the field types used while creating this model.

As we can see, each of these fields is defined to have the following types:

  • CharField: This field type stores shorter string fields, such as Packt Publishing. For very large strings, we use TextField.
  • EmailField: This is similar to CharField but validates whether the string represents a valid email address, for example, [email protected].
  • URLField: Again, this is similar to CharField, but validates whether the string represents a valid URL, for example, https://www.packtpub.com.

Next, we will look at the field options used when creating each of these fields.

Field options

Django provides a way to define field options for a model’s field. These field options are used to set a value or a constraint, and so on. For example, we can set a default value for a field using default=<value> to ensure that every time a record is created in the database for the field, it is set to a default value specified by us. The following are the two field options that we used when defining the Publisher model:

  • help_text: This field option helps us add descriptive text for a field that gets automatically included for Django forms
  • max_length: This option is provided to CharField where it defines the maximum length of the field in terms of the number of characters

Django has many more field types and field options that can be explored from the extensive official Django documentation. As we go about developing our sample book review application, we will learn about the types and fields used for the project. Now, let’s migrate the Django models into the database by following these steps:

  1. Execute the following command in the shell or terminal to do that (run it from the folder where your manage.py file is stored):
    python manage.py makemigrations reviews

The output of the command looks like this:

Migrations for 'reviews':
  reviews/migrations/0001_initial.py
    - Create model Publisher

The makemigrations <appname> command creates the migration scripts for the given app, in this case, for the reviews app. Notice that after running makemigrations, there is a new file created under the migrations folder:

Figure 2.12: New file under the migrations folder

Figure 2.12: New file under the migrations folder

This is the migration script created by Django. When we run makemigrations without the app name, the migration scripts will be created for all the apps in the project.

  1. Next, let’s list the project migration status. Remember that earlier, we applied migrations to Django’s installed apps, and now we have created a new app, reviews. Run the following command in the shell or terminal, and it will show the status of model migrations throughout the project (run it from the folder where your manage.py file is stored):
    python manage.py showmigrations

The output for the preceding command is as follows:

admin
 [X] 0001_initial
 [X] 0002_logentry_remove_auto_add
 [X] 0003_logentry_add_action_flag_choices
auth
 [X] 0001_initial
 [X] 0002_alter_permission_name_max_length
 [X] 0003_alter_user_email_max_length
 [X] 0004_alter_user_username_opts
 [X] 0005_alter_user_last_login_null
 [X] 0006_require_contenttypes_0002
 [X] 0007_alter_validators_add_error_messages
 [X] 0008_alter_user_username_max_length
 [X] 0009_alter_user_last_name_max_length
 [X] 0010_alter_group_name_max_length
 [X] 0011_update_proxy_permissions
contenttypes
 [X] 0001_initial
 [X] 0002_remove_content_type_name
reviews
 [ ] 0001_initial
sessions
 [X] 0001_initial

Here, the [X] mark indicates that the migrations have been applied. Notice the difference that all the other apps’ migrations have been applied except that of reviews. The showmigrations command can be executed to understand the migration status, but this is not a mandatory step while performing model migrations.

  1. Next, let’s understand how Django transforms a model into an actual database table. To do that, run the sqlmigrate command as follows:
    python manage.py sqlmigrate reviews 0001_initial

We should see the following output:

BEGIN;
--
-- Create model Publisher
--
CREATE TABLE "reviews_publisher" ("id" integer
    NOT NULL PRIMARY KEY AUTOINCREMENT, "name"
    varchar(50) NOT NULL, "website" varchar(200)
    NOT NULL, "email" varchar(254) NOT NULL);
COMMIT;

The preceding snippet shows the SQL command equivalent used when Django migrates the database. In this case, we create the reviews_publisher table with the name, website, and email fields with defined field types. Furthermore, all these fields are defined to be NOT NULL, implying that the entries for these fields cannot be null and should have a value. The sqlmigrate command is not a mandatory step while doing the model migrations.

In the next section, we will learn about primary keys and their importance when storing data in a database.

Primary keys

Let’s assume that a database table called users, as its name suggests, stores information about users. Let’s say it has more than 1,000 records and there are at least 3 users with the same name, Joe Burns. How do we uniquely identify these users from the application? The solution is to have a way to uniquely identify each record in the database. This is done using primary keys. A primary key is unique for a database table, and as a rule, a table cannot have two rows with the same primary key. In Django, when the primary key is not explicitly mentioned in the database models, Django automatically creates id as the primary key (an integer type), which auto-increments as new records are created.

In the previous section, notice the output of the python manage.py sqlmigrate command. When creating the Publisher table, the SQL CREATE TABLE command added one more field called id to the table. An id value is defined as PRIMARY KEY AUTOINCREMENT. In relational databases, a primary key is used to uniquely identify an entry in the database. For example, the book table has id as the primary key, which has numbers starting from 1. This value increments by one as new records are created. The integer value of id is always unique across the book table. Since the migration script has already been created by executing makemigrations, let’s now migrate the newly created model in the reviews app by executing the following command:

python manage.py migrate reviews

You should get the following output:

Operations to perform:
    Apply all migrations: reviews
Running migrations:
    Applying reviews.0001_initial... OK

This operation creates the database table for the reviews app. The following is a snippet from DB Browser indicating the new reviews_publisher table has been created in the database:

Figure 2.13: The reviews_publisher table created after executing the migration command

Figure 2.13: The reviews_publisher table created after executing the migration command

So far, we have explored how to create a model and migrate it into the database. Let’s now work on creating the rest of the models for our book review application. As we’ve already seen, the application will have the following database tables:

  • Book: This is the database table that holds the information about the book itself. We have already created a Book model and have migrated this to the database.
  • Publisher: This table holds information about the book publisher.
  • Contributor: This table holds information about the contributor, that is, the author, co-author, or editor.
  • Review: This table holds information about the review comments posted by the reviewers.

Let’s add the Book and Contributor models, as shown in the following code snippet, into reviews/models.py:

class Book(models.Model):
    """A published book."""
    title = models.CharField(
        max_length=70, help_text="The title of the book.")
    publication_date = models.DateField(
        verbose_name="Date the book was published.")
    isbn = models.CharField(
        max_length=20, verbose_name="ISBN number of the
        book.")
class Contributor(models.Model):
    """A contributor to a Book, e.g. author, editor,
        co-author."""
    first_names = models.CharField(
        max_length=50, help_text="The contributor's first
        name or names.")
    last_names = models.CharField(
        max_length=50, help_text="The contributor's last
        name or names.")
    email = models.EmailField(
        help_text="The contact email for the contributor.")

The code is self-explanatory. The Book model has the title, publication_date, and isbn fields. The Contributor model has the first_names and last_names fields and the email ID of the contributor. There are also some newly added models, apart from the ones we have seen in the Publisher model. They have DateField as a new field type, which, as the name suggests, is used to store a date. A new field option called verbose_name is also used. It provides a descriptive name for the field. Next, we will see how relationships work in a relational database.

Relationships

One of the powers of relational databases is the ability to establish relationships between data stored across database tables. Relationships help maintain data integrity by establishing the correct references across tables, which in turn helps maintain the database. Relationship rules, on the other hand, ensure data consistency and prevent duplicates.

In a relational database, there can be the following types of relations:

  • Many-to-one
  • Many-to-many
  • One-to-one

Let’s explore each relationship in detail in the following sections.

Many-to-one

In this relationship, many records (rows/entries) from one table can refer to one record (row/entry) in another table. For example, there can be many books produced by one publisher. This is an example of a many-to-one relationship. To establish this relationship, we need to use the database’s foreign keys. A foreign key in a relational database establishes the relationship between a field from one table and a primary key from a different table.

For example, say you have data about employees belonging to different departments stored in a table called employee_info with their employee ID as the primary key alongside a column that stores their department name; this table also contains a column that stores that department’s department ID. Now, there’s another table called departments_info, which has the department ID as the primary key. In this case, then, the department ID is a foreign key in the employee_info table.

In our bookr app, the Book model can have a foreign key referring to the primary key of the Publisher table. Since we have already created the models for Book, Contributor, and Publisher, let’s now establish a many-to-one relationship across the Book and Publisher models. For the Book model, add the last line:

class Book(models.Model):
    """A published book."""
    title = models.CharField(
        max_length=70, help_text="The title of the book.")
    publication_date = models.DateField(
        verbose_name="Date the book was published.")
    isbn = models.CharField(
        max_length=20, verbose_name="ISBN number of the
        book.")
    publisher = models.ForeignKey(
        Publisher, on_delete=models.CASCADE)

Now the newly added publisher field establishes a many-to-one relationship between Book and Publisher using a foreign key. This relationship ensures the nature of a many-to-one relationship, which is that many books can have one publisher:

  • models.ForeignKey: This is the field option to establish a many-to-one relationship.
  • Publisher: When we establish relationships with different tables in Django, we refer to the model that creates the table; in this case, the Publisher table is created by the Publisher model (or the Publisher Python class).
  • on_delete: This is a field option that determines the action to be taken upon the deletion of the referenced object. In this case, the on_delete option is set to CASCADE(models.CASCADE), which deletes the referenced objects.

For example, assume a publisher has published a set of books. For some reason, if the publisher has to be deleted from the application, the next action is CASCADE, which means deleting all the referenced books from the application. There are many more on_delete actions, such as the following:

  • PROTECT: This prevents the record deletion unless all the referenced objects are deleted
  • SET_NULL: This sets a null value if the database field has been previously configured to store null values
  • SET_DEFAULT: Sets to a default value on the deletion of the referenced object

We will only use the CASCADE option for our book review application.

Many-to-many

In this relationship, multiple records in a table can have a relationship with multiple records in a different table. For example, a book can have multiple co-authors, and each author (contributor) may have written multiple books. So, this forms a many-to-many relationship between the Book and Contributor tables:

Figure 2.14: Many-to-many relationship between books and co-authors

Figure 2.14: Many-to-many relationship between books and co-authors

In models.py, for the Book model, add the last line as shown here:

class Book(models.Model):
    """A published book."""
    title = models.CharField(
        max_length=70, help_text="The title of the book.")
    publication_date = models.DateField(
        verbose_name="Date the book was published.")
    isbn = models.CharField(
        max_length=20, verbose_name="ISBN number of the
        book.")
    publisher = models.ForeignKey(
        Publisher, on_delete=models.CASCADE)
    contributors = models.ManyToManyField(
        'Contributor', through="BookContributor")

The newly added contributors field establishes a many-to-many relationship with Book and Contributor using the ManyToManyField field type:

  • models.ManyToManyField: This is the field type to establish a many-to-many relationship.
  • through: This is a special field option for many-to-many relationships. When we have a many-to-many relationship across two tables, if we want to store some extra information about the relationship, then we can use this to establish the relationship via an intermediary table.

For example, we have two tables, namely Book and Contributor, where we need to store the information on the type of contributor for the book, such as Author, Co-Author, or Editor. Then the type of contributor is stored in an intermediary table called BookContributor. Here is how the BookContributor table/model looks. Make sure you include this model in reviews/models.py:

  class BookContributor(models.Model):
      class ContributionRole(models.TextChoices): 
      AUTHOR = "AUTHOR", "Author"
          CO_AUTHOR = "CO_AUTHOR", "Co-Author" 
          EDITOR = "EDITOR", "Editor"
      book = models.ForeignKey(
          Book, on_delete=models.CASCADE) 
      contributor = models.ForeignKey(
          Contributor, on_delete=models.CASCADE) 
      role = models.CharField(
          verbose_name="The role this contributor had in the \  
          book.", choices=ContributionRole.choices, 
          max_length=20)

Note

The complete models.py file can be viewed here: https://github.com/PacktPublishing/Web-Development-with-Django-Second-Edition/blob/main/Chapter02/final/bookr/reviews/models.py.

An intermediary table such as BookContributor establishes relationships by using foreign keys to both the Book and Contributor tables. It can also have extra fields that can store information about the relationship the BookContributor model has with the following fields:

  • book: This is a foreign key to the Book model. As we saw previously, on_delete=models.CASCADE will delete an entry from the relationship table when the relevant book is deleted from the application.
  • Contributor: Again, this is a foreign key to the Contributor model/table. This is also defined as CASCADE upon deletion.
  • role: This is the field of the intermediary model, which stores the extra information about the relationship between Book and Contributor.
  • class ContributionRole(models.TextChoices): This can be used to define a set of choices by creating a subclass of models.TextChoices. For example, ContributionRole is a subclass created out of TextChoices, which is used by the roles field to define Author, Co-Author, and Editor as a set of choices.
  • choices: This refers to a set of choices defined in the models, and they are useful when creating Django forms using the models.

Note

When the through field option is not provided while establishing a many-to-many relationship, Django automatically creates an intermediary table to manage the relationship.

One-to-one relationships

In this relationship, one record in a table will have a reference to only one record in a different table. For example, a person can have only one driver’s license, so a person with a driver’s license could form a one-to-one relationship:

Figure 2.15: Example of a one-to-one relationship

Figure 2.15: Example of a one-to-one relationship

OneToOneField can be used to establish a one-to-one relationship, as shown here:

class DriverLicence(models.Model):
    person = models.OneToOneField(
        Person, on_delete=models.CASCADE)
    licence_number = models.CharField(max_length=50)

Now that we have explored database relationships, let’s come back to our bookr application and add one more model there.

Adding the Review model

We’ve already added the Book and Publisher models to the reviews/models.py file. The last model that we are going to add is the Review model. The following code snippet should help us do this:

  from django.contrib import auth 
  class Review(models.Model):
      content = models.TextField( 
          help_text="The Review text.")
      rating = models.IntegerField(
          help_text="The rating the reviewer has given.") 
      date_created = models.DateTimeField(
          auto_now_add=True, 
          help_text="The date and time the review was \ 
          created.")
      date_edited = models.DateTimeField(
         null=True, help_text="The date and time the review \ 
         was last edited.")
      creator = models.ForeignKey(
          auth.get_user_model(), on_delete=models.CASCADE) 
      book = models.ForeignKey(
          Book, on_delete=models.CASCADE, 
          help_text="The Book that this review is for.")

Note

The complete models.py file can be viewed here: https://github.com/PacktPublishing/Web-Development-with-Django-Second-Edition/blob/main/Chapter02/final/bookr/reviews/models.py.

The review model/table will be used to store user-provided review comments and ratings for books. It has the following fields:

  • content: This field stores the text for a book review; hence, the field type used is TextField, as this can store a large amount of text.
  • rating: This field stores the review rating of a book. Since the rating will be an integer, the field type used is IntegerField.
  • date_created: This field stores the time and date when the review was written; hence the field type is DateTimeField.
  • date_edited: This field stores the date and time whenever a review is edited. Again, the field type is DateTimeField.
  • Creator: This field specifies the review creator or the person who writes the book review. Notice that this is a foreign key to auth.get_user_model(), which refers to the User model from Django’s built-in authentication module. It has an on_delete=models.CASCADE field option. This explains that when a user is deleted from the database, all the reviews written by that user will be deleted.
  • Book: Reviews have a field called book, a foreign key to the Book model. This is because reviews have to be written for a book review application, and a book can have many reviews, so this is a many-to-one relationship. This is also defined with an on_delete=models.CASCADE field option because once the book is deleted, there is no point in retaining the reviews in the application. So, when a book is deleted, all the reviews referring to the book will also get deleted.

In the next section, we will learn about and implement model methods.

Model methods

In Django, we can write methods inside a model class. These are called model methods and they can be custom or special methods that override the default methods of Django models. One such method is __str__(). This method returns the string representation of the Model instances and can be especially useful while using the Django shell. In the following example, where the __str__() method is added to the Publisher model, the string representation of the Publisher object will be the publisher’s name; hence, self.name is returned, with self referring to the Publisher object:

  class Publisher(models.Model):
      """A company that publishes books.""" 
      name = models.CharField(
          max_length=50, 
          help_text="The name of the Publisher.")
      website = models.URLField(
          help_text="The Publisher's website.") 
      email = models.EmailField(
          help_text="The Publisher's email address.")
      def  str (self): 
          return self.name

Add the _str_() methods to Contributor and Book as well, as follows:

  class Book(models.Model): 
      """A published book."""
      title = models.CharField(
          max_length=70, help_text="The title of the book.") 
      publication_date = models.DateField(
          verbose_name="Date the book was published.") 
      isbn = models.CharField(
          max_length=20, 
          verbose_name="ISBN number of the book.")
      publisher = models.ForeignKey( 
          Publisher, 
              on_delete=models.CASCADE)
      contributors = models.ManyToManyField( 
          'Contributor', through="BookContributor")
      def  str (self): 
          return self.title
  class Contributor(models.Model):
      """A contributor to a Book, e.g. author, editor, 
       co-author."""
      first_names = models.CharField(
          max_length=50, 
          help_text="The contributor's first name or names.")
      last_names = models.CharField(
          max_length=50, 
          help_text="The contributor's last name or names.")
      email = models.EmailField(
          help_text="The contact email for the contributor.")
      def  str (self):
          return self.first_names

Similarly, the string representation of book is title, so the returned value is self.title, with self referring to the Book object. The string representation of Contributor is the first name of the contributor; hence self.first_names is returned. Here, self refers to the Contributor object. Next, we will look at migrating the reviews app.

Migrating the reviews app

Since we have the entire model file ready, let’s now migrate the models into the database, similar to what we did before with the installed apps. Since the reviews app has a set of models created by us, it is important to create the migration scripts before running the migration. Migration scripts help in identifying any changes to the models and will propagate these changes into the database while running the migration. Follow these steps to create migration scripts and then migrate the models into the database:

  1. Execute the following command to create the migration scripts:
    python manage.py makemigrations reviews

You should get an output similar to this:

  reviews/migrations/0002_auto_20191007_0112.py
    - Create model Book
    - Create model Contributor
    - Create model Review
    - Create model BookContributor
    - Add field contributors to book
    - Add field publisher to book

Migration scripts will be created in a folder named migrations in the application folder.

  1. Next, migrate all the models into the database using the migrate command:
    python manage.py migrate reviews

You should see the following output:

Operations to perform:
  Apply all migrations: reviews
Running migrations:
  Applying reviews.0002_auto_20191007_0112... OK

After executing this command, we successfully created the database tables defined in the reviews app. You may use DB Browser for SQLite to explore the tables you have just created after the migration.

  1. To do so, open DB Browser for SQLite, click on the Open Database button (Figure 2.16), and navigate to your project directory:

Figure 2.16: Click the Open Database button

Figure 2.16: Click the Open Database button

  1. Select the db.sqlite3 database file to open it (Figure 2.17).
Figure 2.17: Locating db.sqlite3 in the bookr directory

Figure 2.17: Locating db.sqlite3 in the bookr directory

You should now be able to browse the new sets of tables created. The following screenshot shows the database tables defined in the reviews app:

Figure 2.18: Database tables as defined in the reviews app

Figure 2.18: Database tables as defined in the reviews app

In this section, we learned more about Django models and migrations and how Python’s simple classes can transform themselves into database tables. We also learned about how various class attributes translate into appropriate database columns following the defined field types. Later, we learned about primary keys and different types of relationships that can exist in a database. We also created models for the book review application and migrated those models, translating them into database tables. In the next section, we will learn about how to perform database CRUD operations using Django’s ORM.