标签

 flask 

相关的文章:

这是一个关于使用Flask搭建校园论坛的教程和应用部署的列表页,包含了Flask的模板全局变量、自定义过滤器、Caching的应用,以及使用Docker部署Flask应用的方法。

Blogin -

[Python]flask-babel在工厂模式下的使用

1. 使用flask-babel国际化流程 通过flask-babel的官方文档以及网络上的一些简单示例教程,flask-babel进行国际化的流程如下图所示 首先通过pip install flask-babel进行扩展的安装 pip install flask-babel 安装完成之后通过下面的代码进行初始化 from flask import Flask from flask_babel import Babel app = Flask(__name__) babel = Babel(app) 初始化完成之后就需要在项目的根目录新建flask-babel的配置文件babel.cfg,文件名取什么都无所谓,这里只是好做区分,在babel.cfg文件中一般会输入下面的内容即可 [python: **.py] [jinja2: **/templates/**.html] extensions=jinja2.ext.autoescape,jinja2.ext.with_ 之后通过extract命令将需要翻译的字符串抽离出来保存到根目录的模板文件中去 pybabel extract -F babel.cfg -o messages.pot . 这里的-F参数可以指定相应babel配置文件,-o 参数指定抽离的模板文件名称 . 则表示将模板文件保存到当前目录下,当然也可以指定到其他路径,抽离完模板文件之后,通过init命令初始化我们的目标语言模板文件,基于上一步骤抽离的pot模板文件 pybabel init -i messages.pot -d translations -l zh -i 参数指定根模板文件的路径,-d 指定目标语言的模板文件保存路径(上面命令就是将目标语言的模板文件保存到当前目录的translations/zh目录下) -l 参数则是指定转换的语言,这里指定为中文,通过flask-babel实现国际化的流程大致就是如此,具体细节可以参考官方文档以及网络资料。 2. 工厂模式应用使用flask-babel 单文件应用都是在实例化Flask后直接通过Babel(app)进行初始化的,下面通过一个示例演示如何在工厂模式应用中使用flask-babel。 2.1 项目目录结构 项目目录结构如上图所示 app: 存放flask应用源代码 translations: 存放翻译文件 babel.cfg: flask-babel配置文件 .flaskenv: flask环境变量文件 message.pot: flask-babel 模板翻译文件3 2.2 工厂模式初始化babel 在app目录下的__init__.py文件中输入如下代码 from flask import Flask, request from app.extensions import babel from app.views.index import idx_bp import os basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) def create_app(): app = Flask(__name__) app.config['BABEL_DEFAULT_LOCALE'] = 'zh' app.config['BABEL_TRANSLATION_DIRECTORIES'] = basedir + '/translations' app.register_blueprint(idx_bp) register_extensions(app) @babel.localeselector def get_local(): cookie = request.cookies.get('locale') if cookie in ['zh', 'en']: return cookie return request.accept_languages.best_match(app.config.get('BABEL_DEFAULT_LOCALE')) return app def register_extensions(app): babel.init_app(app) flask-babel是通过装饰器函数localselector去获取当前需要渲染的语言,在create_app工厂方法中我们通过get_local函数去获取请求中cookie是否包含有local的值,如果有则返回cookie的值,如果没有则获取请求中accept_language获取与默认配置最匹配的语言,每次请求进入都会自动调用该函数,因此就可以达到动态切换语言的目的了。 与此同时,通过BABEL_DEFAULT_LOCALE以及BABEL_TRANSLATION_DIRECTORIES分别来配置默认语言以及默认翻译文件存储路径,如果不配置翻译文件默认存储位置,flask-babel默认会去app/translations目录下查找,这与我们翻译文件的路径是不一样。 2.3 蓝图中的翻译 flask-babel提供了三个函数来标志我们需要翻译的字符串分别是gettext、ngettext、lazy_gettext,其中gettext可以标志单个字符串,ngettext可以标记多个字符串,lazy_gettext只在使用到该字符串的时候才进行翻译,在views文件中新建index.py文件,输入下面的代码 from flask import Blueprint, render_template from flask_babel import gettext idx_bp = Blueprint('index', __name__) @idx_bp.route('/') @idx_bp.route('/index') def index(): content = gettext('This is a flask-babel sample application') return render_template('index.html', content=content) 2.4 模板中的翻译 除了可以标记python文件中的字符串,还可以标记模板文件中的字符串,在templates目录中新建index.html文件,输入下面的代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Flask Babel Sample </title> <link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}"> </head> <body> <div class="container"> <h3>{{ _("Factory mode use Flask-Babel") }}</h3> <hr> <p>1.{{ _('Blueprint Sample') }}</p> <p>{{ content }}</p> <p>2.{{ _('Template Sample') }}</p> <p>{{ _("I have a dream that one day this nation will rise up and live out the true meaning of its creed: 'We hold these truths to be self-evident, that all men are created equal.") }}</p> <p>{{ _("I have a dream that one day on the red hills of Georgia, the sons of former slaves and the sons of former slave owners will be able to sit down together at the table of brotherhood.") }}</p> <p>{{ _("I have a dream that one day even the state of Mississippi, a state sweltering with the heat of injustice, sweltering with the heat of oppression, will be transformed into an oasis of freedom and justice.") }}</p> <p>{{ _("I have a dream that my four little children will one day live in a nation where they will not be judged by the color of their skin but by the content of their character.") }}</p> <button class="btn zh"><a class="link" href="">中文</a></button> <button class="btn en"><a class="link" href="">English</a></button> </div> </body> </html> 在模板文件,将需要标志的字符串通过_()进行包括,为什么采用_()进行包裹,因为在babel.cfg文件中添加了extensions=jinja2.ext.autoescape,jinja2.ext.with_,在使用extract命令的时候会自动将其抽离出来。 2.5 抽离模板生成翻译文件 在使用gettext或者_()标记好需要翻译的字符串之后,通过extract命令自动抽离出python文件、模板文件中所有被标记的字符串 pybabel extract -F babel.cfg -o message.pot . 运行结束后会在项目根目录中生成一个message.pot文件,文件内容如下图所示(只截取了部分内容),可以看到文件中的内容都是事先标记好的字符串 上面的message.pot文件为模板文件,通过init命令可以根据模板文件初始化目标语言po文件 pybabel init -i message.pot -d translations -l zh 上述命令会根据上一个步骤中生成的模板文件生成对应语言的翻译文件,在translations/zh/LC_MESSAGES目录中可以看到message.po文件,文件内容与message.pot文件内容一直,我们只需要在msgstr字段中填入对应语言的翻译内容即可,如下代码所示 # Chinese translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR <EMAIL@ADDRESS>, 2022. # msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2022-02-27 20:36+0800\n" "PO-Revision-Date: 2022-02-27 19:54+0800\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language: zh\n" "Language-Team: zh <LL@li.org>\n" "Plural-Forms: nplurals=1; plural=0\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" #: app/templates/index.html:10 msgid "Factory mode use Flask-Babel" msgstr "Flask-Babel在工厂模式下的应用" #: app/templates/index.html:12 msgid "Blueprint Sample" msgstr "蓝图示例" #: app/templates/index.html:14 msgid "Template Sample" msgstr "模板示例" #: app/templates/index.html:15 msgid "" "I have a dream that one day this nation will rise up and live out the " "true meaning of its creed: 'We hold these truths to be self-evident, that" " all men are created equal." msgstr "我梦想有一天,这个国家会站立起来,真正实现其信条的真谛:“我们认为这些真理是不言而喻的——人人生而平等。”" #: app/templates/index.html:16 msgid "" "I have a dream that one day on the red hills of Georgia, the sons of " "former slaves and the sons of former slave owners will be able to sit " "down together at the table of brotherhood." msgstr "我梦想有一天,在佐治亚的红山上,昔日奴隶的儿子将能够和昔日奴隶主的儿子坐在一起,共叙兄弟情谊。" #: app/templates/index.html:17 msgid "" "I have a dream that one day even the state of Mississippi, a state " "sweltering with the heat of injustice, sweltering with the heat of " "oppression, will be transformed into an oasis of freedom and justice." msgstr "我梦想有一天,甚至连密西西比州这个正义匿迹,压迫成风的地方,也将变成自由和正义的绿洲。" #: app/templates/index.html:18 msgid "" "I have a dream that my four little children will one day live in a nation" " where they will not be judged by the color of their skin but by the " "content of their character." msgstr "我梦想有一天,我的四个孩子将在一个不是以他们的肤色,而是以他们的品格优劣来评价他们的国度里生活。" #: app/views/index.py:17 msgid "This is a flask-babel sample application" msgstr "这是一个flask-babel的简单示例应用" 这时候浏览器打开http://127.0.01:5000首页显示页面内容如下,页面显示的中文,因为默认配置的语言为zh 3. 动态切换语言 在第二节中,通过读取请求中cookie的值去判断当前语言,因此我们可以在index.py文件中添加下面的代码实现动态语言切换功能,代码片段如下 @idx_bp.route('/set-locale/<language>') def set_locale(language): resp = redirect(request.referrer) if language: resp.set_cookie('locale', language, max_age=30 * 24 * 60 * 60) return resp 然后将index.html文件中两个a标签的href改为如下代码 <button class="btn zh"><a class="link" href="{{ url_for('index.set_locale', language='zh') }}">中文</a></button> <button class="btn en"><a class="link" href="{{ url_for('index.set_locale', language='en') }}">English</a></button> 这样就可以点击按钮就可以动态切换语言了,如下图 4. 注意事项 工厂模式将localselector装饰函数放到create_app下即可 注意翻译文件的路径,如果不是在项目源码目录下,需要配置BABEL_TRANSLATION_DIRECTORIES 如果更新了标记字符串,使用pybabel update命令不要使用init命令,否则会初始化已翻译的内容 如果更新了标记字符串,记得要重启应用 项目代码:https://github.com/weijiang1994/flask-babel-sample

本文介绍了如何使用Flask-Babel进行国际化。首先通过安装扩展和初始化来配置Flask-Babel。然后通过抽离命令将需要翻译的字符串保存到模板文件中,并通过init命令初始化目标语言模板文件。在工厂模式应用中使用Flask-Babel时,需要在初始化中注册Babel,并通过装饰器函数localselector获取当前需要渲染的语言。Flask-Babel提供了三个函数来标志需要翻译的字符串。可以在python文件和模板文件中使用这些函数进行标记。最后,通过抽离模板生成翻译文件,并可以通过动态切换语言来实现国际化。

相关推荐 去reddit讨论

Blogin -

[系列教程]使用Flask搭建一个校园论坛4-登录注册

1.知识预览 在本届中将学习到以下内容的知识 如何使用wtform来渲染表单 如果使用flask-mail来发送邮件 2.用户注册 在前端中form表单是用的比较多的东西,我们可以使用wtforms这个框架,直接通过后端代码来渲染前端表单。新建bbs/forms.py文件,嵌入以下代码 from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, SelectField, BooleanField, TextAreaField, FileField, Label, HiddenField, \ PasswordField class BaseUserForm(FlaskForm): user_name = StringField(u'用户名', validators=[DataRequired(message='用户名不能为空'), Length(min=1, max=16, message='用户名长度限定在1-16位之间'), Regexp('^[a-zA-Z0-9_]*$', message='用户名只能包含数字、字母以及下划线.')], render_kw={'placeholder': '请输入用户名长度1-16之间'}) nickname = StringField(u'昵称', validators=[DataRequired(message='昵称不能为空'), Length(min=1, max=20, message='昵称长度限定在1-20位之间')], render_kw={'placeholder': '请输入昵称长度1-20之间'}) user_email = StringField(u'注册邮箱', render_kw={'placeholder': '请输入注册邮箱', 'type': 'email'}) submit = SubmitField(u'注册', render_kw={'class': 'btn btn-success btn-xs'}) 在上面的代码中,首先导入相关的库,然后新建了一个BaseUserForm的类,因为用户的信息在很多表单中使用到了,因此可以将共同的属性剥离出来,然后在不同的场合继承该基类,并且可以根据不同的场合在子类中定制我们的表单属性,这样就可以降低代码的冗余量。如果在每个需要使用到用户信息的表单代码中写入同样的内容,那么久显得代码很臃肿了。 BaseUserForm类中使用到了wtforms中的一些属性,比如StringField就相当于是我们前端的input标签,SubmitField就相当于是<input type="submit">,具体可以去看wtforms的官方文档。 继续在上面的文件中嵌入如下代码,新建注册表单类 class RegisterForm(FlaskForm): user_name = StringField(u'用户名', validators=[DataRequired(message='用户名不能为空'), Length(min=1, max=16, message='用户名长度限定在1-16位之间'), Regexp('^[a-zA-Z0-9_]*$', message='用户名只能包含数字、字母以及下划线.')], render_kw={'placeholder': '请输入用户名长度1-16之间'}) nickname = StringField(u'昵称', validators=[DataRequired(message='昵称不能为空'), Length(min=1, max=20, message='昵称长度限定在1-16位之间')], render_kw={'placeholder': '请输入昵称长度1-20之间'}) user_email = StringField(u'注册邮箱', validators=[DataRequired(message='注册邮箱不能为空'), Length(min=4, message='注册邮箱长度必须大于4')], render_kw={'placeholder': '请输入注册邮箱', 'type': 'email'}) password = StringField(u'密码', validators=[DataRequired(message='用户密码不能为空'), Length(min=8, max=40, message='用户密码长度限定在8-40位之间'), EqualTo('confirm_pwd', message='两次密码不一致')], render_kw={'placeholder': '请输入密码', 'type': 'password'}) confirm_pwd = StringField(u'确认密码', validators=[DataRequired(message='用户密码不能为空'), Length(min=8, max=40, message='用户密码长度限定在8-40位之间')], render_kw={'placeholder': '输入确认密码', 'type': 'password'}) colleges = SelectField(u'学院', choices=[(1, '计算机')]) submit = SubmitField(u'注册', render_kw={'class': 'source-button btn btn-primary btn-xs mt-2'}) def __init__(self, *args, **kwargs): super(RegisterForm, self).__init__(*args, **kwargs) cols = College.query.all() self.colleges.choices = [(col.id, col.name) for col in cols] def validate_user_name(self, filed): if User.query.filter_by(username=filed.data).first(): raise ValidationError('用户名已被注册.') def validate_user_email(self, filed): if User.query.filter_by(email=filed.data.lower()).first(): raise ValidationError('邮箱已被注册.') def validate_nickname(self, filed): if User.query.filter_by(nickname=filed.data).first(): raise ValidationError('昵称已被注册') 因为学院的选项有许多,我们可以在类的构造函数中通过数据库去获取数据库中已经存在的学院,然后将其设置到colleges类属性的choices值上,这样当我们打开页面渲染表单时,数据就会自动渲染到select标签option上去了,如下图 然后还新建了三个函数validate_user_name、validate_user_email以及validate_nickname,这三个函数主要是用来判断email、username、nickname三个字段的唯一性,因为在数据库建表的时候将这三个字段设置为unique=True,因此在这里需要做一个唯一性的判断。 使用wtforms时,我们可以通过validate_加上你需要校验的属性字段名称来检验前端用户输入的数据是否符合标准。 表单类的编写已经完成,接下来就是整个注册逻辑的实现了。新建bbs/templates/frontend/register.html文件,嵌入以下代码 {% extends "frontend/base.html" %} {% import 'bootstrap/wtf.html' as wtf %} {% block title %} 用户注册 {% endblock %} {% block content %} <body> <main> <div class="container"> <div class="jumbotron pt-5 pb-1 mt-2"> <div class="row"> <div class="col-md-8"> <h3 class="text-muted"><b>欢迎注册加入狗子学院~</b></h3> <hr class="bg-primary"> <p><b>在这里你可以:</b></p> <ul> <li>浏览当下校园的一些趣事、杂谈以及谁和谁的八卦</li> <li>发布一些咸鱼交易、寻物启事等等</li> <li>发现臭味相投的朋友、开拓自己的圈子</li> </ul> <img src="{{ url_for('static', filename='img/index.jpg') }}" class="rounded img-fluid"> </div> <div class="col-md-4"> <div class="card mb-3 w-100 bg-light"> <div class="card-header"><h4 class="text-muted"><strong>用户注册</strong></h4></div> <div class="card-body"> {% include "_flash.html" %} <form class="bs-component" action="/auth/register/" method="post"> {{ form.csrf_token }} {{ wtf.form_field(form.user_name) }} {{ wtf.form_field(form.nickname) }} {{ wtf.form_field(form.user_email) }} {{ wtf.form_field(form.password) }} {{ wtf.form_field(form.confirm_pwd) }} {{ wtf.form_field(form.colleges) }} <label for="captcha">验证码</label> <div class="input-group"> <input type="text" class="form-control" name="captcha" id="captcha" placeholder="请输入验证码" aria-required="true" aria-describedby="captcha" required> <div class="input-group-append"> <button class="btn btn-success" onclick="sendCapt()" id="sendCaptcha">发送</button> </div> </div> <p class="p-hint">验证码发送成功,10分钟内有效!</p> {{ form.submit }} <hr> <small>已有账号? <a style="text-decoration: none;" href="{{ url_for('.login') }}">登录.</a> </small> </form> </div> </div> </div> </div> </div> </div> </main> 然后打开bbs/blueprint/frontend/auth.py文件,接着在上一节下面嵌入如下代码 @auth_bp.route('/register/', methods=['GET', 'POST']) def register(): colleges = College.query.all() form = RegisterForm() return render_template('frontend/register.html', colleges=colleges, form=form) 在后端代码中我们通过render_template函数返回了前端注册页面,并且携带注册表单的实例参数。在前端html文件中,我们可以通过form.参数名的方式来进行表单渲染。同时还在前端文件中导入了bootstrap/wtf.html,这样就可以将表单的样式渲染成bootstrap的样式,当然也可以不是bootstrap/wtf.html来渲染,在后端表单类中可以通过render_kw参数来指定我们表单的一些特定参数。 在前端页面中我们还手动加入了一行验证码输入框,点击发送按钮就可以将验证码发送到用户填写的邮箱当中去了。为什么不将此输入框写到后端表单中去?因为那样不好处理前端样式了。 访问http://127.0.0.1/auth/register/ 将会看到如下页面: 在注册页面中是需要用户填写邮箱收到的验证码,因此我们需要在后端代码中实现发送邮件的功能。发送邮件的功能是通过flask-email来实现的,打开bbs/extensions.py文件,加入下面的代码,然后在__init__.py文件中进行注册。 from flask_mail import Mail mail = Mail() 在使用发送邮件功能之前,首先我们需要到qq邮箱或者网易邮箱或者其他可以使用的邮箱申请SMTP服务,具体流程可以某度某歌搜索一下,这里就不再累述。将申请到的私密信填入到.env文件中 MAIL_SERVER='smtp.qq.com' MAIL_USERNAME='你的qq邮箱名' MAIL_PASSWORD='qq邮箱秘钥不是登录密码是申请SMTP那串无规则秘钥' 然后在bbs/setting.py文件中加入下面的代码 class BaseConfig(object): # 省略之前代码 BBS_MAIL_SUBJECT_PRE = '[狗子学院]' MAIL_SERVER = os.getenv('MAIL_SERVER') MAIL_PORT = 465 MAIL_USE_SSL = True MAIL_USERNAME = os.getenv('MAIL_USERNAME') MAIL_PASSWORD = os.getenv('MAIL_PASSWORD') MAIL_DEFAULT_SENDER = ('BBS Admin', MAIL_USERNAME) 新建bbs/email.py文件,并将下面代码写入其中。 from threading import Thread from bbs.extensions import mail from flask_mail import Message from flask import current_app, render_template def async_send_mail(app, msg): with app.app_context(): mail.send(msg) def send_email(to_mail, subject, template, **kwargs): message = Message(current_app.config['BBS_MAIL_SUBJECT_PRE'] + subject, recipients=[to_mail], sender=current_app.config['MAIL_USERNAME']) message.body = render_template(template + '.txt', **kwargs) message.html = render_template(template + '.html', **kwargs) th = Thread(target=async_send_mail, args=(current_app._get_current_object(), message)) th.start() return th 在send_email函数中,使用了render_template()来渲染了邮件消息body以及html参数,因此需要先将这两个模板准备好。新建bbs/templates/email/verifyCode.html 与bbs/templates/email/verifyCode.txt 文件,将下面的代码写到文件中去 verifyCode.html <h3 style="font-weight: bold;font-size: 18px">Hello {{ username }},</h3> <p>Welcome to join the <a href="http://bbs.2dogz.cn">狗子学院</a>!This is your register captcha below here.</p> <h1><strong>{{ ver_code }}</strong></h1> <h5><b><i>The captcha will expire after 10 minutes.</i></b></h5> <p style="color: red; font-style: italic"> If this operate is not by yourself, please change your password right now!Maybe your account was cracked.</p> <small>(Please do not reply to this notification, this inbox is not monitored.)</small> verifyCode.txt Hello {{ username }} Welcome to Blogin! Welcome to join the 狗子学院!This is your register captcha below here. {{ ver_code }} The captcha will expire after 10 minutes. If this operate is not by yourself, please change your password right now!Maybe your account was cracked.</p> (Please do not reply to this notification, this inbox is not monitored.) 然后开始编写发送邮件的后端逻辑代码,新建bbs/blueprint/frontend/normal.py 文件,因为发送邮件属于通用行为,因此将其放入normal.py模块中,将以下代码嵌入其中 from flask import Blueprint, send_from_directory, request, jsonify from bbs.extensions import db from bbs.email import send_email from bbs.models import VerifyCode, Gender, Role, College @normal_bp.route('/send-email/', methods=['POST']) def send(): to_email = request.form.get('user_email') username = request.form.get('user_name') ver_code = generate_ver_code() send_email(to_mail=to_email, subject='Captcha', template='email/verifyCode', username=username, ver_code=ver_code) # 判断是否已经存在一个最新的可用的验证码,以确保生效的验证码是用户收到最新邮件中的验证码 exist_code = VerifyCode.query.filter(VerifyCode.who == to_email, VerifyCode.is_work == 1).order_by( VerifyCode.timestamps.desc()).first() if exist_code: exist_code.is_work = False nt = datetime.datetime.now() et = nt + datetime.timedelta(minutes=10) verify_code = VerifyCode(val=ver_code, who=to_email, expire_time=et) db.session.add(verify_code) db.session.commit() return jsonify({'tag': 1, 'info': '邮件发送成功!'}) 我们根据请求中的keyword来获取前端发送过来的请求参数,然后调用send_email()函数进行发送邮件,同时将生成的随机验证码放入到邮件消息体中去。 然后判断数据库中是否已经存在了属于该注册用户的验证码,如果有则将它设置为过期的,然后将新的验证码存入到数据库中,并设置过期时间为10分钟。这里是通过MySQL来保存的验证码信息,也有其他方法来保存验证码信息,比如使用redis来保存,redis可以设置字段过期时间,如果达到了这个时间,再去取这个字段的值就会为None。 接着我们来处理前端发送邮件的代码,打开bbs/templates/frontend/register.html文件,加入下面的代码 <main> ... </main> <script> let time = 60; let reg = /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/; function sendCapt(){ let $sendBtn = $("#sendCaptcha"); let $email = $("#user_email"); let $username = $("#user_name"); if ($username.val() === '' || $email.val() === '' || !reg.test($email.val())){ return false; } $sendBtn.attr('disabled', true); getRandomCode($sendBtn); $.ajax({ url: '/normal/send-email/', type: 'post', data: {'user_name': $username.val(), 'user_email': $email.val()}, success: function (res){ if (res.tag){ $(".p-hint").slideDown(500).delay(3000).hide(500); } } }) } //倒计时 function getRandomCode(obj) { if (time === 0) { time = 60; obj.text('发送'); obj.attr('disabled', false); return; } else { time--; obj.text(time+'(秒)'); } setTimeout(function() { getRandomCode(obj); },1000); } </script> 通过ajax向后端/normal/send-email/发送请求,同时将email与username传递到后端,同时将发送验证码按钮置为不可点击状态,间隔60s才能发送一次,并在前端页面给用户一个提示信息。这样发送验证码邮件的整个流程就完成了,接下里要处理用户点击注册按钮之后的逻辑。 我们使用的是wtfforms来渲染的后端表单,并且在某些表单字段中加入了一些限制信息。在后端代码中,我们可以通过wtfforms的示例来验证我们的表单,打开bbs/blueprint/frontend/auth.py 加入下面的代码 @auth_bp.route('/register/', methods=['GET', 'POST']) def register(): ... if form.validate_on_submit(): username = form.user_name.data nickname = form.nickname.data password = form.confirm_pwd.data email = form.user_email.data college = form.colleges.data captcha = request.form.get('captcha') code = VerifyCode.query.filter(VerifyCode.who == email, VerifyCode.is_work == 1).order_by( VerifyCode.timestamps.desc()).first() if code: if code.val != int(captcha): flash('验证码错误!', 'danger') return redirect(request.referrer) elif code.expire_time < datetime.datetime.now(): flash('验证码已过期!', 'danger') return redirect(request.referrer) else: flash('请先发送验证码到邮箱!', 'info') return redirect(request.referrer) user = User(username=username, college_id=college, nickname=nickname, email=email, password=password, status_id=1) user.generate_avatar() user.set_password(password) code.is_work = False db.session.add(user) db.session.commit() flash('注册成功,欢迎加入二狗学院!', 'success') return redirect(url_for('.login')) 通过form.validate_on_submit()来判断提交的表单是否通过了验证,通过验证之后通过form.字段名.data来获取对应表单字段的值,然后根据邮箱来查找上一步数据库保存的验证码,如果不存在验证码,则提示用户发送验证码到邮箱,因为存在着一种可能用户乱填一个验证码,而不发送验证码到邮箱。然后判断验证码是否正确或者过期,如果未通过,则发送对应的提示消息提示用户,如果通过,则将用户信息保存到数据库中,然后重定向到登录页面。至此,用户注册的功能就已经完成了。 用户登录 相比注册功能,用户登录就比较简单了。新建bbs/templates/frontend/login.html文件,该文件为用户登录的前端页面模板文件,同样的我们使用wtfforms来渲染表单,在文件中嵌入下面的代码 {% extends "frontend/base.html" %} {% import 'bootstrap/wtf.html' as wtf %} {% block title %} 用户登录 {% endblock %} {% block content %} <body> <main> <div class="container"> {% include "_flash.html" %} <div class="jumbotron pt-5 pb-3 mt-5"> <div class="row"> <div class="col-md-8"> <img src="{{ url_for('static', filename='img/index.jpg') }}" class="rounded img-fluid"> </div> <div class="col-md-4"> <div class="card mb-3 w-100 bg-light align-self-center"> <div class="card-header"><h4 class="text-muted"><strong>用户登录</strong></h4></div> <div class="card-body "> <form class="bs-component" action="/auth/login/" method="post"> {{ form.csrf_token }} {{ wtf.form_field(form.usr_email) }} {{ wtf.form_field(form.password) }} {{ wtf.form_field(form.remember_me) }} {{ form.submit }} <hr> <small>没有账号? <a style="text-decoration: none;" href="{{ url_for('.register') }}">注册.</a> </small> </form> </div> </div> </div> </div> </div> </div> </main> </body> {% endblock %} 代码中的form.csrf_token 是一种防止csrf攻击的手段,关于csrf攻击具体可以百度,之后的代码就跟注册模板一样,通过wtfforms进行表单渲染,因此我们需要新建一个渲染登录表单的类,打开bbs/forms.py模块,加入新建登录表单的代码,如下所示 class LoginForm(FlaskForm): usr_email = StringField(u'邮箱/用户名', validators=[DataRequired(message='用户名或邮箱不能为空')], render_kw={'placeholder': '请输入邮箱或用户名'}) password = StringField(u'登录密码', validators=[DataRequired(message='登录密码不能为空'), Length(min=8, max=40, message='登录密码必须在8-40位之间')], render_kw={'type': 'password', 'placeholder': '请输入用户密码'}) remember_me = BooleanField(u'记住我') submit = SubmitField(u'登录', render_kw={'class': 'source-button btn btn-primary btn-xs'}) 接着需要处理后端的登录视图函数,打开bbs/blueprint/frontend/auth.py模块,新建一个视图函数,代码如下所示 from flask_login import current_user, login_user, logout_user @auth_bp.route('/login/', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('index_bp.index')) form = LoginForm() if form.validate_on_submit(): usr = form.usr_email.data pwd = form.password.data user = User.query.filter(or_(User.username == usr, User.email == usr.lower())).first() if user is not None and user.status.name == '禁用': flash('您的账号处于封禁状态,禁止登陆!联系管理员解除封禁!', 'danger') return redirect(url_for('.login')) if user is not None and user.check_password(pwd): if login_user(user, form.remember_me.data): flash('登录成功!', 'success') return redirect(url_for('index_bp.index')) elif user is None: flash('无效的邮箱或用户名.', 'danger') else: flash('无效的密码', 'danger') return render_template('frontend/login.html', form=form) 关于登录逻辑处理,flask有一个十分流行好用的第三方库flask-login,使用该第三库我们可以很方便的处理登录、退出权限控制等操作。 首先通过current_user来判断用户是否已经登录了,如果登录则返回主页面。接着通过LoginForm示例来获取前端登录页面传递过来的值,从数据库获取用户的相关信息,首先判断账号是否被禁用了,如果被禁用则弹出提示信息,并返回给前端页面。接着判断用户密码是否匹配,如果不匹配则返回登录页面,并提示用户密码不匹配,反之则重定向到主页。 这里的登录成功重定向其实可以做的更加人性化,当用户进入到需要登录才能操作的页面时候,这时候会自动跳转到登录页面。如果用户登录成功,应该是返回前一个页面而不是固定返回主页。flask_login的login_required装饰器重定向到登录页面的时候会带一个next参数,因此我们可以通过此参数来让用户登录成功之后重定向到上一页,具体实现很简单,就请读者自主开发吧! 这时候我们打开登录页面,可以看到如下页面 到此,用户登录注册功能就已经全部实现了,下一节将开始讲述论坛主页的实现。

本文介绍了使用wtforms和flask-mail来实现用户注册和登录功能的方法。通过wtforms可以方便地渲染前端表单,并通过验证函数来验证用户输入的数据。使用flask-mail可以发送邮件,实现验证码功能。文章还介绍了如何使用flask-login来处理用户登录的逻辑。

相关推荐 去reddit讨论

Blogin -

[系列教程]使用Flask搭建一个校园论坛9-支持Markdown语法

1. python-markdown python的之所以变得非常的流行,其最强大的地方就是拥有很多第三库,通过第三方库可以快速的实现很多功能,论坛评论markdown语法的支持就是通过python-markdown库实现的,本节展示一下python-markdown库的基本用法。 首先通过pip命令进行安装 pip install markdown 最简单的markdown转换为html代码 from markdown import markdown md = "## 标题2" html = markdown(md) print(html) 我们只需要将要转换的markdown语法的内容传递给markdown.markdown()方法,就会转换成相应的html代码,上述代码的输出结果是<h2>标题2</h2> markdown这个库的生态十分完整,很多常用的功能它都自己内部解决了,如果不能自行解决,在markdown()方法中还可以通过extensions位置参数提供不同的扩展,下面的代码就是通过TOC扩展实现自动添加目录的功能 from markdown import markdown md = """[TOC] ## 1. 标题1 我的祖国 ### 1. 湖南 我的家乡在湖南 """ html = markdown(md, extensions=['markdown.extensions.toc']) print(html) 上面的代码输出如下图所示,通过TOC扩展,就可以实现自动生成目录的功能 上面的代码中的TOC拓展渲染出来的ul没有样式,如果我们想要自定义对应元素的样式该怎么办呢?我们可以自定义我们的扩展,如下面的代码片段所示 from markdown import extensions from markdown.treeprocessors import Treeprocessor class MyMDStyleTreeProcessor(Treeprocessor): def run(self, root): for child in root.iter(): if child.tag == 'ul': child.set('class', 'ul-content') elif child.tag == 'li': child.set('class', 'li-content-text') return root class MyMDStyleExtension(extensions.Extension): def extendMarkdown(self, md): md.registerExtension(self) self.processor = MyMDStyleTreeProcessor() self.processor.md = md self.processor.config = self.getConfigs() md.treeprocessors.add('mystyle', self.processor, '_end') md = """[TOC] ## 1. 标题1 我的祖国 ### 1. 湖南 我的家乡在湖南 """ html = markdown(md, extensions=['markdown.extensions.toc', MyMDStyleExtension()]) print(html) 在上面的代码中,首先通过继承Treeprocessor类,通过判断节点的属性是否为ul或者li来给节点添加上对应的class属性,之后通过继承Extension类,将我们自定义的extension注册到markdown文本上去,上述代码输出如下图所示,可以看到在ul和li标签分别添加上了对应的class属性,在html页面中我们就可以通过设置class属性的css来调整目录的样式了。 2. 预览评论 有了上面一节的基础后就可以开始实现评论markdown语法支持了,在评论区域使用的是两个TAB,第一个TAB是输入评论内容的,第二TAB是预览所输入的评论被渲染后的样式(参照github设计),原始markdown内容如下图所示 点击预览渲染后的样式如下图所示 评论内容是支持代码内容渲染的,在extensions位置参数中添加extensions=['markdown.extensions.codehilite']即可,但是该扩展依赖于Pygments,所以我们使用之前应该先安装Pygments pip install pygments 安装完成之后我们可以使用pygmentize -S default -f html -a .codehilite > styles.css 命令导出代码高亮样式,Pygments支持变成语言以及代码样式非常多,可以访问https://pygments.org/demo/#进行选择,然后通过前面的命令在命令行中导出相应的样式css文件。 完成上述步骤之后,我们就可以自定义我们的markdown扩展了,自定义代码片段如下所示 class MyMDStyleTreeProcessor(Treeprocessor): def run(self, root): for child in root.getiterator(): if child.tag == 'table': child.set("class", "table table-bordered table-hover") elif child.tag == 'img': child.set("class", "img-fluid d-block img-pd10") elif child.tag == 'blockquote': child.set('class', 'blockquote-comment') elif child.tag == 'p': child.set('class', 'mt-0 mb-0 p-break') elif child.tag == 'pre': child.set('class', 'mb-0') elif child.tag == 'h1': child.set('class', 'comment-h1') elif child.tag == 'h2': child.set('class', 'comment-h2') elif child.tag == 'h3': child.set('class', 'comment-h3') elif child.tag in ['h4', 'h5', 'h6']: child.set('class', 'comment-h4') return root # noinspection PyAttributeOutsideInit class MyMDStyleExtension(extensions.Extension): def extendMarkdown(self, md): md.registerExtension(self) self.processor = MyMDStyleTreeProcessor() self.processor.md = md self.processor.config = self.getConfigs() md.treeprocessors.add('mystyle', self.processor, '_end') 与第一节给ul以及li标签添加class属性一致,通过上面的代码可以自定义一些标签的相关样式。有些小伙伴就比较好奇,为什么不用定义代码高亮的样式呢?因为codehilite扩展会自动将我们的代码转换成对应的html,不同的代码内容自动添加不同class,然后通过前面导出的样式文件实现代码高亮,所以这里不需要我们做任何处理。完成了自定义扩展之后,就是后端视图函数的编写了,代码片段如下。 @normal_bp.route('/comment/render-md/', methods=['POST']) @login_required def render_md(): md = request.form.get('md') html = to_html(md) return jsonify({'html': html}) 上面代码片段逻辑应该比较清楚了,就是获取前端传过来的原始md数据,然后通过to_html方式转换成html,然后返回给前端进行渲染,其中to_html()方法代码如下 def to_html(raw): allowed_tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'abbr', 'b', 'br', 'blockquote', 'code', 'del', 'div', 'em', 'img', 'p', 'pre', 'strong', 'span', 'ul', 'li', 'ol'] allowed_attributes = ['src', 'title', 'alt', 'href', 'class'] html = markdown(raw, output_format='html', extensions=['markdown.extensions.fenced_code', 'markdown.extensions.codehilite', 'markdown.extensions.tables', MyMDStyleExtension()]) clean_html = clean(html, tags=allowed_tags, attributes=allowed_attributes) img_url = '<img class="img-emoji" src="/static/emojis/{}" title="{}" alt="{}">' for i in EMOJI_INFOS: for ii in i: emoji_url = img_url.format(ii[0], ii[1], ii[1]) clean_html = re.sub(':{}:'.format(ii[1]), emoji_url, clean_html) return linkify(clean_html) 在to_html()方法中又出现了一下新的内容clean()以及linkify()两个方法,这两个方法都来字bleach库中,关于bleach库这里不做过多的介绍,感兴趣的童鞋可以自己搜索相关文档了解,这里只介绍使用clean()以及linkify()的目的。 在使用clean之前我们定义了allowed_tags以及allowed_attributes两个列表,懂html的童鞋应该都知道这些事HTML的一些标签以及相应的标签属性,然后通过markdown方法将原始内容转换为HTML,然后使用clean方法,clean方法在这里的作用就是将不再allowed_tags以及allowed_attributes中的属性过滤掉。因为在提交评论的时候,一些不安好心的'良民'可能会在评论中添加一些恶意的JavaScript代码,就是我们所说的XSS攻击,通过clean方法就可以有效的防止这种攻击。linkify()方法的作用是将一些url转换为对应的HTML元素,例如下面的代码片段 from bleach import linkify url = "链接: https://2dogz.cn" print(linkify(url)) mail = "邮箱: 804022023@qq.com" print(linkify(mail, parse_email=True)) 输出如下图 到此为止就完成了评论预览的功能了。 3.渲染评论并保存 用户在提交评论后,我们就可以通过上面的方式将评论渲染成HTML内容,然后保存到数据库中去,代码片段如下 @post_bp.route('/post-comment/', methods=['POST']) @login_required @statistic_traffic(db, CommentStatistic) def post_comment(): ... comment_content = to_html(comment_content) com = Comments(body=comment_content, post_id=post_id, author_id=current_user.id) ... return jsonify({'tag': 1}) @post_bp.route('/reply-comment/', methods=['POST']) @login_required @statistic_traffic(db, CommentStatistic) def reply_comment(): ... comment = to_html(comment) post_id = request.form.get('post_id') reply = Comments(body=comment, replied_id=comment_id, author_id=current_user.id, post_id=post_id) ... return jsonify({'tag': 1}) 内容基本与上一节中的添加评论代码一致,只是在其中添加了to_html()方法,将原始的评论内容转换为HTML内容然后保存到数据库中去。 博客文章中的代码只截取了部分关键代码,很多细节代码没有贴出来,如果想要在本地机器上运行调试可以访问我的github仓库或者gitee仓库仓库,如果想要看到实际效果请访问二狗学院!

Python的流行之处在于其丰富的第三方库,其中python-markdown库可以实现论坛评论的markdown语法支持。通过安装markdown库并调用markdown.markdown()方法,可以将markdown语法转换为相应的html代码。此外,可以通过扩展参数实现自动添加目录的功能。还可以自定义扩展来设置标签的样式。在后端视图函数中,可以将评论内容转换为html并保存到数据库中。

相关推荐 去reddit讨论

Blogin -

[Python]flask-githubcard简单使用介绍

安装依赖 跟其他的python第三方库一样,通过pip的方式就可以自动将其安装在自己的项目环境中,flask-githubcard依赖于flask、requests,在安装过程中会自动进行依赖安装。 pip install flask-githubcard 简单使用 新建app.py文件,添加下面的代码 from flask import Flask, render_template from flask_githubcard import GithubCard app = Flask(__name__) app.config['GITHUB_USERNAME'] = 'weijiang1994' app.config['GITHUB_REPO'] = 'Blogin' githubcard = GithubCard(app) 在上面的代码清单中,我们主要是实例化了一个flask实例,同时给其配置了GITHUB_USERNAME 以及 GITHUB_REPO 两个参数,此两个参数就是你要显示目标仓库卡片的相关信息,flask-githubcard支持配置的参数就只有此两个参数,在配置完成后就通过初始化了flask-githubcard实例了。 之后我们在templates目录中新建一个index.html文件,将下面的代码填入到文件中。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Sample-default theme</title> {{ githubcard.init_css() }} {{ githubcard.init_js() }} </head> <body> <div class="container mt-2"> <h4><b>Flask-GithubCard extension sample.</b></h4> <hr> <a href="/default/">default</a> <a href="/darkly/">darkly</a> <div class="container mt-2"> <div class="row "> <div class="col-4">{{ githubcard.generate_card() }}</div> </div> </div> </div> </body> </html> 在上面的代码中,首先在head section中通过init_css()以及init_js()初始化了依赖文件,flask-githubcard的主要依赖bootstrap4、jQuery以及fontawesome,然后在body section中通过generate_card()函数来完成了github card的渲染。 回到app.py文件,添加视图函数 @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run() 运行app.py文件,访问http://127.0.0.1:5000既可以看到如下图所示的效果 参数配置 在第二节中程序示例都采用的是默认的参数,我们可以根据需要配置自己的参数。 配置css、js依赖 默认的css、js依赖是使用的flask-githubcard static下的静态文件,fontawesome使用的jsdelivr的cdn,如果 你想更换的话,可以通过下面的方式来修改 <head> {{githubcard.init_css(bootstrap='path/cdn_url', fontawesome='path/cdn_url')}} {{githubcard.init_js(jquery='path/cdn_url', bootstrap='path/cdn_url')}} </head> 配置主题模式 flask-githubcard提供了两种主题模式,分别是default、darkly,可以通过下面的方式来指定 <head> {{githubcard.init_css(theme='darkly')}} </head> <div> {{githubcard.generate_card(theme='darkly')}} </div> darkly主题模式效果如下图 注意事项 由于使用了github的api在没有进行授权的情况下,唯一IP在每小时内限制的访问次数为60次,超过60次则会报403, 如果访问频率过高,请前往github上授权账号; 在国内访问github会出现超时的现象,可能会导致网页一直无法打开!

这篇文章介绍了如何使用flask-githubcard库来在Flask应用中显示GitHub仓库卡片。文章首先介绍了安装依赖和简单使用的步骤,然后给出了参数配置的方法,包括配置css、js依赖和配置主题模式。最后提醒注意事项。

相关推荐 去reddit讨论

Blogin -

[系列教程]使用Flask搭建一个校园论坛6-论坛首页

1.功能简介 在第二节中,我们已经把首页的基础框架建好了,首页分为主体以及右边的侧边栏个两个部分。在主体中我们需要显示论坛的帖子,其中帖子又可以根据一些条件进行分类,比如今日热帖、最新的帖子以及随机推荐的帖子等等。在侧边栏中需要显示一些用户信息、发布帖子入口等其他信息。本节主要是是处理首页的主体部分内容,以及侧边栏首个栏位(主要是侧边栏下面的没想好要添加什么内容)。 2.显示帖子列表 为了显示帖子列表,我们需要先在渲染主页模板文件的视图函数中获取帖子的相关数据,并传入到首页模板文件中。 a.最新的帖子 打开bbs/blueprint/frontend/index.py模块,添加下面的代码。 from flask import Blueprint, render_template, request, current_app from bbs.models import Post, VisitStatistic from bbs.extensions import db from sqlalchemy.sql.expression import func from bbs.decorators import statistic_traffic index_bp = Blueprint('index_bp', __name__) @index_bp.route('/') @index_bp.route('/index/') def index(): page = request.args.get('page', 1, type=int) pagination = Post.query.filter_by(status_id=1).order_by(Post.update_time.desc()).\ paginate(page, per_page=current_app.config['BBS_PER_PAGE']) latest = pagination.items return render_template('frontend/index.html', latest=latest, pagination=pagination) 在上面的代码中出现了一个新的东西paginate,该方法会返回一个Pagination类,这样就实现了分页操作。 当用户向首页路由发起请求后,首先会获取请求中的参数page,如果不存在则默认为1,然后通过paginate函数进行分页查询,获取到Pagination实例,通过该实例我们可以获取到当前页码的帖子记录,然后将结果返回到模板文件中进行渲染。打开bbs/templates/frontend/index.html文件,加入下面的代码。 {% block content %} {{ moment.locale(auto_detect=True) }} <main> <div class="container mt-2"> {% include "_flash.html" %} <ul class="nav nav-pills"> <li class="nav-item"> <a class="nav-link active" href="{{ url_for('.index') }}">最新</a> </li> <li class="nav-item "> <a class="nav-link" href="{{ url_for('.hot') }}">热帖</a> </li> <li class="nav-item "> <a class="nav-link" href="{{ url_for('.rands') }}">随机</a> </li> </ul> <hr class="bg-secondary"> <div class="row"> <div class="col-md-8 mt-1"> <div class="tab-content"> <div id="latestPost" class="tab-pane active"> {{ post_item(latest) }} <div class="float-right mt-3"> {% if tag %} {{ render_pagination(pagination) }} {% endif %} </div> </div> </div> </div> {% include "frontend/slider.html" %} </div> </div> </main> {% endblock %} 在上面代码中将帖子分为了三个tab分别是最新、热帖以及随机,三个tab分别定义了三个模板文件(另外两个后续会讲),在用户访问index路由的时候默认显示的最新的帖子。 在模板文件中,我们使用了post_item()宏来渲染帖子的列表。因为我们显示帖子列表的页面有几个,所以可以将代码抽离出来进行宏定义,这样就减少了冗余的代码。打开bbs/templates/文件夹,新建macro.html宏定义文件,添加下面的代码到文件中。 {% macro post_item(posts) %} <ul class="list-group"> {% for post in posts %} <li class="list-group-item pl-1"> <div class="d-flex"> {% if post.is_anonymous == 1 %} <img class="avatar-50 rounded mr-2" src="{{ post.user.avatar }}" alt="{{ post.user.nickname }}"> {% else %} <img class="avatar-50 rounded mr-2" src="{{ url_for('static', filename='img/anonymous.jpeg') }}" alt="anonymous"> {% endif %} <div class="flex-grow-1"> <a class="text-decoration-none" href="{{ url_for('post.read', post_id=post.id) }}">{{ post.title }}</a> <div class="row mt-2"> <div class="col"> <p class="mb-0 text-muted f-12"> <a class="badge badge-secondary mr-2" href="{{ url_for('post.post_cate', cate_id=post.cats.id) }}">{{ post.cats.name }}</a> <i class="fa fa-user-o mr-1"></i> {% if post.is_anonymous == 1 %} <a class="text-decoration-none" href="{{ url_for('profile.index', user_id=post.author_id) }}">{{post.user.nickname}}</a> {% else %} 匿名 {% endif %} <span><a class="text-decoration-none ml-2 text-muted flex-grow-1 text-left f-12-b" > <span data-toggle="tooltip" data-placement="right" data-timestamp="{{ post.create_time }}" data-delay="500" data-original-title="" title="{{ post.create_time }}">{{ moment(post.create_time, local=True).fromNow(refresh=True) }}</span> </a></span></p> </div> <div class="col text-right flex-row-reverse d-lg-flex d-none"> <p class="f-12 text-muted mb-0"><i class="fa fa-comment mr-1"></i>{{ post.comments|length }}</p> <p class="f-12 text-muted mb-0"><i class="fa fa-eye mr-1"></i>{{post.read_times}} </p> </div> </div> </div> </div> </li> {% endfor %} </ul> {% endmacro %} 在该宏定义函数中,我们使用了bootstrap4的列表组组件来显示我们的帖子。通过for循环来遍历我们的帖子将每篇帖子的相关信息以列表的形式展示出来。同时如果帖子是用户匿名进行发布的,那么在显示帖子基本信息的时候都会用匿名信息进行填写。 我们通过上一节的帖子发布页面发布几个测试帖子,再次访问首页就可以看到如下所示的页面了。 b.最热的帖子 同样的在bbs/blueprint/frontend/index.py模块中加入下面的代码 @index_bp.route('/hot-post/') def hot(): page = request.args.get('page', 1, type=int) pagination = Post.query.order_by(Post.read_times.desc()).paginate(page, per_page=current_app.config['BBS_PER_PAGE']) hots = pagination.items tag = pagination.total > current_app.config['BBS_PER_PAGE'] return render_template('frontend/index/hot-post.html', hots=hots, pagination=pagination, tag=tag) 上面的代码跟index视图函数的代码十分类似,只是在数据库的查询条件上做了一些变化,通过帖子的阅读次数来进行排序,并将输出的内容进行分页处理。因此模板文件也与index.html文件类似,在bbs/templates/frontend/index目录中新建hot-post.html模板文件,添加下面的代码 {% extends "frontend/base.html" %} {% from "macro.html" import post_item, render_pagination with context%} {% block title %} 主页 {% endblock %} {% block content %} {{ moment.locale(auto_detect=True) }} <main> <div class="container mt-2"> {% include "_flash.html" %} <ul class="nav nav-pills"> <li class="nav-item"> <a class="nav-link" href="{{ url_for('.index') }}">最新</a> </li> <li class="nav-item"> <a class="nav-link active" href="{{ url_for('.hot') }}">热帖</a> </li> <li class="nav-item "> <a class="nav-link" href="{{ url_for('.rands') }}">随机</a> </li> </ul> <hr class="bg-secondary"> <div class="row"> <div class="col-md-8 mt-1"> <div class="tab-content"> <div id="latestPost" class="tab-pane active"> {{ post_item(hots) }} <div class="float-right mt-3"> {% if tag %} {{ render_pagination(pagination) }} {% endif %} </div> </div> </div> </div> {% include "frontend/slider.html" %} </div> </div> </main> {% endblock %} c.随机推荐的帖子 处理完上面两个tab之后只剩下最后一个随机推荐的tab了,随机推荐的帖子数量我这里设置的是20篇帖子,我们可以使用flask-sqlachemy orm框架中func来轻松实现在数据库中随机获取对应条数的数据,在bbs/blueprint/index.py模块中加入下面的视图函数代码 from sqlalchemy.sql.expression import func @index_bp.route('/rand-post/') def rands(): rand = Post.query.filter_by(status_id=1).order_by(func.random()).limit(20) return render_template('frontend/index/rand-post.html', rands=rand) 打开bbs/templates/frontend/index目录中新建rand-post.html模板文件,模板文件中需要添加代码与上面两小节的代码类似,这里就不在累述了,如果读者不清楚可以去项目的github仓库克隆完整的代码查看。 3.首页侧边栏 在上面的小节中,处理完了首页主要内容中的帖子列表显示,到此侧边栏还没有对应的内容。关于侧边栏的设计,根据不同的功能来将每个功能块用一个card来显示,在这一章节的教程中只完成用户个人资料card。 侧边栏slider是一个通用部分的内容,因此我们可以将其抽离出来,在需要使用的时候可以通过include来导入,类似于我们的base.html文件,打开bbs/templates目录,新建slider.html文件,代码清单如下 <div class="col-md-4 mt-1"> <div class="card border-secondary mb-2"> <div class="card-header"><i class="fa fa-user mr-2"></i>用户</div> <div class="card-body"> {% if not current_user.is_authenticated %} <div><small class="text-danger"><b>您尚未登录!</b></small>请先 <a href="/auth/login/"><span class="badge badge-info">登录</span></a> 或 <a href="/auth/register/"><span class="badge badge-success">注册</span></a>! </div> {% else %} <div class="d-flex"> <img src="{{ current_user.avatar }}" class="rounded avatar-50"> <div> <p class="mb-1 ml-1 text-success"><b>{{ current_user.nickname }}</b></p> <p class="mb-1 ml-1 text-muted"><small>@{{ current_user.username }}</small></p> </div> <div class="d-flex flex-row-reverse"> <a href="/post/new/" class="btn btn-secondary h-75 ml-2"><i class="fa fa-plus"></i></a> </div> </div> <hr class="bg-secondary"> <div class="d-flex p-1"> <div class="pr-2 dropdown"> <button class="btn btn-success dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> 主题 </button> <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> {% for theme_name, display_name in config.BBS_THEMES.items() %} <a class="dropdown-item" href="{{ url_for('normal.change_theme', theme_name=theme_name, next=request.full_path) }}"> {{ theme_name }}</a> {% endfor %} </div> </div> <div class="dropdown"> <button class="btn btn-success dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> 操作 </button> <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> <a class="dropdown-item" href="{{url_for('profile.index', user_id=current_user.id)}}"><i class="fa fa-fw fa-user mr-2"></i>个人中心</a> <a class="dropdown-item" href="{{url_for('user.index', user_id=current_user.id)}}"><i class="fa fa-fw fa-cog mr-2"></i>设置</a> {% if current_user.role_id == 1 %} <a class="dropdown-item" href="{{url_for('be_index.index')}}"><i class="fa fa-fw fa-magnet mr-2"></i>后台管理</a> {% endif %} <a class="dropdown-item" href="/auth/logout/"><i class="fa fa-fw fa-sign-out mr-2"></i>登出</a> </div> </div> </div> {% endif %} </div> </div> </div> 首先通过flask-login的current_user属性来判断用户是否已登录,如果没有登录则显示用户登录或者注册的视图。如果用户已经登录,则显示用户已经登录的视图。在已登录视图中,做了一个权限判断如果是普通用户则不显示后台管理的URL,如果是管理员则显示。在这个视图中用户可以做一些操作,如果切换页面主题、进入个人中心、设置个人资料、退出等等,这时候我们再访问首页,效果如下所示。 这里有很多后端endpoint还没有实现,如果读者出现报错,可以把href这个属性全部删除掉! 这一章节的内容就大致说完了,读者可以到项目github仓库下载完整的示例代码。目前代码的前端部分基本上已经完成了。

本文介绍了如何在网站首页显示帖子列表,并根据条件进行分类。通过获取帖子数据并传入模板文件进行渲染,实现了最新帖子、热门帖子和随机推荐帖子的显示。同时还介绍了如何在侧边栏显示用户个人资料,包括用户头像、昵称、用户名等信息,并提供了一些操作选项。

相关推荐 去reddit讨论

Blogin -

[系列教程]使用Flask搭建一个校园论坛10-个人中心(1)

1. 功能介绍 论坛这种内容管理系统必不可少的一个功能模块就是个人中心,在个人中心中可以查看自己的账号基本信息、以及个人在社区的一些操作记录,比如发帖、评论、收藏等等,本论坛的个人中心功能如下图所示 2. 页面布局 个人中心的页面布局如下图所示,上面个人基本信息栏为个人中心所有页面通用部分,不管在个人中心的哪个栏目,上面通用部分的信息内容是不会改变的,下方的四个tab分别对应不同的功能模块,是差异性内容。 2.1 通用部分 在Jinja2的模板语法中,我们可以通过include关键字来将其他的模板文件引入到我们目标模板文件中,因为个人中心的上半部分的信息是通用,因此我们可以将其抽离出来,放入单独的html模板文件中去。除此之外Jinja2还支持macro块,macro的语法如下所示 # macro.html {% macro sample() %} <p>I am macro function!</p> {% endmacro %} # other.html {% from macro.html import sample with context %} <div> {{ sample() }} </div> 为了方便管理,我们把所有的macro相关的都放到了templates/macro.html文件中,在文件中嵌入下面的代码 {% macro profile_header() %} <div class="row"> <div class="col-md-3 col-lg-3 text-center"> <img class="img-profile-bg" alt="{{ user.nickname }}" src="{{ user.avatar }}"> </div> <div class="col-md-9 col-lg-9 div-center-md"> <h3 class="mb-0">{{ user.nickname }}</h3> <p class="text-muted mb-2">@{{ user.username }}</p> {% if user.slogan %} <p class="mb-2">{{ user.slogan }}</p> {% endif %} {% if user.website or user.location %} <p class="mb-2"> {% if user.website %} <a class="text-decoration-none social-label" href="{{ user.website }}" target="_blank" title="{{ user.nickname }}的个人主页"><i class="fa fa-home fa-fw mr-1"></i>{{ user.website }}</a> {% endif %} {% if user.location %} <span class="social-label d-lg-inline d-none"><i class="fa fa-map-marker fa-fw mr-1"></i>{{ user.location }} </span> <span class="social-label d-lg-none d-md-block" title="{{ user.location }}"><i class="fa fa-map-marker fa-fw mr-1"></i>{{ user.location|truncate(8) }} </span> {% endif %} </p> {% endif %} <p> <span class="mr-2">{{ user.post|length }}篇帖子</span> <a href="{{ url_for('profile.follower', user_id=user.id) }}" class="mr-2 span-hand a-link text-decoration-none">{{ user.followers.count() }}位粉丝</a> <a href="{{ url_for('profile.following', user_id=user.id) }}" class="mr-2 span-hand a-link text-decoration-none">{{ user.following.count() }}个关注</a> </p> {% if current_user.id == user.id %} <a class="btn btn-outline-success" href="{{ url_for('user.index', user_id=current_user.id) }}">个人主页</a> {% else %} {% if current_user.is_following(user) %} <a class="btn btn-outline-info" href="/profile/unfollow/{{ user.id }}/">取消关注</a> {% else %} <a class="btn btn-outline-info" href="/profile/follow/{{ user.id }}/">关注TA</a> {% endif %} <!-- 弹出私信窗口 --> <a class="btn btn-outline-primary" data-toggle="modal" data-userid="{{ user.id }}" data-target="#privacyChat">私信</a> {% if not blocked %} <a class="btn btn-outline-danger" data-toggle="modal" data-userid="{{ user.id }}" data-target="#confirmBlockUser">Block</a> {% else %} <a class="btn btn-primary disabled" data-toggle="modal" data-userid="{{ user.id }}" data-target="#confirmBlockUser">Blocked</a> {% endif %} {% endif %} </div> </div> {% endmacro %} 个人中心除了用户自己的,用户同样也可以访问其他用户的个人中心主页,因此在上面的代码中加入了判断,当前个人中心主页是否为当前用户个人的主页,如果不是,则在下方添加不同的功能按钮。 2.2 TAB部分 上面的通用部分内容既可以通过macro实现也可以通过include去实现,tab部分的内容就通过macro实现就比较方便了,在macro中我们也可以传入参数,同样的在macro.html中增加下列代码 {% macro profile_moment(tabName) %} <div class="mt-4"> <ul class="nav nav-tabs nav-justified"> <li class="nav-item"> <a class="nav-link {% if tabName == '帖子' %}active{% endif %}" href="/profile/user/{{ user.id }}/">帖子</a> </li> <li class="nav-item"> <a class="nav-link {% if tabName == '评论' %}active{% endif %}" href="/profile/comment/{{ user.id }}/">评论</a> </li> <li class="nav-item"> <a class="nav-link {% if tabName == '收藏' %}active{% endif %}" href="/profile/collect/{{ user.id }}/">收藏</a> </li> <li class="nav-item"> <a class="nav-link {% if tabName == '社交' %}active{% endif %}" href="/profile/social/{{ user.id }}/">社交</a> </li> </ul> </div> {% endmacro %} 这里与上面稍微有些不同,这里的macro函数带有参数输入,我们在函数体中通过判断参数的不同实现对应ab的被选中,当然这里也可以通过include去实现,可以通过endpoint来设置对应tab被选中。 3. 关注、取消关注用户 很多社交网站都会给用户提供关注的功能,论坛也提供了这个功能,用户既可以被别人关注,同时用户也可以关注他们,所以是多对多的关系,Follow表结构如下 class Follow(db.Model): __tablename__ = 't_follow' id = db.Column(db.INTEGER, primary_key=True, autoincrement=True) follower_id = db.Column(db.INTEGER, db.ForeignKey('t_user.id')) followed_id = db.Column(db.INTEGER, db.ForeignKey('t_user.id')) # 正在关注用户的人 follower = db.relationship('User', foreign_keys=[follower_id], back_populates='following', lazy='joined') # 用户自己正在关注的人 followed = db.relationship('User', foreign_keys=[followed_id], back_populates='followers', lazy='joined') class User(db.Model, db.UserMixin): ... following = db.relationship('Follow', foreign_keys=[Follow.follower_id], back_populates='follower', lazy='dynamic', cascade='all') followers = db.relationship('Follow', foreign_keys=[Follow.followed_id], back_populates='followed', lazy='dynamic', cascade='all') 这里需要注意的是,由于Follow表中的两个字段follower_id, followed_id都是user表的外键,因此我们在定义的时候需要指定每个关系所对应的字段,不然ORM框架会识别不了,follower指定为followe_id,followed指定为followed_id,与此同时,在user表中同样也是指定。 创建好Follow表之后,关注的逻辑就比较简单了,在blueprint/profile.py文件中增加下面的代码。代码逻辑十分简单,就是通过向Follow表中新增或者删除相应的数据,实现关注或者取消关注功能,同时还增加了自己不能关注自己的逻辑。 @profile_bp.route('/follow/<user_id>/', methods=['GET', 'POST']) @login_required def follow_user(user_id): user = User.query.get_or_404(user_id) if user.id == current_user.id: flash('我关注我自己?禁止套娃!', 'info') return redirect(request.referrer) if current_user.is_following(user): flash('你已经关注TA了!', 'info') return redirect(request.referrer) current_user.follow(user) flash('关注成功!', 'success') return redirect(request.referrer) @profile_bp.route('/unfollow/<user_id>/', methods=['GET', 'POST']) @login_required def unfollow_user(user_id): user = User.query.get_or_404(user_id) if current_user.is_following(user): current_user.unfollow(user) if request.method == 'POST': return jsonify({'tag': 1}) flash('取关成功!', 'success') return redirect(request.referrer) 4. 用户帖子 如上面所说,个人中心用户即可以访问自己的也可以访问他人,因此需要在帖子列表上做区分,在frontend/profile.py文件中增加下面的代码 @profile_bp.route('/user/<user_id>/') @login_required def index(user_id): page = request.args.get('page', default=1, type=int) user = User.query.get_or_404(user_id) per_page = current_app.config['BBS_PER_PAGE'] # 其他人查看用户信息时候屏蔽用户匿名发表的帖子 if current_user.id == int(user_id): pagination = Post.query.filter(Post.author_id == user_id, Post.status_id == 1).order_by( Post.create_time.desc()).paginate(page=page, per_page=per_page) posts = pagination.items else: pagination, posts = get_range_post(user.id, page=page, per_page=per_page, range_day=TIME_RANGE.get(user.range_post.name)) blocked = BlockUser.query.filter(BlockUser.user_id == current_user.id, BlockUser.block_user_id == user_id).all() return render_template('frontend/profile/profile.html', user=user, tag=pagination.total > per_page, pagination=pagination, posts=posts, blocked=blocked) 代码的逻辑很简单,如下 通过路由参数中的user_id来获取对应的user,如果没有找到user则返回404; 判断当前user是否为路由参数中的user,如果不是,则只获取该用户状态为正常的帖子; 如果是则获取所有的状态的帖子; 上面还出现了BlockUser的相关代码,这个代码在后续用户主页的相关文章中进行讲解,这里暂时不做讲解! 后端后去数据后,需要通过Jinja2模板进行渲染了,在frontend/profile/profile.html中新增下面的代码 {% extends "frontend/base.html" %} {% from "macro.html" import profile_header, profile_moment, render_pagination, post_list with context %} {% block head %} {{ super() }} <style> .a-title { font-size: 22px; font-weight: bold; } .a-title-sm { font-size: 16px; font-weight: bold; } .f-12-b { font-weight: bold; font-size: 12px; } .span-hand { cursor: pointer; } </style> {% endblock %} {% block title %} {{ user.nickname }}的个人主页 {% endblock %} {% block content %} {{ moment.locale(auto_detect=True) }} <body> <main> <div class="container mt-2"> {% include "_flash.html" %} {{ profile_header() }} {{ profile_moment('帖子') }} {% if posts %} <div class="mt-2"> {{ post_list(posts) }} {% if user.range_post.name != '全部' and current_user.id != user.id %} <div class="mt-2 text-center"> <p class="text-muted"><b>该用户仅展示最近{{ user.range_post.name }}的帖子</b></p> </div> {% endif %} </div> {% else %} {% if current_user.id == user.id %} <div class="text-center mt-5"> <p class="text-muted">你还没有发送过帖子!</p> </div> {% else %} <div class="text-center mt-5"> {% if user.available_post_counts()|length %} {% if user.range_post.name == '隐藏' %} <p class="text-muted"><b>该用户{{ user.range_post.name }}了帖子</b></p> {% else %} <p class="text-muted"><b>该用户仅展示最近{{ user.range_post.name }}的帖子</b></p> {% endif %} {% else %} <p class="text-muted"><b>他还没有发送过帖子!</b></p> {% endif %} </div> {% endif %} {% endif %} {% include "follow-modal.html" %} </div> </main> </body> {% include "frontend/profile/contact-modal.html" %} {% include "frontend/profile/block-user-modal.html" %} {% endblock %} 博客文章中的代码只截取了部分关键代码,很多细节代码没有贴出来,如果想要在本地机器上运行调试可以访问我的github仓库或者gitee仓库仓库,如果想要看到实际效果请访问二狗学院!

该文章介绍了论坛个人中心的功能和页面布局。个人中心包括个人基本信息栏和四个功能模块。通过Jinja2模板语法实现了个人中心页面的通用部分和TAB部分的内容。同时,文章还介绍了用户关注功能的实现和用户帖子的展示。

相关推荐 去reddit讨论

Blogin -

[应用部署]使用Docker部署flask应用

1. 安装Docker 本文所有操作均在ubuntu2204上进行,请知悉!同时Docker的官方文档最开始使用的dockerfile,但是对于新手来说,我个人感觉dockerfile不是太友好(比如对于我来说),因此本文只介绍如何手动在Docker容器里面部署flask应用! 通过apt命令查看软件源中是否包含有docker apt-cache policy docker.io 如果出现类似下图的结果,则说明软件源中存在有docker的安装包直接通过下面的命令进行安装即可 sudo apt-get install docker.io # 查看docker版本详细信息 sudo docker version 如果显示如下图所示的信息,则说明docker成功安装了 如果软件源里面没有docker的包,则可以直接去docker官网下载,然后通过dpkg -i命令进行安装 2. 创建Docker容器 在docker中有两个概念:镜像、容器。镜像顾名思义,就是我们平时所说的操作系统比如ubuntu、centos等,但是在docker中的镜像一般都是经过精简后只提供最小的运行单元,比如你通过docker pull ubuntu 会自动将最新的ubuntu镜像从docker的服务器拉去到本地,通过docker images 可以看到如下图所示的信息,镜像的大小只有77.8MB. 镜像是容器的基础,一个镜像可以创建多个容器,容器就是运行web应用的宿主机,简而言之就是类似于虚拟机,但是跟虚拟机还是有很大的差别,镜像与容器的关系如下图所示。 通过下面的命令就可以创建我们自己的docker容器了 sudo docker pull ubuntu # 从docker镜像仓库中拉去ubuntu最新的镜像 sudo docker run -p 8001:5000 -it ubuntu /bin/bash # 通过本地的ubuntu镜像去创建容器 sudo docker ps -a 通过上面的命令就可以创建一个docker容器了,结果如下图所示则说明容器创建成功了,下面解释一下上面的参数 -p 由于我们是将web应用部署在docker容器内部,容器对于宿主机来说是两个不同的环境,通过-p参数就可以将容器的端口映射到宿主机的端口,比如外部客户端访问宿主机的8000端口的服务器,实际上是访问到容器内部5000端口的服务 -it 加上这个参数我们就可以通过bash的方式进入容器内部 -a ps命令默认只会显示出正在运行的容器,加上-a参数可以显示本机所有的容器 创建好容器之后,就可以通过start命令去运行容器了,显示下图的信息就说明容器已经成功运行了 sudo docker start adoring_wescoff # 跟容器名称 sudo docker ps 3. 进入容器手动部署应用 容器成功启动后,我们就可以通过exec命令进入容器内部,然后部署我们的应用了,不过在这之前我们可以通过rename去给容器重新修改一个名字,因为我们在执行run命令的时候,没有通过name参数给容器指定一个名称,因此docker会自动给我们的容器创建一个随机的名称,如下图所示,将容器重新命名为flask_app,然后通过ps命令去查看,容器显示命名称为flask_app,则说明容器名称修改成功。 sudo docker rename adoring_wescoff flask_app sudo docker ps 成功修改容器名称后,通过exec命令进入容器内部 sudo docker exec -it flask_app /bin/bash 在根目录中通过下面的命令创建一个简单的flask应用,并启动 mkdir demo apt update apt install python3 python3-pip vim # 安装一些必要的包 pip install flask cd demo vim app.py 在app.py文件中输入下面的代码,通过python app.py运行应用 from flask import Flask app = Flask(__name__) @app.route('/') def index(): return "Hello, World![FROM DOCKER]" if __name__ == '__main__': app.run(host="0.0.0.0") 在宿主机中打开浏览器,访问http://127.0.0.1:8000 即可看到下面的内容 到此为止,使用docker容器手动部署flask web应用就完成了。 手动部署肯定不是最好的方式,但是我感觉适合我这种对Linux比较熟悉,但是对docker不是太熟悉的人,后面会继续更新通过DockerFile进行部署的内容!

本文介绍了在Ubuntu上手动部署Flask应用的步骤。首先安装Docker,然后创建Docker容器并运行,最后进入容器内部手动部署应用。通过这些步骤,可以成功部署Flask web应用。

相关推荐 去reddit讨论

Blogin -

[Python]使用flask-apscheduler控制定时任务

Flask-APScheduler介绍 Flask-APScheduler是基于APScheduler库开发的Flask拓展库。APScheduler的全称是Advanced Python Scheduler。允许您将Python代码安排为稍后执行,可以只执行一次,也可以定期执行。您可以随时添加新作业或删除旧作业。如果您将作业存储在数据库中,那么调度程序重启后它们也将存活下来并保持其状态。当调度器重新启动时,它将运行它在离线时应该运行的所有作业,APScheduler文档。 社区的强大性,让我们在使用flask的时候可以很方便的调用APScheduler库,我们可以通过flask-apscheduler来调用APScheduler库。进入你的pyhton虚拟环境,安装flask-apscheduler库 pip install flask-apscheduler 顺利的话,我们可以使用这个第三方库了。 使用flask配置启动定时任务 APSchedule可以使用很多方式进行启动任务,比如interval,或者cron等等,下面就分别介绍一下这两种方式启动任务。 interval间隔时间执行 我们可以通过配置如下参数来每间隔多少时间来启动任务 JOBS = [         {             'id': 'job1',             'func': 'scheduler:task',             'args': (1, 2),             'trigger': 'interval',             'seconds': 10         }     ] 其中func表示你要启动的函数,trigger表示触发方式,这里使用的interval表示间隔触发,second表示间隔的时间长短。 我们可以通过flask配置启动定时任务,栗子如下 """ # coding:utf-8 @Time : 2020/11/19 @Author : jiangwei @mail : jiangwei1@kylinos.cn @File : scheduler.py @Software: PyCharm """ from flask import Flask import datetime from flask_apscheduler import APScheduler aps = APScheduler() class Config(object): JOBS = [ { 'id': 'job1', 'func': 'scheduler:task', 'args': (1, 2), 'trigger': 'interval', 'seconds': 10 } ] SCHEDULER_API_ENABLED = True def task(a, b): print(str(datetime.datetime.now()) + ' execute task ' + '{}+{}={}'.format(a, b, a + b)) if __name__ == '__main__': app = Flask(__name__) app.config.from_object(Config()) scheduler = APScheduler() scheduler.init_app(app) scheduler.start() app.run(port=8000) 上述代码中,通过APScheduler每间隔10秒钟执行一次task函数。 输出结果如下图,我们可以看到每隔10s中执行了一次函数。 上面的例子中,将需要执行的函数定义在该文件内部,如果我们的函数定义在其他文件中,可以通过导包的方式引用。比如下面的栗子, task.py位于当前文件的上一层目录中 # task.py def task_1(a, b): print(a+b) 那么我们可以如下定义JOBS JOBS = [         {             'id': 'job1',             'func': '.task:task_1',             'args': (1, 2),             'trigger': 'interval',             'seconds': 10         }     ] cron启动任务 crontab是Linux中定时任务启动程序,我们可以通过配置crontab的配置文件来定时启动任务。在APScheduler中也可以通过cron的形式来定时启动任务。下载的例子来说明配置方式 from flask import Flask import datetime from flask_apscheduler import APScheduler aps = APScheduler() class Config(object): JOBS = [ { 'id': 'job1', 'func': 'scheduler:task', 'args': (1, 2), 'trigger': 'cron', 'day': '*', 'hour': '13', 'minute': '16', 'second': '20' } ] SCHEDULER_API_ENABLED = True def task(a, b): print(str(datetime.datetime.now()) + ' execute task ' + '{}+{}={}'.format(a, b, a + b)) if __name__ == '__main__': app = Flask(__name__) app.config.from_object(Config()) scheduler = APScheduler() scheduler.init_app(app) scheduler.start() app.run(port=8000) 上述的代码表示,在每天的13:16:20秒启动task()函数。其实看配置就能理解意思,一目了然,其中*代表任意的意思。上述代码运行输出如下 使用装饰器定时启动任务 除了上面通过配置的方式来启动定时任务外,我们还可以使用装饰器的方式来定时启动任务。例子如下所示 from flask import Flask from flask_apscheduler import APScheduler import datetime class Config(object): SCHEDULER_API_ENABLED = True scheduler = APScheduler() # interval examples @scheduler.task('interval', id='do_job_1', seconds=30, misfire_grace_time=900) def job1(): print(str(datetime.datetime.now()) + ' Job 1 executed') # cron examples @scheduler.task('cron', id='do_job_2', minute='*') def job2(): print(str(datetime.datetime.now()) + ' Job 2 executed') @scheduler.task('cron', id='do_job_3', week='*', day_of_week='sun') def job3(): print(str(datetime.datetime.now()) + ' Job 3 executed') @scheduler.task('cron', id='do_job_3', day='*', hour='13', minute='26', second='05') def job4(): print(str(datetime.datetime.now()) + ' Job 4 executed') if __name__ == '__main__': app = Flask(__name__) app.config.from_object(Config()) # it is also possible to enable the API directly # scheduler.api_enabled = True scheduler.init_app(app) scheduler.start() app.run(port=8000) 上述代码的含义如下: job1: 每间隔30s执行一次函数 job2: 每分钟执行一次函数 job3: 每周的星期天执行一次函数 job4: 每天的13:26:05时刻执行一次函数 代码输出: 上面就是APScheduler的一些简单用法啦,抽空根据应用场景写一下实用的应用方法。

Flask-APScheduler是基于APScheduler库开发的Flask拓展库,可以将Python代码安排为稍后执行,可以只执行一次,也可以定期执行。可以通过配置参数来设置任务的触发方式,如间隔时间执行和cron启动任务。还可以使用装饰器的方式来定时启动任务。

相关推荐 去reddit讨论

Blogin -

[系列教程]使用Flask搭建一个校园论坛3-登录注册

1.知识预览 在本章节中,将学习到以下内容: flask-sqlachemy 数据库orm的使用 click 注册命令,初始化项目基础数据 2.轮子 Flask本身的定义是一个微框架,何为其义呢?“微”并不代表整个应用只能塞在一个 Python 文件内, 当然塞在单一文件内也没有问题。 “微”也不代表 Flask 功能不强。 微框架中的“微”字表示 Flask 的目标是保持核心简单而又可扩展。 Flask 不会替你做出许多决定,比如选用何种数据库。 类似的决定,如使用何种模板引擎,是非常容易改变的。 Flask 可以变成你任何想要的东西,一切恰到好处,由你做主。因此,可以在开源的世界中找到非常多的flask扩展,本项目中使用的orm框架是flask-sqlachemy,基于sqlachemy开发的flask扩展。 在bbs目录下新建extensions.py模块,这个模块主要是用来存放我们第三方拓展使用。 bbs/extensions.py from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() 3.项目配置 项目可以有开发环境、测试环境、生产环境等,不同环境所使用的配置肯定也是不同的,同时项目也有一些配置是通用的。在bbs目录下面新建setting.py模块,该模块用来存储项目所有配置参数。 在第三小节中,我们使用了flask-salachemy这个框架,使用这个框架之前,有一些默认参数需要我们进行配置,我们将下面的代码写入到setting.py文件中去。 bbs/setting.py import os basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) class BaseConfig(object): SQLALCHEMY_TRACK_MODIFICATIONS = False DATABASE_USER = os.getenv('DATABASE_USER') DATABASE_PWD = os.getenv('DATABASE_PWD') DATABASE_HOST = os.getenv('DATABASE_HOST') DATABASE_PORT = os.getenv('DATABASE_PORT') class DevelopmentConfig(BaseConfig): SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{}:{}@{}/bbs?charset=utf8mb4'.format(BaseConfig.DATABASE_USER, BaseConfig.DATABASE_PWD, BaseConfig.DATABASE_HOST) # REDIS_URL = "redis://localhost" REDIS_URL = "redis://localhost:6379" 首先通过内置的os模块获取了项目的根目录,同时新建了一个BaseConfig类,在该类中定义了一些参数(配置参数名称一般使用大写命名),由于数据库连接的参数都属于是比较高危的数据,因此将其保存在环境变量中,通过os.getenv()去获取这些参数的具体数值。 DevelopmentConfig类继承于BaseConfig类,该类是在开发环境中使用的。在该类中定义了SQLALCHEMY_DATABASE_URI参数,flask-sqlachemy在初始化时,会自动根据该参数去连接数据库。 数据库连接参数都保存在环境变量中,有以下两种方式将其保存至环境变量: 使用export命令(Linux) export DATABASE_USER=root export DATABASE_PWD=123456 export DATABASE_HOST=127.0.0.1 export DATABASE_PORT=3306 使用该方式的弊端就是我们每次都需要手动去执行这些命令。 .env文件 可以将这些参数保存到.env文件中,然后通过python-dotenv库自动加载.env文件中的内容到环境变量。在university-bbs目录下新建.env文件,并嵌入下面的代码。 DATABASE_USER=weijiang DATABASE_PWD=1994124 DATABASE_HOST=127.0.0.1 DATABASE_PORT=3306 如果没有自动加载我们可以在setting.py模块顶部中加入如下代码 from dotenv import load_dotenv load_dotenv('.env') 4.表的创建 在完成了上面的准备工作之后就可以开始创建数据库表了。作为一个CMS系统,数据库的使用是必不可少的,在这里采用的MySQL数据库,当然你也可以使用其他的关心数据库,如MariaDB等。 在bbs目录下新建一个models.py模块,该模块主要是用来定义我们的数据库表模型用的,嵌入以下代码 bbs/models.py class User(db.Model, UserMixin): __tablename__ = 't_user' id = db.Column(db.INTEGER, primary_key=True, nullable=False, index=True, autoincrement=True) username = db.Column(db.String(40), nullable=False, index=True, unique=True, comment='user name') nickname = db.Column(db.String(40), nullable=False, unique=True, comment='user nick name') password = db.Column(db.String(256), comment='user password') email = db.Column(db.String(128), unique=True, nullable=False, comment='user register email') slogan = db.Column(db.String(40), default='') website = db.Column(db.String(128), default='', comment="user's website") location = db.Column(db.String(128), default='', comment='user location') avatar = db.Column(db.String(100), nullable=False, comment='user avatar') avatar_raw = db.Column(db.String(100), comment='use avatar raw file') create_time = db.Column(db.DATETIME, default=datetime.datetime.now) status_id = db.Column(db.INTEGER, db.ForeignKey('t_status.id')) college_id = db.Column(db.INTEGER, db.ForeignKey('t_college.id')) role_id = db.Column(db.INTEGER, db.ForeignKey('t_role.id'), default=3, comment='user role id default is 3 ' college = db.relationship('College', back_populates='user') role = db.relationship('Role', back_populates='user') status = db.relationship('Status', back_populates='user') 'that is student role') 在上面代码中定义了一个User类,并且让它继承自Model、UserMiXin两个基类。之后在类中定义了一些参数,其中使用db.Column定义就是表的字段,使用db.relationship定义是表与表之间的关系。在有些字段中我们使用了db.Foreignkey,这表示该字段是一个外键。通过外键关系,可以保证数据唯一性与完整性。 在这种小项目中使用外键可以提高开发效率,但如果项目量级大,并且业务并发量大,就不要去使用外键这种数据库层面的逻辑维护,可以将数据完整性维护添加到业务代码中去。 外键的字段命名没有明确的要求,因为使用的其他表的主键id作为外键进行连接,因此为了方便阅读,使用表名_id的形式进行命名,同时ForeignKey类传入参数为表名.字段名。模型类对应的表名由flask-sqlachemy自动生成,如果你在模型类中定义了__tablename__ 参数,则会使用该参数的值作为表名。 在User表中,我们使用了三个外键,分别是status_id、college_id、role_id,因此需要新建Status、College、Role三个表模型,同时role跟permission又是外键关系,因此还需要创建permission模型,在models.py模块中嵌入如下代码 class College(db.Model): __tablename__ = 't_college' id = db.Column(db.INTEGER, primary_key=True, nullable=False, autoincrement=True, index=True) name = db.Column(db.String(100), nullable=False) create_time = db.Column(db.DATETIME, default=datetime.datetime.now) user = db.relationship('User', back_populates='college', cascade='all') class Role(db.Model): __tablename__ = 't_role' id = db.Column(db.INTEGER, primary_key=True, autoincrement=True, index=True) name = db.Column(db.String(40), nullable=False) permission_id = db.Column(db.INTEGER, db.ForeignKey('t_permission.id'), nullable=False) user = db.relationship('User', back_populates='role', cascade='all') permission = db.relationship('Permission', back_populates='role') class Status(db.Model): __tablename__ = 't_status' id = db.Column(db.INTEGER, primary_key=True, autoincrement=True, index=True) name = db.Column(db.String(40), nullable=False) user = db.relationship('User', back_populates='status', cascade='all') class Permission(db.Model): __tablename__ = 't_permission' id = db.Column(db.INTEGER, primary_key=True, autoincrement=True, index=True) name = db.Column(db.String(40), nullable=False) role = db.relationship('Role', back_populates='permission', cascade='all') 定义db.relationship()关系属性有什么作用呢?通过下图来进行解释。 一个角色可以对应多个用户,一个用户只能对应一个角色,我们在Role模型类中定义了user关系变量,在User模型类中定义了role关系变量。 user = User.query.filter_by(id=1).first() print(user.role.name) role = Role.query.filter_by(id=1).first() print(role.user) 上面代码就解释了定义关系变量的作用,在一对多的关系中,在一端我们可以直接通过关系参数获取到对应多端的值,在多端关系参数返回是一个集合,在这个集合中我们可以获取到集合元素的所有值,大大的简化了我们工作。 5.初始化基础数据 完成了上述模型类的定义后,我们需要在数据库中建立我们的表。在__init__.py模块中加入下面的代码: from bbs.models import * from bbs.extensions import db def create_app(config_name=None): # 省略之前的代码 app.config.from_object(DevelopmentConfig) register_extensions(app) return app def register_extensions(app: Flask): db.init_app(app) 在register_extensions()函数中初始化flask-sqlachemy,然后在工厂函数中进行注册调用。 有关Flask的第三方拓展一般都是使用的extension.init_app(app)的方式进行初始化注册! 接下来我们继续在__init.py__模块中添加下面的代码,用作初始化项目的必须基础数据。 def create_app(config_name=None): # 省略已有代码 register_cmd(app) return app def register_cmd(app: Flask): @app.cli.command() def init(): click.confirm('这个操作会清空整个数据库,要继续吗?', abort=True) db.drop_all() click.echo('清空数据库完成!') db.create_all() init_status() click.echo('初始化状态表完成!') init_colleges() click.echo('初始化学院表完成!') init_permission() click.echo('初始化权限表完成!') init_role() click.echo('初始化角色表完成!') db.session.commit() click.echo('数据库初始化完成!') def init_status(): s1 = Status(name='正常') db.session.add(s1) s2 = Status(name='禁用') db.session.add(s2) db.session.commit() def init_colleges(): colleges = ['计算机科学与技术学院', '信息与通信工程学院', '法学院', '外国语学院', '体育学院', '生命科学学院', '文学院'] for college in colleges: c = College(name=college) db.session.add(c) db.session.commit() def init_permission(): permissions = ['ALL', 'SOME', 'LITTLE'] for per in permissions: p = Permission(name=per) db.session.add(p) db.session.commit() def init_role(): roles = ['超级管理员', '老师', '学生'] r1 = Role(name=roles[0], permission_id=1) r2 = Role(name=roles[1], permission_id=2) r3 = Role(name=roles[2], permission_id=3) db.session.add(r1) db.session.add(r2) db.session.add(r3) db.session.commit() 在上述代码中我们定义了一个register_cmd(app)函数,同时在工厂函数中调用了它。在register_cmd函数中,通过app.cli.command()装饰器可以将函数admin()注册为一个flask命令,在admin()函数中,做了数据库初始化的操作。 通过使用前面定义的模型类,实例化对应的模型类对象,然后通过db.session.add(instance)添加到数据库,最后通过db.session.commit()函数将修改提交到数据库中。 在对数据库做了任何修改操作之后,我们都需要进行commit()操作! 确保激活了虚拟环境,在终端输入如下命令,就可以进行数据库初始化操作,通过终端输出的信息,可以看到数据库初始化的进度。 flask init 6. auth蓝图 完成上述工作之后就可以开始用户注册功能开发了。用户注册、登录等操作都属于验证操作,可以将其放入同一个功能蓝图中去,在bbs/blueprint目录下新建auth.py模块,嵌入如下代码。 from flask import Blueprint auth_bp = Blueprint('auth', __name__, url_prefix='/auth') 别忘了在创建新的蓝图之后需要到create_app()函数中去进行注册,否则路由会找不到对应的视图函数而抛出404错误! 注册、登录部分的功能我打算分为两个章节来写,如果一个章节过长,就会导致读者失去耐心,因此,本章节的内容就到这里了。 教程中的资源文件可以进入我的github仓库下载源代码使用 仓库连接 下一节,我们将继续进行用户注册、登录功能的实现啦,尽请期待啦~~~

本章介绍了使用Flask-SQLAlchemy进行数据库ORM操作和使用Click注册命令初始化项目基础数据的方法,以及使用蓝图进行用户注册功能开发的方法。

相关推荐 去reddit讨论

Blogin -

[系列教程]使用Flask搭建一个校园论坛2-基本框架

在上一节中,我们介绍了整个项目的起因、功能设计等,这节开始,我们就是真正开始写(chao)代码了~~ 1. 储备物资 在阅读这个系列教程之前,我们需要在我们的脑海中储备以下知识: Python编程语言的基本语法 Flask框架的基本用法 Jinja2模板引擎基本使用 Python ORM框架 HTML JS CSS 简单的了解 有了以上的基础知识,我们就可以很顺利的阅读这个系列的教程啦~ 2. 开始耕地 很多人都称我们程序员为码农码农,那我们就开始种地吧~ 2.1 服务入口 相信阅读过flask文档的朋友都知道flask包含有以下两种启动方式 通过python脚本的方式运行 from flask import Flask app = Flask(__name__) @app.route('/') def index(): return 'Hello, flask!' if __name__ == '__main__': app.run() 通过命令行的方式 使用该方式启动flask应用的时候,我们需要先在命令行窗口中输入如下命令: export FLASK_APP=app 然后通过flask run 命令来启动应用。 注意:Windows 用户请将export替换成set 在此教程中,我们采用第二种方式来进行应用启动,因为我们项目内容相对比较多,都是CURD:),应用的不同功能都分布在不同的模块中,使用flask的Blueprint来分割每个功能模块。 在bbs目录下新建__init__.py文件,该模块为我们的应用启动入口,在里面嵌入如下代码 from flask import Flask def create_app(): app = Flask('bbs') @app.route('/') def index(): return 'Hello, university bbs' return app 然后在控制台中输入2.1节的命令,访问 http://127.0.0.1:5000,我们就可以看到如下页面了,说明我们的flask应用已经成功启动了。 上面的代码,我们创建了一个名为create_app 的函数,在该函数中我们实例化了一个Flask对象,然后通过装饰器的方式注册了一个路由'/',最后将这个Flask对象返回。当我们使用命令启动flask应用的时候,这个函数就是我们的入口函数,这种方式我们称作为工厂模式。 2.2 开启debug 我们在开发过程中,肯定是修改代码之后就要立即调试,flask提供了debug模式,当我们开启debug的时候,我们修改完了代码,会auto reload 我们的应用,这样我们就不用每次修改了代码之后,手动去重新启动服务了,我们只需要在命令行中输入如下命令即可 export FLASK_ENV=development 2.3 通用部分 我们的网站大概长成下面这个样子 其中,页眉和页脚在我们每个页面中都会出现,这样我们就可以将其抽取出来变为一个公共部分,又jinja2提供模板继承的功能,我们可以在其他页面中继承这些通用部分的内容。 在bbs/templates文件中我们新建一个frontend文件夹,然后新建一个名为base.html的文件,在该文件中嵌入以下内容: bbs/templates/frontend/base.html <!DOCTYPE html> {% from "macro.html" import nav_item with context %} <!--suppress ALL --> <html lang="zh-hans"> {% block head %} <head> <meta charset="UTF-8"> <title>{% block title %}{% endblock %}-二狗学院</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <link rel="shortcut icon" href="{{url_for('static', filename='img/favorite.png')}}" type="image/x-icon"> <link rel="icon" href="{{ url_for('static', filename = 'img/favorite.png') }}" type="image/x-icon"> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <script src="https://cdn.staticfile.org/popper.js/1.15.0/umd/popper.min.js"></script> <link href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"> <link rel="stylesheet" href="{{ url_for('static', filename='themes/darkly.bootstrap.min.css'}}"> <script src="{{ url_for('static', filename='validator/form-validation.js') }}"></script> <script src="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/js/bootstrap.min.js"></script> </head> {% endblock %} {% block nav %} <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container align-self-end"> <a class="navbar-brand" href="/"><i class="fa fa-bbs"></i>狗子学院</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navHome" aria-controls="navHome" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navHome"> <ul class="navbar-nav mr-auto"> <li class="nav-item dropdown mr-5"> <a class="nav-link dropdown-toggle" href="#" id="talk" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-fire mr-1"></i>大食堂</a> <div class="dropdown-menu" aria-labelledby="talk"> <a class="dropdown-item" href="#">杂谈</a> <a class="dropdown-item" href="#">趣事</a> <a class="dropdown-item" href="#">表白</a> </div> </li> <li class="nav-item dropdown mr-5"> <a class="nav-link dropdown-toggle" href="#" id="talk" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-shopping-cart mr-1"></i>便利店</a> <div class="dropdown-menu" aria-labelledby="talk"> <a class="dropdown-item" href="#">寻物</a> <a class="dropdown-item" href="#">咸鱼</a> <a class="dropdown-item" href="#">活动</a> </div> </li> <li class="nav-item dropdown mr-5"> <a class="nav-link dropdown-toggle" href="#" id="talk" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-delicious mr-1"></i>组织</a> <div class="dropdown-menu" aria-labelledby="talk"> <a class="dropdown-item" href="#">学院</a> <a class="dropdown-item" href="#">社团</a> <a class="dropdown-item" href="#">圈子</a> </div> </li> </ul> <form class="form-inline my-2 my-md-0"> <input class="form-control" type="text" placeholder="请输入关键字" aria-label="Search" required> </form> </div> </div> </nav> {% endblock %} {% block content %} {% endblock %} {% block footer %} <footer class="container-fluid mt-4 py-0 bg-dark"> <div class="card-body text-center px-0 f-14"> <p class="card-text mb-1">Copyright&nbsp;©&nbsp;<span>2020</span> <a href="http://2dogz.cn/" target="_blank" title="官网">University BBS</a>&nbsp;Design&nbsp;by&nbsp;Flask1.01. </p> </div> </footer> {% endblock %} {% block script %} <script> $(function () { $('[data-toggle="tooltip"]').tooltip() }) </script> {% endblock %} </html> 代码释义: 我们在base.html 文件中写了几个block块定义,分别是head、nav、title、content、footer以及script。分别用来定义我们的html文件的引用、导航栏、标题、内容、页脚以及js代码块。通过{% block name %},当我们的子模板继承自base.html模板的时候,我们子模板可以重写这个block的定义,当然也可以使用{{ super() }}函数来继承父模板的定义,然后自己定义新的内容。 在head中引用了项目需要引入的框架,比如jQuery、bootstrap4等; 在nav中我们使用bootstrap的导航栏组件轻松完成了顶部导航栏的构建 2.4 主页 我们写完了页面的通用部分之后,就可以在主页中继承该模板了。在bbs/templates/frontend/文件夹中新建index.html文件,并嵌入下面的代码 bbs/templates/frontend/index.html {% extends "frontend/base.html" %} {% from "macro.html" import post_item, render_pagination with context%} {% block title %} 主页 {% endblock %} {% block content %} <main> <div class="container mt-2"> </div> </main> {% endblock %} 我使用extends关键字继承了基模板base.html,然后在content块中写了我们自己需要定义的内容。 在完成了我们主页模板代码之后,我们就需要通过路由将它渲染出来,并显示在用户的网页中。 在bbs目录下新建一个blueprint包,并新增模块index.py, 在其中嵌入如下代码 bbs/blueprint/index.py from flask import Blueprint, render_template index_bp = Blueprint('index_bp', __name__) @index_bp.route('/') @index_bp.route('/index/') def index(): return render_template('frontend/index.html') 首先我们实例化了一个Blueprint对象,然后通过该对象注册了两个路由'/' 、'/index/',并将这两个路由指向视图函数index,在index函数中,我们将我们之前创建的主页文件渲染,然后返回给客户端。 我们在这里注册一个同样的路由,因此我们需要删除__init__.py模块中的index视图函数,同时我们在新建blueprint之后,需要将该蓝图进行注册,__init__.py最新代码如下 from flask import Flask from bbs.blueprint.index import index_bp def create_app(): app = Flask('bbs') register_bp(app) return app def register_bp(app: Flask): app.register_blueprint(index_bp) 此时,我们访问http://127.0.0.1:5000看到的将是如下页面 2.5 错误处理 我们在访问网页的时候,经常会出现404,、500这种错误代码。如果我们使用flask提供的默认错误页面,是下面这种样子,如果用户不小心访问到了,将会一头雾水,不知所措,因此我们需要将特定错误页面进行处理,在用户进入错误页面之后,能对其有效的指引。 flask提供了errorhandle装饰器,能让我们很轻松的处理请求错误。在__init__.py模块中添加新的代码 bbs/__init__.py def create_app(config_name=None):. ... register_error_handlers(app) def register_error_handlers(app: Flask): @app.errorhandler(400) def bad_request(e): return render_template('error/400.html'), 400 @app.errorhandler(403) def forbidden(e): return render_template('error/403.html'), 403 @app.errorhandler(404) def not_found(e): return render_template('error/404.html'), 404 @app.errorhandler(500) def server_error(e): return render_template('error/500.html'), 500 在register_error_handlers函数中,我们分别将对应的错误渲染了对应的错误模板了,因此当用户访问出错的时,页面显示的就是我们自己自定义的错误页面样式了。 在bbs/templates/error/目录中新建对应的模板文件,由于内容一致,这里只展示404页面的代码 bss/templates/error/404.html {% extends "frontend/base.html" %} {% block title %} 页面未找到 {% endblock %} {% block content %} <main> <div class="container mt-2"> <div class="card-body"> <div class="card text-white bg-dark mb-3"> <div class="card-body"> <img src="{{ url_for('static', filename='img/404.png') }}" class="img-fluid d-block mx-auto"> </div> <div class="card-footer text-right"> <a class="btn btn-outline-danger" href="/">返回主页</a> </div> </div> </div> </div> </main> {% endblock %} 代码跟index.html类似,聪明的你应该看得懂,就不做多余的解释了。 然后我们输入一个未定义的路由,404页面就是下面你这个样子啦~~~ 至此,本节的内容就已经全部做完啦~是不是很简单啊~ 教程中的资源文件可以进入我的github仓库下载源代码使用 仓库连接 下一节,我们将开始进行用户注册登录功能的实现啦,尽请期待啦~~~

本节介绍了使用Flask框架开发网站的基本步骤。首先需要储备Python编程语言、Flask框架、Jinja2模板引擎、Python ORM框架、HTML、JS和CSS的基础知识。然后通过命令行启动Flask应用,并使用Blueprint来分割不同功能模块。在通用部分中,使用Jinja2模板继承功能将页眉和页脚抽取为公共部分。在主页中继承通用模板,并通过路由将其渲染出来。处理错误页面时,使用Flask提供的errorhandler装饰器来自定义错误页面。

相关推荐 去reddit讨论

热榜 Top10

...
LigaAI
...
白鲸技术栈
...
eolink
...
Dify.AI
...
观测云
...
ShowMeBug
...
天勤数据

推荐或自荐