FactoryBoy / factory_boy

A test fixtures replacement for Python
https://factoryboy.readthedocs.io/
MIT License
3.5k stars 394 forks source link

django_get_or_create does not work when using cutom model manager create method #1094

Open g-kartik opened 1 week ago

g-kartik commented 1 week ago

Description

A clear and concise description of what the bug is. django_get_or_create does not work when using custom model manger create method

To Reproduce

Share how the bug happened:

Model / Factory code
Factory
class VKUserBaseFactory(factory.django.DjangoModelFactory):
    name = factory.sequence(lambda n: f"vkuser {n}")
    email = factory.sequence(lambda n: f"vkuser{n}@vk.com")
    password = factory.django.Password('password')
    is_staff = False
    is_superuser = False
    is_active = True
    is_email_verified = True

    class Meta:
        model = models.VKUser
        django_get_or_create = ('email',)

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        manager = cls._get_manager(model_class)
        user = manager.create_user(*args, **kwargs)
        return user
Model
class VKUser(PermissionsMixin, AbstractBaseUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=100, blank=False)
    email = models.EmailField(unique=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now, null=True)
    is_email_verified = models.BooleanField(default=True)
    email_verification = GenericRelation(EmailVerification, content_type_field='content_type',
                                         object_id_field='object_id')

    objects = VKUserManager()
The issue

Add a short description along with your code user1 = VKUserFactory(email=vkuser0@vk.com) user2 = VKUserFactory(email=vkuser0@vk.com)

UniqueViolation                           Traceback (most recent call last)
File /opt/venv/lib/python3.8/site-packages/django/db/backends/utils.py:84, in CursorWrapper._execute(self, sql, params, *ignored_wrapper_args)
     [83](https://vscode-remote+attached-002dcontainer-002b6d696e646b6f7368736572766572.vscode-resource.vscode-cdn.net/opt/venv/lib/python3.8/site-packages/django/db/backends/utils.py:83) else:
---> [84](https://vscode-remote+attached-002dcontainer-002b6d696e646b6f7368736572766572.vscode-resource.vscode-cdn.net/opt/venv/lib/python3.8/site-packages/django/db/backends/utils.py:84)     return self.cursor.execute(sql, params)

UniqueViolation: duplicate key value violates unique constraint "authentication_vkuser_email_key"
DETAIL:  Key (email)=(vkuser0@vk.com) already exists.

The above exception was the direct cause of the following exception:

IntegrityError                            Traceback (most recent call last)
Cell In[6], [line 1](vscode-notebook-cell:?execution_count=6&line=1)
----> [1](vscode-notebook-cell:?execution_count=6&line=1) user1 = auth_factory.VKUserFactory(email='vkuser0@vk.com')

File /opt/venv/lib/python3.8/site-packages/factory/base.py:43, in FactoryMetaClass.__call__(cls, **kwargs)
     [41](https://vscode-remote+attached-002dcontainer-002b6d696e646b6f7368736572766572.vscode-resource.vscode-cdn.net/opt/venv/lib/python3.8/site-packages/factory/base.py:41)     return cls.build(**kwargs)
     [42](https://vscode-remote+attached-002dcontainer-002b6d696e646b6f7368736572766572.vscode-resource.vscode-cdn.net/opt/venv/lib/python3.8/site-packages/factory/base.py:42) elif cls._meta.strategy == enums.CREATE_STRATEGY:
---> [43](https://vscode-remote+attached-002dcontainer-002b6d696e646b6f7368736572766572.vscode-resource.vscode-cdn.net/opt/venv/lib/python3.8/site-packages/factory/base.py:43)     return cls.create(**kwargs)
     [44](https://vscode-remote+attached-002dcontainer-002b6d696e646b6f7368736572766572.vscode-resource.vscode-cdn.net/opt/venv/lib/python3.8/site-packages/factory/base.py:44) elif cls._meta.strategy == enums.STUB_STRATEGY:
     [45](https://vscode-remote+attached-002dcontainer-002b6d696e646b6f7368736572766572.vscode-resource.vscode-cdn.net/opt/venv/lib/python3.8/site-packages/factory/base.py:45)     return cls.stub(**kwargs)

File /opt/venv/lib/python3.8/site-packages/factory/base.py:539, in BaseFactory.create(cls, **kwargs)
    [533](https://vscode-remote+attached-002dcontainer-002b6d696e646b6f7368736572766572.vscode-resource.vscode-cdn.net/opt/venv/lib/python3.8/site-packages/factory/base.py:533) @classmethod
...
---> [84](https://vscode-remote+attached-002dcontainer-002b6d696e646b6f7368736572766572.vscode-resource.vscode-cdn.net/opt/venv/lib/python3.8/site-packages/django/db/backends/utils.py:84)     return self.cursor.execute(sql, params)

IntegrityError: duplicate key value violates unique constraint "authentication_vkuser_email_key"
DETAIL:  Key (email)=(vkuser0@vk.com) already exists.

Notes

Add any notes you feel relevant here :) The expected behavior should be that it should try to get the instance using the django_get_or_create fields no matter if any custom manager method is defined.

rbarrois commented 1 week ago

The default behaviour of DjangoModelFactory's _create is to hook into the object's manager's get_or_create call if required, or to use that default manager's create() block. Is there a reason for overriding the _create() block (thus bypassing that behaviour)?

If your custom manager's get_or_create should NOT be called from the factory, you will have to override both the _create() method AND the _get_or_create() one; factory_boy cannot guess which non-standard method to call!