Pull to refresh

Стратегии расширения Django User Model

Reading time 9 min
Views 103K
В Django встроена прекрасная система аутентификации пользователей. В большинстве случаев мы можем использовать ее «из коробки», что экономит много времени разработчиков и тестировщиков. Но иногда нам необходимо расширить ее, чтобы удовлетворять потребностям нашего сайта.

Как правило возникает потребность хранить дополнительные данные о пользователях, например, краткую биографию (about), дату рождения, местоположение и другие подобные данные.

В этой статье пойдет речь о стратегиях, с помощью которых вы можете расширить пользовательскую модель Django, а не писать ее с нуля.



Стратегии расширения


Опишем кратко стратегии расширения пользовательской модели Django и потребности в их применении. А потом раскроем детали конфигурирование по каждой стратегии.

  1. Простое расширение модели (proxy)

    Эта стратегия без создания новых таблиц в базе данных. Используется, чтобы изменить поведение существующей модели (например, упорядочение по умолчанию, добавление новых методов и т.д.), не затрагивая существующую схему базы данных.

    Вы можете использовать эту стратегию, когда вам не нужно хранить дополнительную информацию в базе данных, а просто необходимо добавить дополнительные методы или изменить диспетчер запросов модели.

  2. Использование связи один-к-одному с пользовательской моделью (user profiles)

    Это стратегия с использованием дополнительной обычный модели Django со своей таблицей в базе данных, которая связана пользователем стандартной модели через связь OneToOneField.

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

  3. Расширение AbstractBaseUser

    Это стратегия использования совершенно новой модели пользователя, которая отнаследована от AbstractBaseUser. Требует особой осторожности и изменения настроек в settings.py. В идеале должно быть сделано в начале проекта, так как будет существенно влиять на схему базы данных.

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

  4. Расширение 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.



Резюме


Отлично! Мы рассмотрели четыре различных стратегии расширения «стандартной» пользовательской модели. Я попытался сделать это как можно более подробно. Но, как я уже говорил, лучшего решения не существует. Все будет зависеть от того, что вы хотите получить.


Оригинал
How to Extend Django User Model

Не стесняйте задавать вопросы и высказывать мнения об этом посте!
Tags:
Hubs:
+17
Comments 15
Comments Comments 15

Articles