13👍
Addendum 2023:
According to this RFC, the local part of the address before the @
is to be handled by the mail host at its own terms, it mentions nothing about a standard requiring the local part to be lowercase, neither to handle addresses in a case insensitive way, even if most mail servers will do so nowadays.
This means that a mail server actually can discard mails sent to user.name@mail.tld
if the account is set up as User.Name@mail.tld
. This is also reflected in the original normalize_email method of Django’s BaseUser
class, it only forces the domain part to be lowercase for this reason. Considering this, we have to store the local part of the address in is original format.
That being said, we can still enforce a case insensitive uniqueness check before saving the user to the DB. If we make an educated guess, we can assume that even if a mail server was set up to handle addresses case sensitive for historical reasons, that it has started to validate their uniqueness in a case insensitive way to avoid impersonation.
Django 4.0 and beyond
Django 4.0 comes with Functional unique constraints which is probably the most portable and specific solution:
from django.db.models import UniqueConstraint
from django.db.models.functions import Lower
class User:
# ...
class Meta:
constraints = [
UniqueConstraint(
Lower("email"),
name="user_email_ci_uniqueness",
),
]
Django before 4.0
This is not tested and syntax is from memory (also probably you have to display the validation error manually instead on the field because it will be hidden in the form) but it should convey the potential workaround.
from django.db import models
class User:
# ...
email = models.EmailField(
blank=False,
null=False,
)
email_lower = models.EmailField(
blank=False,
null=False,
unique=True,
editable=False,
error_messages={
"unique": "A user is already registered with this email address",
},
)
def clean(self):
super().clean()
self.email_lower = self.email.lower()
Original Post:
You don’t need to change much to accomplish this – in your case you just need to change the form and make use of Django’s built-in form data cleaners or by making a custom field.
You should use the EmailField
instead of a CharField
for built-in validation. Also you did not post your AuthenticationForm, but i presume you have changed it to include email
instead of username
.
With data cleaners:
class SignUpForm(UserCreationForm):
# your code
email = forms.EmailField(required=True)
def clean_email(self):
data = self.cleaned_data['email']
return data.lower()
class AuthenticationForm(forms.Form):
# your code
email = forms.EmailField(required=True)
def clean_email(self):
data = self.cleaned_data['email']
return data.lower()
With a custom field:
class EmailLowerField(forms.EmailField):
def to_python(self, value):
return value.lower()
class SignUpForm(UserCreationForm):
# your code
email = EmailLowerField(required=True)
class AuthenticationForm(forms.Form):
# your code
email = EmailLowerField(required=True)
This way you can make sure that each email is saved to your database in lowercase and that for each login attempt the email is lowercased before compared to a database value.
20👍
A cleaner approach might be to override the model field itself if you don’t mind losing the formatting of how the user entered their email.
This worked better for me because I only had to change it in one place. Otherwise, you might have to worry about Signup, Login, User Update, API views, etc.
The only method you will have to overwrite is to_python
. This will just lowercase anything being saved to the UserModel.email
field.
from django.db import models
class LowercaseEmailField(models.EmailField):
"""
Override EmailField to convert emails to lowercase before saving.
"""
def to_python(self, value):
"""
Convert email to lowercase.
"""
value = super(LowercaseEmailField, self).to_python(value)
# Value can be None so check that it's a string before lowercasing.
if isinstance(value, str):
return value.lower()
return value
Your user model would then just be..
# Assuming you saved the above in the same directory in a file called model_fields.py
from .model_fields import LowercaseEmailField
class UserModel(AbstractBaseUser, PermissionsMixin):
email = LowercaseEmailField(unique=True)
# other stuff...
- Django StaticFiles and Amazon S3: How to detect modified files?
- Django 1.7 makemigrations freezing/hanging
- Django custom login page
- Django run tasks (possibly) in the far future
- Ignore a specific test using Django
2👍
When creating the account entry, interpret the email as email.lower()
to create a normalised entry in your database.
When parsing the email/username from a log in form, you should also use email.lower()
to match the database entry.
Note: the normalize_email
only sanitises the domain portion of an email address:
classmethod normalize_email(email)
Normalizes email addresses by lowercasing the domain portion of the email address.
2👍
For anyone using Postgres and django 2 or higher:
There is a CIEmailField
that stores and retrieves the email address case sensitive but compares it case insensitive. This has the benefit of preserving case in the address (should a user want their address to look like JohnDoe@ImportantCompany.com
) without allowing multiple accounts with the same email.
Usage is simple:
from django.contrib.postgres.fields import CIEmailField
# ...
class User(AbstractBaseUser, ...):
email = CIEmailField(unique=True, ...)
# ...
Except on migrating I got the error:
django.db.utils.ProgrammingError: type "citext" does not exist
To solve this, follow this so answer and set up the CITextExtension
before the first CreateModel
migration operation:
from django.contrib.postgres.operations import CITextExtension
class Migration(migrations.Migration):
...
operations = [
CITextExtension(),
...
]
Note: both in the so answer and in the official documentation is stated that you should add it before the first CreateModel migration operation. For me it was sufficient to add it in the first migration that uses the CIEmailField
.
- Set global minimum logging level across all loggers in Python/Django
- Timeout when uploading a large file?
- Twitter bootstrap typeahead custom keypress ENTER function
- Using django how can I combine two queries from separate models into one query?
0👍
class UserManager(BaseUserManager):
def create_user(self, email, name, tc, password=None):
if not email:
raise ValueError('Users must have an email address')
user = self.model(
email=self.normalize_email(email).lower(),
name = name,
tc = tc
)
user.set_password(password)
user.save(using=self._db)
return user
- No matching distribution found for django
- Django settings Unknown parameters: TEMPLATE_DEBUG
- Joining ManyToMany fields with prefetch_related in Django
- Django verbose request logging
- Django ModelChoiceField: how to iter through the instances in the template?
0👍
The best practice I do is write middleware and small all the URLs in urls.py
class LowercaseMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.path_info = request.path_info.lower()
response = self.get_response(request)
return response
and don’t forget to add it in middlewares in settings.py
MIDDLEWARE = [
...,
'settings.middlewares.LowercaseMiddleware',
]
and also make all the URLs in url.py in lower case.
- Dynamically filter ListView CBV in Django 1.7
- Django Rest Framework 3 Serializers on non-Model objects?
- How to ensure a Celery task is Preventing overlapping Celery task executions
- Query for enum value in GraphQL