似非プログラマのうんちく

「似非プログラマの覚え書き」出張版

巳年じゃないけど Python やろうぜ(その 17)

国際化と翻訳

多くのフレームワークがそうであるように、Django にも国際化の仕組みが備わっています。

下準備

あらかじめ settings.pyUSE_I18NTrue になっていることを確認します(デフォルトでそうなっていると思います)。

今回は 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 や各種 CSSGitHubリポジトリを参考にしてください。

GitHub - akaneko3/PracticeDjango: Django のお勉強

続きを読む

巳年じゃないけど 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_deleteDjango 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

*1:現状では警告を表示して CASCADE をデフォルトで設定している

続きを読む

巳年じゃないけど 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_namecustomer_phonelabel 以外は同じですね。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 文字以上にしないと怒られる、ということをお見せするために敢えてそのままコピー & ペーストしました。

続きを読む