LoginSignup
11
25

More than 3 years have passed since last update.

Flaskの練習として簡単なToDoリストWebアプリを作成

Last updated at Posted at 2019-07-08

はじめに

Pythonで書くWebアプリケーションフレームワークFlaskの練習として、簡単なToDoリストを作成しました。

概要

  • ルートページ(index.html)にはToDoリストの一覧を表示
  • それぞれの項目に詳細ページ(show.html)
  • 他に、項目編集ページ(edit.html)と新規項目追加フォームページ(new.html)
  • 見た目はBootstrapを使いました

環境

OS macOS mojave
Python 3.7.3
Flask 1.0.3

アプリの作成

まずはアプリの根幹になるファイルapp.pyを作成します。

モジュールのインポートなど

app.py
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
db.create_all()

作業ディレクトリを整理

以下は完成したディレクトリ構造。
*はbootstrapのファイルです。
その他ページの見た目をhtmlでtemplatesに、cssでstaticに作成しました。

.
├── Procfile
├── app.py
├── requirements.txt
├── static
│   ├── css
│   │   └── bootstrap.css *
│   └── js
│       ├── bootstrap.js *
│       └── jquery-3.4.1.js *
├── templates
│   ├── edit.html
│   ├── favicon.png
│   ├── index.html
│   ├── layout.html
│   ├── new.html
│   └── show.html
└── todo.db

データベースの作成

SQLAlchemyというモジュールを用いて、SQL文を書かずとも操作ができるスタイルで作ります。
まずはtouch todo.dbでデータベースファイルを作り、以下のクラスに対応するようにテーブルを作成します。

app.py
class Post(db.Model):

    __tablename__ = "posts"
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    date = db.Column(db.Text())
    title = db.Column(db.Text())
    content = db.Column(db.Text())
    commit = db.Column(db.Integer)

このクラスを外部から呼び出すことでデータベースへの操作を行うことができます。

ルートページの作成

app.py

app.py
@app.route('/') # ルート(/)ページURLにリクエストが送られた時の処理
def index():

    posts = Post.query.all()
    return render_template("index.html", posts = posts)
  • 変数postsにPostクラスの全てのクエリを代入
  • index.htmlのテンプレートを呼び出してレンダリングし、表示。

index.html

続いてindex.htmlを編集します。
layout.htmlで共通部分を作成し、{% block content %}{% endblock %}で挟み込んだ所のみを記述することで余計にコードを書かないようにします。

index.html
{% extends "layout.html" %}
{% block content %}
    <div class="container">
        <h1>ToDo</h1>

        <table class="table table-hover table-responsive">
            <tr><th>Last Update</th><th>Title</th><th></th><th></th><th></th></tr>
            {% for post in posts %}    
            <div class="row">
                <tr>
                    <td>{{ post.date }}</td>
                    {% if post.commit == 1 %}
                        <td><del>{{ post.title }}</del></td>
                    {% else %}
                        <td>{{ post.title }}</td>
                    {% endif %}
                    <td><a href="/show/{{ post.id }}" class="btn btn-success far fa-file-alt"> 詳細 </a></td>
                    <td><a href="/done/{{ post.id }}" class="btn btn-info fas fa-check-circle"> Done </a></td>
                    <td><a href="/undone/{{ post.id }}" class="btn btn-success fas fa-undo-alt"> Undone </a></td>
                </tr>
            </div> 
            {% endfor %}
        </table>
        <p>
            <a href="/new" class="btn btn-primary far fa-file"> 新規 </a>
            <a href="/destroy/alldone" class="btn btn-danger fas fa-trash" onclick="return confirm('完了済みの項目を全て削除します。よろしいですか?')"> 完了済みをすべて削除 </a>
        </p>
    </div>
{% endblock %}
  • forループとtableタグを使ってクエリの一覧表示
  • Postクラス変数commitが1の時完了を、0の時未完了を表し、1の時は横線を入れるよう条件分岐で記述
  • 各種ボタンを作成

詳細ページの作成

app.py

/show/(Postクラスのインスタンスのid)にアクセスした時の振る舞いをapp.pyに記述します。

app.py
@app.route('/show/<int:id>')
def show(id):
    post = Post.query.get(id)
    return render_template("show.html", post = post)
  • /showの後に記述されたint型の値を変数idに格納
  • この変数idとDBのidカラムのデータを変数postに代入
  • show.htmlをテンプレートとしてれ呼び出し、レンダリング

show.html

show.html
{% extends "layout.html" %}
{% block content %}
<div class="container">

    <div class="card bg-light">
        <div class="card-header">
            <h2>{{ post.title }}</h2>
        </div>
        <div class="card-body">
            <blockquote class="blockquote mb-0">
                <p>{{ post.content }}</p>
                <footer class="blockquote-footer"><cite title="Source Title">(Last Update: {{ post.date }})</cite></footer>
            </blockquote>
        {% if post.commit == 1 %}
            <div class="alert alert-success" role="alert">
                完了しています
            </div>
        {% endif %}
        </div>
    </div>

    <br>
    <div>
        <a href="/edit/{{ post.id }}" class="btn btn-warning far fa-edit"> 編集 </a>
        <a href="/destroy/{{ post.id }}" class="btn btn-danger fas fa-trash-alt" onclick="return confirm('この項目を削除します。よろしいですか?')"> 削除 </a>
        <a href="/done/{{ post.id }}" class="btn btn-info fas fa-check-circle"> Done </a>
        <a href="/undone/{{ post.id }}" class="btn btn-success fas fa-undo-alt"> Undone </a>        
    </div>

    <br>
    <p><a href="/" class="btn btn-default">一覧に戻る</a></p>

</div>
{% endblock %}
  • タイトルと、さらに詳しく情報が表示されます
  • 完了しているもの(commit == 1)にはブロックが表示されます

新規追加・編集ページの作成

新規追加・編集ページのレイアウトと、そこに記述された内容をPOSTメソッドで送るロジックを組みます。

new.html

先にテンプレートを作ります。

new.html
{% extends "layout.html" %}
{% block content %}
<div class="container">
    <form action="/create" method="POST">
        <div class="form-group">

            <label for="title">ToDo</label>
            <input type="text" name="title" value="" class="form-control" placeholder="What do you do next?">

        </div>
        <div class="form-group">

            <label for="content">備考</label>
            <textarea class="form-control" name="content" cols="40" rows="10" placeholder="If you have options, write down here."></textarea>

        </div>

        <button type="submit" class="btn btn-primary">保存</button>
        <button type="button" class="btn btn-danger" onclick="history.back()">キャンセル</button>

    </form>
</div>
{% endblock %}
  • /createへPOSTリクエストを送ります
  • 送り先ではname属性で指定した値で取り出せます

app.py

app.py
@app.route('/new')
def new_post():

    return render_template("new.html")

このメソッドに/newにアクセスされた時の動きを記述します。
といってもnew.htmlのテンプレートをレンダリングするだけです。

app.py
@app.route('/create', methods=["POST"])
def create_post():

    new_post = Post()
    new_post.title = request.form["title"]
    new_post.content = request.form["content"]
    new_post.date = str(datetime.today().year) + "-" + str(datetime.today().month) + "-" + str(datetime.today().day)
    new_post.commit = 0
    db.session.add(new_post)
    db.session.commit()

    return redirect(url_for('.index'))

こちらで、実際にnew.htmlで入力された値をDBに格納します。

  • 変数new_postをPostクラスでインスタンス化
  • title, contentにPOSTリクエストで送られてきた値を格納
  • dateは今日の日付
  • 未完了なのでcommitは0に
  • session.addでデータベースへ値を追加
  • session.commitでデータベースの変更を実行します
  • 編集後にわざわざレンダリングせずindex.htmlを表示させます。こうすることでリロードした時に同じものが登録されてしまうのを防げます

edit.html

edit.html
{% extends "layout.html" %}
{% block content %}
<div class="container">
    <form action="/update/{{ post.id }}" method="POST">
        <div class="form-group">

            <label for="title">ToDo</label><br>
            <input type="text" name="title" value="{{ post.title }}" class="form-control">

        </div>
        <div class="form-group">

            <label for="content">詳細</label><br>
            <textarea name="content" cols="40" rows="10" class="form-control">{{ post.content }}</textarea>

        </div>
        <button type="submit" class="btn btn-primary">保存</button>
        <button type="button" class="btn btn-danger" onclick="history.back()">キャンセル</button>

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

こちらもnew.htmlと見た目は一緒ですが、一緒にレンダリングした変数postを使って現在のデータを入力ボックス内に表示してあげないといけません。

  • /update/{{ post.id }}へリクエストを送ります

app.py

app.py
@app.route('/edit/<int:id>')
def edit_post(id):

    post = Post.query.get(id)

    return render_template("edit.html", post = post)

こちらは編集ページなので指定したデータも一緒にレンダリングしてあげないといけません。

app.py
@app.route('/update/<int:id>', methods=["POST"])
def update_post(id):

    post = Post.query.get(id)
    post.title = request.form["title"]
    post.content = request.form["content"]
    post.date = str(datetime.today().year) + "-" + str(datetime.today().month) + "-" + str(datetime.today().day)
    db.session.commit()

    return redirect(url_for('.index'))
  • /createの時と同じような要領で記述します
  • 今回も編集後にわざわざレンダリングせずindex.htmlを表示します

項目の完了(make item done)・削除のロジック

リクエストがdestroy, done, undoneに送られた時の動きをapp.pyに記述します。これらに関するURLにリクエストが送られた時の「動き」を記述すれば良いだけなので、とくに新たなhtmlファイルを書く必要はありません。

Done, Undone

app.py
@app.route('/done/<int:id>')
def done_post(id):

    post = Post.query.get(id)
    post.commit = 1
    post.date = str(datetime.today().year) + "-" + str(datetime.today().month) + "-" + str(datetime.today().day)
    db.session.commit()
    posts = Post.query.all()

    return redirect(url_for('.index'))

@app.route('/undone/<int:id>')
def undone_post(id):

    post = Post.query.get(id)
    post.commit = 0
    post.date = str(datetime.today().year) + "-" + str(datetime.today().month) + "-" + str(datetime.today().day)
    db.session.commit()
    posts = Post.query.all()

    return redirect(url_for('.index'))
  • 変数commitが0か1かで、完了・未完了を分けているのでそれをリクエストによって変更する記述をします

削除

一件のみの削除

app.py
@app.route('/destroy/<int:id>')
def destroy(id):

    post = Post.query.get(id)
    db.session.delete(post)
    db.session.commit()
    posts = Post.query.all()

    return redirect(url_for('.index'))
  • 新規追加や編集の時と記述の仕方はほとんど一緒です
  • session.deleteで削除してコミットします

完了済みを全削除

app.py
@app.route('/destroy/alldone')  
def destroy_alldone():

    posts_done = Post.query.filter_by(commit=1).all()
    for i in posts_done:
        db.session.delete(i)
    db.session.commit()
    posts = Post.query.all()

    return redirect(url_for('.index'))
  • ルートページにこのボタンがあります
  • Post.query.filter_byで変数commitが1のもの(完了しているもの)をとりだし
  • 削除してコミットします
  • 最後にルートページに飛んで終了

最後に

GitHubではHerokuに公開したURLも記述していますがユーザー管理を導入していないので完全に一人用です。
Herokuの使い方はRailsのチュートリアルドキュメントを参考にしました。

11
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
25