環境及びやりたかったこと
Python3.9.0
Django3.2
以下のようにユーザー登録用のビューを作った。
見たままなのだが、軽く解説すると入力されたデータをSignUpFormという自作のFormクラスで検証し、バリデーションが通ればユーザーを登録し、ルートパスにリダイレクトする、という挙動を行うクラスになっている。
また、リダイレクト後の画面で「ユーザー情報を登録しました。ご利用いただきありがとうございます。」というメッセージを表示したいので、SignUpFormのバリデーションが通った際に呼び出されるform_valid()をオーバーライドしてdjango.contrib.messages.info()を使ってメッセージを入力している。1
from django.views.generic import edit, View
from django.contrib import messages
from .models import User
class SignUpView(edit.CreateView):
"""ユーザー登録用ビュークラス"""
model = User
form_class = SignUpForm
template_name = 'users/signup.html'
success_url = '/'
def form_valid(self, form):
"""ユーザー登録時にmessageを表示する"""
messages.info(self.request, 'ユーザー情報を登録しました。ご利用いただきありがとうございます。')
return response
このメッセージの内容を検証したいというのが今回やりたかったこと。
ルーティング、Userモデル、SignUpFormは以下のとおり。
from django.urls import path
from .views import SignUpView
app_name = 'users'
urlpatterns = [
path('signup/', SignUpView.as_view(), name='signup'),
]
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.utils import timezone
from django.db import models
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **kwargs):
# emailを必須に
if not email:
raise ValueError('メールアドレスは必須です')
email = self.normalize_email(email)
user = self.model(email=email, **kwargs)
user.set_password(password)
user.save(using=self.db)
return user
def create_user(self, email, password=None, **kwargs):
kwargs.setdefault('is_staff', False)
kwargs.setdefault('is_superuser', False)
return self._create_user(email, password, **kwargs)
def create_superuser(self, email, password, **kwargs):
kwargs.setdefault('is_staff', True)
kwargs.setdefault('is_superuser', True)
if kwargs.get('is_staff') is not True:
raise ValueError('superuserのis_staffはTrueである必要があります')
if kwargs.get('is_superuser') is not True:
raise ValueError('superuserのis_superuserはTrueである必要があります')
return self._create_user(email, password, **kwargs)
class User(AbstractBaseUser, PermissionsMixin):
"""ユーザーモデル"""
class Meta:
verbose_name_plural = 'ユーザー'
email = models.EmailField(verbose_name='メールアドレス', unique=True)
is_staff = models.BooleanField(verbose_name='is_staff', default=False)
is_active = models.BooleanField(verbose_name='is_active', default=True)
date_joined = models.DateTimeField(verbose_name='登録日', default=timezone.now)
objects = UserManager()
USERNAME_FIELD = "email"
EMAIL_FIELD = "email"
REQUIRED_FIELDS = []
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import User
class SignUpForm(UserCreationForm, UserFormMixin):
"""ユーザーモデル用フォーム"""
class Meta:
model = User
fields = ['email']
テストコードは下記の通り。
from django.test import TestCase
from users.models import User
class TestSignUpView(TestCase):
"""SignUpViewのテストクラス"""
def test_post_success(self):
"""
/users/signup/ にPOSTリクエストすると
ユーザー登録が成功することを検証
"""
# テストクライアントでPOSTリクエストを送信
response = self.client.post('/users/signup/',
{
'email': 'user@example.com',
'password1': 'examplepassword123',
'password2': 'examplepassword123'
})
.
.
.
# メッセージの内容を検証
# message = 何らかの処理
self.assertEqual(message, 'ユーザー情報を登録しました。ご利用いただきありがとうございます。')
ここでresponseからどのようにしてメッセージの内容を取り出し、messageに格納するかで少し詰まったので備忘録として残しておく。
解決した方法
from django.test import TestCase
from django.contrib.messages import get_messages # 追加
from users.models import User
class TestSignUpView(TestCase):
"""SignUpViewのテストクラス"""
def test_post_success(self):
"""
/users/signup/ にPOSTリクエストすると
ユーザー登録が成功することを検証
"""
# テストクライアントでPOSTリクエストを送信
response = self.client.post('/users/signup/',
{
'email': 'user@example.com',
'password1': 'examplepassword123',
'password2': 'examplepassword123'
})
.
.
.
# メッセージの内容を検証
messages = list(get_messages(response.wsgi_request)) #追加
message = str(messages[0]) #追加
self.assertEqual(message, 'ユーザー情報を登録しました。ご利用いただきありがとうございます。')
少し長くなったのでmessageの中身を決める処理を2行に分けた。
追加された2行に関する調査及び疑問点
よくわからないものをわからないまま使うのも姿勢としてよくないだろうと思ったのでこの2行で何をやってるのかをDjangoのソースコードとドキュメントを読んで調べた。
公式ドキュメントを見ると、DjangoのテストクライアントはHttpResponseオブジェクトを生成する際、送信されたリクエストの内容を表すHttpResponseオブジェクトをWSGIRequestというHttpResponseのサブクラスの型に変換し、それをresponse.wsgi_requestに格納するようだった。
django.contrib.messages.get_messages()はHttpResponseオブジェクトの中からFallbackStorageオブジェクトを取得する。
このFallbackStorageオブジェクトが普段メッセージストレージと呼ばれているものを提供するクラスのようだ。
なお、get_messages()はメッセージが存在しない場合は空のリストを返すのだが、メッセージが存在する場合に返されるFallbackStorageオブジェクトは添字に対応していない為、そのままget_messages()[0]として要素を取り出そうとすると、「TypeError: 'FallbackStorage' object is not subscriptable」と怒られる。
そのためlist()で変換しているので、次にFallbackStorageクラスの__iter__()2を確認したのだが、この処理が意味するところは私の力量が足りず理解できなかった。
結果としてはlist()にFallbackStorageオブジェクトを渡すことでMessageオブジェクトのリストが得られるのだが、Messageクラスの__str__()がMessageインスタンスのmessageプロパティを返すようになっているため、メッセージの内容を確認するにはただstr()に渡せばいいようだ。