Blog 蓝图¶
你将使用编写认证蓝图时学到的相同技术来编写博客蓝图。博客应列出所有帖子,允许登录用户创建帖子,并允许帖子作者编辑或删除帖子。
在你实现每个视图时,保持开发服务器运行。当你保存更改时,尝试在浏览器中访问 URL 并进行测试。
蓝图¶
定义蓝图并在应用程序工厂中注册它。
flaskr/blog.py
¶from flask import (
Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort
from flaskr.auth import login_required
from flaskr.db import get_db
bp = Blueprint('blog', __name__)
使用 app.register_blueprint()
从工厂导入并注册蓝图。将新代码放在工厂函数的末尾,然后再返回 app。
flaskr/__init__.py
¶def create_app():
app = ...
# existing code omitted
from . import blog
app.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 将会不同。
索引¶
索引将显示所有帖子,最近的帖子在最前面。使用 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
视图的“编辑”链接。loop.last
是 Jinja for 循环中可用的一个特殊变量。它用于在除最后一个帖子之外的每个帖子后显示一条线,以在视觉上分隔它们。
创建¶
create
视图的工作方式与 auth register
视图相同。要么显示表单,要么验证发布的数据并将帖子添加到数据库,或者显示错误。
你之前编写的 login_required
装饰器用于博客视图。用户必须登录才能访问这些视图,否则他们将被重定向到登录页面。
flaskr/blog.py
¶@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
if request.method == 'POST':
title = request.form['title']
body = request.form['body']
error = None
if 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
获取 post
,并检查作者是否与登录用户匹配。为了避免重复代码,你可以编写一个函数来获取 post
并从每个视图中调用它。
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
参数是为了可以使用该函数获取 post
而无需检查作者。如果你编写一个视图以在页面上显示单个帖子,这将很有用,因为用户不修改帖子,因此用户无关紧要。
flaskr/blog.py
¶@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
post = get_post(id)
if request.method == 'POST':
title = request.form['title']
body = request.form['body']
error = None
if 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>
,它将是一个字符串。要生成指向 update 页面的 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
属性,该属性改为发布到 delete 视图。该按钮使用一些 JavaScript 在提交之前显示确认对话框。
模式 {{ request.form['title'] or post['title'] }}
用于选择表单中显示的数据。当表单尚未提交时,将显示原始 post
数据,但如果发布了无效的表单数据,你希望显示该数据,以便用户可以修复错误,因此改用 request.form
。request
是另一个在模板中自动可用的变量。
删除¶
delete
视图没有自己的模板,delete 按钮是 update.html
的一部分,并发布到 /<id>/delete
URL。由于没有模板,它将仅处理 POST
方法,然后重定向到 index
视图。
flaskr/blog.py
¶@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
get_post(id)
db = get_db()
db.execute('DELETE FROM post WHERE id = ?', (id,))
db.commit()
return redirect(url_for('blog.index'))
恭喜,你现在已经完成编写你的应用程序!花一些时间在浏览器中试用所有功能。但是,在项目完成之前还有更多工作要做。
继续阅读 使项目可安装。