4. Flaskを使いこなす1

Flaskrにユーザー管理画面と認証処理を追加しながら進めていきます。

Flask公式のチュートリアルではユーザーログイン処理がありますが、 config.pyに記述したユーザーのみになっています。

今回はユーザーを追加・編集・削除できるような画面を作成し、 そのユーザーを使ってログインできるようにしましょう。

4.1 ユーザーログイン処理を追加する1

samples/04/01を参考にして下さい

ユーザークラスの追加

models.pyにUserクラスを追加します。

ここでは、emailとpasswordでログインするユーザーを作ります。

flaskr/models.py

from sqlalchemy.orm import synonym
from werkzeug import check_password_hash, generate_password_hash

from flaskr import db


class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), default='', nullable=False)
    email = db.Column(db.String(100), unique=True, nullable=False)
    _password = db.Column('password', db.String(100), nullable=False)

    def _get_password(self):
        return self._password
    def _set_password(self, password):
        if password:
            password = password.strip()
        self._password = generate_password_hash(password)
    password_descriptor = property(_get_password, _set_password)
    password = synonym('_password', descriptor=password_descriptor)

    def check_password(self, password):
        password = password.strip()
        if not password:
            return False
        return check_password_hash(self.password, password)

    @classmethod
    def authenticate(cls, query, email, password):
        user = query(cls).filter(cls.email==email).first()
        if user is None:
            return None, False
        return user, user.check_password(password)

    def __repr__(self):
        return u'<User id={self.id} email={self.email!r}>'.format(
                self=self)


class Entry(db.Model):
    __tablename__ = 'entries'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.Text)
    text = db.Column(db.Text)

    def __repr__(self):
        return '<Entry id={id} title={title!r}>'.format(
                id=self.id, title=self.title)

def init():
    db.create_all()

データベースの作成

つくったらデータベースを初期しておきましょう。

しかし、いちいちpythonインタプリタから初期化するのは面倒なので、 コマンドラインから作成できるようにしておきましょう。

まずは、Flaks-Scriptプラグインをインストールします。:

echo Flask-Script >> requirements.txt
pip install Flask-Script

次にmanage.pyを変更します。

manage.py

from __future__ import print_function
from flask.ext.script import Manager
from flaskr import app, db

manager = Manager(app)

@manager.command
def init_db():
    db.create_all()

if __name__ == '__main__':
    manager.run()

作成したらデータベースを作成しなおしましょう。:

rm flaskr/flaskr.db
python manage.py init_db

ユーザー用のビューを追加

ユーザーを管理するためのページをベースを作りましょう。

views.pyに追加していきます。

from flask import request, redirect, url_for, render_template, flash
from flaskr import app, db
from flaskr.models import Entry

@app.route('/')
def show_entries():
    entries = Entry.query.order_by(Entry.id.desc()).all()
    return render_template('show_entries.html', entries=entries)

@app.route('/add', methods=['POST'])
def add_entry():
    entry = Entry(
            title=request.form['title'],
            text=request.form['text']
            )
    db.session.add(entry)
    db.session.commit()
    flash('New entry was successfully posted')
    return redirect(url_for('show_entries'))


@app.route('/users/')
def user_list():
    return 'list users'

@app.route('/users/<int:user_id>/')
def user_detail(user_id):
    return 'detail user ' + str(user_id)

@app.route('/users/<int:user_id>/edit/', methods=['GET', 'POST'])
def user_edit(user_id):
    return 'edit user ' + str(user_id)

@app.route('/users/create/', methods=['GET', 'POST'])
def user_create():
    return 'create a new user'

@app.route('/users/<int:user_id>/delete/', methods=['DELETE'])
def user_delete(user_id):
    return NotImplementedError('DELETE')

  • routeのパラメータ
    • int: 整数
    • float: 浮動小数点
    • path: 文字列(スラッシュも受け取る)
  • HTTP method
    • GET
    • HEAD
    • POST
    • PUT
    • DELETE
    • OPTIONS

実行してみましょう

manage.pyを変更したので、今後は以下のコマンドで起動します。:

python manage.py runserver

4.2 ユーザーログイン処理を追加する2

samples/04/02を参考にして下さい

ユーザー管理画面を作っていきます。

ユーザー作成 /users/create の追加

flaskr/views.py:

from flaskr.models import Entry, User

...

@app.route('/users/create/', methods=['GET', 'POST'])
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')

flaskr/templates/user/edit.html

{% extends "layout.html" %}

{% block body %}
  <h2>{{ 'Edit User' if user else 'Add User'}}</h2>
  <form action="" method="post">
    <dl>
      <dt>Name:
      <dd><input type=text size=20 name=name value="{{ user.name if user }}">
      <dt>Email:
      <dd><input type=text size=20 name=email value="{{ user.email if user }}">
      <dt>password:
      <dd><input type=password size=20 name=password value="">
      <dd><input type=submit value={{ 'save' if user else 'create' }}>
    </dl>
  </form>
{% endblock %}

ユーザー一覧 /users/ の追加

flaskr/views.py:

@app.route('/users/')
def user_list():
    users = User.query.all()
    return render_template('user/list.html', users=users)

flaskr/templates/user/list.html

{% extends "layout.html" %}

{% block body %}
  <h2>Users</h2> 

  <ul>
  {% for user in users %}
  <li><a href="{{ url_for('user_detail', user_id=user.id) }}">{{ user.name }}</a></li>
  {% else %}
  <li><em>Unbelievable.  No users here so far</em></li>
  {% endfor %}
  </ul>

  <a href="{{ url_for('user_create') }}">create user</a>
{% endblock body %}

ユーザー一覧 /users/<int:user_id>/ の追加

flaskr/views.py:

@app.route('/users/<int:user_id>/')
def user_detail(user_id):
    user = User.query.get(user_id)
    return render_template('user/detail.html', user=user)

flaskr/templates/user/detail.html

{% extends "layout.html" %}

{% block body %}
<h2>{{ user.name }}</h2>

<div>
  <div>{{ user.email }}</div>
</div>

<div>
  <ul>
    <li><a href="{{ url_for('user_edit', user_id=user.id) }}">edit</a></li>
    <li><a class="user-delete-link" href="#" data-delete-url="{{ url_for('user_delete', user_id=user.id) }}">delete</a></li>
  </ul>
</div>

<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>
  $(function() {
    $(".user-delete-link").on("click", function() {
      var delete_url = $(this).attr('data-delete-url');
      $.ajax({
        url: delete_url,
        type: 'DELETE',
        success: function(response) {
          if (response.status == 'OK') {
            window.location = '{{ url_for('user_list') }}';
          } else {
            alert('Delete failed.')
          }
        }
      });
      return false;
    });
  });
</script>

{% endblock body %}

ユーザー一覧 /users/<int:user_id>/edit/ の追加

flaskr/views.py:

from flask import request, redirect, url_for, render_template, flash, abort

...

@app.route('/users/<int:user_id>/edit/', methods=['GET', 'POST'])
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']
        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)

ユーザー一覧 /users/<int:user_id>/delete/ の追加

flaskr/views.py:

from flask import request, redirect, url_for, render_template, flash, abort, \
        jsonify

...

@app.route('/users/<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'})

実行してみましょう

python manage.py runserver

4.3 ユーザーログイン処理を追加する3

samples/04/03を参考にして下さい

さらにモクモクと作っていきましょう。

ログイン画面の追加

flaskr/views.py:

from flask import request, redirect, url_for, render_template, flash, abort, \
        jsonify, session

...

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        user, authenticated = User.authenticate(db.session.query,
                request.form['email'], request.form['password'])
        if authenticated:
            session['user_id'] = user.id
            flash('You were logged in')
            return redirect(url_for('show_entries'))
        else:
            flash('Invalid email or password')
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.pop('user_id', None)
    flash('You were logged out')
    return redirect(url_for('show_entries'))

flaskr/templates/login.html

{% extends "layout.html" %}
{% block body %}
  <h2>Login</h2>
  <form action="{{ url_for('login') }}" method=post>
    <dl>
      <dt>Email:
      <dd><input type=text name=email>
      <dt>Password:
      <dd><input type=password name=password>
      <dd><input type=submit value=Login>
    </dl>
  </form>
{% endblock %}

flaskr/templates/layout.html

<!doctype html>
<title>Flaskr</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<div class=page>
  <h1>Flaskr</h1>

  {% if session.user_id %}
    <a href="{{ url_for('logout') }}">logout</a>
    <a href="{{ url_for('show_entries') }}">entries</a>
    <a href="{{ url_for('user_list') }}">users</a>
  {% else %}
    <a href="{{ url_for('login') }}">login</a>
    <a href="{{ url_for('show_entries') }}">entries</a>
  {% endif %}

  {% for message in get_flashed_messages() %}
    <div class=flash>{{ message }}</div>
  {% endfor %}
  {% block body %}{% endblock %}
</div>

ログインしないと記事を追加できないように変更します。

flaskr/templates/show_entries.html

{% extends "layout.html" %}
{% block body %}

  {% if session.user_id %}
  <form action="{{ url_for('add_entry') }}" method=post class=add-entry>
    <dl>
      <dt>Title:
      <dd><input type=text size=20 name=title>
      <dt>Text:
      <dd><textarea name=text rows=5 cols=20></textarea>
      <dd><input type=submit value=Share>
    </dl>
  </form>
  {% endif %}

  <ul class=entries>
  {% for entry in entries %}
    <li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}
  {% else %}
    <li><em>Unbelievable.  No entries here so far</em>
  {% endfor %}
  </ul>
{% endblock %}

実行してみましょう

python manage.py runserver

4.4 ユーザー管理画面は認証していない人には見せたくない

事前にユーザーを作ってからやらないと・・・ログインできなくなりますw

ユーザー管理画面など一般ユーザーには見せたくないページがあります。

先ほど作成したユーザー管理画面を、ログインしないと見れないように変更します。

flaskr/views.py:

from functools import wraps
from flask import request, redirect, url_for, render_template, flash, abort, \
        jsonify, session, g

...

def login_required(f):
    @wraps(f)
    def decorated_view(*args, **kwargs):
        if g.user is None:
            return redirect(url_for('login', next=request.path))
        return f(*args, **kwargs)
    return decorated_view

@app.before_request
def load_user():
    user_id = session.get('user_id')
    if user_id is None:
        g.user = None
    else:
        g.user = User.query.get(session['user_id'])

...

@app.route('/users/')
@login_required
def user_list():

...

@app.route('/users/<int:user_id>/')
@login_required
def user_detail(user_id):

...

@app.route('/users/<int:user_id>/edit/', methods=['GET', 'POST'])
@login_required
def user_edit(user_id):

...

@app.route('/users/create/', methods=['GET', 'POST'])
@login_required
def user_create():

...

@app.route('/users/<int:user_id>/delete/', methods=['DELETE'])
def user_delete(user_id):
  • before_request
    • routeで追加したendpointの前に呼ばれる。
    • beforeがあるということは、afterもある。
    • session[‘user_id’]に格納したuser.idからUserを取得
    • gというFlaskインスタンス内のグローバルのような変数にuserを追加
  • login_required
    • デコレータを定義
    • g.userを確認してログインしているかをチェックしている。

書き換えたviews.pyはこちら

from functools import wraps
from flask import request, redirect, url_for, render_template, flash, abort, \
        jsonify, session, g
from flaskr import app, db
from flaskr.models import Entry, User

def login_required(f):
    @wraps(f)
    def decorated_view(*args, **kwargs):
        if g.user is None:
            return redirect(url_for('login', next=request.path))
        return f(*args, **kwargs)
    return decorated_view

@app.before_request
def load_user():
    user_id = session.get('user_id')
    if user_id is None:
        g.user = None
    else:
        g.user = User.query.get(session['user_id'])


@app.route('/')
def show_entries():
    entries = Entry.query.order_by(Entry.id.desc()).all()
    return render_template('show_entries.html', entries=entries)

@app.route('/add', methods=['POST'])
def add_entry():
    entry = Entry(
            title=request.form['title'],
            text=request.form['text']
            )
    db.session.add(entry)
    db.session.commit()
    flash('New entry was successfully posted')
    return redirect(url_for('show_entries'))


@app.route('/users/')
@login_required
def user_list():
    users = User.query.all()
    return render_template('user/list.html', users=users)

@app.route('/users/<int:user_id>/')
@login_required
def user_detail(user_id):
    user = User.query.get(user_id)
    return render_template('user/detail.html', user=user)

@app.route('/users/<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)

@app.route('/users/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')

@app.route('/users/<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'})


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        user, authenticated = User.authenticate(db.session.query, 
                request.form['email'], request.form['password'])
        if authenticated:
            session['user_id'] = user.id
            flash('You were logged in')
            return redirect(url_for('show_entries'))
        else:
            flash('Invalid email or password')
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.pop('user_id', None)
    flash('You were logged out')
    return redirect(url_for('show_entries'))



実行してみましょう

python manage.py runserver

ログイン処理ができました。なんとなくそれっぽくなりましたね。