本次我们将会写一个博客的蓝图。要用到的知识和之前写认证蓝图的知识相同。博客蓝图应该列出所有的博客信息,并允许所有的用户来创建博客,还可以让该用户来编辑和删除博客。
在你实现每个蓝图的过程中,建议保证服务器的运行。这样当你保存改变时,你就可以在浏览器中进行测试。
博客蓝图
定义博客蓝图,并在博客工厂中对他进行注册。
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.但是,定义在下面的index视图的端点将会是blog.index。认证视图中的一些也会参考index端点生成url。app.add_url_rule()将会把名为index的端点和 / 的url联系在一起。这样的话url_for(‘index’)以及url_for(‘blog.index’)都会生成相同的url。
在另一个应用中,可能你会给了博客蓝图一个url_prefix并在应用工厂中定义一个分离的index视图,这时index和blog.index所指向的url将会是不同的。这一点很重要!
index
index将会显示所有用户提交的博客。这里我们使用了数据库中的连接操作,这样的话结果中来自用户表里的作者的信息将会是可用的。
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块将会增加一个创建视图的链接。如果登录的用户是某个博客文章的作者时,那他就可以看到“EDIT”链接,其可以转向update视图。loop.last是一个特殊的Jinja内部的可用变量。它用于显示每个博客之后的一行(除了最后一行)。
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
更新和删除视图都必须通过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, "Post id {0} doesn't exist.".format(id))
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_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)
上面这个视图并不像你之前写过的,更新函数接受了一个id作为参数,这个参数将会联系路由中的<\int:id>.一个现实的url将会是这个样子的/1/update.flask将会捕捉到1,确保它是一个int。并将它传给id。如果你将int去掉,传给id的1将会是一个字符串。为了给更新页面生成url,url_for()需要刚才的id作为参数,只有这样,它才知道这个id要给谁:url_for(‘blog.update’,id=post[‘id’].
创建和更新视图其实很相似,主要的不同是更新视图采用了一个post对象以及一个数据库更新的操作。
通过一些非常精妙的重构,我们能够使用一个视图和模板来应对多种情况,但是就教程而言还是分开比较好。
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 %}
这个模板有两个表单,第一个展示了当前页面要编辑的数据,另一个表单仅仅包含一个按钮并且会指定一个行为来转到删除的视图。
模式{{request.form[‘title’] or post[‘title’]}} 被用于选择什么数据出现在表单中。在表单没有被提交时,原始的博客是被显示的,但是如果不合理的表单数据被提交
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'))