博客蓝图

您将使用在编写身份验证蓝图时学到的相同技术来编写博客蓝图。博客应列出所有帖子,允许登录用户创建帖子,并允许帖子的作者对其进行编辑或删除。

在实现每个视图时,请保持开发服务器运行。在保存更改时,尝试在浏览器中访问 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 关联起来,以便 url_for('index')url_for('blog.index') 都能正常工作,无论哪种方式都会生成相同的 / URL。

在另一个应用程序中,你可以为博客蓝图提供一个 url_prefix,并在应用程序工厂中定义一个单独的 index 视图,类似于 hello 视图。然后, indexblog.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.lastJinja 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 %}

更新

updatedelete 视图都需要通过 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>,它将是一个字符串。要生成一个到更新页面的 URL,url_for() 需要传递 id,以便它知道填充什么:url_for('blog.update', id=post['id'])。这也存在于上面的 index.html 文件中。

createupdate 视图看起来非常相似。主要区别在于 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.formrequest 是另一个在模板中自动可用的变量。

删除

删除视图没有自己的模板,删除按钮是 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'))

恭喜,你现在已经完成了应用程序的编写!花点时间在浏览器中尝试所有内容。但是,在项目完成之前还有更多工作要做。

继续 使项目可安装