さくら VPS 1G の CentOS に Python と Apache と mod_wsgi をインストールしてベンチマークとってみた

はじめに

Python2.7 で開発した Web サービスをホストする予定なので、CentOS に Python2.7 をインストールする。Web サービスを動かす構成としては nginx + gunicorn が流行りだけど、まずはド定番の Apache + mod_wsgi を試してみる。

Python2.7.3をインストール

Yum リポジトリに Python2.7 のパッケージは無さそうだったので、ソースコードをビルドしてインストールすることにした。

Python のコンパイルに必要なパッケージをインストール
sudo yum groupinstall -y "Development Tools"
sudo yum install -y zlib-devel ncurses-devel readline-devel sqlite-devel tcl-devel tk-devel openssl-devel
Pythonソースコードからインストール

mod_wsgi を使うので、configure 時に --enable-shared オプションを指定している。

wget http://www.python.org/ftp/python/2.7.3/Python-2.7.3.tgz
tar zvxf Python-2.7.3.tgz
cd Python-2.7.3
./configure --enable-shared --prefix=/opt/python2.7
make
sudo make install

無事インストール完了したら .zshrc に

export PYTHONHOME=/opt/python2.7
export PYTHONPATH=/opt/python2.7/lib/python2.7/site-packages/
export PATH=/opt/python2.7/bin:$PATH

を追加。

source ~/.zshrc

で設定の変更を反映し、

python --version

でバージョンを確認してみる。すると次のエラーが発生した。

python: error while loading shared libraries: libpython2.7.so.1.0: cannot open shared object file: No such file or directory

う〜ん…。libpython2.7.so.1.0 が見つからないみたいだ。シンボリックリンクを作成してみる。

sudo ln -s /opt/python2.7/lib/libpython2.7.so.1.0 /lib64/

もう一度

python --version

を実行して、バージョン情報が表示されれば、ようやくインストール成功。

Python での開発に必須のパッケージをインストール

distribute、pip、virtualenv をインストールする。

curl -O http://python-distribute.org/distribute_setup.py
python distribute_setup.py

easy_install pip

pip install virtualenv

これで Python のセットアップ終了。

Apache をインストール

Apache は yum で一発

mod_wsgi をビルドするから、httpd-devel も一緒にインストールしておく。

yum install -y httpd
yum install -y httpd-devel
Apache を起動
sudo /etc/init.d/httpd start
Apache の自動起動も設定
sudo /sbin/chkconfig httpd on
Apache がちゃんと動いているか確認する
curl http://<IPアドレス>/

HTTP レスポンスが表示されたら OK。

mod_wsgi をインストール

ソースコードからインストールする
wget http://modwsgi.googlecode.com/files/mod_wsgi-3.3.tar.gz
tar vzxf mod_wsgi-3.3.tar.gz
cd mod_wsgi-3.3
./configure --with-apxs=/usr/sbin/apxs --with-python=/opt/python2.7/bin/python2.7
make
sudo make install
mod_wsgi で動かすスクリプトを用意
sudo vim /var/www/cgi-bin/hello.py

を実行し

def application(environ, start_response):
    status = "200 OK"
    response_headers = [("Content-type", "text/plain")]
    start_response(status, response_headers)
    return ["Hello world!"]

を記述して保存する。

Apache の設定
sudo vim /etc/httpd/conf.d/mod_wsgi.conf

を実行して

LoadModule wsgi_module modules/mod_wsgi.so
WSGIPythonHome /opt/python2.7
WSGIPythonPath /opt/python2.7/lib/python2.7/site-packages
WSGIScriptAlias /hello /var/www/cgi-bin/hello.py

を記述して保存。

/usr/sbin/apachectl configtest

で設定ファイルが間違っていないかチェック。エラーが表示されたら修正すること。
問題がなければ

sudo /etc/init.d/httpd restart

で Apache を再起動する。

WSGI アプリをホストできているかチェック
curl http://<IP アドレス>/hello

を実行して、Hello world が表示されれば、Apache + mod_wsgiWSGI アプリを動かせたことになる。

ベンチマークをとってみる

さくら VPS 1G + CentOS + Apache + mod_wsgi の環境を構築したのでベンチをとってみる。測定には Apache Bench を使っている。このためだけに Apache を Windows に入れる、ていうのはイヤだけど仕方ない。

マシンのスペック

Web アプリを動かしている、さくら VPS 1G のスペックは下記ページから引用。

OS CentOS 6 x86_64
CPU 仮想2コア
メモリ 1GB

ベンチをとったマシンのスペックは次の通り。

OS Windows7 Home Premium
CPU Intel Core2 Duo 1.40 GHz
メモリ 4GB
アプリやライブラリ
Apache 2.2.15
Python 2.7.3
mod_wsgi 3.3
アプリやライブラリの設定

/etc/httpd/conf/httpd.conf は初期値のまま。同時接続数などの設定は下記のようになっていた。

KeepAlive Off
StartServers       8
MinSpareServers    5
MaxSpareServers   20
ServerLimit      256
MaxClients       256
MaxRequestsPerChild  4000
同時接続数50固定でリクエスト数を増やしてテスト
リクエスト数 リクエスト/秒
1000 38.99
2000 41.70
4000 42.74
6000 43.24
8000 43.48
10000 43.56
リクエスト数1000固定で同時接続数を増やしてテスト
同時接続数 リクエスト/秒
10 37.05
50 39.50
100 39.58
150 37.93
200 35.01
300 37.59
400 39.55
ベンチマーク終了後のメモリ使用量をチェック

同時接続数400、1000リクエスト後に free を実行してみた。

             total       used       free     shared    buffers     cached
Mem:       1020688     779176     241512          0      35852     134876
-/+ buffers/cache:     608448     412240
Swap:      2096472        788    2095684

まとめ

さくら VPS 1G の CentOS に Python2.7.3 と Apache と mod_wsgi をインストールし、ベンチマークをとってみた。Hello world を返すだけの単純な Web アプリだったので、今回の結果はあまり参考にならないかも。Apache + mod_wsgi の評価は、gunicorn や nginx でのベンチマークと比較して下そう。

内製ライブラリのリビジョン情報を整形して Wiki に投稿するスクリプトを書いてみた

内製ライブラリのビルド担当者になっているので、ビルド依頼が来るたびに

  • Jenkins のリリースビルド用ジョブを実行
  • Wiki に書いているライブラリのリビジョン情報を更新

という作業を行っている。ビルド自体は Rake と Jenkins で自動化したけど、Wiki の更新は今だに手作業。Wiki の更新が単純なくせに面倒くさくって、1日に何度もビルド依頼が来たりしたら、一気にやる気が無くなってしまう。

こればかりは手作業でやるしかないのかなと半ば諦めていたところ、Selenium を使っていて閃いた。Selenium みたいなツールを使って、ブラウザを自動操作してやればいいじゃなか、ってね。

Python で、Subversion リポジトリからリビジョン番号を取得し、整形して、wiki に投稿するスクリプトを書いてみた。Subversion の操作には pysvn、リビジョン情報の整形には Jinja2、wiki の投稿には mechanize を使っている。

update_wiki.py
# -*- coding: utf-8 -*-
import os
import time
import pysvn

# pysvn.Client をラップする
class SvnClient(object):
    def __init__(self):
        self._client = pysvn.Client()

    # 指定したリポジトリの最新のリビジョン番号を取得します。
    def get_recent_revision(self, path, revdate=None):
        if revdate is None:
            start_revision = pysvn.Revision(pysvn.opt_revision_kind.head)
        else:
            start_revision = pysvn.Revision(pysvn.opt_revision_kind.date, revdate)

        # ログを取得
        entry_list = self._client.log(path,
                                      limit=1,
                                      revision_start=start_revision)
        # リビジョン番号と日付を返す
        recent_log = entry_list[0]
        return recent_log["revision"].number, recent_log["date"]


def format_revision_date(revdate):
    st = time.localtime(revdate)
    return time.strftime("%Y/%m/%d", st)
    

context = {}
client = SvnClient()

# 内製ライブラリのアセンブリのリビジョンを取得
BIN_URL = "アセンブリのリポジトリの URL"
context["bin_rev"], revdate = client.get_recent_revision(BIN_URL)

# 内製ライブラリのソースコードのリビジョンを取得
SRC_URL = "ソースコードリポジトリの URL"
context["src_rev"], revdate = client.get_recent_revision(SRC_URL, revdate)

# リビジョンの日付を取得
context["build_date"] = format_revision_date(revdate)

# Jinja2 を使って Wiki 書式を作成
from jinja2 import Environment, FileSystemLoader
template_dir_path = os.path.join(os.path.dirname(__file__), "templates")
env = Environment(loader=FileSystemLoader(template_dir_path))
template = env.get_template("library_template.txt")
content = template.render(**context)
print(content)

# mechanize を使って Wiki にポスト
import mechanize
WIKI_EDIT_PAGE_URL = "Wiki の編集ページの URL"
br = mechanize.Browser()
br.open(WIKI_EDIT_PAGE_URL)
br.select_form(nr=0)
br["msg"] = content.encode("utf_8")
br.submit(name="write") # write ボタンをクリックして投稿
library_template.txt
**ライブラリバージョン

|ビルド日付|{{ build_date }}|
|ソースコードのリビジョン|{{ src_rev }}|
|アセンブリのリビジョン|{{ bin_rev }}|

あとは、このスクリプトを Jenkins のジョブとして実行すればいい。

これだけのスクリプト書くのに、調査も含めて3時間くらいかかった。でも、ビルド作業で中断された集中力を取り戻すのには結構時間かかるから、必要な時間投資だな。むしろ、さっさと自動化するべきだった。

ForeinKey のフィールドをもつモデルのフォームを ModelForm で定義してちょっと困ったこと

例えば、Django で下記のモデルを定義したとする。

from django.db import models
from django.contrib.auth.models import User

class Category(models.Model):
    user = models.ForeinKey(User)
    name = models.CharField(max_length=255)


class Entry(models.Model):
    user = models.ForeinKey(User)
    title = models.CharField(max_length=255)
    content = models.TextField()
    category = models.ForeinKey(Category)

次に ModelForm を使って

from django import forms
from models Entry

class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry
        exclude = ("user",)

というフォームを定義する。

で、このフォームをテンプレートで表示すると、category テーブルにある全てのデータを選択できてしまう。ログインユーザーが作成したカテゴリだけを選択させたいのに。ちょっと困った。

Django のソースコードを見ると、ModelForm は models.ForeinKey のフィールドを forms.ModelChoiceField を使って表示していた。この ModelChoiceField はコンストラクタで選択肢(クエリセット)を渡せるみたいだ。

ということは、ログインユーザーが作成したカテゴリだけを選択させたい場合、ユーザーで絞り込んだクエリセットを渡せばいい。

フォームを定義するときは、ログインユーザーで絞り込んだクエリセットを渡せないので、ビュー内でフォームを作成した後に ModelChoiceField を差し替えれば OK。

form = EntryForm()
queryset = Category.objects.filter(user=request.user)
form.fields["category"] = forms.ModelChoiceField(queryset,
                                                 initial=form.fields["category"].initial)

あちらこらちのビューに同じコードを書き散らすのは行儀がよろしくないので、フォームを生成する関数を定義するべきだな。

Python で Product Advertising API を試そうとしたら嵌った件

Product Advertising API を使った Web サービスを作って小遣いを稼いでやる!」っていうのは半分冗談で、半分本気。Web サービスを作るってのが本気で、小遣い稼ぎが冗談。そんなに甘いモンじゃないからね。

PythonProduct Advertising API を呼び出すためのライブラリは色々あるけど、その中でも PyPI で一番ダウンロードされている python-amazon-product-api ってのが良さそうだ。インストールは pip 一発。

pip install python-amazon-product-api

Product Advertising API のレスポンスは XML で、その XML を解析するために python-amazon-product-api は内部で lxml を使っている。当然、lxml も入れないとダメ。

Mac や Linux なら pip で一発インストールなんだろうけど、あいにく Windows だと zlib やら libxml2 やら諸々無いので失敗してしまう。必須ライブラリの入れるのも、手順が超面倒。幸い、PyPI にバイナリや Windows 用インストーラーが置いてあるから、今回はそれを利用しておく。lxml2.3 しかないけどな!

前置きが長くなってしまった。ようやく準備完了。さっそく、Amazon.jp にある Python 本を検索するスクリプトを書いてみた。

# -*- coding: utf-8 -*-
from amazonproduct import API, AWSError

ACCESS_KEY = "AWSのアクセスキー"
SECRET_KEY = "AWSのシークレットキー"
ASSOCIATE_TAG = "アフィリエイトID"

# ASSOCIATE_TAG も指定すること
api = API(ACCESS_KEY, SECRET_KEY, "jp", ASSOCIATE_TAG)
try:
    # 1ページだけ取得する
    for page in api.item_search("Books", Keywords="Python", limit=1):
        # 該当製品の総数
        print("total results: %s" % page.Items.TotalResults.pyval)
        # ページの総数
        print(page.Items.TotalPages.pyval)
        # ページ内の製品を列挙する
        for product in page.Items.Item:
            print('%s: "%s"' % (product.ItemAttributes.Author, product.ItemAttributes.Title))
except AWSError, e:
    print("code:%s message:%s" % (e.code, e.message))

ACCESS_KEY や SECRET_KEY は、あらかじめ Product Advertising API のアカウントを作成して入手してある。

実行したらこんな感じ。

total results: 264
total pages: 27
辻 真吾: "Pythonスタートブック"
Tarek Ziade: "エキスパートPythonプログラミング"
Mark Lutz: "初めてのPython 第3版"
細田 謙二: "Python入門―2&3対応"
ビープラウド: "Pythonプロフェッショナルプログラミング"
柴田 淳: "みんなのPython"
柴田 淳: "みんなのPython 改訂版"
Alex Martelli: "Python クックブック 第2版"
柏野 雄太: "Python ポケットリファレンス (Pocket Reference)"
Guido van Rossum: "Pythonチュートリアル 第2版"

検索結果の1ページ目を表示することに成功した。でも、たったこれだけのサンプルを書くのに1時間以上かかってしまった…。

最初、下記の Example を写経したんだけど、

Example のままだと

AttributeError: 'LxmlItemSearchPaginator' object has no attribute 'Items'

っていうエラーになった。どうやら、LxmlItemSearchPaginator は for でページを取り出してやらないといけないみたい。Example の方を修正しておいて欲しいよ。まったく。

pip bundle コマンドを実行すると Cannot find command 'hg' っていうエラーが発生するときの回避方法

pip bundle -r requirements.txt project.pybundle

を実行すると

Cannnot find command 'hg'

っていうエラーが出るときがある。中身に .hg ディレクトリが混入しているパッケージをバンドルしようとすると発生するみたいだ。pip の不具合として報告されてた。

close されてないから、まだ対応していないっぽいね。

リンク先にも書いてあるけど、Mercurial をインストールして hg コマンドを使えるようにしたら、このエラーは回避できる。ただ、そのためだけに使っていない Mercurial を入れるのは良い気がしないけど。

やる夫で学ぶ WSGI

         ____    
       / \  /\ キリッ 
.     / (ー)  (ー)\       
    /   ⌒(__人__)⌒ \    <Webアプリケーションフレームワーク(以下 WAF)を公開したお。 
    |      |r┬-|    |      これでやる夫もギークの仲間入りだお。 
     \     `ー'´   / 
    ノ            \ 
  /´               ヽ               
 |    l              \ 
 ヽ    -一''''''"〜〜``'ー--、   -一'''''''ー-、.     
  ヽ ____(⌒)(⌒)⌒) )  (⌒_(⌒)⌒)⌒)) 
         ____ 
       /::::::::::  u\ 
      /:::::::::⌒ 三. ⌒\      
    /:::::::::: ( ○)三(○)\   
    |::::::::::::::::⌒(__人__)⌒  | ________ 
     \::::::::::   ` ⌒´   ,/ .| |          ...| 
    ノ::::::::::u         \ | |>>1gunicornで   .| 
  /:::::::::::::::::      u     | | 動かねえ     | 
 |::::::::::::: l  u             | |>>1はクソWAF  | 
 ヽ:::::::::::: -一ー_~、⌒)^),-、   | |_________.| 
  ヽ::::::::___,ノγ⌒ヽ)ニニ- ̄   | |  | 
       ____
     /      \
   /  _ノ  ヽ、_  \
  /  o゚⌒   ⌒゚o  \   cgi で動かすことしか考えてなかったお・・・
  |     (__人__)    |   Webサーバーに対応してまわるなんて大変だお・・・
  \     ` ⌒´     /    
            / ̄ ̄\
          /   _ノ  \
          |    ( ●)(●)
          |     (__人__)
             |     ` ⌒´ノ   WSGI をサポートすればいいだろ、常考
              |         }
              ヽ        }
            ヽ、.,__ __ノ
   _, 、 -― ''"::l:::::::\ー-..,ノ,、.゙,i 、
  /;;;;;;::゙:':、::::::::::::|_:::;、>、_ l|||||゙!:゙、-、_
 丿;;;;;;;;;;;:::::i::::::::::::::/:::::::\゙'' ゙||i l\>::::゙'ー、
. i;;;;;;;;;;;;;;;;;;;;;;|::::::::::::::\::::::::::\ .||||i|::::ヽ::::::|:::!
/;;;;;;;;;;;;;;;;;;;;;;;;!:::::::::::::::::::\:::::::::ヽ|||||:::::/::::::::i:::|
;;;;;;;;;;;;;;;;;;;;;;;;;;|;;;;:::::::::::::::::::::::\:::::゙、|||:::/::::::::::|:::
     ____   
   /      \ 
  /  ─    ─\  
/    (●)  (●) \ だぶりゅ〜えすじ〜あい? 
|       (__人__)    |   
/     ∩ノ ⊃  / 
(  \ / _ノ |  | 
.\ “  /__|  |   
  \ /___ /   
            / ̄ ̄\ 
          /   _ノ  \ 
          |    ( ●)(●)  
          |     (__人__)   
             |     ` ⌒´ノ WSGI(ウイスキー)だ。 
              |         }  Web Server Gateway Interface の略な。 
              ヽ        }   WebアプリケーションとWebサーバーとの間をつなぐ、
            ヽ、.,__ __ノ    共通のインタフェースのことだ。
   _, 、 -― ''"::l:::::::\ー-..,ノ,、.゙,i 、 
  /;;;;;;::゙:':、::::::::::::|_:::;、>、_ l|||||゙!:゙、-、_ 
 丿;;;;;;;;;;;:::::i::::::::::::::/:::::::\゙'' ゙||i l\>::::゙'ー、 
. i;;;;;;;;;;;;;;;;;;;;;;|::::::::::::::\::::::::::\ .||||i|::::ヽ::::::|:::! 
/;;;;;;;;;;;;;;;;;;;;;;;;!:::::::::::::::::::\:::::::::ヽ|||||:::::/::::::::i:::| 
;;;;;;;;;;;;;;;;;;;;;;;;;;|;;;;:::::::::::::::::::::::\:::::゙、|||:::/::::::::::|::: 

            / ̄ ̄\ 
          /   _ノ  \ 
          |    ( ●)(●)  
          |     (__人__)   
             |     ` ⌒´ノ WSGIをサポートしているWebサーバは、 
              |         }  WSGIをサポートしているWebアプリなら何でも動かせる。 
              ヽ        }   フレームワーク開発者はWSGIをサポートしておけばいい。
            ヽ、.,__ __ノ
   _, 、 -― ''"::l:::::::\ー-..,ノ,、.゙,i 、 
  /;;;;;;::゙:':、::::::::::::|_:::;、>、_ l|||||゙!:゙、-、_ 
 丿;;;;;;;;;;;:::::i::::::::::::::/:::::::\゙'' ゙||i l\>::::゙'ー、 
. i;;;;;;;;;;;;;;;;;;;;;;|::::::::::::::\::::::::::\ .||||i|::::ヽ::::::|:::! 
/;;;;;;;;;;;;;;;;;;;;;;;;!:::::::::::::::::::\:::::::::ヽ|||||:::::/::::::::i:::| 
;;;;;;;;;;;;;;;;;;;;;;;;;;|;;;;:::::::::::::::::::::::\:::::゙、|||:::/::::::::::|::: 
         ___ 
       /      \  
      /ノ  \   u. \    共通のインタフェースを 
    / (●)  (●)    \   サポートするなんて難しそうだお?
    |   (__人__)    u.   |  
     \ u.` ⌒´      / 
    ノ           \

            / ̄ ̄\ 
          /   _ノ  \ 
          |    ( ●)(●)  
          |     (__人__)   
             |     ` ⌒´ノ WSGI に対応したアプリを書くのは難しくない。 
              |         }  下は最小の WSGI アプリの例だ。 
              ヽ        }
            ヽ、.,__ __ノ
   _, 、 -― ''"::l:::::::\ー-..,ノ,、.゙,i 、 
  /;;;;;;::゙:':、::::::::::::|_:::;、>、_ l|||||゙!:゙、-、_ 
 丿;;;;;;;;;;;:::::i::::::::::::::/:::::::\゙'' ゙||i l\>::::゙'ー、 
. i;;;;;;;;;;;;;;;;;;;;;;|::::::::::::::\::::::::::\ .||||i|::::ヽ::::::|:::! 
/;;;;;;;;;;;;;;;;;;;;;;;;!:::::::::::::::::::\:::::::::ヽ|||||:::::/::::::::i:::| 
;;;;;;;;;;;;;;;;;;;;;;;;;;|;;;;:::::::::::::::::::::::\:::::゙、|||:::/::::::::::|::: 
def app(env, start_response):
    """
    最小の WSGI アプリ。
    レスポンスのボディを返す。

    env
        HTTP リクエストが格納された辞書。
    start_response
        呼び出し可能オブジェクト。
        start_response を使って、ステータスコードとレスポンスヘッダを出力する。
    """
    start_response("200 OK", [("Content-type", "text/plain")])
    return "Hello world!"

# Python 標準添付の WSGI サーバーでテスト
if __name__ == "__main__":
    from wsgiref import simple_server
    server = simple_server.make_server("", 8080, app)
    server.serve_forever()
            / ̄ ̄\ 
          /   _ノ  \ 
          |    ( ●)(●)  
          |     (__人__)   
             |     ` ⌒´ノ app 関数が WSGI アプリだ。 
              |         }  WSGI アプリは呼び出し可能なオブジェクトならなんでもいい。 
              ヽ        }   __call__ を実装したクラスのオブジェクトでもいい。
            ヽ、.,__ __ノ
   _, 、 -― ''"::l:::::::\ー-..,ノ,、.゙,i 、 
  /;;;;;;::゙:':、::::::::::::|_:::;、>、_ l|||||゙!:゙、-、_ 
 丿;;;;;;;;;;;:::::i::::::::::::::/:::::::\゙'' ゙||i l\>::::゙'ー、 
. i;;;;;;;;;;;;;;;;;;;;;;|::::::::::::::\::::::::::\ .||||i|::::ヽ::::::|:::! 
/;;;;;;;;;;;;;;;;;;;;;;;;!:::::::::::::::::::\:::::::::ヽ|||||:::::/::::::::i:::| 
;;;;;;;;;;;;;;;;;;;;;;;;;;|;;;;:::::::::::::::::::::::\:::::゙、|||:::/::::::::::|::: 
class App(object):
    """
    最小の WSGI アプリ(その2)。
    """

    def __call__(self, env, start_response):
        """
        レスポンスのボディを返す。
        
        env
            HTTP リクエストが格納された辞書。
        start_response
            呼び出し可能オブジェクト。
            start_response を使って、ステータスコードとレスポンスヘッダを出力する。
        """
        start_response("200 OK", [("Content-type", "text/plain")])
        return "Hello world!"

if __name__ == "__main__":
    from wsgiref import simple_server
    server = simple_server.make_server("", 8080, App())
    server.serve_forever()
            / ̄ ̄\ 
          /   _ノ  \ 
          |    ( ●)(●)  
          |     (__人__)   
             |     ` ⌒´ノ お前の WAF も WSGI をサポートすれば、 
              |         }  gunicorn や Apache(+mod_wsgi) で動かせるようになるぞ。 
              ヽ        }
            ヽ、.,__ __ノ
   _, 、 -― ''"::l:::::::\ー-..,ノ,、.゙,i 、 
  /;;;;;;::゙:':、::::::::::::|_:::;、>、_ l|||||゙!:゙、-、_ 
 丿;;;;;;;;;;;:::::i::::::::::::::/:::::::\゙'' ゙||i l\>::::゙'ー、 
. i;;;;;;;;;;;;;;;;;;;;;;|::::::::::::::\::::::::::\ .||||i|::::ヽ::::::|:::! 
/;;;;;;;;;;;;;;;;;;;;;;;;!:::::::::::::::::::\:::::::::ヽ|||||:::::/::::::::i:::| 
;;;;;;;;;;;;;;;;;;;;;;;;;;|;;;;:::::::::::::::::::::::\:::::゙、|||:::/::::::::::|::: 
             (ヽ三/) ))
         __  ( i)))
        /⌒  ⌒\ \
      /( ●)  (●)\ )    WSGI わかったお!
    ./:::::: ⌒(__人__)⌒::::\      やる夫の WAF もさっそくサポートするお!
    |    (⌒)|r┬-|     |     
    ,┌、-、!.~〈`ー´/   _/
    | | | |  __ヽ、   /
    レレ'、ノ‐´   ̄〉  |
    `ー---‐一' ̄
            / ̄ ̄\ 
          /   _ノ  \ 
          |    ( ●)(●)  
          |     (__人__)   
             |     ` ⌒´ノ (……まぁ、WSGI をサポートしたところで、
              |         }  誰もお前のクソ WAF なんて使わないだろうがな。)  
              ヽ        }
            ヽ、.,__ __ノ
   _, 、 -― ''"::l:::::::\ー-..,ノ,、.゙,i 、 
  /;;;;;;::゙:':、::::::::::::|_:::;、>、_ l|||||゙!:゙、-、_ 
 丿;;;;;;;;;;;:::::i::::::::::::::/:::::::\゙'' ゙||i l\>::::゙'ー、 
. i;;;;;;;;;;;;;;;;;;;;;;|::::::::::::::\::::::::::\ .||||i|::::ヽ::::::|:::! 
/;;;;;;;;;;;;;;;;;;;;;;;;!:::::::::::::::::::\:::::::::ヽ|||||:::::/::::::::i:::| 
;;;;;;;;;;;;;;;;;;;;;;;;;;|;;;;:::::::::::::::::::::::\:::::゙、|||:::/::::::::::|::: 

Django のクラスベース汎用ビューで FormSet を使う方法

Django のクラスベース汎用ビューの CreateView や UpdateView で、下のような FormSet を使いたくなりました。

from django.forms import ModelForm
from django.forms.models import inlineformset_factory
from receipts.models import Receipt, Detail

class ReceiptForm(ModelForm):
    class Meta:
        model = Receipt
        exclude = ("user", "created")

DetailFormSet = inlineformset_factory(Receipt, Detail)

しか〜し!CreateView や UpdateView は FormSet には対応していません。そこで、get_context_data と form_valid オーバーライドして、無理やり FormSet を使ってみました。下は CreateView の例。

from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django.views.generic import CreateView
from receipts.models import Receipt, Detail
from receipts.forms import ReceiptForm, DetailFormSet

class CreateReceiptView(CreateView):
    template_name = "receipts/create_receipt.html"
    form_clas = ReceiptForm

    def get_success_url(self):
        return reverse("receipt_detail", kwargs={"pk": self.object.id})

    def get_context_data(self, **kwargs):
        ctx = super(CreateReceiptView, self).get_context_data(**kwargs)
        if self.request.method == "POST":
            ctx["formset"] = DetailFormSet(self.request.POST, self.request.FILES)
        else:
            ctx["formset"] = DetailFormSet()
        return ctx

    def form_valid(self, form):
        ctx = self.get_context_data()
        formset = ctx["formset"]
        if formset.is_valid():
            self.object = form.save(commit=False)
            self.object.user = self.request.user
            self.object.save()

            # FormSet の内容を保存
            formset.instance = self.object
            formset.save()

            return redirect(self.get_redirect_url())
        else:
            ctx["form"] = form
            return self.render_to_response(ctx)

Django のソースコードを見ながら修正したわけじゃないので、テンプレートに渡すコンテキストに詰めるデータが足りないかも…。今回はこれで一応動きましたが。