部落格藍圖

您將使用撰寫驗證藍圖時學到的相同技巧來撰寫部落格藍圖。部落格應列出所有文章,允許登入的使用者建立文章,並允許文章作者編輯或刪除文章。

在您實作每個視圖時,請保持開發伺服器執行中。當您儲存變更時,請嘗試在瀏覽器中前往 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'))

恭喜,您現在已完成撰寫您的應用程式!花一些時間在瀏覽器中試用所有功能。但是,在專案完成之前,還有更多工作要做。

繼續閱讀 使專案可安裝