Python個(gè)人博客程序開發(fā)實(shí)例后臺(tái)編寫
本篇博客將是Python個(gè)人博客程序開發(fā)實(shí)例的最后一篇。本篇文章將會(huì)詳細(xì)介紹博客后臺(tái)的編寫。
為了支持管理員管理文章、分類、評(píng)論和鏈接,我們需要提供后臺(tái)管理功能。通常來說,程序的這一部分被稱為管理后臺(tái)、控制面板或儀表盤等。這里通常會(huì)提供網(wǎng)站的資源信息和運(yùn)行狀態(tài),管理員可以統(tǒng)一查看和管理所有資源。管理員面板通常會(huì)使用獨(dú)立樣式的界面,所以你可以為這部分功能的模板創(chuàng)建一個(gè)單獨(dú)的基模板。為了保持簡單,Bluelog 的管理后臺(tái)和前臺(tái)頁面使用相同的樣式。
Bluelog 的管理功能比較簡單,我們沒有提供一個(gè)管理后臺(tái)主頁,取而代之的是,我們?cè)趯?dǎo)航欄上添加鏈接作為各個(gè)管理功能的入口。

{% from 'bootstrap/nav.html' import render_nav_item %}
...
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li class="nav-item dropdown">
<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true"
aria-expanded="false">
New <span class="caret"></span>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ url_for('admin.new_post') }}" rel="external nofollow" >Post</a>
<a class="dropdown-item" href="{{ url_for('admin.new_category') }}" rel="external nofollow" >Category</a>
<a class="dropdown-item" href="{{ url_for('admin.new_link') }}" rel="external nofollow" >Link</a>
</div>
</li>
<li class="nav-item dropdown">
<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true"
aria-expanded="false">
Manage <span class="caret"></span>
{% if unread_comments %}
<span class="badge badge-success">new</span>
{% endif %}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ url_for('admin.manage_post') }}" rel="external nofollow" >Post</a>
<a class="dropdown-item" href="{{ url_for('admin.manage_category') }}" rel="external nofollow" >Category</a>
<a class="dropdown-item" href="{{ url_for('admin.manage_comment') }}" rel="external nofollow" >
Comment
{% if unread_comments %}
<span class="badge badge-success">{{ unread_comments }}</span>
{% endif %}
</a>
<a class="dropdown-item" href="{{ url_for('admin.manage_link') }}" rel="external nofollow" >Link</a>
</div>
</li>
{{ render_nav_item('admin.settings', 'Settings') }}
{% endif %}
</ul>
通過添加if判斷,使這些鏈接均在 current_user.is_authenticated 為 True,即用戶已登入的情況下才會(huì)渲染。Manage 下拉按鈕中包含管理文章、分類、評(píng)論的鏈接,New 下拉按鈕包含創(chuàng)建文章、分類的鏈接。
當(dāng)博客中有用戶提交了新的評(píng)論時(shí),我們需要在導(dǎo)航欄中添加提示。為此,我們?cè)?Manage 按鈕的文本中添加了一個(gè) if 判斷,如果 unread_comments 變量的值不為 0,就渲染一個(gè) new 標(biāo)記(badge)。相同的,在下拉列表中的“管理評(píng)論”鏈接文本中,如果 unread_comments 變量不為 0,就渲染出待審核的評(píng)論數(shù)量標(biāo)記。
這個(gè) unread_comments 變量存儲(chǔ)了待審核評(píng)論的數(shù)量,為了能夠在基模板中使用這個(gè)變量,我們需要在 bluelog//init.py 中創(chuàng)建的模板上下文處理函數(shù)中查詢未審核的評(píng)論數(shù)量,并傳入模板上下文。這個(gè)變量只在管理員登錄后才可使用,所以通過添加if判斷實(shí)現(xiàn)根據(jù)當(dāng)前用戶的認(rèn)證狀態(tài)來決定是否執(zhí)行查詢。
@app.context_processor def make_template_context(): ... if current_user.is_authenticated unread_comments = Comment.query.filter_by(reviewed=False).count() else: unread_comments = None return dict(unread_comments=unread_comments)
1.文章管理
我們要分別為分類、文章和評(píng)論創(chuàng)建單獨(dú)的管理頁面,這些內(nèi)容基本相同,因此本節(jié)會(huì)以文章的管理主頁作為介紹的重點(diǎn)。另外,分類的創(chuàng)建、編輯和刪除與文章的創(chuàng)建、編輯和刪除實(shí)現(xiàn)代碼基本相同,這里也將以文章相關(guān)操作的實(shí)現(xiàn)作為介紹重點(diǎn)。
1.1 文章管理主頁
我們?cè)阡秩疚恼鹿芾眄撁娴?manage_post 視圖時(shí),要查詢所有文章記錄,并進(jìn)行分頁處理,然后傳入模板中。
@admin_bp.route('/post/manage')
@login_required
def manage_post():
page = request.args.get('page', 1, type=int)
pagination = Post.query.order_by(Post.timestamp.desc()).paginate(
page, per_page=current_app.config['BLUELOG_MANAGE_POST_PER_PAGE'])
posts = pagination.items
return render_template('admin/manage_post.html', page=page, pagination=pagination, posts=posts)
在這個(gè)視圖渲染的 manage_category.html 模板中,我們以表格的形式顯示文章列表,依次渲染出文章的標(biāo)題、所屬的分類、發(fā)表時(shí)間、文章字?jǐn)?shù)、包含的評(píng)論數(shù)量以及相應(yīng)的操作按鈕。
{% extends 'base.html' %}
{% from 'bootstrap/pagination.html' import render_pagination %}
{% block title %}Manage Posts{% endblock %}
{% block content %}
<div class="page-header">
<h1>Posts
<small class="text-muted">{{ pagination.total }}</small>
<span class="float-right"><a class="btn btn-primary btn-sm"
href="{{ url_for('.new_post') }}" rel="external nofollow" >New Post</a></span>
</h1>
</div>
{% if posts %}
<table class="table table-striped">
<thead>
<tr>
<th>No.</th>
<th>Title</th>
<th>Category</th>
<th>Date</th>
<th>Comments</th>
<th>Words</th>
<th>Actions</th>
</tr>
</thead>
{% for post in posts %}
<tr>
<td>{{ loop.index + ((page - 1) * config.BLUELOG_MANAGE_POST_PER_PAGE) }}</td>
<td><a href="{{ url_for('blog.show_post', post_id=post.id) }}" rel="external nofollow" >{{ post.title }}</a></td>
<td><a href="{{ url_for('blog.show_category', category_id=post.category.id) }}" rel="external nofollow" >{{ post.category.name }}</a>
</td>
<td>{{ moment(post.timestamp).format('LL') }}</td>
<td><a href="{{ url_for('blog.show_post', post_id=post.id) }}#comments" rel="external nofollow" >{{ post.comments|length }}</a></td>
<td>{{ post.body|striptags|length }}</td>
<td>
<form class="inline" method="post"
action="{{ url_for('.set_comment', post_id=post.id, next=request.full_path) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-warning btn-sm">
{% if post.can_comment %}Disable{% else %}Enable{% endif %} Comment
</button>
</form>
<a class="btn btn-info btn-sm" href="{{ url_for('.edit_post', post_id=post.id) }}" rel="external nofollow" >Edit</a>
<form class="inline" method="post"
action="{{ url_for('.delete_post', post_id=post.id, next=request.full_path) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure?');">Delete
</button>
</form>
</td>
</tr>
{% endfor %}
</table>
<div class="page-footer">{{ render_pagination(pagination) }}</div>
{% else %}
<div class="tip"><h5>No posts.</h5></div>
{% endif %}
{% endblock %}
每一個(gè)文章記錄的左側(cè)都顯示一個(gè)序號(hào)標(biāo)記。如果單獨(dú)使用 loop.index 變量渲染數(shù)量標(biāo)記,那么每一頁的文章記錄都將從 1 到 15 重復(fù)(配置變量 BLUELOG_MANAGE_POST_PER_PAGE 的值),因?yàn)槊恳豁撟疃嘀挥?15 條文章記錄。正確的評(píng)論數(shù)量標(biāo)記可以通過 “當(dāng)前迭代數(shù) + ((當(dāng)前頁數(shù) - 1) × 每頁記錄數(shù))” 的形式獲取。
刪除操作會(huì)修改數(shù)據(jù)庫,為了避免 CSRF 攻擊,我們需要使用表單 form 元素來提交 POST 請(qǐng)求,表單中必須使用 CSRFProtect 提供的 csrf_token() 函數(shù)渲染包含 CSRF 令牌的隱藏字段,字段的 name 值需要設(shè)為 csrf_token。另外,用來刪除文章的視圖也需要設(shè)置僅監(jiān)聽 POST 方法。
文章的編輯和刪除按鈕并排顯示,由于兩個(gè)按鈕離得很近,可能會(huì)導(dǎo)致誤操作。而且一旦單擊刪除按鈕,文章就會(huì)立刻被刪除,故我們需要添加一個(gè)刪除確認(rèn)彈窗。對(duì)于我們的程序來說,使用瀏覽器內(nèi)置的確認(rèn)彈窗已經(jīng)足夠,只需要在 button 標(biāo)簽中添加一個(gè) onclick 屬性,設(shè)置為一行 JavaScript 代碼:return confirm(),在 confirm() 中傳入提示信息作為參數(shù)。運(yùn)行程序后,當(dāng)用戶單擊文章下方的刪除按鈕,會(huì)執(zhí)行這行代碼,跳出包含傳入信息的確認(rèn)彈窗,這會(huì)打開瀏覽器內(nèi)置的 confirm 彈窗組件。
當(dāng)用戶單擊確認(rèn)后,confirm() 會(huì)返回 True,這時(shí)才會(huì)訪問鏈接中的 URL。除了管理頁面,我們還在文章內(nèi)容頁面添加了編輯和刪除按鈕。文章管理頁面和文章正文頁面都包含刪除按鈕,但卻存在不同的行為:對(duì)于文章管理頁面來說,刪除文章后我們希望仍然重定向回文章管理頁面,所以對(duì)應(yīng)的 URL 中的 next 參數(shù)使用 request.full_path 獲取當(dāng)前路徑;而對(duì)于文章正文頁面,刪除文章后,原 URL 就不再存在,這時(shí)需要重定向到主頁,所以將 next 設(shè)為主頁 URL。
1.2 創(chuàng)建文章
博客最重要的功能就是撰寫文章,new_post 視圖負(fù)責(zé)渲染創(chuàng)建文章的模板,并處理頁面中表單提交的 POST 請(qǐng)求。
from bluelog.forms import PostForm
from bluelog.models import Post, Category
@admin_bp.route('/post/new', methods=['GET', 'POST'])
@login_required
def new_post():
form = PostForm()
if form.validate_on_submit():
title = form.title.data
body = form.body.data
category = Category.query.get(form.category.data)
post = Post(title=title, body=body, category=category)
# same with:
# category_id = form.category.data
# post = Post(title=title, body=body, category_id=category_id)
db.session.add(post)
db.session.commit()
flash('Post created.', 'success')
return redirect(url_for('blog.show_post', post_id=post.id))
return render_template('admin/new_post.html', form=form)
這里也可以直接通過將表單 category 字段的值賦給 Post 模型的外鍵字段 Post.category_id 來建立關(guān)系,即 category_id=form.category.data。在程序中,為了便于理解,均使用將具體對(duì)象賦值給關(guān)系屬性的方式來建立關(guān)系。
表單驗(yàn)證失敗會(huì)重新渲染模板,并顯示錯(cuò)誤消息。表單驗(yàn)證成功后,我們需要保存文章數(shù)據(jù)。各個(gè)表單字段的數(shù)據(jù)都通過 data 屬性獲取,創(chuàng)建一個(gè)新的 Post 實(shí)例作為文章對(duì)象,將表單數(shù)據(jù)賦值給對(duì)應(yīng)的模型類屬性。另外,因?yàn)楸韱畏诸愖侄危?code>PostForm.category)的值是分類記錄的 id 字段值,所以我們需要從 Category 模型查詢對(duì)應(yīng)的分類記錄,然后通過 Post 模型的 category 關(guān)系屬性來建立關(guān)系,即 category=Category.query.get(form.category.data)。將新創(chuàng)建的 post 對(duì)象添加到新數(shù)據(jù)庫會(huì)話并提交后,使用 redirect() 函數(shù)重定向到文章頁面,將新創(chuàng)建的 post 對(duì)象的 id 作為 URL 變量傳入 url_for() 函數(shù)。
當(dāng)請(qǐng)求類型為 GET 時(shí),這個(gè)視圖會(huì)實(shí)例化用于創(chuàng)建文章的 PostForm 表單,并將其傳入模板。在渲染的模板 new_post.html 中,我們使用 Bootstrap-Flask 提供的 render_form() 宏渲染表單。因?yàn)?PostForm 表單類中使用了擴(kuò)展 Flask-CKEditor 提供的 CKEditor 字段,所以在模板中需要加載 CKEditor 資源,并使用 ckeditor.config() 方法加載 CKEditor 配置。
{% extends 'base.html' %}
{% from 'bootstrap/form.html' import render_form %}
{% block title %}New Post{% endblock %}
{% block content %}
<div class="page-header">
<h1>New Post</h1>
</div>
{{ render_form(form) }}
{% endblock %}
{% block scripts %}
{{ super() }}
<script type="text/javascript" src="{{ url_for('static', filename='ckeditor/ckeditor.js') }}"></script>
{{ ckeditor.config(name='body') }}
{% endblock %}
CKEditor 的資源包我們已經(jīng)下載并放到 static 目錄下,這里只需要加載 ckeditor.js 文件即可。因?yàn)?CKEditor 編輯器只在創(chuàng)建或編輯文章的頁面使用,所以可以只在這些頁面加載對(duì)應(yīng)的資源,而不是在基模板中加載。
1.3 編輯與刪除
編輯文章的具體實(shí)現(xiàn)和撰寫新文章類似,這兩個(gè)功能使用同一個(gè)表單類 PostForm,而且視圖函數(shù)和模板文件都基本相同,主要的區(qū)別是我們需要在用戶訪問編輯頁面時(shí)把文章數(shù)據(jù)預(yù)先放置到表單中。
@admin_bp.route('/post/<int:post_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_post(post_id):
form = PostForm()
post = Post.query.get_or_404(post_id)
if form.validate_on_submit():
post.title = form.title.data
post.body = form.body.data
post.category = Category.query.get(form.category.data)
db.session.commit()
flash('Post updated.', 'success')
return redirect(url_for('blog.show_post', post_id=post.id))
form.title.data = post.title
form.body.data = post.body
form.category.data = post.category_id
return render_template('admin/edit_post.html', form=form)
edit_post 視圖的工作可以概括為:首先從數(shù)據(jù)庫中獲取指定 id 的文章。如果是 GET 請(qǐng)求,使用文章的數(shù)據(jù)作為表單數(shù)據(jù),然后渲染模板。如果是 POST 請(qǐng)求,即用戶單擊了提交按鈕,則根據(jù)表單的數(shù)據(jù)更新文章記錄的數(shù)據(jù)。
和保存文章時(shí)的做法相反,通過把數(shù)據(jù)庫字段的值分別賦給表單字段的數(shù)據(jù),在渲染表單時(shí),這些值會(huì)被填充到對(duì)應(yīng)的 input 標(biāo)簽的 value 屬性中,從而顯示在輸入框內(nèi)。需要注意,因?yàn)楸韱沃械姆诸愖侄问谴鎯?chǔ)分類記錄的 id 值,所以這里使用 post.category_id 作為 form.category.data 的值。
通過 delete_post 視圖可以刪除文章,我們首先從數(shù)據(jù)庫中獲取指定 id 的文章記錄,然后使 db.session.delete() 方法刪除記錄并提交數(shù)據(jù)庫。
from bluelog.utils import redirect_back
@admin_bp.route('/post/<int:post_id>/delete', methods=['POST'])
@login_required
def delete_post(post_id):
post = Post.query.get_or_404(post_id)
db.session.delete(post)
db.session.commit()
flash('Post deleted.', 'success')
return redirect_back()
這個(gè)視圖通過設(shè)置 methods 參數(shù)實(shí)現(xiàn)僅允許 POST 方法。因?yàn)樵谖恼鹿芾眄撁婧臀恼聝?nèi)容頁面都包含刪除按鈕,所以這里使用 redirect_back() 函數(shù)來重定向回上一個(gè)頁面。
2.評(píng)論管理
在編寫評(píng)論管理頁面前,我們要在文章內(nèi)容頁面的評(píng)論列表中添加刪除按鈕。
<div class="float-right">
<a class="btn btn-light btn-sm"
href="{{ url_for('.reply_comment', comment_id=comment.id) }}" rel="external nofollow" >Reply</a>
{% if current_user.is_authenticated %}
<a class="btn btn-light btn-sm" href="mailto:{{ comment.email }}" rel="external nofollow" >Email</a>
<form class="inline" method="post"
action="{{ url_for('admin.delete_comment', comment_id=comment.id, next=request.full_path) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-danger btn-sm"
onclick="return confirm('Are you sure?');">Delete
</button>
</form>
{% endif %}
</div>
因?yàn)閯h除按鈕同時(shí)會(huì)被添加到評(píng)論管理頁面的評(píng)論列表中,所以我們?cè)趧h除評(píng)論的 URL 后附加了 next 參數(shù),用于重定向回上一個(gè)頁面。如果當(dāng)前用戶是管理員,我們還會(huì)顯示除了管理員發(fā)表的評(píng)論以外的評(píng)論者郵箱,渲染成 mailto 鏈接。
和文章管理頁面類似,在評(píng)論管理頁面我們也會(huì)將評(píng)論以表格的形式列出,這里不再給出具體代碼。和文章管理頁面相比,評(píng)論管理頁面主要有兩處不同:添加批準(zhǔn)評(píng)論的按鈕以及在頁面上提供評(píng)論數(shù)據(jù)的篩選功能,我們將重點(diǎn)介紹這兩個(gè)功能的實(shí)現(xiàn)。在前臺(tái)頁面,除了評(píng)論刪除按鈕,我們還要向管理員提供關(guān)閉評(píng)論的功能,我們先來看看評(píng)論開關(guān)的具體實(shí)現(xiàn)。
2.1 關(guān)閉評(píng)論
盡管交流是社交的基本要素,但有時(shí)作者也希望不被評(píng)論打擾。為了支持評(píng)論開關(guān)功能,我們需要在 Post 模型中添加一個(gè)類型為 db.Boolean 的 can_comment 字段,用來存儲(chǔ)是否可以評(píng)論的布爾值,默認(rèn)值為 True。
class Post(db.Model): ... can_comment = db.Column(db.Boolean, default=True)
然后我們需要在模板中評(píng)論區(qū)右上方添加一個(gè)開關(guān)按鈕:
{% if current_user.is_authenticated %}
<form class="float-right" method="post"
action="{{ url_for('admin.set_comment', post_id=post.id, next=request.full_path) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-warning btn-sm">
{% if post.can_comment %}Disable{% else %}Enable{% endif %} Comment
</button>
</form>
{% endif %}
在管理文章的頁面,我們還在每一個(gè)文章的操作區(qū)添加了關(guān)閉和開啟評(píng)論的按鈕,渲染的方式基本相同,具體可以到源碼倉庫中查看。
<button type="submit" class="btn btn-warning btn-sm">
{% if post.can_comment %}Disable{% else %}Enable{% endif %} Comment
</button>
另外,在設(shè)置回復(fù)評(píng)論狀態(tài)的 reply_comment 視圖中,我們?cè)陂_始添加一個(gè) if 判斷,如果對(duì)應(yīng)文章不允許評(píng)論,那么就直接重定向回文章頁面。
@blog_bp.route('/reply/comment/<int:comment_id>')
def reply_comment(comment_id):
comment = Comment.query.get_or_404(comment_id)
if not comment.post.can_comment:
flash('Comment is disabled.', 'warning')
return redirect(url_for('.show_post', post_id=comment.post.id))
return redirect(
url_for('.show_post', post_id=comment.post_id, reply=comment_id, author=comment.author) + '#comment-form')
我們根據(jù) post.can_comment 的值來渲染不同的按鈕文本和表單 action 值。因?yàn)檫@個(gè)功能很簡單,所以兩個(gè)按鈕指向同一個(gè) URL,URL 對(duì)應(yīng)的 set_comment 視圖如下所示。
@admin_bp.route('/post/<int:post_id>/set-comment', methods=['POST'])
@login_required
def set_comment(post_id):
post = Post.query.get_or_404(post_id)
if post.can_comment:
post.can_comment = False
flash('Comment disabled.', 'success')
else:
post.can_comment = True
flash('Comment enabled.', 'success')
db.session.commit()
return redirect_back()
我們當(dāng)然可以分別創(chuàng)建一個(gè) enable_comment() 和 disable_comment() 視圖函數(shù)來開啟和關(guān)閉評(píng)論,但是因?yàn)楸容^簡單,所以我們可以將這兩個(gè)操作統(tǒng)一在 set_comment() 視圖函數(shù)中完成。在這個(gè)視圖函數(shù)里,我們首先獲取文章對(duì)象,然后根據(jù)文章的 can_comment 的值來設(shè)置相反的布爾值。
最后,我們還需要在評(píng)論表單的渲染代碼前添加一個(gè)判斷語句。如果管理員關(guān)閉了當(dāng)前博客的評(píng)論,那么一個(gè)相應(yīng)的提示會(huì)取代評(píng)論表單,顯示在評(píng)論區(qū)底部。
{% from 'bootstrap/form.html' import render_form %}
...
{% if post.can_comment %}
<div id="comment-form">
{{ render_form(form, action=request.full_path) }}
</div>
{% else %}
<div class="tip"><h5>Comment disabled.</h5></div>
{% endif %}
為了避免表單提交后因?yàn)?URL 中包含 URL 片段而跳轉(zhuǎn)到頁面的某個(gè)位置(Html 錨點(diǎn)),這里顯式地使用 action 屬性指定表單提交的目標(biāo) URL,使用 request.full_path 獲取不包含 URL 片段的當(dāng)前 URL(但包含我們需要的查詢字符串)。

2.2 評(píng)論審核
對(duì)于沒有通過審核的評(píng)論,在評(píng)論表格的操作列要添加一個(gè)批準(zhǔn)按鈕。如果評(píng)論對(duì)象的 reviewed 字段值為 False,則顯示 “批準(zhǔn)” 按鈕,并將該行評(píng)論以橙色背景顯示(添加 table-warning 樣式類)。
<td>
{% if not comment.reviewed %}
<form class="inline" method="post"
action="{{ url_for('.approve_comment', comment_id=comment.id, next=request.full_path) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-success btn-sm">Approve</button>
</form>
{% endif %}
...
</td>
因?yàn)檫@個(gè)操作會(huì)修改數(shù)據(jù),我們同樣需要使用表單 form 元素來提交 POST 請(qǐng)求。批準(zhǔn)按鈕指向的 approve_comment 視圖僅監(jiān)聽 POST 方法。
@admin_bp.route('/comment/<int:comment_id>/approve', methods=['POST'])
@login_required
def approve_comment(comment_id):
comment = Comment.query.get_or_404(comment_id)
comment.reviewed = True
db.session.commit()
flash('Comment published.', 'success')
return redirect_back()
在 approve_comment 視圖中,我們將對(duì)應(yīng)的評(píng)論記錄的 reviewed 字段設(shè)為 Ture,表示通過審核。通過審核后的評(píng)論會(huì)顯示在文章頁面下方的評(píng)論列表中。雖然評(píng)論的批準(zhǔn)功能只在管理評(píng)論頁面提供,我們?nèi)匀辉谶@里使用 redirect_back() 函數(shù)返回上一個(gè)頁面,這是因?yàn)樵u(píng)論管理頁面根據(jù)查詢參數(shù) filter 的值會(huì)顯示不同的過濾結(jié)果,而在 “全部” 和 “未讀” 結(jié)果中的未讀評(píng)論記錄都會(huì)有 “Approve” 按鈕,所以我們需要重定向回正確的過濾分類下。
為了正確返回上一個(gè)頁面,在表單 action 屬性中的 URL 后需要將 next 查詢參數(shù)的值設(shè)為 request.full_path 以獲取包含查詢字符串的完整路徑。
2.3 篩選評(píng)論
因?yàn)樵u(píng)論的數(shù)據(jù)比較復(fù)雜,我們需要在管理頁面提供評(píng)論的篩選功能。評(píng)論主要分為三類:所有評(píng)論、未讀評(píng)論和管理員發(fā)布的評(píng)論。我們將使用查詢參數(shù) filter 傳入篩選的評(píng)論類型,這三種類型分別使用 all、unread 和 admin 表示。在渲染評(píng)論管理主頁的 manage_comment 視圖中,我們從請(qǐng)求對(duì)象中獲取鍵為 filter 的查詢參數(shù)值,然后根據(jù)這個(gè)值獲取不同類別的記錄。
@admin_bp.route('/comment/manage')
@login_required
def manage_comment():
filter_rule = request.args.get('filter', 'all') # 'all', 'unreviewed', 'admin'
page = request.args.get('page', 1, type=int)
per_page = current_app.config['BLUELOG_COMMENT_PER_PAGE']
if filter_rule == 'unread':
filtered_comments = Comment.query.filter_by(reviewed=False)
elif filter_rule == 'admin':
filtered_comments = Comment.query.filter_by(from_admin=True)
else:
filtered_comments = Comment.query
pagination = filtered_comments.order_by(Comment.timestamp.desc()).paginate(page, per_page=per_page)
comments = pagination.items
return render_template('admin/manage_comment.html', comments=comments, pagination=pagination)
除了通過查詢字符串獲取篩選條件,也可以為 manage_comment 視圖附加一個(gè)路由,比如 @admin_bp.route(‘/comment/manage/<filter>’),通過 URL 變量 filter 獲取。另外,在 URL 規(guī)則中使用 any 轉(zhuǎn)換器可以指定可選值。
在 manage_comment.html 模板中,我們添加一排導(dǎo)航標(biāo)簽按鈕,分別用來獲取 “全部” “未讀” 和 “管理員” 類別的評(píng)論
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link disabled" href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Filter </a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.args.get('filter', 'all') == 'all' %}active{% endif %}"
href="{{ url_for('admin.manage_comment', filter='all') }}" rel="external nofollow" >All</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.args.get('filter') == 'unread' %}active{% endif %}"
href="{{ url_for('admin.manage_comment', filter='unread') }}" rel="external nofollow" >Unread {% if unread_comments %}<span
class="badge badge-success">{{ unread_comments }}</span>{% endif %}</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.args.get('filter') == 'admin' %}active{% endif %}"
href="{{ url_for('admin.manage_comment', filter='admin') }}" rel="external nofollow" >From Admin</a>
</li>
</ul>
三個(gè)選項(xiàng)的 URL 都指向 manage_comment 視圖,但都附加了查詢參數(shù) filter 的對(duì)應(yīng)值。
再次提醒一下,當(dāng)使用 url_for 生成 URL 時(shí),傳入的關(guān)鍵字參數(shù)如果不是 URL 變量,那么會(huì)作為查詢參數(shù)附加在 URL 后面。
這里的導(dǎo)航鏈接沒有使用 render_nav_item(),為了更大的靈活性而選擇手動(dòng)處理。在模板中,我們通過 request.args.get(‘filter’,‘all’) 獲取查詢參數(shù) filter 的值來決定是否為某個(gè)導(dǎo)航按鈕添加 active 類。默認(rèn)激活 All 按鈕,如果用戶單擊了篩選下拉列表中的 “Unread” 選項(xiàng),客戶端會(huì)發(fā)出一個(gè)請(qǐng)求到 http://localhost:5000/manage/comment?filter=unread,manage_comment 視圖就會(huì)返回對(duì)應(yīng)的未讀記錄,而模板中的 Unread 導(dǎo)航按鈕也會(huì)顯示激活狀態(tài),這時(shí)操作區(qū)域也會(huì)顯示一個(gè) Approve 按鈕。

3.分類管理
分類的管理功能比較簡單,這里不再完整講解,具體可以到源碼倉庫中查看。分類的刪除值得一提,實(shí)現(xiàn)分類的刪除功能有下面兩個(gè)要注意的地方:
- 禁止刪除默認(rèn)分類。
- 刪除某一分類時(shí)前,把該分類下的所有文章移動(dòng)到默認(rèn)分類中。
為了避免用戶刪除默認(rèn)分類,首先在模板中渲染分類列表時(shí)需要添加一個(gè) if 判斷,避免為默認(rèn)分類渲染編輯和刪除按鈕。在刪除分類的視圖函數(shù)中,我們?nèi)匀恍枰俅悟?yàn)證被刪除的分類是否是默認(rèn)分類。在視圖函數(shù)中使用刪除分類時(shí),我們首先判斷分類的 id,如果是默認(rèn)分類(因?yàn)槟J(rèn)分類最先創(chuàng)建,id 為 1),則返回錯(cuò)誤提示。
@admin_bp.route('/category/<int:category_id>/delete', methods=['POST'])
@login_required
def delete_category(category_id):
category = Category.query.get_or_404(category_id)
if category.id == 1:
flash('You can not delete the default category.', 'warning')
return redirect(url_for('blog.index'))
category.delete()
flash('Category deleted.', 'success')
return redirect(url_for('.manage_category'))
上面的視圖函數(shù)中,刪除分類使用的 delete() 方法是我們?cè)?Category 類中創(chuàng)建的方法,這個(gè)方法實(shí)現(xiàn)了第二個(gè)功能:將被刪除分類的文章的分類設(shè)為默認(rèn)分類,然后刪除該分類記錄。
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), unique=True)
posts = db.relationship('Post', back_populates='category')
def delete(self):
default_category = Category.query.get(1)
posts = self.posts[:]
for post in posts:
post.category = default_category
db.session.delete(self)
db.session.commit()
我們使用 Category.query.get(1) 獲取默認(rèn)分類記錄。這個(gè)方法迭代要?jiǎng)h除分類的所有相關(guān)文章記錄,為這些文章重新指定分類為默認(rèn)分類,然后 db.session.delete() 方法刪除分類記錄,最后提交數(shù)據(jù)庫會(huì)話。
到目前為止,Bluelog 程序的開發(fā)已經(jīng)基本結(jié)束了。
到此這篇關(guān)于Python個(gè)人博客程序開發(fā)實(shí)例后臺(tái)編寫的文章就介紹到這了,更多相關(guān)Python個(gè)人博客內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python使用OpenCV對(duì)彩色圖像進(jìn)行通道分離的項(xiàng)目實(shí)踐
通道分離是將彩色圖像的每個(gè)像素分解為三個(gè)通道(紅、綠、藍(lán))的過程,本文主要介紹了Python使用OpenCV對(duì)彩色圖像進(jìn)行通道分離的項(xiàng)目實(shí)踐,感興趣的可以了解一下2023-08-08
Pytorch distributed 多卡并行載入模型操作
這篇文章主要介紹了Pytorch distributed 多卡并行載入模型操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
Python+eval函數(shù)實(shí)現(xiàn)動(dòng)態(tài)地計(jì)算數(shù)學(xué)表達(dá)式詳解
Python的 eval() 允許從基于字符串或基于編譯代碼的輸入中計(jì)算任意Python表達(dá)式。當(dāng)從字符串或編譯后的代碼對(duì)象的任何輸入中動(dòng)態(tài)計(jì)算Python表達(dá)式時(shí),此函數(shù)非常方便。本文將利用eval實(shí)現(xiàn)動(dòng)態(tài)地計(jì)算數(shù)學(xué)表達(dá)式,需要的可以參考一下2022-09-09
Python3并發(fā)寫文件與Python對(duì)比
這篇文章主要介紹了Python3并發(fā)寫文件原理解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11
python爬蟲基礎(chǔ)教程:requests庫(二)代碼實(shí)例
這篇文章主要介紹了python爬蟲基礎(chǔ)教程:requests庫(二),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
一個(gè)Python優(yōu)雅的數(shù)據(jù)分塊方法詳解
在做需求過程中有一個(gè)對(duì)大量數(shù)據(jù)分塊處理的場景,具體來說就是幾十萬量級(jí)的數(shù)據(jù),分批處理,每次處理100個(gè)。這時(shí)就需要一個(gè)分塊功能的代碼。本文為大家分享了一個(gè)Python中優(yōu)雅的數(shù)據(jù)分塊方法,需要的可以參考一下2022-05-05

