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