Django 34.2. Простой профиль пользователя - страница настроек
В этом посте продолжим делать личный кабинет. Создадим страницу настроек профиля с формами смены данных и пароля пользователя и сделаем страницу со всеми постами пользователя.
Дополнительные материалы
Для скачивания материалов необходимо войти или зарегистрироваться
Файлы также можно получить в Telegram-боте по коду: 828730
Реклама
В предыдущем посте, мы сделали страницу профиля, теперь надо сделать страницу настроек пользователя.
На этой странице пользователь сможет изменить свои данные и поменять пароль.
Также сделаем страницу со всеми постами пользователя.
В процессе напишем две формы, два представления и два шаблона.
Работать будем с файлами в директории приложения user_app
. Приступим.
Формы для страницы настроек.
Откроем файл forms.py
в директории приложения user_app
.
У нас будет две формы:
- Форма изменения данных пользователя - это имя пользователя (username), email, имя и фамилия.
- Форма смены пароля.
Форма изменения данных.
Это типовая форма модели.
Создадим класс UserInfoForm
, унаследованный от forms.ModelForm
.
Описываем четыре поля, добавляя названия полей и стили: username
, email
, first_name
, last_name
.
В подклассе Meta
указываем модель User
и вышеупомянутые поля.
Код формы:
class UserInfoForm(forms.ModelForm):
username = forms.CharField(
max_length=150,
label='Имя пользователя (username)',
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Введите имя пользователя'
})
)
email = forms.EmailField(
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'Введите email'
})
)
first_name = forms.CharField(
max_length=150,
label='Имя',
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Введите ваше имя'
})
)
last_name = forms.CharField(
max_length=150,
label='Фамилия',
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Введите вашу фамилию'
})
)
class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name']
Форма изменения пароля.
Форму смены пароля писать с нуля не будем, т.к. Django предоставляет встроенную форму PasswordChangeForm
.
Мы всего лишь переопределим поля, добавив стили форме.
Создадим класс UserPasswordForm
, унаследованный от PasswordChangeForm
и пропишем три поля: old_password
, new_password1
, new_password2
.
Код формы:
class UserPasswordForm(PasswordChangeForm):
old_password = forms.CharField(
max_length=128,
label='Старый пароль',
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': 'Введите старый пароль'
})
)
new_password1 = forms.CharField(
max_length=128,
label='Новый пароль',
help_text=password_validation.password_validators_help_text_html(),
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': 'Введите новый пароль'
})
)
new_password2 = forms.CharField(
max_length=128,
label='Подтверждение нового пароля',
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': 'Повторите новый пароль'
})
)
Представление страницы настроек.
Достаточно сложное и объёмно представление.
Нам надо передать на страницу две формы, а затем обрабатывать их независимо друг от друга.
Помимо того, что страница должна быть доступна только авторизованному пользователю, она также должна быть доступна только тому пользователю, чья страница настроек открыта.
Например, у нас есть маршрут /user/testuser/settings
, где testuser
это имя пользователя. И если по этому адресу перейдёт testuser
, то он увидит настройки, но если другой пользователь откроет эту страницу, то должна быть ошибка 403 Forbidden
.
Откроем файл views.py
.
Создадим класс UserSettingsView
, в этот раз у нас будет два наследования:
- От класса
LoginRequiredMixin
, проверяющего авторизован пользователь или нет. - От уже знакомого нам класса
TemplateView
.
Прописываем единственное поле template_name
с указанием файла шаблона.
Далее у нас будет переопределено три метода:
Первым мы переопределим метод dispatch()
. С ним мы ещё не сталкивались.
Метод dispatch()
- это один из ключевых методов в классовых представлениях Django. Он является частью жизненного цикла обработки запросов и играет важную роль в определении того, какой метод (GET, POST, PUT, DELETE и т. д.) должен обрабатывать входящий HTTP-запрос.
dispatch()
работает следующим образом:
- Когда клиентский запрос поступает на представление, Django вызывает метод
dispatch()
этого представления. dispatch()
анализирует метод HTTP запроса (GET, POST, PUT, DELETE и т. д.), который пришел от клиента.- В зависимости от метода запроса,
dispatch()
вызывает соответствующий метод класса представления. Например, если это GET-запрос,dispatch()
вызовет методget()
, если это POST-запрос - методpost()
, и так далее. - После обработки запроса соответствующим методом,
dispatch()
возвращает HTTP-ответ.
В нашем методе, мы проверяем, является ли пользователь переданный в адресной строке текущим авторизованным пользователем.
Если является - вызываем метод dispatch()
из родительского класса, в противном случае возвращаем 403 Forbidden
.
Вторым переопределённым методом будет get_context_data
. С ним мы уже сталкивались ранее и он вполне должен быть знаком, но и тут есть нюансы.
В этом методе мы передаём две формы и т.к. у нас формы моделей, нам при их сохранении в переменную, в качестве аргумента необходимо передать объект модели, с которым будет работать форма. Если этого не сделать, при сохранении данных будет создаваться новый объект модели, а не обновляться необходимый.
В качестве аргумента для формы UserInfoForm
мы передаём ключевой аргумент instance
с объектом пользователя.
А для формы UserPasswordForm
, мы передаём объект пользователя в качестве позиционного аргумента.
Третьим мы переопределим метод post
. Он достаточно большой и с первого взгляда можно запутаться, но всё достаточно тривиально.
В теле метода у нас находится if-elif-else
конструкция.
if
проверяет отправку формы с изменением данных.elif
проверяет отправку формы изменения пароля.else
возвращает эту же страницу без изменений.
Структура внутри if
и elif
в целом одинаковая, различается только действие в случае успеха.
В переменную form
помещаем нашу форму, передав в неё объект пользователя и данные отправленные с формы на сайте.
Напомню, что для UserInfoForm
, объект пользователя передаётся как ключевой аргумент instance
, т.е. он будет вторым аргументом, а данные формы как позиционный, т.е. первым.
В случае с формой UserPasswordForm
, оба аргумента передаются как позиционные и первым идёт объект пользователя, а вторым данные формы.
Затем проверяем валидность формы.
Если форма валидна и данные корректны:
Сохраняем форму, тем самым внеся изменения в связанный с ней объект пользователя.
Записываем сообщение об успешном изменении, используя функцию messages
.
Возвращаем результат.
В этом моменте происходят различия кода.
При обработке формы изменения данных, для возврата результата мы используем функцию redirect
, перенаправляющую пользователя на новую страницу. Она нужна, поскольку в адресной строке прописывается имя пользователя и при изменении имени пользователя и вызове стандартного метода get
, происходит перенаправление, но путь в адресной строке не меняется, что влечёт за собой ошибки, а при использовании redirect
, пользователь переходит на новый URL-адрес
, включающий в себя новое имя пользователя.
При обработке формы изменения пароля, для возврата результата мы используем стандартный get
.
Если форма не валидна:
Получаем контекст представления и при помощи метода render
обновляем страницу, включающую в себя введённые ранее пользователем данные.
Код представления:
class UserSettingsView(LoginRequiredMixin, TemplateView):
template_name = 'user_app/profile_settings_page.html'
def dispatch(self, request, *args, **kwargs):
if request.user get_object_or_404(User, username=self.kwargs.get('username')):
return super().dispatch(request, *args, **kwargs)
else:
raise HttpResponseForbidden("Вы не имеете доступа к этой странице.")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_info_form'] = forms.UserInfoForm(instance=self.request.user)
context['user_password_form'] = forms.UserPasswordForm(self.request.user)
context['title'] = f'Настройки профиля {self.request.user}'
return context
def post(self, request, *args, **kwargs):
if 'user_info_form' in request.POST:
form = forms.UserInfoForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
messages.success(request, 'Данные успешно изменены.')
return redirect('user_app:user_profile_settings', form.cleaned_data.get('username'))
else:
context = self.get_context_data(**kwargs)
context['user_info_form'] = form
return render(request, self.template_name, context)
elif 'user_password_form' in request.POST:
form = forms.UserPasswordForm(request.user, request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Пароль успешно изменён.')
return self.get(request, *args, **kwargs)
else:
context = self.get_context_data(**kwargs)
context['user_password_form'] = form
return render(request, self.template_name, context)
else:
return self.get(request, *args, **kwargs)
Шаблон страницы настроек профиля.
В директории с шаблонами приложения user_app
создадим файл profile_settings_page.html
.
В этом файле прописываем две формы: отображение сообщения об успешном изменении и кнопку возврата в профиль.
Обратите внимание на код кнопок отправки формы. В них мы указываем атрибут name
. Именно по этому атрибуту мы отслеживаем в представлении то, какая форма была отправлена.
Код шаблона:
{% extends 'blog/base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container mt-3">
<div class="row d-flex justify-content-between align-items-center">
<div class="col-lg-10 col-sm-12">
<h2>Настройки профиля {{ user }}</h2>
</div>
<div class="col-lg-2 col-sm-12 text-right">
<a class="btn btn-primary my-btn h-100"
href="{% url 'user_app:user_profile' username=user.username %}">Вернуться в профиль</a>
</div>
</div>
<hr>
<div class="row d-flex justify-content-evenly">
<div class="row text-center">
{% if messages %}
<p class="alert alert-success">
{% for message in messages %}
{{ message }}<br>
{% endfor %}
</p>
{% endif %}
</div>
<div class="col-lg-4 col-sm-12">
<h3 class="mb-3 mt-3">Изменить данные:</h3>
<form method="post">
{% csrf_token %}
{{ user_info_form.as_p }}
<button class="btn btn-primary my-btn mt-2" type="submit" name="user_info_form">Отправить</button>
</form>
</div>
<div class="col-lg-4 col-sm-12">
<h3 class="mb-3 mt-3">Изменить пароль:</h3>
<form method="post">
{% csrf_token %}
{{ user_password_form.as_p }}
<button class="btn btn-primary my-btn mt-2" type="submit" name="user_password_form">Отправить
</button>
</form>
</div>
</div>
</div>
{% endblock %}
Страница с постами пользователя.
Представление.
В файле views.py
создадим класс UserPostsView
, унаследованный от ListView
.
Пропишем три поля:
template_name
- файл шаблона.context_object_name
- имя переменной в шаблонеpaginate_by
- количество элементов на странице
Переопределим метод get_queryset
. В нём вернём список постов, отфильтрованных по переданному в URL-адресе пользователю.
В переопределённом методе get_context_data
, в блоке try-except
получим объект пользователя по имени пользователя из URL-адреса и запишем его в контекст.
Код представления:
class UserPostsView(ListView):
template_name = 'user_app/user_posts_page.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
return PostModel.post_manager.filter(author__username=self.kwargs.get('username'))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
try:
author = get_object_or_404(User, username=self.kwargs.get("username"))
except User.DoesNotExist:
raise Http404("Пользователь не найден")
context['author'] = author
context['title'] = f'Посты пользователя {author}'
return context
Шаблон.
В директории с шаблонами приложения user_app
создадим файл user_posts_page.html
.
В качестве шаблона я использовал код из файла tag_page.html
.
URL-паттерны страницы настроек и всех постов пользователя.
Откроем файл urls.py
в директории приложения user_app
и добавим следующие строки в список urlpatterns
:
path('<str:username>/settings/', views.UserSettingsView.as_view(), name='user_profile_settings'),
path('<str:username>/posts/', views.UserPostsView.as_view(), name='user_posts'),
Завершение.
Осталось изменить шаблон страницы профиля, описанный в прошлом посте, а именно, исправить код кнопок.
Кнопка все посты:
<a class="btn btn-primary my-btn mb-3"
href="{% url 'user_app:user_posts' username=user_profile.username %}">Все посты {{ user_profile }}
</a>
Кнопка настроек профиля:
<a class="btn btn-primary my-btn me-1"
href="{% url 'user_app:user_profile_settings' username=user_profile.username %}">Настройки профиля
</a>
Готово. За эти два поста, мы реализовали публичный профиль пользователя, страницу настроек и страницу всех постов, используя стандартную модель пользователя Django.
Все статьи