8

I've created a custom UserModel and used Email as main authenticating id instead of username.

The problem that it is case sensitive, as it counts test@gmail.com,Test@gmail.com as 2 different accounts.

I need to force it to deal with both as 1 account ignoring if it upper or lower case.

Here are my files :

models.py

class UserModelManager(BaseUserManager):
    def create_user(self, email, password, pseudo):
        user = self.model()
        user.name = name
        user.email = self.normalize_email(email=email)
        user.set_password(password)
        user.save()

        return user

    def create_superuser(self, email, password):
        '''
        Used for: python manage.py createsuperuser
        '''
        user = self.model()
        user.name = 'admin-yeah'
        user.email = self.normalize_email(email=email)
        user.set_password(password)

        user.is_staff = True
        user.is_superuser = True
        user.save()

        return user


class UserModel(AbstractBaseUser, PermissionsMixin):
    ## Personnal fields.
    email = models.EmailField(max_length=254, unique=True)
    name = models.CharField(max_length=16)
    ## [...]

    ## Django manage fields.
    date_joined = models.DateTimeField(auto_now_add=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELD = ['email', 'name']

    objects = UserModelManager()

    def __str__(self):
        return self.email

    def get_short_name(self):
        return self.name[:2].upper()

    def get_full_name(self):
        return self.name

signup view in views.py

def signup(request):
    if request.method == 'POST':
        signup_form = SignUpForm(request.POST)
        if signup_form.is_valid():
            signup_form.save()
            username = signup_form.cleaned_data.get('username')
            raw_password = signup_form.cleaned_data.get('password1')
            user = authenticate(username=username, password=raw_password)
            return redirect('signup_confirm')
    else:
        signup_form = SignUpForm()

    context = {
        'signup_form': signup_form,
    }
    return render(request, 'fostania_web_app/signup.html', context)


def signup_confirm(request):
    return render(request, 'fostania_web_app/signup_confirm.html')

the sign up form in forms.py:

class SignUpForm(UserCreationForm):
    email = forms.CharField(required=True, help_text='البريد الإلكترونى الخاص بك - يجب ان يكون حقيقى (يستخدم لتسجيل الدخول) ')
    name = forms.CharField(required=True, help_text='إسمك الحقيقى -  سيظهر كأسم البائع')
    password1 = forms.CharField(widget=forms.PasswordInput,
                                help_text='كلمة المرور - حاول ان تكون سهلة التذكر بالنسبة لك')
    password2 = forms.CharField(widget=forms.PasswordInput,
                                help_text='تأكيد كلمة المرور - إكتب نفس كلمة المرور السابقة مرة أخرى')

    class Meta:
        model = UserModel
        fields = ('email','name',  'password1', 'password2', )
        labels = {
            'name': 'إسمك الحقيقى -  سيظهر كأسم البائع',
            'email': 'البربد الإلكترونى Email',
            'password1': 'كلمة المرور',
            'password2': 'تأكيد كلمة المرور'
        }

All that I need now is to make it simply ignores the case sensitivity.

update

here is my login files

urls.py

   path('login/', auth_views.login, name='login'),

registartion/login.html

    <form method="post">
    {% csrf_token %}
    البريد الإلكترونى E-Mail &nbsp;<Br>{{ form.username }}
                    <br><br>
                     كلمة المرور &nbsp;<Br>{{ form.password }}
                    <Br><br>
      <div align="center">
    <button class ="btn btn-primary" submit>تسجيل الدخول</button><br><br>
      <a href="{% url 'signup' %}"><button type="button" class="btn btn-warning">
          إنشاء حساب جديد</button></a>
          </div>

  </form>
iacob
  • 14,010
  • 5
  • 54
  • 92
Ahmed Wagdi
  • 2,953
  • 6
  • 34
  • 83
  • You write `pseudo` instead of `password` in your first function? – Willem Van Onsem Jun 17 '18 at 10:15
  • The most obvious way is - when user registers - force his email to `.lower()`. And the same when user tries to authenticate again. Did u try that? – Chiefir Jun 17 '18 at 10:17
  • @WillemVanOnsem Didn't notice that I didn't change it, as I found the whole function in an online guide. but authentication is working fine with it written as pseudo, wil it affects the function in some point? – Ahmed Wagdi Jun 17 '18 at 10:20
  • @Chiefir the problem when the user logs in from a mobile device, by default android keyboard will change the first latter to capital, which will cause an error ( wrong email). – Ahmed Wagdi Jun 17 '18 at 10:22
  • 2
    but that is exactly what I mean - you handle user email in your code and convert it all to the lower case, does not matter how user provided it. – Chiefir Jun 17 '18 at 10:23
  • Read this: https://stackoverflow.com/a/34095774 – djvg Apr 13 '22 at 07:57
  • and [this warning](https://django-improved-user.readthedocs.io/en/stable/email_warning.html) from a package providing similar functionality. – djvg Apr 13 '22 at 08:08

4 Answers4

13

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...
jojo
  • 9,063
  • 2
  • 47
  • 69
getup8
  • 5,172
  • 1
  • 25
  • 29
6

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.

wiesion
  • 2,160
  • 10
  • 20
  • Great I think that is the detailed answer, It worked with the signup form, but the problem that I don't have a view for the login view, as it only needs the from in the template: `project/registration/login.html` – Ahmed Wagdi Jun 17 '18 at 10:43
  • 1
    You can inherit from Django's `AuthenticationForm` the same way you do for `UserCreationForm` – wiesion Jun 17 '18 at 11:19
  • Great , I did it but it shows an extra email field, can you please check it in here https://stackoverflow.com/questions/16428336/django-authentication-custom-login-page – Ahmed Wagdi Jun 17 '18 at 12:12
  • You mean you have now 2 email fields on the login page? And how is that question related? – wiesion Jun 17 '18 at 13:04
  • Sorry wrong link posted ,, here is the link https://stackoverflow.com/questions/50896287/django-custom-login-form-show-extra-field also the view is not working , when i submit it only refresh the page – Ahmed Wagdi Jun 17 '18 at 13:06
  • I think going in-depth on this matter takes really a lot of time - i suggest you look into the source code of [django-email-authentication](https://github.com/tkaemming/django-email-authentication) - or just install it :) – wiesion Jun 17 '18 at 13:17
  • Out of curiosity. Is `to_python` the best function to override when creating `EmailLowerField`? In another answer by Danil, he used `get_prep_value` instead of `to_python`. Any thoughts on that? https://stackoverflow.com/a/49181581 – Valachio Sep 15 '20 at 05:24
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.

Community
  • 1
  • 1
iacob
  • 14,010
  • 5
  • 54
  • 92
  • 1
    There's a [reason](https://code.djangoproject.com/ticket/5605) django only lowercases the domain part. From [RFC5321](https://www.rfc-editor.org/rfc/rfc5321#section-2.4): "The local-part of a mailbox MUST BE treated as case sensitive. ... In particular, for some hosts, the user "smith" is different from the user "Smith"." – djvg Apr 13 '22 at 08:02
1

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.