# flaskで作るweb application入門 Day2 - 8/1 早大本庄プログラミング合宿 2日目 講義資料 - 意味は飛ばし飛ばし理解して、まずは自作のブログを作ります。その後、自分なりに機能を追加してオリジナルのブログ/SNSを作って下さい。 - [参考資料:Flask Tutorial](https://flask.palletsprojects.com/en/1.1.x/tutorial/) ## Flaskを使ってwebアプリケーションを作る ### Hello world for practical app. 以下のlinuxコマンド入力。 ``` $ mkdir flask-tutorial $ cd flask-tutorial $ mkdir flaskr $ cd flaskr ``` flaskr内に\_\_init__.py作成、以下のファイルを作る。 ``` import os from flask import Flask def create_app(test_config=None): # create and configure the app app = Flask(__name__, instance_relative_config=True) app.config.from_mapping( SECRET_KEY='dev', DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), ) if test_config is None: # load the instance config, if it exists, when not testing app.config.from_pyfile('config.py', silent=True) else: # load the test config if passed in app.config.from_mapping(test_config) # ensure the instance folder exists try: os.makedirs(app.instance_path) except OSError: pass # a simple page that says hello @app.route('/hello') def hello(): return 'Hello, World!' return app ``` 以下のlinuxコマンド入力。flask run は必ずflask-tutorial上で ``` $ export FLASK_APP=flaskr $ export FLASK_ENV=development $ flask run ``` Windowsの場合 ``` > set FLASK_APP=flaskr > set FLASK_ENV=development > flask run ``` ### Database #### databaseとは - ただの表。値を保存しておく表や辞書形式のデータ。json形式で保存されることもある。 ### Databaseの作成 #### Databaseを操作するファイル ``` # flasker/db.py import sqlite3 import click from flask import current_app, g from flask.cli import with_appcontext def get_db(): if 'db' not in g: g.db = sqlite3.connect( current_app.config['DATABASE'], detect_types=sqlite3.PARSE_DECLTYPES ) g.db.row_factory = sqlite3.Row return g.db def close_db(e=None): db = g.pop('db', None) if db is not None: db.close() ``` 現在のファイル階層は、 ``` flask-tutorial/ ├── flaskr/ ├── __init__.py └── db.py ``` のようになっている。 #### SQL文の作成 flaskr/schema.sql ファイルを作成 ``` DROP TABLE IF EXISTS user; DROP TABLE IF EXISTS post; CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL ); CREATE TABLE post ( id INTEGER PRIMARY KEY AUTOINCREMENT, author_id INTEGER NOT NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title TEXT NOT NULL, body TEXT NOT NULL, FOREIGN KEY (author_id) REFERENCES user (id) ); ``` #### Databaseを初期化する関数の作成 flaskr/db.pyに以下を追加 ``` # flaskr/db.py def init_db(): db = get_db() with current_app.open_resource('schema.sql') as f: db.executescript(f.read().decode('utf8')) @click.command('init-db') @with_appcontext def init_db_command(): """Clear the existing data and create new tables.""" init_db() click.echo('Initialized the database.') def init_app(app): app.teardown_appcontext(close_db) app.cli.add_command(init_db_command) ``` \_\_init__.pyに以下を追加。 ``` def create_app(): app = ... # 上の部分は、階層を示すための部分です。コピペしないで下さい。 # create_app関数の下に、以下の2行を追記 from . import db db.init_app(app) return app ``` Linuxコマンド flask-tutorialの階層で以下を実行。 ``` $ flask init-db ``` Initialized the database.が出たら成功。 instance/flaskr.sqlite ファイルができていることを確認。 現在のファイル階層は、 ``` /home/user/Projects/flask-tutorial ├── flaskr/ │ ├── __init__.py │ ├── db.py │ └── schema.sql ├── instance/ │ └── flaskr.sqlite ``` のようになっている。 ### Blueprintの作成 - view: webの見た目を決める機能 - Blueprint: 関連するviewやコードを1つにまとめる手法。 flaskr/auth.pyを作成 ``` import functools from flask import ( Blueprint, flash, g, redirect, render_template, request, session, url_for ) from werkzeug.security import check_password_hash, generate_password_hash from flaskr.db import get_db # auth という名前のBlueprintを作成する。 bp = Blueprint('auth', __name__, url_prefix='/auth') ``` bpは今までのappと同じように使える。 @app.route("/") => @bp.route("/") flaskr/\_\_init__.pyのcreate_app()関数に以下を追加。 ``` def create_app(): app = ... # existing code omitted from . import auth app.register_blueprint(auth.bp) return app ``` flaskr/auth.pyに以下を追加。 #### register関数 ``` # register 関数 @bp.route('/register', methods=('GET', 'POST')) def register(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] db = get_db() error = None if not username: error = 'Username is required.' elif not password: error = 'Password is required.' elif db.execute( 'SELECT id FROM user WHERE username = ?', (username,) ).fetchone() is not None: error = 'User {} is already registered.'.format(username) if error is None: db.execute( 'INSERT INTO user (username, password) VALUES (?, ?)', (username, generate_password_hash(password)) ) db.commit() return redirect(url_for('auth.login')) flash(error) return render_template('auth/register.html') ``` #### Login関数 ``` # flaskr/auth.py @bp.route('/login', methods=('GET', 'POST')) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] db = get_db() error = None user = db.execute( 'SELECT * FROM user WHERE username = ?', (username,) ).fetchone() if user is None: error = 'Incorrect username.' elif not check_password_hash(user['password'], password): error = 'Incorrect password.' if error is None: session.clear() session['user_id'] = user['id'] return redirect(url_for('index')) flash(error) return render_template('auth/login.html') @bp.before_app_request def load_logged_in_user(): user_id = session.get('user_id') if user_id is None: g.user = None else: g.user = get_db().execute( 'SELECT * FROM user WHERE id = ?', (user_id,) ).fetchone() ``` #### Logout関数 ``` # flaskr/auth.py @bp.route('/logout') def logout(): session.clear() return redirect(url_for('index')) ``` #### Login Required 関数 ``` def login_required(view): @functools.wraps(view) def wrapped_view(**kwargs): if g.user is None: return redirect(url_for('auth.login')) return view(**kwargs) return wrapped_view ``` #### url_for() 関数 - 引数で指定した名前のrouteに関して全体のURLを取得する。 blueprintがauthでroute名がloginの場合: url_for("auth.login")=> http://[ipaddress]/loginに変換される。 ### template 変数付きのHTMLファイル。 flaskr上でtemplatesフォルダ作成。 ``` mkdir templates cd templates ``` templates にbase.htmlを作成して、以下を記述。 flaskr/templates/base.html ``` <!doctype html> <title>{% block title %}{% endblock %} - Flaskr</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <nav> <h1>Flaskr</h1> <ul> {% if g.user %} <li><span>{{ g.user['username'] }}</span> <li><a href="{{ url_for('auth.logout') }}">Log Out</a> {% else %} <li><a href="{{ url_for('auth.register') }}">Register</a> <li><a href="{{ url_for('auth.login') }}">Log In</a> {% endif %} </ul> </nav> <section class="content"> <header> {% block header %}{% endblock %} </header> {% for message in get_flashed_messages() %} <div class="flash">{{ message }}</div> {% endfor %} {% block content %}{% endblock %} </section> ``` flaskr/templates/auth/register.html を作成して以下を記述。 ``` {% extends 'base.html' %} {% block header %} <h1>{% block title %}Register{% endblock %}</h1> {% endblock %} {% block content %} <form method="post"> <label for="username">Username</label> <input name="username" id="username" required> <label for="password">Password</label> <input type="password" name="password" id="password" required> <input type="submit" value="Register"> </form> {% endblock %} ``` #### {% extends 'base.html' %}とは - 指定したファイルを拡張する。 - base.htmlにある内容は上書き、ない内容は付け足す。 例: base.html 内の{% block content %}{% endblock %} は、register.html の{% block content %} {% endblock %}内のhtmlタグが付け足される。 flaskr/templates/auth/login.htmlに以下を記述。 ``` {% extends 'base.html' %} {% block header %} <h1>{% block title %}Log In{% endblock %}</h1> {% endblock %} {% block content %} <form method="post"> <label for="username">Username</label> <input name="username" id="username" required> <label for="password">Password</label> <input type="password" name="password" id="password" required> <input type="submit" value="Log In"> </form> {% endblock %} ``` これで一通りの機能は実装済み。 ` $ flask run ` をした後、 http://127.0.0.1:5000/auth/register にアクセスしてみよう。 ### CSS ファイルの指定 base.html の最初のほうに `<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">` を書いた。 flaskrフォルダの下にstaticフォルダを作成。その下にstyle.cssを作成して以下を書き込む。 ``` html { font-family: sans-serif; background: #eee; padding: 1rem; } body { max-width: 960px; margin: 0 auto; background: white; } h1 { font-family: serif; color: #377ba8; margin: 1rem 0; } a { color: #377ba8; } hr { border: none; border-top: 1px solid lightgray; } nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } nav h1 { flex: auto; margin: 0; } nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } nav ul { display: flex; list-style: none; margin: 0; padding: 0; } nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } .content { padding: 0 1rem 1rem; } .content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } .content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } .flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } .post > header { display: flex; align-items: flex-end; font-size: 0.85em; } .post > header > div:first-of-type { flex: auto; } .post > header h1 { font-size: 1.5em; margin-bottom: 0; } .post .about { color: slategray; font-style: italic; } .post .body { white-space: pre-line; } .content:last-child { margin-bottom: 0; } .content form { margin: 1em 0; display: flex; flex-direction: column; } .content label { font-weight: bold; margin-bottom: 0.5em; } .content input, .content textarea { margin-bottom: 1em; } .content textarea { min-height: 12em; resize: vertical; } input.danger { color: #cc2f2e; } input[type=submit] { align-self: start; min-width: 10em; } ``` http://127.0.0.1:5000/auth/login を見るとログイン画面がきれいになっている。 ------ 認証部分終わり ------ ファイル構造は、 ``` home/user/Projects/flask-tutorial ├── flaskr/ │ ├── __init__.py │ ├── db.py │ ├── schema.sql │ ├── auth.py │ ├── blog.py │ ├── templates/ │ │ ├── base.html │ │ ├── auth/ │ │ │ ├── login.html │ │ │ └── register.html │ │ └── │ └── static/ │ └── style.css ``` ## 自作ブログ/SNS機能の追加 flaskr/blog.pyに以下を書き込む。 ``` from flask import ( Blueprint, flash, g, redirect, render_template, request, url_for ) from werkzeug.exceptions import abort from flaskr.auth import login_required from flaskr.db import get_db bp = Blueprint('blog', __name__) ``` flaskr/\_\_init__.pyのcreate_app()に以下を追記。 ``` def create_app(): app = ... # existing code omitted from . import blog app.register_blueprint(blog.bp) app.add_url_rule('/', endpoint='index') return app ``` flaskr/blog.pyに以下を追記。 ``` @bp.route('/') def index(): db = get_db() posts = db.execute( 'SELECT p.id, title, body, created, author_id, username' ' FROM post p JOIN user u ON p.author_id = u.id' ' ORDER BY created DESC' ).fetchall() return render_template('blog/index.html', posts=posts) ``` flaskr/templates/下にblogフォルダを作成して、その下にindex.htmlを作成する。以下をindex.htmlに記入。 ``` {% extends 'base.html' %} {% block header %} <h1>{% block title %}Posts{% endblock %}</h1> {% if g.user %} <a class="action" href="{{ url_for('blog.create') }}">New</a> {% endif %} {% endblock %} {% block content %} {% for post in posts %} <article class="post"> <header> <div> <h1>{{ post['title'] }}</h1> <div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div> </div> {% if g.user['id'] == post['author_id'] %} <a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a> {% endif %} </header> <p class="body">{{ post['body'] }}</p> </article> {% if not loop.last %} <hr> {% endif %} {% endfor %} {% endblock %} ``` ### Create関数(blog記事を新しく作るとき) ``` # flaskr/blog.py @bp.route('/create', methods=('GET', 'POST')) @login_required def create(): if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) else: db = get_db() db.execute( 'INSERT INTO post (title, body, author_id)' ' VALUES (?, ?, ?)', (title, body, g.user['id']) ) db.commit() return redirect(url_for('blog.index')) return render_template('blog/create.html') ``` flaskr/templates/blog/create.htmlを作成して、以下を書き込み。 ``` {% extends 'base.html' %} {% block header %} <h1>{% block title %}New Post{% endblock %}</h1> {% endblock %} {% block content %} <form method="post"> <label for="title">Title</label> <input name="title" id="title" value="{{ request.form['title'] }}" required> <label for="body">Body</label> <textarea name="body" id="body">{{ request.form['body'] }}</textarea> <input type="submit" value="Save"> </form> {% endblock %} ``` ### Update関数 書いたブログを編集する。 flaskr/blog.py ``` def get_post(id, check_author=True): post = get_db().execute( 'SELECT p.id, title, body, created, author_id, username' ' FROM post p JOIN user u ON p.author_id = u.id' ' WHERE p.id = ?', (id,) ).fetchone() if post is None: abort(404, "Post id {0} doesn't exist.".format(id)) if check_author and post['author_id'] != g.user['id']: abort(403) return post ``` flaskr/blog.pyに以下を記述。 ``` @bp.route('/<int:id>/update', methods=('GET', 'POST')) @login_required def update(id): post = get_post(id) if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) else: db = get_db() db.execute( 'UPDATE post SET title = ?, body = ?' ' WHERE id = ?', (title, body, id) ) db.commit() return redirect(url_for('blog.index')) return render_template('blog/update.html', post=post) ``` flaskr/templates/blog/update.htmlを作成して以下を書き込み。 ``` {% extends 'base.html' %} {% block header %} <h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1> {% endblock %} {% block content %} <form method="post"> <label for="title">Title</label> <input name="title" id="title" value="{{ request.form['title'] or post['title'] }}" required> <label for="body">Body</label> <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea> <input type="submit" value="Save"> </form> <hr> <form action="{{ url_for('blog.delete', id=post['id']) }}" method="post"> <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');"> </form> {% endblock %} ``` ### Detele関数 flaskr/blog.py ``` @bp.route('/<int:id>/delete', methods=('POST',)) @login_required def delete(id): get_post(id) db = get_db() db.execute('DELETE FROM post WHERE id = ?', (id,)) db.commit() return redirect(url_for('blog.index')) ``` flask run してそれぞれの機能を試す。できたら終了。 より詳しく知りたい方は、 [Flask Tutorial Make the Project Installable~](https://flask.palletsprojects.com/en/1.0.x/tutorial/install/) pip install できるパッケージとして発行できたりする。 # 入門レベルは終了、ここから先は - わざと脆弱なサイトを作って攻撃してみる。 - GitHub pages等で公開する。 - いいね機能、返信機能、広告機能を付けるには? - 文章=>動画に置き換えれば動画配信サイト、音楽に置き換えれば音楽配信サイトに -