新增app/web/gift.py:
from .blueprint import web
from flask_login import login_required, current_user
from app.models.gift import Gift
from app.models.base import db
__author__ = 'cannon'
@web.route('/my/gifts')
@login_required
def my_gifts():
return 'my gifts'
@web.route('/gifts/book/')
@login_required
def save_to_gifts(isbn):
gift = Gift()
gift.isbn = isbn
gift.uid = current_user.id # current_user就是实例化后的user模型, 原因在于models/user的get_user
db.session.add(gift)
db.session.commit()
鱼豆的逻辑:
修改save_to_gifts函数(先在setting.py配置文件中增加BEANS_UPLOAD_ONE_BOOK=0.5的每次上传书籍系统赠送鱼豆个数):
@web.route('/gifts/book/')
@login_required
def save_to_gifts(isbn):
gift = Gift()
gift.isbn = isbn
gift.uid = current_user.id # current_user就是实例化后的user模型, 原因在于models/user的get_user
current_user.beans += current_app.config['BEANS_UPLOAD_ONE_BOOK'] # 增加0.5个鱼豆
db.session.add(gift)
db.session.commit()
save_to_gift函数逻辑非常不严谨, 还有很多问题要处理
1.是否满足isbn规范
2.不允许一个用户同时赠送多本相同的书
3.一个用户不可能同时成为赠送者和索要者
在model/user.py的class User(UserMixin, Base): 中增加一个方法:
def can_save_to_list(self, isbn):
if is_isbn_or_key(isbn) != 'isbn': # 是否满足isbn规范
return False
yushu_book = YuShuBook()
yushu_book.search_by_isbn(isbn)
if not yushu_book.first:
return False
# 不允许一个用户同时赠送多本相同的书
#
gifting = Gift.query.filter_by(uid=self.id, isbn=isbn, launched=False).first() # launched表示 如果有书但没送出去,你也不能上传书
wishing = Wish.query.filter_by(uid=self.id, isbn=isbn, launched=False).first()
if not gifting and not wishing:
return True
else:
return False
其实代码中gifting
和wishing
还有很大的坑filter_by,等我们去填。
在save_to_gifts使用can_save_to_list:
@web.route('/gifts/book/')
@login_required
def save_to_gifts(isbn):
if current_user.can_save_to_list(isbn):
#调用在model中写的校验,而没有去form 写校验。
#这里是用户的行为, model里校验用起来方便。form的校验复用性差
gift = Gift()
gift.isbn = isbn
gift.uid = current_user.id
current_user.beans += current_app.config['BEANS_UPLOAD_ONE_BOOK']
db.session.add(gift)
db.session.commit()
else:
flash('这本书已添加至你的赠送清单或已存在于你的心愿清单,请不要重复添加!')
return redirect(url_for('web.book.detail', isbn=isbn))
gift.uid = current_user.id
突然中断了, 鱼豆并没有加上。这会导致数据的异常。所以我们必须保证,这里gift和user两个表, 要么同时操作,要么都不操作。
在数据库中保证数据一致性的方法称为:事务。
其实sqlalchemy已经有相关机制了(我们代码中也写了):
db.session.add(gift)
db.session.commit()
只有执行了这代码,数据才会提交给数据库。 但我们的逻辑还有点问题,需要修改。
如果执行中出错, 我们得进行回滚:
@web.route('/gifts/book/' )
@login_required
def save_to_gifts(isbn):
if current_user.can_save_to_list(isbn):
try:
gift = Gift()
gift.isbn = isbn
gift.uid = current_user.id
current_user.beans += current_app.config['BEANS_UPLOAD_ONE_BOOK']
db.session.add(gift)
db.session.commit()
except Exception as e:
db.session.rollback() # 数据库回滚
raise e
else:
flash('这本书已添加至你的赠送清单或已存在于你的心愿清单,请不要重复添加!')
return redirect(url_for('web.book.detail', isbn=isbn))
如果程序执行到db.session.commit()
失败了, 没有进行rollback操作,那么不仅仅这次操作会失败 以后所有的数据库插入操作都会失败。
只要执行db.session.commit() 都进行 rollback。 我们接下来会对这样的代码进行优化。
我们以前讲过with上下文机制:
class MyResource:
def __enter__(self):
print('connect to resource')
return self
def __exit__(self, exc_type, exc_value, tb):
print('close resource connection')
def query(self):
print('query data')
with MyResource() as resource:
1/0
resource.query()
代码的核心其实是 query函数, 我们可以使用 @contextmanager来代替:
class MyResource:
# def __enter__(self):
# print('connect to resource')
# return self
#
# def __exit__(self, exc_type, exc_value, tb):
# print('close resource connection')
def query(self):
print('query data')
# with MyResource() as resource:
# 1/0
# resource.query()
from contextlib import contextmanager
@contextmanager
def make_myresource():
print('connect to resource')
yield MyResource()
print('close resource connection')
with make_myresource() as r:
r.query()
print('书名')
, 何如给书名加上‘《》’? 我们可以使用@contextmanager:
from contextlib import contextmanager
@contextmanager
def book_mark():
print('《', end='') # end=''为了去掉换行符
yield # 没有用到 with···as, 所以不需要返回值
print('》', end='')
with book_mark() :
print('书名', end='')
结果为《书名》
接下来解决第三节最后的问题
app/models/base.py:
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger
from contextlib import contextmanager
class SQLAlchemy(_SQLAlchemy): # 给SQLAlchemy添加auto_commit方法
@contextmanager
def auto_commit(self):
try:
yield
self.session.commit()
except Exception as e:
raise e
db = SQLAlchemy()
class Base(db.Model):
__abstract__ = True
create_time = Column('create_time', Integer)
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)
app/web/gift.py的save_to_gifts视图函数:
@web.route('/gifts/book/')
@login_required
def save_to_gifts(isbn):
if current_user.can_save_to_list(isbn):
with db.auto_commit(): # 运用新写的auto_commit上下文
gift = Gift()
gift.isbn = isbn
gift.uid = current_user.id
current_user.beans += current_app.config['BEANS_UPLOAD_ONE_BOOK']
db.session.add(gift)
# db.session.commit()
else:
flash('这本书已添加至你的赠送清单或已存在于你的心愿清单,请不要重复添加!')
return redirect(url_for('web.book.detail', isbn=isbn))
同样的,只要是db.session.commit的地方都改一下
app/web/auth.py的register视图函数:
@web.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if request.method == 'POST' and form.validate():
with db.auto_commit():
user = User()
user.set_attrs(form.data)
db.session.add(user)
# db.session.commit()
return redirect(url_for('web.login'))
return render_template('auth/register.html', form=form)
优化app/models/base.py的Base模型, 使得创建实例变量时自动生成创建时间。:
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger
from contextlib import contextmanager
from datetime import datetime
class SQLAlchemy(_SQLAlchemy):
@contextmanager
def auto_commit(self):
try:
yield
self.session.commit()
except Exception as e:
raise e
db = SQLAlchemy()
class Base(db.Model):
__abstract__ = True
create_time = Column('create_time', Integer)
status = Column(SmallInteger, default=1)
def __init__(self): # 增加__init__函数
self.create_time = int(datetime.now().timestamp()) # 自动生成时间戳
def set_attrs(self, attrs_dict):
for key, value in attrs_dict.items():
if hasattr(self, key) and key != 'id':
setattr(self, key, value)
问题:如果我们不使用__init__,而是在create_time中增加default参数的方法,能自动生成创建时间吗?
答案: 这样所有的实例化的数据库的数据, 创建时间都一样。在flask程序运行初始化的时候,会db.create_all, 这时候就会自动创建类生成时间,而不是实例的生成时间。
我们在save_to_gifts中, 随后会redirect到详情页, 这样的操作其实很奇怪:
1.在详情页点击 赠送此书
2.赠送完成后, 在没有去别的页面的情况下, 又渲染了一遍详情页。
这样的情况,我们就可以使用ajax技术。在点击赠送此书
后, 只会发送请求,而不需要重新渲染原来的页面。
我们会在详情页中显示 赠出书籍的记录:
创建app/view_models/trade.py:
class TradeInfo:
def __init__(self, goods): # 传入wishes或gifts
self.total = 0
self.trades = []
self.__parse(goods)
def __parse(self, goods):
self.total = len(goods)
self.trades = [self.__map_to_trade(single) for single in goods]
def __map_to_trade(self, single):
return dict(
user_name=single.user.nickname,
time=single.create_time.strftime('%Y-%m-%d'), # 存在数据库中的是时间戳,需要转化
id=single.id
)
我们在TradeInfo类中time=single.create_time.strftime('%Y-%m-%d')
的create_time得到的是int的时间戳,无法使用strftime方法。 我们修改app/models/base.py的class Base:
class Base(db.Model):
__abstract__ = True
create_time = Column('create_time', Integer)
status = Column(SmallInteger, default=1)
def __init__(self):
self.create_time = int(datetime.now().timestamp()) # 自动生成时间戳
def set_attrs(self, attrs_dict):
for key, value in attrs_dict.items():
if hasattr(self, key) and key != 'id':
setattr(self, key, value)
@property
def create_datetime(self): # int类型转为时间戳
if self.create_time:
return datetime.fromtimestamp(self.create_time)
else:
return None
修改TradeInfo的__map_to_trade方法:
class TradeInfo:
def __init__(self, goods): # 传入wishes或gifts
self.total = 0
self.trades = []
self.__parse(goods)
def __parse(self, goods):
self.total = len(goods)
self.trades = [self.__map_to_trade(single) for single in goods]
def __map_to_trade(self, single):
if single.create_datetime:
time = single.create_datetime.strftime('%Y-%m-%d'), # 存在数据库中的是时间戳,需要转化
else:
time = '未知'
return dict(
user_name=single.user.nickname,
time=time,
id=single.id
)
修改app/web/book.py的book_detail视图函数:
@web.route('/book//detail')
def book_detail(isbn):
has_in_gift = False # 在礼物清单
has_in_wish = False # 在心愿清单
# 获取书籍详情数据
yushu_book = YuShuBook()
yushu_book.search_by_isbn(isbn)
book = BookViewModel(yushu_book.first)
if current_user.is_authenticated: # 用户是否登录, 没有登录就当不在礼物清单,也不在心愿清单的默认情况处理
if Gift.query.filter_by(uid=current_user.id, isbn=isbn, launched=False).first(): # 用户是否是赠送者
has_in_gift = True
if Wish.query.filter_by(uid=current_user.id, isbn=isbn, launched=False).first(): # 用户是否是索要者
has_in_wish = True
trade_gifts = Gift.query.filter_by(isbn=isbn, launched=False).all()
trade_wishes = Wish.query.filter_by(isbn=isbn, launched=False).all()
trade_gifts_model = TradeInfo(trade_gifts)
trade_wishes_model = TradeInfo(trade_wishes)
return render_template('book_detail.html', book=book, wishes=trade_wishes_model, gifts=trade_gifts_model,
has_in_gift=has_in_gift, has_in_wish=has_in_wish)
在第二节最后,说filter_by有坑。原因是:
我们项目中删除数据库的数据,其实是通过Base的status置为0的软删除方法
这样一来,如果直接使用filter_by会把删除的数据,一起找到。
当然我可以在filter_by中加入参数status=1
,但每次都必须加的话,太不合理了。我们需要改写filter_by
我们使用的filter_by是flask_sqlalchemy的Basequery继承sqlalchemy的orm.query, 在flask_sqlalchemy的初始化的时候,给了我们重写Basequery的机会:
源码SQlAlchemy的__init__:
def __init__(self, app=None, use_native_unicode=True, session_options=None,
metadata=None, query_class=BaseQuery, model_class=Model):
self.use_native_unicode = use_native_unicode
self.Query = query_class
self.session = self.create_scoped_session(session_options)
self.Model = self.make_declarative_base(model_class, metadata)
self._engine_lock = Lock()
self.app = app
_include_sqlalchemy(self, query_class)
if app is not None:
self.init_app(app)
我们自己写一个Query,传给query_class即可
修改app/models/base.py:
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy, BaseQuery
from sqlalchemy import Column, Integer, SmallInteger
from contextlib import contextmanager
from datetime import datetime
class SQLAlchemy(_SQLAlchemy):
@contextmanager
def auto_commit(self):
try:
yield
self.session.commit()
except Exception as e:
raise e
class Query(BaseQuery): # 继承BaseQuery, 并重写filter_by
def filter_by(self, **kwargs):
if 'status' not in kwargs.keys():
kwargs['status'] = 1 # 在**kwargs字典中加入status即可
return super(Query, self).filter_by(**kwargs)
db = SQLAlchemy(query_class=Query) # 传入query_class
class Base(db.Model):
__abstract__ = True
create_time = Column('create_time', Integer)
status = Column(SmallInteger, default=1)
def __init__(self):
self.create_time = int(datetime.now().timestamp())
def set_attrs(self, attrs_dict):
for key, value in attrs_dict.items():
if hasattr(self, key) and key != 'id':
setattr(self, key, value)
@property
def create_datetime(self):
if self.create_time:
return datetime.fromtimestamp(self.create_time)
else:
return None
这样我们使用filter_by的时候就不用传status=1
了。