Once in a while you might find yourself in the situation in which a django CMS plugin you are using in a project is no longer supported or no longer suited for your goals.

But then, you have hundreds (or thousands) of instances of the old plugin and you need a quick way to migrate all the data to a new one.

Luckily, it’s possible to achieve this using a rather simple data migration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from django.db import migrations


def migrate_to_new_plugin(apps, schema_editor):
    OldPluginModel = apps.get_model("myapp", "PluginModel")
    NewPluginModel = apps.get_model("myapp", "PluginModel")
    for obj in OldPluginModel.objects.all():
                   
        new_obj = NewPluginModel()
        new_obj.id = obj.id
        new_obj.placeholder = obj.placeholder
        new_obj.parent = obj.parent
        new_obj.position = obj.position
        new_obj.language = obj.language
        new_obj.creation_date = obj.creation_date
        new_obj.depth = obj.depth
        new_obj.path = obj.path
        new_obj.numchild = obj.numchild
        new_obj.plugin_type = "MyNewPluginName"
        # Add something like `new_obj.field_name = obj.field_name` for any field in the the new plugin
        obj.delete()
        new_obj.save()
        # Copy any many to many field after save:`new_plugin.many2many.set(old_plugin.many2many.all())`

class Migration(migrations.Migration):

    dependencies = [
        ("myapp", "other_migration"),
    ]

    operations = [migrations.RunPython(migrate_to_new_plugin, migrations.RunPython.noop)]

How it works

In order to have a successful migration we must achieve the following:

  • keep the plugins tree consistent
  • keep the reference to the plugin valid (eg: the tag embedded in text fields when the plugin is embeddable in a text field)
  • keep custom data intact (at least the ones supported by the new plugin model)

To achieve this we can use the fact that Django models instances are python classes disconnected from the database until an explicit database update is run.

Deceiving Django ORM and the database

So the first step (lines 9-10) is to create an instance of the new plugin class without saving it to the database, then set the id of the new plugin to the one that’s being removed; at the end (21-22) we delete the old instance before saving the new one to free any constraint on unique fields.

By keeping the same id, plugin instances embedded in text plugins (whose reference is kept by hardcoding the id in the text body) are moved to the new model.

Migrating tree data

Django CMS plugins are stored in the database using the materialized path algorithm by Django-treebeard, and the plugins hierarchy information is stored in a few fields which neees to be copied verbatim (lines 11-18). This bypass the tree algorithm and we effectively replace a single node in the tree without affecting the tree balancement or triggering the automatic tree rebalance.

Migrating custom data

We now can save the custom model data to the new one. To achieve this we can add the lists of assignments from the field on the old plugin to the one the new plugin: new_plugin.field_name = old_plugin.field_name. If the plugin has any many to many relationship we can copy those as well, after saving our new plugin, using new_plugin.many2many.set(old_plugin.many2many.all()).

Closing

The above snippet has still room for improvements (like using _meta.get_fields to copy the fields data without writing every single field, but should provide you a good starting point to customise them your needs.