# 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等で公開する。
- いいね機能、返信機能、広告機能を付けるには?
- 文章=>動画に置き換えれば動画配信サイト、音楽に置き換えれば音楽配信サイトに
-