[Anne]在Dashboard中展示第二张报表

本文介绍了如何在Dashboard中添加新用户报表,并通过提取共性部分进行代码重构,以提高效率。利用Flask的 Jinja2 模板引擎动态渲染HTML,简化页面生成,同时在Python中抽象路由管理,减少重复代码。

本篇博客会先添加一张新用户相关的报表,然后提取两次添加报表相同的模块,对代码进行重构,方便之后的报表添加

展示新用户报表

对于一个应用来说新用户也是一个十分重要的观测数据
首先在数据库中新建一张new_user表并添加一些数据

CREATE TABLE new_user(
	`date` date,
	`province` varchar(20),
	`new_user` int,
	primary key(`date`, `province`)
);
INSERT INTO new_user VALUES
	('2021-01-01', 'Beijing', 10),
	('2021-01-01', 'Shanghai', 20),
	('2021-01-02', 'Beijing', 30);

按照日活报表相同的方法分别新增
new_user.js

$.ajax({
    url: "/data/new_user",
    type: "get",
    success: function (data) { 
        let table = $('#new-user-table').DataTable();
        table.clear();
        let dataSet = [];
        for (let i = 0; i < data.length; ++i) {
            let rowSet = [];
            let detail = data[i];
            rowSet.push(detail["date"])
            rowSet.push(detail["province"])
            rowSet.push(detail["new_user"])
            dataSet.push(rowSet);
        }
        table.rows.add(dataSet).draw();
        table.columns.adjust().draw();
    },
});

new_user.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
        <meta name="description" content="" />
        <meta name="author" content="" />
        <title>Anne</title>
        <link href="https://cdn.datatables.net/1.11.2/css/jquery.dataTables.min.css" rel="stylesheet" />
        <link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet" />
        <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/js/all.min.js" crossorigin="anonymous"></script>
    </head>
    <body class="sb-nav-fixed">
        <nav class="sb-topnav navbar navbar-expand navbar-dark bg-dark">
            <!-- Navbar Brand-->
            <a class="navbar-brand ps-3" href="/">Anne</a>
            <!-- Sidebar Toggle-->
            <button class="btn btn-link btn-sm order-1 order-lg-0 me-4 me-lg-0" id="sidebarToggle" href="#!"><i class="fas fa-bars"></i></button>
        </nav>
        <div id="layoutSidenav">
            <div id="layoutSidenav_nav">
                <nav class="sb-sidenav accordion sb-sidenav-dark" id="sidenavAccordion">
                    <div class="sb-sidenav-menu">
                        <div class="nav">
                            <div class="sb-sidenav-menu-heading">Core</div>
                            <a class="nav-link" href="/dau">
                                <div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt"></i></div>
                                DAU
                            </a>
                            <a class="nav-link" href="/new_user">
                                <div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt"></i></div>
                                New User
                            </a>
                        </div>
                    </div>
                </nav>
            </div>
            <div id="layoutSidenav_content">
                <main>
                    <div class="container-fluid px-4">
                        <div class="card mb-4">
                            <div class="card-header">
                                <i class="fas fa-table me-1"></i>
                                New User
                            </div>
                            <div class="card-body">
                                <table id="new-user-table">
                                    <thead>
                                        <tr>
                                            <th>Date</th>
                                            <th>Province</th>
                                            <th>New User</th>
                                        </tr>
                                    </thead>
                                    <tfoot>
                                        <tr>
                                            <th>Date</th>
                                            <th>Province</th>
                                            <th>New User</th>
                                        </tr>
                                    </tfoot>
                                    <tbody></tbody>
                                </table>
                            </div>
                        </div>
                    </div>
                </main>
            </div>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
        <script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
        <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
        <script src="https://cdn.datatables.net/1.11.2/js/jquery.dataTables.min.js"></script>
        <script src="{{ url_for('static', filename='js/new_user.js') }}"></script>
    </body>
</html>

以及相关db model和路由

class NewUser(db.Model):
    __tablename__ = "new_user"
    __table_args__ = {
        "autoload": True,
        "autoload_with": db.engine
    }

@app.route("/data/new_user")
def data_new_user():
    data = NewUser.query.all()
    ret = []
    for row in data:
        ret.append({
            "date": row.date.strftime("%Y-%m-%d"),
            "province": row.province,
            "new_user": row.new_user,
        })
    return jsonify(ret)

@app.route("/new_user")
def new_user():
    return render_template("new_user.html")

这样一张新的报表就完成了

提取相同内容

观察两张报表的代码不难发现除了部分内容不同外,绝大部分内容是相同的。如果每次新增报表都需要这样写一遍代码,整个添加报表的过程显得十分的低效并且会使得代码变得臃肿。因此可以将其中共性的部分提取出来,从而简化新增报表的流程

js

js代码的差异主要是URL,table的id以及data的处理三个部分
对于table的id而言,目前一个页面只有一张报表,这个情况下,可以将table的id固定下来
对于data的处理,可以通过修改data传输的格式来解决
最终js文件为data.js,代码如下

let query_data = function(url) {
    $.ajax({
        url: url,
        type: "get",
        success: function (data) { 
            let table = $('#data-table').DataTable();
            table.clear();
            table.rows.add(data).draw();
            table.columns.adjust().draw();
        },
    });
};

html

Flask使用jinjia2来渲染页面,因此可以做到页面内的不同信息通过参数的方式在render_template的时候传入
在jinjia2中可以会将双引号内的内容进行替换,比如下面的html

<!DOCTYPE html>
<html>
	{{ message }}
</html>

通过python调用render_template

@app.route("/jinjia2")
def jinjia2():
    return render_template("jinjia2.html", message = "Hello Anne")

最终会将页面渲染为

<!DOCTYPE html>
<html>
    Hello Anne
</html>

通过这个方式可以将表头的标题进行替换,
但通常来说,一张表的列数是不固定的,无法预先定义,在jinjia2中可以通过{%for%}来处理循环
比如对于页面

<!DOCTYPE html>
<html>
    {% for message in messages %}
        <div>{{ message }}</div>
    {% endfor %}
</html>

通过python调用render_template

@app.route("/jinjia2")
def jinjia2():
    return render_template("jinjia2.html", messages = ["Hello Anne", "Hello Hathaway"])

最终会将页面渲染为

<!DOCTYPE html>
<html>
    
        <div>Hello Anne</div>
    
        <div>Hello Hathaway</div>
    
</html>

可以看到渲染出的html中有许多的空白行
通过jinjia2可以通过Whitespace Control,修改空行和缩进,但仍然无法完美解决问题,需要通过添加换行以及loop的检测,最终的html模板为

<!DOCTYPE html>
<html>
    {% for message in messages -%}
        <div>{{ message }}</div>{{"
    "  if not loop.last }}
    {%- endfor %}
</html>

渲染出的页面为

<!DOCTYPE html>
<html>
    <div>Hello Anne</div>
    <div>Hello Hathaway</div>
</html>

通过使用render_template函数以及jinjia2的循环,可以实现通过通过参数动态的渲染出页面
最终的html页面代码如下

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
        <meta name="description" content="" />
        <meta name="author" content="" />
        <title>Anne</title>
        <link href="https://cdn.datatables.net/1.11.2/css/jquery.dataTables.min.css" rel="stylesheet" />
        <link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet" />
        <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/js/all.min.js" crossorigin="anonymous"></script>
    </head>
    <body class="sb-nav-fixed">
        <nav class="sb-topnav navbar navbar-expand navbar-dark bg-dark">
            <!-- Navbar Brand-->
            <a class="navbar-brand ps-3" href="/">Anne</a>
            <!-- Sidebar Toggle-->
            <button class="btn btn-link btn-sm order-1 order-lg-0 me-4 me-lg-0" id="sidebarToggle" href="#!"><i class="fas fa-bars"></i></button>
        </nav>
        <div id="layoutSidenav">
            <div id="layoutSidenav_nav">
                <nav class="sb-sidenav accordion sb-sidenav-dark" id="sidenavAccordion">
                    <div class="sb-sidenav-menu">
                        <div class="nav">
                            <div class="sb-sidenav-menu-heading">Core</div>
                            <a class="nav-link" href="/dau">
                                <div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt"></i></div>
                                DAU
                            </a>
                            <a class="nav-link" href="/new_user">
                                <div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt"></i></div>
                                New User
                            </a>
                        </div>
                    </div>
                </nav>
            </div>
            <div id="layoutSidenav_content">
                <main>
                    <div class="container-fluid px-4">
                        <div class="card mb-4">
                            <div class="card-header">
                                <i class="fas fa-table me-1"></i>
                                {{ title }}
                            </div>
                            <div class="card-body">
                                <table id="data-table">
                                    <thead>
                                        <tr>
                                            {% for col in cols -%}
                                            <th>{{ col }}</th>{{"
                                            "  if not loop.last }}
                                            {%- endfor %}
                                        </tr>
                                    </thead>
                                    <tfoot>
                                        <tr>
                                            {% for col in cols -%}
                                            <th>{{ col }}</th>{{"
                                            "  if not loop.last }}
                                            {%- endfor %}
                                        </tr>
                                    </tfoot>
                                    <tbody></tbody>
                                </table>
                            </div>
                        </div>
                    </div>
                </main>
            </div>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
        <script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
        <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
        <script src="https://cdn.datatables.net/1.11.2/js/jquery.dataTables.min.js"></script>
        <script src="{{ url_for('static', filename='js/data.js') }}"></script>
        <script>
            query_data("{{ data_url }}")
        </script>
    </body>
</html>

python

Flask可以通过add_url_rule的方式来添加路由,通过对python代码的抽象,选择将相同的内容抽象成一个类

class TableModel():
    title = ""
    cols = []
    uri = ""
    db_model = None

    def __init__(self, title, cols, uri, db_model, app):
        self.title = title
        self.cols = cols
        self.uri = uri
        self.db_model = db_model
        if app is not None:
            self.register_app(app)

    def register_app(self, app):
        app.add_url_rule(self.url, self.endpoint, self.render)
        app.add_url_rule(self.data_url, self.data_endpoint, self.data)

    @property
    def url(self):
        return f"/{self.uri}"

    @property
    def endpoint(self):
        return self.uri

    def render(self):
        return render_template("table.html", title=self.title, cols=self.cols, data_url=self.data_url)

    @property
    def data_url(self):
        return f"/data/{self.uri}"

    @property
    def data_endpoint(self):
        return f"data.{self.uri}"

    def data(self):
        data = self.db_model.query.all()
        ret = []
        for row in data:
            ret.append(row.table_row)
        return jsonify(ret)

这样新增一张报表只需要

class DAU(db.Model):
    __tablename__ = "DAU"
    __table_args__ = {
        "autoload": True,
        "autoload_with": db.engine
    }
    
    @property
    def table_row(self) -> list:
        return [
            self.date.strftime("%Y-%m-%d"),
            self.province,
            self.DAU,
        ]

TableModel("DAU", ["Date", "Province", "DAU"], "dau", DAU, app)

大大简化了新增报表需要的代码量

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值