Update Tags in Django
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.