Flask実践入門 - 基本的なアプリ構成を問い合わせフォームをつくりながら学ぶ
Python製のマイクロWebフレームワークであるFlaskは最小限の機能のみを提供するシンプルさが特徴です。一方で、シンプルゆえ、アプリを構築する際、どのような構成にするか迷ってしまう局面もあります。本記事はFlaskを使い、実際にアプリをつくりながら基本的な構成の考え方をハンズオンで学びます。解説はテックタッチ株式会社の佐藤昌基さんです。
FlaskはPython製のマイクロWebフレームワークですが、いわゆるフルスタックのそれとは違い、最小限の機能のみを提供するフレームワークで、最低限の規約こそあれど、アプリの構成を自由に決められます。
また、シンプルではあるもののデータベース機能をはじめ、多くの拡張機能をサポートしており、これらの拡張機能はFlask自体に実装されているかのように簡単に利用できます。必要に応じてさまざまな拡張機能を追加することで、小規模から大規模まで多様なケースで利用できるように設計されています。
一方で、自由であるがゆえに実践的なアプリ構成に迷うこともあります。本稿では、「ユーザーからの問い合わせアプリ」の実装をモチーフに、Flaskを使った実践的なアプリケーション構成の一例を紹介します。
実践的なアプリケーション全体像
本稿では、実践的なアプリケーション構成の説明を行うため、アプリ自体の作り込みはせず組み立て方を中心に解説します。アプリ自体の作り込みに関することは、筆者が執筆に参加した「Python FlaskによるWebアプリ開発入門」に詳しく書いていますので参考にしてみてください。
本稿で作成するアプリケーション全体構成
. ├── apps │ ├── app.py │ ├── auth │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ ├── templates │ │ │ └── auth │ │ │ ├── base.html │ │ │ ├── login.html │ │ │ └── signup.html │ │ └── views.py │ ├── config.py │ └── contact │ ├── __init__.py │ ├── templates │ │ └── contact │ │ │ ├── base.html │ │ ├── complete.html │ │ └── index.html │ └── views.py ├── local.sqlite ├── .env ├── .env.local ├── .env.testing └── migrations
プロジェクトセットアップ
本稿ではflaskapp
というプロジェクト名で仮想環境を有効にしてアプリケーションを作成します。また、Flask拡張やライブラリは以下を利用するのであらかじめインストールしておきましょう
(venv) $ pip install python-dotenv email-validator flask flask-sqlalchemy flask-migrate flask-login flask-wtf
ライブラリ | 説明 |
---|---|
python-dotenv | .envファイルの値を環境変数として利用する |
email-validator | メールアドレス構文バリデータ |
flask | Flask本体 |
flask-sqlalchemy | SQLAlchemyのFlask拡張 |
flask-migrate | SQLAlchemyデータベースのMigrationを処理するFlask拡張 |
flask-login | ユーザーセッション管理するFlask拡張 |
flask-wtf | フォーム検証機能のFlask拡張 |
コンフィグ設定
まずはapps/config.py
を作成し以下のコンフィグを追加しましょう。実践的なアプリでは開発環境の他にstaging環境、本番環境、テスト環境などが存在するのでそれぞれ専用のコンフィグ設定を行います。
from pathlib import Path basedir = Path(__file__).parent.parent class BaseConfig: """ BaseConfigクラス """ SECRET_KEY = os.environ["SECRET_KEY"] WTF_CSRF_SECRET_KEY = os.environ["WTF_CSRF_SECRET_KEY"] class LocalConfig(BaseConfig): """ BaseConfigクラスを継承してLocalConfigクラスを作成する """ SQLALCHEMY_DATABASE_URI = f"sqlite:///{basedir / 'local.sqlite'}" SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ECHO = True class StagingConfig(BaseConfig): """ BaseConfigクラスを継承してStagingConfigクラスを作成する """ SQLALCHEMY_DATABASE_URI = f"sqlite:///{basedir / 'stg.sqlite'}" SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ECHO = False class ProductionConfig(BaseConfig): """ BaseConfigクラスを継承してProductionConfigクラスを作成する """ SQLALCHEMY_DATABASE_URI = f"sqlite:///{basedir / 'prd.sqlite'}" SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ECHO = False class TestingConfig(BaseConfig): """ BaseConfigクラスを継承してTestingConfigクラスを作成する """ SQLALCHEMY_DATABASE_URI = f"sqlite:///{basedir / 'testing.sqlite'}" SQLALCHEMY_TRACK_MODIFICATIONS = False # CSRFトークンチェックを無効にする WTF_CSRF_ENABLED = False # config辞書にマッピングする config = { "testing": TestingConfig, "local": LocalConfig, "stg": StagingConfig, "prd": ProductionConfig, }
環境変数設定
本稿では各環境ごとに.envファイルを作成し環境毎に環境変数を切り替えられるようにします。また、デプロイする環境にもよりますが、staging環境や本番環境についてはデプロイ先サーバーの環境変数を利用する想定で本プロジェクトへは追加しません。
開発環境用
FLASK_APP=apps.app:create_app FLASK_ENV=development FLASK_DEBUG=1 SECRET_KEY="2AZSMss3p5QPbcY2hBsJ" WTF_CSRF_SECRET_KEY="2AZSMss3p5QPbcY2hBsJ"
テスト用
FLASK_APP=apps.app:create_app('testing') FLASK_ENV=development SECRET_KEY="2AZSMss3p5QPbcY2hBsJ" WTF_CSRF_SECRET_KEY="2AZSMss3p5QPbcY2hBsJ"
create_app 関数を利用し Flask アプリを生成する
続いてapps/app.py
に以下のコードを追加します。create_app
関数はFlaskアプリを生成する関数で、これを使うことで、簡単に開発環境やステージング環境、本番環境、テスト環境を切り替えることができます。コンフィグは先程設定したconfigを読み込みます。
from flask import Flask from apps.config import config def create_app(config_key="local"): app = Flask(__name__) app.config.from_object(config[config_key]) return app
これでプロジェクトのセットアップができました。
実践 1: 問い合わせアプリをつくる
ここから実際にアプリの構成に入っていきます。まずはBlueprint
を利用してアプリを追加していきます。Blueprintとは、アプリケーションを分割するためのFlaskの機能です。分割することでアプリケーションが大規模になっても簡潔な状態を保つことができ、保守性が向上します。本稿で作成するアプリは構成の説明を中心とするため、問い合わせアプリと簡易的なものとしています。
Blueprint
はアプリを画面のパーツ毎に分けることも可能ですが、その他、サブドメインごとに分割できたり、APIであれば境界づけられたコンテキスト単位で分割できたりと、さまざまな単位で分割可能です。適切なドメイン単位で分割することで、管理しやすい構成になるでしょう。まとめるとBlueprint
は以下のような特徴を持っています。
- アプリケーションが分割できる
- URLプレフィックスやサブドメインを指定してほかのアプリケーションルートと区別ができる
- Blueprint単位でテンプレートが分けられる
- Blueprint単位で静的ファイルが分けられる
さっそく、apps/app.py
にregister_blueprint
を使って問い合わせアプリを追加してみましょう。
from flask import Flask from apps.config import config def create_app(config_key="local"): app = Flask(__name__) app.config.from_object(config[config_key]) # 問い合わせアプリを追加する from apps.contact import views as contact_views app.register_blueprint(contact_views.contact, url_prefix="/contact") return app
続いてapps/contact/views.py
を作成し問い合わせ用のエンドポイントを追加します。エンドポイントは、問い合わせフォーム画面と完了画面を返すGET
と問い合わせフォームを処理するPOST
を作成します。
from email_validator import EmailNotValidError, validate_email from flask import Blueprint, flash, redirect, render_template, request, url_for contact = Blueprint( "contact", __name__, template_folder="templates", static_folder="static" ) @contact.route("/", methods=["GET", "POST"]) def index(): if request.method == "POST": # フォームの値を取得してバリデーションを行う user_name = request.form["user_name"] email = request.form["email"] message = request.form["message"] is_valid = True if not user_name: flash("ユーザー名を入力してください。") is_valid = False if not email: flash("メールアドレスを入力してください。") is_valid = False try: validate_email(email) except EmailNotValidError: flash("メールアドレスが不正です。") is_valid = False if not message: flash("メッセージを入力してください。") is_valid = False if not is_valid: return redirect(url_for("contact.index")) # DBに保存したり、メールを送信したりする処理をする # --ここでは省略する print(user_name, email, message) return redirect(url_for("contact.complete")) return render_template("contact/index.html") @contact.route("/complete") def complete(): return render_template("contact/complete.html")
エンドポイント追加後flask routes
コマンドを実行するとエンドポイントの情報が確認できます。
$ flask routes Endpoint Methods Rule ---------------- --------- ------------------------------- contact.complete GET /contact/complete contact.index GET, POST /contact/ contact.static GET /contact/static/<path:filename> static GET /static/<path:filename>
apps/contact/templates/contact/base.html
を作成し問い合わせアプリ用の共通テンプレートを作成します。テンプレートエンジンであるjinja2を使うと共通テンプレートが利用でき、必要なテンプレートだけ書くことが可能です。ただしBlueprintを使っている場合、templates
直下にアプリ用のディレクトリを挟まないと他アプリとtemplates
のパスが被ってしまい、意図した HTML が表示されなくなるので注意してください。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>{% block title %}{% endblock %}</title> </head> <body> {% block content %}{% endblock %} </body> </html>
contact/base.html
を継承しapps/contact/templates/contact/index.html
を追加し問い合わせフォーム画面を作成します。
{% extends "contact/base.html" %} {% block title %}問い合わせフォーム{% endblock %} {% block content %} <div> <h1>問い合わせフォーム</h1> {% with messages = get_flashed_messages() %} {% if messages %} <ul> {% for message in messages %} <li>{{ message }}</li> {% endfor %} </ul> {% endif %} {% endwith %} <form action="{{url_for('contact.index')}}" method="POST" novalidate="novalidate" > <p> <label for="user_name">名前:</label><input type="text" name="user_name" /> </p> <p> <label for="email">メールアドレス:</label><input type="text" name="email" /> </p> <p> <label for="message">内容:</label><textarea name="message" cols="40" rows="10"></textarea> </p> <p><button type="submit">送る</button></p> </form> </div> {% endblock %}
同様にcontact/base.html
を継承しapps/contact/templates/contact/complete.html
を追加し問い合わせ完了画面を作成します。
{% extends "contact/base.html" %} {% block title %}問い合わせフォーム{% endblock %} {% block content %} <p>お問い合わせありがとうございました。</p> {% endblock %}
動作確認
以上で問い合わせフォームが形になりました。flask run
コマンドを実行してhttp://127.0.0.1:5000/contact/
にアクセスして動作確認をしてみましょう。
(venv) $ flask run
問い合わせフォームが表示され、何も入力せずに「送る」ボタンをクリックするとバリデーションが効いていることが確認できます。また、値を入力すると正常にサブミットされることが確認できます。
実践 3: 認証機能
ここからはもう少し機能を追加してみます。各アプリが利用する共通認証機能を作成し、先程作成した問い合わせアプリをログイン済みユーザーしか利用できないように変更します。
データーベースと連携
認証機能を利用するには、まずはユーザー情報が必要です。ユーザー情報をDBに保存できるよう、FlaskアプリをDBと連携します。
SQLAlchemy(ORM)・DBマイグレーション・認証機能が利用できるようapps/app.py
にFlask拡張機能であるflask_sqlalchemy
、flask_migrate
、flask_login
を読み込み、以下のように記述します。
from flask import Flask from flask_login import LoginManager from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy from apps.config import config db = SQLAlchemy() login_manager = LoginManager() login_manager.login_view = "auth.login" login_manager.login_message = "" def create_app(config_key="local"): app = Flask(__name__) app.config.from_object(config[config_key]) db.init_app(app) Migrate(app, db) login_manager.init_app(app) from apps.contact import views as contact_views app.register_blueprint(contact_views.contact, url_prefix="/contact") # 認証用アプリ追加 from apps.auth import views as auth_views app.register_blueprint(auth_views.auth, url_prefix="/auth") return app
問い合わせアプリとは違い、フォームをより宣言的に実装できるよう、今回はflask-wtf
拡張機能を利用します。apps/auth/form.py
を作成しましょう。
from flask_wtf import FlaskForm from wtforms import PasswordField, StringField, SubmitField from wtforms.validators import DataRequired, Email, Length class SignUpForm(FlaskForm): username = StringField( "ユーザー名", validators=[ DataRequired("ユーザ名は必須です。"), Length(1, 30, "30文字以内で入力してください。"), ], ) email = StringField( "メールアドレス", validators=[ DataRequired("メールアドレスは必須です。"), Email("メールアドレスの形式で入力してください。"), ], ) password = PasswordField("パスワード", validators=[DataRequired("パスワードは必須です。")]) submit = SubmitField("新規登録") class LoginForm(FlaskForm): email = StringField( "メールアドレス", validators=[ DataRequired("メールアドレスは必須です。"), Email("メールアドレスの形式で入力してください。"), ], ) password = PasswordField("パスワード", validators=[DataRequired("パスワードは必須です。")]) submit = SubmitField("ログイン")
続いてapps/auth/templates/auth/base.html
を作成し認証機能用の共通テンプレートを作成します。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>{% block title %}{% endblock %}</title> </head> <body> {% block content %}{% endblock %} </body> </html>
さらに、auth/base.html
を継承しapps/auth/templates/auth/login.html
を作成しログイン画面用のテンプレートを作成します。
{% extends "auth/base.html" %} {% block title %}ログイン{% endblock %} {% blockcontent %} <div> <header>ログイン</header> <section> <form method="post" action="{{ url_for('auth.login') }}"> {% for message in get_flashed_messages() %} <span>{{ message }}</span> {% endfor %} {{ form.csrf_token }} <p>{{ form.email(placeholder="メールアドレス") }}</p> <p>{{ form.password(placeholder="パスワード") }}</p> <p>{{ form.submit() }}</p> <p></p> </form> </section> </div> {% endblock %}
同様にauth/base.html
を継承しapps/auth/templates/auth/signup.html
を作成しサインアップ画面用のテンプレートを作成します。
{% extends "auth/base.html" %} {% block title %}ユーザー新規登録{% endblock %} {% block content %} <div> <header>ユーザー新規登録</header> <section> <form method="post" action="{{ url_for('auth.signup', next=request.args.get('next')) }}" > {{ form.csrf_token }} {% for message in get_flashed_messages() %} <div>{{ message }}</div> {% endfor %} <p>{{ form.username(size=30, placeholder="ユーザー名") }}</p> <p>{{ form.email(placeholder="メールアドレス") }}</p> <p>{{ form.password(placeholder="パスワード") }}</p> <p>{{ form.submit() }}</p> </form> </section> </div> {% endblock %}
apps/auth/models.py
を作成しユーザーモデルを作成します。このモデルで定義した情報がDBのテーブルとして出力されます。
from datetime import datetime from apps.app import db, login_manager from flask_login import UserMixin from werkzeug.security import check_password_hash, generate_password_hash class User(db.Model, UserMixin): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String, index=True) email = db.Column(db.String, unique=True, index=True) password_hash = db.Column(db.String) created_at = db.Column(db.DateTime, default=datetime.now) updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) @property def password(self): raise AttributeError("読み取り不可") @password.setter def password(self, password): self.password_hash = generate_password_hash(password) def verify_password(self, password): return check_password_hash(self.password_hash, password) def is_duplicate_email(self): return User.query.filter_by(email=self.email).first() is not None @login_manager.user_loader def load_user(user_id): return User.query.get(user_id)
apps/auth/views.py
を追加し認証用のエンドポイントを追加します。エンドポイントは、ログイン画面と新規登録画面を返すGET
と、ログイン / 新規作成処理 / ログアウト処理をするPOST
を作成します。
from flask import ( Blueprint, render_template, redirect, url_for, flash, request, ) from apps.auth.forms import SignUpForm, LoginForm from apps.auth.models import User from apps.app import db from flask_login import login_user, logout_user auth = Blueprint("auth", __name__, template_folder="templates", static_folder="static") @auth.route("/signup", methods=["GET", "POST"]) def signup(): form = SignUpForm() if form.validate_on_submit(): user = User( username=form.username.data, email=form.email.data, password=form.password.data, ) if user.is_duplicate_email(): flash("指定のメールアドレスは登録済みです") return redirect(url_for("auth.signup")) db.session.add(user) db.session.commit() login_user(user) _next = request.args.get("next") if _next is None or not _next.startswith("/"): _next = url_for("contact.index") return redirect(_next) return render_template("auth/signup.html", form=form) @auth.route("/login", methods=["GET", "POST"]) def login(): form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() if user is not None and user.verify_password(form.password.data): login_user(user) return redirect(url_for("contact.index")) flash("メールアドレスかパスワードか不正です") return render_template("auth/login.html", form=form) @auth.route("/logout", methods=["POST"]) def logout(): logout_user() return redirect(url_for("auth.login"))
flask_sqlalchemy
、flask_migrate
を連携するとflask db
コマンドが利用可能になります。下記コマンドを実行してテーブルを生成します。
// 初期化 (venv) $ flask db init // テーブル生成するためのコードを生成する (venv) $ flask db migrate // テーブル生成する (venv) $ flask db upgrade
実行するとusers
テーブルができていることが確認できます。なお、本稿ではSQLiteを利用していますが、MySQLなど他のDBを利用することも可能です。画像はVSCodeで確認したものです。
問い合わせアプリをログイン必須にする
仕上げに、問い合わせアプリをログイン済みユーザーのみ利用可能にしてみます。apps/contact/views.py
の各エンドポイントに@login_required
デコレータを追加します。
# 省略 from flask_login import login_required # 省略 @contact.route("/", methods=["GET", "POST"]) @login_required def index(): # 省略 @contact.route("/complete") @login_required def complete(): # 省略
ログイン状態がわかるように問い合わせアプリのapps/contact/templates
にログイン状態を表示します。
<!-- 省略 --> <body> <div> {% if current_user.is_authenticated %} <p> <span>{{ current_user.username }}</span> - <span><a href="{{ url_for('auth.logout') }}">ログアウト</a></span> </p> {% endif %} </div> <!-- 省略 --> </body>
動作確認
ログイン必須化は以上の通りです。最後に動作を確認してみましょう。http://127.0.0.1:5000/contact
にアクセスするとログイン画面にリダイレクトされるようになりました。
http://127.0.0.1:5000/auth/signup
にアクセスするとユーザー新規登録画面が表示され、ユーザー情報を入力すると問い合わせ画面が表示されるように変わりました。
まとめ
以上の通り、問い合わせアプリ作成、そして認証機能を追加しました。ここまでご覧いただけた方は、適切なドメインでアプリを追加することでスケールしやすいプロジェクト構成をFlaskで構築できることが感じていただけたのではないでしょうか。
今回紹介したアプリ以外にも、ちょっとしたアイデアをアプリとして形にする際、DBや認証機能、Formなどの基本を把握していれば簡単に応用できると思います。ぜひ、みなさんのイメージするアプリを手元でつくってみてください。
佐藤昌基(MASAKI Satou)Twitter: @taisa831
編集:はてな編集部