Современный способ создание настраиваемой модели User в Django

Spread the love

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

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

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

AbstractUser против AbstractBaseUser

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

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

Шаги одинаковы для каждого выбора:

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

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

Предположим, что у вас уже установлен Pipenv. Начнем с создания нового проекта Django вместе с приложением users:

$ mkdir django-custom-user-model && cd django-custom-user-model
$ pipenv shell --python 3.6
(django-custom-user-model)$ pipenv install django==2.1.5
(django-custom-user-model)$ django-admin.py startproject hello_django .
(django-custom-user-model)$ 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',
]

Тесты

Я предпочитаю разрабатывать приложения через тестирование. Добавим тесты в users/tests.py, и убедимся что тесты возвращают ошибку.

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


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('super@user.com', '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)

Команда запуска тестов:

(django-custom-user-model)$ python manage.py test

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

Далее, нам нужно добавить пользовательский Manager, с помощью подкласса BaseUserManager, который использует email в качестве уникального идентификатора вместо username.

Создайте файл managers.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)

Модель User

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

AbstractUser

Внесите в users/models.py следующие изменения:

from django.db import models
from django.contrib.auth.models import AbstractUser
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. Удаляем поле username
  3. Делаем поле email обязательным и уникальным
  4. Задаем USERNAME_FIELD–для определения уникального идентификатора в модели User со значением email
  5. Указываем, что все objects для класса происходят от CustomUserManager

AbstractBaseUser

Внесите в users/models.py следующие изменения :

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

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. Добваляем поля emailis_staffis_active, и date_joined
  3. Устанавливаем USERNAME_FIELD–которое определяет уникальный идентификатор для модели User значением email
  4. Указываем что objects для класса происходят от CustomUserManager

Настройка

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

AUTH_USER_MODEL = 'users.CustomUser'

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

(django-custom-user-model)$ python manage.py makemigrations --dry-run --verbosity 3

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

# Generated by Django 2.1.5 on 2019-02-06 14:24

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


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('auth', '0009_alter_user_last_name_max_length'),
    ]

    operations = [
        migrations.CreateModel(
            name='CustomUser',
            fields=[
                ('id', models.AutoField(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=30, 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,
            },
        ),
    ]

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

(django-custom-user-model)$ python manage.py makemigrations
(django-custom-user-model)$ python manage.py migrate

Посмотрим на созданную схему БД:

$ sqlite3 db.sqlite3

SQLite version 3.16.0 2016-11-04 19:09:39
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 "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(30) 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
);

Теперь вы можете использовать модель User внутри приложения с помощью get_user_model() или settings.AUTH_USER_MODEL. Если вы хотите уточнить этот момент можете почитать об этот тут: ссылка на модель пользователя из официальных документов.

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

(django-custom-user-model)$ python manage.py createsuperuser
Email address: test@test.com
Password:
Password (again):
Superuser created successfully.

Далее убедимся, что тесты проходят:

----------------------------------------------------------------------
Ran 2 tests in 0.282s

OK

Формы

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

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

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 = ('email',)

Админка

Укажем стандартной админке Django использовать эти новые формы, создав подкласс 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)

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

Заключение

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

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

Хотите узнать больше о настройке модели Django? Посмотрите на следующие ресурсы:

  1. Options & Objects: Настройка Django User Model
  2. Как расширить Django User Model
  3. Получение максимальной отдачи от Django User Model (video)
  4. Настройка аутентификации в Django
  5. Django: How to Extend The User Model (aka Custom User Model)

Автор Michael Herman оригинал: Creating a Custom User Model in Django

Была ли вам полезна эта статья?
[13 / 4.7]

Spread the love
Подписаться
Уведомление о
guest
8 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
егор
егор
5 лет назад

Это разве нормально, что вроде бы простое действие, которое нужно делать для каждого второго проекта, выливается в инструкцию на 14 скринов? Мне казалось, фреймворки должны избавлять от рутины, а не добавлять…

stayyy
stayyy
3 лет назад

Спасибо, очень подробно расписано

nsam
nsam
2 лет назад

Большое спасибо за подробную статью и ссылки!
Стало гораздо понятнее:)

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

Меня устраивают поля стандартной модели, но, возможно, я захочу добавить свои поля. Какую мне модель использовать?

AlexDolls
AlexDolls
2 лет назад
Reply to  cianoid

Думаю, что AbstractUser, т.к. используя её, все имеющиеся поля сохраняются

Дастан
Дастан
2 лет назад

А я вот не понял как можно свой пароль поставить?
Заменить поле пароля на свой имеющий

Cap
Cap
1 год назад

Спасибо! Подсмотрел раздел Админка, т.к. при наследовании от AbstractUser и использовании стандартного admin.py у меня были глюки с отображением пароля…

Last edited 1 год назад by Cap