Templates
render_template()でhtmlファイルを呼び出している。
このhtmlファイルはflaskrパッケージの中のtemplatesディレクトリに保存する必要がある。
Jinja template
Templatesフォルダには、静的また動的なデータファイルがありtemplateはその中のデータと共に最終的なドキュメントを作成する。FlaskではJinjaというtemplate ライブラリを用いている
今回のアプリケーションの中では、ブラウザ向けのHTMLファイルにレンダリングするためにtemplatesフォルダを用いる。
自動エスケープ機能
Flaskではユーザが入力した値で の様にHTMLを混乱させる様なものがあっても安全な値に置換してくれる
Jinjaの {{ ~ }} {% ~ %}
- {{ ~ }} :
- 最終的なHTMLファイルの出力結果を示しています。(Flaskのview functionの結果の引数など)
- {% ~ %} :
- for や ifを示すブロック
- Pythonとは違い、インデントではなくブロックでstart,endを示す({% for i in list %} {% endfor %})
<!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>
<li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
{% else %}
<li><a href="{{ url_for(auth.register) }}">Register</a></li>
<li><a href="{{ url_for(auth.login) }}">Log in</a></li>
{% 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>
gオブジェクト :
gオブジェクトはg.userがセットされているかどうかに応じて、usernameとログアウトリンクが表示されるか、新規登録かログインの画面が表示されるかを決める。
url_for() :
手打ちでURLを書いてview functionに紐づける代わりに自動でURLを生成させるのに使っている
flashの内容をHTMLに反映させる :
get_flashed_messages()を使って、view functionで使ったflash()処理に入れたエラー情報を表示させる
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
他のtemplateに上書きされて使われる箇所
- {% block title %}
- ブラウザーのタブと、ウィンドウのタイトルに表示されるタイトルを司る
- {% block header %}
- titleに似ているが、ページ上のタイトルを司る
- {% block content %}
- 各ページにおけるコンテントを司る(ログインフォームやブログ投稿など)
・blueprintとtemplates
Blueprint用のtemplateは、templatesフォルダの中のblueprintと同じ名前を持ったフォルダの中で管理すれば、統一感を維持できる。
Register
{% 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' %} :
この表記は当template(register)が、base.htmlの各ブロックの表記を置換することをJinjaに伝えている。
{% block header %}
{% block title %} Register {% endblock %}
{% endblock %} :
title blockに入れた値がheaderにも出力されて同じ処理を2回も描かなくて良くなるという手法(header : window, title : page)
inputタグのrequired :
必須項目事項の指定ができる、が、ブラウザのバージョンやユーザーの入力環境によってはrequestされる可能性もあるので、常にユーザからの入力された値は検証しなければならない
Log In
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Login{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="username">Username</label>
<input type="text" name="username" id="username" required>
<label for="password">Password</label>
<input type="password" name="password">
<input type="submit" value="Log in">
</form>
{% endblock %}
Static Files
Authentication templateに用いられるデザインは単調なのでCSSファイルを適用させる。このデザインは静的なのでtemplatesではなくstaticフォルダで管理する
Flaskはflaskr/staticディレクトリからの相対PATHでstatic viewを追加する、base.htmlはすでにstyle.cssへのリンクを持っている
{{ url_for('static',filename=・・・) }}
staticファイルにはcssの他Javascriptや画像などのファイルも含まれる。それらのファイルはすべてflaskr/staticフォルダに格納されて、url_forを用いてアクセスされる。
Blog Blueprint
Blockはすべての投稿を一覧化し、ログインしたユーザには投稿作成・作成者には投稿した内容の編集と削除ができる様にする
The Blueprint
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__)
appインスタンスにBlog Blueprintを譲渡する
import os
from flask import Flask
def create_app(test_config=None):
# appインスタンスの生成と設定
app = Flask(__name__, instance_relative_config=True) # check, instance_relative_config
app.config.from_mapping(
SECRET_KEY = 'dev',
DATABASE = os.path.join(app.instance_path, 'flaskr.sqlite')
)
print('app.instance_path : ', app.instance_path)
if test_config is None:
# test状態ではなく、configインスタンスが存在する場合はconfigインスタンスを読み込む
app.config.from_pyfile('config.py', silent=True) # check, from_pyfile
else:
# test_configが渡されていたら読み込み
app.config.from_mapping(test_config)
# フォルダの存在を確認する
try:
os.makedirs(app.instance_path)
except OSError:
pass
# hello出力
@app.route('/hello')
def hello():
return 'Hello, World!!'
# db情報のimportと登録
from . import db
db.init_app(app)
# blueprintのimportと登録
from . import auth
app.register_blueprint(auth.bp)
#---------------------------------------------
#追加部分 :
from . import blog
app.register_blueprint(blog.bp)
app.add_url_rule('/', endpoint='index')
#---------------------------------------------
return app
url_prefixがない
auth系(loginなど)のURLへはurl_prefixを入れていた
bp = Blueprint('auth', __name__,url_prefix='/auth')
blog系のbpはこのサービスのメインコンテンツなのでメインの目次として扱うためにurl_prefixを実装していない。
app.add_url_rule('/',endpoint='index')について
url_for('index')
とurl_for('blog.index')
が共に /
というURLを生み出せる様に URLルールを変更している
blog blueprintの中のエンドポイント index
は何もしないとblog.index
となる.。
エンドポイント"Index"
以下のindexへの処理は全投稿内容を、投稿日時で降順で並べる。Userテーブルから投稿者の情報を利用できる様にするためにJOINが使われている。
@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)
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Posts{% endblock %}</h1>
{% if g.user %}
<a href="{{url_for('blog.create')}}" class="action">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 href="{{ 'blog.update', id=post['id'] }}" class="action">Edit</a>
{% endif %}
</header>
<p class="body">{{ post['body'] }}</p>
</article>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
{% endblock %}
{% if not loop.last %} :
Jinjaの特殊な記法で、loop.lastはその時点のループが最後の処理かどうかを判定します。この場合、最後ではないループ処理の場合は区切り線<hr>を入れるという処理になっています。
Create
register viewと同じ様な動きをするviewです。
・フォームを表示
・投稿された情報を検証し、データベースに追加するかもしくはエラーを出すかをします
@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')
blog.indexを使っている理由 :
恐らくコードの可読性を高めるためにブループリント名を明示していると考えられる。
TODO :
このurl_forのblog.indexをindexに変換して動作に違いが出るかを検証
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}New Post{endblock}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="title">Title</label>
<input type="text" 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 %}
各HTMLのタグの補足 :
labelタグ
- for: 以下のinputやtextareaタグのidと紐づけると次のメリットがある
- ブラウザの読み上げ機能が連動する
- labelをクリックするとinputがアクティブになる(スマホなど)
inputタグ
- name:
- 入力欄コントロールの名前。名前/値の組の部分としてフォームと一緒に送信される
- id :
- labelタグのforと連動させる値
- type="submit"のvalue :
- ボタンのラベルとして表示される文字列を示します。ボタンはその他の真の値を持ちません。
- 指定しなかったらブラウザのデフォルト値が表示される(送信など)
textareaタグ
- name:
- フォームが送信されたときにデータポイントに関連付けられた名前を設定している
- id:
- labelタグのforと連動させる値
Update
updateとdeleteのviewは投稿情報をユーザIDを使って取得する必要がある。そして、投稿者がログインしているユーザーと同じかどうかを判定する
重複した内容のコードを作る事を避けるため、投稿情報を取得して返すget_post 関数を作成する
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 = user_id'
' WHERE p.id=?',
(id,)
).fetchone()
if post is None:
abort(404, f"Post id {id} doesn't exist.")
if check_author and post['author_id'] != g.user['id']:
abort(403)
return post
abort()関数
HTTPstatusコードを指定して吐き出す関数。第二引数の文字列は任意の文字列となり、指定しなければ各のstatusのデフォルトの値が表示される
デフォルト値 :
404
-- Not Found
403
-- Forbidden
401
-- => 今回はlogin画面への自動遷移
check_author引数
このキーワード引数は、投稿者とログインしているユーザーが一致しているかの照合検証を行うかどうかを指定している。
if check_author and post['author_id'] != g.user['id']:
abort(403)
get_post({id}, check_author=False)
で呼び出す:
=> 投稿を編集しない一般のユーザー向けに投稿情報を表示する場合に使用
get_post({id})
で呼び出す:
=> 編集機能を意図する時の呼び出し方。投稿者とログインユーザーの照合を投稿情報を返す前に実行する
@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)
URL内の<int:id>
@bp.route('/<int:id>/update' ~
:
idは元々データベーススキーマではINTEGERとして定義されている
FlaskはURLに含まれた「1」という数字がINTEGER型かどうかを確かめてから関数に対して INTEGER型のidとして引き渡している。
もし、文字列が入っていれば Errorコード404を返す
もし、<int:id>を指定しなかったら、idは123の様な数字であってもSTRING型"123"として引き渡される。
{% expands 'base.html' %}
{% block header %}
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="title">Title</label>
<input type="text" 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 type="submit" class="danger" value="Delete" onclick="return confirm('Are you sure?');">
</form>
{% endblock %}
{{ request.form['title'] or post['title'] }}
:
フォームが送信される前は元々の投稿情報を表示し、もし正しくない情報が入力された場合はその投稿した時の値を表示します。
Delete
Delete viewはtemplateを持っていない、update.htmlのボタンの一部としてdeleteボタンが存在しており、このボタンから /\<id\>/delete
へRequestされる
templateがないのでPOSTメソッドだけを扱い、実行されたらindex viewへ遷移させるという機能だけを持つ
@bp.route('/<int:id>/delete', method=('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'))
=> ようやく最終セクション ・プロジェクトをインストール可能にする