Расширение модели пользователя в Django 4.1
Существующая система аутентификации Django работает стабильно и безопасно, мы можем использовать ее без каких-либо изменений в коде, что увеличивает скорость разработки проекта, а также упрощает его тестирование.
Если вы хотите выразить благодарность автору сайта, статей и курса по Django, вы можете сделать это по ссылке ниже:
Но порой стандартного функционала может не хватать, например: нам необходимо создать дополнительные поля для пользователя, добавить аватарку пользователя. Исходя из этого, нам необходимо произвести изменения в системе аутентификации, и для этого в Django существует 4 стратегии расширения пользовательской модели.
Способы расширения модели пользователей Django
Существует четыре различных способа расширения существующей пользовательской модели, которые будут рассмотрены в статье:
- Использование прокси-модели.
- Использование связи один-к-одному с пользовательской моделью.
- Создание модели пользователя с помощью расширения класса
AbstractBaseUser
; - Создание модели пользователя с помощью расширения класса
AbstractUser
.
Использование прокси-модели
Прокси-модель - это модель, которая наследуется от существующей модели пользователя, без создания новой таблицы в базе данных. Она используется для изменения параметров и поведения уже существующей модели, например, изменения сортировки по умолчанию, добавления новых методов, которые не затрагивают схему таблицы в базе данных.
Когда следует использовать прокси-модель?
Прокси-модель используется для расширения существующей пользовательской модели, когда базе данных не требуется хранить дополнительную информацию, но необходимо добавить в базовую модель дополнительные методы или модифицировать ее менеджер, обрабатывающий запросы к базе данных.
Пример кода прокси-модели
from django.contrib.auth.models import User
from .managers import PersonManager
class Person(User):
objects = PersonManager()
class Meta:
proxy = True
ordering = ('first_name', )
def do_something(self):
...
Пояснение:
В приведенном примере мы создаем прокси-модель с названием Person, указав внутри Meta класса параметр proxy = True
.
Сама прокси-модель используется в этом примере для переопределения параметров сортировки по умолчанию, назначения нового менеджера и определения нового метода do_something
.
Обратите внимание, что User.objects.all()
и Person.objects.all()
обращаются к одной и той же таблице базы данных. Единственное отличие заключается в поведении, определенном для прокси-модели.
Использование связи «один-к-одному» с пользовательской моделью
В данном случае мы создаем модель Django, которая будет иметь свою собственную схему в базе данных и связь «один-к-одному» с существующей моделью пользователя через OneToOneField
.
Когда следует использовать связи один-к-одному?
Связь «один-к-одному» используется, когда необходимо сохранить дополнительную информацию о существующей пользовательской модели, которая не связана с процессом аутентификации. Обычно такая модель называется профилем пользователя.
Пример кода связи один-к-одному
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to='images/avatars/', blank=True)
bio = models.TextField(max_length=500, blank=True)
birth_date = models.DateField(null=True, blank=True)
Но для этой модели нам необходимо создать сигналы на автоматическое создание и обновление, когда мы создаем или обновляем стандартную модель пользователя (User).
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to='images/avatars/', blank=True)
bio = models.TextField(max_length=500, blank=True)
birth_date = models.DateField(null=True, blank=True)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
Как видите, основная нагрузка — добавление вызовов create_user_profile()
и save_user_profile()
каждый раз при сохранении объекта, в том числе и при создании. Этот тип сигнала называется post_save.
Как обращаться к полям профиля в шаблоне?
Пример:
<h1>{{ user.get_full_name }}</h1>
<ul>
<li>Имя пользователя: {{ user.username }}</li>
<li>Информация: {{ user.profile.bio }}</li>
<li>Дата рождения: {{ user.profile.birth_date }}</li>
</ul>
<div class="avatar">
<img src="{{ user.profile.avatar.url }}" alt="{{ user.username }}"/>
</div>
Пример форм для профиля:
class UserForm(forms.ModelForm):
class Meta:
model = User
fields = ('first_name', 'last_name', 'email')
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ('avatar', 'bio', 'birth_date')
Представление на основе функций для работы с двумя формами
@login_required
@transaction.atomic
def update_profile(request):
if request.method == 'POST':
user_form = UserForm(request.POST, instance=request.user)
profile_form = ProfileForm(request.POST, instance=request.user.profile)
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
messages.success(request, _('Ваш профиль был успешно обновлен!'))
return redirect('settings:profile')
else:
messages.error(request, _('Пожалуйста, исправьте ошибки.'))
else:
user_form = UserForm(instance=request.user)
profile_form = ProfileForm(instance=request.user.profile)
return render(request, 'profiles/profile.html', {
'user_form': user_form,
'profile_form': profile_form
})
Формы на странице пользователя:
<form method="post">
{% csrf_token %}
{{ user_form.as_p }}
{{ profile_form.as_p }}
<button type="submit">Сохранить изменения</button>
</form>
Проблема: дополнительные запросы N+1
Django будет формировать запрос в БД только при доступе к одному из связанных свойств Profile. Иногда это приводит к нежелательным последствиям, таким как выполнение сотен или тысяч запросов на страницу.
Решение:
Эту проблему можно смягчить с помощью метода select_related()
.
Зная заранее, что вам потребуется доступ к связанным данным, вы можете предварительно получить их в одном запросе к базе данных:
users = User.objects.all().select_related('profile')
Подробнее об оптимизации SQL запросов можно рассмотреть в одном из уроков
Создание модели пользователя через расширение AbstractBaseUser
Наследуясь от AbstractBaseUser
мы создаем новую пользовательскую модель. Работа и интеграция ее в проект потребует от вас некоторых усилий и настроек в конфигурации settings.py.
Настоятельно рекомендуется производить все манипуляции с моделиями до начала работы над проектом, так как этот способ повлияет на всю схему базы данных.
Использование этого метода в готовом проекте может вызвать проблемы при внедрении новой модели.
Когда использовать этот метод?
Использование пользовательской модели необходимо, когда приложение предъявляет особые требования к процессу аутентификации. Например, если вам нужно использовать адрес электронной почты вместо имени пользователя.
Пример кода при использовании наследования AbstractBaseUser*
from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import gettext_lazy as _
from .managers import UserManager
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(_('email'), unique=True)
first_name = models.CharField(_('name'), max_length=30, blank=True)
last_name = models.CharField(_('surname'), max_length=30, blank=True)
date_joined = models.DateTimeField(_('registered'), auto_now_add=True)
is_active = models.BooleanField(_('is_active'), default=True)
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def get_full_name(self):
'''
Возвращает first_name и last_name с пробелом между ними.
'''
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
В данном примере кода все сделано так, чтобы новая пользовательская модель была как можно ближе к существующей пользовательской модели.
Поскольку мы наследуем от AbstractBaseUser
, нам необходимо определить несколько свойств и методов:
USERNAME_FIELD
— строка, описывающая имя поля в модели пользователя, которая используется как идентификатор. Поле должно быть уникальным (то есть иметь значение unique=True, установленное в его определении);REQUIRED_FIELDS
— список имен полей, которые будут запрашиваться при создании пользователя через команду управления createsuperuser;is_active
— логический атрибут, указывающий, является ли пользователь активным;get_full_name()
— более длинный формальный идентификатор для пользователя. В этом примере будем использовать полное имя пользователя, но это может быть любая строка, которая идентифицирует пользователя;
Нам необходимо определить UserManager. Это связано с тем, что существующий менеджер определяет методы create_user()
и create_superuser()
.
Пример UserManager, который удовлетворяет наши требования:
from django.contrib.auth.base_user import BaseUserManager
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
"""
Создает и сохраняет пользователя с введенным им email и паролем.
"""
if not email:
raise ValueError('email должен быть указан')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault('is_superuser', 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)
Он удаляет существующий UserManager, а также имя пользователя и свойство is_staff
.
Нам также нужно обновить конфиг settings.py, а именно параметр AUTH_USER_MODEL
AUTH_USER_MODEL = 'system.User'
Таким образом мы уточняем, что мы должны использовать нашу собственную модель вместо стандартной.
В приведенном выше примере пользовательская модель была создана в приложении с именем system.
Как ссылаться на эту модель? Рассмотрим модель под названием Book:
from django.db import models
from testapp.core.models import User
class Book(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255)
author = models.ForeignKey(User, on_delete=models.CASCADE)
Но мы рекомендуем делать это следующим образом:
from django.db import models
from django.conf import settings
class Book(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
Создание модели пользователя через расширение AbstractUser
Этот метод также подразумевает создание новой модели пользователя, но уже унаследованной от AbstractUser
.
Здесь уместны все те же замечания что и от наследования от AbstractBaseUser
: необходимость дополнительных усилий по реализации и обновлению некоторых параметров через конфиг settings.py, а также сложности интеграции в готовый проект.
Когда следует использовать этот метод?
Используется в случае, если вам хватает того, как работает аутентификация Django, и вы не хотите ничего менять, но вам все равно нужно добавить дополнительную информацию прямо в пользовательскую модель (User) без создания дополнительной модели.
Пример кода при использовании наследования AbstractUser
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
avatar = models.ImageField(upload_to='images/avatars/', blank=True)
bio = models.TextField(max_length=500, blank=True)
birth_date = models.DateField(null=True, blank=True)
После этого вам необходимо обновить конфиг settings.py, определяя свойство AUTH_USER_MODEL
AUTH_USER_MODEL = 'system.User'
Как ссылаться на данную модель? Также, как и со способом AbstractBaseUser
from django.db import models
from django.conf import settings
class Book(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
Вот и все, мы рассмотрели четыре способа расширения пользовательской модели.