Создание пользовательской модели User в Django
Как полностью заменить поле имени пользователя на поле электронной почты для аутентификации в Django?
Этот пост пошагово объясняет, как создать модель пользователя User в Django, чтобы адрес электронной почты использовался в качестве основного идентификатора пользователя вместо имени пользователя для аутентификации.
Не забывайте, что процесс, описанный в этом посте, требует значительных изменений в схеме базы данных. Поэтому он рекомендуется только для новых проектов. Если речь идет о существующем проекте, то вам, скорее всего, придется сделать резервную копию данных и заново создать базу данных. Чтобы узнать больше об этом, просмотрите следующие ресурсы:
- Замена пользовательской модели пользователя руководство из официальной документации Django
- Переход на пользовательскую модель пользователя в Django статья в блоге
.
Цели
К концу этой статьи вы должны уметь:
- Опишите разницу между
AbstractUser
иAbstractBaseUser
- Объясните, почему вам следует настроить собственную модель пользователя при запуске нового проекта Django.
- Запустите новый проект Django с пользовательской моделью пользователя.
- Используйте адрес электронной почты в качестве основного идентификатора пользователя вместо имени пользователя для аутентификации.
- Практика тестирования - первая разработка при реализации пользовательской модели пользователя.
AbstractUser или AbstractBaseUser
По умолчанию модель User в Django использует имя пользователя для уникальной идентификации пользователя при аутентификации. Если вы предпочитаете использовать адрес электронной почты, вам необходимо создать пользовательскую модель User, используя подкласс AbstractUser
или AbstractBaseUser
.
Варианты:
AbstractUser
: Используйте этот вариант, если вас устраивают существующие поля в модели User и вы просто хотите удалить поле имени пользователя.AbstractBaseUser
: Используйте эту опцию, если вы хотите начать с нуля, создав свою собственную, совершенно новую модель User.
В этом посте мы рассмотрим оба варианта:
AbstractUser
иAbstractBaseUser
.
Шаги для каждого из них одинаковы:
- Создайте собственную модель пользователя и менеджера.
- Обновите settings.py
- Настроить формы
UserCreationForm
иUserChangeForm
- Обновите админку
Начиная новый проект 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
Здесь:
- Создан новый класс
CustomUser
, который является подклассомAbstractUser
. - Удалено поле имени пользователя
- Сделал поле электронной почты обязательным и уникальным
- Установлен
USERNAME_FIELD
- который определяет уникальный идентификатор для модели пользователя - на электронную почту - Указано, что все объекты для класса поступают из
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
Здесь:
- Создан новый класс
CustomUser
, который является подклассом AbstractBaseUser. - Добавлены поля для электронной почты,
is_staff
,is_active
иdate_joined
. - Установлен
USERNAME_FIELD
- который определяет уникальный идентификатор для модели пользователя - на электронную почту - Указано, что все объекты для класса поступают из
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)
Вот и все. Запустите сервер и войдите на сайт администратора. Вы должны иметь возможность добавлять и изменять пользователей, как обычно.
Заключение
В этом посте мы рассмотрели, как создать пользовательскую модель User, чтобы в качестве основного идентификатора пользователя вместо имени пользователя для аутентификации можно было использовать адрес электронной почты.
Вы можете найти окончательный код для обоих вариантов, AbstractUser
и AbstractBaseUser
, в репозитории django-custom-user-model. Последние примеры кода включают шаблоны, представления и URL, необходимые для аутентификации пользователя.