Blog 蓝图
接下来使用编写身份验证蓝图时学习的相同技术来编写 blog 蓝图。Blog 应该列出所有帖子,允许登录用户创建帖子,并允许帖子作者编辑或删除帖子。
在实现每个视图时,请保持开发服务器处于运行状态。保存更改时,请在浏览器中访问相应的 URL 进行测试。
蓝图
定义蓝图并在应用程序工厂中注册。
flaskr/blog.py
from flask import (Blueprint, flash, g, redirect, render_template, request, url_for)from werkzeug.exceptions import abortfrom flaskr.auth import login_requiredfrom flaskr.db import get_dbbp = Blueprint('blog', __name__)
在工厂中导入,并使用 app.register_blueprint() 注册蓝图。将新代码放在工厂函数末尾,返回应用程序之前。
flaskr/__init__.py
def create_app():app = ...# existing code omittedfrom . import blogapp.register_blueprint(blog.bp)app.add_url_rule('/', endpoint='index')return app
与 auth 蓝图不同,blog 蓝图没有 url_prefix。因此,index 视图位于 /,create 视图位于 /create,依此类推。博客是 Flaskr 的主要功能,所以博客索引将成为主要索引是有道理的。
但是,下面定义的 index 视图的端点是 blog.index。一些身份验证视图引用了普通 index 端点。app.add_url_rule() 将端点名 'index' 与 / url 相关联,这样 url_for('index') 或 url_for('blog.index') 都起作用,都能生成相同的 / URL。
在另一个应用程序中,可以为 blog 蓝图提供 url_prefix,并在应用程序工厂中定义一个单独的 index 视图,类似于 hello 视图。index 和 blog.index 端点和 URL 会是不同的。
Index
index 将显示所有帖子,其中最新的显示在前面。SQL 中使用了 JOIN,以便可以在结果中使用 user 表的作者信息。
flaskr/blog.py
@bp.route('/')def index():db = get_db()posts = db.execute('SELECT p.id, title, body, created, author_id, username'' FROM post p JOIN user u ON p.author_id = u.id'' ORDER BY created DESC').fetchall()return render_template('blog/index.html', posts=posts)
flaskr/templates/blog/index.html
{% extends 'base.html' %}{% block header %}<h1>{% block title %}Posts{% endblock %}</h1>{% if g.user %}<a class="action" href="{{ url_for('blog.create') }}">New</a>{% endif %}{% endblock %}{% block content %}{% for post in posts %}<article class="post"><header><div><h1>{{ post['title'] }}</h1><div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div></div>{% if g.user['id'] == post['author_id'] %}<a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>{% endif %}</header><p class="body">{{ post['body'] }}</p></article>{% if not loop.last %}<hr>{% endif %}{% endfor %}{% endblock %}
当用户登录时,header 块会添加到 create 视图的链接。当用户是帖子的作者时,会看到一个指向该帖子 update 视图的 “Edit” 链接。loop.last 是 Jinja 中一个用于循环的特殊变量。它用于在每个帖子后面显示一行,最后一个帖子除外,以便在视觉上将它们分开。
创建
create 视图与 auth 的 register 视图的工作原理相同。要么显示表单,要么验证发布的数据并将发布添加到数据库,要么显示错误。
之前编写的 login_required 装饰器用于 blog 视图。用户必须登录才能访问这些视图,否则它们将被重定向到登录页面。
flaskr/blog.py
@bp.route('/create', methods=('GET', 'POST'))@login_requireddef create():if request.method == 'POST':title = request.form['title']body = request.form['body']error = Noneif not title:error = 'Title is required.'if error is not None:flash(error)else:db = get_db()db.execute('INSERT INTO post (title, body, author_id)'' VALUES (?, ?, ?)',(title, body, g.user['id']))db.commit()return redirect(url_for('blog.index'))return render_template('blog/create.html')
flaskr/templates/blog/create.html
{% extends 'base.html' %}{% block header %}<h1>{% block title %}New Post{% endblock %}</h1>{% endblock %}{% block content %}<form method="post"><label for="title">Title</label><input name="title" id="title" value="{{ request.form['title'] }}" required><label for="body">Body</label><textarea name="body" id="body">{{ request.form['body'] }}</textarea><input type="submit" value="Save"></form>{% endblock %}
更新
update 和 delete 视图都需要按 id 获取 帖子,并检查作者是否与登录用户匹配。为了避免代码重复,可以编写一个函数来获取帖子,并从每个视图调用它。
flaskr/blog.py
def get_post(id, check_author=True):post = get_db().execute('SELECT p.id, title, body, created, author_id, username'' FROM post p JOIN user u ON p.author_id = u.id'' WHERE p.id = ?',(id,)).fetchone()if post is None:abort(404, f"Post id {id} doesn't exist.")if check_author and post['author_id'] != g.user['id']:abort(403)return post
abort() 将引发一个特殊异常,返回 HTTP 状态代码。它需要一条可选消息来显示错误,否则将使用默认消息。404 表示“未找到”,403 表示“禁止”。(401 表示“未授权”,但也可以重定向到登录页面,而不是返回该状态。)
定义 check_author 参数是为了在不检查作者的情况下用该函数获取帖子。如果写一个视图来在页面上显示一篇单独的帖子,而不用修改帖子,则此时用户并不重要,这将非常有用。
flaskr/blog.py
@bp.route('/<int:id>/update', methods=('GET', 'POST'))@login_requireddef update(id):post = get_post(id)if request.method == 'POST':title = request.form['title']body = request.form['body']error = Noneif not title:error = 'Title is required.'if error is not None:flash(error)else:db = get_db()db.execute('UPDATE post SET title = ?, body = ?'' WHERE id = ?',(title, body, id))db.commit()return redirect(url_for('blog.index'))return render_template('blog/update.html', post=post)
与目前为止编写的视图不同,update 函数接受一个参数 id,它对应于路由中的 <int:id>。真正的 URL 看起来像 /1/update。Flask 将捕获 1,确保它是 int,并将其作为 id 参数传递。如果不指定 int: 而指定 <id>,则它将是一个字符串。要生成更新页面的 URL,需要向 url_for() 传递 id,以便它知道要填写什么:url_for('blog.update', id=post['id'])。这也在上面的 index.html 文件中。
create 和 update 视图看起来非常相似。主要区别在于 update 视图使用 post 对象和 UPDATE 查询,而不是 INSERT。通过一些巧妙的重构,可以对两个操作使用一个视图和模板,但对于本教程来说,将它们分开会更清楚。
flaskr/templates/blog/update.html
{% extends 'base.html' %}{% block header %}<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>{% endblock %}{% block content %}<form method="post"><label for="title">Title</label><input name="title" id="title"value="{{ request.form['title'] or post['title'] }}" required><label for="body">Body</label><textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea><input type="submit" value="Save"></form><hr><form action="{{ url_for('blog.delete', id=post['id']) }}" method="post"><input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');"></form>{% endblock %}
此模板有两个表单。第一个将编辑的数据发布到当前页面(/<id>/update)。另一个表单只包含一个按钮,并指定 action 属性,将表单发布到删除视图。该按钮使用一些 JavaScript 代码,在提交前显示确认对话框。
模式 {{ request.form['title'] or post['title'] }} 用于选择表单中显示的数据。当表单尚未提交时,会显示原始的 post 数据,但如果提交了无效的表单数据,你希望显示该数据,以便用户可以修复错误,则改为使用 request.form。request 是另一个在模板中自动可用的变量。
删除
删除视图没有自己的模板,删除按钮是 update.html 的一部分,并提交到 /<id>/delete URL。由于没有模板,它将只处理 POST 方法,然后重定向到 index 视图。
flaskr/blog.py
@bp.route('/<int:id>/delete', methods=('POST',))@login_requireddef delete(id):get_post(id)db = get_db()db.execute('DELETE FROM post WHERE id = ?', (id,))db.commit()return redirect(url_for('blog.index'))
恭喜,现在应用程序已经写完了!花点时间在浏览器中尝试用所有功能。然而,在项目完成之前还有很多事情要做。
原文:
https://flask.palletsprojects.com/en/2.0.x/tutorial/blog/




