GIG

赴くままに技術を。

押して開く

半年ぐらいずっと同じような指摘をいただいている。

「会になっていない」
「口割りまで降りていない」

だったり、矢が下に行ったり。

弓道で本当に一連の動作が大事だなと思えるのは、こういった原因が直前の動作の引き分けにあるのではなく、遡ると取り懸けに原因があったり、足踏みにあったり、奥が深い。

まだ何が原因かは突き止めていないが、先日の練習でイメージが湧いたのは、「妻手を肩にのせるように」というアドバイスだった。 意識すると妻手の肩が入っておらず、それが先の指摘に続いていた。そうすると今度は小さく引いているような気がしてくる(右肩がクルッと回っているような感覚)。これでは次はおそらく大きく引くことという指摘をもらうだろう。

そうなると今の時代は便利で、Youtubeで人の行射を眺めたり、解説を読んだり。最後にたどり着いたのは「押して開くこと」。しばらくはこの押して開くを無意識にできるよう、なんどもなんども反復したい。

SwaggerでWeb APIを作る - DBと連携する

前の記事で書いたWeb APIを今度はDBと連携させる。前回までは辞書オブジェクトにデータを保存していたので、アプリケーションを再起動させるとPOSTしたデータが失われることになる。

DBとしてSQLiteを使ってみる。 業務では専らPostgreSQLなんだけど、開発時や組み込みで使われることがある(らしい)。

まずはdb.pyを作り、下記のような接続処理を書く。

petshop/db.py

create_engineの引数では、DBの接続パスと、文字列をunicode文字列として扱うようにcovert_unicode=Trueを指定する。

次にモデルを定義する。モデルで定義した属性は、そのままDBの属性となる(O/Rマッピング機能)。 Petモデルを以下のように定義する。

petshop/model.py

as_dict()は、検索系のAPIで辞書データを返す必要があるため、そのようなメソッドを用意している。 モデルまで作成したので、モデルからDBを初期化する。

$ python -c "from db import init_db; init_db()"
$ cat petstore.db 
nn??tablepetspetsCREATE TABLE pets (
    id INTEGER NOT NULL, 
    name VARCHAR(100), 
    tag VARCHAR(20), 
    created DATETIME, 
    PRIMARY KEY (id)

コントローラー部分は、DB連携のためPetモデルの処理とdb_sessionの操作を追加している。

petstore/default_controller.py

一覧取得find_petsは、複数のタグによる検索が可能となっているので、リストでtagsが取得される。それを条件に展開するので、Pet.tag == tagをリストの要素分作って、sqlalchemy._or()の要素に展開するように実装している。 またlimitによる件数の制限は、取得したものについて行うようにしている(本当は検索するときに指定すべき)。

or_filters = or_(*[Pet.tag == tag for tag in tags])
pets = Pet.query.filter(or_filters).all()

SwaggerでWeb APIを作る - APIを実装する

この記事はFujitsu Advent Calendarの20日目です。

Swaggerとは?

前のポストではSwaggerでWeb APIを設計し、ドキュメント化、モックサーバの起動について書いた。 改めてSwaggerについて。

  • REST API設計とそのツール群
    • 仕様書(YAML形式)を書くことでそれから機能を作れたり、ドキュメント化して公開するなど仕様の齟齬をなくす
    • メンテされなくなったExcelで書いた仕様書といったものを防げそう
  • REST APIの標準化団体Open API Initiativeが仕様として採用した

Flaskサーバを実装

使うAPI仕様書は、出来合いのPetstore(Simple)のものを使う(Swagger Editorで[File] > [Open Example...]でpetstore_simple.yamlを選択)。その仕様に沿ったサーバ機能の雛形は、同様にSwagger Editorの[Generate Server]から好みのフレームワークを選択することで取得できる。

ディレクトリ構造は以下のようなかたち。 swagger.yamlAPI仕様書。ロジックはdefault_controller.pyにつくる。

.
├── LICENSE
├── README.md
├── __init__.py
├── app.py
├── controllers
│   ├── __init__.py
│   └── default_controller.py
└── swagger
    └── swagger.yaml

Swaggerに準拠したFlaskのフレームワークとしては、connexionがある。 hjacobs/connexion-exampleに習い、今回はデータを辞書として持ち回るように実装する。

まずはPythonのライブラリを2つほどインストールする。

$ pip install Flask connexion

次にアプリケーションの起点であるapp.pyを見てみる。
API仕様書のswagger.yamlapp.add_apiで読み込み、8080ポートでサーバを起動することが書いてあるのみ。

#!/usr/bin/env python3

import connexion
        
if __name__ == '__main__':
    app = connexion.App(__name__, specification_dir='./swagger/')
    app.add_api('swagger.yaml')
    app.run(port=8080)

default_controller.pyが処理を書くメインとなっている。 このパスとメソッド名をドットでつないだものがoperationIdで指定しているものである。 connexionは、辞書で返却するとjsonで返してくれる(jsonifyなどでjson化しなくて良い)。

import logging
from connexion import NoContent

logger = logging.getLogger(__name__)

PETS = {}


def add_pet(pet):
    id = pet['id']
    # idがすでに使用されているか
    exists = id in PETS
    if exists:
        logger.info('If exists, update %s', id)
        PETS[id].update(pet)
    else:
        logger.info('If not exists, create %s', id)
        PETS[id] = pet
    return PETS[id], 200


def delete_pet(id):
    if id in PETS:
        logger.info('Deleting pet %s', id)
        del(PETS[id])
        return NoContent, 204
    else:
        return NoContent, 404
      
      
def find_pet_by_id(id):
    pet = PETS.get(id)
    return pet or (NoContent, 404)


def find_pets(tags=None, limit=None):
    return [pet for pet in PETS.values()
            if not tags or pet['tag'] in tags][:limit]

実装したREST APIを使ってみる

  • まずはデータのPost。
    • レスポンスとして投入したデータを返却する。
$ cat pet.json 
{"id":1,"name":"foo","tag":"dog"}
$ cat pet2.json 
{"id":2,"name":"bar", "tag":"cat"}
$ curl -i -X POST --header "Content-Type: application/json" --header "Accept: application/json" http://localhost:8080/api/pets -d @pet.json
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 46
Server: Werkzeug/0.11.11 Python/3.5.2
Date: Mon, 19 Dec 2016 14:02:06 GMT

{
  "tag": "dog",
  "name": "foo",
  "id": 1
}
$ curl -i -X POST --header "Content-Type: application/json" --header "Accept: application/json" ttp://localhost:8080/api/pets -d @pet2.json
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 46
Server: Werkzeug/0.11.11 Python/3.5.2
Date: Mon, 19 Dec 2016 14:02:11 GMT

{
  "tag": "cat",
  "name": "bar",
  "id": 2
}
  • 一覧を取得
$ curl -i http://localhost:8080/api/pets
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 118
Server: Werkzeug/0.11.11 Python/3.5.2
Date: Mon, 19 Dec 2016 14:32:26 GMT

[
  {
    "name": "foo",
    "id": 1,
    "tag": "dog"
  },
  {
    "name": "bar",
    "id": 2,
    "tag": "cat"
  }
]
  • 一覧を取得(取得件数を1件(limit=1)を指定)
$ curl -i http://localhost:8080/api/pets?limit=1
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 60
Server: Werkzeug/0.11.11 Python/3.5.2
Date: Mon, 19 Dec 2016 14:33:26 GMT

[
  {
    "name": "foo",
    "id": 1,
    "tag": "dog"
  }
]
  • 一覧を取得(tagsを指定)
    • dogのみ取得
$ curl -i http://localhost:8080/api/pets?tags="dog"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 60
Server: Werkzeug/0.11.11 Python/3.5.2
Date: Mon, 19 Dec 2016 14:35:20 GMT

[
  {
    "name": "foo",
    "id": 1,
    "tag": "dog"
  }
]
  • catまたはdogのtagのものを取得
$ curl -i http://localhost:8080/api/pets?tags="dog,cat"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 118
Server: Werkzeug/0.11.11 Python/3.5.2
Date: Mon, 19 Dec 2016 14:35:36 GMT

[
  {
    "name": "foo",
    "id": 1,
    "tag": "dog"
  },
  {
    "name": "bar",
    "id": 2,
    "tag": "cat"
  }
]
  • 削除してから再び取得
    • 削除して204となる
    • id=1を取得して404となる
$ curl -i -X DELETE http://localhost:8080/api/pets/1
HTTP/1.0 204 NO CONTENT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Server: Werkzeug/0.11.11 Python/3.5.2
Date: Mon, 19 Dec 2016 14:37:23 GMT
$ curl -i http://localhost:8080/api/pets/1
HTTP/1.0 404 NOT FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 0
Server: Werkzeug/0.11.11 Python/3.5.2
Date: Mon, 19 Dec 2016 14:37:43 GMT

その他

レスポンスはpretty printされて返却される

github.com

add_apiのオプションとして渡せたらいいなという議論がある。今はflask_compressを使う。

x-swagger-router-controller:をオペレーションの上に書くとエラー

x-swagger-router-controllerとしては、controllerのモジュールのパス(上記の例だとcontrollers.default_controllerを記載して、oeprationIdはメソッドのみを記載するといった使い方ができそうだけど、URL直下に書くと以下のようなエラーが起きる。

Traceback (most recent call last):
  File "app.py", line 8, in <module>
    app.add_api('swagger.yaml')
  File "/Users/hermesian/.anyenv/envs/pyenv/versions/dev352/lib/python3.5/site-packages/connexion/app.py", line 146, in add_api
    debug=self.debug)
  File "/Users/hermesian/.anyenv/envs/pyenv/versions/dev352/lib/python3.5/site-packages/connexion/api.py", line 102, in __init__
    validate_spec(spec)
  File "/Users/hermesian/.anyenv/envs/pyenv/versions/dev352/lib/python3.5/site-packages/swagger_spec_validator/validator20.py", line 88, in validate_spec
    validate_apis(apis, bound_deref)
  File "/Users/hermesian/.anyenv/envs/pyenv/versions/dev352/lib/python3.5/site-packages/swagger_spec_validator/validator20.py", line 151, in validate_apis
    oper_params = deref(oper_body.get('parameters', []))
AttributeError: 'str' object has no attribute 'get'

ドキュメントではオペレーション直下に書いているけど、それならoperationIdだけで良さそう。

API仕様書のresponseを実装側で上書きしてしまう

API仕様書ではリターンコードが204だが、アプリケーション側で200を返すように書くと、後者で返される。レスポンスデータについても同様。 これ結構悩ましい問題。レスポンスのバリデーションが必要そう。

SwaggerでWeb APIを作る - Web APIの設計

Web APIの設計

Web APIの設計でExcelを使って定義書を作成していたが、仕様の変更等によって気がついたら設計書とシステムに乖離しているなんてことがあった。そのようなことがないようにWeb APIの定義情報を常に正とするようなアプリにしたい。

そこでSwaggerは、いくつかの企業によるコンソーシアムによって、Web APIの標準化を行うための規約とそのツール群を使う。

Swagger

Swaggerのサイトに行くとSwagger-editorやSwagger-UIなどツールがいくつかあるけど、swagger-nodeとbootprint-openapiで一通りできる。

GitHub - swagger-api/swagger-node: Swagger module for node.js

$ npm install -g swagger

GitHub - bootprint/bootprint-openapi: Bootprint-module to render OpenAPI specifications

$ npm install -g bootprint
$ npm install -g bootprint-openapi
$ npm -g install html-inline

手順としては、以下。

Swagger-editorでWeb API定義ファイルを作成する

はじめにプロジェクトのディレクトリを作る。 その際にNodeJSのwebフレームワークを選択させられる。これは後で作るモックサーバをカスタマイズするときに改造していくものである(Flaskが対応していたら便利だけど)。 expressを選択する。

$ swagger project create example

次に生成されたディレクトリに降りて、エディタを起動する。

$ cd example/
$ swagger project edit

Webブラウザが起動し、エディタが表示される。 GUIに対応できない環境(x windowを起動していないサーバなど)で利用するときは、-s | --silentを付ける。

今回はSwagger Editorのpetstores_simple.yamlを使う。

ドキュメント化する

bootprint-openapiでSwaggerファイルをHTML化する。

$ pwd
example
$ bootprint openapi api/swagger/swagger.yaml target

するとtargetディレクトリ配下にhtml, css, javascriptが書き出され、適当なwebサーバに配置することで閲覧できる。 さらにcss, javascriptをHTMLにまとめたいときは下記のようにし、dist.htmlを公開すれば良い。

$ html-inline target/index.html > target/dist.html

モックサーバを立てる

モックサーバはサーバ機能をまだ実装していない段階で、クライアント側の仕様検討、実装を待ちにすることなく行う目的で建てる。

$ pwd
example
$ swagger project start -m

クライアント側(同一端末だが)でAPIを叩く。 (2016.12.12 修正: 下記で/petsを叩いていたのだけど、そもそもbathPath: /apiなので、 そのようなAPIはないのは当たり前だった。

$ curl -i http://localhost:10010/api/pets
HTTP/1.1 404 Not Found
X-Powered-By: Express
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 21
Date: Mon, 12 Dec 2016 04:50:26 GMT
Connection: keep-alive

Cannot GET /api/pets

おや? 同じ事象がRunning in mock modeチュートリアル例でも起きる。

調べてみると、下記のissueに遭遇。x-swagger-router-controllerを指定しないとダメとのこと。 これを付けるのは、pathかoperationの階層で定義すれば良い。これとoperationIdAPIから呼ばれるメソッドを指定する。

issueでも言われているけど、モックなのにこれを指定しないといけないのは使い勝手が悪い気が...。 Flaskでswaggerに沿ったサーバ機能を作ることができるconnexionでは、むしろoperationレベルでのみでxx-swagger-router-controllerを設定できる。これだとoperationIdのみでもう良くないかと思えてきた。なのでモック使うとき以外は特に使用しないことにする。

github.com

pathに含まれるoperationで全て同じとする。

..(省略)..

  /pets:
    x-swagger-router-controller: pets

..(省略)..

  /pets/{id}:
    x-swagger-router-controller: pets

..(省略)..

改めてクライアント側で叩く。 そのまま使うと決められた値(numberなら1, stringなら"Sample text")が返ってくる。 レスポンスをカスタマイズしたい場合は、Implementing mock mode controllersを参照。

$ curl -X GET -i http://localhost:10010/api/pets
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Sat, 24 Sep 2016 13:08:50 GMT
Connection: keep-alive
Content-Length: 51

[{"id":1,"name":"Sample text","tag":"Sample text"}]
$ cat pet.json 
{"id":1,"name":"foo","tag":"dog"}
$ curl -X POST --header "Content-Type: application/json" --header "Accept: application/json" -i http://localhost:10010/api/pets -d @pet.json
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Sat, 24 Sep 2016 13:29:25 GMT
Connection: keep-alive
Content-Length: 49

{"id":1,"name":"Sample text","tag":"Sample text"}
$ curl -X GET -i http://localhost:10010/api/pets/1
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Sat, 24 Sep 2016 13:36:02 GMT
Connection: keep-alive
Content-Length: 49

{"id":1,"name":"Sample text","tag":"Sample text"}
$ curl -X DELETE -i http://localhost:10010/api/pets/1
HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Sat, 24 Sep 2016 13:37:25 GMT
Connection: keep-alive
Content-Length: 196

{"message":"Response validation failed: void does not allow a value","code":"INVALID_TYPE","failedValidation":true,"path":["paths","/pets/{id}","delete","responses","204"],"originalResponse":"{}"}

最後だけこけた。どうやらレスポンスが空は許可されていないみたい。 対象療法だけど、レスポンスを追加した。

    delete:
      description: deletes a single pet based on the ID supplied
      operationId: deletePet
      parameters:
        - name: id
          in: path
          description: ID of pet to delete
          required: true
          type: integer
          format: int64
      responses:
        '200':
          description: pet deleted
          schema:
            $ref: '#/definitions/pet'
        default:
          description: unexpected error
          schema:
            $ref: '#/definitions/errorModel'

どうにか。

$ curl -X DELETE -i http://localhost:10010/api/pets/1
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Sat, 24 Sep 2016 13:51:11 GMT
Connection: keep-alive
Content-Length: 49

{"id":1,"name":"Sample text","tag":"Sample text"}

サーバ機能の雛形を生成する

最後にAPI仕様書からサーバ機能の雛形を作る

swagger-nodeの方ではないSwagger-Editorを用いると、サーバ機能のダウンロードができる(swagger-codegenでも良い)。 インストールしても良いが、Dockerイメージがあるので、コンテナにて行う。

$ docker pull swaggerapi/swagger-editor
$ docker run -p 80:8080 swaggerapi/swagger-editor

あとはhttp://(YOUR_CONTAINER_URI)にアクセスし、API仕様書の内容をコピー&ペーストして、[Generate Server]からお好みのフレームワークの雛形をダウンロードする。これからFlaskでサーバ機能を作るので、Flaskを選択する。

NvidiaのDeep Learning Quest(無料分)を受ける

すでにひとしきり広まった感があるDeep Learning。 Nvidiaがかなり本腰入れていて、それ向けの教育プログラムまである。

f:id:hermesian:20160504130503p:plain

どういうものかは知っておきたいので、無料枠で受けれる「ディープラーニング入門」を受講してみよう。実際AWS上に構築された環境を触りながらできるハンズオン形式で、有料のプログラムもAWSの利用料金ぐらいの値段みたい(とNvidia Deep Learning Day 2016 Springで紹介されていた)。

私たちは、あなたがあなたのアプリケーションは、研究者や開発者として必要な最高のどのフレームワークスーツを決める手助けを目的とした深い学習> のための最も一般的なソフトウェアフレームワークを見学します。この見学では、あなたのアプリケーションに一番適したディープラーニングフレーム> ワークを決定する事をゴールにします。ディープラーニングの予備知識は必要ありません。

たしかにDLフレームワークは乱立している印象がある。Software links « Deep Learningを見ると38種もあるのか。

起動に4分程度かかるとのこと。Jupyter Notebook形式なのね。 始まるとなんとタイマーが起動して、55分の間だけ利用できるとのこと。

f:id:hermesian:20160504130510p:plain

各DLフレームワークの採用基準も記載があった。 NVDLDでも下記のような比較がされてたけど、TheanoとTorch7の比較(性能面、機能面)があまり理解できていない。 これはもう使ってみないとわからない世界なのかも。 Lua言語の習得コスト考えるとTheanoとかPythonサポートしているものになりそうな印象。

GPUリソースをどうやって調達しようかな...。

Djangoアプリケーションのデプロイ

開発サーバでなく、製品版ではどうするかというと2通りの方法があるみたい。

  1. Apache Httpサーバ + mod_wsgi
  2. Nginx + Gunicorn

2.の方がパフォーマンスが優れているという話も見かけたけど、今回は1.を試してみる。 wsgiは"ウィスギィ"と読むのか。

検証環境として、Virtualbox上に立てたCentOS 7を使う。

 cat /etc/centos-release
CentOS Linux release 7.2.1511 (Core)

webブラウザから確認するため、VagrantfileにIPアドレスを固定するよう設定する。

  • Vagrantfile
...
  config.vm.network "private_network", ip: "192.168.33.10"
...
Python 3.5.1のインストール

デフォルトでインストールされているPythonのバージョンが2.7.5なので、3.5.1をインストールする。

# mkdir tmp
# cd tmp
# wget https://www.python.org/ftp/python/3.5.1/Python-3.5.1.tgz
# tar -xzf Python-3.5.1.tgz
# cd Python-3.5.1
# ./configure --enable-shared
# make
# make altinstall

インストールの確認をしたが、関連するライブラリのパスが通っていない...。 パスを通す。

# /usr/bin/python3.5 -V/usr/bin/python3.5: error while loading shared libraries: libpython3.5m.so.1.0: cannot open shared object file: No such file or directory
# echo "/usr/local/lib/python3.5" > /etc/ld.so.conf.d/python33.conf
# echo "/usr/local/lib" >> /etc/ld.so.conf.d/python33.conf
# ldconfig
Apache Httpサーバ およびmod_wsgiのインストール

tmpディレクトリのままでApache Httpサーバとmod_wsgiをインストールする。

# yum install httpd
# wget https://github.com/GrahamDumpleton/mod_wsgi/archive/4.5.2.tar.gz
# tar -xzf 4.5.2.tar.gz
# cd mod_wsgi-4.5.2/
# ./configure --with-python=/usr/local/bin/python3.5
# make
# make install
Djangoのインストール

pipのバージョンが古いかったので、アップグレードも実施。

# pip3.5 freeze
You are using pip version 7.1.2, however version 8.1.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
# /usr/local/bin/pip3.5 install --upgrade pip
# /usr/local/bin/pip3.5 install django
確認用プロジェクトで確認する

helloプロジェクトを作成。

# su - vagrant
$ cd ~
$ mkdir -p dev/python/django
$ cd dev/python/django/
$ django-admin startproject hello

マイグレーション

$ /usr/local/bin/python3.5 manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, sessions, contenttypes
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying sessions.0001_initial... OK

管理者アカウントを作っておいて、管理画面の表示も確認しよう。

$ /usr/local/bin/python3.5 manage.py createsuperuser
Username (leave blank to use 'vagrant'): admin
Email address: admin@email.com
Password:
Password (again):
Superuser created successfully.

前回はstaticフォルダにJavascriptやらCSSを入れて参照していたけど、管理画面のものはどこにあるのかなと思ったらそれをstaticフォルダに集めれとのこと。これをしないとあとで管理画面のスタイルが崩れて表示される。

collectstaticコマンド を実行するとSTATIC_ROOTで指定したディレクトリに格納される。

  • hello/settings.py
...
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
...

それでもって

$ /usr/local/bin/python3.5 manage.py collectstatic
mod_wsgiの設定
oadModule wsgi_module modules/mod_wsgi.so

WSGIDaemonProcess hello python-path=/home/vagrant/dev/python/django/hello
WSGIProcessGroup hello
WSGIScriptAlias / /home/vagrant/dev/python/django/hello/hello/wsgi.py process-group=hello

<Directory /home/vagrant/dev/python/django/hello/hello>
    <Files wsgi.py>
        Require all granted
    </Files>
</Directory>

Alias /static /home/vagrant/dev/python/django/hello/static
<Directory /home/vagrant/dev/python/django/hello/static>
    Require all granted
</Directory>

apacheユーザがhelloプロジェクトにアクセスできるようにする。

$ sudo usermod -a -G vagrant apache
$ sudo chmod 775 -R ~/dev/python/django/hello/
Apache Httpサーバの起動

あとはApache Httpサーバを起動して、http://192.168.33.10/http://192.168.33.10/adminで開発用サーバと同じ画面が見れるはず。

# systemctl start httpd
# systemctl enable httpd

ただ1点気になったのが、Apache Httpサーバのエラーログに/はないと言われるのはなんでだろう。。

[wsgi:error] [pid 4116] Not Found: /

Django1.9+Bootstrap3でログイン表示を作る

認証・認可は、Djangoに限らず、Webフレームワークを使い出してまず外せない機能。

Django公式マニュアルを見てみると、機能としてデフォルトで持っていて、それを用途に応じて拡張していく方針とのこと。 Users, Groupモデル、パスワードをハッシュ化して保持して保持するといった提供されている。

1回やり方を確認しておけば何にでも応用できそうなのででBootstrap3を利用し、ログイン画面を表示してみる。

環境

環境としては、こちら。

項目 バージョン
Python 3.5.1
Django 1.9.5
jQuery v2.2.2
Bootstrap v3.3.6

Djangoの認証

Djangoで認証・認可の機能は、django.contrib.authで使用できる。これはプロジェクトを生成するときに元から含まれている。プロジェクト作成後、マイグレーション(python manage.py migrate)とすると認証・認可用のテーブルが作成される。

つくってみる

はじめにプロジェクトから作成する。 radishはただのプロジェクトの例なので、適宜変更してください。

$ django-admin startproject radish
$ python manage.py migrate

次にaccountsdashboardアプリを作成する。 accountsには認証・認可機能を含めで、dashboardには認証された後のリダイレクト先のアプリを含める想定。

$ python manage.py startapp accounts
$ python manage.py startapp dashboard

アプリを作ったら、忘れずsettings.pyINSTALLED_APPSに追記しておく。これを忘れると、後で出てくるテンンプレートが存在しないというエラーが発生するので、忘れずに。

  • radish/radish/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # local
    'accounts.apps.AccountsConfig',
    'dashboard.apps.DashboardConfig',
]
Bootstrapを配置する

JavascriptCSSのライブラリは、radish/staticフォルダ内に入れておく。するとsettings.pyの下記の記載に沿って、参照可能となる。

  • radish/radish/settings.py
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/

STATIC_URL = '/static/'

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
)

jQueryやらBootstrapやらをインストールする。それぞれダウンロードするのが面倒であったので、Bowerで入れる。

  • radish/static/
$ bower init
...
(対話型でbower.jsonを作成)
...
$ bower install jquery -S
$ bower install bootstrap -S
トップ画面、ログイン画面、ログイン後画面(ダッシュボード画面)を作る

各画面共通で使うNavbarなどはbase.htmlに記述し、他の画面はそれを継承する。またそういう共通のHTMLはradish/templates配下に入れておく。しかし、このパスはDjangoが見つけてくれないので、またsettigs.pyに追記する。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  # 追記箇所
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

それではbase.html{% load staticfiles %}と書くと、下のように{% static xxx%}でjsやcssを利用できる。{% if user.is_authenticated %}~{% else %}で条件分岐しているところは、認証されている場合のみ有効にするメニュー表示である。また {% block container %}~{% endblock %}は、base.htmlを継承した方のHTMLが埋め込まれる箇所となっている。

{% load staticfiles %}

<!DOCTYPE html>
  <html lang="ja">
    <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1">

      <tile>{% block title %} Radish | {{ site_name }}{% endblock %}</tile>

      <!-- CSS -->
      <link href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}" rel="stylesheet" />
      <link href="{% static 'bootstrap/dist/css/bootstrap-theme.min.css' %}" rel="stylesheet" />
      <link href="{% static 'font-awesome/css/font-awesome.css' %}" rel="stylesheet" />
  </head>
  <body>
    <nav class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
          <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <sapn class="icon-bar"></sapn>
              <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">Radish</a>
          </div><!-- /.navbar-header -->

          <div id="navbar" class="navbar-collapse collapse">
            {% if user.is_authenticated %}
              <ul class="nav navbar-nav navbar-right">
                  <li>
                      <a href="#">Dashboard</a>
                  </li>
                  <li>
                      <a href="#">Settings</a>
                  </li>
              </ul>
            {% else %}
              <form class="navbar-form navbar-right">
                  <button type="button" class="btn btn-primary"
                    onclick="location.href=&quot;{% url 'login' %}&quot;;">
                    Login</button>
              </form>
            {% endif %}
          </div><!-- /.navbar-collapse -->
        </div><!-- /.container -->
    </nav>

    {% block container %}
    {% endblock %}

    <hr/>
    <footer>
      <p>&copy; hermesian</p>
    </footer>

    <!-- JavaScript -->
    <script src="{% static 'jquery/dist/jquery.min.js' %}"></script>
    <script src="{% static 'bootstrap/dist/js/bootstrap.min.js' %}"></script>
  </body>
</html>

次にトップ画面home.htmlは下記。 これはBootstrapのjumbotronの例をそのまま利用。

{% extends 'base.html' %}

{% load staticfiles %}

{% block container %}
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
    <div class="container">
      <h1>Radish</h1>
      <p>
          "Radish is a dashboard for collecting visualizing report."
      </p>
    </div>
</div>
<div class="container">
      <!-- Example row of columns -->
      <div class="row">
        <div class="col-md-4">
          <h2>Heading</h2>
          <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
          <p><a class="btn btn-default" href="#" role="button">View details »</a></p>
        </div>
        <div class="col-md-4">
          <h2>Heading</h2>
          <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
          <p><a class="btn btn-default" href="#" role="button">View details »</a></p>
       </div>
        <div class="col-md-4">
          <h2>Heading</h2>
          <p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>
          <p><a class="btn btn-default" href="#" role="button">View details »</a></p>
        </div>
      </div>
  </div>
{% endblock %}

home.htmlのログインボタンで遷移する先は、accounts/tempalte/accounts/login.html<form>タグの下に{% csrf_token %}とあるのは、DjangoCSRF対策機能である。

{% extends 'base.html' %}

{% load staticfiles %}

{% block container %}
<div class="container">
  <div class="page-header">
      <h3>
        Login
      </h3>
  </div>
  <div class="row">
    <div class="col-md-4 col-md-offset-4">
        <div class="panel panel-default">
            <div class="panel-body">
                <form accept-charset="utf-8" role="form" action="{% url 'login' %}" method="post">
                    {% csrf_token %}
                    <fieldset>
                        <div class="form-group">
                            <input type="text" id="username" name="username" class="form-control"
                                   placeholder="Username" required autofocus>
                        </div>
                        <div class="form-group">
                            <input type="password" id="password" name="password" class="form-control"
                                   placeholder="Password" required>
                        </div>
                        {% if login_failed %}
                          <p class="text-danger">Sorry, that login was invalid.  Please try again.</p>
                        {% endif %}
                        <input class="btn btn-lg btn-success btn-block" type="submit" value="Log in">
                    </fieldset>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

最後に認証後のdashboard画面dashboard/template/dashboard.html。といっても今回は空っぽ。

{% extends 'base.html' %}
ビューとルーティングを作る

accountsアプリ

  • radish/accounts/views.py

何かしらで認証されっぱなしの状態にならないように、logoutを最初に呼んで初期化してる。

import logging
from django.shortcuts import render_to_response, HttpResponseRedirect
from django.template import RequestContext
from django.contrib.auth import authenticate, login, logout

logger = logging.getLogger(__name__)


def login_view(request):

    #強制的にログアウト
    logout(request)
    username = password = ''

    login_failed = False

    if request.POST:
        username = request.POST['username'].replace(' ', '').lower()
        password = request.POST['password']
        user = authenticate(username=username, password=password)
        if user is not None:
            if user.is_active:
                login(request, user)
                return HttpResponseRedirect('/dashboard')
        else:
            login_failed = True

    return render_to_response('accounts/login.html',
                              {'login_failed': login_failed},
                              context_instance=RequestContext(request))

上で作ったビューをwebブラウザから/accounts/loginでアクセスできるよう設定する。

  • radish/accounts/urls.py
from django.conf.urls import url
from .views import login_view

urlpatterns = [
    url(r'^login/$', login_view, name='login'),
]

dashboardアプリ

  • radish/dashboard/views.py

@login_requiredをつけて、認証後のみアクセス可能なようにしている。

import logging

from django.shortcuts import render
from django.contrib.auth.decorators import login_required


@login_required
def index(request):
    return render(request, 'dashboard/index.html')
  • radish/dashboard/urls.py
from django.conf.urls import url
from .views import index

urlpatterns = [
    url(r'^$', index, name='dashboard'),
]
動作確認

ユーザを作成する。今回はスーパーユーザを造り、そのままそのアカウントでログインしてみる。

$ python manage.py createsuperuser
...
(対話的に作成)
...

動作確認として、開発サーバを起動$ python manage.py runserver、webブラウザからhttp://localhost:8000/にアクセスして、先ほど登録したスーパーユーザでログインし、NavbarにDashboard, Settingsというメニューが出ていることを確認する。