Python {Article107}

ようこそ「Python」へ...

Python, Flask, SQLite3でToDoリストのWebアプリを作成するには

ここではPython, Flask, SQLite3を使用してデータドリブン型のWebアプリを作成する方法について解説します。 PythonでWebアプリを開発するにはFlask, Django, FastAPI等がありますが、ここでは比較的簡単なWebアプリの開発に向いているFlaskを使用します。 なお、Djangoを使用したWebアプリを開発する方法については後日別記事にて解説します。

この記事ではMicrosoftのVisual Studio Code(VS Code)を使用していますが、Jupyter NotebookなどのツールでもOKです。 説明文の左側に図の画像が表示されていますが縮小されています。 画像を拡大するにはマウスを画像上に移動してクリックします。 画像が拡大表示されます。拡大された画像を閉じるには右上の[X]をクリックします。 画像の任意の場所をクリックして閉じることもできます。
click image to zoom!
図A:
click image to zoom!
図B:
click image to zoom!
図C:


FlaskとSQLite3でデータベース連動型のToDoリストWebアプリを作成する

  1. プロジェクトフォルダを作成してPythonの仮想環境を作成する

    まずはプロジェクトフォルダ「PythonFlaskToDoTest」を作成します。 フォルダ名は好みのフォルダ名に変更してください。

    click image to zoom!
    図1-1
    Visual Studio Code (VS Code)を起動したら「File」メニューから「Open Folder...」をクリックします。


    click image to zoom!
    図1-2
    「Open Folder」のダイアログが表示されたらプロジェクトフォルダ「PythonFlaskToDoTest」を選択してダイアログを閉じます。


  2. Pythonの仮想環境を作成してFlask関連のライブラリをインストールする

    ここではVS CodeからPythonの仮想環境を作成してFlaskに関連するライブラリをインストールする手順を解説します。 VS Codeから新規ファイル「requirements.txt」を作成したら行1-10を入力(コピペ)して保存します。 このファイルはFlaskに関連するライブラリをインストールするときに使用します。

    requirements.txt:
    click==8.1.3
    colorama==0.4.5
    Flask==2.2.2
    Flask-SQLAlchemy==2.5.1
    greenlet==1.1.3
    itsdangerous==2.1.2
    Jinja2==3.1.2
    MarkupSafe==2.1.1
    SQLAlchemy==1.4.40
    Werkzeug==2.2.2

    click image to zoom!
    図2-1
    図2-1はVS Codeの画面です。




    VS CodeのTERMINALから入力するコマンド:
    C:\Users\XPS8910\AppData\Local\Programs\Python\Python310\python -m venv venv
    
    pip install -r requirements.txt
    
    pip list

    click image to zoom!
    図2-2
    VS Codeの「Terminal」メニューから「New Terminal」をクリックして「TERMINAL」ウィンドウを開きます。 TERMINALから行1の「C:\....\python -m venv venv」をコピペして仮想環境を作成します。 ここでは「Python 3.10.x」のパスを指定して実行しています。 この場合Python 3.10.xの仮想環境が作成されます。

    行1のオレンジ色の部分(パス)は各自の環境に合わせて書き換えてください。 python.exeのパスが分からないときはWindowsのコマンドプロンプトから「where python.exe」を入力して実行すると表示されます。 詳細は「記事(Article093)」を参照してください。

    仮想環境の作成が完了するとフォルダ「venv」が作成されます。 TERMINALからゴミ箱のアイコン「Kill Terminal」をクリックしてTEMINALウィンドウを閉じます。


    click image to zoom!
    図2-3
    ここではVS Codeが使用するインタープリター(python.exe)を設定します。 VS Codeから「Ctrl + Shift + P」を同時に押してコマンドリストウィンドウを開きます。 コマンドリストから「Python Select Interpreter」▶「インタープリターパスを入力...」▶「検索」を選択します。

    「Pythonインタープリターを選択」のダイアログが表示されたら「venv/Scripts」フォルダに格納されている「python.exe」を選択します。 これでVS Codeが使用するインタープリター(Python 3.10.x)の設定が完了しました。


    click image to zoom!
    図2-4
    ここではPythonの仮想環境にFlask関連のライブラリをインストールします。 VS Codeの「Terminal」メニューから「New Terminal」をクリックして「TERMINAL」ウィンドウを開きます。 TERMINALに「(venv)」が表示されていることを確認します。 「(venv)」が表示されない場合は、仮想環境が正常に作成されていないことになります。

    TERMINALから行3の「pip install -r requirements.txt」をコピペして実行します。 これでFlaskを利用するための各種ライブラリがインストールされます。


    click image to zoom!
    図2-5
    念のために「pip list」を実行してライブラリの一覧を表示します。 「Flask, Flask-SQLAlchemy,...」等のライブラリがインストールされています。


  3. ToDoリストのHTMLファイルの雛形を作成する

    VS Codeから新規フォルダ「templates」を作成したら新規ファイル「home0.html」を作成します。 HTMLファイルが表示されたら行1-119を入力(コピペ)します。 ここで作成するHTMLファイルの作成手順については 「記事(Article017)」 で解説しています。ちなみに、このHTMLページはBootstrap 5.2のフレームワークを利用しています。 Bootstrapを使用するとパソコン、タブレット、スマホに対応したレスポンシブデザインのサイトが簡単に作成できます。

    templates/home0.html:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Flask ToDo App</title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">     
    </head>
    <body> 
        <header>  
            <nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-dark">    
                <div class="container-fluid">
                    <a class="navbar-brand" href="#">📝</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
                        <span class="navbar-toggler-icon"></span>
                    </button>
                    <div class="collapse navbar-collapse" id="navbarContent">
                        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                            <li class="nav-item">
                                <a class="nav-link active" aria-current="page" href="#">Home</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="#">Goal</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="#">Philosophy</a>
                            </li>                                                          
                            <li class="nav-item dropdown">
                                <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                    GO
                                </a>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" href="#">Action 1</a></li>
                                    <li><a class="dropdown-item" href="#">Action 2</a></li>
                                    <li><hr class="dropdown-divider"></li>
                                    <li><a class="dropdown-item" href="#">Action 9</a></li>
                                </ul>
                            </li>
                        </ul>
                        <form class="d-flex" role="search">
                            <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
                            <button class="btn btn-outline-success" type="submit">Search</button>
                        </form>
                    </div>
                </div>
            </nav>
        </header>
    
        <main class="mt-5">
            <div class="container pt-5">            
                <h1 class="text-center">🎉Flask ToDo App📝</h1>
                <h2 class="text-center fs-6"> (ToDo|Task|備忘録 ウェブアプリ)</h2>
                <hr />
                <form class="d-flex" action="/add" method="post">
                    <input class="form-control me-2" type="text" placeholder="Enter to do... (ToDOリストを入力してください...)" aria-label="Add">
                    <button class="btn btn-primary" style="width: 150px;" type="submit">Add (登録)</button>                  
                </form>
                <hr />
                <table class="table">
                    <thead>
                      <tr>
                        <th scope="col">#</th>
                        <th scope="col">Title<br />(タイトル)</th>
                        <th scope="col">Created<br />(作成日)</th>
                        <th scope="col">Updated<br />(更新日)</th>
                      </tr>
                    </thead>
                    <tbody>                                      
                        <tr>
                            <th class="text-center" scope="row">1<br>⬜</th>
                            <td>
                                <span class="fs-5">To do title 1... Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit officia, animi nihil eum repudiandae, alias dolore harum, </span><br />
                                <a class="btn btn-primary btn-sm mx-2" href="/update/1">Update (未完▶完了)</a> 
                                <a class="btn btn-danger btn-sm mx-2" href="/delete/1">Delete (削除)</a>                        
                            </td>
                            <td>2022/09/01 12:19:10</td>
                            <td></td>
                        </tr>
                        <tr>
                            <th class="text-center" scope="row">2<br>✅</th>
                            <td>
                                <span class="fs-5">To do title 2... Lorem ipsum dolor, sit amet consectetur adipisicing elit. Provident, quae totam itaque deserunt natus eius iste. Aut quas obcaecati eum saepe, impedit quo asperiores ad ullam hic, doloribus, unde sed!</span><br />
                                <a class="btn btn-primary btn-sm mx-2 disabled" href="/update/2">Update (完了)</a>                          
                                <a class="btn btn-danger btn-sm mx-2" href="/delete/2">Delete (削除)</a>                           
                            </td>
                            <td>2022/09/01 12:20:10</td>
                            <td>2022/09/01 12:50:20</td>
                        </tr>
                        <tr>
                            <th class="text-center" scope="row">3<br>⬛</th>
                            <td>
                                <span class="fs-5">To do title 3... Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit officia, animi nihil eum repudiandae, alias dolore harum, </span><br />
                                <a class="btn btn-primary btn-sm mx-2" href="/update/3">Update (未完▶完了)</a> 
                                <a class="btn btn-danger btn-sm mx-2" href="/delete/3">Delete (削除)</a>                        
                            </td>
                            <td>2022/09/01 12:19:10</td>
                            <td></td>
                        </tr>                    
                    </tbody>
                </table>
            </div>         
        </main>
    
        <div class="invisible" style="height: 40px; border: 1px solid orangered">
            margin to fix fixed-bottom
        </div>   
    
        <footer class="page-footer fixed-bottom font-small bg-dark py-1">
            <div class="container text-center">                
              <span class="text-muted">  
                Copyright &copy; <a class="text-reset text-decoration-none" href="https://money-or-ikigai.com/" target="_blank">Akio Kasai</a>
              </span>          
            </div>
        </footer>
    
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>    
    </body>
    </html>

  4. Webページに「Hello World!」を表示してみる

    ここではFlaskを使用してブラウザに「Hello World!」を表示します。 VS Codeから新規ファイル「hello.py」を作成したら行1-11を入力(コピペ)します。 行1ではFlaskのライブラリを取り込んでいます。 行3ではFlaskのインスタンスを生成しています。

    行5ではFlaskのルートデコレータ(Route Decorator)で「/」を設定しています。 この場合ブラウザから「http://localhost:5000/」のようなURLを入力したとき関数「home()」が実行されます。 ちなみに、Flaskのルートデコレータは複数指定することができます。 例えば、「@app.route('/home')」「@app/route('index')」などが指定できます。 この場合ブラウザから「http://localhost:5000/home」「http://localhost:5000/index」を入力すると関数「home()」が実行されます。

    行6-8では関数「home()」を定義しています。 ここではホームページに「Hello World!」を表示します。

    行10-11ではFlaskをデバッグモードで実行します。 デバッグモードで実行するとプログラムやHTMLファイルを変更したとき、 ブラウザのリロードボタン(再読み込み)をクリックするだけで変更が反映されます。 行10のifステートメントでは「hello.py」のプログラムを直接起動したときのみFlaskを実行するようにしています。 つまり、importステートメント等で「hello.py」を取り込んだときはFlaskが実行されないようにしています。

    hello.py:
    from flask import Flask
    
    app = Flask(__name__)   # create a Flask instance
    
    @app.route('/')
    def home():
        return 'Hello World!'
        # return '<h1>Hello World!</h1>'
    
    if __name__ == '__main__':
        app.run(debug=True)

    click image to zoom!
    図4-1
    Flask経由でPythonのプログラムを起動するには、 VS CodeのTERMINALウィンドウから「python hello.py」を入力して実行します。 HTMLページを表示するには「http://127.0.0.1:5000」にマウスを移動して[Ctrl]を押しながらクリックします。 Pythonのファイル名を「app.py」で作成したときは「flask run」で起動することもできます。


    click image to zoom!
    図4-2
    ブラウザが起動して「Hello World!」が表示されます。 ブラウザのURLには「http://127.0.0.1:5000」が表示されていますが、 IPアドレス(127.0.0.1)を「http://localhost:5000」のように書き換えることもできます。 「:5000」はポート番号です。


  5. WebページにToDoリストの雛形を表示してみる

    ここでは前出で作成したHTMLの雛形「home0.html」をFlask経由でブラウザに表示します。 VS Codeから新規ファイル「todo_temp.py」を作成したら行1-12を入力(コピペ)します。

    行5-7ではFlaskのルートデコレータで複数のroute()を指定しています。 いずれの場合も関数「home()」が実行されます。 行9ではFlaskのrender_template()メソッドでホームページ「home0.html」をブラウザに表示しています。

    todo_temp.py:
    from flask import Flask, render_template
    
    app = Flask(__name__)   # create a Flask instance
    
    @app.route('/')
    @app.route('/home')
    @app.route('/index')
    def home():
        return render_template('home0.html')    
    
    if __name__ == '__main__':
        app.run(debug=True)

    click image to zoom!
    図5
    VS CodeのTEMINALから「python todo_temp.py」を入力したら 「http://127.0.0.1:5000」にマウスを移動して[Ctrl]を押しながらクリックします。 ブラウザにToDoリストの雛形が表示されます。


  6. Flask起動時に実行されるPythonの「app.py」ファイルを作成する

    ここではFlaskからSQLite3のデータベースを操作する機能を組み込みます。 VS Codeから新規ファイル「app.py」を作成したら行1-68を入力(コピペ)して保存します。

    行1-4ではPythoのライブラリを取り込んでいます。 行8ではSQLite3のデータベース名「todo.db」を定義しています。 行12-13ではFlaskの環境変数を設定しています。 行14ではSQLite3のインスタンスを生成しています。

    行17-18では関数「get_japan_time()」を定義しています。 この関数では「utcnow()」で世界標準時間を取得してローカル時間(日本)に変換しています。 Webサーバーをどこに設置されてもよいようにここでは世界標準時間を使用しています。 特にデータベースに日時を格納するときはローカル時間ではなく世界標準時間を格納するようにします。

    行21-27ではSQLite3のテーブル「Todo」のクラスを定義しています。 ここでは「Todo」テーブルのフィールド名、データ型、長さ等を定義しています。 「Todo」テーブルは「id, title, complete, date_updated, date_created」のフィールドから構成されています。 「id」のフィールドには「primary_key=True」を指定して主キーにします。 この場合、「id」はSQLite3が自動的に生成します。 「date_update, date_added」のフィールドには関数名「get_japan_time」を指定しています。 関数を指定するときはカッコ「()」を不要です。 ちなみに、世界標準時間を格納するときは行27のように記述します。

    行30-31ではSQLite3のデータベース「todo.db」が存在するかどうかチェックしています。 データベースが存在しないときは、dbオブジェクトのcreate_all()メソッドを呼び出してSQLite3のデータベース「todo.db」を作成します。

    行34-39では関数「home()」を定義しています。 この関数ではSQLite3のデータベース「todo.db」からテーブル「Todo」に格納されている全てのレコードを抽出してホームページ「home1.html」に渡します。 行38ではdb.sessionのquery(Todo).all()メソッドで「Todo」テーブルのすべてのレコードを抽出して変数「todo_list」に格納します。 このとき内部的には、SQLite3の「Todo」テーブルに対してSQLの「SELECT * FROM Todo」が実行されます。

    行42-48では関数「add()」を定義しています。 この関数ではホームページ「home1.html」から入力したToDoリストの入力データ「title」を取得して、 SQLite3の「Todo」テーブルに追加します。 行44ではrequest.form.get()メソッドでホームページから入力したToDoリストのデータ「title」を取得して変数「title」に保存しています。 行45ではTodoクラスのインスタンスを生成して変数「new_todo」に保存しています。 Todoクラスの引数には「titile, complete」を指定しています。 行46ではdb.sessionのadd()メソッドでTodoクラスのオブジェクトを追加しています。 つまり、「Todo」テーブルに新規レコードを追加しています。 内部的にはSQLの「INSERT INTO Todo (title, complete) VALUES(?, ?)」が実行されます。 行47ではdb.sessionのcommit()メソッドでディスクに反映しています。 行48ではredirect()メソッドでホームページを呼び出しています。 つまり、行37-39の関数「home()」が呼び出されます。 関数「home()」ではSQLite3の「Todo」テーブルの全てのレコードを抽出してホームページに表示します。 url_for()メソッドは物理的なURLを生成します。

    行51-57では関数「update()」を定義しています。 この関数ではSQLite3の「Todo」テーブルの特定レコードの「complete, date_updated」フィールドを更新します。 更新するレコードのid「todo_id」は関数「update()」の引数として渡されます。 行53ではdb.session.query(Todo)のfilter()メソッドで「Todo」テーブルのレコードを絞り込んで先頭のレコード「first()」を変数「todo」に保存します。 変数「todo」にはTodoクラスのオブジェクトが格納されます。 filter()メソッドの引数には更新レコードのid(Todo.id)を指定しています。 行54-55ではTodoクラスの「complete, date_updated」属性を更新しています。 つまり「Todo」テーブルのレコードのフィールド「complete, date_updated」を更新しています。 内部的にはSQLの「UPDATE Todo SET completed = ?, date_updated = ?, WHERE id = ?)が実行されます。 行56ではdb.sessionのcommit()メソッドでディスクに反映しています。 行57ではFlaskのredirect()メソッドで「home」を呼び出しています。 この場合関数「home()」が実行されます。 関数「home()」ではSQLite3の「Todo」テーブルの全てのレコードを抽出してホームページに表示します。 url_for()メソッドは物理的なURLを生成します。

    行61-65では関数「delete()」を定義しています。 この関数ではSQLite3の「Todo」テーブルから特定のレコードを削除します。 削除するレコードのid「todo_id」は関数「delete()」の引数として渡されます。 行62ではdb.session.query(Todo)のfilter()メソッドで「Todo」テーブルのレコードを絞り込んで先頭のレコード「first()」を変数「todo」に保存します。 変数「todo」にはTodoクラスのオブジェクトが格納されます。 filter()メソッドの引数には削除レコードのid(Todo.id)を指定しています。 行63ではdb.sessionのdelete()メソッドで「Todo」テーブルから特定のレコードを削除します。 delete()メソッドの引数には削除するレコードのクラス(Todo)を指定します。 内部的にはSQLの「DELETE FROM Todo WHERE id = ?」が実行されます。 行64ではdb.sessionのcommit()メソッドでディスクに反映しています。 行65ではFlaskのredirect()メソッドで「home」を呼び出しています。 この場合関数「home()」が実行されます。 関数「home()」ではSQLite3の「Todo」テーブルの全てのレコードを抽出してホームページに表示します。 url_for()メソッドは物理的なURLを生成します。

    app.py:
    import os.path
    from datetime import datetime, timedelta
    from flask import Flask, render_template, request, redirect, url_for
    from flask_sqlalchemy import SQLAlchemy
    
    # set up sqlite3 database
    
    DB_FILENAME = 'todo.db'
    
    app = Flask(__name__)   # create a Flask instance
    
    app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_FILENAME}'	
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    db = SQLAlchemy(app)
    
    # define helper function : convert utc to jp time
    def get_japan_time():
        return datetime.utcnow() + timedelta(hours=9)
    
    # define sqlite3 table class : Todo
    class Todo(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        title = db.Column(db.String(100))
        complete = db.Column(db.Boolean)
        date_updated = db.Column(db.DateTime, default=get_japan_time)
        date_created = db.Column(db.DateTime, default=get_japan_time)
        # date_created = db.Column(db.DateTime, default=datetime.utcnow)
    
    # sqlite3 database does not exist ?
    if not os.path.isfile(DB_FILENAME):
        db.create_all() # create db
    
    # @app.get('/')
    @app.route('/')
    @app.route('/home')
    @app.route('/index')
    def home():
        todo_list = db.session.query(Todo).all()
        return render_template('home1.html', todo_list=todo_list)
    
    # @app.post('/add')
    @app.route('/add', methods=['POST'])
    def add():
        title = request.form.get('title')
        new_todo = Todo(title=title, complete=False)
        db.session.add(new_todo)
        db.session.commit()
        return redirect(url_for('home'))
    
    # @app.get('/update/<int:todo_id>')
    @app.route('/update/<int:todo_id>', methods=['GET'])
    def update(todo_id):
        todo = db.session.query(Todo).filter(Todo.id == todo_id).first()    
        todo.complete = not todo.complete
        todo.date_updated = get_japan_time()
        db.session.commit()
        return redirect(url_for('home'))
    
    # @app.get('/delete/<int:todo_id>')
    @app.route('/delete/<int:todo_id>', methods=['GET'])
    def delete(todo_id):
        todo = db.session.query(Todo).filter(Todo.id == todo_id).first()
        db.session.delete(todo)
        db.session.commit()
        return redirect(url_for('home'))
    
    if __name__ == '__main__':
        app.run(debug=True)

  7. Webページとデータベースを連動させる

    ここではToDoリストのHTMLの雛形をデータベースと連動させます。 前出のToDoリストの雛形「home0.html」を行1-69のように書き換えたら「home1.html」の名前で保存します。

    HTMLファイルにサーバー側で実行するコードを記述するにはJinja2の書式で記述します。 ステートメントを記述するときは「{%...%}」、変数などを記述するときは「{{...}}」のように記述します。 コメントは「{#...#}」のように記述します。

    行16-19ではHTMLのformタグを配置しています。 form要素のaction属性には「/add」、method属性には「post」を指定しています。 この場合行18の「Add(登録)」ボタンをクリックするとホームページがポストバックされてapp.pyの関数「add()」が実行されます。 form要素のmethod属性に「post」を指定しているので、ルートデコレータのmethodsには「POST」を指定します。 関数「add()」でテキストボックスに入力したデータを取得するには、「request.form.get('title')」のように記述します。 get()メソッドの引数には、行17のinput(text)タグのname属性に設定した名前「title」を指定します。

    行31-53のforループでは「todo_list」に格納されてTodoクラスを取得して処理しています。 変数「todo」にはTodoクラスのオブジェクトが格納されます。 Todoクラスは「id, title, completed, date_updated, date_created」の属性(プロパティ)から構成されています。

    行34-41はTodoクラスのcompleted属性がTrueのとき実行されます。 行34ではTodoクラスの「id」属性の値を表示しています。 行36ではTodoクラスの「title(タイトル)」属性の値を表示しています。 行37-38ではTodoクラスの「id」属性の値を表示しています。 anchorタグのhref属性には「/update/1」、「/delete/1」のようにTodoクラスのidが挿入されます。 リンクをクリック(※)するとサーバー側でapp.pyプログラムの関数「update(1)」「delete(1)」が実行されます。 anchorタグのリンクをクリックしてサーバー側に移動したときはルートデコレータのmethodsに「GET」を指定します。 「GET」はデフォルトなので省略できます。 行40-41ではTodoクラスの「date_created(作成日), date_updated(更新日)」属性の値を表示しています。 date_created, date_updatedには作成日、更新日がdatetime型で格納されています。 ここではPythonのstrftime()メソッドで日付・時間の書式を設定しています。

    ※ToDoリストが完了したときは[Update]ボタンのclass属性に「disabled」を追加してクリックできないようにしています。

    行43-50はTodoクラスのcompleted属性がFalseのとき実行されます。 行43ではTodoクラスの「id」属性の値を表示しています。 行45ではTodoクラスの「title(タイトル)」属性の値を表示しています。 行46-47ではTodoクラスの「id」属性の値を表示しています。 anchorタグのhref属性には「/update/1」、「/delete/1」のようにTodoクラスのidが挿入されます。 リンクをクリックするとサーバー側でapp.pyプログラムの関数「update(1)」「delete(1)」が実行されます。 anchorタグのリンクをクリックしてサーバー側に移動したときはルートデコレータのmethodsに「GET」を設定します。 「GET」はデフォルトなので省略できます。 行49ではTodoクラスの「date_created(作成日)」属性の値を表示しています。

    templates/home1.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
    	:::   
    </head>
    <body> 
        <header>  
    	:::
        </header>
    
        <main class="mt-5">
            <div class="container pt-5">            
                <h1 class="text-center">🎉Flask ToDo App📝</h1>
                <h2 class="text-center fs-6"> (ToDo|Task|備忘録 ウェブアプリ)</h2>
                <hr />
                <form class="d-flex" action="/add" method="post">
                    <input class="form-control me-2" type="text" name="title" placeholder="Enter to do... (ToDOリストを入力してください...)" aria-label="Add">
                    <button class="btn btn-primary" style="width: 150px;" type="submit">Add (登録)</button>                  
                </form>
                <hr />
                <table class="table">
                    <thead>
                      <tr>
                        <th scope="col">#</th>
                        <th scope="col">Title<br />(タイトル)</th>
                        <th scope="col">Created<br />(作成日)</th>
                        <th scope="col">Updated<br />(更新日)</th>
                      </tr>
                    </thead>
                    <tbody>
                        {% for todo in todo_list %} 
                            <tr>
                                {% if todo.complete %} 
                                    <th class="text-center" scope="row">{{ todo.id }}<br>✅</th>
                                    <td>
                                        <span class="fs-5">{{ todo.title }}</span><br />
                                        <a class="btn btn-primary btn-sm mx-2 disabled" href="/update/{{ todo.id }}">Update (完了)</a>                        
                                        <a class="btn btn-danger btn-sm mx-2" href="/delete/{{ todo.id }}">Delete (削除)</a>                           
                                    </td>
                                    <td>{{ todo.date_created.strftime('%Y/%m/%d %H:%M:%S') }}</td>
                                    <td>{{ todo.date_updated.strftime('%Y/%m/%d %H:%M:%S') }}</td>
                                {% else %}
                                    <th class="text-center" scope="row">{{ todo.id }}<br>⬛</th>
                                    <td>
                                        <span class="fs-5">{{ todo.title }}</span><br />
                                        <a class="btn btn-primary btn-sm mx-2" href="/update/{{ todo.id }}">Update (未完▶完了)</a> 
                                        <a class="btn btn-danger btn-sm mx-2" href="/delete/{{ todo.id }}">Delete (削除)</a>                        
                                    </td>
                                    <td>{{ todo.date_created.strftime('%Y/%m/%d %H:%M:%S') }}</td>
                                    <td></td>
                                {% endif %}
                            </tr>                    
                        {% endfor %}                     
                    </tbody>
                </table>
            </div>         
        </main>
    
        <div class="invisible" style="height: 40px; border: 1px solid orangered">
            margin to fix fixed-bottom
        </div>   
    
        <footer class="page-footer fixed-bottom font-small bg-dark py-1">
    	:::
        </footer>
    
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>    
    </body>
    </html>

  8. ToDoリストのWebアプリを起動する

    ここではFlask経由でToDoリストのWebアプリを起動して実際にデータを入力してみます。


    click image to zoom!
    図8-1
    VS CodeのTERMINALウィンドウから「flask run」を入力して実行します。 [Ctrl]を押しながら「http://127.0.0.1:5000」をクリックしてブラウザにWebアプリを表示します。


    click image to zoom!
    図8-2
    ブラウザにToDoリストのWebアプリが表示されました。 SQLite3のデータベースが空なのでToDoリストは0件になっています。


    click image to zoom!
    図8-3
    ToDoリストのテキストボックスにデータを入力したら[Add(登録)]ボタンをクリックして登録します。 ここでは2件のデータを既に入力しています。


    click image to zoom!
    図8-4
    ToDoリストが完了したときは[Update(未完▶完了)]ボタンをクリックします。 Webページが更新されてチェックマーク✅が表示されます。 ToDoリストが完了すると[Update]ボタンが無効になります。 [Delete(削除)]ボタンをクリックすると該当するToDoリストをデータベースから削除します。


    click image to zoom!
    図8-5
    図8-5ではブラウザの横幅を狭めてスマホに表示したような状態にしています。 メニューが消えて代わりにハンバーガーボタンが表示されます。


    click image to zoom!
    図8-6
    ハンバーガーボタンをクリックするとメニューが表示されます。


  9. HTMLファイルの全てを掲載


    templates/home1.html:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Flask ToDo App</title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">     
    </head>
    <body> 
        <header>  
            <nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-dark">    
                <div class="container-fluid">
                    <a class="navbar-brand" href="#">📝</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
                        <span class="navbar-toggler-icon"></span>
                    </button>
                    <div class="collapse navbar-collapse" id="navbarContent">
                        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                            <li class="nav-item">
                                <a class="nav-link active" aria-current="page" href="#">Home</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="#">Goal</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="#">Philosophy</a>
                            </li>                                                          
                            <li class="nav-item dropdown">
                                <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                    GO
                                </a>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" href="#">Action 1</a></li>
                                    <li><a class="dropdown-item" href="#">Action 2</a></li>
                                    <li><hr class="dropdown-divider"></li>
                                    <li><a class="dropdown-item" href="#">Action 9</a></li>
                                </ul>
                            </li>
                        </ul>
                        <form class="d-flex" role="search">
                            <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
                            <button class="btn btn-outline-success" type="submit">Search</button>
                        </form>
                    </div>
                </div>
            </nav>
        </header>
    
        <main class="mt-5">
            <div class="container pt-5">            
                <h1 class="text-center">🎉Flask ToDo App📝</h1>
                <h2 class="text-center fs-6"> (ToDo|Task|備忘録 ウェブアプリ)</h2>
                <hr />
                <form class="d-flex" action="/add" method="post">
                    <input class="form-control me-2" type="text" name="title" placeholder="Enter to do... (ToDOリストを入力してください...)" aria-label="Add">
                    <button class="btn btn-primary" style="width: 150px;" type="submit">Add (登録)</button>                  
                </form>
                <hr />
                <table class="table">
                    <thead>
                      <tr>
                        <th scope="col">#</th>
                        <th scope="col">Title<br />(タイトル)</th>
                        <th scope="col">Created<br />(作成日)</th>
                        <th scope="col">Updated<br />(更新日)</th>
                      </tr>
                    </thead>
                    <tbody>
                        {% for todo in todo_list %} 
                            <tr>
                                {% if todo.complete %} 
                                    <th class="text-center" scope="row">{{ todo.id }}<br>✅</th>
                                    <td>
                                        <span class="fs-5">{{ todo.title }}</span><br />
                                        <a class="btn btn-primary btn-sm mx-2 disabled" href="/update/{{ todo.id }}">Update (完了)</a>                        
                                        <a class="btn btn-danger btn-sm mx-2" href="/delete/{{ todo.id }}">Delete (削除)</a>                           
                                    </td>
                                    <td>{{ todo.date_created.strftime('%Y/%m/%d %H:%M:%S') }}</td>
                                    <td>{{ todo.date_updated.strftime('%Y/%m/%d %H:%M:%S') }}</td>
                                {% else %}
                                    <th class="text-center" scope="row">{{ todo.id }}<br>⬛</th>
                                    <td>
                                        <span class="fs-5">{{ todo.title }}</span><br />
                                        <a class="btn btn-primary btn-sm mx-2" href="/update/{{ todo.id }}">Update (未完▶完了)</a> 
                                        <a class="btn btn-danger btn-sm mx-2" href="/delete/{{ todo.id }}">Delete (削除)</a>                        
                                    </td>
                                    <td>{{ todo.date_created.strftime('%Y/%m/%d %H:%M:%S') }}</td>
                                    <td></td>
                                {% endif %}
                            </tr>                    
                        {% endfor %}                     
                    </tbody>
                </table>
            </div>         
        </main>
    
        <div class="invisible" style="height: 40px; border: 1px solid orangered">
            margin to fix fixed-bottom
        </div>   
    
        <footer class="page-footer fixed-bottom font-small bg-dark py-1">
            <div class="container text-center">                
              <span class="text-muted">  
                Copyright &copy; <a class="text-reset text-decoration-none" href="https://money-or-ikigai.com/" target="_blank">Akio Kasai</a>
              </span>          
            </div>
        </footer>
    
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>    
    </body>
    </html>