简体   繁体   中英

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 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 , ie 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.

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),
    )

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM