├── .gitignore
├── .gitmodules
├── LICENSE
├── README.md
├── docs
├── 1、注册登录用户
│ ├── README.md
│ ├── 找回密码.png
│ ├── 注册用户.png
│ └── 登录用户.png
├── 2、业务管理
│ ├── README.md
│ └── 新建业务
│ │ ├── 业务列表.png
│ │ ├── 创建业务-1.png
│ │ ├── 创建业务-2.png
│ │ ├── 创建业务-3.png
│ │ ├── 创建业务-4.png
│ │ ├── 创建业务-5.png
│ │ └── 创建业务-6.png
├── 3、业务权限
│ ├── README.md
│ ├── 业务用户管理
│ │ ├── 保存业务角色.png
│ │ ├── 分配业务角色.png
│ │ ├── 完成添加业务.png
│ │ ├── 完成角色分配.png
│ │ ├── 新增业务用户.png
│ │ └── 选中业务用户.png
│ ├── 菜单管理
│ │ ├── 保存成功.png
│ │ ├── 保存菜单.png
│ │ ├── 保存菜单2.png
│ │ ├── 创建二级菜单.png
│ │ └── 创建菜单.png
│ ├── 角色管理
│ │ ├── 修改角色.png
│ │ ├── 删除角色.png
│ │ ├── 新建角色.png
│ │ └── 角色命名.png
│ └── 角色菜单管理
│ │ ├── 修改角色菜单.png
│ │ ├── 修改角色菜单2.png
│ │ └── 角色菜单页面.png
├── 4、业务数据
│ ├── README.md
│ └── 图表编辑
│ │ ├── 全局过滤组件拖拽.png
│ │ ├── 报表组件保存成功.png
│ │ ├── 报表组件拖拽.png
│ │ ├── 报表组件拖拽成功.png
│ │ ├── 报表组件设置编辑.png
│ │ ├── 编辑全局过滤组件.png
│ │ ├── 编辑新页面.png
│ │ ├── 过滤组件保存成功.png
│ │ ├── 过滤组建联动.png
│ │ └── 进入编辑页面.png
├── README.md
└── 其他图片
│ ├── 默认业务图.png
│ └── 默认首页.png
├── requirements-base.txt
├── setup.py
└── src
└── seed
├── __init__.py
├── api
├── __init__.py
├── _base.py
├── common.py
├── endpoints
│ ├── __init__.py
│ ├── account.py
│ ├── buser.py
│ ├── buserrole.py
│ ├── buserselect.py
│ ├── bussiness.py
│ ├── databases.py
│ ├── filters.py
│ ├── menu.py
│ ├── pages.py
│ ├── panels.py
│ ├── queries.py
│ ├── role.py
│ ├── rolemenu.py
│ └── user.py
├── front.py
├── urls.py
├── users
│ ├── __init__.py
│ ├── active_account.py
│ ├── login.py
│ ├── register.py
│ └── reset_password.py
└── utils
│ ├── __init__.py
│ ├── dbtest.py
│ ├── dbtypes.py
│ ├── files.py
│ └── sql_analyze.py
├── cache
├── __init__.py
├── active_account.py
├── auth.py
├── base.py
├── redis.py
├── reset_password.py
├── session.py
└── user_bussiness.py
├── conf
├── __init__.py
└── server.py
├── data
└── config
│ ├── config.yaml.default
│ └── seed_conf.py.default
├── drives
├── __init__.py
├── base.py
├── email.py
├── impala.py
├── mysql.py
└── postgresql.py
├── libs
├── __init__.py
├── data_access
│ ├── __init__.py
│ ├── app.py
│ ├── dataware
│ │ ├── __init__.py
│ │ └── base.py
│ ├── formatters
│ │ ├── __init__.py
│ │ ├── bar.py
│ │ ├── base.py
│ │ ├── funnel.py
│ │ ├── line.py
│ │ ├── linestack.py
│ │ ├── map.py
│ │ ├── pie.py
│ │ ├── sankey.py
│ │ └── table.py
│ ├── middledata
│ │ ├── __init__.py
│ │ └── base.py
│ └── utils
│ │ ├── __init__.py
│ │ └── auto_register.py
└── filter_access
│ ├── __init__.py
│ └── app.py
├── logging
├── README.md
├── __init__.py
└── handlers.py
├── models
├── __init__.py
├── _base.py
├── account.py
├── analogdatas.py
├── bmanager.py
├── buser.py
├── buserrole.py
├── bussiness.py
├── databases.py
├── filters.py
├── init.py
├── menu.py
├── panels.py
├── role.py
└── rolemenu.py
├── runner
├── __init__.py
├── commands
│ ├── __init__.py
│ ├── init.py
│ ├── run.py
│ └── upgrade.py
├── initializer.py
└── setting.py
├── schema
├── __init__.py
└── base.py
├── services
├── __init__.py
├── app.py
└── wsgi.py
├── static
├── 3rdpartylicenses.txt
├── OpenSans-Regular.629a55a7e793da068dc5.ttf
├── assets
│ ├── compConfig
│ │ ├── compConfig.js
│ │ └── reportConfig.js
│ ├── fonts
│ │ ├── LICENSE.txt
│ │ ├── OpenSans-Bold.ttf
│ │ ├── OpenSans-BoldItalic.ttf
│ │ ├── OpenSans-ExtraBold.ttf
│ │ ├── OpenSans-ExtraBoldItalic.ttf
│ │ ├── OpenSans-Italic.ttf
│ │ ├── OpenSans-Light.ttf
│ │ ├── OpenSans-LightItalic.ttf
│ │ ├── OpenSans-Regular.ttf
│ │ ├── OpenSans-Semibold.ttf
│ │ └── OpenSans-SemiboldItalic.ttf
│ ├── images
│ │ ├── arrow.png
│ │ ├── avatar.jpg
│ │ ├── bgt.jpg
│ │ ├── bt.jpg
│ │ ├── businessIcon.png
│ │ ├── businessNo.png
│ │ ├── cloudPlane.png
│ │ ├── dbqst.jpg
│ │ ├── dbt.jpg
│ │ ├── drq.jpg
│ │ ├── giphy1.gif
│ │ ├── ldt.jpg
│ │ ├── line.gif
│ │ ├── loading-bars.svg
│ │ ├── loading-spinning-bubbles.svg
│ │ ├── loading.gif
│ │ ├── loginIcon.png
│ │ ├── logo.png
│ │ ├── logo1.png
│ │ ├── mans.png
│ │ ├── map.jpg
│ │ ├── password-meter.png
│ │ ├── sankey.jpg
│ │ ├── srq.jpg
│ │ ├── superman.png
│ │ ├── userFace.png
│ │ ├── xldx.jpg
│ │ ├── xlfx.jpg
│ │ └── zxt.jpg
│ ├── js
│ │ ├── bowser.min.js
│ │ └── jquery.min.js
│ └── theme
│ │ └── fonts
│ │ ├── roboto-v15-latin-regular.eot
│ │ ├── roboto-v15-latin-regular.svg
│ │ ├── roboto-v15-latin-regular.ttf
│ │ ├── roboto-v15-latin-regular.woff
│ │ └── roboto-v15-latin-regular.woff2
├── favicon.ico
├── fontawesome-webfont.674f50d287a8c48dc19b.eot
├── fontawesome-webfont.912ec66d7572ff821749.svg
├── fontawesome-webfont.af7ae505a9eed503f8b8.woff2
├── fontawesome-webfont.b06871f281fee6b241d6.ttf
├── fontawesome-webfont.fee66e712a8a08eef580.woff
├── index.html
├── inline.07bd3aeef3ad0ebf96d7.bundle.js
├── line.567f57385ea3dde2c9ae.gif
├── loading-bars.0e7c657c2cbc1629a789.svg
├── main.04553bc590534d61b1af.bundle.js
├── mans.1302759b8be148f12915.png
├── polyfills.71faae03c9efad617caa.bundle.js
├── scripts.3905038f780eac8a5c4d.bundle.js
├── styles.2bd39d9150bfd833564f.bundle.css
└── vendor.effac60f981278e0c4fd.bundle.js
└── utils
├── __init__.py
├── auth.py
├── database.py
├── distutils
├── __init__.py
├── base.py
└── build_assets.py
├── file.py
├── helper.py
├── imports.py
├── mail.py
├── permissions.py
├── response.py
└── time.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled python files.
2 | *.py[cod]
3 | build
4 | dist
5 |
6 |
7 | # Packages.
8 | __pycache__
9 | *.egg-info
10 | migrations
11 |
12 |
13 | # Temporary files.
14 | *~
15 | *.swp
16 |
17 | # Hidden files.
18 | .*
19 | !.gitmodules
20 | !.gitignore
21 |
22 | *.log
23 | config.py
24 |
25 | sftp-config.json
26 | self_script
27 | *.rdb
28 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "seed_static"]
2 | path = seed_static
3 | url = https://github.com/BoyaaDataCenter/seed_static.git
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # 项目概述
3 | Seed自助数据展示系统只是一款简易的BI系统,它方便快捷,可以通过简易的拖拽并配置报表,使只会SQL的统计人员都能快速搭建出属于自己的数据可视化报表。
4 |
5 |

6 |

7 |
8 |
9 |
10 | ## 测试网址
11 | http://seed.boyaa.com
12 |
13 | 测试账号: admin 密码: admin123
14 |
15 | ## 系统操作手册
16 | [如何操作Seed自助数据展示系统](docs/README.md)
17 |
18 | ## 如何安装
19 | 1. 需要环境
20 | ```
21 | 系统环境: Linux, Mac和Windows
22 | 运行环境: Python3.5+
23 | 其他软件: Redis, MySQL(Postgresql)
24 |
25 | 注: 数据库一定要使用空库。
26 | ```
27 | 2. 安装
28 | - pip安装
29 | ```
30 | pip install boyaa-seed
31 | ```
32 |
33 | - 编译安装
34 | ```
35 | 获取代码 git clone git@github.com:BoyaaDataCenter/seed.git
36 | 进入文件夹 cd seed
37 | 编译代码 python setup.py install
38 | ```
39 | 3. 初始化seed的config文件
40 | ```
41 | 执行seed init
42 | ```
43 | 4. config文件设置
44 | ```
45 | 打开用户根目录下的.seed/seed_conf.py文件
46 | 进行数据库等相关的配置
47 | 如: vim ~/.seed/seed_conf.py
48 | ```
49 | 5. 初始化数据库
50 | ```
51 | 进行数据库初始化, 执行
52 | seed upgrade
53 | 即可
54 | ```
55 | 6. 运行web程序
56 | ```
57 | 执行 seed run web运行web系统
58 | 注:因uwsgi模块不支持Windows,故只能以开发模式运行:seed run web --debug=True
59 | ```
60 | 7. 访问
61 | ```
62 | 127.0.0.1:5000 可访问系统
63 | ```
64 |
65 | ## 如何升级
66 | 1. 获取到最新代码
67 | 2. 打包seed
68 | ```
69 | 进入seed项目根目录
70 | 运行 python setup.py install
71 | ```
72 | 3. 运行web程序
73 | ```
74 | 执行 seed run web运行web系统
75 | ```
76 | 4. 访问
77 | ```
78 | 127.0.0.1:5000 可访问系统
79 | ```
80 |
81 | ## 开发模式
82 | 1. 安装seed的pip运行文件到第三方库中
83 | ```
84 | python setup.py develop
85 | ```
86 | 2. 运行seed数据
87 | ```
88 | seed init
89 | ```
90 | 3. 设置数据库
91 | ```
92 | vim ~/.seed/seed_config.py
93 | ```
94 | 4. 运行web
95 | ```
96 | seed run web --debug=True
97 | ```
98 |
99 |
100 | ## 图形说明
101 |
102 | ### 1、桑基图使用规范
103 | 桑基图源数据需可以将数据按照以下形式组合:
104 | ```
105 | selet source, target, value from table
106 | union all
107 | selet source, target, value from table
108 | ```
109 | (上面一条sql的target需要和下一条sql的source相同,否则就不能形成桑基图形式
110 | source, target 这两个字段别名已固定)
111 |
112 |
113 | ##### 数据示例说明(以mysql为例):
114 | ```
115 | CREATE TABLE `sankey_testdata` (
116 | `state` varchar(20) ,
117 | `address` varchar(20) ,
118 | `province` varchar(20) ,
119 | `value` int
120 | ) ENGINE=innodb DEFAULT CHARSET=utf8;
121 | ```
122 | ```
123 | insert into sankey_testdata values
124 | ('东部地区', '东三省', '黑龙江',246),
125 | ('东部地区', '东三省', '吉林', 319),
126 | ('东部地区', '东三省', '辽宁', 871),
127 | ('东部地区', '华南', '广东', 323),
128 | ('东部地区', '华南', '广西', 250),
129 | ('东部地区', '华南', '海南', 431),
130 | ('东部地区', '华南', '福建', 236),
131 | ('东部地区', '华南', '香港', 334),
132 | ('东部地区', '华南', '澳门', 544),
133 | ('东部地区', '华南', '台湾', 915),
134 | ('东部地区', '环渤海', '北京', 687),
135 | ('东部地区', '环渤海', '天津', 340),
136 | ('东部地区', '环渤海', '内蒙古',234),
137 | ('东部地区', '环渤海', '河北', 282),
138 | ('东部地区', '环渤海', '山东', 102),
139 | ('东部地区', '长三角', '上海', 201),
140 | ('东部地区', '长三角', '江苏', 717),
141 | ('东部地区', '长三角', '浙江', 669),
142 | ('西部地区', '西北', '青海', 335),
143 | ('西部地区', '西北', '甘肃', 357),
144 | ('西部地区', '西北', '宁夏', 456),
145 | ('西部地区', '西北', '山西', 119),
146 | ('西部地区', '西北', '新疆', 984),
147 | ('西部地区', '西南', '云南', 611);
148 | ```
149 | ##### 桑基图查询SQL示例:
150 | ```
151 | SELECT
152 | state as source,
153 | address as target,
154 | sum(value) as value
155 | FROM sankey_testdata
156 | GROUP BY state,address
157 | union all
158 | SELECT
159 | address as source,
160 | province as target ,
161 | sum(value) as value
162 | FROM sankey_testdata
163 | GROUP BY address,province
164 | ```
165 |
166 |
167 | ### 2、地图使用规范
168 |
169 | 地图源数据至少需要以下字段
170 | ```
171 | 经度
172 | 纬度
173 | 区域名称
174 | 区域id
175 | 区域上级id
176 | 区域级别id(注:区域级别需按以下分类,否则可能导致数据显示混乱)
177 | 1-国家
178 | 2-省份
179 | 3-地市
180 | 4-区县/乡镇(街道)
181 | 7-社区
182 | 8-具体位置
183 | ```
184 |
185 | ##### 数据示例说明(以mysql为例):
186 | ```
187 | CREATE TABLE `map_testdata` (
188 | `fdate` date,
189 | `fid` int comment '区域id',
190 | `region_name` varchar(100) comment '区域名称',
191 | `fpid` int comment '区域上级id',
192 | `region_id` int comment '区域级别id',
193 | `value1` int,
194 | `value2` int,
195 | `value3` int,
196 | `lat` varchar(100) comment '经度',
197 | `lng` varchar(100) comment '纬度'
198 | ) ENGINE=innodb DEFAULT CHARSET=utf8;
199 | ```
200 | ```
201 | insert into map_testdata values
202 | ('2019-05-08',1,'中国',0,1,25000,10000,20000,'37.550339','104.114129'),
203 | ('2019-05-08',107712,'广东省',1,2,5000,3000,1000,'23.408003729025','113.39481755876'),
204 | ('2019-05-08',112083,'深圳市',107712,3,3000,1000,1500,'22.546053546205','114.02597365732'),
205 | ('2019-05-08',112313,'南山区',112083,4,2000,300,200,'22.558887751083','113.95072266574'),
206 | ('2019-05-08',null,null,'112313',7,2,2,2,'22.5557455','114.026432'),
207 | ('2019-05-08',null,null,'112313',7,40,40,40,'22.5658103','114.0948389'),
208 | ('2019-05-08',null,null,'112313',7,166,166,166,'22.53615273','114.1144042'),
209 | ('2019-05-08',null,null,'112313',7,80,80,80,'22.5232017','114.0353637'),
210 | ('2019-05-08',null,null,'112313',7,14,14,14,'22.53781229','114.1248561'),
211 | ('2019-05-08',null,null,'112313',7,1,1,1,'22.578339','114.140053'),
212 | ('2019-05-08',null,null,'112313',7,1,1,1,'22.52957','114.200043'),
213 | ('2019-05-08',null,null,'112313',7,33,33,33,'22.60465685','114.1245172'),
214 | ('2019-05-08',null,null,'112313',7,154,154,154,'22.5465231','114.0245698'),
215 | ('2019-05-08',null,null,'112313',7,1,1,1,'22.530012','114.199852'),
216 | ('2019-05-08',null,null,'112313',7,12,12,12,'22.6010765','113.8757611'),
217 | ('2019-05-08',null,null,'112313',7,5,5,5,'22.5274626','114.195732'),
218 | ('2019-05-08',null,null,'112313',7,3,3,3,'22.58916333','113.9847973'),
219 | ('2019-05-08',null,null,'112313',7,2,2,2,'22.510313','114.146149'),
220 | ('2019-05-08',null,null,'112313',7,52,52,52,'22.56592831','114.1653427'),
221 | ('2019-05-08',null,null,'112313',7,6,6,6,'22.5230185','113.8840307'),
222 | ('2019-05-08',null,null,'112313',7,8,8,8,'22.55479675','113.876808'),
223 | ('2019-05-08',null,null,'112313',7,2,2,2,'22.527546','114.1382295'),
224 | ('2019-05-08',null,null,'112313',7,20,20,20,'22.60403745','113.8813462'),
225 | ('2019-05-08',null,null,'112313',7,3,3,3,'22.53090833','114.0340117'),
226 | ('2019-05-08',null,null,'112313',7,24,24,24,'22.52259221','114.1844415'),
227 | ('2019-05-08',null,null,'112313',7,6,6,6,'22.56845417','113.8482768'),
228 | ('2019-05-08',null,null,'112313',7,15,15,15,'22.50921313','114.1460804'),
229 | ('2019-05-08',null,null,'112313',7,1,1,1,'22.52459','113.873619'),
230 | ('2019-05-08',null,null,'112313',7,54,54,54,'22.51644502','113.9074763'),
231 | ('2019-05-08',null,null,'112313',7,110,120,135,'22.54528264','113.9450687');
232 | ```
233 |
234 |
235 | ##### 地图查询SQL示例:
236 | 以下SQL中的{}表示变量,使用地图时
237 | region_id,slat,elat,slng,elng,fpid均为必须参数
238 |
239 | postgresql写法:
240 | ```
241 | SELECT
242 | a.region_name,
243 | a.fid as fpid,
244 | a.lat,
245 | a.lng,
246 | a.value1,
247 | a.value2,
248 | a.value3
249 | from map_testdata a
250 | WHERE region_id = {region_id}
251 | AND cast(lat AS decimal(20, 10)) > {slat}
252 | AND cast(lat AS decimal(20, 10))< {elat}
253 | AND cast(lng AS decimal(20, 10)) > {slng}
254 | AND cast(lng AS decimal(20, 10))< {elng}
255 | AND CASE cast( {fpid} AS bool ) WHEN TRUE THEN fpid={fpid} ELSE 1=1 END
256 | ```
257 |
258 | mysql写法:
259 | ```
260 | SELECT
261 | a.region_name,
262 | a.fid as fpid,
263 | a.lat,
264 | a.lng,
265 | a.value1,
266 | a.value2,
267 | a.value3
268 | from map_testdata a
269 | WHERE region_id = {region_id}
270 | AND cast(lat AS decimal(20, 10)) > {slat}
271 | AND cast(lat AS decimal(20, 10))< {elat}
272 | AND cast(lng AS decimal(20, 10)) > {slng}
273 | AND cast(lng AS decimal(20, 10))< {elng}
274 | AND CASE {fpid}=true WHEN TRUE THEN fpid={fpid} ELSE 1=1 END
275 | ```
--------------------------------------------------------------------------------
/docs/1、注册登录用户/README.md:
--------------------------------------------------------------------------------
1 | # 注册和登录用户
2 | Seed系统目前只支持账户密码登录的方式,下面将介绍如何进行注册,登录 和 找回密码。
3 |
4 | 注册新系统的第一个用户既为超级管理员,超级管理员有所有的权限。之后的用户即为普通用户,超级管理员可以通过系统里面的设置来更改其他用户的权限。
5 |
6 | ## 注册
7 | 输入Seed系统对应的域名会进入登陆页面,可以通过登陆页面的下方进入注册页面。如下图所示。
8 | 
9 | 注册内容和平时的网站一样,唯一需要值得注意的是,如果当前系统配置了可用的邮件服务配置,注册之后将会通过 邮件验证 来激活账户,才能正常的使用当前账户。
10 |
11 | 建议无论邮件服务配置是否开启,都建议使用自己有效可用的邮箱来注册。
12 |
13 | ## 登录
14 | 输入Seed系统对应的域名,在没有登陆的状态,将会直接跳转到登陆页面。登录页面入下图所示。
15 | 
16 |
17 | ## 找回密码
18 | **注意: 找回密码因为涉及到邮件发送,只有在邮件服务配置成功的时候才能使用**
19 | 如果用户忘记了密码,可以通过点击登录页面的忘记密码进入,密码找回页面。密码找回页面如下图所示
20 | 
21 | 在输入账号或者邮箱之后, 系统会通过配置的邮件服务,发送找回密码的邮件给用户,让用户通过邮件中的链接输入新的密码即可。
--------------------------------------------------------------------------------
/docs/1、注册登录用户/找回密码.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/1、注册登录用户/找回密码.png
--------------------------------------------------------------------------------
/docs/1、注册登录用户/注册用户.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/1、注册登录用户/注册用户.png
--------------------------------------------------------------------------------
/docs/1、注册登录用户/登录用户.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/1、注册登录用户/登录用户.png
--------------------------------------------------------------------------------
/docs/2、业务管理/README.md:
--------------------------------------------------------------------------------
1 | # 业务管理
2 | Seed系统是通过业务将不同属性的报表区分开的。
3 |
4 | 每个游戏工作室的报表需求不一样,那么可以通过给每个游戏工作室分配不同的业务。将对应业务的权限交给对应工作室的负责做统计报表的工作人员,那么对应的工作人员就可以在自己工作室的业务下进行相关的业务报表配置。
5 |
6 | ## 新建和编辑业务
7 | 用户登录进入系统之后,就可以进入业务选择的页面。和普通用户不一样的是,超级管理员拥有对业务添加的权限,分配的业务管理员拥有对业务编辑的权限。
8 | 由于新建业务和编辑业务是同样的操作逻辑,这里将会一并介绍。
9 |
10 | ### 业务列表
11 | 业务列表 如下图所示
12 | 
13 | 只有超级管理员才能看到新建业务, 超级管理员可以通过 点击 新建业务进入新建业务的业务逻辑。
14 |
15 | 超级管理员和业务管理员都可以通过点击某个业务右上方的配置按钮 对当前业务已有的配置进行编辑。
16 |
17 | 业务配置总共分三步,分别是业务概况,业务数据库配置和业务数据保存。
18 |
19 | ### 业务概况设置
20 | 
21 | 由上图所示,业务概况需要配置业务名称,业务概要 和 选择当前业务的业务管理员。
22 | 业务名称和业务概要主要是用来介绍当前业务的主要属性。
23 |
24 | 业务管理员则是指定当前业务所有权限归谁使用,业务管理员可以指定多个人,因为超级管理员级别在业务管理员之上,在配置的时候,可以不用配置超级管理员为业务管理员。
25 |
26 | ### 业务数据库设置
27 | 
28 | 接下来进入业务数据库配置,业务数据库主要是在配置报表和展示报表时的取数,如果当前业务的报表数据需要从PostgreSQL A中获取,则需要在这里配置PostgreSQL A的连接配置。
29 |
30 | 点击添加业务数据库,则可以进入业务数据库配置页面。如下图所示
31 | 
32 | 需要提前选择配置数据库的类型,目前只支持MySQL和PostgreSQL两种常见的关系型数据库。然后就是常规的数据库连接配置了。可以在配置完之后,点一下测试连接。当测试连接成功之后,则可以点击保存。
33 |
34 | 新建业务数据库成功后,则返回了业务数据库列表。可以看出业务可以支持多个数据库的连接。
35 | 
36 |
37 | ### 业务数据保存
38 | 在配置完业务数据库之后,点击下一步,将会将当前业务的所有配置展示在最后一步
39 | 
40 |
41 | 点击完成,则添加了新的业务到业务列表中
42 | 
43 |
44 | ## 删除业务
45 | 删除按钮则可以通过业务上方的删除按钮进行删除。
46 |
47 | ## 备注
48 | 暂无
--------------------------------------------------------------------------------
/docs/2、业务管理/新建业务/业务列表.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/业务列表.png
--------------------------------------------------------------------------------
/docs/2、业务管理/新建业务/创建业务-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-1.png
--------------------------------------------------------------------------------
/docs/2、业务管理/新建业务/创建业务-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-2.png
--------------------------------------------------------------------------------
/docs/2、业务管理/新建业务/创建业务-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-3.png
--------------------------------------------------------------------------------
/docs/2、业务管理/新建业务/创建业务-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-4.png
--------------------------------------------------------------------------------
/docs/2、业务管理/新建业务/创建业务-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-5.png
--------------------------------------------------------------------------------
/docs/2、业务管理/新建业务/创建业务-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-6.png
--------------------------------------------------------------------------------
/docs/3、业务权限/README.md:
--------------------------------------------------------------------------------
1 | # 业务权限
2 | 对于业务的每一个数据都是敏感的,Seed系统做了针对页面的权限处理。每个有业务权限的用户都有不同的角色,每个角色关联了不同的页面。
3 |
4 | ## 菜单管理
5 | 菜单管理是用来对自定义的数据报表页面进行配置。
6 |
7 | 可以通过管理中心-菜单管理 进入到配置页面。
8 | 
9 |
10 | 点击页面右上角的 添加一级菜单 可以添加一个新的一级菜单, 在弹出来的对话框中填入菜单的名字,点击保存。
11 | 
12 |
13 | 通过点击刚刚创建的 游戏玩家 的一级菜单对应右边的+ 创建二级菜单。
14 | 
15 |
16 | 通过同样的步骤,可以创建出最终入下图的菜单列表。
17 | 
18 |
19 | **注意: 真的的保存需要点击右上方的 保存修改 按钮才是正在的保存成功**
20 |
21 | ## 角色管理
22 | 可以通过管理中心-角色管理 进入 角色管理页面。
23 |
24 | ### 新增角色
25 | 点击页面右上方的 新增角色 添加新的角色。
26 | 
27 |
28 | 然后进入弹出角色命名对话框
29 | 
30 |
31 | 点击保存,成功添加角色。
32 |
33 | ### 修改角色
34 | 点击对应角色右边的编辑按钮,弹出 编辑角色 对话框。
35 | 
36 |
37 | 点击保存,成功修改角色
38 |
39 | ### 删除角色
40 | 点击对应角色右边的删除按钮即可。
41 | 
42 |
43 | ## 角色菜单关联
44 | 之前介绍了角色 和 菜单 的管理,那么接下来则是将角色和菜单之间建立关联。
45 |
46 | 点击页面的管理中心-角色菜单管理进入 角色菜单管理页面。
47 | 
48 |
49 | 点击上方的角色按钮,则可以跳转到对应角色关联的菜单页面,点击是否可见对应页面的按钮,则可以开启当前角色对应页面的查看权限。
50 |
51 | 
52 | 修改财务角色对应的菜单页面查看角色如图。
53 |
54 | **注意: 真的的保存需要点击右上方的 保存修改 按钮才是正在的保存成功**
55 |
56 | ## 业务用户管理
57 | 管理员需要将需要看数据的用户添加到业务中,并分配对应的角色,这样普通用户才能有对应业务的权限和能看到对应页面的数据。
58 |
59 | 可以通过管理中心-业务用户管理 进入 业务用户管理页面
60 |
61 | ### 新增业务用户
62 | 点击右上方 添加业务用户
63 | 
64 |
65 | 弹出用户选择页面,可以进行选择多个用户,然后点击保存。
66 | 
67 |
68 | 点击保存后,用户成功添加到业务。
69 | 
70 |
71 | ### 分配用户角色
72 | 每添加一个业务用户,需要给对应的用户分配用户角色,才能看到角色对应有权限的页面。
73 |
74 | 点击用户对应的编辑按钮,弹出用户角色的分配对话框
75 | 
76 |
77 | 选择对应的角色,并保存
78 | 
79 |
80 | 完成用户的角色分配
81 | 
82 | ## 备注
83 | 暂无
--------------------------------------------------------------------------------
/docs/3、业务权限/业务用户管理/保存业务角色.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/保存业务角色.png
--------------------------------------------------------------------------------
/docs/3、业务权限/业务用户管理/分配业务角色.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/分配业务角色.png
--------------------------------------------------------------------------------
/docs/3、业务权限/业务用户管理/完成添加业务.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/完成添加业务.png
--------------------------------------------------------------------------------
/docs/3、业务权限/业务用户管理/完成角色分配.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/完成角色分配.png
--------------------------------------------------------------------------------
/docs/3、业务权限/业务用户管理/新增业务用户.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/新增业务用户.png
--------------------------------------------------------------------------------
/docs/3、业务权限/业务用户管理/选中业务用户.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/选中业务用户.png
--------------------------------------------------------------------------------
/docs/3、业务权限/菜单管理/保存成功.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/菜单管理/保存成功.png
--------------------------------------------------------------------------------
/docs/3、业务权限/菜单管理/保存菜单.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/菜单管理/保存菜单.png
--------------------------------------------------------------------------------
/docs/3、业务权限/菜单管理/保存菜单2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/菜单管理/保存菜单2.png
--------------------------------------------------------------------------------
/docs/3、业务权限/菜单管理/创建二级菜单.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/菜单管理/创建二级菜单.png
--------------------------------------------------------------------------------
/docs/3、业务权限/菜单管理/创建菜单.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/菜单管理/创建菜单.png
--------------------------------------------------------------------------------
/docs/3、业务权限/角色管理/修改角色.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色管理/修改角色.png
--------------------------------------------------------------------------------
/docs/3、业务权限/角色管理/删除角色.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色管理/删除角色.png
--------------------------------------------------------------------------------
/docs/3、业务权限/角色管理/新建角色.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色管理/新建角色.png
--------------------------------------------------------------------------------
/docs/3、业务权限/角色管理/角色命名.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色管理/角色命名.png
--------------------------------------------------------------------------------
/docs/3、业务权限/角色菜单管理/修改角色菜单.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色菜单管理/修改角色菜单.png
--------------------------------------------------------------------------------
/docs/3、业务权限/角色菜单管理/修改角色菜单2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色菜单管理/修改角色菜单2.png
--------------------------------------------------------------------------------
/docs/3、业务权限/角色菜单管理/角色菜单页面.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色菜单管理/角色菜单页面.png
--------------------------------------------------------------------------------
/docs/4、业务数据/README.md:
--------------------------------------------------------------------------------
1 | # 业务数据
2 | 数据报表是整个Seed系统最核心的部分,目前整个系统支持4种数据展示图表组件,4种数据过滤组件。
3 |
4 | 从逻辑来看,整个业务数据主要包括两部分图表编辑和图表查看。图表编辑主要使用人群为超级管理员和业务管理员,图表查看主要为普通用户。
5 |
6 | ## 图表编辑
7 | 图表编辑主要以拖拽,选择和输入为主。图表编辑可以通过每个页面的右上方的 编辑页面进入。
8 | 
9 |
10 | 点击进入之后,主要可以看到 分为两个区域的 插件选择区域 和 右边的数据展示区域。其中数据展示区域又分为 全局过滤插件区 和 报表展示区。具体如下图所示
11 |
12 | 
13 |
14 | 整个页面的交互逻辑是 从左边的 插件选择区域 将需要的插件通过拖拽的方式 拖拽到对应的位置。然后页面会立马显示出来,然后点击对应插件的配置按钮通过配置界面对插件进行配置。
15 |
16 | 可以从插件选择区域看出,所有插件分成两类,一个是全局过滤插件 和 报表插件。其中报表插件在配置的过程中还可以配置局部的过滤插件。下面将从这三个方面进行介绍。
17 |
18 | ### 全局过滤插件
19 | 过滤插件顾名思义是用来过滤数据的,可以从全局的过滤插件中看到,当前支持单日期,双日期,多选,单选四种类型的过滤组件。从SQL的角度出发,过滤插件就是组装在where条件下,然后通过筛选过滤插件,动态的更新SQL的where条件获取不同的数据。
20 | 如何配置
21 | 那么全局过滤插件,则是说当前这个插件的过滤条件适用于当前页面的 报表插件 和 过滤插件。 在报表插件中可以用来过滤展示的数据,在过滤插件用可以用来做数据筛选的级联。
22 |
23 | #### 如何配置
24 | 从全局过滤插件中选择一种过滤插件拖拽到数据展示区域的全局过滤插件区域。如下图
25 | 
26 |
27 | #### 如何编辑
28 | 点击对应过滤组件的编辑按钮,弹出组件编辑页面, 如下图所示。
29 | 
30 | 可以从上图的配置页面可以看到,需要配置 中文名称,这是当前组件在整个页面的唯一标识,数据库字段则是当前过滤组件在其他图表插件中的数据库字段。 数据源有两种方式,一个是手动配置, 另一个是SQL配置。
31 |
32 | 这里选择了SQL配置,选择SQL配置需要选择数据从哪个数据库来,数据库查询的SQL。
33 |
34 | #### 保存成功
35 | 在配置完之后,点击保存,则会立刻去后台拉取数据,具体如下图。
36 | 
37 |
38 | ### 报表插件
39 | 报表插件是用来展示数据的,当前支持折线图,对比图,饼状图和对比趋势图四种图表展示插件。
40 |
41 | #### 如何配置
42 | 从报表插件中选择一种报表插件拖拽到数据展示区域的报表展示区。
43 | 
44 |
45 | #### 如何编辑
46 | 点击对应报表插件的编辑按钮,弹出插件编辑页面,如下图所示。
47 | 
48 |
49 | 报表名称和报表描述是对当前报表的功能进行说明和标记。
50 | 数据源则是指定当前报表的SQL从哪个数据库取数。
51 | SQL配置则是输入取数的SQL。
52 |
53 | 其中在SQL配置下面有个 游戏 的标签,这就是之前配置的全局过滤组件的标签。将光标放置到where 后面,点击游戏这个标签,则会自动生成game_id={game_id}, 其中{}中的内容不可以改变,其他部分都可以根据SQL适当更改。
54 |
55 | SQL配置好之后,点击生成指标和维度。
56 | 下面会自动生成数据项,对数据项进行配置中文名,是指标还是维度类型,还有其他必要的配置。
57 |
58 | #### 保存成功
59 | 在配置完成之后,点击保存,则会立刻从后台拉取数据,具体体现下图。
60 | 
61 |
62 | 其中可以通过选择不同的游戏来进行过滤图表中的,如下图所示
63 | 
64 |
--------------------------------------------------------------------------------
/docs/4、业务数据/图表编辑/全局过滤组件拖拽.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/全局过滤组件拖拽.png
--------------------------------------------------------------------------------
/docs/4、业务数据/图表编辑/报表组件保存成功.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/报表组件保存成功.png
--------------------------------------------------------------------------------
/docs/4、业务数据/图表编辑/报表组件拖拽.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/报表组件拖拽.png
--------------------------------------------------------------------------------
/docs/4、业务数据/图表编辑/报表组件拖拽成功.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/报表组件拖拽成功.png
--------------------------------------------------------------------------------
/docs/4、业务数据/图表编辑/报表组件设置编辑.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/报表组件设置编辑.png
--------------------------------------------------------------------------------
/docs/4、业务数据/图表编辑/编辑全局过滤组件.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/编辑全局过滤组件.png
--------------------------------------------------------------------------------
/docs/4、业务数据/图表编辑/编辑新页面.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/编辑新页面.png
--------------------------------------------------------------------------------
/docs/4、业务数据/图表编辑/过滤组件保存成功.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/过滤组件保存成功.png
--------------------------------------------------------------------------------
/docs/4、业务数据/图表编辑/过滤组建联动.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/过滤组建联动.png
--------------------------------------------------------------------------------
/docs/4、业务数据/图表编辑/进入编辑页面.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/进入编辑页面.png
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Seed操作手册
2 | Seed系统操作主要分为以下四个部分
3 |
4 | - [注册登录用户](1、注册登录用户/README.md)
5 | - [业务管理](2、业务管理/README.md)
6 | - [业务权限](3、业务权限/README.md)
7 | - [业务数据](4、业务数据/README.md)
--------------------------------------------------------------------------------
/docs/其他图片/默认业务图.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/其他图片/默认业务图.png
--------------------------------------------------------------------------------
/docs/其他图片/默认首页.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/其他图片/默认首页.png
--------------------------------------------------------------------------------
/requirements-base.txt:
--------------------------------------------------------------------------------
1 | alembic==1.0.0
2 | asn1crypto==0.24.0
3 | astroid==2.0.1
4 | certifi==2018.8.24
5 | cffi==1.11.5
6 | chardet==3.0.4
7 | click==6.7
8 | colorama==0.3.9
9 | cryptography==2.3
10 | decorator==4.3.0
11 | flake8==3.5.0
12 | Flask==1.0.2
13 | Flask-Cors==3.0.6
14 | flask-marshmallow==0.9.0
15 | Flask-Migrate==2.2.1
16 | Flask-Script==2.0.6
17 | Flask-SQLAlchemy==2.3.2
18 | idna==2.7
19 | infinity==1.4
20 | intervals==0.8.1
21 | isort==4.3.4
22 | itsdangerous==0.24
23 | Jinja2==2.10.1
24 | lazy-object-proxy==1.3.1
25 | Mako==1.0.7
26 | MarkupSafe==1.0
27 | marshmallow==2.15.3
28 | marshmallow-sqlalchemy==0.14.0
29 | mccabe==0.6.1
30 | psycopg2==2.7.5
31 | pycodestyle==2.3.1
32 | pycparser==2.18
33 | pyflakes==1.6.0
34 | pylint==2.0.1
35 | PyMySQL==0.9.2
36 | python-dateutil==2.7.3
37 | python-editor==1.0.3
38 | redis==2.10.6
39 | requests>=2.20.0
40 | rope==0.11.0
41 | six==1.11.0
42 | SQLAlchemy==1.3.3
43 | SQLAlchemy-Utils==0.33.3
44 | typed-ast==1.1.0
45 | urllib3==1.25
46 | validators==0.12.2
47 | Werkzeug==0.14.1
48 | wrapt==1.10.11
49 | WTForms==2.2.1
50 | WTForms-Alchemy==0.16.7
51 | WTForms-Components==0.10.3
52 | bcrypt==3.1.4
53 | uWSGI==2.0.18
54 | sqlparse==0.3.0
55 | impyla==0.14.1
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from setuptools import setup, find_packages
5 | from distutils.command.build import build as BuildCommand
6 | from setuptools.command.develop import develop as DevelopCommand
7 | from setuptools.command.install import install as InstallCommand
8 |
9 | ROOT = os.path.realpath(os.path.join(os.path.dirname(
10 | sys.modules['__main__'].__file__)))
11 |
12 | # Add Sentry to path so we can import distutils
13 | sys.path.insert(0, os.path.join(ROOT, 'src'))
14 |
15 | from seed.utils.distutils.build_assets import BuildAssetsCommand
16 |
17 |
18 | VERSION = '0.1.5.6'
19 |
20 | with open('README.md', 'r', encoding='utf-8') as fh:
21 | long_description = fh.read()
22 |
23 |
24 | def get_requirements(env):
25 | with open('requirements-{}.txt'.format(env)) as fp:
26 | return [x.strip() for x in fp.read().split('\n') if not x.startswith('#')]
27 |
28 |
29 | install_requires = get_requirements('base')
30 |
31 |
32 | def package_files(directory):
33 | paths = []
34 | for (path, directories, filenames) in os.walk(directory):
35 | for filename in filenames:
36 | paths.append(os.path.join(path, filename))
37 | return paths
38 |
39 |
40 | static_files = package_files(os.path.join('src', 'seed', 'static'))
41 | static_files.extend(package_files(os.path.join('src', 'seed', 'data')))
42 |
43 |
44 | class SeedBuildCommand(BuildCommand):
45 | def run(self):
46 | self.run_command('build_assets')
47 | BuildCommand.run(self)
48 |
49 |
50 | class SeedDevelopCommand(DevelopCommand):
51 | def run(self):
52 | DevelopCommand.run(self)
53 |
54 |
55 | class SeedInstallCommand(InstallCommand):
56 | def run(self):
57 | InstallCommand.run(self)
58 |
59 |
60 | cmdclass = {
61 | 'build': SeedBuildCommand,
62 | 'develop': SeedDevelopCommand,
63 | 'install': SeedInstallCommand,
64 | 'build_assets': BuildAssetsCommand
65 | }
66 |
67 |
68 | setup(
69 | name="boyaa-seed",
70 | version=VERSION,
71 | auther="Boyaa DataCenter",
72 | auther_email="d@boyaa.com",
73 | description="seed data report system",
74 | long_description=long_description,
75 | long_description_content_type="text/markdown",
76 | url="http://seed.boyaa.com",
77 | install_requires=install_requires,
78 | packages=find_packages("src"),
79 | package_dir={
80 | "": "src"
81 | },
82 | package_data={
83 | "seed": ["static/*"]
84 | },
85 | data_files=static_files,
86 | include_package_data=True,
87 | entry_points={
88 | "console_scripts": [
89 | "seed = seed.runner:main",
90 | ]
91 | },
92 | cmdclass=cmdclass,
93 | classifiers=[
94 | "Programming Language :: Python :: 3",
95 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
96 | "Operating System :: OS Independent"
97 | ]
98 | )
99 |
--------------------------------------------------------------------------------
/src/seed/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
--------------------------------------------------------------------------------
/src/seed/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/api/__init__.py
--------------------------------------------------------------------------------
/src/seed/api/_base.py:
--------------------------------------------------------------------------------
1 | import inspect
2 |
3 | from flask import request, g
4 | from marshmallow import ValidationError
5 |
6 | from seed.models import db
7 | from seed.api.common import _MethodView
8 | from seed.models._base import BussinessModel
9 |
10 | RESTFUL_METHODS = ['GET', 'POST', 'PUT', 'DELETE']
11 |
12 |
13 | class HttpMethods(object):
14 | GET = 'GET'
15 | POST = 'POST'
16 | PUT = 'PUT'
17 | DELETE = 'DELETE'
18 |
19 |
20 | class RestfulBaseView(_MethodView):
21 | """ BaseView for Restful style
22 | """
23 | __abstract__ = True
24 |
25 | pk_type = 'string'
26 |
27 | session = db.session
28 |
29 | model_class = None
30 | schema_class = None
31 |
32 | access_methods = [HttpMethods.GET, HttpMethods.POST, HttpMethods.PUT, HttpMethods.DELETE]
33 |
34 | def __init__(self, *args, **kwargs):
35 | super(RestfulBaseView, self).__init__(*args, **kwargs)
36 |
37 | def get(self, model_id=None):
38 | """ GET
39 | GET /base
40 | get paragraph node list
41 |
42 | GET /base/
43 | get single node which id is model_id
44 |
45 | Arguments:
46 | model_id {int} -- resource id
47 | """
48 | query_session = self.session.query(self.model_class)
49 |
50 | if issubclass(self.model_class, BussinessModel):
51 | query_session = query_session.filter_by(bussiness_id=g.bussiness_id)
52 |
53 | if model_id:
54 | data = query_session.filter_by(id=model_id).first()
55 | data = self.schema_class(exclude=self.model_class.column_filter).dump(data)
56 | else:
57 | data = query_session.all()
58 | data = self.schema_class(many=True, exclude=self.model_class.column_filter).dump(data)
59 |
60 | return self.response_json(self.HttpErrorCode.SUCCESS, data=data.data)
61 |
62 | def post(self):
63 | """ POST
64 | """
65 | input_json = request.get_json()
66 |
67 | if isinstance(input_json, list):
68 | schema = self.schema_class(many=True)
69 | else:
70 | schema = self.schema_class()
71 | datas, errors = schema.load(input_json)
72 |
73 | if errors:
74 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors)
75 |
76 | if isinstance(datas, list):
77 | [data.save() for data in datas]
78 | else:
79 | datas.save()
80 | return self.response_json(self.HttpErrorCode.SUCCESS)
81 |
82 | def put(self, model_id=None):
83 | """ PUT
84 |
85 | Arguments:
86 | model_id {int} -- resource id
87 | """
88 | input_json = request.get_json()
89 | if model_id:
90 | datas, errors = self.schema_class().load(
91 | input_json, instance=self.model_class.query.get(model_id), partial=True
92 | )
93 | if errors:
94 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors)
95 | datas.save()
96 | datas = self.schema_class().dump(datas)
97 | else:
98 | datas, errors = self.schema_class().load(input_json, many=True)
99 | if errors:
100 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors)
101 |
102 | [data.save() for data in datas]
103 | datas = self.schema_class().dump(datas, many=True)
104 |
105 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas.data)
106 |
107 | def delete(self, model_id):
108 | """ DELETE
109 |
110 | Arguments:
111 | model_id {int} -- resource id
112 | """
113 | data = self.model_class.query.get(model_id)
114 | if data:
115 | data.delete()
116 | return self.response_json(self.HttpErrorCode.SUCCESS, 'Success!')
117 | return self.response_json(self.HttpErrorCode.ERROR, 'The data is not exists!')
118 |
119 | @classmethod
120 | def register_api(cls, app):
121 | if hasattr(cls, 'url'):
122 | url = cls.url or '/' + cls.__name__.lower()
123 | else:
124 | url = cls.__name__.lower()
125 |
126 | view_func = cls.as_view(cls.__name__.lower())
127 |
128 | for method in RESTFUL_METHODS:
129 | if method not in cls.access_methods:
130 | continue
131 |
132 | method_params = inspect.signature(getattr(cls, method.lower())).parameters
133 | defaults = {
134 | param_key: param_value.default
135 | for param_key, param_value in method_params.items()
136 | if param_key != 'self' and param_value.empty is not param_value.default
137 | }
138 | pk = list(method_params.keys())[1] if len(method_params.keys()) > 1 else ''
139 |
140 | if defaults:
141 | app.add_url_rule(
142 | url, defaults=defaults,
143 | view_func=view_func,
144 | methods=[method],
145 | )
146 | app.add_url_rule(
147 | '%s/<%s:%s>' % (url, cls.pk_type, pk),
148 | view_func=view_func,
149 | methods=[method]
150 | )
151 | elif len(method_params) > 1:
152 | app.add_url_rule(
153 | '%s/<%s:%s>' % (url, cls.pk_type, pk),
154 | view_func=view_func,
155 | methods=[method]
156 | )
157 | else:
158 | app.add_url_rule(
159 | url,
160 | view_func=view_func, methods=[method]
161 | )
162 |
163 | return app
164 |
--------------------------------------------------------------------------------
/src/seed/api/common.py:
--------------------------------------------------------------------------------
1 | from flask.views import MethodView
2 | from flask import jsonify, request
3 |
4 | from seed.utils.response import response, HttpErrorCode
5 |
6 |
7 | class _MethodView(MethodView):
8 | """ MethodView base package
9 | """
10 | __abstract__ = True
11 |
12 | HttpErrorCode = HttpErrorCode
13 |
14 | def __init__(self, *args, **kwargs):
15 | super(_MethodView, self).__init__(*args, **kwargs)
16 |
17 | def response_json(self, code, msg=None, data={}):
18 | return jsonify(response(code, msg, data))
19 |
20 | @classmethod
21 | def register_api(cls, app):
22 | raise NotImplementedError()
23 |
--------------------------------------------------------------------------------
/src/seed/api/endpoints/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/api/endpoints/__init__.py
--------------------------------------------------------------------------------
/src/seed/api/endpoints/account.py:
--------------------------------------------------------------------------------
1 | from flask import g
2 | from seed.schema.base import BaseSchema
3 | from seed.api._base import RestfulBaseView, HttpMethods
4 |
5 | from seed.models.account import Account as AccountModel
6 | from seed.utils.auth import api_require_user, require_super_admin
7 |
8 |
9 | class AccountSchema(BaseSchema):
10 | class Meta:
11 | model = AccountModel
12 |
13 |
14 | class Account(RestfulBaseView):
15 | """ Account
16 | """
17 | model_class = AccountModel
18 | schema_class = AccountSchema
19 |
20 | access_methods = [HttpMethods.GET, HttpMethods.PUT, HttpMethods.DELETE]
21 |
22 | decorators = [api_require_user]
23 |
24 | def put(self, model_id=None):
25 | if not require_super_admin() and g.user.id != int(model_id):
26 | return self.response_json(self.HttpErrorCode.FORBIDDEN)
27 |
28 | return super(Account, self).put(model_id=model_id)
29 |
30 | def delete(self, model_id):
31 | if not require_super_admin():
32 | return self.response_json(self.HttpErrorCode.FORBIDDEN)
33 |
34 | return super(Account, self).delete(model_id=model_id)
35 |
--------------------------------------------------------------------------------
/src/seed/api/endpoints/buser.py:
--------------------------------------------------------------------------------
1 | from flask import request, g
2 | from sqlalchemy import and_
3 |
4 | from seed.schema.base import BaseSchema
5 | from seed.api._base import RestfulBaseView, HttpMethods
6 | from seed.models import BUser as BUserModel
7 | from seed.models import Account as AccountModel
8 | from seed.models import BManager as BManagerModel
9 |
10 | from seed.models.role import Role
11 | from seed.models.buserrole import BUserRole
12 |
13 | from seed.utils.auth import api_require_admin
14 |
15 |
16 | class BUserSchema(BaseSchema):
17 | class Meta:
18 | model = BUserModel
19 | include_fk = True
20 |
21 |
22 | class Buser(RestfulBaseView):
23 | """ 用户业务映射关系添加
24 | """
25 | model_class = BUserModel
26 | schema_class = BUserSchema
27 | decorators = [api_require_admin]
28 |
29 | def get(self, bussiness_id):
30 | users = self.session.query(BUserModel.id, AccountModel)\
31 | .join(AccountModel, AccountModel.id == BUserModel.user_id)\
32 | .filter(BUserModel.bussiness_id == bussiness_id)\
33 | .all()
34 |
35 | datas = []
36 | for user in users:
37 | data = user.Account.row2dict()
38 | data['account_id'], data['id'] = data['id'], user.id
39 | data['brole'] = self._get_role(data['id'])
40 |
41 | datas.append(data)
42 |
43 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas)
44 |
45 | def _get_role(self, uid):
46 | roles = self.session.query(Role)\
47 | .join(BUserRole, and_(
48 | BUserRole.role_id == Role.id,
49 | BUserRole.user_id == uid,
50 | BUserRole.bussiness_id == g.bussiness_id)
51 | ).all()
52 | roles = [role.row2dict() for role in roles]
53 | return roles
54 |
55 |
56 | class UnBuserList(RestfulBaseView):
57 | """ 不属于当前业务的用户名单
58 | """
59 | url = 'un_busers'
60 | decorators = [api_require_admin]
61 | access_methods = [HttpMethods.GET]
62 |
63 | def get(self):
64 |
65 | in_user_query = self.session.query(BUserModel.user_id)\
66 | .filter(BUserModel.bussiness_id == g.bussiness_id)
67 |
68 | b_managers_query = self.session.query(BManagerModel.user_id)\
69 | .filter(BManagerModel.bussiness_id == g.bussiness_id)
70 |
71 | users = self.session.query(AccountModel)\
72 | .filter(~AccountModel.id.in_(in_user_query))\
73 | .filter(~AccountModel.id.in_(b_managers_query))
74 |
75 | users = [user.row2dict() for user in users]
76 | return self.response_json(self.HttpErrorCode.SUCCESS, data=users)
77 |
--------------------------------------------------------------------------------
/src/seed/api/endpoints/buserrole.py:
--------------------------------------------------------------------------------
1 | from flask import request, g
2 |
3 | from seed.schema.base import BaseSchema
4 | from seed.api._base import RestfulBaseView
5 | from seed.models.buserrole import BUserRole as BUserRoleModel
6 | from seed.models.role import Role as RoleModel
7 | from seed.utils.auth import api_require_login
8 |
9 |
10 | class BUserRoleSchema(BaseSchema):
11 | class Meta:
12 | model = BUserRoleModel
13 | include_fk = True
14 |
15 |
16 | class BUserRole(RestfulBaseView):
17 | """ 用户角色
18 | """
19 | model_class = BUserRoleModel
20 | schema_class = BUserRoleSchema
21 | decorators = [api_require_login]
22 |
23 | def get(self, model_id):
24 | query_session = self.session.query(self.model_class)
25 | datas = query_session.filter(self.model_class.user_id == model_id).all()
26 | datas = [row.row2dict() for row in datas] if datas else []
27 |
28 | roles = RoleModel.get_roles(g.bussiness_id)
29 | role_id_map = {role.id: role.role for role in roles}
30 |
31 | for data in datas:
32 | data['role'] = role_id_map.get(data['role_id'], '未知')
33 |
34 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas)
35 |
36 | def put(self, model_id):
37 | input_json = request.get_json()
38 |
39 | # 删除旧数据
40 | query_session = self.session.query(self.model_class)
41 | query_session.filter(self.model_class.user_id == model_id).delete()
42 | self.session.commit()
43 |
44 | # 添加新数据
45 | datas, errors = self.schema_class().load(input_json, many=True)
46 | if errors:
47 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors)
48 | [data.save() for data in datas]
49 |
50 | datas = [row.row2dict() for row in datas] if datas else []
51 |
52 | roles = RoleModel.get_roles(g.bussiness_id)
53 | role_id_map = {role.id: role.role for role in roles}
54 |
55 | for data in datas:
56 | data['role'] = role_id_map.get(data['role_id'], '未知')
57 |
58 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas)
--------------------------------------------------------------------------------
/src/seed/api/endpoints/buserselect.py:
--------------------------------------------------------------------------------
1 | from flask import request, g
2 | from seed.cache.user_bussiness import UserBussinessCache
3 | from seed.api._base import RestfulBaseView
4 |
5 | from seed.utils.permissions import has_bussiness_permission
6 |
7 | from seed.utils.auth import api_require_login
8 |
9 |
10 | class BUserSelect(RestfulBaseView):
11 | """ 用户当前选择业务
12 | """
13 | decorators = [api_require_login]
14 |
15 | def get(self):
16 | current_bussiness_id = g.bussiness_id
17 | return self.response_json(
18 | self.HttpErrorCode.SUCCESS,
19 | data={'bussiness_id': current_bussiness_id}
20 | )
21 |
22 | def post(self):
23 | input_json = request.get_json()
24 | bussiness_id = input_json['bussiness_id']
25 | if not has_bussiness_permission(g.user, bussiness_id):
26 | return self.response_json(self.HttpErrorCode.FORBIDDEN)
27 |
28 | UserBussinessCache().set(g.user.id, bussiness_id)
29 | return self.response_json(self.HttpErrorCode.SUCCESS)
--------------------------------------------------------------------------------
/src/seed/api/endpoints/bussiness.py:
--------------------------------------------------------------------------------
1 | from flask import request, g
2 |
3 | from seed.schema.base import BaseSchema
4 | from seed.api._base import RestfulBaseView
5 | from seed.models import Bussiness as BussinessModel
6 | from seed.models import BManager as BManagerModel
7 | from seed.models import Account as AccountModel
8 |
9 | from seed.utils.permissions import get_permission_datas_by_user
10 | from seed.utils.helper import common_batch_crud
11 | from seed.utils.auth import api_require_login
12 |
13 |
14 | class BussinessSchema(BaseSchema):
15 | class Meta:
16 | model = BussinessModel
17 |
18 |
19 | class BManagerSchema(BaseSchema):
20 | class Meta:
21 | model = BManagerModel
22 | include_fk = True
23 |
24 |
25 | class Bussiness(RestfulBaseView):
26 | """ bussiness
27 | """
28 | model_class = BussinessModel
29 | schema_class = BussinessSchema
30 | decorators = [api_require_login]
31 |
32 | def get(self, bussiness_id=None):
33 | """ GET
34 | """
35 | if bussiness_id:
36 | data = self.session.query(
37 | self.model_class
38 | ).filter_by(id=bussiness_id).first()
39 | data = data.row2dict() if data else {}
40 | return self.response_json(self.HttpErrorCode.SUCCESS, data=data)
41 | else:
42 | # 所有业务
43 | permission_datas, un_permission_datas = get_permission_datas_by_user(g.user)
44 |
45 | bmanagers = self.session.query(
46 | BManagerModel.id, BManagerModel.bussiness_id,
47 | BManagerModel.user_id, AccountModel.name
48 | )\
49 | .join(AccountModel, BManagerModel.user_id == AccountModel.id)\
50 | .as_list()
51 |
52 | b_managers_map = {}
53 | for bmanager in bmanagers:
54 | b_managers_map.setdefault(bmanager['bussiness_id'], []).append(bmanager)
55 |
56 | for data in permission_datas:
57 | data['managers'] = b_managers_map.get(data['id'], [])
58 | for data in un_permission_datas:
59 | data['managers'] = b_managers_map.get(data['id'], [])
60 |
61 | datas = {'my_bussiness': permission_datas, 'other_bussiness': un_permission_datas}
62 |
63 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas)
64 |
65 | def post(self):
66 | """ POST
67 | """
68 | input_json = request.get_json()
69 |
70 | schema = self.schema_class()
71 | bussiness, errors = schema.load(input_json)
72 |
73 | if errors:
74 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors)
75 | bussiness.save()
76 |
77 | # 管理员增改删
78 | managers = input_json.get('managers', [])
79 | for manager in managers:
80 | manager['bussiness_id'] = bussiness.id
81 |
82 | common_batch_crud(BManagerSchema, BManagerModel, managers)
83 |
84 | return self.response_json(self.HttpErrorCode.SUCCESS, data={'bussiness_id': bussiness.id})
85 |
86 | put = post
87 |
--------------------------------------------------------------------------------
/src/seed/api/endpoints/databases.py:
--------------------------------------------------------------------------------
1 | from flask import request, g
2 |
3 | from seed.schema.base import BaseSchema
4 | # from seed.api._base import RestfulBaseView
5 | from seed.api.endpoints.buser import Buser
6 | from seed.models.databases import Databases as DatabasesModel
7 | from seed.utils.auth import api_require_admin
8 | from seed.utils.helper import common_batch_crud
9 |
10 |
11 | class DatabasesSchema(BaseSchema):
12 | class Meta:
13 | model = DatabasesModel
14 | include_fk = True
15 |
16 |
17 | class Databases(Buser):
18 | """ 数据库设置
19 | """
20 | model_class = DatabasesModel
21 | schema_class = DatabasesSchema
22 | decorators = [api_require_admin]
23 |
24 | def get(self, bussiness_id):
25 | """ 通过业务ID得到数据库列表
26 | """
27 | query_session = self.session.query(self.model_class)
28 | datas = query_session.filter(self.model_class.bussiness_id == bussiness_id).all()
29 | datas = [row.row2dict() for row in datas] if datas else []
30 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas)
31 |
32 | def put(self, bussiness_id):
33 | input_json = request.get_json()
34 |
35 | for database in input_json:
36 | database['bussiness_id'] = bussiness_id
37 |
38 | databases = common_batch_crud(DatabasesSchema, DatabasesModel, input_json)
39 |
40 | return self.response_json(self.HttpErrorCode.SUCCESS, data=databases)
--------------------------------------------------------------------------------
/src/seed/api/endpoints/filters.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from flask import request, g
4 | from marshmallow import pre_load, post_dump
5 |
6 | from seed.schema.base import BaseSchema
7 |
8 | from seed.api._base import RestfulBaseView
9 |
10 | from seed.models.filters import Filters as FiltersModel
11 | from seed.utils.auth import api_require_login
12 |
13 |
14 | class FilterSchema(BaseSchema):
15 | class Meta:
16 | model = FiltersModel
17 | include_fk = True
18 |
19 | @post_dump(pass_many=True)
20 | def dumps_index(self, in_data, many):
21 | if many:
22 | for data in in_data:
23 | data['conditions'] = json.loads(data['conditions'])
24 | data['cascades'] = json.loads(data['cascades'] or '{}')
25 | else:
26 | in_data['conditions'] = json.loads(in_data['conditions'])
27 | in_data['cascades'] = json.loads(in_data['cascades'] or '{}')
28 |
29 | return in_data
30 |
31 | @pre_load(pass_many=True)
32 | def loads_conditions(self, out_data, many):
33 | if many:
34 | for data in out_data:
35 | data['conditions'] = json.dumps(data['conditions'])
36 | data['cascades'] = json.dumps(data.get('cascades', {}))
37 | else:
38 | out_data['conditions'] = json.dumps(out_data['conditions'])
39 | out_data['cascades'] = json.dumps(out_data.get('cascades', {}))
40 | return out_data
41 |
42 |
43 | class Filters(RestfulBaseView):
44 | """ 过滤组件
45 | """
46 | model_class = FiltersModel
47 | schema_class = FilterSchema
48 | decorators = [api_require_login]
49 |
--------------------------------------------------------------------------------
/src/seed/api/endpoints/menu.py:
--------------------------------------------------------------------------------
1 | from flask import request, g
2 |
3 | from seed.schema.base import BaseSchema
4 | from seed.api._base import RestfulBaseView, HttpMethods
5 | from seed.models.menu import Menu as MenuModel
6 | from seed.utils.auth import api_require_admin
7 |
8 |
9 | class MenuSchema(BaseSchema):
10 | class Meta:
11 | model = MenuModel
12 |
13 |
14 | class Menu(RestfulBaseView):
15 | """ menu
16 | """
17 | model_class = MenuModel
18 | schema_class = MenuSchema
19 |
20 | decorators = [api_require_admin]
21 |
22 | access_methods = [HttpMethods.GET, HttpMethods.POST]
23 |
24 | def get(self):
25 | """ GET
26 | """
27 | query_session = self.session.query(self.model_class).filter(self.model_class.bussiness_id==g.bussiness_id)
28 | menu_data = query_session.all()
29 | menus = self._encode_menus(menu_data)
30 | return self.response_json(self.HttpErrorCode.SUCCESS, data=menus)
31 |
32 | def post(self):
33 | """ 更新菜单结构
34 | """
35 | menus = request.get_json()
36 | self._decode_menus(menus)
37 |
38 | query_session = self.session.query(self.model_class).filter(self.model_class.bussiness_id==g.bussiness_id)
39 | menu_data = query_session.all()
40 | menus = self._encode_menus(menu_data)
41 |
42 | return self.response_json(self.HttpErrorCode.SUCCESS, data=menus)
43 |
44 | def _encode_menus(self, menu_data):
45 | menu_data = {'-'.join([str(row.parent_id), str(row.left_id)]): row.row2dict() for row in menu_data}
46 |
47 | menus = {'id': 0}
48 | middle_menu = [menus]
49 | while middle_menu:
50 | current_menu = middle_menu.pop()
51 | parent_id, left_id = current_menu['id'], 0
52 |
53 | while True:
54 | current_key = '-'.join([str(parent_id), str(left_id)])
55 | if current_key not in menu_data:
56 | break
57 | current_menu.setdefault('sub_menus', []).append(menu_data[current_key])
58 | middle_menu.append(menu_data[current_key])
59 | left_id = menu_data[current_key]['id']
60 |
61 | return menus.get('sub_menus', [])
62 |
63 | def _decode_menus(self, menus, parent_id=0, left_id=0):
64 | if not menus:
65 | return
66 |
67 | for menu in menus:
68 | # 更新或插入新的菜单
69 | # 获取到菜单对应的ID
70 | left_id = current_id = self._update_menu_item(menu, parent_id, left_id)
71 | self._decode_menus(menu.get('sub_menus', []), parent_id=current_id, left_id=0)
72 |
73 | def _update_menu_item(self, menu, parent_id, left_id):
74 | menu.update({
75 | 'parent_id': parent_id,
76 | 'left_id': left_id
77 | })
78 | schema = self.schema_class()
79 | datas, errors = schema.load(menu)
80 | if menu.get('status', 0) == -1:
81 | datas.delete()
82 | else:
83 | datas.save()
84 | return datas.id
85 |
--------------------------------------------------------------------------------
/src/seed/api/endpoints/pages.py:
--------------------------------------------------------------------------------
1 | from flask import request, g
2 | from sqlalchemy.exc import InvalidRequestError
3 |
4 | from seed.api._base import RestfulBaseView, HttpMethods
5 |
6 | from seed.models.filters import Filters as FiltersModel
7 | from seed.api.endpoints.filters import FilterSchema
8 | from seed.models.panels import Panels as PanelsModel
9 | from seed.api.endpoints.panels import PanelSchema
10 |
11 | from seed.utils.helper import common_batch_crud
12 |
13 |
14 | class Pages(RestfulBaseView):
15 | """ 页面
16 | """
17 | access_methods = [HttpMethods.GET, HttpMethods.PUT]
18 |
19 | def get(self, page_id):
20 | global_filters = self._get_global_filters(page_id)
21 | panels = self._get_panels(page_id)
22 |
23 | data = {
24 | "global_filters": global_filters,
25 | "panels": panels
26 | }
27 | return self.response_json(self.HttpErrorCode.SUCCESS, data=data)
28 |
29 | def put(self, page_id):
30 | input_data = request.get_json()
31 |
32 | global_filters, panels = input_data['global_filters'], input_data['panels']
33 | for global_filter in global_filters:
34 | global_filter['bussiness_id'] = g.bussiness_id
35 | global_filters = common_batch_crud(FilterSchema, FiltersModel, global_filters)
36 |
37 | panels = self._panels_batch_crud(PanelSchema, panels)
38 |
39 | data = {'global_filters': global_filters, 'panels': panels}
40 |
41 | return self.response_json(self.HttpErrorCode.SUCCESS, data=data)
42 |
43 | def _get_global_filters(self, page_id):
44 | """ 获取页面全局过滤组件数据
45 | """
46 | query_session = self.session.query(FiltersModel)
47 |
48 | filters = query_session.filter_by(page_id=page_id, dtype='page').all()
49 | filters, errors = FilterSchema(many=True, exclude=FiltersModel.column_filter).dump(filters)
50 |
51 | return filters
52 |
53 | def _get_panels(self, page_id):
54 | """ 获取过滤组件数据
55 | """
56 | panel_query_session = self.session.query(PanelsModel)
57 | panels = panel_query_session.filter_by(page_id=page_id).order_by(PanelsModel.sort).all()
58 | panels, errors = PanelSchema(many=True, exclude=PanelsModel.column_filter).dump(panels)
59 |
60 | filter_query_session = self.session.query(FiltersModel)
61 | filters = filter_query_session.filter_by(page_id=page_id, dtype='model').all()
62 | filters, errors = FilterSchema(many=True, exclude=FiltersModel.column_filter).dump(filters)
63 |
64 | filters_pid_map = {}
65 | for filter in filters:
66 | filters_pid_map.setdefault(filter['belong_id'], []).append(filter)
67 |
68 | for panel in panels:
69 | panel['filters'] = filters_pid_map.get(panel['id'], [])
70 |
71 | return panels
72 |
73 | def _panels_batch_crud(self, panel_schema, panels):
74 | schema_instance = panel_schema()
75 |
76 | # 删除panels
77 | saved_panels = []
78 | for panel in panels:
79 | one_panel, errors = schema_instance.load(panel)
80 | if errors:
81 | raise Exception(errors)
82 |
83 | if panel.get('status') == -1:
84 | # 删除对应的panel
85 | try:
86 | one_panel.delete()
87 | except InvalidRequestError:
88 | pass
89 |
90 | else:
91 | # 新增和修改对应的panel
92 | one_panel.save()
93 | panel_filters = panel.get('filters', [])
94 |
95 | for filter in panel_filters:
96 | filter['belong_id'] = one_panel.id
97 | filter['bussiness_id'] = g.bussiness_id
98 | filter['page_id'] = one_panel.page_id
99 |
100 | panel_filters = common_batch_crud(FilterSchema, FiltersModel, panel_filters)
101 | panel, errors = panel_schema(exclude=PanelsModel.column_filter).dump(one_panel)
102 | panel['filters'] = panel_filters
103 | saved_panels.append(panel)
104 | return saved_panels
105 |
--------------------------------------------------------------------------------
/src/seed/api/endpoints/panels.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from flask import request, g
4 | from marshmallow import pre_load, post_dump
5 |
6 | from seed.schema.base import BaseSchema
7 | from seed.api._base import RestfulBaseView
8 |
9 | from seed.models.panels import Panels as PanelsModel
10 | from seed.utils.auth import api_require_admin
11 |
12 |
13 | class PanelSchema(BaseSchema):
14 | class Meta:
15 | model = PanelsModel
16 | include_fk = True
17 |
18 | @post_dump(pass_many=True)
19 | def dumps_indexs(self, in_data, many):
20 | if many:
21 | for data in in_data:
22 | data['indexs'] = json.loads(data['indexs'])
23 | data['dimensions'] = json.loads(data['dimensions'])
24 | else:
25 | in_data['indexs'] = json.loads(in_data['indexs'])
26 | in_data['dimensions'] = json.loads(in_data['dimensions'])
27 |
28 | return in_data
29 |
30 | @pre_load
31 | def loads_indexs(self, out_data):
32 | out_data['indexs'] = json.dumps(out_data['indexs'])
33 | out_data['dimensions'] = json.dumps(out_data['dimensions'])
34 | return out_data
35 |
36 |
37 | class Panels(RestfulBaseView):
38 | """ 数据面板
39 | """
40 | model_class = PanelsModel
41 | schema_class = PanelSchema
42 | decorators = [api_require_admin]
--------------------------------------------------------------------------------
/src/seed/api/endpoints/queries.py:
--------------------------------------------------------------------------------
1 |
2 | from flask import request
3 |
4 | from seed.schema.base import BaseSchema
5 | from seed.api._base import RestfulBaseView, HttpMethods
6 |
7 | from seed.api.endpoints.panels import PanelSchema, PanelsModel
8 | from seed.api.endpoints.filters import FilterSchema, FiltersModel
9 | from seed.api.endpoints.databases import DatabasesSchema
10 |
11 | from seed.models._base import session
12 | from seed.models.databases import Databases
13 | from seed.models.account import Account
14 |
15 | from seed.utils.auth import api_require_login
16 | from seed.utils.database import get_db_instance
17 |
18 | from seed.libs.data_access.app import DataAccess
19 | from seed.libs.filter_access.app import FilterAccess
20 |
21 | from seed.cache.session import SessionCache
22 |
23 |
24 | class QueryData(RestfulBaseView):
25 | """ 获取数据
26 | """
27 | url = 'query_data'
28 | decorators = [api_require_login]
29 |
30 | access_methods = [HttpMethods.POST]
31 |
32 | def post(self, panel_id=None):
33 | try:
34 | query_params = request.get_json()
35 | except:
36 | query_params = {}
37 |
38 | # 用户名称添加到参数中
39 | session_token = request.cookies.get('session_token', '')
40 | user = None
41 | query_params.get("query", {}).setdefault("isadmin", 0)
42 | if session_token:
43 | user_id = SessionCache().get_user_id_by_token(session_token)
44 | user = Account.query.filter_by(id=user_id).first()
45 | query_params.get("query", {}).setdefault("username", user.name)
46 | else:
47 | username = request.cookies.get('admin_name', None)
48 | user = Account.query.filter_by(account=username).first()
49 | query_params.get("query", {}).setdefault("username", username)
50 |
51 | if user and user.role in ('super_admin', 'admin'):
52 | query_params.get("query", {}).update({"isadmin": 1})
53 |
54 | if panel_id:
55 | panel_data = self.session.query(PanelsModel).filter_by(id=panel_id).first()
56 | panel_data, errors = PanelSchema(exclude=PanelsModel.column_filter).dump(panel_data)
57 | if errors:
58 | return self.response_json(self.HttpErrorCode.ERROR, msg=str(errors))
59 | else:
60 | panel_data = {}
61 |
62 | panel_data.update(query_params)
63 | try:
64 | dtype, db = get_db_by_id(panel_data['db_source'])
65 | query_datas = DataAccess(dtype, db, **panel_data).get_datas()
66 | except Exception as e:
67 | error_message = str(e)
68 | return self.response_json(self.HttpErrorCode.ERROR, msg=error_message)
69 |
70 | return self.response_json(self.HttpErrorCode.SUCCESS, data=query_datas)
71 |
72 |
73 | class QueryFilters(RestfulBaseView):
74 | """ 获取Filters数据
75 | """
76 | url = 'query_filters'
77 | decorators = [api_require_login]
78 |
79 | access_methods = [HttpMethods.POST]
80 |
81 | def post(self, filter_id=None):
82 | try:
83 | query_params = request.get_json()
84 | except:
85 | query_params = {}
86 |
87 | if filter_id:
88 | filter_data = self.session.query(FiltersModel).filter_by(id=filter_id).first()
89 | filter_data, errors = FilterSchema(exclude=FiltersModel.column_filter).dump(filter_data)
90 | if errors:
91 | return self.response_json(self.HttpErrorCode.ERROR, msg=str(errors))
92 | else:
93 | filter_data = {}
94 |
95 | filter_data.update(query_params)
96 |
97 | if not filter_data:
98 | return self.response_json(self.HttpErrorCode.ERROR, msg='过滤设置不能为空')
99 |
100 | if filter_data['condition_type'] in ('sql'):
101 | try:
102 | dtype, db = get_db_by_id(filter_data['db_source'])
103 | except Exception as e:
104 | error_message = str(e)
105 | return self.response_json(self.HttpErrorCode.ERROR, msg=error_message)
106 |
107 | filter_data['conditions'] = FilterAccess(db, filter_data['conditions'], filter_data.get('query', {})).query_datas()
108 |
109 | return self.response_json(self.HttpErrorCode.SUCCESS, data=filter_data)
110 |
111 |
112 | def get_db_by_id(db_source):
113 | db_data = session.query(Databases).filter_by(id=db_source).first()
114 | db_conf, errors = DatabasesSchema().dump(db_data)
115 | if errors:
116 | raise Exception(errors)
117 |
118 | db = get_db_instance(**db_conf)
119 | return db_conf['dtype'], db
120 |
--------------------------------------------------------------------------------
/src/seed/api/endpoints/role.py:
--------------------------------------------------------------------------------
1 | from seed.schema.base import BaseSchema
2 | from seed.api._base import RestfulBaseView
3 | from seed.models.role import Role as RoleModel
4 | from seed.utils.auth import api_require_admin
5 |
6 |
7 | class RoleSchema(BaseSchema):
8 | class Meta:
9 | model = RoleModel
10 | include_fk = True
11 |
12 |
13 | class Role(RestfulBaseView):
14 | """ role
15 | """
16 | model_class = RoleModel
17 | schema_class = RoleSchema
18 |
19 | decorators = [api_require_admin]
--------------------------------------------------------------------------------
/src/seed/api/endpoints/rolemenu.py:
--------------------------------------------------------------------------------
1 | import json
2 | from flask import request, g
3 | from sqlalchemy import and_
4 |
5 | from seed.schema.base import BaseSchema
6 | from seed.api._base import RestfulBaseView, HttpMethods
7 | from seed.models.rolemenu import RoleMenu as RoleMenuModel
8 | from seed.models.menu import Menu as MenuModel
9 | from seed.utils.auth import api_require_admin
10 |
11 |
12 | class RoleMenuSchema(BaseSchema):
13 | class Meta:
14 | model = RoleMenuModel
15 | include_fk = True
16 |
17 |
18 | class RoleMenu(RestfulBaseView):
19 | """ 角色菜单设置
20 | """
21 | model_class = RoleMenuModel
22 | schema_class = RoleMenuSchema
23 |
24 | decorators = [api_require_admin]
25 |
26 | access_methods = [HttpMethods.GET, HttpMethods.PUT]
27 |
28 | def get(self, role_id):
29 | """ GET
30 | """
31 | role_menu = self._get_role_menu(role_id)
32 |
33 | menus = self._encode_menus(role_menu)
34 |
35 | return self.response_json(self.HttpErrorCode.SUCCESS, data=menus)
36 |
37 | def put(self, role_id):
38 | request_json = request.get_json()
39 | menus = request_json['menu']
40 | self._decode_menus(menus, role_id)
41 | return self.response_json(self.HttpErrorCode.SUCCESS)
42 |
43 | def _get_role_menu(self, model_id):
44 | role_datas = self.session.query(RoleMenuModel)\
45 | .filter(RoleMenuModel.role_id == model_id, RoleMenuModel.bussiness_id == g.bussiness_id).all()
46 | role_data_map = {role_data.menu_id: role_data for role_data in role_datas}
47 |
48 | menu_datas = self.session.query(MenuModel)\
49 | .filter(MenuModel.bussiness_id == g.bussiness_id).all()
50 |
51 | menu_datas_with_permission = {}
52 |
53 | for menu_data in menu_datas:
54 | menu_data = menu_data.row2dict()
55 | role_data = role_data_map[menu_data['id']] if menu_data['id'] in role_data_map else None
56 |
57 | menu_data['role_permission'] = role_data.role_permission if role_data else False
58 | menu_data['menu_id'] = menu_data['id']
59 | if role_data:
60 | menu_data['id'] = role_data.id
61 | else:
62 | menu_data['id'] = None
63 |
64 | menu_datas_with_permission['-'.join([str(menu_data['parent_id']), str(menu_data['left_id'])])] = menu_data
65 |
66 | return menu_datas_with_permission
67 |
68 | def _encode_menus(self, menu_data):
69 | menu_list = []
70 | for key, value in menu_data.items():
71 | if not value.get('id'):
72 | value.pop('id')
73 |
74 | menu_list.append(value)
75 |
76 | return menu_list
77 |
78 | def _decode_menus(self, menus, role_id, parent_id=0, left_id=0):
79 | if not menus:
80 | return
81 |
82 | for menu in menus:
83 | # 更新或插入新的菜单
84 | # 获取到菜单对应的ID
85 | # if menu.get('role_permission', False):
86 | self._insert_or_update_menu(menu, role_id, menu.get('role_permission', False))
87 | current_id = menu['menu_id']
88 | self._decode_menus(menu.get('sub_menus', []), role_id=role_id, parent_id=current_id, left_id=0)
89 |
90 | def _insert_or_update_menu(self, menu, role_id, role_permission):
91 | role_menu = {
92 | "role_id": role_id,
93 | "menu_id": menu['menu_id'],
94 | "role_permission": role_permission
95 | }
96 | if 'id' in menu:
97 | role_menu['id'] = menu['id']
98 | schema = self.schema_class()
99 | datas, errors = schema.load(role_menu)
100 | datas.save()
101 | return datas.id
102 |
--------------------------------------------------------------------------------
/src/seed/api/endpoints/user.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import and_
2 | from flask import current_app, g
3 |
4 | from seed.schema.base import BaseSchema
5 | from seed.api._base import RestfulBaseView, HttpMethods
6 | from seed.models.account import Account as AccountModel
7 | from seed.models.role import Role
8 | from seed.models.buserrole import BUserRole
9 | from seed.models.bmanager import BManager
10 | from seed.models.rolemenu import RoleMenu
11 | from seed.models.buser import BUser
12 | from seed.models.menu import Menu as MenuModel
13 | from seed.utils.auth import api_require_login, require_admin
14 |
15 |
16 | class UserSchema(BaseSchema):
17 | class Meta:
18 | model = AccountModel
19 |
20 |
21 | class User(RestfulBaseView):
22 | """ 用户相关
23 | """
24 | model_class = AccountModel
25 | schema_class = UserSchema
26 | decorators = [api_require_login]
27 |
28 | access_methods = [HttpMethods.GET]
29 |
30 | def get(self):
31 | """ 获取用户信息, 如果是SSO的校验
32 | 成功后自动添加用户信息到用户列表
33 | """
34 | user = g.user.row2dict()
35 | user['brole'] = self._get_role(g.user.id)
36 |
37 | return self.response_json(self.HttpErrorCode.SUCCESS, data=user)
38 |
39 | def _get_role(self, uid):
40 | roles = self.session.query(Role.role)\
41 | .join(BUserRole, and_(
42 | BUserRole.role_id == Role.id,
43 | BUserRole.user_id == uid,
44 | BUserRole.bussiness_id == g.bussiness_id)
45 | ).all()
46 | return roles
47 |
48 |
49 | class UserMenu(RestfulBaseView):
50 | """ 获取当前用户的菜单
51 | """
52 | url = '/user/menu'
53 |
54 | decorators = [api_require_login]
55 |
56 | access_methods = [HttpMethods.GET]
57 |
58 | def get(self):
59 | if require_admin():
60 | # 如果是业务管理员以上,返回当前业务的所有菜单
61 | query_session = self.session.query(MenuModel, "True").filter(MenuModel.bussiness_id == g.bussiness_id)
62 | else:
63 | # 其他角色通过关联用户角色表来获取到当前的菜单
64 | query_session = self.session.query(MenuModel, RoleMenu.role_permission)\
65 | .join(RoleMenu, and_(RoleMenu.bussiness_id == g.bussiness_id, MenuModel.id == RoleMenu.menu_id))\
66 | .join(BUser, and_(BUser.bussiness_id==g.bussiness_id, BUser.user_id==g.user.id))\
67 | .join(BUserRole, and_(BUserRole.bussiness_id == g.bussiness_id, RoleMenu.role_id == BUserRole.role_id, BUserRole.user_id==BUser.id))
68 |
69 | menu_data = query_session.all()
70 | menus = self._encode_menus(menu_data)
71 |
72 | return self.response_json(self.HttpErrorCode.SUCCESS, data=menus)
73 |
74 | def _encode_menus(self, menus):
75 | menu_data = []
76 |
77 | for menu, role_permission in menus:
78 | if role_permission:
79 | temp = menu.row2dict()
80 | temp.update({"role_permission": True})
81 | menu_data.append(temp)
82 |
83 | del_list = []
84 | for j in menu_data:
85 | menu_id = j["id"]
86 | for i in menu_data:
87 | if menu_id == i["parent_id"]:
88 | j.setdefault("sub_menus", []).append(i)
89 | del_list.append(i)
90 |
91 | for del_m in del_list:
92 | menu_data.remove(del_m)
93 |
94 | return menu_data
95 |
--------------------------------------------------------------------------------
/src/seed/api/front.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask import Blueprint, render_template, send_from_directory
4 |
5 | from seed.utils.helper import template_folder_path
6 |
7 |
8 | bp = Blueprint('front', __name__)
9 |
10 |
11 | @bp.route('/', defaults={'path': ''})
12 | @bp.route('/')
13 | def index(path):
14 | """
15 | 如果是静态文件则利用seed_from_directory来进行加载
16 | """
17 | if path and os.path.exists(os.path.join(template_folder_path, path)):
18 | return send_from_directory(template_folder_path, path)
19 |
20 | return render_template('index.html')
--------------------------------------------------------------------------------
/src/seed/api/urls.py:
--------------------------------------------------------------------------------
1 | from inspect import isclass
2 |
3 | from flask import Blueprint
4 |
5 | from seed import api
6 | from seed.utils.helper import get_package_members, get_immediate_cls_attr
7 | from seed.api.common import _MethodView
8 |
9 |
10 | def register_api(app):
11 | """ register api interface
12 | """
13 |
14 | predicate = lambda m: isclass(m) and issubclass(m, _MethodView) and not get_immediate_cls_attr(m, '__abstract__')
15 | members = get_package_members(api, predicate, '')
16 | for pre_url, handlers in members.items():
17 | bp = Blueprint(pre_url, __name__)
18 | for handler in set(handlers):
19 | bp = handler.register_api(bp)
20 |
21 | app.register_blueprint(bp, url_prefix=pre_url)
22 |
--------------------------------------------------------------------------------
/src/seed/api/users/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/api/users/__init__.py
--------------------------------------------------------------------------------
/src/seed/api/users/active_account.py:
--------------------------------------------------------------------------------
1 | from flask import request
2 |
3 | from seed.api._base import RestfulBaseView, HttpMethods
4 | from seed.cache.active_account import ActiveAccountCache
5 | from seed.models.account import Account
6 |
7 |
8 | class ActiveAccount(RestfulBaseView):
9 | """ 激活账户
10 | """
11 | url = 'active_account'
12 | access_methods = [HttpMethods.GET]
13 |
14 | def get(self):
15 | active_token = request.args.get('active_token')
16 | if not active_token:
17 | return self.response_json(
18 | self.HttpErrorCode.PARAMS_VALID_ERROR,
19 | msg='激活token参数缺失'
20 | )
21 |
22 | user_id = ActiveAccountCache().get_user_by_active_token(active_token)
23 | if not user_id:
24 | return self.response_json(
25 | self.HttpErrorCode.PARAMS_VALID_ERROR,
26 | msg='激活token已经失效'
27 | )
28 |
29 | account = self.session.query(Account).filter_by(id=user_id).first()
30 | if not account:
31 | return self.response_json(
32 | self.HttpErrorCode.PARAMS_VALID_ERROR,
33 | msg='激活账户不存在'
34 | )
35 |
36 | account.role = 'user'
37 | account.status = 1
38 | account.save()
39 |
40 | return self.response_json(self.HttpErrorCode.SUCCESS)
41 |
--------------------------------------------------------------------------------
/src/seed/api/users/login.py:
--------------------------------------------------------------------------------
1 | import time
2 | import bcrypt
3 |
4 | from flask import request, make_response, current_app
5 |
6 | from seed.models.account import Account
7 | from seed.cache.session import SessionCache
8 |
9 | from seed.api._base import RestfulBaseView, HttpMethods
10 |
11 |
12 | class Login(RestfulBaseView):
13 | """ 登录
14 | """
15 | access_methods = [HttpMethods.POST]
16 |
17 | def post(self):
18 | """ POST
19 | """
20 | input_json = request.get_json()
21 | if 'account' not in input_json:
22 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, '账号不能为空')
23 |
24 | if 'password' not in input_json:
25 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, '密码不能为空')
26 |
27 | account, password = input_json.get('account'), input_json.get('password')
28 |
29 | # 获取账号
30 | account = Account.query.filter_by(account=account).first()
31 |
32 | if not bcrypt.checkpw(password.encode('utf-8'), account.password.encode('utf-8')):
33 | return self.response_json(self.HttpErrorCode.AUTHORIZED_ERROR)
34 |
35 | # Cookie设置
36 | res = make_response(self.response_json(self.HttpErrorCode.SUCCESS))
37 |
38 | session_token = SessionCache().create_session(account.id)
39 | res.set_cookie(
40 | 'session_token',
41 | session_token,
42 | expires=time.time()+24*60*60,
43 | domain=request.host
44 | )
45 |
46 | return res
47 |
48 |
49 | class Logout(RestfulBaseView):
50 | """ 登出
51 | """
52 | access_methods = [HttpMethods.GET]
53 |
54 | def get(self):
55 | session_token = request.cookies.get('session_token', None)
56 | auth_type = current_app.config["AUTH_TYPE"]
57 | sso_url = current_app.config["SSO_URL"]
58 | login_url = request.host_url + "login"
59 | if sso_url and auth_type == "SSO":
60 | response = make_response(self.response_json(-14, data=sso_url))
61 | else:
62 | response = make_response(self.response_json(self.HttpErrorCode.SUCCESS, data=login_url))
63 |
64 | response.set_cookie("admin_uid", '', expires=0, domain=".oa.com")
65 | response.set_cookie("admin_key", '', expires=0, domain=".oa.com")
66 | if not session_token:
67 | return response
68 |
69 | SessionCache().delete(session_token)
70 |
71 | response.set_cookie('session_token', session_token, expires=0)
72 |
73 | return response
74 |
--------------------------------------------------------------------------------
/src/seed/api/users/register.py:
--------------------------------------------------------------------------------
1 | import bcrypt
2 | from flask import request
3 |
4 | from seed.api._base import RestfulBaseView, HttpMethods
5 | from seed.cache.active_account import ActiveAccountCache
6 | from seed.schema.base import BaseSchema
7 | from seed.models.account import Account
8 | from seed.utils.mail import send_active_email
9 |
10 |
11 | class AccountSchema(BaseSchema):
12 | """ 账户
13 | """
14 | class Meta:
15 | model = Account
16 |
17 |
18 | class Register(RestfulBaseView):
19 | """ 注册
20 | """
21 | access_methods = [HttpMethods.POST]
22 |
23 | def post(self):
24 | """ POST
25 | """
26 | input_json = request.get_json()
27 |
28 | if input_json['password'] != input_json['confirm_password']:
29 | return self.response_json(
30 | self.HttpErrorCode.PARAMS_VALID_ERROR,
31 | msg='密码和确认密码不一致'
32 | )
33 |
34 | if 'active_url' not in input_json:
35 | return self.response_json(
36 | self.HttpErrorCode.PARAMS_VALID_ERROR,
37 | msg='active_url缺失'
38 | )
39 | active_url = input_json['active_url']
40 |
41 | if self.session.query(Account).filter_by(account=input_json['account']).first():
42 | return self.response_json(
43 | self.HttpErrorCode.PARAMS_VALID_ERROR,
44 | msg='账号已经被占用了'
45 | )
46 |
47 | if self.session.query(Account).filter_by(email=input_json['email']).first():
48 | return self.response_json(
49 | self.HttpErrorCode.PARAMS_VALID_ERROR,
50 | msg='邮箱已经被注册了'
51 | )
52 |
53 | input_json['password'] = bcrypt.hashpw(input_json['password'].encode('utf-8'), bcrypt.gensalt())
54 |
55 | account, errors = AccountSchema().load(input_json, partial=True)
56 | if errors:
57 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors)
58 | account.save()
59 |
60 | active_token = ActiveAccountCache().create_active_token(account.id)
61 | redirect_url = '{active_host}/users/active_account&active_token={active_token}'
62 | redirect_url = redirect_url.format(active_host=request.host, active_token=active_token)
63 | try:
64 | send_active_email(account.email, active_url, redirect_url)
65 | except Exception as e:
66 | # 如果邮件发送失败,则放弃邮件验证
67 | print(e)
68 | account.role = 'user'
69 | account.save()
70 |
71 | return self.response_json(self.HttpErrorCode.SUCCESS)
72 |
--------------------------------------------------------------------------------
/src/seed/api/users/reset_password.py:
--------------------------------------------------------------------------------
1 | import bcrypt
2 | from flask import request
3 |
4 | from seed.models.account import Account
5 | from seed.api._base import RestfulBaseView, HttpMethods
6 | from seed.cache.reset_password import ResetPasswordCache
7 | from seed.utils.mail import send_reset_password_email
8 |
9 |
10 | class ForgetPassword(RestfulBaseView):
11 | """ 忘记密码, 发送重置密码的邮件
12 | """
13 | access_methods = [HttpMethods.POST]
14 | url = 'forget_password'
15 |
16 | def post(self):
17 | input_json = request.get_json()
18 | if 'account' not in input_json:
19 | return self.response_json(
20 | self.HttpErrorCode.PARAMS_VALID_ERROR,
21 | msg='account参数缺失'
22 | )
23 | account = input_json['account']
24 |
25 | if 'reset_url' not in input_json:
26 | return self.response_json(
27 | self.HttpErrorCode.PARAMS_VALID_ERROR,
28 | msg='reset_url参数缺失'
29 | )
30 | reset_url = input_json['reset_url']
31 |
32 | account_session = self.session.query(Account)
33 |
34 | if '@' in account:
35 | account = account_session.filter_by(email=account).first()
36 | else:
37 | account = account_session.filter_by(account=account).first()
38 |
39 | if not account:
40 | return self.response_json(
41 | self.HttpErrorCode.PARAMS_VALID_ERROR,
42 | msg='该账户不存在,请查证后再试'
43 | )
44 |
45 | reset_token = ResetPasswordCache().create_active_token(account.id)
46 | reset_redirect_url = '{active_host}/users/active_account&active_token={active_token}'
47 | reset_redirect_url = reset_redirect_url.format(active_host=request.host, reset_token=reset_token)
48 | send_reset_password_email(account.email, reset_url, reset_redirect_url)
49 |
50 | return self.response_json(self.HttpErrorCode.SUCCESS)
51 |
52 |
53 | class ResetPasssword(RestfulBaseView):
54 | """ 重置密码
55 | """
56 | access_methods = [HttpMethods.POST]
57 | url = 'reset_password'
58 |
59 | def post(self):
60 | input_json = request.get_json()
61 | if input_json['password'] != input_json['confirm_password']:
62 | return self.response_json(
63 | self.HttpErrorCode.PARAMS_VALID_ERROR,
64 | msg='密码和确认密码不一致'
65 | )
66 |
67 | if 'reset_token' not in input_json:
68 | return self.response_json(
69 | self.HttpErrorCode.PARAMS_VALID_ERROR,
70 | msg='reset_token参数不存在'
71 | )
72 |
73 | reset_account_id = ResetPasswordCache().get_user_by_active_token(input_json['reset_token'])
74 | account = self.session.query(Account).filter_by(id=reset_account_id).first()
75 |
76 | password = bcrypt.hashpw(input_json['password'].encode('utf-8'), bcrypt.gensalt())
77 | account.password = password
78 | account.save()
79 |
80 | return self.response_json(self.HttpErrorCode.SUCCESS)
81 |
--------------------------------------------------------------------------------
/src/seed/api/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/api/utils/__init__.py
--------------------------------------------------------------------------------
/src/seed/api/utils/dbtest.py:
--------------------------------------------------------------------------------
1 | from flask import request
2 |
3 | from seed.api._base import RestfulBaseView, HttpMethods
4 | from seed.drives import ALL_DRIVES
5 |
6 |
7 | class DatabaseTest(RestfulBaseView):
8 | url = 'database_test'
9 |
10 | access_methods = [HttpMethods.POST]
11 |
12 | def post(self):
13 | db_conf = request.get_json()
14 |
15 | try:
16 | drive = ALL_DRIVES[db_conf['dtype']]
17 | drive_instance = drive(
18 | db_conf['ip'], int(db_conf['port']), db_conf['name'],
19 | db_conf['user'], db_conf['password']
20 | )
21 | success = drive_instance.test_connection()
22 | message = '数据库连接成功!'
23 | except Exception as e:
24 | success = False
25 | message = str(e)
26 |
27 | if success:
28 | return self.response_json(self.HttpErrorCode.SUCCESS, msg=message)
29 |
30 | return self.response_json(self.HttpErrorCode.ERROR, msg=message)
--------------------------------------------------------------------------------
/src/seed/api/utils/dbtypes.py:
--------------------------------------------------------------------------------
1 |
2 | from flask import request
3 |
4 | # from seed.api.common import _MethodView
5 | from seed.api._base import RestfulBaseView, HttpMethods
6 | from seed.drives import ALL_DRIVES
7 |
8 |
9 | class DatabaseTypes(RestfulBaseView):
10 | url = 'database_types'
11 |
12 | access_methods = [HttpMethods.GET]
13 |
14 | def get(self):
15 |
16 | data = [{'value': drive, 'label': drive} for drive in ALL_DRIVES.keys()]
17 | return self.response_json(self.HttpErrorCode.SUCCESS, data=data)
--------------------------------------------------------------------------------
/src/seed/api/utils/files.py:
--------------------------------------------------------------------------------
1 | from flask import request
2 |
3 | # from seed.api.common import _MethodView
4 | from seed.utils.file import LocalFile
5 | from seed.api._base import RestfulBaseView, HttpMethods
6 |
7 |
8 | class Files(RestfulBaseView):
9 |
10 | access_methods = [HttpMethods.POST]
11 |
12 | def post(self):
13 | upload_file = request.files['file']
14 | local_file = LocalFile()
15 | file_path = local_file.save(upload_file)
16 | if file_path:
17 | file_url = request.host_url + file_path.replace('\\', '/')
18 | return self.response_json(self.HttpErrorCode.SUCCESS, '上传成功', {'file_url': file_url})
19 | return self.response_json(self.HttpErrorCode.ERROR, '上传失败')
20 |
--------------------------------------------------------------------------------
/src/seed/api/utils/sql_analyze.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from flask import request
4 | import sqlparse
5 | from sqlparse.tokens import Keyword
6 | from sqlparse.sql import IdentifierList, Identifier
7 |
8 | from seed.api._base import RestfulBaseView, HttpMethods
9 |
10 |
11 | class SqlFieldAnalysis(RestfulBaseView):
12 | url = 'sql_fields'
13 |
14 | access_methods = [HttpMethods.POST]
15 |
16 | def post(self):
17 | input_json = request.get_json()
18 |
19 | if 'sqls' not in input_json:
20 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg='获取SQL失败')
21 |
22 | chartype = input_json.get("chartType")
23 | sqls = input_json['sqls']
24 | print("origin_sql:", sqls)
25 | sql_str = sqls.lower().replace('\n', ' ').replace('\t', ' ')
26 |
27 | # 去除注释信息,否则列名会被解析错误
28 | sql = sqlparse.format(sql_str, strip_comments=True)
29 | stmt = sqlparse.parse(sql)[0]
30 | tokens_list = stmt.tokens
31 |
32 | fields = []
33 |
34 | for token in tokens_list:
35 | if token.ttype is Keyword:
36 | continue
37 | if isinstance(token, IdentifierList):
38 | for identifier in token.get_identifiers():
39 | field_name = identifier.get_name()
40 | fields.append(field_name)
41 | elif isinstance(token, Identifier):
42 | break
43 |
44 | sankey_fields = ["source", "target"]
45 | map_fields = ["region_name", "lat", "lng"]
46 |
47 | # 桑基图和地图SQL中必须包含指定字段
48 | if chartype == "sankey":
49 | if not set(sankey_fields).issubset(set(fields)):
50 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg='SQL字段不符合要求')
51 |
52 | elif chartype == "map":
53 | if not set(map_fields).issubset(set(fields)):
54 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg='SQL字段不符合要求')
55 |
56 | return self.response_json(self.HttpErrorCode.SUCCESS, data={'fields': fields})
57 |
--------------------------------------------------------------------------------
/src/seed/cache/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import current_app
2 |
3 | from seed.cache.redis import RedisCache
4 |
5 | __all__ = ['DefaultCache']
6 |
7 | DefaultCache = RedisCache
--------------------------------------------------------------------------------
/src/seed/cache/active_account.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 |
4 | from flask import current_app
5 |
6 | from seed.cache.redis import RedisCache
7 |
8 |
9 | class ActiveAccountCache(RedisCache):
10 | def __init__(self):
11 | self.redis_client = current_app.cache
12 | self.middle_key = 'active_account'
13 |
14 | def make_key(self, key):
15 | key = ":".join([self.middle_key, str(key)])
16 | return super(ActiveAccountCache, self).make_key(key)
17 |
18 | def create_active_token(self, user_id):
19 | active_token = ''.join([random.choice(string.ascii_lowercase + string.digits) for key in range(48)])
20 | self.set(active_token, user_id, timeout=24 * 60 * 60)
21 | return active_token
22 |
23 | def get_user_by_active_token(self, active_token):
24 | return self.get(active_token)
25 |
--------------------------------------------------------------------------------
/src/seed/cache/auth.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/cache/auth.py
--------------------------------------------------------------------------------
/src/seed/cache/base.py:
--------------------------------------------------------------------------------
1 | from threading import Thread
2 |
3 | class BaseCache(Thread):
4 | prefix = "seed"
5 |
6 | def __init__(self, prefix=None):
7 | if not prefix:
8 | self.prefix = prefix
9 |
10 | def make_key(self, key):
11 | return '{}:{}'.format(
12 | self.prefix,
13 | key
14 | )
15 |
16 | def get(self, key):
17 | raise NotImplementedError
18 |
19 | def set(self, key, value, timeout):
20 | raise NotImplementedError
21 |
22 | def delete(self, key):
23 | raise NotImplementedError
24 |
--------------------------------------------------------------------------------
/src/seed/cache/redis.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from seed.cache.base import BaseCache
4 |
5 |
6 | class RedisCache(BaseCache):
7 | max_size = 50 * 1024 * 1024
8 |
9 | def __init__(self, redis):
10 | self.redis_client = redis
11 |
12 | def set(self, key, value, timeout=None):
13 | key = self.make_key(key)
14 | v = json.dumps(value)
15 | if len(v) > self.max_size:
16 | raise ValueError("Cache value too large: %r %r" %(key, len(value)))
17 | if timeout:
18 | self.redis_client.set(key, v, int(timeout))
19 | else:
20 | self.redis_client.set(key, v)
21 |
22 | def get(self, key):
23 | key = self.make_key(key)
24 | result = self.redis_client.get(key)
25 | if result:
26 | result = json.loads(result)
27 | return result
28 |
29 | def delete(self, key):
30 | key = self.make_key(key)
31 | self.redis_client.delete(key)
--------------------------------------------------------------------------------
/src/seed/cache/reset_password.py:
--------------------------------------------------------------------------------
1 | from flask import current_app
2 | from seed.cache.active_account import ActiveAccountCache
3 |
4 |
5 | class ResetPasswordCache(ActiveAccountCache):
6 | def __init__(self):
7 | self.redis_client = current_app.cache
8 | self.middle_key = 'reset_password'
9 |
--------------------------------------------------------------------------------
/src/seed/cache/session.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 |
4 | from flask import current_app
5 |
6 | from seed.cache.redis import RedisCache
7 |
8 |
9 | class SessionCache(RedisCache):
10 | def __init__(self):
11 | self.redis_client = current_app.cache
12 | self.middle_key = 'session'
13 |
14 | def make_key(self, key):
15 | key = ':'.join([self.middle_key, str(key)])
16 | return super(SessionCache, self).make_key(key)
17 |
18 | def create_session(self, user_id):
19 | token = ''.join([random.choice(string.ascii_uppercase + string.digits) for key in range(32)])
20 | self.set(token, user_id, timeout=24 * 60 * 60)
21 | return token
22 |
23 | def get_user_id_by_token(self, token):
24 | return self.get(token)
25 |
--------------------------------------------------------------------------------
/src/seed/cache/user_bussiness.py:
--------------------------------------------------------------------------------
1 | from flask import current_app
2 |
3 | from seed.cache.redis import RedisCache
4 |
5 |
6 | class UserBussinessCache(RedisCache):
7 | def __init__(self):
8 | self.redis_client = current_app.cache
9 | self.middle_key = 'user_bussiness'
10 |
11 | def make_key(self, key):
12 | key = ':'.join([self.middle_key, str(key)])
13 | return super(UserBussinessCache, self).make_key(key)
--------------------------------------------------------------------------------
/src/seed/conf/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/conf/__init__.py
--------------------------------------------------------------------------------
/src/seed/conf/server.py:
--------------------------------------------------------------------------------
1 | """
2 | seed.conf.server
3 | ~~~~~~~~~~~~~~~~
4 |
5 | There setting act as default(base) settings for the seed-provided web-server
6 | """
7 |
8 | import os
9 |
10 | ENVRIOMENT = os.environ.get('SEED_ENVRIOMENT', 'production')
11 |
12 | IS_DEV = ENVRIOMENT == 'development'
13 |
14 | DEBUG = IS_DEV
15 |
16 | # Seed logs formatting
17 | LOGGING = {
18 | 'default_level': 'INFO',
19 | 'version': 1,
20 | 'disable_existing_loggers': True,
21 | 'handlers': {
22 | 'null': {
23 | 'class': 'logging.NullHandler',
24 | },
25 | 'console': {
26 | 'class': 'logging.StreamHandler'
27 | },
28 | },
29 | }
--------------------------------------------------------------------------------
/src/seed/data/config/config.yaml.default:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/data/config/config.yaml.default
--------------------------------------------------------------------------------
/src/seed/data/config/seed_conf.py.default:
--------------------------------------------------------------------------------
1 | # this file is just for python, with a touch of Flask which means
2 | # you can inhert and tweek setting to your hearts content
3 |
4 | #############
5 | # databases #
6 | #############
7 | #
8 | # 系统本身支持MySQL和PostgreSQL数据库
9 | # PostgreSQL ENGINE 选择 postgresql+psycopg2
10 | # MySQL ENGINE 选择 mysql+pymysql
11 | #
12 | DATABASES = {
13 | 'default': {
14 | 'ENGINE': 'mysql+pymysql',
15 | 'NAME': 'seed',
16 | 'USER': '',
17 | 'PASSWORD': '',
18 | 'HOST': '',
19 | 'PORT': '',
20 | 'CHARSET': 'utf8mb4',
21 | },
22 | }
23 |
24 | SQLALCHEMY_DATABASE_URI = '{ENGINE}://{USER}:{PASSWORD}@{HOST}:{PORT}/{NAME}?charset={CHARSET}'.format(**DATABASES['default'])
25 | SQLALCHEMY_TRACK_MODIFICATIONS = True
26 | SQLALCHEMY_POOL_SIZE = 100
27 | SQLALCHEMY_POOL_TIMEOUT = 10
28 | SQLALCHEMY_MAX_OVERFLOW = 50
29 |
30 | SQLALCHEMY_POOL_RECYCEL = 3600
31 |
32 |
33 | ###########
34 | # General #
35 | ###########
36 |
37 | DEBUG = %(debug_flag)s
38 |
39 | # Web server host and port
40 | HOST = '127.0.0.1'
41 | PORT = 5000
42 |
43 | # login user type, you can choice sso and default login type
44 | # if you choose sso, you must setting XXXX
45 |
46 | AUTH_TYPE = "LOGIN"
47 | SSO_URL = ""
48 |
49 | DEFAULT_TIME_ZONE = "Asia/Shanghai"
50 |
51 |
52 | #########
53 | # cache #
54 | #########
55 |
56 | # Seed currently only support redis for cache
57 | SEED_CACHE = "redis"
58 | # For example::
59 | # redis://[:password]@localhost:6379/0
60 | # rediss://[:password]@localhost:6379/0
61 | # unix://[:password]@/path/to/socket.sock?db=0
62 | REDIS_URL = "redis://127.0.0.1:6379/0"
63 |
64 |
65 | #########
66 | # email #
67 | #########
68 |
69 | # Seed email setting
70 | MAIL_BACKEND = 'smtp'
71 | MAIL_HOST = ''
72 | MAIL_PORT = 5000
73 | MAIL_USER = ''
74 | MAIL_PASSWORD = ''
75 | MAIL_USE_TLS = True
76 | MAIL_FROM = ''
77 |
--------------------------------------------------------------------------------
/src/seed/drives/__init__.py:
--------------------------------------------------------------------------------
1 | from seed.drives.impala import Impala
2 | from seed.drives.mysql import MySQL
3 | from seed.drives.postgresql import PostgreSQL
4 |
5 |
6 | ALL_DRIVES = {
7 | 'impala': Impala,
8 | 'mysql': MySQL,
9 | 'postgresql': PostgreSQL
10 | }
--------------------------------------------------------------------------------
/src/seed/drives/base.py:
--------------------------------------------------------------------------------
1 | DEFUALT_RETRY_COUNT = 3
2 |
3 |
4 | class BaseDrive(object):
5 | def __init__(self, ip, port, name, user, password):
6 | self.ip = ip
7 | self.port = port
8 | self.name = name
9 | self.user = user
10 | self.password = password
11 |
12 | self._connect()
13 |
14 | def query(self, sql, params=None, retry_count=1):
15 | raise NotImplementedError("Need to implemented!")
16 |
17 | def execute(self, sql, params=None):
18 | raise NotImplementedError("Need to implemented!")
19 |
20 | def test_connection(self):
21 | rows = self.query("SELECT 1")
22 | return len(rows) == 1
23 |
24 | def _connect(self):
25 | raise NotImplementedError("Need to implemented!")
26 |
27 | def _raise_retry_count(self, retry_count):
28 | if retry_count < 0:
29 | raise Exception("Retry time out")
30 |
31 | def _get_connection(self):
32 | if self.alive():
33 | self._connect()
34 |
35 | def _commit(self):
36 | """ 完成SQL执行
37 | """
38 | self.conn.commit()
39 |
40 | def _rollback(self):
41 | """ 回滚SQL
42 | """
43 | self.conn.rollback()
44 |
45 | def _gen_cursor(self):
46 | cursor = self.conn.cursor()
47 | return cursor
48 |
49 | def alive(self):
50 | """ 测试连接是否还存在
51 | """
52 | if self.conn:
53 | return True if self.conn.closed == 0 else False
54 |
55 | return False
56 |
57 | def close(self):
58 | """ 关闭连接
59 | """
60 | self.conn.close()
61 |
--------------------------------------------------------------------------------
/src/seed/drives/email.py:
--------------------------------------------------------------------------------
1 | import smtplib
2 | from email.mime.text import MIMEText
3 | from email.header import Header
4 | from email.utils import formatdate
5 |
6 |
7 | class Email(object):
8 | def __init__(self, configs):
9 | self.mail_backend = configs['MAIL_BACKEND']
10 | self.mail_host = configs['MAIL_HOST']
11 | self.mail_port = configs['MAIL_PORT']
12 | self.mail_user = configs['MAIL_USER']
13 | self.mail_password = configs['MAIL_PASSWORD']
14 | self.mail_use_tls = configs['MAIL_USE_TLS']
15 | self.mail_from = configs['MAIL_FROM']
16 |
17 | def send_mail(self, to_mail_list, title, message, message_type='plain', cc_mail_list=[]):
18 | to_mails = '; '.join(to_mail_list)
19 | cc_mails = '; '.join(cc_mail_list)
20 |
21 | mime_text = MIMEText(message.encode('utf-8'), message_type, 'utf-8')
22 | mime_text['Subject'] = Header(title, 'utf-8')
23 | mime_text['From'] = self.mail_from
24 | mime_text['To'] = to_mails
25 | mime_text['Cc'] = cc_mails
26 | mime_text['Date'] = formatdate()
27 |
28 | smtp = smtplib.SMTP(self.mail_host, self.mail_port)
29 | smtp.ehlo()
30 | smtp.starttls()
31 | smtp.ehlo()
32 | smtp.login(self.mail_user, self.mail_password)
33 |
34 | smtp.sendmail(self.mail_from, to_mail_list+cc_mail_list, mime_text.as_string())
35 |
36 | smtp.close()
37 |
--------------------------------------------------------------------------------
/src/seed/drives/impala.py:
--------------------------------------------------------------------------------
1 | import decimal
2 | from datetime import datetime, date
3 |
4 | from impala import dbapi
5 |
6 | from seed.drives.base import BaseDrive, DEFUALT_RETRY_COUNT
7 |
8 |
9 | class Impala(BaseDrive):
10 | def query(self, sql, params=None, retry_count=DEFUALT_RETRY_COUNT):
11 | """ 查询SQL语句
12 | """
13 | self._raise_retry_count(retry_count)
14 |
15 | cursor = self._gen_cursor()
16 | try:
17 | query_data = self._query(cursor, sql, params)
18 | return query_data
19 | except (dbapi.OperationalError, dbapi.DatabaseError, dbapi.InterfaceError):
20 | if not retry_count:
21 | raise
22 |
23 | self._connect()
24 | return self.query(sql, params=params, retry_count=retry_count-1)
25 | finally:
26 | cursor.close()
27 |
28 | def _query(self, cursor, sql, params):
29 | self._execute(cursor, sql, params=params)
30 | columns_name = [d[0] for d in cursor.description]
31 | query_datas = [Row(zip(columns_name, row)) for row in cursor]
32 |
33 | return self._replace_type(query_datas)
34 |
35 | def _execute(self, cursor, sql, params=None):
36 | """ 执行SQL
37 | """
38 | try:
39 | try:
40 | cursor.execute(sql, params)
41 | except Exception:
42 | cursor.execute("INVALIDATE METADATA;")
43 | cursor.execute(sql, params)
44 | self._commit()
45 | except dbapi.ProgrammingError as e:
46 | self._rollback()
47 | raise Exception(str(e))
48 | except (dbapi.OperationalError, dbapi.DatabaseError, dbapi.InterfaceError) as e:
49 | raise
50 | except Exception:
51 | self._rollback()
52 | raise
53 |
54 | def _connect(self):
55 | try:
56 | self.conn = dbapi.connect(
57 | database=self.name,
58 | user=self.user,
59 | password=self.password,
60 | host=self.ip,
61 | port=self.port,
62 | )
63 | except Exception:
64 | raise
65 |
66 | def _replace_type(self, datas):
67 | """ 替换数据类型
68 | """
69 | for data in datas:
70 | for key, value in data.items():
71 | if isinstance(value, datetime):
72 | data[key] = value.strftime('%Y-%m-%d')
73 | if isinstance(value, date):
74 | data[key] = value.strftime('%Y-%m-%d')
75 | if isinstance(value, decimal.Decimal):
76 | data[key] = float(value)
77 | return datas
78 |
79 |
80 | class Row(dict):
81 | """访问对象那样访问dict,行结果"""
82 | def __getattr__(self, name):
83 | try:
84 | return self[name]
85 | except KeyError:
86 | raise AttributeError(name)
--------------------------------------------------------------------------------
/src/seed/drives/mysql.py:
--------------------------------------------------------------------------------
1 | import decimal
2 |
3 | import pymysql
4 | from datetime import datetime, date
5 |
6 | # from seed.drives.postgresql import PostgreSQL
7 | from seed.drives.base import BaseDrive, DEFUALT_RETRY_COUNT
8 |
9 |
10 | class MySQL(BaseDrive):
11 | def query(self, sql, params=None, retry_count=DEFUALT_RETRY_COUNT):
12 | """ 查询SQL语句
13 | """
14 | self._raise_retry_count(retry_count)
15 |
16 | cursor = self._gen_cursor()
17 | try:
18 | query_data = self._query(cursor, sql, params)
19 | return query_data
20 | except (pymysql.OperationalError, pymysql.DatabaseError, pymysql.InterfaceError) as e:
21 | if not retry_count:
22 | raise
23 |
24 | self._connect()
25 | return self.query(sql, params=params, retry_count=retry_count-1)
26 | finally:
27 | cursor.close()
28 |
29 | def _query(self, cursor, sql, params):
30 | self._execute(cursor, sql, params=params)
31 | datas = list(cursor.fetchall())
32 | return self._replace_type(datas)
33 |
34 | def _execute(self, cursor, sql, params=None):
35 | """ 执行SQL
36 | """
37 | try:
38 | cursor.execute(sql, params)
39 | self._commit()
40 | except pymysql.ProgrammingError as e:
41 | self._rollback()
42 | raise Exception(str(e))
43 | except (pymysql.OperationalError, pymysql.DatabaseError, pymysql.InterfaceError) as e:
44 | raise
45 | except Exception as e:
46 | self._rollback()
47 | raise
48 |
49 | def _connect(self):
50 | try:
51 | self.conn = pymysql.connect(
52 | db=self.name,
53 | user=self.user,
54 | password=self.password,
55 | host=self.ip,
56 | port=self.port,
57 | charset='utf8',
58 | cursorclass=pymysql.cursors.DictCursor
59 | )
60 | except Exception as e:
61 | raise
62 |
63 | def _replace_type(self, datas):
64 | """ 替换数据类型
65 | """
66 | for data in datas:
67 | for key, value in data.items():
68 | if isinstance(value, datetime):
69 | data[key] = value.strftime('%Y-%m-%d')
70 | if isinstance(value, date):
71 | data[key] = value.strftime('%Y-%m-%d')
72 | if isinstance(value, decimal.Decimal):
73 | data[key] = float(value)
74 | return datas
75 |
--------------------------------------------------------------------------------
/src/seed/drives/postgresql.py:
--------------------------------------------------------------------------------
1 | import decimal
2 | from datetime import datetime, date
3 |
4 | import psycopg2
5 |
6 | from seed.drives.base import BaseDrive, DEFUALT_RETRY_COUNT
7 |
8 | # 解决 pg TypeError: Decimal('30737') is not JSON serializable的问题
9 | DEC2FLOAT = psycopg2.extensions.new_type(
10 | psycopg2.extensions.DECIMAL.values,
11 | 'DEC2FLOAT',
12 | lambda value, curs: float(value) if value is not None else None)
13 | psycopg2.extensions.register_type(DEC2FLOAT)
14 | # 解决DataTime is not Json serializable的问题
15 | DATE2STR = psycopg2.extensions.new_type(
16 | psycopg2.extensions.PYDATE.values,
17 | 'DATE2STR',
18 | lambda value, curs: value.split('.')[0] if value is not None else None)
19 | psycopg2.extensions.register_type(DATE2STR)
20 | DATETIME2STR = psycopg2.extensions.new_type(
21 | psycopg2.extensions.PYDATETIME.values,
22 | 'DATETIME2STR',
23 | lambda value, curs: value.split('.')[0] if value is not None else None)
24 | psycopg2.extensions.register_type(DATETIME2STR)
25 |
26 |
27 | class PostgreSQL(BaseDrive):
28 | def query(self, sql, params=None, retry_count=DEFUALT_RETRY_COUNT):
29 | """ 查询SQL语句
30 | """
31 | self._raise_retry_count(retry_count)
32 |
33 | cursor = self._gen_cursor()
34 | try:
35 | query_datas = self._query(cursor, sql, params)
36 | return query_datas
37 | except (psycopg2.OperationalError, psycopg2.DatabaseError, psycopg2.InterfaceError) as e:
38 | # 可能数据库会主动断开连接
39 | # 尝试重新连接, 并重试
40 | if not retry_count:
41 | raise
42 |
43 | self._connect()
44 | return self.query(sql, params=params, retry_count=retry_count-1)
45 | finally:
46 | cursor.close()
47 |
48 | def _query(self, cursor, sql, params):
49 | self._execute(cursor, sql, params=params)
50 | columns_name = [d[0] for d in cursor.description]
51 | query_datas = [Row(zip(columns_name, row)) for row in cursor]
52 |
53 | return self._replace_type(query_datas)
54 |
55 | def _execute(self, cursor, sql, params=None):
56 | """ 执行SQL
57 | """
58 | try:
59 | cursor.execute(sql, params)
60 | self._commit()
61 | except psycopg2.ProgrammingError as e:
62 | # syntax error
63 | self._rollback()
64 | raise Exception(str(e))
65 | except psycopg2.extensions.QueryCanceledError as e:
66 | raise psycopg2.QueryCanceledError(str(e))
67 | except (psycopg2.OperationalError, psycopg2.DatabaseError, psycopg2.InterfaceError) as e:
68 | # server closed the connection unexpectedly
69 | raise
70 | except Exception as e:
71 | self._rollback()
72 | raise
73 |
74 | def _connect(self):
75 | try:
76 | self.conn = psycopg2.connect(
77 | dbname=self.name,
78 | user=self.user,
79 | password=self.password,
80 | host=self.ip,
81 | port=self.port,
82 | options='-c statement_timeout=60s',
83 | )
84 | except Exception as e:
85 | raise
86 |
87 | def _replace_type(self, datas):
88 | """ 替换数据类型
89 | """
90 | for data in datas:
91 | for key, value in data.items():
92 | if isinstance(value, datetime):
93 | data[key] = value.strftime('%Y-%m-%d')
94 | if isinstance(value, date):
95 | data[key] = value.strftime('%Y-%m-%d')
96 | if isinstance(value, decimal.Decimal):
97 | data[key] = float(value)
98 | return datas
99 |
100 |
101 | class Row(dict):
102 |
103 | """访问对象那样访问dict,行结果"""
104 |
105 | def __getattr__(self, name):
106 | try:
107 | return self[name]
108 | except KeyError:
109 | raise AttributeError(name)
--------------------------------------------------------------------------------
/src/seed/libs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/__init__.py
--------------------------------------------------------------------------------
/src/seed/libs/data_access/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/data_access/__init__.py
--------------------------------------------------------------------------------
/src/seed/libs/data_access/app.py:
--------------------------------------------------------------------------------
1 | from seed.libs.data_access.dataware.base import DataModel
2 | from seed.libs.data_access.middledata.base import MiddleData
3 | from seed.libs.data_access.formatters.base import FormatterFactory
4 |
5 |
6 | class DataAccess(object):
7 | def __init__(self, dtype, db, *args, **kwargs):
8 | # self.dtype = dtype
9 | self.db = db
10 |
11 | self.sql = kwargs['sql']
12 | if not self.sql:
13 | raise Exception("SQL不能为空!")
14 |
15 | self.indexs = kwargs['indexs']
16 | if kwargs.get("charttype", "table") in ('table',):
17 | if not self.indexs:
18 | pass
19 | else:
20 | if not self.indexs:
21 | raise Exception("指标不能为空!")
22 |
23 | self.dimensions = kwargs['dimensions']
24 | if not self.dimensions:
25 | raise Exception("维度不能为空!")
26 |
27 | self.query = kwargs.get('query', {})
28 |
29 | self.charttype = kwargs.get('charttype', 'chart')
30 |
31 | self.format_args = {'vth_columns': kwargs.get('vth_columns', [])}
32 |
33 | def get_datas(self):
34 | # 获取原数据
35 | try:
36 | source_data, sql_info = self.get_source_data()
37 | middle_data = self.transfer_middle_data(source_data)
38 | except Exception as e:
39 | raise Exception('数据库取数错误: %s' % str(e))
40 |
41 | # 格式化
42 | try:
43 | format_data = self.format_data(middle_data)
44 | except Exception as e:
45 | raise Exception('数据格式化出错: %s' % str(e))
46 |
47 | return format_data
48 |
49 | def get_source_data(self):
50 | source_data, sql_info = DataModel(
51 | self.db, self.sql, self.query, self.dimensions, self.indexs
52 | ).query_data()
53 | return source_data, sql_info
54 |
55 | def transfer_middle_data(self, source_data):
56 | middle_data = MiddleData(
57 | source_data, self.dimensions, self.indexs
58 | ).convert()
59 | return middle_data
60 |
61 | def format_data(self, middle_data):
62 |
63 | formatter = FormatterFactory(self.charttype).formatter_class()
64 | format_data = formatter(
65 | self.indexs, self.dimensions, middle_data, self.format_args
66 | ).format_data()
67 |
68 | return format_data
69 |
--------------------------------------------------------------------------------
/src/seed/libs/data_access/dataware/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/data_access/dataware/__init__.py
--------------------------------------------------------------------------------
/src/seed/libs/data_access/dataware/base.py:
--------------------------------------------------------------------------------
1 | class DataModel(object):
2 | def __init__(self, db, sql, query, dimensions, indexs):
3 | self.db = db
4 | self.sql = sql
5 | self.query = query
6 | self.dimensions = dimensions
7 | self.indexs = indexs
8 | self.gen_sql()
9 |
10 | def gen_sql(self):
11 | """ 用维度和指标把SQL包一层
12 | """
13 | select_fileds = [item['dimension'] for item in self.dimensions]\
14 | + ['sum('+item['index']+') as ' + item['index'] for item in self.indexs]
15 | group_fileds = [item['dimension'] for item in self.dimensions]
16 | self.sql = """
17 | SELECT
18 | {select_fileds}
19 | FROM (
20 | {origin_sql}
21 | ) a
22 | GROUP BY
23 | {group_fileds}
24 | """.format(
25 | select_fileds=','.join(select_fileds),
26 | origin_sql=self.sql,
27 | group_fileds=','.join(group_fileds)
28 | )
29 |
30 | def format_sql(self):
31 | query_list = []
32 | for key, value in self.query.items():
33 | if isinstance(value, list):
34 | # SQL in 连接条件只有一个元素时,不能带逗号. 故需分开处理
35 | if len(value) > 1:
36 | for va in value:
37 | query_list.append(va)
38 |
39 | self.query[key] = tuple(query_list)
40 | else:
41 | self.query[key] = "('%s')" % ', '.join([str(item) for item in value])
42 |
43 | return self.sql.format(**self.query)
44 |
45 | def query_data(self):
46 | sql = self.format_sql()
47 | # SQL中双引号会被视为列,故需替换为单引号
48 | sql.replace('"', "'")
49 | print("formated sql:", sql)
50 | return self.db.query(sql), sql
51 |
--------------------------------------------------------------------------------
/src/seed/libs/data_access/formatters/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/data_access/formatters/__init__.py
--------------------------------------------------------------------------------
/src/seed/libs/data_access/formatters/bar.py:
--------------------------------------------------------------------------------
1 | from seed.libs.data_access.formatters.line import LineFormatter
2 |
3 |
4 | class BarFormatter(LineFormatter):
5 | def __init__(self, *args, **kwargs):
6 | super(BarFormatter, self).__init__(*args, **kwargs)
--------------------------------------------------------------------------------
/src/seed/libs/data_access/formatters/base.py:
--------------------------------------------------------------------------------
1 | from inspect import isclass
2 |
3 | from seed.libs.data_access.utils.auto_register import get_package_members, get_immediate_cls_attr
4 |
5 |
6 | class FormatterFactory(object):
7 | registerd_charttypes = {}
8 |
9 | def __init__(self, charttype):
10 | self.charttype = charttype
11 |
12 | def formatter_class(self):
13 | from seed.libs.data_access.formatters.line import LineFormatter
14 | if self.charttype not in self.registerd_charttypes:
15 | self._register_charttypes()
16 |
17 | return self.registerd_charttypes.get(self.charttype.lower()+'formatter', LineFormatter)
18 |
19 | def _register_charttypes(self):
20 | from seed.libs.data_access import formatters
21 | predicate = lambda m: isclass(m) and issubclass(m, BaseFormatter) and not get_immediate_cls_attr(m, '__abstract__')
22 | members = get_package_members(formatters, predicate)
23 | self.registerd_charttypes = {member.__name__.lower(): member for member in members}
24 |
25 |
26 | class BaseFormatter(object):
27 | __abstract__ = True
28 |
29 | def __init__(self, indexs, dimensions, data, format_args):
30 | self.indexs = indexs
31 | self.dimensions = dimensions
32 | self.data = data
33 | self.format_args = format_args
34 |
--------------------------------------------------------------------------------
/src/seed/libs/data_access/formatters/funnel.py:
--------------------------------------------------------------------------------
1 | import json
2 | from seed.libs.data_access.formatters.line import LineFormatter
3 |
4 |
5 | class FunnelFormatter(LineFormatter):
6 | def __init__(self, *args, **kwargs):
7 | super(FunnelFormatter, self).__init__(*args, **kwargs)
8 |
9 | def format_data(self):
10 | result = {"series": []}
11 | ret = {"data": []}
12 | for dimenstr, datas in self.data.items():
13 | info = {}
14 | dimendict = json.loads(dimenstr)
15 | namelist = [dimen.get("dimension", "") for dimen in self.dimensions]
16 | infoname = []
17 | for name in namelist:
18 | infoname.append(str(dimendict.get(name, "")))
19 |
20 | info["name"] = "-".join(infoname)
21 | info["value"] = 0
22 | for key, value in datas.items():
23 | if isinstance(value, (float, int)):
24 | info["value"] += value
25 | ret["data"].append(info)
26 |
27 | result["series"].append(ret)
28 |
29 | return result
30 |
--------------------------------------------------------------------------------
/src/seed/libs/data_access/formatters/line.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from seed.libs.data_access.formatters.base import BaseFormatter
4 |
5 |
6 | class LineFormatter(BaseFormatter):
7 | def __init__(self, *args, **kwargs):
8 | super(LineFormatter, self).__init__(*args, **kwargs)
9 |
10 | def format_data(self):
11 | # 得到分布项
12 | categories, series = self._get_chart_categories_and_series()
13 |
14 | # 获取series图形数据
15 | series = self._convert_series_data(categories, series)
16 |
17 | return {"categories": categories, "series": series}
18 |
19 | def _get_chart_categories_and_series(self):
20 | """
21 | 获取categoies和series的维度
22 | """
23 | series = []
24 |
25 | category_columns, series_columns = self._get_chart_columns()
26 |
27 | # 从数据中获取到对应的categories然后通过数据的维度进行组合
28 | categories = self._get_categories_sort_by_dimensions(category_columns, series_columns, series)
29 |
30 | categories = self._get_categories_sort_by_indexs(categories, category_columns)
31 |
32 | if not series:
33 | series = [item['index'] for item in self.indexs]
34 |
35 | return categories, series
36 |
37 | def _get_categories_sort_by_dimensions(self, category_columns, series_columns, series):
38 | categories_dict = []
39 |
40 | for key in self.data.keys():
41 | key = json.loads(key)
42 | temp_dict = {}
43 | cateogry = {category_column: key[category_column] for category_column in category_columns}
44 | ser_key = {series_column: key[series_column] for series_column in series_columns}
45 | temp_dict.update(cateogry)
46 | temp_dict.update(ser_key)
47 |
48 | if temp_dict not in categories_dict:
49 | categories_dict.append(temp_dict)
50 |
51 | if series_columns:
52 | series_key = '-'.join([str(key[series_column]) for series_column in series_columns])
53 | if series_key not in series:
54 | series.append(series_key)
55 |
56 | # categories排序
57 | for dimension in self.dimensions:
58 | if dimension['sort'] == 'desc':
59 | categories_dict.sort(key=lambda x: x[dimension['dimension']], reverse=True)
60 | if dimension['sort'] == 'asc':
61 | categories_dict.sort(key=lambda x: x[dimension['dimension']])
62 |
63 | categories = [
64 | '-'.join([str(category_dict[category_column]) for category_column in category_columns])
65 | for category_dict in categories_dict
66 | ]
67 | return categories
68 |
69 | def _get_chart_columns(self):
70 | category_columns, series_columns = [item['dimension'] for item in self.dimensions], []
71 | return category_columns, series_columns
72 |
73 | def _convert_series_data(self, categories, series):
74 | # 从数据中获取到趋势类型的数据
75 | category_columns, series_columns = self._get_chart_columns()
76 | series_map = {index['index']: index for index in self.indexs}
77 |
78 | middle_data = {}
79 | if series_columns:
80 | for key, value in self.data.items():
81 | key = json.loads(key)
82 | category_key = '-'.join([str(key[category_column]) for category_column in category_columns])
83 | if series_columns:
84 | series_key = '-'.join([str(key[series_column]) for series_column in series_columns])
85 | middle_data.setdefault(category_key, {})[series_key] = sum(value.values())
86 | else:
87 | middle_data = {
88 | '-'.join([str(json.loads(key)[category_column]) for category_column in category_columns]): value for
89 | key, value in self.data.items()
90 | }
91 |
92 | series_datas = []
93 | for serie in series:
94 | data = []
95 | for category in categories:
96 | data.append(middle_data.get(str(category), {}).get(serie, '-'))
97 |
98 | series_data = {'data': data, 'dim': serie, 'name': serie}
99 | series_data.update(series_map.get(serie, {}))
100 | series_datas.append(series_data)
101 |
102 | return series_datas
103 |
104 | def _get_categories_sort_by_indexs(self, categories, category_columns):
105 | if not len(categories):
106 | return categories
107 |
108 | if any(index['sort'] in ('desc', 'asc') for index in self.indexs):
109 | # 按指标进行排序
110 | categories = self._categories_sort_by_value(category_columns)
111 |
112 | return categories
113 |
114 | def _categories_sort_by_value(self, category_columns):
115 | middle_date = {}
116 |
117 | sort_index = None
118 | sort_reverse = None
119 | for index in self.indexs:
120 | if index['sort'] in ('desc', 'asc'):
121 | sort_index = index['index']
122 | sort_reverse = True if index['sort'] != 'desc' else False
123 | break
124 |
125 | for i, j in self.data.items():
126 | i = json.loads(i)
127 | middle_date.update(
128 | {'-'.join([str(i[category_column]) for category_column in category_columns]): j[sort_index]}
129 | )
130 | categories_data = sorted(middle_date.items(), key=lambda d: d[1] if d[1] else 0, reverse=sort_reverse)
131 | categories = [v[0] for v in categories_data]
132 | return categories
133 |
--------------------------------------------------------------------------------
/src/seed/libs/data_access/formatters/linestack.py:
--------------------------------------------------------------------------------
1 | from seed.libs.data_access.formatters.line import LineFormatter
2 |
3 |
4 | class LineStackFormatter(LineFormatter):
5 | def __init__(self, *args, **kwargs):
6 | super(LineStackFormatter, self).__init__(*args, **kwargs)
7 |
8 | def _get_chart_columns(self):
9 | category_columns = ['fdate']
10 |
11 | series_columns = [item['dimension'] for item in self.dimensions if item['dimension'] not in category_columns]
12 |
13 | return category_columns, series_columns
14 |
15 | def _get_chart_categories_and_series(self):
16 | categories, series = super(LineStackFormatter, self)._get_chart_categories_and_series()
17 | categories = list(set(categories))
18 |
19 | # 全是数字
20 | is_num = all([isinstance(c, (int, float)) for c in categories]) if categories else False
21 | # 全是字符串数字
22 | is_strnum = all([isinstance(c, (str,)) and c.isdigit() for c in categories]) if categories else False
23 |
24 | if is_num or is_strnum:
25 | categories = sorted(categories, key=lambda k: float(k))
26 | else:
27 | categories = sorted(categories)
28 |
29 | return categories, series
30 |
--------------------------------------------------------------------------------
/src/seed/libs/data_access/formatters/map.py:
--------------------------------------------------------------------------------
1 | import json
2 | from seed.libs.data_access.formatters.base import BaseFormatter
3 |
4 |
5 | class MapFormatter(BaseFormatter):
6 | def __init__(self, *args, **kwargs):
7 | super(MapFormatter, self).__init__(*args, **kwargs)
8 |
9 | def format_data(self):
10 | data = []
11 | for key, value in self.data.items():
12 | k = json.loads(key)
13 | k.update(value)
14 | data.append(k)
15 |
16 | return {"data": data}
17 |
--------------------------------------------------------------------------------
/src/seed/libs/data_access/formatters/pie.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | # from seed.libs.data_access.formatters.base import BaseFormatter
4 | from seed.libs.data_access.formatters.line import LineFormatter
5 |
6 |
7 | class PieFormatter(LineFormatter):
8 | def __init__(self, *args, **kwargs):
9 | super(PieFormatter, self).__init__(*args, **kwargs)
--------------------------------------------------------------------------------
/src/seed/libs/data_access/formatters/sankey.py:
--------------------------------------------------------------------------------
1 | import json
2 | from seed.libs.data_access.formatters.base import BaseFormatter
3 |
4 |
5 | class SankeyFormatter(BaseFormatter):
6 | def __init__(self, *args, **kwargs):
7 | super(SankeyFormatter, self).__init__(*args, **kwargs)
8 |
9 | def format_data(self):
10 | links = []
11 | node_list = []
12 | nodes = []
13 | for key, value in self.data.items():
14 | data = json.loads(key)
15 | for _, v in data.items():
16 | node_list.append(v)
17 |
18 | tmp = {}
19 | for _, v in value.items():
20 | tmp['value'] = v
21 |
22 | data.update(tmp)
23 | links.append(data)
24 |
25 | node_list = list(set(node_list))
26 | for n in node_list:
27 | nodes.append({"name": n})
28 |
29 | return {"links": links, "nodes": nodes}
30 |
--------------------------------------------------------------------------------
/src/seed/libs/data_access/formatters/table.py:
--------------------------------------------------------------------------------
1 | import re
2 | import json
3 |
4 | from seed.libs.data_access.formatters.base import BaseFormatter
5 |
6 |
7 | class TableFormatter(BaseFormatter):
8 | def __init__(self, *args, **kwargs):
9 | super(TableFormatter, self).__init__(*args, **kwargs)
10 |
11 | def format_data(self):
12 |
13 | self._ver_to_hor()
14 |
15 | table_keys = [item['dimension'] for item in self.dimensions] + [item['index'] for item in self.indexs]
16 |
17 | # 得到table的表格数据
18 | table_datas = self._convert_table_data(table_keys, self.data)
19 |
20 | # 对表格数据进行排序
21 | table_datas = self._sort_table_column(table_datas)
22 |
23 | # 处理比率类型数据
24 | table_datas = self._convert_rate_data(table_keys, table_datas)
25 |
26 | # 计算总值均值数据
27 | table_datas = self._compute_total_and_mean(table_datas)
28 |
29 | # 得到table的显示项
30 | display_name = self._get_display_name(table_keys)
31 |
32 | return {"data": table_datas, "displayName": display_name}
33 |
34 | def _ver_to_hor(self):
35 | if not self.format_args.get('vth_columns'):
36 | return
37 |
38 | converted_data = {}
39 |
40 | index = self.indexs[0]['index']
41 | vth_columns = self.format_args.get('vth_columns')
42 |
43 | self.dimensions = [item for item in self.dimensions if item['dimension'] not in vth_columns]
44 | self.indexs = []
45 |
46 | for dimensions, values in self.data.items():
47 | dimensions = json.loads(dimensions)
48 | converted_dimensions, vth_dimensions = {}, []
49 | for key, value in dimensions.items():
50 | if key in vth_columns:
51 | vth_dimensions.append(str(value))
52 | else:
53 | converted_dimensions[key] = value
54 | if '-'.join(vth_dimensions) not in self.indexs:
55 | self.indexs.append('-'.join(vth_dimensions))
56 | converted_data.setdefault(json.dumps(converted_dimensions), {})['-'.join(vth_dimensions)] = values[index]
57 |
58 | self.indexs = [{'index': index, 'name': index} for index in self.indexs]
59 | self.data = converted_data
60 |
61 | def _convert_table_data(self, table_keys, datas):
62 | # 将数据从中间格式转换成表格格式
63 | table_datas = []
64 |
65 | for key, values in datas.items():
66 | values.update(json.loads(key))
67 | row = {}
68 | for key in table_keys:
69 | row[key] = values.get(key, '-')
70 |
71 | table_datas.append(row)
72 |
73 | return table_datas
74 |
75 | def _sort_table_column(self, data):
76 | # 进行table类型的排序
77 | dimen_indexs = self.dimensions + self.indexs
78 |
79 | field_list = []
80 | for di in dimen_indexs:
81 | tmp = {}
82 | if di.get("dimension"):
83 | tmp["field"] = di.get("dimension")
84 | elif di.get("index"):
85 | tmp["field"] = di.get("index")
86 |
87 | tmp["sort"] = di.get("sort")
88 | field_list.append(tmp)
89 |
90 | # 倒序让排序时维度排序为主
91 | field_list.reverse()
92 |
93 | for field in field_list:
94 | if field['sort'] == 'desc':
95 | data.sort(key=lambda x: x[field["field"]] if isinstance(x[field["field"]], (float, int)) else 0,
96 | reverse=True)
97 | if field['sort'] == 'asc':
98 | data.sort(key=lambda x: x[field["field"]] if isinstance(x[field["field"]], (float, int)) else 0)
99 |
100 | return data
101 |
102 | def _convert_rate_data(self, table_keys, datas):
103 | """处理比率类型指标的数据"""
104 | table_datas = []
105 |
106 | indexs = self.indexs[:]
107 | temp_indexs = [i['index'] for i in indexs]
108 |
109 | for tk in table_keys:
110 | if tk not in temp_indexs:
111 | indexs.append({"index": tk})
112 |
113 | for data in datas:
114 | row = {}
115 | for index in indexs:
116 | row[index['index']] = self._format_rate_data(index.get('rate'), data.get(index['index'], '-'))
117 |
118 | table_datas.append(row)
119 |
120 | return table_datas
121 |
122 | def _format_rate_data(self, israte, data):
123 | """转换比例数据"""
124 | if not israte:
125 | return data
126 |
127 | if isinstance(data, (float, int)):
128 | ratedata = str(round(data * 100, 2)) + '%'
129 | return ratedata
130 |
131 | return data
132 |
133 | def _compute_total_and_mean(self, datas):
134 | """计算总值均值数据"""
135 |
136 | rate_dimension = [item['dimension'] for item in self.dimensions] + [item['index'] for item in self.indexs if
137 | item["rate"]]
138 | num = len(datas)
139 |
140 | total_dict = {}
141 | mean_dict = {}
142 |
143 | for data in datas:
144 | for key, value in data.items():
145 | total_dict[key] = total_dict.setdefault(key, 0) + (
146 | value if value and isinstance(value, (int, float)) else 0)
147 |
148 | for k, v in total_dict.items():
149 | mean_dict[k] = round(v / float(num), 2)
150 |
151 | first_dimension = self.dimensions[0]["dimension"]
152 |
153 | for k, v in total_dict.items():
154 | if k in rate_dimension:
155 | total_dict[k] = '-'
156 | else:
157 | total_dict[k] = round(v, 2)
158 |
159 | total_dict[first_dimension] = "总值"
160 |
161 | for k, v in mean_dict.items():
162 | if k in rate_dimension:
163 | mean_dict[k] = '-'
164 |
165 | mean_dict[first_dimension] = "均值"
166 |
167 | datas.append(total_dict)
168 | datas.append(mean_dict)
169 |
170 | return datas
171 |
172 | def _get_display_name(self, keys):
173 | # 获取displayName格式的数据显示
174 | titles = []
175 |
176 | dim_display_map = {}
177 |
178 | for item in self.dimensions:
179 | dim_display_map[item['dimension']] = item['name']
180 |
181 | for item in self.indexs:
182 | dim_display_map[item['index']] = item['name']
183 |
184 | for key in keys:
185 | titles.append({"name": key, "displayName": dim_display_map[key]})
186 |
187 | return titles
188 |
--------------------------------------------------------------------------------
/src/seed/libs/data_access/middledata/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/data_access/middledata/__init__.py
--------------------------------------------------------------------------------
/src/seed/libs/data_access/middledata/base.py:
--------------------------------------------------------------------------------
1 | import json
2 | import collections
3 |
4 |
5 | class MiddleData(object):
6 | """ 原始数据格式 转 中间数据格式
7 | row = [
8 | {'dau': 100, 'dsu': 200, 'fdate': '2017-01-01'},
9 | {'dau': 100, 'dsu': 200, 'fdate': '2017-01-02'},
10 | ]
11 |
12 | reuslt = {
13 | json.loads({'fdata': '2017-01-01'}): {'dau': 100, 'dsu': 200},
14 | json.loads({'fdata': '2017-01-02'}): {'dau': 200, 'dsu': 100},
15 | }
16 | """
17 |
18 | def __init__(self, source_data, dimensions, indexs):
19 | self.source_data = source_data
20 | self.dimensions = [item['dimension'] for item in dimensions]
21 | self.indexs = [item['index'] for item in indexs]
22 |
23 | def convert(self):
24 | middle_datas = {}
25 | # 兼容表格指标可以为空的情况
26 | self.indexs = self.dimensions if not self.indexs else self.indexs
27 |
28 | for row in self.source_data:
29 |
30 | # 根据dimensions构造当前数据项的key
31 | row_keys = collections.OrderedDict()
32 | for dimension in self.dimensions:
33 | row_keys[dimension] = row[dimension]
34 |
35 | for index in self.indexs:
36 | middle_key = json.dumps(row_keys)
37 | if middle_datas.get(middle_key, {}).get(index) is None:
38 | middle_datas.setdefault(middle_key, {})[index] = row.get(index, None)
39 |
40 | return middle_datas
41 |
--------------------------------------------------------------------------------
/src/seed/libs/data_access/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/data_access/utils/__init__.py
--------------------------------------------------------------------------------
/src/seed/libs/data_access/utils/auto_register.py:
--------------------------------------------------------------------------------
1 | from inspect import getmembers
2 |
3 |
4 | def get_module_members(modules, predicate):
5 | members = getmembers(modules, predicate)
6 | return map(lambda m: m[1], members)
7 |
8 |
9 | def get_package_members(package, predicate):
10 | from pkgutil import iter_modules
11 | from importlib import import_module
12 |
13 | members = []
14 | for _, name, ispkg in iter_modules(package.__path__, package.__name__+'.'):
15 | if ispkg:
16 | sub_members = get_package_members(
17 | import_module(name), predicate
18 | )
19 | members.append(sub_members)
20 |
21 | members.extend(get_module_members(import_module(name), predicate))
22 |
23 | return members
24 |
25 |
26 | def get_immediate_cls_attr(cls, attrname):
27 | if not issubclass(cls, object):
28 | return None
29 |
30 | for base in cls.__mro__:
31 | if attrname in base.__dict__ and base is cls:
32 | return getattr(base, attrname)
33 |
34 | return None
35 |
--------------------------------------------------------------------------------
/src/seed/libs/filter_access/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/filter_access/__init__.py
--------------------------------------------------------------------------------
/src/seed/libs/filter_access/app.py:
--------------------------------------------------------------------------------
1 | class FilterAccess(object):
2 | def __init__(self, db, conditions, query, *args, **kwargs):
3 | self.db_instance = db
4 | self.sql = conditions
5 | self.query = query
6 |
7 | def query_datas(self):
8 | for key, value in self.query.items():
9 | if isinstance(value, list):
10 | self.query[key] = '(%s)' % ', '.join([str(item) for item in value])
11 |
12 | sql = self.sql.format(**self.query)
13 | print(sql)
14 |
15 | rows = self.db_instance.query(sql)
16 | return rows
17 |
--------------------------------------------------------------------------------
/src/seed/logging/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/logging/README.md
--------------------------------------------------------------------------------
/src/seed/logging/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/logging/__init__.py
--------------------------------------------------------------------------------
/src/seed/logging/handlers.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/logging/handlers.py
--------------------------------------------------------------------------------
/src/seed/models/__init__.py:
--------------------------------------------------------------------------------
1 | from ._base import *
2 | from .account import *
3 | from .bussiness import *
4 | from .databases import *
5 | from .menu import *
6 | from .role import *
7 | from .bmanager import *
8 | from .buser import *
9 | from .panels import *
10 | from .filters import *
11 |
12 | from .analogdatas import *
13 |
--------------------------------------------------------------------------------
/src/seed/models/_base.py:
--------------------------------------------------------------------------------
1 | import json
2 | from datetime import datetime
3 |
4 | from flask import g
5 | from flask_migrate import Migrate
6 | from flask_marshmallow import Marshmallow
7 | from sqlalchemy.sql import sqltypes
8 | from sqlalchemy.ext.declarative import declared_attr
9 |
10 | from flask_sqlalchemy import SQLAlchemy, BaseQuery, orm
11 |
12 | from seed.utils.time import convert_utc_to_local
13 |
14 | __all__ = ['db', 'ma', 'migrate', 'session', 'BaseModel', 'BussinessModel']
15 |
16 |
17 | DEFAULT_DATETIME = '1970-01-01 00:00:00'
18 |
19 |
20 | class SeedQuery(BaseQuery):
21 | def __init__(self, *args, **kwargs):
22 | super(SeedQuery, self).__init__(*args, **kwargs)
23 |
24 | def as_list(self, *clolumns):
25 | return [{key: getattr(row, key, None) for key in row.keys()} for row in self]
26 |
27 |
28 | class SessionMixin(object):
29 | column_filter = []
30 |
31 | def __init__(self, **kwargs):
32 | for k, v in kwargs.items():
33 | setattr(self, k, v)
34 |
35 | def save(self):
36 | db.session.add(self)
37 | db.session.commit()
38 | return self
39 |
40 | def flush(self):
41 | db.session.flush(self)
42 | db.session.commit()
43 | return self
44 |
45 | def delete(self):
46 | db.session.delete(self)
47 | db.session.commit()
48 | return self
49 |
50 | def row2dict(self):
51 | d = {}
52 | for column in self.__table__.columns:
53 | if column.name in self.column_filter:
54 | continue
55 |
56 | if isinstance(column.type, sqltypes.DateTime):
57 | local_time = convert_utc_to_local(getattr(self, column.name), 'Asia/Shanghai')
58 | d[column.name] = local_time.strftime('%Y-%m-%d %H:%M:%S') if local_time else DEFAULT_DATETIME
59 | else:
60 | d[column.name] = getattr(self, column.name)
61 | return d
62 |
63 |
64 | db = SQLAlchemy(query_class=SeedQuery)
65 | migrate = Migrate()
66 | session = orm.scoped_session(orm.sessionmaker(autocommit=True))
67 | ma = Marshmallow()
68 |
69 |
70 | class BaseModel(db.Model, SessionMixin):
71 | __abstract__ = True
72 | column_filter = ['created', 'updated']
73 |
74 | id = db.Column(db.Integer, primary_key=True)
75 | created = db.Column(
76 | db.DateTime, nullable=False, default=datetime.utcnow()
77 | )
78 | updated = db.Column(
79 | db.DateTime, default=datetime.utcnow(),
80 | onupdate=datetime.utcnow()
81 | )
82 |
83 | def __init__(self, *args, **kwargs):
84 | super(BaseModel, self).__init__(*args, **kwargs)
85 |
86 | def delete(self):
87 | if hasattr(self, 'status'):
88 | self.status = -1
89 | self.save()
90 | else:
91 | super(BaseModel, self).delete()
92 |
93 |
94 | class BussinessModel(BaseModel):
95 | """ 与业务绑定的Model
96 | """
97 | __abstract__ = True
98 | column_filter = ['created', 'updated', 'bussiness_id']
99 |
100 | @declared_attr
101 | def bussiness_id(cls):
102 | return db.Column(db.Integer, db.ForeignKey('bussiness.id'))
103 |
104 | def __init__(self, *args, **kwargs):
105 | self.bussiness_id = g.bussiness_id
106 | super(BussinessModel, self).__init__(*args, **kwargs)
107 |
108 |
109 | class PageModel(BussinessModel):
110 | """ 与页面绑定的Model
111 | """
112 | __abstract__ = True
113 |
114 | column_filter = ['created', 'updated', 'bussiness_id', 'page_id']
115 |
116 | @declared_attr
117 | def page_id(cls):
118 | return db.Column(db.Integer, db.ForeignKey('menu.id'))
--------------------------------------------------------------------------------
/src/seed/models/account.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | # from flask.ext.sqlalchemy import Column
3 | from ._base import db, BaseModel
4 |
5 |
6 | __all__ = ['Account',]
7 |
8 |
9 | class Account(BaseModel):
10 | column_filter = ['updated', 'password']
11 |
12 | sso_id = db.Column(db.Integer, default=-1) # SSO_ID
13 |
14 | account = db.Column(db.String(40), unique=True, index=True, nullable=False)
15 | password = db.Column(db.String(256))
16 | email = db.Column(db.String(40), nullable=False)
17 |
18 | avatar = db.Column(db.Text)
19 | name = db.Column(db.String(40), nullable=False)
20 | sex = db.Column(db.String(20), nullable=False, default='male')
21 | depart_id = db.Column(db.Integer, default=-1) # -1为未知
22 |
23 | role = db.Column(db.String(20), default='new')
24 | status = db.Column(db.Integer, default=0) # -1:不可用 0: 未激活 1: 正常使用
25 |
26 | login_at = db.Column(
27 | db.DateTime, default=datetime.utcnow()
28 | )
--------------------------------------------------------------------------------
/src/seed/models/analogdatas.py:
--------------------------------------------------------------------------------
1 | from seed.models._base import db
2 |
3 | __all__ = ['AnalogdataDimensions', 'AnalogdataGamedatas']
4 |
5 |
6 | class AnalogdataDimensions(db.Model):
7 | """ 测试数据-维度表
8 | """
9 | id = db.Column(db.Integer, primary_key=True)
10 | game_id = db.Column(db.Integer)
11 | game_name = db.Column(db.Text)
12 | platform_id = db.Column(db.Integer)
13 | platform_name = db.Column(db.Text)
14 | version_id = db.Column(db.Integer)
15 | version_name = db.Column(db.Text)
16 |
17 |
18 | class AnalogdataGamedatas(db.Model):
19 | """ 测试数据-数据表
20 | """
21 | id = db.Column(db.Integer, primary_key=True)
22 | fdate = db.Column(db.Text)
23 | game_id = db.Column(db.Integer)
24 | platform_id = db.Column(db.Integer)
25 | version_id = db.Column(db.Integer)
26 | fregucnt = db.Column(db.Integer)
27 | factucnt = db.Column(db.Integer)
28 | fpayucnt = db.Column(db.Integer)
29 | fincome = db.Column(db.Float)
30 |
--------------------------------------------------------------------------------
/src/seed/models/bmanager.py:
--------------------------------------------------------------------------------
1 | from ._base import db, BaseModel
2 |
3 | __all__ = ["BManager", ]
4 |
5 |
6 | class BManager(BaseModel):
7 | """ 业务管理员模型
8 | """
9 | bussiness_id = db.Column(db.Integer, db.ForeignKey('bussiness.id'))
10 | user_id = db.Column(db.Integer, nullable=False)
11 |
--------------------------------------------------------------------------------
/src/seed/models/buser.py:
--------------------------------------------------------------------------------
1 | from ._base import db, BussinessModel
2 | __all__ = ["BUser", ]
3 |
4 |
5 | class BUser(BussinessModel):
6 | """ 业务用户权限模型
7 | """
8 | user_id = db.Column(db.Integer, nullable=False)
--------------------------------------------------------------------------------
/src/seed/models/buserrole.py:
--------------------------------------------------------------------------------
1 | from ._base import db, BussinessModel
2 |
3 | __all__ = ["BUserRole", ]
4 |
5 |
6 | class BUserRole(BussinessModel):
7 | """ 用户权限模型
8 | """
9 | user_id = db.Column(db.Integer, nullable=False)
10 | role_id = db.Column(db.Integer, nullable=False)
--------------------------------------------------------------------------------
/src/seed/models/bussiness.py:
--------------------------------------------------------------------------------
1 | from . import db, BaseModel
2 |
3 | __all__ = ['Bussiness']
4 |
5 |
6 | class Bussiness(BaseModel):
7 | """ bussiness model class
8 | """
9 |
10 | name = db.Column(db.Text, nullable=False)
11 | description = db.Column(db.Text)
12 |
13 | bmanager = db.relationship('BManager', cascade="all,delete", backref="bussiness")
14 | databases = db.relationship('Databases', cascade="all,delete", backref="bussiness")
15 | filters = db.relationship('Filters', cascade="all,delete", backref="bussiness")
16 | menu = db.relationship('Menu', cascade="all,delete", backref="bussiness")
17 | role = db.relationship('Role', cascade="all,delete", backref="bussiness")
18 | role_menu = db.relationship('RoleMenu', cascade="all,delete", backref="bussiness")
--------------------------------------------------------------------------------
/src/seed/models/databases.py:
--------------------------------------------------------------------------------
1 | from . import db, BaseModel
2 |
3 | __all__ = ['Databases']
4 |
5 |
6 | class Databases(BaseModel):
7 | """ databases configure
8 | """
9 | bussiness_id = db.Column(db.Integer, db.ForeignKey('bussiness.id'))
10 | dtype = db.Column(
11 | db.Text, nullable=False,
12 | comment='Database type, like mysql, postgresql'
13 | )
14 | name = db.Column(
15 | db.Text, nullable=False,
16 | comment='Database name'
17 | )
18 | ip = db.Column(
19 | db.Text, nullable=False,
20 | comment='Database IP address'
21 | )
22 | port = db.Column(
23 | db.Text, nullable=False,
24 | comment='Database port'
25 | )
26 | user = db.Column(
27 | db.Text, nullable=False,
28 | comment='Database user login'
29 | )
30 | password = db.Column(
31 | db.Text, nullable=False,
32 | comment='Database user password'
33 | )
34 |
--------------------------------------------------------------------------------
/src/seed/models/filters.py:
--------------------------------------------------------------------------------
1 | from seed.models._base import db, PageModel
2 |
3 | __all__ = ['Filters']
4 |
5 |
6 | class Filters(PageModel):
7 | """ Filter model class
8 | """
9 | belong_id = db.Column(
10 | db.Integer,
11 | comment='Filter belong which model'
12 | )
13 | dtype = db.Column(
14 | db.Text, nullable=False,
15 | comment='Filter belong type, page or model'
16 | )
17 | stype = db.Column(
18 | db.Text, nullable=False,
19 | default='single', comment='Single or mulitple choice'
20 | )
21 | ename = db.Column(
22 | db.Text, nullable=False,
23 | comment='Field english name'
24 | )
25 | cname = db.Column(
26 | db.Text, nullable=False,
27 | comment='Field chinese name'
28 | )
29 | cascades = db.Column(
30 | db.Text, default=0,
31 | comment='The Filter field cascade id if it exists'
32 | )
33 | db_source = db.Column(
34 | db.Integer,
35 | comment='The filter data query database id only for sql'
36 | )
37 | condition_type = db.Column(
38 | db.Text, nullable=False,
39 | comment='The Filter condition setting type, manual or sql'
40 | )
41 | conditions = db.Column(
42 | db.Text, nullable=False,
43 | comment='The Filter conditions, it will save as json string'
44 | )
45 | sort = db.Column(
46 | db.Integer, default=0,
47 | comment='The filter sort'
48 | )
49 |
--------------------------------------------------------------------------------
/src/seed/models/menu.py:
--------------------------------------------------------------------------------
1 | from ._base import db, BussinessModel
2 |
3 | # from seed.models import Panels, Filters
4 |
5 | __all__ = ["Menu",]
6 |
7 |
8 | class Menu(BussinessModel):
9 | name = db.Column(db.Text, nullable=False)
10 |
11 | parent_id = db.Column(db.Integer, default=0)
12 | left_id = db.Column(db.Integer, default=0)
13 |
14 | panels = db.relationship('Panels', cascade="all,delete", backref="menu")
15 | filters = db.relationship('Filters', cascade="all,delete", backref="menu")
--------------------------------------------------------------------------------
/src/seed/models/panels.py:
--------------------------------------------------------------------------------
1 | from seed.models._base import db, PageModel
2 |
3 | __all__ = ['Panels']
4 |
5 |
6 | class Panels(PageModel):
7 | """ data panels model
8 | """
9 | name = db.Column(
10 | db.Text, nullable=False,
11 | comment='Panel name'
12 | )
13 |
14 | desc = db.Column(
15 | db.Text, nullable=False,
16 | comment='Panel description'
17 | )
18 |
19 | x = db.Column(
20 | db.Float, nullable=False,
21 | comment='The panel x coordinate'
22 | )
23 |
24 | y = db.Column(
25 | db.Float, nullable=False,
26 | comment='The panel y coordinate'
27 | )
28 |
29 | w = db.Column(
30 | db.Float, nullable=False,
31 | comment='The panel width'
32 | )
33 |
34 | h = db.Column(
35 | db.Float, nullable=False,
36 | comment='The panel height'
37 | )
38 |
39 | db_source = db.Column(
40 | db.Integer, nullable=False,
41 | comment='Panel sql query database id'
42 | )
43 |
44 | charttype = db.Column(
45 | db.Text, nullable=False,
46 | comment='Panel data chart type'
47 | )
48 |
49 | sql = db.Column(
50 | db.Text, nullable=False,
51 | comment='Panel sql string'
52 | )
53 |
54 | indexs = db.Column(
55 | db.Text, nullable=False,
56 | comment='Data index json configure'
57 | )
58 |
59 | dimensions = db.Column(
60 | db.Text, nullable=False,
61 | comment='Data dimensions configure'
62 | )
63 |
64 | sort = db.Column(
65 | db.Integer, nullable=False,
66 | comment='The panel sort'
67 | )
--------------------------------------------------------------------------------
/src/seed/models/role.py:
--------------------------------------------------------------------------------
1 | from . import db, BussinessModel
2 |
3 | __all__ = ['Role', ]
4 |
5 |
6 | class Role(BussinessModel):
7 | role = db.Column(db.Text, default='new')
8 |
9 | @classmethod
10 | def get_roles(cls, bussiness_id=None):
11 | query_session = cls.query
12 | if bussiness_id:
13 | query_session = query_session.filter_by(bussiness_id=bussiness_id)
14 |
15 | return query_session.all()
16 |
--------------------------------------------------------------------------------
/src/seed/models/rolemenu.py:
--------------------------------------------------------------------------------
1 | from ._base import db, BussinessModel
2 |
3 | __all__ = ["RoleMenu",]
4 |
5 |
6 | class RoleMenu(BussinessModel):
7 | role_id = db.Column(db.Integer)
8 | menu_id = db.Column(db.Integer)
9 |
10 | role_permission = db.Column(db.Boolean, default=True)
--------------------------------------------------------------------------------
/src/seed/runner/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import click
3 |
4 | from seed.utils.imports import import_string
5 |
6 | version_string = '0.1'
7 |
8 |
9 | @click.group()
10 | @click.option(
11 | '--config',
12 | default='',
13 | envvar='SEED_CONF',
14 | help='Path to config file',
15 | metavar='PATH'
16 | )
17 | @click.version_option(version=version_string)
18 | @click.pass_context
19 | def cli(ctx, config):
20 | if config:
21 | os.environ['SEED_CONF'] = config
22 | else:
23 | os.environ['SEED_CONF'] = '~/.seed'
24 |
25 |
26 | cmds = [
27 | 'seed.runner.commands.run.run',
28 | 'seed.runner.commands.init.init',
29 | 'seed.runner.commands.upgrade.upgrade',
30 | ]
31 |
32 | for cmd in cmds:
33 | cli.add_command(import_string(cmd))
34 |
35 |
36 | def main():
37 | cli()
38 |
--------------------------------------------------------------------------------
/src/seed/runner/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/runner/commands/__init__.py
--------------------------------------------------------------------------------
/src/seed/runner/commands/init.py:
--------------------------------------------------------------------------------
1 | import os
2 | import click
3 |
4 | from seed.runner.commands.run import Address
5 |
6 |
7 | @click.command()
8 | @click.option(
9 | '--dev', default=False, is_flag=True, help='Use settings more conducive to local development'
10 | )
11 | @click.argument('directory', required=False)
12 | @click.pass_context
13 | def init(ctx, dev, directory):
14 | """ Initialize new configuration directory.
15 | """
16 | from seed.runner.setting import discover_configs, generate_settings
17 |
18 | if directory:
19 | os.environ['SEED_CONF'] = directory
20 |
21 | directory, py, yaml = discover_configs()
22 |
23 | if directory and not os.path.exists(directory):
24 | os.makedirs(directory)
25 |
26 | py_contents, yaml_contents = generate_settings(dev)
27 |
28 | if os.path.isfile(yaml):
29 | click.confirm(
30 | "File already exists at '%s', overwrite?" % click.format_filename(yaml),
31 | abort=True
32 | )
33 |
34 | with click.open_file(yaml, 'w') as fp:
35 | fp.write(yaml_contents)
36 |
37 | if os.path.isfile(py):
38 | click.confirm(
39 | "File already exists at '%s', overwrite?" % click.format_filename(py),
40 | abort=True
41 | )
42 |
43 | with click.open_file(py, 'w') as fp:
44 | fp.write(py_contents)
--------------------------------------------------------------------------------
/src/seed/runner/commands/run.py:
--------------------------------------------------------------------------------
1 | import click
2 |
3 |
4 | class AddressParamType(click.ParamType):
5 | name = 'address'
6 |
7 | def __call__(self, value, param=None, ctx=None):
8 | if value is None:
9 | return (None, None)
10 | return self.convert(value, param, ctx)
11 |
12 | def convert(self, value, param, ctx):
13 | if ':' in value:
14 | host, port = value.split(':', 1)
15 | port = int(port)
16 | else:
17 | host = value
18 | port = None
19 | return host, port
20 |
21 |
22 | Address = AddressParamType()
23 |
24 |
25 | @click.group()
26 | def run():
27 | "Run a service."
28 |
29 | @run.command()
30 | @click.option(
31 | '--debug', '-d', default=False,
32 | help='The debug option for web'
33 | )
34 | def web(debug):
35 | from seed.services.app import application
36 | from seed.services.wsgi import SeedWSGIHttpServer
37 |
38 | if debug:
39 | application.run()
40 | else:
41 | SeedWSGIHttpServer(application).run()
42 |
--------------------------------------------------------------------------------
/src/seed/runner/commands/upgrade.py:
--------------------------------------------------------------------------------
1 | import os
2 | import click
3 |
4 |
5 | @click.command()
6 | @click.option('--sql', is_flag=False,
7 | help=("Don't emit SQL to database - dump to standard "
8 | "output instead"))
9 | @click.pass_context
10 | def upgrade(ctx, sql=False):
11 | """ Upgrade database data and strcuct
12 | """
13 | from seed.services.app import SeedHttpServer
14 | from seed.runner.setting import discover_configs
15 |
16 | _, config_file, _ = discover_configs()
17 | SeedHttpServer(
18 | config_file=config_file
19 | ).upgrade(sql)
20 |
--------------------------------------------------------------------------------
/src/seed/runner/initializer.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 |
4 | def initialize_app(config):
5 | """
6 | initialize a application
7 | """
8 |
9 | configure_logging()
10 |
11 |
12 | def configure_logging():
13 | """
14 | configure logging
15 | """
16 | from seed.conf import server
17 | logging.config.dictConfig(server.LOGGING)
--------------------------------------------------------------------------------
/src/seed/runner/setting.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | DEFAULT_SETTINGS_CONF = 'config.yaml'
5 | DEFAULT_SETTINGS_OVERRIDE = 'seed_conf.py'
6 |
7 |
8 | def discover_configs():
9 | """ discover seed config files
10 | """
11 |
12 | try:
13 | config = os.environ['SEED_CONF']
14 | except KeyError:
15 | config = '~/.seed'
16 |
17 | config = os.path.expanduser(config)
18 |
19 | return (
20 | config, os.path.join(config, DEFAULT_SETTINGS_OVERRIDE),
21 | os.path.join(config, DEFAULT_SETTINGS_CONF)
22 | )
23 |
24 |
25 | def load_config_template(path, version='default'):
26 | from pkg_resources import resource_string
27 | return resource_string('seed', 'data/config/%s.%s' % (path, version)).decode('utf-8')
28 |
29 |
30 | def generate_settings(dev):
31 | """ generate default setting file contents
32 | """
33 |
34 | context = {
35 | 'debug_flag': dev,
36 | }
37 |
38 | py = load_config_template(DEFAULT_SETTINGS_OVERRIDE, 'default') % context
39 | yaml = load_config_template(DEFAULT_SETTINGS_CONF, 'default') % context
40 |
41 | return py, yaml
42 |
--------------------------------------------------------------------------------
/src/seed/schema/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/schema/__init__.py
--------------------------------------------------------------------------------
/src/seed/schema/base.py:
--------------------------------------------------------------------------------
1 | from seed.models._base import session, ma
2 |
3 |
4 | class BaseSchema(ma.ModelSchema):
5 |
6 | class Meta:
7 | sqla_session = session
8 | include_fk = True
9 |
--------------------------------------------------------------------------------
/src/seed/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/services/__init__.py
--------------------------------------------------------------------------------
/src/seed/services/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | import redis
5 | import flask_migrate
6 | from flask import Flask, g
7 | from flask_cors import CORS
8 |
9 | from seed.runner.setting import discover_configs
10 |
11 | from seed.models import db, migrate, session, ma
12 | from seed.api.urls import register_api
13 | from seed.utils.auth import SSOAuth, SessionAuth
14 | from seed.cache.user_bussiness import UserBussinessCache
15 | from seed.models.init import init_database_default_analogdata, init_database_default_datas, is_new_databases
16 | from seed.utils.helper import template_folder_path, static_folder_path
17 |
18 |
19 | class SeedHttpServer(object):
20 | def __init__(
21 | self, config_file, extra_options=None
22 | ):
23 | self.app = self.create_app(config_file)
24 | self.register_cors()
25 |
26 | self.register_databases()
27 | self.register_cache()
28 | self.register_api()
29 | self.register_hook()
30 |
31 | def create_app(self, config_file):
32 | app = Flask(
33 | __name__,
34 | static_url_path='/static',
35 | static_folder=static_folder_path,
36 | template_folder=template_folder_path
37 | )
38 |
39 | app.config.from_pyfile(config_file)
40 |
41 | return app
42 |
43 | def register_databases(self):
44 | db.init_app(self.app)
45 | migrate.init_app(app=self.app, db=db)
46 | ma.init_app(self.app)
47 |
48 | with self.app.app_context():
49 | session.configure(bind=db.engine)
50 |
51 | def register_cache(self):
52 | redis_pool = redis.ConnectionPool.from_url(
53 | self.app.config['REDIS_URL'],
54 | decode_responses=True,
55 | charset="utf-8",
56 | )
57 | self.app.cache = redis.Redis(connection_pool=redis_pool)
58 |
59 | def register_api(self):
60 | register_api(self.app)
61 |
62 | from seed.api import front
63 | self.app.register_blueprint(front.bp, url_prefix='')
64 |
65 | def register_hook(self):
66 | """ 注册Hook
67 | """
68 | @self.app.before_request
69 | def login_user():
70 |
71 | if self.app.config['AUTH_TYPE'] == 'SSO':
72 | auth = SSOAuth()
73 | else:
74 | auth = SessionAuth()
75 |
76 | g.user = auth.get_current_user()
77 |
78 | # debugger
79 | g.user = auth.debbuger_user()
80 |
81 | if g.user:
82 | g.bussiness_id = UserBussinessCache().get(g.user.id) or -1
83 | else:
84 | g.bussiness_id = -1
85 |
86 | def register_cors(self):
87 | CORS(self.app, supports_credentials=True)
88 |
89 | def run(self):
90 | self.app.run(self.app.config['HOST'], self.app.config['PORT'])
91 |
92 | def upgrade(self, sql):
93 | print('数据库开始初始化')
94 | with self.app.app_context():
95 | migrate_directory = self.app.extensions['migrate'].directory
96 |
97 | migrate_path, _, _ = discover_configs()
98 | migrate_path = os.path.join(migrate_path, migrate_directory)
99 |
100 | if not os.path.exists(os.path.join(migrate_path, 'alembic.ini')):
101 | # 判断是不是第一次初始化数据库
102 | is_empty = input('第一次初始化数据库,请确保你的数据库是空的。Y/N? ')
103 | if is_empty != 'Y':
104 | return
105 |
106 | flask_migrate.init(migrate_path)
107 |
108 | flask_migrate.migrate(migrate_path, sql=sql)
109 | flask_migrate.upgrade(migrate_path, sql=sql)
110 |
111 | if is_new_databases():
112 | # 写入默认数据
113 | print('初始化模拟数据')
114 | init_database_default_analogdata()
115 | print('初始化默认业务模块')
116 | init_database_default_datas()
117 |
118 | print("数据库升级完成")
119 |
120 |
121 | def apply_application():
122 | _, config_file, _ = discover_configs()
123 | http_server = SeedHttpServer(
124 | config_file=config_file
125 | )
126 | return http_server.app
127 |
128 |
129 | application = apply_application()
130 |
--------------------------------------------------------------------------------
/src/seed/services/wsgi.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | DEFAULT_HOST = '127.0.0.1'
5 | DEFAULT_PORT = 5000
6 |
7 |
8 | def convert_options_to_uwsgi_env(options):
9 | for key, value in options.items():
10 | key = 'UWSGI_' + key.upper().replace('-', '_')
11 |
12 | if isinstance(value, int):
13 | value = str(value)
14 |
15 | yield key, value
16 |
17 |
18 | class SeedWSGIHttpServer(object):
19 | def __init__(self, debug=False, workers=None):
20 |
21 | options = {}
22 | options.setdefault('module', 'seed.services.app:application')
23 | options.setdefault('protocol', 'http')
24 | options.setdefault('workers', 3)
25 | options.setdefault('threads', 4)
26 | options.setdefault('http-timeout', 60)
27 | options.setdefault('need-app', True)
28 | options.setdefault('virtualenv', sys.prefix)
29 | options.setdefault('die-on-term', True)
30 | options.setdefault(
31 | 'log-format',
32 | '%(addr) - %(user) [%(ltime)] "%(method) %(uri) %(proto)" %(status) %(size) "%(referer)" "%(uagent)"'
33 | )
34 |
35 | from seed.services.app import application
36 | options.setdefault('%s-socket' % options['protocol'], '%s:%s' % (
37 | application.config.get('HOST', DEFAULT_HOST),
38 | application.config.get('PORT', DEFAULT_PORT)
39 | ))
40 |
41 | options['master'] = True
42 | options['enable-threads'] = True
43 |
44 | self.options = options
45 |
46 | def prepare_environment(self, env=None):
47 | if not env:
48 | env = os.environ
49 |
50 | for key, value in convert_options_to_uwsgi_env(self.options):
51 | env.setdefault(key, value)
52 |
53 | virtualenv_path = os.path.dirname(os.path.abspath(sys.argv[0]))
54 | current_path = str(os.path.realpath(__file__))
55 | if virtualenv_path not in current_path:
56 | env['PATH'] = '%s:%s' % (virtualenv_path, current_path)
57 |
58 | def run(self):
59 | self.prepare_environment()
60 | os.execvp('uwsgi', ('uwsgi', ))
61 |
--------------------------------------------------------------------------------
/src/seed/static/OpenSans-Regular.629a55a7e793da068dc5.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/OpenSans-Regular.629a55a7e793da068dc5.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/compConfig/compConfig.js:
--------------------------------------------------------------------------------
1 | const config_ = {
2 | "list": [
3 | {
4 | "groupType": "compGroup",
5 | "groupTitle": "报表插件",
6 | "groupList": [
7 | {
8 | "name": "折线图",
9 | "icon": "./../../assets/images/zxt.jpg",
10 | "type": "line"
11 | },
12 | {
13 | "name": "对比图",
14 | "icon": "./../../assets/images/dbt.jpg",
15 | "type": "bar_cross"
16 | },
17 | {
18 | "name": "饼状图",
19 | "icon": "./../../assets/images/bt.jpg",
20 | "type": "pie"
21 | },
22 | {
23 | "name": "对比趋势图",
24 | "icon": "./../../assets/images/dbqst.jpg",
25 | "type": "linestack"
26 | },
27 | {
28 | "name": "漏斗图",
29 | "icon": "./../../assets/images/ldt.jpg",
30 | "type": "funnel"
31 | },
32 | {
33 | "name": "桑基图",
34 | "icon": "./../../assets/images/sankey.jpg",
35 | "type": "sankey"
36 | },
37 | {
38 | "name": "地图",
39 | "icon": "./../../assets/images/map.jpg",
40 | "type": "map"
41 | },
42 | {
43 | "name": "表格",
44 | "icon": "./../../assets/images/bgt.jpg",
45 | "type": "table"
46 | }
47 | ]
48 | },
49 | {
50 | "groupType": "compGroup",
51 | "groupTitle": "全局过滤插件",
52 | "groupList": [
53 | {
54 | "name": "单日期",
55 | "icon": "./../../assets/images/drq.jpg",
56 | "type": "flatpickr_single",
57 | "cascades": {}
58 | },
59 | {
60 | "name": "双日期",
61 | "icon": "./../../assets/images/srq.jpg",
62 | "type": "flatpickr_range",
63 | "cascades": {}
64 | },
65 | {
66 | "name": "下拉单选",
67 | "icon": "./../../assets/images/xldx.jpg",
68 | "type": "singleSelect",
69 | "list": [],
70 | "value": "",
71 | "ename":"",
72 | "cname":"",
73 | "db":"",
74 | "sql":"",
75 | "sourceType":"dict",
76 | "cascades": {}
77 | },
78 | {
79 | "name": "下拉多选",
80 | "icon": "./../../assets/images/xlfx.jpg",
81 | "type": "multiSelect",
82 | "list": [],
83 | "value": "",
84 | "ename":"",
85 | "cname":"",
86 | "db":"",
87 | "sql":"",
88 | "sourceType":"dict",
89 | "cascades": {}
90 | }
91 | ]
92 | }
93 | ]
94 | }
95 |
96 | export {config_};
97 |
--------------------------------------------------------------------------------
/src/seed/static/assets/compConfig/reportConfig.js:
--------------------------------------------------------------------------------
1 | const config_ = {
2 | "line":{
3 | "totalNum": 100, // 指标纬度个数总和
4 | "dimensionsNum": 100, // 维度的个数
5 | "indexsNum": 100, //指标的个数
6 | "dimensionsLimit":1, // 维度至少的个数
7 | "tips": "提示:该报表支持多指标多维度配置!请至少配置一个指标和一个维度~",
8 | "changeList":[]
9 | },
10 | "bar":{
11 | "direction": "cross",
12 | "totalNum": 100, // 指标纬度个数总和
13 | "dimensionsNum": 100, // 维度的个数
14 | "indexsNum": 100, //指标的个数
15 | "dimensionsLimit":1, // 维度至少的个数
16 | "tips": "提示:该报表支持多指标多维度配置!请至少配置一个指标和一个维度~",
17 | "changeList":[['bar', '对比图'], ['pie', '饼状图']]
18 | },
19 | "pie":{
20 | "totalNum": 100, // 指标纬度个数总和
21 | "dimensionsNum": 100, // 维度的个数
22 | "indexsNum": 100, //指标的个数
23 | "dimensionsLimit":1, // 维度至少的个数
24 | "tips": "提示:该报表支持多指标多维度配置!请至少配置一个指标和一个维度~",
25 | "changeList":[['pie', '饼状图'], ['bar', '对比图']]
26 | },
27 | "linestack":{
28 | "totalNum": 100, // 指标纬度个数总和
29 | "dimensionsNum": 100, // 维度的个数
30 | "indexsNum": 1, //指标的个数
31 | "dimensionsLimit":2, // 维度至少的个数
32 | "tips": "提示:该报表只支持单指标多维度配置!维度中必须有fdate这个维度",
33 | "changeList":[['linestack', '对比趋势图'],['bar', '对比图'], ['pie', '饼状图']],
34 | "limit": {
35 | "dimensions": ["fdate"]
36 | }
37 | },
38 | "funnel":{
39 | "totalNum": 100, // 指标纬度个数总和
40 | "dimensionsNum": 100, // 维度的个数
41 | "indexsNum": 100, //指标的个数
42 | "dimensionsLimit":1, // 维度至少的个数
43 | "tips": "提示:该报表支持多指标多维度配置!请至少配置一个指标和一个维度~"
44 | },
45 | "sankey":{
46 | "totalNum": 100, // 指标纬度个数总和
47 | "dimensionsNum": 2, // 维度的个数
48 | "indexsNum": 1, //指标的个数
49 | "dimensionsLimit": 2, // 维度至少的个数
50 | "tips": "提示:该报表支持配置多个指标和两个维度(source、target)~",
51 | "noTable": true
52 | },
53 | "map":{
54 | "totalNum": 100, // 指标纬度个数总和
55 | "dimensionsNum": 4, // 维度的个数
56 | "indexsNum": 1, //指标的个数
57 | "dimensionsLimit": 4, // 维度至少的个数
58 | "tips": "提示:该报表支持配置多个指标和四个维度:地区(region_name)、地区上级id(fpid)、纬度(lat)、经度(lng)~",
59 | "noTable": true
60 | },
61 | "table":{
62 | "totalNum": 100, // 指标纬度个数总和
63 | "dimensionsNum": 100, // 维度的个数
64 | "indexsNum": 100, //指标的个数
65 | "needIndexs": false, //是否必须有指标
66 | "dimensionsLimit":1, // 维度至少的个数
67 | "tips": "提示:该报表支持多指标多维度配置!"
68 | }
69 | }
70 |
71 | export {config_};
72 |
--------------------------------------------------------------------------------
/src/seed/static/assets/fonts/OpenSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-Bold.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/fonts/OpenSans-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-BoldItalic.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/fonts/OpenSans-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-ExtraBold.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/fonts/OpenSans-ExtraBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-ExtraBoldItalic.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/fonts/OpenSans-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-Italic.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/fonts/OpenSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-Light.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/fonts/OpenSans-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-LightItalic.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/fonts/OpenSans-Semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-Semibold.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/fonts/OpenSans-SemiboldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-SemiboldItalic.ttf
--------------------------------------------------------------------------------
/src/seed/static/assets/images/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/arrow.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/avatar.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/bgt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/bgt.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/bt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/bt.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/businessIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/businessIcon.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/businessNo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/businessNo.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/cloudPlane.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/cloudPlane.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/dbqst.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/dbqst.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/dbt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/dbt.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/drq.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/drq.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/giphy1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/giphy1.gif
--------------------------------------------------------------------------------
/src/seed/static/assets/images/ldt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/ldt.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/line.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/line.gif
--------------------------------------------------------------------------------
/src/seed/static/assets/images/loading-bars.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/seed/static/assets/images/loading-spinning-bubbles.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/seed/static/assets/images/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/loading.gif
--------------------------------------------------------------------------------
/src/seed/static/assets/images/loginIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/loginIcon.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/logo.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/logo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/logo1.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/mans.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/mans.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/map.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/map.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/password-meter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/password-meter.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/sankey.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/sankey.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/srq.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/srq.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/superman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/superman.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/userFace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/userFace.png
--------------------------------------------------------------------------------
/src/seed/static/assets/images/xldx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/xldx.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/xlfx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/xlfx.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/images/zxt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/zxt.jpg
--------------------------------------------------------------------------------
/src/seed/static/assets/js/bowser.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bowser - a browser detector
3 | * https://github.com/ded/bowser
4 | * MIT License | (c) Dustin Diaz 2015
5 | */
6 | !function(e,t,n){typeof module!="undefined"&&module.exports?module.exports=n():typeof define=="function"&&define.amd?define(t,n):e[t]=n()}(this,"bowser",function(){function t(t){function n(e){var n=t.match(e);return n&&n.length>1&&n[1]||""}function r(e){var n=t.match(e);return n&&n.length>1&&n[2]||""}var i=n(/(ipod|iphone|ipad)/i).toLowerCase(),s=/like android/i.test(t),o=!s&&/android/i.test(t),u=/nexus\s*[0-6]\s*/i.test(t),a=!u&&/nexus\s*[0-9]+/i.test(t),f=/CrOS/.test(t),l=/silk/i.test(t),c=/sailfish/i.test(t),h=/tizen/i.test(t),p=/(web|hpw)os/i.test(t),d=/windows phone/i.test(t),v=/SamsungBrowser/i.test(t),m=!d&&/windows/i.test(t),g=!i&&!l&&/macintosh/i.test(t),y=!o&&!c&&!h&&!p&&/linux/i.test(t),b=n(/edge\/(\d+(\.\d+)?)/i),w=n(/version\/(\d+(\.\d+)?)/i),E=/tablet/i.test(t),S=!E&&/[^-]mobi/i.test(t),x=/xbox/i.test(t),T;/opera/i.test(t)?T={name:"Opera",opera:e,version:w||n(/(?:opera|opr|opios)[\s\/](\d+(\.\d+)?)/i)}:/opr|opios/i.test(t)?T={name:"Opera",opera:e,version:n(/(?:opr|opios)[\s\/](\d+(\.\d+)?)/i)||w}:/SamsungBrowser/i.test(t)?T={name:"Samsung Internet for Android",samsungBrowser:e,version:w||n(/(?:SamsungBrowser)[\s\/](\d+(\.\d+)?)/i)}:/coast/i.test(t)?T={name:"Opera Coast",coast:e,version:w||n(/(?:coast)[\s\/](\d+(\.\d+)?)/i)}:/yabrowser/i.test(t)?T={name:"Yandex Browser",yandexbrowser:e,version:w||n(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)}:/ucbrowser/i.test(t)?T={name:"UC Browser",ucbrowser:e,version:n(/(?:ucbrowser)[\s\/](\d+(?:\.\d+)+)/i)}:/mxios/i.test(t)?T={name:"Maxthon",maxthon:e,version:n(/(?:mxios)[\s\/](\d+(?:\.\d+)+)/i)}:/epiphany/i.test(t)?T={name:"Epiphany",epiphany:e,version:n(/(?:epiphany)[\s\/](\d+(?:\.\d+)+)/i)}:/puffin/i.test(t)?T={name:"Puffin",puffin:e,version:n(/(?:puffin)[\s\/](\d+(?:\.\d+)?)/i)}:/sleipnir/i.test(t)?T={name:"Sleipnir",sleipnir:e,version:n(/(?:sleipnir)[\s\/](\d+(?:\.\d+)+)/i)}:/k-meleon/i.test(t)?T={name:"K-Meleon",kMeleon:e,version:n(/(?:k-meleon)[\s\/](\d+(?:\.\d+)+)/i)}:d?(T={name:"Windows Phone",windowsphone:e},b?(T.msedge=e,T.version=b):(T.msie=e,T.version=n(/iemobile\/(\d+(\.\d+)?)/i))):/msie|trident/i.test(t)?T={name:"Internet Explorer",msie:e,version:n(/(?:msie |rv:)(\d+(\.\d+)?)/i)}:f?T={name:"Chrome",chromeos:e,chromeBook:e,chrome:e,version:n(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)}:/chrome.+? edge/i.test(t)?T={name:"Microsoft Edge",msedge:e,version:b}:/vivaldi/i.test(t)?T={name:"Vivaldi",vivaldi:e,version:n(/vivaldi\/(\d+(\.\d+)?)/i)||w}:c?T={name:"Sailfish",sailfish:e,version:n(/sailfish\s?browser\/(\d+(\.\d+)?)/i)}:/seamonkey\//i.test(t)?T={name:"SeaMonkey",seamonkey:e,version:n(/seamonkey\/(\d+(\.\d+)?)/i)}:/firefox|iceweasel|fxios/i.test(t)?(T={name:"Firefox",firefox:e,version:n(/(?:firefox|iceweasel|fxios)[ \/](\d+(\.\d+)?)/i)},/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(t)&&(T.firefoxos=e)):l?T={name:"Amazon Silk",silk:e,version:n(/silk\/(\d+(\.\d+)?)/i)}:/phantom/i.test(t)?T={name:"PhantomJS",phantom:e,version:n(/phantomjs\/(\d+(\.\d+)?)/i)}:/slimerjs/i.test(t)?T={name:"SlimerJS",slimer:e,version:n(/slimerjs\/(\d+(\.\d+)?)/i)}:/blackberry|\bbb\d+/i.test(t)||/rim\stablet/i.test(t)?T={name:"BlackBerry",blackberry:e,version:w||n(/blackberry[\d]+\/(\d+(\.\d+)?)/i)}:p?(T={name:"WebOS",webos:e,version:w||n(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)},/touchpad\//i.test(t)&&(T.touchpad=e)):/bada/i.test(t)?T={name:"Bada",bada:e,version:n(/dolfin\/(\d+(\.\d+)?)/i)}:h?T={name:"Tizen",tizen:e,version:n(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i)||w}:/qupzilla/i.test(t)?T={name:"QupZilla",qupzilla:e,version:n(/(?:qupzilla)[\s\/](\d+(?:\.\d+)+)/i)||w}:/chromium/i.test(t)?T={name:"Chromium",chromium:e,version:n(/(?:chromium)[\s\/](\d+(?:\.\d+)?)/i)||w}:/chrome|crios|crmo/i.test(t)?T={name:"Chrome",chrome:e,version:n(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)}:o?T={name:"Android",version:w}:/safari|applewebkit/i.test(t)?(T={name:"Safari",safari:e},w&&(T.version=w)):i?(T={name:i=="iphone"?"iPhone":i=="ipad"?"iPad":"iPod"},w&&(T.version=w)):/googlebot/i.test(t)?T={name:"Googlebot",googlebot:e,version:n(/googlebot\/(\d+(\.\d+))/i)||w}:T={name:n(/^(.*)\/(.*) /),version:r(/^(.*)\/(.*) /)},!T.msedge&&/(apple)?webkit/i.test(t)?(/(apple)?webkit\/537\.36/i.test(t)?(T.name=T.name||"Blink",T.blink=e):(T.name=T.name||"Webkit",T.webkit=e),!T.version&&w&&(T.version=w)):!T.opera&&/gecko\//i.test(t)&&(T.name=T.name||"Gecko",T.gecko=e,T.version=T.version||n(/gecko\/(\d+(\.\d+)?)/i)),!T.windowsphone&&!T.msedge&&(o||T.silk)?T.android=e:!T.windowsphone&&!T.msedge&&i?(T[i]=e,T.ios=e):g?T.mac=e:x?T.xbox=e:m?T.windows=e:y&&(T.linux=e);var N="";T.windowsphone?N=n(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i):i?(N=n(/os (\d+([_\s]\d+)*) like mac os x/i),N=N.replace(/[_\s]/g,".")):o?N=n(/android[ \/-](\d+(\.\d+)*)/i):T.webos?N=n(/(?:web|hpw)os\/(\d+(\.\d+)*)/i):T.blackberry?N=n(/rim\stablet\sos\s(\d+(\.\d+)*)/i):T.bada?N=n(/bada\/(\d+(\.\d+)*)/i):T.tizen&&(N=n(/tizen[\/\s](\d+(\.\d+)*)/i)),N&&(T.osversion=N);var C=N.split(".")[0];if(E||a||i=="ipad"||o&&(C==3||C>=4&&!S)||T.silk)T.tablet=e;else if(S||i=="iphone"||i=="ipod"||o||u||T.blackberry||T.webos||T.bada)T.mobile=e;return T.msedge||T.msie&&T.version>=10||T.yandexbrowser&&T.version>=15||T.vivaldi&&T.version>=1||T.chrome&&T.version>=20||T.samsungBrowser&&T.version>=4||T.firefox&&T.version>=20||T.safari&&T.version>=6||T.opera&&T.version>=10||T.ios&&T.osversion&&T.osversion.split(".")[0]>=6||T.blackberry&&T.version>=10.1||T.chromium&&T.version>=20?T.a=e:T.msie&&T.version<10||T.chrome&&T.version<20||T.firefox&&T.version<20||T.safari&&T.version<6||T.opera&&T.version<10||T.ios&&T.osversion&&T.osversion.split(".")[0]<6||T.chromium&&T.version<20?T.c=e:T.x=e,T}function r(e){return e.split(".").length}function i(e,t){var n=[],r;if(Array.prototype.map)return Array.prototype.map.call(e,t);for(r=0;r=0){if(n[0][t]>n[1][t])return 1;if(n[0][t]!==n[1][t])return-1;if(t===0)return 0}}function o(e,r,i){var o=n;typeof r=="string"&&(i=r,r=void 0),r===void 0&&(r=!1),i&&(o=t(i));var u=""+o.version;for(var a in e)if(e.hasOwnProperty(a)&&o[a]){if(typeof e[a]!="string")throw new Error("Browser version in the minVersion map should be a string: "+a+": "+String(e));return s([u,e[a]])<0}return r}function u(e,t,n){return!o(e,t,n)}var e=!0,n=t(typeof navigator!="undefined"?navigator.userAgent||"":"");return n.test=function(e){for(var t=0;tSEED自助报表
--------------------------------------------------------------------------------
/src/seed/static/inline.07bd3aeef3ad0ebf96d7.bundle.js:
--------------------------------------------------------------------------------
1 | !function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,a,c){for(var u,i,f,l=0,s=[];l
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/seed/static/mans.1302759b8be148f12915.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/mans.1302759b8be148f12915.png
--------------------------------------------------------------------------------
/src/seed/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/utils/__init__.py
--------------------------------------------------------------------------------
/src/seed/utils/auth.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import functools
4 | from datetime import datetime
5 |
6 | import requests
7 | from flask import current_app, jsonify, g, request
8 |
9 | from seed.utils.response import response, HttpErrorCode
10 | from seed.models.account import Account
11 | from seed.models.bmanager import BManager
12 | from seed.cache import DefaultCache
13 | from seed.cache.session import SessionCache
14 | from seed.cache.user_bussiness import UserBussinessCache
15 |
16 |
17 | class APIRequireRole(object):
18 | roles = {
19 | "new": 0,
20 | "user": 1,
21 | "admin": 2,
22 | "super_admin": 3
23 | }
24 |
25 | def __init__(self, role):
26 | self.role = role
27 |
28 | def __call__(self, method):
29 | @functools.wraps(method)
30 | def wrapper(*args, **kwargs):
31 | if not g.user:
32 | # 未登录
33 | auth_type = current_app.config["AUTH_TYPE"]
34 | sso_url = current_app.config["SSO_URL"]
35 | login_url = request.host_url + "login"
36 | if sso_url and auth_type == "SSO":
37 | return jsonify(response(-14, data=sso_url))
38 | else:
39 | return jsonify(response(HttpErrorCode.UNAUTHORIZED, data=login_url))
40 |
41 | if g.user.status == -1:
42 | # 废弃账号
43 | return jsonify((response(HttpErrorCode.FORBIDDEN)))
44 |
45 | if not self.role or g.user.id == 1:
46 | return method(*args, **kwargs)
47 |
48 | if self.roles[g.user.role] < self.roles[self.role]:
49 | # 没有足够的权限
50 | return jsonify(response(HttpErrorCode.FORBIDDEN))
51 |
52 | return method(*args, **kwargs)
53 |
54 | return wrapper
55 |
56 |
57 | api_require_login = APIRequireRole(None)
58 | api_require_new = APIRequireRole("new")
59 | api_require_user = APIRequireRole("user")
60 | api_require_admin = APIRequireRole("admin")
61 | api_require_super_admin = APIRequireRole("super_admin")
62 |
63 |
64 | class RequireRole(object):
65 | roles = {
66 | "new": 0,
67 | "user": 1,
68 | "admin": 2,
69 | "super_admin": 3
70 | }
71 |
72 | def __init__(self, role):
73 | self.role = role
74 |
75 | def __call__(self):
76 | if not g.user:
77 | # 未登录
78 | return False
79 |
80 | if g.user.status == -1:
81 | # 废弃账号
82 | return False
83 |
84 | if not self.role or g.user.id == 1:
85 | return True
86 |
87 | if self.roles[g.user.role] < self.roles[self.role]:
88 | # 没有足够的权限
89 | return False
90 |
91 | return True
92 |
93 |
94 | require_login = RequireRole(None)
95 | require_new = RequireRole("new")
96 | require_user = RequireRole("user")
97 | require_admin = RequireRole("admin")
98 | require_super_admin = RequireRole("super_admin")
99 |
100 |
101 | class BaseAuth(object):
102 | """ 验证基础类
103 | """
104 | def get_current_user(self):
105 | """ 获取当前用户
106 | """
107 | raise NotImplementedError("Not Implemented")
108 |
109 | def login_user(self, user):
110 | """ 登录用户
111 | """
112 | raise NotImplementedError("Not Implemented")
113 |
114 | def logout_user(self):
115 | """ 登出用户
116 | """
117 | raise NotImplementedError("Not Implemented")
118 |
119 | def debbuger_user(self):
120 | if not require_super_admin() or 'debugger' not in request.args:
121 | return g.user
122 |
123 | user = Account.query.filter_by(id=int(request.args['debugger'])).first()
124 |
125 | if user:
126 | bussiness_id = UserBussinessCache().get(user.id) or 1
127 | if self._is_bussiness_admin(user.id, bussiness=bussiness_id):
128 | user.role = 'admin'
129 |
130 | return user if user else None
131 |
132 | def _is_bussiness_admin(self, uid, bussiness=1):
133 | roles = BManager.query.filter_by(bussiness_id=bussiness, user_id=uid).all()
134 | return True if roles else False
135 |
136 |
137 | class SessionAuth(BaseAuth):
138 | """ Session登录权限类
139 | """
140 | def get_current_user(self):
141 | session_token = request.cookies.get('session_token', '')
142 | user_id = SessionCache().get_user_id_by_token(session_token)
143 | if not user_id:
144 | return None
145 |
146 | user = Account.query.filter_by(id=user_id).first()
147 | if user:
148 | bussiness_id = UserBussinessCache().get(user.id) or 1
149 | # TODO 需要修复db.model自动保存的问题
150 | if self._is_bussiness_admin(user.id, bussiness=bussiness_id) and user.role != 'super_admin':
151 | user.role = 'admin'
152 |
153 | user.role = 'super_admin' if user.id == 1 else user.role
154 |
155 | return user
156 |
157 |
158 | class SSOAuth(BaseAuth):
159 | """ SSO权限校验登录类
160 | """
161 | def get_current_user(self):
162 | # 获取当前用户信息在redis中存储的信息
163 | uid = request.cookies.get('admin_uid', None)
164 | uid_key = request.cookies.get('admin_key', None)
165 |
166 | if not (uid and uid_key):
167 | return None
168 |
169 | # 判断信息的有效性
170 | today = datetime.strftime(datetime.today(), '%Y-%m-%d')
171 | login_at = self._get_login_cache(uid)
172 | if today != login_at:
173 | # 去SSO中校验用户的有效性
174 | user_info = self._sso_verification(uid, uid_key)
175 | if not user_info:
176 | return None
177 |
178 | # 创建新用户 或者 获取用户的user_id
179 | user = self._get_user_id(user_info)
180 |
181 | self._account_valid(user)
182 |
183 | # 存储用户信息到Redis中
184 | self._set_login_cache(uid, today)
185 | else:
186 | user = Account.query.filter_by(sso_id=int(uid)).first()
187 |
188 | if user:
189 | bussiness_id = UserBussinessCache().get(user.id) or 1
190 | if self._is_bussiness_admin(user.id, bussiness=bussiness_id) and user.role != 'super_admin':
191 | user.role = 'admin'
192 |
193 | user.role = 'super_admin' if user.id == 1 else user.role
194 |
195 | return user
196 |
197 | def cache(self):
198 | return DefaultCache(current_app.cache)
199 |
200 | def _account_valid(self, user):
201 | if user.status == -1:
202 | raise Exception("账号已经被注销")
203 |
204 | def _get_user_id(self, user_info):
205 | account = Account.query.filter_by(sso_id=int(user_info['id'])).first()
206 | if not account:
207 | # 创建信息用户
208 | account = Account(
209 | sso_id=int(user_info['id']),
210 | account=user_info['username'],
211 | email=user_info['email'],
212 | avatar='http://oahead-static.17c.cn/oahead/' + str(int(user_info['code'])) + '.png',
213 | name=user_info['cname'],
214 | role='user',
215 | status=1
216 | )
217 | else:
218 | account.login_at = datetime.utcnow()
219 | account.save()
220 | return account
221 |
222 | def _sso_verification(self, uid, uid_key):
223 | check_sso_user_url = "http://sso.ifere.com:8871/api?do=getInfo&uid=%s&key=%s&appid=1172" % (uid, uid_key)
224 |
225 | try:
226 | data_str = requests.get(check_sso_user_url, timeout=5)
227 | ret = json.loads(data_str.text)
228 |
229 | if "ret" not in ret or '1' != str(ret['ret']):
230 | logging.warning('user is invalid uid:%s uid_key:%s' % (uid, uid_key))
231 | return []
232 |
233 | return ret
234 | except Exception as e:
235 |
236 | logging.warning('error: %s' % e)
237 | return []
238 |
239 | def _set_login_cache(self, uid, login_at):
240 | self.cache().set(':'.join([uid, 'sso_login']), login_at)
241 |
242 | def _get_login_cache(self, uid):
243 | return self.cache().get(':'.join([uid, 'sso_login']))
244 |
--------------------------------------------------------------------------------
/src/seed/utils/database.py:
--------------------------------------------------------------------------------
1 | from seed.drives import ALL_DRIVES
2 |
3 |
4 | def get_db_instance(dtype, ip, port, name, user, password, **kwargs):
5 | # TODO 自动收集当前支持的驱动
6 | db = ALL_DRIVES[dtype](ip, int(port), name, user, password)
7 | return db
8 |
--------------------------------------------------------------------------------
/src/seed/utils/distutils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/utils/distutils/__init__.py
--------------------------------------------------------------------------------
/src/seed/utils/distutils/base.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import os
4 | import sys
5 |
6 | from distutils import log
7 | from subprocess import check_output
8 | from distutils.core import Command
9 |
10 |
11 | class BaseBuildCommand(Command):
12 | user_options = [
13 | ('work-path', 'w', 'The working directory for source files. Default is .')
14 | ]
15 |
16 | def initialize_options(self):
17 | self.work_path = '.'
18 |
19 | def finalize_options(self):
20 | pass
21 |
22 | def update_mainfests(self):
23 | pass
24 |
25 | def _setup_git(self):
26 | work_path = self.work_path
27 | if os.path.exists(os.path.join(work_path, '.git')):
28 | log.info("initializing git submodules")
29 | self._run_command(['git', 'submodule', 'init'])
30 | self._run_command(['git', 'submodule', 'update', '--remote'])
31 |
32 | def _setup_npm(self):
33 | node_version = []
34 |
35 | log.info("setup node and npm.....")
36 | for app in ['node', 'npm']:
37 | try:
38 | log.info('testing %s version.....' % app)
39 | node_version.append(self._run_command([app, '--version']).rstrip())
40 | except OSError as e:
41 | log.fatal(
42 | 'Cannot find {app} excutable. Please install {app}'
43 | 'and try again.'.format(app=app)
44 | )
45 | sys.exit(1)
46 |
47 | if node_version[0] < b'v6.8.1':
48 | log.fatal('The node version need v6.8.1, the current node version is %s' % node_version[0])
49 | sys.exit(1)
50 |
51 | log.info('using node ({0}) and npm ({1})'.format(*node_version))
52 | self._run_command(['npm', 'install'])
53 |
54 | def _run_command(self, cmd, env=None):
55 | log.debug('running [%s]' % (' '.join(cmd), ))
56 | try:
57 | return check_output(cmd, cwd=self.work_path, env=env, shell=True)
58 | except Exception:
59 | log.error('command failed [%s] via [%s]' % (' '.join(cmd), self.work_path, ))
60 | raise
61 |
62 | def sub_commands(self):
63 | pass
64 |
65 | def run(self):
66 | self._setup_git()
67 | self._setup_npm()
68 | self._build()
69 | self.update_mainfests()
70 |
--------------------------------------------------------------------------------
/src/seed/utils/distutils/build_assets.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import os
4 | import sys
5 | import shutil
6 | import traceback
7 |
8 | from distutils import log
9 |
10 | from .base import BaseBuildCommand
11 |
12 |
13 | class BuildAssetsCommand(BaseBuildCommand):
14 | user_options = BaseBuildCommand.user_options + [
15 |
16 | ]
17 |
18 | description = 'build static media assets'
19 |
20 | def initialize_options(self):
21 | BaseBuildCommand.initialize_options(self)
22 |
23 | def _build(self):
24 | try:
25 | self._build_static()
26 | except Exception:
27 | traceback.print_exc()
28 | log.fatal(
29 | 'unable to build Seed\'s static assets!\n'
30 | )
31 | sys.exit(1)
32 |
33 | self._move_statics()
34 |
35 | def _build_static(self):
36 | os.chdir('./seed_static')
37 |
38 | log.info('Seed static start install node modules.')
39 | self._run_command(['npm', 'install'])
40 |
41 | log.info('Seed static start build static files.')
42 | self._run_command(['npm', 'run', 'buildProd'])
43 |
44 | log.info('Seed static start build finish.')
45 | os.chdir('..')
46 |
47 | def _move_statics(self):
48 | source = os.path.join(*'./seed_static/dist'.split('/'))
49 | target = os.path.join(*'./src/seed/static'.split('/'))
50 |
51 | files = os.listdir(target)
52 | for file in files:
53 | if file in ['.gitignore']:
54 | continue
55 |
56 | if os.path.isdir(os.path.join(target, file)):
57 | shutil.rmtree(os.path.join(target, file))
58 | else:
59 | os.remove(os.path.join(target, file))
60 |
61 | files = os.listdir(source)
62 | for file in files:
63 | shutil.move(os.path.join(source, file), os.path.join(target, file))
64 |
65 | log.info('Move static files to seed finish.')
66 |
--------------------------------------------------------------------------------
/src/seed/utils/file.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 |
4 | from seed.utils.helper import local_file_path
5 |
6 |
7 | class LocalFile(object):
8 | def save(self, file):
9 | filename = '_'.join([str(int(time.time())), file.filename])
10 | upload_path = os.path.join(local_file_path, filename)
11 |
12 | if not os.path.exists(local_file_path):
13 | os.makedirs(local_file_path)
14 |
15 | try:
16 | file.save(upload_path)
17 | except Exception as e:
18 | print(e)
19 | return None
20 |
21 | return os.path.join('files', filename)
22 |
--------------------------------------------------------------------------------
/src/seed/utils/helper.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from pathlib import Path
4 | from inspect import getmembers
5 |
6 | from sqlalchemy.exc import InvalidRequestError
7 |
8 |
9 | def get_module_members(modules, predicate):
10 |
11 | members = getmembers(modules, predicate)
12 | return map(lambda m: m[1], members)
13 |
14 |
15 | def get_immediate_cls_attr(cls, attrname):
16 | if not issubclass(cls, object):
17 | return None
18 |
19 | for base in cls.__mro__:
20 | if attrname in base.__dict__ and base is cls:
21 | return getattr(base, attrname)
22 |
23 | return None
24 |
25 |
26 | def get_package_members(package, predicate, pre_url=''):
27 | from pkgutil import iter_modules
28 | from importlib import import_module
29 |
30 | members = {}
31 |
32 | for _, name, ispkg in iter_modules(package.__path__, package.__name__+'.'):
33 | if ispkg:
34 | sub_members = get_package_members(
35 | import_module(name), predicate,
36 | '/'.join([pre_url, name.split('.')[-1]])
37 | )
38 | members.update(sub_members)
39 |
40 | members.setdefault(pre_url, []).extend(
41 | get_module_members(import_module(name), predicate)
42 | )
43 |
44 | return members
45 |
46 |
47 | def common_batch_crud(schema, model, datas):
48 | """ 批量做增改删操作
49 | """
50 | # 删除多余的数据
51 | schema_instance = schema()
52 | delete_datas = [data for data in datas if data.get('status') == -1]
53 | delete_datas, errors = schema_instance.load(delete_datas, many=True)
54 | if errors:
55 | raise Exception(errors)
56 |
57 | for delete_data in delete_datas:
58 | try:
59 | delete_data.delete()
60 | except InvalidRequestError:
61 | pass
62 |
63 | # 新增和修改数据
64 | modify_datas = [data for data in datas if data.get('status') != -1]
65 | modify_datas, errors = schema_instance.load(modify_datas, many=True)
66 | if errors:
67 | raise Exception(errors)
68 | [modify_data.save() for modify_data in modify_datas]
69 |
70 | datas = schema(many=True, exclude=model.column_filter).dump(modify_datas)
71 | return datas
72 |
73 |
74 | template_folder_path = os.path.join(
75 | str(Path(os.path.dirname(os.path.realpath(__file__))).parent),
76 | 'static'
77 | )
78 |
79 | root_path = str(Path(os.path.dirname(os.path.realpath(__file__))).parent)
80 |
81 | static_folder_path = os.path.join(template_folder_path, 'static')
82 |
83 | local_file_path = os.path.join(template_folder_path, 'files')
84 |
--------------------------------------------------------------------------------
/src/seed/utils/imports.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class ModelProxyCache(dict):
4 | def __missing__(self, path):
5 | if '.' not in path:
6 | return __import__(path)
7 |
8 | module_name, class_name = path.rsplit('.', 1)
9 |
10 | module = __import__(module_name, {}, {}, [class_name])
11 | handler = getattr(module, class_name)
12 |
13 | self[path] = handler
14 | return handler
15 |
16 |
17 | _cache = ModelProxyCache()
18 |
19 |
20 | def import_string(path):
21 | """ Path must be models.path.ClassName
22 | """
23 | result = _cache[path]
24 | return result
25 |
--------------------------------------------------------------------------------
/src/seed/utils/mail.py:
--------------------------------------------------------------------------------
1 | from flask import current_app
2 |
3 | from seed.drives.email import Email
4 |
5 |
6 | def send_active_email(to_mail, active_url, redirect_url):
7 | """ 发送激活邮件
8 | """
9 | title = '新用户激活'
10 | mail_content = """
11 | 亲爱的用户:
12 | 感谢你使用Seed自定义报表系统,请点击以下链接激活用户:
13 | {active_url}?redirect_url={redirect_url}
14 | 此链接24小时之后失效,请尽快激活邮箱。
15 | Seed团队
16 | """.format(active_url=active_url, redirect_url=redirect_url)
17 | Email(current_app.config).send_mail([to_mail], title, mail_content)
18 |
19 |
20 | def send_reset_password_email(to_mail, active_url, redirect_url):
21 | """ 发送重置密码邮件
22 | """
23 | title = '密码重置'
24 | mail_content = """
25 | 亲爱的用户:
26 | 你的密码重置连接如下:
27 | {active_url}?redirect_url={redirect_url}
28 | 此链接24小时之后失效,请尽快点击重置密码。
29 | Seed团队
30 | """.format(active_url=active_url, redirect_url=redirect_url)
31 | Email(current_app.config).send_mail([to_mail], title, mail_content)
32 |
--------------------------------------------------------------------------------
/src/seed/utils/permissions.py:
--------------------------------------------------------------------------------
1 | from seed.models.bussiness import Bussiness
2 | from seed.models.bmanager import BManager
3 | from seed.models.buser import BUser
4 | from seed.models._base import session
5 |
6 |
7 | def get_permission_datas_by_user(user):
8 | all_datas = session.query(Bussiness).all()
9 | all_datas = [data.row2dict() for data in all_datas]
10 |
11 | if user.role == 'super_admin':
12 | # 所有权限
13 | for data in all_datas:
14 | data.update({'edit': True, 'delete': True})
15 | permission_datas = all_datas
16 | un_permission_datas = []
17 |
18 | else:
19 | # 管理的业务
20 | order_datas = session.query(Bussiness)\
21 | .join(BManager, Bussiness.id == BManager.bussiness_id)\
22 | .filter(BManager.user_id == user.id)\
23 | .all()
24 | order_ids = [data.id for data in order_datas]
25 | order_datas = [data.row2dict() for data in order_datas]
26 | for data in order_datas:
27 | data.update({'edit': True, 'delete': False})
28 |
29 | # 有权限的业务
30 | permission_datas = session.query(Bussiness)\
31 | .join(BUser, Bussiness.id == BUser.bussiness_id)\
32 | .filter(BUser.user_id == user.id)\
33 | .all()
34 | permission_ids = [data.id for data in permission_datas]
35 | permission_datas = [data.row2dict() for data in permission_datas]
36 | for data in permission_datas:
37 | data.update({'edit': False, 'delete': False})
38 |
39 | permission_datas = [
40 | data for data in permission_datas if data['id'] not in order_ids
41 | ]
42 | permission_datas = order_datas + permission_datas
43 | permission_ids = [data['id'] for data in permission_datas]
44 |
45 | un_permission_datas = [
46 | data for data in all_datas if data['id'] not in permission_ids
47 | ]
48 | return permission_datas, un_permission_datas
49 |
50 |
51 | def has_bussiness_permission(user, bussiness_id):
52 | permission_datas, _ = get_permission_datas_by_user(user)
53 | return any([bussiness_id == bussiness['id'] for bussiness in permission_datas])
54 |
--------------------------------------------------------------------------------
/src/seed/utils/response.py:
--------------------------------------------------------------------------------
1 | from flask import jsonify
2 |
3 |
4 | class HttpErrorCode(object):
5 | SUCCESS = 200
6 | ERROR = 400
7 | UNAUTHORIZED = 401
8 | FORBIDDEN = 403
9 | NOT_FUND = 404
10 | AUTHORIZED_ERROR = 420
11 | PARAMS_VALID_ERROR = 421
12 |
13 |
14 | ERROR_DICT = {
15 | HttpErrorCode.SUCCESS: 'SUCCESS!',
16 | HttpErrorCode.UNAUTHORIZED: 'Unauthorized!',
17 | HttpErrorCode.PARAMS_VALID_ERROR: 'Params validation error!',
18 | HttpErrorCode.FORBIDDEN: 'No permission!',
19 | HttpErrorCode.NOT_FUND: 'Not found!',
20 | HttpErrorCode.AUTHORIZED_ERROR: 'Password is valid!',
21 | }
22 |
23 |
24 | def response(code, message=None, data={}):
25 | response_content = {'code': code, 'data': data}
26 | response_content['message'] = message if message else ERROR_DICT.get(code, 'Unknown error code!')
27 | return response_content
28 |
29 |
30 | def response_json(code, msg=None, data={}):
31 | return jsonify(response(code, msg, data))
32 |
--------------------------------------------------------------------------------
/src/seed/utils/time.py:
--------------------------------------------------------------------------------
1 | from dateutil import tz
2 |
3 |
4 | def convert_utc_to_local(utc_datetime, time_zone):
5 | zone = tz.gettz(time_zone)
6 | return utc_datetime.replace(tzinfo=zone)
--------------------------------------------------------------------------------