巳年じゃないけど Python やろうぜ(その 17)
国際化と翻訳
多くのフレームワークがそうであるように、Django にも国際化の仕組みが備わっています。
下準備
あらかじめ settings.py
の USE_I18N
が True
になっていることを確認します(デフォルトでそうなっていると思います)。
今回は customer_search
に適用してみます。customer_search
フォルダの直下に locale
フォルダをあらかじめ作成しておきます。
翻訳するフレーズをマークする
customer_search/forms.py
from django import forms from .models import Customer from django.utils.translation import ugettext as _ class CustomerForm(forms.Form): customer_id = forms.IntegerField( label=_('Customer ID'), widget=forms.TextInput(attrs={'size': 5}), min_value=1, ) customer_name = forms.CharField( required=False, label=_('Customer name'), widget=forms.TextInput(attrs={'size': 20, 'readonly': True}), ) customer_phone = forms.CharField( required=False, label=_('Phone'), widget=forms.TextInput(attrs={'size': 20, 'readonly': True}), )
customer_search/templates/customer_search/search.html
{% load static i18n %} {% trans 'customer search' as page_title %} <!doctype html> <html> <head> <title>{{ page_title | title }}</title> <link rel="stylesheet" href="{% static 'customer_search/css/search.css' %}"> </head> <body> <h1>{{ page_title | title }}</h1> <form method="post" action="."> {% csrf_token %} <table> {% for field in form %} <tr> <th>{{ field.label_tag }}</th> <td> {{ field }} {% if field.errors %} <span class="error"> {% trans 'Error!' %} {% for error in field.errors %} {{ error }} {% endfor %} </span> {% endif %} </td> </tr> {% endfor %} </table> <input type="submit" value="{% trans 'Search' %}"> </form> </body> </html>
テンプレート内では i18n
を読み込むことにより trans
という組み込みタグが使えるようになります。
巳年じゃないけど Python やろうぜ(その 16)
汎用ビューの活用
Django には、用途に応じて使える汎用ビューが多数用意されています。
django_test/urls.py
from django.conf.urls import url, include from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^hello/', include('hello.urls')), url(r'^customer_search/', include('customer_search.urls')), url(r'^cities/', include('major_city.urls')), # 追加 ]
major_city/urls.py
from django.conf.urls import url from django.views.generic import ListView, DetailView from .models import City app_name = 'major_city' urlpatterns = [ url( r'^$', ListView.as_view( queryset=City.objects.all(), context_object_name='cities', template_name='major_city/list.html' ), name='index' ), url( r'^(?P<pk>\d+)/$', DetailView.as_view( model=City, context_object_name='city', template_name='major_city/detail.html' ), name='show' ), ]
今回は一覧表示系のビューに使用する ListView
と、詳細表示系のビューに使用する DetailView
を使うことにしました。
name
を使ってそれぞれ名前を付けています。app_name
は「名前空間のようなもの」で、逆引きの際に 'major_city:index'
のようにすることで、他のアプリの index
と区別できるようになります。
URL の逆引き
major_city/templates/major_city/list.html
{% load static %} <!doctype html> <html> <head> <title>日本の政令指定都市</title> <link rel="stylesheet" href="{% static 'major_city/css/list.css' %}"> </head> <body> <table border="1"> <caption>日本の政令指定都市</caption> <thead> <tr> <th>No.</th> <th>都道府県</th> <th>都市名</th> <th></th> </tr> </thead> <tbody> {% for city in cities %} <tr> <td class="no">{{ city.id }}</td> <td class="pref">{{ city.prefecture.name }}</td> <td class="name">{{ city.name }}</td> <td class="button"> <form method="get" action="{% url 'major_city:show' city.id %}"> <input type="submit" value="詳細"> </form> </td> </tr> {% endfor %} </tbody> </table> </body> </html>
テンプレート内では url
タグを使って URL の逆引きができます。
major_city/templates/major_city/detail.html
や各種 CSS は GitHub のリポジトリを参考にしてください。
巳年じゃないけど Python やろうぜ(その 15)
初期データの一括投入
今回から、当ブログではすっかりお馴染みの「政令指定都市一覧」を使って、Django の様々な機能を見ていきます。
$ ./manage.py startapp major_city
例によってアプリを作成するところからスタート。
Model を作ります。
from django.db import models # Create your models here. class District(models.Model): name = models.CharField(max_length=30) class Prefecture(models.Model): name = models.CharField(max_length=30) district = models.ForeignKey(District, on_delete=models.CASCADE) class City(models.Model): name = models.CharField(max_length=30) prefecture = models.ForeignKey(Prefecture, on_delete=models.CASCADE) designated = models.DateField() area = models.DecimalField(max_digits=7, decimal_places=2) population = models.IntegerField()
ForeignKey
が初登場しました。これは参照する親モデルのクラス名と、親クラスを削除した時の挙動(ON DELETE ~)を指定します。ちなみに on_delete
は Django 2.0 以降で必須になるそうです*1ので、デフォルトに甘えずに指定しておくと移行が楽になるでしょう。
マイグレーションを作成します。
$ ./manage.py makemigrations major_city (0.001) SELECT name, type FROM sqlite_master WHERE type in ('table', 'view') AND NOT name='sqlite_sequence' ORDER BY name; args=None (0.000) SELECT "django_migrations"."app", "django_migrations"."name" FROM "django_migrations"; args=() Migrations for 'major_city': major_city/migrations/0001_initial.py: - Create model City - Create model District - Create model Prefecture - Add field prefecture to city
SQL を確認してみましょう。
$ ./manage.py sqlmigrate major_city 0001 (中略) BEGIN; -- -- Create model City -- CREATE TABLE "major_city_city" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "designated" date NOT NULL, "area" decimal NOT NULL, "population" integer NOT NULL ); -- -- Create model District -- CREATE TABLE "major_city_district" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL ); -- -- Create model Prefecture -- CREATE TABLE "major_city_prefecture" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "district_id" integer NOT NULL REFERENCES "major_city_district" ("id") ); -- -- Add field prefecture to city -- ALTER TABLE "major_city_city" RENAME TO "major_city_city__old"; CREATE TABLE "major_city_city" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "designated" date NOT NULL, "area" decimal NOT NULL, "population" integer NOT NULL, "prefecture_id" integer NOT NULL REFERENCES "major_city_prefecture" ("id") ); INSERT INTO "major_city_city" ( "id", "name", "designated", "area", "population", "prefecture_id" ) SELECT "id", "name", "designated", "area", "population", NULL FROM "major_city_city__old"; DROP TABLE "major_city_city__old"; CREATE INDEX "major_city_prefecture_a34a99d3" ON "major_city_prefecture" ("district_id"); CREATE INDEX "major_city_city_71a71d54" ON "major_city_city" ("prefecture_id"); COMMIT;
若干回りくどい処理をしていますが、これはモデル名のアルファベット順に処理をしようとしているからでしょうか。
最後はマイグレーションファイルを適用するのを忘れずに。
$ ./manage.py migrate
巳年じゃないけど Python やろうぜ(その 14)
エラー表示の話
フォームのバリデーションエラーの表示場所をカスタマイズする方法があっさりとわかったので、次なるお題に移る前にさくっと書いておきます。
{% load static %} <!doctype html> <html> <head> <title>顧客検索</title> <link rel="stylesheet" href="{% static 'customer_search/css/search.css' %}"> </head> <body> <h1>顧客検索</h1> <form method="post" action="."> {% csrf_token %} <table> {% for field in form %} <tr> <th>{{ field.label_tag }}</th> <td> {{ field }} {% if field.errors %} <span class="error"> {% for error in field.errors %} {{ error }} {% endfor %} </span> {% endif %} </td> </tr> {% endfor %} </table> <input type="submit" value="検索"> </form> </body> </html>
.error { color: red; } .error::before { content: 'Error!'; }
フォームの表示を簡略化せずにフィールドごとにやれば良かったんですね。
エラーが無いときは通常どおり。
巳年じゃないけど Python やろうぜ(その 13)
ロギングの話
デフォルトでもある程度のログは出力されるのですが、データベースとの連携においては、少なくとも開発の段階では SQL の発行状況などを確認したいと思うはずです。これは settings.py
に設定を記述することで可能になります。
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'handlers': ['console'], 'level': 'DEBUG', }, }, }
最初の二つの設定は、ほぼこれで固定と思って間違いありません。'version': 1
は dictConfig のフォーマットの現時点で唯一のバージョンを指定するもので、'disable_existing_loggers': False
は、既にある Logger を無効化しない設定です。デフォルトで True
になっているので、書かないと既存の Logger が全て無効化されてしまいます。
それ以降は個別の用途に合わせて自由に書ける項目です。handlers
はログの出力先で、用途に応じてファイルへの保存や管理者宛のメールなども設定できます。今回はコンソールへの出力を設定しています。level
は最も強い(= デバッグ用の低レベルなシステム情報も出力する) DEBUG
に設定しています。loggers
で、データベース関連のバックエンドに関して DEBUG
レベルでコンソールにログを出力させる設定をしています。これによりデータベースに対して発行される SQL をコンソールで見られるようになります。
巳年じゃないけど Python やろうぜ(その 12)
フォームとビューの作成
まずはビューの作成に必要なフォームから作ります。既に Django バリバリ使ってる人なら「ModelForm 使えば楽やろ」って言うと思いますが、実は ModelForm には「(自動生成される id などの)AutoField はフォーム部品化の対象外」だったりするのと、今回はいろいろと属性をいじりたいということもあって、「それだったら一から作った方がいいんじゃね ?」ということで見送りとなりました… orz
というわけで forms.py
から。
from django import forms from .models import Customer class CustomerForm(forms.Form): customer_id = forms.IntegerField( label='顧客ID', widget=forms.TextInput(attrs={'size': 5}), min_value=1, ) customer_name = forms.CharField( required=False, label='顧客名', widget=forms.TextInput(attrs={'size': 20, 'readonly': True}), ) customer_phone = forms.CharField( required=False, label='連絡先', widget=forms.TextInput(attrs={'size': 20, 'readonly': True}), )
label
は以前ご説明の通りですが、それ以外は初お目見えですね。
IntegerField
は整数値を入力するべきフォームになります。widget
はデフォルトでは NumberInput
*1なのですが、ご指名(?)で TextInput
に登場していただきます。attrs
には辞書形式で input
要素に指定する属性と属性値の組み合わせを指定します。このコードの例だと
<input id="id_customer_id" name="customer_id" size="5" type="text" required />
と同等になります。min_value
は後でバリデーションチェックに使われます。テーブルの ID なので正の数でないとおかしいですよね。だから最小値は 1 です。
customer_name
と customer_phone
は label
以外は同じですね。widget
はデフォルトの TextInput
なのですが、属性を付与したいので敢えて指定します。required
はデフォルトは True
なのですが、この二つは検索結果を埋め込むためのものなので、送信時に空になっていても良いように False
にします。また readonly
属性を付けて、このフォームからは編集できない*2ようにします。
*1:localize がデフォルトで False のため。localize を True にすると TextInputになるようです。
*2:あくまでもユーザーインターフェース上の話であって、これによってデータが完全に守られるというわけではないことには注意が必要です。
巳年じゃないけど Python やろうぜ(その 11)
管理画面を使う
管理者ユーザーの作成
管理画面を使うにはまず管理者ユーザーを作成しなければなりません。
$ ./manage.py createsuperuser Username (leave blank to use '(snip)'): admin Email address: admin@example.com Password: Password (again): This password is too short. It must contain at least 8 characters. This password is too common. Password: Password (again): Superuser created successfully.
メールアドレスはでっち上げで公式のチュートリアルと同じものを入れてますが、実際に入力するときはちゃんと使えるアドレスを入れておいた方が良いかと思います(自分も実際にはそうしています)。
ちなみにパスワードは 8 文字以上にしないと怒られる、ということをお見せするために敢えてそのままコピー & ペーストしました。