In the previous part of the series we created the skeleton of the Portfolio website. Now it’s time to create the models.
Table of Contents
Models in Django
But what are models in the first place? In Django, models are just Python classes that represent the data. Each model defines its structure in such a way as to be understood by the underlying database. This means it defines the field types and constraints like maximum size, default values, etc., so that the database knows how to store the data. This said, a model is independent of any particular concrete database and can work with all.
Data and Relationships
When talking about models, we have to consider data and relationships. Here’s a UML diagram of the models we’re going to use in our app:
Each rectangle represents a model class and each line represents a relationship.
Let’s start with the models. Each rectangle is divided into three parts. We have the name of the class in the top part. In the middle part are the fields along with their types. In the bottom part are the methods with their return types. We won’t need any fancy methods for our models, so let’s limit ourselves to just two: __str__
and get_absolute_url
. Each model will redefine it. Besides, we don’t need the second of the aforementioned methods in the Link
model at all.
The main model class in our app is Project
. Each project will have a name
and description
. These two fields are defined as strings. We’ll also be able to add an image for each project, hence the image
field of type ImageField
.
In order to use the ImageField
, we have to install Pillow, so let’s do just that. Type the following command in the terminal:
python -m pip install Pillow
We’ll also specify the date when the project was added to the Portfolio. To this end, we’ll need a date_added
field of type DateField
. Each project will belong to exactly one category of type Category
. This type is defined here as well, with just a single field. Each project will use one or more technologies of type Technology
, which is also defined here.
Let’s have a look at the lines now to see the relationships between the models. The numbers above the lines represent their multiplicities.
So, there is a line between Project
and Category
, which means these two models are related. The number 1 near the Category
means a project must belong to exactly one category. The range 0..* next to the Project
means there may be 0 or more projects in a category.
Likewise, a Project
must use 1 or more technologies (1..*), and each technology may be used in 0 or more projects (0..*).
Finally, each project will have 0 or more (0..*) links (to Github, YouTube, Amazon, etc.) of type Link
. Each link must belong to exactly one project (1). You can see this type in the diagram as well. It has three string fields and a field of type Project
that will be used to associate the link with the book it belongs to.
The Project Model
Now that we know what our models will look like, let’s implement them. Models are usually implemented in the models.py file. Let’s start with the Project
model and see on this example how models are defined in general. We’re going to put the code in the models.py folder inside the catalog folder. So, here’s the code:
from django.db import models
from django.urls import reverse
# The class inherits from models.Model
class Project(models.Model):
"""Model representing a project"""
# The name is a string with max length of 200 characters.
name = models.CharField(max_length=200)
# description is a text field with max length of 5000 characters.
description = models.TextField(max_length=5000,
help_text='Enter a description of the project here.')
# image is an image file field - the images will be uploaded to a specified directory.
image = models.ImageField(upload_to='images/')
# date_added is an image file field - the images will be uploaded to a specified directory.
date_added = models.DateField(null=True,
blank=True,
verbose_name='date of creation')
# category is a ForeignKey field because a project can belong to only one category
# and a category may contain multiple projects.
category = models.ForeignKey('Category',
on_delete=models.RESTRICT,
null=True)
# technologies is a ManyToManyField because a project may use multiple technologies
# and a technology may be used in multiple projects.
technologies = models.ManyToManyField('Technology',
help_text='Select technologies used in this project.')
class Meta:
ordering = ['-date_added']
def __str__(self):
"""String for representing the Model object."""
return self.name
def get_absolute_url(self):
"""Returns the URL to access a detail record for this project."""
return reverse('project-detail', args=[str(self.id)])
And now, let’s have a closer look at what we have here. Generally, there are some fields, some methods and a nested class.
Field Types
As you can see, the fields are of specific types that will make it easier to store them in the database. Actually, each field corresponds to one column in the database table. We also specify the validation criteria for each field. Let’s have a look at the particular fields one by one.
So, the name
field is of type CharField
. This type is used to define strings of an up to moderate length. It has a max_length
attribute set to 200 characters. This should be enough for a name of a project:
name = models.CharField(max_length=200)
The description
field is of type TextField
. This type is for longer strings of arbitrary length. Here we’re limiting the length of the string to 5000 characters and setting a text label that will be visible as a hint in the admin site:
description = models.TextField(max_length=5000,
help_text='Enter a description of the project here.')
Next is the image
field of type ImageField
. It has an upload_to
attribute set to the directory where the uploaded images will be put:
image = models.ImageField(upload_to='images/')
The date_added
field is of type DateField
. This type is used to store dates. There’s also the DateTimeField
available in case we need to store both the date and the time, which isn’t the case in our app, though. The field has a couple interesting attributes.
The first one is null
. It’s set to True
, which means the database will store a Null value if no category is assigned.
The second attribute is blank
. It’s set to True
, which means blank values will be allowed in the forms.
The verbose_name
attribute is used to provide a name for the field labels. By default the names are inferred from the field name, so in our case the label for this field would read Date Added
. We use the attribute if we want it to be displayed differently:
date_added = models.DateField(null=True,
blank=True,
verbose_name='date of creation')
The category
field is of type ForeignKey
. This type is used to define a one-to-many relationship. In our case Category
is on the “one” side and Project
is on the “many” side, which means a category may have many projects, but a project may belong to just one category. At least this is how we implement this relationship in our app.
If you look at the category
field, it takes as its first argument the name of the model it’s related to. Here the model must be passed as a string because the Category
class hasn’t been defined yet:
category = models.ForeignKey('Category',
on_delete=models.RESTRICT,
null=True)
If it had, we could use the model class directly, like so:
category = models.ForeignKey(Category,
on_delete=models.RESTRICT,
null=True)
The next attribute in the category
field is on_delete=models.RESTRICT
. This will prevent the category associated with the project from being deleted if it’s associated with any project.
Finally, there’s the technologies
field of type ManyToManyField
. As the name suggests, this type is used to define a many-to-many relationship. In our case, a Project
may implement many technologies and a Technology
may be used in many projects:
technologies = models.ManyToManyField('Technology',
help_text='Select technologies used in this project.')
The Methods
We also define two methods in the model. The first method, __str__
, is used to return a human-readable representation of the object. In particular, it will display the name of the project.
The second method is get_absolute_url
. This method will return a URL to the detail view of this particular object and we’ll discuss it in more detail when we need it.
Metadada
We can define a nested class, class Meta
, inside a class if we want to declare model-related metadata. There’s a whole bunch of stuff we can use in this class to impact the model class it’s defined in. For example, we can set the ordering of records that are returned when we query the model type. To do that, we just set the ordering attribute to the field or fields (enclosed in a list) we want to sort by.
Let’s say we want the projects to be ordered chronologically, so by date. This is how we do it:
class Meta:
ordering = ['-date_added']
Here, we’re ordering by just one field, but we could specify multiple fields in the list in the order we want to sort by. The minus sign before the field name means we want to sort in descending order. Dates are ordered chronologically, strings are ordered alphabetically.
The Meta
class in the Project
model is pretty simple. We’ll define a slightly more complicated Meta
class in some of the other models. Speaking of which…
The Category Model
We’ll define all the models in the models.py file. Let’s start with the Category
model. Here’s the code to add below the Project
class definition:
class Category(models.Model):
"""Model representing a category."""
name = models.CharField(
max_length=200,
unique=True,
help_text="Enter a category name."
)
icon = models.CharField(
max_length=100,
default = '',
help_text = 'Enter an icon name.'
)
def __str__(self):
"""String for representing the Model object."""
return self.name
def get_absolute_url(self):
"""Returns the url to access a particular category instance."""
return reverse('category-detail', args=[str(self.id)])
class Meta:
constraints = [
UniqueConstraint(
Lower('name'),
name='category_name_case_insensitive_unique',
violation_error_message = "A category with this name already exists."
),
]
verbose_name_plural = 'categories'
As far as the two methods are concerned, __str__
and get_absolute_url
, they are used for the same purposes as in the Project
class.
The name field is of type CharField
, so it’s going to be a string. Its max_length
attribute is set to 200 characters, which seems more than enough for a category name. We also set the unique
parameter to True
to ensure there’s only one category with a given name in the entire database.
The icon
field is also of type CharField
and we’ll use it to store the name of the icon that will graphically represent the category.
Here we have a little more complex Meta
class:
class Meta:
constraints = [
UniqueConstraint(
Lower('name'),
name='category_name_case_insensitive_unique',
violation_error_message = "A category with this name already exists."
),
]
verbose_name_plural = 'categories'
First of all, for this to work, we have to add the following import statements at the top of the file in the import section or directly above the class definition:
from django.db.models import UniqueConstraint
from django.db.models.functions import Lower
Here, we’re defining some constraints for the Category
model. Although we set the unique attribute for the name
field to True
, it doesn’t take case into account. So, names like ‘data science
’, ‘Data science
’ and ‘Data Science
’ are considered three different names and may occur side by side in the database. This isn’t what we want, so in the Meta
class we specify the UniqueConstraint
. By setting Lower('name')
we ensure the lowercase version of the name is unique, so all three aforementioned names will be first converted to lowercase and then compared. This time, the uniqueness constraint will be violated and the error message the violation_error_message
attribute is set to will be displayed.
We also set the verbose_name_plural
attribute to the correct plural form ‘categories
‘. If we don’t do that, the incorrect form ‘categorys
‘ will be generated then in the admin site.
The Technology Model
We’re going to define the Technology
model in a very similar way:
class Technology(models.Model):
"""Model representing a technology."""
name = models.CharField(
max_length=200,
unique=True,
help_text='Enter a technology name.'
)
icon = models.CharField(
max_length=100,
default = '',
help_text = 'Enter an icon name.'
)
def __str__(self):
"""String for representing the Model object."""
return self.name
def get_absolute_url(self):
"""Returns the url to access a particular technology instance."""
return reverse('technology-detail', args=[str(self.id)])
class Meta:
constraints = [
UniqueConstraint(
Lower('name'),
name='technology_name_case_insensitive_unique',
violation_error_message = 'A technology with this name already exists.'
),
]
verbose_name_plural = 'technologies'
Reordering the Models
If you look at the models.py file, the models are defined in a specific order. In particular, we first defined the Project
class, then Category
, and finally Technology
.
As Category
and Technology
are defined after the Project
class, we can’t use their class names inside the Project
class to define the fields. Instead, we have to use the names of the classes as strings:
category = models.ForeignKey('Category',
on_delete=models.RESTRICT,
null=True)
technologies = models.ManyToManyField('Technology',
help_text='Select technologies used in this project.')
Let’s move the definitions of Category
and Technology
to the top of the file so that the definition of Project
follows them. Next, let’s replace the strings shown above with class names:
category = models.ForeignKey(Category,
on_delete=models.RESTRICT,
null=True)
technologies = models.ManyToManyField(Technology,
help_text='Select technologies used in this project.')
Finally, there’s one more model to define.
The Link Model
Let’s define the Link
model as last, so after the definition of Project
, because this way we’ll be able to use the class name of Project
inside Link
to define a field. Here’s the code:
class Link(models.Model):
"""Model representing a link."""
# the name of the link like 'Github', 'Amazon', etc.
name = models.CharField(max_length=100)
# the logo icon for the link, like the Github logo
icon = models.CharField(
max_length=100,
default = '',
help_text = 'Enter an icon name.'
)
# the URL to the link target
address = models.CharField(max_length=100)
# the project the link belongs to
project = models.ForeignKey(Project, on_delete=models.RESTRICT, null=True)
def __str__(self):
"""String for representing the Model object."""
return self.name
The code is simple and requires no further explanation, I think.
The Database Migrations
In the previous part of the series we created the skeleton of the application and ran the database migrations to inform the database of its structure. Now that we’ve added all the model classes, we must let the database know about them. So, let’s re-run the migrations in the terminal:
python manage.py makemigrations
python manage.py migrate
Now that we have our models in place, we can feed some data to the database. To do that, we’ll use the admin site, which is the topic of the next part in the series.