一. viewmodel意义体现和filter函数的巧妙应用
我们想要在搜索页面上每一本书都显示作者/出版社/价格
我们在app/view_models/book.py的BookViewModel中已经定义了self.author
,self.publisher
,self.price
, 我们需要将他们用'/'连接起来显示。在Jinja2模版中,做这样的操作比较困难, 我们选择在app/view_models/book.py的BookViewModel中实现:
class BookViewModel:
def __init__(self, book):
self.title = book['title']
self.publisher = book['publisher']
self.author = '、'.join(book['author'])
self.image = book['image']
self.price = book['price']
self.summary = book['summary']
self.pages = book['pages']
@property
def intro(self):
intros = filter(lambda x: True if x else False, [self.author, self.publisher, self.price])
# filter(func, iterable) 是比较好用的python内置函数
return '/'.join(intros)
只要在Jinja2模版中{{ book.intro }}
就可以显示了(search_result.html 28行)
二. 书籍详情页面业务逻辑分析
详情页的业务逻辑:
可以访问网站查看逻辑:http://www.yushu.im/book/9787806579060/detail
三. 实现书籍详情页面
在鱼书搜索结果点击想看的书, 会跳转到详情页面。即book_detail视图函数对应的template页面:
app/web/book.py新增book_detail视图函数:
@web.route('/book/<isbn>/detail')
def book_detail(isbn):
yushu_book = YuShuBook()
yushu_book.search_by_isbn(isbn)
book = BookViewModel(yushu_book.books[0])
return render_template('book_detail.html', book=book, wishes=[], gifts=[])
这里yushu_book.books[0]
对于别人去调用你的代码, 可读性不好。
修改YuShuBook类, 新增first方法:
@property
def first(self):
return self.books[0] if self.total >= 1 else None
yushu_book.books[0]
改为yushu_book.first
四. 模型与模型的关系
实现在详情页中 赠送此书的 逻辑。
我们用新的模型gift
来定义要赠送的书籍, user
定义用户
我们先修改原来的models文件夹的结构:
新增app/models/base.py:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
让book.py的db从base.py导入:
from app.models.base import db
gift.py, user.py也从base.py导入db
user.py:
from app.models.base import db
from sqlalchemy import Column, Integer, String, Boolean, Float
class User(db.Model):
id = Column(Integer, primary_key=True)
nickname = Column(String(24), nullable=False)
phone_number = Column(String(18), unique=True)
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))
from app.models.base import db
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.orm import relationship
class Gift(db.model):
id = Column(Integer, primary_key=True)
launched = Column(Boolean, default=False) # 是否送出去了
user = relationship('User')
uid = Column(Integer, ForeignKey('user.id'))
isbn = Column(String(15), nullable=False)
五. 自定义基类模型
数据库存在假删除
, 在数据库模型中增加status项,来判断是否删除。而不是直接物理删除。 这样的好处是: 当我们要分析用户行为时,就能派上用场
.
在app/models/base.py中新增:
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger
db = SQLAlchemy()
class Base(db.Model):
status = Column(SmallInteger) # 是否删除
在book.py, gift.py, user.py中, 统一从继承db.Model
改为Base
但是运行会报错:
sqlalchemy.exc.ArgumentError: Mapper Mapper|Base|base could not assemble any primary key columns for mapped table 'base'
sqlalchemy会尝试创建Base表, 但我们只想让他作为基类:
修改base.py:
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger
db = SQLAlchemy()
class Base(db.Model):
__abstract__ = True # 这样sqlalchemy就不会去创建Base的表了
status = Column(SmallInteger)
六. 用户注册
我们新增用户系统
统一写在app/web/auth.py中:
from .blueprint import web
from flask import render_template, request
__author__ = 'cannon'
@web.route('/register')
def register():
return render_template('auth/register.html', form={'data': {}})
运行flask, 访问register的url, 我们可以看到:
我们只是用http的get
方法获取了网页, 接下来需要使用post
方法, 对昵称、邮箱、密码提交进入数据库。
七.Python的动态赋值
我们在app/forms下新建auth.py:
from wtforms import Form, StringField, IntegerField, PasswordField
from wtforms.validators import Length, NumberRange, DataRequired, Email
class RegisterForm(Form):
nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少两个字符,最多10个字符')])
email = StringField(validators=[DataRequired(), Length(min=8, max=64), Email(message='电子邮件格式错误')])
password = PasswordField(validators=[Length(min=6, max=32), DataRequired(message='密码不能为空')])
修改视图函数app/web/auth.py的register函数:
@web.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if request.method == 'POST' and form.validate(): # 通过验证,则存入数据库
user = User()
user.nickname = form.nickname.data
user.password = form.password.data
user.email = form.email.data
return render_template('auth/register.html', form={'data': {}})
如果user和form有很多属性需要赋值,这样的代码显然不合适。
修改app/models/base.py,新增set_attrs方法:
class Base(db.Model):
__abstract__ = True
status = Column(SmallInteger)
def set_attrs(self, attrs_dict):
for key, value in attrs_dict.items():
if hasattr(self, key) and key != 'id':
setattr(self, key, value)
利用set_attrs方法,优化register函数:
@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) # form.data得到form所有参数数据的dict形式
return render_template('auth/register.html', form={'data': {}})
发现一个问题:
form中有password,但User模型中没有。
我们可以直接在User中增加password吗?
答案是: 不可以. 我们数据库中保存的密码不能为明文。写入前需要加密
八. password加密处理
我们在app/models/user.py中,对password进行加密,而register视图函数不做修改:
app/models/user.py:
from app.models.base import Base
from sqlalchemy import Column, Integer, String, Boolean, Float
from werkzeug.security import generate_password_hash, check_password_hash
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', String(128), nullable=False) # 数据库表的字段名默认为为_password, 这里手动设置为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))
@property
def password(self):
return self._password
@password.setter
def password(self, raw):
self._password = generate_password_hash(raw)
def check_password(self, raw): # 检查传入密码是否正确
return check_password_hash(self._password, raw)
九. ORM的方式保存模型
在app/web/auth.py的register视图函数中,orm的方式把数据存入数据库:
并且我们得把form传给模版,让页面把用户输入的错误信息显示出来
@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) # form.data得到form所有参数数据的dict形式
db.session.add(user) # 将数据保存进数据库
db.session.commit()
return render_template('auth/register.html', form=form) # 将form传给模版
十. 自定义验证器
数据库中已有email, 以后昵称等 验证情况flask并没有内置, 我们需要自定义
修改app/forms/auth.py:
from wtforms import Form, StringField, IntegerField, PasswordField
from wtforms.validators import Length, NumberRange, DataRequired, Email, ValidationError
from app.models.user import User
class RegisterForm(Form):
nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少两个字符,最多10个字符')])
email = StringField(validators=[DataRequired(), Length(min=8, max=64), Email(message='电子邮件格式错误')])
password = PasswordField(validators=[Length(min=6, max=32), DataRequired(message='密码不能为空')])
# 自定义验证器
def validate_email(self, field):
'''
validate_email函数名中的email,会帮助定位到email,表名这个验证器是email的验证器
:param field: field代表传入的email参数
:return:
'''
if User.query.filter_by(email=field).first(): # filter_by可以传入一组查询条件
raise ValidationError('电子邮件已被注册') # validators特有的异常
def validate_nickname(self, field):
if User.query.filter_by(nickname=field).first():
raise ValidationError('该昵称已被注册')
十一. redirect重定向
在注册完成后,需要自动跳转到login登录页面。 我们可以在视图函数中使用redirect重定向
app/forms/auth.py:
from .blueprint import web
from flask import render_template, request, redirect, url_for
from app.forms.auth import RegisterForm, LoginForm
from app.models.user import User
from app.models.base import db
__author__ = 'cannon'
@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 redirect(url_for('web.login')) # 注册完成后重定向, 原理是修改location的信息,即url_for得到的url
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(): # 通过验证,则存入数据库
user = User.query.filter_by(email=form.email.data).first
if user and user.check_password(form.password.data): # 检查密码是否正确
pass
else:
flash('账号不存在或密码错误')
return render_template('auth/login.html', form=form)
我们在app/forms/auth.py中增加LoginForm:
class LoginForm(Form):
email = StringField(validators=[DataRequired(), Length(min=8, max=64), Email(message='电子邮件格式错误')])
password = PasswordField(validators=[Length(min=6, max=32), DataRequired(message='密码不能为空')])
十二. cookie
用户系统的逻辑:
‘票据’就是cookie
cookie的应用举例:
1.浏览一个需要登录的网站,保存密码后, 下次浏览该网站会自动登录
2.在京东搜索一个电饭煲, 之后浏览腾讯视频时,会给你电饭煲的推送广告。
简单使用:
response = make_response('设置cookie')
response.set_cookie('name', 'cannon', 100) # 参数key,value,过期时间100s
如果不设置有效期,cookie在浏览器关闭就会消失。
十三. login_user 实现用户登录
先pip install flask_login
, 修改app/init.py:
from flask import Flask
from app.models.base import db
from flask_login import LoginManager # 导入login
login_manager = LoginManager()
def create_app():
app = Flask(__name__)
app.config.from_object('app.secure')
app.config.from_object('app.setting')
register_blueprint(app)
db.init_app(app)
db.create_all(app=app)
login_manager.init_app(app) # 初始化login_manager
return app
def register_blueprint(app):
from app.web.blueprint import web
app.register_blueprint(web)
修改app/web/auth.py login视图函数:
from flask_login import login_user
@web.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm(request.form)
if request.method == 'POST' and form.validate(): # 通过验证,则存入数据库
user = User.query.filter_by(email=form.email.data).first
if user and user.check_password(form.password.data):
login_user(user, remember=True) # 设置remember=True则默认保存365天, 没有remember则是一次性cookie
# 实质依然是把 user数据保存在cookie中
else:
flash('账号不存在或密码错误')
return render_template('auth/login.html', form=form)
我们可以才配置文件中对cookie进行设置(可以访问www.pythondoc.com/flask-login查阅):
user是我们自定义的数据结构, login_user是如何判断选择数据来存入cookie呢?
我们需要在User模型中定义get_id函数
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', String(128), nullable=False)
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
def password(self):
return self._password
@password.setter
def password(self, raw):
self._password = generate_password_hash(raw)
def check_password(self, raw):
return check_password_hash(self._password, raw)
def get_id(self): # 给flask_login插件使用
return self.id
除了get_id以外, flask_login模块还有很多有用需要在model中定义的特定名称的函数, 如果要一一写一遍会很麻烦。有个简单方法: 使用UserMixin
app/models/user.py:
from app.models.base import Base
from sqlalchemy import Column, Integer, String, Boolean, Float
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin # 导入
class User(UserMixin, Base): #继承UserMixin
id = Column(Integer, primary_key=True)
nickname = Column(String(24), nullable=False)
phone_number = Column(String(18), unique=True)
_password = Column('password', String(128), nullable=False)
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
def password(self):
return self._password
@password.setter
def password(self, raw):
self._password = generate_password_hash(raw)
def check_password(self, raw): # 检查传入密码是否正确
return check_password_hash(self._password, raw)
假如你想让cookie不是通过id验证身份, 依然要重写get_id函数,get_id函数默认使用的是id来做身份数据的。
十四. 登录才能访问
如何对一些视图函数做权限控制,比如需要用户登录才能访问的视图函数。 app/web/gift.py:
from .blueprint import web
from flask_login import login_required # 使用login_required
__author__ = 'cannon'
@web.route('/my/gifts')
@login_required
def my_gifts():
pass
同时需要在user模型中增加必要的代码:
app/models/user.py:
from app.models.base import Base
from sqlalchemy import Column, Integer, String, Boolean, Float
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from app import login_manager # 导入之前在create_app中注册的login_manager
class User(UserMixin, Base):
id = Column(Integer, primary_key=True)
nickname = Column(String(24), nullable=False)
phone_number = Column(String(18), unique=True)
_password = Column('password', String(128), nullable=False)
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
def password(self):
return self._password
@password.setter
def password(self, raw):
self._password = generate_password_hash(raw)
def check_password(self, raw):
return check_password_hash(self._password, raw)
@login_manager.user_loader
def get_user(uid): # 给flask_login 的login_required用得
return User.query.get(int(uid)) # 通过id返回用户模型
十五. 重定向需求
需求一:
现在在没有登录的情况下,访问@login_required的视图函数会报错。
这不是我们想要的,我们想要的是自动跳转到登录页面。
我们可以在create_app中增加login_manager.login_view = 'web.login'
来实现。: app/init.py:
from flask import Flask
from app.models.base import db
from flask_login import LoginManager
login_manager = LoginManager()
def create_app():
app = Flask(__name__)
app.config.from_object('app.secure')
app.config.from_object('app.setting')
register_blueprint(app)
db.init_app(app)
db.create_all(app=app)
login_manager.init_app(app)
login_manager.login_view = 'web.login' # 定义 没登录情况下,访问@login_required的视图函数 的重定向url
return app
def register_blueprint(app):
from app.web.blueprint import web
app.register_blueprint(web)
现在没有登录的情况下访问 my_gifts视图函数, 会跳转到login界面:
可以看到,在跳转后,flask_login会自动闪现Please log in to access this page.
, 我们不想要英文提示, 所以得自定义flash的内容。
同样需要在create_app函数中增加内容:
def create_app():
app = Flask(__name__)
app.config.from_object('app.secure')
app.config.from_object('app.setting')
register_blueprint(app)
db.init_app(app)
db.create_all(app=app)
login_manager.init_app(app)
login_manager.login_view = 'web.login'
login_manager.login_message = '请先登录或注册' # 重定义flash的内容
return app
需求二:
跳转到login页面, 登录完成后, 我们需要再自动跳转到 login之前的页面。这要才有较好的用户体验。
修改login视图函数:
@web.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm(request.form)
if request.method == 'POST' and form.validate(): # 通过验证,则存入数据库
user = User.query.filter_by(email=form.email.data).first
if user and user.check_password(form.password.data):
login_user(user)
next = request.args.get('next') # 获取之前页面的url
if not next: # 如果没有之前页面的情况得考虑到
next = url_for('web.index')
return redirect(next)
else:
flash('账号不存在或密码错误')
return render_template('auth/login.html', form=form)
十六. 重定向攻击
如果人为的给login的url添加next参数,比如:localhost:5000/login?next=http://www.qq.com
,
我们在login界面点击登录后, 会自动跳转到腾讯的网站。这是很危险的。
解决办法也很简答, 依然通过修改login视图函数实现:
@web.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm(request.form)
if request.method == 'POST' and form.validate():
user = User.query.filter_by(email=form.email.data).first
if user and user.check_password(form.password.data):
login_user(user)
next = request.args.get('next')
if not next or next.startwith('/'): # next不是以'/'开头, 就强行跳转到index主页面
next = url_for('web.index')
return redirect(next)
else:
flash('账号不存在或密码错误')
return render_template('auth/login.html', form=form)