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

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

Memcached をインストールする

Python シリーズ番外編で Memcached のインストール方法を紹介します。

Windows の場合

こちらが参考になります。

Windows で Memcached を使う - make world

memcached-win64-1.4.4-14.zip をダウンロードしてきて適当なフォルダ(例えば C:\memcached) に解凍します。

あとはコマンドプロンプト

C:\memcached\memcached -d install
C:\memcached\memcached -d start

いささか古いバージョンになってしまいますが、最新のものを Windows でビルドする方法が良く分からないのでパス。

これを利用するとビルドできる*1っぽい ?
GitHub - LukeCarrier/windows-memcached: Abandoned attempt at getting memcached built on Windows

Fedora の場合

sudo dnf install memcached
sudo systemctl enable memcached
sudo systemctl start memcached

こちらも若干古いバージョンですが、最新版より少し遅れているだけなので大きな問題はなさそうです。あとは適宜 sudo dnf upgrade しておけば、更新が来た段階でアップデートされますし。

一応自前でビルドも試しましたがいまいちでした。他のディストリビューション(CentOS, Ubuntu, etc..)ではまた少し事情が変わってくるかもしれませんが。

*1:しかし作者は「Windows はひどいサーバープラットフォームだからこれは使うな」と言っている

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

Ajax の活用(続き)

バリデーションエラーの出力

フォームのバリデーションエラーに関する情報は form.errors に辞書形式で格納されているので、それを利用すれば行けることがわかりました。

views.py

from django.shortcuts import render
from django.http.response import JsonResponse
from django.utils.translation import ugettext as _
from .forms import CustomerForm
from .models import Customer

# Create your views here.


def index(request):
    form = CustomerForm(label_suffix='')
    return render(request, 'customer_search/search.html', {
        'form': form,
    })


def search(request):
    form = CustomerForm(request.POST)
    if form.is_valid():
        customer_id = request.POST['customer_id']
        try:
            customer = Customer.objects.get(pk=customer_id)
            customer_name = customer.name
            customer_phone = customer.phone
        except Customer.DoesNotExist:
            customer_name = _('No data!')
            customer_phone = _('No data!')
    else:
        customer_name = _('Error!')
        customer_phone = _('Error!')
    return JsonResponse({
        'customer_name': customer_name,
        'customer_phone': customer_phone,
        'errors': form.errors,
    })

メソッド名が微妙に変わってるので、対応して urls.py も修正しておきます。

from django.conf.urls import url
from . import views

app_name = 'customer_search'
urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^search/$', views.search, name='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' %}">
    <script src="//code.jquery.com/jquery-3.2.0.min.js"></script>
    <script src="{% static 'customer_search/js/js.cookie.js' %}"></script>
    <script src="{% static 'customer_search/js/csrf_token.js' %}"></script>
  </head>
  <body>
    <h1>{{ page_title | title }}</h1>
    <form method="post" action="{% url 'customer_search:search' %}">
      {% csrf_token %}
      <table>
        {% for field in form %}
          <tr>
            <th>{{ field.label_tag }}</th>
            <td>
              {{ field }}
              <span class="error" id="{{ field.html_name }}_errors"></span>
            </td>
          </tr>
        {% endfor %}
      </table>
      <input type="submit" value="{% trans 'Search' %}">
    </form>
    <script src="{% static 'customer_search/js/search.js' %}"></script>
  </body>
</html>

span 要素は DOM で追加する方がスマートかなと思ったけど、やってみると意外に苦戦した*1ので、とりあえずこうしておきます。

static/customer_search/js/search.js

$('form').submit(function (event) {
  event.preventDefault();
  $.post(
    $('form').attr('action'),
    { 'customer_id': $('#id_customer_id').val() },
    function (res) {
      $('#id_customer_name').val(res.customer_name);
      $('#id_customer_phone').val(res.customer_phone);
      var errors = res.errors;
      var messages = [];
      if(!$.isEmptyObject(errors)) {
        messages = errors.customer_id;
      }
      $('#customer_id_errors').text(messages.join(''));
    }
  );
});

エラーメッセージはフォームのフィールド名ごとに配列で格納されているので、join で連結*2します。

画面遷移が発生していないことが確認できます。

*1:自分が下手な書き方をしたせいでエラーメッセージが消えずにどんどん増えていくという間抜けなことにw

*2:今回のケースでは複数のエラーメッセージが格納されるケースは確認できていないので、必要ないと言えば必要ないのですがw

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

Ajax の活用

以前作成した customer_search アプリケーションを Ajax を活用して作り変えてみましょう。

Ajax 活用のための準備

Ajax 活用のために、いくつかの準備が必要です。

1. js.cookie.js をダウンロードする

GitHub - js-cookie/js-cookie: A simple, lightweight JavaScript API for handling browser cookies

上記サイトから js.cookie.js をダウンロードします。間違っても script 要素の src 属性に URL を直接打ち込まないように(GitHub は CDN ではありません !)。

持って来た js.cookie.js は django_test/customer_search/static/customer_search/js/ に保存します。

2. csrf_token.js の作成

以下の内容で django_test/customer_search/static/customer_search/js/ 内に csrf_token.js を作成します。ほぼ公式ドキュメントからのコピペです。

var csrftoken = Cookies.get('csrftoken');

function csrfSafeMethod(method) {
  // these HTTP methods do not require CSRF protection
  return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

$.ajaxSetup({
  beforeSend: function(xhr, settings) {
    if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
      xhr.setRequestHeader('X-CSRFToken', csrftoken);
    }
  }
});
続きを読む

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

キャッシュサーバーの活用

これまではセッションの情報をデータベース(SQLite3)で管理してきましたが、別途キャッシュサーバーを用意して、そちらで管理することもできます。

キャッシュサーバーとしてよく使われるのが Memcached です。Memcachedインストールは済んでいて、サービスが起動しているものと仮定します。*1

python3-memcached をインストールします。

$ pip install python3-memcached

django_test/settings.py に以下を追記します。

# Session engine
# https://docs.djangoproject.com/en/1.10/ref/settings/#session-engine
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'


# Cache
# https://docs.djangoproject.com/en/1.10/ref/settings/#caches
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

コメントは参照すべきドキュメントの URL を明示するためにつけただけなので、実際には無くても大丈夫です。

Memcached が正しくインストールされていて、かつサービスが開始されていれば、他は何も変更する必要はありません。

Memcached の各 OS ごとのインストール方法については、記事を改めて解説させていただきます。

*1:WindowsLinux 系 OS とで手順が異なります。

巳年じゃないけど 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 をデフォルトで設定している

続きを読む