На предыдущих уроках мы реализовали регистрацию, авторизацию и восстановление пароля. На этом этапе в проекте Cinemahub уже существует полноценная система пользователей, но она пока использует стандартную модель User, предоставляемую Django.
Однако в реальных проектах почти всегда возникает необходимость хранить дополнительную информацию о пользователе:
- фотографию профиля;
- дату рождения;
- город;
- настройки профиля;
- служебные поля (например, роль пользователя в системе).
Даже в рамках Cinemahub это может быть полезно: в будущем пользователь может иметь собственный профиль, список избранных фильмов, историю просмотров, персональные рекомендации и т.д.
В этом уроке мы разберём, какими способами Django позволяет расширять модель пользователя, и подробно реализуем наиболее универсальный и профессиональный подход — через AbstractUser.
В Django существует два официальных способа расширить информацию о пользователе.
Создаётся дополнительная модель, например UserProfile, связанная с User через OneToOneField.
Идея:
Основная модель User остаётся стандартной, а все дополнительные данные хранятся в отдельной таблице.
Создаётся собственная модель пользователя, которая полностью заменяет стандартную и наследуется от AbstractUser.
| Подход | Когда использовать | Плюсы | Минусы |
|---|---|---|---|
| One-to-One профиль | Проект уже в production, нельзя менять User | Не затрагивает стандартную модель | Дополнительные JOIN-запросы |
| AbstractUser | Новый проект или ранний этап | Все данные в одной модели, гибкость | Нужно принять решение заранее |
Проект Cinemahub находится на этапе активной разработки, и мы уже контролируем систему авторизации. Это идеальный момент, чтобы:
- сразу определить структуру пользователя;
- избежать лишних связей;
- упростить дальнейшую работу с профилем.
Поэтому в рамках курса мы будем использовать кастомную модель пользователя на базе AbstractUser.
Создадим собственную модель пользователя в приложении users.
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
photo = models.ImageField(
upload_to="users/%Y/%m/%d/",
blank=True,
null=True,
verbose_name="Фото профиля"
)
date_birth = models.DateField(
blank=True,
null=True,
verbose_name="Дата рождения"
)-
унаследовались от
AbstractUser, сохранив:usernameemailpasswordis_staff,is_activeи т.д.
-
добавили собственные поля:
photo— для фотографии пользователя;date_birth— дата рождения.
Таким образом, вся информация о пользователе теперь хранится в одной модели.
Теперь необходимо явно указать Django, что проект Cinemahub использует нашу модель пользователя, а не стандартную.
AUTH_USER_MODEL = "users.User"User.
После изменения модели создаём миграции.
python manage.py makemigrations
python manage.py migrateЕсли до этого проект уже использовал стандартного User, Django может выдать ошибки миграций.
В учебном проекте допустим «жёсткий сброс»:
rm -rf users/migrations/
rm db.sqlite3
python manage.py makemigrations
python manage.py migrateВ production-проектах так делать нельзя, но для обучения это допустимо и полезно для понимания механики.
Чтобы управлять пользователями через админку, зарегистрируем модель.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
admin.site.register(User, UserAdmin)После этого:
- запускаем сервер;
- переходим в
/admin/; - видим пользователей с нашими дополнительными полями.
После замены модели пользователя никогда нельзя импортировать User напрямую.
❌ Неправильно:
from django.contrib.auth.models import User✅ Правильно:
from django.contrib.auth import get_user_model
User = get_user_model()Это гарантирует, что Django всегда будет использовать актуальную модель пользователя, даже если она изменится.
Создадим администратора уже для новой модели:
python manage.py createsuperuserЗайди в админку и убедись, что:
- пользователь создаётся корректно;
- дополнительные поля отображаются.
Добавим форму редактирования профиля пользователя.
from django import forms
from django.contrib.auth import get_user_model
import datetime
class ProfileUserForm(forms.ModelForm):
this_year = datetime.date.today().year
date_birth = forms.DateField(
widget=forms.SelectDateWidget(
years=range(this_year - 100, this_year - 10)
),
label="Дата рождения"
)
class Meta:
model = get_user_model()
fields = [
"photo",
"username",
"email",
"first_name",
"last_name",
"date_birth",
]Создадим простой шаблон профиля.
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if user.photo %}
<p><img src="{{ user.photo.url }}" width="150"></p>
{% else %}
<p><img src="{{ default_image }}" width="150"></p>
{% endif %}
{{ form.as_p }}
<button type="submit">Сохранить</button>
</form>MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
DEFAULT_USER_IMAGE = MEDIA_URL + "users/default.png"from django.conf import settings
extra_context = {
"title": "Профиль пользователя",
"default_image": settings.DEFAULT_USER_IMAGE,
}Модель пользователя уже содержит поле photo, но стандартный UserAdmin не показывает его автоматически.
Создадим собственный класс админки.
Файл users/admin.py:
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
@admin.register(User)
class CustomUserAdmin(UserAdmin):
fieldsets = UserAdmin.fieldsets + (
("Дополнительная информация", {
"fields": ("photo", "date_birth"),
}),
)Что здесь происходит:
-
мы наследуемся от стандартного
UserAdmin; -
добавляем новый блок полей;
-
поле
photoпоявляется:- при создании пользователя;
- при редактировании пользователя.
Теперь можно:
- зайти в админку;
- открыть пользователя;
- загрузить фотографию;
- сохранить изменения.
По умолчанию пользователь регистрируется без фото, но в Cinemahub логично разрешить загрузку аватара уже на этапе регистрации.
Предположим, у тебя уже есть форма RegisterUserForm, унаследованная от UserCreationForm.
Файл users/forms.py:
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
from django import forms
class RegisterUserForm(UserCreationForm):
photo = forms.ImageField(
required=False,
label="Фото профиля"
)
class Meta:
model = get_user_model()
fields = (
"username",
"email",
"photo",
"password1",
"password2",
)Обрати внимание:
photoдобавлено явно;- поле необязательное — пользователь может пропустить его.
Чтобы браузер корректно отправлял файл, форма обязательно должна использовать multipart/form-data.
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Зарегистрироваться</button>
</form>Форма профиля у тебя уже есть:
class ProfileUser(LoginRequiredMixin, UpdateView):
model = get_user_model()
form_class = ProfileUserFormОсталось убедиться, что:
Файл users/forms.py:
class ProfileUserForm(forms.ModelForm):
class Meta:
model = get_user_model()
fields = (
"photo",
"username",
"email",
"first_name",
"last_name",
"date_birth",
)Файл users/templates/users/profile.html:
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if user.photo %}
<p><img src="{{ user.photo.url }}" width="150"></p>
{% endif %}
{{ form.as_p }}
<button type="submit">Сохранить</button>
</form>-
Перейди в админку.
-
Открой пользователя.
-
Загрузи фото и укажи дату рождения.
-
Сохрани изменения.
-
Перейди на страницу профиля.
-
Убедись, что:
- фото отображается;
- данные корректно сохраняются.
Добавить все изменения в свой проект.
- В чём разница между
UserиAbstractUser? - Почему важно заранее выбирать способ расширения пользователя?
- Когда лучше использовать One-to-One профиль?
- Зачем нужен
get_user_model()? - Что произойдёт, если изменить
AUTH_USER_MODELпосле миграций? - Где настраивается хранение загруженных файлов?
- Почему
ImageFieldтребуетenctype="multipart/form-data"?