"BOKU"のITな日常

BOKUが勉強したり、考えたことを頭の整理を兼ねてまとめてます。

文字・数字・日付などの一行テキスト入力&バリデーションサンプル。/django3

f:id:arakan_no_boku:20190320212948j:plain

一番使う頻度の高い「一行テキスト入力」とバリデーションのサンプルです。

環境は Windows + anaconda です。

目次

 1行テキスト入力を「CharField」だけでやるメリット

 Djangoの一行入力には「DateField」・「DecimalField」などの種類があります。

これを「CharField」一択でやります。

HTML5から<input>タグで「pattern=」で正規表現を指定して入力形式制限できるので、これを利用して「数値」「カナ」「日付」など任意のデータ形式に絞ります。

そうすると、エラーメッセージの出し方の統一感がでる別のメリットもあります。

djangoで用意されているFieldを使うと、型チェックがサイバーサイドになるため、max-lengthとかのチェックとそれ以外のチェックで、エラーメッセージの出方が変わってしまいますが、patternでやるとそれがありません。

1行テキスト入力1:基本形
    labels = ['テキスト','パスワード','整数','カタカナ','Email','西暦日付'] 
one = forms.CharField(
          label=labels[0],
          required=True,
          disabled=False,
          max_length=30,
          min_length=1,
          widget=forms.TextInput(attrs={
               'id': 'one',
               'placeholder':'特に入力文字に制限ありません',}))   

patternは使ってません。

「id」と、入力例や注意事項を初期表示する「placeholer」を追加してます。 

1行テキスト入力2:パスワード
     labels = ['テキスト','パスワード','整数','カタカナ','Email','西暦日付']
     two = forms.CharField(
          label=labels[1],
          required=True,
          disabled=False,
          max_length=20,
          min_length=1,
          widget=forms.PasswordInput(attrs={
               'id': 'two',
               'placeholder':'半角英数字_-@のみ入力可能です。',
               'pattern':'^[A-Za-z0-9_@-]+$'}))

パスワードに利用可能な文字種の制限を、patternに正規表現で行っています。

1行テキスト入力3:整数
     labels = ['テキスト','パスワード','整数','カタカナ','Email','西暦日付']
     three = forms.CharField(
          label=labels[2],
          required=False,
          disabled=False,
          max_length=10,
          min_length=1,
          widget=forms.TextInput(attrs={
               'id': 'three',
               'placeholder':'数字のみ入力可能です。',
               'pattern':'^[0-9]+$'}))
  

数字のみに制限をかけるpatternを設定しています。 

1行テキスト入力4:カタカナ(全角・半角)
     labels = ['テキスト','パスワード','整数','カタカナ','Email','西暦日付']
     four = forms.CharField(
          label=labels[3],
          required=False,
          disabled=False,
          max_length=30,
          min_length=1,
          widget=forms.TextInput(attrs={
               'id': 'four',
               'placeholder':'カタカナのみ入力可能です。',
               'pattern':'^[ヲ-゚ァ-ヴ]+$'}))

半角カナと全角カナ両方のみ許可するpatternを設定しています。

全角カナだけにするときは、「ァ-ヴ」だけでいいです。 

1行テキスト入力5:EMailアドレス
     labels = ['テキスト','パスワード','整数','カタカナ','Email','西暦日付']
     five = forms.CharField(
          label=labels[4],
          required=False,
          disabled=False,
          max_length=100,
          min_length=1,
          widget=forms.EmailInput(attrs={
               'id': 'five',
               'placeholder':'Eメールアドレス(ex:xxxxxx@xxx.xx.jp)',}))

patternではなく、DjangoのEmailInputを使ってバリデーションをしている例です。 

1行テキスト入力6:日付
     labels = ['テキスト','パスワード','整数','カタカナ','Email','西暦日付']
     six = forms.CharField(
          label=labels[5],
          required=False,
          disabled=False,
          max_length=10,
          min_length=8,
          widget=forms.TextInput(attrs={
               'id': 'five',
               'placeholder':'西暦4ケタ月日(ex:2019/1/1)',
               'pattern':'(19[0-9]{2}|20[0-9]{2})/([1-9]|1[0-2])/([1-9]|[12][0-9]|3[01])'}))

一応、ある程度日付らしい入力しか受け付けないようにpatternを組んでます。

ですが、完璧ではありません。

うるう年とかもあるし、日付を正規表現だけでチェックするのは不可能です。

考え方としては、ある程度はじければよくて、例えば、2018/2/31なんてのは通ってしまうのですが、その辺はサーバーサイドでチャックすればいいや・・って、感覚です。

 patternの正規表現を工夫すれば色々やれますが、サンプルなので、このへんで。 

HTML :テキストボックスをforループで表示

上記で作成したFieldを表示するHTMLです。

django-widget-tweaksインストール・設定されている必要があります。

まだの場合は

pip install django-widget-tweaks

でインストールして、settings.pyのINSTALLED_APPSブロックに追加しておきます。

INSTALLED_APPS = [
      'widget_tweaks',
]

 

全体のソースです。

exsample.htmlという名前にしました。

{% load static %}
{% load widget_tweaks %}
<!DOCTYPE html>    
<html>
	<head>
		<link rel="stylesheet" href="https://fonts.xz.style/serve/inter.css" />
		<link
			rel="stylesheet"
			href="https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css"
		/>
	</head>
	<body>
		<table>
			<tr>
				<td><h1>Hello World!!</h1></td>
			</tr>
			<tr>
				<td><img src="{% static "images/k.png" %}" /></td>
			</tr>
			<form action="" method="post">
				{% csrf_token %}
				<h1>ビルトインフォームサンプル</h1>
				{% if ret %} {% for key,val in results.items %}
				<tr>
					<td><h4>{{ key }}</h4></td>
					<td><h4>{{ val }}</h4></td>
				</tr>
				{% endfor %} {% else %} {% for key,val in errors.items %}
				<tr>
					<td>
						<h2>{{ key }} : {{ val }}</h2>
					</td>
				</tr>
				{% endfor %} {% for field in form %}
				<tr>
					<td><h4>{{ field.label_tag }}</h4></td>
					<td>{{ field|add_class:"form-control font-weight-bold" }}</td>
				</tr>
				{% endfor %}
				<tr>
					<td colspan="2">
						<button type="submit" class="btn btn-primary">送信する</button>
					</td>
				</tr>
				{% endif %}
			</form>
		</table>
	</body>
</html>
 
{% endblock %}

フィールドの数が増えたり減ったりしたときにHTMLを書き直さなくてよいように、formからfieldを取り出して、ループでまわして処理してます。

{% for key,val in results.items %}
	<tr>
	<td><h4>{{ key }}</h4></td>
	<td><h4>{{ val }}</h4></td>
    </tr>
{% endfor %}

同様に、サーバー側でエラーチェックやった結果は、「errors」という辞書にセットしていて、入力された結果は「results」という辞書にセットしているので、こちらも数が増えてもHTMLを修正しなくて良いように、ループで処理してます。

{% for key,val in errors.items %}
	<tr>
		<td>
			<h2>{{ key }} : {{ val }}</h2>
			</td>
		</tr>
{% endfor %}

Views.py:サーバー側バリデーションと表示の切り分け

自分流ですが、バリデーションはviews.pyの中でまとめてやってます。

views.pyのソース全体です。

from django.shortcuts import render
from . import forms
from django.template.context_processors import csrf
import datetime


def demo(request):
    # エラーメッセージをセットする辞書
    errors = {}
    # 入力結果を格納する辞書
    results = {}
    # 入力がすべてOKなら「OK」をセットする。HTML側の判断でも使う。
    ret = ''
    # エラーがひとつでもあればTrueにする
    is_error = False
    # Submitボタンからの遷移の場合
    if request.method == 'POST':
        # 入力されたデータの受取
        results['one'] = request.POST["one"]
        results['two'] = request.POST["two"]
        results['three'] = request.POST["three"]
        results['four'] = request.POST["four"]
        results['five'] = request.POST["five"]
        results['six'] = request.POST["six"]
        # 入力データを初期値にしてformを生成。
        form = forms.UserForm(initial={
            'one': results['one'],
            'two': results['two'],
            'three': results['three'],
            'four': results['four'],
            'five': results['five'],
            'six': results['six']
        })
        # サーバー側バリデーションの例
        if _is_password_error(results['two']):
            errors['パスワード'] = '認証できませんでした。'
            is_error = True
        if _is_date_error(results['six']):
            errors['西暦日付'] = 'ただしい日付ではありません。'
            is_error = True
        # エラーがあればエラーメッセージをだして再入力
        if is_error:
            ret = ''
            c = {'form': form, 'errors': errors, 'ret': ret, }
            c.update(csrf(request))
        # エラーがなければ、入力内容を表示しておしまい。
        else:
            ret = 'OK'
            c = {'results': results, 'ret': ret, }
        return render(request, 'exsample.html', c)
    # 初期表示の場合(GET)
    else:
        # 初期表示の時にセッションもクリアする
        request.session.clear()
        # フォームの初期化
        form = forms.UserForm()
        c = {'form': form, 'ret': ret, }
        # CFRF対策(必須)
        c.update(csrf(request))
    return render(request, 'exsample.html', c)

# サーバー側バリデーション。認証のかわりのつもり


def _is_password_error(value):
    # 認証チェックの代わりにngがはいってればエラーにする
    if 'ng' in value:
        return True
    else:
        return False

# サーバー側バリデーション。日付に変換できるかを試している


def _is_date_error(datestring):
    try:
        tdatetime = datetime.datetime.strptime(datestring, '%Y/%m/%d')
        datetime.date(tdatetime.year, tdatetime.month, tdatetime.day)
        return False
    except BaseException:
        return True

ちょっと長いですが、複数のフォームを一度に書いているのと、バリデーションのロジックが追加されているだけなので、特に補足しません。

URL生成の設定

 今回は「http://localhost:8000.exp/」のURLでアクセスするようにしました。

htmlが「exsample」メソッドがviews.pyのdemoですから、以下をurls.pyに追加します。

path('exp/', views.demo, name='exsample'),

実行結果イメージ

python manage.py runserver」でサーバーを実行し「http://localhost:8000.exp/」でアクセスします。

 

さっくりとハードコピーだけ貼り付けておきます。 

初期表示です。

f:id:arakan_no_boku:20210504162057p:plain


プレースフォルダが表示されてます。 

クライアントサイドの型チェックエラーはこんな感じ。 

f:id:arakan_no_boku:20210504162327p:plain

数字の正規表現エラーになってます。

patternでひっかかると、このメッセージになります。

requireとかと同じ感じなので、なんとなく統一感はでます。 

サーバーサイドエラーは以下のようなメッセージになってます。

f:id:arakan_no_boku:20210504162655p:plain


エラーメッセージを赤にするの忘れてました(笑)

入力OK時の画面です。

f:id:arakan_no_boku:20210504163022p:plain

まあ。こんなもんかな。

参考:forms.pyのソース全文
from django import forms


class UserForm(forms.Form):
    labels = ['テキスト', 'パスワード', '整数', 'カタカナ', 'Email', '西暦日付']
    one = forms.CharField(
        label=labels[0],
        required=True,
        disabled=False,
        max_length=30,
        min_length=1,
        widget=forms.TextInput(attrs={
            'id': 'one',
            'placeholder': '特に入力文字に制限ありません', }))

    two = forms.CharField(
        label=labels[1],
        required=True,
        disabled=False,
        max_length=20,
        min_length=1,
        widget=forms.PasswordInput(attrs={
            'id': 'two',
            'placeholder': '半角英数字_-@のみ入力可能です。',
            'pattern': '^[A-Za-z0-9_@-]+$'}))

    three = forms.CharField(
        label=labels[2],
        required=False,
        disabled=False,
        max_length=10,
        min_length=1,
        widget=forms.TextInput(attrs={
            'id': 'three',
            'placeholder': '数字のみ入力可能です。',
            'pattern': '^[0-9]+$'}))

    four = forms.CharField(
        label=labels[3],
        required=False,
        disabled=False,
        max_length=30,
        min_length=1,
        widget=forms.TextInput(attrs={
            'id': 'four',
            'placeholder': 'カタカナのみ入力可能です。',
            'pattern': '^[ヲ-゚ァ-ヴ]+$'}))

    five = forms.CharField(
        label=labels[4],
        required=False,
        disabled=False,
        max_length=100,
        min_length=1,
        widget=forms.EmailInput(attrs={
            'id': 'five',
            'placeholder': 'Eメールアドレス(ex:xxxxxx@xxx.xx.jp)', }))

    six = forms.CharField(
        label=labels[5],
        required=False,
        disabled=False,
        max_length=10,
        min_length=8,
        widget=forms.TextInput(attrs={
            'id': 'five',
            'placeholder': '西暦4ケタ月日(ex:2019/1/1)',
            'pattern': '(19[0-9]{2}|20[0-9]{2})/([1-9]|1[0-2])/([1-9]|[12][0-9]|3[01])'}))

ではでは。