pythondjangodjango-fixturesdjango-polymorphic

How to manually generate fixtures for Django Polymorphic Models?


I have some Django Polymorphic models:

import uuid
from django.db import models
from polymorphic.models import PolymorphicModel

class Fruit(PolymorphicModel):
    class Meta:
        abstract = True

class Apple(Fruit):
    variety=models.CharField(max_length=30,primary_key=True)

class Grape(Fruit):
    id=models.UUIDField(primary_key=True, default=uuid.uuid4)
    colour=models.CharField(max_length=30)

Then I can create some fixtures:

[
    {"model": "test_polymorphic.apple", "pk": "bramley", "fields": {}},
    {"model": "test_polymorphic.apple", "pk": "granny smith", "fields": {}},
    {"model": "test_polymorphic.grape", "pk": "00000000-0000-4000-8000-000000000000", "fields": { "colour": "red"} },
    {"model": "test_polymorphic.grape", "pk": "00000000-0000-4000-8000-000000000001", "fields": { "colour": "green"} }
]

and use python -m django loaddata fixture_name to load it into the database which "appears" to be successful.

Then if I use:

from test_polymorphic import models
models.Apple.objects.all()

It raises the error:

PolymorphicTypeUndefined: The model Apple#bramley does not have a `polymorphic_ctype_id` value defined.
If you created models outside polymorphic, e.g. through an import or migration, make sure the `polymorphic_ctype_id` field points to the ContentType ID of the model subclass.

Using loaddata bypasses the save() method of the model so the default content-types are not set on the models.

I could find the appropriate content types using:

from django.contrib.contenttypes.models import ContentType
for model_name in ("apple", "grape"):
    print(
      model_name,
      ContentType.objects.get(app_label="test_polymorphic", model=model_name).id,
    )

Which outputs:

apple: 3
grape: 2

and then change the fixture to:

[
  {
    "model": "test_polymorphic.apple",
    "pk": "bramley",
    "fields": { "polymorphic_ctype_id": 3 }
  },
  {
    "model": "test_polymorphic.apple",
    "pk": "granny smith",
    "fields": { "polymorphic_ctype_id": 3 }
  },
  {
    "model": "test_polymorphic.grape",
    "pk": "00000000-0000-4000-8000-000000000000",
    "fields": { "colour": "red", "polymorphic_ctype_id": 2 }
  },
  {
    "model": "test_polymorphic.grape",
    "pk": "00000000-0000-4000-8000-000000000001",
    "fields": { "colour": "green", "polymorphic_ctype_id": 2 }
  }
]

Including the polymorphic content type in the fields and then the fixture works. However, this value is effectively a magic number and I am not convinced that it will not vary if:


Question:

Can I rely on the polymorphic_ctype_id to be a fixed value and, if not, how should I set the default polymorphic content type on the model when loading fixtures (if using loaddata bypasses the save method of the model and I cannot be certain what the id values of the content types would be)?


Solution

  • The Django Polymorphic - "Migrating existing models to polymorphic" documentation shows how to migrate non-polymorphic models to polymorphic models:

    The following Python code can be used to fill the value of a model:

    from django.contrib.contenttypes.models import ContentType
    from myapp.models import MyModel
    
    new_ct = ContentType.objects.get_for_model(MyModel)
    MyModel.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=new_ct)
    

    They give an example of how to include this in a migration but it can also be run manually after using loaddata to load a fixture where the values are not set.

    If there are multiple models that need updating then you can write a helper function:

    from django.contrib.contenttypes.models import ContentType
    
    def set_polymorphic_ctype(model):
         app_label = model._meta.app_label
         name = model._meta.model_name
         ctype = ContentType.objects.get(app_label=app_label, model=name)
         return model.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=ctype)
    

    and call it for each of the models:

    set_polymorphic_ctype(models.Apple)
    set_polymorphic_ctype(models.Grape)
    

    If you are loading fixtures in a TestCase then I didn't find a callback method that would run after the fixtures are loaded but you can call the helper function during test set-up.

    class FruitTestCase(TestCase):
    
        fixtures = [ "fixture_name" ]
    
        def setUp(self):
            set_polymorphic_ctype(models.Apple)
            set_polymorphic_ctype(models.Grape)
    
        def test_something(self):
            ...