IAN WALDRON IAN WALDRON

Update Tags in Django

Considerations in building an interface for updating tag names.
August 20, 2023

Background

Consider the following scenario: We have a Django app that for publishing content and we use tags to describe relationships between content. 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 module. Rather, I share a collection of concepts used at various stages in the solutions implementation.

Set Up

We'll use the follow model "Article" with some unknown number of fields to demonstrate how we can engage with the tags we've created for this model. 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 list view and template to display the tags associated with the above model. Then we'll add update functionality. Let's jump in.

Tag List View

We want to display a list of the tags associated with Articles. I'm going to use 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 has 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):
        queryset = self.model.objects.all().distinct().annotate(articles=Count('article'))
        ordering = self.get_ordering()
        if ordering:
            if isinstance(ordering, str):
                ordering = (ordering,)
            queryset = queryset.order_by(*ordering)
        return queryset

In the above, I'm retrieving all distinct tags while annotating each object with a count of how many relationships to Article exist. I copied the ordering language over from the native method. Since the get_queryset method doesn't accept a queryset as a parameter, we can't super the method directly while preserving our queryset data already retrieved. The native method does, however, check to see if the class attribute "self.queryset" as already set and will use that value, if so. With that in mind, you could alternatively use:


class TagListView(ListView):
    model = Tag

    def get_queryset(self):
        self.queryset = self.model.objects.all().distinct().annotate(articles=Count('article'))
        return super().get_queryset()

I like seeing the ordering logic explicitly. But DRY principles would likely suggest calling super() on the method.

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 list view, 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 just this.