1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

add_errorの使い方

Posted at

今回はDjangoでのバリデーション処理に使う
add_errorの使い方についてアウトプット記事を書いていきます。

目的

ユーザー編集の部分で add_error を使って
複数フィールドのバリデーションエラーを発生させる

以下が公式ドキュメントのエラー処理の部分です。

前提

必要最低限のファイルの内容しか書きません
ご了承ください。

以下に前提条件を記します。

  • フォルダ構成
  • Model
  • View

フォルダ構成

manage.py
config/
apps/
    accounts/
        models.py
        forms.py
    profiles/
        views.py

Model

accounts/models.py
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

profiles/views.py
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

accounts/forms.py
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
テンプレートで使用していました。

そのため、デバックでエラー処理ができているが
バリデーションエラーが出力されないといったことになってしまいました。

もしよかったら共有してくださるとありがたいです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?