「Flask实战」鱼书项目实战七

flask鱼书项目实战


实现保存礼物

#web/gitf.py
from flask import current_app

from app.models.base import db
from app.models.sql_gift import Gift
from . import web
from flask_login import login_required, current_user

······

@web.route('/gifts/book/<isbn>')
@login_required
def save_to_gifts(isbn):
    gift = Gift()
    gift.isbn = isbn
    #这里的current_user指代实例化的User对象,通过之前的git_id方法,在flask_login底层实例话的对象
    gift.uid = current_user.id
    current_user.beans += current_app.config['BEANS_UPLOAD_ONE_BOOK']
    db.session.add(gift)
    db.session.commit()

这里的每次添加鱼豆的个数卸载配置文件内,自行添加

#setting.py
BEANS_UPLOAD_ONE_BOOK = 0.5

上面的判断显然是不严谨的,需要添加判断条件,来确认是否将书籍添加到礼物列表中,在sql_user.py下进行判断

from sqlalchemy import Column, Integer, Float, String, Boolean
from werkzeug.security import generate_password_hash, check_password_hash

from app.libs.helper import is_isbn_or_key
from app.models.base import Base, db
from app import login_manager

from flask_login import UserMixin

from app.models.sql_gift import Gift
from app.models.sql_wish import Wish
from app.spider.yushu_book import YuShuBook


class User(Base, UserMixin):
    id = Column(Integer, primary_key=True)
    _password = Column('password', String(128), nullable=False)
    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))

    @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)

    # 新增验证gitf方法,先判断isbn输入是否正确,再判断api中是否存在这个isbn书籍
    def can_save_to_list(self, isbn):
        if is_isbn_or_key(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()
        #Wisth和sql_gift.py模块一样,把类名改一下即可
        wishing = Wish.query.filter_by(uid=self.id, isbn=isbn, launched=False).first()

        if not gifting and not wishing:
            return True
        else:
            return False


@login_manager.user_loader
def get_user(uid):
    return User.query.get(int(uid))

事务与回滚

事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行

这里操作了两个模型,即两个数据表,所以涉及到数据库的事务处理,但是sqlalchemy模块本身就支持事务,因为只有在执行了commit的时候才会一起提交到数据库,否则就不提交,所以这里使用异常处理,如果捕获异常,则释放,否则后面的sql语句也不会被提交,即数据回滚

@web.route('/gifts/book/<isbn>')
@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('这本书已经添加至你的赠送清单或者存在你的心愿清单')

上下文管理器

关于上下文管理器可以看

上面把添加数据库的代码做了事务处理,那么不难想到其他的操作数据库应该也需要相同的处理,这样每一个数据库操作的时候都会添加同样的代码,这里就可以使用上下文管理器来简化操作

#modles/base.py
#这里把导入的SQLAlchemy重新命名为_SQLAlchemy,想不出子类的名字的时候可以这么使用=
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy


#继承_SQLAlchemy,并且创建上下文管理器
class SQLAlchemy(_SQLAlchemy):
    #python提供了简化创建上下文管理器的装饰器,否则需要定义类方法
    @contextmanager
    def auto_commit(self):
        try:
            #这里的yield就是返回出去执行的处理数据库的方法执行完之后执行下面的代码
            yield
            self.session.commit()
        except Exception as e:
            self.session.rollback()
            raise e

#这里需要把db放到子类下面,否则找不到SQLAlchemy()
db = SQLAlchemy()

然后就可以替换刚刚的代码

#web/gitf.py

@web.route('/gifts/book/<isbn>')
@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)
    else:
        flash('这本书已经添加至你的赠送清单或者存在你的心愿清单')

同样的,之前注册的代码

#web/auth.py

@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)

变量的陷阱

这里再测试赠送礼物功能之前,还有几个小问题,第一个是创建时间再base.py中定义了create_time,但是还没有给这个赋值,这里线给他赋值

from datetime import datetime

·····

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())

这里有一个问题,就是既然status可以设置默认值,为什么不把create_time也设置成默认值。
这里这个问题的原因就是,create_time被设置成了类变量,类变量再app启动的时候就已经被赋值成功了,所以如果设置成类变量,所有的create_time都会变成同一个时间,然而,self指向实例,create_time就是指对象被实例化的时间,所以这里要使用self来设置创建时间

启动app测试提交书籍,还是失败,原因是gift.py里的save_to_gift没有返回,再flask中视图函数必须要有返回,否贼就会报错

合理使用Ajax

这里如果使用return redirect(url_for('web.book_detail', isbn=isbn))把他重定向回到详情页面,会刷新一次,用户体验不是特别好,像这种提交数据,但不希望刷新页面的场景,就可以使用Ajax来异步提交,这里的Ajax,,之后补上,先用redirect, orz

添加心愿清单

这块视图函数几乎和save_to_gift一模一样,直接给出代码

#web/wish.py

from flask import current_app, flash, redirect, url_for
from flask_login import login_required, current_user

from app.models.base import db
from app.models.sql_wish import Wish
from . import web


@web.route('/wish/book/<isbn>')
@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))

处理数据

首先再BookViewModle.py中添加两个新的数据,使书籍详情页面显示完整

class BookViewModle:
    def __init__(self,book):
        self.title = book['title']
        self.publisher= book['publisher']
        self.pages= book['pages']
        self.price = book['price']
        self.author = '、'.join(book['author'])
        self.summary = book['summary']
        self.isbn = book['isbn']
        self.image= book['image']
        #新数据
        self.pubdate = book['pubdate']
        self.binding = book['binding']

然后在处理赠送书籍列表,前面说过了,在原始数据和试图数据之前需要有个处理数据的viewmodle,所以在view_modles下创建新的trade.py作为书籍详情页的viewmodle

#view_modles/trade.py

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
        )

然后在视图函数中编写试图

#web/book.py
@web.route('/book/<isbn>/detail')
def book_detail(isbn):
    has_in_gifts = False
    has_in_wishes = False

    # 取书籍详情数据
    yushu_book = YuShuBook()
    yushu_book.search_by_isbn(isbn)
    book = BookViewModle(yushu_book.first)

    if current_user.is_authenticated:
        if Gift.query.filter_by(uid=current_user.id, isbn=isbn, launched=False).first():
            has_in_gifts = True
        if Wish.query.filter_by(uid=current_user.id, isbn=isbn, launched=False).first():
            has_in_wishes = 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_modle = TradeInfo(trade_gifts)
    trade_wishes_modle = TradeInfo(trade_wishes)

    return render_template('book_detail.html', book=book, wishes=trade_wishes_modle, gifts=trade_gifts_modle, has_in_gifts=has_in_gifts, has_in_wishes=has_in_wishes)

要理解这个函数里的一些参数,需要结合模板来看

{% if not has_in_gifts and not has_in_wishes %}
            <div class="col-md-1">
                <a class="btn btn-outline"
                   href="#modal">
                    赠送此书
                </a>
            </div>
            <div style="margin-left:30px;" class="col-md-1">
                <a class="btn btn-outline"
                   href="{{ url_for('web.save_to_wish', isbn=book.isbn) }}">
                    加入到心愿清单
                </a>
            </div>
        {% elif has_in_wishes %}
            <div class="col-md-3">
                <span class="bg-info">已添加至心愿清单</span>
            </div>
        {% else %}
            <div class="col-md-3">
                <span class="bg-info">已添加至赠送清单</span>
            </div>
        {% endif %}

这里时用于区分三种状态,用has_in_wisheshas_in_gifts来区分。

重写基类的filter_by

因为再查询的时候我们要确定这条记录有没有被删除,前面说过了,删除使用标志位status来表示的,但是每条查询都传入标志位, 显然是个废话(因为查询肯定是要查询没有被删除的)所以采用重写filter_by的方法来解决。
首先,filter_by继承了flask_sqlalchemyflask_sqlalchemy继承了sqlalchemyBaseQuery所以定义的Query只需要继承BaseQuery即可

class Query(BaseQuery):
    def filter_by(self, **kwargs):
        if 'status' not in kwargs.keys():
            kwargs['status'] = 1
        #调用父类的filter_by方法,把新的kwargs传入进行查询并返回
        return super(Query, self).filter_by(**kwargs)

然后在db中覆盖这个方法即可

db = SQLAlchemy(query_class=Query)

参与评论