[16] [烧瓶里的部落格] 06. 部落格的 Blueprint

部落格的 Blueprint 和会员验证时候的做法一样
部落格页面应该列出所有文章,允许已登入的会员建立新文章,并允许作者修改和删除文章

Blueprint

定义 blueprint 并且注册到 application factory 中

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() 在工厂中导入和注册 blueprint
将新的程序放在工厂函数的 return 之前

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

与验证的 blueprint 不同,部落格 blueprint 没有url_prefix的前缀
所以indexview 会用於/create会用於/create,以此类推
部落格是 Flaskr 的主要功能,因此把部落格作为首页是合理的!

但是,下文的index view 的 endpoint 会被定义为blog.index
一些验证的验证 view 会重新导向到叫做index的 endpoint
这边使用 app.add_url_rule() 指定 路径/ 的 endpoint 名称为'index'
这样url_for('index')url_for('blog.index')都会产生同样指向/路径的网址

在其他应用程序中,可能会在工厂中给部落格的 blueprint 一个url_prefix并定义一个独立的indexview
类似之前做过的helloview。在这种情况下indexblog.index的 endpoint 和网址会有所不同

Index 首页

首页会从新到旧显示所有文章,使用JOIN来取得文章作者的资料

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 view 的网址
当使用者是文章作者时,可以看到一个「Edit」网址,指向update view
loop.last 是一个在 Jinja for 回圈内部可用的特殊变数
用於在每个文章後面显示一条线来分隔,最後一篇文章除外

Create 建立文章

create 的 view 和 register view 原理相同,负责显示表单或是送出内容
并将通过验证的资料已加入资料库,或者显示错误讯息

之前写在auth.pylogin_required装饰器在这边就用上了!
使用者必须登入以後才能访问这些 view,否则会被跳转到登入页面

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 更新文章

updatedelete view 都需要通过id来取得post,并且检查作者与登入的使用者是否一致?
因为这部分是重复使用的,可以写一个函数来取得post,并且在不同的 view 中呼叫来使用

取得文章的函数:
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)

和所有以前的 view 不同,update函数有一个id参数
该参数对应路由中的<int:id>,一个真正的 URL 类似/1/update
Flask 会捕捉到 URL 中的1,确保格式是int,并将其作为id参数传递给 view
如果只写了<id>而没有指定int:的话就会用字串的方式传递
要产生一个更新页面的 URL,需要将 id 参数加入 url_for()
例如:url_for('blog.update', id=post['id']),前面的index.html档案中也是

createupdate的 view 看上去是相似的,主要的不同之处在於update view 使用了post物件
和一个UPDATE query 而不是INSERT query
作为一个明智的重构者,可以使用一个 view 和一个模板来同时完成这两项工作
但是作为一个初学者,把它们分别处理会更容易理解一些

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 %}

这个模板有两个 form,第一个提交已编辑过的数据给当前页面(/<id>/update
另一个 form 只包含一个按钮,它指定一个action属性,指向删除 view
这个按钮使用了一些 JavaScript 用来在送出前显示一个确认对话框

参数{{ request.form['title'] or post['title'] }}用於选择在表单显示什麽资料
当表单还未送出时,显示原本的 post 资料
但是,如果提交了无效的资料,你希望显示错误以便於使用者修改时
就显示request.form中的资料,request 又是一个自动在模板中可用的变数!

Delete 删除文章

删除视图没有自己的模板,删除按钮已包含於update.html之中
该按钮指向/<id>/delete URL
既然没有模板,该 view 只处理 POST 方法并重新导向到index view

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'))

接着就是测试功能的时间!试试看功能是否可以正常使用吧


<<:  [Day29] Maker making IoT完赛心得与一些後续的期待!

>>:  Day 16 Matcher 介绍 (上)

从疫情聊聊 WFH 是福音还是地狱

习惯是可以半自动执行的行为,但很依赖节奏;修练则是终身的追求,所以可以灵活。节录自 刘轩《天上总会...

四招解决Spotify 黑画面问题!--〖2022亲测有效〗

Spotify 打开时是黑屏的怎麽办?明明老版本还是可以正常使用的,为什麽更新後Spotify会出现...

Day 9:架设 Prometheus (1)

昨天我们成功的让 Prometheus 可以采集到一些指标了,可是为了了解服务的状态,我们还需要自己...

Day 9. 新手也能懂的物件导向

新手在学写程序时一定常常看到物件、类别、介面、抽象、继承...奇怪的外星语,可能知道跟物件导向有关但...

Day17-JDK堆栈跟踪工具:jstack(二)

前言 延续着上篇内容,这篇要继续来介绍jstack有些什麽options可以使用 options 介...