[Flask-SQLAlchemy]レコード編集と論理削除による削除機能の実装
はじめに
PythonのWebアプリケーションフレームワークFlaskを使って、「消耗品買い物リスト」を作ってみるシリーズ第12回目です。
今回は消耗品の編集/削除機能を作ります。
前回まで
- 0回 : Flaskで簡単なWebサービスを作ってみる
- 1回 : スプレッドシート+Google Apps Scriptでプロトタイプを作る
- 2回 : FigmaでUI制作をしてみたが早々に次のフェーズに進んだ件
- 3回 : DockerでFlaskが動き、簡単なテストが通る状態を作る
- 4回 : [スクショ付き]AWSにVPCを構築し、EC2を立てる
- 5回 : EC2でDockerコンテナを起動し、Flaskを動かす
- 6回 : CircleCIでFlaskアプリの自動テストができるようにする
- 7回 : CircleCIでIP制限のあるEC2インスタンスに自動デプロイできるようにする
- 8回 : ユーザーストーリーマッピングで初期開発機能を洗い出す
- 9回 : [Flask]uuidを生成して専用URLを作成する
- 10回 : flask-sqlalchemy/flask-migrate/mysql-connector-pythonでMySQL連携を実装する
- 11回 : [Flask-SQLAlchemy]レコードの登録機能の実装
今回やること
前回までで、新規アイテムの登録ができるようになりました。
今回は、登録したアイテムの情報の編集と、削除ができるようにします。
(変更ログの仕組みは次回以降に)
以下の機能を実装します。
- アイテムリストに、編集/削除ボタンを表示
- 編集
- 編集ボタンを押すと編集ページに遷移
- 編集内容を送信するとアイテム情報を更新できる
- 削除
- 削除ボタンを押すとアイテムが削除できる
レコードの更新方法
まず、SQLAlchemyにおけるレコードの更新方法は、
- レコードを取得
- 変数を更新
- レコードをadd->commit
という流れになります。具体例で示すと以下です。
# Itemモデルをidで取得
item = models.Item.searchById(id)
# Itemモデルのname変数を'fuga'に
item.name = 'fuga'
# 変更した内容をDBに反映
try:
db.session.add(item)
db.session.commit()
except Exception as e:
print(e)
raise e
フロントエンドでは、入力したい値を送れるようにフォームを作り、バックエンドでは値を受け取って上記のような処理を行い、完了ページに飛ばす、というのを作っていきます。
ちなみに、削除はレコードを削除するのではなくstatus列をもたせて値を変更する、いわゆる論理削除の実装で進めます。
編集/削除ボタン追加
先にフロントエンドから作ります。
アイテム一覧の画面でアイテムごとに編集/削除ボタンを追加します。
ついでに、アイテムの名前以外の情報も表示するようにしておきます。
- tbl/templates/user_page.html
(略)
<div>
<ul>
{% if page.items.count() > 0 %}
{% for p in page.items %}
<li>
Name: {{ p.name }},
Last update: {{ p.updated_at }},
Cycle days: {{ p.purchase_cycle_days }},
Stock rate: {{ p.stock_rate }},
Status: {{ p.status }}
<a href="{{ url_for('edit_item', item_id=p.id, uuid=page.uuid) }}">[編集]</a><a href="{{ url_for('delete_item', item_id=p.id, uuid=page.uuid) }}">[削除]</a>
</li>
{% endfor %}
{% else %}
<p>No item set yet.</p>
{% endif %}
<li>
<form name="input" class="ui" id="new_item" action="item/create" accept-charset="UTF-8" method="post">
<div>[+]
<label>Name: </label><input type="text" name="name" id="name" placeholder="Input item name..." />
<label>Cycle days: </label><input type="text" name="purchase_cycle_days" id="purchase_cycle_days" placeholder="1..90" />
<label>Stock rate: </label><input type="text" name="stock_rate" id="stock_rate" placeholder="0.0..0.1" />
<input type="hidden" name="user_page_id" value="{{ page.id }}" />
<input type="submit" value="Add item" />
</div>
</form>
</li>
</ul>
</div>
(略)
ポイントは以下の箇所です。
<a href="{{ url_for('edit_item', item_id=p.id, uuid=page.uuid) }}">[編集]</a><a href="{{ url_for('delete_item', item_id=p.id, uuid=page.uuid) }}">[削除]</a>
編集ボタンを押すと、編集ページに遷移するedit_item
メソッドを呼び出し、
削除ボタンを押すと、削除完了ページに遷移するdelete_item
メソッドを呼び出します。
どちらも、引数としてはitem_idとページのuuidを渡してます。
理由としては、item_idだけで更新できてしまうと、他のユーザーのアイテムの編集/削除ができてしまうからです。
作成した本人のみが持っている情報(=uuid)をあわせて渡すことで、はじめて更新ができるようにします。
編集ページ作成
続いて、編集ボタンを押すと遷移する編集ページを作ります。
入力フォームの要素は、アイテムの新規登録のフォームとほぼ同じなので、新規登録と編集で要素を流用することを検討します。
includeを使ったテンプレート部品の作成
共通化するフォームを作成します。
まずは、新規登録のフォームを見てみます。
- tbl/templates/user_page.html
<li>
<form name="input" class="ui" id="new_item" action="item/create" accept-charset="UTF-8" method="post">
<div>[+]
<label>Name: </label><input type="text" name="name" id="name" placeholder="Input item name..." />
<label>Cycle days: </label><input type="text" name="purchase_cycle_days" id="purchase_cycle_days" placeholder="1..90" />
<label>Stock rate: </label><input type="text" name="stock_rate" id="stock_rate" placeholder="0.0..0.1" />
<input type="hidden" name="user_page_id" value="{{ page.id }}" />
<input type="submit" value="Add item" />
</div>
</form>
</li>
これを一部切り出し、item_form.html
というファイルを新たに作成します。
若干調整も加えます。
- tbl/templates/item_form.html
<div>[+]
<label>品目名: </label>
<input type="text" class="validate[required]" name="name" id="name"
{% if submit_word =='Edit item' %} value="{{ item.name }}"{% endif %}/>
<label>購入サイクル(日数): </label>
<input type="number" class="validate[custom[number]]" name="purchase_cycle_days" id="purchase_cycle_days" min="1" max="90"
{% if submit_word =='Edit item' %} value="{{ item.purchase_cycle_days }}"{% endif %}/>
<label>残量ステータス(0.0〜1.0): </label>
<input type="number" class="validate[custom[number]]" name="stock_rate" id="stock_rate" min="0.0" max="1.0" step="0.1"
{% if submit_word =='Edit item' %} value="{{ item.stock_rate }}"{% endif %}/>
{% if submit_word == 'Edit item' %}
<input type="hidden" name="item" value="{{ item.id }}" />
<input type="hidden" name="user_page_id" value="{{ item.user_page_id }}" />
{% else %}
<input type="hidden" name="user_page_id" value="{{ page.id }}" />
{% endif %}
<input type="submit" value="{{ submit_word }}" />
</div>
加えた調整としては、
- placeholderは無くしました。
- 数値を入れる箇所(購入サイクル、残量ステータス)に数値以外の値が入らないように
input type="number"
に変更しました。 {% if submit_word == 'Edit item' %}
とある箇所では、submit_wordの値で編集か新規登録かを判別し、表示させる値を切り替えています。- 余談ですが、値のバリデーションを行うためにjQuery-Validation-Engineを入れています。
新規登録フォームの置き換え
ファイルを作成したので、新規登録フォームの部分を置き換えます。
- tbl/templates/user_page.html
<li>
<form name="input" class="ui" id="new_item" action="item/create" accept-charset="UTF-8" method="post">
{% with submit_word = 'Add Item' %}
{% include "item_form.html" %}
{% endwith %}
</form>
</li>
{% include "item_form.html" %}
でさきほど作成したitem_form.htmlを読み込みます。{% with submit_word = 'Add Item' %}
で、そのitem_form.htmlにsubmit_word変数を渡します(前項で編集か新規登録かを判別する時に使う値です)<form>
要素の中で、どのメソッドを呼び出すかを指定しています。
編集フォームの作成
新規登録フォームと同じように、編集ページも作ります。
- tbl/templates/item_edit.html
{% extends "application.html" %}
{% block body %}
<div>
<h1>{{ title }} : {{ item.name }}</h1>
<div>
<form name="input" class="ui" id="new_item" action="/item/edit/{{item.id}}/{{uuid}}" accept-charset="UTF-8" method="post">
{% with submit_word = 'Edit item' %}
{% include 'item_form.html' %}
{% endwith %}
</form>
</div>
</div>
{% endblock %}
<form>
要素のactionの値に、/item/edit/{{item.id}}/{{uuid}}
を入れて、編集完了時に送るリクエスト先を指定しています。
編集ページ遷移/編集結果反映処理の実装
表示側の準備ができたので、バックエンドの処理を書いていきます。実装する機能は
- 編集ボタンを押した時に編集ページに遷移
- 編集結果を送信した時に編集内容をDBに書き込み、完了ページに遷移
の2つです。
- tbl/app.py
(略)
@app.route('/item/edit/<item_id>/<uuid>')
def show_edit_item_page(item_id, uuid):
"""アイテム編集画面に遷移する
"""
title = 'Item Edit'
item = models.Item.searchById(item_id)
return render_template('item_edit.html', title=title, item=item, uuid=uuid)
@app.route('/item/edit/<item_id>/<uuid>', methods=['POST'])
def edit_item(item_id, uuid):
"""アイテムを編集する
"""
# POSTでない場合は編集ページに遷移
if request.args == "" or request.method != 'POST':
return show_edit_item_page(item_id)
if request.form['user_page_id'] is None: # user_page_idが無い場合は戻す
return redirect(url_for('index'))
user_page_id = request.form['user_page_id']
# UPDATE処理
item = models.Item.searchById(item_id)
# item.page_idのuuidと一致するか確認
if check_is_correct_uuid(item, uuid) is False:
return "uuid does not match."
# 値の更新
if request.form['name']:
item.name = request.form['name']
if request.form['purchase_cycle_days']:
item.purchase_cycle_days = request.form['purchase_cycle_days']
if request.form['stock_rate']:
item.stock_rate = request.form['stock_rate']
try:
db.session.add(item)
db.session.commit()
title = 'Item edit completed'
url = '/page/' + uuid
return_msg = 'Back to the page'
# ページ表示
return render_template('complete.html', title=title, url=url, return_msg=return_msg)
except Exception as e:
print(e)
raise e
def check_is_correct_uuid(item, uuid):
"""Itemインスタンスから取得したuuidと、送られたuuidが一致するかチェック
"""
# item_idから該当uuid取得
user_page_id = item.user_page_id
user_page = models.UserPage.searchByUserPageId(user_page_id)
if uuid != user_page.uuid:
return False
return True
編集ボタンを押すと、edit_item
メソッドが呼ばれます。
この段階では、何もデータを送っていないので、show_edit_pageメソッドに飛ばされます。これが編集ページになります。
# POSTでない場合は編集ページに遷移
if request.args == "" or request.method != 'POST':
return show_edit_item_page(item_id)
編集結果を送信した時は、check_is_correct_uuid
メソッドでuuidの照合をした上で、レコードのUPDATE処理に進みます。
item = models.Item.searchById(item_id)
でレコードを取得し、各種パラメータが送られていれば、
item.xxx = request.form['xxx']
のようにレコードに値を代入しています。
最後に、そのレコードを
db.session.add(item)
db.session.commit()
してDBに書き込んだ上で、完了ページに遷移させています。
完了ページは前回作った汎用的なものを使いまわしているので割愛します。
これでアイテム情報の編集ができるようになりました。
削除の実装
編集ができたら次は削除です。
削除については論理削除、つまり「ユーザーから見たら消えているが、レコードとしては残っている」方式で実装するので、やることは編集とあまり変わりません。
表示部分の処理は編集機能の実装の際にすでに作っているので、バックエンドの処理を書いています。
- tbl/app.py
(略)
@app.route('/item/delete/<item_id>/<uuid>')
def delete_item(item_id, uuid):
"""アイテムを削除する
item_idだけで削除できるとまずいので、uuidも渡す
"""
item = models.Item.searchById(item_id)
# item.page_idのuuidと一致するか確認
if check_is_correct_uuid(item, uuid) is False:
return "uuid does not match."
item.status = "deleted"
try:
db.session.add(item)
db.session.commit()
title = 'Item delete completed'
url = '/page/' + uuid
return_msg = 'Back to the page'
# ページ表示
return render_template('complete.html', title=title, url=url, return_msg=return_msg)
except Exception as e:
print(e)
raise e
アイテム一覧ページで削除ボタンが押されると、delete_item
メソッドにitem_id,uuidが渡されます。
中身の処理はedit_itemとほぼ同じで、更新内容はstatus
をdeleted
にするのみです。
statusの値をactive->deletedに変更し、完了ページに遷移させています。
modelの修正
statusの値を変更しても、今のままでは削除したアイテムも普通に見えてしまうので、Itemモデルを編集します。
- tbl/models/models.py
(略)
class Item(db.Model):
"""消耗品・アイテムのモデル
"""
(略)
def searchByUserPageId(user_page_id):
return db.session.query(Item)\
.filter(Item.status == 'active')\
.filter(Item.user_page_id == user_page_id)
def searchById(id):
return db.session.query(Item)\
.filter(Item.status == 'active')\
.filter(Item.id == id)\
.one()
searchByXXXメソッドの中に、.filter(Item.status == 'active')
を追記します。
アイテムを取得する時に、statusがactiveのものだけを取得するようにします。
ここまでで、編集/削除機能が一通り実装できました。
動作確認
編集
編集を押すと編集ページに遷移します。
値を変更して、送信すると完了ページに遷移します。
Back to the pageでアイテム一覧ページに戻ります。
アイテム情報が更新されていることが確認できます。
削除
さきほどのアイテム一覧ページで、削除を押すと、削除完了ページに遷移します。
Back to the pageでアイテム一覧ページに戻ります。
アイテムが消えていることが確認できます。
まとめ
アイテムの編集と削除機能の実装ができました。
これで、基本的なデータ操作の実装は完了となります。
改めて、完了ページ挟むのが面倒だなという気持ちが増しています。
次回以降は、以下の項目に取り組んでいきます。
- 変更ログの実装
- ページ遷移をしないでレコード追加・更新ができるようにする
ディスカッション
コメント一覧
まだ、コメントがありません