Avoid username field creation in custom user model for django allauth

Viewed 1650

I'm using a custom user model with allauth and I need to have the username field omitted. I've already seen the docs and a whole bunch of stackoverflow answers about using ACCOUNT_USER_MODEL_USERNAME_FIELD = None but all of this still leads my database to have an username field.

Now since the db still has an username field with the unique constraint set on and allauth will not put a username in the field with the aforementioned setting set to None, this causes me to face IntegrityError after the very first user creation. I know I can solve this by just having the aforementioned setting be set to 'username' but I'm curious, how do I just not make the username, because I'm never using it.

My model:

class CustomUser(AbstractUser):
    # Custom user model for django-allauth
    first_name = None
    last_name = None

    def delete(self):
        # Custom delete - make sure user storage is also purged
        # Purge the user storage
        purge_userstore(self.email)
        # Call the original delete method to take care of everything else
        super(CustomUser, self).delete()

It doesn't really do much except override the delete function. This override is irrelevant in this topic but I included it just for the sake of completeness. It also sets first_name and last_name to None, which works perfectly and removes those fields from the database as expected. I've tried setting user to None but that does nothing. I've also tried setting username to None but that will raise a FieldNotFound error with ACCOUNT_USER_MODEL_USERNAME_FIELD = None

My settings (the relevant bit):

AUTHENTICATION_BACKENDS = (
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",
)
AUTH_USER_MODEL = 'custom_account.CustomUser'
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_AUTHENTICATION_METHOD = 'email'

My migration file:

migrations.CreateModel(
            name='CustomUser',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
                ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
            ],
....

This migration generation confuses me to no end. Why is the username field still there? Why is it set to be the only unique constraint even though I've clearly set ACCOUNT_UNIQUE_EMAIL = True?

Note: This migration file is generated from a clean slate, this is the first and only migration file generated from the code I have presented.

At first I thought, my settings were simply not being read. But I checked django.conf.settings and allauth.account.app_settings (in the shell) for these changes and they were all updated. What's going on here?

Note: Amongst the many stackoverflow questions I've searched, this question in particular seems to explain my issue perfectly. With one small problem, the answer by the creator of allauth himself suggested to use ACCOUNT_USER_MODEL_USERNAME_FIELD = "username" as the model in question was "clearly using the username field". But the answer does not explain what to do when you don't want to use the username field at all.

1 Answers

Looks like the only way to get rid of the username field is to override the AbstractUser's username field and/or use a completely custom model from the ground up. Thought overriding AbstractBaseUser should work too, albeit AbstractBaseUser provides less functionality.

class CustomUser(AbstractUser):
    # Custom user model for django-allauth
    # Remove unnecessary fields
    username = None
    first_name = None
    last_name = None
    # Set the email field to unique
    email = models.EmailField(_('email address'), unique=True)
    # Get rid of all references to the username field
    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

This model will remove the username field and make the email field unique, as well as change all references to the USERNAME_FIELD to 'email'. Do note that REQUIRED_FIELDS should be empty as the USERNAME_FIELD cannot be in there. When using allauth, this is not a problem, the email and password requirement are managed by allauth anyway.

The settings that I've mentioned in the question should remain the same, specifically-

ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_REQUIRED = True
Related