4

Currently, in django.contrib.auth, there can be two users with the username 'john' and 'John'. How can I prevent this from happening.

The most straightforward approach is add a clean method in contib.auth.models and convert it to a lowercase before saving but i dont want to edit the contrib.auth package.

Thanks.

nknj
  • 2,436
  • 5
  • 31
  • 45

5 Answers5

4

Listen on pre_save for the Users model and then do your checks there. Least intrusive and most portable way.

Here is an example on how this would look like (adapted from the user profile example):

def username_check(sender, instance, **kwargs):
    if User.objects.filter(username=instance.username.lower()).count():
       raise ValidationError('Duplicate username')

pre_save.connect(username_check, sender=User)
Burhan Khalid
  • 169,990
  • 18
  • 245
  • 284
  • Thanks! Im a little new to signals. Where can I place the pre_save to ensure that it is run? I put it in models.py but it causes my post_save signal to stop working! With the pre_save i get the error `create_user_profile() takes exactly 3 arguments (2 given)`. It works fine without the pre-save. – nknj Aug 12 '12 at 14:16
  • 1
    Some people prefer to put them in a separate `signals.py` file. As long as its importable; it doesn't matter. – Burhan Khalid Aug 12 '12 at 14:18
  • This is minor, but wouldn't it be better to return an IntegrityError? This is what Django currently returns for this type of error. – bikemule Feb 21 '18 at 20:30
  • The problem with this is that when users sign up they may have a password manager that saves the uppercase or titlecase username they entered. However this will silently lowercase it, and so mean that they can't later sign in with their saved credentials. Ask me how I know! – bjw Mar 16 '21 at 08:46
2

I have used clean method in the user creation form by extending the class UserCreationForm in my custom CustomerRegistrationForm.

The code for the same is as follows:

class CustomerRegistrationForm(UserCreationForm):
    # All your form code comes here like creating custom field, etc..
    
    ......
    ......
    ......
    ......


    class Meta:
        model = User
        fields = ['username', 'email', 'password1', 'password2']

    ......
    ......
    ......
    ......


    # This is the main centre of interest
    # We create the clean_username field specific cleaner method in order to get and set the required field data
    # In this case, the username field
    def clean_username(self):
        username = self.cleaned_data.get('username')  # get the username data
        lowercase_username = username.lower()         # get the lowercase version of it

        return lowercase_username

This approach is best as we'll not have to write extra messy code for pre_save event in database, and also, it comes with all django validator functionalities like duplicate username check, etc.

Every other thing will be automatically handled by django itself.

Do comment if there needs to be any modification in this code. T

Charitra Agarwal
  • 362
  • 1
  • 11
1

There is a better option if you are using Postgres. Postgres has a case-insensitive field type, citext. As of 1.11, Django has this available in django.contrib.postgres.fields.citext. You may also need to deal with case-sensitivity in the url regular expressions.

bikemule
  • 316
  • 1
  • 9
0

I'd probably solve this on the model using a custom field for username.

from django.db import models


class LowercaseCharField(models.CharField):
    """
    Override CharField to convert to lowercase before saving.
    """
    def to_python(self, value):
        """
        Convert text to lowercase.
        """
        value = super(LowercaseCharField, 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

And then in your model..

from django.contrib.auth.models import AbstractUser
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.utils.translation import gettext_lazy as _

# Assuming you saved the above in the same directory in a file called model_fields.py
from .model_fields import LowercaseCharField

class User(AbstractUser):
    username = LowercaseCharField(
        # Copying this from AbstractUser code
        _('username'),
        max_length=150,
        unique=True,
        help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
        validators=[UnicodeUsernameValidator(),],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )
    # other stuff...

Usernames will all get saved as lowercase "automatically".

getup8
  • 6,949
  • 1
  • 27
  • 31
  • See my comment on solution above... this will cause password managers to fail. – bjw Mar 16 '21 at 08:50
  • @bjw not necessarily, but yes it depends how you handle login of your users. If you implement this, you also need to implement case insensitive login. Luckily if you use something like django allauth, you can just set ACCOUNT_PRESERVE_USERNAME_CASING = False. You can also just override the Django user model. – getup8 Mar 21 '21 at 04:09
0

Updating the accepted answer to be more current:

from django.db.models.signals import pre_save
from django.db.utils import IntegrityError
from django.dispatch import receiver

@receiver(pre_save, sender=User)
def username_check(instance, sender, **kwargs):
    """Ensure that username unique constraint is case insensitive"""
    if sender.objects.filter(username__iexact=instance.username.lower()):
        raise IntegrityError("Duplicate username")
Cal Abel
  • 173
  • 1
  • 4