Skip to content

Marina Mele's site

Reflections on family, values, and personal growth

Menu
  • Home
  • About
Menu
TaskBuster ForeignKey relationship

Part X – Model creation, ForeignKey relations, testing and the Django Admin

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:

taskbuster-start-model-diagram-v2

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:

taskbuster-project-model

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).
TaskBuster ForeignKey relationship
One to Many relationship: each orchestra musician has a Foreign Key relationship with its conductor
  • 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 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:

taskbuster-edit-profile-project

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:

taskbuster-profile-listing-2

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! 🙂

Tag Model UML diagramThe 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

Profile Model Admin

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!

Leave a Reply Cancel reply

You must be logged in to post a comment.

Categories

  • Personal Growth and Development
  • Artificial Intelligence
  • Mindful Parenting and Family Life
  • Productivity and Time Management
  • Mindfulness and Wellness
  • Values and Life Lessons
  • Posts en catalĂ 
  • Other things to learn
Follow @marina_mele
  • Cookie Policy
  • Privacy Policy
©2025 Marina Mele's site | Built using WordPress and Responsive Blogily theme by Superb
This website uses cookies to improve your experience. If you keep navigating through this website, we'll assume you're ok with this, but you can opt-out if you wish.Accept Read More
Privacy & Cookies Policy

Privacy Overview

This website uses cookies to improve your experience while you navigate through the website. Out of these, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may affect your browsing experience.
Necessary
Always Enabled
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
Non-necessary
Any cookies that may not be particularly necessary for the website to function and is used specifically to collect user personal data via analytics, ads, other embedded contents are termed as non-necessary cookies. It is mandatory to procure user consent prior to running these cookies on your website.
SAVE & ACCEPT