Tags can be nested using tag trees for detailed categorisation, with tags having parents, children and siblings.
Tags in tag trees denote parents using the forward slash character (/
). For example, Animal/Mammal/Cat
is a Cat
with a parent of Mammal
and grandparent of Animal
.
To use a slash in a tag name, escape it with a second slash; for example the tag name Animal/Vegetable
can be entered as Animal//Vegetable
.
A custom tag tree model must be a subclass of tagtreemodel
instead of the normal tagmodel
; for automatically-generated tag models, this is managed by setting the option_tree
field option to True
.
Because tree tag names are fully qualified (include all ancestors) and unique, there is no difference to normal tags in how they are set or compared.
A TagTreeModel
subclasses tagmodel
; it inherits all the normal fields and methods, and adds the following:
Note
Field values are computed and set automatically in the save()
method -so don't try to use them until the tag has been saved.
A ForeignKey
to the parent tag. Tagulous sets this automatically when saving, creating missing ancestors as needed.
The reverse relation manager for parent
, eg mytag.children.all()
.
A CharField
containing the name of the tag without its ancestors.
Example: a tag named Animal/Mammal/Cat
has the label Cat
A SlugField
containing the slug for the tag label.
Example: a tag named Animal/Mammal/Cat
has the slug cat
A TextField
containing the path for this tag - this slug, plus all ancestor slugs, separated by the /
character, suitable for use in URLs. Tagulous sets this automatically when saving.
Example: a tag named Animal/Mammal/Cat
has the path animal/mammal/cat
An IntegerField
containing the level of this tag in the tree (starting from 1).
Merge the specified tags into this tag.
tags
can be a queryset, list of tags or tag names, or a tag string.
If children=False
, only the specified tags will be merged; tagged items will be reassigned to this tag, but if there are child tags they will not be touched. If child tags do exist, although the merged tags' counts will be 0, they will not be cleared.
If children=True
, child tags will be merged into children of this tag, retaining structure; eg merging Pet
into Animal
will merge Pet/Mammal
into Animal/Mammal
, Pet/Mammal/Cat
into Animal/Mammal/Cat
etc. Tags will be created if they don't exist.
Returns a queryset of all ancestors, ordered by level.
Returns a queryset of all descendants, ordered by level.
Returns a queryset of all siblings, ordered by name.
This includes the node itself; if you don't want it in the results, exclude it afterwards, eg:
siblings = node.get_siblings().exclude(pk=node.pk)
A TagTreeModelManager
is the standard manager for a tagtreemodel
; it is a subclass of tagmodel_manager
so provides those methods, but its queries return a tagtreemodel_queryset
instead.
Return all tags as a nested list, as lists of (tag, children)
tuples in the format:
[(tag, [child_tuple, ...]), ...]
For example:
[
(level_1_tag, [
(level_2_tag_1, [...]),
(level_2_tag_2, [...]),
]),
(level_1_tag, [...]),
]
Tags will be in alphabetical order.
This is returned by the tagtreemodel_manager
; it is a subclass of tagmodel_queryset
so provides those methods, but also:
Returns a new queryset containing the nodes from the calling queryset, plus their ancestor nodes.
Returns a new queryset containing the nodes from the calling queryset, plus their descendant nodes.
Returns a new queryset containing the nodes from the calling queryset, plus theirm sibling nodes.
When converting from a normal tag model to a tag tree model, you will need to add extra fields. One of those (path
) is a unique field, which means extra steps are needed to build the migration.
These instructions will convert an existing TagModel
to a TagTreeModel
. Look through the code snippets and change the app and model names as required:
Create a data migration to escape the tag names.
You can skip this step if you have been using slashes in normal tags and want them to be converted to nested tree nodes.
Run
manage.py makemigrations myapp --empty
and add:def escape_tag_names(apps, schema_editor): model = apps.get_model('myapp', 'Tagulous_MyModel_Tags') for tag in model.objects.all(): tag.name = tag.name.replace('/', '//') tag.save() operations = RunPython(escape_tag_names)
Create a schema migration to change the model fields. Because paths are not allowed to be null, you need to add the
path
field as a non-unique field, set some unique data on it (such as the object'spk
), and then change the field to add back the unique constraint.To do this reliably on all database types, see Migrations that add unique fields in the official Django documentation.
If you are only working with databases which support transactions, you can use a tagulous helper to add the unique field:
When you create the migration, Django will prompt you for a default value for the unique
path
field; answer with'x'
(do the same for thelabel
field when asked).Change the new migration to use the Tagulous helper to add the
path
field.Add the unique field:
import tagulous.models.migrations ... class Migration(migrations.Migration): # ... rest of Migration as generated operations = [ ... # Leave other operations as they are, just replace AddField: ] + tagulous.models.migration.add_unique_field( model_name='_tagulous_mymodel_tags', name='path', field=models.TextField(unique=True), preserve_default=False, set_fn=lambda obj: setattr(obj, 'path', str(obj.pk)), ) + [ ... ]
Warning
Although
add_unique_column
andadd_unique_field
do work with non-transactional databases, it is not without risk. Seemigrations
for more details.We have changed the abstract base class of the tag model, but Django migrations have no native way to do this. You will need to use the Tagulous helper operation
ChangeModelBases
to do it manually, otherwise future data migrations will think it is aTagModel
, not aTagTreeModel
.Modify the migration from step 2; if you followed the official Django documentation and have several migrations, modify the last one. Add the
ChangeModelBases
to the end of youroperations
list, as the last operation:import tagulous.models.migrations class Migration(migrations.Migration): # ... rest of Migration as generated operations = [ # ... rest of operations tagulous.models.migrations.ChangeModelBases( name='_tagulous_mymodel_tags', bases=(tagulous.models.models.BaseTagTreeModel, models.Model), ) ]
Create another data migration to rebuild the tag model and set the paths:
def rebuild_tag_model(apps, schema_editor): model = apps.get_model('myapp', 'Tagulous_MyModel_Tags') model.objects.rebuild() operations = RunPython(rebuild_tag_model)
If you skipped step 1, this will also create and set parent tags as necessary.
- Run the migrations
You can see a working migration using steps 2 and 3 in the Tagulous tests, for Django migrations <tests/tagulous_tests_migration/django_migrations_expected/0003_tree.py>
.