Djangoメモ(24) : ユーザー認証を実装する〜パスワード変更
A Complete Beginner's Guide to Djangoのチュートリアルを参考にパスワード変更機能を実装する。
パスワード変更
サインアップ、ログアウト、ログイン、パスワードリセット機能を実装してきたが、最後にパスワード変更機能を実装する。
Djangoではパスワード変更のビューも提供されているのでmyproject/urls.py
に以下の2行を追加。
path( 'settings/password/', auth_views.PasswordChangeView.as_view( template_name='password_change.html'), name='password_change'), path('settings/password/done/', auth_views.PasswordChangeDoneView.as_view( template_name='password_change_done.html'), name='password_change_done'),
パスワード変更機能はログインしているユーザーのみが使用できる機能だが、ログインが必要な処理かどうかは@login_required
デコレータ(次のチュートリアルで説明)により実現されている。
もしログインしていないユーザーがこのページにアクセスしようとした場合はログインページにリダイレクトされる。
ログインページの定義はmyproject/settings.py
に下記行を追加する。
LOGIN_URL = 'login'
設定が完了したので、新しいパスワード入力フォームのtemplates/password_change.html
テンプレートを作成する。
{% extends 'base.html' %} {% block title %}Change password{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item active">Change password</li> {% endblock %} {% block content %} <div class="row"> <div class="col-lg-6 col-md-8 col-sm-10"> <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" class="btn btn-success">Change password</button> </form> </div> </div> {% endblock %}
これでsettings/password/
にアクセスすると下のページが表示されるようになる。
バリデーションエラーメッセージも問題なく表示される。
もしログインしていない状態でsettings/password/
にアクセスするとLOGIN_URL = 'login'
で設定したページにリダイレクトされる。
次にパスワード変更が完了したことをユーザーに伝えるtemplates/password_change_done.html
テンプレートを作成する。
{% extends 'base.html' %} {% block title %}Change password successful{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'password_change' %}">Change password</a></li> <li class="breadcrumb-item active">Success</li> {% endblock %} {% block content %} <div class="alert alert-success" role="alert"> <strong>Success!</strong> Your password has been changed! </div> <a href="{% url 'home' %}" class="btn btn-secondary">Return to home page</a> {% endblock %}
パスワード変更が完了した際には下のページが表示される。
パスワード変更のテスト
パスワード変更のテストは下記コードを参照。
accounts/tests/test_view_password_change.py
(クリックで展開)from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.models import User
from django.contrib.auth import views as auth_views
from django.urls import reverse, resolve
from django.test import TestCase
class PasswordChangeTests(TestCase):
def setUp(self):
username = 'john'
password = 'secret123'
user = User.objects.create_user(username=username, email='john@doe.com', password=password)
url = reverse('password_change')
self.client.login(username=username, password=password)
self.response = self.client.get(url)
def test_status_code(self):
self.assertEquals(self.response.status_code, 200)
def test_url_resolves_correct_view(self):
view = resolve('/settings/password/')
self.assertEquals(view.func.view_class, auth_views.PasswordChangeView)
def test_csrf(self):
self.assertContains(self.response, 'csrfmiddlewaretoken')
def test_contains_form(self):
form = self.response.context.get('form')
self.assertIsInstance(form, PasswordChangeForm)
def test_form_inputs(self):
'''
The view must contain four inputs: csrf, old_password, new_password1, new_password2
'''
self.assertContains(self.response, '<input', 4)
self.assertContains(self.response, 'type="password"', 3)
class LoginRequiredPasswordChangeTests(TestCase):
def test_redirection(self):
url = reverse('password_change')
login_url = reverse('login')
response = self.client.get(url)
self.assertRedirects(response, f'{login_url}?next={url}')
class PasswordChangeTestCase(TestCase):
'''
Base test case for form processing
accepts a `data` dict to POST to the view.
'''
def setUp(self, data={}):
self.user = User.objects.create_user(username='john', email='john@doe.com', password='old_password')
self.url = reverse('password_change')
self.client.login(username='john', password='old_password')
self.response = self.client.post(self.url, data)
class SuccessfulPasswordChangeTests(PasswordChangeTestCase):
def setUp(self):
super().setUp({
'old_password': 'old_password',
'new_password1': 'new_password',
'new_password2': 'new_password',
})
def test_redirection(self):
'''
A valid form submission should redirect the user
'''
self.assertRedirects(self.response, reverse('password_change_done'))
def test_password_changed(self):
'''
refresh the user instance from database to get the new password
hash updated by the change password view.
'''
self.user.refresh_from_db()
self.assertTrue(self.user.check_password('new_password'))
def test_user_authentication(self):
'''
Create a new request to an arbitrary page.
The resulting response should now have an `user` to its context, after a successful sign up.
'''
response = self.client.get(reverse('home'))
user = response.context.get('user')
self.assertTrue(user.is_authenticated)
class InvalidPasswordChangeTests(PasswordChangeTestCase):
def test_status_code(self):
'''
An invalid form submission should return to the same page
'''
self.assertEquals(self.response.status_code, 200)
def test_form_errors(self):
form = self.response.context.get('form')
self.assertTrue(form.errors)
def test_didnt_change_password(self):
'''
refresh the user instance from the database to make
sure we have the latest data.
'''
self.user.refresh_from_db()
self.assertTrue(self.user.check_password('old_password'))
テストコードのself.user.refresh_from_db()
はデータベースから最新の値を再読込する処理。
最後にテストが通ることを確認しておく。
$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). ................................................................... ---------------------------------------------------------------------- Ran 67 tests in 9.474s OK Destroying test database for alias 'default'...
まとめ
PasswordChangeView
はパスワード変更処理PasswordChangeDoneView
はパスワード変更が完了したことを表示- パスワード変更はログインしているユーザーのみが利用可能
- ログインしていないユーザーはログインページにリダイレクトされる
LOGIN_URL = 'login'
でログインページを指定