Django
Blog for Poems in Django Part III

Blog for Poems in Django Part III

In this blog post, we will learn how to create tags for a post using django-taggit. Then we will learn to retrieve posts by similarity.We will also learn to generate latest posts and most commented posts. At the end we will learn to use markdown in Django.

Table of Contents

Adding the tagging functionality

A very common functionality in blogs is to categorize posts using tags. Tags allow you to categorize content in a non-hierarchical manner, using simple keywords. A tag is simply a label or keyboard that can be assigned to posts. We will create a tagging system by integrating a third-party Django tagging application into the project.

djanog-taggit is a reusable application that primarily offers you a Tag model and a manager to easily add tags to any model.

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

#terminal
pip install django-taggit==3.0.0

Open settings.py file and add taggit to our INSTALLED_APPS setting.

...................................................
ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
    'taggit',
]

.............................................

Open the blog/models.py file of our blog application and add 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 us to add, retrieve and remove tags form Post objects.

Make migrations

#terminal 
python manage.py makemigrations blog
python manage.py migrate

Now, open the Django shell by running the following command in terminal.

#terminal
python manage.py shell

Run the following code to retrieve on of the posts (the one with 1 ID)

from blog.models import Post
post = Post.objects.get(id=1)
post.tags.add("river","poem","nature")
post.tags.all()

Press Ctrl+d and now run the server and visit http://127.0.0.1:8000/admin/

Click on Posts to see list of posts.

Now, click on first post. Here we can see tags.

Open the blog/post/list.html template and add the following HTML code.

{% extends "blog/base.html" %}
{% block title %}My Blog {% endblock %}

{% block content %}
<h1>My Blog</h1>
    {% for post in posts %}
        <h2>
            <a href="{{ post.get_absolute_url}}">
                {{ post.title }}
            </a>
        </h2>
        <p class="tags">Tags: {{ post.tags.all|join: ","}}</p>
        <p class="date">
            Published {{ post.publish }} by {{ post.author }}
        </p>
        {{ post.body|truncatewords:30|linebreaks }}
    {% endfor %}
{% include "pagination.html" with page=page_obj %}
{% endblock %}

Visit http://127.0.0.1:8000/blog/

Open the blog/views.py and add the following lines of code to let users list all posts tagged with a specific tag.

.............................................
from taggit.models import Tag


# Create your views here.
class TaggedPostsListView(ListView):
    template_name = "blog/post/tagged_posts_list.html"
    context_object_name = "tagged_posts"

    def get_queryset(self):
        tag_slug = self.kwargs['tag_slug']
        tag = get_object_or_404(Tag,slug=tag_slug)
        return Post.objects.filter(tags=tag)
...............................................

Now, create blog/post/tagged_posts_lists.html

{% extends "blog/base.html" %}
{% block title %}My Blog {% endblock %}

{% block content %}
<h1>My Blog</h1>
    {% for post in tagged_posts %}
        <h2>
            <a href="{{ post.get_absolute_url}}">
                {{ post.title }}
            </a>

        </h2>
       <p class="tags">Tags: {{ post.tags.all|join:","}}</p>
        {{ post.body|truncatewords:30|linebreaks }}
    {% endfor %}
{% include "pagination.html" with page=page_obj %}
{% endblock %}

Now, add the url in blog/urls.py

from django.urls import path
from . import views

app_name = "blog"

urlpatterns = [
    #post views
    
    path("",views.PostListView.as_view(),name="post_list"),
    path("<int:year>/<int:month>/<int:day>/<slug:post>",views.post_detail,name="post_detail"),
    path("<int:post_id>/comment/",views.post_comment,name='post_comment'),
    path("tagged/<slug:tag_slug>/",views.TaggedPostsListView.as_view(),name="tagged-posts"),

]

For the sake of demonstration I have added “nature” and “poem” tags to different poems.

Visit http://127.0.0.1:8000/blog/tagged/nature/

Visit http://127.0.0.1:8000/blog/tagged/poem/

Retrieving posts by similarity

Tags allow us to categorize posts in a non-hierarchical manner. Posts about similar topics will have several tags in common. We will build a functionality to display similar posts by the number of tags they share. In this way, when the user reads a post, we can suggest to them that they read other related posts.

In order to retrieve similar posts for a specific post, we need to perform the following steps:

  • Retrieve all tags for the current post
  • Get all posts the are tagged with any of those tags
  • Exclude the current post from that list to avoid recommending the same post
  • Order the results by the number of tags shared with the current post
  • In the case of two or more posts with the same number of tags, recommend the most recent post.
  • Limit the query to the number of posts we want to recommend.

Edit the bolb/views.py as follows:

from typing import Any
from django.db.models.query import QuerySet
from django.shortcuts import render,get_object_or_404
from django.core.paginator import Paginator,EmptyPage
from django.views.generic import ListView
from django.views.decorators.http import require_POST

from django.db.models import Count

from .models import Post
from .forms import CommentForm

from taggit.models import Tag

.........................................................................



def post_detail(request,year,month,day,post):
    post = get_object_or_404(Post,
                             status=Post.Status.PUBLISHED,
                             slug = post,
                             publish__year =year,
                             publish__month=month,
                             publish__day=day)
    print("***************",post)

    #List of active comments for this post
    comments = post.comments.filter(active=True)
    #form for users to comment
    form = CommentForm()

    #List of similar posts
    post_tags_ids = post.tags.values_list('id',flat=True)
    similar_posts = Post.published.filter(tags__in=post_tags_ids).exclude(id=post.id)
    similar_posts = similar_posts.annotate(same_tags=Count('tags')).order_by('-same_tags','-publish')[:4]



    
    return render(request,'blog/post/detail.html',{'post':post,
                                                   'comments':comments,
                                                   'form':form,
                                                   'similar_posts':similar_posts})

......................................................................

Now edit the detail.html as follows.

{% extends "blog/base.html" %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
<h1>
    {{post.title}}
</h1>
<p class="date">
    Published {{ post.publish }} by {{ post.author }}
</p>
{{ post.body|linebreaks }}
<h2>Similar Posts</h2>
    {% for post in similar_posts %}
        <p>
            <a href="{{ post.get_absolute_url }}">
                {{ post.title }}
            </a>
        </p>
        {% empty %}
        There are no similar posts yet.
    {% endfor %}

...............................................................

Creating custom template tags and filters

Django allows us to create our own template tags to perform custom actions. Custom template tags come in very handy when we need to add a functionality to our templates that is not covered by the core set of Django template tags. This can be a tag to execute a QuerySet or any server-side processing that we want to reuse across templates.

Creating a simple template tag

Create a directory named templatetags inside the blog application. Within that create __init__.py and blog_tags.py.

Now, add the following line of codes in blog_tags.py to create a simple tag to retrieve the total posts that have been published on the blog.

from django import template
from ..models import Post

register = template.Library()

@register.simple_tag
def total_posts():
    return Post.published.count()

We have created a simple template tag that returns the number of posts published in the blog. Each module that contains template tags needs to define a variable called register to be a valid tag library. This variable is an instance to template.Library and it’s used to register the template tags and filters of the application.

In above code, we have defined a tag called total_posts with a simple Python function. We have added the @register.simple_tag decorator to the function, to register it as a simple tag. Django will use the function’s name as the tag name. If we want to register it using different name, we can do so by specifying a name attribute, such as @register.simple_tag(name=’my_tag’).

After adding a new template tags module, we need to restart the Django development server in order to use the new tags and filters in templates.

Before using custom template tags, we have to make them available for the template using the {% load %} tag. As mentioned before, we need to use the name of the Python module containing your template tags and filters.

Edit the blog/templates/base.html as follow.

{% load blog_tags %}
{% load static %}
<!DOCTYPE html>
<html>
    <head>
        <title>
            {% block title %}{% endblock %}
        </title>
        <link href="{% static "css/blog.css"%}" rel="stylesheet">

    </head>
    <body>
        <div id="content">
            {% block content %}
            {% endblock %}
        </div>
        <p>
            There are total {% total_posts %} posts so far.
        </p>

       
    </body>
</html>

Restart the server and visit:

Creating an inclusion template tag

We will create another tag to display the latest posts in the sidebar of the blog. This time, we will implement an inclusion tag. Using an inclusion tag, we can render a template with context variables returned by out template tags.

Edit the templatetags/blog_tags.py and add the following code:

@register.inclusion_tag('blog/post/latest_posts.html')
def show_latest_posts(count=5):
    latest_posts = Post.published.order_by('-publish')[:count]
    return {'latest_posts':latest_posts}

In above code, we have registered the template tag using the @register.inclusion_tag decorator. We have specified the template that wil be rendered with the returned values using blog/post/latest_posts.html. The template tag will accept an optional count parameter that defaults to 5.

The function returns a dictionary of variables instead of a simple value. Inclusion tags have to return a dictionary of values, which is used as the context to render the specified template. The template tag we just created allows us to specify the optional number of posts to display as {% show_latest_posts 3 %}

Now, create blog/post/latest_posts.html and add the following code.

<ul>
    {% for post in latest_posts %}
    <li>
        <a href="{{ post.get_absolute_url }}">
            {{ post.title }}
        </a>
    </li>
    {% endfor %}
</ul>

Now, add the following lines of code in base.html.

{% load blog_tags %}
{% load static %}
<!DOCTYPE html>
<html>
    <head>
        <title>
            {% block title %}{% endblock %}
        </title>
        <link href="{% static "css/blog.css"%}" rel="stylesheet">

    </head>
    <body>
        <div id="content">
            {% block content %}
            {% endblock %}
        </div>
        <div id="sidebar">
            <p>
                There are total {% total_posts %} posts so far.
            </p>
            <h3>
                latest_posts
            </h3>
            {% show_latest_posts %}
    

        </div>
        
       
    </body>
</html>

Create a template tag that returns a QuerySet

We will create a simple template tag that returns a value. We will store the result in a variable that can be reused, rather that outputting it directly. We will create a tag to display the most commented posts.

Edit the templatetags/blog_tags.py file and add the following import and template tag to it.

from django import template
from django.db.models import Count

from ..models import Post

register = template.Library()

@register.simple_tag
def total_posts():
    return Post.published.count()

@register.inclusion_tag('blog/post/latest_posts.html')
def show_latest_posts(count=5):
    latest_posts = Post.published.order_by('-publish')[:count]
    return {'latest_posts':latest_posts}

@register.simple_tag
def get_most_commented_posts(count=5):
    return Post.published.annotate(
        total_comments = Count('comments')
    ).order_by('-total_comments')[:count]

In the above template tag, we built a QuerySet using the annotate() function to aggregate the total number of comments for each post. We use the Count aggregation function to store the number of comments in teh computed total_comments field for each Post object. We ordered the QuerySet by the computed field in descending order.

Edit the blog/base.html as follows.

{% load blog_tags %}
{% load static %}
<!DOCTYPE html>
<html>
    <head>
        <title>
            {% block title %}{% endblock %}
        </title>
        <link href="{% static "css/blog.css"%}" rel="stylesheet">

    </head>
    <body>
        <div id="content">
            {% block content %}
            {% endblock %}
        </div>
        <div id="sidebar">
            <p>
                There are total {% total_posts %} posts so far.
            </p>
            <h3>
                latest_posts
            </h3>
            {% show_latest_posts %}
            <h3>Most commented posts </h3>
            {% get_most_commented_posts as most_commented_posts %}
            <ul>
                {% for post in most_commented_posts %}
                <li>
                    <a href="{{ post.get_absolute_url }}">
                        {{ post.title }}
                    </a>
                </li>
                {% endfor %}
    

        </div>
        
       
    </body>
</html>

Markdown

Markdown is a plain text formatting syntax that is very simple to use, and it’s intended to be converted into HTML. We can write posts using simple Markdown syntax and get the context automatically converted into HTML code.

#terminal
pip install markdown==3.4.1

Then, edit the templatetags/blog_tags.py file and include the following code.

from django import template
from django.db.models import Count
from django.utils.safestring import mark_safe

import markdown

from ..models import Post

............................
@register.filter(name='markdown')
def markdown_format(text):
    return mark_safe(markdown.markdown(text))

We register template filters in the same way as template tags. To prevent a name clash between the function name and the markdown module, we have named the function markdown_format and we have named the filter markdown for use in templates, such as {{ variables|markdown }}.

Edit the blog/post/detail.html template and add the following new code highlighted in bold.

{% extends "blog/base.html" %}
{% load blog_tags %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
<h1>
    {{post.title}}
</h1>
<p class="date">
    Published {{ post.publish }} by {{ post.author }}
</p>
{{ post.body|markdown }}
<h2>Similar Posts</h2>
..........................................................

Edit the blog/post/list.html template and add the following code.

{% extends "blog/base.html" %}
{% load blog_tags %}
{% block title %}My Blog {% endblock %}

{% block content %}
<h1>My Blog</h1>
    {% for post in posts %}
        <h2>
            <a href="{{ post.get_absolute_url}}">
                {{ post.title }}
            </a>
        </h2>
        <p class="tags">Tags: {{ post.tags.all|join:","}}</p>
        <p class="date">
            Published {{ post.publish }} by {{ post.author }}
        </p>
        {{ post.body|markdown|truncatewords:30 }}
    {% endfor %}
{% include "pagination.html" with page=page_obj %}
{% endblock %}

Now visit http://127.0.0.1:8000/admin/blog/post/add and create a new post.

Inside the body add the following code.

In the realm of *battles*, a warlord stands tall,
In battles waged, their valor **blooms** bright,

* This is the first list item.
* Here's the second list item.
    > A blockquote would look great below the second list item.
* And here's the third list item.

and the [link](https://twitter.com/aisolopreneur)

        <html>
          <head>
            <title>Test</title>
          </head>

That was code