question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Custom User factory fails when using a CustomUserManager in Django 2

See original GitHub issue

Description

Trying to use a factory to create a user instance for a user model that extends AbstractUser and implements CustomUserManager results in a TypeError.

To Reproduce

  1. Implement a CustomUserManager in the way recommended here for a custom User model that extends the AbstractUser base class and doesn’t override the username field. Add a field called id. https://docs.djangoproject.com/en/2.2/topics/auth/customizing/#django.contrib.auth.models.CustomUserManager
  2. Create a factory for that User model and write a @classmethod as suggested in the FactoryBoy documentation: https://factoryboy.readthedocs.io/en/latest/recipes.html#custom-manager-methods
  3. Try to create a new User using the UserFactory you just created.
Model / Factory code
# Factories
class CompanyToProfileFactory(factory.DjangoModelFactory):
    """Factory for `client.CompanyToProfile` Django model."""
    class Meta:
        model = models.CompanyToProfile

    company = factory.SubFactory(CompanyFactory)
    profile = factory.SubFactory(ProfileFactory)
    access = factory.SubFactory(AccessFactory)
    created = factory.Faker("past_datetime", tzinfo=pytz.UTC)
    updated = factory.Faker("past_datetime", tzinfo=pytz.UTC)

class ProfileFactory(factory.DjangoModelFactory):
    """Factory for `personal.Profile` Django model."""
    class Meta:
        model = models.Profile

    class Params:
        superuser = factory.Trait(is_admin=True, is_superuser=True, is_staff=True)

    id = factory.Faker("uuid4")
    title = factory.SubFactory(TitleFactory)
    initials = factory.Faker("word")
    date_of_birth = factory.Faker("past_date")
    email = factory.Faker("email")
    gender = factory.SubFactory(GenderFactory)
    ethnicity = factory.SubFactory(EthnicityFactory)
    phone_number = factory.Faker("phone_number")
    mobile_number = factory.Faker("phone_number")
    profile_type = factory.SubFactory(ProfileTypeFactory)
    created = factory.Faker("past_datetime", tzinfo=pytz.UTC)
    updated = factory.Faker("past_datetime", tzinfo=pytz.UTC)

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        """Override the default ``_create`` with our custom call."""
        manager = cls._get_manager(model_class)
        # The default would use ``manager.create(*args, **kwargs)``
        return manager.create_user(*args, **kwargs)

# model and manager
class ProfileManager(BaseUserManager):
    def create_user(self, username='', email='', password=None):
        """
        Creates and saves a User with the given email, date of
        birth and password.
        """
        if not email:
            raise ValueError('Users must have an email address')
        try:
            email = validate_email(email)
        except ValidationError:
            raise ValueError('Invalid email address')
        user = self.model(
            username=username,
            email=self.normalize_email(email),
        )

        user.save(using=self._db)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, username='', email='', password=''):
        """
        Creates and saves a superuser with the given email, date of
        birth and password.
        """
        if not password:
            raise ValueError('SuperUser must have password')
        user = self.create_user(
            username=username,
            email=email,
            password=password
        )
        user.is_admin = True
        user.is_superuser = True
        user.is_staff = True
        user.email = email
        user.save(using=self._db)
        return user


class Profile(AbstractUser):
    """Basic information about a person that uses the system."""
    objects = ProfileManager()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # slug = AutoSlugField(populate_from=['first_name', 'initials', 'last_name', 'date_of_birth'])
    title = models.ForeignKey(Title, null=True, on_delete=models.DO_NOTHING)
    # first_name
    initials = models.CharField(max_length=20, null=True, blank=True, default=None)
    # last_name
    date_of_birth = models.DateField(null=True)
    email = models.EmailField(_('email address'), blank=True, unique=True)
    gender = models.ForeignKey(Gender, null=True, on_delete=None)
    ethnicity = models.ForeignKey(Ethnicity, null=True, on_delete=None)
    phone_number = models.CharField(max_length=15, null=True, blank=True)
    mobile_number = models.CharField(max_length=15, null=True, blank=True)
    # profile type
    profile_type = models.ForeignKey(ProfileType, null=True, default=None, on_delete=SET_NULL)
    # Administrative Fields
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['last_name']
        indexes = [
            models.Index(fields=['phone_number']),
            models.Index(fields=['mobile_number']),
            models.Index(fields=['email']),
            models.Index(fields=['first_name', 'last_name']),
        ]

class CompanyToProfile(models.Model):
    """Relationship of a user to the company"""
    company = models.ForeignKey(Company, on_delete=CASCADE)
    profile = models.ForeignKey(Profile, on_delete=CASCADE)
    access = models.ForeignKey(Access, null=True, default=None, on_delete=CASCADE)
    # Administrative Fields
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f'{self.profile.get_full_name()} - {self.access.access} - {self.company.company}'

The issue

Trying to create an object using a factory that relates in any way to the custom User factory (in this case ProfileFactory which is a SubFactory of CompanyToProfileFactory (see code below) will cause a TypeError (see error below)

# code that causes exception:
class CompanyToProfileTestCase(TestCase):
    """Tests for `client.CompanyToProfile` Django model."""

    def setUp(self):
        self.company_to_profile = CompanyToProfileFactory()

# error from running pytest in CLI:
    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        """Override the default ``_create`` with our custom call."""
        manager = cls._get_manager(model_class)
        # The default would use ``manager.create(*args, **kwargs)``
>       return manager.create_user(*args, **kwargs)
E       TypeError: create_user() got an unexpected keyword argument 'id'

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

3reactions
rbarroiscommented, Oct 22, 2019

The issue with your code is that your create_user method only accepts a subset of the fields defined in your factory (username, email and password).

However, you call it with the set of every field defined in your factory:

>>> manager.create_user(*args, **kwargs)
# Is equivalent to
>>> manager.create_user(
    id=<uuid>, title=<Title: ...>, initials="foo",
    date_of_birth=datetime.date(1970, 11, 18),
)

The cleanest solution to your problem would be to adjust your create_user function to accept al possible fields, as stated in the django docs section that you mentioned (emphasis mine):

The prototype of create_user() should accept the username field, plus all required fields as arguments.

This would be:

# In ProfileManager
def create_user(self, username, email, password=None, **kwargs):
    self.validate_email(email)
    user = self.model(
        username=username, email=self.normalize_email(email),
        **kwargs,  # Here, we copy all provided fields as well
    )
    user.save(using=self._db)
    user.set_password(password)
    user.save(using=self._db)
    return user

# In ProfileFactory
@classmethod
def _create(cls, model_class, username, email, password=None, **kwargs):
    manager = cls._get_manager(model_class)
    return manager.create_user(
        username=username,
        email=email,
        password=password,
        **kwargs,
    )
0reactions
blairg23commented, Oct 25, 2019

Btw, I did not have to do anything different to the _create() method in the factory:

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        """Override the default ``_create`` with our custom call."""
        manager = cls._get_manager(model_class)
        # The default would use ``manager.create(*args, **kwargs)``
        return manager.create_user(*args, **kwargs)
Read more comments on GitHub >

github_iconTop Results From Across the Web

Django custom user error - Stack Overflow
It looks like your indentation is wrong. get_by_natural_key() is a function, not a method of your manager class.
Read more >
Customizing authentication in Django
This document provides details about how the auth system can be customized. Authentication backends provide an extensible system for when a username and ......
Read more >
What You Need to Know to Manage Users in Django Admin
User management in Django admin is a tricky subject. If you enforce too many permissions, then you might interfere with day-to-day operations.
Read more >
Creating a Custom User Model (Django) - YouTube
Free website building course with Django & Python: https://codingwithmitch.com/courses/building-a-website- django -python/In this video I show ...
Read more >
how to avoid repetition in custom User manager testing-django
You can use a helper method and a fixture to reduce the repeated code. ... import UserFactory pytestmark = pytest.mark.django_db class TestsUsersManagers: ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found