Moving Wagtail pages in a migration

Viewed 1735

I'm restructuring my Wagtail app to remove an IndexPage that only has a single item in it, and moving that item to be a child of the current IndexPage's parent.

basically moving from this:

Page--| |--IndexPage--| |--ChildPages (there's only ever 1 of these)

to this:

Page--| |--ChildPage

I've made the changes to the models so that this structure is used for creating new content and fixed the relevant views to point to the ChildPage directly. But now I want to migrate the current data to the new structure and I'm not sure how to go about it... Ideally this would be done in a migration so that we would not have to do any of this manipulation by hand.

Is there a way to move these ChildPage's up the tree programmatically during a migration?

2 Answers

Unfortunately there's a hard limitation that (probably) rules out the possibility of doing page tree adjustments within migrations: tree operations such as inserting, moving and deleting pages are implemented as methods on the Page model, and within a migration you only have access to a 'dummy' version of that model, which only gives you access to the database fields and basic ORM methods, not those custom methods.

(You might be able to work around this by putting from wagtail.wagtailcore.models import Page in your migration and using that instead of the standard Page = apps.get_model("wagtailcore", "Page") approach, but I wouldn't recommend that - it's liable to break if the migration is run at a point in the migration sequence where the Page model is still being built up and doesn't match the 'real' state of the model.)

Instead, I'd suggest writing a Django management command to do the tree manipulation - within a management command it is safe to import the Page model from wagtailcore, as well as your specific page models. Page provides a method move(target, pos) which works as per the Treebeard API - the code for moving your child pages might look something like:

from myapp.models import IndexPage

# ...
for index_page in IndexPage.objects.all():
    for child_page in index_page.get_children():
        child_page.move(index_page, 'right')
    index_page.delete()

Theoretically it should be possible to build a move() using the same sort of manipulations that Daniele Miele demonstrates in Django-treebeard and Wagtail page creation. It'd look something like this Python pseudocode:

def move(page, target):
  # assuming pos='last_child' but other cases follow similarly,
  # just with more bookkeeping

  # first, cut it out of its old tree
  page.parent.numchild -= 1
  for sib in page.right_siblings: # i.e. those with a greater path
    old = sib.path
    new = sib.path[:-4] + (int(sib.path[-4:])-1):04
    sib.path = new
    for nib in sib.descendants:
      nib.path = nib.path.replace_prefix(old, new)
  
  # now, update itself
  old_path = page.path
  new_path = target.path + (target.numchild+1):04
  page.path = new_path
  old_url_path = page.url_path
  new_url_path = target.url_path + page.url_path.last
  page.url_path = new_url_path
  old_depth = page.depth
  new_depth = target.depth + 1
  page.depth = new_depth

  # and its descendants
  depth_change = new_depth - old_depth
  for descendant in page.descendants:
    descendant.path = descendant.path.replace_prefix(old_path, new_path)
    descendant.url_path = descendant.url_path.replace_prefix(old_path, new_path)
    descendant.depth += depth_change

  # finally, update its new parent
  target.numchild += 1

The core concept that makes this manipulation simpler than it looks is: when a node gets reordered or moved, all its descendants need to be updated, but the only update they need is the exact same update their ancestor got. It's applied as a prefix replacement (if str) or a difference (if int), neither of which requires knowing anything about the descendant's exact value.

That said, I haven't tested it; it's complex enough to be easy to mess up; and there's no way of knowing if I updated every invariant that Wagtail cares about. So there's something to be said for the management command way as well.

Related