URLに変数を入れるような処理が出てきました。
実際にWebアプリを実装すると今回の流れはかなり実用性の高いパターンになるかもね〜。
flaskチュートリアルのブログ機能、「Update」と「Delete」の機能追加をやっていきます。これら二つの機能は、テンプレートプログラムを共有していますので、2view プログラムに対して1テンプレートのHTMLとなっている点が今までと少し違います。
進めているチュートリアルは次のページとなります。
前回の記事はこちらです。
こんな人の役に立つかも
・PythonでWebアプリケーションを作成したい人
・flaskのチュートリアルを行なっている人
・flaskのチュートリアルでブログアプリflaskrを作成している人
共通の処理
投稿のアップデートまたは削除を行う際に、2つのプログラムで共通する処理があります。それは、「記事の作者が、今ログインしているユーザーかどうか?」を判定することです。
この処理は2回とも同じ処理を書くのではなく、関数として作成してあとで呼び出すことで一回のプログラミングですみますので、先に定義しておきます。
「flaskr」フォルダ内の「blog.py」に次のプログラムを追記します。
#引数として投稿記事のidをとり、check_authorをTrueとします。
def get_post(id, check_author=True):
#①データベースにアクセスして投稿記事内容がデータベースから取得できるかをみます。
post = get_db().execute(
'SELECT p.id, title, body, created, author_id, username'
' FROM post p JOIN user u ON p.author_id = u.id'
' WHERE p.id = ?',
(id,)
).fetchone()
#②postがNoneのとき、指定した投稿記事IDが存在ないのでエラー処理です。
if post is None:
abort(404, "Post id {0} doesn't exist.".format(id))
#③データベースから取得した記事記事情報が存在する場合、その記事の「author_id」が現在ログインしているIDと同一か比較します。
if check_author and post['author_id'] != g.user['id']:
abort(403)
return post
「id」という文字がたくさん出てくるので、混乱注意です。
SQL文(シングルクオーテーションで囲まれたSELECT~WHEREの一連の文章)の中のidは、それぞれのデータベーステーブルのidを示しています。
get_postの引数としてとるidは、関数に与える「投稿記事のid」を示します。check_authorはフラグのような使い方で、基本的にTrueにしておきますが、ここをFalseにすると③の条件を常にFalseにすることができます。
処理の①では、SELECT文でpostテーブルとuserテーブルを結合して、ユーザー情報も含んだ投稿記事テーブルを作成します。そして、WHERE句で「引数としてとった投稿記事ID」と一致している「postテーブルのid」の投稿記事データに絞るような条件としています。
「ID」の戦国時代ですね。
②では、SQLでデータベースにアクセスしてpostがNONEの場合、投稿記事がなかったのでエラーになります。abortという関数でエラーを返します。abortに404などと指定することで「404見つかりません」や「403アクセス禁止」などの任意のエラーを返すことができるようです。
③では、ログインしているユーザーのIDとデータベースから取得した記事のauthor_idが一致しているか判定をする、この処理の要部になります。
③投稿のアップデート(Update)
共通の処理ができましたので、次に投稿のアップデートの処理を実装します。
まずは、次のプログラムを「blog.py」に追加します。
#①URLに投稿記事IDを取ることで記事IDを渡しています。
@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
#①投稿記事IDの作者がログインユーザーと一致するかをみて記事内容を取得
post = get_post(id)
#②POSTで処理されたときはこちらの条件
if request.method == 'POST':
#③フォーム内容を取得
title = request.form['title']
body = request.form['body']
error = None
#④タイトル未入力の場合
if not title:
error = 'Title is required.'
#⑤タイトルが入力されていてエラーがない場合
if error is not None:
flash(error)
else:
#⑥データベースに接続してSQLのUPDATEでデータを更新
db = get_db()
db.execute(
'UPDATE post SET title = ?, body = ?'
' WHERE id = ?',
(title, body, id)
)
#データベースはコミットしてなんぼ!
db.commit()
return redirect(url_for('blog.index'))
return render_template('blog/update.html', post=post)
①では過去にクイックスタートで行なった変数をURLで使うパターンを利用しています。
また、ルーティングのURLにくるidの値は、「index.html」で
url_for('blog.update', id=post['id'])
としている「id」の値が入ります。
url_forに変数の指定、view関数でルーティングに変数を取る、という流れ、パターンとして覚えておきたいです。
②の処理で、POST、フォームに入力した後の「Save」を押したときに入る処理です。続いて③でフォーム内容を取得しています。
④にて、今回はタイトル未入力の場合はエラーとします。
⑤で、先ほどタイトルエラーがない場合の条件を設定し、⑥でデータベースに接続してアップデート処理を行います。
SQLのUPDATEは
「UPDATE テーブル名 SET 項目,項目 WHERE 条件」
というようになっています。
そのため、今回は「投稿記事IDとpostテーブルのidが一致するpostテーブルのデータ行にtitleとbody項目を更新」という内容のSQLになります。
UPDATEしたら、データベースをcommitして更新をデータベースに適用します。
最後にredirectで「INDEX」画面に戻しておきます。
viewによる処理が実装できましたので、テンプレートである「update.html」を 「templates/blog」フォルダ内に作成していきます。
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="title">Title</label>
<input name="title" id="title"
value="{{ request.form['title'] or post['title'] }}" required>
<label for="body">Body</label>
<textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
<input type="submit" value="Save">
</form>
<hr>
<form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
<input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
</form>
{% endblock %}
一番最初のbase.htmlは今までのテンプレートと同様の流れですね。
そして、ヘッダーとタイトルを設定するブロックも同様です。
今回、contentブロックでは以下の2つのトリガーとなるボタンが存在しています。
・updateのview関数を実行するトリガーになる「Save」ボタン
<input type=”submit” value=”Save”>
・deleteのview関数を実行するトリガーになる「Delete」
<input class=”danger” type=”submit” value=”Delete” onclick=”return confirm(‘Are you sure?’);”>
ちなみに、onclickを入れておくことで注意を喚起するダイアログボックスが出ます。
このように今回のテンプレートは2つのview関数を呼び出すためのテンプレートになります。
④投稿の削除(Delete)
Delete処理のテンプレートは、先に紹介したUpdateと共有していますので、view関数のみの作成となります。
「blog.py」に以下のプログラムを追加します。
@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
get_post(id)
db = get_db()
db.execute('DELETE FROM post WHERE id = ?', (id,))
db.commit()
return redirect(url_for('blog.index'))
update処理とほぼ同じ流れとなっています。
get_postで削除しようとしている投稿記事がユーザーIDの所有しているものかを確認しています。ここでupdateとは違い、戻り値である記事内容は取得していません。削除なので、ログインユーザーが記事所有者であるか確認ができれば良いので、関数を呼び出しているだけです。get_postではログインユーザーが記事の所有者でなければ403エラーとなりますので、これで十分です。
ログインユーザーが記事の所有者である条件をクリアしたら、あとはデータベースにSQL文で「DELET」 処理を行なっていきます。
「DELETE テーブル名 WHERE 条件」としますので、プログラムのSQL文は
「postテーブルで投稿記事IDと同じpostテーブルのidのきじを削除する」という意味合いになります。
最後に、データベースにcommitして反映させ、INDEXにリダイレクトしています。
全体的に、処理の流れというものがパターンかできそうです。この流れで色々な機能に応用できそうな気がしています。
実行は、flask-tutrial階層より、Pythonの仮想環境を起動して、デバッグ用のWebサーバーを起動、そして「localhost:5000/」にブラウザからアクセスします。(今まで何回もやっているので、省略させていただきますm__m)