如何在DRF + django-rest-auth中使用自定义用户模型保存注册时的额外字段?

10
使用Django REST Framework(DRF)和django-rest-auth,我创建了一个带有一个额外字段的自定义用户模型。我的目标是使用django-rest-auth注册端点在一个请求中注册新用户,并发送所有数据以创建新用户,包括额外字段的数据。
我使用AbstractUser,因为它似乎推荐给初学者,更高级的开发人员可以使用AbstractBaseUser。这也是为什么以下SO答案对于我想要实现的内容来说过于复杂:链接在此处
我知道这个问题已经被问了多次,但答案并不完全符合我的需求。对于像我这样的初学者来说,这很复杂。
所以,我的问题是,有人能解释如何实现我想要的吗?
我正在使用:
Django              2.1.4
django-allauth      0.38.0
django-rest-auth    0.9.3
djangorestframework 3.9.0

以下是我目前为止的代码:

使用这个教程到达了这个代码

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

由于你正在使用DRF,因此你应该为你的自定义用户创建一个serializer,然后为该serializer创建一个视图和URL。这个视图可以有get/post/put请求。 - coderDude
2个回答

41

我自己寻找答案。花了一些时间在源代码中挖掘,我意识到这个解决方案可能缺少对添加到自定义用户模型的额外字段进行实际验证的部分,但我将在以后研究它。

我下面写的内容是为了潜在的博客文章。

我假设您知道如何设置DRF项目并安装上述包。django-rest-auth文档清楚地说明了如何安装该包(https://django-rest-auth.readthedocs.io/en/latest/index.html),确保还按照步骤安装django-rest-auth的用户注册部分。

创建一个新的应用程序 'users'

这个应用程序将保存我实现自定义用户模型的自定义代码。我还将其安装在Django主设置文件中:

settings.py:

INSTALLED_APPS = [
    ...
    'users',
]

创建自定义用户模型

需要注意的是,我只添加了一个自定义字段,但您当然可以添加任何想要的字段。

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)

告诉Django使用CustomUser模型

settings.py:

…
AUTH_USER_MODEL = 'users.CustomUser'

在Django管理界面注册自定义用户模型

users.admin.py:

from django.contrib import admin

from .models import CustomUser


admin.site.register(CustomUser)

创建并运行迁移

这是我第一次为该项目执行此操作。

在命令行中:

python manage.py makemigrations users
python manage.py migrate

注册新用户并添加额外字段

如果现在启动Django开发服务器,您将在管理页面中看到自定义用户模型,并带有额外的字段。

但是当您访问“http://127.0.0.1:8000/rest-auth/registration/”时,尚未看到额外的字段。

在用户注册的过程中,使用了两个重要的类,分别是:

  • 一个序列化器'rest_auth.registration.RegisterSerializer'
  • 一个适配器'allauth.account.adapter.DefaultAccountAdapter'

我们将创建这两个的自定义版本,并继承其父类所有功能。

创建自定义RegisterSerializer

在用户应用/文件夹中创建一个新的文件'serializers.py'。

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

在这里,我为自定义用户模型上的每个额外字段创建一个新字段。 所以在我的情况下,我添加了这个:

preferred_locale = serializers.CharField(
        required=False,
        max_length=2,
    )
此外,get_cleaned_data方法应该返回一个字典,其中包含注册新用户时要保存的所有字段的数据。
这是默认RegisterSerializer的原始方法:
def get_cleaned_data(self):
    return {
        'username': self.validated_data.get('username', ''),
        'password1': self.validated_data.get('password1', ''),
        'email': self.validated_data.get('email', '')
    }

正如您所看到的,它返回一个字典,包含新用户的所有数据。对于您添加到自定义用户模型的每个额外字段,您想要向该字典添加一个键值项。

在我的情况下,只需要为字段“preferred_locale”添加数据,这是生成的方法:

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

告诉Django使用此新序列化程序

settings.py:

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

防止错误

如果您尝试注册新用户,可能会在运行开发服务器的控制台中收到以下错误消息:

ConnectionRefusedError: [Errno 111] Connection refused

虽然仍会创建一个用户,但您可以通过将以下行添加到您的settings.py文件中来修复此错误:

settings.py:

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

当您删除用户时,可能会出现另一个错误:

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

要解决这个问题,在您的settings.py中添加以下内容:

settings.py:

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

在此之后,您应该先应用迁移才能继续:

python manage.py migrate

创建自定义的AccountAdapter

在完成上述步骤后,前往“http://127.0.0.1:8000/rest-auth/registration/”会显示额外字段。但是,当您注册一个新用户并发送额外字段的数据时,额外字段的数据不会被保存。

解决这个问题的最后一步是创建一个自定义的AccountAdapter。

在我们的用户应用程序/文件夹中创建一个名为'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

如果您已经正确按照上述步骤操作,那么您可以在form.cleaned_data字典中访问额外添加字段的数据。这是由我们自定义的RegisterSerializer的 get_cleaned_data 方法返回的字典。

在上面的 save_user 方法中,我们可以使用这些数据并将其保存到适当的字段中,如下所示:

user.preferred_locale = data.get('preferred_locale')
告诉 Django 使用这个新的适配器。
settings.py:
ACCOUNT_ADAPTER = 'users.adapter.CustomAccountAdapter'

现在,您可以使用django-rest-auth注册端点'/rest-auth/registration/'注册用户,并发送您添加的额外字段的数据。这将全部保存在一次请求中。

再次强调,需要为每个字段添加自定义验证。但这是另一个话题,我会在后面深入探讨,并在找到确切方法时更新文章。


3
这是一个惊人的解决方案,唯一的问题是我无法通过管理员面板注册用户。具体问题是它没有对密码进行哈希处理。 - Diego Fortes
3
我在Stackoverflow上看到的最佳答案。非常感谢。 - Pavel Antspovich
@DiegoFortes请确保您使用django.contrib.auth.admin.UserAdmin作为基类,而不是django.contrib.admin.ModelAdmin - Dustin Wyatt

4

让我们来解析你的问题。请注意,我正在向您解释Django REST Framework的基础知识。

覆盖用户模型

  • [x] 步骤1:您覆盖User模型:您已经完成了这个步骤。其他选择?是的,创建一个具有指向User模型的OneToOneForeignKey的模型。
  • [x] 步骤2:使用此CustomUserModel。要做到这一点,您需要在settings.py中设置AUTH_USER_MODELLink to official documentation。您已经完成了这个步骤。
  • [ ] 步骤3:创建一个UserManager来处理用户的注册和其他信息。您尚未完成此步骤。

通过API进行注册

  • [ ] 创建一个序列化器(serializer),清楚地提到了你期望从最终用户那里得到的所有必需字段。如果没有自定义字段,甚至可以使用 serializer.ModelSerializer
  • [ ] 在 serializer 中处理显式验证。如有必要,使用 def validate(self, attrs)。这是官方文档链接。
  • [ ] 最后,创建一个视图并使用 APIView,因为你会想要使用上面创建的 UserManager 来注册用户。

我还可以向您介绍我自己构建的应用程序。这是链接:DRF-USER。我对 User 模型进行了一定程度的自定义,并遵循了相同的流程。

希望这可以帮助您。


@Rik 抱歉,我错误地提到您没有完成第二步。请告诉我您在问题中面临的进一步挑战,以便我可以回答并使其成为被接受的答案。 - Himanshu Shankar
1
你的回答很有帮助,但是还有很多问题需要解决。我查看了源代码并自己想出了更详细的解决方案。 - Rik Schoonbeek
@RikSchoonbeek 看到了你的回答。在提供的情况下,这是一个很好的解释性和有效的答案。正如我在我的答案中提到的那样,这更加通用于 djangorestframework,与 allauth 包无关。 - Himanshu Shankar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接