Adding the tagging functionality

After implementing your comment system, you will create a way to tag our posts. You will do this by integrating a third-party Django tagging application in our project. The django-taggit module is a reusable application that primarily offers you a Tag model and a manager to easily add tags to any model. You can take a look at its source code at https://github.com/alex/django-taggit.

First, you will need to install django-taggit via pip by running the following command:

pip install django_taggit==0.22.2

Then, open the settings.py file of the mysite project and add taggit to your INSTALLED_APPS setting, as follows:

INSTALLED_APPS = [
# ...
'blog.apps.BlogConfig',
'taggit',
]

Open the models.py file of your blog application and add the TaggableManager manager provided by django-taggit to the Post model using the following code:

from taggit.managers import TaggableManager

class Post(models.Model):
# ...
tags = TaggableManager()

The tags manager will allow you to add, retrieve, and remove tags from Post objects.

Run the following command to create a migration for your model changes:

python manage.py makemigrations blog

You should get the following output:

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 to synchronize your model changes:

python manage.py migrate  

You will see an output indicating that migrations have been applied, as follows:

Applying taggit.0001_initial... OK
Applying taggit.0002_auto_20150616_2121... OK
Applying blog.0003_post_tags... OK

Your database is now ready to use django-taggit models. Let's learn how to use the tags manager. Open the terminal with the python manage.py shell command and enter the following code; first, we will retrieve one of our posts (the one with the 1 ID):

>>> from blog.models import Post
>>> post = Post.objects.get(id=1)

Then, add some tags to it and retrieve its tags to check whether they were successfully added:

>>> post.tags.add('music', 'jazz', 'django')
>>> post.tags.all()
<QuerySet [<Tag: jazz>, <Tag: music>, <Tag: django>]>

Finally, remove a tag and check the list of tags again:

>>> post.tags.remove('django')
>>> post.tags.all()
<QuerySet [<Tag: jazz>, <Tag: music>]>

That was easy, right? Run the python manage.py runserver command to start the development server again and open http://127.0.0.1:8000/admin/taggit/tag/ in your browser. You will see the admin page with the list of Tag objects of the taggit application:

Navigate to http://127.0.0.1:8000/admin/blog/post/ and click on a post to edit it. You will see that posts now include a new Tags field, as follows, where you can easily edit tags:

Now, we will edit our blog posts to display tags. Open the blog/post/list.html template and add the following HTML code below the post title:

<p class="tags">Tags: {{ post.tags.all|join:", " }}</p>

The join template filter works as the Python string join() method to concatenate elements with the given string. Open http://127.0.0.1:8000/blog/ in your browser. You should be able to see the list of tags under each post title:

Now, we will edit our post_list view to let users list all posts tagged with a specific tag. Open the views.py file of your blog application, import the Tag model form django-taggit, and change the post_list view to optionally filter posts by a tag, as follows:

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 in each page
# ...

The post_list view now works as follows:

  1. It takes an optional tag_slug parameter that has a None default value. This parameter will come in the URL.
  2. Inside the view, we build the initial QuerySet, retrieving all published posts, and if there is a given tag slug, we get the Tag object with the given slug using the get_object_or_404() shortcut.
  3. Then, we filter the list of posts by the ones that contain the given tag. Since this is a many-to-many relationship, we have to filter by tags contained in a given list, which, in our case, contains only one element.

Remember that QuerySets are lazy. The QuerySets to retrieve posts will only be evaluated when we loop over the post list when rendering the template.

Finally, modify the render() function at the bottom of the view to pass the tag variable to the template. The view should finally 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 in each page
page = request.GET.get('page')
try:
posts = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer deliver the first page
posts = paginator.page(1)
except EmptyPage:
# If page is out of range deliver last page of results
posts = paginator.page(paginator.num_pages)
return render(request, 'blog/post/list.html', {'page': page,
'posts': posts,
'tag': tag})

Open the urls.py file of your blog application, comment out the class-based PostListView URL pattern, and uncomment the post_list view, like this:

path('', views.post_list, name='post_list'),
# path('', views.PostListView.as_view(), name='post_list'),

Add the following additional URL pattern to list posts by tag:

path('tag/<slug:tag_slug>/',
views.post_list, name='post_list_by_tag'),

As you can see, both patterns point to the same view, but we are naming them differently. The first pattern will call the post_list view without any optional parameters, whereas the second pattern will call the view with the tag_slug parameter. We use a slug path converter for matching the parameter as a lowercase string with ASCII letters or numbers, plus the hyphen and underscore characters.

Since we are using the post_list view, edit the blog/post/list.html template and modify the pagination to use 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 is accessing the blog, they will see the list of all posts. If they filter by posts tagged with a specific tag, they will see the tag that they are filtering by. Now, change the way tags are displayed, as follows:

<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, we loop through all the tags of a post displaying a custom link to the URL to filter posts by that tag. We build the URL with {% url "blog:post_list_by_tag" tag.slug %}, using the name of the URL and the slug tag as its parameter. We separate the tags by commas.

Open http://127.0.0.1:8000/blog/ in your browser and click on any tag link. You will see the list of posts filtered by that tag, like this: