Visual StudioとFlaskで作る書籍管理アプリ:外枠を作る

この記事では、Flaskアプリにおける、SQLiteとの接続と切断、BluePrintを用いた機能の分割、テンプレートの実装の方法などを説明します。

Flask書籍管理アプリにおいて、細かい機能は飛ばし、とりあえずの外枠を実装します。
この記事の中で、書籍管理アプリにおいて必要となるファイルはすべて作成したうえで、エラーが起こらずにアプリを起動させるところまで進めます。

この記事はVisual StudioとFlaskで書籍管理アプリを作ってみたという一連の記事の中の1つです。
詳しくは以下の記事も参照してください
Visual Studioで作ってみたFlaskアプリ
Visual StudioとFlaskで作る書籍管理アプリ:概要

この記事ではWebアプリの開発を何も知らない人を対象とします。といっても、この記事の著者もWebアプリ開発に明るくありません。自分の勉強の意味も込めて書きました。
誤りが含まれるかもしれませんが、ご容赦ください。何かあれば、ご指摘いただけますと幸いです。



スポンサードリンク

目次

  1. ファイルの分割と機能の連携
  2. DB接続と切断を行う:db.py
  3. Home画面を表示させる:views.py
  4. ログイン機能を実装する:auth.py
  5. 書籍管理機能を実装する:book.py
  6. __init__.pyの記述
  7. テンプレートの実装
  8. 動作確認

 

1.ファイルの分割と機能の連携

アプリケーション開発時にエラーが出てしまって動かなくなる原因は、大きく2つあります。
1つは機能同士の連携がうまくいかないこと。
1つは機能の実装がそもそも間違っていることです。

連携ができていないのか、個別の機能で実装をミスしたのかを判断するのが難しいです。特に、アプリを開発し始めた時はすごく大変です(私自身大変でしたし、今でも大変です)。

というわけで、まずはアプリケーションの全体像をつかみます。「機能同士の連携」がある程度うまくいっているということを確認したうえで、個別の機能の作り込みに移っていきます。

今回は書籍管理アプリという単純な題材ではありますが、データベースは使うし、ログイン機能などもあるので、それなりには複雑です。
ファイルは適宜分割しますし、それらを取りまとめる工夫も必要です。そういったところをここで説明します。

ファイル構成については『Visual StudioとFlaskで作る書籍管理アプリ:概要 』を参照してください。

上記の記事にもありますが、ファイル構成は、最終的には以下のようになります。

instanceフォルダの中身は『SQLiteとDB Browser for SQLiteを使ってデータベースを構築する』で解説したので、それ以外を作成していきます。

 

2.DB接続と切断を行う:db.py

SQLiteを用いたデータベースは別の記事でもう作ってあるという前提なので、データベースとの接続と切断の機能を実装しましょう。

ソリューションエクスプローラーの「flask_book_management_1」というフォルダ(プロジェクトではなくフォルダです)を右クリックして『追加』→『新しい項目』→『空のPythonファイル』を選択します。
「db.py」という名称で保存して、以下のように実装します。

"""
DB接続と切断を行う
"""

import sqlite3
from flask import current_app, g

def get_db():
    """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の切断"""
    db = g.pop('db', None)

    if db is not None:
        db.close()

これはほぼ公式のFlaskチュートリアルのままでして、チュートリアルから使わない関数を削除してコメントを追加しただけです。
SQL-Alchemyなど別のライブラリを使うのではない限り、似たような実装になるのかなと思います。

get_db()

get_db関数でデータベースと接続します。これはログイン機能であったり書籍の追加機能を実装する際に、データベースとアクセスする際、適宜これを呼び出すようにします。

close_db()

名前の通りDBを切断する関数です。
接続したら必ず切断しないといけないですね。しかし、普通に実装していると、複雑な分岐があるロジックを組む際に、うっかりで切断をし忘れてしまうということも起こりえます。
そこで、close_db()は、毎回呼び出されるように、後で指定をします。この指定は『__init__.py』で行います。

補足

今回のアプリでは、SLECT文やUPDATE文などのSQL文を実際に記して、データベースを操作します。
しかし、SQL-Alchemyを使うことで、SQLを書かずに済ますこともできます。
外部サイトですが『Flask-SQLAlchemyの使い方』に解説が載っています。

今回はチュートリアル通り、普通にSQLを発行するやり方をとります。
SQLを直接記述する方が、何をやっているのかわかりやすいかなと(個人的に)思います。



スポンサードリンク

 

3.Home画面を表示させる:views.py

「views.py」は、Flaskプロジェクト作成時にすでに用意されていますので、それを改造します。

改造といっても、コメントを日本語訳したのを除けば、余計な関数を削除しただけです。
以下がviews.pyの完成版です。

"""
FlaskアプリケーションのRouteとView
"""

from datetime import datetime
from flask import render_template
from flask_book_management_1 import app

@app.route('/')
@app.route('/home')
def home():
    """インデックスページの表示"""
    return render_template(
        'index.html',
        title='Home Page',
        year=datetime.now().year,
    )

このコードの意味が分からない方は、以下の記事も参照してください。
VisualStudioを使ったFlaskアプリ開発の基本
Visual StudioのFlaskプロジェクトの活用

 

4.ログイン機能を実装する:auth.py

ログイン機能は「auth.py」で実装します。

これもほぼほぼチュートリアルと同じですが、このPythonファイルはなかなか長いものになるので、最初からすべて実装するのではなく、まずは関数の名前などの外枠だけを作っておきます。

プログラムの中における『pass』というのは、何も処理しないという意味です。

"""
ログイン処理などを行う
"""

from datetime import datetime
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 flask_book_management_1.db import get_db

bp = Blueprint('auth', __name__, url_prefix='/auth')


@bp.route('/create_user', methods=('GET', 'POST'))
def create_user():
    """
    GET :ユーザー登録画面に遷移
    POST:ユーザー登録処理を実施
    """
    pass


@bp.route('/login', methods=('GET', 'POST'))
def login():
    """
    GET :ログイン画面に遷移
    POST:ログイン処理を実施
    """
    pass


@bp.route('/logout')
def logout():
    """ログアウトする"""
    pass


@bp.before_app_request
def load_logged_in_user():
    """
    どのURLが要求されても、ビュー関数の前で実行される関数
    ログインしているか確認し、ログインされていればユーザー情報を取得する
    """
    pass


def login_required(view):
    """
    ユーザーがログインされているかどうかをチェックし、
    そうでなければログインページにリダイレクト
    """
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            flash('ログインをしてから操作してください', category='alert alert-warning')
            return redirect(url_for('auth.login'))

        return view(**kwargs)

    return wrapped_view


BluePrint

Flaskで機能ごとにファイルを分割する際には、BluePrintを使います。
使い方としては、13行目のように『bp = Blueprint(‘auth’, __name__, url_prefix=’/auth’)』と指定しておきます。
こうすることで、例えば16行目に『@bp.route(‘/create_user’~~』とありますが、これのURLが『auth/create_user』という扱いになります。

13行目で作った『bp』は、後ほど『__init__.py』でインポートします。

ユーザー登録とログイン

ユーザー登録とログインは、同じURLで、GETで来た時とPOSTで来た時とで処理を分けるようにしています。

例えば、
『auth/create_user』というURLにアクセスされたら、ユーザー登録画面に遷移します。
そのうえで、さらに新規ユーザー名とパスワードを指定してもらって、Formからそれを送信してもらったら、今度は実際にユーザー登録処理を行う、という感じです。

ログアウト

これは処理の分岐は無く、単にログアウトするだけです。
『auth/logout』というURLにアクセスされたら、ログアウト処理が走ります。

ここまでは何となくわかるんですが、後の2つの関数がちょっと、「関数の呼ばれ方」が特殊です。

load_logged_in_user()

40行目で『@bp.before_app_request』と指定されていますね。
URLの代わりにこいつが指定されると「どのURLが指定されても」実行される関数となります。
ここでは、ログインをしているかどうかを判断し、ログインされていればユーザー情報を取得するロジックを後ほど実装します。

login_required(view)

書籍管理機能では、ログインを必須としています。
でも、いちいち「ログインをしているかどうかを判断して、ログインされていなければ、ログイン画面に飛ばす」という処理を、書籍管理機能で毎回実装するのは面倒です。

そこでこの関数のように「関数をラップ(包む)する関数」を定義しておくことで、先の機能を簡単に、他の関数に追加してやることができます。

57行目で『flash(‘ログインをしてから操作してください’, category=’alert alert-warning’)』としてあります。
『flash(‘画面に表示する文字’)』とすれば、遷移先の画面に任意の文字を表示させることができます。
ここでcategoryというのも渡すことができます。『alert alert-warning』というのはBootstrapのclassの1つで、これをclassに指定すると、黄色い文字でワーニングを表すことになります。

ワーニングのイメージは『Bootstrapに用意されているクラス【color編】|Webお役立ちネタ帳』を参照してください。
alert-infoやらalert-dangerやらは今後も頻繁に使います。

 

5.書籍管理機能を実装する:book.py

書籍管理機能は「book.py」で実装します。
これも外枠だけを示します。
雰囲気はログイン機能と同じ感じです。BluePrintなどの説明はログイン機能と同じなので省略します。

関数の頭に『@login_required』とついています。
これはログイン機能で実装した「ログインされていなければログイン画面に遷移させる」という機能で包み込むという指定です。

"""
書籍一覧の取得・新規追加・編集・削除を行う
"""

from datetime import datetime
from flask import (
    Blueprint, flash, redirect, render_template, request, url_for, session
)
from werkzeug.exceptions import abort
from flask_book_management_1.auth import login_required
from flask_book_management_1.db import get_db

bp = Blueprint('book', __name__, url_prefix='/book')


@bp.route('/')
@login_required
def index_book():
    """書籍の一覧を取得する"""
    pass


@bp.route('/create_book', methods=('GET', 'POST'))
@login_required
def create_book():
    """
    GET :書籍登録画面に遷移
    POST:書籍登録処理を実施
    """
    pass


@bp.route('/<int:book_id>/update_book', methods=('GET', 'POST'))
@login_required
def update_book(book_id):
    """
    GET :書籍更新画面に遷移
    POST:書籍更新処理を実施
    """
    pass


@bp.route('/<int:book_id>/delete_book', methods=('GET', 'POST'))
@login_required
def delete_book(book_id):
    """
    GET :書籍削除確認画面に遷移
    POST:書籍削除処理を実施
    """
    pass


def get_book_and_check(book_id):
    """書籍の取得と存在チェックのための関数"""
    pass


書籍の取得

これはGETメソッドしか受け付けません。
書籍一覧を取得して、画面に表示させるだけです。

書籍の追加

これはGETとPOSTメソッドを受け付け、各々、「書籍追加画面への遷移」と「書籍追加処理」を行います。

書籍の更新・削除

更新と削除は、「対象となる書籍」を指定したうえでの操作となる点が、今までと異なります。
URLに書籍IDを含めるようにしました。これで対象を判断します。

get_book_and_check(book_id)

突然出てきた関数でして、私もチュートリアルをやる前はこれの必要性に気が付きませんでした。

この関数が何をやるかというと、書籍の更新・削除において「存在しない書籍ID」が対象になっていないかどうかをチェックします。
普通に画面の指示に従って操作をしている間は、存在しない書籍への処理が動くことはないはずです。しかし、URLに書籍IDを含めているので、やろうと思えば、自分でURLを指定して「存在しない書籍への動作」をさせることもできるわけです。例えば『book/9999/update_book』というURLに直接アクセスしたら、書籍IDが9999のものを(たとえ存在しなくても)対象に取ることができます。

 

6.__init__.pyの記述

色々とファイルを分割したので「__init__.py」がインポートする対象が増えます。また、データベースとの接続や切断についても設定をする必要があります。

まとめて記すと以下のようになります。

"""
The flask application package.
"""
import os
from flask import Flask

app = Flask(__name__)
app.config.from_mapping(
    SECRET_KEY='dev',
    DATABASE=os.path.join(app.instance_path, 'flask_book_management_1.db')
)

# アプリケーションコンテキストが終了したときに
# 毎回DBを切断する
from .db import close_db
app.teardown_appcontext(close_db)

# インデックスページの読み込み
import flask_book_management_1.views

# ログイン機能の追加
import flask_book_management_1.auth
app.register_blueprint(auth.bp)

# 書籍管理機能の追加
import flask_book_management_1.book
app.register_blueprint(book.bp)

app.config

8行目でapp.configを設定しています。
ここで、SECRET_KEYという見るからに秘密にしておいた方がよさそうなものを堂々と書いていますが、本番リリースする際にはこんなやり方をとらないように注意してください。
本来はinstanceフォルダにコンフィグファイルを入れて、それを読み込むようです。
今回はずるをしてここに設定しておきました。

データベースファイルのパスもここで指定しています。

DBの切断

db.pyを読み込んだのち『app.teardown_appcontext(close_db)』とすることで、コメントにもある通り、勝手にDBの切断処理が走るようになります。
これで切断のし忘れがなくなります。

その他のインポート

雑にくくりましたが、19行目以降は、ファイル分割したやつらをまとめてインポートしています。
これを忘れると動かないので注意します。

 

7.テンプレートの実装

ログイン機能などは置いておいて、とりあえずlaytout.htmlとindex.htmlを修正しましょう。

laytout.htmlは以下のようになります。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }} - 書籍管理</title>
    <link rel="stylesheet" type="text/css" href="/static/content/bootstrap.min.css" />
    <link rel="stylesheet" type="text/css" href="/static/content/site.css" />
    <script src="/static/scripts/modernizr-2.6.2.js"></script>
</head>

<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a href="/" class="navbar-brand">書籍管理</a>
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <!--ログイン状態によって、メニューを切り替える-->
                    {% if g.user %}
                    <li><a>ようこそ{{ g.user['username'] }}さん!</a></li>
                    <li><a href="{{ url_for('auth.logout') }}">ログアウト</a></li>
                    {% else %}
                    <li><a href="{{ url_for('auth.create_user') }}">新規ユーザー登録</a></li>
                    <li><a href="{{ url_for('auth.login') }}">ログイン</a></li>
                    {% endif %}
                </ul>
            </div>
        </div>
    </div>

    <div class="container body-content">
        {% block content %}{% endblock %}
        <hr />
        <footer>
            <p>&copy; {{ year }} - 書籍管理</p>
        </footer>
    </div>

    <script src="/static/scripts/jquery-1.10.2.js"></script>
    <script src="/static/scripts/bootstrap.js"></script>
    <script src="/static/scripts/respond.js"></script>
    {% block scripts %}{% endblock %}

</body>
</html>

これはほとんどがどうでもいい修正で、6,20,42行目において「Application name」とか書かれていた部分を「書籍管理」に書き換えただけです。

重要なのは25~32行目です。
ここで、ログイン状態によって、ナビゲーションバーに表示させる内容を変えました。
ログインしていない場合は、新規ユーザー追加とログイン画面へのリンクを表示させます。
ログイン済みの場合は、ユーザー名と「ログアウト」へのリンクを表示させます(ただし、ユーザー名は、ナビゲーションバーに載せるのは変だったなと、記事を書きながら思いました。ナビゲーションバーは、リンクがはられることが多いので、ユーザー名は別の場所の方がいいかもしれません)。

contactなどは使わないので削除しました。

 

続いてindex.htmlです。

{% extends "layout.html" %}

{% block content %}

    {% for category, message in get_flashed_messages(with_categories=true) %}
    <div class="{{ category }}">{{ message }}</div>
    {% endfor %}

    <div class="jumbotron">
        <h1>書籍管理アプリ</h1>
        <p class="lead">書籍の登録・閲覧・編集・削除を行うFlaskアプリケーションです</p>
        <p><a href="{{ url_for('book.index_book') }}" class="btn btn-primary btn-large">書籍一覧へ &raquo;</a></p>
    </div>


{% endblock %}

こちらは、余計なリンクを削除して、アプリ名称を「書籍管理」に変更しました。

また、重要な変更として、Python側から「flash」で渡されたエラーメッセージなどがもしあれば、画面上に表示させるようにしてあります(5~7行目)。
classにflashのcategoryを設定することで、内容に応じて赤のエラーや黄色のワーニングなどを切り替えることができるようになります。

 

8.動作確認

ここまでの実装がすべて終わったら、F5キーを押してデバッグ実行してみましょう。
以下のような画面が表示されるはずです。

ちなみに、リンクをクリックすると(何も機能が実装されていないので)エラーになります。
トップページを表示させるところまでが今回の実装です。
 

次は『Visual StudioとFlaskで作る書籍管理アプリ:ログイン機能の実装』に進みます。

 

関連する記事

 

参考文献

Welcome to Flask
→Flaskの全体像をつかむことができる公式サイトです。ここのチュートリアルを参考にしました。


独学プログラマー Python言語の基本から仕事のやり方まで

 
Pythonのことがさっぱりわからないという方は、この本から入ると良いかと思います。あまり分厚くない本ですので、読み切ることは難しくないはずです。
Pythonの基本文法やコードを書くコツなどが載っています。
 

入門Python3

 
こちらは分厚い本ですが、その分細かく載っています。Flaskの解説も少しあります。辞書的な使い方をしても良い本かと思います。
 

演習で力がつく HTML/CSSコーディングの教科書

 
HTMLとCSSはこの本を読んで勉強しました。
Web開発の書籍はたくさんあるので、お好きなものを選ばれたら良いかなと思います。
 



スポンサードリンク

 
更新履歴
2018年5月26日:新規作成

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください