Django Foreign Key Example - How to Add Categories to a Blog

Foreign keys give you the power to link the tables in the database as you see fit, but they can be confusing at first.

In a previous post, we went through how to use Foreign Keys.

Today, I’m going to go through a full example of how you can add a Foreign Key to your project.

Not only that, we’re also going to go through how to work with Foreign Keys in forms. By default, Django uses a drop-down menu for Foreign Key fields, but I’m going to show you how to add radio buttons instead.

The example I’m going to use is a blogging application. In a previous post, we added tags to a blog, which uses a ManyToManyField. We are going to allow users to assign posts a category, where there is a many to one relationship between posts and categories.

To add categories to a blogging application, these are the steps we need to take:

  1. Add a category model

  2. Add the Foreign Key to the Post model

  3. Create some categories in the admin panel

  4. Update our form

  5. Add a URL/view so that users can view posts by category

If you would like to follow this tutorial, you can use this repository. If you get stuck, then refer to this repository which has the completed code.

1. Add the Category model

This example uses django-autoslug. I’ve used it because it will automatically provide a unique slug based on the category name. I prefer using this over the slugify method because django-autoslug can guarantee uniqueness without additional code, which is important if you want to use them for URLs.

To install it, run the following line or add it to requirements.in.

$ pip install django-autoslug

I want Django admin to display “Categories” instead of “Categorys”. To do this, I have set the verbose_name_plural in the Meta subclass.

I have also defined __str__ so that the categories listed in the admin panel are easy to read.

# models.py

class Category(models.Model):
    class Meta:
        verbose_name_plural = "categories"

    name = models.CharField(max_length=100, unique=True)
    slug = AutoSlugField(populate_from="name", unique=True)

    def __str__(self):
        return self.name

2. Add the Foreign Key to the Post model

Foreign keys are used when there is a Many-to-One or One-to-Many relationship between two models.

The relationship between Posts and Categories is Many-to-One. This is because a post can only have one category but a category can have multiple posts.

It is important to set the related_name attribute so that you can access the posts from a category object.

When deciding what to set for on_delete, you have to ask “what do you do with the many, when you delete the one?”. This is the same as asking “when you delete a category, what do you want to do with the posts?”

When we delete a category, we want to keep the posts but we don’t want the post to point to a category that doesn’t exist. Therefore, models.SET_NULL is the most appropriate choice here. If we used models.CASCADE, then deleting a category will also delete the related posts.

# models.py

category = models.ForeignKey(
        to=Category,
        related_name="posts",
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

Make and run migrations

We have added fields to two models, so we must create and run migrations to apply our changes to the database.

$ python manage.py makemigrations

and then run them:

$ python manage.py migrate

3. Add some categories

Register the Category model in the admin panel

It is important to register your models after running the migrations so that they appear in the admin panel.

# admin.py

admin.site.register(models.Category, admin.ModelAdmin)

Create some categories

The next step is to create some categories.

Notice how the screenshot displays “Categories” instead of “Categorys”. This is because we set the verbose_plural_name in the model.

A screenshot form admin showing a list of categories created in the admin panel

4. Update the form

The next step is to update the form. All we need to do here is add “category” to the fields list. The default widget for a foreign key is a drop down. You can change this to radio buttons if you prefer. We will do this in the next step.

# forms.py

class PostForm(forms.ModelForm):
    class Meta:
        model = models.Post
        fields = ["title", "body", "category", "tags"]

    tags = forms.ModelMultipleChoiceField(
        queryset=models.Tag.objects.all(), widget=forms.CheckboxSelectMultiple
    )

A screenshot of the add post form showing the dropdown menu for the post category.

Add radio buttons (optional)

To use radio buttons for our Foreign Key field, use the forms.ModelChoiceField. To use this, you will need to specify the choices by providing a queryset and a widget.

# forms.py

from django import forms
from . import models


class PostForm(forms.ModelForm):
    class Meta:
        model = models.Post
        fields = ["title", "body", "category", "tags"]

    tags = forms.ModelMultipleChoiceField(
        queryset=models.Tag.objects.all(), widget=forms.CheckboxSelectMultiple
    )

    category = forms.ModelChoiceField(
        queryset=models.Category.objects.all(), widget=forms.RadioSelect
    )

Here's the result:

A screenshot of the add post form showing the radio buttons to select the post category. This replaces the drop-down menu.

5. Allow users to view posts by category

Add a new URL

Add a new URL to urlpatterns like the slug below. Including the name “category” in the path will help avoid naming conflicts (where a URL matches multiple paths causing the wrong view to be used).

# blog/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("add", views.add_post, name="add_post"),
    path("<slug:slug>", views.post_detail, name="post_detail"),
    path("<slug:slug>/publish", views.publish, name="publish"),
    path("<slug:slug>/edit", views.edit_post, name="edit"),
    path("<slug:slug>/delete", views.delete_post, name="delete"),
    path("tags/<int:tag_id>", views.list_posts_by_tag, name="tag"),
    path("category/<slug:slug>", views.list_posts_by_category, name="category"),
]

Add a new view

The next step is to add the view. Here, we are using the get_object_or_404 shortcut so that a 404 is raised if the category does not appear in the database.

# views.py

def list_posts_by_category(request, slug):
    category = get_object_or_404(Category, slug=slug)

    posts = Post.objects.filter(category=category)

    context = {"category_name": category.name, "posts": posts}

    return render(request, "index.html", context)

Update our templates

I am reusing index.html for rendering post lists filtered by tag or category. I’ve passed the name of the category from the view to the template to render the correct heading.

# index.html

{% if tag_name %}
<h1>Tag: {{tag_name}}</h1>
{% elif category_name %}
<h1>Category: {{category_name}}</h1>
{% else %}
<h1>All Posts</h1>
{% endif %}

A screenshot the page that shows the posts for a given category.

Update the post detail template

I want to display the category above the title. I want the user to be able to click on the category and see other posts with the same category.

A screenshot of the post detail page. The category name is displayed above the title.

# post_detail.html

{% if post.category %}
<a href="{% url 'category' post.category.slug %}" class="text-decoration-none">
    <h4 class="text-muted">{{post.category}}</h4>
</a>
{% endif %}

Conclusion

We have gone through 5 steps to add a Category model to a blogging application and link it to the Post model using a Foreign Key.

Django uses drop-down menus by default for rendering Foreign Key fields in forms, but I have shown that you can change those to radio buttons by adding just one line of code to forms.py.