この記事は約3分で読めます。
こんにちは。AWS CLIが好きな福島です。
はじめに
インフラエンジニアのキャリアがメインの私が社内でPythonを使ったアプリ開発の学習をする機会があったため、先日から学んだことをアウトプットしています。
今回は以下の技術を活用し、TODO管理ができるWebアプリを作ってみたいと思います。
- Python
- プログラミング言語
- Flask
- Webアプリフレームワーク
- Jinja2
- テンプレートエンジン
- Bootstrap
- フロントエンドツールキット
- DynamoDB
- AWSのマネージドなNoSQLサービス
各技術の入門に関するブログも書いているため、ご興味ある方は以下もご確認ください。
完成イメージ
完成イメージは以下の通りです。
設計
まずは、簡単に設計します。
機能
今回は、TODO管理として以下の機能を実装することにします。
- 一覧表示
- 追加
- 更新
- 削除
画面および画面遷移図
画面および遷移図に加え、機能もまとめて書いちゃってます。
URLパス設計
URLのパスは以下の通りにします。
No | パス設計 | メソッド | 備考 |
---|---|---|---|
1 | / | GET | トップページとなります。 TODO一覧を表示するページになります。 |
2 | /todos | GET | 追加/更新するTODOの内容を入力するページになります。 |
3 | /todos/add | POST | TODOを追加し、トップページにリダイレクトします。 |
4 | /todos/update | POST | TODOを更新し、トップページにリダイレクトします。 |
5 | /todos/delete | POST | TODOを削除し、トップページにリダイレクトします。 |
3-5でリダイレクトしているのは、POST
メソッドを送信した後にブラウザの再読み込みにより、フォームの二重送信されるのを防ぐためです。
この仕組みをPost/Redirect/Get
と呼びます。
アーキテクチャ図
アーキテクチャ図は以下の通りです。
- Webサーバの
Werkzeug
は、Flask
に組み込まれているライブラリになります。あくまで開発用途なので、本番ではgnicorn
などを使うのが推奨されています。 - アプリケーションにおける3層アーキテクチャ(プレゼンテーション層,ビジネスロジック層,データアクセス層)と
Flask
のMTVモデル
(Model
,Template
,View
)を組み合わせると上記イメージかなと思います。 Model
がビジネスロジックやデータベースのアクセスを担当し、Template
がHTMLなどのテンプレートを管理、View
がユーザーからのリクエストを受け取り、Template
やModel
をコントロールするイメージです。
アーキテクチャの部分は、以下の記事を参考にさせていただきました。
機能ごとのシーケンス
各機能における処理の詳細は、以下の通りです。 ()で記載しているA-Hは、アーキテクチャ図の記号とリンクしています。
TODO一覧の表示
- ①ユーザーがWebサーバにリクエスト(A)
- ②Webサーバからアプリケーションサーバ(View)にリクエスト(B)
- ③ViewからModelにデータの取得を指示(C)
- ④ModelがデータベースからTODO一覧を取得(D)
- ⑤Modelが処理結果をViewに返す(E)
- ⑥ViewがTemplateからHTMLテンプレートを取得(F)
- ⑦⑤,⑥を基にWebページをレンダリングして、Webサーバにレスポンス(G)
- ⑧Webサーバからユーザーにレスポンス(H)
TODOの追加/更新/削除
TODOの追加/更新/削除処理はほぼ同じ流れになるため、まとめて記載します。
- ①ユーザーがWebサーバにリクエスト(A)
- ②Webサーバからアプリケーションサーバ(View)にリクエスト(B)
- ③ViewからModelにデータの追加/更新/削除を指示(C)
- ④ModelがデータベースにTODOを追加/更新/削除(D)
- ⑤Modelが処理結果をViewに返す(E)
- ⑥ViewがWebサーバにTODO一覧ページへのリダイレクトを指示(G)
- ⑦WebサーバからユーザーにTODO一覧ページへのリダイレクトを指示(H)
- ⑧TODO一覧の表示と同様の処理
TODOに含める情報
TODOには以下の情報を含めることにします。
- タイトル
- 任意の文字列
- 詳細
- 任意の文字列
- ステータス
- In Progress or Pending or Completed
テーブル設計
テーブル名は、TodoTable
にします。
また保存するデータは、TODOに含める情報に加え、アイテムを特定するためキーとして、TodoIDを設定します。
No | キー | 型 | 備考 |
---|---|---|---|
1 | TodoID | str | パーティションキー |
2 | Title | str | - |
3 | Detail | str | - |
4 | TodoStatus | str | - |
ディレクトリ/ファイル構成
今回は、以下の構成にします。
$ tree todo-app todo-app ├── models.py ├── templates │ ├── index.html │ └── todo_form.html └── views.py $
実装
まずは、DynamoDBを作成します。
aws dynamodb create-table \ --table-name todo-table \ --attribute-definitions \ AttributeName=TodoID,AttributeType=S \ --key-schema \ AttributeName=TodoID,KeyType=HASH \ --provisioned-throughput \ ReadCapacityUnits=5,WriteCapacityUnits=5
Flaskをインストールします。
pip install flask
各プログラムファイルを作成します。
models.py
import uuidimport boto3from boto3.dynamodb.types import TypeDeserializerTABLE_NAME = "TodoTable"dynamodb_client = boto3.client("dynamodb")def dynamo_to_python(dynamo_object):deserializer = TypeDeserializer()return {k: deserializer.deserialize(v) for k, v in dynamo_object.items()}def get_item(args):if not args.get("todo_id"):return {}todo_id = args.get("todo_id")item = dynamodb_client.get_item(TableName=TABLE_NAME, Key={"TodoID": {"S": todo_id}})["Item"]return dynamo_to_python(item)def scan_items():items = []response = dynamodb_client.scan(TableName=TABLE_NAME)for dynamo_object in response["Items"]:items.append(dynamo_to_python(dynamo_object))return itemsdef put_item(form):todo_id = str(uuid.uuid4())title = form["title"]detail = form["detail"]status = form["status"]dynamodb_client.put_item(TableName=TABLE_NAME,Item={"TodoID": {"S": todo_id},"Title": {"S": title},"Detail": {"S": detail},"TodoStatus": {"S": status},},)def update_item(form):todo_id = form["todo_id"]title = form["title"]detail = form["detail"]status = form["status"]dynamodb_client.update_item(TableName=TABLE_NAME,Key={"TodoID": {"S": todo_id}},UpdateExpression="SET Title=:title, Detail=:detail, TodoStatus=:satus",ExpressionAttributeValues={":title": {"S": title},":detail": {"S": detail},":satus": {"S": status},},)def delete_item(todo_id):dynamodb_client.delete_item(TableName=TABLE_NAME, Key={"TodoID": {"S": todo_id}})
views.py
from flask import Flask, redirect, render_template, request, url_forfrom models import delete_item, get_item, put_item, scan_items, update_itemapp = Flask(__name__)@app.route("/")def index():items = scan_items()return render_template("index.html", items=items)@app.route("/todos")def todo_form():item = get_item(request.args)return render_template("todo_form.html", item=item)@app.route("/todos/add", methods=["POST"])def add_todo():put_item(request.form)return redirect(url_for("index"))@app.route("/todos/update", methods=["POST"])def update_todo():update_item(request.form)return redirect(url_for("index"))@app.route("/todos/delete", methods=["POST"])def delete_todo():delete_item(request.form["todo_id"])return redirect(url_for("index"))
templates/index.html
<!doctype html><html lang="ja"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"><title>TODO管理</title></head><body><div class="container mt-5"><h1 class="text-center">TODO管理</h1><div class="text-end"><a href="{{ url_for('todo_form') }}" class="btn btn-info mb-3">追加</a></div><table class="table table-bordered text-center"><thead class="table-success"><tr><th>タイトル</th><th>詳細</th><th>ステータス</th><th>アクション</th></tr></thead><tbody>{% for item in items %}<tr><td>{{ item.Title }}</td><td>{{ item.Detail }}</td><td>{{ item.TodoStatus }}</td><td><a href="{{ url_for('todo_form', todo_id=item.TodoID) }}" class="btn btn-primary">更新</a><form action="{{ url_for('delete_todo') }}" method="post" class="d-inline"><input type="hidden" name="todo_id" value="{{ item.TodoID }}"><button type="submit" class="btn btn-danger">削除</button></form></td></tr>{% endfor %}</tbody></table></div><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script></body></html>
templates/todo_form.html
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"><title>TODO追加/更新</title></head><body><div class="container mt-5"><h1 class="text-center">TODO追加/更新</h1><form action="{{ url_for('add_todo') if not item.get('TodoID') else url_for('update_todo') }}" method="post"><div class="form-group"><label for="title">タイトル</label><input type="text" class="form-control" id="title" name="title" value="{{ item.get('Title', '') }}" required></div><div class="form-group"><label for="detail">詳細</label><textarea class="form-control" id="detail" name="detail" rows="3" required>{{ item.get('Detail', '') }}</textarea></div><div class="form-group"><label for="status">ステータス</label><select class="form-control" id="status" name="status" required><option value="Pending" {% if item.get('TodoStatus') == 'Pending' %}selected{% endif %}>Pending</option><option value="In Progress" {% if item.get('TodoStatus') == 'In Progress' %}selected{% endif %}>In Progress</option><option value="Completed" {% if item.get('TodoStatus') == 'Completed' %}selected{% endif %}>Completed</option></select></div>{% if item.get('TodoID') %}<input type="hidden" name="todo_id" value="{{ item.get('TodoID') }}">{% endif %}<button type="submit" class="btn btn-primary">保存</button></form></div></body></html>
動作確認
以下のコマンドでFlask
を起動します。
flask --app views run --debug
- 実行例
$ pwd /blog-sample/todo-app $ $ flask --app views run --debug * Serving Flask app 'views' * Debug mode: on WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000 Press CTRL+C to quit * Restarting with stat * Debugger is active! * Debugger PIN: 114-794-361
http://127.0.0.0:5000
にアクセスします。
TODO追加の確認
追加ボタンを押下します。
任意の値を入力し、保存ボタンを押下します。
値が保存されていることを確認します。
TODO更新の確認
更新ボタンを押下します。
任意で値を変更し、保存ボタンを押下します。
値が更新されていることを確認します。
TODOの削除
削除ボタンを押下します。
データが削除されることを確認します。
終わりに
今回は、WebアプリとしてTODOアプリを作成してみました。 どなたかのお役に立てれば幸いです。