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