Update Tags in Django

Considerations in building a custom admin interface for updating tag names.

Background

Consider the following scenario: We have a Django app for publishing content and we use tags to add context and establish relationships amongst related context. Tags, whether you're using a third-party package like "django-taggit" or using your own model, will likely be a many-to-many relationship. That is, a tag may relate to multiple objects and an object may relate to multiple tags. Following the creation of a tag, perhaps we decide there's a better label to describe the relationship. Or perhaps you discovered a spelling error.

We need a simple interface to visualize the tags that we've created and select a tag for updates. Furthermore, we'll add a relationship count so we can see how many times a specific tag has been used.

It's worth mentioning before we jump in, what's described here is not a complete solution. My goal isn't to provide a complete step-by-step on how to build a Django app. Rather, I share a collection of concepts used at various stages in the solutions implementation.

Set Up

We'll use a model "Article" for demonstrations purposes to show how we can engage with the tags that have been created. Additionally, we'll be using the third-party package "django-taggit." Be sure to refer to the package's docs for set up and usage information.

from taggit.managers import TaggableManager
from django.db import Models

class Article(models.Model):
    ...
    tags = TaggableManager(blank=True)

First, we'll create a Django ListView and template to display the tags associated with the above model. Then we'll add update functionality. Let's jump in.

Tag ListView

We want to display a list of the tags associated with "Article" instances. I'm going to use a Django ListView to accomplish this. This view class has built-in functionality for handling querysets, pagination, etc. It's a good solution for what we're looking for.

Given the many-to-many relationship, I want to override the ListView's get_queryset method entirely to enforce distinct and add an annotation. First, I want to call distinct() on the queryset to avoid duplicates. Then, I'm going to annotate the number of "Article" relationships for each tag so we can visualize how many times it's been used.

from taggit.models import Tag
from django.views.generic import ListView
from django.db.models import Count

class TagListView(ListView):
    model = Tag

    def get_queryset(self):
        self.queryset = self.model.objects.all().distinct().annotate(posts=Count('articles'))
        # call super to apply ordering
        return super().get_queryset()

In the above, I'm retrieving all distinct tags while annotating each object with a count of how many relationships to Article exist. When "super()" is called on the method, the upstream ordering logic is applied. 

Once we have our view class working, we can set up a template to display the tags. I'll use a table to display this data and use a row for each instance. In every row, I'll display the tag's name, how many relationships exist to Article, and a link to an update view. For the link, I want to add one layer of complexity: I want to pass the page we're currently on to the update view so that way we can return to the same place we were. I'll add the below snippet following the Django url template tag.

{% if page_obj %}?page={{ page_obj.page }}{% endif %}

Basically, I want to preserve the query string pagination reference.

Tag Update View

For the update view, the only special consideration we have deals with the return/success URL. As mentioned in the

ListView, we want to be able to return to the specific page we were on. For the view's "get_success_url method," we can either build a query string using a python f-string (or similar approach like string format) or alternatively we can use the python package "urllib." I'll show both methods below.

# using urllib

from urllib.parse import urlencode

def get_success_url(self):
    url = reverse("url_pattern_name")
    page = self.request.GET.get("page", None)
    if page:
        url += "?%s" % urlencode(query={"page": page})
    return url

# using f-string

def get_success_url(self):
    url = reverse("url_pattern_name")
    page = self.request.GET.get("page", None)
    if page:
        url += f"?page={page}"
    return url

If you're confident there isn't anything in the existing query string that you wouldn't otherwise not want to preserve, you could just take the entire query string as is with:

self.request.META["QUERY_STRING"]

Note, this will still exclude the leading "?" so be sure to add that in with "'?%s' % query_string" or other method.

Last, we want to access the "page" kwarg from the template so that way we can pass this argument to any back buttons. But let's wrap with an "if" statement so we're only appending the argument if present.

{% if 'page' in request.GET %}?page={{ request.GET.page }}{% endif %}

Final Thoughts

When working with tags, its useful to be able to adjust their labels through a user interface. Especially as the number of references to a specific tag grow, there may be a better label to describe the association. The above is a simple approach to accomplish that.

Details
Published
August 20, 2023
Topics
Tags