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

作者选择了 COVID-19 救援基金作为 Write for Donations计划的一部分接受捐款。

介绍

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

此教程是如何使用Flask和SQLite的One-to-Many Database Relationships的续集(https://andsky.com/tech/tutorials/how-to-use-one-to-many-database-relationships-with-flask-and-sqlite)。遵循后,您成功创建了一个Flask应用程序来管理要做项目,在列表中组织项目,并将新项目添加到数据库中。在本教程中,您将添加功能来标记要做项目作为完整,编辑和删除项目,并将新列表添加到数据库中。

Todo Application

前提条件

在你开始遵循这个指南之前,你将需要:

步骤 1 - 设置 Web 应用程序

在此步骤中,您将设置任务应用程序,以便为修改做好准备. 如果您遵循了前提部分中的教程,并且仍然在本地机器中有代码和虚拟环境,您可以跳过此步骤。

首先使用Git来克隆上一节教程的代码库:

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

导航至全瓶:

1cd flask-todo

创建一个新的虚拟环境:

1python -m venv env

激活环境:

1source env/bin/activate

安装瓶子:

1pip install Flask

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

1python init_db.py

然后,设置以下环境变量:

1export FLASK_APP=app
2export FLASK_ENV=development

FLASK_APP表示您目前正在开发的应用程序,在这种情况下是app.pyFLASK_ENV指定模式,将其设置为开发模式,这将允许您调试应用程序(记住不要在生产环境中使用这种模式)。

然后运行开发服务器:

1flask run

如果你去你的浏览器,你会看到应用程序在以下URL上运行在 http://127.0.0.1:5000/

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

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

步骤 2 – 标记要做项目为完整

在此步骤中,您将添加一个按钮,将每个任务标记为完成。

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

作为提醒,当前项目表中的列如下:

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

首先,打开schema.sql以更改项目表:

1nano schema.sql

项目表中添加一个名为完成的新列:

 1[label flask_todo/schema.sql]
 2DROP TABLE IF EXISTS lists;
 3DROP TABLE IF EXISTS items;
 4
 5CREATE TABLE lists (
 6    id INTEGER PRIMARY KEY AUTOINCREMENT,
 7    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
 8    title TEXT NOT NULL
 9);
10
11CREATE TABLE items (
12    id INTEGER PRIMARY KEY AUTOINCREMENT,
13    list_id INTEGER NOT NULL,
14    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
15    content TEXT NOT NULL,
16    done INTEGER NOT NULL DEFAULT 0,
17    FOREIGN KEY (list_id) REFERENCES lists (id)
18);

保存并关闭文件。

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

然后,使用init_db.py程序重新启动数据库,将您对schema.sql所做的更改应用:

1python init_db.py

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

1nano app.py

您将从index()函数中获取项目的id完成列的值,该函数从数据库中获取列表和项目,并将其发送到index.html文件中进行显示。

 1[label flask_todo/app.py]
 2@app.route('/')
 3def index():
 4    conn = get_db_connection()
 5    todos = conn.execute('SELECT i.id, i.done, i.content, l.title \
 6                          FROM items i JOIN lists l \
 7                          ON i.list_id = l.id ORDER BY l.title;').fetchall()
 8
 9    lists = {}
10
11    for k, g in groupby(todos, key=lambda t: t['title']):
12        lists[k] = list(g)
13
14    conn.close()
15    return render_template('index.html', lists=lists)

保存并关闭文件。

通过此更改,您可以使用i.id获取使用任务项目的 ID 和使用i.done完成列的值。

要了解这种变化,请打开list_example.py,这是一个小型的示例程序,您可以使用它来了解数据库的内容:

1nano list_example.py

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

 1[label flask_todo/list_example.py]
 2from itertools import groupby
 3from app import get_db_connection
 4
 5conn = get_db_connection()
 6
 7todos = conn.execute('SELECT i.id, i.done, i.content, l.title \
 8                      FROM items i JOIN lists l \
 9                      ON i.list_id = l.id ORDER BY l.title;').fetchall()
10
11lists = {}
12
13for k, g in groupby(todos, key=lambda t: t['title']):
14    lists[k] = list(g)
15
16for list_, items in lists.items():
17    print(list_)
18    for item in items:
19        print('    ', item['content'], '| id:',
20              item['id'], '| done:', item['done'])

保存和退出文件。

运行示例程序:

1python list_example.py

以下是产量:

1[secondary_label Output]
2Home
3     Buy fruit | id: 2 | done: 0
4     Cook dinner | id: 3 | done: 0
5Study
6     Learn Flask | id: 4 | done: 0
7     Learn SQLite | id: 5 | done: 0
8Work
9     Morning meeting | id: 1 | done: 0

没有一个项目被标记为完成,因此每个项目的完成值为0,这意味着

打开app.py:

1nano app.py

在文件的末尾添加一条路径 /do/:

1[label flask_todo/app.py]
2. . .
3@app.route('/<int:id>/do/', methods=('POST',))
4def do(id):
5    conn = get_db_connection()
6    conn.execute('UPDATE items SET done = 1 WHERE id = ?', (id,))
7    conn.commit()
8    conn.close()
9    return redirect(url_for('index'))

此新路线只接受POST请求。do()视图函数采用id参数,即您想要标记为完成的项目的ID。在该函数内,您打开数据库连接,然后使用UPDATE SQL 语句将完成列的值设置为1,以便该项目被标记为完成。

您在 execute()方法中使用 ? 定位符,并通过包含该 ID 的 Tuple 安全地将数据插入数据库,然后进行交易并关闭连接并重定向到索引页面。

添加路线以将项目标记为完成后,您需要另一个路线来取消此操作并将项目返回未完成状态。

1[label flask_todo/app.py]
2. . .
3@app.route('/<int:id>/undo/', methods=('POST',))
4def undo(id):
5    conn = get_db_connection()
6    conn.execute('UPDATE items SET done = 0 WHERE id = ?', (id,))
7    conn.commit()
8    conn.close()
9    return redirect(url_for('index'))

此路径类似于 /do/ 路径,而 undo() 视图函数与 do() 函数完全相同,除非您将 done 值设置为 0 而不是 1

保存并关闭app.py文件。

现在您需要一个按钮来标记完成未完成项目,取决于项目的状态,打开index.html模板文件:

1nano templates/index.html

更改for元素内部的内部循环的内容,以显示如下:

 1[label flask_todo/templates/index.html]
 2{% block content %}
 3    <h1>{% block title %} Welcome to FlaskTodo {% endblock %}</h1>
 4    {% for list, items in lists.items() %}
 5        <div class="card" style="width: 18rem; margin-bottom: 50px;">
 6            <div class="card-header">
 7                <h3>{{ list }}</h3>
 8            </div>
 9            <ul class="list-group list-group-flush">
10                {% for item in items %}
11                    <li class="list-group-item"
12                    {% if item['done'] %}
13                    style="text-decoration: line-through;"
14                    {% endif %}
15                    >{{ item['content'] }}
16                    {% if not item ['done'] %}
17                        {% set URL = 'do' %}
18                        {% set BUTTON = 'Do' %}
19                    {% else %}
20                        {% set URL = 'undo' %}
21                        {% set BUTTON = 'Undo' %}
22                    {% endif %}
23
24                  <div class="row">
25                        <div class="col-12 col-md-3">
26                            <form action="{{ url_for(URL, id=item['id']) }}"
27                                method="POST">
28                                <input type="submit" value="{{ BUTTON }}"
29                                    class="btn btn-success btn-sm">
30                            </form>
31                        </div>
32                    </div>
33                    </li>
34                {% endfor %}
35            </ul>
36        </div>
37    {% endfor %}
38{% endblock %}

在这个循环中,如果项目被标记为完成,则使用 CSS 值为通过行文本装饰属性,您可以从项目完成的值中知道。 然后使用 Jinja 语法设置来声明两个变量,即URLBUTTON。 如果项目没有被标记为完成,则按钮将具有值 Do,并将 URL 引导到/do/路线,如果项目被标记为完成,则按钮将具有值 Undo,并将指向/undo/. 然后,您将使用这些变量在输入表格中,根据项目状态提交正确的请求。

运行服务器:

1flask run

您现在可以将项目标记为已完成的索引页面 http://127.0.0.1:5000/. 接下来,您将添加编辑任务项目的能力。

步骤3 - 编辑要做的项目

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

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

首先,打开 app.py 文件:

1nano app.py

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

 1[label flask_todo/app.py]
 2. . .
 3@app.route('/<int:id>/edit/', methods=('GET', 'POST'))
 4def edit(id):
 5    conn = get_db_connection()
 6
 7    todo = conn.execute('SELECT i.id, i.list_id, i.done, i.content, l.title \
 8                         FROM items i JOIN lists l \
 9                         ON i.list_id = l.id WHERE i.id = ?', (id,)).fetchone()
10
11    lists = conn.execute('SELECT title FROM lists;').fetchall()
12
13    if request.method == 'POST':
14        content = request.form['content']
15        list_title = request.form['list']
16
17        if not content:
18            flash('Content is required!')
19            return redirect(url_for('edit', id=id))
20
21        list_id = conn.execute('SELECT id FROM lists WHERE title = (?);',
22                                 (list_title,)).fetchone()['id']
23
24        conn.execute('UPDATE items SET content = ?, list_id = ?\
25                      WHERE id = ?',
26                     (content, list_id, id))
27        conn.commit()
28        conn.close()
29        return redirect(url_for('index'))
30
31    return render_template('edit.html', todo=todo, lists=lists)

在此新视图函数中,您使用id参数获取您要编辑的任务项目的 ID、所属列表的 ID、完成列的值、项目的内容以及使用 SQLJOIN的列表标题。

如果请求是正常的 GET 请求,则条件 if request.method == 'POST' 不运行,因此应用程序执行最后一个 render_template()函数,将todo' 和 lists' 都传送到 edit.html` 文件中。

但是,如果提交了表格,则条件 request.method == 'POST' 变成了 true,在这种情况下,您提取了用户提交的内容和列表标题。如果没有提交的内容,您闪烁了消息 Content is required!`,并重定向到相同的编辑页面。否则,您收集了用户提交的列表的ID;这允许用户从一个列表移动到另一个列表。

保存并关闭文件。

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

1nano templates/edit.html

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

 1[label flask_todo/templates/edit.html]
 2{% extends 'base.html' %}
 3
 4{% block content %}
 5
 6<h1>{% block title %} Edit an Item {% endblock %}</h1>
 7
 8<form method="post">
 9    <div class="form-group">
10        <label for="content">Content</label>
11        <input type="text" name="content"
12               placeholder="Todo content" class="form-control"
13               value="{{ todo['content'] or request.form['content'] }}"></input>
14    </div>
15
16    <div class="form-group">
17        <label for="list">List</label>
18        <select class="form-control" name="list">
19            {% for list in lists %}
20                {% if list['title'] == request.form['list'] %}
21                    <option value="{{ request.form['list'] }}" selected>
22                        {{ request.form['list'] }}
23                    </option>
24
25                {% elif list['title'] == todo['title'] %}
26                    <option value="{{ todo['title'] }}" selected>
27                        {{ todo['title'] }}
28                    </option>
29
30                {% else %}
31                    <option value="{{ list['title'] }}">
32                        {{ list['title'] }}
33                    </option>
34                {% endif %}
35            {% endfor %}
36        </select>
37    </div>
38    <div class="form-group">
39        <button type="submit" class="btn btn-primary">Submit</button>
40    </div>
41</form>
42{% endblock %}

您将为内容输入使用 `{{ todo['content'] 或 request.form['content'] }} 的值,这意味着值将是任务项目的当前内容,或者用户在试图提交表单时提交的内容。

对于列表选择表格,您将循环通过列表变量,如果列表标题与request.form对象中存储的标题相同(从未成功的尝试中),然后将该列表标题设置为所选值。

保存并关闭文件。

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

1nano templates/index.html

row类更改div标签的内容,以如下方式添加另一个列:

 1[label flask_todo/templates/index.html]
 2. . .
 3<div class="row">
 4    <div class="col-12 col-md-3">
 5        <form action="{{ url_for(URL, id=item['id']) }}"
 6            method="POST">
 7            <input type="submit" value="{{ BUTTON }}"
 8                class="btn btn-success btn-sm">
 9        </form>
10    </div>
11    <div class="col-12 col-md-3">
12        <a class="btn btn-warning btn-sm"
13        href="{{ url_for('edit', id=item['id']) }}">Edit</a>
14    </div>
15</div>

保存并关闭文件。

这是一个 标准 <a> 链接标签,指向每个项目的相关 /edit/ 路径。

运行服务器,如果你还没有:

1flask run

您现在可以去索引页面 http://127.0.0.1:5000/ 并尝试修改任务项目. 在下一步,您将添加一个按钮来删除项目。

步骤 4 – 删除要做的项目

在此步骤中,您将添加删除特定任务项目的能力。

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

1nano app.py

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

1[label flask_todo/app.py]
2. . .
3@app.route('/<int:id>/delete/', methods=('POST',))
4def delete(id):
5    conn = get_db_connection()
6    conn.execute('DELETE FROM items WHERE id = ?', (id,))
7    conn.commit()
8    conn.close()
9    return redirect(url_for('index'))

保存并关闭文件。

当发送一个POST请求时,您使用DELETESQL语句删除与匹配的id值的项目,然后执行交易并关闭数据库连接,然后返回索引页面。

接下来,打开templates/index.html,添加一个删除按钮:

1nano templates/index.html

编辑按钮下方添加以下突出的div标签:

 1[label flask_todo/templates/index.html]
 2<div class="row">
 3    <div class="col-12 col-md-3">
 4        <form action="{{ url_for(URL, id=item['id']) }}"
 5            method="POST">
 6            <input type="submit" value="{{ BUTTON }}"
 7                class="btn btn-success btn-sm">
 8        </form>
 9    </div>
10
11    <div class="col-12 col-md-3">
12        <a class="btn btn-warning btn-sm"
13        href="{{ url_for('edit', id=item['id']) }}">Edit</a>
14    </div>
15
16    <div class="col-12 col-md-3">
17        <form action="{{ url_for('delete', id=item['id']) }}"
18            method="POST">
19            <input type="submit" value="Delete"
20                class="btn btn-danger btn-sm">
21        </form>
22    </div>
23</div>

此新的提交按钮将向每个项目的 /delete/ 路径发送一个 POST 请求。

保存并关闭文件。

然后运行开发服务器:

1flask run

转到索引页面并尝试新的删除按钮 - 你现在可以删除任何你想要的项目。

现在您已经添加了删除现有任务项目的功能,您将继续在下一步中添加新列表的功能。

步骤 5 – 添加新列表

到目前为止,只能直接从数据库中添加列表. 在此步骤中,您将添加创建新列表的能力,当用户添加新项目时,而不是仅仅在现有列表之间进行选择。

首先,打开app.py:

1nano app.py

然后,通过将下列突出的行添加到 if request.method == 'POST' 条件中来修改 'create()' 视图函数:

 1[label flask_todo/app.py]
 2. . .
 3@app.route('/create/', methods=('GET', 'POST'))
 4def create():
 5    conn = get_db_connection()
 6
 7    if request.method == 'POST':
 8        content = request.form['content']
 9        list_title = request.form['list']
10
11        new_list = request.form['new_list']
12
13        # If a new list title is submitted, add it to the database
14        if list_title == 'New List' and new_list:
15            conn.execute('INSERT INTO lists (title) VALUES (?)',
16                         (new_list,))
17            conn.commit()
18            # Update list_title to refer to the newly added list
19            list_title = new_list
20
21        if not content:
22            flash('Content is required!')
23            return redirect(url_for('index'))
24
25        list_id = conn.execute('SELECT id FROM lists WHERE title = (?);',
26                                 (list_title,)).fetchone()['id']
27        conn.execute('INSERT INTO items (content, list_id) VALUES (?, ?)',
28                     (content, list_id))
29        conn.commit()
30        conn.close()
31        return redirect(url_for('index'))
32
33    lists = conn.execute('SELECT title FROM lists;').fetchall()
34
35    conn.close()
36    return render_template('create.html', lists=lists)

保存并关闭文件。

在这里,您将将一个名为new_list的新表单字段的值保存到变量中。您将后来将这个字段添加到create.html文件中。接下来,在list_title ==New Listnew_list条件下,您会检查list_title是否具有New List的值,这表明用户希望创建一个新列表。您还会检查new_list变量的值不是None,如果满足了这个条件,您会使用一个INSERT INTO SQL 语句将新提交的列表名称添加到lists`表中。

接下来,打开create.html,添加一个新的<option>标签,让用户添加一个新列表:

1nano templates/create.html

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

 1[label flask_todo/templates/create.html]
 2    <div class="form-group">
 3        <label for="list">List</label>
 4        <select class="form-control" name="list">
 5            <option value="New List" selected>New List</option>
 6            {% for list in lists %}
 7                {% if list['title'] == request.form['list'] %}
 8                    <option value="{{ request.form['list'] }}" selected>
 9                        {{ request.form['list'] }}
10                    </option>
11                {% else %}
12                    <option value="{{ list['title'] }}">
13                        {{ list['title'] }}
14                    </option>
15                {% endif %}
16            {% endfor %}
17        </select>
18    </div>
19
20    <div class="form-group">
21        <label for="new_list">New List</label>
22        <input type="text" name="new_list"
23                placeholder="New list name" class="form-control"
24                value="{{ request.form['new_list'] }}"></input>
25    </div>
26
27    <div class="form-group">
28        <button type="submit" class="btn btn-primary">Submit</button>
29    </div>

保存并关闭文件。

您已添加了一个新的<option>标签,以参考新列表选项,这将允许用户指定他们想要创建一个新列表,然后您添加另一个<div>输入字段名为new_list,这个字段是用户将输入他们想要创建的新列表的标题。

最后,运行开发服务器:

1flask run

然后访问索引页面:

1http://127.0.0.1:5000/

现在的应用程序将如下:

Todo Application

通过新添加到应用程序中,用户现在可以将要完成的项目标记为完整或将完成的项目恢复到未完成的状态,编辑和删除现有项目,并为不同类型的要完成的任务创建新列表。

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

结论

您现在有一个完整的任务应用程序,用户可以创建新的任务项目,将项目标记为完整,并编辑或删除现有项目,以及创建新列表的能力。您已经修改了Flask Web应用程序,添加了新的功能,并修改了数据库项目,特别是在一对多关系中。

Published At
Categories with 技术
comments powered by Disqus