How to add Tags to your Blog (ManyToManyField Example)
In this tutorial, I am going to show you how to add tags to basic Django blog using a ManyToManyField
.
Blogs are a common project used in beginner’s tutorials. This tutorial by Django Girls is one of my favourites, which has instructions on how build a blog.
A good way to practice Django and learn new skills is to add new features to the projects created in tutorials.
For blogs, it is good if users can quickly find more posts belonging to a topic they are interested in.
The steps we will need to take are:
Add the Tag model to
models.py
Update the Post model
Create and run migrations
Register your Tag model in
admin.py
Create some tags in
/admin
Add tags to the Add Post form
Display the tags on your posts
Allow users to view posts by tag
Code
To follow along with this tutorial, you can clone this repository which has a basic blogging application without tags.
Finished code can be found here.
1. Add the Tag model
The first step is to add a model for the tags.
When I did a Node.js course using MongoDB, tags were stored as JSON in the same model as the blog post. When using a relational database like SQLite or PostgreSQL, it is more common to see tags stored in their own model.
This means that all tags can be managed in one place. We can use a Many-to-Many relation to link rows in the Post table with rows in the Tag table.
We use Many-to-Many because one post can have multiple tags and one tag can be applied to multiple posts. If you’d like to build a better understanding of how you can link tables in Django, I have a summary in this post. I also have a separate post that explains how to use Many-to-Many relations in more detail.
The model
The Tag model will have one field: a CharField
to store the name:
# models.py
class Tag(models.Model):
name=models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
We need to add a ManyToManyField
to allow posts and tags to be linked.
You will need to make a decision about which model to add the ManyToManyField
to. The model that gets the field will get the form widget.
I want to be able to add tags to a post when creating the post, so I’m going to put it onto the Post model.
If you prefer to assign posts to an individual tag like the image below, then put it on the tag model.
If you add the ManyToManyField
to the Tag model, the form widget is only available on the create tag form.
2. Update the Post model
On the Post
model, I have added a ManyToManyField
called “tags”. When defining a ManyToManyField
, I have set the model I want to link with (Tag) and the related_name
of “posts”. This means if I have a tag object, I can access all the posts that use it through tag.posts.all()
.
# models.py
class Post(models.Model):
STATUS_CHOICES = [
("draft", "Draft"),
("published", "Published"),
]
title = models.CharField(max_length=255)
slug = AutoSlugField(populate_from="title", unique=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="draft")
body = models.TextField()
created_date = models.DateTimeField(auto_now_add=True)
published_date = models.DateTimeField(blank=True, null=True)
updated_date = models.DateTimeField(blank=True, null=True)
tags = models.ManyToManyField(to=Tag, related_name="posts", blank=True)
3. Migrate
Now that we have made our changes to our models, we need to create and run migrations to update the database. If you’re new to Django and want to build your understanding, I have a beginner’s guide to migrations.
4. Register your Tag model in admin.py
Go into admin.py
and register your Tag model.
# admin.py
from django.contrib import admin
from blog import models
# Register your models here.
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "status"]
admin.site.register(models.Post, PostAdmin)
admin.site.register(models.Tag, admin.ModelAdmin)
5. Add some tags in /admin
Go into /admin
and find your tag model. We need to add some tags so we can start working with them. Rather than build another form to manage tags, we’ll just add them in /admin
for now.
Now, go to your Post model in /admin
and open one of the posts. We now have our new field ‘tags’ in the form:
6. Add tags to the Add Post form
Currently, our add post form looks like this. We want to be able to assign tags to our posts in the form itself.
Our current add post form. The next step is to add tags to this form.
You can go into forms.py
and add “tags” to the fields
attribute of Meta
like this:
# forms.py
from django import forms
from . import models
class PostForm(forms.ModelForm):
class Meta:
model = models.Post
fields = ["title", "body", "tags"]
However, the default widget for a Many to Many field isn’t very user friendly.
What we need to do instead is specify which widget the form should use.
In the code below, we specify the ModelMultipleChoiceField
for tags with the CheckboxSelectMultiple
widget.
The choices for the ModelMultipleChoiceField
are defined in the queryset
attribute. Here, Tag.objects.all()
is sufficient.
If your blog supports multiple users, then you will need to filter the tags by user. To do this, you will to pass the user object to your form, so you can construct your queryset.
This is the result:
We can render checkboxes instead of the default widget using a ModelMultipleChoiceField
7. Display the tags on your posts
Right now, my post detail page looks like this:
I want the tags for the post to be displayed below the title. I want each tag to be a link to a page that lists all the posts that share that tag.
In the template for your post (I’m using “post_detail.html”), add the following to display the tags (I'm using Bootstrap to style my templates):
<div>
{% for tag in post.tags.all %}
<a href="#" class="badge text-decoration-none bg-secondary">
{{tag}}
</a>
{% endfor %}
</div>
Now the tags are shown on the page!
8. Allow users to view posts by tag
The final step is to let users view posts for a particular tag.
a. Add the route to urls.py
Our route in urls.py
must contain something that will identify the exact tag that we will get posts for.
As the tag name is a CharField
, it is not suitable to use in URLs. Instead, I’ve chosen to use the ID of the tag.
Make sure you include “tags” at the front your path. This to prevent conflicts between URLs. For example, I could have used an ID instead of a slug for post URLs. If a user types yourwebsite.com/posts/1
and two URLs use that pattern, then Django is going to use the first URL pattern that matches. This means it will call the post_detail
view and ignore the list_posts_by_tag
view.
# 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")
]
Text-only URLs
If you would rather have the tag name in your URL, then you will need to add a slug field to your Tag model. Every slug must be unique to guarantee that a URL returns the correct tag. There is a package called django-autoslug that will help you do this. I also have a tutorial on using django-autoslug to create URL friendly slugs.
b. Create a new view
Your view will need to identify the tag from the ID supplied in the URL and raise a 404 if no such tag is found.
Once it has found the tag, it needs to filter the Post table for published posts which use that tag.
Finally, you can pass your queryset of posts and the tag name to your template. I am reusing my index.html template and will add some logic to display the correct heading.
This is the code for the view:
# views.py
def list_posts_by_tag(request, tag_id):
tag = get_object_or_404(Tag, id=tag_id)
posts = Post.objects.filter(status="published", tags=tag)
context = {
"tag_name": tag.name,
"posts": posts
}
return render(request, "index.html", context)
c. Create or update a template
I’m reusing my index.html template. I added this snippet to display the correct heading:
# index.html
{% if tag_name %}
<h1>Tag: {{tag_name}}</h1>
{% else %}
<h1>All Posts</h1>
{% endif %}
The result:
d. Add the url to your post detail page
The final step is to update the links we added to the post detail page in Step 7.
# post_detail.html
<div>
{% for tag in post.tags.all %}
<a href="{% url 'tag' tag.id %}" class="badge text-decoration-none bg-secondary">
{{tag}}
</a>
{% endfor %}
</div>
Now the links are clickable!
Conclusion
We can use a ManyToManyField
to add tags our blog posts. This allows us to store our tags in a separate table to our blog posts. This makes it easy to query the database to find which posts to belong to a given tag. This would be much harder if you stored all tags in the Post table.
After adding the Tag model, we updated the form to add a post to allow users to select tags. This involved specifying the form widget as the default widget isn’t very user friendly.
With the form fixed, we updated our templates to render the tags on our post detail page and added a view to display posts belonging to a particular tag.