LoginSignup
11
14

More than 3 years have passed since last update.

Flaskで作るSNS Flask(Template)編

Last updated at Posted at 2020-11-25

初めに

Udemyの下記講座の受講記録

Python+FlaskでのWebアプリケーション開発講座!!~0からFlaskをマスターしてSNSを作成する~
https://www.udemy.com/course/flaskpythonweb/

この記事ではFlaskのView側(Template側)について記載する。

Flaskとは

  • PythonのWebフレームワーク
  • MVT(モデル・ビュー・テンプレート)

  • Model:データ挿入、更新、削除など、データベースにアクセスする処理を実行する。

  • View:ユーザーの入力を受け付け、処理に必要なモデルを呼び出し、その結果をTemplateに渡す。

  • Template:Viewから渡されたデータをもとに動的にページを作成してユーザーに結果を表示する。

Tutorial

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return '<h1>Hello World!</h1>'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)
  • hostには’0.0.0.0'を指定する。ただし、'0.0.0.0'はどんな接続でも受け入れる状態のことで、厳密には有効なアドレスではないらしい。

  • '0.0.0.0'と同様に、'127.0.0.1'というhostもよく利用される。
    '127.0.0.1'は、自分自身を指すIPアドレスである。そのため、自分自身のサービスが動作しているかどうかを確認したり、自分自身のコンピュータ上で動作しているサービスへ接続する場合に利用したりできる。

  • どちらのhostアドレスもURL上'localhost'に置き換えて接続できる。

debug=True によるデバッグ

app.run()にデバッグオプション"debug=True"を設定することで、エラーが発生した時にブラウザにエラーの詳細が表示されるようになる。

スクリーンショット 2020-10-29 20.43.23.png

水色の行にカーソルを当てると、右端にコンソールのアイコンが表示される。
このアイコンを選択して表示される画面に、コンソールに表示されているPINを入力すると、ブラウザ上でコンソール入力によるデバッグが可能となる。

・Browser
スクリーンショット 2020-10-29 20.45.48.png

・Console
スクリーンショット 2020-10-29 20.47.04.png

・Debug
スクリーンショット 2020-10-29 20.49.38.png

ルーティング

#1つのメソッドにルーティングを複数指定することも可能
@app.route('/')
@app.route('/hello')
def index():
    return '<h1>Hello World!</h1>'

#URLのパラメータに応じて表示を変更する
@app.route('/post_name/<post_name>')
def show_post_name(post_name):
    print(type(post_name))
    return '<h1>{}</h1>'.format(post_name)

#パラメータのデータ型をして可能(デフォルトはstring)
#指定したデータ型以外が渡された場合はNotFoundになる。
@app.route('/post_id/<int:post_id>')
def show_post_id(post_id):
    print(type(post_id))
    return '<h1>{}</h1>'.format(post_id)

#ルーティングで複数のパラメータを取得することも可能
@app.route('/post_data/<int:post_id>/<post_name>')
def show_post_data(post_id, post_name):
    return '<h1>{}`s id = {}</h1>'.format(post_name, post_id)

テンプレート

  • Flaskで利用するHTMLファイル。
  • Jinjaというライブラリを使用している。
  • Templatesフォルダ配下に格納する。
  • "render_template()"メソッドで処理で使用するテンプレートファイルを指定する。
@app.route('/')
def index():
    return render_template('index.html')
  • テンプレートを格納するフォルダ名は基本的に”Templates"だが、別名に変更したい場合は、appにFlaskを代入する際に変更後のフォルダを指定する。
#テンプレートを格納するフォルダを'Template_2'に変更する例
app = Flask(__name__, template_folder='Template_2'

テンプレートへの引数の受け渡し

  • Template側では、受け取った変数を表示する箇所を"{{ 変数名 }}"と記入する。
  • スクリプト側では、render_template()でテンプレートの後にカンマ区切りで"テンプレート側変数名=スクリプト側変数名"の形式で記入する。
@app.route('/home/<string:user_name>')
def home(user_name):
    login_user = user_name
    return render_template('home.html', user_name = login_user)

辞書型変数を使用する場合

@app.route('/home/<string:user_name>/<int:age>')
def home(user_name, age):
    login_user ={
        'name': user_name
        ,'age': age
    }
    return render_template('home.html', user_info = login_user)
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flask Page</title>
</head>
<body>
    <h1>{{user_info.name}}({{user_info['age']}})さんがログインしました</h1>
</body>
</html>

辞書型変数から特定の要素を取得する場合は、以下どちらの形式でも良い。

  • 辞書変数名.キー
  • 辞書変数名[キー]

テンプレート内への制御構文埋め込み

  • {% %} : if,forなどの制御構文埋め込み
  • {{ }} : 変数の埋め込み
  • {# #} : コメント
def user_list():
    users = ['Taro2','Jiro','Sabro','Shiro']
    is_login = True
    return render_template('userlist.html', users = users, is_login = is_login)
<body>
    <ul>
        {% for user in users %}
            <li>{{ user }}</li>
        {% endfor %}
    </ul>
    {# 制御構文に終了タグが必要になる。(endfor, endifなど) #}
    {% if is_login %}
        <p>ログイン済みです</p>
    {% else %}
        <p>ログインしていません</p>
    {% endif %}
    {% if 'Taro' in users %}
        <p>Taro is Exist.</p>
    {% endif %}
</body>

テンプレートの継承

  • 継承元テンプレートに"{% block content %} --- {% end block %}"
  • 継承先テンプレートに"{% extend '継承先テンプレート' %} --- {% block content %} --- {% end block %}"
  • "content"はブロックごとに異なる名称を設定する。継承元に定義されているブロックと同じ名称が設定されている継承先のブロックが反映される。
  • 継承元の要素利用したい(例えば、継承元の"title"タグに記載されている内容を継承先でも行事したい場合)は、"{{ super() }}"タグを利用する。
#base.html
<!-- 継承元 -->
<html>
<head>
<!-- 継承元の要素を利用したい場合は、"{{ super() }}"を使用する。 -->
{% block title %}{% page_title %}{{ super() }} {% endblock %}
</head>
<body>
{% block content %}
  ここに継承先ファイルの内容が埋め込まれる
{% endblock %}
</body>
</html>

#sub.html
<!-- 継承先 -->
{% extends "template.html" %}

{% block content %}
  ここに継承先ファイルに埋め込む内容を記入する
{% endblock %}

{% block subcontent %}
  複数ブロック存在する場合は異なるブロック名称を設定する。
{% endblock %}

継承元

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}MyPage{% endblock %}</title>
</head>
<body>
    {% block content %}
    {% endblock %}
</body>
</html>

継承先

{% extends 'base.html' %}
{% block title %}Index {{ super() }}{% endblock %}
{% block content %}
    <h1>Hello From Flask, HTML</h1>
{% endblock %}

結果
スクリーンショット 2020-11-01 14.49.26.png
赤枠内でsuper()を使用することで継承先で指定した文字だけでなく継承元の要素であるもじもはんえいされていることが確認できる。

フィルター

  • "{% 変数 | フィルター %}"の形式で記載することで、変数そのものは変更せずに、表示だけフィルターで処理した結果を表示することが可能。
  • "{% filter filter名%} {% block contetnt %}~{%endblock%} {% endfilter %}"とすることで、ブロック全体にフィルターの効果を反映することができる。
{# ブロック単位でフィルターを適用する場合 #}
{# {% filter upper %} #}
{% block content %}
    {# 個別にフィルターを適用する場合 #}
    <h1>Hello From Flask, HTML</h1>
    <!-- 大文字 -->
    <h1>{{ "Hello From Flask, HTML on filter-upper" | upper}}</h1>
    <!-- 文頭のみ大文字 -->
    <h1>{{ "Hello From Flask, HTML on filter-capitalize" | capitalize}}</h1>
    <!-- 単語の先頭のみ大文字 -->
    <h1>{{ "Hello From Flask, HTML on filter-title" | title}}</h1>
    <!-- 小文字 -->
    <h1>{{ "Hello From Flask, HTML on filter-lower" | lower}}</h1>
    <!-- 変数に値が設定されていない場合のデフォルト値 -->
    <p>{{ user|default("valiable user does not Exist") }}</p>
    <!-- urlのリンク化。(target="_blak")追加で新規タブで開く -->
    <p>{{ 'jinja : https://jinja.palletsprojects.com/en/2.11.x/' | urlize(target="_blank")}}</p>

{% endblock %}
{# {% endfilter %} #}
{% block content %}
<h1>先頭の人:{{users | first}}</h1>
<h1>最後の人:{{users | last}}</h1>
<h1>ソートして先頭の人:{{users | sort | first}}</h1>
<h1>ランダム:{{users | random}}</h1>

<ul>
    {# フィルタで逆順にソートしてセットする #}
    {% for user in users | reverse %}
        {# userの"Ta"を"Go"に置換 #}
        <li>{{ user | replace('Ta', 'Go')}}</li>
    {% endfor %}
</ul>
    {# 制御構文に終了タグが必要になる。(endfor, endifなど) #}
    {% if is_login %}
        <p>ログイン済みです</p>
    {% else %}
        <p>ログインしていません</p>
    {% endif %}
    {% if 'Taro' in users %}
        <p>Taro is Exist.</p>
    {% endif %}
{% endblock %}

カスタムフィルター

独自のフィルターを作成する事ができる。
作成したフィルタの使用方法は組み込みフィルタと同様。

from datetime import datetime

#文字列を反転させるフィルタ
@app.template_filter('reverse_name')
def reverse(name):
    return name[-1::-1]

@app.template_filter('birth_year')
def calc_birth_year(age):
    now_timestamp = datetime.now()
    return str(now_timestamp.year - int(age)) + '年'
<!-- 誕生年を表示する(birth_year) -->
{% for user in users %}
    <li>{{user.name}} {{user.age}} born at {{user.age | birth_year}}</li>
{% endfor %}

{% for user in users | reverse %}
    <!-- userの"Ta"を"Go"に置換 -->
    <li>{{ user | reverse | replace('Ta', 'Go')}}</li>
{% endfor %}

画面遷移・エラーハンドリング

指定した関数のページに遷移
<a href="{{ url_for('index')}}">New page </a>

<!-- app.pyの下記関数のこと
@app.route('/')
def index():
    return render_template('index.html')
-->

画像ファイルを表示(静的ファイルを読み込む場合は第1引数は'static'とする)
<img src="{{ url_for('static', filename=ファイルパス}}">
from flask import Flask, render_template, redirect, url_for, abort

#リダイレクト
return redirect(url_for('info', variable='man'))

#指定したエラーを発生させるメソッド
#設定したメッセージはエラーハンドラーで"error.description"とすることで取得可能
abort(エラー番号, メッセージ)

#エラーハンドラー(エラー発生時に関数を呼び出す)
#関数の引数にはエラーオブジェクトを渡すことが必須。(引数名は任意)
#’エラー番号'にはコンソールに表示されているエラー番号を設定する。
@app.errorhandler(エラー番号)
def page_not_found(e):
    return render_template('not_found.html')[,サーバーに返すエラー番号ステータスコード]

例1

@app.route('/user/<string:user_name>/<int:age>')
def user(user_name, age):
    if user_name in ['Taro','Jiro','Saburo']:
        #homeメソッドのurlにリダイレクトする
        return redirect(url_for('home', user_name=user_name, age=age))
    else:
        #特定のエラーを発生させる
        abort(500, 'リダイレクト不可のユーザー')

@app.errorhandler(500)
def system_error(error):
    #上記abortメソッドで設定したメッセージを取得する。
    error_description = error.description
    return render_template('system_error.html', error_description=error_description), 500

例2

@app.errorhandler(404)
#引数にerrorオブジェクトは必須
def page_not_found(e):
    #404エラー発生時に以下のテンプレートページを開く。戻り値として404を返す。
    return render_template('page_not_found.html', error=e), 404
{% extends 'base.html' %}
{% block title %}Not Found{% endblock %}
{% block content %}
<h1>ページが見つかりません</h1>
<p>{{ error }}</p>
{% endblock %}

結果
スクリーンショット 2020-11-03 18.15.30.png

Form

Templateに作成している入力フォームから送信されたリクエストを取得する。

#リクエストデータを取得するモジュール
from flask import request

#GETリクエスト
var = request.args.get('フォーム名')

#POSTリクエスト
var = rwquest.form.get('フォーム名'

#リクエストのタイプを確認する(GETかPOSTか)
request.method

#関数が受け付けるリクエストのタイプを指定する(デフォルトは'GET')
@app.route('/',methods=['GET','POST']

@app.route('/home')
_def home():
    print(request.full_path)
    print(request.method)
    print(request.args)
    return render_template('home.html')

実行結果

#request.full_path
172.17.0.1 - - [08/Nov/2020 14:33:58] "GET / HTTP/1.1" 200 -
/home?last_name=yamada&first_name=taro&job=%E5%85%AC%E5%8B%99%E5%93%A1&gender=%E7%94%B7%E6%80%A7&message=this+is+a+pen.

#request.method
GET

#request.args
ImmutableMultiDict([('last_name', 'yamada'), ('first_name', 'taro'), ('job', '公務員'), ('gender', '男性'), ('message', 'this is a pen.')])
172.17.0.1 - - [08/Nov/2020 14:34:24] "GET /home?last_name=yamada&first_name=taro&job=公務員&gender=男性&message=this+is+a+pen. HTTP/1.1" 200 -

POSTメソッドにしたい場合
requestをPOSTにしたい場合は、formタグのmethod属性に’POST'を指定する。

{% extends 'base.html' %}
{% block content %}
<h1>サインアップページ</h1>
<form action='{{ url_for("home") }}' method='POST'>
・・・
#'method'ではなく'methods'。's'を忘れない。
#'[]'も必須。
#デフォルト(省略時)は'GET'
@app.route('/home', methods=['GET','POST'])
def home():

結果(コンソールで確認)

172.17.0.1 - - [08/Nov/2020 15:31:30] "GET / HTTP/1.1" 200 -
/home?
POST
ImmutableMultiDict([])

ファイルアップロード

secure_filename(ファイル名)
'werkzeug'というWSGIライブラリのメソッド。(Flaskをインストールすると一緒についてくる。)
ファイル名を安全な形に変換する処理。ただし、日本語には対応していないため、日本語名称が設定されているファイルがアップロード対象になる可能性がある場合は、別のライブラリで事前に対処しておく必要がある。
→pykakasi

pykakasi
漢字や平仮名など、日本語を指定した文字に変換してくれるライブラリ。

import pykakasi
kakasi = pykakasi.kakasi()
kakasi.setMode('H','a') #ひらがなをアルファベット小文字に変換
kakasi.setMode('K','a') #カナ文字をアルファベット小文字に変換
kakasi.setMode('J','a') #漢字をアルファベット小文字に変換
conv = kakasi.getConverter()
conv.do('テスト')
>'tesuto'
conv.do('案山子')
>'kakashi' 

HTML側

<form method='POST' enctype='multipart/form-data'>
    <input type="file" name='file'>
    <input type="submit" value='アップロード'>
</form>

Python側

from werkzeug.utils import secure_filename
import pykakasi

class Kakasi:
    kakasi = pykakasi.kakasi()
    kakasi.setMode('H', 'a')
    kakasi.setMode('K', 'a')
    kakasi.setMode('J', 'a')
    conv = kakasi.getConverter()

###  省略  ###

@app.route('/upload', methods=['GET','POST'])
def upload():
    if request.method == 'GET':
        return render_template('upload.html')
    elif request.method == 'POST':
        #requestからファイルアップロード情報を取得
        file = request.files['file']
        #日本語ファイル名をascii表記に変換する。
        convert_file_name = Kakasi.japanese_to_ascii(file.filename)
        #ファイル名を安全な内容に変換
        save_filename = secure_filename(convert_file_name)
        #osモジュールのjoin処理で保存するファイルパスを作成し、saveで保存
        file.save(os.path.join('./static/images', save_filename))

wtforms

フォームの実装簡略化、XSSなどのセキュリティ対策として有効なライブラリ
https://wtforms.readthedocs.io/en/2.3.x/fields/

例えば以下のタグが、

<form method='POST'>
    <label for='name'>名前</label>
    <input type='text' name='name'>
</form>

wtforms.Formを使えば、以下のように記述できる

<form method='POST'>
  {{ form.name.label }} {{form.name()}
</form>

pythonのスクリプトは下記の通り(一部省略)

from flask import Flask, render_template, request
from wtforms import StringField, SubmitField,IntegerField
from wtforms.form import Form

class UserForm(Form):
    name = StringField('名前')

return render_template('index.html', form=form, name=name)

'form.name.label' には、スクリプト側で「〜Field」として定義した時に指定した名称が表示される。

csrf対策

<form action="" method='POST'>
    <!--フォームの先頭に記述する -->
    {{ form.csrf_token }} 
    {{ form.name.label }}{{ form.name() }}
    {{ form.age.label }}{{ form.age() }}
    {{ form.submit()}}
</form>

template関数とセッション

template関数とは、ビューヘルパーのこと。
複数のTemplateで共通的に使用したい部分を別ファイルに切り出しておいて、使いたい時にインポートして使用することができる。

  • template関数の宣言は"macro func_name(arg)"
  • xxx | safe : エスケープ処理を行わない

Template関数

{% macro render_field(field) %}
<dt>{{ field.label }}</dt>
<dd>{{ field(**kwargs) | safe}}</dd>
{% if field.errors %}
<ul class="error">
    {% for error in field.errors %}
    <li>{{ error }}</li>
    {% endfor %}
</ul>
{% endif %}
{% endmacro %}

Template

{% extends 'base.html' %}
<!-- template関数(ヘルパー)をインポートする -->
{% from '_formhelpers.html' import render_field %}
{% block content %}
<form method="POST">
    {{ form.csrf_token }}
    <!-- temlate関数の処理が適用される。 -->
    {{ render_field(form.name) }}
    {{ form.submit }}
</form>
{% endblock %}

スクリプト

class UserForm(Form):
    name = StringField('名前:')
    age = IntegerField('年齢:')
    password = PasswordField('パスワード:')
    birthday = DateField('誕生日:', format='%Y/%m/%d')
    gender = RadioField('性別:', choices=[('man', '男性'), ('woman', '女性')])
    major = SelectField(
        '専攻', choices=[('bungaku', '文学部'), ('hougaku', '法学部'), ('rigaku', '理学部')])
    is_japanese = BooleanField('日本人?:')
    message = TextAreaField('メッセージ:')
    submit = SubmitField('送信')


@app.route('/', methods=['GET', 'POST'])
def index():
    form = UserForm(request.form)

    # POSTされたデータをセッションに格納する。
    if request.method == 'POST' and form.validate():
        session['name'] = form.name.data
        session['age'] = form.age.data
        session['password'] = form.password.data
        session['birthday'] = form.birthday.data
        session['gender'] = form.gender.data
        session['major'] = form.major.data
        session['nationality'] = '日本人' if form.is_japanese.data else '外国人'
        session['message'] = form.message.data
        return redirect(url_for('show_user'))

    return render_template('user_regist.html', form=form)

Fieldのいろいろ

<!-- formのサイズ(幅)を指定する -->
<form method="POST">
    {{ form.csrf_token }}
    {# temlate関数の処理がそれぞれに適用される。 #}
    {{ render_field(form.name, size=100) }}
    <!-- template関数(render_field)を使用しない場合 -->
    <!-- {{ form.age(size=200)}} -->
from wtforms import Form, widgets

class UserForm(Form):
    #デフォルト値設定
    name = StringField('名前:', default='山田花子')
    #placeholder設定(render_kwに辞書型として設定する)
    birthday = DateField('誕生日:', format='%Y/%m/%d',render_kw={'placeholder': 'yyy/mm/dd'})

    #formをTextAreaタイプに変更する
    name = StringField('名前:', widget=widgets.TextArea())
 <!-- 初期値(チェックボックス) -->
 {{ render_field(form.is_japanese,checked=true) }}
 <!-- タグのクラス属性を追加 -->
 {{ render_field(form.age, class='age-class') }}

バリデーション

組み込みバリデーション
Flaskにもともと定義されているバリデーション。

2020-01-10追記
各バリデーションチェックでエラー時に画面に日本語メッセージを表示したい場合は、バリデーション関数の引数に表示したいメッセージを渡す。(引数がない場合はデフォルトの英語メッセージが表示される。)

from wtforms.validators import DataRequired, EqualTo, Length, NumberRange, ValidationError

class UserForm(Form):
    name = StringField('名前:', widget=widgets.TextArea(), validators=[
                       DataRequired('データを入力してください')], default='山田花子')
    age = IntegerField('年齢:', validators=[NumberRange(0, 100, '入力値が不正です')])
    password = PasswordField('パスワード:', validators=[
                             Length(4, 10, '4文字以上10文字以下'), EqualTo('confirm_password', 'パスワード不一致')])
    confirm_password = PasswordField('再入力', validators=[])

自作バリデーション
独自のバリデーションを作成したい場合に、関数とおなじように作成することが可能。

クラス内でバリデーションを定義する場合

#フォーマット
def validate_<対象フォーム名>form, field):
    処理
    raise ValidattionError('エラーメッセージ')
#作成例
class UserForm(Form):
    name = StringField('名前:', widget=widgets.TextArea(), default='山田花子')

    # 自作バリデーション。submit処理時にnameフィールドの値に対してチェック処理を実行
    def validate_name(form, field):
        if field.data == 'nanashi':
            raise ValidationError('この名前は使用できません。')

クラスの外でバリデーションを定義する場合

def validate_<対象フォーム名>(form, field):
    <処理>
    raise ValidationError('エラーメッセージ')
def validate_name(form, field):
    if field.data == 'nanashi2':
        raise ValidationError('その名前も使用できません')

class UserForm(Form):
    name = StringField('名前:', widget=widgets.TextArea(),
           validators=[validate_name,DataRequired('データを入力してください')], default='山田花子')
11
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
14