日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Python個人博客程序開發實例框架設計_python

作者:皮皮要HAPPY ? 更新時間: 2023-01-07 編程語言

本文要學習的示例程序是一個個人博客程序:Bluelog。博客是典型的 CMSContent 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">&nbsp;{{ 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.indexauth.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-dangeralert-warningalert-lightalert-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">&times;</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.pyadmin.py 腳本,這些腳本中分別創建了 authadmin 藍本,藍本實例的名稱分別為 auth_bpadmin_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 元素 idcomment,如果我們訪問 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

欄目分類
最近更新