メモ_Flask-AdminとFlask-LoginでWebアプリ用の管理画面を構築

趣味で作成しているWebアプリ(Python+Flask+waitress)に管理画面をつけたので手順メモ。

管理画面で行いたいことは、データベース(MySQL)への新規データ追加、既存レコード編集、削除だけとシンプルなので、Flask-AdminとFlask-Loginでささっと構築。

慣れていない人でも2~3時間もあれば完成する。とても簡単。

 

手順

  1. 必要な情報をそろえる
  2. 必要な環境、パッケージを用意
  3. 管理画面とログイン画面のコードを書く
  4. 動作確認

 

1. 必要な情報をそろえる

まずは必要な情報を揃えます。管理対象のデータベース情報を確認します。DB名、テーブル名、カラム名など。これをもとにモデル定義します。

DBへの接続はWebアプリ側で用意してあるのでそれを流用します。

 

2. 必要な環境、パッケージを用意

必要なのは以下の通り。pipでinstallします。

  • Flask-Admin
  • Flask-SQLAlchemy
  • Flask-Login
  • Werkzeug
  • pymysql

いきなり本環境に入れるのは怖いため、実験用のvenv仮想環境を用意して、そこで一通り試してから本番用プロジェクトにマージします。

3. 管理画面とログイン画面のコードを書く

管理画面とログイン画面を定義するコードを書きます。

とりあえずapp.pyに全部書いて動作確認します。

基本動作が確認できたら、可読性、保守性のため 本番用app.py はできるだけシンプルにしておきたいので、管理画面/ログイン画面用に書いたコードを app.py の外にadmin.py とかに分離して、BluePrintで読み込ませます。

管理画面を使うユーザーは自分だけでDBにUserテーブル作るのも面倒なので、ログイン用アカウントもコードで定義しちゃいます(行儀悪いけど)。パスワードはさすがに平文管理はまずいのでハッシュ化して、dotenvに保持します。

管理画面のhtmlテンプレートは、デフォではFlask-Adminのライブラリ内にありますが、カスタムテンプレートを指定できるので、ライブラリ内からコピーして、templates/admin/base.html において、それを加工していきます。
ライブラリの場所は下記のような感じです。

<your_virtualenv>/lib/python3.8/site-packages/flask_admin/templates/bootstrap3/admin/base.html
ログイン画面用のテンプレート(login.html)も、/templatesフォルダ内に置きます。

アプリ全体のCSRFがあるので、ログイン画面のform要素も忘れずに forms.pyに定義しておきます(未定義だとThe CSRF token is missing.でエラーになる)。

ログアウトボタン(/logout)を base.html に追加します。

 

4. 動作確認

実験環境で基本部分を確認できたら、本番用のプロジェクトにマージして動作確認。本当にお手軽に構築できるのが素敵。

 

 

コード

from flask import Flask, jsonify, request, redirect, url_for, render_template
from flask import Blueprint
from waitress import serve
from flask_wtf.csrf import CSRFProtect
from dotenv import load_dotenv
import os

from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin, AdminIndexView
from flask_admin.contrib.sqla import ModelView
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
from werkzeug.security import check_password_hash
from forms import LoginForm

~その他のimport部は省略~



app = Flask(__name__)

# .envファイルから環境変数を読み込む
load_dotenv()

# その他の既存コードは省略

# SQLAlchemyの設定
app.config['SQLALCHEMY_DATABASE_URI'] = f"mysql+pymysql://{os.getenv('DATABASE_USER')}:{os.getenv('DATABASE_PASSWORD')}@{os.getenv('DATABASE_HOST')}/{os.getenv('DATABASE_NAME')}"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# テーブルのモデル定義。
class Info(db.Model):
    __tablename__ = 'info'
    id = db.Column(db.Integer, primary_key=True)
    natural_number = db.Column(db.Integer, nullable=False)
    explanation = db.Column(db.Text)
    url = db.Column(db.Text)

class PrimeList(db.Model):
    __tablename__ = 'prime_list'
    id = db.Column(db.Integer, primary_key=True)
    prime_num = db.Column(db.Integer)
    prime_info = db.Column(db.Text)

# Flask-Loginの設定
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

# ハッシュ化されたパスワードを設定
ADMIN_USERNAME = '*****'
ADMIN_PASSWORD_HASH = 'pbkdf2:sha256:*****'


class User(UserMixin):
    def __init__(self, id):
        self.id = id

@login_manager.user_loader
def load_user(user_id):
    if user_id == ADMIN_USERNAME:
        return User(user_id)
    return None

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        username = form.username.data
        password = form.password.data
        if username == ADMIN_USERNAME and check_password_hash(ADMIN_PASSWORD_HASH, password):
            user = User(username)
            login_user(user)
            return redirect(url_for('admin.index'))
    return render_template('login.html', form=form)

@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('login'))

class MyAdminIndexView(AdminIndexView):
    def is_accessible(self):
        return current_user.is_authenticated

class MyModelView(ModelView):
    def is_accessible(self):
        return current_user.is_authenticated

# Flask-Adminの設定
admin = Admin(app, name='My Admin', template_mode='bootstrap3', index_view=MyAdminIndexView(), base_template='admin/base.html')
admin.add_view(MyModelView(Info, db.session))
admin.add_view(MyModelView(PrimeList, db.session))