Hello, I spent a good part of today implementing what must be the most common scenario for custom user models: case-insensitive email as username. (Yes. This horse has been beaten to death. Multiple times.)
Since it was the first time I implemented a custom user model from scratch by myself, I’d like to share my experience in case that’s useful to others. Do you think there’s a better solution? Do you have concrete ideas for improving Django in this area? The main alternative I’m aware of is a custom email field based on PostgreSQL’s citext type. Perhaps I’ll try that next time. Anyway, here’s what I did this time. 1) The documentation is excellent I know a lot of effort has been put into improving it and it shows. Congratulations to everyone involved. 2) Custom indexes would be convenient Since I want to preserve emails as entered by the users, I cannot simply lowercase them. That would have been too easy. I ended up with this migration to add the appropriate unique index on LOWER(email). See the comments for details. operations = [ migrations.CreateModel( name='User', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # … # unique=True was removed from the autogenerated line; a unique index is created below. ('email', models.EmailField(error_messages={'unique': 'A user with that email already exists.'}, max_length=254, verbose_name='email address')), # … ], # … ), migrations.RunSQL( # Based on editor._create_index_sql(User, [User._meta.get_field('email')], '_lower') sql='CREATE UNIQUE INDEX "blabla_user_email_f86edd9d_lower" ON "blabla_user" (LOWER("email"))', reverse_sql='DROP INDEX "blabla_user_email_f86edd9d_lower"', state_operations=[ migrations.AlterField( model_name='user', name='email', field=models.EmailField(error_messages={'unique': 'A user with that email already exists.'}, max_length=254, unique=True, verbose_name='email address') ), ], ), ] It took me some time to get there. At first I tried simply removing unique=True on the email field but that didn’t work well. I know there’ve been discussions about custom indexes. They would make this use case much easier. 3) Redefining forms isn’t too bad I was getting quite bored of copy-paste-tweaking snippets (custom model, custom manager, custom admin, …) when I got to defining custom forms. Fortunately, a small mixin was all I needed. (Read on for why this code uses `User.objects.get_by_natural_key(email)`.) from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm from django.core.exceptions import ValidationError class CaseInsensitiveUniqueEmailMixin: """ ModelForm mixin that checks for email unicity, case-insensitively. """ def clean_email(self): email = self.cleaned_data['email'] User = self._meta.model field = User._meta.get_field('email') try: User.objects.get_by_natural_key(email) except User.DoesNotExist: return email else: raise ValidationError( message=field.error_messages['unique'], code='unique', ) class UserChangeForm(CaseInsensitiveUniqueEmailMixin, BaseUserChangeForm): pass class UserCreationForm(CaseInsensitiveUniqueEmailMixin, BaseUserCreationForm): pass 4) The ugly hack My first ideas was to write a custom authentication backend to look up users by email case-insensitively. But I was getting bored and I noticed that django.contrib.auth uses `UserModel._default_manager.get_by_natural_key` to look up users. So... class UserManager(BaseUserManager): """ Manager for the User class defined below. Quite similar to django.contrib.auth.models.UserManager. """ # ... def get_by_natural_key(self, email): qs = self.annotate(email_lower=Lower('email')) return qs.get(email_lower=email.lower()) /!\ This is entirely dependent on implementation details of django.contrib.auth. It can break when you upgrade Django; don’t blame it on me. /!\ That said, the nice side effect of this implementation is that it makes the unicity check in createsuperuser work as expected. I’m not aware of any other way to fix it with the database schema I chose. I suppose an implementation of custom unique indexes with support for checking unicity constraints would make that point moot. Best regards, -- Aymeric. -- You received this message because you are subscribed to the Google Groups "Django developers (Contributions to Django itself)" group. To unsubscribe from this group and stop receiving emails from it, send an email to django-developers+unsubscr...@googlegroups.com. To post to this group, send email to django-developers@googlegroups.com. Visit this group at http://groups.google.com/group/django-developers. To view this discussion on the web visit https://groups.google.com/d/msgid/django-developers/DFFA548E-F746-4123-AD50-4DBF8BDA3925%40polytechnique.org. For more options, visit https://groups.google.com/d/optout.