Django 5 для начинающих

Прогресс по курсу:  9/1004

10.7 Профили пользователей: Модели и сигналы
3 из 3 шагов пройдено

Пришло время смоделировать наш профиль, чтобы в базе данных были сохранены изображение профиля пользователя и биографию.

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

В Django мы можем создавать отношения один к одному между моделями, используя OneToOneField поле модели. Подробнее мы изучали это в разделе 2.8 "Организация связей между таблицами" данного курса.

В отношениях «один-к-одному» одна запись в таблице связана с одной и только одной записью в другой таблице с использованием внешнего ключа. Пример — экземпляр модели пользователя связан с одним и только одним экземпляром профиля.

Хорошо, давайте создадим модель профиля с полями: поле связи, слаг, аватар (изображение профиля), биографию и день рождения. Вы можете добавить больше, если хотите.

from django.db import models
from django.contrib.auth.models import User
from django.core.validators import FileExtensionValidator
from django.urls import reverse
from apps.services.utils import unique_slugify

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    slug = models.SlugField(verbose_name='URL', max_length=255, blank=True, unique=True)
    avatar = models.ImageField(
        verbose_name='Аватар',
        upload_to='images/avatars/%Y/%m/%d/',
        default='images/avatars/default.png',
        blank=True,
        validators=[FileExtensionValidator(allowed_extensions=('png', 'jpg', 'jpeg'))])
    bio = models.TextField(max_length=500, blank=True, verbose_name='Информация о себе')
    birth_date = models.DateField(null=True, blank=True, verbose_name='Дата рождения')

    class Meta:
        """
        Сортировка, название таблицы в базе данных
        """
        ordering = ('user',)
        verbose_name = 'Профиль'
        verbose_name_plural = 'Профили'

    def save(self, *args, **kwargs):
        """
        Сохранение полей модели при их отсутствии заполнения
        """
        if not self.slug:
            self.slug = unique_slugify(self, self.user.username)
        super().save(*args, **kwargs)

    def __str__(self):
        """
        Возвращение строки
        """
        return self.user.username

    def get_absolute_url(self):
        """
        Ссылка на профиль
        """
        return reverse('profile_detail', kwargs={'slug': self.slug})

В примере выше мы использовали поля slug, avatar, bio, birth_date для данных профиля, а еще мы используем связь один-к-одному с встроенной пользовательской моделью Django.

Как и у модели Post, мы используем валидатор для изображений, который разрешает только загрузку указанных расширений изображения.

Ещё мы добавили метод сохранения для генерации slug, и метод get_absolute_url чтобы у пользователя был свой уникальный адрес, по которому мы сможем заходить в профиль.

Теперь давайте сделаем автатар, который будет показываться по умолчанию. Для этого в папку media/images/avatars/ добавим файл default.png:

 
Теперь структура проекта выглядит следующим образом:


 

Добавление сигналов для создания профиля

Теперь нам необходимо добавить сигналы для создания профиля при создании пользователя, а также сохранения пользователя и его профиля при редактировании. Django рекомендует размещать сигналы в каталоге приложения как отдельный модуль.

Поэтому создайте файл signals.py, внутри приложения accounts, и добавьте в него следующий код:

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import Profile


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

create_user_profile это функция приемника, которая запускается каждый раз при создании пользователя. Пользователь является отправителем, который несет ответственность за отправку уведомления.

post_save это сигнал, который отправляется в конце метода сохранения.

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


Следующим шагом является подключение приемников методом ready() конфигурации приложения путем импорта модуля сигналов.

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

Добавим новую функцию в наш класс:

from django.apps import AppConfig


class AccountsConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'apps.accounts'
    verbose_name = 'Аккаунты'

    def ready(self):
        import apps.accounts.signals

Что мы сделали, так это переопределили ready() метод конфигурации пользовательского приложения для выполнения задачи инициализации, которая регистрирует сигналы.


  • Комментариев
Будьте вежливы и соблюдайте наши принципы сообщества. Пожалуйста, не оставляйте решения и подсказки в комментариях, для этого есть отдельный форум.
Оставить комментарий

Очень часто в проектах я видел, что используется переопределение встроенной моодели пользователя на пользовательскую (через класс AbstractUser)

Для нашего проекта это было бы так:

settings.py:
AUTH_USER_MODEL = 'apps.accounts.Profile'


apps.accounts.models.py:
class Profile(AbstractUser):
    # user  - это поле уже есть в AbstractUser
    slug = models.SlugField(...
    avatar = models.ImageField(...
    bio = models.TextField(...
    birth_date = models.DateField(...

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

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

Изменен Евгений Куликов

@Евгений_Куликов, такой подход проще когда уже есть немного знаний как работает стандартная User система в Django, а в этом курсе мы как раз и знакомимся с ней, при этом еще учимся работать с сигналами. AbstractUser и AbstractBaseUser мы рассматриваем уже во втором курсе.

@Илья_Перминов, Спасибо все понял!

Мне очень  понравился Ваш курс, так что вторую часть я обязательно буду брать. А за работу с сигналами - огромное спасибо - именно тут я впервые об этом услышал.

Изменен Евгений Куликов

@Евгений_Куликов, Спасибо большое, очень приятно такое слышать!

Не до конца понятен смысл слага для аккаунта. У пользователя есть уникальное имя, которое (по идее) должно его идентифицировать, можно его бы и использовать для url. Или там разрешены любые символы, не только латиница, и из-за этого приходится создавать слаг?

Так же было бы интересно реализовать авторизацию по Email адресу, для чего его нужно сделать обязательным и уникальным.

@ilya_kutaev, По большому счету вариантов много, но у поля username длина 150 символов и они могут содержать буквенно-цифровые, _, @, +, . и - символы. Поэтому проще всего, это прогнать поле username через unique_slugify, чтобы получать всегда валидный адрес.

По поводу авторизации через Email, можете посмотреть код на гитхабе из второго курса. Там мы переопределяем полностью базовую модель.

@Илья_Перминов, Спасибо, не буду торопить события ))

Да, я проверил, имя пользователя может содержать кириллицу, например, так что тут слаг становится вполне уместен.

Изменен ilya kutaev
@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()

Если Models.objects.create() сразу сохраняет запись в БД имеет смысл создавать вторую функцию?

@Аскер_Молов, Спасибо, вы правы, убрали лишнюю функцию в сигналах.

Для моделей не указаны импорты в коде

@Николай_Петров, Спасибо, поправили.

Пропало поле в AccountsConfig

verbose_name = 'Аккаунты'
Изменен Николай Петров

Придирка конечно глупая но тут лишняя скобка

@Шамбер_Егор, не лишняя, эта скобка закрывающая у ImageField(.

Изменен Дмитрий Селезнев
if not self.slug:

вот оно отсутствие слага , я понял почему мы не делали это условие раньше ибо в админке мы добавляли prepopulated_fields при регистрации модели Post в админке, а здесь на плечи slugify падает и генерация и проверка на уникальность.

по дефолту в модели изображение jpg а в скрине png

@Ilia_Boiarintsev, спасибо, исправил.