Создание пользовательской модели User в Django

Как полностью заменить поле имени пользователя на поле электронной почты для аутентификации в Django?

Этот пост пошагово объясняет, как создать модель пользователя User в Django, чтобы адрес электронной почты использовался в качестве основного идентификатора пользователя вместо имени пользователя для аутентификации.

Не забывайте, что процесс, описанный в этом посте, требует значительных изменений в схеме базы данных. Поэтому он рекомендуется только для новых проектов. Если речь идет о существующем проекте, то вам, скорее всего, придется сделать резервную копию данных и заново создать базу данных. Чтобы узнать больше об этом, просмотрите следующие ресурсы:

  1. Замена пользовательской модели пользователя руководство из официальной документации Django
  2. Переход на пользовательскую модель пользователя в Django статья в блоге

.

Цели

К концу этой статьи вы должны уметь:

  1. Опишите разницу между AbstractUser и AbstractBaseUser
  2. Объясните, почему вам следует настроить собственную модель пользователя при запуске нового проекта Django.
  3. Запустите новый проект Django с пользовательской моделью пользователя.
  4. Используйте адрес электронной почты в качестве основного идентификатора пользователя вместо имени пользователя для аутентификации.
  5. Практика тестирования - первая разработка при реализации пользовательской модели пользователя.

AbstractUser или AbstractBaseUser

По умолчанию модель User в Django использует имя пользователя для уникальной идентификации пользователя при аутентификации. Если вы предпочитаете использовать адрес электронной почты, вам необходимо создать пользовательскую модель User, используя подкласс AbstractUser или AbstractBaseUser.

Варианты:

  1. AbstractUser: Используйте этот вариант, если вас устраивают существующие поля в модели User и вы просто хотите удалить поле имени пользователя.
  2. AbstractBaseUser: Используйте эту опцию, если вы хотите начать с нуля, создав свою собственную, совершенно новую модель User.

В этом посте мы рассмотрим оба варианта: AbstractUser и AbstractBaseUser.

Шаги для каждого из них одинаковы:

  1. Создайте собственную модель пользователя и менеджера.
  2. Обновите settings.py
  3. Настроить формы UserCreationForm и UserChangeForm
  4. Обновите админку

Начиная новый проект Django, настоятельно рекомендуется установить пользовательскую модель User. Без нее вам придется создать другую модель (например, UserProfile) и связать ее с моделью Django User с помощью OneToOneField, если вы хотите добавить новые поля в модель User.

.

Настройка проекта

Начните с создания нового проекта Django вместе с пользовательским приложением:

$ mkdir django-custom-user-model && cd django-custom-user-model
$ python3 -m venv env
$ source env/bin/activate

(env)$ pip install Django==3.2.2
(env)$ django-admin startproject hello_django .
(env)$ python manage.py startapp users

НЕ применяйте миграции. Помните: вы должны создать пользовательскую модель User до применения первой миграции.

Добавьте новое приложение в список INSTALLED_APPS в settings.py:

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

    'users',
]

Тесты

Давайте воспользуемся подходом, основанным на тестировании:

from django.contrib.auth import get_user_model
from django.test import TestCase


class UsersManagersTests(TestCase):

    def test_create_user(self):
        User = get_user_model()
        user = User.objects.create_user(email='normal@user.com', password='foo')
        self.assertEqual(user.email, 'normal@user.com')
        self.assertTrue(user.is_active)
        self.assertFalse(user.is_staff)
        self.assertFalse(user.is_superuser)
        try:
            # username is None for the AbstractUser option
            # username does not exist for the AbstractBaseUser option
            self.assertIsNone(user.username)
        except AttributeError:
            pass
        with self.assertRaises(TypeError):
            User.objects.create_user()
        with self.assertRaises(TypeError):
            User.objects.create_user(email='')
        with self.assertRaises(ValueError):
            User.objects.create_user(email='', password="foo")

    def test_create_superuser(self):
        User = get_user_model()
        admin_user = User.objects.create_superuser(email='super@user.com', password='foo')
        self.assertEqual(admin_user.email, 'super@user.com')
        self.assertTrue(admin_user.is_active)
        self.assertTrue(admin_user.is_staff)
        self.assertTrue(admin_user.is_superuser)
        try:
            # username is None for the AbstractUser option
            # username does not exist for the AbstractBaseUser option
            self.assertIsNone(admin_user.username)
        except AttributeError:
            pass
        with self.assertRaises(ValueError):
            User.objects.create_superuser(
                email='super@user.com', password='foo', is_superuser=False)

Добавьте спецификации в файл users/tests.py, а затем убедитесь, что тесты не работают.

Менеджер моделей

Сначала нам нужно добавить пользовательский менеджер, подкласс BaseUserManager, который использует электронную почту в качестве уникального идентификатора вместо имени пользователя.

Создайте файл manager.py в папке "users" каталог:

from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import ugettext_lazy as _


class CustomUserManager(BaseUserManager):
    """
    Custom user model manager where email is the unique identifiers
    for authentication instead of usernames.
    """
    def create_user(self, email, password, **extra_fields):
        """
        Create and save a User with the given email and password.
        """
        if not email:
            raise ValueError(_('The Email must be set'))
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **extra_fields):
        """
        Create and save a SuperUser with the given email and password.
        """
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError(_('Superuser must have is_staff=True.'))
        if extra_fields.get('is_superuser') is not True:
            raise ValueError(_('Superuser must have is_superuser=True.'))
        return self.create_user(email, password, **extra_fields)

Модель пользователя

Решите, какой вариант вы хотите использовать: создание подкласса от AbstractUser или AbstractBaseUser.

AbstractUser

Обновите users/models.py:

from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import ugettext_lazy as _

from .managers import CustomUserManager


class CustomUser(AbstractUser):
    username = None
    email = models.EmailField(_('email address'), unique=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    def __str__(self):
        return self.email

Здесь:

  1. Создан новый класс CustomUser, который является подклассом AbstractUser.
  2. Удалено поле имени пользователя
  3. Сделал поле электронной почты обязательным и уникальным
  4. Установлен USERNAME_FIELD - который определяет уникальный идентификатор для модели пользователя - на электронную почту
  5. Указано, что все объекты для класса поступают из CustomUserManager

AbstractBaseUser

Обновите users/models.py:

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from .managers import CustomUserManager


class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_('email address'), unique=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    date_joined = models.DateTimeField(default=timezone.now)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    def __str__(self):
        return self.email

Здесь:

  1. Создан новый класс CustomUser, который является подклассом AbstractBaseUser.
  2. Добавлены поля для электронной почты, is_staff, is_active и date_joined.
  3. Установлен USERNAME_FIELD - который определяет уникальный идентификатор для модели пользователя - на электронную почту
  4. Указано, что все объекты для класса поступают из CustomUserManager

Настройки

Добавьте следующую строку в файл settings.py, чтобы Django знал, как использовать новый класс User:

AUTH_USER_MODEL = 'users.CustomUser'

Теперь можно создать и применить миграцию, которая создаст новую базу данных, использующую пользовательскую модель User. Прежде чем мы это сделаем, давайте посмотрим, как будет выглядеть миграция без создания файла миграции, с флагом --dry-run:

(env)$ python manage.py makemigrations --dry-run --verbosity 3

Вы должны увидеть что-то похожее на:

# Generated by Django 3.2.2 on 2021-05-12 20:43

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('auth', '0012_alter_user_first_name_max_length'),
    ]

    operations = [
        migrations.CreateModel(
            name='CustomUser',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
                ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
                ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
                ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')),
                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
            ],
            options={
                'verbose_name': 'user',
                'verbose_name_plural': 'users',
                'abstract': False,
            },
        ),
    ]

Если вы пошли по маршруту AbstractBaseUser, у вас не будет полей для first_name или last_name. Почему?

Убедитесь, что миграция не включает поле имени пользователя. Затем создайте и примените миграцию:

(env)$ python manage.py makemigrations
(env)$ python manage.py migrate

Просмотр схемы:

$ sqlite3 db.sqlite3

SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.

sqlite> .tables

auth_group                         django_migrations
auth_group_permissions             django_session
auth_permission                    users_customuser
django_admin_log                   users_customuser_groups
django_content_type                users_customuser_user_permissions

sqlite> .schema users_customuser

CREATE TABLE IF NOT EXISTS "users_customuser" (
  "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
  "password" varchar(128) NOT NULL,
  "last_login" datetime NULL,
  "is_superuser" bool NOT NULL,
  "first_name" varchar(150) NOT NULL,
  "last_name" varchar(150) NOT NULL,
  "is_staff" bool NOT NULL,
  "is_active" bool NOT NULL,
  "date_joined" datetime NOT NULL,
  "email" varchar(254) NOT NULL UNIQUE
);

Если вы пошли по маршруту AbstractBaseUser, почему last_login является частью модели?

Вы можете ссылаться на модель User либо с помощью get_user_model(), либо с помощью settings.AUTH_USER_MODEL. Дополнительная информация приведена в Руководство модели пользователя из официальной документации.

Также, когда вы создаете суперпользователя, вам должно быть предложено ввести email, а не имя пользователя:

(env)$ python manage.py createsuperuser

Email address: test@test.com
Password:
Password (again):
Superuser created successfully.

Убедитесь, что тесты пройдены:

(env)$ python manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.282s

OK
Destroying test database for alias 'default'...

Формы

Затем давайте создадим подкласс форм UserCreationForm и UserChangeForm, чтобы они использовали новую модель CustomUser.

Создайте в «users» новый файл с именем forms.py:

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

from .models import CustomUser


class CustomUserCreationForm(UserCreationForm):

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


class CustomUserChangeForm(UserChangeForm):

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

Админ

Скажите админке использовать эти формы, создав подкласс UserAdmin в users/admin.py:

from django.contrib import admin
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', 'is_staff', 'is_active',)
    list_filter = ('email', 'is_staff', 'is_active',)
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Permissions', {'fields': ('is_staff', 'is_active')}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'password1', 'password2', 'is_staff', 'is_active')}
        ),
    )
    search_fields = ('email',)
    ordering = ('email',)


admin.site.register(CustomUser, CustomUserAdmin)

Вот и все. Запустите сервер и войдите на сайт администратора. Вы должны иметь возможность добавлять и изменять пользователей, как обычно.

django admin

Заключение

В этом посте мы рассмотрели, как создать пользовательскую модель User, чтобы в качестве основного идентификатора пользователя вместо имени пользователя для аутентификации можно было использовать адрес электронной почты.

Вы можете найти окончательный код для обоих вариантов, AbstractUser и AbstractBaseUser, в репозитории django-custom-user-model. Последние примеры кода включают шаблоны, представления и URL, необходимые для аутентификации пользователя.

Вернуться на верх