第十二章 python与flask的结合应用(2/2)



八、 编写向赠送者请求此书

编写书籍详情页面的“向他请求此书”的业务逻辑:

8.1 成品的网站功能展示

图示关系:

8.2 编写交易模型Drift

新建目录:

app|models|drift.py中:

from sqlalchemy import Column, Integer, String
from app.models.base import Base


class Drift(Base):
    '''一次具体的交易信息'''
    id = Column(Integer, primary_key=True)

    # 邮寄信息
    recipient_name = Column(String(20), nullable=False)
    address = Column(String(100), nullable=False)
    message = Column(String(200))
    mobile = Column(String(20), nullable=False)
    
    # 书籍信息
    isbn = Column(String(13))
    book_title = Column(String(50))
    book_author = Column(String(30))
    book_img = Column(String(50))

    # 请求者的信息
    requester_id = Column(Integer)
    requester_nickname = Column(String(20))

    # 赠送者信息
    gifter_id = Column(Integer)
    gift_id = Column(Integer)
    gifter_nickname = Column(String(20))

思考题:

  • 如何设计交易状态?
  • 所有信息均是平铺的,没有模型关联。这么写有什么好处?

8.3 合理利用数据冗余记录历史状态

比较模型关联重复记录信息
gift模型中:
user = relationship('User')
drift模型中:
如上所述,全部重复记录
特点实时:一变则变两个地方都记录了相同的信息,这叫数据冗余
历史状态的记录
生活场景 淘宝上的交易订单记录,其价格是过去某时间点的价格
日志

数据库设计时,到底是选择模型关联,还是重复记录信息,应根据使用场景确定,各有优劣。

8.4 鱼漂的三个条件检测

对于状态类型,最好使用枚举类型,而不是使用数字代表,后者可读性差。

新建目录:

在自定义模块的枚举类app|libs|enums.py中:

from enum import Enum


class PendingStatus(Enum):
    Waiting = 1
    Success = 2         # pending = PendingStatus.Success +  Success = 2,相比直接 pending = 2要更加易读
    Reject = 3
    Redraw = 4

在模型层app|models|drift.py中:

...

class Drift(Base):
    '''一次具体的交易信息'''
    id = Column(Integer, primary_key=True)

    # 邮寄信息
    recipient_name = Column(String(20), nullable=False)
    address = Column(String(100), nullable=False)
    message = Column(String(200))
    mobile = Column(String(20), nullable=False)

    # 书籍信息
    isbn = Column(String(13))
    book_title = Column(String(50))
    book_author = Column(String(30))
    book_img = Column(String(50))

    # 请求者的信息
    requester_id = Column(Integer)
    requester_nickname = Column(String(20))

    # 赠送者信息
    gifter_id = Column(Integer)
    gift_id = Column(Integer)
    gifter_nickname = Column(String(20))

    pending = Column('pending', SmallInteger, default=1)            # 新定义一个字段,以表示鱼漂的状态

在模型层app|models|gift.py中:

...

class Gift(Base):
    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)          


    def is_yourself_gift(self, uid):                                # 定义一个新的函数,供下面调用   
        '''判断你想要的书,是否是跟你想赠送给别人的书是同一本'''
        return True if self.id == uid else False

在模型层app|models|user.py中:

from math import floor


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

    def can_send_drift(self):                        # 新定义一个函数,以供下面调用
        ''' 判断用户是否能够发送鱼漂'''
        if self.beans < 1:
            return False                                # 把一定不能发送的特殊情况,先列出来

        # 当前用户已成功赠书的数量
        success_gifts_count = Gift.query.filter_by(uid=self.id, launched=True).count()

        # 当前用户已成功索取的书的数量        # 从鱼漂模型中找
        success_receive_count = Drift.query.filter_by(requester_id=self.id,
                                                      pending=PendingStatus.Success).count()

        # 每索取两本书,自己必须赠送一本书          # floor取底板值,取整呗
        return True if \
            floor(success_receive_count / 2) <= floor(success_gifts_count)\
            else False

    
    @property
    def summary(self):                           # 这次不用viewmodel,因为该数据具有一些具体的意义
        return dict(nickname=self.nickname, beans=self.beans, email=self.email,
                    send_receive=str(self.send_counter)+'/'+str(self.receive_counter))

在视图函数app|web|drift.py中:

...

@web.route('/drift/<int:gid>', methods=['GET', 'POST'])
@login_required
def send_drift(gid):
    current_gift = Gift.query.get_or_404(gid)                    # 拿到当前的礼物

    if current_gift.is_yourself_gift(current_user.id):            # 调用上面的is_yourself_gift函数
        flash('这本书是你自己的 :) ,不能向自己索要书籍噢')
        return redirect(url_for('web.book_detail', isbn=current_gift.isbn))

    can = current_user.can_send_drift()                              # 调用上面用户模型层中,刚写好的函数
    if not can:                                                 # 如果鱼豆不够时
        return render_template('not_enough_beans.html', beans=current_user.beans)


    gifter = Gift.user.summary              # 调用上面用户模型中,刚写好的summary属性,以将原始数据,转换为视图数据
    return render_template('drift.html',
                           gifter=gifter, user_beans=current_user.beans)

一般情况下,适配页面的话,定义在viewmodel里。

上述不用viewmodel,是因为数据具有一些具体的意义。再如果,当这几个数据组合在一起的使用频度非常高,也建议定义在模型中。

8.5 完成鱼漂即请求此书

上述是完成了GET请求的逻辑,接下来处理POST的业务逻辑。

在验证层app|forms|book.py中:

class DriftForm(Form):
    '''检测drift的相关表单信息'''
    recipient_name = StringField(validators=[DataRequired(), Length(min=2, max=20, message='收件人姓名长度必须在2到20个字符之间')])
    mobile = StringField(validators=[DataRequired(), Regexp('^1[0-9]{10}$', 0, '请输入正确的手机号')])       # 正则验证手机号
    message = StringField()
    address = StringField(validators=[DataRequired(), Length(min=10, max=70, message='地址还不到10个字吗?尽量写详细一些吧')])

在视图函数app|web|drift.py中:

...


@web.route('/drift/<int:gid>', methods=['GET', 'POST'])
@login_required
def send_drift(gid):
    current_gift = Gift.query.get_or_404(gid)                # 拿到当前的礼物
    if current_gift.is_yourself_gift(current_user.id):
        flash('这本书是你自己的 :) ,不能向自己索要书籍噢')
        return redirect(url_for('web.book_detail', isbn=current_gift.isbn))

    can = current_user.can_send_drift()                      # 调用下上面刚写好的函数
    if not can:                                              # 如果鱼豆不够时
        return render_template('not_enough_beans.html', beans=current_user.beans)

    form = DriftForm(request.form)
    if request.method == 'POST' and form.validate():        

        save_drift(form, current_gift)                      # 调用下方的新函数,以把所有drift的信息保存到数据库中
        send_mail(current_gift.user.email, '有人想要一本书', 'email/get_gift.html', wisher=current_user, gift=current_gift)

    gifter = current_gift.user.summary                      # 原始数据,转换为视图数据
    return render_template('drift.html',
                           gifter=gifter, user_beans=current_user.beans, form=form)



def save_drift(drift_form, current_gift):           # 单独提取出来的方法,建议放在视图函数的模块最后
    '''因为drift的信息较多,所以单独编写一个函数用于保存至数据库'''
    with db.auto_commit():
        drift = Drift()

        # 四个邮寄信息
        drift_form.populate_obj(drift)          # 将前者form表单的信息,复制装载到刚实例化的对象后者中     # 前后两者里面的变量名必须一致

        # 请求者的信息
        drift.requester_id = current_user.id
        drift.requester_nickname = current_user.nickname

        # 赠送者信息
        drift.gift_id = current_gift.id
        drift.gifter_id = current_gift.user.id
        drift.gifter_nickname = current_gift.user.nickname

        # 书籍信息
        # book = current_gift.book.first
        # drift.book_title = book['title']                  # 这里的book是个字典
        book = BookViewModel(current_gift.book)               # 这里的book是个对象
        drift.book_title = book.title                       # 所以,也可以用点的方式赋值
        drift.book_author = book.author
        drift.book_img = book.image
        drift.isbn = book.isbn

        # 鱼豆消耗
        current_user.beans -= 1

        db.session.add(drift)

在书籍详情页面,请求此书:

填写作为索书人的邮寄信息:

书籍拥有者的邮箱会受到一封邮件:

我的鱼豆由1减为了0:

上述是当鱼豆充足时的情况,下面演示下鱼豆为0时的情况:

九、 编写交易记录页面

9.1 查询drift模型,得到交易记录页面

该页面的交易历史记录,是存储在drift表中。如果检索所有的交易记录,就是对drift模型做查询。

在视图函数app|web|drift.py中:

...


@web.route('/drift/<int:gid>', methods=['GET', 'POST'])
@login_required
def send_drift(gid):
    current_gift = Gift.query.get_or_404(gid)              
    if current_gift.is_yourself_gift(current_user.id):
        flash('这本书是你自己的 :) ,不能向自己索要书籍噢')
        return redirect(url_for('web.book_detail', isbn=current_gift.isbn))

    can = current_user.can_send_drift()                  
    if not can:                                          
        return render_template('not_enough_beans.html', beans=current_user.beans)

    form = DriftForm(request.form)
    if request.method == 'POST' and form.validate():        
        save_drift(form, current_gift)                     
        send_mail(current_gift.user.email, '有人想要一本书', 'email/get_gift.html', wisher=current_user, gift=current_gift)
        return redirect(url_for('web.pending'))            # 增加,以返回即将编写的鱼漂交易记录页面
    gifter = current_gift.user.summary                      
    return render_template('drift.html', gifter=gifter, user_beans=current_user.beans, form=form)



@web.route('/pending')
@login_required
def pending():                                        # 鱼漂交易记录页面
    '''一个一个的历史交易记录'''
    drifts = Drift.query.filter(or_(Drift.requester_id == current_user.id, Drift.gifter_id == current_user.id)).order_by(desc(Drift.create_time)).all()

9.2 Drift ViewModel

1.成品的交易记录页面的功能展示:

(1)有可能是“请求者”,也有可能是“向他请求”。

(2)有可能是“等待你邮寄”,也有可能是“等待对方邮寄”。

以上的不同,根源在于用户的身份不同。

真正的产品制作时,建议将两种情况分开,代码写起来简单些。本项目放在一起,是为了多学点知识点。

2.不建议在viewmodel中直接使用current_user

  • 新建目录:

  • 在视图模型app|viewmodels|drift.py中:
from flask_login import current_user

def requester_or_gifter(self, drift):
    '''判断用户是索书人,还是赠书人'''
    if drift.requester_id == current_user.id:           # 不建议在viewmodel中使用current_user
        ...

不建议在viewmodel中直接使用current_user。这是面向对象中类的设计原则。因为破坏了类的封装性,让viewmodel与current_user形成了紧密的耦合。

所以,应该将其作为参数,传递进来。即:

  • 在视图模型app|viewmodels|drift.py中:
def requester_or_gifter(self, drift, current_user_id):            # 作为变量参数,传递进来
    '''判断用户是索书人,还是赠书人'''
    if drift.requester_id == current_user_id:         
        ...

3.编码鱼漂交易记录页面

在视图模型app|viewmodels|drift.py中:

from app.libs.enums import PendingStatus


class DriftCollection:
    '''处理一组'''
    def __init__(self, drifts, current_user_id):
        self.data = []
        self.__parse(drifts, current_user_id)               # 仅调用下下面的方法,下面已经添加赋值了

    def __parse(self, drifts, current_user_id):
        for drift in drifts:                                 # 对每一个drift,都构建一个viewmodel
            temp = DriftViewModel(drift, current_user_id)    # 实例化一个对象
            self.data.append(temp.data)                      # 将对象中的字典,存储添加到上面的空列表中去


class DriftViewModel:
    '''处理单本'''
    def __init__(self, drift, current_user_id):             # 不用一个个定义实例属性了,用一个字典来代表所有的实例属性
        self.data = {}
        self.data = self.__parse(drift, current_user_id)      # 下面定义完了,那么就在构造函数里逐级调用下,并添加

    @staticmethod
    def requester_or_gifter(drift, current_user_id):
        '''判断用户是索书人,还是赠书人'''
        if drift.requester_id == current_user_id:            # 不建议在viewmodel中使用current_user
            you_are = 'requester'
        else:
            you_are = 'gifter'
        return you_are

    def __parse(self, drift, current_user_id):              # 处理原始数据,向viewmodel转化的过程
        you_are = self.requester_or_gifter(drift, current_user_id)
        pending_status = PendingStatus.pending_str(drift.pending, you_are)        # 调用刚在下面定义的枚举类中的新方法
        r = {
            'you_are': you_are,                              # 非直接显示的变量,却是控制显示的间接变量
            'drift_id': drift.id,
            'book_title': drift.book_title,
            'book_author': drift.book_author,
            'book_img': drift.book_img,
            'date': drift.create_datetime.strftime('%Y-%m-%d'),     # 将时间戳转换为年月日
            'operator': drift.requester_nickname if you_are != 'requester' else drift.gifter_nickname,
            'message': drift.message,
            'address': drift.address,
            'status_str': pending_status,                    # 上面的pending_status,过来赋值一下
            'recipient_name': drift.recipient_name,
            'mobile': drift.mobile,
            'status': drift.pending
        }
        return r

在自定义文件app|libs|enums.py中:

from enum import Enum


class PendingStatus(Enum):                        # 枚举类
    Waiting = 1
    Success = 2         
    Reject = 3
    Redraw = 4

    @classmethod
    def pending_str(cls, status, key):            # 定义一个类方法,以供上面调用
        '''用于汇集、取出所有的状态'''
        
        # 双层字典:一个status:对应两个key            # python没有switch case语句,所以通常用字典来模拟。
        key_map = {
            1: {
                'requester': '等待对方邮寄',
                'gifter': '等待你邮寄'
            },
            3: {
                'requester': '对方已拒绝',
                'gifter': '你已拒绝'
            },
            4: {
                'requester': '你已撤销',
                'gifter': '对方已撤销'
            },
            2: {
                'requester': '对方已邮寄',
                'gifter': '你已邮寄,交易完成'
            }
        }
        return key_map[status][key]                    # 最终是要拿到里面的文字

在视图函数app|web|drift.py中:

把上面定义好了,最后在视图函数中的调用,真是简洁啊~

...

@web.route('/pending')
@login_required
def pending():
    '''该页面显示一个一个的历史交易记录'''
    drifts = Drift.query.filter(or_(Drift.requester_id == current_user.id, Drift.gifter_id == current_user.id)).order_by(desc(Drift.create_time)).all()

    views = DriftCollection(drifts, current_user.id)           # 调用上面的视图模型中的类,实例化一个viewmodel   #这里用的是current_user.id,不是current_user_id
    
    return render_template('pending.html', drifts=views.data)  # views.data才是真正的数据

在浏览器中,查看鱼漂即书籍交易页面:

根据下面的成品页面,少了些按钮,后续补。

9.3 对比三种单体与集合的类模式

总结归纳所有的viewmodel

(1)关于book的viewmodel:

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.isbn = book['isbn']
        self.pages = book['pages']
        self.pubdate = book['pubdate']
        self.binding = book['binding']

    @property                   
    def intro(self):                                # 定义实例方法
        intros = filter(lambda x: True if x else False, [self.author, self.publisher, self.price])
        return '/'.join(intros)


class BookCollection():
    ''''处理一组书籍'''
    def __init__(self):                                # 定义构造函数
        self.total = 0
        self.books = []
        self.keyword = ''

    def fill(self, yushu_book, keyword):              # 定义实例方法
        self.total = yushu_book.total
        self.books = [BookViewModel(book) for book in yushu_book.books]
        self.keyword = keyword

(2)关于gift的viewmodel

from app.view_models.book import BookViewModel


class MyGifts:
    '''集合的一组礼物''''
    def __init__(self, gifts_of_mine, wish_count_list):
        self.gifts = []
        self.__gifts_of_mine = gifts_of_mine    
        self.__wish_count_list = wish_count_list
        self.gifts = self.__parse()                           

    def __parse(self):         
        temp_gifts = []
        for gift in self.__gifts_of_mine:
            my_gift = self.__matching(gift)
            temp_gifts.append(my_gift)
        return temp_gifts

    def __matching(self, gift):
        count = 0        
        for wish_count in self.__wish_count_list:
            if gift.isbn == wish_count['isbn']:
                count = wish_count['count']
        r = {                                        # 以字典的形式,承载单体的概念,而不是再定义一个类
            'wishes_count': count,
            'book': BookViewModel(gift.book),
            'id': gift.id
        }                               
        return r

(3)关于drift的viewmodel

from app.libs.enums import PendingStatus


class DriftCollection:
    '''处理一组'''
    def __init__(self, drifts, current_user_id):
        self.data = []
        self.__parse(drifts, current_user_id)           

    def __parse(self, drifts, current_user_id):
        for drift in drifts:                                
            temp = DriftViewModel(drift, current_user_id)        
            self.data.append(temp.data)    


class DriftViewModel:
    '''处理单本'''
    def __init__(self, drift, current_user_id):    
        self.data = {}                                    # 没有在构造函数中,定义所有的字段
        self.data = self.__parse(drift, current_user_id)       

    @staticmethod
    def requester_or_gifter(drift, current_user_id):
        '''判断用户是索书人,还是赠书人'''
        if drift.requester_id == current_user_id:        
            you_are = 'requester'
        else:
            you_are = 'gifter'
        return you_are

    def __parse(self, drift, current_user_id):   
        you_are = self.requester_or_gifter(drift, current_user_id)
        pending_status = PendingStatus.pending_str(drift.pending, you_are)
        r = {                                            # 你上面的构造函数不一个一个定义字段,这那么这里字典就得补上这些字段
            'you_are': you_are,                           
            'drift_id': drift.id,
            'book_title': drift.book_title,
            'book_author': drift.book_author,
            'book_img': drift.book_img,
            'date': drift.create_datetime.strftime('%Y-%m-%d'),   
            'operator': drift.requester_nickname if you_are != 'requester' else drift.gifter_nickname,
            'message': drift.message,
            'address': drift.address,
            'status_str': pending_status,
            'recipient_name': drift.recipient_name,
            'mobile': drift.mobile,
            'status': drift.pending
        }
        return r
对比关于book的viewmodel关于gift的viewmodel关于drift的viewmodel
共性既有单体的一本书,也有集合的一组书既有单体的一本书,也有集合的一组书既有单体的一本书,也有集合的一组书
特点因为用最标准的类来处理,所以写法最典型、最基础、最标准形式上只有集合,没有单体。
实质有单体,隐藏在了字典中
结合了前两个的特点:
兼具字典方便的特性,又可以扩展
优点怎么扩展都可以,直接定义新方法写的代码少自己写起来简单
缺点ps:个人觉得很工整,不麻烦 :)没办法扩展,不能为单体增加一些处理方法别人读起来复杂
推荐最推荐最不推荐不太推荐

以上非特例,基本上很多业务项目都会面临单体与集合的概念。

不只是viewmodel,设计任何对象时,都可以运用上面的三种写法。用最适合自己的。

9.4 更好的使用枚举

在视图函数app|web|drift.py中:

1.pending是数字,Redraw是枚举类。如何拿到Redraw枚举类对应的数字?

@web.route('/drift/<int:did>/redraw')
@login_required
def redraw_drift(did):                                          # did:drift的id号
    '''撤销操作'''
    with db.auto_commit:
        drift = Drift.query.filter(Drift.id == did).first_or_404()
        drift.pending = PendingStatus.Redraw                     # 将默认的状态1改为4


    return redirect(url_for('web.pending'))     # 真实项目中,这里最好用js来写ajax的代码

下面在别人看1、2、3、4时,别人不知道代表什么意思

在自定义文件app|libs|enums.py中:

from enum import Enum

class PendingStatus(Enum):                        # 枚举类
    Waiting = 1
    Success = 2         
    Reject = 3
    Redraw = 4

    @classmethod
    def pending_str(cls, status, key):            
        '''用于汇集、取出所有的状态'''
        key_map = {
            1: {
                'requester': '等待对方邮寄',
                'gifter': '等待你邮寄'
            },
            3: {
                'requester': '对方已拒绝',
                'gifter': '你已拒绝'
            },
            4: {
                'requester': '你已撤销',
                'gifter': '对方已撤销'
            },
            2: {
                'requester': '对方已邮寄',
                'gifter': '你已邮寄,交易完成'
            }
        }
        return key_map[status][key]                    

改为:

from enum import Enum

class PendingStatus(Enum):
    Waiting = 1                       # 视为类变量,下面字典中的status就可以不用数字了
    Success = 2        
    Reject = 3
    Redraw = 4

    @classmethod
    def pending_str(cls, status, key):
        '''用于'''
        # 双层字典:一个status:对应两个key
        key_map = {
            cls.Waiting: {
                'requester': '等待对方邮寄',
                'gifter': '等待你邮寄'
            },
            cls.Reject: {
                'requester': '对方已拒绝',
                'gifter': '你已拒绝'
            },
            cls.Redraw: {
                'requester': '你已撤销',
                'gifter': '对方已撤销'
            },
            cls.Success: {
                'requester': '对方已邮寄',
                'gifter': '你已邮寄,交易完成'
            }
        }
        return key_map[status][key]           # 最终是要拿到状态文字

在模型层app|models|drift.py中:

class Drift(Base):
    '''一次具体的交易信息,建表!'''
    id = Column(Integer, primary_key=True)

    # 邮寄信息
    recipient_name = Column(String(20), nullable=False)
    address = Column(String(100), nullable=False)
    message = Column(String(200))
    mobile = Column(String(20), nullable=False)

    # 书籍信息
    isbn = Column(String(13))
    book_title = Column(String(50))
    book_author = Column(String(30))
    book_img = Column(String(50))

    # 请求者的信息
    requester_id = Column(Integer)
    requester_nickname = Column(String(20))

    # 赠送者信息
    gift_id = Column(Integer)
    gifter_id = Column(Integer)
    gifter_nickname = Column(String(20))

    _pending = Column('pending', SmallInteger, default=1)            # 变为私有变量    # 表示鱼漂的状态,是个数字

    @property
    def pending(self):                              # 目的:方法转换成属性
        '''将数字转换为枚举类型'''
        return PendingStatus(self._pending)         # 类似类的实例化,以返回的是枚举类型

    @pending.setter
    def pending(self, status):                      # status:传进来的是上面的枚举类型
        '''将上面的得到的属性:枚举类型转换为数字类型'''
        self._pending = status.value
此处,为什么要有setter?

在浏览器中,查看鱼漂即书籍交易页面,成功显示了状态的按钮:

9.5 动态语言与开发人员的素养

动态语言不易维护、不易读,非常的乱?

动态语言只是给了你一种写出随意代码的能力,更轻松。但是这并不代表随便、胡乱写代码,该遵守的编程素养还是要遵守。

动态语言写的非常快,但是过段时间就不知道自己写的是什么了。这不是语言问题,而是开发人员的素养问题。

老师:

动态语言比静态语言难很多。建议第一门语言,是java或C#。

9.6 超权现象防范

1.问题

继续编写撤销操作redraw_drift()

在视图函数app|web|drift.py中:

@web.route('/drift/<int:did>/redraw')
@login_required
def redraw_drift(did):                                          
    '''撤销操作'''
    with db.auto_commit:
        drift = Drift.query.filter(Drift.id == did).first_or_404()
        drift.pending = PendingStatus.Redraw                     

        current_user.beans += 1                                 # 将鱼豆还给你

    return redirect(url_for('web.pending'))     

上述业务逻辑编写完了,但是有个严重的安全问题:

一般情况下:

uid1 ---> did1
uid2 ---> did2

但是:

用户1,可以通过修改URL@web.route('/drift/<int:did>/redraw')中的did编号,修改不属于他的鱼漂2。

而必须登录的login_required只能限制必须是登录状态,不能防止超权。

所以,只能增加一个判断条件:发送鱼漂想要书的请求者id,必须与当前用户的id是同一个人。

2.改正

在视图函数app|web|drift.py中:

...

@web.route('/drift/<int:did>/redraw')
@login_required
def redraw_drift(did):                                         
    '''撤销操作'''
    with db.auto_commit:
        drift = Drift.query.filter_by(requester_id=current_user.id, id=did).first_or_404()            # 增加判断条件
        drift.pending = PendingStatus.Redraw                     
        current_user.beans += 1                                 
    return redirect(url_for('web.pending'))  

编写网站,潜意识认为:用户总是会按照给他设计好的一个个的按钮来访问视图函数。但是,每个视图函数总是对应一个URl的。

换句话说,用户是可以自己来编写或伪造一个URL来访问你的视图函数的。

十、完成交易记录页面

10.1 拒绝请求

编写“拒绝”按钮的业务逻辑:

在视图函数app|web|drift.py中:

...

@web.route('/drift/<int:did>/reject')
@login_required
def reject_drift(did):
    '''拒绝操作:只能是书籍的赠送者才能去操作'''
    with db.auto_commit():
        drift = Drift.query.filter(Gift.uid == current_user.id, Drift.id == did).first_or_404()
        drift.pending = PendingStatus.Reject                     # 将默认的状态1改为3

        requester = User.query.get_or_404(drift.requester_id)
        requester.beans += 1                                 # 将鱼豆还给你
    return redirect(url_for('web.pending'))     # 真实项目中,这里最好用js来写ajax的代码

我向用户“哲欣”,发送了个鱼漂,作为索取方我想要这本书。但是对方不想给我,于是他的操作如下:

同时,我这边是也受到了他的拒绝:

10.2 邮寄成功

上面完成了拒绝的操作,相应的,如果同意赠送给他这本书,并且已经把书邮寄给了书籍索要者的话,那么就需要点击“已邮寄”按钮,已确认完成这次交易:

成品网站的功能展示:

在视图函数app|web|drift.py中:

...

@web.route('/drift/<int:did>/mailed')
@login_required
def mailed_drift(did):
    '''已邮寄的操作'''
    with db.auto_commit():      # 确保所有事务的更改,保证一致性
        drift = Drift.query.filter_by(gifter_id=current_user.id, id=did).first_or_404()
        drift.pending = PendingStatus.Success                               # 将默认的状态1改为2
        current_user.beans += 1

        # 把该礼物的状态,改为已送出
        gift = Gift.query.filter_by(id=drift.gift_id).first_or_404()        # 写法好理解些
        gift.launched = True

        # 心愿已完成         # 就像淘宝你把购物车中的订单支付完了,购物车的商品就要自动消失
        Wish.query.filter_by(isbn=drift.isbn, uid=drift.requester_id, launched=False).update({Wish.launched: True})      # 本质跟上面两行的写法一样

    return redirect(url_for('web.pending'))

我向用户“哲欣”,发送了个鱼漂,作为索取方我想要这本书。对方也想赠送给我这本书,且已邮寄给我,于是他的操作如下:

十一、其他功能

11.1 撤销礼物

在赠送清单和心愿清单的两个页面中,“撤销”按钮的业务逻辑:

在视图函数app|web|gift.py中:

...

@web.route('/gifts/<gid>/redraw')
@login_required
def redraw_from_gifts(gid):
    gift = Gift.query.filter_by(id=gid, launched=False).first_or_404()
    with db.auto_commit():
        current_user.beans -= current_app.config['BEANS_UPLOAD_ONE_BOOK']
        gift.delete()                               # 当所有的模型,遇到删除操作时,都可以这样来编写。可读性好
        # gift.status = 0
    return redirect(url_for('web.my_gifts'))

在模型层app|models|base.py中:

...   

class Base(db.Model):                                
    __abstract__ = True                                
    status = Column(SmallInteger, default=1)          
    create_time = Column('create_time', Integer)         

    def __init__(self):
        self.create_time = int(datetime.now().timestamp())        
        pass

    def delete(self):                                # 在基类模型中,新定义一个方法
        self.status = 0

在上述两个清单中撤销操作时,礼物不能是等待交易的状态:要想撤销操作,必须先把交易完成。

在视图函数app|web|gift.py中:

...

@web.route('/gifts/<gid>/redraw')
@login_required
def redraw_from_gifts(gid):
    gift = Gift.query.filter_by(id=gid, launched=False).first_or_404()
    
    drift = Drift.query.filter_by(gift_id=gid, pending=PendingStatus.Waiting).first()
    if drift:                                                    # 该礼物,如果还在鱼漂交易模型中
        flash('这个礼物正处于交易状态,请先前往鱼漂完成该交易')
    else:     
        with db.auto_commit():
            current_user.beans -= current_app.config['BEANS_UPLOAD_ONE_BOOK']
            gift.delete()                               # 当所有的模型,遇到删除操作时,都可以这样来编写。可读性好
    return redirect(url_for('web.my_gifts'))

在浏览器中,成功的撤销了礼物:

11.2 撤销心愿操作

在视图函数app|web|wish.py中:

...

@web.route('/wish/book/<isbn>/redraw')
@login_required
def redraw_from_wish(isbn):
    '''鱼漂只和gift有关联,与wish是没有关联的'''
    wish = Wish.query.filter_by(isbn=isbn, launched=False).first_or_404()
    with db.auto_commit():
        wish.delete()
    return redirect(url_for('web.my_wish'))

在浏览器中,成功的撤销了心愿:

11.3 向他人赠送书籍

之前编写了作为索书人,向赠送者请求此书;

现在编写,作为赠书人,主动向他人送书。

两者的业务逻辑其实是镜像关系,但是如果镜像编写,相当麻烦。因此,可以尽量重复利用之前的逻辑线。

1.关系图示:

2.简单编码一个函数即可

在视图函数app|web|wish.py中:

...

@web.route('/satisfy/wish/<int:wid>')
@login_required
def satisfy_wish(wid):
    '''向他人赠送此书'''
    wish = Wish.query.get_or_404(wid)           # 要满足别人的心愿,哪个心愿?赋值给变量wish
    gift = Gift.query.filter_by(uid=current_user.id, isbn=wish.isbn).first()        # 我的赠送清单中,有没有这个礼物?
    if not gift:
        flash('你还没有上传此书,'
              '请点击“加入到赠送清单”添加此书。添加前,请确保自己可以赠送此书')
    else:
        send_mail(wish.user.email, '有人想送你一本书', 'email/satisfy_wish.html', wish=wish, gift=gift)
        flash('已向他/她发送了一封邮件,如果他/她愿意接受你的赠送,你将受到一个鱼漂')
    return redirect(url_for('web.book_detail', isbn=wish.isbn))

3.在浏览器中,成功通过邮件的方式,向他人赠送书籍:

以上,成功实现了向他人赠送此书的业务逻辑。

11.4 个人中心页面

在视图函数app|web|main.py中:

@web.route('/personal')
@login_required
def personal_center():                                        # 个人中心的页面视图函数
    return render_template('personal.html', user=current_user.summary)        # 调用的是下面的summary属性

在模型层app|models|user.py中:

class User(Base, UserMixin): 
   ...

    @property
    def summary(self):         # 这次不用viewmodel,因为数据具有一些具体的意义
        return dict(nickname=self.nickname, beans=self.beans, email=self.email,
                    send_receive=str(self.send_counter)+'/'+str(self.receive_counter))

在模板中渲染:

在浏览器中,刚好对应上面的四个信息:

本节编写好了个人中心的业务逻辑,但是,其中的“修改”按钮即关于更改密码的还没有完成。

11.5 修改密码

在视图函数app|web|auth.py中:

@web.route('/change/password', methods=['GET', 'POST'])
@login_required
def change_password():
    form = ChangePasswordForm(request.form)                        # 调用下面刚写好的验证层ChangePasswordForm

    if request.method == 'POST' and form.validate():
        with db.auto_commit():
            current_user.password = form.new_password1.data     # 为什么要加个data?
        flash('密码已更新成功')
        return redirect(url_for('web.personal_center'))
    return render_template('auth/change_password.html', form=form)

在验证层app|forms|auth.py中:

class ChangePasswordForm(Form):
    old_password = PasswordField(validators=[DataRequired()])
    
    new_password1 = PasswordField(validators=[DataRequired(), Length(6, 32, message='密码长度至少需要在6到20个字符串之间'), EqualTo('new_password2', message='再次输入的密码不相同')])

    new_password2 = PasswordField(validators=[DataRequired()])

在浏览器中成功修改了密码:

11.6 限频装饰器

自己编写一个装饰器,用于限制邮件发送的次数。因课程遗漏,且没看懂老师代码,暂放于此。后续待补。

声明:Jerry's Blog|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 第十二章 python与flask的结合应用(2/2)


Follow excellence, and success will chase you.