Django + django-localflavorで、フォームの郵便番号入力や都道府県選択を作成してみた

Djangoのフォームで

  • 郵便番号入力
  • 都道府県選択

を簡単に作る方法を調べたところ、 django-localflavor がありました。

 
Web上を調べたところ、Django 1.6以降削除されたの記事を見かけたため、使えないのかなと思いました。

ただ、公式ドキュメントのChangeLogを読むと、Django2.0に対応しているとのことでした。
Changelog — django-localflavor 2.0 documentation

 
そこで、Django2.0で試してみました。

 
目次

 

環境

 

環境構築

Djangoアプリ作成まで

いつも通りなので省略します。

# Djangoアプリを作るまで
$ python -m venv env364
$ source env364/bin/activate
$ pip install django
$ django-admin startproject myproject
$ cd myproject/
$ python manage.py startapp myapp

 

django-localflavorのインストール

PyPIにあるため、pipでインストールします。
django-localflavor 2.0 : Python Package Index

$ pip install django-localflavor
...
Successfully installed django-localflavor-2.0

 

django-localflavor向けの設定(settings.py)

settings.pyのうち、

  • INSTALLED_APPS
  • LANGUAGE_CODE
  • USE_I18N

を修正します。 (USE_I18Nは、デフォルトで True かもしれません)

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # アプリ
    'myapp',
    # django-localflavor用
    'localflavor',
]

LANGUAGE_CODE = 'ja'

USE_I18N = True

 

アプリの中身を作成

テンプレート(templates/form.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>MyForm</title>
</head>
<body>
    <form action="" method="post">
        {% csrf_token %}
        {{ form.as_p }}
    </form>
</body>
</html>

 
プロジェクトのurls.py

from django.urls import path
from django.views.generic import FormView
from myapp.forms import MyForm


urlpatterns = [
    path('', FormView.as_view(
        template_name = 'form.html',
        form_class = MyForm
    ))
]

 
あとは、myapp.forms.MyFormに、django-localflavorを使った実装を書いていきます。

 

django-localflavorを試す

公式ドキュメントを見ると、以下の機能が含まれていました。
http://django-localflavor.readthedocs.io/en/latest/localflavor/jp/

  • localflavor.jp.forms.JPPostalCodeField
  • localflavor.jp.forms.JPPrefectureCodeSelect
  • localflavor.jp.forms.JPPrefectureSelect

 
それぞれ試してみます。

 

JPPostalCodeField

Formに実装してみます。

# MyForm
from django import forms
from localflavor.jp.forms import JPPostalCodeField


class MyForm(forms.Form):
    my_postal_code = JPPostalCodeField()

すると、テンプレートには

<p>
  <label for="id_my_postal_code">My postal code:</label>
  <input type="text" name="my_postal_code" required id="id_my_postal_code" />
</p>

と出力されます。

 
また、バリデーション機能もあるため、 a のような誤った値を入力すると

<ul class="errorlist">
  <li>XXXXXか、XXXXX-XXXXの形式で郵便番号を入力してください。</li>
</ul>
<p>
  <label for="id_my_postal_code">My postal code:</label>
  <input type="text" name="my_postal_code" value="a" required id="id_my_postal_code" />
</p>

とエラーが出ます。

 

JPPrefectureCodeSelect

同じくFormに実装してみます。

注意点としては、

  • JPPrefectureCodeSelectは、widget( django.forms.fields.Select を継承)であること
  • CharFieldのwidgetに設定すること
    • ChoiceFieldだと、optionタグが表示されない

です。

from django import forms
from localflavor.jp.forms import JPPrefectureCodeSelect


class MyForm(forms.Form):
    my_pref_code_ng = forms.ChoiceField(widget=JPPrefectureCodeSelect)
    my_pref_code_ok = forms.CharField(widget=JPPrefectureCodeSelect)

 
フォームを表示してみると

<!-- ChoiceFieldのwidgetとして設定したもの -->
<p>
  <label for="id_my_pref_code_ng">My pref code ng:</label>
  <select name="my_pref_code_ng" id="id_my_pref_code_ng"></select>
</p>

<!-- CharFieldのwidgetとして設定したもの -->
<p>
  <label for="id_my_pref_code_ok">My pref code ok:</label>
  <select name="my_pref_code_ok" id="id_my_pref_code_ok">
  <option value="01">北海道</option>
  <option value="02">青森県</option>
  ...
</p>

となります。

 
なお、都道府県の並び順に関しては、ソースコード

Prefectures ordered to conform with the Japanese entry of ISO-3166.
This ordering is widely used in Japan.
See:
http://en.wikipedia.org/wiki/ISO_3166-2:JP

# https://github.com/django/django-localflavor/blob/master/localflavor/jp/jp_prefectures.py

とありました。ISO-3166準拠のようです。

 

JPPrefectureSelect

JPPrefectureCodeSelectとほぼ同じですが、違いはoptionタグのvalue属性の値です。

from django import forms
from localflavor.jp.forms import JPPrefectureSelect


class MyForm(forms.Form):
    my_pref_ng = forms.ChoiceField(widget=JPPrefectureSelect)
    my_pref_ok = forms.CharField(widget=JPPrefectureSelect)

として表示してみると

<!-- ChoiceFieldのwidgetとして設定したもの -->
<p>
  <label for="id_my_pref_ng">My pref ng:</label>
  <select name="my_pref_ng" id="id_my_pref_ng">
  </select>
</p>

<!-- CharFieldのwidgetとして設定したもの -->
<p>
  <label for="id_my_pref_ok">My pref ok:</label>
  <select name="my_pref_ok" id="id_my_pref_ok">
  <option value="hokkaido">北海道</option>
  <option value="aomori">青森県</option>
  ...
</p>

となります。value属性に都道府県名のアルファベット表記が設定されます。

 

JPPrefectureCodeSelectとJPPrefectureSelectの初期表示を変更する

引数 initial を使います。

それぞれコードとアルファベット表記を指定します。

class MyForm(forms.Form):
    my_pref_code_default = forms.CharField(widget=JPPrefectureCodeSelect, initial='20')
    my_pref_default = forms.CharField(widget=JPPrefectureSelect, initial='nagano')

 
なお、initialに指定できる値は、localflavor/jp/jp_prefectures.py にある

  • JP_PREFECTURES
  • JP_PREFECTURE_CODES

に記載されています。

 
表示してみます。

<!-- JPPrefectureCodeSelect版 --> 
<p>
  <label for="id_my_pref_code_default">My pref code default:</label>
  <select name="my_pref_code_default" id="id_my_pref_code_default">
  <option value="01">北海道</option>
  ...
  <option value="20" selected>長野県</option>
</p>

<!-- JPPrefectureSelect版 --> 
<p>
  <label for="id_my_pref_default">My pref default:</label>
  <select name="my_pref_default" id="id_my_pref_default">
  <option value="hokkaido">北海道</option>
  ...
  <option value="nagano" selected>長野県</option>
</p>

 

JP_PREFECTURESとJP_PREFECTURE_CODES

以下のような定義となっています。

# https://github.com/django/django-localflavor/blob/master/localflavor/jp/jp_prefectures.py

JP_PREFECTURES = (
    ('hokkaido', _('Hokkaido'),),
...
)

JP_PREFECTURE_CODES = (
    ('01', _('Hokkaido'),),
...
)

 

テーブルに含まれている都道府県だけ表示したい場合

今までの方法は、全部の都道府県を表示するものでした。

しかし、場合によっては、都道府県のうちトランザクションテーブルに含まれるものだけ表示したいことがあるかもしれません。

とはいえ、django-localflavorではそのような機能がないため、自分で実装します。

 
以下のModelがあるとします。

# models.py
from django.db import models

class RingoProductingArea(models.Model):
    pref = models.CharField('都道府県名', max_length=10)
    ratio = models.PositiveSmallIntegerField('割合', default=0)

 
また、RingoProductingArea Modelのデータ(fixture)は以下であり、ここに含まれる「青森県・長野県・山形県」だけを表示したいとします。
出典:日本国内のりんご生産量|りんご大学

[
  {
    "model": "myapp.RingoProductingArea",
    "pk": 1,
    "fields": {
      "pref": "青森県",
      "ratio": 58
    }
  },
  {
    "model": "myapp.RingoProductingArea",
    "pk": 2,
    "fields": {
      "pref": "長野県",
      "ratio": 18
    }
  },
  {
    "model": "myapp.RingoProductingArea",
    "pk": 3,
    "fields": {
      "pref": "山形県",
      "ratio": 5
    }
  }
]

 
この場合、Formの __init__() の中でchoicesの値を作成・設定します。

あまりきれいなロジックではないので、何かいい方法をご存知の方がいらっしゃいましたら、ご指摘ください。

from django import forms
from django.db.models.aggregates import Count
from localflavor.jp.jp_prefectures import JP_PREFECTURES
from localflavor.jp.forms import JPPrefectureCodeSelect, JPPrefectureSelect, JPPostalCodeField
from localflavor.us.forms import USPSSelect, USZipCodeField
from .models import RingoProductingArea


class MyForm(forms.Form):
    limit_pref = forms.ChoiceField(label='Modelに存在する都道府県', choices=[('', '')])

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 都道府県が登録されているデータを取得する
        q = RingoProductingArea.objects.values_list('pref', flat=True) \
            .annotate(count_status=Count('pref')) \
            .filter(count_status__gt=0).distinct()
        # choicesの形(tupleのlist)にしておく
        product_prefs = [(pref, pref) for pref in q]

        # product_prefsと比較しやすいよう、JP_PREFECTURESを日本語表記の都道府県tupleのlistにしておく
        # 元々は、(ローマ字表記, 日本語表記)のtuple
        all_prefs_by_jp = [(pref_by_jp, pref_by_jp) for pref_by_en, pref_by_jp in JP_PREFECTURES]

        # 都道府県が登録されているデータだけにする
        exists_prefs = [pref for pref in all_prefs_by_jp if pref in product_prefs]
        # 先頭にメッセージを入れる
        exists_prefs.insert(0, ('', '都道府県を選ぶ'))
        # limit_prefフィールドのchoicesとして設定
        self.fields['limit_pref'].choices = exists_prefs
        # とはえ、初期値は別のもの
        self.fields['limit_pref'].initial = '長野県'

 
結果です。

<p>
  <label for="id_limit_pref">Modelに存在する都道府県:</label>
  <select name="limit_pref" required id="id_limit_pref">
    <option value="">都道府県を選ぶ</option>
    <option value="青森県">青森県</option>
    <option value="山形県">山形県</option>
    <option value="長野県" selected>長野県</option>
  </select>
</p>

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/django-localflavor-sample