一 、用户注册
非关键代码、重复的代码,直接复制并解释。
1.1 建立用户系统
赠送此书的业务逻辑,应该怎么完成?
同时该业务逻辑又涉及到用户user这个模型。因此,应该建立一套用户系统。即常见的登录、注册、找回密码、修改密码等机制。
1.2 编写注册页面
在视图函数app|web|auth.py中:
pyfrom flask import render_template
from . import web
@web.route('/register')
def register():
return render_template('auth/register.html')
-->
❌sqlalchemy.exc.ArgumentError: Mapper mapped class Base->base could not assemble any primary key columns for mapped table 'base' # 基类模板在创建base这个数据表时,没有找到主键primary key这是由于默认下,app|models下面的每一个类均会创建一个数据表。不让该类创建数据表即可。
改正:
在app|models|base.py中:
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import SmallInteger, Column, Integer
db = SQLAlchemy()
class Base(db.Model): # 让base是一个基类,同时不去创建表
__abstract__ = True # 告诉SQL alchemy,我不想创建一个表数据
status = Column(SmallInteger, default=1) 在浏览器中访问http://localhost:81/register:注册页面访问成功。
在哪里接受和处理用户提交的三个信息?
直接在视图函数register()中处理以下两种不同的请求:
(1)返回注册页面---------------------------------------get 请求
(2)处理注册页面所提交来的用户信息-----------post 请求
from flask import render_template, request
from . import web
@web.route('/register', methods=['GET', 'POST']) # 视图函数默认只能处理get,所以要加上一个post动词
def register():
request.form # 拿到post提交的表单信息
return render_template('auth/register.html', form={'data':{}})二、利用Python动态语言特性,简化赋值、统一赋值
2.1 校验post提交的表单信息
拿到post提交的表单信息之后,就要进行校验。
在验证层中,新建一个目录:
在app|forms|auth.py中:
from wtforms import Form, StringField, PasswordField
from wtforms.validators import Length, Email, DataRequired
class RegisterForm(Form): # 对注册的表单信息进行验证
email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮箱不符合规范')])
nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')])
password = PasswordField(validators=[DataRequired('密码不可以为空,请输入你的密码'), Length(6, 32)])2.2 利用基类模型,在视图函数中简化赋值、统一赋值
在app|models|base.py中:
用户提交过来的信息本项目中只有3条,可以直接在视图函数register中 赋值。但是,如果以后项目中用户提交过来的信息非常多呢?显然一个一个赋值不太现实。因此,为简化赋值,统一赋值,在基类模板中新定义一个方法set_attrs()。
原理则是充分利用Python动态语言的特性:相同的覆盖,不相同的保留。
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import SmallInteger, Column, Integer
db = SQLAlchemy()
class Base(db.Model):
__abstract__ = True
status = Column(SmallInteger, default=1)
def set_attrs(self, attrs_dict): # 简化赋值,统一赋值
'''
遍历用户注册时传入的字典数据,如果有key与数据表模型中某一个属性相同,那么就将字典的key所对应的值,赋给模型的相关属性
'''
for key, value in attrs_dict.items():
if hasattr(self, key) and key != 'id': # 判断当前的对象,是否也包含名字为key的属性 # id号不能被赋值
setattr(self, key, value) # 动态赋值在视图函数app|web|auth.py中:
from flask import render_template, request
from . import web
from ..forms.auth import RegisterForm
from ..models.user import User
@web.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form) # 调用上面刚建好的类,并实例化一个对象
if request.method == 'POST' and form.validate(): # 当只有post提交信息,且通过了validate校验,才执行后续
user = User() # 在数据库中,实例化一条新用户的注册信息
user.set_attrs(form.data) # 调用上面刚建好的方法set_attrs() # form.data:包含了从客户端提交过来的所有参数
return render_template('auth/register.html', form={'data':{}})三、Python的属性描述符实现getter、setter
3.1 问题
user模型下面,压根儿就没有定义过属性password。同时,
用户传过来的密码是明文的,但是要保存到数据库中就必须对明文密码进行加密。
所以,上节所写不合理。
3.2 加密用户提交明文密码的两种方案
方案1:
机械呆板,不合理。因为数据库中不能以明文的形式保存密码。即使是后续对其进行了加密并覆盖的操作,也不行。
在app|models|user.py中:
from sqlalchemy import Column, Integer, String, Boolean, Float
from app.models.base import db
from app.models.base import db, Base
class User(Base): # 下面是其一些属性,类变量
id = Column(Integer, primary_key=True)
nickname = Column(String(24), nullable=False)
phone_number = Column(String(18), unique=True)
password = Column() # 定义一个password的属性
email = Column(String(50), unique=True, nullable=False)
confirmed = Column(Boolean, default=False)
beans = Column(Float, default=0)
send_counter = Column(Integer, default=0)
receive_counter = Column(Integer, default=0)
wx_open_id = Column(String(50))
wx_name = Column(String(32))在视图函数app|web|auth.py中: 完成先加密,再覆盖的操作。
from flask import render_template, request
from . import web
from ..forms.auth import RegisterForm
from ..models.user import User
from werkzeug.security import generate_password_hash # 导入这个函数,用于加密密码
@web.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if request.method == 'POST' and form.validate():
user = User()
user.set_attrs(form.data) # 将用户提交来的表单信息,全部赋值给数据表模型,此时密码是明文的!
user.password = generate_password_hash(form.password.data) # 对密码属性先加密,再覆盖上面
return render_template('auth/register.html', form={'data':{}})方案2:
其他用户提交过来的信息,都统一存储到了user模型中。对有特殊要求的个别数据:
(1)将属性的名字改成_password。因为属性变量名已改,所以在统一动态赋值的时候,用户提交过来的密码明文是不会存储到user模型中去的。
(2)为了对个别数据进行多一步操作,新定义以下两个函数用于预处理,即加密。
在app|models|user.py中:
from sqlalchemy import Column, Integer, String, Boolean, Float
from werkzeug.security import generate_password_hash
from app.models.base import db, Base
class User(Base):
# __tablename__ ='user1'
id = Column(Integer, primary_key=True)
nickname = Column(String(24), nullable=False)
phone_number = Column(String(18), unique=True)
_password = Column('password', String(128)) # 将下面刚定义好、已预先处理的的属性传进来
email = Column(String(50), unique=True, nullable=False)
confirmed = Column(Boolean, default=False)
beans = Column(Float, default=0)
send_counter = Column(Integer, default=0)
receive_counter = Column(Integer, default=0)
wx_open_id = Column(String(50))
wx_name = Column(String(32))
@property # 将函数变为默认的只读属性 getter
def password(self):
return self._password # 最终,返回下面已加密过的_password
@password.setter # 首先,将password属性变为只写属性setter
def password(self, raw): # 将原始密码传进来
self._password = generate_password_hash(raw)在视图函数app|web|auth.py中:
就不用在视图函数中操作先加密,再覆盖了。密码加密的操作已经封装到了上面的user模型中了。视图函数表面上仍然风轻云淡、没有变化。
from flask import render_template, request
from . import web
from ..forms.auth import RegisterForm
from ..models.user import User
from werkzeug.security import generate_password_hash
@web.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if request.method == 'POST' and form.validate():
user = User()
user.set_attrs(form.data)
return render_template('auth/register.html', form={'data':{}})两种方案的比较
| 方案1 | 方案2 | |
|---|---|---|
| 操作方式 | 先将用户提交来的表单信息,全部赋值给数据表模型, 然后在视图函数中对password个别属性加密并覆盖掉上面 | 将数据表模型的属性名字改成_password先对个别数据进行预处理加密 然后将结果同其他大部队一道,赋值给 _password |
| 思维 | 先整体,后特殊 | 先特殊,后整体 |
如果想让类下面的某一个变量,是只读的,或者是只写的,那么就可以用上面提到的setter、getter。
四、用ORM的方式保存模型
4.1 简单的对象-关系映射ORM
1.用户的注册信息最终要添加到数据库中去。但是,如何通过模型的方式实现呢?
在视图函数app|web|auth.py中:
from flask import render_template, request
from . import web
from ..forms.auth import RegisterForm
from ..models.user import User
from werkzeug.security import generate_password_hash
@web.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if request.method == 'POST' and form.validate():
user = User()
user.set_attrs(form.data)
db.session.add(user) # 将user模型,添加到数据库
db.session.commit() # 提交
return render_template('auth/register.html', form={'data':{}})上面就是用ORM的思想操作数据库:即整个过程,不和数据库打交道,而是通过一个个的模型来完成数据库的操作。
ORM:"对象-关系映射"(Object/Relational Mapping) 的缩写。
2.小故障:Install 'email_validator' for email validation support.
课程没这个问题,是因为WTForms 3.0.1版本问题,从 WTForms 2.3.0开始,电子邮件验证器 Email() 由email-validator的外部库处理。
所以,需要再安装一下email-validator:
3.用户的注册信息成功添加到了数据库中去
在浏览器中:
在Navicat可视化数据表中:
4.2 解决输入非法的邮箱格式时出现的两个问题
1.当输入非法的邮箱格式时,出现两个问题:
- 没有显示邮箱格式异常的错误提示
- 跳转后的页面中,用户输入的信息全部丢失
因为报错的数据、用户填写的信息,都是放在了form里。
所以,要解决上述问题,就要把form也当作要渲染的数据,传入到render_template()函数中。
from flask import render_template, request
from . import web
from ..forms.auth import RegisterForm
from ..models.user import User
from werkzeug.security import generate_password_hash
@web.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if request.method == 'POST' and form.validate():
user = User()
user.set_attrs(form.data)
db.session.add(user)
db.session.commit()
return render_template('auth/register.html', form=form) # 将form传入进去2.在浏览器中,什么也不填写:
成功出现了错误提示
3.在浏览器中,只填写前两个栏目:
刷新之后,用户上次提交的信息还在、没有丢失,成功保存了上次的信息。
五、用自定义验证器校验是否重名
5.1 在具体的业务逻辑中校验参数
问题:
参数校验时,只校验了参数的字面意思,并没有把参数放到具体的业务逻辑中来校验。
例如:Email虽然符合邮箱的规范,但是数据库中已经存在了一个同名的Email呢?nickname同理。
上述业务性质的校验,应该放在验证层里统一校验。
因此,需要自己编写一些验证器,即自定义验证器。
5.2 如何用模型做数据库的查询
用db.session来查询。另外,还有更为便捷的方式:User.query.filter_by()
from wtforms import Form, StringField, PasswordField
from wtforms.validators import Length, Email, DataRequired, ValidationError
from app.models.user import User
class RegisterForm(Form):
email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮箱不符合规范')])
nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')])
password = PasswordField(validators=[DataRequired('密码不可以为空,请输入你的密码'), Length(6, 32)])
def validate_email(self, field): # field:代表从客户端传过来的email参数
'''
自定义一个验证器,验证邮箱是否重名
'''
if User.query.filter_by(email=field.data).first(): # 查询条件 # 用first来触发这个查询:不管查询出多少条,只返回一条
raise ValidationError('电子邮件已被注册') # 特定的异常同名邮箱被报出错误提示:
除了对Email做是否重名的业务逻辑校验,对nickname也要做:同理。
from wtforms import Form, StringField, PasswordField
from wtforms.validators import Length, Email, DataRequired, ValidationError
from app.models.user import User
class RegisterForm(Form):
email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮箱不符合规范')])
nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')])
password = PasswordField(validators=[DataRequired('密码不可以为空,请输入你的密码'), Length(6, 32)])
def validate_email(self, field):
'''
自定义一个验证器,验证邮箱是否重名
'''
if User.query.filter_by(email=field.data).first():
raise ValidationError('电子邮件已被注册')
def validate_nickname(self, field):
'''
自定义一个验证器,验证昵称是否重名
'''
if User.query.filter_by(nickname=field.data).first():
raise ValidationError('昵称已存在') 六、redirect重定向
在用户成功注册了之后,应该跳转到哪个页面去?
注册失败,还是停留在注册页面;
注册成功,应该跳转到登录页面,要求用户再次输入账户、密码,进行登录。这是主流的方式。
在视图函数app|web|auth.py中:
from flask import render_template, request, redirect, url_for
from . import web
from ..forms.auth import RegisterForm, LoginForm
from ..models.base import db
from ..models.user import User
@web.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if request.method == 'POST' and form.validate():
user = User()
user.set_attrs(form.data)
db.session.add(user)
db.session.commit()
redirect(url_for('web.login')) # 重定向,跳转到下面刚定义的登录页面视图函数
return render_template('auth/register.html', form=form)
@web.route('/login', methods=['GET', 'POST'])
def login(): # 新定义一个登录页面的视图函数
form =LoginForm(request.form) # 对下面新定义的登录验证层,实例化一个对象
if request.method == 'POST' and form.validate():
pass
return render_template('auth/login.html', form=form)在验证层app|forms|auth.py中:
from wtforms import Form, StringField, PasswordField
from wtforms.validators import Length, Email, DataRequired, ValidationError
from app.models.user import User
class RegisterForm(Form):
email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮箱不符合规范')])
nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')])
password = PasswordField(validators=[DataRequired('密码不可以为空,请输入你的密码'), Length(6, 32)])
def validate_email(self, field):
'''
自定义一个验证器,验证邮箱是否重名
'''
if User.query.filter_by(email=field.data).first():
raise ValidationError('电子邮件已被注册')
def validate_nickname(self, field):
'''
自定义一个验证器,验证昵称是否重名
'''
if User.query.filter_by(nickname=field.data).first():
raise ValidationError('昵称已存在')
class LoginForm(Form): # 新建一个登录的验证层,供在上面的视图函数中实例化使用
email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮箱不符合规范')])
password = PasswordField(validators=[DataRequired('密码不可以为空,请输入你的密码'), Length(6, 32)]) 












Comments | NOTHING