Как правило возникает потребность хранить дополнительные данные о пользователях, например, краткую биографию (about), дату рождения, местоположение и другие подобные данные.
В этой статье пойдет речь о стратегиях, с помощью которых вы можете расширить пользовательскую модель Django, а не писать ее с нуля.
Стратегии расширения
Опишем кратко стратегии расширения пользовательской модели Django и потребности в их применении. А потом раскроем детали конфигурирование по каждой стратегии.
Простое расширение модели (proxy)
Эта стратегия без создания новых таблиц в базе данных. Используется, чтобы изменить поведение существующей модели (например, упорядочение по умолчанию, добавление новых методов и т.д.), не затрагивая существующую схему базы данных.
Вы можете использовать эту стратегию, когда вам не нужно хранить дополнительную информацию в базе данных, а просто необходимо добавить дополнительные методы или изменить диспетчер запросов модели. >
Использование связи один-к-одному с пользовательской моделью (user profiles)
Это стратегия с использованием дополнительной обычный модели Django со своей таблицей в базе данных, которая связана пользователем стандартной модели через связьOneToOneField
.
Вы можете использовать эту стратегию, чтобы хранить дополнительную информацию, которая не связана с процессом аутентификации (например, дата рождения). Обычно это называется пользовательский профиль. >
Расширение AbstractBaseUser
Это стратегия использования совершенно новой модели пользователя, которая отнаследована отAbstractBaseUser
. Требует особой осторожности и изменения настроек вsettings.py
. В идеале должно быть сделано в начале проекта, так как будет существенно влиять на схему базы данных.
Вы можете использовать эту стратегию, когда ваш сайт имеет специфические требования в отношении процесса аутентификации. Например, в некоторых случаях имеет смысл использовать адрес электронной почты в качестве идентификации маркера вместо имени пользователя. >
Расширение AbstractUser
Это стратегия использования новой модели пользователя, которая отнаследована отAbstractUser
. Требует особой осторожности и изменения настроек вsettings.py
. В идеале должно быть сделано в начале проекта, так как будет существенно влиять на схему базы данных.
Вы можете использовать эту стратегию, когда сам процесс аутентификации Django вас полностью удовлетворяет и вы не хотите его менять. Тем не менее, вы хотите добавить некоторую дополнительную информацию непосредственно в модели пользователя, без необходимости создавать дополнительный класс (как в варианте 2).>
Простое расширение модели (proxy)
Это наименее трудоемкий способ расширить пользовательскую модель. Полностью ограничен в недостатках, но и не имеет никаких широких возможностей.
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):
...
В приведенном выше примере мы определили расширение модели
User
моделью Person
. Мы говорим Django это прокси-модель, добавив следующее свойство внутри class Meta
:Proxy = True
Также в примере назначен пользовательский диспетчер модели, изменен порядок по умолчанию, а также определен новый метод
do_something()
.Стоит отметить, что
User.objects.all()
и Person.objects.all()
будет запрашивать ту же таблицу базы данных. Единственное отличие состоит в поведении, которое мы определяем для прокси-модели.Использование связи один-к-одному с пользовательской моделью (user profiles)
Скорее всего, это то, что вам нужно. Лично я использую этот метод в большинстве случаев. Мы будем создавать новую модель Django для хранения дополнительной информации, которая связана с моделью пользователя.
Имейте в виду, что использование этой стратегии порождает дополнительные запросы или соединения внутри запроса. В основном все время, когда вы будете запрашивать данные, будет срабатывать дополнительный запрос. Но этого можно избежать для большинства случаев. Я скажу пару слов о том, как это сделать, ниже.
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)
bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=30, blank=True)
birth_date = models.DateField(null=True, blank=True)
Теперь добавим немножко магии: определим сигналы, чтобы наша модель
Profile
автоматически обновлялась при создании/изменении данных модели 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)
bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=30, 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()
к событию сохранения модели User
. Такой сигнал называется post_save
.А теперь пример шаблона Django с использованием данных
Profile
:<h2>{{ user.get_full_name }}</h2>
<ul>
<li>Username: {{ user.username }}</li>
<li>Location: {{ user.profile.location }}</li>
<li>Birth Date: {{ user.profile.birth_date }}</li>
</ul>
А еще можно вот так:
def update_profile(request, user_id):
user = User.objects.get(pk=user_id)
user.profile.bio = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit...'
user.save()
Вообще говоря, вы никогда не должны вызывать методы сохранения
Profile
. Все это делается с помощью модели User
.Если вам необходимо работать с формами, то ниже приведены примеры кода для этого. Помните, что вы можете за один раз (из одной формы) обрабатывать данные более одной модели (класса).
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 = ('url', 'location', 'company')
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, _('Your profile was successfully updated!'))
return redirect('settings:profile')
else:
messages.error(request, _('Please correct the error below.'))
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">Save changes</button>
</form>
И об обещанной оптимизации запросов. В полном объеме вопрос рассмотрен в другой моей статье.
Но, если коротко, то Django отношения ленивы. Django формирует запрос к таблице базы данных, если необходимо прочитать одно из ее полей. Относительно нашего примера, эффективным будет использование метода
select_related()
.Зная заранее, что вам необходимо получить доступ к связанным данным, вы можете c упреждением сделать это одним запросом:
users = User.objects.all().select_related('Profile')
Расширение AbstractBaseUser
Если честно, я стараюсь избегать этот метод любой ценой. Но иногда это не возможно. И это прекрасно. Едва ли существует такая вещь, как лучшее или худшее решение. По большей части, существует более или менее подходящее решение. Если это является наиболее подходящим решением для вас, что ж — идите вперед.
Я должен был сделать это один раз. Честно говоря, я не знаю, существует ли более чистый способ сделать это, но не нашел ничего другого.
Мне нужно было использовать адрес электронной почты в качестве
auth token
, а username
абсолютно был не нужен. Кроме того, не было никакой необходимости флага is_staff
, так как я не использовал Django Admin.Вот как я определил свою собственную модель пользователя:
from __future__ import unicode_literals
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 ugettext_lazy as _
from .managers import UserManager
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(_('email address'), unique=True)
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=30, blank=True)
date_joined = models.DateTimeField(_('date joined'), auto_now_add=True)
is_active = models.BooleanField(_('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):
'''
Returns the first_name plus the last_name, with a space in between.
'''
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
'''
Returns the short name for the user.
'''
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
'''
Sends an email to this User.
'''
send_mail(subject, message, from_email, [self.email], **kwargs)
Я хотел сохранить ее как можно ближе к «стандартной» модели пользователя. Отнаследовав от
AbstractBaseUser
мы должны следовать некоторым правилам:USERNAME_FIELD
— строка с именем поля модели, которая используется в качестве уникального идентификатора (unique=True
в определении);
REQUIRED_FIELDS
— список имен полей, которые будут запрашиваться при создании пользователя с помощью команды управленияcreatesuperuser
is_active
— логический атрибут, который указывает, считается ли пользователь «активным»;
get_full_name()
— длинное описание пользователя: не обязательно полное имя пользователя, это может быть любая строка, которая описывает пользователя;
get_short_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):
"""
Creates and saves a User with the given email and password.
"""
if not email:
raise ValueError('The given email must be set')
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
от полей username
и is_staff
.Последний штрих. Необходимо изменить
settings.py
:AUTH_USER_MODEL = 'core.User'
Таким образом, мы говорим Django использовать нашу пользовательскую модель вместо поставляемой «в коробке». В примере выше, я создал пользовательскую модель внутри приложения с именем
core
.Как ссылаться на эту модель?
Есть два способа. Рассмотрим модель под названием
Course
:from django.db import models
from testapp.core.models import User
class Course(models.Model):
slug = models.SlugField(max_length=100)
name = models.CharField(max_length=100)
tutor = models.ForeignKey(User, on_delete=models.CASCADE)
В целом нормально. Но, если вы планируете использовать приложение в других проектах или распространять, то рекомендуется использовать следующий подход:
from django.db import models
from django.conf import settings
class Course(models.Model):
slug = models.SlugField(max_length=100)
name = models.CharField(max_length=100)
tutor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
Расширение AbstractUser
Это довольно просто, поскольку класс
django.contrib.auth.models.AbstractUser
обеспечивает полную реализацию пользовательской модели по-умолчанию в качестве абстрактной модели.from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=30, blank=True)
birth_date = models.DateField(null=True, blank=True)
После этого необходимо изменить
settings.py
:AUTH_USER_MODEL = 'core.User'
Как и в предыдущей стратегии, в идеале это должно быть сделано в начале проекта и с особой осторожностью, посколько изменит всю схему базы данных. Также хорошим правилом будет создавать ключи к пользовательской модели через импорт настроек
from django.conf import settings
и использования settings.AUTH_USER_MODEL
вместо непосредственной ссылки на класс User
.Резюме
Отлично! Мы рассмотрели четыре различных стратегии расширения «стандартной» пользовательской модели. Я попытался сделать это как можно более подробно. Но, как я уже говорил, лучшего решения не существует. Все будет зависеть от того, что вы хотите получить.
- Простое расширение модели (proxy) — вас устраивает процесс аутентификации и хранение данных пользователя, вам просто хотелось бы изменить некоторое поведение.
- Использование связи один-к-одному с пользовательской моделью (user profiles) — вас устраивает процесс аутентификации, но хотелось бы добавить дополнительные данные пользователя (данные будут хранится в специальной модели).
Расширение AbstractBaseUser
— процесс аутентификации «из коробки» вам не подходит.Расширение AbstractUser
— вас устраивает процесс аутентификации, но хотелось бы добавить дополнительные данные пользователя (данные будут хранится непосредственно в пользовательской модели, а не в специальной модели).
Комментарии (15)
antonksa
27.10.2016 22:02Отличная статья, спасибо!
Скажите, а зачем два метода для перехватывания сигналов, если можно их объединить в один
И да, разумеется необходимо напомнить о существовании метода django.contrib.auth.get_user_model() который позволяет получить в любом месте модель, определенную в settings.pydmitryklimenko
28.10.2016 10:35Спасибо. Но я всего лишь переводчик в данном случае.
Я переадресовал Ваш вопрос и замечание автору. Как только он ответит, я напишу сюда)
dmitryklimenko
28.10.2016 14:05-1> а зачем два метода для перехватывания сигналов, если можно их объединить в один
Автор пишет, что никогда не задумывался над этим, просто в таком виде изучил это в прошлом. И соглашается, что лучше это переписать:
@receiver(post_save, sender=User) def create_or_update_user_profile(sender, instance, created, **kwargs): if created: Profile.objects.create(user=instance) instance.profile.save()
> необходимо напомнить о существовании метода django.contrib.auth.get_user_model()
Тут автор, ссылаясь на последний абзац документации Django:you should reference the User model with the AUTH_USER_MODEL setting in code that is executed at import time. get_user_model() only works once Django has imported all models.
пишет, что «Это означает, что для определения моделей, регистрации сигналов, создания миграций и т.д., get_user_model () не работает.»
TyVik
28.10.2016 14:01В чём преимущество использования сигналов перед переопределением метода save? Можно ли как-нибудь в самом описании поля указать чтобы связная модель всегда создавалась?
dmitryklimenko
28.10.2016 14:33-1> В чём преимущество использования сигналов перед переопределением метода save?
Если я правильно понял Ваш вопрос, то при переопределении методаsave()
, Вы будете править исходники Django, а при использовании сигналов — нет.
> Можно ли как-нибудь в самом описании поля указать чтобы связная модель всегда создавалась?
Вот тут в официальной документации прямо сказано, что нет, и предлагается использовать сигналы для этого.they do not get auto created when a user is created, but a
django.db.models.signals.post_save
could be used to create or update related models as appropriateTyVik
28.10.2016 15:05+1Править исходники Django — да ни в жизнь, просто добавить метод в модель:
def save(self, *args, **kwargs): is_created = self.pk is None super(BaseModel, self).save(*args, **kwargs) if is_created: one_to_one_model.objects.create(<some params>)
ну, как-то так… Преимущество я вижу в том, что все манипуляции с зависимой моделью расположены внутри базовой, т.е. ближе к месту, где он реально используется.dmitryklimenko
28.10.2016 16:22-1На сколько я понимаю, у Вас в проекте может быть приложение (не Ваше, и не одно), которое вообще ничего не знает о
Profile
, но по каким-то причинам иногда создает/изменяет/удаляетUser
. Более того, у Вас может быть несколькоProfile
(например от разных приложений) — в каком из них Вы будете размещать Ваш метод?
Меня смущает Ваш комментарий. По-моему, базовой моделью являетсяUser
, а (все)Profile
– зависимые. Потому чтоUser
может существовать безProfile
, но не наоборот.TyVik
28.10.2016 16:40По-моему, я начал Вас понимать… Да, если есть несколько приложений, которые добавляют свои атрибуты к некой базовой модели, то переопределять её save не вариант — тут уже сигналами придётся обходиться. Спасибо за пример.
antonksa
28.10.2016 18:06+2Это плохой подход, проверять на наличие ключа.
У модели есть свойство, которое можно использовать.
```
def save(self, *args, **kwargs)
super(self.__class__, self).save(*args, **kwargs)
if self._state.adding is True:
one_to_one_model.objects.create()
```
awaik
04.11.2016 11:15Спасибо за статью!
Прошу совета :)
Насчет «Расширение AbstractBaseUser» — как я понял вы это делали для использования email как логин.
Вопрос — почему вы предпочли это готовому решению
https://github.com/dabapps/django-email-as-username
(просто тоже делаю проект где email == username и интересно ваше мнение)dmitryklimenko
05.11.2016 13:26Ммм… Это перевод. Перед аппрувом подвергся редакторской правке, поэтому это стало не очень заметно (убрали вниз в ссылку в спойлере, в моей первоначальной редакции ссылка на оригинал была первым предложением).
Я сейчас делаю проект на Django, где хотел бы использовать, как и Вы, e-mail в качестве логина. При поиске решений наткнулся на статью, перевод которой решил сделать для Хабра.
Ссылку, которую Вы дали (спасибо! за неё, она мне не попадалась), я посмотрю в понедельник, т.к. сейчас в командировке и есть только мобильный интернет. И отвечу Вам сразу после этого.
Terras
Хорошая обзорная статья.
dmitryklimenko
Спасибо.