網(wǎng)站首頁 編程語言 正文
本文要學(xué)習(xí)的示例程序是一個個人博客程序:Bluelog。博客是典型的 CMS
(Content Management System
,內(nèi)容管理系統(tǒng)),通常由兩部分組成:一部分是博客前臺,用來展示開放給所有用戶的博客內(nèi)容;另一部分是博客后臺,這部分內(nèi)容僅開放給博客管理員,用來對博客資源進(jìn)行添加、修改和刪除等操作。
1.數(shù)據(jù)庫(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)) # 博客標(biāo)題 blog_sub_title = db.Column(db.String(100)) # 博客副標(biāo)題 name = db.Column(db.String(30)) # 用戶姓名 about = db.Column(db.Text) # 關(guān)于信息 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') # 分類和文章之間是一對多關(guān)系 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)) # 標(biāo)題 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') # 分類和文章之間是一對多關(guān)系 comments = db.relationship('Comment', back_populates='post', cascade='all, delete-orphan') # 文章和評論是一對多關(guān)系
Comment
模型中創(chuàng)建的外鍵字段 post_id
存儲 Post
記錄的主鍵值。我們在這里設(shè)置了級聯(lián)刪除,也就是說,當(dāng)某個文章記錄被刪除時,該文章所屬的所有評論也會一并被刪除,所以在刪除文章時不用手動刪除對應(yīng)的評論。
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') # 文章和評論是一對多關(guān)系 replies = db.relationship('Comment', back_populates='replied', cascade='all, delete-orphan') # 設(shè)置級聯(lián)刪除 replied = db.relationship('Comment', back_populates='replies', remote_side=[id]) # 自關(guān)聯(lián)多對一需用 remote_side=id 指定 ‘一' 的一方
博客程序中的評論要支持存儲回復(fù)。我們想要為評論添加回復(fù),并在獲取某個評論時可以通過關(guān)系屬性獲得相對應(yīng)的回復(fù),這樣就可以在模板中顯示出評論之間的對應(yīng)關(guān)系。那么回復(fù)如何存儲在數(shù)據(jù)庫中呢?
你當(dāng)然可以再為回復(fù)創(chuàng)建一個 Reply 模型,然后使用一對多關(guān)系將評論和回復(fù)關(guān)聯(lián)起來。但是我們將介紹一個更簡單的解決辦法,因為回復(fù)本身也是評論,如果可以在評論模型內(nèi)建立層級關(guān)系,那么就可以在一個模型中表示評論和回復(fù)。
這種在同一個模型內(nèi)的一對多關(guān)系在 SQLAlchemy 中被稱為鄰接列表關(guān)系(Adjacency List Relationship)。具體來說,我們需要在 Comment 模型中添加一個外鍵指向它自身。這樣我們就得到一種層級關(guān)系:每個評論對象都可以包含多個子評論,即回復(fù)。
這個關(guān)系和我們之前熟悉的一對多關(guān)系基本相同。仔細(xì)回想一下一對多關(guān)系的設(shè)置,我們需要在 “多” 這一側(cè)定義外鍵,這樣 SQLAlchemy 就會知道哪邊是 “多” 的一側(cè)。這時關(guān)系對 “多” 這一側(cè)來說就是多對一關(guān)系。但是在鄰接列表關(guān)系中,關(guān)系的兩側(cè)都在同一個模型中,這時 SQLAlchemy 就無法分辨關(guān)系的兩側(cè)。在這個關(guān)系函數(shù)中,通過將 remote_side
參數(shù)設(shè)為 id
字段,我們就把 id
字段定義為關(guān)系的遠(yuǎn)程側(cè)(Remote Side),而 replied_id
就相應(yīng)地變?yōu)楸镜貍?cè)(Local Side),這樣反向關(guān)系就被定義為多對一,即多個回復(fù)對應(yīng)一個父評論。
集合關(guān)系屬性 replies
中的 cascade
參數(shù)設(shè)為 all
,因為我們期望的效果是,當(dāng)父評論被刪除時,所有的子評論也隨之刪除。
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.生成虛擬數(shù)據(jù)(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 模板上下文
在基模板的導(dǎo)航欄以及博客主頁中需要使用博客的標(biāo)題、副標(biāo)題等存儲在管理員對象上的數(shù)據(jù),為了避免在每個視圖函數(shù)中渲染模板時傳入這些數(shù)據(jù),我們在模板上下文處理函數(shù)中向模板上下文添加了管理員對象變量(admin)。另外,在多個頁面中都包含的邊欄中包含分類列表,我們也把分類數(shù)據(jù)傳入到模板上下文中。
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
對象獲取博客的標(biāo)題和副標(biāo)題。
<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 渲染導(dǎo)航鏈接
導(dǎo)航欄上的按鈕應(yīng)該在對應(yīng)的頁面顯示激活狀態(tài)。舉例來說,當(dāng)用戶單擊導(dǎo)航欄上的 “關(guān)于” 按鈕打開關(guān)于頁面時,“關(guān)于” 按鈕應(yīng)該高亮顯示。Bootstrap 為導(dǎo)航鏈接提供了一個 active
類來顯示激活狀態(tài),我們需要為當(dāng)前頁面對應(yīng)的按鈕添加 active
類。
這個功能可以通過判斷請求的端點來實現(xiàn),對 request
對象調(diào)用 endpoint
屬性即可獲得當(dāng)前的請求端點。如果當(dāng)前的端點與導(dǎo)航鏈接指向的端點相同,就為它添加 active
類,顯示激活樣式。
<li {% if request.endpoint == 'blog.index' %}class="active"{% endif %}> <a href="{{ url_for('blog.index') }}" >Home</a> </li>
有些教程中會使用 endswith()
方法來比較端點結(jié)尾。但是藍(lán)本擁有獨立的端點命名空間,即 “<藍(lán)本名>.<端點名>”,不同的端點可能會擁有相同的結(jié)尾,比如 blog.index
和 auth.index
,這時使用 endswith()
會導(dǎo)致判斷錯誤,所以最妥善的做法是比較完整的端點值。
不過在 Bluelog
的模板中我們并沒有使用這個 nav_link()
宏,因為 Bootstrap-Flask
提供了一個更加完善的 render_nav_item()
宏,它的用法和我們創(chuàng)建的 nav_link()
宏基本相同。這個宏可以在模板中通過 bootstrap/nav.html
路徑導(dǎo)入,它支持的常用參數(shù)如下表所示。
3.3 Flash消息分類
我們目前的 Flash
消息應(yīng)用了 Bootstrap 的 alert-info
樣式,單一的樣式使消息的類別和等級難以區(qū)分,更合適的做法是為不同類別的消息應(yīng)用不同的樣式。比如,當(dāng)用戶訪問出錯時顯示一個黃色的警告消息;而普通的提示信息則使用藍(lán)色的默認(rèn)樣式。Bootstrap 為提醒消息(Alert)提供了 8 種基本的樣式類,即 alert-primary
、alert-secondary
、alert-success
、alert-danger
、alert-warning
、alert-light
、alert-dark
。
要開啟消息分類,我們首先要在消息渲染函數(shù) get_flashed_messages
中將 with_categories
參數(shù)設(shè)為 True
。這時會把消息迭代為一個類似于(分類,消息)的元組,我們使用消息分類字符來構(gòu)建樣式類。
<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 中主要包含下面這些表單:登錄表單、文章表單、分類表單、評論表單、博客設(shè)置表單。這里我們僅介紹登錄表單、文章表單、分類表單和評論表單,其他的表單在實現(xiàn)上基本相同,不再詳細(xì)介紹。
刪除資源也需要使用表單來實現(xiàn),這里之所以沒有創(chuàng)建表單類,是因為后面我們會介紹在實現(xiàn)刪除操作時為表單實現(xiàn) CSRF
保護(hù)的更方便的做法,屆時表單可以手動在模板中寫出。
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
、“記住我” 復(fù)選框 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()]
文章創(chuàng)建表單由標(biāo)題字段 title
、分類選擇字段 category
、正文字段 body
和 “提交” 按鈕組成,其中正文字段使用 Flask-CKEditor
提供的 CKEditorField
字段。
下拉列表字段使用 WTForms 提供的 SelectField
類來表示 HTML 中的 標(biāo)簽。下拉列表的選項(即 標(biāo)簽)通過參數(shù) choices
指定。choices
必須是一個包含兩元素元組的列表,列表中的元組分別包含選項值和選項標(biāo)簽。我們使用分類的 id
作為選項值,分類的名稱作為選項標(biāo)簽,這兩個值通過迭代 Category.query.order_by(Category.name).all()
返回的分類記錄實現(xiàn)。選擇值默認(rèn)為字符串類型,我們使用 coerce
關(guān)鍵字指定數(shù)據(jù)類型為整型。default
用來設(shè)置默認(rèn)的選項值,我們將其指定為 1,即默認(rèn)分類的 id
。
因為 Flask-SQLAlchemy
依賴于程序上下文才能正常工作(內(nèi)部使用 current_app
獲取配置信息),所以這個查詢調(diào)用要放到構(gòu)造方法中執(zhí)行,在構(gòu)造方法中對 self.category.choices
賦值的效果和在類中實例化 SelectField
類并設(shè)置 choices
參數(shù)相同。
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.')
分類創(chuàng)建字段僅包含分類名稱字段(name)和提交字段。分類的名稱要求不能重復(fù),為了避免寫入重復(fù)的分類名稱導(dǎo)致數(shù)據(jù)庫出錯,我們在 CategoryForm
類中添加了一個 validate_name
方法,作為 name
字段的自定義行內(nèi)驗證器,它將在驗證 name
字段時和其他驗證函數(shù)一起調(diào)用。在這個驗證方法中,我們使用字段的值 filed.data
作為 name
列的參數(shù)值進(jìn)行查詢,如果查詢到已經(jīng)存在同名記錄,那么就拋出 ValidationError
異常,傳遞錯誤消息作為參數(shù)。
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
驗證器確保輸入的數(shù)據(jù)為有效的 URL
。
和匿名用戶的表單不同,管理員不需要填寫諸如姓名、電子郵箱等字段。我們單獨為管理員創(chuàng)建了一個表單類,這個表單類繼承自 CommentForm
類。
class AdminCommentForm(CommentForm): author = HiddenField() email = HiddenField() site = HiddenField()
在這個表單中,姓名、Email、站點字段使用 HiddenField
類重新定義。這個類型代表隱藏字段,即 HTML 中的 < input type=“hidden” >。
5.視圖函數(shù)(blueprints:admin、auth、blog)
在上面我們已經(jīng)創(chuàng)建了所有必須的模型類、模板文件和表單類。經(jīng)過程序規(guī)劃和設(shè)計后,我們可以創(chuàng)建大部分視圖函數(shù)。這些視圖函數(shù)暫時沒有實現(xiàn)具體功能,僅渲染對應(yīng)的模板,或是重定向到其他視圖。以 blog
藍(lán)本為例。
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
藍(lán)本類似,我們在 blueprints
子包中創(chuàng)建了 auth.py
、admin.py
腳本,這些腳本中分別創(chuàng)建了 auth
和 admin
藍(lán)本,藍(lán)本實例的名稱分別為 auth_bp
和 admin_bp
。
6.電子郵件支持(emails.py)
因為博客要支持評論,所以我們需要在文章有了新評論后發(fā)送郵件通知管理員。而且,當(dāng)管理員回復(fù)了讀者的評論后,也需要發(fā)送郵件提醒讀者。
因為郵件的內(nèi)容很簡單,我們將直接在發(fā)信函數(shù)中寫出正文內(nèi)容,這里只提供了 HTML 正文。我們有兩個需要使用電子郵件的場景:
- 當(dāng)文章有新評論時,發(fā)送郵件給管理員;
- 當(dāng)某個評論被回復(fù)時,發(fā)送郵件給被回復(fù)用戶。
為了方便使用,我們在 emails.py
中分別為這兩個使用場景創(chuàng)建了特定的發(fā)信函數(shù),可以直接在視圖函數(shù)中調(diào)用。這些函數(shù)內(nèi)部則通過調(diào)用我們創(chuàng)建的通用發(fā)信函數(shù) send_mail()
來發(fā)送郵件。
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()
函數(shù)用來發(fā)送新評論提醒郵件。我們通過將 url_for()
函數(shù)的 _external
參數(shù)設(shè)為 True
來構(gòu)建外部鏈接。鏈接尾部的 #comments
是用來跳轉(zhuǎn)到頁面評論部分的URL片段(URL fragment
),comments
是評論部分 div
元素的 id
值。這個函數(shù)接收表示文章的 post
對象作為參數(shù),從而生成文章正文的標(biāo)題和鏈接。
URL 片段又稱片段標(biāo)識符(fragment identifier
),是 URL 中用來標(biāo)識頁面中資源位置的短字符,以 #
開頭,對于 HTML 頁面來說,一個典型的示例是文章頁面的評論區(qū)。假設(shè)評論區(qū)的 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()
函數(shù)則用來發(fā)送新回復(fù)提醒郵件。這個發(fā)信函數(shù)接收 comment
對象作為參數(shù),用來構(gòu)建郵件正文,所屬文章的主鍵值通過 comment.post_id
屬性獲取,標(biāo)題則通過 comment.post.title
屬性獲取。
在 Bluelog 源碼中,我們沒有使用異步的方式發(fā)送郵件,如果你希望編寫一個異步發(fā)送郵件的通用發(fā)信函數(shù) 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
需要注意的是,因為我們的程序?qū)嵗峭ㄟ^工廠函數(shù)構(gòu)建的,所以實例化 Thread
類時,我們使用代理對象 current_app
作為 args
參數(shù)列表中 app
的值。另外,因為在新建的線程時需要真正的程序?qū)ο髞韯?chuàng)建上下文,所以我們不能直接傳入 current_app
,而是傳入對 current_app
調(diào)用 _get_current_object()
方法獲取到的被代理的程序?qū)嵗?/p>
原文鏈接:https://blog.csdn.net/be_racle/article/details/127138519
相關(guān)推薦
- 2022-10-15 C++中?Sort函數(shù)詳細(xì)解析_C 語言
- 2022-04-16 PyInstaller如何打包依賴文件至目標(biāo)程序目錄_python
- 2022-11-30 Rust實現(xiàn)面向?qū)ο蟮姆椒╛Rust語言
- 2024-01-15 spring boot jpa 執(zhí)行test測試,發(fā)現(xiàn)執(zhí)行未報錯但是事務(wù)會自動回滾
- 2023-03-01 Gorm更新零值問題解決思路與過程_Golang
- 2022-07-04 C#實現(xiàn)中文日歷Calendar_C#教程
- 2022-11-09 Android?使用maven?publish插件發(fā)布產(chǎn)物(aar)流程實踐_Android
- 2022-04-09 node sass下載失敗解決方案
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支