Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to save extra fields on registration using custom user model in DRF + django-rest-auth

Using Django REST Framework (DRF), with django-rest-auth, I created a custom user model with one extra field. My aim is to use the django-rest-auth registration endpoint to register a new user in one request, and thus sending all the data to create a new user, including the data for the extra field.

I am using AbstractUser, since it seems recommended for beginners, where more advanced developers could use AbstractBaseUser. This is also why the following SO answers looks too complicated for what I want to achieve: link here.

I know this question has been asked multiple times, but the answers are not exactly what I am looking for. For a beginner like me this is complicated stuff.

So, my question is, can anyone explain how to achieve what I want?

I am using:

Django              2.1.4
django-allauth      0.38.0
django-rest-auth    0.9.3
djangorestframework 3.9.0

Here's the code that I have up until this point:

Used this tutorial to get to this code

settings.py:

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '!gxred^*penrx*qlb=@p)p(vb!&6t78z4n!poz=zj+a0_9#sw1'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'rest_framework',
    'rest_framework.authtoken',

    'rest_auth',

    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'rest_auth.registration',

    'users',
]

SITE_ID = 1

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'DRF_custom_user.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'DRF_custom_user.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/

STATIC_URL = '/static/'

AUTH_USER_MODEL = 'users.CustomUser'

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

users.models.py:

from django.contrib.auth.models import AbstractUser
from django.db import models


class CustomUser(AbstractUser):
    preferred_locale = models.CharField(blank=True, null=True, max_length=2)

users.admin.py:

from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin

from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser

class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    model = CustomUser
    list_display = ['email', 'preferred_locale']

admin.site.register(CustomUser, CustomUserAdmin)

users.forms.py:

from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser


class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm):
        model = CustomUser
        fields = ('email', )


class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = UserChangeForm.Meta.fields
like image 812
Rik Schoonbeek Avatar asked Dec 29 '18 12:12

Rik Schoonbeek


2 Answers

Let's break your question. Please note that I am explaining the basics of Django REST Framework to you.

Overriding User's Model

  • [x] Step 1: You override User model: You did this. Alternative? Yes. Create a model that has OneToOneForeignKey pointed to User model.
  • [x] Step 2: Use this CustomUserModel. To do this, you need to set AUTH_USER_MODEL in settings.py Link to official documentation You did this.
  • [ ] Step 3: Create a UserManager to handle user's registration and other information. You haven't done this.

Registration from API

  • [ ] Create a serializer that clearly mentions all the required fields that you're expecting from an end user. You can even use serializer.ModelSerializer if there are no custom fields.
  • [ ] Handle explicit verifications in serializer itself. Use def validate(self, attrs) if required. Here is the official document link.
  • [ ] Finally, create a view and use APIView as you will want to register a user using UserManager that you created above.

I can also refer to you an app that I built myself. Here's the link: DRF-USER. I customized User model to some extent and followed the same process.

Hope this helps.

like image 67
Himanshu Shankar Avatar answered Sep 28 '22 03:09

Himanshu Shankar


I went looking for an answer myself. Spend some time digging in the source code. I realize this solution may be missing the actual validation for the extra fields added to the custom user model, but I will look into that later.

What I have written below I wrote with a potential blog post in mind.

I am going to assume you know how to set up a DRF project and install the above packages. The django-rest-auth documentation is clear on how to install that package (https://django-rest-auth.readthedocs.io/en/latest/index.html), make sure to also follow the steps to install the part of django-rest-auth for user registration.

Create a new app ‘users’

This app will hold my custom code for implementing the custom user model. I also install it in the Django main settings file:

settings.py:

INSTALLED_APPS = [
    ...
    'users',
]

Create my custom user model

Notice that I just added one custom field, but you can add whatever fields you want ofcourse.

users.models.py:

from django.contrib.auth.models import AbstractUser
from django.db import models


class CustomUser(AbstractUser):
    preferred_locale = models.CharField(max_length=2, blank=True, null=True)

Tell django to use the CustomUser model

settings.py:

…
AUTH_USER_MODEL = 'users.CustomUser'

Register Custom user model at Django admin

users.admin.py:

from django.contrib import admin

from .models import CustomUser


admin.site.register(CustomUser)

Make migrations and run them

This is the first time I do this for this project.

In command line:

python manage.py makemigrations users
python manage.py migrate

Registering new users with extra fields

If you start the Django development server now, you’ll see in the admin that you can see the custom user model, with the extra fields.

But when you go to ‘http://127.0.0.1:8000/rest-auth/registration/’ you don’t see the extra fields yet.

In the process of user registration two important classes are used, namely:

  • a serializer ‘rest_auth.registration.RegisterSerializer’
  • an adapter ‘allauth.account.adapter.DefaultAccountAdapter’

We’ll make a custom version of both of these, inheriting all the functionality of it’s parent class.

Creating a custom RegisterSerializer

Create a new file ‘serializers.py’ in the users app/folder.

users.serializers.py:

from rest_framework import serializers

from allauth.account.adapter import get_adapter
from allauth.account.utils import setup_user_email

from rest_auth.registration.serializers import RegisterSerializer


class CustomRegisterSerializer(RegisterSerializer):
    preferred_locale = serializers.CharField(
        required=False,
        max_length=2,
    )

    def get_cleaned_data(self):
        data_dict = super().get_cleaned_data()
        data_dict['preferred_locale'] = self.validated_data.get('preferred_locale', '')
        return data_dict

Here I create a new field for each extra field on the custom user model. So in my case a added this:

preferred_locale = serializers.CharField(
        required=False,
        max_length=2,
    )

Also, the get_cleaned_data method should return a dict which contains all the data for the fields that you want to have saved when registering a new user.

This is what the original method (of the default RegisterSerializer looks like):

def get_cleaned_data(self):
    return {
        'username': self.validated_data.get('username', ''),
        'password1': self.validated_data.get('password1', ''),
        'email': self.validated_data.get('email', '')
    }

As you can see it returns a dictionary, containing all the data for the new user. You want to add a keyval entry to this dictionary for each extra field you have added to your custom user model.

In my case, needing only to add data for the field ‘preferred_locale’, this is the resulting method:

def get_cleaned_data(self):
    data_dict = super().get_cleaned_data()
    data_dict['preferred_locale'] = self.validated_data.get('preferred_locale', '')
    return data_dict

Tell django to use this new serializer

settings.py:

REST_AUTH_REGISTER_SERIALIZERS = {
    'REGISTER_SERIALIZER': 'users.serializers.CustomRegisterSerializer',
}

Preventing errors

If you will try to register a new user, you might get the following error in the console where your development server is running: ConnectionRefusedError: [Errno 111] Connection refused

Although a user is still created, you can fix this error by adding the following line to your settings.py file:

settings.py:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Another error that will occur when you delete a user is:

django.db.utils.OperationalError: no such table: allauth_socialaccount

To solve this, add this to your settings.py:

settings.py:

INSTALLED_APPS = [
    ...
    'allauth.socialaccount',  
]

After that, you should apply migrations before you can continue:

python manage.py migrate

Creating a custom AccountAdapter

After the above steps, going to ‘http://127.0.0.1:8000/rest-auth/registration/’ will show you the extra fields. But when you register a new user, and send the data for the extra fields, the extra field’s data is not saved.

The last thing we need to do to solve this is to create a custom AccountAdapter

In our users app/folder create a new file named ‘adapter.py’:

users.adapter.py:

from allauth.account.adapter import DefaultAccountAdapter


class CustomAccountAdapter(DefaultAccountAdapter):

    def save_user(self, request, user, form, commit=False):
        user = super().save_user(request, user, form, commit)
        data = form.cleaned_data
        user.preferred_locale = data.get('preferred_locale')
        user.save()
        return user

Here, if you have followed the above steps correctly, you can access the data of the extra added fields in the form.cleaned_data dictionary. This is the dictionary that is returned by the get_cleaned_data method from our custom RegisterSerializer.

In the save_user method above, we can then use this data and save it to the appropriate fields, like so:

user.preferred_locale = data.get('preferred_locale')

Tell Django to use this new adapters

settings.py:

ACCOUNT_ADAPTER = 'users.adapter.CustomAccountAdapter'

Now you can register your user, using the django-rest-auth registration endpoint '/rest-auth/registration/', and send the data for the extra fields you added. This will all be saved in one request.

Again, I realize that custom validation for each field needs to be added. But that's another topic that I will dive into later, and update the post when I found out how that works precisely.

like image 45
Rik Schoonbeek Avatar answered Sep 28 '22 04:09

Rik Schoonbeek