氏名の変更ができるようになったので,次はパスワードの変更機能を実装しよう.まず,パスワード変更のためのページとパスワード変更が完了したあとに表示されるページの URL を定義します.
users/urls.py
from django.urls import path
from . import views
app_name = 'users'
urlpatterns = [
    path('', views.users_index, name='index'),
    path('<int:user_id>/', views.users_profile, name='profile'),
    path('<int:user_id>/update/', views.users_update, name='update'),
    path('login/', views.LoginView.as_view(), name='login'),
    path('logout/', views.LogoutView.as_view(), name='logout'),
    path('password/', views.PasswordChangeView.as_view(), name='password_change_form'),
    path('password_change_done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
]
次に,forms.py には Django のソース contrib から次の2つのクラスをコピーします.
class SetPasswordForm(forms.Form):class PasswordChangeForm(SetPasswordForm):form-control クラスを追加します.
users\forms.py
class SetPasswordForm(forms.Form):
    """
    A form that lets a user change set their password without entering the old
    password
    """
    error_messages = {
        "password_mismatch": _("The two password fields didn’t match."),
    }
    new_password1 = forms.CharField(
        label=_("New password"),
        widget=forms.PasswordInput(attrs={"autocomplete": "new-password", 'class': 'form-control'}),
        strip=False,
        help_text=password_validation.password_validators_help_text_html(),
    )
    new_password2 = forms.CharField(
        label=_("New password confirmation"),
        strip=False,
        widget=forms.PasswordInput(attrs={"autocomplete": "new-password", 'class': 'form-control'}),
    )
    def __init__(self, user, *args, **kwargs):
        self.user = user
        super().__init__(*args, **kwargs)
    def clean_new_password2(self):
        password1 = self.cleaned_data.get("new_password1")
        password2 = self.cleaned_data.get("new_password2")
        if password1 and password2:
            if password1 != password2:
                raise ValidationError(
                    self.error_messages["password_mismatch"],
                    code="password_mismatch",
                )
        password_validation.validate_password(password2, self.user)
        return password2
    def save(self, commit=True):
        password = self.cleaned_data["new_password1"]
        self.user.set_password(password)
        if commit:
            self.user.save()
        return self.user
class PasswordChangeForm(SetPasswordForm):
    """
    A form that lets a user change their password by entering their old
    password.
    """
    error_messages = {
        **SetPasswordForm.error_messages,
        "password_incorrect": _(
            "Your old password was entered incorrectly. Please enter it again."
        ),
    }
    old_password = forms.CharField(
        label=_("Old password"),
        strip=False,
        widget=forms.PasswordInput(
            attrs={"autocomplete": "current-password", "autofocus": True, 'class': 'form-control'}
        ),
    )
    field_order = ["old_password", "new_password1", "new_password2"]
    def clean_old_password(self):
        """
        Validate that the old_password field is correct.
        """
        old_password = self.cleaned_data["old_password"]
        if not self.user.check_password(old_password):
            raise ValidationError(
                self.error_messages["password_incorrect"],
                code="password_incorrect",
            )
        return old_password
さらに,views.py についても Django の contrib から次の3つのクラスをコピーし,数行を編集します.
class PasswordContextMixin:class PasswordChangeView(PasswordContextMixin, FormView):class PasswordChangeDoneView(PasswordContextMixin, TemplateView):users\views.py
from urllib.parse import urlparse, urlunparse
from django.conf import settings
# Avoid shadowing the login() and logout() views below.
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import login_required
# from django.contrib.auth.forms import (
#    AuthenticationForm,
#    PasswordChangeForm,
#    PasswordResetForm,
#    SetPasswordForm,
# )
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.http import HttpResponseRedirect, QueryDict
from django.shortcuts import resolve_url
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme, urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
from django.shortcuts import render
from django.shortcuts import get_object_or_404
from django.shortcuts import redirect
from django.urls import reverse
from django.contrib import messages
# from django.shortcuts import redirect
# from django.urls import reverse
from .forms import UserForm
from .forms import AuthenticationForm
from .forms import PasswordChangeForm
UserModel = get_user_model()
# Create your views here.
...(中略)...
class PasswordContextMixin:
    extra_context = None
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({"title": self.title, **(self.extra_context or {})})
        return context
class PasswordChangeView(PasswordContextMixin, FormView):
    form_class = PasswordChangeForm
    success_url = reverse_lazy("users:password_change_done")
    template_name = "users/password_change_form.html"
    title = _("Password change")
    @method_decorator(sensitive_post_parameters())
    @method_decorator(csrf_protect)
    @method_decorator(login_required(login_url='/users/login/'))
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)
    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs["user"] = self.request.user
        return kwargs
    def form_valid(self, form):
        form.save()
        # Updating the password logs out all other sessions for the user
        # except the current one.
        update_session_auth_hash(self.request, form.user)
        return super().form_valid(form)
class PasswordChangeDoneView(PasswordContextMixin, TemplateView):
    template_name = "users/password_change_done.html"
    title = _("Password change successful")
    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)
パスワード変更の HTML ファイルを作成します.
users/templates/users/password_change_form.html
{% extends "base.html" %}
{% block title %}
パスワード変更
{% endblock %}
{% block content %}
<h1 class="my-5">コメントアプリケーション</h1>
<div class="card">
    <div class="card-header">パスワードリセット</div>
    <div class="card-body">
        {% if form.errors %}
            <p class="errornote">
                次のエラーがありました.
            </p>
        {% endif %}
        <form method="post">
            {% csrf_token %}
            {{ form.old_password.errors }}
            {{ form.new_password1.errors }}
            {{ form.new_password2.errors }}
            {{ form.as_p }}
            <button type="submit" class="btn btn-primary btn-block">
                パスワードを変更する
            </button>
        </form>
    </div>
    <div class="card-footer">
        <p>
        </p>
    </div>
  </div>
{% endblock content %}
パスワード変更が完了したあとに表示されるページを作成します.
users/templates/users/password_change_done.html
{% extends "base.html" %}
{% block title %}
パスワード変更完了
{% endblock %}
{% block content %}
<h1 class="my-5">パスワードを変更しました</h1>
<ul>
  <li>
    <a href="/">トップページへ</a>
  </li>
</ul>
{% endblock content %}
Navbar にパスワード変更のためのリンクを作成します.
comments/templates/base.html
{% if user.is_authenticated %}
  <ul class="navbar-nav ml-auto">
    <li class="nav-item dropdown">
      <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
        {{ user.username }}
      </a>
      <ul class="dropdown-menu">
        <li><a class="dropdown-item" href="{% url 'users:profile' user.id %}">Profile</a></li>
        <li><a class="dropdown-item" href="{% url 'users:update' user.id %}">Edit Profile</a></li>
        <li><a class="dropdown-item" href="{% url 'users:password_change_form' %}">Change Password</a></li>
        <li><a class="dropdown-item" href="#">Action</a></li>
        <li><a class="dropdown-item" href="#">Another action</a></li>
        <li><hr class="dropdown-divider"></li>
        <li><a class="dropdown-item" href="{% url 'users:logout' %}">ログアウト</a></li>
      </ul>
    </li>
  </ul>
{% else %}
  <ul class="navbar-nav ml-auto">
    <li class="nav-item">
      <a class="nav-link" href="{% url 'users:index' %}">Login</a>
    </li>
  </ul>
{% endif %}
Navbar から「Change Password」のリンクを開きます.
パスワードを変更します.
パスワードの変更ができれば,次のページに移動します.
  なお,ログアウトした状態で http://127.0.0.1:8000/users/password/ にアクセスしてもログイン画面にリダイレクトされることを確認すると良いでしょう.また,views.py の 67 行目 @method_decorator(login_required(login_url='/users/login/')) を変更し忘れるとエラーになることも合わせて確認しておくと良いでしょう.