網站首頁 編程語言 正文
Python個人博客程序開發實例框架設計中,我們已經完成了 數據庫設計、數據準備、模板架構、表單設計、視圖函數設計、電子郵件支持 等總體設計的內容,本篇博客將介紹博客前臺的實現。博客前臺需要開放給所有用戶,這里包括 顯示文章列表、博客信息、文章內容和評論 等功能。
1.分頁顯示文章列表
為了在主頁顯示文章列表,我們要先在渲染主頁模板的 index
視圖中的數據庫中獲取所有文章記錄并傳入模板。
@blog_bp.route('/') def index():
在主頁模板(index.html
)中,我們使用 for 語句迭代所有文章記錄,依次渲染文章標題、發表時間和正文。
因為我們已經生成了虛擬數據,其中包含 50 篇文章。現在運行程序,首頁會顯示一個很長的文章列表,根據創建的隨機日期排序,最新發表的排在上面。
1.1 獲取分頁記錄
如果所有的文章都在主頁顯示,無疑將會延長頁面加載時間。而且用戶需要拖動滾動條來瀏覽文章,太長的網頁會讓人感到沮喪,從而降低用戶體驗度。更好的做法是對文章數據進行分頁處理,每一頁只顯示少量的文章,并在頁面底部顯示一個分頁導航條,用戶通過單擊分頁導航上的頁數按鈕來訪問其他頁的文章。Flask-SQLAlchemy
提供了簡單的分頁功能,使用 paginate()
查詢方法可以分頁獲取文章記錄。
@blog_bp.route('/') def index(): page = request.args.get('page', 1, type=int) # 從查詢字符串獲取當前頁數 per_page = current_app.config['BLUELOG_POST_PER_PAGE'] # 每頁數量 pagination = Post.query.order_by(Post.timestamp.desc()).paginate(page, per_page=per_page) # 分頁對象 posts = pagination.items # 當前頁數的記錄列表 return render_template('blog/index.html', pagination=pagination, posts=posts)
為了實現分頁,我們把之前的查詢執行函數 all()
換成了 paginate()
,它接收的兩個最主要的參數分別用來決定把記錄分成幾頁 per_page
,返回哪一頁的記錄 page
。page
參數代表當前請求的頁數,我們從請求的查詢字符串(request.args
)中獲取,如果沒有設置則使用默認值 1,指定 int
類型可以保證在參數類型錯誤時使用默認值;per_page
參數設置每頁返回的記錄數量,為了方便統一修改,這個值從配置變量 BLUELOG_POST_PER_PAGE
獲取。
調用查詢方法 paginate()
會返回一個 Pagination
類實例,它包含分頁的信息,我們將其稱為分頁對象。對這個 pagination
對象調用 items
屬性會以列表的形式返回對應頁數(默認為第一頁)的記錄。在訪問這個 URL 時,如果在 URL 后附加了查詢參數 page
來指定頁數,例如 http://localhost:5000/?page=2
,這時發起請求調用 items
變量將會獲得第二頁的 10 條記錄。
1.2 渲染分頁導航部件
我們不能讓用戶通過在 URL 中附加查詢字符串來實現分頁瀏覽,而是應該在頁面底部提供一個分頁導航部件。這個分頁導航部件應該包含上一頁、下一頁以及跳轉到每一頁的按鈕,每個按鈕都包含指向主頁的 URL,而且 URL 中都添加了對應的查詢參數 page
的值。使用 paginate
方法時,它會返回一個 Pagination
類對象,這個類包含很多用于實現分頁導航的方法和屬性,我們可以用它來獲取所有關于分頁的信息。
對于博客來說,設置一個簡單的包含上一頁、下一頁按鈕的分頁部件就足夠了。在視圖函數中,我們將分頁對象 pagination
傳入模板,然后在模板中使用它提供的方法和屬性來構建分頁部件。
Bootstrap-Flask
已經內置了一個包含同樣功能,而且提供更多自定義設置的 render_pager()
宏。除此之外,它還提供了一個 render_pagination()
宏,可以用來渲染一個標準的 Bootstrap Pagination
分頁導航部件。render_pagination()
宏支持的常用參數如下表所示。
{% from 'bootstrap/pagination.html' import render_pager %} ... {{ render_pager(pagination) }}
2.顯示文章正文
文章頁面通過 show_post
視圖渲染,路由的 URL 規則中包含一個 post_id
變量,我們將 post_id
作為主鍵值來查詢對應的文章對象,并傳入模板 post.html
中渲染。
@blog_bp.route('/post/<int:post_id>') def show_post(post_id): post = Post.query.get_or_404(post_id) return render_template('blog/post.html', post=post)
3.文章固定鏈接
在 Bluelog 程序中,文章的固定鏈接使用文章記錄的 id
值來構建,比如 http://example.com/post/120
表示 id
為 120
的文章。如果你想要一個可讀性更強、對用戶和搜索引擎更友好的固定鏈接,可以考慮把標題轉換成英文或拼音,使用處理后的標題(即 slug)構建固定鏈接,比如 http://example.com/post/hello-world
表示標題為 Hello World 的文章。
在 Bluelog 中,為了方便用戶獲取固定鏈接,我們在文章正文下面添加了一個分享按鈕,這個分享按鈕用來打開包含文章固定鏈接的模態框(Modal,又被譯為模態對話框)。
4.顯示分類文章列表
分頁處理在數量上讓文章更有組織性,但在文章內容上,我們還需要添加分類來進一步組織文章。在渲染分類頁面的 show_category
視圖中,首先需要獲取對應的分類記錄,然后獲取分類下的所有文章,進行分頁處理,最后將分類記錄 category
、分頁文章記錄 posts
和分頁對象 pagination
都傳入模板。
@blog_bp.route('/category/<int:category_id>') def show_category(category_id): category = Category.query.get_or_404(category_id) page = request.args.get('page', 1, type=int) per_page = current_app.config['BLUELOG_POST_PER_PAGE'] pagination = Post.query.with_parent(category).order_by(Post.timestamp.desc()).paginate(page, per_page) posts = pagination.items return render_template('blog/category.html', category=category, pagination=pagination, posts=posts)
我們需要獲取對應分類下的所有文章,如果我們直接調用 category.posts
,會以列表的形式返回該分類下的所有文章對象,但是我們需要對這些文章記錄附加其他查詢過濾器和方法,所以不能使用這個方法。在上面的查詢中,我們仍然從 post
模型出發,使用 with_parent()
查詢方法傳入分類對象,最終篩選出屬于該分類的所有文章記錄。因為調用 with_parent()
查詢方法會返回查詢對象,所以我們可以繼續附加其他查詢方法來過濾文章記錄。
5.顯示評論列表
評論列表在顯示文章的頁面顯示,我們首先在顯示文章的 show_post
視圖中獲取對應的文章,然后使用 Comment.query.with_parent(post)
方法獲取文章所屬的評論,并對其進行排序和分頁處理(per_page
的值通過配置變量 BLUELOG_COMMENT_PER_PAGE
獲取),獲取對應頁數的評論記錄,最后傳入模板中。
@blog_bp.route('/post/<int:post_id>', methods=['GET', 'POST']) def show_post(post_id): post = Post.query.get_or_404(post_id) page = request.args.get('page', 1, type=int) per_page = current_app.config['BLUELOG_COMMENT_PER_PAGE'] pagination = Comment.query.with_parent(post).filter_by(reviewed=True).order_by(Comment.timestamp.asc()).paginate(page, per_page) comments = pagination.items return render_template('blog/post.html', post=post, pagination=pagination, form=form, comments=comments)
評論列表里僅需要列出通過審核的評論,所以在視圖函數里的數據庫查詢使用filter_by(reviewed=True)
來篩選出通過審核的評論記錄。雖然這個篩選也可以通過在模板中迭代評論列表時通過 reviewed
屬性實現,但更合理的做法是盡量在視圖函數中實現邏輯操作。
評論是個人博客唯一的社交元素,故不僅要實現添加評論功能,還要在評論上添加回復按鈕,這樣可以使作者和評論者之間的雙向交流變成不同用戶之間的多維交流。在頁面中,評論有多種組織方式,比如將回復通過縮進嵌套到父評論下面的嵌套式、所有評論都對齊列出的平鋪式。Bluelog 中將使用平鋪式顯示評論列表,回復的評論會顯示一個回復標記,并在正文添加被回復的評論內容。
我們在文章正文下方渲染評論列表和分頁導航部件。
評論的下方使用 Bootstrap-Flask 提供的 render_pagination()
來渲染一個標準分頁導航部件。在調用 render_pagination()
宏時,除了傳入分頁對象 pagination
外,我們還使用關鍵字 fragment
傳入了向分頁按鈕的鏈接中添加的 URL 片段(評論區元素的 id
為 comments
),以便單擊分頁按鈕后跳轉到頁面的評論部分,而不是停在頁面頂部。
6.發表評論與回復
因為評論表單要顯示在文章頁面的評論列表下方,所以評論數據的驗證和保存在 show_post
視圖中處理。
from bluelog.models import Comment from bluelog.forms import AdminCommentForm, CommentForm from bluelog.emails import send_new_comment_email @blog_bp.route('/post/<int:post_id>', methods=['GET', 'POST']) def show_post(post_id): post = Post.query.get_or_404(post_id) page = request.args.get('page', 1, type=int) per_page = current_app.config['BLUELOG_COMMENT_PER_PAGE'] pagination = Comment.query.with_parent(post).filter_by(reviewed=True).order_by(Comment.timestamp.asc()).paginate(page, per_page) comments = pagination.items if current_user.is_authenticated: # 如果當前用戶已登錄,使用管理員表單 form = AdminCommentForm() form.author.data = current_user.name form.email.data = current_app.config['BLUELOG_EMAIL'] form.site.data = url_for('.index') from_admin = True reviewed = True else: # 未登錄則使用普通表單 form = CommentForm() from_admin = False reviewed = False if form.validate_on_submit(): author = form.author.data email = form.email.data site = form.site.data body = form.body.data comment = Comment( author=author, email=email, site=site, body=body, from_admin=from_admin, post=post, reviewed=reviewed) db.session.add(comment) db.session.commit() if current_user.is_authenticated: # 根據登錄狀態不同顯示不同的提示信息 flash('Comment published.', 'success') else: flash('Thanks, your comment will be published after reviewed.', 'info') send_new_comment_email(post) # 發送郵件給管理員 return redirect(url_for('.show_post', post_id=post_id)) return render_template('blog/post.html', post=post, pagination=pagination, form=form, comments=comments)
current_user.is_authenticated
變量是從 Flask-Login
導入的,這個布爾值代表當前用戶的登錄狀態。
在處理評論時,我們主要需要對管理員和匿名用戶做出區分。首先通過 current_user.is_authenticated
屬性判斷當前用戶的認證狀態:如果當前用戶已經通過認證,那么就實例化管理員表單類 AdminCommentForm
,并把表單類中的姓名(author)、電子郵箱(email)、站點(site)這三個隱藏字段預先賦予正確的值,創建 from_admin
和 reviewed
變量,兩者均設為 True
;如果當前用戶是匿名用戶,則實例化普通的評論表單類 CommentForm
,創建 from_admin
和 reviewed
變量,兩者均設為 False
。
在表單提交并通過驗證后,我們像往常一樣獲取數據并保存。實例化 Comment
類時,傳入的 from_admin
和 reviewed
參數值使用對應的變量。在保存評論記錄后,我們也需要根據當前用戶的認證狀態閃現不同的消息:如果當前用戶是管理員,發送 “提交評論成功”;如果是匿名用戶,則發送 “評論已進入審核隊列,審核通過后將顯示在評論列表中”,另外還要調用 send_new_comment_email()
函數向管理員發送一個提醒郵件,傳入文章對象(post)作為參數。
7.支持回復評論
我們已經在數據庫中添加了評論與被回復評論的鄰接列表關系,那么如何實現回復功能呢?首先,需要知道當用戶單擊回復按鈕時,對應的是哪一條評論。可以通過渲染一個隱藏的表單來存儲被回復評論的 id
,然后在用戶提交表單時再查找它。更簡單的做法是添加一個新的視圖,通過路由 URL 規則中的變量來傳遞這個值,我們在前面編寫評論列表模板時加入了回復按鈕。
<a class="btn btn-primary btn-sm" href="{{ url_for('.reply_comment',comment_id=comment.id) }}" ></a>
@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')
在這個視圖函數的 return
語句中,我們將程序重定向到原來的文章頁面。附加的關鍵字參數除了必須的 post_id
變量外,我們還添加了兩個多余的參數:reply
和 author
,對應的值分別是被回復評論的 id
和被回復評論的作者。url_for()
函數后附加的 URL 片段 #comment-form
用來將頁面焦點跳到評論表單的位置。
當使用 url_for()
函數構建 URL 時,任何多余的關鍵字參數(即未在目標端點的 URL)都會被自動轉換為查詢字符串。當我們單擊某個評論右側的回復按鈕時,重定向后的頁面 URL 將會是http://localhost:5000/photo/23?id=4&author=peter#comment-form。
簡單來說,reply_comment
視圖扮演了中轉站的角色。它把通過 URL 變量接收的數據通過查詢字符串傳遞給了需要處理評論的視圖。
下一步,我們需要在回復狀態添加提示,在評論表單上方顯示一個回復提醒條,讓用戶知道自己現在處于回復狀態。我們在模板中評論表單上方通過 request.args
屬性獲取查詢字符串傳遞的信息以在回復提示條顯示被回復的用戶名稱。
{% if request.args.get('reply') %} <div class="alert alert-dark"> Reply to <strong>{{ request.args.get('author') }}</strong>: <a class="float-right" href="{{ url_for('.show_post',post_id=post.id) }}" >Cancel</a> </div> {% endif %}
在 show_post
視圖中,處理評論的代碼也要進行相應更新。
from bluelog.emails import send_new_reply_email @blog_bp.route('/post/<int:post_id>', methods=['GET', 'POST']) def show_post(post_id): ... if form.validate_on_submit(): ... replied_id = request.args.get('reply') if replied_id: # 如果 URL 中 reply 查詢參數存在,那么說明是回復 replied_comment= Comment.query.get_or_404(replied_id) comment.replied = replied_comment send_new_reply_email(replied_comment) # 發送郵件給被回復用戶 ...
新添加的 if
語句判斷請求 URL 的查詢字符串中是否包含 replied_id
的值,如果包含,則表示提交的評論是一條回復。我們根據 relied_id
的值查找對應的評論對象,然后存儲到被提交評論的 replied
屬性以建立數據庫關系,最后調用 send_new_reply_email()
函數發送提示郵件給被回復的評論的作者,傳入被回復評論作為參數。
8.網站主題切換
主題切換的功能很簡單,具體原理就是根據用戶的選擇加載不同的 CSS 文件。為了方便切換,我們在程序 static 目錄下的 CSS 文件夾中下載了兩個 Bootswatch 中的 Bootstrap 主題 CSS 文件,分別命名為 perfect_blue.min.css
和 black_swan.min.css
。
在配置文件中,我們新建一個變量,保存主題名稱(與 CSS 文件名相對應)和顯示名稱的映射字典:
# ('theme name', 'display name') BLUELOG_THEMES = {'perfect_blue': 'Perfect Blue', 'black_swan': 'Black Swan'}
為了讓這個功能能夠被所有用戶使用,我們將會把這個主題選項的值存儲在客戶端 cookie
中,新創建的 change_theme
視圖用于將主題名稱保存到名為 theme
的 cookie
中。
@blog_bp.route('/change-theme/<theme_name>') def change_theme(theme_name): if theme_name not in current_app.config['BLUELOG_THEMES'].keys(): abort(404) response = make_response(redirect_back()) response.set_cookie('theme', theme_name, max_age=30 * 24 * 60 * 60) return response
視圖函數中的 if
判斷用來確保 URL 變量中的主題名稱在支持的范圍內,如果出錯就返回 404
錯誤響應。
我們使用 make_response()
方法生成一個重定向響應,這里使用了重定向到上一個頁面的重定向輔助函數 redirect_back()
,因為主題切換下拉列表將添加在邊欄,用戶可能在任一頁面切換主題。通過對響應對象 response
調用 set_cookie
設置 cookie
,將主題的名稱保存在名為 theme
的 cookie
中,我們使用 max_age
參數將 cookie
的過期時間設為 30 天。
在基模板的 < head> 元素內,我們根據用戶的 theme cookie
的值來加載對應的 CSS 文件,如果 theme cookie
的值不存在,則會使用默認值 perfect_blue
,加載默認的 perfect_blue.min.css
。
<link href="{{ url_for('static', filename='css/%s.min.css' % request.cookies.get('theme', 'perfect_blue')) }}" type="text/css">
在邊欄最下方,我們添加用于設置主題的下拉選擇列表。
<div class="dropdown"> <button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Change Theme </button> <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> {% for theme_name, display_name in config.BLUELOG_THEMES.items() %} <a class="dropdown-item" href="{{ url_for('blog.change_theme', theme_name=theme_name, next=request.full_path) }}" > {{ display_name }}</a> {% endfor %} </div> </div>
在上面的 HTML 代碼中,我們通過迭代主題配置變量 BLUELOG_THEMES
,渲染出下拉選項,選項的 URL 指向 change_theme
端點,傳入主題名稱作為 URL 變量 theme_name
的值。現在,如果在下拉框中選擇 Black Swan,theme cookie
的值就會被設為 black_swan
,頁面重載后會加載 black_swan.min.css
,從而起到切換主題的效果。
原文鏈接:https://blog.csdn.net/be_racle/article/details/127836633
相關推薦
- 2022-02-02 使用layui框架時,select的onchange事件沒有生效。
- 2022-10-11 C++eof()判斷是否讀取到文件尾
- 2023-01-12 Golang單元測試與斷言編寫流程詳解_Golang
- 2022-09-23 windows10本地搭建FTP服務器圖文教程_FTP服務器
- 2022-09-25 Iterable,Collection和List的常見方法簽名及含義
- 2023-05-06 docker?search命令的具體使用_docker
- 2022-07-07 深入理解Go語言實現多態?_Golang
- 2022-09-24 React報錯之Object?is?possibly?null的問題及解決方法_React
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支