Python Flask搭建个人博客详细回顾—(2.2 工厂函数,表单等)

本文围绕个人博客Flask项目展开,回顾上节内容后,详细介绍构造文件__init__.py中工厂函数创建程序实例,阐述蓝图与工厂函数结合优势及程序上下文概念;还说明了定义博客表单forms.py的各类表单,以及辅助函数文件helpers.py的功能,最后提醒工厂函数部分需深入理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

个人博客Demo: link.
GitHub项目完整链接:link


回顾上一节主要讲了以下5个方面内容:


2.2.1 构造文件__init__.py

  • 程序实例在工厂函数中创建。工厂函数一般在程序包的构造文件__init__.py中创建,一般命名为create_app()
  • 在之前提到过蓝图就像是模子,而工厂函数(create_app)就是用这个模子(blueprint)加上其他的材料(配置文件,程序上下文等)生产出对应的可直接运行的程序实例
  • 蓝图和工厂函数结合使用,使得测试和部署更加方便,因为我们不用将加载的配置写死在某处,而直接在不同的地方创建不同的程序实例

由于我们还没有开始编写程序的视图函数,所以在这里先在bluemaps文件夹下的admin,blog和login文件中创建对应蓝图实例,以blog中创建蓝图实例示例代码如下:

from flask import Blueprint	 # 导入Blueprint类

# 实例化Blueprint类,第一参数为蓝图名称,第二参数为包或模块名称,此处用特殊变量__name__代替
blog_bm = Blueprint('blog', __name__)	

@blog_bm
  • 在__init__.py文件中定义create_app()工厂函数进行程序实例注册,由于很多处理函数都要注册到程序上,为避免将工厂函数弄得太长,我们根据类别将代码分离成多个函数,这些函数都接收程序实例为参数

  • 需要注册的处理函数有

    • 注册日志处理器
    • 注册拓展实例化
    • 注册蓝图(定义url前缀)
    • 注册自定义shell命令(生成虚拟数据命令,初始化数据库命令等)
    • 注册shell上下文处理函数
    • 注册模板上下文处理函数

如何理解程序的上下文:插入一条轮子哥在知乎的回答

  • 每一段程序都有很多外部变量。只有像Add这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫上下文。
  • 个人理解 :工厂函数将这些外部变量以函数形式装在一个"容器"里,能够让模板,shell命令在运行时附带上这些变量,使得程序正常运行

代码如下:

import os
import click
from flask import Flask, request
import logging
import pymysql

from Blog.bluemaps.admin import admin_bm
from Blog.bluemaps.blog import blog_bm
from Blog.bluemaps.login import login_bm
from Blog.extensions import db, ckeditor, moment, bootstrap, login, csrf
from Blog.settings import config
from Blog.models import Admin, Category, Post, Comment
from Blog.fakes import fake_admin, fake_post, fake_category, fake_comment
from flask_login import current_user
from logging.handlers import RotatingFileHandler

basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))   # 根目录

pymysql.install_as_MySQLdb()


# 定义工厂函数
def create_app(config_name=None):
    if config_name == None:
        config_name = os.getenv('FLASK_CONFIG', 'development')  # 设置配置名

    app = Flask('Blog')
    app.config.from_object(config[config_name])  # 从settings.py中的配置字典获取配置

    # 注册工厂函数
    register_template_context(app)
    register_commands(app)
    register_shell_context(app)
    register_extensions(app)
    register_bluemap(app)
    register_logging(app)

    return app


# 日志管理
def register_logging(app):
	
	# 生成logs文件夹
	folder = os.path.exists(os.path.join(basedir, 'logs'))
    if not folder:
        os.makedirs(os.path.join(basedir, 'logs'))

    class RequestFormatter(logging.Formatter):

        def format(self, record):
            record.url = request.url
            record.remote_addr = request.remote_addr
            return super(RequestFormatter, self).format(record)

    request_formatter = RequestFormatter(
        '[%(asctime)s] %(remote_addr)s requested %(url)s\n'
        '%(levelname)s in %(module)s: %(message)s'
    )

    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

    file_handler = RotatingFileHandler(os.path.join(basedir, 'logs/MyBlog.log'),
                                       maxBytes=10 * 1024 * 1024, backupCount=10)
    file_handler.setFormatter(formatter)
    file_handler.setLevel(logging.INFO)

    if not app.debug:
        app.logger.addHandler(file_handler)


# 组织蓝本
def register_bluemap(app):
    app.register_blueprint(blog_bm)
    app.register_blueprint(login_bm, url_prefix='/login')
    app.register_blueprint(admin_bm, url_prefix='/admin')


# 模板上下文
def register_template_context(app):
    @app.context_processor
    def make_template_context():
        admin = Admin.query.first()
        categories = Category.query.order_by(Category.name).all()
        posts = Post.query.order_by(Post.timestamp).all()

        if current_user.is_authenticated:
            unread_comments = Comment.query.filter_by(reviewed=False).count()
        else:
            unread_comments = None
        return dict(
            admin=admin,
            categories=categories,
            posts=posts,
            unread_comments=unread_comments
        )


# 调用init_app()方法,传入程序实例,完成拓展初始化
def register_extensions(app):
    bootstrap.init_app(app)
    db.init_app(app)
    moment.init_app(app)
    ckeditor.init_app(app)
    login.init_app(app)
    csrf.init_app(app)

    return app


# shell上下文
def register_shell_context(app):
    @app.shell_context_processor
    def make_shell_context():
        return dict(db=db)


# shell命令
def register_commands(app):
    @app.cli.command()  # 添加命令行接口
    @click.option('--drop', is_flag=True, help='重新创建数据库表')  # 使用click提供的option装饰器添加自定义数量支持
    def initdb(drop):  # 初始化数据库,传入drop参数
        """初始化数据库"""
        if drop:
            click.confirm('该操作将删除原有数据库,确定删除?', abort=True)
            db.drop_all()
            click.echo('删除数据库')
        db.create_all()
        click.echo('已重置数据库')

    @app.cli.command()
    @click.option('--username', prompt=True, help='登录用户名')
    @click.option('--password', prompt=True, hide_input=True,  # hide_input隐藏输入内容
                  confirmation_prompt=True, help='登录密码')  # confirmation_prompt设置二次确认输入
    def init(username, password):  # 博客初始化,传入用户名和密码
  
        click.echo('配置数据库中...')
        db.create_all()

        admin = Admin.query.first()  # 从数据库中查找管理员记录
        if admin is not None:  # 如果数据库中已经有管理员记录就更新用户名和密码
            click.echo('管理员已存在,更新中...')
            admin.username = username
            admin.set_password(password)  # 调用Admin模型类中的set_password()方法,生成password
        else:  # 没有管理员记录则创建新的管理员记录
            click.echo('新建管理员账户...')
            admin = Admin(
                username=username,
                blog_title='你的博客',
                name='你的名字',
                about='Anything about you.'
            )
            admin.set_password(password)
            db.session.add(admin)  # 将新创建对象添加到数据库会话

        category = Category.query.first()
        if category is None:  # 如果没有分类则创建默认分类
            click.echo('创建默认分类...')
            category = Category(name='Default')
            db.session.add(category)

        db.session.commit()  # 调用session.commit(),将改动提交到数据库
        click.echo('完成.')


    @app.cli.command()
    @click.option('--category', default=5, help='分类数量,默认值为5个')
    @click.option('--post', default=25, help='文章数量,默认值为25篇')
    @click.option('--comment', default=150, help='评论数量,默认值为150个')
    def forge(category, post, comment):
        db.drop_all()
        db.create_all()

        click.echo('生成管理员...')
        fake_admin()

        click.echo('生成 %d 个分类...' % category)
        fake_category(category)

        click.echo('生成 %d 篇文章...' % post)
        fake_post(post)

        click.echo('生成 %d 个评论...' % comment)
        fake_comment(comment)

        click.echo('数据生成完毕!')

工厂函数对程序进行加工处理后程序就可以初步运行了,在开始运行之前再做以下工作

在蓝图文件夹下blog.py文件中添加以下代码

@blog_bm.route('/')  # 设置路由
def index():	# 定义主页视图函数
    return "跑起来啦!"

创建一个.flaskenv文件,在文件中写入两行代码

  • FLASK_APP=Blog   --指定程序实例名称
  • FLASK_ENV=development  --将运行环境设置为development(默认production)

在项目根目录下打开命令行窗口,输入以下代码运行程序

  • pipenv shell —— 运行虚拟环境
  • flask run ——运行flask程序

如下图:

在这里插入图片描述

网页运行结果如图(因为没有渲染模板所以只有之前视图函数返回的文字):

在这里插入图片描述


2.2.2 定义博客表单 forms.py

表单用于获取用户输入,博客定义的表单类有:

  • 登录表单:用户名,密码,记住我等字段
  • 评论表单:游客名,评论内容
  • 设置表单:博客标题,关于,管理员昵称
  • 新增/编辑文章表单:文章标题,内容,所属分类采用下拉列表,定义__init__方法
  • 新增/编辑分类表单:分类名,定义验证器验证分类名不能重复

表单较为简单,代码如下:

from flask_wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField, SelectField, ValidationError
from wtforms.validators import DataRequired, Length
from flask_ckeditor import CKEditorField
from Blog.models import Category


# 登录表单
class LoginForm(Form):
    username = StringField('用户名', validators=[DataRequired()])
    password = PasswordField('密码', validators=[DataRequired(), Length(1, 32)])
    remember = BooleanField('记住我')
    submit = SubmitField('登录')


# 评论表单
class CommentForm(Form):
    name = StringField('昵称', validators=[DataRequired()])
    comment = TextAreaField('评论内容', validators=[DataRequired(), Length(1, 256)])
    submit = SubmitField('发布评论')


# 设置表单
class SettingForm(Form):
    blog_title = StringField('博客名称', validators=[DataRequired()])
    name = StringField('昵称', validators=[DataRequired(), Length(1, 20)])
    about = TextAreaField('关于', validators=[DataRequired()])
    submit = SubmitField('更新')


# 新增文章
class PostForm(Form):
    title = StringField('标题', validators=[DataRequired()])
    category = SelectField('分类', coerce=int, default=1)
    body = CKEditorField('正文', validators=[DataRequired()])
    submit = SubmitField('提交')

    # 文章表单中分类下拉列表的选项必须是包含两个元素元组的列表,元组分别包含选项值和选项标签,
    # 使用分类id作为选项值,分类名称作为选项标签,通过迭代Category.query.order_by(Category.name).all()返回分类记录实现
    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()]


# 新增分类
class CategoryForm(Form):
    name = StringField('分类名', validators=[DataRequired()])
    submit = SubmitField('提交')

    # 表单自定义行内验证器,用于验证分类名重复
    def validate_name(self, field):
        if Category.query.filter_by(name=field.data).first():
            raise ValidationError('分类名已存在')

2.2.3 辅助函数文件 helpers.py

  • 辅助函数文件主要包括判断链接是否安全,防止url被篡改以及返回重定向回上一页面(如登录后返回上一页面),详情参考代码注释
from flask import redirect, request, url_for, current_app

try:
    from urlparse import urlparse, urljoin
except ImportError:
    from urllib.parse import urlparse, urljoin


# 判断安全链接,防止形成开放重定向漏洞
def is_safe_url(target):
    # request.host_url获取程序内主机URL
    ref_url = urlparse(request.host_url)
    # urljoin()函数将目标URL转换为绝对URL
    test_url = urlparse(urljoin(request.host_url, target))
    # 最后对目标URL的URL模式和主机地址进行验证,确保只返回属于程序内部的URL
    return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc

# 重定向回上一页面
def redirect_back(default='blog.index', **kwargs):
	# referrer用来记录发源地址的HTTP首部字段,即访问来源
	# 在url中手动加入包含当前url的查询参数,一般命名next
    for target in request.args.get('next'), request.referrer:	# 先从next获取url,没有再从referrer获取
        if not target:	# 如果没有url
            continue
        if is_safe_url(target):	# 判断url是否安全
            return redirect(target)
    return redirect(url_for(default, **kwargs))	# 未获取到任何上一页面url时,就返回到默认页面


# 给允许上传的文件加上“.”
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in current_app.config['BLOG_ALLOWED_IMAGE_EXTENSIONS']

小结:

工厂函数部分需要好好消化,涉及到很多不容易理解的概念,代码也较多且复杂
后面将开始编写视图函数和模板

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值