Django-treebard and Wagtail page creation
Wagtail is an open source CMS written in Python and built on the Django web framework. One of the most interesting features made available by Django is the possibility of creating data programmatically through the data migrations. This feature, in a wagtail based project, means that it should be possible to create pages/content programmatically and only then use the actual cms web interface in order to edit it.
For example, a use case for doing that could be when you need to migrate your whole website from a technology stack to another one including wagtail. The data is already available and all you need is to write the data migrations to create the pages and their content.
A Wagtail website has a tree structure based on django treebeard as tree system manager. It means that creating programmatically a page is to say creating a node of the tree. Here comes a limitation of django: what if you want to create a Wagtail page in a django data migration? Because of how django migrations work, unfortunately the django-treebeard API is not available so you need to work around it. The following is our solution (one of the options) and how we implemented it, but first we need some background information.
Latest model vs Historical model
In a data migration either the latest version of a model or the ‘’historical’’ one can be used. The data migrations are not designed for your latest/current model, they’re designed for a specific version of them (a specific moment of the 'history' of your models). Using an historical version of the model has the advantages that the data migration will not have problems trying to access fields that might no longer exist in the database (e.g. when you run your migrations against another database than original one or when the database has changed over time).
Wagtail site structure
As we said above, Wagtail provides a tree based website with a root node (level 0), branches and children nodes (nth-level). For example the home page (level 1) of your website is the first child of the root node. Both leaves and nodes are pages, the difference is that a leaf has no further children, the related branch ends there, while the nodes pages can have one or more children .
In order to be a node of the tree, i.e. a page of the website, the wagtail pages are so defined:
A MP node is a node of a Materialized Path tree. In a materialized path approach each node of the tree will have a path attribute, representing the full path from the root to the node. Also, two extra fields are stored in every node, depth and numchild:
- the depth is the level of the leaf (node) and it’s 1 for the root, 2 for its children and so on
- the numchild is the number of the leaves (children) added to that specific node
This makes the read operations faster, at the cost of a little more maintenance on tree updates/inserts/deletes.
How does the path work? the path of the root is 0001 and the root itself can not have siblings. Its children will have as path 00010001 and 00010002. The second child of the first one will have as path 000100010002 and so on through the all tree.
Adding a child to the tree
In order to add a page (node) to your website (tree) all you need is to have the parent and the child page. The django-treebeard API provide us with some useful methods like add_child() which lets us to easily add a page:
1 parent_page = Page.objects.filter(slug=‘homepage’) 2 child_page = Page.objects.create(slug=‘contacts’) 3 parent_page.add_child(instance=child_page)
The new page will be reachable at www.mysite.com/homepage/contacts.
Now let's go back to the initial question (as well as the reason why you're reading this post): what if you want to add a page to your website in a data migration? You can not use the django-treebeard API, like well pointed out in this post, but you can write a custom function to reproduce the result of add_child() and work around this limitation. Here you can find the custom add_child we use in our data migration:
def add_child(cls, apps, parent_page, klass, **kwargs): ContentType = apps.get_model('contenttypes.ContentType') page_content_type = ContentType.objects.get_or_create( model=klass.__name__.lower(), app_label=klass._meta.app_label, ) url_path = '%s%s/' % (parent_page.url_path, kwargs.get('slug')) created_page = klass.objects.create( content_type=page_content_type, path='%s00%02d' % (parent_page.path, parent_page.numchild + 1), depth=parent_page.depth + 1, numchild=0, url_path=url_path, **kwargs ) parent_page.numchild += 1 parent_page.save()
The first thing to define is the content type, that's why we have to pass the argument klass. It's the class of the node we want to add to the three and it has necessarily to extend the wagtail Page class (i.e. has to extend MP_Node).
The url_path definition follows the same logic seen about the path of a node: it contains the needed info to ''go back'' to the root node, that's why we include the parent_page.path.
After that we can create the actual node. From the code above we can note that:
- new nodes always have no children
- the depth is the parent's depth increased by one (it can be read like ''the child belongs always to the next (lower level) generation of the parent's one'')
- the path, as already mentioned, is an extension of the parent's path and it depends on the number of the children of this one
The last two code lines remind us to increase the number of parent node's children (and save it).
Wallah! You just added a new page into your website from a data migration.
We have seen how a wagtail website is structured and how a tree based structure works. Unfortunately in a django data migration the django-treeebard API is not available. Even if this lack is quite annoying, with a simple utility function you can remedy it and keep building your site tree wherever you want without polluting your data migration with boiler plate code.
- Django-treebard and Wagtail page creation
- The Charity Sport Tournament in Lublin
- New Release of Agilo for Trac (0.9.15/1.3.15)
- Incontro DevOps Italia 2016
- Configuring Test Kitchen output for Jenkins
- Configuring Test Kitchen on Jenkins
- Better infrastructure management a.k.a. IAC (Infrastructure as Code)
- Our approach to automated visual regression testing
- Test parallelization with Lettuce, take 2
- New Release of Agilo for Trac (0.9.14/1.3.14)