After creating the comment system, it’s time to implement tags for posts. Let’s do this by integrating a third-party Django application. The django-taggit
module is an application consisting of a Tag
model and a manager for adding tags to any model. Here’s its source code: https://github.com/jazzband/django-taggit. First you need to install django-taggit
with pip
using the following command:
pip install django_taggit==0.22.2
Then open the settings.py
file of the mysite
project and add taggit
to the INSTALLED_APPS
setting:
INSTALLED_APPS = [
# ..
'blog.apps.BlogConfig',
'taggit',
]
Open the models.py
file of the blog
application and add the TaggableManager
from django-taggit
to the Post
model using the following code:
from taggit.managers import TaggableManager
class Post(models.Model):
# ...
tags = TaggableManager()
The tags
manager allows you to add, remove, and retrieve tags from Post
objects. Use the following command to create a migration for model changes:
python manage.py makemigrations blog
The following output should appear:
Migrations for 'blog':
blog\migrations\0003_post_tags.py
- Add field tags to post
Now run the following command to create the required database tables for django-taggit
models and synchronize model changes:
python manage.py migrate
A output will appear confirming the migrations applied:
Applying taggit.0001_initial... OK
Applying taggit.0002_auto_20150616_2121... OK
Applying blog.0003_post_tags... OK
The database is now ready to use django-taggit
models. But first we need to figure out how the tags
manager works. Open a terminal with the python manage.py shell
command and type the following code. The first thing to do is to get one of the posts (with ID 1
):
>>> from blog.models import Post
>>> post = Post.objects.get(id=1)
Then add some tags and try to return them to see if they were added:
>>> post.tags.add('music', 'jazz', 'django')
>>> post.tags.all()
<QuerySet[<Tag: jazz>, <Tag: music>, <Tag: django>]>
Finally, remove them and check the list again:
>>> post.tags.remove('django')
>>> post.tags.all()
<QuerySet[<Tag: jazz>, <Tag: music>]>
That was easy, wasn’t it? Run the python manage.py runserver
command to start the development server and open https://127.0.0.1:8000/admin/taggit/tag in your browser. This will display an administrative page with a list of Tag
application Taggit
objects: Go to https://127.0.0.1:8000/admin/blog/post/ and click on the post to edit it. The posts now include a Tags field that you can use to easily edit them:
Let’s edit blog posts to display tags. Open the
blog/post/list.html
template and add the following HTML code under the post title:
<p class="tags">Tags {{ post.tags.all|join:", " }}</p>
The join
template filter works in the same way as the join()
string method to combine elements with the selected string. Open https://127.0.0.1:8000/blog/ in your browser. A list of tags will now appear below each post title: Let’s edit the
post_list
view so that users can see all the posts associated with a particular tag. Open the views.py
file of the blog
application, import the Tag
model from django-taggit
and change the post_list
view to optionally filter posts by tag:
from taggit.models import Tag
def post_list(request, tag_slug=None)
object_list = post.published.all()
tag = None
if tag_slug
tag = get_object_or_404(Tag, slug=tag_slug)
object_list = object_list.filter(tags__in=[tag])
paginator = Paginator(object_list, 3) # 3 posts per page
# ...
The post_list
view works as follows:
- It takes the optional
tag_slug
parameter with default valueNone
. It will be in the URL. - The view creates the first QuerySet, which gets all the published posts. If
slug
is specified, theTag
object can be retrieved through it withget_object_or_404()
. - The list is then filtered so that only those that include the tag remain. This is a many-to-many relationship, so filter by the tags in the list, which in this case contains only one item.
It’s worth remembering that QuerySets are lazy. They will only be executed when iterating over the post_list when rendering the template. Finally, you need to modify the render()
function at the bottom of the view to pass the tag
tag to the template. As a result, the view will look like this:
def post_list(request, tag_slug=None)
object_list = post.published.all()
tag = None
if tag_slug
tag = get_object_or_404(Tag, slug=tag_slug)
object_list = object_list.filter(tags__in=[tag])
paginator = Paginator(object_list, 3) # 3 posts on each page
page = request.GET.get('page')
try
posts = paginator.page(page)
except PageNotAnInteger
# If the page is not an integer, put the first page
posts = paginator.page(1)
except EmptyPage
# If the page is bigger than the maximum, deliver the last page of the results
posts = paginator.page(paginator.num_pages)
return return(request,
'blog/post/list.html',
{'page': page,
{ 'posts': posts,
{ 'tag': tag})
Open the blog
application’s urls.py
file, comment out the PostListView
URL template based on the class, and uncomment the post_list
view:
path('', views.post_list, name='post_list'),
# path('', views.PostListView.as_view(), name='post_list'),
Add the following additional URL template to list posts by tag:
path(''tag/<slug:tag_slug>/',
views.post_list, name='post_list_by_tag'),
Both templates point to the same view, but they are called differently. The first will call the post_list
view with no additional parameters, while the second uses tag_slug
. Here a slug
path converter is used to map the parameter as a lowercase string consisting of ASCII characters, hyphens, and underscores. Because the post_list
view is used, you need to edit the blog/post/list.html
template and change the pagination so that it uses the posts
object:
{% include ".../pagination.html" with page=posts %}
Add the following lines above the {% for %}
loop:
{% if tag %}
<h2>Posts tagged with "{{ tag.name }}"</h2>
{% endif %}
If the user visits the blog, he will see a list of posts. If he tries to filter content by a specific tag, the tag he is filtering by. Change the way the tags are displayed:
<p class="tags">
Tags
{% for tag in post.tags.all %}
<a href="{% url "blog:post_list_by_tag" tag.slug %}">
{{ tag.name }}
</a>
{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
Now go through all the post tags, displaying a custom link in the URL to filter posts by that tag. The URL will be built with {% url "blog:post_list_by_tag" tag.slug %}
with URL and slug
as parameters. The tags are separated by commas. Open https://127.0.0.1:8000/blog/ in your browser and click on the tag link. A list of posts with that tag will appear: