11

For some reasons I want to apply a major change on my models. I want to rework my whole design somehow, but the Django migration implementations keep in mind the previous design by not updating my models bases.

Let me show quickly what I had before then what I have now.

app1.TopLevel
  |_ app1.IntermediateLevel
    |_ app2.LowLevel

I had 3 models like those, then now I would like to cut off this design to something more suited to my current project, such as

app2.TopLevel
  |_ app2.LowLevel

My major changes are, first I don't want an intermediate model anymore, second I don't need top keep the app1.TopLevel that way.

I don't have issues with the data (I run multiple migration, some with Python to put the data into temporary fields, then put back the data in the right field later and remove those temporary fields).

My issue is, when we create an inherited model we define its bases ;

migrations.CreateModel(
        name='IntermediateLevel',
        fields=[
            ('toplevel_ptr', models.OneToOneField(
                 auto_created=True,
                 on_delete=django.db.models.deletion.CASCADE,
                 parent_link=True,
                 primary_key=True, 
                 serialize=False, 
                 to='app1.TopLevel'),
            )
        ],
        bases=('app1.TopLevel',),
    )

In that cases I would got something like

Local field 'toplevel_ptr' in class 'LowLevel' clashes with field of similar name from base class 'IntermediateLevel'`

I read the official documentation and the source code (for migrations) but so far I didn't see anything about it. Is it possible to tell to the migration system that we changed a model bases (its parents) ?

Otherwise, the only one solution I got would be to create new models, run python migrations to copy the data from the old models to the new ones. Then remove the old models and rename the new ones to get the names I want.

Jean Jung
  • 1,200
  • 12
  • 29
mille_a
  • 121
  • 8
  • 1
    Try to alter field with a new one to one field linked to your new base class. – Emin Bugra Saral Jul 12 '17 at 08:40
  • 1
    @mille_a I am at the same problem right now! Have been at it for the past 4 days and can't figure it out. When I try to completely remove the UpperLevelModel kind of like your IntermediateLevel model, I get an Invalid Bases Error. We're you able to solve your problem? Please help – Nazariy Aug 30 '17 at 15:56
  • Is this question relevant to your case @mille_a https://stackoverflow.com/questions/16487977/what-value-do-i-use-for-ptr-when-migrating-reparented-classes-with-south/21817578 ? – tutuca Aug 06 '19 at 15:02
  • Ultimately the only way I found to properly do it is to : * Create the new models under a temporary names * Migrate the data from the previous models to the new ones * Delete the previous models * Rename the new models So you new multiple migrations to do it – mille_a Dec 30 '19 at 01:47

1 Answers1

2

Had the same issue with the bases and I ended up writing my own migration operation like below. However, worth mention that the whole process looks like follows:

  1. Add a new IntegerField to the model with null=True
  2. Copy data from xxx_ptr to the new field
  3. Remove the xxx_ptrfield
  4. Run the RemoveModelBasesOptions operation
  5. Rename temporary Integer field into id and change it to AutoField
  6. Remove the old model which I inherited from

One thing to note is that if you have models with ForeignKey to your model, it will preserve the link anyway, so it is safe.

class RemoveModelBasesOptions(ModelOptionOperation):
    def __init__(self, name):
        super().__init__(name)

    def deconstruct(self):
        kwargs = {
            'name': self.name,
        }
        return (
            self.__class__.__qualname__,
            [],
            kwargs
        )

    def state_forwards(self, app_label, state):
        model_state = state.models[app_label, self.name_lower]
        model_state.bases = (models.Model,)
        state.reload_model(app_label, self.name_lower, delay=True)

    def database_forwards(self, app_label, schema_editor, from_state,
                          to_state):
        pass

    def database_backwards(self, app_label, schema_editor, from_state,
                           to_state):
        pass

    def describe(self):
        return "Remove bases from the model %s" % self.name

    @property
    def migration_name_fragment(self):
        return 'remove_%s_bases' % self.name_lower

Then, simply call it as an operation in your migration:

class Migration(migrations.Migration):
    dependencies = [
        ('xxxx', '0025_xxxx'),
    ]

    operations = [
        RemoveModelBasesOptions('Foo')
    ]
Andrii Zarubin
  • 2,165
  • 1
  • 18
  • 31