FlaskとSQLAlchemyで読書記録Webアプリを作る(3)
前回まで2回分が前置きとなってしまいました。失礼しました。今回は前置きなく、早速スクリプトを記述していきます。
読書記録Webアプリの骨組みの作成①(データベースを利用せず)
まずは、データベースを使わずに、取り敢えず、読書記録一覧を表示するインデックスページを作りましょう。読書記録のデータは、データベースから取得するのではなく、スクリプトの中で与えるようにします。
前回ご紹介した最小限のプログラムHello.py
を、今後の展開を考慮して構成を以下の通り分解するイメージです。最終的に前回ご紹介の全体構成につなげていく予定です。
books.py web/ __init__.py views.py templates/ base.html index.html
これは、books.pyを実行すれば、読書記録のリストがホームページ表示されるという、ただ、それだけのアプリです。
具体的に、各スクリプトを記述していきましょう。
# books.py from web import app if __name__ == '__main__': app.run(debug=True)
このbooks.pyが置いてあるディレクトリで、python books.py
を実行すると、app.run
にて、デバッグモード(debug=True)にてアプリが立ち上がります。その実行するアプリは、webディレクトリの__init__.py
からインポートしておきます(from web import app
)。
# web/__init__.py from flask import Flask # from flask_sqlalchemy import SQLAlchemy # from flask_migrate import Migrate app = Flask(__name__) # db = SQLAlchemy(app) # SQLAlchemyを利用する場合 # migrate = Migrate(app, db) # Flask-Migrateを利用する場合 from web import views
ここがメインのスクリプトです。今回は、データベースを利用しないため、データベースに関連する行をコメントアウト(#)しています。そうすると、ここでは、flaskからFlask クラスをインポートし、Flaskインスタンスを生成するだけです。前回、Hello World!を表示するために__init__.py
に記載した、URLを示すデコレータと、そのURLを指定された時に実行される関数の組み合わせは、views.pyとして別ファイルに記述します。そして、ここでは、views.pyをインポートしておきます。
# web/views.py from web import app from flask import render_template @app.route('/') def index(): books = [{'title':'坂の上の雲', 'author':'司馬遼太郎', 'genre':'小説', 'date':'2018-06-01'}, {'title':'竜馬がゆく', 'author':'司馬遼太郎', 'genre':'小説', 'date':'2017-12-31'}, {'title':'ノーサイド・ゲーム', 'author':'池井戸潤', 'genre':'小説', 'date':'2019-06-30'}] return render_template('index.html', books=books)
ここで、URLを示すデコレータと、そのURLを指定された時に実行される関数の組み合わせを作成します。ルートディレクトリhttp://127.0.0.1:5000/
をブラウザのURL欄に入力すると、index関数が呼び出されます。
そして、index.html
を表示します。その際に引数で指定したbooks(この場合、読書記録が保持されたリスト)もindex.htmlに渡されます。
<!-- web/templates/index.html --> {% extends "base.html" %} {% block content %} <br> <table class="table"> <thead class="thead-light"> <tr> <th scope="col">書籍名</th> <th scope="col">著者</th> <th scope="col">ジャンル</th> <th scope="col">読了日</th> </tr> </thead> {% for book in books%} <tbody> <tr> <td> {{ book.title }} </td> <td> {{ book.author }} </td> <td> {{ book.genre }} </td> <td> {{ book.date }} </td> </tr> </tbody> {% endfor %} </table> {% endblock %}
{}で囲んで記述しているのは、flaskをインストールした際に、同時にインストールされているjinja2と呼ばれるテンプレートエンジンです(jinja-wikipedia)。html文書上で、for文やif文その他をpythonのように使える優れものです。作者はflaskと同じ作者ですので、flaskとの親和性は高いのではないでしょうか。
index.html
が開かれると、同時に渡されたbooksリストから、{% for book in books %}
と{% endfor %}
に囲まれた部分により、1つずつ辞書を取得し、テーブルに展開していくようになっています。
なお、{% extends "base.html" %}
により、{% block content %}``{% endblock %}
で囲まれた部分以外は、以下のbase.html
に記載されたものがindex.htmlにも適用されます。
<!-- web/templates/base.html --> <!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>読書記録データベース</title> <!-- ここからはBootstrap4を利用するために、Bootstrapのダウンロードサイト(https://getbootstrap.com/docs/4.3/getting-started/download/)から"BootstrapCDN"を以下にコピペします. --> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> <!-- ここまで --> <link rel="stylesheet" href="{{ url_for('static',filename='css/style.css')}}"> </head> <body> <div class="container"> <br> <h4>読書記録データベース</h4> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <a class="navbar-brand" href="{{ url_for('index') }}">ホーム</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">読書記録の入力</a> </li> </ul> </div> </nav> {% block content %} {% endblock %} </div> </body> </html>
Bootsrapとは、Webページを効率よく開発するためのWebフレームワークで、Webページでよく使われるフォーム、メニュー、ボタンなどのテンプレートが用意されています。これらを使うことにより、デザインを気にすることなく(Bootstrapに任せて)、プログラムの他のところに注力できるメリットがあります。デザインはシンプルですが、そこそこ統一感のあるものができます。
これを使わない手はありませんので、base.htmlにおいて、Bootstrap4のサイトから、BootstrapCDNの必要な箇所をコピペしています。
そして、ナビゲーションバー(メニューバー)をclass="navbar"
等で設定しています。
さて、ここでまで出来たところで、 python books.py
を実行します。すると、以下の画面が表示されました。取り敢えず、データベースのない基本構成はできました。
読書記録Webアプリの骨組みの作成②(データベースを利用)
次に、pythonに標準で添付されているデータベースsqlite3を利用できるように、スクリプトを変更していきます。全体構成は以下の通り。config.py
, models.py
, forms.py
, register_book.html
を追加します。また、__init__.py
, views.py
を修正します
books.py config.py #追加 web/ __init__.py #修正 views.py #修正 models.py #追加 forms.py #追加 templates/ base.html index.html register_book.html #追加
__init__.py
は、以下の通り修正します。前回コメントアウトしていた部分を全て使えるように#を削除します。
# web/__init__.py from flask import Flask from config import Config from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate app = Flask(__name__) app.config.from_object(Config) db = SQLAlchemy(app) migrate = Migrate(app, db) from web import views
config.py
にて、sqlite3データベースの名前や場所等を設定します。同時に、__init__.py
においてfrom config import Config
を追加します。
# config.py import os basedir = os.path.abspath(os.path.dirname(__file__)) class Config(object): SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'web/app.db') SQLALCHEMY_TRACK_MODIFICATIONS = False SECRET_KEY = 'my_secret_key'
SQLALCHEMY_DATABASE_URI
にて、app.dbという名称のsqlite3データベースをwebディレクトリ下に置くことを設定しています。また、SQLALCHEMY_TRACK_MODIFICATIONS
をTrueに設定すると、オブジェクトの変更等がある度にメッセージが出されますので、ここではFalseに設定します。
さらに、ここでは、SECRET_KEY
として任意の文字列を設定しています。flask-wtfでは、CSRF(*)対策が可能ですが、それを利用するために、SECRET_KEY
を設定しておく必要があります。
(*)クロスサイトリクエストフォージェリの略であり、Webアプリケーションの脆弱性を利用したサイバー攻撃の一種のこと。
さて、データベースの設定を行います。SQLAlchemyでは、クラスにおいてテーブル名、カラム名、データ型を定義しておくと、自動的に各データベースに変換するメソッドがあります。今回は、読書記録を保存するBookクラスを以下の通り設定します。ここでは、flask-sqlalchemyを利用していますので、__init__.py
からdb
のみをインポートしておけば足ります。
# web/ from web import db class Book(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(64), index=True) author = db.Column(db.String(64), index=True) genre = db.Column(db.String(64), index=True) date = db.Column(db.Date) def __repr__(self): return '<Book {}>'.format(self.title)
さらに、読書記録を入力するページ(register_book.html
)を作成しますので、入力フォームもクラスとして設定します。wtforms
モジュールから必要なフィールドStringField
, DateField
, SubmitField
をインポートして利用します。wtforms
を利用するとvalidators
により、入力制限をかけることも可能です。ここでは、title
とauthor
は必ず入力が必要とするDataRequired
を入力制限として設定しています。
# web/forms.py from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, DateField from wtforms.validators import DataRequired class BookForm(FlaskForm): title = StringField('書籍', validators=[DataRequired()]) author = StringField('著者', validators=[DataRequired()]) genre = StringField('ジャンル') date = DateField('読了日', format="%Y-%m-%d") submit = SubmitField('登録')
さて、views.py
を修正していきましょう。
@app.route('/register')
以下を追加します。アドレス欄に/register
を入力等すると、register_book
関数(読書記録の登録機能)が呼び出されます。
# web/views.py from web import app, db from web.forms import BookForm from web.models import Book from flask import render_template, redirect, url_for @app.route('/') def index(): books = Book.query.all() return render_template('index.html', books=books) @app.route('/register', methods=['GET','POST']) def register_book(): form = BookForm() if form.validate_on_submit(): book = Book(title=form.title.data, author=form.author.data, genre=form.genre.data, date=form.date.data) db.session.add(book) db.session.commit() return redirect(url_for('index')) return render_template('register_book.html', form=form)
register_book関数においては、まず、BookFormクラスをインスタンス化します。そして、validate_on_submitメソッドにより、POSTリクエストがあるかどうか、また、有効なものかどうかをチェックします。そうであれば、入力されたtitle, author, genre, dateをもとに作成したBookオブジェクトをデータベースにデータとして追加(db.session.add(book)
)します。さらに、コミット(db.session.commit()
)することにより、データベースへの登録が確定します。
そして、url_for('index')
により、index関数を発動するURLを取得し、そのURLにredirect
(遷移・移動)します。POSTリクエストがない、もしくは無効であれば、register_book.html
を表示します。
def index():
以下も、データベースがある前提での記述に修正します。
ここでは、データベースから読書記録の全データを取得して、一覧表示にするだけです。
したがって、前段で記述したリストbooks = [{...},{...},{...}]
をbooks = Book.query.all()
に修正します。
最後に、register_book.html
を以下の通り、作成します。
Bootstrap4の機能・デザインを利用するために、class="..."
を指定しています。また、{{ form.hidden_tag()}}
によりCSRF対策として自動的にトークンを発行します。
# web/templates/register_book.html {% extends "base.html" %} {% block content %} <div class="container"> <form class="form-group" method="POST"> {{ form.hidden_tag() }} <br> <div class="form-group"> {{ form.title.label(class="form-control-label") }} {{ form.title(class="form-control form-control-lg") }} </div> <div class="form-group"> {{ form.author.label(class="form-control-label") }} {{ form.author(class="form-control form-control-lg") }} </div> <div class="form-group"> {{ form.genre.label(class="form-control-label") }} {{ form.genre(class="form-control form-control-lg") }} </div> <div class="form-group"> {{ form.date.label(class="form-control-label") }} {{ form.date(class="form-control form-control-lg", placeholder="2019/5/1") }} </div> <div class="form-group"> {{form.submit(class="btn btn-primary")}} </div> </form> </div> {% endblock %}
なお、base.html
のリンク<a class="nav-link" href="#">読書記録の入力</a>
の#
を{{url_for('register_book')}}
で置き換えておきます。
以上で、データベースへの登録のみ可能な読書記録Webアプリができました。修正、削除機能がないので不十分ですが、取り敢えず、実行してみましょう。
ターミナル画面でexport FLASK_APP=books.py
とexport FLASK_ENV=development
を入力します。これらにより、ターミナル画面でflask run
と入力すると、デバッグモードでbooks.pyが実行できるようになります。
また、以下の一連の操作により、データベースへのテーブルの作成等が簡単に行えるようになります。
flask run
を実行する前に、データベースを作成しておきましょう。
$ flask db init
このコマンドにより、books.pyと同じレベルにmigrationフォルダが作成されます。このフォルダ下には変更履歴等が保存されていきます。
$ flask db migrate -m"first setup"
このコマンドは、データベース移行のスクリプトを作成しますが、データベースはこの時点では変更されません。-mの後の" "の間には任意の文字列を入力します。今回は、データベースの初回セッティングのためfirst setup
とでも書いておきます。
データベースに変更を加えるには、以下のコマンドを実行する必要があります。
$ flask db upgrade
すると、それぞれ以下のような表示が出ます。
$ flask db init Creating directory /Users/tak/Desktop/flask-reading-records-v1/migration s ... done Creating directory /Users/tak/Desktop/flask-reading- records-v1/migrations/versions ... done Generating /Users/tak/Desktop/flask-reading- records-v1/migrations/script.py.mako ... done Generating /Users/tak/Desktop/flask-reading-records-v1/migrations/env.py ... done Generating /Users/tak/Desktop/flask-reading-records-v1/migrations/README ... done Generating /Users/tak/Desktop/flask-reading- records-v1/migrations/alembic.ini ... done records-v1/migrations/alembic.ini ... done Please edit configuration/connection/logging settings in '/Users/tak/Desktop/flask-reading-records-v1/migrations/alembic.ini' before proceeding. $ flask db migrate -m"first setp" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'book' INFO [alembic.autogenerate.compare] Detected added index 'ix_book_author' on '['author']' INFO [alembic.autogenerate.compare] Detected added index 'ix_book_genre' on '['genre']' INFO [alembic.autogenerate.compare] Detected added index 'ix_book_title' on '['title']' Generating /Users/tak/Desktop/flask-reading-records-v1/migrations/versions/91eea8077bdd_first_setp.py ... done $ flask db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade -> 91eea8077bdd, first setp
無事、データベースが初期化できたようです。
それでは、books.py
を実行しましょう。ターミナルからflask run
でも実行できますし、また、python books.py
でも実行できます。
$ flask run * Serving Flask app "books" (lazy loading) * Environment: development * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 282-501-988
http://127.0.0.1:5000/
をブラウザのURL欄にコピペします。
インデックスページが表示されました。まだ、データはありませんので、表題以外は何も表示されません。メニュー画面の"読書記録の入力"をクリックしてみます。
入力画面が表示されました。早速、入力してみましょう。
登録をクリックします。ついでに、もう1件入力してみます。以下の通り、2件とも無事、表示できました。
今回は以上です。入力だけできても、修正もできない、削除もできないとなると困りますよね。次回以降、CRUD操作全体(Create, Read, Update, Delete)を網羅していきたいと思います。
なお、今回作成したスクリプトをご参考までに以下にアップロードしております。flask-reading-records-v0
がデータベースがない版、flask-reading-records-v1
がデータベースあり版です。
GitHub - tak-akashi/flask-reading-records: Web application for reading records using flask