FlaskおよびSQLiteとの1対多のデータベース関係でアイテムを変更する方法

提供:Dev Guides
移動先:案内検索

著者はCOVID-19救済基金を選択し、 Write forDOnationsプログラムの一環として寄付を受け取りました。

序章

Flaskは、Python言語を使用してWebアプリケーションを構築するためのフレームワークであり、SQLiteは、Pythonでアプリケーションデータを格納するために使用できるデータベースエンジンです。 このチュートリアルでは、1対多の関係でFlaskとSQLiteを使用して構築されたアプリケーションのアイテムを変更します。

このチュートリアルは、FlaskおよびSQLiteで1対多のデータベース関係を使用する方法の続きです。 それを実行した後、To Doアイテムを管理し、リスト内のアイテムを整理し、データベースに新しいアイテムを追加するためのFlaskアプリケーションを正常に作成しました。 このチュートリアルでは、To Doアイテムを完了としてマークしたり、アイテムを編集および削除したり、データベースに新しいリストを追加したりする機能を追加します。 チュートリアルの終わりまでに、アプリケーションには、完了したToDoの編集および削除ボタンと取り消し線が含まれます。

前提条件

このガイドに従う前に、次のものが必要です。

  • ローカルのPython3プログラミング環境については、 Python3シリーズのローカルプログラミング環境をインストールしてセットアップする方法のチュートリアルに従ってください。 このチュートリアルでは、プロジェクトディレクトリをflask_todoと呼びます。
  • (オプション)ステップ1 では、このチュートリアルで作業するToDoアプリケーションのクローンを作成するオプションがあります。 ただし、オプションでFlaskおよびSQLiteで1対多のデータベース関係を使用する方法を使用できます。 このページから最終コードにアクセスできます。
  • ルートの作成、HTMLテンプレートのレンダリング、SQLiteデータベースへの接続などの基本的なFlaskの概念の理解。 これらの概念に精通していない場合は、 Python 3 でFlaskを使用してWebアプリケーションを作成する方法、および Python3でsqlite3モジュールを使用する方法を確認してください。

ステップ1—Webアプリケーションのセットアップ

このステップでは、変更の準備ができるようにToDoアプリケーションを設定します。 前提条件セクションのチュートリアルに従い、ローカルマシンにコードと仮想環境が残っている場合は、この手順をスキップできます。

最初にGitを使用して、前のチュートリアルのコードのリポジトリのクローンを作成します。

git clone https://github.com/do-community/flask-todo

flask-todoに移動します。

cd flask-todo

次に、新しい仮想環境を作成します。

python -m venv env

環境をアクティブにします。

source env/bin/activate

Flaskをインストールします:

pip install Flask

次に、init_db.pyプログラムを使用してデータベースを初期化します。

python init_db.py

次に、次の環境変数を設定します。

export FLASK_APP=app
export FLASK_ENV=development

FLASK_APPは、現在開発中のアプリケーションを示します。この場合はapp.pyです。 FLASK_ENVはモードを指定します。開発モードの場合は、developmentに設定します。これにより、アプリケーションをデバッグできます。 (実稼働環境ではこのモードを使用しないでください。)

次に、開発サーバーを実行します。

flask run

ブラウザにアクセスすると、http://127.0.0.1:5000/の次のURLでアプリケーションが実行されます。

開発サーバーを閉じるには、CTRL + Cキーの組み合わせを使用します。

次に、アプリケーションを変更して、アイテムを完了としてマークする機能を追加します。

ステップ2—To-Doアイテムを完了としてマークする

このステップでは、各ToDo項目を完了としてマークするボタンを追加します。

アイテムを完了としてマークできるようにするには、データベースのitemsテーブルに新しい列を追加して、各アイテムのマーカーを設定し、アイテムが完了したかどうかを確認してから、 app.pyファイルの新しいルート。ユーザーのアクションに応じて、この列の値を変更します。

念のため、itemsテーブルの列は現在次のとおりです。

  • id:アイテムのID。
  • list_id:アイテムが属するリストのID。
  • created:アイテムの作成日。
  • content:アイテムのコンテンツ。

まず、schema.sqlを開いて、itemsテーブルを変更します。

nano schema.sql

doneという名前の新しい列をitemsテーブルに追加します。

フラスコ_todo/schema.sql

DROP TABLE IF EXISTS lists;
DROP TABLE IF EXISTS items;

CREATE TABLE lists (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    title TEXT NOT NULL
);

CREATE TABLE items (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    list_id INTEGER NOT NULL,
    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    content TEXT NOT NULL,
    done INTEGER NOT NULL DEFAULT 0,
    FOREIGN KEY (list_id) REFERENCES lists (id)
);

ファイルを保存して閉じます。

この新しい列は、整数値0または1を保持します。 値0はブール値falseを表し、1は値trueを表します。 デフォルトは0です。これは、ユーザーがアイテムに完了のマークを付けるまで、追加した新しいアイテムは自動的に未完成になることを意味します。完了のマークを付けると、done列の値が1

次に、init_db.pyプログラムを使用してデータベースを再度初期化し、schema.sqlで実行した変更を適用します。

python init_db.py

次に、変更のためにapp.pyを開きます。

nano app.py

アイテムのidと、index()関数のdone列の値をフェッチします。この関数は、データベースからリストとアイテムをフェッチして、表示用のindex.htmlファイル。 SQLステートメントに必要な変更は、次のファイルで強調表示されています。

フラスコ_todo/app.py

@app.route('/')
def index():
    conn = get_db_connection()
    todos = conn.execute('SELECT i.id, i.done, i.content, l.title \
                          FROM items i JOIN lists l \
                          ON i.list_id = l.id ORDER BY l.title;').fetchall()

    lists = {}

    for k, g in groupby(todos, key=lambda t: t['title']):
        lists[k] = list(g)

    conn.close()
    return render_template('index.html', lists=lists)

ファイルを保存して閉じます。

この変更により、i.idを使用してToDo項目のIDを取得し、i.doneを使用してdone列の値を取得します。

この変更を理解するには、list_example.pyを開きます。これは、データベースの内容を理解するために使用できる小さなサンプルプログラムです。

nano list_example.py

前と同じSQLステートメントの変更を実行してから、最後のprint()関数を変更して、アイテムIDとdoneの値を表示します。

フラスコ_todo/list_example.py

from itertools import groupby
from app import get_db_connection

conn = get_db_connection()

todos = conn.execute('SELECT i.id, i.done, i.content, l.title \
                      FROM items i JOIN lists l \
                      ON i.list_id = l.id ORDER BY l.title;').fetchall()

lists = {}

for k, g in groupby(todos, key=lambda t: t['title']):
    lists[k] = list(g)

for list_, items in lists.items():
    print(list_)
    for item in items:
        print('    ', item['content'], '| id:',
              item['id'], '| done:', item['done'])

ファイルを保存して終了します。

サンプルプログラムを実行します。

python list_example.py

出力は次のとおりです。

OutputHome
     Buy fruit | id: 2 | done: 0
     Cook dinner | id: 3 | done: 0
Study
     Learn Flask | id: 4 | done: 0
     Learn SQLite | id: 5 | done: 0
Work
     Morning meeting | id: 1 | done: 0

どのアイテムも完了としてマークされていないため、各アイテムのdoneの値は0であり、これはfalseを意味します。 ユーザーがこの値を変更してアイテムを完了としてマークできるようにするには、app.pyファイルに新しいルートを追加します。

app.pyを開きます:

nano app.py

ファイルの最後にルート/do/を追加します。

フラスコ_todo/app.py

. . .
@app.route('/<int:id>/do/', methods=('POST',))
def do(id):
    conn = get_db_connection()
    conn.execute('UPDATE items SET done = 1 WHERE id = ?', (id,))
    conn.commit()
    conn.close()
    return redirect(url_for('index'))

この新しいルートは、POSTリクエストのみを受け入れます。 do()ビュー関数は、id引数を取ります。これは、完了としてマークするアイテムのIDです。 関数内でデータベース接続を開き、UPDATE SQLステートメントを使用して、done列の値を1に設定し、アイテムに完了のマークを付けます。 。

execute()メソッドで?プレースホルダーを使用し、IDを含むタプルを渡して、データベースにデータを安全に挿入します。 次に、トランザクションをコミットして接続を閉じ、インデックスページにリダイレクトします。

アイテムを完了としてマークするルートを追加した後、このアクションを元に戻し、アイテムを未完了のステータスに戻すための別のルートが必要です。 ファイルの最後に次のルートを追加します。

フラスコ_todo/app.py

. . .
@app.route('/<int:id>/undo/', methods=('POST',))
def undo(id):
    conn = get_db_connection()
    conn.execute('UPDATE items SET done = 0 WHERE id = ?', (id,))
    conn.commit()
    conn.close()
    return redirect(url_for('index'))

このルートは/do/ルートに似ており、undo()ビュー機能は、doneの値を設定することを除いて、do()機能とまったく同じです。 1の代わりに0に。

app.pyファイルを保存して閉じます。

アイテムの状態に応じて、やることアイテムを完了または未完了としてマークするためのボタンが必要になりました。index.htmlテンプレートファイルを開きます。

nano templates/index.html

<ul>要素内の内側のforループの内容を次のように変更します。

フラスコ_todo/templates / index.html

{% block content %}
    <h1>{% block title %} Welcome to FlaskTodo {% endblock %}</h1>
    {% for list, items in lists.items() %}
        <div class="card" style="width: 18rem; margin-bottom: 50px;">
            <div class="card-header">
                <h3>{{ list }}</h3>
            </div>
            <ul class="list-group list-group-flush">
                {% for item in items %}
                    <li class="list-group-item"
                    {% if item['done'] %}
                    style="text-decoration: line-through;"
                    {% endif %}
                    >{{ item['content'] }}
                    {% if not item ['done'] %}
                        {% set URL = 'do' %}
                        {% set BUTTON = 'Do' %}
                    {% else %}
                        {% set URL = 'undo' %}
                        {% set BUTTON = 'Undo' %}
                    {% endif %}



                  <div class="row">
                        <div class="col-12 col-md-3">
                            <form action="{{ url_for(URL, id=item['id']) }}"
                                method="POST">
                                <input type="submit" value="{{ BUTTON }}"
                                    class="btn btn-success btn-sm">
                            </form>
                        </div>
                    </div>
                    </li>
                {% endfor %}
            </ul>
        </div>
    {% endfor %}
{% endblock %}

このforループでは、item['done']の値からわかるように、アイテムが完了としてマークされている場合、text-decorationプロパティにline-throughCSS値を使用します。 ]。 次に、Jinja構文setを使用して、URLBUTTONの2つの変数を宣言します。 アイテムが完了としてマークされていない場合、ボタンの値は Do になり、URLは/do/ルートに転送されます。アイテムが完了としてマークされている場合、ボタンには元に戻すの値で、/undo/を指します。 その後、アイテムの状態に応じて適切なリクエストを送信するinputフォームでこれらの変数の両方を使用します。

サーバーを実行します。

flask run

インデックスページhttp://127.0.0.1:5000/でアイテムに完了のマークを付けることができるようになりました。 次に、やること項目を編集する機能を追加します。

ステップ3—To-Doアイテムの編集

このステップでは、アイテムを編集するための新しいページを追加して、各アイテムのコンテンツを変更し、アイテムを別のリストに割り当てることができるようにします。

新しい/edit/ルートをapp.pyファイルに追加します。これにより、ユーザーが既存のアイテムを変更できる新しいedit.htmlページがレンダリングされます。 また、index.htmlファイルを更新して、各アイテムにEditボタンを追加します。

まず、app.pyファイルを開きます。

nano app.py

次に、ファイルの最後に次のルートを追加します。

フラスコ_todo/app.py

. . .
@app.route('/<int:id>/edit/', methods=('GET', 'POST'))
def edit(id):
    conn = get_db_connection()

    todo = conn.execute('SELECT i.id, i.list_id, i.done, i.content, l.title \
                         FROM items i JOIN lists l \
                         ON i.list_id = l.id WHERE i.id = ?', (id,)).fetchone()

    lists = conn.execute('SELECT title FROM lists;').fetchall()

    if request.method == 'POST':
        content = request.form['content']
        list_title = request.form['list']

        if not content:
            flash('Content is required!')
            return redirect(url_for('edit', id=id))

        list_id = conn.execute('SELECT id FROM lists WHERE title = (?);',
                                 (list_title,)).fetchone()['id']

        conn.execute('UPDATE items SET content = ?, list_id = ?\
                      WHERE id = ?',
                     (content, list_id, id))
        conn.commit()
        conn.close()
        return redirect(url_for('index'))

    return render_template('edit.html', todo=todo, lists=lists)

この新しいビュー関数では、id引数を使用して、編集するTo DoアイテムのID、それが属するリストのID、doneの値をフェッチします。列、アイテムのコンテンツ、およびSQLJOINを使用したリストタイトル。 このデータはtodo変数に保存します。 次に、データベースからすべてのTo Doリストを取得し、それらをlists変数に保存します。

リクエストが通常のGETリクエストの場合、条件if request.method == 'POST'は実行されないため、アプリケーションは最後のrender_template()関数を実行し、todolistsの両方を渡します。 edit.htmlファイルに。

ただし、フォームが送信された場合、条件request.method == 'POST'trueになります。この場合、ユーザーが送信したコンテンツとリストタイトルを抽出します。 コンテンツが送信されなかった場合は、メッセージContent is required!をフラッシュして、同じ編集ページにリダイレクトします。 それ以外の場合は、ユーザーが送信したリストのIDを取得します。 これにより、ユーザーはToDoアイテムをあるリストから別のリストに移動できます。 次に、UPDATE SQLステートメントを使用して、やること項目のコンテンツをユーザーが送信した新しいコンテンツに設定します。 リストIDについても同じようにします。 最後に、変更をコミットして接続を閉じ、ユーザーをインデックスページにリダイレクトします。

ファイルを保存して閉じます。

この新しいルートを使用するには、edit.htmlという新しいテンプレートファイルが必要です。

nano templates/edit.html

この新しいファイルに次の内容を追加します。

フラスコ_todo/templates / edit.html

{% extends 'base.html' %}

{% block content %}

<h1>{% block title %} Edit an Item {% endblock %}</h1>

<form method="post">
    <div class="form-group">
        <label for="content">Content</label>
        <input type="text" name="content"
               placeholder="Todo content" class="form-control"
               value="{{ todo['content'] or request.form['content'] }}"></input>
    </div>

    <div class="form-group">
        <label for="list">List</label>
        <select class="form-control" name="list">
            {% for list in lists %}
                {% if list['title'] == request.form['list'] %}
                    <option value="{{ request.form['list'] }}" selected>
                        {{ request.form['list'] }}
                    </option>

                {% elif list['title'] == todo['title'] %}
                    <option value="{{ todo['title'] }}" selected>
                        {{ todo['title'] }}
                    </option>

                {% else %}
                    <option value="{{ list['title'] }}">
                        {{ list['title'] }}
                    </option>
                {% endif %}
            {% endfor %}
        </select>
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>
{% endblock %}

コンテンツ入力には値{{ todo['content'] or request.form['content'] }}を使用します。 これは、値がTo Doアイテムの現在のコンテンツか、フォームの送信に失敗したときにユーザーが送信した内容のいずれかになることを意味します。

リスト選択フォームでは、lists変数をループし、リストのタイトルがrequest.formオブジェクトに格納されているものと同じである場合(失敗した試行から)、そのリストを設定します選択した値としてのタイトル。 それ以外の場合、リストのタイトルがtodo変数に格納されているものと等しい場合は、それを選択した値として設定します。 これは、変更前のToDo項目の現在のリストタイトルです。 残りのオプションは、selected属性なしで表示されます。

ファイルを保存して閉じます。

次に、index.htmlを開いて、Editボタンを追加します。

nano templates/index.html

divタグの内容を"row"クラスに変更して、次のように別の列を追加します。

フラスコ_todo/templates / index.html

. . .
<div class="row">
    <div class="col-12 col-md-3">
        <form action="{{ url_for(URL, id=item['id']) }}"
            method="POST">
            <input type="submit" value="{{ BUTTON }}"
                class="btn btn-success btn-sm">
        </form>
    </div>
    <div class="col-12 col-md-3">
        <a class="btn btn-warning btn-sm"
        href="{{ url_for('edit', id=item['id']) }}">Edit</a>
    </div>
</div>

ファイルを保存して閉じます。

これは標準リンクタグ関連するを指します/edit/各アイテムのルート。

まだ実行していない場合は、サーバーを実行します。

flask run

これで、インデックスページhttp://127.0.0.1:5000/に移動して、やること項目の変更を試すことができます。 次のステップでは、アイテムを削除するためのボタンを追加します。

ステップ4—ToDoアイテムの削除

このステップでは、特定のToDoアイテムを削除する機能を追加します。

まず、新しい/delete/ルートを追加し、app.pyを開く必要があります。

nano app.py

次に、ファイルの最後に次のルートを追加します。

フラスコ_todo/app.py

. . .
@app.route('/<int:id>/delete/', methods=('POST',))
def delete(id):
    conn = get_db_connection()
    conn.execute('DELETE FROM items WHERE id = ?', (id,))
    conn.commit()
    conn.close()
    return redirect(url_for('index'))

ファイルを保存して閉じます。

delete()ビュー関数は、id引数を受け入れます。 POST要求が送信されたら、DELETE SQLステートメントを使用して、一致するid値を持つアイテムを削除し、トランザクションをコミットしてデータベース接続を閉じます。インデックスページに戻ります。

次に、templates/index.htmlを開いて、Deleteボタンを追加します。

nano templates/index.html

Editボタンの下に次の強調表示されたdivタグを追加します。

フラスコ_todo/templates / index.html

<div class="row">
    <div class="col-12 col-md-3">
        <form action="{{ url_for(URL, id=item['id']) }}"
            method="POST">
            <input type="submit" value="{{ BUTTON }}"
                class="btn btn-success btn-sm">
        </form>
    </div>

    <div class="col-12 col-md-3">
        <a class="btn btn-warning btn-sm"
        href="{{ url_for('edit', id=item['id']) }}">Edit</a>
    </div>

    <div class="col-12 col-md-3">
        <form action="{{ url_for('delete', id=item['id']) }}"
            method="POST">
            <input type="submit" value="Delete"
                class="btn btn-danger btn-sm">
        </form>
    </div>
</div>

この新しい送信ボタンは、各アイテムの/delete/ルートにPOSTリクエストを送信します。

ファイルを保存して閉じます。

次に、開発サーバーを実行します。

flask run

インデックスページに移動して、新しいDeleteボタンを試してください。これで、必要なアイテムを削除できます。

既存のやること項目を削除する機能を追加したので、次のステップで新しいリストを追加する機能を追加します。

ステップ5—新しいリストを追加する

これまでのところ、リストはデータベースから直接追加することしかできません。 このステップでは、既存のリストから選択するだけでなく、ユーザーが新しいアイテムを追加したときに新しいリストを作成する機能を追加します。 New Listという新しいオプションを組み込みます。このオプションを選択すると、ユーザーは作成する新しいリストの名前を入力できます。

まず、app.pyを開きます。

nano app.py

次に、if request.method == 'POST'条件に次の強調表示された行を追加して、create()ビュー機能を変更します。

フラスコ_todo/app.py

. . .
@app.route('/create/', methods=('GET', 'POST'))
def create():
    conn = get_db_connection()

    if request.method == 'POST':
        content = request.form['content']
        list_title = request.form['list']

        new_list = request.form['new_list']

        # If a new list title is submitted, add it to the database
        if list_title == 'New List' and new_list:
            conn.execute('INSERT INTO lists (title) VALUES (?)',
                         (new_list,))
            conn.commit()
            # Update list_title to refer to the newly added list
            list_title = new_list

        if not content:
            flash('Content is required!')
            return redirect(url_for('index'))

        list_id = conn.execute('SELECT id FROM lists WHERE title = (?);',
                                 (list_title,)).fetchone()['id']
        conn.execute('INSERT INTO items (content, list_id) VALUES (?, ?)',
                     (content, list_id))
        conn.commit()
        conn.close()
        return redirect(url_for('index'))

    lists = conn.execute('SELECT title FROM lists;').fetchall()

    conn.close()
    return render_template('create.html', lists=lists)

ファイルを保存して閉じます。

ここでは、new_listという新しいフォームフィールドの値を変数に保存します。 このフィールドは後でcreate.htmlファイルに追加します。 次に、list_title == 'New List' and new_list条件で、list_titleの値が'New List'であるかどうかを確認します。これは、ユーザーが新しいリストを作成することを希望していることを示します。 また、new_list変数の値がNoneでないことを確認します。この条件が満たされた場合は、INSERT INTO SQLステートメントを使用して、新しく送信されたリストタイトルをに追加します。 listsテーブル。 トランザクションをコミットしてから、list_title変数の値を更新して、後で使用するために新しく追加されたリストの値と一致させます。

次に、create.htmlを開いて、新しい<option>タグを追加し、ユーザーが新しいリストを追加できるようにします。

nano templates/create.html

次のコードで強調表示されたタグを追加して、ファイルを変更します。

フラスコ_todo/templates / create.html

    <div class="form-group">
        <label for="list">List</label>
        <select class="form-control" name="list">
            <option value="New List" selected>New List</option>
            {% for list in lists %}
                {% if list['title'] == request.form['list'] %}
                    <option value="{{ request.form['list'] }}" selected>
                        {{ request.form['list'] }}
                    </option>
                {% else %}
                    <option value="{{ list['title'] }}">
                        {{ list['title'] }}
                    </option>
                {% endif %}
            {% endfor %}
        </select>
    </div>

    <div class="form-group">
        <label for="new_list">New List</label>
        <input type="text" name="new_list"
                placeholder="New list name" class="form-control"
                value="{{ request.form['new_list'] }}"></input>
    </div>

    <div class="form-group">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>

ファイルを保存して閉じます。

New Listオプションを参照するために、新しい<option>タグを追加しました。これにより、ユーザーは新しいリストを作成することを指定できます。 次に、new_listという名前の入力フィールドを持つ別の<div>を追加します。このフィールドは、ユーザーが作成する新しいリストのタイトルを入力する場所です。

最後に、開発サーバーを実行します。

flask run

次に、インデックスページにアクセスします。

http://127.0.0.1:5000/

アプリケーションは次のようになります。

アプリケーションに新たに追加されたことで、ユーザーはTo Doアイテムを完了としてマークしたり、完了したアイテムを未完了状態に復元したり、既存のアイテムを編集および削除したり、さまざまな種類のToDoタスクの新しいリストを作成したりできるようになりました。

アプリケーションの完全なソースコードは、 DigitalOcean CommunityRepositoryで閲覧できます。

結論

これで、新しいリストを作成する機能に加えて、ユーザーが新しいTo Doアイテムを作成し、アイテムを完了としてマークし、既存のアイテムを編集または削除できる完全なToDoアプリケーションができました。 Flask Webアプリケーションを変更し、それに新しい機能を追加し、特に1対多の関係でデータベースアイテムを変更しました。 Flaskを使用してアプリに認証を追加する方法-ログインを学習して、Flaskアプリケーションにセキュリティを追加することで、このアプリケーションをさらに開発できます。