7. Flaskを使いこなす2¶
favicon¶
samples/07/01_faviconを参考にして下さい
ログを見ていると404エラーがでている行があります。:
127.0.0.1 - - [04/Nov/2013 20:36:33] "GET /favicon.ico HTTP/1.1" 404 -
faviconです。ブラウザがfavicon.icoを取得しにきているのですが、 そんなルートは追加していないのでエラーになります。
faviconを配信させるには、html側で指定するか、 Flask側で/favicon.icoのURLルールを追加するとよいです。:
import os
from flask import send_from_directory
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
参考: | Adding a favicon — Flask 0.10.1 documentation |
---|
jinja2 macro¶
samples/07/02_macroを参考にして下さい。
テンプレートを書いていると、同じような処理がでてくることがあります。
そんなとき、htmlテンプレートを関数のようにまとめることができるのが、 jinja2のmacro機能です。
今回作成したFlaskrのユーザー管理画面のdetailをマクロ化してみると、
flaskr/templates/user/_helpers.html
{% macro detail(user, show_edit=false, show_delete=false) %}
<h2>{{ user.name }}</h2>
<div>
<div>{{ user.email }}</div>
</div>
<div>
<ul>
{% if show_edit %}
<li><a href="{{ url_for('user_edit', user_id=user.id) }}">edit</a></li>
{% endif %}
{% if show_edit %}
<li><a class="user-delete-link" href="#" data-delete-url="{{ url_for('user_delete', user_id=user.id) }}">delete</a></li>
{% endif %}
</ul>
</div>
{% endmacro %}
次のように使います。
flaskr/templates/user/detail.html:
{% from 'user/_helpers.html' import detail with context %}
...
{% block body %}
{{ detail(user) }}
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>
...
フィルター¶
samples/07/03_filterを参考にして下さい。
Jinja2に独自のフィルターを登録したい場合には、以下のようにします。:
def do_datetime(dt, format='%Y-%m-%d @ %H:%M'):
formatted = ''
if dt is not None:
formatted = dt.strftime(format)
return formatted
app.jinja_env.filters['datetime'] = do_datetime
上のフィルターを使う場合にはこうします:
{{ user.modified | datetime }}
{{ user.created | datetime('%Y-%m-%d') }}
こうすることで、データベースや変数では扱いやすいdatetimeオブジェクトで持ち、 ビューで見た目だけを整形して出力することができます。
ログ¶
samples/07/04_loggingを参考にして下さい
flaskオブジェクトのloggerを使うことでログを出力できます。
まず、ロガーのレベル設定をします。 「このレベル以上のログを出力する」という指定です。:
app.logger.setLevel(logging.DEBUG)
ログのレベルは DEBUG -> INFO -> WARNING -> ERROR -> CRITICAL の順で高くなります。
logging.DEBUGに設定したので、全てのログが コンソール(デフォルトの出力先)に出力されます。
利用する場合には、views.pyなどで以下のように使います。:
app.logger.debug('debug message')
app.logger.info('info message')
app.logger.warning('warning message')
app.logger.error('error message')
app.logger.critical('critical message')
このままではFlaskをdebug=Trueで実行した時にコンソールにメッセージがでるだけです。
次に、ファイルに出力するよう設定します。
flaskr/logs.pyを追加します。
import os
import logging
from logging.handlers import RotatingFileHandler
def not_exist_makedirs(path):
if not os.path.exists(path):
os.makedirs(path)
def init_app(app, log_dir='.'):
formatter = logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'
)
debug_log = os.path.join(app.root_path, '../logs/debug.log')
not_exist_makedirs(os.path.dirname(debug_log))
debug_file_handler = RotatingFileHandler(
debug_log, maxBytes=100000, backupCount=10
)
debug_file_handler.setLevel(logging.INFO)
debug_file_handler.setFormatter(formatter)
app.logger.addHandler(debug_file_handler)
error_log = os.path.join(app.root_path, '../logs/error.log')
not_exist_makedirs(os.path.dirname(error_log))
error_file_handler = RotatingFileHandler(
error_log, maxBytes=100000, backupCount=10
)
error_file_handler.setLevel(logging.ERROR)
error_file_handler.setFormatter(formatter)
app.logger.addHandler(error_file_handler)
app.logger.setLevel(logging.DEBUG)
- ここでは、debug.logとしてINFO以上のログを、 error.logとしてERROR以上のログを、 ファイルに記録するように設定しています。
- formatterも指定できるので、JSONフォーマットでログを記録することもできます。
モジュール分割¶
samples/07/05_blueprintを参考にして下さい
ビューが増えてきて1つのファイルにするには大きくなってしまった場合、 モジュール(Blueprint)に分けることができます。
URLの登録の仕方は@app.routeのときと同じで、@bp.routeで追加することができます。
sample_user.py:
from flask import Blueprint
bp = Blueprint('users', __name__, url_prefix='/users')
@bp.route('/')
def user_list():
return 'list'
ここで登録した@bp.route(‘/’)は、url_prefix=’/users’と結合され、 @app.route(‘/users/’)を登録したのと同じことになります。
作成したBlueprintのインスタンスbpをappに登録します。
app.py:
from flask import Flask
from sample_user import bp
app = Flask(__name__)
app.register_blueprint(bp)
これで、直接app.routeしたときと同じ動作をします。
注意することはあまりありませんが、Flaskのインスタンスが 持っているloggerなどはBlueprintのインスタンスにはありません。
そのため、current_app経由でアクセスすることになります。:
from flask import current_app
current_app.logger.debug('hoge')
デバッグ¶
開発時にはブレークポイントを使ってデバッグできると便利です。
幾つか方法はありますがオススメは以下の2つです。
- debugを使う
- IDE(pycharmなど)を使う
debug¶
Flaskにかぎらずpythonプログラムのデバッグには簡単に使用できます。
インストール:
pip install debug
使い方は簡単で、ブレークポイントを張りたいところに、import debugを記述するだけです。:
import debug
ide¶
pycharmやvisualstudioを使うことで、かなり簡単にデバッグすることが可能です。
テスト¶
samples/07/06_testを参考にして下さい
Flaskではapp.test_clientにテスト用のクライアントがあります。
これをrequestを投げてresponseをもらうということが簡単にできます。
$ python
>>> from flaskr import app
>>> client = app.client()
>>> r = client.get('/entries/')
>>> r.status
>>> r.data
これらを利用して、テストをしていきます。 (Flaskには関係のないライブラリは普通にテストすればよいです)
テストを実行する場合には、unittestそのままでもよいですが、 nosettests, pytestを利用するとより便利です。
フォームのクラス化とCSRF対策¶
samples/07/07_wtformsを参考にして下さい
フォームをWTFormsというライブラリを使って、クラス化します。
WTFormsを使う利点としては次のようなことがあります。
- 各入力項目の仕様をまとめられる
- セキュリティ対策(CSRF対策)を簡単に実装できる
まず、wtformsというフォームツールをFlaskで簡単に扱うための Flask-WTFをインストールします。:
pip install Flask-WTF
LoginFormを追加します。
from flask.ext.wtf import Form
from wtforms import TextField, PasswordField, SubmitField
from wtforms.validators import Required, Length
class LoginForm(Form):
email = TextField('email', validators=[
Required(message='Required'),
Length(min=1, max=100, message='1-100')
])
password = PasswordField('password', validators=[
Required(message='Required'),
Length(min=1, max=100, message='1-100')
])
submit = SubmitField('login')
viewをLoginFormを利用するように変更します。
from flask import Blueprint, render_template, request, redirect, \
url_for, flash, session, abort, jsonify
from flaskr.models import User, db
from flaskr.frontend import login_required
from flaskr.forms import LoginForm
bp = Blueprint('users', __name__, url_prefix='/users')
@bp.route('/')
@login_required
def user_list():
users = User.query.all()
return render_template('user/list.html', users=users)
@bp.route('/<int:user_id>/')
@login_required
def user_detail(user_id):
user = User.query.get(user_id)
return render_template('user/detail.html', user=user)
@bp.route('/<int:user_id>/edit/', methods=['GET', 'POST'])
@login_required
def user_edit(user_id):
user = User.query.get(user_id)
if user is None:
abort(404)
if request.method == 'POST':
user.name=request.form['name']
user.email=request.form['email']
if request.form['password']:
user.password=request.form['password']
#db.session.add(user)
db.session.commit()
return redirect(url_for('.user_detail', user_id=user_id))
return render_template('user/edit.html', user=user)
@bp.route('/create/', methods=['GET', 'POST'])
@login_required
def user_create():
if request.method == 'POST':
user = User(name=request.form['name'],
email=request.form['email'],
password=request.form['password'])
db.session.add(user)
db.session.commit()
return redirect(url_for('.user_list'))
return render_template('user/edit.html')
@bp.route('/<int:user_id>/delete/', methods=['DELETE'])
def user_delete(user_id):
user = User.query.get(user_id)
if user is None:
response = jsonify({'status': 'Not Found'})
response.status_code = 404
return response
db.session.delete(user)
db.session.commit()
return jsonify({'status': 'OK'})
@bp.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm(request.form)
if form.validate_on_submit():
user, authenticated = User.authenticate(db.session.query,
form.email.data, form.password.data)
if authenticated:
session['user_id'] = user.id
flash('You were logged in')
return redirect(url_for('index'))
else:
flash('Invalid email or password')
return render_template('login.html', form=form)
@bp.route('/logout')
def logout():
session.pop('user_id', None)
flash('You were logged out')
return redirect(url_for('index'))
テンプレートをLoginFormを利用するように変更します。
{% extends "layout.html" %}
{% block body %}
<h2>Login</h2>
{% for error in form.errors %}
<p class=error><strong>Error:</strong> {{ error }}
{% endfor %}
<form action="{{ url_for('.login') }}" method=post>
{{ form.hidden_tag() }}
<dl>
<dt>{{ form.email.label }}:
<dd>{{ form.email(size=20) }}
<dt>{{ form.password.label }}:
<dd>{{ form.password(size=20) }}
<dd>{{ form.submit }}
</dl>
</form>
{% endblock %}
実行してみましょう。:
python manage.py runserver
今までと変わらない動作をしていると思います。
しかし、ログイン画面でソースを見てみるとcsrf_tokenが追加されています。
これはwtformsのhidden_tagまたはcsrf_tokenで出力され、 POSTしたときcsrf_tokenのチェックが行われます。
その他いろいろ¶
Patterns for Flask — Flask 0.10.1 documentation
- Uploading Files
- Celery Based Background Tasks