Django REST Framework: force lowercase username - django

I am just starting to play around with Django REST Framework for a side project, and I am trying to force all usernames to be lowercase (since by default they are case sensitive and I don't want people to have usernames like kevin and Kevin for example).
This is the code I am using to create a user:
# The serializer
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
token = serializers.SerializerMethodField()
def create(self, validated_data):
user = get_user_model().objects.create(**validated_data)
user.set_password(validated_data['password'])
user.save()
return user
def get_token(self, user):
token = Token.objects.create(user=user)
return token.key
class Meta:
model = get_user_model()
fields = ('username', 'first_name', 'last_name', 'email', 'password', 'token')
# The View
class RegisterUserView(CreateAPIView):
model = User
serializer_class = UserSerializer
permission_classes = (permissions.AllowAny, )
# And the URL pattern
urlpatterns = [
path(r'user/register/', RegisterUserView.as_view()),
]
So the user can create a new account by posting at least a username and password to user/register, and in the response he'll get the full user object (with first name, last name, email and the auth token). This works.
However, I am struggling with forcing lowercase usernames. For example when I add something like validated_data['username'] = validated_data['username'].lower() to the create function in the serializer, the server simply generates an error 500 "UNIQUE constraint failed: auth_user.username" when trying to create a user with the same username (but different case). That's of course not ideal, that error 500.
I've found some suggestions to add a custom user manager, something like this:
lass MyUserManager(BaseUserManager):
def get_by_natural_key(self, username):
return self.get(username__iexact=username)
But that didn't do anything either, after hooking it up via a custom user class and adding to the settings file. I could still create users with capitals in their username.
What's the simplest solution to get this to simply work?

If you're using a custom user model and want this enforced throughout Django, you can create a custom validator. For example, create a validators.py as a sibling to the models.py which contains your custom user model:
from django.core import validators
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
#deconstructible
class MyUsernameValidator(validators.RegexValidator):
"""
Validator for usernames.
- Only ASCII lowercase letters, numbers and underscore are supported.
- Must start with a letter.
"""
regex = r'^[a-z][a-z0-9_]+$'
message = _(
'Enter a valid username. This value may contain only lowercase ASCII letters, '
'numbers, and underscores. Must start with a letter.'
)
flags = 0
Then in your custom user model, include:
from .validators import MyUsernameValidator
...
class User(AbstractBaseUser, PermissionsMixin):
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = [
'first_name', 'last_name', 'email',
]
username = models.CharField(
max_length=16,
unique=True,
help_text=_(
'Required. 16 characters or fewer. Lowercase letters, digits _ only; must start with a letter.'
),
validators=[MyUsernameValidator()],
error_messages={
'unique': _("A user with that username already exists."),
},
)
There are some slightly simpler methods, but this will enforce it through Django, as long as you stick to the ORM. Good luck!

Probably the reason you are getting "UNIQUE constraint failed: auth_user.username" error is because username validation is run on unprocessed version of the username (i.e with uppercase letters), but when inserting you are trying to create a user with username converted to lowercase. To exemplify, let's say you have a user in the db with the following username:
my_username
Then you are trying to create a user with the following username:
My_Username
This passes validation because there is no user in the db with username "My_Username", but while creating, you are trying to create a user with username "my_username", which exists in the database, so you are getting an IntegrityError.
There are many ways to solve this problem, you can modify the username before passing it to the serializer, so in the serializer context username will always be lowercase. You can also use a custom ModelManager as you tried already, but you'd need to convert the uesrname to lowercase in the manager before saving. Either way, you need to validate your data with the correct version of the username, to do this you can add a validation to your serializer like this:
class UserSerializer(serializers.ModelSerializer):
...
validate_username(self, value):
try:
get_user_model().objects.get(username=value.lower())
except ObjectDoesNotExist:
# Good to go
pass
else:
# Username in use
raise serializers.ValidationError(...)
return value

Related

Temporary users in Django

I would like to implement temporary users in my project. The reason is that people should be able to auth in my application via Facebook Oauth or Facebook Account Kit.
I've got 2 decisions, but each one is not ideal.
1) Create User and TemporaryUser models. The first one will have all info about regular user + unique db fields. This TemporaryUser will have only phone_number or facebook_id. Temporary user will be created on auth/ endpoint and response will have auth_token to be able perform registration later with all needed fields.
The issue is: how will I determine that user is temporary and his auth_token is legit only for registration/ endpoint?
2) Create basic User model with 2 types: regular or temporary. This model will have only general fields. Also there will be different models with OneToOne relation with User. Only Users with type regular will be able to have that instances with OneToOne relation.
The issue is: User model should have USERNAME_FIELD , REQUIRED_FIELDS to be able to login by admin panel + users with different types should have different managers.
UPDATED
class User(AbstractBaseUser, TimeStampedModel, PermissionsMixin):
is_regular = models.BooleanField(
default=False
)
id = models.CharField(
max_length=11,
primary_key=True,
default=custom_uuid,
editable=False
)
phone_number = PhoneNumberField(
null=True
)
facebook_id = models.CharField(
max_length=255,
null=True
)
objects = UserManager()
USERNAME_FIELD = 'phone_number'
REQUIRED_FIELDS = []
From discussion, there are many ways to tackle this issue. But using two different models for authentication purpose is a bad idea. Because Django Auth was designed assuming one User Model.
This is how I would have approached:
Rather than using facebook_id, I would have stored email from facebook's graph API. While creating the password, I would have used an unique UUID.
Put a temporary flag(a Model Boolean Field) to track if the user is registered through social media. So when he tries to login to normal path or registration, I would know that he does not have a password, so I would send him to reset his password. After successful reset password, I would have removed that temporary flag.
Other information which are absolute necessary, I would have used a separate model to store them. For example:
class Profile(models.Model):
...
phone_number = models.CharField(max_length=30)
user = models.OneToOneField(settings.AUTH_USER_MODEL)
And checked with login that if the User has a profile or not:
if not hasattr(user, 'profile'):
# redirect to more information page
For other permissions for accessing admin site should be restricted creation through normal registration or social registration. For these users, those fields(is_staff, is_superuser etc should be False by default. For admin user creation, you can use createsuperuser command. For staff users, you can later assign a normal user to staff by making the is_staff flag True from adminsite or Django Shell.
Update(from comments...)
You can use custom Backend for authenticating facebook user:
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
class CustomModelbackend(ModelBackend):
def authenticate(self, phone_no=None, password=None, facebook_id=None, **kwargs):
# Override
UserModel = User
try:
user = UserModel.objects.get(Q(phone_no=phone_no) | Q(facebook_id=facebook_id))
if password and user.check_password(password):
return user
elif facebook_id:
return user
except Exception as e:
# log exception
and add it to AUTHENTICATION_BACKENDS in settings.py:
AUTHENTICATION_BACKENDS = ['path.to.custom_backend.CustomBackend']
and call authenticate method like this:
authenticate(phone_no=phone_number, password=password) # for normal auth
authenticate(facebook_id=facebook_id) # for social auth
But you can't make your phone_no unique as there is chance of the phone_no being empty for social login, but you can put it in USERNAME_FIELD. So you will see warnings when you run django developer server(runserver command)
Update (2)
You can try like this:
NORMAL_USER = "N"
OAUTH_USER = "O"
AT_USER = "A"
USER_TYPES=(
(NORMAL_USER, 'Normal User'),
(OAUTH_USER, 'Oauth User'),
(AT_USER, 'Account Toolkit User')
)
class User(...):
username = models.CharField(max_length=255, default=uuid.uuid4, unique=True) # it might be unnecessary
account_type = models.CharField(max_length=2, choices = USER_TYPES, default=NORMAL_USER)
identifier = models.CharField(max_length=255)
...
class Meta:
unique_togather = ('account_type', 'identifier',)
# for Oauth Creation
User.objects.create(account_type=OAUTH_USER, identifier=facebook_id) # or email
# for Toolkit Creation
User.objects.create(account_type=AT_USER, identifier=phone_number)
# For normal User
User.objects.create(identifier=username, username=username)
#backend, and does not need to use ModelBackend, it will work with adminsite
class CustomBackend(...):
def authenticate(self, **kwargs):
try:
identifier= kwargs.get('username')
password=kwargs.get('password')
account_type=kwargs.get('account_type', NORMAL_USER)
user = User.objects.get(identifier=identifier, account_type=account_type)
if user.check_password(password):
return user
except Exception:
# log exception
# authentication
from django.contrib.auth import authenticate
user = authenticate(username=identifier, password=password, account_type=OAUTH_USER)

Django not searching correct table name for custom user model

I'm trying to use a custom user model in my Django project. After running migrations, the database table is named accounts_listuser while Django appears to be looking for them in accounts_user. How can I change this so Django looks at the correct table (accounts_user) for the user model instead of accounts_listuser?
In my settings.py:
# Auth user models
AUTH_USER_MODEL = 'accounts.User'
AUTHENTICATION_BACKENDS = (
'accounts.authentication.PersonaAuthenticationBackend'
)
The accounts/models.py file:
class User(models.Model):
email = models.EmailField(primary_key=True)
last_login = models.DateTimeField(default=timezone.now)
REQUIRED_FIELDS = ()
USERNAME_FIELD = 'email'
is_authenticated = True
is_anonymous = False
And the file (accounts/authentication.py) where the query fails:
from django.contrib.auth import get_user_model
User = get_user_model()
# .. user's email retrieved ..
def get_user(self, email):
try:
return User.objects.get(email=email)
except User.DoesNotExist:
return None
The full source code can be found here if it helps.
Not sure what you mean by correct table in this case, but this behavior sounds about right since your model is called user and is declared in an app named accounts.
You can overwrite the auto-generated table name using class Meta attribute db_table: https://docs.djangoproject.com/en/1.10/ref/models/options/#db-table
Also just a note, the django docs recommend that your custom user model inherits from AbstractBaseUser, otherwise you will be responsible for implementing quite a few things on your own. https://docs.djangoproject.com/en/1.10/topics/auth/customizing/#specifying-a-custom-user-model

How enforce filling up the user profile after first social login in django-allauth?

I've created UserProfile model in my application:
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
primary_key=True,
verbose_name=_('user'),
related_name='user_profile')
locality = models.CharField(max_length=85)
voivodship = models.CharField(max_length=20,
choices=Vovoidship.choices,
validators=[Vovoidship.validator])
postcode = models.IntegerField()
street = models.CharField(max_length=75)
building = models.SmallIntegerField()
premises = models.CharField(max_length=80)
phonenumber = PhoneNumberField(blank=True)
#staticmethod
def post_save_create(sender, instance, created, **kwargs):
if created:
profile, created = UserProfile.objects.get_or_create(user=instance)
post_save.connect(UserProfile.post_save_create, sender=User)
Now, I felt into my own trap. I don't want to loose constraints and keep the requirement in the database that address fields need to be filled up. I'm using django-allauth. While using the setting ACCOUNT_SIGNUP_FORM_CLASS = 'management.forms.SignupForm' solves the problem for traditional sign up form, if the user logs in first time using the social account I got hit by constraint violation for obvious reasons:
IntegrityError at /accounts/google/login/callback/
null value in column "postcode" violates not-null constraint
DETAIL: Failing row contains (4, , , null, , null, , ).
Hence the question, how to correctly implement the request for filling up the information for fields in the application UserProfile? I'm surprised that django-allauth doesn't have a build in handler for that the same way as ACCOUNT_SIGNUP_FORM_CLASS is done.
As I'm new to Django please assume I rather don't know something than it should be obvious. Thanks.
I think you need to:
1.- Create your custom Signup Class, for you to do the additional work
class SignupForm(forms.Form):
locality = forms.CharField(max_length=85)
voivodship = forms.CharField(max_length=20)
postcode = forms.IntegerField()
etc.
def signup(self, request, user):
# I think the profile will exist at this point based on
# your post_save_create. But if not, just create it here
if user.user_profile:
user.user_profile.locality = self.cleaned_data['locality']
user.user_profile.voivodship = self.cleaned_data['voivodship']
user.user_profile.postcode = self.cleaned_data['postcode']
...
user.user_profile.save()
2.- Set ACCOUNT_SIGNUP_FORM_CLASS = 'yourproject.yourapp.forms.SignupForm' to have allauth use your form
3.- Set SOCIALACCOUNT_AUTO_SIGNUP=False to ensure the form is presented even with social signup.
With some credits to davka I've managed to form a working solution which required creating UserProfile object inside signup() method of the SignupForm class, but because of database/model constrains it has be be filled with data during creation. The result:
class SignupForm(ModelForm):
first_name = CharField()
last_name = CharField()
class Meta:
model = UserProfile
exclude = ['user', 'phonenumber']
def signup(self, request, user):
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
profile, created = UserProfile.objects.get_or_create(
user=user, defaults={
'locality': self.cleaned_data['locality'],
'voivodship': self.cleaned_data['voivodship'],
'postcode': self.cleaned_data['postcode'],
'street': self.cleaned_data['street'],
'building': self.cleaned_data['building'],
'premises': self.cleaned_data['premises'],
})
if created: # This prevents saving if profile already exist
profile.save()
The solution doesn't totally fit into DRY principle, but shows the idea. Going further it could probably iterate over results matching model fields.
Two elements need to be set correctly in settings.py:
ACCOUNT_SIGNUP_FORM_CLASS = 'yourapp.forms.SignupForm' to enable this form in allauth
SOCIALACCOUNT_AUTO_SIGNUP = False this - contrary to the intuition - let the allauth display the form before finishing the signup if the user selected social sign in but don't have an account; it works safely if the account already exists (username and/or e-mail address depending on other settings) as just does't allow to finish registration (why they call it sign up?) and the user is forced to log in and link social account.

Password fields rendering as plain text in documentation

I am using Django rest framework 3.1 and django-rest-swagger 0.3.2. Things are working fairly well however I am having an issue with the password input fields in my login serializer. My login serializer is pretty simple, it inherits from rest_framework.authtoken.serializers.AuthTokenSerializer:
class MyLoginSerializer(AuthTokenSerializer):
def validate(self, attrs):
# small amount of validation logic here
The AuthTokenSerializer has the password field defined with the proper style:
class AuthTokenSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField(style={'input_type': 'password'})
But Swagger displays the password input in the POST documentation as plain text (input type='text'). I'm not sure what is going wrong here, shouldn't django-rest-swagger be able to interpret the style here and render the input accordingly? Am I missing something? Any advice is appreciated, thanks much!
I'm still using DRF 2.4.4, but this is what I have to * out the field:
from django.forms.widgets import PasswordInput
class UserSerializer(serializers.HyperlinkedModelSerializer):
password = serializers.CharField(
required=False,
write_only=True, # <- never send password (or hash) to the client
allow_none=True, # <- allow users to not specify a password
blank=True,
# this creates a password input field for DRF-only
# front end (not sure about swagger)
# vvvvvvvvvvvvvvvvvv
widget=PasswordInput
)
class Meta:
model = User
fields = ('url', 'id', 'username', 'password', [...]

Disabling email-style usernames in Django 1.2 with django-registration

Django 1.2 allows usernames to take the form of an email address.
Changed in Django 1.2: Usernames may
now contain #, +, . and - characters
I know that's a much-requested feature, but what if you don't want the new behavior? It makes for messy usernames in profile URLs and seems to break django-registration (if a user registers an account with an email-style username, the link in the django-registration activation email returns 404).
Does anyone have a recipe for restoring the old behavior and disabling email-style usernames?
There is no straightforward way to revert back to old behavior.
The easiest way to handle this would be to enforce client and server side validation of usernames according to your demands. django-registration is not an actively developed component, I wouldn't count on anything coming from that direction. Just add some extra validation on your side.
To quote Jacob on this matter:
[...] another common request is to allow the
use of email addresses as usernames.
Custom registration/signup forms can
deal with further restrictions.
django-registration actually wasn't the problem here. The problem was that I had subclassed its RegistrationForm, redefining the username field with new help_text. In so doing, I had prevented it from using its own regex field. To fix this, I had to pull a few bits and pieces from RegistrationForm into my EnhancedRegistrationForm subclass.
Note the regex line, which mirrors the old-style username character restrictions (which is what I want).
from registration.forms import RegistrationForm
# Carry these over from RegistrationForm - needed in the form definition below
attrs_dict = {'class': 'required'}
from django.utils.translation import ugettext_lazy as _
class EnhancedRegistrationForm(RegistrationForm):
first_name = forms.CharField(label='first name', max_length=30, required=True)
last_name = forms.CharField(label='last name', max_length=30, required=True)
username = forms.RegexField(regex=r'^\w+$',
max_length=30,
widget=forms.TextInput(attrs=attrs_dict),
help_text='Email addresses cannot be used as usernames.',
required=True,
label=_("Username"),
error_messages={'invalid':"You cannot use an email address as a username, sorry."})
class Meta:
fields = ('first_name','last_name','username','email','password1','password2')
def save(self, *args, **kwargs):
"""
Overriding save, so call the parent form save and return the new_user
object.
"""
new_user = super(EnhancedRegistrationForm, self).save(*args, **kwargs)
new_user.first_name = self.cleaned_data['first_name']
new_user.last_name = self.cleaned_data['last_name']
new_user.save()
return new_user