1. 程式人生 > 程式設計 >Flask 掃盲系列-許可權設定

Flask 掃盲系列-許可權設定

在前面的學習中,我們設定了系統的註冊和登陸功能,已經基本滿足了一個小型 Web 應用的需求。那麼如果我們想通過這個網站來賺些小錢呢,就需要提供更高階的功能,當然這些高階功能不是免費開放的,設計一個許可權系統,來控制高階應用的使用。

撰寫高階功能

所謂的高階功能就是使用者捨得花錢去購買的功能,像我這種喜歡薅羊毛的主,只配用用基礎功能了。

我這裡設計的高階功能,就是豐富 K 線圖,在我們原來 K 線圖的基礎上新增移動平均線和成交量。

移動平均線

移動平均線是技術分析中非常普遍的一項指標,“平均”是指單位週期內的平均收盤價格,“移動”則是指將新的交易日收盤價納入計算週期的同時,剔除最早的交易收盤價。

我們先來觀察下通過 tushare 獲取到的資料

可以看到,資料中的 Ma5、Ma10 和 Ma20 值可以用來製作移動平均線,可以通過折線圖的方式來展現。

import pyecharts.options as opts
from pyecharts.charts import Line


def moving_average() -> Line:
    c = (
        Line()
        .add_xaxis(df.index.tolist())
        .add_yaxis("Ma5",df['ma5'].values.tolist(),is_smooth=True)
        .add_yaxis("Ma10"
,df['ma10'].values.tolist(),is_smooth=True) .add_yaxis("Ma20",df['ma20'].values.tolist(),is_smooth=True) .set_global_opts(title_opts=opts.TitleOpts(title="移動平均線")) .set_series_opts( label_opts=opts.LabelOpts(is_show=False),) ) return c moving_average().render_notebook() 複製程式碼

成交量

對於成交量,可以通過柱狀圖來展示,柱狀圖的高度,就是成交量的大小。把上漲時的成交量顯示成紅色,下跌時的成交量顯示成綠色。

import pyecharts.options as opts
from pyecharts.charts import Line,Bar


volume_rise=[df.volume[x] if df.close[x] > df.open[x] else "0" for x in range(0,len(df.index))]
volume_drop=[df.volume[x] if df.close[x] <= df.open[x] else "0" for x in range(0,len(df.index))]


def volume() -> Bar:
    c = (
        Bar()
        .add_xaxis(df.index.tolist())
        .add_yaxis("volume_rise",volume_rise,stack=True,color=["#ec0000"])
        .add_yaxis("volume_drop",volume_drop,color=["#00da3c"])
        .set_global_opts(title_opts=opts.TitleOpts(title="成交量"),datazoom_opts=[opts.DataZoomOpts()],)
        .set_series_opts(
            label_opts=opts.LabelOpts(is_show=False),)
    )
    return c


volume().render_notebook()
複製程式碼

整合三個圖表

下面我們就把三個圖示,K 線圖,移動平均線圖和成交量圖合成到一起 首先把 K 線圖和移動平均線圖層疊到一起

def kline_base() -> Kline:
    kline = (
        Kline()
        .add_xaxis(df.index.tolist())
        .add_yaxis("日K圖",df[['open','close','low','high']].values.tolist(),markpoint_opts=opts.MarkLineOpts(
            data=[opts.MarkLineItem(type_="max",value_dim="close")]
        ),markline_opts=opts.MarkLineOpts(
            data=[opts.MarkLineItem(type_="max",itemstyle_opts=opts.ItemStyleOpts(
                       color="#ec0000",color0="#00da3c",border_color="#8A0000",border_color0="#008F28",),)
        .set_global_opts(
            yaxis_opts=opts.AxisOpts(is_scale=True,splitarea_opts=opts.SplitAreaOpts(
                    is_show=True,areastyle_opts=opts.AreaStyleOpts(opacity=1)
                ),xaxis_opts=opts.AxisOpts(is_scale=True,axislabel_opts=opts.LabelOpts(rotate=-30)),title_opts=opts.TitleOpts(title="股票走勢"),toolbox_opts=opts.ToolboxOpts(is_show=True),)
    )
    line = (
        Line()
        .add_xaxis(df.index.tolist())
        .add_yaxis("Ma5",)
    )
    kline.overlap(line)
    return kline
複製程式碼

接下來再通過 grid 把成交量圖新增到主圖表中

...
    bar = (
        Bar()
        .add_xaxis(df.index.tolist())
        .add_yaxis("volume_rise",color=["#ec0000"],)
        .add_yaxis("volume_drop",color=["#00da3c"],)
        .set_global_opts(title_opts=opts.TitleOpts(),legend_opts=opts.LegendOpts(pos_right="20%"))
        .set_series_opts(
            label_opts=opts.LabelOpts(is_show=False),)
    )
    
    overlap_kline_line = kline.overlap(line)
    grid = Grid()
    grid.add(
        overlap_kline_line,grid_opts=opts.GridOpts(pos_left="10%",pos_right="8%",height="50%"),)
    grid.add(
        bar,grid_opts=opts.GridOpts(
            pos_left="10%",pos_top="70%",height="16%"
        ),)
...
複製程式碼

至此,我們所謂的“高階”圖表就完成了,下面就開始結合 Flask,嵌入我們產生的圖表

編寫各個圖表頁面

首先我們先把新產生的兩個圖表嵌入到 Web 應用中,每個圖表都是一個獨立的頁面

後臺函式

先來建立生成移動平均線和成交量圖表的函式

# 移動平均線
def moving_average_chart(mydate,data_5,data_10,data_20,name) -> Line:
    moving_average = (
        Line()
        .add_xaxis(mydate)
        .add_yaxis("ma5",is_smooth=True)
        .add_yaxis("ma10",is_smooth=True)
        .add_yaxis("ma20",is_smooth=True)
        .set_global_opts(title_opts=opts.TitleOpts(title="%s-移動平均線" % name),)
    )
    return moving_average


# 成交量
def volume_chart(mydate,name) -> Bar:
    bar = (
        Bar()
        .add_xaxis(mydate)
        .add_yaxis("volume_rise",color=["#00da3c"])
        .set_global_opts(title_opts=opts.TitleOpts(title="%s-成交量" % name),)
    )
    return bar
複製程式碼

然後再修改 get_stock_data 函式,返回我們需要的資料

def get_stock_data(code,ctime):
    df = ts.get_hist_data(code)
    df_time = df[:ctime]
    mydate = df_time.index.tolist()
    kdata = df_time[['open','high']].values.tolist()
    madata_5 = df_time['ma5'].values.tolist()
    madata_10 = df_time['ma10'].values.tolist()
    madata_20 = df_time['ma20'].values.tolist()
    volume_rise = [df_time.volume[x] if df_time.close[x] > df_time.open[x] else "0" for x in range(0,len(df_time.index))]
    volume_drop = [df_time.volume[x] if df_time.close[x] <= df_time.open[x] else "0" for x in range(0,len(df_time.index))]
    return [mydate,kdata,madata_5,madata_10,madata_20,volume_drop]
複製程式碼

接著再增加生成兩個圖表所對應的檢視函式

@app.route("/Line",methods=['GET','POST'])
def get_moving_average():
    stock_name = request.form.get('stockName')
    query_time = request.form.get('queryTime')
    if not stock_name:
        stock_name = '平安銀行'
    if not query_time:
        query_time = 30
    if int(query_time) > 30:
        if current_user.is_authenticated:
            pass
        else:
            abort(403)

    status,stock_code = check_stock(stock_name)
    if status == 0:
        return 'error stock code or name'
    mydate,volume_drop = get_stock_data(stock_code[0],int(query_time))
    c = moving_average_chart(mydate,stock_code[1])
    return c.dump_options()


@app.route("/Bar",'POST'])
def get_volume():
    stock_name = request.form.get('stockName')
    query_time = request.form.get('queryTime')
    if not stock_name:
        stock_name = '平安銀行'
    if not query_time:
        query_time = 30
    if int(query_time) > 30:
        if current_user.is_authenticated:
            pass
        else:
            abort(403)

    status,int(query_time))
    c = volume_chart(mydate,stock_code[1])
    return c.dump_options()
複製程式碼

然後還要新增對應的前端頁面

@app.route("/mavg",'POST'])
def moving_average():
    return render_template("mavg.html")


@app.route("/volume",'POST'])
def volume():
    return render_template("volume.html")
複製程式碼

最後建立上面的兩個 html 檔案,並修改

{% extends "base.html" %}

{% block title %}我的股票走勢圖{% endblock %}


{% block page_content %}
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
     <button type="button" class="close" data-dismiss="alert">&times;</button>
     {{ message }}
 </div>
{% endfor %}
<body>
     <div id="form-div">
         <form id="form1" onsubmit="return false" action="#" method="post">
             <p id="p1">股票名稱:
                 <input name="stockName" type="text" id="stockName" tabindex="1" size="16" value="" placeholder="股票名稱"/>
<!--                 <input type="button" onclick="add1();" value="新增" />-->
             </p>
             <p id="p2">查詢時間:
                 <input name="queryTime" type="text" id="queryTime" tabindex="2" size="16" value="" placeholder="輸入30查詢近30天資料"/>
             </p>
             <p><input type="submit" value="查詢" onclick="getData()"></p>
         </form>
     </div>
    <div id="Bar" style="width:1000px; height:600px;"></div>
    <script>
        $(
            function () {
                var chart = echarts.init(document.getElementById('Bar'),'white',{renderer: 'canvas'});
                $.ajax({
                    type: "GET",url: "http://127.0.0.1:5000/Bar",dataType: 'json',success: function (result) {
                        chart.setOption(result);
                    }
                });
            }
        );
        function getData() {
            var chart = echarts.init(document.getElementById('Bar'),{renderer: 'canvas'});
            $.ajax({
                type: "POST",//方法型別
                dataType: "json",//預期伺服器返回的資料型別
                url: "/Bar",//url
                data: $('#form1').serialize(),success: function (result) {
                    chart.setOption(result);
                },error: function(err) {
                    if (err.status === 403) {
                        alert("請先登陸系統!");
                    }
                    else {
                        alert("錯誤的股票程式碼!");
                    }
                }
            });
        }
        function add1(){
            var input1 = document.createElement('input');
            input1.setAttribute('type','text');
            input1.setAttribute('name','organizers[]');

            var btn1 = document.getElementById("p1");
            //btn1.insertBefore(input1,null);
            btn1.appendChild(input1);
        }
    </script>
</body>
{% endblock %}

{% block scripts %}
{{ super() }}
    <script src="https://cdn.bootcss.com/jquery/3.0.0/jquery.min.js"></script>
    <script type="text/javascript" src="https://assets.pyecharts.org/assets/echarts.min.js"></script>
{% endblock %}
複製程式碼

同時在 base.html 中新增入口地址

...
<ul class="nav navbar-nav">
                <li><a href="{{ url_for('moving_average')}}">Moving Average</a></li>
            </ul>
            <ul class="nav navbar-nav">
                <li><a href="{{ url_for('volume')}}">Volume</a></li>
            </ul>
...
複製程式碼

現在我們的 Web 應用就是下圖的樣子了

下面我們就可以進入今天的正題了,設定許可權。

許可權設計

定義表結構

首先定義許可權表結構

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(64),unique=True)
    users = db.relationship('WebUser',backref='role')

    @staticmethod
    def init_roles():
        roles = ['User','Admin']
        for r in roles:
            role = Role.query.filter_by(name=r).first()
            if role is None:
                role = Role(name=r)
                db.session.add(role)
        db.session.commit()
複製程式碼

我們定義了兩種許可權,User 和 Admin,那麼只有擁有 Admin 許可權的使用者才可以訪問高階功能。

這裡還使用了外來鍵關聯到了 WebUser 表上,所以需要同步修改 WebUser 表

# 使用者表結構
class WebUser(UserMixin,db.Model):
    __tablename__ = 'webuser'
    id = db.Column(db.Integer,primary_key=True)
    user_id = db.Column(db.String(64),unique=True,index=True)
    email = db.Column(db.String(64),index=True)
    username = db.Column(db.String(64),index=True)
    password_hash = db.Column(db.String(128))
    role_id = db.Column(db.Integer,db.ForeignKey('roles.id'),default=1)
...
複製程式碼

因為我們修改了原始表的表結構,所以需要進行表結構的遷移操作,這裡可以使用外掛 flask-migrate 來幫助我們實現

表結構遷移

先安裝 flask-migrate 外掛

pip install flask-migrate
複製程式碼

然後在程式中配置 flask_migrate

from flask_migrate import Migrate
...
migrate = Migrate(app,db,render_as_batch=True)
...
複製程式碼

建立遷移倉庫

flask db init
複製程式碼

該命令會在當前目錄下生成遷移資料夾,所有的遷移指令碼都會儲存在其中。

建立遷移指令碼

flask db migrate
複製程式碼

最後就是更新資料庫,如果你和我一樣是使用的 sqllite 資料庫的話,那麼需要對遷移指令碼做些修改

開啟 migrations 下 versions 裡的 py 檔案,找到語句 “batch_op.create_foreign_key”,修改如下

batch_op.create_foreign_key('role_key','roles',['role_id'],['id'])
複製程式碼

然後再執行下面的命令

flask db upgrade
複製程式碼

最後我們初始化角色 進入 flask shell,執行如下操作完成角色表的初始化

flask shell
from app import Role
Role.init_roles()
複製程式碼

這樣就完成了資料庫的遷移和初始化。

許可權校驗

下面我們就可以開始編寫許可權校驗部分了

校驗函式

對於校驗函式,我們可以寫在 WebUser 類中,這樣就可以通過 current_user 來呼叫

...
    def is_admin(self):
        if self.role_id is 2:
            return True
        else:
            return False
...
複製程式碼

再建立一個必須是 admin role 的使用者才能訪問的檢視

@app.route('/fullchart/','POST'])
@login_required
def fullchart():
    if current_user.is_admin():
        return "OK"
    flash('You have not permission to access this page')
    return redirect(url_for('index'))
複製程式碼

整合前後端

把頁面入口新增到 base.html 頁面上

<ul class="nav navbar-nav">
                <li><a href="{{ url_for('fullchart')}}">Full Chart</a></li>
            </ul>
複製程式碼

然後新建一個 full chart 函式,用於產生高階圖表

# full chart
def full_chart(mydate,name):
    kline = (
        Kline()
...
複製程式碼

同樣的,編寫為前端提供的介面函式

@app.route("/FullChart",'POST'])
def get_fullcharte():
    stock_name = request.form.get('stockName')
    query_time = request.form.get('queryTime')
...
複製程式碼

最後建立 fullchart.html 並做響應修改,同時把 fullchart 檢視函式指向該模板

@app.route('/fullchart/','POST'])
@login_required
def fullchart():
    if current_user.is_admin():
        return render_template('fullchart.html')
    flash('You have not permission to access this page')
    return redirect(url_for('index'))
複製程式碼

至此,我們的高階圖表功能也完成了

只有擁有 admin 許可權的使用者才能訪問哦!