博客蓝图

博客蓝图与验证蓝图所使用的技术一样。博客页面应当列出所有的帖子,允许已登录 用户创建帖子,并允许帖子作者修改和删除帖子。

在实现每个视图时,保持开发服务器运行。保存更改时,请尝试转到浏览器中的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() <Flask.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 ,这样 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 表示“未找到”,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 会捕捉到 URL 中的 1,确保其为一个 int ,并将其作为 id 参数传递给视图。如果没有指定 int: 而是仅仅写了 <id> ,那么将会传递一个字符串。 要生成一个指向更新页面的 URL,需要传递 id 参数给 url_for():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_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'))

恭喜你,你的应用已经写完了!花点时间在浏览器中试试这个应用吧。然而,构建一个完整的应用还有一些工作要做。

下面请阅读 项目可安装化 。