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:
Add a category model
Add the Foreign Key to the Post model
Create some categories in the admin panel
Update our form
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.
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
)
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:
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 %}
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.
# 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
.