Для работы с пользователями, Django предоставляет готовую модель User. Часто, одной этой модели недостаточно. Приходится ее расширять, либо переписывать, если не устраивает стандартная реализация.
Несколько причин, почему может не устраивать стандартная реализация:
- Вам нужно вместо двух полей first_name и last_name, одно поле full_name.
- Не устраивает то, что поле email – необязательно (blank=True).
- Не устраивает то, что USERNAME_FIELD = ‘username’. USERNAME_FIELD указывает на поле, которое является уникальным идентификатором для пользователя. Кроме того, это поле используется на странице с авторизацией. Получается, что по умолчанию, пользователь вводит username и password. Если вам нужна авторизация через email, то USERNAME_FIELD нужно переопределить на ‘email’.
Рекомендую посмотреть исходный код модели User здесь. Так будет проще понять, о чем я говорю.
В этом уроке поговорим о двух методах расширения модели User:
(1) Создание модели User, которая наследуется либо от AbstractUser, либо от AbstractBaseUser.
Класс AbstractUser, по функционалу, является той самой моделью User, которую вы получаете от Django из коробки. Если вас устраивает стандартная реализация, но нужно ее расширить, то наследуйтесь от AbstractUser. Если нет, то наследуйтесь от AbstractBaseUser и переопределяйте поля сами.
(2) Создание двух моделей: User и Profile, которые связаны друг с другом отношением один-к-одному (OneToOneField).
Зачем создавать две модели? Дело в том, что в программировании есть концепция Single Responsibility Principle (Принцип единственной обязанности).
«На каждый объект должна быть возложена одна единственная обязанность»
Single Responsibility Principle
Другими словами, класс и метод должны делать только одну вещь. Не нужно создавать так называемый God Object, который делает все, что только можно. Не нужно создавать метод, в котором реализована и валидация, и сохранение объекта в файл, и отправка сообщения пользователю… Для каждого метода должна существовать только одна причина его изменения. Если в методе вы хотите изменить как происходит валидация, то это одна причина изменения. Если хотите изменить реализацию отправки сообщения пользователю, то это другая причина. Когда причин больше чем одна, это значит, что метод делает слишком много.
Итак, следуя принципу единственной обязанности, мы создаем:
- Модель User, которая отвечает только за аутентификацию и авторизацию пользователя в системе.
- Модель Profile, которая хранит всевозможную информацию о пользователе для отображения на странице.
User, наследуемый от AbstractUser
Это самый простой способ, т.к. класс AbstractUser уже предоставляет все, что нужно.
Cоздаем приложение:
$ django-admin startapp users
Создаем модель:
# users/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): bio = models.CharField(max_length=160, null=True, blank=True) birthday = models.DateField(null=True, blank=True) def __str__(self): return self.username
Добавляем путь к модели User в настройках проекта, чтобы Django использовал нашу модель вместо стандартной:
# settings.py AUTH_USER_MODEL = 'users.User'
User, наследуемый от AbstractBaseUser
Cоздаем приложение:
$ django-admin startapp users
Создаем модель:
# users/models.py from django.core.mail import send_mail from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.auth.models import PermissionsMixin from django.contrib.auth.validators import UnicodeUsernameValidator from django.db import models from django.utils.translation import ugettext_lazy as _ class User(AbstractBaseUser, PermissionsMixin): username_validator = UnicodeUsernameValidator() username = models.CharField( max_length=150, unique=True, validators=[username_validator], ) email = models.EmailField(unique=True) full_name = models.CharField(max_length=255) bio = models.CharField( max_length=160, null=True, blank=True ) birthday = models.DateField( null=True, blank=True ) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username', 'full_name'] objects = UserManager() def __str__(self): return self.email def get_short_name(self): return self.email def get_full_name(self): return self.full_name def email_user(self, subject, message, from_email=None, **kwargs): send_mail(subject, message, from_email, [self.email], **kwargs)
- USERNAME_FIELD – уникальный идентификатор пользователя. Это поле, вместе с паролем, используется при авторизации.
- REQUIRED_FIELDS – список полей, которые потребуется ввести при создании пользователя через команду createsuperuser. Также этот список нередко используется сторонними библиотеками, при создании формы с регистрацией. Нормальная форма с регистрацией включает в себя: USERNAME_FIELD, REQUIRED_FIELDS и password.
- is_active – активен ли пользователь или нет. Когда пользователь пытается удалить аккаунт, мы присваиваем полю is_active=false. Аккаунт из базы данных не удаляется. Это делается ради сохранения информации об активности пользователя.
- is_staff – имеет ли пользователь доступ к панели администратора или нет.
Создаем свой UserManger и переопределяем методы, которые отвечают за создание пользователя:
# users/models.py from django.contrib.auth.base_user import BaseUserManager class UserManager(BaseUserManager): use_in_migrations = True def _create_user(self, email, username, full_name, password, **extra_fields): """ Create and save a user with the given username, email, full_name, and password. """ if not email: raise ValueError('The given email must be set') if not username: raise ValueError('The given username must be set') if not full_name: raise ValueError('The given full name must be set') email = self.normalize_email(email) username = self.model.normalize_username(username) user = self.model( email=email, username=username, full_name=full_name, **extra_fields ) user.set_password(password) user.save(using=self._db) return user def create_user(self, email, username, full_name, password=None, **extra_fields): extra_fields.setdefault('is_staff', False) extra_fields.setdefault('is_superuser', False) return self._create_user( email, username, full_name, password, **extra_fields ) def create_superuser(self, email, username, full_name, password, **extra_fields): extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', 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, username, full_name, password, **extra_fields )
Добавляем путь к модели User в настройках проекта, чтобы Django использовал нашу модель вместо стандартной:
# settings.py AUTH_USER_MODEL = 'users.User'
User и Profile со связью один-к-одному
Cоздаем приложение:
$ django-admin startapp profiles
Модель User переопределять не будем. Возьмем ту, что предоставляет Django из коробки. Создадим модель Profile:
# profiles/models.py from django.conf import settings from django.db import models class Profile(models.Model): user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE ) bio = models.CharField( max_length=160, null=True, blank=True ) birthday = models.DateField( null=True, blank=True ) def __str__(self): return self.user.username
Осталось решить две проблемы:
- После создания пользователя, не создается новый Profile, прикрепленный к этому пользователю.
- После вызова метода save у пользователя, не вызывается метод save у прикрепленного к нему профиля. Из-за этого приходится каждый раз вызывать метод save после изменения профиля:
>>> user.username = 'apirobot' >>> user.profile.bio = 'Something about me' >>> user.profile.save() >>> user.save()
Signals to the rescue! Сигналы позволяют определённым отправителям (senders) уведомлять некоторый набор получателей (receivers) о совершении действий. Используем встроенный в Django сигнал post_save, который отсылается после завершения работы метода save:
# profiles/signals.py from django.contrib.auth import get_user_model from django.db.models.signals import post_save from django.dispatch import receiver from .models import Profile User = get_user_model() @receiver(post_save, sender=User) def create_or_update_user_profile(sender, instance, created, **kwargs): if created: instance.profile = Profile.objects.create(user=instance) instance.profile.save()
Не забудьте подключить сигналы в методе ready конфигурационного класса:
# profiles/apps.py from django.apps import AppConfig class ProfilesConfig(AppConfig): name = 'profiles' def ready(self): import profiles.signals
Тестируем профиль через shell:
>>> from django.contrib.auth import get_user_model >>> User = get_user_model() >>> User.objects.create_user(username='apirobot', password='password') <User: apirobot> >>> user = User.objects.first() >>> user.profile <Profile: apirobot> >>> user.profile.bio = 'Something about me' >>> user.save() >>> User.objects.first().profile.bio 'Something about me'
Напоследок хотелось бы сказать, что не стоит забывать про оптимизацию запросов к базе данных. По умолчанию, Django ORM не включает в результат связанные объекты. Поэтому, при каждом обращении к пользователю через профиль, делается новый запрос к базе данных:
# Запрос к базе данных profile = Profile.objects.get(id=1) # Снова запрос к базе данных для получения объекта User user = profile.user
Это может стать проблемой, когда вы получаете список всех профилей и отображаете в html шаблоне данные профиля и связанного с ним пользователя:
# profiles/views.py from django.views import generic from .models import Profile class ProfileListView(generic.ListView): model = Profile
<!-- profiles/templates/profiles/profile_list.html --> {% for profile in profile_list %} {{ profile.user.username }} {# Делает запрос к базе данных для каждого профиля #} {{ profile.bio }} {% endfor %}
Такая проблема решается с помощью метода select_related, который добавляет к результату запроса связанный объект:
# Запрос к базе данных profile = Profile.objects.select_related('user').get(id=1) # Не делает запрос к базе данных, т.к. `profile.user` # был получен в предыдущем запросе user = profile.user
Исправляем представление из предыдущего примера:
# profiles/views.py class ProfileListView(generic.ListView): queryset = Profile.objects.select_related('user')
Заключение
В этом уроке мы поговорили о методах расширения модели User. Если вы не определились с выбором между User + Profile или просто User, то следуйте своему сердцу. После просмотра исходного кода проектов на GitHub, я понял, что каждый делает так, как хочет. В одних проектах всё пихают в модель User, в других используют User + Profile.
Имеет смысл создавать модель Profile, когда в моделе User много кода или когда вы хотите, чтобы модель User отвечала только за аутентификацию и авторизацию пользователя. Если же ваша модель User небольшая и вы не хотите париться по поводу создания сигналов и оптимизации запросов, то Profile вам не нужен.
Разве в UserManager().create_user() необходимы и достаточны extra_fields.setdefault(‘is_staff’, False)?
Если передать ‘is_stuff’: True, то получится, что через create_user() создается superuser, верно?
Так можно создать staff пользователя. Superuser и staff немного разные вещи.
В следующей строчке то же самое про is_superuser.
Отличная статься, спасибо, полезно
Добрый день. Спасибо за статью. Подскажите пожалуйста, как можно сделать так, что бы при создания пользователя – создавался пользователь СУБД и все запросы к бд выполнялись именно от того пользователя, который авторизован? В стандартном подходе, применяется (если понимаю правильно) -фильтрация данных по пользователю, что мне не подходит, т.к бд закрыта, доступны лишь представления, в которых есть данные доступные именно тому пользователю, о которого поступает запрос. Я так понимаю, можно идти по пути переопределения модели и в создании пользователя вызывать создание пользователя СУБД… Но тогда придется и все другие методы моделей переписывать. Есть ли более правильный и простой путь?