HOWTO · Django

Django のパスワード変更

この説明では、ユーザーが Django の Web ページにログインした後にパスワードを変更するための Web ページを作成する方法について学習します。

このページの内容

Django 標準の PasswordChangeView を使うと、認証済みユーザーは現在のアカウントのパスワードを変更できます。これは、現在のパスワードがなくてもアカウントへのアクセスを取り戻せるようにするパスワードリセットとは異なるフローです。以下では、フォーム、完了ページ、URL 名、ホームページからのリンクを一連の流れとして設定します。

パスワード変更ビューを設定する

users/views.py で Django の PasswordChangeView を継承し、フォーム用テンプレートを指定します。reverse_lazy を使って success_url を設定すると、URL 設定の読み込み後に Django が名前付きの遷移先を解決します。完了ページには PasswordChangeDoneView を使います。PasswordResetDoneView はパスワードリセット用です。

この自己完結型の設定用ハーネスでは、Django の代わりに小さなスタンドインを使っているため、Django プロジェクトがなくても名前と設定のつながりを確認できます。実際のプロジェクトでは、インポートとアプリケーション定義を残し、ハーネスのセットアップと最後のアサーションは省いてください。Django のパスワード変更フォームは、送信された現在のパスワードと、新しいパスワードの確認値を、設定済みのパスワードバリデーターで検証します。

このセクションのコードと表示されるラベルや出力は英語のままです。

# Harness only: stand-ins let this configuration run without Django installed.
import sys
import types

django = types.ModuleType("django")
contrib = types.ModuleType("django.contrib")
auth = types.ModuleType("django.contrib.auth")
auth_views = types.ModuleType("django.contrib.auth.views")
urls = types.ModuleType("django.urls")
django.__path__ = []
contrib.__path__ = []
auth.__path__ = []

class View:
    @classmethod
    def as_view(cls):
        return cls

class PasswordChangeView(View):
    pass

class PasswordChangeDoneView(View):
    pass

def reverse_lazy(name):
    return name

def path(route, view, name=None):
    return (route, view, name)

def include(module, namespace=None):
    return (module, namespace)

auth_views.PasswordChangeView = PasswordChangeView
auth_views.PasswordChangeDoneView = PasswordChangeDoneView
urls.reverse_lazy = reverse_lazy
urls.path = path
urls.include = include
sys.modules.update({
    "django": django,
    "django.contrib": contrib,
    "django.contrib.auth": auth,
    "django.contrib.auth.views": auth_views,
    "django.urls": urls,
})

users_views = types.ModuleType("users_views")
users_views.ChangePasswordView = None
users_views.ChangePasswordDoneView = None
sys.modules["users_views"] = users_views

# users/views.py
from django.contrib.auth.views import PasswordChangeDoneView, PasswordChangeView
from django.urls import reverse_lazy

class ChangePasswordView(PasswordChangeView):
    template_name = "users/password-change.html"
    success_url = reverse_lazy("users:password-change-done")

class ChangePasswordDoneView(PasswordChangeDoneView):
    template_name = "users/password-change-done.html"

users_views.ChangePasswordView = ChangePasswordView
users_views.ChangePasswordDoneView = ChangePasswordDoneView

# users/urls.py
# In the project file, import these classes from .views.
from django.urls import path
from users_views import ChangePasswordDoneView, ChangePasswordView

app_name = "users"
urlpatterns = [
    path("change-password/", ChangePasswordView.as_view(), name="password-change"),
    path("change-password/done/", ChangePasswordDoneView.as_view(), name="password-change-done"),
]

# project urls.py
from django.urls import include, path

project_urlpatterns = [
    path("users/", include("users.urls", namespace="users")),
]

assert ChangePasswordView.success_url == "users:password-change-done"
assert [route[2] for route in urlpatterns] == ["password-change", "password-change-done"]
assert project_urlpatterns[0][1] == ("users.urls", "users")

実際のプロジェクトでは、アプリの URL ファイルで from .views import ChangePasswordDoneView, ChangePasswordView とビューをインポートします。この例では、独立して実行できる1つのブロックにまとめるため、users_views を使っています。プロジェクトの URL ファイルでは、django.urls から include と path をインポートし、ハーネスと同じ path("users/", include("users.urls", namespace="users")) の設定を使用してください。

アプリとプロジェクトの URL パターンを登録する

上のハーネスには、アプリ側とプロジェクト側の両方の URL 設定が含まれています。実際のプロジェクトでは、users/urls.py に app_name = "users" を設定し、プロジェクトの URL 設定でそのモジュールを namespace="users" とともに読み込みます。この2つを設定すると users:password-change-done を解決できるようになります。アプリ側のルート名と reverse_lazy の遷移先も一致させてください。

パスワード変更用と完了用のテンプレートを作成する

users/templates/users/password-change.html を作成し、共通の base.html を継承します。このテンプレートは同じ URL に POST し、フォーム内に {% csrf_token %} を含め、現在のパスワードと新しいパスワードの入力欄を {{ form.as_p }} で表示します。CSRF トークンは Django のリクエスト偽造対策を支援するもので、認証やパスワード検証の代わりにはなりません。

このセクションのコードと表示されるラベルや出力は英語のままです。

{% extends 'base.html' %}

{% block title %}Change password{% endblock title %}

{% block content %}
<h1>Change your password</h1>
<form action="" method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Change password</button>
</form>
{% endblock content %}

成功時に表示する users/templates/users/password-change-done.html を作成します。同じベーステンプレートを継承し、パスワードが変更されたことを伝え、プロジェクトの home-view ルートに戻るリンクを設けてください。POST が成功するとこのページにリダイレクトされます。このページを表示するだけではパスワードは変更されません。

{% extends 'base.html' %}

{% block title %}Password changed{% endblock title %}

{% block content %}
<h1>Your password has been changed</h1>
<a href="{% url 'home-view' %}">Home page</a>
{% endblock content %}

ホームページからリンクする

ホームテンプレートに {% url 'users:password-change' %} を使ってフォームへのリンクを追加します。Django 標準のパスワード変更ビューは認証済みユーザーを必要とするため、ログイン後のナビゲーションにリンクを置くか、未ログインの訪問者をビューからリダイレクトするようにしてください。

このセクションのコードと表示されるラベルや出力は英語のままです。

<a href="{% url 'users:password-change' %}">Change password</a>

パスワード変更の流れを実行して確認する

通常の開発環境でプロジェクトを起動し、テスト用アカウントでログインして、新しく追加したホームページのリンクを開きます。現在のパスワードと、条件を満たす一致した新しいパスワードを送信してください。Django が完了ページへリダイレクトすることを確認したら、いったんログアウトし、新しいパスワードで再度ログインします。これは手動で確認するための手順であり、この記事の環境でフローを実行したという報告ではありません。

名前空間とルートの関係は、次の簡単なルート名チェックで確認できます。

このセクションのコードと表示されるラベルや出力は英語のままです。

app_name = "users"
route_name = "password-change-done"
success_target = f"{app_name}:{route_name}"
print(success_target)

よくある設定エラーを解決する

Django が users:password-change-done を逆引きできないと報告した場合は、app_name、プロジェクト側の include に指定した名前空間、ルートの name、reverse_lazy の遷移先が一致しているか確認してください。TemplateDoesNotExist エラーが出る場合は、多くの場合、ビューで指定したテンプレートのパスと、アプリの templates/users/ ディレクトリ内の配置が一致していません。綴りを確認し、Django がテンプレートを検出できるようアプリがインストールされていることも確かめてください。

フォームで CSRF エラーが返る場合は、POST フォーム内に {% csrf_token %} を置き、Django の通常のリクエスト処理を通じてページが送信されることを確認してください。訪問者がログインページへリダイレクトされるのは、この認証済みユーザー向け機能では想定どおりです。パスワード変更を試す前にログインしてください。変更に成功していないのに完了ページが表示される場合は、フォームがビューへ POST していること、送信した現在のパスワードと新しいパスワードの確認値がプロジェクトのバリデーターを満たしていることを確認してください。