In this part of the TaskBuster tutorial we’re going to continue defining our Models. We will define the Project and Tag models, which have a Foreign Key relationship with the Profile model.
Moreover, we will talk about custom validation, testing and customizing the Admin Site with inline models.
The outline of this part is:
- UML Diagram Revision
- The Project Model: Foreign Key Relationships and custom validators
- Tests for the Project Model
- Django Admin for the Project Model: custom list display and Model Inline
- The Tag Model: Another simple model with a ForeignKey relationship
Let’s start! 🙂
UML Diagram Revision
In this part of the tutorial, we’re going to continue defining our models. Remember that last time we created the Profile Model, which had a OneToOne relationship with the User model.
Let’s take a look at the UML diagram of our models:

As a recap:
- the Profile Model has a OneToOne relationship with the User model.
- Both the Project and the Tag models have a ForeignKey relationship with the Profile Model,
- and the Task model has
- a ForeignKey relationship with the Project Model
- a ManyToMany relationship with the Tag model
- a self ForeignKey relationship with itself.
A little bit of everything! 🙂
The Project Model: Foreign Key Relationships and custom validators
First, we’re going to define the Project Model, which will be used to group our tasks under multiple tag names.
The detailed UML diagram for this model is:

In taskbuster/apps/taskmanager/models.py write:
from django.core.validators import RegexValidator
class Project(models.Model):
# Relations
user = models.ForeignKey(
Profile,
related_name="projects",
verbose_name=_("user")
)
# Attributes - Mandatory
name = models.CharField(
max_length=100,
verbose_name=_("name"),
help_text=_("Enter the project name")
)
color = models.CharField(
max_length=7,
default="#fff",
validators=[RegexValidator(
"(^#[0-9a-fA-F]{3}$)|(^#[0-9a-fA-F]{6}$)")],
verbose_name=_("color"),
help_text=_("Enter the hex color code, like #ccc or #cccccc")
)
# Attributes - Optional
# Object Manager
objects = managers.ProjectManager()
# Custom Properties
# Methods
# Meta and String
class Meta:
verbose_name = _("Project")
verbose_name_plural = _("Projects")
ordering = ("user", "name")
unique_together = ("user", "name")
def __str__(self):
return "%s - %s" % (self.user, self.name)
and in taskbuster/apps/taskmanager/managers.py add:
class ProjectManager(models.Manager):
pass
Let’s read step by step this code:
- The Project model has a Foreign Key relationship with the Profile Model.
- This means that:
- each project instance must be related to one User Profile (the profile field is mandatory),
- and each User Profile can be related to zero, one or more than one projects.
- From a project instance, named myproject, we can obtain its related profile with:Â myproject.user
- yes, note that the attribute name defined in Project is user and not profile.
- From a profile instance, named myprofile, we can obtain its related projects with:Â myprofile.projects.all()Â .
- Without specifying a related_name, by default you should access the projects of a profile with myprofile.project_set.all()Â .
- Note that myprofile.project returns an object manager, so that if we want to obtain the project instances, we need to use some of the usual query methods, like all(), filter(), exclude(), etc. We could even call the custom methods defined in the custom ProjectManager class.
- As we saw in the previous part, verbose name just indicates the human-readable name of this attribute.
- Note that it uses the translation function ugettext_lazy (read the previous part to see the import).
- This means that:

- The name of the project it’s a CharField attribute, with a max length of 100 characters.
- A help_text is a text that will appear in the model forms, so that the user knows what he should write.
- Color is another CharField attribute, with a max length of 7.
- We expect an Hex color, which is composed by 3 hexadecimal number that go from 00 to FF, each of them indicating the level of red, green and blue. When put together, they make a 6 character string, plus #, like #XXXXXX.
- For example, black is #000000 and white is #FFFFFF.
- However, when the three numbers are formed by pairs of the same number, like #001122, each of them can be abbreviated with a single digit, like #012. This way, black can also be written as #000, and white as #FFF.
- By default, this field will have a white color.
- In order to accept only correct values of Hex colors, we use a custom validator. The RegexValidator accepts only strings that match the specified regular expression.
- We expect an Hex color, which is composed by 3 hexadecimal number that go from 00 to FF, each of them indicating the level of red, green and blue. When put together, they make a 6 character string, plus #, like #XXXXXX.
- We include the custom object manager defined in managers.py, ProjectManager.
- In Meta, we define:
- the human-readable name of the class
- the default ordering when querying project instances
- the unique_together property, which defines at the database level, that for the same profile we can’t write two projects with the same name.
- The __str__ method is called whenever the str() method is called on an object, like in the admin site, or showing the object in the Django templates.
Perfect! Now that we have our models defined, we need to migrate these changes into the database:
$ python manage.py check $ python manage.py makemigrations taskmanager $ python manage.py migrate taskmanager
Next, let’s write some tests for this Model.
Tests for the Project Model
When writing tests for Django models, I usually focus only in custom attributes and functions, that have some property or behavior that isn’t Django’s default.
For example, I’m not going to test the correct behavior of max_length in a CharField, as it’s a built in feature, that for sure it’s been tested enough by Django developers.
However, I should test that I wrote a good regular expression for the custom validation of the color attribute. Let’s do that then!
In taskbuster/apps/taskmanager/tests.py add the following test:
from django.core.exceptions import ValidationError
class TestProjectModel(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create(
username="taskbuster", password="django-tutorial")
self.profile = self.user.profile
def tearDown(self):
self.user.delete()
def test_validation_color(self):
# This first project uses the default value, #fff
project = models.Project(
user=self.profile,
name="TaskManager"
)
self.assertTrue(project.color == "#fff")
# Validation shouldn't rise an Error
project.full_clean()
# Good color inputs (without Errors):
for color in ["#1cA", "#1256aB"]:
project.color = color
project.full_clean()
# Bad color inputs:
for color in ["1cA", "1256aB", "#1", "#12", "#1234",
"#12345", "#1234567"]:
with self.assertRaises(
ValidationError,
msg="%s didn't raise a ValidationError" % color):
project.color = color
project.full_clean()
And let’s explain what it does:
- The setUp method is executed at the beginning of each test:
- It creates a user instance
- the user instance fires a signal that creates the related profile instance
- both are saved in self for later use.
- test_validation_color tests different inputs of the color attribute:
- first it creates a project with the default value and checks that it doesn’t raise a ValidationError
- next, it checks other correct inputs
- and then checks that bad inputs raise a ValidationError.
- Note that in order to raise a ValidationError we need to call the full_clean() method of the instance, as simply saving the method won’t work.
- The tearDown method is executed at the end of each test:
- it deletes the user instance
- deleting the user instance also deletes all the related instances that depend on this one:
- the profile depends on the user
- the project depends on the profile
- this way we leave the testing database as clean as at the beginning of the test
Ok! now that we understand this test, you can run it with:
$ python manage.py test taskbuster.apps.taskmanager.tests.TestProjectModel
Hope it worked! 🙂
Django Admin for the Project Model: custom list display and Model Inline
Now that we have our model defined and tested, we can include it in the Admin Site.
However, as Projects are related to a specific user profile, we’re going to modify the ProfileAdmin we defined in the previous post. This way, when editing a specific profile, we’ll be able to add or edit its related projects.
In taskbuster/apps/taskmanager/admin.py, replace the ProfileAdmin with:
# -*- coding: utf-8 -*-
from django.contrib import admin
from . import models
class ProjectsInLine(admin.TabularInline):
model = models.Project
extra = 0
@admin.register(models.Profile)
class ProfileAdmin(admin.ModelAdmin):
list_display = ("username", "interaction", "_projects")
search_fields = ["user__username"]
inlines = [
ProjectsInLine
]
def _projects(self, obj):
return obj.projects.all().count()
You can see the changes by running a server:
$ python manage runserver
and going to the Admin Profile list. We’ll discuss the previous code step by step in a minute.
First, edit (or create if you don’t have one) a Profile instance. You’ll see something similar to:

Note that at the bottom, I’ve created two different projects, Blog and TaskBuster. But wait, why do they appear here?
This is thanks to defining the ProjectsInLine class, which inherits from the Django TabularInLine:
class ProjectsInLine(admin.TabularInline):
model = models.Project
extra = 0
the extra parameter indicates how many extra Projects should appear when editing a Profile instance (they will appear empty). Try to change it to 5 and see what happens! 🙂
Moreover, the connection between ProjectsInLine and the ProfileAdmin is done here:
@admin.register(models.Profile)
class ProfileAdmin(admin.ModelAdmin):
...
inlines = [
ProjectsInLine
]
...
Note that when creating a new project inside a profile instance, the relation between these two objects is automatically set (we don’t need to specify the profile field in the project instance). Moreover, only Projects related to the current profile instance are shown here.
On the other hand, if we go back to the Profile Listing, we’ll see something like:

Which shows a list of Profile instances, specifying the username, the interaction value and… how many projects it has?
You thought that you could only show model attributes here? Well, apparently you can also define custom functions 🙂
Let’s take a look at the property list_display:
list_display = ("username", "interaction", "_projects")
it contains the username, the interaction and another property _projects that is not a model attribute nor a custom property. However, you’ll see a custom method defined inside the ProfileAdmin:
def _projects(self, obj):
return obj.projects.all().count()
It takes two arguments: self (the ProfileAdmin instance) and obj (the Profile instance we’re editing).
So this method is simply querying all projects related to the profile instance and counting them. Great! 🙂
The Tag Model: Another simple model with a ForeignKey relationship
As we can see in the UML diagram, the Tag Model is very similar to the Project Model:
- It has a ForeignKey relationship with the Profile model
- It has a name property
As it doesn’t have any extra functionalities, we can define it straightforward in our models.py and managers.py files:
# models.py
class Tag(models.Model):
# Relations
user = models.ForeignKey(
Profile,
related_name="tags",
verbose_name=_("user")
)
# Attributes - Mandatory
name = models.CharField(
max_length=100,
verbose_name=_("Name")
)
# Attributes - Optional
# Object Manager
objects = managers.TagManager()
# Custom Properties
# Methods
# Meta and String
class Meta:
verbose_name = _("Tag")
verbose_name_plural = _("Tags")
ordering = ("user", "name",)
unique_together = ("user", "name")
def __str__(self):
return "%s - %s" % (self.user, self.name)
# managers.py
class TagManager(models.Manager):
pass
And edit the admin.py file to add the Tag model inline:
# admin.py
# -*- coding: utf-8 -*-
from django.contrib import admin
from . import models
class ProjectsInLine(admin.TabularInline):
model = models.Project
extra = 0
class TagsInLine(admin.TabularInline):
model = models.Tag
extra = 0
@admin.register(models.Profile)
class ProfileAdmin(admin.ModelAdmin):
list_display = ("username", "interaction", "_projects", "_tags")
search_fields = ["user__username"]
inlines = [
ProjectsInLine, TagsInLine
]
def _projects(self, obj):
return obj.projects.all().count()
def _tags(self, obj):
return obj.tags.all().count()
Great! Now migrate your database:
$ python manage.py check $ python manage.py makemigrations taskmanager $ python manage.py migrate taskmanager
check the results in the admin site:
$ python manage.py runserver

Run your tests again and make sure nothing is broken! 🙂
$ python manage.py test
and finally, commit your changes:
$ git status $ git add . $ git commit -m "End of part X"
That’s all for today!
Please, give a +1 if useful! Thanks!
