factory_boy icon indicating copy to clipboard operation
factory_boy copied to clipboard

Set attribute using LazyAttribute only if another attribute exists

Open LeonardoGentile opened this issue 7 years ago • 2 comments

Hello, I have this kind of Django model:

from djmoney.models.fields import MoneyField
from moneyed import Money

class Item(models.Model):
    total_due = MoneyField(max_digits=17,
                           decimal_places=7,
                           default_currency=DEFAULT_CURRENCY,
                           currency_choices=CURRENCIES_CHOICES,
                           null=True, blank=True)

    c_total_due = = MoneyField(max_digits=17,
                           decimal_places=7,
                           default_currency=DEFAULT_CURRENCY,
                           currency_choices=CURRENCIES_CHOICES,
                           null=True, blank=True)

The field c_total_due is computed from total_due so its value is always related to it.

Given that, if the total_due is set then I would like my factory to generate the the c_total_due to be exactly the same as total_due if not explicitly set:

class ItemFactory(factory.DjangoModelFactory):
    class Meta:
        model = Item

    c_total_due = factory.LazyAttribute(lambda o: o.total_due)

The problem is that if I don't explicitly create a factory object by passing a total_due this will fail.

AttributeError: The parameter total_charged is unknown.

Is there a way to set a value of an attribute based on another one using the LazyAttribute lambda only if the first attribute is set in the first place?

I hope I was clear : ]

LeonardoGentile avatar Jun 01 '18 14:06 LeonardoGentile

Recommended option:

You can simply set total_due = None in your factory (which is actually what would be used by Django if you don't provide anything):

class ItemFactory:
    total_due = None
    c_total_due = factory.SelfAttribute('total_due')

This way, ItemFactory() will call Item.objects.create(total_due=None, c_total_due=None), and ItemFactory(total_due=42) calls Item.objects.create(total_due=42, c_total_due=42).

Other option:

Another option would be to use factory.Maybe:

class ItemFactory:
    c_total_due = factory.Maybe('total_due', factory.SelfAttribute('total_due'), None)

This will set c_total_due to the value of total_due if total_due is set, and will hide it if total_due isn't provided:

  • ItemFactory() will call Item.objects.create()
  • ItemFactory(total_due=42) calls Item.objects.create(total_due=42, c_total_due=42).

This second option is mostly useful if your model itself has some rules to compute default values for total_due / c_total_due that you don't want to override/copy to the factory.

rbarrois avatar Jun 01 '18 16:06 rbarrois

Thank you very much!

The second option seems to fit perfectly for my requirements. Although, I couldn't make it work :\

class ItemFactory(factory.DjangoModelFactory):
    class Meta:
        model = Item

    c_total_due = factory.Maybe('total_due', factory.SelfAttribute('total_due'), None)

class SpecificFactory(ItemFactory):
    another_field = None

And I call it like this:

my_obj = SpecificFactory(another_field=42, **{'total_due': 400})

I can see that my_obj.total_due == 400 but my_obj.c_total_due seems to be always None.

Did I skip a step or am I missing something?

I'm using

factory-boy==2.11.1 
Django==1.11.5

with python 3.5

LeonardoGentile avatar Jun 04 '18 09:06 LeonardoGentile