Intro to Django One-to-Many Relationships




Learning Objectives

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



Roadmap

  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. Essential Questions
  11. Bonus: 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.




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:

How does the above relationship "read"? (Click For Answer)

"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?(Click For Answer)

main_app/models.py




Creating the basics for 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 we will define next...




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. The second item represents the human-friendly "display" value.




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 always, we should add a __str__ method to 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...

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

python 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:

$ python 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.all()[0]   # or Cat.objects.first()
>>> c
<Cat: Maki>
# obtain all feeding objects for a cat (no surprise there aren't any)
>>> 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>
>>> 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
>>> Feeding(date='2018-10-06', meal='D', cat=c).save()
>>> 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(date='2018-10-07', meal='L', cat_id=3).save()
>>> Cat.objects.get(id=3).feeding_set.all()
<QuerySet [<Feeding: Breakfast on 2018-10-07>, <Feeding: Lunch on 2018-10-07>]>
exit()



Now how much would you pay for that ORM?

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




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.




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):
  date = models.DateField('feeding date')
  ...



Refresh and...




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:




Adding New Feeding Functionality

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

Previously, you were shown how to use class-based views to productively help 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 as new Model on 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 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 model form.

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 we've already 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">
    ...
  </table>
</div>



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:




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>
  ...
</head>



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:

  1. Select the element(s)
  2. "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>
  var 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:




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>
  var 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
  var selectEl = document.getElementById('id_meal');
  M.FormSelect.init(selectEl);
</script>
{% endblock %}



Refresh and the Meal <select> is looking good:




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>
</div>



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:




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




Here it is:

def add_feeding(request, cat_id):
  # create the ModelForm 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!




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.




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. What are ModelForms used for?

3. What technique did we use to pass the id of the cat to the add_feeding view function?




BONUS: 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:




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




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"></table>
</div>



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




Resources

Django Model API

Django ModelForms

Copyright © General Assembly 2022

Created by DanielJS