akatak’s blog

プログラム初心者の50代ビジネスマンがセカンドキャリアを目指して働きながらPythonを中心に独学しています。自らの覚え書きや成果物、感じたことなどを脈絡もなく書き連ねるブログです。

統計検定2級に合格しました

先日受験した統計検定2級の結果が、届いてました。無事、統計検定2級に合格していました。ホームページに正解が掲載されていたので、自己採点していましたが、なんとか合格できて良かった。

実は、試験、案外難しかったんですよね。

受験前には2015年以降の過去問をすべてやって臨んだのですが、過去問にない問題を出題するようにしているのか、年々難しくなっている印象を受けました。

過去問は2015〜2017年の問題を先に行い、最後に2018年の問題を解きました。当初7割前後だった正答率は、試験直前に解いた2018年の問題では正答率は8割台となり、大丈夫かなと高を括っていましたが、なんのなんの、初めて見る問題に時間を取られてしまい、思った以上にてこづった次第です。

こちらが合格証です。

f:id:akatak:20190721113602j:plain

それはそうと、ご参考になるか分かりませんが、僕の勉強法は以下の通り(実際には試行錯誤の側面も多々ありましたが、現在、振り返って整理すると以下の通り)。

(1)統計Web/統計学の時間

結局、こちらのホームページに最もお世話になったと思います。

bellcurve.jp

エクセル統計を作っている会社が、社員の方向けに展開していた統計検定2級向けの勉強会資料を公開していただいているようで、統計検定2級の範囲を過不足なく網羅しています。 初心者にも分かりやすく書かれているのに加えて、各単元の最後には基礎を確認するための練習問題があり、これらを繰り返し勉強すると基礎力がつくと思います。

ただし、統計検定2級の過去問をされた方は分かると思いますが、難しい問題も一定程度出ており、それらの対策をどのように行っていくのかが余裕を持って合格できるかどうかのポイントになるのではないでしょうか。

(2)統計検定2級模擬問題集1〜3

これも統計Webを運営している会社が発行しているもので、残念ながらKindle版しかないようですが、過去問以外に何か統計検定2級対策の問題をするとしたらこの位しか見あたりませんでした。内容としては、統計検定2級の傾向を捉えていると思います。問題数は多くありませんが、過去問以外に問題を解きたいという方の選択肢かなと思います。

統計検定2級 模擬問題集1

統計検定2級 模擬問題集1

(3)統計検定2級公式問題集(過去問)

統計検定のホームページにも過去問は掲載されていますが、問題と正解しか掲載されておらず、解説はありません。したがって、分からない問題の解説を読み、理解を促進するためには、この公式問題集が必要になります。

僕は、冒頭に書きましたように、2015〜2018年までの問題を全て解きました。

日本統計学会公式認定 統計検定 2級 公式問題集[2016〜2018年]

日本統計学会公式認定 統計検定 2級 公式問題集[2016〜2018年]

その他、以前の記事にも書きましたが、以下の参考書を使って勉強を行いました。ただし、統計検定2級のためのテキストではないため、検定の範囲を超える内容があったり、選択問題のみの統計検定2級対策としては、そこまで掘り下げなくても良い内容もあったりします。

(4)入門統計学〜検定から多変量解析・実験計画法まで〜

入門の名に相応しく、網羅的に概念的な理解をするのに適した書籍です。数式は最小限に抑えられており、数式を使って理解したい方には向きません。統計の初学者にとっては、記述統計(平均・分散・標準偏差)はもちろん、確率分布(正規分布、標準正規分布、t分布、χ2分布、F分布)から検定・多変量解析・実験計画法まで、更にはノンパラメトリック手法・因子分析・主成分分析も一応網羅しており、一通りの概念を理解するのは良い本かと思います。

入門 統計学 −検定から多変量解析・実験計画法まで−

入門 統計学 −検定から多変量解析・実験計画法まで−

(5)基本統計学第4版

数理的な側面からしっかり説明しているほか、具体例も豊富です。また、標本が少ない場合の検定等でなぜ自由度n-1を使うのかを厳密に説明しています。統計検定2級の範囲を勉強する中で、理論的側面を掘り下げたい場合に、選択的に利用すると良いテキストです。

基本統計学 第4版

基本統計学 第4版

統計検定2級の受験をされる方に参考にしていただけたら幸いです。

FlaskとSQLAlchemyで読書記録Webアプリを作る(7)

これまで機能のアップデートを図ってきた読書記録Webアプリですが、最終化を図りたいと思います。

今回の主要テーマは検索機能の実装ですが、まずは、それ以外のところを改良していきます。

入力項目の追加(オススメ度・コメント)

折角データベースを作成するので、入力項目を充実させましょう。読書をして、その本が良かったのかどうか、「オススメ度」を5段階で入力できるようにします。また、本の感想などを入力できるように「コメント」欄も作成しましょう。

まずは、models.pyとforms.pyの修正です。models.pyにおいては、Bookクラスにrecommendedとcommentを追加します。

# web/models.py
class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(64), index=True)
    genre = db.Column(db.String(64), index=True)
    date = db.Column(db.Date)
    recommended = db.Column(db.Integer)      #追加
    comment = db.Column(db.String(256))      #追加
    author_id = db.Column(db.Integer, db.ForeignKey('author.id'))

    def __repr__(self):
        return '<Book {}>'.format(self.title)

同様に、forms.pyにおいては、recommendedはSelectFieldにて(データベースに保存する値,'表示')の組み合わせをchoicesで指定します。また、commentはTextAreaFieldにて指定しました。

# web/forms.py
class BookForm(FlaskForm):
    title = StringField('書籍', validators=[DataRequired()])
    author = SelectField('著者', coerce=int, validators=[DataRequired()])
    genre = SelectField('ジャンル',choices=[('小説','小説'),('経営','経営'),('歴史','歴史'),('ビジネス','ビジネス'),('宗教哲学','宗教哲学'),('自然科学','自然科学'),('社会科学','社会科学'),('工学','工学'),('芸術','芸術'),('言語','言語'),('趣味','趣味'),('その他','その他')])
    date = DateField('読了日', format="%Y-%m-%d")
    recommended = SelectField('オススメ度', choices=[('5','5: とてもオススメ'),('4','4: ややオススメ'),('3','3: 普通'),('2','2: 余りオススメしない'),('1','1: 全くオススメしない')]) #追加
    comment = TextAreaField('コメント')  #追加
    submit = SubmitField('登録')

これらの変更と併せて、register.html、index.html、each.htmlを修正します。 register.htmlとeach.htmlには、以下の入力を追加します。

  <div class="form-group">
    {{ form.recommended.label(class="form-control-label") }}
    {{ form.recommended(class="form-control form-control-lg") }}
  </div>
  <div class="form-group">
    {{ form.comment.label(class="form-control-label") }}
    {{ form.comment(class="form-control form-control-lg") }}
  </div>

また、index.htmlは、以下の通り、修正します。コメントは長くなる場合がありますので、index.htmlの一覧には表示しないことにしました。

<table class="table">
  <thead class="thead-light">
    <tr>
      <th scope="col">書籍名</th>
      <th scope="col">著者</th>
      <th scope="col">ジャンル</th>
      <th scope="col">読了日</th>        
      <th scope="col">オススメ度</th>     追加
    </tr>
  </thead>

  {% for book in books.items%}
      <tbody>
        <tr>
          <td> <a href="{{ url_for('books.update', id=book.id) }}"> {{ book.title }} </a></td>
          {% for author in authors %}
            {% if author.id == book.author_id %}
              <td> <a href="{{ url_for('authors.update', id=book.author_id)}}"> {{ author.name }}</a></td>
            {% endif %}
          {% endfor %}
          <td> {{ book.genre }} </td>
          <td> {{ book.date }} </td>
          <td> {{ book.recommended }} </td>    追加

        </tr>
      </tbody>
  {% endfor %}
</table>

読了日の入力の簡素化

そうそう「読了日」も改良しましょう。フォーマットが”yyyy-mm-dd”等と予め指定する仕様になっているので、カレンダーから選択できるようにすれば、入力の際にフォーマットも気にせず楽ですよね。wtformsにはそのようなフォーマットが準備されています。具体的には、wtforms.fields.html5からDateFieldをインポートするように変更します。BookFormクラスは変更の必要がありません。

# 修正前
from wtforms import StringField, DateField, SubmitField, SelectField, TextAreaField

# 修正後
from wtforms import StringField, SubmitField, SelectField, TextAreaField
from wtforms.fields.html5 import DateField

検索機能の実装

それでは、今回の主要テーマである検索機能の実装をしていきます。 検索は、ひとつの大きな機能ですので、フォルダ構成を以下の通り変更します。また、Blueprint機能を利用します。

books.py
config.py
web/
    __init__.py                <---  修正
    models.py
    forms.py                   <--- 修正
    books/ 
       views.py
    authors/ 
         views.py
    searches/                  <--- フォルダを新規作成
         views.py              <--- 新規作成
    templates/
               base.html        <--- 修正
               books/ 
                      index.html
                      register.html
                      each.html 
               authors/
                      index.html
                      register.html
                      each.html
               searches/
                      search.html
                      search_result.html

まずは、forms.pyにSearchFormクラスを追加します。

# web/forms.py
class SearchForm(FlaskForm):
    title = StringField('書籍')
    author = SelectField('著者', coerce=int)
    start_date = DateField('検索開始日', format="%Y-%m-%d")
    end_date = DateField('検索終了日', format="%Y-%m-%d")
    submit = SubmitField('検索')

取り敢えず、views.pyを以下の通り作成します(箱を準備します)。

# web/searches/views.py
from web import app, db
from web.forms import SearchForm
from web.models import Book, Author
from flask import render_template, redirect, url_for, request, Blueprint

searches = Blueprint('searches', __name__)

@searches.route('/searches/', methods=['GET','POST'])
def index_search():
    return render_template('searches/search.html', form=form)

そうそう、__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.books.views import books
from web.authors.views import authors
from web.searches.views import searches    # これと
app.register_blueprint(books)
app.register_blueprint(authors)
app.register_blueprint(searches)           # これを追加

ここまでは枠組みです。それでは検索機能を作っていきます。 まずは、検索ページですね。以下の通り作成します。register.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.start_date.label(class="form-control-label") }}
        {{ form.start_date(class="form-control form-control-lg") }}
      </div>
      <div class="form-group">
        {{ form.end_date.label(class="form-control-label") }}
        {{ form.end_date(class="form-control form-control-lg") }}
      </div>
      {% if errors %}
        <p>{{ errors }}</p>
      {% endif %}
      <div class="form-group">
        {{form.submit(class="btn btn-primary")}}
      </div>

    </form>
  </div>
{% endblock %}

メニュー画面に「検索」を追加しておきましょう。

# web/templates/base.html

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav mr-auto">
            <li class="nav-item active">
              <a class="nav-link" href="{{url_for('books.register')}}">読書の記録</a>
            </li>

            <li class="nav-item active">
              <a class="nav-link" href="{{url_for('authors.index')}}">著者一覧</a>
            </li>

            <li class="nav-item active">
              <a class="nav-link" href="{{url_for('authors.register')}}">著者の登録</a>
            </li>

            <li class="nav-item active">
              <a class="nav-link" href="{{url_for('searches.index_search')}}">検索</a>
            </li>

          </ul>
        </div>

検索結果表示画面(search_results.html)は、基本的にはindex.htmlと一緒です。 ここまでできたところで、views.pyを作り込んでいきましょう。まず、全体像です。

# web/searches/views.py

@searches.route('/searches/', methods=['GET','POST'])
def index_search():

    registered_authors = db.session.query(Author).order_by('name')
    authors_list = [(0,"")]
    for i in registered_authors:
        authors_list.append([i.id, i.name])

    form = SearchForm()
    form.author.choices = authors_list

    if form.start_date.data is None:
        form.start_date.data = datetime.date(datetime.datetime.today().year,1,1)
    if form.end_date.data is None:
        form.end_date.data = datetime.datetime.today()

    if form.validate_on_submit():

        if form.author.data != 0:
            books = Book.query.filter(Book.title.like('%' + form.title.data + '%')).filter(Book.author_id==form.author.data).filter(Book.date>=form.start_date.data).filter(Book.date<=form.end_date.data)
        else:
            books = Book.query.filter(Book.title.like('%' + form.title.data + '%')).filter(Book.date>=form.start_date.data).filter(Book.date<=form.end_date.data)

        books = books.order_by(Book.date.desc()).paginate(page=1, per_page=app.config['ITEMS_PER_PAGE'], error_out=False)
        authors = db.session.query(Author).join(Book, Book.author_id == Author.id).all()

        session['title'] = form.title.data
        session['author'] = form.author.data
        session['start_date'] = form.start_date.data.strftime('%Y-%m-%d')
        session['end_date'] = form.end_date.data.strftime('%Y-%m-%d')

        return render_template('searches/search_results.html', books=books, authors=authors)
    return render_template('searches/search.html', form=form)

上から具体的に説明していきます。

registered_authors = db.session.query(Author).order_by('name')
    authors_list = [(0,"")]
    for i in registered_authors:
        authors_list.append([i.id, i.name])

検索において、著者を選ぶリストを作成する部分です。読書記録を登録する画面では authors_list = [(i.id, i.name) for i in registered_authors]の一行でしたが、検索においては author_list =以下3行になっています。検索においては、リストから誰も選ばないという選択肢を作るためです。デフォルトでの表示は(0,"")となります。

if form.start_date.data is None:
    form.start_date.data = datetime.date(datetime.datetime.today().year,1,1)
if form.end_date.data is None:
    form.end_date.data = datetime.datetime.today()

これは検索画面を開いた際に、検索開始日と検索終了日のデフォルト値を作成するものです。これがないと、検索時に必ず、検索開始日と検索終了日を入力しないといけなくなるため手間です。それを避けるためのものです。デフォルト値は、検索開始日が当年の1月1日、検索終了日が当日としています。

 if form.author.data != 0:
        books = Book.query.filter(Book.title.like('%' + form.title.data + '%')).filter(Book.author_id==form.author.data).filter(Book.date>=form.start_date.data).filter(Book.date<=form.end_date.data)
 else:
        books = Book.query.filter(Book.title.like('%' + form.title.data + '%')).filter(Book.date>=form.start_date.data).filter(Book.date<=form.end_date.data)

これは、検索画面において、著者が選択されていない、すなわち、form.author.dataが0の場合と、著者が選択されている場合の処理を分けるようにしています。book.author_id = 0は存在しないため、同じ処理を行うと検索結果が現れなくなってしまうためです。

 session['title'] = form.title.data
 session['author'] = form.author.data
 session['start_date'] = form.start_date.data.strftime('%Y-%m-%d')
 session['end_date'] = form.end_date.data.strftime('%Y-%m-%d')

検索結果もページネーション機能を利用します。異なるページに移る際にも、検索条件を保持できるようにFlaskのSessionを利用しています。これにより検索条件はSessionを通じて、サーバー上に暗号化されて保存されます(config.pyにてSECRET_KEYの設定が必要)。なお、wtformsのDateFieldはdatetime形式で日付が保存されるため、Sessionに保存する際には、一旦、テキスト形式に変換しています。

検索結果の2ページ以降を表示する箇所を作成します。 先ほど、Sessionを通じて保存した検索条件を取り出して、formに格納します。その際に日付についてはテキスト形式からdatetime形式に変換しておきます。

それ以降の検索については、index_search()と同様です。

 @searches.route('/searches/<int:page_num>', methods=['GET','POST'])
def search_results(page_num):

    form = SearchForm()

    form.title.data = session.get('title')
    form.author.data = session.get('author')
    form.start_date.data = datetime.datetime.strptime(session.get('start_date'),'%Y-%m-%d')
    form.end_date.data = datetime.datetime.strptime(session.get('end_date'),'%Y-%m-%d')

    if form.author.data != 0:
        books = Book.query.filter(Book.title.like('%' + form.title.data + '%')).filter(Book.author_id==form.author.data).filter(Book.date>=form.start_date.data).filter(Book.date<=form.end_date.data)
    else:
        books = Book.query.filter(Book.title.like('%' + form.title.data + '%')).filter(Book.date>=form.start_date.data).filter(Book.date<=form.end_date.data)
    books = books.order_by(Book.date.desc()).paginate(page=page_num, per_page=app.config['ITEMS_PER_PAGE'], error_out=False)
    authors = db.session.query(Author).join(Book, Book.author_id == Author.id).all()

    return render_template('searches/search_results.html', books=books, authors=authors)

以上です。画面を見てみましょう。 まずは、検索画面。

f:id:akatak:20190720160511p:plain

著者を選択せずに、検索。

f:id:akatak:20190720162859p:plain

どうやらワークしていますね。テストのため1ページ当たりの件数を1件に設定していますので、ちょっと変ですが。 これで一旦、読書記録Webアプリは完成しました。

読書記録Webアプリで個人で利用することを想定していますので、ログイン機能は実装していません。 これについては、今回のシリーズとは別の機会にご紹介できればと思います。

今回もスクリプトを以下にアップロードしております。ご参考まで。

github.com

FlaskとSQLAlchemyで読書記録Webアプリを作る(6)

こんにちは。早速ですが、読書記録Webアプリの機能アップを更に図っていきましょう。

今回の主要テーマはページネーション(pagination)です。日本語ではページ割りというのかもしれません。アプリが現在のままだとページの概念がないので、読書記録を登録していくと、どんどん行が下に伸びていってしまいます。

それでは困りますよね。一定の行数を超えたら次のページに移動しないと表示されないようにする必要があります。このような時に利用するのがPagination機能で、実はFlask-SQLAlchemyには、予めこのPagination機能が備わっています。

Pagination機能を利用する

具体的には、Queryオブジェクトのpaginateメソッドを利用します。 例えば、Bookクラスの全てのデータをデータベースから取り出すには

books = Book.query.all()

と記述しました。pagination機能を使うには

books = Book.query.paginate(page=1, per_page=10, error_out=False).items

のように、all()paginate().itemsで置き換えます。

paginateメソッドには、上記のように引数を3つ渡します。

page=はページ番号です。1ページから始まります。per_page=は1ページあたりの表示件数です。error_out=は、Trueに設定すると、データのあるページ数を超えてページを設定するとエラーを返します。Falseに設定しておくと、データのあるページ数を超えてページを指定すると、エラーではなく、空のリストが返されます。

paginateメソッドにより、paginationオブジェクトが返されます。上記のitemsはpaginationオブジェクトの属性の一つで、リクエストしたページに属するデータのリストが入っています。上記の場合、1ページ目に属する10件のデータリストとなります。

その他にも使える属性があり、以下のFlask-SQLAlchemyのドキュメントに記載されています(英語ですが)。 https://flask-sqlalchemy.palletsprojects.com/en/2.x/api/

さて、実際にスクリプトを変更していきましょう。 まず、1ページ当たりの表示件数を、使い回しができるように、configクラスで設定しておきましょう。

class Config(object):
    # ...
    ITEMS_PER_PAGE = 3

1ページ当たりの表示件数を3件と少なくしているのは、あまりたくさんデータを入力しなくても、ページネーション機能の確認を容易にするためです。

次に、views.pyを修正していきます。

先ほどは、books = Book.query.paginate(page=1, per_page=10, error_out=False).itemsとしましたが、これだと読書リストをhtmlに渡すだけになりますので、ここでは、Book.query.paginate(page=1, per_page=10, error_out=False)までのPaginationオブジェクトをhtmlに渡すようにします。 そうすることにより、Paginationオブジェクトを渡されたhtml側でPaginationオブジェクトの属性を利用することで、各種処理をできるようになります。

@books.route('/', methods=['GET','POST'])
def index():

    books = Book.query.order_by(Book.date.desc()).paginate(page=1, per_page=app.config['ITEMS_PER_PAGE'], error_out=False)
    authors = db.session.query(Author).join(Book, Book.author_id == Author.id).all()
    return render_template('books/index.html', books=books, authors=authors)

変更前は、books = Book.query.all()で得られた読書リストが渡されていましたので、{% for book in books %}...{% endfor %}でしたが、今回はPaginationオブジェクトが渡されていますので、{% for book in books.items %}...{% endfor %}となっています。

ここまでは1ページ目ですので、2ページ目以降に対応する関するを作成します。1ページ目のページネーションにより表示されたページに飛べるようにします。ページ数がクリックされますので、そのページをpage_numとして関数に引き渡します。

@books.route('/books/pages/<int:page_num>', methods=['GET','POST'])
def index_pages(page_num):

    books = Book.query.order_by(Book.date.desc()).paginate(page=page_num, per_page=app.config['ITEMS_PER_PAGE'], error_out=False)
    authors = db.session.query(Author).join(Book, Book.author_id == Author.id).all()
    return render_template('books/index.html', books=books, authors=authors)

表示する側の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>
      <th scope="col">オススメ度</th>
    </tr>
  </thead>

  {% for book in books.items %}
      <tbody>
        <tr>
          <td> <a href="{{ url_for('books.each_book', id=book.id) }}"> {{ book.title }} </a></td>
          {% for author in authors %}
            {% if author.id == book.author_id %}
              <td> <a href="{{ url_for('authors.each_author', id=book.author_id)}}"> {{ author.name }}</a></td>
            {% endif %}
          {% endfor %}
          <td>{{ book.genre }}</td>
          <td> {{ book.date }}</td>
          <td> {{ book.recommended }} </td>
        </tr>
      </tbody>
  {% endfor %}
</table>
<p align="right">合計 {{ books.total }} 冊 </p>

<nav aria-label="Page navigation example">
  <ul class="pagination justify-content-center">
     {% for page in books.iter_pages() %}
      {% if page %}
        {% if page != books.page %}
            <li class="page-item"><a class="page-link" href="{{ url_for('books.index_pages', page_num=page) }}">{{ page }}</a></li>
        {% else %}
            <li class="page-item active"><a class="page-link">{{ page }}</a></li>
        {% endif %}
      {% else %}
        <span> ... </span>
      {% endif %}
    {% endfor %}
  </ul>
</nav>

そして、Paginationの部分は、{% for page in books.iter_pages() %}...{% endfor %}と、Paginationオブジェクト"books"のイテレータiter_pages()を利用しています。ただし、ページが増えてくると、ページ数を返さない場合がありますので、{% if page %}{% else %}{% endif %}にて...を表示するようにしています。

また、その上の行で<p align="right">合計 {{ books.total }} 冊 </p>とあり、Paginationオブジェクトの属性total(全体件数)を利用しています。

views.pyから渡すものをPaginationオブジェクトにすると、html側でいろいろと利用でき、便利ですね。

さて、同様に著者についてもページネーションを適用します。authors/views.pyを以下の通りに変更します。

@authors.route('/authors/index')
def index_author():
    authors = Author.query.order_by(Author.name).paginate(page=1, per_page=app.config['AUTHORS_PER_PAGE'], error_out=False)
    return render_template('authors/index_author.html', authors=authors)

@authors.route('/authors/index/<int:page_num>', methods=['GET','POST'])
def index_author_pages(page_num):
    authors = Author.query.order_by(Author.name).paginate(page=page_num, per_page=app.config['AUTHORS_PER_PAGE'], error_out=False)
    return render_template('authors/index_author.html', authors=authors)

templates/authors/index.htmlも、同様に以下の通り変更します。

{% extends "base.html" %}
{% block content %}
 <br>
 <h4>著者一覧</h4>

 <br>
 <table class="table">
   <thead class="thead-light">
     <tr>
       <th scope="col">著者</th>
       <th scope="col">説明</th>
     </tr>
   </thead>

  {% for author in authors.items %}
    <tbody>
      <tr>
        <td> <a href="{{ url_for('authors.each_author', id=author.id)}}">{{ author.name }}</a> </td>
        <td>{{ author.extras }}</td>
      </tr>
    </tbody>
  {% endfor %}
  </table>

<nav aria-label="Page navigation example">
  <ul class="pagination justify-content-center">
    {% for page in authors.iter_pages(left_edge=2, left_current=2, right_current=5, right_edge=2) %}
      {% if page %}
        {% if page != authors.page %}
            <li class="page-item"><a class="page-link" href="{{ url_for('authors.index_author_pages', page_num=page) }}">{{ page }}</a></li>
        {% else %}
            <li class="page-item active"><a class="page-link">{{ page }}</a></li>
        {% endif %}
      {% else %}
        <span> ... </span>
      {% endif %}
    {% endfor %}
  </ul>
</nav>

{% endblock %}

読書記録を表示させるとこんな感じ。

f:id:akatak:20190718224547p:plain

うまく表示できましたでしょうか。PaginationでもBootstrapを利用し、見栄えを良くしています。

修正時のボタン表示の変更

読書記録や著者の登録内容を変更する際に表示されるボタンが「登録」「削除」ではやはり違和感がありますよね。内容を修正する際のボタンは「修正」とするのが良いかなと思います。

forms.pyに以下を追加します。BookFormとAuthorFormをもとにBookUpdateFormとAuthorUpdateFormを作成します。

class BookUpdateForm(BookForm):
    submit = SubmitField('修正')

class AuthorUpdateForm(AuthorForm):
    submit = SubmitField('修正')

そして、views.pyのupdate関数のform=BookForm()とform=AuthorForm()をBookUpdateForm()とAuthorUpdateForm()に修正します。

本日のアップデートは以上です。 次回は読書記録として入力したい「おすすめ度」「コメント」欄を追加するとともに「検索機能」を導入することで、アプリの最終化を図りたいと思います。

ここまでのアプリもアップロードしておきます。ご参考まで。

github.com

FlaskとSQLAlchemyで読書記録Webアプリを作る(5)

さて、前回までに作成しましたアプリの機能アップを図っていきましょう。

まず、著者をデータベースとして登録し、読書記録データの登録時に著者リストから選択できるようにします。

その場合、著者についても、読書記録と同様にCRUD機能を網羅しておいた方が良さそうです。

ところが、読書記録と同じように、同じフォルダに著者のCRUD用ファイルを作っていくと、似たようなファイルが量産され、混乱することが容易に想像できます。その際に利用すると便利なのが、Flaskに予め用意されているBlueprint機能です。

Blueprint機能を導入する

Blueprintを利用すると、アプリケーションのview.pyを機能毎に分けて管理ができるようになります。例えば、今回のように、読書記録(Book)を登録・閲覧・修正・削除する機能と、著者を登録・閲覧・修正・削除する機能を別々に構成するので、アプリケーションを構造化できるようになります。

まずは、既に作成した読書記録の一連のCRUD処理を、Blueprint機能を使って、構造化してみましょう。

今後のことを考えて、フォルダ構成を以下の通りとします。新たにbooksフォルダをwebフォルダとtemplatesフォルダ下に作成し、views.pyとindex.html, register_book.html, each_book.htmlをそれぞれ移動させます。register_book.htmlとeach_book.htmlの名称はそのままでももちろん良いのですが、折角booksフォルダ下におき、booksのregister.html等、関係が明確なので、簡素化のために名称変更しました。

books.py
config.py
web/
    __init__.py
    models.py
    forms.py
    books/            <---  フォルダを作成
      views.py      <--- ここに移動
    templates/
      base.html  
      books/          <--- フォルダを作成  
          index.html       <--- ここに移動  
          register.html   <--- 名前を変更  
          each.html  <--- 名前を変更

さて、view.pyを以下の通り修正します。 Blueprintをインポートし、books = Blueprint('books', __name__)を一行挿入します。これと対になるように、__init__.pyにもfrom Web.books.views import booksおよびapp.register_blueprint(books)の2行を挿入します。

from flask import Blueprint    # 追加

books = Blueprint('books', __name__)     # 追加

@books.route('/')
def index():
    books = Book.query.order_by(Book.date.desc()).all()
    return render_template('books/index.html', books=books)

@books.route('/books/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('books.index'))
    return render_template('books/register.html', form=form)

@books.route('/books/<int:id>/update', methods=['GET','POST'])
def update_book(id):
    book = Book.query.get(id)

    form = BookForm()

    if form.validate_on_submit():
        book.title = form.title.data
        book.author = form.author.data
        book.genre = form.genre.data
        book.date = form.date.data
        db.session.commit()

        return redirect(url_for('books.index'))

    elif request.method == 'GET':
        form.title.data = book.title
        form.author.data = book.author
        form.genre.data = book.genre
        form.date.data = book.date

    return render_template('books/each.html', form=form, id=id)

@books.route('/books/<int:id>/delete', methods=['GET','POST'])
def delete_book(id):
    book = Book.query.get(id)
    db.session.delete(book)
    db.session.commit()
    return redirect(url_for('books.index'))

また、@app.route('/register')@books.route('/books/register')等と修正するほか、url_for('index')url_for('books.index')等と変更します。

# 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.books.views import books  #追加
app.register_blueprint(books)             #追加

著者(Authorクラス)の導入とリレーションの設定

models.pyを以下の通り修正します。Bookクラスでは、author(文字列)ではなく、author_id(整数)のみ保有し、Authorクラスとのリレーションを構築します。Bookクラスには、Foreign Keyとしてauthor.idを以下の通り登録します。

author_id = db.Columns(db.Integer, db.ForeignKey('author.id')) 

また、Authorクラスには、以下の通り、登録します。

books = db.relationship('Book', backref='writer', lazy='dynamic')

backrefがないと、双方向のリレーションがうまく構築できないようです。また、backref='author'にすると、flask-Migrateを利用したデータベース構築がうまくいかないため、backref='writer'にしております。lazyパラメータは、リレーションが構築されたデータを取得する方法を設定するものです。dynamicはクエリを都度行うような場合に設定しておくと良いようです。

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)
    author_id = db.Columns(db.Integer, db.ForeignKey('author.id')) # 追加

    def __repr__(self):
        return '<Book {}>'.format(self.title)

# Authorクラスとして以下を追加
class Author(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), index=True,unique=True)
    extras = db.Column(db.String(64))
    books = db.relationship('Book', backref='writer', lazy='dynamic')

    def __repr__(self):
        return '<Author {}>'.format(self.name)

forms.pyに以下を追加します。

class AuthorForm(FlaskForm):
    name = StringField('著者', validators=[DataRequired()])
    extras = StringField('説明')
    submit = SubmitField('登録')

著者(Aurhor)のCRUD操作

さて、AuthorについてもCRUD操作を行えるようにするため、フォルダ構成を以下のようにします。

books.py
config.py
web/
    __init__.py                    <---  修正
    models.py
    forms.py
    books/ 
      views.py
    authors/                      <--- フォルダを作成
               views.py          <---  books/views.pyをコピペ・修正
    templates/
               base.html        <--- 修正
               books/ 
                         index.html
                         register.html
                         each.html 
              authors/                      <--- フォルダを作成
                        index.html        <---  books/index.htmlをコピペ・修正
                        register.html    <---  books/register.htmlをコピペ・修正
                        each.html         <---  books/register.htmlをコピペ・修正

まず、コピペしたauthors/views.pyを修正します。booksauthorsで置き換えていくイメージです。これにもBlueprint機能を適用しますので、authors/views.pyには以下の通り記述。

authors = Blueprint('authors', __name__)

また、init.pyには以下を追加します。

from web.authors.views import authors
app.register_blueprint(authors)

authors/views.pyは以下の通り。

# web/authors/views.py
from web import db
from web.forms import AuthorForm
from web.models import Book, Author
from flask import render_template, redirect, url_for, request
from flask import Blueprint

authors = Blueprint('authors', __name__)

@authors.route('/authors', methods=['GET'])
def index():
    authors = Author.query.order_by(Author.name).all()
    return render_template('/authors/index.html', authors=authors)

@authors.route('/authors/register', methods=['GET','POST'])
def register():
    form = AuthorForm()

    if form.validate_on_submit():
        author = Author(name=form.name.data, extras=form.extras.data)
        db.session.add(author)
        db.session.commit()
        return redirect(url_for('authors.index'))
    return render_template('authors/register.html', form=form)

@authors.route('/authors/<int:id>/update', methods=['GET','POST'])
def update(id):
    author = Author.query.get(id)

    form = AuthorForm()

    if form.validate_on_submit():
        author.name = form.name.data
        author.extras = form.extras.data
        db.session.commit()

        return redirect(url_for('authors.index'))

    elif request.method == 'GET':
        form.name.data = author.name
        form.extras.data = author.extras

    return render_template('authors/each.html', form=form, id=id)

@authors.route('/authors/<int:id>/delete', methods=['GET','POST'])
def delete(id):
    author = Author.query.get(id)
    db.session.delete(author)
    db.session.commit()
    return redirect(url_for('authors.index'))

これに合わせて、`base.html'も修正します。具体的にはメニューバーに「著者一覧」「著者登録」も追加します。

  <nav class="navbar navbar-expand-lg navbar-dark bg-dark">

        <a class="navbar-brand" href="{{ url_for('books.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="{{url_for('books.register')}}">読書の記録</a>
            </li>

            <li class="nav-item active">
              <a class="nav-link" href="{{url_for('authors.index')}}">著者一覧</a>
            </li>
            <li class="nav-item active">
              <a class="nav-link" href="{{url_for('authors.register')}}">著者の登録</a>
            </li>
          </ul>
        </div>
      </nav>

ジャンルをリストから選択できるようにする(wtformsのselectfieldを使う)

wtformsモジュールには、フォームで使うfieldが色々準備されています。これまで使ったStringField、DateFieldのほか、BookeanField、FloatField、IntegerField、PasswordField、RadioField、TextAreaField、TextField等です。その中に、SelectFieldがあり、リストの中から一つ選択する場合に利用します。

具体的には、SelectField('表題', choices=[(value1,label1),(value2,label2),(value3,label3)])と書きます。choicesとして、(value,label)の組合せをリストとして指定します。labelがリストに表示される項目、valueがデータベースに保存される値となります。

分類の仕方は好き好きかと思いますが、図書館の分類は参考になるでしょう。 http://www.libnet.pref.okayama.jp/shiryou/ndc/index.htm
これだと細かすぎるので、ご自身が読まれる本の分類のみピックアップすれば良いのではないかと思います。後は、好みですよね。「小説」と「歴史小説」を分けたい方もいらっしゃるかもしれません。

私は、取り敢えず、以下の通り設定しました。

from flask_wtf import FlaskForm
from wtforms import StringField, DateField, SubmitField, SelectField
from wtforms.validators import DataRequired

class BookForm(FlaskForm):
    title = StringField('書籍', validators=[DataRequired()])
    author = StringField('著者', validators=[DataRequired()])
    genre = SelectField('ジャンル',choices=[('小説','小説'),('経営','経営'),('歴史','歴史'),('ビジネス','ビジネス'),('宗教哲学','宗教哲学'),('自然科学','自然科学'),('社会科学','社会科学'),('工学','工学'),('芸術','芸術'),('言語','言語'),('趣味','趣味'),('その他','その他')])
    date = DateField('読了日', format="%Y-%m-%d")
    submit = SubmitField('登録')

以下の通り、ジャンルにおいて、デフォルトでは最初のリスト、ここでは「小説」が表示されています。

f:id:akatak:20190714224713p:plain

「小説」をクリックすると、以下の通り、choicesで設定したリストのうちlabelが表示されて、選択できるようになりました。

f:id:akatak:20190714224438p:plain

著者リストを別メニューにて作成し、選択できるようにする(selectfieldを動的に設定する)

さて、最初からある程度分類が決まっているようなものについては、上記のように設定すれば良いでしょう。ところが、著者の場合には、次々登録されていきます。 これに対応するために、著者として登録すると、当該著者がリストに追加され、読書記録データを作成する場合に、そのリストから著者を選べるようにする必要があります。

実はwtformsには、このように動的選択(dynamic choice)ができる仕組みが準備されています。

具体的には、まず、forms.pyのBookFormクラスを以下の通り修正します。

    # author = StringField('著者', validators=[DataRequired()])  # 修正前
    author = SelectField('著者', coerce=int, validators=[DataRequired()])  # 修正後

そして、books/views.pyを以下の通り修正します。

@books.route('/', methods=['GET'])
def index():
    books = Book.query.order_by(Book.date.desc()).all()
    authors = db.session.query(Author).join(Book, Book.author_id == Author.id).all()    #追加
    return render_template('/books/index.html', books=books, authors=authors)

@books.route('/books/register', methods=['GET','POST'])
def register():

    registered_authors = db.session.query(Author).order_by('name')  # 追加
    authors_list = [(i.id, i.name) for i in registered_authors]  # 追加

    form = BookForm()
    form.author.choices = authors_list  # 追加

    if form.validate_on_submit():
        # book = Book(title=form.title.data, author=form.author.data, genre=form.genre.data, date=form.date.data)  # 修正前
        book = Book(title=form.title.data,genre=form.genre.data, date=form.date.data, author_id=form.author.data)  # 修正後
        db.session.add(book)
        db.session.commit()
        return redirect(url_for('books.index'))
    return render_template('books/register.html', form=form)

@books.route('/books/<int:id>/update', methods=['GET','POST'])
def update(id):
    book = Book.query.get(id)

    registered_authors = db.session.query(Author).order_by('name') #追加
    authors_list = [(i.id, i.name) for i in registered_authors]  # 追加

    form = BookForm()
    form.author.choices = authors_list  # 追加

    if form.validate_on_submit():

        author = db.session.query(Author).filter(Author.id == form.author.data).first()   # 追加
        book.title = form.title.data
        book.genre = form.genre.data
        book.author_id = form.author.data  #  追加
        book.author = author.name  # 修正
        book.date = form.date.data
        db.session.commit()

        return redirect(url_for('books.index'))

    elif request.method == 'GET':

        form.title.data = book.title
        form.author.data = book.author_id   # 修正
        form.genre.data = book.genre
        form.date.data = book.date

    return render_template('books/each.html', form=form, id=id)

なお、templates/books/index.htmlも以下の通り修正します。

{% for book in books%}
      <tbody>
        <tr>
          <td> <a href="{{ url_for('books.update', id=book.id) }}"> {{ book.title }} </a></td>
          {% for author in authors %}
            {% if author.id == book.author_id %}
              <td> <a href="{{ url_for('authors.update', id=book.author_id)}}"> {{ author.name }}</a></td>
            {% endif %}
          {% endfor %}
          <td> {{ book.genre }} </td>
          <td> {{ book.date }} </td>
        </tr>
      </tbody>
  {% endfor %}

以下の通り、データベースに登録した著者名をリストから利用できるようになりました。

f:id:akatak:20190715135517p:plain

著者の二重登録エラーを回避する

models.pyにおいて、Authorクラスのname(著者名)はunique=Trueと設定していますので、二重登録をしようとするとエラーが排出されます。同じ本は2回読むこともあろうかと思いますので、models.pyにおいてunique=Trueには設定していません。

著者の二重登録のみ回避する仕組みを導入します。

authors/register.html と authors/update.htmlのif form.validate_on_submit()の箇所に以下を挿入します。

    if form.validate_on_submit():
        check= Author.query.filter(Author.name == form.name.data).first()
        if check:
            errors = '既にこの著者は登録されています。他の著者名を登録してください。'
            return render_template('authors/register.html', form=form, errors=errors)

以下の通り、二重登録しようとするとエラーメッセージが出ました。

f:id:akatak:20190715124526p:plain

当該著者の読書記録がある状況での著者の削除を回避する

現状ですと、読書記録に登録した著者を削除できてしまいます。そのような操作ができないようにviews.pyを修正します。

@authors.route('/authors/<int:id>/delete', methods=['GET','POST'])
def delete(id):
    author = Author.query.get(id)

#  ここから追加します。
    check = Book.query.filter(Book.author_id == id).first()
    if check:
        errors = "この著者による本を先に削除してください。"
        form = AuthorForm()
        form.name.data = author.name
        form.extras.data = author.extras
        return render_template('authors/each.html', form=form, id=id, errors=errors)
#   追加はここまで。
    db.session.delete(author)
    db.session.commit()
    return redirect(url_for('authors.index'))

これもきちんとエラーメッセージが出るようになりました。

f:id:akatak:20190715124701p:plain

削除の際にポップアップ画面を追加する

BootstrapにMordalというコンポーネントがあり、このポップアップに該当します。books/each.htmlとauthors/each.htmlの「削除」ボタンを押したらポップアップが表示されるように、以下の通り、修正します。Bootstrapのページからコピペして修正することで簡単に対応できます。

    <div class="form-group">
        {{form.submit(class="btn btn-primary")}}
        <button type="button" class="btn btn-danger" data-toggle="modal" data-target="#delete_modal">
          削除
        </button>

        <!-- Modal -->
        <div class="modal fade" id="delete_modal" tabindex="-1" role="dialog" aria-labelledby="ModalLabel" aria-hidden="true">
          <div class="modal-dialog" role="document">
            <div class="modal-content">
              <div class="modal-header">
                <h5 class="modal-title" id="ModalLabel">削除の再確認</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                  <span aria-hidden="true">&times;</span>
                </button>
              </div>
              <div class="modal-body">
                本当に削除してもいいですか?
              </div>
              <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
                <a class="btn btn-danger" href="{{ url_for('authors.delete', id=id)}}">削除する</a>
              </div>
            </div>
          </div>
        </div>

    </div>

f:id:akatak:20190715124819p:plain

無事ポップアップ画面が出ました。「閉じる」を押すと、画面がクローズ。「削除する」を押すと、きちんとデータが削除できました。

今回は、かなりいろいろな機能を説明しました。参考までに、以下にスクリプトをアップしておきました。

flask-reading-records/flask-reading-records-v3 at master · tak-akashi/flask-reading-records · GitHub

次回は、ページネーション等を説明したいと思います。

本日はこれにて失礼します。

FlaskとSQLAlchemyで読書記録Webアプリを作る(4)

前回に続いて、読書記録Webアプリを作って行きましょう。

前回は読書記録データの作成、読み出しを行いましたので、今回は読書記録データの更新、削除とCRUD操作を網羅したいと思います。CRUDとは、Create(データの新規作成)、Read(データの読み込み)、Update(データの更新)、Delete(データの削除)のことです。これらを網羅すれば一通りの機能を作成できます。

Create(読書記録データの作成機能)

前回作成したものですが、念のため、再度記載します。

# 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('/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)

インポートしたBookFormクラスをインスタンス化します。validate_on_submitメソッドにより、POSTリクエスト(register_book.htmlへの入力)があり、かつ、その内容が有効かどうかをチェックします。

そうであれば、入力されたtitle, author, genre, dateをもとにBookオブジェクトを生成し、それをdb.session.add(book)により、データベースにデータとして追加します。さらに、db.session.commit()により、データベースへの登録を完了します。

そして、url_for('index')により、index関数を発動するURLを取得し、そのURLにredirect(遷移・移動)します。POSTリクエストがない、もしくは無効であれば、register_book.htmlを表示します。

register_book.htmlは以下の通りです。

# 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 %}

Read(読書記録データの一覧機能)

データ作成機能により作成した読書記録を一覧表示させる機能です。 これも前回記載しましたが、そのまま再掲します。

# 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)

しかし、books = Book.query.all()だと、登録順に表示されてしまいます。一番最近に読んだ本が最上位に来るように修正しましょう。以下のようにorder_by(Book.date.desc())を入れればOKです。desc()はdescening、即ち「降順」を表しています。「昇順」にしたい場合にはasc()にします。

books = Book.query.order_by(Book.date.desc()).all()
@app.route('/')
def index():
    books = Book.query.order_by(Book.date.desc()).all()
    return render_template('index.html', books=books)

render_templatetemplatesフォルダ下にあるindex.htmlを表示します。その際にbooksを一緒にindex.htmlに渡します。booksは、Bookオブジェクトのリストとなっています。

index.htmlは以下の通りです。booksリストから一つ一つBookオブジェクトを取り出して、そのtitleやauthorといったプロパティを表示させます。

<!-- 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 %}

Update(読書記録データの修正機能)

続いて、データを修正できるようにしましょう。 indexページの読書記録一覧において、書籍をクリックすると、読書記録の登録ページに飛ぶようにします。そこではクリックした書籍データが表示されていて、そのデータを修正して登録すると、修正できるようにします。

index.htmlの書籍名が表示されている箇所<td> {{ book.title }} </td>{{ book.title }}<a href="#"> </a>で挟みます。また、#の箇所には{{url_for('update_book',id=book.id)}}を入力します。

<td> <a href="{{url_for('update_book',id=book.id)}}">{{ book.title }} </a></td>

さて、view.pyを修正しましょう。

まず、requestを追加でインポートします。また、view.pyの最後に、以下の@app.route('/update/<int:id>')以下を追加します。

# web/views.py

from flask import render_template, redirect, url_for, request

@app.route('/<int:id>/update', methods=['GET','POST'])
def update_book(id):
    book = Book.query.get(id)

    form = BookForm()

    if form.validate_on_submit():
        book.title = form.title.data
        book.author = form.author.data
        book.genre = form.genre.data
        book.date = form.date.data
        db.session.commit()

        return redirect(url_for('index'))

    elif request.method == 'GET':
        form.title.data = book.title
        form.author.data = book.author
        form.genre.data = book.genre
        form.date.data = book.date

    return render_template('each_book.html', form=form, id=id)

register_book.htmlをコピーして、`each_book.html'を作っておきます。現時点では中身は全く同じで結構です(後ほど、削除機能の作成の際に修正します)。

ここで、python books.pyもしくは環境変数を設定しておいて、flask runにて実行してみます。

f:id:akatak:20190713165115p:plain

書籍名が青くなっており、リンクが貼られました。前回とは読書記録データが多少ことなっておりますが、ご容赦ください。

一つ目の書籍名をクリックしてみます。

f:id:akatak:20190713165344p:plain

読書記録データが表示されました。書籍名に「よん」を加えて、登録ボタンを押してみます。

f:id:akatak:20190713165518p:plain

f:id:akatak:20190713165554p:plain

きちんと修正ができました。ここで、具体的に見ていきましょう。

ます、indexページにて、書籍名をクリックすると、update_book関数が呼び出されます。その際に idとして読書記録データのbook.idも渡されます。

なお、@app.route内は、/<id>/updateでも問題ありません。通常、必ず整数を受け取るように制限したい場合が多く、その場合、<int:id>とします。そうすると、整数以外の値が渡された際にはエラーになります。

さて、book = Book.query.get(id)により、データベースから、当該idに紐ついているデータが検索され、bookに渡されます。一方で、登録の際と同様に、BookFormをインスタンス化しておきます。

書籍名をクリックした際は、情報を取得するだけですのでrequest.methodはGETになります。従って、elif以下となり、bookが保持しているデータを項目毎にformに渡しています。その上で、register_book.htmlを表示していますので、登録する際と同じ画面を利用していますが、データが表示されています。

そこで、データの一部を変更して、「登録」ボタン(Submit)を押下すると、今度は同じ関数のif以下が適用され、formに保存されている修正後のデータが、該当するbookのプロパティに保存され、commitすることで修正が確定します。

Delete(読書記録データの削除機能)

最後に、データを削除する機能です。 これは、削除したいデータをBook.query.get(id)にて呼び出して、当該データをdb.session.deleteにて削除し、最終db.session.commit()で確定させます。そして、indexページに戻ります。以下がスクリプトです。

@books.route('/book/<int:id>/delete', methods=['GET','POST'])
def delete_book(id):
    book = Book.query.get(id)
    db.session.delete(book)
    db.session.commit()
    return redirect(url_for('index'))

そして、each_book.htmlを修正します。以下の通り、<a>...</a>タグを追加します。

      <div class="form-group">
        {{form.submit(class="btn btn-primary")}}
        <a class="btn btn-danger" href="{{ url_for('delete_book', id=id)}}">削除</a>

      </div>

以上です。それでは実行してみましょう。indexページにて「吾輩は猫である」をクリックします。

f:id:akatak:20190713175856p:plain

削除ボタンが追加されています。削除ボタンを押してみます。

f:id:akatak:20190713180026p:plain

無事削除ができました。 これでCRUD機能が実装できました。 今回のスクリプトを以下にv2としてアップロードしておきましたので、参考にしてみてください。

github.com

アプリの機能としてはまだ不十分かと思いますので、次回以降バージョンアップを図っていきたいと思います。

例えば、著者を別画面で登録できるようにして、読書記録登録時には、そこから選択できるようにしたり、ペジネーション(読書記録が多くなってきた場合に1ページあたりの表示件数を制限)できるようにしたりしていきたいと思います。

今回はこれにて。

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を実行します。すると、以下の画面が表示されました。取り敢えず、データベースのない基本構成はできました。

f:id:akatak:20190704215826p:plain

読書記録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により、入力制限をかけることも可能です。ここでは、titleauthorは必ず入力が必要とする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.pyexport 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欄にコピペします。

f:id:akatak:20190706112758p:plain

インデックスページが表示されました。まだ、データはありませんので、表題以外は何も表示されません。メニュー画面の"読書記録の入力"をクリックしてみます。

f:id:akatak:20190706113109p:plain

入力画面が表示されました。早速、入力してみましょう。

f:id:akatak:20190706113232p:plain

登録をクリックします。ついでに、もう1件入力してみます。以下の通り、2件とも無事、表示できました。

f:id:akatak:20190706113525p:plain

今回は以上です。入力だけできても、修正もできない、削除もできないとなると困りますよね。次回以降、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

FlaskとSQLAlchemyで読書記録Webアプリを作る(2)

前回、読書記録Webアプリで利用するモジュールの概略をご説明しましたので、今回から具体的にWebアプリの作成に入っていきましょう。

まずは、Flaskホームページのクイックスタートに掲載されている最小規模のアプリケーション例からご紹介します。

FlaskでHello World!を表示する

予めpip install flask等でFlaskをインストールしておきます。そして、ファイル名をhello.pyとでもして、以下のスクリプトを書きます。

# hello.py

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return "Hello World!"

if __name__ == '__main__':
    app.run()

これだけです。ターミナルからpython hello.pyと実行します。

$ python hello.py
 * Running on http://127.0.0.1:5000/

すると上記のように表示されますので、ブラウザのアドレス入力欄にhttp://127.0.0.1:5000/を入力します。”Hello World!”と出力されました。おめでとうございます。簡単に作成できました。この気軽さがFlaskの良いところですね。

上記のスクリプトでやっていることは以下の通りです。 まず、 flaskからFlask クラスをインポートし、次にFlaskインスタンスを生成します。 そして、route() デコレータを使用し、関数を起動するURLをFlaskインスタンスに教えます。今回は、routeの中が/なので、実行後にhttp://127.0.0.1:5000/を入力すると、デコレータの次に記載の関数が呼び出され、Hello World!という文字列が返されます。

最後に runメソッドを使い、ローカルサーバー上でアプリケーションを実行します。if __name__ == '__main__':は、そのスクリプトがPyrhonから直接実行されたときだけ、 そのサーバ上で動くことになります。モジュールとして他のプログラムからインポートされたときにはapp.run()は実行されません。

読書記録Webアプリの構成を考える

簡単にFlaskアプリが作成できることを確認できましたところで、読書記録Webアプリの作成に取り掛かっていきます。 まず、最初に全体の構成を見てみましょう。

books.py
config.py
web/
    __init__.py
    forms.py
    models.py
    app.db
    books/
        views.py
    templates/
        base.html
        books/
            index.html
            each_book.html
            register_book.html

(以下は、いずれ追加していきます)
    authors/
    publishers/
    searches/
ファイル/ディレクトリ名 説明
books.py 本アプリの実行用のファイル。中身は非常にシンプル
config.py 各種設定を格納しておくファイル。init.pyで各種設定を記述しても良いが、config.pyに設定を外出ししておくと、init.pyファイルがすっきりする。
web/init.py 本アプリを立ち上げた際に、最初に実行され、様々なコンポーネントを読み込む
web/forms.py 書籍名など、ユーザー入力用のフォームを定めるもの
web/models.py データベースのテーブル構成を定めるもの。テーブルに相当するクラスを定義しておきます。ここできちんと定義しておくと、sqlalchemy等により、簡単にsqlite3等にテーブルが生成されます。
web/app.db sqlite3データベースです。どのフォルダ下に作るのかについては、上記のconfig.pyで指定しておきます。
web/books/views.py 本アプリのURLとそのURLを指定した場合に実行するスクリプトを記述するファイルです。routeデコレータでurlを指定し、その直後に関数を記述するようにします。webフォルダ直下にviews.pyをおいても良いのですが、今回は、書籍だけでなく、著者や発行者も入力できるようにするつもりですので、書籍を入力する場合のviews.pyだと分かるようにbooksフォルダ傘下にviews.pyを置いています
web/templates/ このtemplatesの傘下には、htmlファイルを基本的に配置します。booksというフォルダを作成したのは、この後、書籍だけでなく、著者は発行者についても登録、削除等を行うようにしたいので、同じような名称のhtmlファイルができるので、予めフォルダで分けておきたいと思っているからです。

さて、具体的に次回からコーディングの例をご紹介していきますね。