デジタル忍者ブログ

デジタル忍者ブログ

2019/05/12

DjangoでFormsのValidator処理をカスタムしてみた

DjangoのFormsを使うことで、一般的なValidator処理を行うことができます。


例えば、アカウント名の入力項目は必須のはずが、入力されずに登録ボタンを押したケースがあります。


この場合のformsクラスは次のように示すことで実現できます。


# forms.py
class SampleForm(forms.Form):
  # 中略

  # 必須項目
  username = forms.CharField(max_length = 30, widget=forms.TextInput(attrs={'size':50}))

  # 任意項目の場合
  # username = forms.CharField(max_length = 30, widget=forms.TextInput(attrs={'size':50}), required=False)

  # 中略


これにより、入力されずに登録ボタンが押下された場合、


そのテキストフィールドの下に「フィールドを入力してください。」


というメッセージが吹き出しとして表示されます。




Validator処理としてはこれでもいいのですが、


やり方によっては、画面上部に表示させたいという場合があります。


残念ながら画面上部に表示させる方法が、Formsの機能には存在しません。


そこで、Validator処理をカスタマイズして、画面上部にエラーメッセージを表示させます。


まず、formクラス内にclean()メソッドがあります。


このメソッドにより、フィールド変数に対して独自のValidator処理を入れることができます。




アカウント名は必須であり、最大30文字までであり、かつ半角文字(記号は受け付けない)のみ入力できます。


しかし、すでに使用しているアカウント名を入力することはできません。


以上の制約を実装すると以下のような書き方になります。


# forms.py
from . import validatorUtil
from .models import MODEL_USER

class SampleForm(forms.Form):
  # 中略

  # 必須項目
  username = forms.CharField(max_length = 30, widget=forms.TextInput(attrs={'size':50}))

  # 中略

  def clean(self):
    msgs = []
    data = super().clean()
    v_username = data.get('username')
    msgs.append(validatorUtil.requireInputCheck(v_username, u'アカウント名'))
    msgs.append(validatorUtil.maxLengthCheck(v_username, 30, u'アカウント名'))
    msgs.append(validatorUtil.alphabetNumberCheck(v_username, '', u'アカウント名'))
    try:
      user = MODEL_USER.objects.get(username=v_username)
      msgs.append(forms.ValidationError(u'アカウント名は既に存在しています。', code='existedValue'))
    except MODEL_USER.DoesNotExist:
      pass

    errorMsgs = []
    for i, m in enumerate(msgs):
      if m != None:
        errorMsgs.append(m)

    if len(errorMsgs) != 0:
      raise forms.ValidationError(errorMsgs)
    return data
# validatorUtil.py
from django import forms

def requireInputCheck(target, item):
  if target == None or len(target) == 0:
    return forms.ValidationError(u'%(item)sは必ず入力してください。', 
        code='require', params={'item': item },)
  return None

def maxLengthCheck(target, mlen, item):
  if target == None:
    return None
  if len(target) > mlen:
    return forms.ValidationError(u'%(item)sは%(maxLength)d文字以内で入力してください。', 
        code='invalidLength', params={'item': item, 'maxLength': mlen},)
  return None


def alphabetNumberCheck(target, ignoreCharacter, item):
  if target == None or len(target) == 0:
    return None

  if len(ignoreCharacter) != 0:
    for c in ignoreCharacter:
      target.replace(c, '')

  naLen = 0
  for c in target:
    if unicodedata.east_asian_width(c) == 'Na':
      naLen = naLen + 1;
  if len(target) != naLen:
    return forms.ValidationError(u'%(item)sは半角文字で入力してください。', 
        code='invalidCharacter', params={'item': item },)
  return None

エラーメッセージを表示させるために、forms.ValidationErrorを使用しています。

メッセージを国際化に対応するには次のように、gettextのラッパーを使用するようです。

(公式サイトには記載されていますが、試していない・・・)

from django.utils.translation import gettext as _
# 中略
return forms.ValidationError(_('This is Error!!'), code='invalid')

また、forms.ValidationErrorはlistとして複数のメッセージを指定できるので、

clean()のValidator処理後にメッセージをまとめて、最終的に

raise forms.ValidationError(errorMsgs)

をしています。

これにより、forms.errorsを出力させると、"__all__"というkeyに登録されます。



views.pyではform.in_valid()の結果がFalseの場合に元の画面を表示するような処理を加えます。

後は、画面を表示する際は、次のようにします。

<!-- sample.html -->
{% if form.non_field_errors|length != 0 %}
<div id='errorMessage'>{{ form.non_field_errors }}</div>
{% endif %}

form.non_field_errorsという指定が、"__all__"のkeyの値を表示します。

しかも、各メッセージを<li>要素として出力してくれるので、

スタイルシートで表現のカスタマイズも容易です。



問題点

実はこの方法だと、作りによっては問題がありました。



1) 登録画面でエラーメッセージを表示させたまま、元の画面に遷移

2) 再度登録画面に遷移

3) 本来非表示になるはずのエラーメッセージが表示されたまま(え!なんで?)



というケースの問題が発生していたのです。しまいには、



1) サーバ起動後に初めて登録画面を遷移したら、いきなりエラーメッセージ表示された。(さらになんで?)



という現象もorz



viewsのロジックで・・・

# views.py

# 中略

# Form初期処理
def initForm(data = None):
  # いろいろな初期設定 中略
  if data == None:
    data = {}
  data.update({'data1': data1, ・・・})
  form = SampleForm(data)
  return form

# 登録画面へ遷移
def goSample(request):
  form = initForm()
  return render(request, 'sample.html', { 'form': form })


# 登録処理
@transaction.atomic
def inSample(request):
  # 不正アクセスのための制御
  if not request.user.is_authenticated:
    return HttpResponseRedirect('/app/logout')

  if request.method == 'POST':
    form = initForm(request.POST.copy())
    if form.is_valid():
      # 中略
    else:
      return render(request, 'sample.html', { 'form': form })
  else:
    # 中略

ということをしていましたが、どうも、

初期処理Forms.__init__()メソッドを実行すると、

必ずclean()メソッドを行ってくれるようです。

そのため、登録画面に遷移するタイミングで、1回clean()処理を行ったことになります。



そこで、

登録画面への遷移するタイミングだけclean()を行わず、

それ以降の登録処理にはclean()を行う

という面倒な処理が必要となりました。



悩んだ挙句、以下のコードでなんとか解決しました。

# forms.py

# 中略

class SampleForm(forms.Form):
  # 中略

  # clear()を初期処理のみ行わないようにするための制御を追加
  initialized = False

  def __init__(self, *args, **kwargs):
    self.initialized = True

  # 中略

  def clean(self):
    if self.initialized == True:
      return
    msgs = []
    data = super().clean()
    # 中略
# views.py

# 中略

# 登録処理
@transaction.atomic
def inSample(request):
  # 不正アクセスのための制御
  if not request.user.is_authenticated:
    return HttpResponseRedirect('/app/logout')

  if request.method == 'POST':
    form = initUSDMC0310Form(request.POST.copy())
    form.initialized = False  #追加
    if form.is_valid():
      # 中略


ややごり押し感となりましたが、


もっとスマートな方法があれば、教えてください。



参考URL:

https://docs.djangoproject.com/ja/2.1/ref/forms/validation/

https://docs.djangoproject.com/en/2.1/ref/forms/fields/

https://stackoverflow.com/questions/2525905/how-do-i-display-the-django-all-form-errors-in-the-template


Comment Form

コメント内容(必須)

Comment

現在、コメントはありません。