How to implement a tagging system in Flask

You have probably noticed that each article on this blog contains several tags, and these tags are hyperlinks so that when you click them, only articles containing the particular tag appears. Although it is very straightforward, at first I just could not figure out how to do it.

It took me a while to find out how to implement this. When I first tried, I just did not work, so I started to believe that I would need some sort of one-to-many mappings in my database or that I would need to write some strange logic for templating. None of it is needed.

Let us see how it is done. I have this model for a post.

class PostModel(db.Model):
""" Data model for post """
__tablename__ = "posts"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(128), unique=False, nullable=False)
body = db.Column(db.Text, unique=False, nullable=False)
tags = db.Column(db.Text, unique=False, nullable=True)
time_posted = db.Column(db.DateTime, unique=False, nullable=False)

Tags are stores simply as a string, which is separated by commas. In routes.py file where the logic is, I have this function

@app.route("/tag/<string:tag>")
def tagged_posts(tag:str):
    """ Route for seeing posts that have particular tag """
    posts = PostModel.query.filter(PostModel.tags.contains(tag)).all()
   return render_template("tags.html", posts=posts)

This function is connected to a URL with the format <sometag> which is a standard. The only this function does is, that it queries the database and looks for other posts containing the substring, then it renders a template and sends the queried results.

In my tags.html template, this is the code that renders all the posts with tags

{% for post in posts.items %}
    <article class="mycontainer">
        <h3>{{ post.title }}</h3>
        <p>{{ post.body | safe | truncate(250) }}</p>
            {% for tag in post.tags.split(",") %}
            {% if tags != None %}
                    <a href={{url_for('tagged_posts', tag=tag.strip()) }} "><div class="float-left tags">{{ tag }}&nbsp</div></a> 
            {% endif  %}        
            {% endfor %}
        <div class="float-right">
            <a href="{{ url_for('post', post_id = post.id) }}"><button class="uk-button uk-button-small">Read More</button></a>
        </div>
        <br>
    </article>
<br>
{% endfor %}

The important part concerning tags is in the middle.

    {% for tag in post.tags.split(",") %}
    {% if tags != None %}
          <a href={{url_for('tagged_posts', tag=tag.strip()) }} "><div class="float-left tags">{{ tag }}&nbsp</div></a> 
    {% endif  %}        
    {% endfor %}

It is actually quite interesting that within jinja you can run python function like .split() or .strip() which is exactly what I did here. I have to say I had quite a hard time making tags separated by whitespace because by default jinja strips them off. I have tried many ways described in jinja documentation or on stack overflow, but none of them did the work.

After half-day of searching how to make it work, I found out an elegant and simple way. This HTML entity called 'nonbreaking whitespace': &nbsp. I have never used it before. Here it made the job when appended to the tag. This was the only way to make a space between the tags that worked for me.

Another interesting part was actually the url_for function. This took me a while because at first, I did not know how to let jinja know, which tag was clicked (and thus which URL should be built). The tags are stored in one column as a list, and I did not know how to tell jinja which of them was clicked. I started to believe I would have to write some JS code for frontend with an onclick event etc.

But it is as simple as that. The pattern is the same as when clicking a button for reading a selected post. When there is a loop, jinja will automatically know which element of the list was clicked, so the URL for it can be constructed this easy way.

The strip() function is there just to remove the whitespace, it would work even without it, the browser would generate %20 char and inserts in the place of whitespace in the URL.

So in a nutshell, this is all how to implement a tagging system. The same logic as is in tags.html is implemented in posts.html template of course since that is the template that is rendered as a homepage