Example Usage
This section contains code examples of how to set up and use Tagulous. If you’d like a more interactive demonstration, there is a live demo running on the nanodjango playground, or you can download it and run locally with uv run example.py to play with some of these code examples.
Automatic tag models
This simple example creates a SingleTagField (a glorified ForeignKey)
and two TagField (a typical tag field, using ManyToManyField):
from django.db import models
from django_tagulous.models import SingleTagField, TagField
class Person(models.Model):
title = SingleTagField(
label="Your preferred title",
initial="Mr, Mrs, Ms",
)
name = models.CharField(max_length=255)
skills = TagField(
force_lowercase=True,
max_count=5,
)
This will create two new models at runtime to store the tags,
Tagulous_Person_titleandTagulous_Person_skills.These models will act like normal models, and can be managed in the database using Django migrations
Person.titlewill now act as aForeignKeytoTagulous_Person_titlePerson.skillswill now act as aManyToManyFieldtoTagulous_Person_skills
Initial tags need to be loaded into the database with the Loading initial tags management command.
You can use the fields to assign and query values:
# Person.skills.tag_model == Tagulous_Person_skills
# Set tags on an instance with a string
instance = Person()
instance.skills = 'run, "kung fu", jump'
# They're not committed to the database until you save
instance.save()
# Get a list of all tags
tags = Person.skills.tag_model.objects.all()
# Assign a list of tags
instance.skills = ['jump', 'kung fu']
# Tags are readable before saving
# str(instance.skills) == 'jump, "kung fu"'
instance.save()
# Step through the list of instances in the tag model
for skill in instance.skills.all():
do_something(skill)
# Compare tag fields
if instance.skills == other.skills:
return True
Custom models
You can create a tag model manually, and specify it in one or more tag fields:
from django_tagulous.models import TagModel, TagField
class Hobbies(TagModel):
class TagMeta:
# Tag options
initial = "eating, coding, gaming"
force_lowercase = True
autocomplete_view = 'hobbies_autocomplete'
class Person(models.Model):
name = models.CharField(max_length=255)
hobbies = TagField(to=Hobbies)
Options for a custom tag model must be set in TagMeta - you cannot pass them as arguments in tag fields.
See Tag Models to see which field names Tagulous uses internally.
Tag Trees
A tag field can specify tree=True to use slashes in tag names to denote
children:
from django_tagulous.models import TagField
class Person(models.Model):
name = models.CharField(max_length=255)
skills = TagField(
force_lowercase=True,
max_count=5,
tree=True,
)
This can’t be set in the tag model’s TagMeta object; the tag model must
instead subclass django_tagulous.models.TagTreeModel:
from django_tagulous.models import TagField, TagTreeModel
class Hobbies(TagTreeModel):
class TagMeta:
initial = "food/eating, food/cooking, gaming/football"
force_lowercase = True
autocomplete_view = 'hobbies_autocomplete'
class Person(models.Model):
name = models.CharField(max_length=255)
hobbies = TagField(to=Hobbies)
You can add tags as normal, and then query using tree relationships:
person.hobbies = "food/eating/mexican, sport/football"
person.save()
# Get all root nodes: "food", "gaming" and "sport"
root_nodes = Hobbies.objects.filter(parent=None)
# Get the direct children of food: "food/eating", "food/cooking"
food_children = Hobbies.objects.get(name="food").children.all()
# Get all descendants of food:
# "food/eating", "food/eating/mexican", "food/cooking"
food_children = Hobbies.objects.get(name="food").get_descendants()
See Tag Trees to see a full list of available tree methods and properties.
Tag URL
You can set the get_absolute_url tag option to a callable to give tag
objects absolute URLs without needing to create a custom tag model:
from django.db import models
from django.urls import reverse
from django_tagulous.models import TagField
class Person(models.Model):
name = models.CharField(max_length=255)
skills = TagField(
get_absolute_url=lambda tag: reverse(
'by_skill', kwargs={'skill_slug': tag.slug}
),
)
The get_absolute_url method can now be called as normal; for example, from
a template:
{% for skill in person.skills.all %}
<a href="{{ skill.get_absolute_url }}">{{ skill.name }}</a>
{% endfor %}
If you are using a tree, you will want to use the path instead:
skills = TagField(
tree=True,
get_absolute_url=lambda tag: reverse(
'by_skill', kwargs={'skill_path': tag.path}
),
)
See the get_absolute_url option for more details.
Creating the tag view
The get_absolute_url argument only builds the link; you still need to write the view
it points to.
Because a tag field is just a normal relationship, filtering by the tag is a plain Django query. For example, to filter by a skill slug, you can use the following view:
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('skills/<slug:skill_slug>/', views.by_skill, name='by_skill'),
]
# views.py
from django.shortcuts import render
def by_skill(request, skill_slug):
people = Person.objects.filter(skills__slug=skill_slug)
return render(request, 'by_skill.html', {
'skill_slug': skill_slug,
'people': people,
})
See Querying using tag fields for more ways to filter by tag fields.
ModelForms
A ModelForm with tag fields needs no special treatment:
from django.db import models
from django import forms
from django_tagulous.models import TagField
class Person(models.Model):
name = models.CharField(max_length=255)
skills = TagField()
class PersonForm(forms.ModelForm):
class Meta:
fields = ['name', 'skills']
model = Person
They are normal forms so can be used in normal ways; for example, with class-based views:
from django.views.generic.edit import CreateView
class PersonCreate(CreateView):
model = Person
fields = ['name', 'skills']
or with view functions:
def person_create(request, template_name="my_app/person_form.html"):
form = PersonForm(request.POST or None)
if form.is_valid():
form.save()
return redirect('home')
return render(request, template_name, {'form': form})
However, because a TagField is based on a ManyToManyField, if you save
your form using commit=False, you will need to call save_m2m to save
the tags:
from django.conf import settings
from django.db import models
from django import forms
from django_tagulous.models import TagField
class Pet(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
skills = TagField()
class PetForm(forms.ModelForm):
class Meta:
fields = ['owner', 'name', 'skills']
model = Pet
def pet_create(request, template_name="my_app/pet_form.html"):
form = PetForm(request.POST or None)
if form.is_valid():
pet = form.save(commit=False)
pet.owner = request.user
# Next line will save all non M2M fields (including SingleTagField)
pet.save()
# Next line will save any ``TagField`` values
form.save_m2m()
return redirect('home')
return render(request, template_name, {'form': form})
As shown above, this only applies to TagField - a SingleTagField is
based on ForeignKey, so will be saved without needing save_m2m.
See Forms for how to use tag fields in forms.
Forms without models
Tagulous form fields take tag options as a single TagOptions object, rather
than as separate arguments as a model form does:
from django import forms
from django_tagulous.forms import SingleTagField, TagField
from django_tagulous.models import TagOptions
class PersonForm(forms.ModelForm):
title = SingleTagField(
autocomplete_tags=['Mr', 'Mrs', 'Ms']
)
name = forms.CharField(max_length=255)
skills = TagField(
tag_options=TagOptions(
force_lowercase=True,
),
autocomplete_tags=['running', 'jumping', 'judo']
)
A SingleTagField will return a string, and a TagField will return a
list of strings:
form = PersonForm(data={
'title': 'Mx',
'skills': 'Running, judo',
})
assert form.is_valid()
assert form.cleaned_data['title'] == 'Mx'
assert form.cleaned_data['skills'] == ['running', 'judo']
See Forms for how to use tag fields in forms.
Filtering embedded autocomplete
Autocomplete AJAX Views
To use AJAX to populate your autocomplete using JavaScript, set the tag option
autocomplete_view in your models to a value for reverse():
from django_tagulous.models import TagField
class Person(models.Model):
name = models.CharField(max_length=255)
skills = TagField(
autocomplete_view='person_skills_autocomplete'
)
You can then use the default autocomplete views directly in your urls:
from django.urls import path
from django_tagulous.views import autocomplete
from myapp.models import Person
urlpatterns = [
path(
'person/skills/autocomplete/',
autocomplete,
{'tag_model': Person.skills.tag_model},
name='person_skills_autocomplete',
),
]
See Views and Templates for more details.
Filtering an autocomplete view
Add a wrapper function which filters the queryset before it calls the normal
autocomplete view:
from django.contrib.auth.decorators import login_required
from django_tagulous.views import autocomplete
@login_required
def autocomplete_pet_skills(request):
return autocomplete(
request,
Pet.skills.tag_model.objects.filter_or_initial(
pet__owner=request.user
).distinct()
)
Django REST Framework
The Django REST framework’s ModelSerializer will serialize tag fields to their
primary keys; for example:
from rest_framework.serializers import ModelSerializer
class PersonKeySerializer(ModelSerializer):
class Meta:
model = Person
fields = ["name", "title", "skills"]
person = Person.objects.create(name="adam", title="mr", skills="run, jump")
PersonKeySerializer(Person).data == {
"name": "adam",
"title": 1,
"skills": [1, 2],
}
If you’d prefer to serialize to strings, use the Tagulous TagSerializer:
from django_tagulous.contrib.drf import TagSerializer
class PersonStringSerializer(TagSerializer):
class Meta:
model = Person
fields = ["name", "title", "skills"]
person = Person.objects.create(name="adam", title="mr", skills="run, jump")
PersonStringSerializer(Person).data == {
"name": "adam",
"title": "mr",
"skills": ["run", "jump"],
}