今回はDjangoでのバリデーション処理に使う
add_error
の使い方についてアウトプット記事を書いていきます。
目的
ユーザー編集の部分で add_error
を使って
複数フィールドのバリデーションエラーを発生させる
以下が公式ドキュメントのエラー処理の部分です。
前提
必要最低限のファイルの内容しか書きません
ご了承ください。
以下に前提条件を記します。
- フォルダ構成
- Model
- View
フォルダ構成
manage.py
config/
apps/
accounts/
models.py
forms.py
profiles/
views.py
Model
import datetime
from django.db import models
from django.contrib.auth.models import AbstractUser
from cloudinary.models import CloudinaryField
# デフォルトを定義
DEFAULT_VALUES = {
'icon_image': 'https://res.cloudinary.com/dyfjcfcfm/image/upload/v1742972473/default_icon_uienbj.jpg',
'header_image': 'https://res.cloudinary.com/dyfjcfcfm/image/upload/v1742977104/header_image_q4wgax.jpg',
'self_introduction': 'NO_SELF_INTRODUCTION',
'place': 'NO_PLACE',
'website': 'NO_WEBSITE',
'birthdate': datetime.date(1000, 1, 1),
}
class CustomUser(AbstractUser):
# 不要なフィールドは None にする
first_name = None
last_name = None
date_joined = None
username = models.CharField(
verbose_name='ユーザー名',
max_length=50,
unique=True,
validators=[AbstractUser.username_validator],
help_text='英数字とアンダースコアのみ使用できます。50文字以内で入力して下さい。',
error_messages={
'unique': 'このユーザー名はすでに使用されています。',
'invalid': '使用できない文字が含まれています',
}
)
email = models.EmailField(
verbose_name='メールアドレス',
max_length=254,
unique=True,
help_text='.com, .co.jp, .jp で終わる254文字以内のメールアドレスを使用できます。',
error_messages={
'unique': 'このメールアドレスはすでに使用されています。',
'invalid': '有効なメールアドレスを入力して下さい。',
'required': 'メールアドレスは必須です。',
}
)
icon_image = CloudinaryField('icon_image', default=DEFAULT_VALUES['icon_image'])
header_image = CloudinaryField('header_image', default=DEFAULT_VALUES['header_image'])
self_introduction = models.CharField(
max_length=160,
blank=True,
default=DEFAULT_VALUES['self_introduction'],
help_text='最大文字数は160文字です。'
)
place = models.CharField(
max_length=30,
blank=True,
default=DEFAULT_VALUES['place'],
help_text='30文字以内で入力して下さい'
)
website = models.CharField(
max_length=100,
blank=True,
default=DEFAULT_VALUES['website']
)
phone_number = models.CharField(
verbose_name="電話番号",
max_length=10,
default="NO_PHONE",
blank=True,
help_text='ハイフンなしで入力して下さい'
)
birthdate = models.DateField(
verbose_name="生年月日",
default=DEFAULT_VALUES['birthdate'],
blank=True,
help_text='今日より前の日付を選択して下さい'
)
class Meta:
db_table = "users"
def __str__(self):
return self.username
View
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import UpdateView
from apps.accounts.models import CustomUser
from apps.accounts.forms import CustomUpdateForm
class ProfileUpdateView(LoginRequiredMixin, UpdateView):
model = CustomUser
form_class = CustomUpdateForm
template_name = "profile.html"
success_url = reverse_lazy('profile:posts')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['view_mode'] = 'edit'
return context
def form_invalid(self, form):
""" バリデーション失敗 """
return super().form_invalid(form)
def form_valid(self, form):
""" バリデーション成功 """
return super().form_valid(form)
処理の流れ
プロフィールページ
↓
プロフィール編集ボタンクリック
↓
プロフィール情報編集フォーム出力
↓
編集して更新ボタンをクリック
↓
エラーがある場合バリデーションエラーを出力
↓
バリデーションが成功の場合はプロフィールページに戻る
今回は何故ValidationErrorを使わないのか
ValidationError
を使用しない理由は以下です。
- 複数のフィールドで相互検証やエラーを蓄積したい
-
ValidationError
は単一フィールドの検証で処理を中断してしまう
一例ですが、add_error
を使うと以下の3つの状態で更新ボタンをクリックした際に全てに対してエラーを出力可能です。
- ユーザー名で使用できない記号を使った → エラー
- メールアドレスで文字数オーバーで入力した → エラー
- パスワードで簡単すぎるパスワードにした → エラー
例えば以下のように生年月日をDBに保存する際
今日の日付より前の日付だった場合は
「生年月日」という単一フィールドの検証で処理を中断する必要があるため
ValidationError
を使用することが適しています。
import datetime
from django.core.exceptions import ValidationError
class User(models.Model):
birth_date = models.DateField()
def clean(self):
if self.birth_date and self.birth_date > datetime.date.today():
raise ValidationError({'birth_date': '生年月日は今日より前の日付である必要があります'})
Form
from apps.accounts.models import CustomUser
from django import forms
from apps.accounts.models import DEFAULT_VALUES
from cloudinary.forms import CloudinaryFileField
class CustomUpdateForm(forms.ModelForm):
class Meta:
model = CustomUser
fields = ['username', 'email', 'header_image', 'icon_image', 'self_introduction', 'place', 'website', 'birthdate']
labels = {
'username': 'ユーザー名(必須)',
'email': 'メールアドレス(必須)',
'self_introduction': '自己紹介',
'place': '場所',
'website': 'Webサイト',
'birthdate': '生年月日',
}
widgets = {
'self_introduction': forms.Textarea(attrs={
'name': 'self_introduction',
'rows': 4,
'class': 'form-control',
'id': 'id_self_introduction'
}),
'birthdate': forms.NumberInput(attrs={
'type': 'date',
'name': 'birthdate',
'class': 'form-control',
'id': 'birthdate'
}),
}
def clean(self):
cleaned_data = super().clean()
old_password = cleaned_data.get('old_password')
new_password = cleaned_data.get('new_password')
confirm_password = cleaned_data.get('confirm_password')
# 古いパスワードのみが入力された場合
if old_password and not new_password and not confirm_password:
self.add_error('new_password', '新しいパスワードを入力して下さい')
# 新しいパスワードが入力されたが古いパスワードがない場合
if (new_password or confirm_password) and not old_password:
self.add_error('old_password', '古いパスワードを入力して下さい')
# 新しいパスワードと確認用パスワードが一致しない場合
if new_password and confirm_password and new_password != confirm_password:
self.add_error('confirm_password', '新しいパスワードと確認用パスワードが一致しません')
# 古いパスワードの検証
if old_password and self.instance and self.instance.pk:
if not self.instance.check_password(old_password):
self.add_error('old_password', '古いパスワードが正しくありません')
return cleaned_data
Template
今回はadd_error
でフィールドを指定しているので
field.errors
リストにエラーメッセージが追加されます。
そのため、テンプレート側ではエラーがあったらループで出力されるようにします。
{% if field.errors %}
<div class="alert alert-danger">
<ul>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
non_field_error
は?
今回のFormは全てfieldを指定していたため
field.errors
で完了しています。
ただ、以下の例の場合はnon_field_errors
を使うことになります。
-
add_error
でフィールドを指定していない(None)場合 -
ValidationError
を指定する場合
def clean(self):
cleaned_data = super().clean()
start_date = cleaned_data.get('start_date')
end_date = cleaned_data.get('end_date')
if start_date and end_date and start_date > end_date:
# フィールド名としてNoneを指定することでnon_field_errorsになる
self.add_error(None, "終了日は開始日より後である必要があります")
class LoginForm(forms.Form):
username = forms.CharField()
password = forms.CharField(widget=forms.PasswordInput)
def clean(self):
cleaned_data = super().clean()
username = cleaned_data.get('username')
password = cleaned_data.get('password')
user = authenticate(username=username, password=password)
if not user:
# 認証失敗をフォーム全体のエラーとして表示
raise ValidationError("ユーザー名またはパスワードが正しくありません")
最後に
このアウトプットを書いた理由として
add_error
をよく理解せずに実装をしており
フィールドを指定しているのにも関わらずnon_field_errors
を
テンプレートで使用していました。
そのため、デバックでエラー処理ができているが
バリデーションエラーが出力されないといったことになってしまいました。
もしよかったら共有してくださるとありがたいです。