Building a configurable taxonomy in Wagtail (Django)

LB (Ben) Johnston
9 min readFeb 9, 2018

--

Brewing Methods Chart from Rebel Roasters Coffee — Inspiration for our Taxonomy

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.

Business case

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.

Goals

  • 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.

Prerequisites

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.

Code walkthrough

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 path, depth, and numchild field 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 Node model:

  • 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 Node model:

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 __init__ and __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 get_depth and is_root from this API.
  • parent is a field that provides a user interface to select the parent of the node being edited (or created). We have extended the ModelChoiceField class to create a custom BasicNodeChoiceField where 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.
  • instance will 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 parent field 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 get_parent().
  • save needs to be changed to work with the django-treebeard API, as we cannot just create or move Nodes directly.
  • First, we get the Node instance that is attempting to be saved, then we get the value of parent submitted 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 add_root.
  • Creating a Node, but not the root Node, which must be placed as a child under an existing parent Node via add_child on the parent node.
  • Making non-parent changes to any Node is handled by the normal save method.
  • Moving an existing node to a new location under a different parent Node, handled by move(parent, pos='sorted-child').
  • 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 ModelAdmin:

  • model is set to our Node model class.
  • list_display has our name and alias field, along with a method available on the MP_Node class get_parent.
  • inspect_view_enabled means 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.

  • Node now also extends index.Indexed — this provides the ability for this model to be indexed for searching. See also the search_fields definition on the model for the fields we have added to the index.
  • get_as_listing_header is a method that renders a custom template that shows off the 'depth' of our Nodes. We also set the short_description and admin_order_field attributes on this method, used by modeladmin to show a nice column header.
  • get_parent is just the same method provided by MP_node. However, we need to re-declare it on the model to set the short_description used by modeladmin.
  • delete method 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 Node is not a friendly name for our team. We have elected to use Topic instead. modeladmin will also honor this reference and automatically use it in the admin interface.

Here is the template used by our get_as_listing_header method.

Then we need to update the definition of our NodeAdmin to take advantage of our pretty get_as_listing_headermethod.

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.

Walkthrough:

  • NodeButtonHelper has 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.
  • AddChildNodeViewClass extends the CreateView class. 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_object_or_404.
  • get_page_title gives the user a nicer title on the create page, relevant to the parent they selected.
  • get_initial sets the initial values for our NodeForm. No changes are needed to NodeForm for 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).

In closing

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.

You can view the full models.py file on a GitHub gist. There are a few minor additions and tweaks based on the project I based this blog on.

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.

Screenshots from Wagtail’s admin — interacting with our new Topics.

--

--

LB (Ben) Johnston

LB (pronounced ‘Albi’), love coffee, systems, travel, technology and also coffee. Incredibly thankful for my amazing wife Bec and baby Leo.