博客蓝图¶
你将使用在编写认证蓝图时学习的相同技术来编写博客蓝图。博客应该列出所有帖子,允许已经登入的用户创建帖子,并且允许帖子的作者编辑或删除它。
当你实现每个视图时,让开发服务器保持运行。当你保存你的变动,试着在浏览器访问 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() 注册蓝图。把新代码放到工厂函数的底部,在返回应用实例之前。
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
和认证蓝图不同,博客蓝图没有 url_prefix。所以 index 视图将会在 /,create 视图将会在 /create,以此类推。博客是 Flaskr 的主要功能,所以博客索引将会是主索引。
然而,下面定义的 index 视图的端点将会是 blog.index。一些认证视图引用了一个普通的 index 端点。app.add_url_rule() 把端点名 index 和 / 关联到一起,这样 url_for('index') 或 url_for('blog.index') 都可以工作,不管哪种方式都会生成相同的 URL,即 /。
在另一个应用中,你可以给博客蓝图一个 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 视图的链接。当用户是帖子的作者时,他们会看到一个“Edit”链接,指向对应那个帖子的 update 视图。loop.last 是一个在 Jinja for 循环 中可用的特殊变量。它被用来在每一个帖子(除了最后一个)下面显示一条线,以便在视觉上分离它们。
创建¶
create 视图和认证的 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 表示“Not Found(未找到)”,403 表示“Forbidden(禁止)”。(401 表示“Unauthorized(未授权)”,但是你会重定向到登录页面,而不是返回那个状态码。)
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>,它将是一个字符串。为了生成一个指向更新页面的 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 。因为没有模板,它将只处理 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'))
恭喜你,你现在已经完成了应用的编写!花一些时间在浏览器里尝试所有功能。然而,在项目完成之前,还有一些事情要做。
继续阅读 让项目可安装。