iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Software Development

都是 P 開頭的程式語言,你是在 py 啥啦系列 第 16

[16] [燒瓶裡的部落格] 06. 部落格的 Blueprint

  • 分享至 

  • xImage
  •  

部落格的 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'))

接著就是測試功能的時間!試試看功能是否可以正常使用吧


上一篇
[15] [燒瓶裡的部落格] 05. 靜態檔案
下一篇
[17] [燒瓶裡的部落格] 07. 專案可安裝化(打包)
系列文
都是 P 開頭的程式語言,你是在 py 啥啦30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言