"BOKU"のITな日常

62歳・文系システムエンジニアの”BOKU”は日々勉強を楽しんでます

文字も数字も日付も「CharField」ですます一行テキスト入力サンプル。/django3.0+Bootstrap4

デモ画面をDjangoで作っていて、時々、度忘れして困ることがあります。

毎回、調べ直すのは面倒なので、自分用を兼ねて、サンプルを書いておきます。

まず、一番使う頻度の高い「一行テキスト入力」から。

環境は Windows + anaconda です。

f:id:arakan_no_boku:20190320212948j:plain

 

一番よく使う「1行テキスト入力」は「CharField」だけでやる

 

Djangoの一行入力には、入力対象のデータタイプによって、「DateField」・「DecimalField」など沢山の種類があります。

でも、いろんなFieldの使い方を覚えるのは苦痛です(笑)。

自分は「CharField」一択でやってます。

HTML5から<input>タグで「pattern=」で正規表現を指定して入力形式制限ができます。

これを利用して「数値」「カナ」「日付」など任意のデータ形式に絞るわけです。

そうすると、もうひとつ良いことがあります。

エラーメッセージの出し方の統一感がでることです。

djangoで用意されているFieldを使うと、型チェックがサイバーサイドになります。

そうすると、max-lengthとかのチェックとそれ以外のチェックで、エラーメッセージの出方が変わってしまいます。

でも、patternでやると、requireやmax-length、min-lengthと同じ感じでエラーメッセージがだせるようになります。

これが自分的には、なんとなく良い感じなのです。

いくつか、サンプルを。

 

まず基本形(forms.py)
    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は使ってません。

JavaScriptで操作する場合に便利なので「id」と、入力例や注意事項を初期表示する「placeholer」を追加してます。

 

パスワード入力(form.py)
     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_@-]+$'}))

パスワードに利用可能な文字種の制限もあわせて行ってます。

 

整数(form.py)
     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]+$'}))
  

単純に数字のみに制限かけてます。

 

カタカナ(全角・半角:form.py)
     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':'^[ヲ-゚ァ-ヴ]+$'}))

半角カナと全角カナ両方です。 

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

 

EMailアドレス(form.py)
     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を使ってます。

 

日付(forms.py)
     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])'}))

一応、ある程度日付らしい入力しか受け付けないようにはなってます。

ただ、うるう年とかもあるし、日付を正規表現だけで完璧にするのは無理なので、そこそこにしてます。

だから、2018/2/31なんてのは通ってしまうのですが、その辺はサーバーサイドでチャックすればいいや・・って、感覚です。

 

まあ、こんなもの。

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

 

Field表示用のHTML

 

上記で作成したFieldを表示するHTMLも作っておきます。

以下の記事で作った「base.html」を流用して、変更部分だけを書きます。

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com

今回は、「{% block content %}{% endblock %}」の中身だけ。

CSSは「bootstrap4」。

全体のソースです。

{% extends 'base.html' %}
{% load static %}
{% load bootstrap4 %}
{% load widget_tweaks %}

{% block header %}
<link rel="stylesheet" href="{% static "css/dj_talk.css" %}"></link>
{% endblock %}

{% block title %}
  CharFieldのサンプルいろいろ
{% endblock %}

{% block content %}
<div class="container bg-light">

    <form action="" method="post">{% csrf_token %}
        <h1>ビルトインフォームサンプル</h1>
        {% if ret %}
        	{% for key,val in results.items %}
        	<div class="form-group row my-4">  
            	<label class="col-lg-3 col-form-label">
            		<h4>{{ key }}</h4>
            	</label>
       			<div class="col-lg-6">       
	            	<h4>{{ val }}</h4>
    			</div>
    		</div>        	
        	{% endfor %}
        {% else %}
        	{% for key,val in errors.items %}
        		<h2>{{ key }} : {{ val }}</h2>
        	{% endfor %}
        	{% for field in form %}
        	<div class="form-group row my-4">  
            	<label class="col-lg-3 col-form-label"><h4>{{ field.label_tag }}</h4></label>
       			<div class="col-lg-6">       
	            	{{ field|add_class:"form-control font-weight-bold" }}
    			</div>
    		</div>
			{% endfor %}
		   	<div class="form-group row my-4">	
    			<div class="col-lg-3">       
	        	    <button type="submit" class="btn btn-primary">送信する</button>
    			</div>
        	</div>
         {% endif %}
     </form>
</div>  
{% endblock %}

ちょっと汎用的にしてます。

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

エッセンスだけ抜き出すとこんな感じです。

 {% for field in form %}
     <div class="form-group row my-4">
          <label class="col-lg-3 col-form-label">

               <h4>{{ field.label_tag }}</h4>

          </label>
          <div class="col-lg-6">
                {{ field|add_class:"form-control font-weight-bold" }}
          </div>
     </div>
{% endfor %} 

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

入力結果の表示がこう。

{% for key,val in results.items %}
    <div class="form-group row my-4">
        <label class="col-lg-3 col-form-label">
            <h4>{{ key }}</h4>
        </label>
        <div class="col-lg-6">
            <h4>{{ val }}</h4>
        </div>
    </div>
{% endfor %} 

 エラーメッセージの表示がこう。

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

 まあ、見たまんまです。

 

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

 

Djangoの作法でしょうか。

バリデーションを「forms.py」に書くサンプルをよく見ます。

でも。

自分はそれがやりづらい(個別チェックも相関チェックも)ので、バリデーションはviews.pyの中でまとめてやるようにしてます。

正しいかどうかは別にして、自分にとってはやりやすいです。

 

views.pyのソース
from django.http.response import HttpResponse
from django.shortcuts import render, render_to_response
from datetime import datetime as dt
from django.contrib.staticfiles.templatetags.staticfiles import static
from . import forms
from django.template.context_processors import csrf
import datetime
import os

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,'demo02.html',c)
    # 初期表示の場合(GET)
    else:
        # 初期表示の時にセッションもクリアする
        request.session.clear()
        # フォームの初期化
        form = forms.UserForm()
        c = {'form': form,'ret':ret,}
        # CFRF対策(必須)
        c.update(csrf(request))
    return render(request,'demo02.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')
        tdate = datetime.date(tdatetime.year, tdatetime.month, tdatetime.day)
        return False
    except:
        return True

これが基本形です。

ちょっと長いですが、やってることはシンプルです。

エラーがあって入力画面を再描画するときに、元の入力値を残すために、form生成時にinitialで渡さないといけないとか・・、こういうところが、後で忘れたりすると、ネットで調べても意外と見つからず苦労したりするので、端折らずに書いておきます。

 

実行

 

例によって、runserverで実行します。

プロジェクトの作成とか実行の仕方について、不明な場合は以下をどうぞ。

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com

 

この記事で書いたサンプルの実行時イメージです

 

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

 

初期表示

f:id:arakan_no_boku:20190210005633j:plain

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

 

クライアントサイドの型チェックエラー

 

f:id:arakan_no_boku:20190210010057j:plain

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

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

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

 

サーバーサイドエラー

f:id:arakan_no_boku:20190210010454j:plain

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

多少カッコ悪いのは・・まあ、しゃあない。

 

入力OK時の画面

 

f:id:arakan_no_boku:20190210012106j: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])'}))

 

ではでは。