pythondjangofactory-boygeneric-foreign-key

Django Factory for model with Generic Foreign Key


I'm trying to write up a Factory for a model with a GFK for testing but I can't seem to get it working. I've referred to the common recipes in the docs, but my models don't match up exactly, and I'm also running into an error. Here are my models

class Artwork(models.Model):
    ...
    region = models.ForeignKey("Region", on_delete=models.SET_NULL, null=True, blank=True)

class Region(models.Model):
    # Could be either BeaconRegion or SpaceRegion
    region_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    region_object_id = models.PositiveIntegerField()
    region = GenericForeignKey("region_content_type", "region_object_id")


class SpaceRegion(models.Model):
    label = models.CharField(max_length=255)
    regions = GenericRelation(
        Region,
        content_type_field="region_content_type",
        object_id_field="region_object_id",
        related_query_name="space_region",
    )


class BeaconRegion(models.Model):
    label = models.CharField(max_length=255)
    regions = GenericRelation(
        Region,
        content_type_field="region_content_type",
        object_id_field="region_object_id",
        related_query_name="beacon_region",
    )

Essentially, an Artwork can be placed in one of two Regions; a SpaceRegion or BeaconRegion.

I've created the following Factorys for the corresponding models

class RegionFactory(factory.django.DjangoModelFactory):
    region_object_id = factory.SelfAttribute("region.id")
    region_content_type = factory.LazyAttribute(
        lambda o: ContentType.objects.get_for_model(o.region)
    )

    class Meta:
        exclude = ["region"]
        abstract = True


class BeaconRegionFactory(RegionFactory):
    label = factory.Faker("sentence", nb_words=2)
    region = factory.SubFactory(RegionFactory)

    class Meta:
        model = Region


class SpaceRegionFactory(RegionFactory):
    label = factory.Faker("sentence", nb_words=2)
    region = factory.SubFactory(RegionFactory)

    class Meta:
        model = Region


class ArtworkFactory(factory.django.DjangoModelFactory):
    ...
    region = factory.SubFactory(SpaceRegionFactory)

In my test, I try to create an Artwork using ArtworkFactory(), but it errors with

AttributeError: The parameter 'region' is unknown. Evaluated attributes are {}, definitions are <DeclarationSet: {'region_object_id': <SelfAttribute('region.id', default=<class 'factory.declarations._UNSPECIFIED'>)>, 'region_content_type': <factory.declarations.LazyAttribute object at 0x1068cf430>, 'label': <factory.faker.Faker object at 0x1068cf880>}>

What am I doing wrong here?


Solution

  • The issue comes when resolving ArtworkFactory.region.region, i.e SpaceRegionFactory.region.

    From your models, it seems that:

    The first step in those complex relation chains is to write the code without factories:

    >>> shire = SpaceRegion(label="Shire")
    >>> shire_generic = Region(region=shire)
    >>> the_ring = Artwork(region=shire_generic)
    

    This tells us that the Region is always created after the SpaceRegion or BeaconRegion, giving the following factories:

    
    class SpaceRegionFactory(factory.django.DjangoModelFactory):
        class Meta:
            model = models.SpaceRegion
        label = factory.Faker("sentence", n_words=2)
    
    
    class RegionFactory(factory.django.DjangoModelFactory):
        class Meta:
            model = models.Region
    
        region = factory.SubFactory(SpaceRegionFactory)
    
    
    class ArtworkFactory(factory.django.DjangoModelFactory):
        class Meta:
            model = models.Artwork
    
        region = factory.SubFactory(RegionFactory)
    

    With this, you should be able to get your code working. Note how we're simply setting the region field on Region: Django's internals will extract the object content type / content ID automatically.

    Additional options

    You could tune the RegionFactory to let callers decide whether they want a SpaceRegion or a BeaconRegion:

    class RegionFactory(factory.django.DjangoModelFactory):
        class Meta:
            models = Region
    
        class Params:
            space = True  # Request a SpaceRegion
    
        region = factory.Maybe(
            factory.SelfAttribute("space"),
            factory.SubFactory(SpaceRegion),
            factory.SubFactory(BeaconRegion),
        )