Skip to main content

Django One-to-Many Models & ModelForms

Django Logo

Learning Objectives

Students will be able to:
Implement a one-to-many relationship in Django Models
Traverse a Model's related data
Use a custom ModelForm to generate form inputs for a Model
Assign the foreign key when creating a new "child" object
Add a custom method to a Model

Road Map

  1. Ready the Starter Code
  2. A Cat's Got to Eat!
  3. Add the New Feeding Model
  4. Add a Field for the Foreign Key
  5. Make and Run the Migration
  6. Test the Models in the Shell
  7. Don't Forget the Admin Portal
  8. Displaying a Cat's Feedings
  9. Adding New Feeding Functionality
  10. Lab
  11. Essential Questions
  12. Bonus Challenge: Adding a Custom Method to Check the Feeding Status

Ready the Starter Code

This lesson will pick up from where the last lesson left off, however, just in case, please sync with the starter code in the catcollector repo:

  1. cd ~/code/catcollector
  2. git fetch --all
  3. git reset --hard origin/main

Then you can start the web server with python3 manage.py runserver

A Cat's Got to Eat

In this lesson we're going to look at how to add another model that demonstrates working with one-to-many relationships in Django.

Since a cat's got to eat, let's go with this relationship:

Cat - Feeding

How does the above relationship "read"?

A Cat has many Feedings" -and- "A Feeding belongs to a Cat"

Add the New Feeding Model

What file are we going to add the new Feeding Model in?

main_app/models.py

Creating the basics for the Feeding Model

Using the ERD above as a guide, let's add the new Feeding Model (below the current Cat Model):

# Add new Feeding model below Cat model
class Feeding(models.Model):
date = models.DateField()
meal = models.CharField(max_length=1)

Note that we're going to use just a single-character to represent what meal the feeding is for: Breakfast, Lunch or Dinner...

Field.choices

Django has a feature, Field.choices, that will make the single-characters more user-friendly by automatically generating a select dropdown in the form using descriptions that we define.

The first step is to define a tuple of 2-tuples. Because we might need to access this tuple within the Cat class also, let's define it above both of the Model classes:

# A tuple of 2-tuples
MEALS = (
('B', 'Breakfast'),
('L', 'Lunch'),
('D', 'Dinner')
)
# new code above

class Cat(models.Model):

As you can see, the first item in each 2-tuple represents the value that will be stored in the database, e.g. B.

The second item represents the human-friendly "display" value, e.g., Breakfast.

Now let's enhance the meal field as follows:

class Feeding(models.Model):
date = models.DateField()
meal = models.CharField(
max_length=1,
# add the 'choices' field option
choices=MEALS,
# set the default value for meal to be 'B'
default=MEALS[0][0]
)

Add the __str__ Method

As recommended, we should override the __str__ method on Models so that they provide more meaningful output when they are printed:

class Feeding(models.Model):
...

def __str__(self):
# Nice method for obtaining the friendly value of a Field.choice
return f"{self.get_meal_display()} on {self.date}"

Check out the convenient get_meal_display() method Django automagically creates to access the human-friendly value of a Field.choice like we have on meal.

Add the Foreign Key

Since a Feeding belongs to a Cat, it must hold the id of the cat object it belongs to - yup, it needs a foreign key!

Here's how it's done - Django style:

class Feeding(models.Model):
date = models.DateField()
meal = models.CharField(
max_length=1,
choices=MEALS,
default=MEALS[0][0]
)
# Create a cat_id FK
cat = models.ForeignKey(Cat, on_delete=models.CASCADE)

def __str__(self):
return f"{self.get_meal_display()} on {self.date}"

As you can see, the ForeignKey field-type is used to create a one-to-many relationship.

The first argument provides the parent Model.

In a one-to-many relationship, the on_delete=models.CASCADE is required. It ensures that if a Cat record is deleted, all of the child Feedings will be deleted automatically as well - thus avoiding orphan records (seriously, that's what they're called).

In the database, the column in the feedings table for the FK will actually be called cat_id because Django by default appends _id to the name of the attribute we use in the Model.

Make and Run the Migration

We added/updated a new Model, so it's that time again...

python3 manage.py makemigrations
Now what do we need to type?

python3 manage.py migrate

After creating a Model, especially one that relates to another, it's always a good idea to test drive the Model and relationships in the shell.

Test the Models in the Shell

Besides testing Models and their relationships, the following will demonstrate how to work with the ORM in views.

First, open the shell:

python3 manage.py shell

Now let's import everything models.py has to offer:

>>> from main_app.models import *
>>> Feeding
<class 'main_app.models.Feeding'>
>>> MEALS
(('B', 'Breakfast'), ('L', 'Lunch'), ('D', 'Dinner'))

So far, so good!

May the Test Drive Commence

# get first cat object in db
>>> c = Cat.objects.first() # or Cat.objects.all()[0]
>>> c
<Cat: Maki>
# obtain all feeding objects for a cat using the "related manager" object
>>> c.feeding_set.all()
<QuerySet []>
# create a feeding for a given cat
>>> c.feeding_set.create(date='2018-10-06')
<Feeding: Breakfast on 2018-10-06>
# yup, it's there and the default of 'B' for the meal worked
>>> Feeding.objects.all()
<QuerySet [<Feeding: Breakfast on 2018-10-06>]>
# and it belongs to a cat
>>> c.feeding_set.all()
<QuerySet [<Feeding: Breakfast on 2018-10-06>]>
# get the first feeding object in the db
>>> f = Feeding.objects.first()
>>> f
<Feeding: Breakfast on 2018-10-06>
# cat is the name of the field we defined in the Feeding model
>>> f.cat
<Cat: Maki>
>>> f.cat.description
'Lazy but ornery & cute'
# another way to create a feeding for a cat
>>> f = Feeding(date='2018-10-06', meal='L', cat=c)
>>> f.save()
>>> f
<Feeding: Lunch on 2018-10-06>
>>> c.feeding_set.all()
<QuerySet [<Feeding: Breakfast on 2018-10-06>, <Feeding: Lunch on 2018-10-06>]>
# finish the day's feeding, this time using the create method
>>> Feeding.objects.create(date='2018-10-06', meal='D', cat=c)
>>> c.feeding_set.count()
3
# list cat ids
>>> for cat in Cat.objects.all():
... print(cat.id)
...
2
3
# feed another cat
>>> c = Cat.objects.get(id=3)
>>> c
<Cat: Whiskers>
>>> c.feeding_set.create(date='2018-10-07', meal='B')
<Feeding: Breakfast on 2018-10-07>
>>> Feeding.objects.filter(meal='B')
<QuerySet [<Feeding: Breakfast on 2018-10-06>, <Feeding: Breakfast on 2018-10-07>]>
# the foreign key (cat_id) can be used as well
>>> Feeding.objects.filter(cat_id=2)
<QuerySet [<Feeding: Breakfast on 2018-10-06>, <Feeding: Lunch on 2018-10-06>, <Feeding: Dinner on 2018-10-06>]>
>>> Feeding.objects.create(date='2018-10-07', meal='L', cat_id=3)
>>> Cat.objects.get(id=3).feeding_set.all()
<QuerySet [<Feeding: Breakfast on 2018-10-07>, <Feeding: Lunch on 2018-10-07>]>
exit()

Behind the scenes, an enormous amount of SQL was being generated and sent to the database!

💪 Practice Exercise (5 minutes)

Practice creating and accessing feedings for a cat...

  1. Retrieve the last cat using the Cat model and its object manager's last() method and assign to a variable named last_cat.Hint: Refer to how the first() method is being used to retrieve the first cat above.

  2. Use the create() method on last_cat's related manager to add a Lunch and a Dinner feeding for today.Hint: We used this approach to create the very first feeding above.

  3. Verify both feeding objects were created by calling the all() method on last_cat's related manager.

Don't Forget the Admin Portal

Remember, before a Model can be CRUD'd using the built-in Django admin portal, the Model must be registered.

Update main_app/admin.py so that it looks as follows:

from django.contrib import admin
# add Feeding to the import
from .models import Cat, Feeding

admin.site.register(Cat)
# register the new Feeding Model
admin.site.register(Feeding)

Now browse to localhost:8000/admin and click on the Feeding link.

Admin Feeding

Check out those select drop-downs for assigning both the Meal and the Cat!

Custom Field Labels

Let's say though that you would like a less vague label than Date.

Because Django subscribes to the philosophy that a Model is the single, definitive source of truth about your data, you can bet that's where we will add customization.

In main_app/models.py, add the desired user-friendly label to the field-type like so:

class Feeding(models.Model):
# the first optional positional argument overrides the label
date = models.DateField('feeding date')
...

Refresh and...

Custom Label

What's cool is that the custom labels will be used in all of Django's ModelForms too!

Displaying a Cat's Feedings

Just like it would make sense in an app to show comments for a post when that post is displayed, a cat's detail page is where it would make sense to display a cat's feedings.

No additional views or templates necessary. All we have to do is update the detail.html.

Our imaginary wireframe calls for a cat's feedings to be displayed to the right of the cat's details. We can do this using Materialize's grid system to define layout columns.

Here's the new content of detail.html. Best to copy/paste this new markup, then we'll review:

{% extends 'base.html' %}
{% block content %}

<h1>Cat Details</h1>

<div class="row">
<div class="col s6">
<div class="card">
<div class="card-content">
<span class="card-title">{{ cat.name }}</span>
<p>Breed: {{ cat.breed }}</p>
<p>Description: {{ cat.description }}</p>
{% if cat.age > 0 %}
<p>Age: {{ cat.age }}</p>
{% else %}
<p>Age: Kitten</p>
{% endif %}
</div>
<div class="card-action">
<a href="{% url 'cats_update' cat.id %}">Edit</a>
<a href="{% url 'cats_delete' cat.id %}">Delete</a>
</div>
</div>
</div>
<div class="col s6">
<table class="striped">
<thead>
<tr><th>Date</th><th>Meal</th></tr>
</thead>
<tbody>
{% for feeding in cat.feeding_set.all %}
<tr>
<td>{{feeding.date}}</td>
<td>{{feeding.get_meal_display}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

A refresh results in this:

Cat Details Page

Adding New Feeding Functionality

Now we want to add the capability to add a new feeding when viewing a cat's details.

Previously we used class-based views to productively perform create and update data operations for a Model.

The CBV we used automatically created a ModelForm for the Model that was specified like this: model = Cat.

The CBV then used the ModelForm behind the scenes to generate the inputs and provide the posted data to the server.

Because we want to be able to show a form for adding a feeding on the detail page of a cat, we're going to see in this lesson how to create a custom ModelForm from scratch that will:

  1. Generate a feeding's inputs inside of the <form> tag we provide.
  2. Be used to validate the posted data by calling is_valid().
  3. Persist the model instance to the database by calling save() on the instance of the ModelForm.

There's several steps we're going to need to complete, so let's get started...

Create the ModelForm for the Feeding Model

We could define the ModelForm inside of the models.py module, but we're going to follow a best practice of defining it inside of a forms.py module instead:

touch main_app/forms.py

Let's open it and add this code:

from django.forms import ModelForm
from .models import Feeding

class FeedingForm(ModelForm):
class Meta:
model = Feeding
fields = ['date', 'meal']

Note that our custom form inherits from ModelForm and has a nested class Meta: to declare the Model being used and the fields we want inputs generated for. Confusing? Absolutely, but it's just the way it is, so accept it and sleep well.

Many of the attributes in the Meta class are in common with CBVs because the CBV was using them behind the scenes to create a ModelForm as previously mentioned.

For more options, check out the Django ModelForms documentation.

Passing an Instance of FeedingForm

FeedingForm is a class that needs to be instantiated in the cats_detail view function so that it can be "rendered" inside of detail.html.

Here's the updated cats_detail view function in views.py:

from .models import Cat
# Import the FeedingForm
from .forms import FeedingForm

...

# update this view function
def cats_detail(request, cat_id):
cat = Cat.objects.get(id=cat_id)
# instantiate FeedingForm to be rendered in the template
feeding_form = FeedingForm()
return render(request, 'cats/detail.html', {
# include the cat and feeding_form in the context
'cat': cat, 'feeding_form': feeding_form
})

feeding_form is set to an instance of FeedingForm and then it's passed to detail.html just like cat.

Displaying FeedingForm Inside of detail.html

Okay, so we're going to need a form used to submit a new feeding.

We're going to display a <form> at the top of the feedings column in detail.html.

This is how we can "render" the ModelForm's inputs within <form> tags in templates/cats/detail.html:

<div class="col s6">
<!-- new code below -->
<form method="POST">
{% csrf_token %}
{{ feeding_form.as_p }}
<input type="submit" class="btn" value="Add Feeding">
</form>
<!-- new code above -->
<table class="striped">
...

As always, we need to include the {% csrf_token %} for security purposes.

A form's action attribute determines the URL that a form is submitted to. For now, we'll leave it out and come back to it in a bit.

The {{ feeding_form.as_p }} will generate the <input> tags wrapped in <p> tags for each field we specified in FeedingForm.

Let's see what it looks like - not perfect but it's a start:

No Date Picker

Unfortunately, the Feeding Date field is just a basic text input. This is what Django uses by default for DateFields.

Also, we don't see a drop-down for the Meal field as expected. However, it's in the HTML, but it's not rendering correctly due to the use of Materialize.

Add Materialize's JavaScript Library

Luckily we can use Materialize to solve both problems. However, solving the problems will require that we include Materialize's JavaScript library and add a bit of our own JavaScript to "initialize" the inputs.

First, update base.html to include the Materialize JS library:

<head>
...
<link rel="stylesheet" type="text/css" href="{% static 'style.css' %}">
<!-- Add the following below --->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
...
Using a Date-picker for Feeding Date

Okay now let's make the <input> for Feeding Date a sweet date-picker using Materialize.

Materialize requires us to "initialize" certain inputs using a bit of JavaScript. We want this JS to run after the elements are in the DOM, so we'll just put the JS at the bottom of the template.

Let's add <script> tags at the bottom of of detail.html:

</div>

<!-- below all HTML -->
<script>

</script>
{% endblock %}

Using Materialize, it takes two steps to get the inputs the way we want them:

  • Select the element(s)
  • "Initialize" the element(s) using Materialize's library

Now let's add the JS to inside of the <script> tags to initialize the date-picker:

</div>

<script>
const dateEl = document.getElementById('id_date');
M.Datepicker.init(dateEl, {
format: 'yyyy-mm-dd',
defaultDate: new Date(),
setDefaultDate: true,
autoClose: true
});
</script>
{% endblock %}

As you can see, the ModelForm automatically generated an id attribute for each <input>. Always be sure to use Devtools to figure out how to select elements for styling, etc.

There are plenty of additional options for date-pickers than the four used above. Materialize's date-picker documentation has the details.

Refresh and test it out by clicking inside of Feeding Date - you should see a sweet date-picker pop up like this:

Date Picker

Now let's fix the dropdown...

Fix the Meal <select>

Because we used choices=MEALS in the Feeding model, FeedingForm generated a <select> instead of the typical <input type="text"> for a CharField.

But, <select> dropdowns also need to be initialized when using Materialize.

It doesn't require any options, just select it and init it:

</div>

<script>
const dateEl = document.getElementById('id_date');
M.Datepicker.init(dateEl, {
format: 'yyyy-mm-dd',
defaultDate: new Date(),
setDefaultDate: true,
autoClose: true
});

// add additional JS to initialize select below
const selectEl = document.getElementById('id_meal');
M.FormSelect.init(selectEl);
</script>
{% endblock %}

Refresh and the Meal <select> is looking good:

Meal Select

Now with the UI done, let's think about the URL to POST the form to...

Add Another path(...) to urls.py

Every Feeding object needs a cat_id that holds the primary key of the cat object that it belongs to.

Therefore, we need to ensure that the route includes a URL parameter for capturing the cat's id like we've done in other routes.

Okay, let's add a route to the urlpatterns list in urls.py like this:

urlpatterns = [
...
path('cats/<int:cat_id>/add_feeding/', views.add_feeding, name='add_feeding'),
]

The above route is basically saying that the <form>'s action attribute will need to look something like /cats/2/add_feeding. Let's go there now...

Add the action Attribute to the <form>

Now that we have a named URL, let's add the action attribute to the <form> in detail.html:

<div class="col s6">
<!-- add the action attribute as follows -->
<form action="{% url 'add_feeding' cat.id %}" method="POST">
{% csrf_token %}
{{ feeding_form.as_p }}
<input type="submit" class="btn" value="Add Feeding">
</form>

Once again, we're using the better practice of using the url template tag to write out the correct the URL for a route.

If the URL requires values for named parameters such as <int:cat_id>, the url template tag accepts them after the name of the route.

Note that arguments provided to template tags are always separated using a space character, not a comma.

We won't be able to refresh and check it out until we add the views.add_feeding function...

Add the views.add_feeding View Function to views.py

Let's start by stubbing up an add_feeding view function in views.py so that the server will start back up and we can inspect our <form>:

...

# add this new function below cats_detail
def add_feeding(request, cat_id):
pass

Using pass is a way to define an "empty" Python function.

Refreshing and inspecting the elements using DevTools shows that our <form> is looking good:

Form element

Now let's turn our attention back to the add_feeding function.

Here it is:

def add_feeding(request, cat_id):
# create a ModelForm instance using the data in request.POST
form = FeedingForm(request.POST)
# validate the form
if form.is_valid():
# don't save the form to the db until it
# has the cat_id assigned
new_feeding = form.save(commit=False)
new_feeding.cat_id = cat_id
new_feeding.save()
return redirect('detail', cat_id=cat_id)

After ensuring that the form contains valid data, we save the form with the commit=False option, which returns an in-memory model object so that we can assign the cat_id before actually saving to the database.

Always be sure to redirect instead of render if data has been changed in the database.

We also need to import redirect by adding after render:

# main_app/views.py
from django.shortcuts import render, redirect

Add a feeding and rejoice!

Add Feeding

Well, almost...

It would be nice to have the most recent dates displayed at the top.

Yup, a Model is the single, definitive source of truth...

The answer lies in adding a class Meta with the ordering Model Meta option within the Feeding Model:

class Feeding(models.Model):
...
def __str__(self):
# Nice method for obtaining the friendly value of a Field.choice
return f"{self.get_meal_display()} on {self.date}"

# change the default sort
class Meta:
ordering = ['-date']

Feed the cats!

Be sure to check out the BONUS section on your own.

Lab Assignment

As usual, Lab time is to be spent implementing the same features in your Finch Collector project.

Let's wrap up with a few questions...

❓ Essential Questions

Take a moment to review, then on to the student picker!

1. When two Models have a one-many relationship, which Model must include a field of type models.ForeignKey, the one side, or the many side?

2. True or False: ModelForms are used to generate the form inputs for a model, validate the submitted form and save the data in the database.

3. Assuming a ForeignKey field named author is on a Book Model; and we have an author object, we can access the author's books via author.book_set?

4. In the above Author ---< Book scenario, how would we access a book object's author?

Bonus Challenge: Adding a Custom Method to Check the Feeding Status

When adding business logic to an application, always consider adding that logic to the Models first.

This approach is referred to as "fat models / skinny views" and results in keeping code DRY.

We're going to add a custom method to the Cat Model that help's enable the following messaging based upon whether or not the cat has had at least the number of feedings for the current day as there are MEALS.

When a cat does not have at least the number of MEALS:

Hungry Cat

And when the cat has been completely fed for the day:

Full Cat

The fed_for_today Custom Model Method

Let's add a one line method to the Cat Model class:

from django.db import models
from django.urls import reverse
# add this import
from datetime import date

class Cat(models.Model):
...
# add this new method
def fed_for_today(self):
return self.feeding_set.filter(date=date.today()).count() >= len(MEALS)

Be sure to add the import at the top of models.py.

The fed_for_today method demonstrates the use of filter() to obtain a <QuerySet> for today's feedings, then count() is chained on to the query to return the actual number of objects returned.

The above approach in more efficient than this similar code:

len( self.feeding_set.filter(date=date.today()) )

because this code would return all objects from the database when all we need is the number of objects returned by count(). In other words, don't return data from the database if you don't need it.

Update the detail.html Template

All that's left is to sprinkle in a little template code like this:

...
<div class="col s6">
<form action="{% url 'add_feeding' cat.id %}" method="POST">
{% csrf_token %}
{{ feeding_form.as_p }}
<input type="submit" class="btn" value="Add Feeding">
</form>
<!-- new markup below -->
<br>
{% if cat.fed_for_today %}
<div class="card-panel teal-text center-align">{{cat.name}} has been fed all meals for today</div>
{% else %}
<div class="card-panel red-text center-align">{{cat.name}} might be hungry</div>
{% endif %}
<!-- new markup above-->
<table class="striped">

Congrats on using a custom Model method to implement business logic!

Resources

Django Model API

Django ModelForms