Расширение модели пользователя в Django 4.1
Django

Расширение модели пользователя в Django 4.1

Razilator

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

Но порой стандартного функционала может не хватать, например: нам необходимо создать дополнительные поля для пользователя, добавить аватарку пользователя. Исходя из этого, нам необходимо произвести изменения в системе аутентификации, и для этого в Django существует 4 стратегии расширения пользовательской модели.

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

Существует четыре различных способа расширения существующей пользовательской модели, которые будут рассмотрены в статье:

  1. Использование прокси-модели.
  2. Использование связи один-к-одному с пользовательской моделью.
  3. Создание модели пользователя с помощью расширения класса AbstractBaseUser;
  4. Создание модели пользователя с помощью расширения класса AbstractUser.

Использование прокси-модели

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

Когда следует использовать прокси-модель?

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

Пример кода прокси-модели

models.py
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.

Когда следует использовать связи один-к-одному?

Связь «один-к-одному» используется, когда необходимо сохранить дополнительную информацию о существующей пользовательской модели, которая не связана с процессом аутентификации. Обычно такая модель называется профилем пользователя.

Пример кода связи один-к-одному

models.py
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).

models.py
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.

Как обращаться к полям профиля в шаблоне?

Пример:

profile.html
<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>

Пример форм для профиля:

forms.py
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')

Представление на основе функций для работы с двумя формами

views.py
@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
    })

Формы на странице пользователя:

profile.html
<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*

models.py
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, который удовлетворяет наши требования:

managers.py
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

settings.py
AUTH_USER_MODEL = 'system.User'

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

В приведенном выше примере пользовательская модель была создана в приложении с именем system.

Как ссылаться на эту модель? Рассмотрим модель под названием Book:

models.py
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)

Но мы рекомендуем делать это следующим образом:

models.py
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

models.py
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

settings.py
AUTH_USER_MODEL = 'system.User'

Как ссылаться на данную модель? Также, как и со способом AbstractBaseUser

models.py
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)

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

;