如何使用Flask和SQLite修改一对多数据库关系中的项目

来自菜鸟教程
跳转至:导航、​搜索

作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。

介绍

Flask 是一个使用 Python 语言构建 Web 应用程序的框架,而 SQLite 是一个数据库引擎,可以与 Python 一起使用来存储应用程序数据。 在本教程中,您将修改使用 Flask 和 SQLite 构建的具有一对多关系的应用程序中的项目。

本教程是如何使用Flask和SQLite的一对多数据库关系的续篇。 遵循它之后,您已经成功创建了一个 Flask 应用程序来管理待办事项、组织列表中的项目以及将新项目添加到数据库中。 在本教程中,您将添加将待办事项标记为已完成、编辑和删除项目以及将新列表添加到数据库的功能。 在本教程结束时,您的应用程序将包含已完成的待办事项的编辑和删除按钮以及删除线。

先决条件

在开始遵循本指南之前,您需要:

第 1 步 — 设置 Web 应用程序

在此步骤中,您将设置待办事项应用程序以准备进行修改。 如果您按照先决条件部分中的教程进行操作,并且本地计算机中仍有代码和虚拟环境,则可以跳过此步骤。

先用Git克隆上一篇教程代码的仓库:

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

导航到 flask-todo

cd flask-todo

然后创建一个新的虚拟环境:

python -m venv env

激活环境:

source env/bin/activate

安装烧瓶:

pip install Flask

然后,使用 init_db.py 程序初始化数据库:

python init_db.py

接下来,设置以下环境变量:

export FLASK_APP=app
export FLASK_ENV=development

FLASK_APP表示您当前正在开发的应用程序,本例为app.pyFLASK_ENV 指定模式——将其设置为 development 用于开发模式,这将允许您调试应用程序。 (切记不要在生产环境中使用此模式。)

然后运行开发服务器:

flask run

如果您使用浏览器,您将在 http://127.0.0.1:5000/ 的以下 URL 上运行应用程序。

要关闭开发服务器,请使用 CTRL + C 组合键。

接下来,您将修改应用程序以添加将项目标记为完成的功能。

第 2 步 — 将待办事项标记为已完成

在此步骤中,您将添加一个按钮以将每个待办事项标记为已完成。

为了能够将项目标记为完成,您将在数据库中的 items 表中添加一个新列,以便为每个项目添加一个标记,以便您知道它是否已完成,然后您将创建一个app.py 文件中的新路由,以根据用户的操作更改此列的值。

提醒一下,items 表中的列当前如下:

  • id:物品的ID。
  • list_id:项目所属列表的ID。
  • created:项目的创建日期。
  • content:项目的内容。

首先打开schema.sql修改items表:

nano schema.sql

items 表中添加一个名为 done 的新列:

flask_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)
);

保存并关闭文件。

这个新列将保存整数值 01; 值 0 表示布尔值 false1 表示值 true。 默认值为 0,这意味着您添加的任何新项目将自动未完成,直到用户将项目标记为完成,在这种情况下,done 列的值将更改为 1

然后,使用 init_db.py 程序再次初始化数据库,以应用您在 schema.sql 上执行的修改:

python init_db.py

接下来打开app.py进行修改:

nano app.py

您将获取项目的 idindex() 函数中的 done 列的值,该函数从数据库中获取列表和项目并将它们发送到index.html 文件用于显示。 以下文件中突出显示了对 SQL 语句的必要更改:

烧瓶待办事项/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 获取待办事项的 ID,使用 i.done 获取 done 列的值。

要了解此更改,请打开 list_example.py,这是一个小型示例程序,可用于了解数据库的内容:

nano list_example.py

对 SQL 语句进行与之前相同的修改,然后将最后一个 print() 函数更改为显示项目 ID 和 done 的值:

flask_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/

烧瓶待办事项/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 的元组以安全地将数据插入数据库。 然后提交事务并关闭连接并重定向到索引页面。

添加路由以将项目标记为已完成后,您需要另一个路由来撤消此操作并将项目返回到未完成状态。 在文件末尾添加以下路由:

烧瓶待办事项/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()视图功能与do()功能完全相同,只是你设置了done的值到 0 而不是 1

保存并关闭 app.py 文件。

您现在需要一个按钮来根据项目的状态将待办事项标记为已完成或未完成,打开 index.html 模板文件:

nano templates/index.html

<ul> 元素内部的 for 循环的内容更改为如下所示:

flask_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 循环中,如果项目被标记为已完成,则可以为 text-decoration 属性使用 line-through CSS 值,这可以从 [X154X 的值中得知]。 然后使用 Jinja 语法 set 来声明两个变量,URLBUTTON。 如果项目未标记为已完成,则按钮将具有值 Do 并且 URL 将指向 /do/ 路由,如果项目被标记为已完成,则按钮将具有Undo 的值并将指向 /undo/。 之后,您在 input 表单中使用这两个变量,该表单根据项目的状态提交正确的请求。

运行服务器:

flask run

您现在可以在索引页 http://127.0.0.1:5000/ 上将项目标记为已完成。 接下来,您将添加编辑待办事项的功能。

第 3 步 — 编辑待办事项

在此步骤中,您将添加一个用于编辑项目的新页面,以便您可以修改每个项目的内容并将项目分配给不同的列表。

您将向 app.py 文件添加一个新的 /edit/ 路由,这将呈现一个新的 edit.html 页面,用户可以在其中修改现有项目。 您还将更新 index.html 文件以将 Edit 按钮添加到每个项目。

首先,打开app.py文件:

nano app.py

然后在文件末尾添加以下路由:

烧瓶待办事项/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 参数来获取要编辑的待办事项的 ID、它所属列表的 ID、done 的值列、项目的内容和使用 SQL JOIN 的列表标题。 将此数据保存在 todo 变量中。 然后,您从数据库中获取所有待办事项列表并将它们保存在 lists 变量中。

如果请求是普通的GET请求,条件if request.method == 'POST'不运行,所以应用程序执行最后一个render_template()函数,同时传递todolistsedit.html 文件。

但是,如果提交了表单,则条件 request.method == 'POST' 变为 true,在这种情况下,您提取用户提交的内容和列表标题。 如果未提交任何内容,则闪烁消息 Content is required! 并重定向到同一编辑页面。 否则,您获取用户提交的列表的 ID; 这允许用户将待办事项从一个列表移动到另一个列表。 然后,您使用 UPDATE SQL 语句将待办事项的内容设置为用户提交的新内容。 您对列表 ID 执行相同操作。 最后,您提交更改并关闭连接,并将用户重定向到索引页面。

保存并关闭文件。

要使用这条新路线,您需要一个名为 edit.html 的新模板文件:

nano templates/edit.html

将以下内容添加到这个新文件中:

flask_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'] }} 用于内容输入。 这表示该值将是待办事项的当前内容或用户在尝试提交表单失败时提交的内容。

对于列表选择表单,循环遍历 lists 变量,如果列表标题与存储在 request.form 对象中的标题相同(尝试失败),则设置该列表标题作为选定值。 否则,如果列表标题等于存储在 todo 变量中的标题,则将其设置为选定值。 这是待办事项在任何修改之前的当前列表标题; 然后显示其余选项而不显示 selected 属性。

保存并关闭文件。

然后,打开index.html添加一个Edit按钮:

nano templates/index.html

使用 "row" 类更改 div 标记的内容以添加另一列,如下所示:

flask_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 步 — 删除待办事项

在此步骤中,您将添加删除特定待办事项的功能。

您首先需要添加一个新的 /delete/ 路由,打开 app.py

nano app.py

然后在文件末尾添加以下路由:

烧瓶待办事项/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 标签:

flask_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() 视图函数:

烧瓶待办事项/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

通过在以下代码中添加突出显示的标签来修改文件:

flask_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>

保存并关闭文件。

您添加了一个新的 <option> 标签来引用 New List 选项,这将允许用户指定他们想要创建一个新列表。 然后添加另一个 <div> 和一个名为 new_list 的输入字段,用户将在此字段中输入他们希望创建的新列表的标题。

最后,运行开发服务器:

flask run

然后访问索引页面:

http://127.0.0.1:5000/

该应用程序现在将如下所示:

随着应用程序的新增功能,用户现在可以将待办事项标记为已完成或将已完成的事项恢复为未完成状态,编辑和删除现有项目,并为不同类型的待办事项创建新列表。

您可以在 DigitalOcean Community Repository 中浏览应用程序的完整源代码。

结论

您现在拥有一个完整的待办事项应用程序,用户可以在其中创建新的待办事项、将项目标记为完成、编辑或删除现有项目,此外还可以创建新列表。 您已经修改了 Flask Web 应用程序,为其添加了新功能,并专门以一对多关系修改了数据库项目。 您可以通过学习 如何使用 Flask-Login 为您的应用添加身份验证来进一步开发此应用程序,以增加您的 Flask 应用程序的安全性。