Python個(gè)人博客程序開(kāi)發(fā)實(shí)例后臺(tái)編寫(xiě)
本篇博客將是Python個(gè)人博客程序開(kāi)發(fā)實(shí)例的最后一篇。本篇文章將會(huì)詳細(xì)介紹博客后臺(tái)的編寫(xiě)。
為了支持管理員管理文章、分類、評(píng)論和鏈接,我們需要提供后臺(tái)管理功能。通常來(lái)說(shuō),程序的這一部分被稱為管理后臺(tái)、控制面板或儀表盤(pán)等。這里通常會(huì)提供網(wǎng)站的資源信息和運(yùn)行狀態(tài),管理員可以統(tǒng)一查看和管理所有資源。管理員面板通常會(huì)使用獨(dú)立樣式的界面,所以你可以為這部分功能的模板創(chuàng)建一個(gè)單獨(dú)的基模板。為了保持簡(jiǎn)單,Bluelog 的管理后臺(tái)和前臺(tái)頁(yè)面使用相同的樣式。
Bluelog 的管理功能比較簡(jiǎn)單,我們沒(méi)有提供一個(gè)管理后臺(tái)主頁(yè),取而代之的是,我們?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>
通過(guò)添加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è)變量只在管理員登錄后才可使用,所以通過(guò)添加if判斷實(shí)現(xiàn)根據(jù)當(dāng)前用戶的認(rèn)證狀態(tài)來(lá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ú)的管理頁(yè)面,這些內(nèi)容基本相同,因此本節(jié)會(huì)以文章的管理主頁(yè)作為介紹的重點(diǎn)。另外,分類的創(chuàng)建、編輯和刪除與文章的創(chuàng)建、編輯和刪除實(shí)現(xiàn)代碼基本相同,這里也將以文章相關(guān)操作的實(shí)現(xiàn)作為介紹重點(diǎn)。
1.1 文章管理主頁(yè)
我們?cè)阡秩疚恼鹿芾眄?yè)面的 manage_post
視圖時(shí),要查詢所有文章記錄,并進(jìn)行分頁(yè)處理,然后傳入模板中。
@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)記,那么每一頁(yè)的文章記錄都將從 1 到 15 重復(fù)(配置變量 BLUELOG_MANAGE_POST_PER_PAGE
的值),因?yàn)槊恳豁?yè)最多只有 15 條文章記錄。正確的評(píng)論數(shù)量標(biāo)記可以通過(guò) “當(dāng)前迭代數(shù) + ((當(dāng)前頁(yè)數(shù) - 1) × 每頁(yè)記錄數(shù))” 的形式獲取。
刪除操作會(huì)修改數(shù)據(jù)庫(kù),為了避免 CSRF 攻擊,我們需要使用表單 form
元素來(lái)提交 POST 請(qǐng)求,表單中必須使用 CSRFProtect 提供的 csrf_token()
函數(shù)渲染包含 CSRF 令牌的隱藏字段,字段的 name
值需要設(shè)為 csrf_token
。另外,用來(lái)刪除文章的視圖也需要設(shè)置僅監(jiān)聽(tīng) POST 方法。
文章的編輯和刪除按鈕并排顯示,由于兩個(gè)按鈕離得很近,可能會(huì)導(dǎo)致誤操作。而且一旦單擊刪除按鈕,文章就會(huì)立刻被刪除,故我們需要添加一個(gè)刪除確認(rèn)彈窗。對(duì)于我們的程序來(lái)說(shuō),使用瀏覽器內(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ì)打開(kāi)瀏覽器內(nèi)置的 confirm
彈窗組件。
當(dāng)用戶單擊確認(rèn)后,confirm()
會(huì)返回 True
,這時(shí)才會(huì)訪問(wèn)鏈接中的 URL。除了管理頁(yè)面,我們還在文章內(nèi)容頁(yè)面添加了編輯和刪除按鈕。文章管理頁(yè)面和文章正文頁(yè)面都包含刪除按鈕,但卻存在不同的行為:對(duì)于文章管理頁(yè)面來(lái)說(shuō),刪除文章后我們希望仍然重定向回文章管理頁(yè)面,所以對(duì)應(yīng)的 URL 中的 next
參數(shù)使用 request.full_path
獲取當(dāng)前路徑;而對(duì)于文章正文頁(yè)面,刪除文章后,原 URL 就不再存在,這時(shí)需要重定向到主頁(yè),所以將 next
設(shè)為主頁(yè) URL。
1.2 創(chuàng)建文章
博客最重要的功能就是撰寫(xiě)文章,new_post
視圖負(fù)責(zé)渲染創(chuàng)建文章的模板,并處理頁(yè)面中表單提交的 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)
這里也可以直接通過(guò)將表單 category
字段的值賦給 Post
模型的外鍵字段 Post.category_id
來(lái)建立關(guān)系,即 category_id=form.category.data
。在程序中,為了便于理解,均使用將具體對(duì)象賦值給關(guān)系屬性的方式來(lái)建立關(guān)系。
表單驗(yàn)證失敗會(huì)重新渲染模板,并顯示錯(cuò)誤消息。表單驗(yàn)證成功后,我們需要保存文章數(shù)據(jù)。各個(gè)表單字段的數(shù)據(jù)都通過(guò) data
屬性獲取,創(chuàng)建一個(gè)新的 Post
實(shí)例作為文章對(duì)象,將表單數(shù)據(jù)賦值給對(duì)應(yīng)的模型類屬性。另外,因?yàn)楸韱畏诸愖侄危?code>PostForm.category)的值是分類記錄的 id
字段值,所以我們需要從 Category
模型查詢對(duì)應(yīng)的分類記錄,然后通過(guò) Post
模型的 category
關(guān)系屬性來(lái)建立關(guān)系,即 category=Category.query.get(form.category.data)
。將新創(chuàng)建的 post
對(duì)象添加到新數(shù)據(jù)庫(kù)會(huì)話并提交后,使用 redirect()
函數(shù)重定向到文章頁(yè)面,將新創(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)建或編輯文章的頁(yè)面使用,所以可以只在這些頁(yè)面加載對(duì)應(yīng)的資源,而不是在基模板中加載。
1.3 編輯與刪除
編輯文章的具體實(shí)現(xiàn)和撰寫(xiě)新文章類似,這兩個(gè)功能使用同一個(gè)表單類 PostForm,而且視圖函數(shù)和模板文件都基本相同,主要的區(qū)別是我們需要在用戶訪問(wèn)編輯頁(yè)面時(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ù)庫(kù)中獲取指定 id
的文章。如果是 GET 請(qǐng)求,使用文章的數(shù)據(jù)作為表單數(shù)據(jù),然后渲染模板。如果是 POST 請(qǐng)求,即用戶單擊了提交按鈕,則根據(jù)表單的數(shù)據(jù)更新文章記錄的數(shù)據(jù)。
和保存文章時(shí)的做法相反,通過(guò)把數(shù)據(jù)庫(kù)字段的值分別賦給表單字段的數(shù)據(jù),在渲染表單時(shí),這些值會(huì)被填充到對(duì)應(yīng)的 input
標(biāo)簽的 value
屬性中,從而顯示在輸入框內(nèi)。需要注意,因?yàn)楸韱沃械姆诸愖侄问谴鎯?chǔ)分類記錄的 id
值,所以這里使用 post.category_id
作為 form.category.data
的值。
通過(guò) delete_post
視圖可以刪除文章,我們首先從數(shù)據(jù)庫(kù)中獲取指定 id
的文章記錄,然后使 db.session.delete()
方法刪除記錄并提交數(shù)據(jù)庫(kù)。
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è)視圖通過(guò)設(shè)置 methods
參數(shù)實(shí)現(xiàn)僅允許 POST 方法。因?yàn)樵谖恼鹿芾眄?yè)面和文章內(nèi)容頁(yè)面都包含刪除按鈕,所以這里使用 redirect_back()
函數(shù)來(lái)重定向回上一個(gè)頁(yè)面。
2.評(píng)論管理
在編寫(xiě)評(píng)論管理頁(yè)面前,我們要在文章內(nèi)容頁(yè)面的評(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)論管理頁(yè)面的評(píng)論列表中,所以我們?cè)趧h除評(píng)論的 URL 后附加了 next
參數(shù),用于重定向回上一個(gè)頁(yè)面。如果當(dāng)前用戶是管理員,我們還會(huì)顯示除了管理員發(fā)表的評(píng)論以外的評(píng)論者郵箱,渲染成 mailto
鏈接。
和文章管理頁(yè)面類似,在評(píng)論管理頁(yè)面我們也會(huì)將評(píng)論以表格的形式列出,這里不再給出具體代碼。和文章管理頁(yè)面相比,評(píng)論管理頁(yè)面主要有兩處不同:添加批準(zhǔn)評(píng)論的按鈕以及在頁(yè)面上提供評(píng)論數(shù)據(jù)的篩選功能,我們將重點(diǎn)介紹這兩個(gè)功能的實(shí)現(xiàn)。在前臺(tái)頁(yè)面,除了評(píng)論刪除按鈕,我們還要向管理員提供關(guān)閉評(píng)論的功能,我們先來(lái)看看評(píng)論開(kāi)關(guān)的具體實(shí)現(xiàn)。
2.1 關(guān)閉評(píng)論
盡管交流是社交的基本要素,但有時(shí)作者也希望不被評(píng)論打擾。為了支持評(píng)論開(kāi)關(guān)功能,我們需要在 Post 模型中添加一個(gè)類型為 db.Boolean
的 can_comment
字段,用來(lái)存儲(chǔ)是否可以評(píng)論的布爾值,默認(rèn)值為 True
。
class Post(db.Model): ... can_comment = db.Column(db.Boolean, default=True)
然后我們需要在模板中評(píng)論區(qū)右上方添加一個(gè)開(kāi)關(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 %}
在管理文章的頁(yè)面,我們還在每一個(gè)文章的操作區(qū)添加了關(guān)閉和開(kāi)啟評(píng)論的按鈕,渲染的方式基本相同,具體可以到源碼倉(cāng)庫(kù)中查看。
<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è)陂_(kāi)始添加一個(gè) if
判斷,如果對(duì)應(yīng)文章不允許評(píng)論,那么就直接重定向回文章頁(yè)面。
@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
的值來(lái)渲染不同的按鈕文本和表單 action
值。因?yàn)檫@個(gè)功能很簡(jiǎn)單,所以兩個(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ù)來(lái)開(kāi)啟和關(guān)閉評(píng)論,但是因?yàn)楸容^簡(jiǎn)單,所以我們可以將這兩個(gè)操作統(tǒng)一在 set_comment()
視圖函數(shù)中完成。在這個(gè)視圖函數(shù)里,我們首先獲取文章對(duì)象,然后根據(jù)文章的 can_comment
的值來(lái)設(shè)置相反的布爾值。
最后,我們還需要在評(píng)論表單的渲染代碼前添加一個(gè)判斷語(yǔ)句。如果管理員關(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)到頁(yè)面的某個(gè)位置(Html 錨點(diǎn)),這里顯式地使用 action
屬性指定表單提交的目標(biāo) URL,使用 request.full_path
獲取不包含 URL 片段的當(dāng)前 URL(但包含我們需要的查詢字符串)。
2.2 評(píng)論審核
對(duì)于沒(méi)有通過(guò)審核的評(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
元素來(lái)提交 POST 請(qǐng)求。批準(zhǔn)按鈕指向的 approve_comment
視圖僅監(jiān)聽(tīng) 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
,表示通過(guò)審核。通過(guò)審核后的評(píng)論會(huì)顯示在文章頁(yè)面下方的評(píng)論列表中。雖然評(píng)論的批準(zhǔn)功能只在管理評(píng)論頁(yè)面提供,我們?nèi)匀辉谶@里使用 redirect_back()
函數(shù)返回上一個(gè)頁(yè)面,這是因?yàn)樵u(píng)論管理頁(yè)面根據(jù)查詢參數(shù) filter
的值會(huì)顯示不同的過(guò)濾結(jié)果,而在 “全部” 和 “未讀” 結(jié)果中的未讀評(píng)論記錄都會(huì)有 “Approve” 按鈕,所以我們需要重定向回正確的過(guò)濾分類下。
為了正確返回上一個(gè)頁(yè)面,在表單 action
屬性中的 URL 后需要將 next
查詢參數(shù)的值設(shè)為 request.full_path
以獲取包含查詢字符串的完整路徑。
2.3 篩選評(píng)論
因?yàn)樵u(píng)論的數(shù)據(jù)比較復(fù)雜,我們需要在管理頁(yè)面提供評(píng)論的篩選功能。評(píng)論主要分為三類:所有評(píng)論、未讀評(píng)論和管理員發(fā)布的評(píng)論。我們將使用查詢參數(shù) filter
傳入篩選的評(píng)論類型,這三種類型分別使用 all
、unread
和 admin
表示。在渲染評(píng)論管理主頁(yè)的 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)
除了通過(guò)查詢字符串獲取篩選條件,也可以為 manage_comment
視圖附加一個(gè)路由,比如 @admin_bp.route(‘/comment/manage/<filter>’
),通過(guò) URL 變量 filter
獲取。另外,在 URL 規(guī)則中使用 any
轉(zhuǎn)換器可以指定可選值。
在 manage_comment.html
模板中,我們添加一排導(dǎo)航標(biāo)簽按鈕,分別用來(lái)獲取 “全部” “未讀” 和 “管理員” 類別的評(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)航鏈接沒(méi)有使用 render_nav_item()
,為了更大的靈活性而選擇手動(dòng)處理。在模板中,我們通過(guò) request.args.get(‘filter’,‘all’)
獲取查詢參數(shù) filter
的值來(lái)決定是否為某個(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.分類管理
分類的管理功能比較簡(jiǎn)單,這里不再完整講解,具體可以到源碼倉(cāng)庫(kù)中查看。分類的刪除值得一提,實(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ù)庫(kù)會(huì)話。
到目前為止,Bluelog 程序的開(kāi)發(fā)已經(jīng)基本結(jié)束了。
到此這篇關(guān)于Python個(gè)人博客程序開(kāi)發(fā)實(shí)例后臺(tái)編寫(xiě)的文章就介紹到這了,更多相關(guān)Python個(gè)人博客內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
通過(guò)Python收集匯聚MySQL 表信息的實(shí)例詳解
這篇文章主要介紹了通過(guò)Python收集匯聚MySQL 表信息的實(shí)例代碼,核心代碼是創(chuàng)建保存數(shù)據(jù)的腳本,收集的功能腳本,代碼簡(jiǎn)單明了,需要的朋友可以參考下2021-10-10利用Python實(shí)現(xiàn)RSA加密解密方法實(shí)例
過(guò)去幾天我一直在嘗試用Python實(shí)現(xiàn)RSA算法,下面這篇文章主要給大家介紹了關(guān)于利用Python實(shí)現(xiàn)RSA加密解密的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04Python pandas RFM模型應(yīng)用實(shí)例詳解
這篇文章主要介紹了Python pandas RFM模型應(yīng)用,結(jié)合實(shí)例形式詳細(xì)分析了pandas RFM模型的概念、原理、應(yīng)用及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2019-11-11python使用pyecharts庫(kù)畫(huà)地圖數(shù)據(jù)可視化的實(shí)現(xiàn)
這篇文章主要介紹了python使用pyecharts庫(kù)畫(huà)地圖數(shù)據(jù)可視化的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03Python Django Cookie 簡(jiǎn)單用法解析
這篇文章主要介紹了Python Django Cookie 簡(jiǎn)單用法解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08python3+PyQt5實(shí)現(xiàn)拖放功能
這篇文章主要為大家詳細(xì)介紹了python3+PyQt5實(shí)現(xiàn)拖放功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04使用python將時(shí)間轉(zhuǎn)換為指定的格式方法
今天小編就為大家分享一篇使用python將時(shí)間轉(zhuǎn)換為指定的格式方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-11-11