Updated 2018–02–17: Added brewing methods chart + screenshots of final UI.
This article aims to present a way to implement a fully flexible taxonomy system inside your Django app. The editing implementation will rely heavily on the use of Wagtail (a CMS built on Django) but will still be relevant if only Django is used. This is a repost from my original article on Codementor.
The case for a taxonomy can be broad — you may be developing a blog, are bored, and really want a complicated way to tag posts. Alternatively, you may be working with a knowledge management system and need to provide a structured system to manage hierarchal categorization of your team’s information.
Either way, it is important to understand your goals before writing a single line of code. Or at least write some code, get frustrated, and then come back to think about what you are trying to do.
- Build a flexible system to manage a nested (tree shaped) taxonomy.
- We must be able to go arbitrarily deep.
- We must be able to add the canonical (correct) terms but also have space to provide and search via the non-correct terms (such as abbreviations).
- We need to minimize dependencies and stay as close to Django conventions as possible (for future maintainability).
- Avoid any difficult to understand terms in the user interface (e.g. taxonomy).
What is a business taxonomy?
Glad you asked! Think of a taxonomy as a globally shared vocabulary for the business or organization. This vocabulary is often used throughout all documentation, categorization, and training, but never really written down in one place.
Taxonomies help organize content and knowledge into hierarchical relationships, adding detail to terms and concepts the further you go down the levels.
These two links add a bit more context:
Wearing the right hat
When I worked on a similar project for a client, one thing I found hard was switching between the right hats.
One hat was the business analyst, a.k.a, the guy who needs to translate what the boss has asked for. With this hat on, I found that there were legitimate concerns over how the company’s information could be managed, searchable, and categorized to help add value to the organization as a whole.
The next hat was that of the developer. Here, I had to work with existing code and frameworks to implement a complex solution quickly and simply, along with consideration for future development wherever possible.
Finally, the hat that matters in the long run — the one of the everyday user. It was this hat I often found the hardest to don after wearing the others for a long time.
The concepts, both abstract and data model side, made sense to me and it felt like everyone else would get on board easily. In reality, I had to remember that I had been thinking and brainstorming this project for a long time and had the chance to really internalize the goals and way to think.
In the end, we landed on a great single sentence that helped our end users grok the concept of our ‘taxonomy’. We also ditched the name taxonomy all together and used a more friendly internally relevant terminology instead.
Installation of Wagtail 2.0. As of publication, this is still a release candidate but is solid enough to use.
We will be using Django 2.0 and all Python 3.5+ syntax (because it is awesome!).
Finally, we will be taking advantage of an incredible Python project called django-treebeard. I first found out about this project in depth after working with Wagtail for a while.
Essentially, this library takes all of the heavy lifting of managing a nested tree model inside a standard relational database. It is projects like this that get me excited about the power of Python and also the way Django can be extended. Shout out to @tabo for this epic project.
Note: If you have Wagtail up and running, you will not need to install django-treebeard. For a raw Django project, you will need to install the package.
1 — the ‘Node’ model
Naming this is hard. For now, we will just call our elements inside the taxonomy a ‘node’. Our nodes will extend the django-treebeard project’s Materialized Path Tree nodes, described as follows:
- Each node has one single path in the tree (think URL paths).
- There must be one single root node that all other nodes connect to.
- Nodes can be ordered in relation to their siblings. Initially, we will just order them by their name, the field.
- Nodes have a
numchildfield whose values should not be changed directly.
- The default set up can have a depth of 63, which I am sure will be sufficient for our use case.
We will be adding our own fields to the
name- a CharField that represents the canonical name of the Node.
aliases- a TextField where each line represents another potential name or abbreviation for the Node.
node_order_index- an IntegerField which can be used in the future if we want to implement custom ordering in the user interface.
Here is our initial model definition for the
After you have this model declared, you will want to run migrations in your console:
$ python3 ./manage.py makemigrations
$ python3 ./manage.py migrate
2 — The form
For the sake of simplicity, we will assume all of the code will go in the same
models.py file. In practice, you would be best served splitting up into separate files, but it is easier to get up and running with everything in one place.
We will be using the Wagtail system of building forms, but you can apply the main
__save__ overrides to any Django form or even Django modeladmin.
Key items to note:
- The djang-treebeard node API reference will come in handy here, we will be using methods like
is_rootfrom this API.
parentis a field that provides a user interface to select the parent of the node being edited (or created). We have extended the
ModelChoiceFieldclass to create a custom
BasicNodeChoiceFieldwhere we can get a nice indication of the Node structure in our select box.
__init__on our form has been modified to do a few things.
instancewill be an instance of Node bound to the values provided when the form submits, when creating or editing a Node.
- If we are editing the root node (
instance.is_root()) or creating the first node (
Node.objects.count() is 0) we want to ensure that the
parentfield is hidden and will not throw an error if not filled out.
- If we are editing an existing node we want to pre-select the node’s parent via
saveneeds to be changed to work with the
django-treebeardAPI, as we cannot just create or move Nodes directly.
- First, we get the Node
instancethat is attempting to be saved, then we get the value of
parentsubmitted with the form (which will be None for the root Node).
- If we are not committing changes on this save call, we can simply return the instance provided.
- Otherwise, we want to handle the following cases:
- Creating the first Node, which will become the root Node, handled by the classmethod
- Creating a Node, but not the root Node, which must be placed as a child under an existing parent Node via
add_childon the parent node.
- Making non-parent changes to any Node is handled by the normal
- Moving an existing node to a new location under a different parent Node, handled by
- Finally, we tell Wagtail to use this form class when editing the Node model via
Node.base_form_class = NodeForm.
3 — Wagtail modeladmin editing
We will now use the Wagtail modeladmin module. This is a powerful way to add CRUD operations to our models in the admin interface. It is similar (in concept) to Django’s modeladmin, but not the same. It also makes extensive use of the awesome Class-based views.
Note: The Class-based views provide a great way to add functionality to Django without reinventing the wheel. They are easy to customize and provide a great API that is easy to extend and gives you a great example of a structure for view classes.
We will be declaring a new class that will extend
modelis set to our
aliasfield, along with a method available on the
inspect_view_enabledmeans that the users can click on a simple view page to look at details but not edit anything on the Node.
We will then register our custom
ModelAdmin in a new file called
wagtail_hooks.py. This is a special file name convention that Wagtail will ensure runs before the admin interface is prepared.
4 — Node model enhancements
For round two of our model definition, we will add some nice helper methods to be used later.
Nodenow also extends
index.Indexed— this provides the ability for this model to be indexed for searching. See also the
search_fieldsdefinition on the model for the fields we have added to the index.
get_as_listing_headeris a method that renders a custom template that shows off the 'depth' of our Nodes. We also set the
admin_order_fieldattributes on this method, used by
modeladminto show a nice column header.
get_parentis just the same method provided by
MP_node. However, we need to re-declare it on the model to set the
deletemethod is overridden to block the deletion of the root Node. This is really important — if it is deleted, the node tree will be corrupted and chaos will enter the ancient forest.
__str__magic method is used to show a nice string representation of our Nodes.
- Finally, we have decided that
Nodeis not a friendly name for our team. We have elected to use
modeladminwill also honor this reference and automatically use it in the admin interface.
Here is the template used by our
Then we need to update the definition of our
NodeAdmin to take advantage of our pretty
5 — Finishing up
We can now add a relation to our Nodes on any of our other models, where appropriate.
We can add a many-to-one relationship using ForeignKey.
We can add a many-to-many relationship using ManyToManyField.
We now have an interface to manage our taxonomy, along with a way to link the nodes to any other model within Django.
Bonus points — Adding icing on the root Node
Hide delete button on root Node
It is nice to not show buttons that users are not meant to use. Thankfully,
modeladmin makes it easy to override how the buttons for each row are generated.
Add button to quickly add a child node
This is a bit more involved, but worth it to understand how to work with class-based views and modeladmin in depth.
NodeButtonHelperhas a few changes to essentially create and insert a new button,
add_child_button, which will provide a simple way to pre-fill the parent field on a create Node view.
CreateViewclass. Here, we do a few things:
__init__gets the pk (primary key) from the request and checks it is valid via the prepared queryset and
get_page_titlegives the user a nicer title on the create page, relevant to the parent they selected.
get_initialsets the initial values for our
NodeForm. No changes are needed to
NodeFormfor this to work.
- Inside our
NodeAdmin, we override two methods:
add_child_view— this gives the modeladmin module a reference to a view to assign to the relevant URL.
get_admin_urls_for_registration— this adds our new URL for the above view to the registration process (Wagtail admin requires all admin URL patterns to be registered a specific way).
I really hope this has been helpful from both the technical and ‘thinking it through’ perspective.
There is a lot of room for improvement in this implementation, but this is a solid starting point. From here, you can build your own amazing taxonomy systems in every application… that needs it.
In my working example I fleshed out the Coffee brewing methods, if we think of each coffee ‘drink’ as the entity that contains links to how it was brewed, we end up with a fun taxonomy example. Below are a few screenshots of how this would look in Wagtail.