網站首頁 編程語言 正文
本文要學習的示例程序是一個個人博客程序:Bluelog。博客是典型的 CMS
(Content Management System
,內容管理系統),通常由兩部分組成:一部分是博客前臺,用來展示開放給所有用戶的博客內容;另一部分是博客后臺,這部分內容僅開放給博客管理員,用來對博客資源進行添加、修改和刪除等操作。
1.數據庫(models.py)
from datetime import datetime from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from bluelog.extensions import db
1.1 管理員 Admin
class Admin(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) # 主鍵字段 username = db.Column(db.String(20)) # 用戶名 password_hash = db.Column(db.String(128)) # 密碼散列值 blog_title = db.Column(db.String(60)) # 博客標題 blog_sub_title = db.Column(db.String(100)) # 博客副標題 name = db.Column(db.String(30)) # 用戶姓名 about = db.Column(db.Text) # 關于信息 def set_password(self, password): self.password_hash = generate_password_hash(password) def validate_password(self, password): return check_password_hash(self.password_hash, password)
1.2 分類 Category
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()
1.3 文章 Post
class Post(db.Model): id = db.Column(db.Integer, primary_key=True) # 主鍵字段 title = db.Column(db.String(60)) # 標題 body = db.Column(db.Text) # 正文 timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True) # 時間戳 can_comment = db.Column(db.Boolean, default=True) # 是否能被評論 category_id = db.Column(db.Integer, db.ForeignKey('category.id')) # 所屬分類,外鍵字段 category = db.relationship('Category', back_populates='posts') # 分類和文章之間是一對多關系 comments = db.relationship('Comment', back_populates='post', cascade='all, delete-orphan') # 文章和評論是一對多關系
Comment
模型中創建的外鍵字段 post_id
存儲 Post
記錄的主鍵值。我們在這里設置了級聯刪除,也就是說,當某個文章記錄被刪除時,該文章所屬的所有評論也會一并被刪除,所以在刪除文章時不用手動刪除對應的評論。
1.4 評論 Comment
class Comment(db.Model): id = db.Column(db.Integer, primary_key=True) # 主鍵字段 author = db.Column(db.String(30)) # 作者 email = db.Column(db.String(254)) # 電子郵件 site = db.Column(db.String(255)) # 站點 body = db.Column(db.Text) # 正文 from_admin = db.Column(db.Boolean, default=False) # 是否是管理員的評論 reviewed = db.Column(db.Boolean, default=False) # 是否通過審核 timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True) # 時間戳 replied_id = db.Column(db.Integer, db.ForeignKey('comment.id')) # 外鍵 post_id = db.Column(db.Integer, db.ForeignKey('post.id')) # 外鍵 post = db.relationship('Post', back_populates='comments') # 文章和評論是一對多關系 replies = db.relationship('Comment', back_populates='replied', cascade='all, delete-orphan') # 設置級聯刪除 replied = db.relationship('Comment', back_populates='replies', remote_side=[id]) # 自關聯多對一需用 remote_side=id 指定 ‘一' 的一方
博客程序中的評論要支持存儲回復。我們想要為評論添加回復,并在獲取某個評論時可以通過關系屬性獲得相對應的回復,這樣就可以在模板中顯示出評論之間的對應關系。那么回復如何存儲在數據庫中呢?
你當然可以再為回復創建一個 Reply 模型,然后使用一對多關系將評論和回復關聯起來。但是我們將介紹一個更簡單的解決辦法,因為回復本身也是評論,如果可以在評論模型內建立層級關系,那么就可以在一個模型中表示評論和回復。
這種在同一個模型內的一對多關系在 SQLAlchemy 中被稱為鄰接列表關系(Adjacency List Relationship)。具體來說,我們需要在 Comment 模型中添加一個外鍵指向它自身。這樣我們就得到一種層級關系:每個評論對象都可以包含多個子評論,即回復。
這個關系和我們之前熟悉的一對多關系基本相同。仔細回想一下一對多關系的設置,我們需要在 “多” 這一側定義外鍵,這樣 SQLAlchemy 就會知道哪邊是 “多” 的一側。這時關系對 “多” 這一側來說就是多對一關系。但是在鄰接列表關系中,關系的兩側都在同一個模型中,這時 SQLAlchemy 就無法分辨關系的兩側。在這個關系函數中,通過將 remote_side
參數設為 id
字段,我們就把 id
字段定義為關系的遠程側(Remote Side),而 replied_id
就相應地變為本地側(Local Side),這樣反向關系就被定義為多對一,即多個回復對應一個父評論。
集合關系屬性 replies
中的 cascade
參數設為 all
,因為我們期望的效果是,當父評論被刪除時,所有的子評論也隨之刪除。
1.5 社交鏈接 Link
程序還包含了一個添加社交鏈接的功能。
class Link(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(30)) url = db.Column(db.String(255))
2.生成虛擬數據(fakes.py)
from faker import Faker fake = Faker()
def fake_admin(): def fake_categories(count=10): def fake_posts(count=50): def fake_links():
3.模板
3.1 模板上下文
在基模板的導航欄以及博客主頁中需要使用博客的標題、副標題等存儲在管理員對象上的數據,為了避免在每個視圖函數中渲染模板時傳入這些數據,我們在模板上下文處理函數中向模板上下文添加了管理員對象變量(admin)。另外,在多個頁面中都包含的邊欄中包含分類列表,我們也把分類數據傳入到模板上下文中。
from bluelog.models import Admin, Category def create_app(config_name=None): ... register_template_context(app) return app def register_template_context(app): @app.context_processor def make_template_context(): admin = Admin.query.first() categories = Category.query.order_by(Category.name).all() return dict(admin=admin, categories=categories)
在基模板 base.html
和主頁模板 index.html
中,我們可以直接使用傳入的 admin
對象獲取博客的標題和副標題。
<div class="page-header"> <h1 class="display-3">{{ admin.blog_title|default('Blog Title') }}</h1> <h4 class="text-muted"> {{ admin.blog_sub_title|default('Blog Subtitle') }}</h4> </div>
3.2 渲染導航鏈接
導航欄上的按鈕應該在對應的頁面顯示激活狀態。舉例來說,當用戶單擊導航欄上的 “關于” 按鈕打開關于頁面時,“關于” 按鈕應該高亮顯示。Bootstrap 為導航鏈接提供了一個 active
類來顯示激活狀態,我們需要為當前頁面對應的按鈕添加 active
類。
這個功能可以通過判斷請求的端點來實現,對 request
對象調用 endpoint
屬性即可獲得當前的請求端點。如果當前的端點與導航鏈接指向的端點相同,就為它添加 active
類,顯示激活樣式。
<li {% if request.endpoint == 'blog.index' %}class="active"{% endif %}> <a href="{{ url_for('blog.index') }}" >Home</a> </li>
有些教程中會使用 endswith()
方法來比較端點結尾。但是藍本擁有獨立的端點命名空間,即 “<藍本名>.<端點名>”,不同的端點可能會擁有相同的結尾,比如 blog.index
和 auth.index
,這時使用 endswith()
會導致判斷錯誤,所以最妥善的做法是比較完整的端點值。
不過在 Bluelog
的模板中我們并沒有使用這個 nav_link()
宏,因為 Bootstrap-Flask
提供了一個更加完善的 render_nav_item()
宏,它的用法和我們創建的 nav_link()
宏基本相同。這個宏可以在模板中通過 bootstrap/nav.html
路徑導入,它支持的常用參數如下表所示。
3.3 Flash消息分類
我們目前的 Flash
消息應用了 Bootstrap 的 alert-info
樣式,單一的樣式使消息的類別和等級難以區分,更合適的做法是為不同類別的消息應用不同的樣式。比如,當用戶訪問出錯時顯示一個黃色的警告消息;而普通的提示信息則使用藍色的默認樣式。Bootstrap 為提醒消息(Alert)提供了 8 種基本的樣式類,即 alert-primary
、alert-secondary
、alert-success
、alert-danger
、alert-warning
、alert-light
、alert-dark
。
要開啟消息分類,我們首先要在消息渲染函數 get_flashed_messages
中將 with_categories
參數設為 True
。這時會把消息迭代為一個類似于(分類,消息)的元組,我們使用消息分類字符來構建樣式類。
<main class="container"> {% for message in get_flashed_messages(with_categories=True) %} <div class="alert alert-{{ message[0] }}" role="alert"> <button type="button" class="close" data-dismiss="alert">×</button> {{ message[1] }} </div> {% endfor %} ... </main>
4.表單(forms.py)
Bluelog 中主要包含下面這些表單:登錄表單、文章表單、分類表單、評論表單、博客設置表單。這里我們僅介紹登錄表單、文章表單、分類表單和評論表單,其他的表單在實現上基本相同,不再詳細介紹。
刪除資源也需要使用表單來實現,這里之所以沒有創建表單類,是因為后面我們會介紹在實現刪除操作時為表單實現 CSRF
保護的更方便的做法,屆時表單可以手動在模板中寫出。
4.1 登錄表單
from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField, BooleanField from wtforms.validators import DataRequired class LoginForm(FlaskForm): username = StringField('Username', validators=[DataRequired(), Length(1, 20)]) password = PasswordField('Password', validators=[DataRequired(), Length(1, 128)]) remember = BooleanField('Remember me') submit = SubmitField('Log in')
登錄表單由用戶名字段 username
、密碼字段 password
、“記住我” 復選框 remember
和 “提交” 按鈕 submit
組成。
4.2 文章表單
from flask_ckeditor import CKEditorField from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, SelectField from wtforms.validators import DataRequired, Length from bluelog.models import Category class PostForm(FlaskForm): title = StringField('Title', validators=[DataRequired(), Length(1, 60)]) category = SelectField('Category', coerce=int, default=1) body = CKEditorField('Body', validators=[DataRequired()]) submit = SubmitField() def __init__(self, *args, **kwargs): super(PostForm, self).__init__(*args, **kwargs) self.category.choices = [(category.id, category.name) for category in Category.query.order_by(Category.name).all()]
文章創建表單由標題字段 title
、分類選擇字段 category
、正文字段 body
和 “提交” 按鈕組成,其中正文字段使用 Flask-CKEditor
提供的 CKEditorField
字段。
下拉列表字段使用 WTForms 提供的 SelectField
類來表示 HTML 中的 標簽。下拉列表的選項(即 標簽)通過參數 choices
指定。choices
必須是一個包含兩元素元組的列表,列表中的元組分別包含選項值和選項標簽。我們使用分類的 id
作為選項值,分類的名稱作為選項標簽,這兩個值通過迭代 Category.query.order_by(Category.name).all()
返回的分類記錄實現。選擇值默認為字符串類型,我們使用 coerce
關鍵字指定數據類型為整型。default
用來設置默認的選項值,我們將其指定為 1,即默認分類的 id
。
因為 Flask-SQLAlchemy
依賴于程序上下文才能正常工作(內部使用 current_app
獲取配置信息),所以這個查詢調用要放到構造方法中執行,在構造方法中對 self.category.choices
賦值的效果和在類中實例化 SelectField
類并設置 choices
參數相同。
4.3 分類表單
from wtforms import StringField, SubmitField, ValidationError from wtforms import DataRequired from bluelog.models import Category class CategoryForm(FlaskForm): name = StringField('Name', validators=[DataRequired(), Length(1, 30)]) submit = SubmitField() def validate_name(self, field): if Category.query.filter_by(name=field.data).first(): raise ValidationError('Name already in use.')
分類創建字段僅包含分類名稱字段(name)和提交字段。分類的名稱要求不能重復,為了避免寫入重復的分類名稱導致數據庫出錯,我們在 CategoryForm
類中添加了一個 validate_name
方法,作為 name
字段的自定義行內驗證器,它將在驗證 name
字段時和其他驗證函數一起調用。在這個驗證方法中,我們使用字段的值 filed.data
作為 name
列的參數值進行查詢,如果查詢到已經存在同名記錄,那么就拋出 ValidationError
異常,傳遞錯誤消息作為參數。
4.4 評論表單
from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, TextAreaField from wtforms.validators import DataRequired, Email, URL, Length, Optional class CommentForm(FlaskForm): author = StringField('Name', validators=[DataRequired(), Length(1, 30)]) email = StringField('Email', validators=[DataRequired(), Email(), Length(1, 254)]) site = StringField('Site', validators=[Optional(), URL(), Length(0, 255)]) body = TextAreaField('Comment', validators=[DataRequired()]) submit = SubmitField()
在這個表單中,email
字段使用了用于驗證電子郵箱地址的 Email
驗證器。另外,因為評論者的站點是可以留空的字段,所以我們使用 Optional
驗證器來使字段可以為空。site
字段使用 URL
驗證器確保輸入的數據為有效的 URL
。
和匿名用戶的表單不同,管理員不需要填寫諸如姓名、電子郵箱等字段。我們單獨為管理員創建了一個表單類,這個表單類繼承自 CommentForm
類。
class AdminCommentForm(CommentForm): author = HiddenField() email = HiddenField() site = HiddenField()
在這個表單中,姓名、Email、站點字段使用 HiddenField
類重新定義。這個類型代表隱藏字段,即 HTML 中的 < input type=“hidden” >。
5.視圖函數(blueprints:admin、auth、blog)
在上面我們已經創建了所有必須的模型類、模板文件和表單類。經過程序規劃和設計后,我們可以創建大部分視圖函數。這些視圖函數暫時沒有實現具體功能,僅渲染對應的模板,或是重定向到其他視圖。以 blog
藍本為例。
from flask import render_template, Blueprint blog_bp = Blueprint('blog', __name__) @blog_bp.route('/') def index(): return render_template('blog/index.html') @blog_bp.route('/about') def about(): return render_template('blog/about.html') @blog_bp.route('/category/<int:category_id>') def show_category(category_id): return render_template('blog/category.html') @blog_bp.route('/post/<int:post_id>', methods=['GET', 'POST']) def show_post(post_id): return render_template('blog/post.html')
和 blog
藍本類似,我們在 blueprints
子包中創建了 auth.py
、admin.py
腳本,這些腳本中分別創建了 auth
和 admin
藍本,藍本實例的名稱分別為 auth_bp
和 admin_bp
。
6.電子郵件支持(emails.py)
因為博客要支持評論,所以我們需要在文章有了新評論后發送郵件通知管理員。而且,當管理員回復了讀者的評論后,也需要發送郵件提醒讀者。
因為郵件的內容很簡單,我們將直接在發信函數中寫出正文內容,這里只提供了 HTML 正文。我們有兩個需要使用電子郵件的場景:
- 當文章有新評論時,發送郵件給管理員;
- 當某個評論被回復時,發送郵件給被回復用戶。
為了方便使用,我們在 emails.py
中分別為這兩個使用場景創建了特定的發信函數,可以直接在視圖函數中調用。這些函數內部則通過調用我們創建的通用發信函數 send_mail()
來發送郵件。
from flask import url_for def send_mail(subject, to, html): ...
def send_new_comment_email(post): post_url = url_for('blog.show_post', post_id=post.id, _external=True) + '#comments' send_mail(subject='New comment', to=current_app.config['BLUELOG_EMAIL'], html='<p>New comment in post <i>%s</i>, click the link below to check:</p>' '<p><a href="%s" >%s</a></P>' '<p><small style="color: #868e96">Do not reply this email.</small></p>' % (post.title, post_url, post_url))
send_new_comment_email()
函數用來發送新評論提醒郵件。我們通過將 url_for()
函數的 _external
參數設為 True
來構建外部鏈接。鏈接尾部的 #comments
是用來跳轉到頁面評論部分的URL片段(URL fragment
),comments
是評論部分 div
元素的 id
值。這個函數接收表示文章的 post
對象作為參數,從而生成文章正文的標題和鏈接。
URL 片段又稱片段標識符(fragment identifier
),是 URL 中用來標識頁面中資源位置的短字符,以 #
開頭,對于 HTML 頁面來說,一個典型的示例是文章頁面的評論區。假設評論區的 div
元素 id
為 comment
,如果我們訪問 http://example.com/post/7#comment
,頁面加載完成后將會直接跳到評論部分。
def send_new_reply_email(comment): post_url = url_for('blog.show_post', post_id=comment.post_id, _external=True) + '#comments' send_mail(subject='New reply', to=comment.email, html='<p>New reply for the comment you left in post <i>%s</i>, click the link below to check: </p>' '<p><a href="%s" >%s</a></p>' '<p><small style="color: #868e96">Do not reply this email.</small></p>' % (comment.post.title, post_url, post_url))
send_new_reply_email()
函數則用來發送新回復提醒郵件。這個發信函數接收 comment
對象作為參數,用來構建郵件正文,所屬文章的主鍵值通過 comment.post_id
屬性獲取,標題則通過 comment.post.title
屬性獲取。
在 Bluelog 源碼中,我們沒有使用異步的方式發送郵件,如果你希望編寫一個異步發送郵件的通用發信函數 send_mail()
,可以使用以下方式。
from threading import Thread from flask import current_app from flask_mail import Message from bluelog.extensions import mail def _send_async_mail(app, message): with app.app_context(): mail.send(message) def send_mail(subject, to, html): app = current_app._get_current_object() message = Message(subject, recipients=[to], html=html) thr = Thread(target=_send_async_mail, args=[app, message]) thr.start() return thr
需要注意的是,因為我們的程序實例是通過工廠函數構建的,所以實例化 Thread
類時,我們使用代理對象 current_app
作為 args
參數列表中 app
的值。另外,因為在新建的線程時需要真正的程序對象來創建上下文,所以我們不能直接傳入 current_app
,而是傳入對 current_app
調用 _get_current_object()
方法獲取到的被代理的程序實例。
原文鏈接:https://blog.csdn.net/be_racle/article/details/127138519
相關推薦
- 2022-06-01 Androidstudio調用攝像頭拍照并保存照片_Android
- 2022-12-28 React?Server?Component混合式渲染問題詳解_React
- 2022-09-27 C#校驗時間格式的場景分析_C#教程
- 2023-01-13 分布式緩存Redis與Memcached的優缺點區別比較_數據庫其它
- 2022-06-13 Python學習之線程池與GIL全局鎖詳解_python
- 2023-01-07 Python中sys.argv用法圖文詳解_python
- 2022-12-28 kotlin開發cli工具小技巧詳解_Android
- 2022-10-04 C++兩種素數判定方法_C 語言
- 最近更新
-
- 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同步修改后的遠程分支