9.1 鱼豆
我们的鱼书有一个经济系统,在上传一本书的时候,将获取0.5个鱼豆。赠送一个本书的时候,再获取1个鱼豆。索要一本书的时候,消耗一个鱼豆,其中赠送和索要书籍是用户之间鱼豆互相加减,上传的时候是系统赠送。
基于上面的规则,我们来编写赠送鱼书的视图函数。
1.判断当前书籍是否可以加入赠送清单
1.如果isbn编号不符合规则,不允许添加 2.如果isbn编号对应的书籍不存在,不允许添加 3.同一个用户,不能同时赠送同一本书籍 4.一个用户对于一本书不能既是赠书者,又是索要者 5.3和4合并成一条,就是一本书必须即不在心愿清单又不在赠书列表里才可以添加
并不是web编程就简单,算法就难,他们都有自己难和简单的地方,对于web编程来说,他不需要算法,数学的支撑。但是需要很强的逻辑思维能力,因为业务一直在变化,需要非常好的抽象能力来适应变化。
models/user.py
def can_save_to_list(self, isbn):
"""
判断可以将书籍加入心愿清单
1.如果isbn编号不符合规则,不允许添加
2.如果isbn编号对应的书籍不存在,不允许添加
3.同一个用户,不能同时赠送同一本书籍
4.一个用户对于一本书不能既是赠书者,又是索要者
5.3和4合并成一条,就是一本书必须即不在心愿清单又不在赠书列表里才可以添加
:param isbn:
:return:
"""
if not is_isbn_or_key(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()
wishing = Wish.query.filter_by(uid=self.id, isbn=isbn, launched=False).first()
return not wishing and not gifting
复制代码
之所以要把这个逻辑判断方法加在models里而不是在form里,是因为编程是活的,要视情况而定,这个can_save_to_list加载models在使用起来更加灵活,复用性更强。
2.添加赠送清单,增加鱼豆
添加赠送清单,增加鱼豆对应了两个数据库操作,如果其中一个在执行过程中失败了,那么另一个也不能提交,这用到了数据库的事务。 给用户添加鱼豆需要获取当前用户,我们可以从flask_login的current_user获取当前用户
@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.add(current_user)
db.session.commit()
except Exception as e:
db.session.rollback()
raise e
else:
flash("这本书以添加进您的赠送清单或已经存在于您的心愿清单,请不要重复添加")
return redirect(url_for('web.book_detail', isbn=isbn))
复制代码
3.添加心愿清单
web/wishs.py
@web.route('/wish/book/')
@login_required
def save_to_wish(isbn):
if current_user.can_save_to_list(isbn):
with db.auto_commit():
wish = Wish()
wish.isbn = isbn
wish.uid = current_user.id
db.session.add(wish)
else:
flash("这本书以添加进您的赠送清单或已经存在于您的心愿清单,请不要重复添加")
return redirect(url_for('web.book_detail', isbn=isbn))
复制代码
4.巧用ajax
上面我们在添加赠送书籍完成之后,由重定向回了书籍详情页面。由于我们之前就是在数据详情页面,做了一次操作以后又重定向回去了,这样的操作时非常浪费服务器资源的。我们可以用ajax异步请求来改善这个问题。
另一个消耗服务器性能的点在于书籍详情页面的模板渲染工作,所以另一种优化方案,就是将页面作为一个静态页面缓存起来,下一次重定向只需要将缓存的页面读取出来返回即可
9.2 contextmanager
1.contextmanager简单讲解
contextmanager可以简化上下文管理器,不需要我们编写__enter__和__exit__函数。他给了我们一个机会,让我们把之前一个不是上下文管理器的类变成一个上下文管理器,而不需要我们去修改这个类的源代码
其中的yield的作用,是中断当前函数执行流程,先去执行yield出去的部分的代码执行流程
下面的代码的作用,在书籍前后自动加上《》
@contextmanager
def book_mark():
print('《', end='')
yield
print('》', end='')
with book_mark():
print('钢铁',end='')
复制代码
2.结合继承,contextmanager,yield,rollback来简化try-except的数据库事务代码
1.我们可以通过contextmanager实现一个上下文管理器,将try-except的代码放在contextmanager里,将具体的业务逻辑代码yield出去 2.SQLAlchemy并没有这个上下文管理器,但是我们可以做一个子类,来扩展他的功能 3.编写子类的时候,命名是非常不好起的,我们可以改变父类的名字,给子类命名为原父类的名字
models/base.py
from flask_sqlalchemy import SQLAlchemy as _SQLAlcmemy
class SQLAlchemy(_SQLAlcmemy):
@contextmanager
def auto_commit(self):
try:
yield
self.session.commit()
except Exception as e:
self.session.rollback()
raise e
复制代码
使用auto_commit的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():
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.add(current_user)
else:
flash("这本书以添加进您的赠送清单或已经存在于您的心愿清单,请不要重复添加")
return "aaa"
复制代码
使用auto_commit的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)
return redirect(url_for('web.login'))
return render_template('auth/register.html', form=form)
复制代码
干货:
1.遇到比较复杂的问题,应该把他单独的分离出来,在一个单独的文件里来编写一些非常简单的源码,因为业务越简单,越能够让我们去关注知识和原理本身的相关问题。 2.高级编程不是在于学习更高级的语法(学会更好),更关键的在于能够用自己所学的知识,写出更好的代码来 3.对知识的综合运用能力很重要,将单个的知识点组合在一起写出一段很好的代码来
9.3 书籍交易视图模型
书籍详情页,除了需要显示书籍详情信息外。还应该显示其他信息,这些信息分为三类 1.默认情况下,显示想要赠送这本书的人的列表,包括名字和上传时间。 2.如果当前用户是此书的赠送者,应该显示索要这本书的人的列表。 3.如果当前用户是此书的索要者,应该显示想要赠送这本书的人的列表。
综上所述,我们一共需要两个列表,这本书的索要人列表和这本书的赠书人的列表,根据不同情况进行展示。
# 赠书人列表和索要人列表
trade_gifts = Gift.query.filter_by(isbn=isbn).all()
trade_wishs = Wish.query.filter_by(isbn=isbn).all()
复制代码
我们在view_model中处理这两个列表的原始数据,加工成我们姓名,上传时间的列表。由于gifts,wishs两个的加工逻辑一样,只是数据库表不一样,所以可以写一个统一的类trade来处理
class TradeInfo:
def __init__(self, goods):
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
)
复制代码
create_time 本是int类型,要进行strftime格式化操作需要转化成string类型,这个操作每个模型都要用到,所以编写在base.py里
@property
def create_datetime(self):
if self.create_time:
return str(self.create_time)
else:
return None
复制代码
接下来完善书籍详情视图函数。区分上面说的三种情况。使用current_user的is_authenticated可以判断用户是否登录。然后分别以当前用户id为查询条件去wish表和gift表里查询,如果能查询到,则将对应的has_in_gifts/has_in_wishs设置为True
@web.route("/book//detail")
def book_detail(isbn):
has_in_gifts = False
has_in_wishs = 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).first():
has_in_gifts = True
if Wish.query.filter_by(uid=current_user.id).first():
has_in_wishs = True
# 赠书人列表和索要人列表
trade_gifts = Gift.query.filter_by(isbn=isbn).all()
trade_wishs = Wish.query.filter_by(isbn=isbn).all()
return render_template("book_detail.html", book=book,
wishes=trade_wishs, gifts=trade_gifts,
has_in_wishs=has_in_wishs, has_in_gifts=has_in_gifts)
复制代码
9.4 重写filter_by
由于我们的删除操作都是逻辑删除,所以在查询的时候应该默认查询status=1的记录(即未删除的记录),但是如果在每一个filter_by里都这么写,就太麻烦了,我们的思路是重写默认的filter_by函数,加上status=1的限制条件。
那么我们就需要先了解原来SQLAlchemy的继承关系 Flask的SQLAlchemy中有一个BaseQuery,BaseQuery继承了orm.Query(原SQLAlchemy的类),这里面有filter_by函数;也就是说BaseQuery通过继承orm.Query拥有了filter_by的能力
flask_sqlalchemy
...
...
class SQLAlchemy(object):
Query = None
def __init__(self, app=None, use_native_unicode=True, session_options=None,
metadata=None, query_class=BaseQuery, model_class=Model):
...
...
class BaseQuery(orm.Query):
...
...
复制代码
orm.Query
def filter_by(self, **kwargs):
# for循环拼接关键字参数查询条件
clauses = [_entity_descriptor(self._joinpoint_zero(), key) == value
for key, value in kwargs.items()]
return self.filter(sql.and_(*clauses))
复制代码
所以如果我们要重写filter_by,需要自己编写子类,继承BaseQuery,重写filter_by函数,将status=1加入到kwargs
class Query(BaseQuery):
def filter_by(self, **kwargs):
if 'status' not in kwargs:
kwargs['status'] = 1
return super(Query, self).filter_by(**kwargs)
复制代码
最后,Flask的SQLAlchemy给了我们一种方法,让我们应用自己的Query类,即在实例化的时候传入关键字参数query_class
db = SQLAlchemy(query_class=Query)
复制代码