Introduction
Django web applications interact with data through Python objects known as models. These models dictate the organization of stored data, specifying field types, maximum sizes, default values, selection options, documentation help text, form label text, and more. Importantly, the definition of a model remains independent of the underlying database, offering flexibility to choose from various options within project settings. Once a database is selected, direct communication with it becomes unnecessary. Instead, developers focus on defining the model structure and additional code, leaving Django to handle the intricate database interactions.
Designing the LocalLibrary models
Before delving into coding the LocalLibrary models, it's crucial to consider the data needed and the relationships between entities. We must store book details like title, summary, author, language, category, and ISBN, along with managing multiple available copies and potentially complex author information. Using separate models for distinct "objects" such as books, book instances, and authors is advisable. Employing models for selection-list options, like genres and languages, offers flexibility as options may evolve. Django's relationship options allow for defining connections like one-to-one, one-to-many, and many-to-many. The UML diagram provided outlines the models to be defined, along with their relationships and multiplicities. Models for books, book instances, authors, and genres are established, with certain values like book instance status being hardcoded for simplicity. This UML diagram serves as a guide for designing the LocalLibrary models effectively.
Defining the LocalLibrary Models
In this section, we will start defining the models for the library. Open models.py (in /django-locallibrary-tutorial/catalog/). The boilerplate at the top of the page imports the model module, which contains the model base class models. The model that our models will inherit from.
from django.db import models
# Create your models here.
Model for Book Genres
Add the following code snippet to the end of your models.py file to implement the Genre model. This model serves the purpose of storing information about various book categories, such as fiction, non-fiction, romance, military history, and more. Unlike using free text or a selection list, we've designed this model to ensure that the possible genre values can be effectively managed through the database rather than being hardcoded.
from django.urls import reverse # Utilized in get_absolute_url() to retrieve URL for a specified ID
from django.db import models
from django.db.models import UniqueConstraint # Enforces fields to contain unique values
from django.db.models.functions import Lower # Produces a lower-cased value of a field
class Genre(models.Model):
"""Model representing different book genres."""
name = models.CharField(
max_length=200,
unique=True,
help_text="Enter a book genre (e.g. Science Fiction, French Poetry, etc.)"
)
def __str__(self):
"""String representation of the Model object."""
return self.name
def get_absolute_url(self):
"""Returns the URL to access a particular genre instance."""
return reverse('genre-detail', args=[str(self.id)])
class Meta:
constraints = [
UniqueConstraint(
Lower('name'),
name='genre_name_case_insensitive_unique',
violation_error_message="Genre already exists (case insensitive match)"
),
]
Explanation
The model consists of a single CharField named 'name', which describes the genre. This field is limited to 200 characters and includes helpful text for guidance.
The 'name' field is set to unique (unique=True) to ensure there's only one record for each genre.
Following the field declaration, there's a str() method defined, which returns the name of the genre for a particular record.
Additionally, a get_absolute_url() method is defined to generate a URL that provides access to a detailed record of this model. For this method to work correctly, a URL mapping with the name 'genre-detail' needs to be defined, along with an associated view and template.
By setting unique=True on the 'name' field, we prevent the creation of genres with identical names. However, variations such as "fantasy", "Fantasy", or "FaNtAsY" are not prevented. To address this, the last part of the model definition utilizes constraints within the model's metadata. It specifies that the lowercase of the value in the 'name' field must be unique in the database. If a duplicate is encountered, the specified violation_error_message is displayed. Multiple constraints can be defined for a field or fields, offering flexibility in data validation and management. Refer to the Constraints reference, including UniqueConstraint() and Lower(), for further information.
Book Model
class Book(models.Model):
"""Model representing a book (but not a specific copy of a book)."""
title = models.CharField(max_length=200)
author = models.ForeignKey('Author', on_delete=models.RESTRICT, null=True)
# Foreign Key used because book can only have one author, but authors can have multiple books.
# Author as a string rather than object because it hasn't been declared yet in file.
summary = models.TextField(
max_length=1000, help_text="Enter a brief description of the book")
isbn = models.CharField('ISBN', max_length=13,
unique=True,
help_text='13 Character <a href="https://www.isbn-international.org/content/what-isbn'
'">ISBN number</a>')
# ManyToManyField used because genre can contain many books. Books can cover many genres.
# Genre class has already been defined so we can specify the object above.
genre = models.ManyToManyField(
Genre, help_text="Select a genre for this book")
def __str__(self):
"""String for representing the Model object."""
return self.title
def get_absolute_url(self):
"""Returns the URL to access a detail record for this book."""
return reverse('book-detail', args=[str(self.id)])
Explanation
- The Book model comprises fields for the title, author, summary, ISBN, and genre.
- 'title' stores the title of the book.
- 'author' is a ForeignKey, linking each book to a specific author. This allows authors to have multiple books.
- 'summary' is a TextField, accommodating longer descriptions of the book.
- 'isbn' represents the ISBN number of the book. It's set to unique to ensure each book has a globally unique ISBN.
- 'genre' is a ManyToManyField, allowing a book to belong to multiple genres and vice versa.
- In the 'author' field, 'Author' is referred to as a string due to its declaration occurring later in the file.
- The 'str()' method returns the title of the book for better readability.
- The 'get_absolute_url()' method generates a URL to access detailed information about a specific book instance.
- It's noted that 'on_delete=models.RESTRICT' is used for the 'author' field to prevent the deletion of associated authors if referenced by any book. This differs from the default behavior 'on_delete=models.CASCADE', which would delete the book if its author is deleted. Other options such as 'PROTECT' and 'SET_NULL' are mentioned for handling author deletions.
BookInstance Model
Now, let's introduce the BookInstance model, which should be placed among your other models. This model signifies a particular copy of a book that patrons might borrow from the library. It encompasses details such as the availability status, expected return date, imprint (specific version) details, and a unique identifier for the book within the library.
Some elements of this model will seem familiar, as it utilizes:
- ForeignKey to establish the association with the corresponding Book (each book may have multiple copies, but a copy is tied to only one Book). The 'on_delete=models.RESTRICT' setting ensures that a Book cannot be deleted while any BookInstance references it.
- CharField to represent the imprint or specific version of the book.
import uuid # Required for unique book instances
class BookInstance(models.Model):
"""Model representing a specific copy of a book (i.e. that can be borrowed from the library)."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4,
help_text="Unique ID for this particular book across whole library")
book = models.ForeignKey('Book', on_delete=models.RESTRICT, null=True)
imprint = models.CharField(max_length=200)
due_back = models.DateField(null=True, blank=True)
LOAN_STATUS = (
('m', 'Maintenance'),
('o', 'On loan'),
('a', 'Available'),
('r', 'Reserved'),
)
status = models.CharField(
max_length=1,
choices=LOAN_STATUS,
blank=True,
default='m',
help_text='Book availability',
)
class Meta:
ordering = ['due_back']
def __str__(self):
"""String for representing the Model object."""
return f'{self.id} ({self.book.title})'
Additionally, we introduce several new field types
- UUIDField is employed for the 'id' field to designate it as the primary key for this model. This field type assigns a globally unique value to each instance, ensuring uniqueness across all books in the library.
- DateField is used for the 'due_back' date, indicating when the book is expected to be available after being borrowed or undergoing maintenance. This field can be left blank or null, as required when the book is available. Within the model metadata (Class Meta), this field is utilized to order records when queried.
- 'status' is a CharField defining a choice list. As seen, we specify a tuple containing tuples of key-value pairs and pass it to the 'choices' argument. The display value in each pair is what the user can select, while the keys represent the values actually saved when an option is chosen. We've set the default value as 'm' (maintenance) since books are initially created as unavailable before being stocked on shelves.
- The 'str()' method portrays the BookInstance object using a combination of its unique id and the associated Book's title.
Author Model
To expand your models.py file, we'll now introduce the Author model, provided below. This model encapsulates the details of an author and should be placed beneath your existing code.
class Author(models.Model):
"""Model representing an author."""
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
date_of_birth = models.DateField(null=True, blank=True)
date_of_death = models.DateField('Died', null=True, blank=True)
class Meta:
ordering = ['last_name', 'first_name']
def get_absolute_url(self):
"""Returns the URL to access a particular author instance."""
return reverse('author-detail', args=[str(self.id)])
def __str__(self):
"""String for representing the Model object."""
return f'{self.last_name}, {self.first_name}'
You may find these fields and methods familiar. The model defines an author with attributes such as first name, last name, and optional dates of birth and death. By default, the 'str()' method returns the author's name in the format 'last name, first name'. Additionally, the 'get_absolute_url()' method is implemented to generate the URL for accessing individual author details by reversing the 'author-detail' URL mapping.
Language Model
class Language(models.Model):
"""Model representing a Language (e.g. English, French, Japanese, etc.)"""
name = models.CharField(max_length=200,
unique=True,
help_text="Enter the book's natural language (e.g. English, French, Japanese etc.)")
def get_absolute_url(self):
"""Returns the url to access a particular language instance."""
return reverse('language-detail', args=[str(self.id)])
def __str__(self):
"""String for representing the Model object (in Admin site etc.)"""
return self.name
This code snippet defines a Django model named Language
. It includes a name
field to represent the name of the language, along with methods for generating absolute URLs (get_absolute_url
) and string representations of the model objects (__str__
). The get_absolute_url
method is used to retrieve the URL to access a particular language instance, while the __str__
method returns the name of the language as its string representation.
Run the database migrations
All your models have now been created. Now re-run your database migrations to add them to your database.
python3 manage.py makemigrations
python3 manage.py migrate
The command python3 manage.py makemigrations in Django analyzes model changes and generates migration files, while python3 manage.py migrate applies these migrations to update the database schema. With makemigrations, Django detects any modifications to models and creates corresponding migration files in the app's migrations directory. These files represent the database schema changes in a database-agnostic format. On the other hand, migrate executes these migration files, altering the database schema accordingly. This synchronization ensures that the database structure aligns with the current state of the models. Together, these commands facilitate the efficient management of database evolution within Django projects, enabling developers to make model adjustments seamlessly while maintaining data integrity and consistency.
Django admin site
The Django admin application can use your models to automatically build a site area that you can use to create, view, update, and delete records. This can save you a lot of time during development, making it very easy to test your models and get a feel for whether you have the right data.
Registering Model
To begin, navigate to the admin.py file located within the catalog application directory (/django-locallibrary-tutorial/catalog/admin.py). This file already includes the necessary import statement for django.contrib.admin. Next, register the models by adding the provided code snippet to the bottom of the file. This code imports the models and utilizes admin.site.register to register each of them. It's a straightforward method of integrating models with the admin site. Keep in mind that Django's admin site offers extensive customization options beyond this basic registration process, which we'll explore later in the article.
from django.contrib import admin
from .models import Author, Genre, Book, BookInstance, Language
# Register your models here.
admin.site.register(Book)
admin.site.register(Author)
admin.site.register(Genre)
admin.site.register(BookInstance)
admin.site.register(Language)
Creating a superuser
To access the admin site and manage records, a user account with Staff status and appropriate permissions is required. To create such an account, referred to as a "superuser," use the createsuperuser command in the terminal, located in the same directory as manage.py. Upon execution, the command prompts for a username, email address, and a strong password.
python3 manage.py createsuperuser
Once completed, the new superuser is added to the database. Restart the development server with runserver to test the login functionality. This ensures that the superuser can access and utilize the admin site effectively.
python3 manage.py runserver
Logging in and using the site
To access the site's administrative features, navigate to the /admin URL (e.g., http://127.0.0.1:8000/admin) and log in using the credentials of the newly created superuser. After logging in, you'll be directed to a page displaying all models grouped by installed application. Clicking on a model name leads to a screen listing its associated records, which can be further edited by clicking on them. Additionally, you can initiate the creation of a new record by clicking the "Add" link next to each model. This streamlined interface facilitates efficient management of data, allowing administrators to navigate, view, and modify records effortlessly.
To create a new book, click on the "Add" link located to the right of the "Books" section. This action will prompt a dialog box similar to the one shown below. Take note of how the field titles, widget types, and any accompanying help text align with the specifications outlined in the model. Fill in the fields with the desired values. If necessary, you can create new authors or genres by selecting the "+" button next to the respective fields. Alternatively, you can choose existing values from the provided lists. After completing the form, you have the option to save the record by clicking "SAVE," "Save and add another," or "Save and continue editing." This intuitive process allows for seamless creation and management of book records within the administrative interface.
Once you've completed adding books, navigate back to the main admin page by clicking on the "Home" link located in the top bookmark. From there, click on the "Books" link to view the current list of books, or explore other model lists by clicking on their respective links. Upon adding several books, the list may resemble the screenshot provided below. Each book's title is prominently displayed, reflecting the value returned by the str() method defined in the Book model as discussed in the previous article. This intuitive navigation within the admin interface facilitates easy access to and management of book records, enhancing the administrative experience for users.
Within this list, books can be managed efficiently. To delete a book, simply mark the checkbox next to the respective book, choose "delete" from the Action drop-down list, and confirm by clicking the "Go" button. Conversely, new books can be added by selecting the "ADD BOOK" button. Editing a book is straightforward: click on its name to access the edit page, which closely resembles the "Add" page. The main distinctions include the page title ("Change book") and the addition of buttons for Delete, HISTORY, and VIEW ON SITE. The presence of the "VIEW ON SITE" button is attributed to the implementation of the get_absolute_url() method in our model, providing convenient navigation options for administrators.
Conclusion
This article has provided a comprehensive guide to designing, defining, and accessing Django models within the context of the LocalLibrary admin site example. By emphasizing the importance of thoughtful model design and demonstrating practical implementation steps, developers are equipped with the essential knowledge and skills to create a structured and scalable admin site. Through the creation of models for books, book instances, authors, genres, and languages, this article has illustrated the versatility of Django's model system and its seamless integration with the admin interface. By following the outlined procedures, developers can efficiently manage data and streamline administrative tasks, resulting in robust and user-friendly web solutions.