akatak blog

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

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ページあたりの表示件数を制限)できるようにしたりしていきたいと思います。

今回はこれにて。

akatak.hatenadiary.jp

akatak.hatenadiary.jp

akatak.hatenadiary.jp

akatak.hatenadiary.jp

akatak.hatenadiary.jp

akatak.hatenadiary.jp