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 Region
s; a SpaceRegion
or BeaconRegion
.
I've created the following Factory
s 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?
The issue comes when resolving ArtworkFactory.region.region
, i.e SpaceRegionFactory.region
.
From your models, it seems that:
Region
is a table which points to either SpaceRegion
or BeaconRegion
SpaceRegion
and BeaconRegion
are simple tables, with a helper to retrieve the related Region
object.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.
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),
)