Pull to refresh

Профили пользователей: плюсы, минусы, подводные камни

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

Извращаться приходилось всем: не только пользователям джанги, но и самим её core-разработчикам. Помните, например, как в Django 1.2 внезапно стало возможно использовать в поле username символы собаки (@) и точки? Знаете зачем? Чтобы в качестве логинов можно было использовать адреса e-mail.

Нам, простым пользователям, тоже жилось несладко. Для того, чтобы изменить профиль пользователя, добавив ему какие-нибудь интересных полей — обычная, казалось бы, вещь, да? — приходилось действовать разными способами.

  • Взять, к примеру, наследование. Требовалось создать собственную модель, унаследованную от auth.User

    # models.py
    from django.db import models
    from django.contrib.auth.models import User
    
    class MyUser(User):
        birthday = models.DateField()    
    

    … и написать миддльварь, которая бы подменяла класс для request.user. Или не миддльварь, а AUTHENTICATION_BACKEND. Неважно :)

    Плюсом схемы было то, что наш, «клиентский» код (то бишь код проектов) не усложнялся, мы просто работали с нашим пользователем, как с обычным джанговским.

    Основной минус такой схемы то, что при наследовании моделей заполняется не одна таблица в БД, а две: исходная джанговская django_auth и наша ourproj_user, в которой есть внешний ключ на django_auth. Да, наследование моделей в Django — это всего лишь OneToOneField с какими-то дополнительными атрибутами. Захотите использовать — имейте в виду.

  • Не менее известный костыль, предложенный самими создателями Django — так называемая модель профиля. Нам предлагалось создать модель Profile со связью один-к-одному на auth.User

    # models.py
    from django.db import models
    from django.contrib.auth.models import User
    
    class Profile(models.Model):
        user = models.OneToOneField(User)
        birthday = models.DateField()
    

    … а потом добавить в settings нечто такое:

    AUTH_PROFILE_MODULE = 'accounts.Profile'

    После этого в клиентском коде мы могли работать с профилем:

    
    profile = request.user.get_profile()
    profile.birthday = datetime.today()
    profile.save()
    

    Плюс у такой схемы — не знаю :) может, любой набор любых полей с любым именем? С минусами всё прозрачнее:
    • сложность поддержки: у нас теперь не один объект для редактирования, а два. Надо не забыть, что дату рождения мы меняем у profile, а, к примеру, пароль у user. Немудрено и запутаться.

    • нецелевая растрата ресурсов: каждое обращение к get_profile() вызывает запрос к БД. На страницах, где инстанс пользователя всего один (редактирование, например) это не страшно. Если же такая штука будет, например, в комментариях, результат будет плачевен. Разумеется, select_related(), как вы понимаете, не спасёт, поскольку не User зависит от Profile, а наоборот.
    • а ещё всё надо руками делать! Создание модели User не означает, что автоматом создастся связанная модель Profile. Зато при обращении к get_profile() для вновь созданного юзера вылетит исключение — вот тут сомневаться не приходится. И хоть эта беда лечится в несколько строк простейшим сигналом,

      # profile.models
      from django.db import models
      from django.contrib.auth.models import User
      
      class Profile(models.Model):
          'что-нибудь хорошее'
      
      def create_profile(sender, **kwargs):
          if kwargs['created']:
              Profile.objects.create(user=kwargs['instance'])
      models.signals.post_save.connect(create_profile, sender=User)
      

      всё же раздражает необходимость её «ручного» решения.
  • Манкипатчинг, то есть изменение поведения программы без переписывания кода. После инициализации приложения в каком-нибудь месте проекта (как правило, в корневом urls, settings или models специально заведённого приложении) писали код, модифицирующий нашего Юзера:

    # monkey_patching.models
    from django.db import models
    from django.contrib.auth.models import User
    User.add_to_class('birthday', models.DateField() )
    

    Плюс, как и при наследовании, лёгкость в клиентском коде. Минусы — неочевидность. С магией, как известно, нужно обращаться очень осторожно, ведь можно невзначай переопределить какую-нибудь штуку, а всплывёт совсем в другом месте. C другой стороны, если очень осторожно, то почему бы и нет?

Кстати, об обезьянах...


Ребята, подарившие миру легендарный sorl.thumbnail, очередной раз отличились и произвели на счет ещё одну убойную штуку. Встречайте: django-primate, приложение, которое используя методы манкипатчинга (приматы и обезьяны, чуете корреляцию?) позволяют очень просто превратить вашу собственную модель в auth.User. То есть, по-русски говоря, составлять профиль из нужных полей.

Начать использовать достаточно легко. Сначала надо поставить django-primate. Можно с PyPI:

pip install django-primate

… а можно и последнюю версию из их репозитория:

pip install  -e git+https://github.com/aino/django-primate.git#egg=django-primate

Вызывать патч нужно при старте. Создатели рекомендуют применять его в manage.py

#!/usr/bin/env python
from django.core.management import setup_environ, ManagementUtility
import imp
try:
    imp.find_module('settings') # Assumed to be in the same directory.
except ImportError:
    import sys
    sys.stderr.write(
        "Error: Can't find the file 'settings.py' in the directory "
        "containing %r. It appears you've customized things.\nYou'll have to "
        "run django-admin.py, passing it your settings module.\n" % __file__
        )
    sys.exit(1)

import settings

if __name__ == "__main__":
    setup_environ(settings)
    import primate
    primate.patch()
    ManagementUtility().execute()

теперь остаётся только указать в settings класс модели, который мы хотим использовать

AUTH_USER_MODEL = 'users.models.User'

… и можно начинать придумывать модели:

# users.models
from django.db import models
from primate.models import UserBase, UserMeta

class User(UserBase):
    __metaclass__ = UserMeta
    birthday = models.DateField()
    # На что фантазии хватит?


Теперь каждый раз, когда какой-либо компонент проекта будет обращаться к django.contrib.auth.models.User, он будет получать модель users.models.User. Обратное тоже верно. Админка пропатчится автоматом, никаких специальных действий для её подключения производить не надо.

По умолчанию, модель пользователя django-primate имеет следующие отличия от auth.User:
  • Убраны поля first_name и last_name, зато добавлено name;
  • Максимальная длина поля username составляет 50 символов (в auth.User было 30)
  • Полю email добавлен уникальный индекс
  • Метод get_profile возвращает self, так что можно не опасаться за код, использующий user.get_profile()

В остальном же модель пользователя primate гуляет как утка, плавает как утка и крякает как утка ведёт себя очень похоже на джанговский первоисточник.

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

Ещё один момент: для совместимости с джангой, primate пропачит модель так, что её app_label станет не users (как в примере), а auth. Это особо касается пользователей South, которые могут некоторое время не понимать, почему

./manage.py schemamigration users --auto

не создаёт никаких миграций.

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

Спасибо за внимание :)
Tags:
Hubs:
Total votes 67: ↑64 and ↓3 +61
Views 11K
Comments Comments 45