Стратегії розширення Django User Model

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

Не стесняйте ставити запитання та висловлювати думки про цьому пості!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.