Создание форм в Django для редактирования профиля через OneToOneField
Для декомпозиции проекта, в модуле system я создам папку forms с исполняющим файлом __init__.py и с файлом profiles.py
Выглядеть это будет следующим образом...
Напоминаю, что вы можете не использовать такую же структуру папок, как использую я.
Для меня такое разделение - чистота, понятливость, четкость, декомпозиция, где что находится.

Перейдем к созданию формы для модели профиля:
modules/system/forms/profiles.py
from django import forms
from django.contrib.auth.models import User
from modules.system.models import Profile
class ProfileUpdateForm(forms.ModelForm):
"""
Форма обновления данных профиля пользователя
"""
class Meta:
model = Profile
fields = ('slug', 'status', 'bio', 'avatar', 'date_birthday')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы обновления
"""
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs.update({
'class': 'form-control',
'autocomplete': 'off'
})
И для модели пользователя (в том же файле):
class UserUpdateForm(forms.ModelForm):
"""
Форма обновления данных пользователя
"""
class Meta:
model = User
fields = ('username', 'email', 'first_name', 'last_name')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы обновления
"""
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs.update({
'class': 'form-control',
'autocomplete': 'off'
})
def clean_email(self):
"""
Проверка email на уникальность
"""
email = self.cleaned_data.get('email')
username = self.cleaned_data.get('username')
if email and User.objects.filter(email=email).exclude(username=username).exists():
raise forms.ValidationError('Email адрес должен быть уникальным')
return email
Получится вместе так:
from django import forms
from django.contrib.auth.models import User
from modules.system.models import Profile
class ProfileUpdateForm(forms.ModelForm):
"""
Форма обновления данных профиля пользователя
"""
class Meta:
model = Profile
fields = ('slug', 'bio', 'avatar', 'date_birthday')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы обновления
"""
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs.update({
'class': 'form-control',
'autocomplete': 'off'
})
class UserUpdateForm(forms.ModelForm):
"""
Форма обновления данных пользователя
"""
class Meta:
model = User
fields = ('username', 'email', 'first_name', 'last_name')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы обновления
"""
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs.update({
'class': 'form-control',
'autocomplete': 'off'
})
def clean_email(self):
"""
Проверка email на уникальность
"""
email = self.cleaned_data.get('email')
username = self.cleaned_data.get('username')
if email and User.objects.filter(email=email).exclude(username=username).exists():
raise forms.ValidationError('Email адрес должен быть уникальным')
return email
Пояснения:
- Наши формы основаны на классе ModelForm. Наследуемся от него.
- В мета мы указали необходимую модель за параметр, а также необходимые поля для показа при редактировании формы.
- В методе dif __init__ мы обновляем стили всех полей, добавляя им атрибуты из Bootstrap, а именно class="form-control"
- В методе def clean_email мы проверяем введенный email на уникальность, что такого в базе данных нет.
Таким образом, у нас есть две формы для пользователя. Теперь ими нужно воспользоваться, для этого мы создадим новое представление в views/profiles.py, не забываем про все импорты!
modules/system/views/profiles.py
from modules.system.forms.profiles import ProfileUpdateForm, UserUpdateForm
from django.views.generic import UpdateView
from django.db import transaction
from django.urls import reverse_lazy
class ProfileEditView(UpdateView):
"""
Представление для редактирования профиля пользователя.
"""
model = Profile
form_class = ProfileUpdateForm
template_name = 'modules/system/profiles/profile-edit.html'
def get_object(self, queryset=None):
return self.request.user.profile
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Редактирование профиля пользователя: {self.request.user.username}'
if self.request.POST:
context['user_form'] = UserUpdateForm(self.request.POST, instance=self.request.user)
else:
context['user_form'] = UserUpdateForm(instance=self.request.user)
return context
def form_valid(self, form):
context = self.get_context_data()
user_form = context['user_form']
with transaction.atomic():
if all([form.is_valid(), user_form.is_valid()]):
user_form.save()
form.save()
else:
context.update({'user_form': user_form})
return self.render_to_response(context)
return super(ProfileEditView, self).form_valid(form)
def get_success_url(self):
return reverse_lazy('profile', kwargs={'slug': self.object.slug})
Пояснения:
- Мы используем представление на основе классов обновления контента - UpdateView.
- В методе get_object мы передаем текущего пользователя, чтобы не редактировать чужие профили.
- В контексте мы добавляем форму пользователя, где ссылаемся на текущего пользователя.
- В методе form_valid мы используем transaction.atomic, для корректного сохранения данных в нашей БД.
- Проверяем обе формы на правильность, и сохраняем их.
- В методе get_success_url мы ссылаемся на наш профиль, т.е после сохранения мы переходим к нашему профилю.
Давайте добавим в urls.py наше представление:
modules/system/urls.py
from django.urls import path
from modules.system.views import ProfileView, ProfileEditView
urlpatterns = [
path('user/edit/', ProfileEditView.as_view(), name='profile-edit'),
path('user/<str:slug>/', ProfileView.as_view(), name='profile'),
]
Пояснения:
- Обязательно разместите user/edit/ первее user/<str:slug>/, чтобы не было проблем совпадающими именами ссылок.
Отлично, теперь создадим шаблон profile-edit.html в папке templates/modules/system/profiles/
templates/modules/system/profiles/profile-edit.html
{% extends 'main.html' %}
{% block content %}
<div class="card mb-3 border-0 nth-shadow">
<div class="card-body">
<div class="card-title nth-card-title">
<h4>Изменение профиля</h4>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ user_form.as_p }}
{{ form.as_p }}
<div class="d-grid gap-2 d-md-block mt-2">
<button type="submit" class="btn btn-dark">Подтвердить изменение профиля</button>
</div>
</form>
</div>
</div>
{% endblock %}
Пояснения:
- Мы добавили рендеринг двух форм, а также {% csrf_token %} для защиты от фейковых данных и взломов.
- Добавили enctype="multipart/form-data" к форме для загрузки медиа файлов!
Давайте посмотрим на результат:

Отлично, давайте я изменю аватар из прошлого урока, посмотрим на изменения.

Чтож, все сохранилось! И это отлично. Аватарка изменилась, мы перешли в свой же профиль. В следующем уроке мы рассмотрим теорию о CRUD запросах (UpdateView, CreateView, DeleteView), а потом рассмотрим миксины.