Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

post_generation hook appears to clash with Trait if they override the same attribute and both are called on create(). #1046

Open
kvothe9991 opened this issue Sep 29, 2023 · 0 comments

Comments

@kvothe9991
Copy link

kvothe9991 commented Sep 29, 2023

Description

When a post_generation hook wraps a field name that is also overriden by a Trait, and both are called on .create(), then none of them appear to have their desired effect.

To Reproduce

Should happen just by running the provided code.

Model / Factory code
# -------------------------- models.py --------------------------
class Category(models.Model):
    name = models.CharField(max_length=255, unique=True)
    slug = models.SlugField(max_length=255, unique=True)
    available = models.BooleanField(default=True)

    class Meta:
        verbose_name = 'category'
        verbose_name_plural = 'categories'
        indexes = [models.Index(fields=['name'])]

    def __str__(self):
        return self.name


class Product(models.Model):
    categories = models.ManyToManyField(
        Category,
        through='ProductInCategory',
        related_name='products'
    )

    name = models.CharField(max_length=150)
    slug = models.SlugField(max_length=255)
    description = models.TextField(blank=True)
    image = models.ImageField(upload_to='images/products/')

    price = models.DecimalField(max_digits=7, decimal_places=2)
    is_active = models.BooleanField(default=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ('-created_at',)
        indexes = [
            models.Index(fields=['name']),
        ]

    def __str__(self):
        return f'{self.name}'

class ProductInCategory(models.Model):
    '''
    Intermediate model for many-to-many relationship between Category and Product.
    '''
    category = models.ForeignKey(Category, null=True, on_delete=models.SET_NULL)
    product = models.ForeignKey(Product, null=True, on_delete=models.SET_NULL)

    class Meta:
        unique_together = ('category', 'product')



# -------------------------- factories.py --------------------------
class CategoryFactory(DjangoModelFactory):
    '''
    Category model factory.

    Allows associated models at build time through the following keywords:
        - Product:
            - `products:` as an override.
            - `with_products:` trait for automatic creation.
    '''
    class Meta:
        model = 'shop.Category'
        django_get_or_create = ('name',)

    class Params:
        with_products = factory.Trait(
            products=factory.RelatedFactoryList(
                'apps.shop.tests_v2.factories.ProductInCategoryFactory',
                'category',
                size=lambda: random.randint(1, 3),
        ))

    name = factory.Sequence(lambda n: f'Category {n}')
    slug = factory.LazyAttribute(lambda o: slugify(o.name))
    available = True

    @factory.post_generation
    def products(self, create, extracted, **kwargs):
        '''
        Catch `products` keyword override at build/creation time.
        '''
        if not (create and extracted):
            return
        self.products.add(*extracted)


class ProductFactory(DjangoModelFactory):
    '''
    Product model factory.
    '''
    class Meta:
        model = 'shop.Product'
        django_get_or_create = ('name',)

    name = factory.Sequence(lambda n: f'Product {n}')
    slug = factory.LazyAttribute(lambda o: slugify(o.name))
    description = factory.Faker('text')
    image = factory.django.ImageField()
    price = factory.Faker('pydecimal', left_digits=2, right_digits=2, positive=True)
    is_active = True

class ProductInCategoryFactory(DjangoModelFactory):
    '''
    Product <--> Category relationship intermediate model factory.
    '''
    class Meta:
        model = 'shop.ProductInCategory'
        django_get_or_create = ('category', 'product')

    category = factory.SubFactory(CategoryFactory)
    product = factory.SubFactory(ProductFactory)
The issue

When used in isolation, CategoryFactory.create(products=[...]) or CategoryFactory.create(with_products=True) work as expected, that is to say: the first one uses the provided products list and sets them to the Category model object, and the second one creates new products for the Category model object. But when used together as CategoryFactory.create(with_products=True, products=[...]) then the resulting category object has no related products at all. I would understand if one were to override the other and the result was only one of the previous examples, but this seemed like a bug. Am I doing something wrong?

n_products = random.randint(1, 10)
some_products = ProductFactory.create_batch(n_products)
category = CategoryFactory.create(with_products=True, products=some_products)
assert category.products.exists()    # Fails.
assert category.products.count() > 0    # Also fails.

Edit 1: Apologies, I forgot to add the Category <-> Product intermediate model from models.py, it's there now.

Edit 2: Please note, I don't think it's because of where the M2M field is placed, since I tried it the other way around (setting the post_generation hook and the Trait on ProductFactory) and it happened that way as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant