作者选择了COVID-19 救济基金来接受捐赠,作为Write for DOnations计划的一部分。
介绍
Flask 是一个使用 Python 语言构建 Web 应用程序的框架,SQLite 是一个数据库引擎,可以与 Python 一起使用来存储应用程序数据。在本教程中,您将修改使用 Flask 和 SQLite 构建的具有一对多关系的应用程序中的项目。
本教程是如何使用 Flask 和 SQLite 的一对多数据库关系的延续。遵循它之后,您已经成功地创建了一个 Flask 应用程序来管理待办事项、在列表中组织项目并将新项目添加到数据库中。在本教程中,您将添加将待办事项标记为已完成、编辑和删除项目以及向数据库添加新列表的功能。在本教程结束时,您的应用程序将包含编辑和删除按钮以及已完成待办事项的删除线。

先决条件
在开始遵循本指南之前,您需要:
-
本地 Python 3 编程环境,请遵循如何为 Python 3系列安装和设置本地编程环境中的分发教程。在本教程中,我们将调用我们的项目目录
flask_todo。 -
(可选)在第 1 步中,您可以选择克隆我们将在本教程中处理的待办事项应用程序。但是,您可以选择学习如何使用 Flask 和 SQLite 的一对多数据库关系。您可以从此页面访问最终代码。
-
了解基本的 Flask 概念,例如创建路由、呈现 HTML 模板和连接到 SQLite 数据库。如果您不熟悉这些概念,请查看如何在 Python 3 中使用 Flask 制作 Web 应用程序和如何在 Python 3 中使用 sqlite3 模块,但这不是必需的。
第 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.py在这种情况下。FLASK_ENV指定模式——将其设置development为开发模式,这将允许您调试应用程序。(切记不要在生产环境中使用这种模式。)
然后运行开发服务器:
- flask run
如果您转到浏览器,您将在以下 URL 上运行该应用程序http://127.0.0.1:5000/。
要关闭开发服务器,请使用CTRL + C组合键。
接下来,您将修改应用程序以添加将项目标记为完成的功能。
步骤 2 — 将待办事项标记为已完成
在此步骤中,您将添加一个按钮以将每个待办事项标记为已完成。
为了能够将项目标记为完成,您将items在数据库的表中添加一个新列,为每个项目设置一个标记,以便您知道它是否已完成,然后您将在app.py文件中创建一个新路径根据用户的操作更改此列的值。
提醒一下,表中的列items目前如下:
id: 物品的ID。list_id:项目所属列表的ID。created:项目的创建日期。content: 项目的内容。
首先打开schema.sql修改items表:
- nano schema.sql
done向items表中添加一个名为的新列:
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项目的 和done列的值,该index()函数从数据库中获取列表和项目并将它们发送到index.html文件以供显示。对 SQL 语句的必要更改在以下文件中突出显示:
@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)
保存并关闭文件。
通过此修改,您可以使用 获取待办事项的 ID 和使用i.id的done列的值i.done。
要了解此更改,请打开 open list_example.py,这是一个可用于了解数据库内容的小型示例程序:
- nano list_example.py
对SQL语句进行与之前相同的修改,然后将最后一个print()函数更改为显示项目ID和 的值done:
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.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为已完成。在函数内部,您打开一个数据库连接,然后使用UPDATESQL 语句将done列的值设置为,1以便将项目标记为已完成。
您?在execute()方法中使用占位符并传递包含 ID 的元组以安全地将数据插入到数据库中。然后提交事务并关闭连接并重定向到索引页面。
添加一个路由将项目标记为完成后,您需要另一个路由来撤消此操作并将项目返回到未完成状态。在文件末尾添加以下路由:
. . .
@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()view函数和函数完全一样,do()只是你设置的值是doneto0而不是1。
保存并关闭app.py文件。
您现在需要一个按钮来根据项目的状态将待办事项标记为已完成或未完成,打开index.html模板文件:
- nano templates/index.html
for将<ul>元素内的内部循环的内容更改为如下所示:
{% 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循环中,如果项目被标记为已完成,您将使用属性的line-throughCSS 值text-decoration,您可以从item['done']. 然后使用 Jinja 语法set声明两个变量,URL和BUTTON. 如果项目未标记为完成,按钮的值为Do,URL 将指向/do/路由,如果项目标记为完成,按钮的值为Undo并指向/undo/。之后,您可以在input根据项目状态提交正确请求的表单中使用这两个变量。
运行服务器:
- flask run
您现在可以在索引页上将项目标记为已完成http://127.0.0.1:5000/。接下来,您将添加编辑待办事项的功能。
步骤 3 — 编辑待办事项
在此步骤中,您将添加一个用于编辑项目的新页面,以便您可以修改每个项目的内容并将项目分配到不同的列表。
您将向/edit/该app.py文件添加一个新路由,这将呈现一个新edit.html页面,用户可以在其中修改现有项目。您还将更新index.html文件以Edit向每个项目添加一个按钮。
首先,打开app.py文件:
- nano 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()函数,将todo和传递lists给edit.html文件。
但是,如果提交了表单,则条件request.method == 'POST'变为true,在这种情况下,您提取用户提交的内容和列表标题。如果没有提交任何内容,您会显示消息Content is required!并重定向到相同的编辑页面。否则,您获取用户提交的列表的 ID;这允许用户将待办事项从一个列表移动到另一个列表。然后,您使用UPDATESQL 语句将待办事项的内容设置为用户提交的新内容。您对列表 ID 执行相同的操作。最后,提交更改并关闭连接,并将用户重定向到索引页面。
保存并关闭文件。
要使用这个新路由,你需要一个名为 的新模板文件edit.html:
- nano 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
使用类更改div标记的内容"row"以添加另一列,如下所示:
. . .
<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>
保存并关闭文件。
这是一个标准<a>链接标签,指向/edit/每个项目的相关路线。
如果您还没有运行服务器:
- flask run
您现在可以转到索引页面http://127.0.0.1:5000/并尝试修改待办事项。在下一步中,您将添加一个按钮来删除项目。
第 4 步 – 删除待办事项
在此步骤中,您将添加删除特定待办事项的功能。
您首先需要添加一个新/delete/路由,打开app.py:
- nano 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请求被发送时,您使用DELETESQL 语句删除具有匹配id值的项目,然后提交事务并关闭数据库连接,并返回到索引页面。
接下来,打开templates/index.html添加一个Delete按钮:
- nano templates/index.html
div在Edit按钮下方添加以下突出显示的标签:
<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
然后,create()通过向if request.method == 'POST'条件添加以下突出显示的行来修改视图函数:
. . .
@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 INTOSQL 语句将新提交的列表标题添加到lists表中。您提交事务,然后更新list_title变量的值以匹配新添加的列表以供以后使用。
接下来,打开create.html添加一个新<option>标签让用户添加一个新列表:
- nano 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 应用程序添加安全性。