Compare commits
366 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a2739774a | ||
|
|
012f3a2f63 | ||
|
|
64c5d505e7 | ||
|
|
3914432d9f | ||
|
|
cc6ac68223 | ||
|
|
05ee833fe5 | ||
|
|
ff736aae93 | ||
|
|
c0a68f91ca | ||
|
|
8d6abeb891 | ||
|
|
163e5eb2db | ||
|
|
e786f60cdd | ||
|
|
abe2db3c82 | ||
|
|
fa734dd479 | ||
|
|
6e9b94aed2 | ||
|
|
2a9f5be895 | ||
|
|
2ea34bfbd5 | ||
|
|
edbd6862a2 | ||
|
|
6c99a78821 | ||
|
|
3db048e650 | ||
|
|
e470a716e5 | ||
|
|
95bf503529 | ||
|
|
4e9155f09b | ||
|
|
3e60c5b5f9 | ||
|
|
b72fb5238f | ||
|
|
74d89c3968 | ||
|
|
5eab2b6e4f | ||
|
|
d2f14e41ad | ||
|
|
483863e238 | ||
|
|
ad95bea301 | ||
|
|
344f754fc7 | ||
|
|
749f5d80d2 | ||
|
|
ac3bfb6b80 | ||
|
|
ef48bdd0df | ||
|
|
a2e07a89e1 | ||
|
|
7b55a3db64 | ||
|
|
3c8b4b6ac0 | ||
|
|
e8212501e2 | ||
|
|
fa063a8611 | ||
|
|
b89f1671c3 | ||
|
|
c6c54d8013 | ||
|
|
0005d45d85 | ||
|
|
1052f6a07b | ||
|
|
5dcbae292a | ||
|
|
ed915aa2cb | ||
|
|
82a0ef612a | ||
|
|
8ea49866bc | ||
|
|
a0a7c25b18 | ||
|
|
f8c8f2963b | ||
|
|
5a980f3b54 | ||
|
|
c0be63afcd | ||
|
|
150b92163f | ||
|
|
3a25cdb53c | ||
|
|
45dcda0cc0 | ||
|
|
b4ffb2105f | ||
|
|
3398aa3ba9 | ||
|
|
94149161b3 | ||
|
|
1907f1ac0a | ||
|
|
bda002398c | ||
|
|
66c28bd389 | ||
|
|
8eacc4aad3 | ||
|
|
7a152d3591 | ||
|
|
4b05a28f4c | ||
|
|
0392b3b101 | ||
|
|
fad2cb2e18 | ||
|
|
df0b78cafc | ||
|
|
c69ea7b33e | ||
|
|
ce5c4c9d8d | ||
|
|
3f7aaa0228 | ||
|
|
e37909d478 | ||
|
|
1578c9d710 | ||
|
|
01604a27db | ||
|
|
58036dbeb9 | ||
|
|
7917a118c8 | ||
|
|
86c202b94a | ||
|
|
a5cc87eb55 | ||
|
|
2fabc210b2 | ||
|
|
4c644ae8cb | ||
|
|
351bae1fea | ||
|
|
bc7bc3cda6 | ||
|
|
aea5ca938b | ||
|
|
8706de5ef4 | ||
|
|
ed96740ab1 | ||
|
|
f719241938 | ||
|
|
ac22e47883 | ||
|
|
7555ba1707 | ||
|
|
341ea62412 | ||
|
|
2ab80758f0 | ||
|
|
a030409c46 | ||
|
|
036d8da9b6 | ||
|
|
7fb219bb31 | ||
|
|
b10c3ebdc0 | ||
|
|
778401dd2d | ||
|
|
418c78fa83 | ||
|
|
48d3d86017 | ||
|
|
c6a2073537 | ||
|
|
78ec0ce069 | ||
|
|
857d2940f8 | ||
|
|
82e689fd83 | ||
|
|
0cda4c04f7 | ||
|
|
91742ee27b | ||
|
|
3c6afdac76 | ||
|
|
f299889c4a | ||
|
|
c82fcbb468 | ||
|
|
eaf5fdcc55 | ||
|
|
40b7bd2c94 | ||
|
|
868621a3f1 | ||
|
|
eefc43d863 | ||
|
|
8b21a69a15 | ||
|
|
0964c6300a | ||
|
|
8c2db4f5ff | ||
|
|
e15a49a2bd | ||
|
|
ecfad1952a | ||
|
|
92b8cde7ae | ||
|
|
6fbe3a214d | ||
|
|
4695066e11 | ||
|
|
d125eac5a6 | ||
|
|
83d6565cad | ||
|
|
1e2a9a652e | ||
|
|
c781d1f559 | ||
|
|
ffc6246105 | ||
|
|
3271f00f87 | ||
|
|
77bfc87679 | ||
|
|
1fe0a89338 | ||
|
|
58971a3781 | ||
|
|
502e1f4d27 | ||
|
|
6ab0c3e758 | ||
|
|
2015db53ab | ||
|
|
73edafb95f | ||
|
|
e8f5edd9c3 | ||
|
|
2d69633660 | ||
|
|
b5c583ba7d | ||
|
|
9d230e4752 | ||
|
|
21be91a894 | ||
|
|
a527d367d9 | ||
|
|
eeda1bea4a | ||
|
|
2a0de4f0d3 | ||
|
|
792a22e606 | ||
|
|
6726d0167e | ||
|
|
dddafa4826 | ||
|
|
db27235f61 | ||
|
|
282ab9a6a1 | ||
|
|
15c87ddd26 | ||
|
|
a36dcfa1e5 | ||
|
|
ba5c2ab490 | ||
|
|
584bf57344 | ||
|
|
0c38343aca | ||
|
|
d8f41919ea | ||
|
|
81d2fac8b6 | ||
|
|
69d36cf858 | ||
|
|
2701cf9352 | ||
|
|
84b5426932 | ||
|
|
36a8471dd0 | ||
|
|
1d02f3e138 | ||
|
|
da0f084b0c | ||
|
|
1993fdd509 | ||
|
|
9fb5e95a55 | ||
|
|
706986e187 | ||
|
|
e3291fe22b | ||
|
|
ec137c9f84 | ||
|
|
1158bbb790 | ||
|
|
e880af6f1e | ||
|
|
547cc30818 | ||
|
|
d0f562b6ed | ||
|
|
ad3a190e96 | ||
|
|
9cc071edcd | ||
|
|
4177f84a62 | ||
|
|
c1db1c21d0 | ||
|
|
422f86da22 | ||
|
|
dac7ea90ae | ||
|
|
8ff773af3b | ||
|
|
b6557de0ca | ||
|
|
4a68bf2f2b | ||
|
|
9c370169d3 | ||
|
|
f62f0b440d | ||
|
|
4a8f907c7f | ||
|
|
1301346772 | ||
|
|
012c5a9c9c | ||
|
|
683b5164aa | ||
|
|
3eeea14b97 | ||
|
|
7a9ef47a68 | ||
|
|
495976726e | ||
|
|
50bcc4346f | ||
|
|
2f4a6e6b1f | ||
|
|
836b645507 | ||
|
|
ed3e3f12e0 | ||
|
|
7280cbbb68 | ||
|
|
848e4a62af | ||
|
|
98413bf125 | ||
|
|
21d61794bc | ||
|
|
72e046fd6d | ||
|
|
4f85de3247 | ||
|
|
6ad048b86a | ||
|
|
b9976cc2dd | ||
|
|
7a453da303 | ||
|
|
421e89823a | ||
|
|
436a722ce8 | ||
|
|
3ea38a59b7 | ||
|
|
665d675deb | ||
|
|
46a1854e24 | ||
|
|
a3c4e02aa1 | ||
|
|
199a75293d | ||
|
|
3f58c1cb7a | ||
|
|
2c8b51f463 | ||
|
|
7a752b1803 | ||
|
|
69d23c6f69 | ||
|
|
652ad1355a | ||
|
|
c0bd8c42a7 | ||
|
|
3657878d25 | ||
|
|
0e5d343f9f | ||
|
|
95f7046e49 | ||
|
|
233774a981 | ||
|
|
47b0ee7fe7 | ||
|
|
b9c6ab6fcd | ||
|
|
a042a65555 | ||
|
|
4ca0edc104 | ||
|
|
9c7e8097db | ||
|
|
3b2fd573bc | ||
|
|
83a6e41015 | ||
|
|
f12dcb3f1f | ||
|
|
c11cf378cb | ||
|
|
0764383989 | ||
|
|
b6e05c997d | ||
|
|
2576de48ec | ||
|
|
787b7b61c9 | ||
|
|
5cf2eef7ad | ||
|
|
1981567c59 | ||
|
|
3979628281 | ||
|
|
9a8506448f | ||
|
|
e7b63a61ba | ||
|
|
e106324c70 | ||
|
|
a42ccd0e18 | ||
|
|
5fc1390598 | ||
|
|
1942f1af4e | ||
|
|
c8e235bed6 | ||
|
|
cf93402763 | ||
|
|
3dcef90bbe | ||
|
|
6f8bae8d5c | ||
|
|
888a6f1c63 | ||
|
|
6d587fc1e2 | ||
|
|
630ec1e774 | ||
|
|
8a17d6f82b | ||
|
|
326149195f | ||
|
|
71ca7370e2 | ||
|
|
0dcf8ae794 | ||
|
|
9c5fe4f483 | ||
|
|
c9ff9d0716 | ||
|
|
27c9eff716 | ||
|
|
de603df07c | ||
|
|
6793a09b8b | ||
|
|
f60f84964a | ||
|
|
bba4472009 | ||
|
|
438480b2f1 | ||
|
|
3129c33adc | ||
|
|
ac0aaefe0f | ||
|
|
ef43dc4900 | ||
|
|
03f467abfa | ||
|
|
68d31dc515 | ||
|
|
9215cfd105 | ||
|
|
9c765c67e1 | ||
|
|
2fd2c76bdb | ||
|
|
798f9e8a7f | ||
|
|
1879d0d2fd | ||
|
|
e1d9f555c8 | ||
|
|
d3c2bbbb5b | ||
|
|
453d1e3875 | ||
|
|
d03a40d04f | ||
|
|
f2f0e06cd6 | ||
|
|
ae74105b74 | ||
|
|
314d7d79c2 | ||
|
|
c2b0c3f25b | ||
|
|
4a26e1476a | ||
|
|
735f17c2d8 | ||
|
|
47bccac7f9 | ||
|
|
f7f35151ac | ||
|
|
b49b1f0cc6 | ||
|
|
11ec6fb150 | ||
|
|
591360b879 | ||
|
|
8c7e8aee9f | ||
|
|
8554bf18f4 | ||
|
|
0779cc1a84 | ||
|
|
1b8a502d66 | ||
|
|
087d478094 | ||
|
|
82d0b19bc2 | ||
|
|
5cb7ec500c | ||
|
|
7a21f44eab | ||
|
|
9383508a85 | ||
|
|
b6a4be25f2 | ||
|
|
7234d2b3e9 | ||
|
|
14640be036 | ||
|
|
af60e9a0fa | ||
|
|
259c51b23c | ||
|
|
5b60f60b70 | ||
|
|
38ad2db7a7 | ||
|
|
0529c2747a | ||
|
|
fe5e44913d | ||
|
|
d7edbde434 | ||
|
|
b6c013dad7 | ||
|
|
452bc0a63a | ||
|
|
275d380fe0 | ||
|
|
354d230c2a | ||
|
|
37a0167193 | ||
|
|
6f4f5a771e | ||
|
|
297c4df74f | ||
|
|
b1b49aa0db | ||
|
|
b1f9faca0f | ||
|
|
05f659bc87 | ||
|
|
218036c0ea | ||
|
|
666f072b58 | ||
|
|
3086addd10 | ||
|
|
e72f2ce01c | ||
|
|
de638e7e4b | ||
|
|
500433e06f | ||
|
|
e29ae657f6 | ||
|
|
57d90e8a71 | ||
|
|
3332d1abb5 | ||
|
|
a6d6fb3c54 | ||
|
|
b3ceb8ce6d | ||
|
|
773690677e | ||
|
|
becbe7e7f9 | ||
|
|
a812f1304b | ||
|
|
5e3bf1b9c4 | ||
|
|
f379ac754d | ||
|
|
f34b7967d5 | ||
|
|
60ca268851 | ||
|
|
38bb65f9d5 | ||
|
|
1f81ac5854 | ||
|
|
95a046c2fe | ||
|
|
92fa2f2d76 | ||
|
|
8a0420c14f | ||
|
|
bcc5e2f77a | ||
|
|
389add2a8a | ||
|
|
30d7467369 | ||
|
|
6e3f05c58f | ||
|
|
7ed699593c | ||
|
|
00fc9cdad4 | ||
|
|
1e38f73829 | ||
|
|
bee0d672b3 | ||
|
|
089e0ca5c1 | ||
|
|
1dbdc816af | ||
|
|
b542ef9086 | ||
|
|
3720fbe4a0 | ||
|
|
91d15ec15b | ||
|
|
e8d39d0150 | ||
|
|
3786b4123b | ||
|
|
767cbbe366 | ||
|
|
49d56f6378 | ||
|
|
e6f898bef4 | ||
|
|
0d9ee1f4d4 | ||
|
|
a03b14a32d | ||
|
|
5a5861a0e8 | ||
|
|
a6e20d88da | ||
|
|
94767d8bc1 | ||
|
|
28a96de928 | ||
|
|
665e5ce092 | ||
|
|
b604893697 | ||
|
|
cdf3d7cab8 | ||
|
|
18b44e2aa4 | ||
|
|
69b05e94b4 | ||
|
|
93cdd46e64 | ||
|
|
5bd57232d8 | ||
|
|
2cfd8f0798 | ||
|
|
edf45c08d1 | ||
|
|
988daf03dd | ||
|
|
c686a6e74a | ||
|
|
dffe47e79c | ||
|
|
fd3adeed57 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,4 +4,6 @@
|
||||
|
||||
.history/
|
||||
.vscode/
|
||||
web/package-lock.json
|
||||
web/package-lock.json
|
||||
|
||||
*.bat
|
||||
459
README.zh.md
459
README.zh.md
@@ -1,214 +1,245 @@
|
||||
# Django-Vue3-Admin
|
||||
|
||||
[](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [](https://python.org/) [](https://docs.djangoproject.com/zh-hans/3.2/) [](https://nodejs.org/zh-cn/) [](https://gitee.com/liqianglog/django-vue-admin)
|
||||
|
||||
[预 览](https://demo.dvadmin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
|
||||
|
||||
|
||||
|
||||
💡 **「关于」**
|
||||
|
||||
我们是一群热爱代码的青年,在这个炙热的时代下,我们希望静下心来通过Code带来一点我们的色彩和颜色。
|
||||
|
||||
因为热爱,所以拥抱未来!
|
||||
|
||||
|
||||
## 平台简介
|
||||
|
||||
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) 是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。
|
||||
django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台,权限粒度达到列级别,前后端分离,后端采用django + django-rest-framework,前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus
|
||||
|
||||
|
||||
|
||||
|
||||
* 🧑🤝🧑前端采用 Vue3+TS+pinia+fastcrud(感谢[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/))
|
||||
* 👭后端采用 Python 语言 Django 框架以及强大的 [Django REST Framework](https://pypi.org/project/djangorestframework)。
|
||||
* 👫权限认证使用[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),支持多终端认证系统。
|
||||
* 👬支持加载动态权限菜单,多方式轻松权限控制。
|
||||
* 👬全新的列权限管控,粒度细化到每一列。
|
||||
* 💏特别鸣谢:[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/)。
|
||||
* 💡特别感谢[jetbrains](https://www.jetbrains.com/) 为本开源项目提供免费的 IntelliJ IDEA 授权。
|
||||
|
||||
#### 🏭 环境支持
|
||||
|
||||
| Edge | Firefox | Chrome | Safari |
|
||||
| --------- | ------------ | ----------- | ----------- |
|
||||
| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
|
||||
|
||||
> 由于 Vue3 不再支持 IE11,故而 ElementPlus 也不支持 IE11 及之前版本。
|
||||
|
||||
|
||||
|
||||
## 在线体验
|
||||
|
||||
👩👧👦演示地址:[https://demo.dvadmin.com](https://demo.dvadmin.com)
|
||||
|
||||
- 账号:superadmin
|
||||
|
||||
- 密码:admin123456
|
||||
|
||||
👩👦👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2)
|
||||
|
||||
|
||||
|
||||
## 交流
|
||||
|
||||
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩👦👦
|
||||
|
||||
- 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩👦👦
|
||||
|
||||
- django-vue-admin交流01群(已满):812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi)
|
||||
- django-vue-admin交流02群(已满):687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi)
|
||||
- django-vue-admin交流03群:442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
|
||||
|
||||
- 二维码
|
||||
|
||||
<img src='https://images.gitee.com/uploads/images/2022/0530/233203_5fb11883_5074988.jpeg' width='200'>
|
||||
|
||||
## 源码地址
|
||||
|
||||
gitee地址(主推):[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩👦👦
|
||||
|
||||
github地址:[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩👦👦
|
||||
|
||||
|
||||
## 内置功能
|
||||
|
||||
1. 👨⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
|
||||
2. 🧑⚕️部门管理:配置系统组织机构(公司、部门、角色)。
|
||||
3. 👩⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
|
||||
4. 🧑🎓按钮权限控制:授权角色的按钮权限和接口权限,可做到每一个接口都能授权数据范围。
|
||||
5. 🧑🎓字段列权限控制:授权页面的字段显示权限,具体到某一列的显示权限。
|
||||
7. 👨🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
8. 👬接口白名单:配置不需要进行权限校验的接口。
|
||||
9. 🧑🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
10. 🧑🔧地区管理:对省市县区域进行管理。
|
||||
11. 📁附件管理:对平台上所有文件、图片等进行统一管理。
|
||||
12. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html):基于Django-Vue-Admin框架开发的应用和插件。
|
||||
|
||||
## 插件市场 🔌
|
||||
更新中...
|
||||
|
||||
## 仓库分支说明 💈
|
||||
主分支:master(稳定版本)
|
||||
开发分支:develop
|
||||
|
||||
|
||||
## 准备工作
|
||||
~~~
|
||||
Python >= 3.11.0 (最低3.9+版本)
|
||||
nodejs >= 16.0
|
||||
Mysql >= 8.0 (可选,默认数据库sqlite3,支持5.7+,推荐8.0版本)
|
||||
Redis (可选,最新版)
|
||||
~~~
|
||||
|
||||
## 前端♝
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://gitee.com/huge-dream/django-vue3-admin.git
|
||||
|
||||
# 进入项目目录
|
||||
cd web
|
||||
|
||||
# 安装依赖
|
||||
npm install yarn
|
||||
yarn install --registry=https://registry.npm.taobao.org
|
||||
|
||||
# 启动服务
|
||||
yarn build
|
||||
# 浏览器访问 http://localhost:8080
|
||||
# .env.development 文件中可配置启动端口等参数
|
||||
# 构建生产环境
|
||||
# yarn run build
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 后端💈
|
||||
|
||||
~~~bash
|
||||
1. 进入项目目录 cd backend
|
||||
2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py
|
||||
3. 在 env.py 中配置数据库信息
|
||||
mysql数据库版本建议:8.0
|
||||
mysql数据库字符集:utf8mb4
|
||||
4. 安装依赖环境
|
||||
pip3 install -r requirements.txt
|
||||
5. 执行迁移命令:
|
||||
python3 manage.py makemigrations
|
||||
python3 manage.py migrate
|
||||
6. 初始化数据
|
||||
python3 manage.py init
|
||||
7. 初始化省市县数据:
|
||||
python3 manage.py init_area
|
||||
8. 启动项目
|
||||
python3 manage.py runserver 0.0.0.0:8000
|
||||
或使用 uvicorn :
|
||||
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8
|
||||
~~~
|
||||
## 开发建议
|
||||
前后端backend与web各自单独一个窗口打开进行开发
|
||||
|
||||
### 访问项目
|
||||
|
||||
- 访问地址:[http://localhost:8080](http://localhost:8080) (默认为此地址,如有修改请按照配置文件)
|
||||
- 账号:`superadmin` 密码:`admin123456`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### docker-compose 运行
|
||||
|
||||
~~~shell
|
||||
# 先安装docker-compose (自行百度安装),执行此命令等待安装,如有使用celery插件请打开docker-compose.yml中celery 部分注释
|
||||
docker-compose up -d
|
||||
# 初始化后端数据(第一次执行即可)
|
||||
docker exec -ti dvadmin3-django bash
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
python manage.py init_area
|
||||
python manage.py init
|
||||
exit
|
||||
|
||||
前端地址:http://127.0.0.1:8080
|
||||
后端地址:http://127.0.0.1:8080/api
|
||||
# 在服务器上请把127.0.0.1 换成自己公网ip
|
||||
账号:superadmin 密码:admin123456
|
||||
|
||||
# docker-compose 停止
|
||||
docker-compose down
|
||||
# docker-compose 重启
|
||||
docker-compose restart
|
||||
# docker-compose 启动时重新进行 build
|
||||
docker-compose up -d --build
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
## 演示图✅
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
# Django-Vue3-Admin
|
||||
|
||||
[](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [](https://python.org/) [](https://docs.djangoproject.com/zh-hans/3.2/) [](https://nodejs.org/zh-cn/) [](https://gitee.com/liqianglog/django-vue-admin)
|
||||
|
||||
[预 览](https://demo.dvadmin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
|
||||
|
||||
|
||||
|
||||
💡 **「关于」**
|
||||
|
||||
我们是一群热爱代码的青年,在这个炙热的时代下,我们希望静下心来通过Code带来一点我们的色彩和颜色。
|
||||
|
||||
因为热爱,所以拥抱未来!
|
||||
|
||||
|
||||
## 平台简介
|
||||
|
||||
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) 是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。
|
||||
django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台,权限粒度达到列级别,前后端分离,后端采用django + django-rest-framework,前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus
|
||||
|
||||
|
||||
|
||||
|
||||
* 🧑🤝🧑前端采用 Vue3+TS+pinia+fastcrud(感谢[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/))
|
||||
* 👭后端采用 Python 语言 Django 框架以及强大的 [Django REST Framework](https://pypi.org/project/djangorestframework)。
|
||||
* 👫权限认证使用[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),支持多终端认证系统。
|
||||
* 👬支持加载动态权限菜单,多方式轻松权限控制。
|
||||
* 👬全新的列权限管控,粒度细化到每一列。
|
||||
* 💏特别鸣谢:[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/)。
|
||||
* 💡特别感谢[jetbrains](https://www.jetbrains.com/) 为本开源项目提供免费的 IntelliJ IDEA 授权。
|
||||
|
||||
#### 🏭 环境支持
|
||||
|
||||
| Edge | Firefox | Chrome | Safari |
|
||||
| --------- | ------------ | ----------- | ----------- |
|
||||
| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
|
||||
|
||||
> 由于 Vue3 不再支持 IE11,故而 ElementPlus 也不支持 IE11 及之前版本。
|
||||
|
||||
|
||||
|
||||
## 在线体验
|
||||
|
||||
👩👧👦演示地址:[https://demo.dvadmin.com](https://demo.dvadmin.com)
|
||||
|
||||
- 账号:superadmin
|
||||
|
||||
- 密码:admin123456
|
||||
|
||||
👩👦👦文档地址:[DVAdmin官网](https://www.django-vue-admin.com)
|
||||
|
||||
|
||||
|
||||
## 交流
|
||||
|
||||
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩👦👦
|
||||
- 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩👦👦
|
||||
- django-vue-admin交流01群(已满):812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi)
|
||||
- django-vue-admin交流02群(已满):687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi)
|
||||
- django-vue-admin交流03群(已满):442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
|
||||
- django-vue-admin交流04群:442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
|
||||
|
||||
|
||||
|
||||
## 给框架点赞
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<img src='https://django-vue-admin.com/alipay.jpg' width='200'>
|
||||
<img src='https://django-vue-admin.com/wechat.jpg' width='200'>
|
||||
</div>
|
||||
|
||||
|
||||
## 源码地址
|
||||
|
||||
gitee地址(主推):[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩👦👦
|
||||
|
||||
github地址:[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩👦👦
|
||||
|
||||
|
||||
## 内置功能
|
||||
|
||||
1. 👨⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
|
||||
2. 🧑⚕️部门管理:配置系统组织机构(公司、部门、角色)。
|
||||
3. 👩⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
|
||||
4. 🧑🎓按钮权限控制:授权角色的按钮权限和接口权限,可做到每一个接口都能授权数据范围。
|
||||
5. 🧑🎓字段列权限控制:授权页面的字段显示权限,具体到某一列的显示权限。
|
||||
7. 👨🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
8. 👬接口白名单:配置不需要进行权限校验的接口。
|
||||
9. 🧑🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
10. 🧑🔧地区管理:对省市县区域进行管理。
|
||||
11. 📁附件管理:对平台上所有文件、图片等进行统一管理。
|
||||
12. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html):基于Django-Vue-Admin框架开发的应用和插件。
|
||||
|
||||
## 插件市场 🔌
|
||||
1. #### [dvadmin3-folw 后台审批流插件](https://bbs.django-vue-admin.com/plugMarket/139.html)
|
||||
|
||||
2. #### [dvadmin3 celery插件前端](https://bbs.django-vue-admin.com/plugMarket/134.html)
|
||||
|
||||
3. #### [dvadmin3 celery插件后端](https://bbs.django-vue-admin.com/plugMarket/133.html)
|
||||
|
||||
4. #### [dvadmin3-build插件](https://bbs.django-vue-admin.com/plugMarket/136.html)
|
||||
|
||||
5. #### [dvadmin3-uniapp](https://e.coding.net/dvadmin-private/code/dvadmin3-uniapp-app.git)
|
||||
|
||||
6. #### dvadmin3-folw-uniapp 审批(开发中,近期上线)
|
||||
|
||||
|
||||
|
||||
## 仓库分支说明 💈
|
||||
主分支:master(稳定版本)
|
||||
开发分支:develop
|
||||
|
||||
|
||||
## 准备工作
|
||||
~~~
|
||||
Python >= 3.11.0 (最低3.9+版本)
|
||||
nodejs >= 16.0
|
||||
Mysql >= 8.0 (可选,默认数据库sqlite3,支持5.7+,推荐8.0版本)
|
||||
Redis (可选,最新版)
|
||||
~~~
|
||||
|
||||
## 前端♝
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://gitee.com/huge-dream/django-vue3-admin.git
|
||||
|
||||
# 进入项目目录
|
||||
cd web
|
||||
|
||||
# 安装依赖
|
||||
npm install yarn
|
||||
yarn install --registry=https://registry.npmmirror.com
|
||||
|
||||
# 启动服务
|
||||
yarn build
|
||||
# 浏览器访问 http://localhost:8080
|
||||
# .env.development 文件中可配置启动端口等参数
|
||||
# 构建生产环境
|
||||
# yarn run build
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 后端💈
|
||||
|
||||
~~~bash
|
||||
1. 进入项目目录 cd backend
|
||||
2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py
|
||||
3. 在 env.py 中配置数据库信息
|
||||
mysql数据库版本建议:8.0
|
||||
mysql数据库字符集:utf8mb4
|
||||
4. 安装依赖环境
|
||||
pip3 install -r requirements.txt
|
||||
5. 执行迁移命令:
|
||||
python3 manage.py makemigrations
|
||||
python3 manage.py migrate
|
||||
6. 初始化数据
|
||||
python3 manage.py init
|
||||
7. 初始化省市县数据:
|
||||
python3 manage.py init_area
|
||||
8. 启动项目
|
||||
python3 manage.py runserver 0.0.0.0:8000
|
||||
或使用 uvicorn :
|
||||
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8
|
||||
~~~
|
||||
## 开发建议
|
||||
前后端backend与web各自单独一个窗口打开进行开发
|
||||
|
||||
### 访问项目
|
||||
|
||||
- 访问地址:[http://localhost:8080](http://localhost:8080) (默认为此地址,如有修改请按照配置文件)
|
||||
- 账号:`superadmin` 密码:`admin123456`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### docker-compose 运行
|
||||
|
||||
~~~shell
|
||||
# 先安装docker-compose (自行百度安装),执行此命令等待安装,如有使用celery插件请打开docker-compose.yml中celery 部分注释
|
||||
docker-compose up -d
|
||||
# 初始化后端数据(第一次执行即可)
|
||||
docker exec -ti dvadmin3-django bash
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
python manage.py init_area
|
||||
python manage.py init
|
||||
exit
|
||||
|
||||
前端地址:http://127.0.0.1:8080
|
||||
后端地址:http://127.0.0.1:8080/api
|
||||
# 在服务器上请把127.0.0.1 换成自己公网ip
|
||||
账号:superadmin 密码:admin123456
|
||||
|
||||
# docker-compose 停止
|
||||
docker-compose down
|
||||
# docker-compose 重启
|
||||
docker-compose restart
|
||||
# docker-compose 启动时重新进行 build
|
||||
docker-compose up -d --build
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
## 演示图✅
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 审批流插件
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
||||
@@ -8,25 +8,25 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
||||
|
||||
http_application = get_asgi_application()
|
||||
|
||||
from application.routing import websocket_urlpatterns
|
||||
from application.ws_routing import websocket_urlpatterns
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": http_application,
|
||||
'websocket': AllowedHostsOriginValidator(
|
||||
AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
websocket_urlpatterns # 指明路由文件是devops/routing.py
|
||||
AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
websocket_urlpatterns # 指明路由文件是devops/routing.py
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import functools
|
||||
import os
|
||||
|
||||
from celery.signals import task_postrun
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||
|
||||
from django.conf import settings
|
||||
@@ -15,7 +17,7 @@ else:
|
||||
from celery import Celery
|
||||
|
||||
app = Celery(f"application")
|
||||
app.config_from_object('django.conf:settings')
|
||||
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
||||
platforms.C_FORCE_ROOT = True
|
||||
|
||||
@@ -38,3 +40,12 @@ def retry_base_task_error():
|
||||
return wrapper
|
||||
|
||||
return wraps
|
||||
|
||||
|
||||
@task_postrun.connect
|
||||
def add_periodic_task_name(sender, task_id, task, args, kwargs, **extras):
|
||||
periodic_task_name = kwargs.get('periodic_task_name')
|
||||
if periodic_task_name:
|
||||
from django_celery_results.models import TaskResult
|
||||
# 更新 TaskResult 表中的 periodic_task_name 字段
|
||||
TaskResult.objects.filter(task_id=task_id).update(periodic_task_name=periodic_task_name)
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.core.cache import cache
|
||||
from dvadmin.utils.validator import CustomValidationError
|
||||
|
||||
dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis
|
||||
|
||||
|
||||
def is_tenants_mode():
|
||||
@@ -68,6 +72,9 @@ def init_dictionary():
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
if dispatch_db_type == 'redis':
|
||||
cache.set(f"init_dictionary", _get_all_dictionary())
|
||||
return
|
||||
if is_tenants_mode():
|
||||
from django_tenants.utils import tenant_context, get_tenant_model
|
||||
|
||||
@@ -88,7 +95,9 @@ def init_system_config():
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
|
||||
if dispatch_db_type == 'redis':
|
||||
cache.set(f"init_system_config", _get_all_system_config())
|
||||
return
|
||||
if is_tenants_mode():
|
||||
from django_tenants.utils import tenant_context, get_tenant_model
|
||||
|
||||
@@ -107,6 +116,9 @@ def refresh_dictionary():
|
||||
刷新字典配置
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
cache.set(f"init_dictionary", _get_all_dictionary())
|
||||
return
|
||||
if is_tenants_mode():
|
||||
from django_tenants.utils import tenant_context, get_tenant_model
|
||||
|
||||
@@ -122,6 +134,9 @@ def refresh_system_config():
|
||||
刷新系统配置
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
cache.set(f"init_system_config", _get_all_system_config())
|
||||
return
|
||||
if is_tenants_mode():
|
||||
from django_tenants.utils import tenant_context, get_tenant_model
|
||||
|
||||
@@ -141,6 +156,11 @@ def get_dictionary_config(schema_name=None):
|
||||
:param schema_name: 对应字典配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
init_dictionary_data = cache.get(f"init_dictionary")
|
||||
if not init_dictionary_data:
|
||||
refresh_dictionary()
|
||||
return cache.get(f"init_dictionary") or {}
|
||||
if not settings.DICTIONARY_CONFIG:
|
||||
refresh_dictionary()
|
||||
if is_tenants_mode():
|
||||
@@ -157,6 +177,12 @@ def get_dictionary_values(key, schema_name=None):
|
||||
:param schema_name: 对应字典配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
dictionary_config = cache.get(f"init_dictionary")
|
||||
if not dictionary_config:
|
||||
refresh_dictionary()
|
||||
dictionary_config = cache.get(f"init_dictionary")
|
||||
return dictionary_config.get(key)
|
||||
dictionary_config = get_dictionary_config(schema_name)
|
||||
return dictionary_config.get(key)
|
||||
|
||||
@@ -169,8 +195,8 @@ def get_dictionary_label(key, name, schema_name=None):
|
||||
:param schema_name: 对应字典配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
children = get_dictionary_values(key, schema_name) or []
|
||||
for ele in children:
|
||||
res = get_dictionary_values(key, schema_name) or []
|
||||
for ele in res.get('children'):
|
||||
if ele.get("value") == str(name):
|
||||
return ele.get("label")
|
||||
return ""
|
||||
@@ -187,6 +213,11 @@ def get_system_config(schema_name=None):
|
||||
:param schema_name: 对应字典配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
init_dictionary_data = cache.get(f"init_system_config")
|
||||
if not init_dictionary_data:
|
||||
refresh_system_config()
|
||||
return cache.get(f"init_system_config") or {}
|
||||
if not settings.SYSTEM_CONFIG:
|
||||
refresh_system_config()
|
||||
if is_tenants_mode():
|
||||
@@ -203,10 +234,32 @@ def get_system_config_values(key, schema_name=None):
|
||||
:param schema_name: 对应系统配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
system_config = cache.get(f"init_system_config")
|
||||
if not system_config:
|
||||
refresh_system_config()
|
||||
system_config = cache.get(f"init_system_config")
|
||||
return system_config.get(key)
|
||||
system_config = get_system_config(schema_name)
|
||||
return system_config.get(key)
|
||||
|
||||
|
||||
def get_system_config_values_to_dict(key, schema_name=None):
|
||||
"""
|
||||
获取系统配置数据并转换为字典 **仅限于数组类型系统配置
|
||||
:param key: 对应系统配置的key值(字典编号)
|
||||
:param schema_name: 对应系统配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
values_dict = {}
|
||||
config_values = get_system_config_values(key, schema_name)
|
||||
if not isinstance(config_values, list):
|
||||
raise CustomValidationError("该方式仅限于数组类型系统配置")
|
||||
for ele in get_system_config_values(key, schema_name):
|
||||
values_dict[ele.get('key')] = ele.get('value')
|
||||
return values_dict
|
||||
|
||||
|
||||
def get_system_config_label(key, name, schema_name=None):
|
||||
"""
|
||||
获取获取系统配置label值
|
||||
|
||||
@@ -399,12 +399,16 @@ DICTIONARY_CONFIG = {}
|
||||
# ================================================= #
|
||||
# 租户共享app
|
||||
TENANT_SHARED_APPS = []
|
||||
# 普通租户独有app
|
||||
TENANT_EXCLUSIVE_APPS = []
|
||||
# 插件 urlpatterns
|
||||
PLUGINS_URL_PATTERNS = []
|
||||
# 所有模式有的
|
||||
SHARED_APPS = []
|
||||
# ********** 一键导入插件配置开始 **********
|
||||
# 例如:
|
||||
# from dvadmin_upgrade_center.settings import * # 升级中心
|
||||
# from dvadmin_celery.settings import * # celery 异步任务
|
||||
from dvadmin3_celery.settings import * # celery 异步任务
|
||||
# from dvadmin_third.settings import * # 第三方用户管理
|
||||
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
|
||||
# from dvadmin_tenants.settings import * # 租户管理
|
||||
|
||||
33
backend/application/sse_views.py
Normal file
33
backend/application/sse_views.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# views.py
|
||||
import time
|
||||
|
||||
import jwt
|
||||
from django.http import StreamingHttpResponse
|
||||
|
||||
from application import settings
|
||||
from dvadmin.system.models import MessageCenterTargetUser
|
||||
from django.core.cache import cache
|
||||
|
||||
|
||||
def event_stream(user_id):
|
||||
last_sent_time = 0
|
||||
|
||||
while True:
|
||||
# 从 Redis 中获取最后数据库变更时间
|
||||
last_db_change_time = cache.get('last_db_change_time', 0)
|
||||
# 只有当数据库发生变化时才检查总数
|
||||
if last_db_change_time and last_db_change_time > last_sent_time:
|
||||
count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count()
|
||||
yield f"data: {count}\n\n"
|
||||
last_sent_time = time.time()
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def sse_view(request):
|
||||
token = request.GET.get('token')
|
||||
decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
|
||||
user_id = decoded.get('user_id')
|
||||
response = StreamingHttpResponse(event_stream(user_id), content_type='text/event-stream')
|
||||
response['Cache-Control'] = 'no-cache'
|
||||
return response
|
||||
@@ -15,6 +15,7 @@ Including another URLconf
|
||||
"""
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import path, include, re_path
|
||||
from django.views.static import serve
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
from rest_framework import permissions
|
||||
@@ -24,6 +25,7 @@ from rest_framework_simplejwt.views import (
|
||||
|
||||
from application import dispatch
|
||||
from application import settings
|
||||
from application.sse_views import sse_view
|
||||
from dvadmin.system.views.dictionary import InitDictionaryViewSet
|
||||
from dvadmin.system.views.login import (
|
||||
LoginView,
|
||||
@@ -40,6 +42,7 @@ dispatch.init_system_config()
|
||||
dispatch.init_dictionary()
|
||||
# =========== 初始化系统配置 =================
|
||||
|
||||
permission_classes = [permissions.AllowAny, ] if settings.DEBUG else [permissions.IsAuthenticated, ]
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="Snippets API",
|
||||
@@ -50,9 +53,36 @@ schema_view = get_schema_view(
|
||||
license=openapi.License(name="BSD License"),
|
||||
),
|
||||
public=True,
|
||||
permission_classes=(permissions.AllowAny,),
|
||||
permission_classes=permission_classes,
|
||||
generator_class=CustomOpenAPISchemaGenerator,
|
||||
)
|
||||
# 前端页面映射
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import render
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
|
||||
def web_view(request):
|
||||
return render(request, 'web/index.html')
|
||||
|
||||
|
||||
def serve_web_files(request, filename):
|
||||
# 设定文件路径
|
||||
filepath = os.path.join(settings.BASE_DIR, 'templates', 'web', filename)
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(filepath):
|
||||
raise Http404("File does not exist")
|
||||
|
||||
# 根据文件扩展名,确定 MIME 类型
|
||||
mime_type, _ = mimetypes.guess_type(filepath)
|
||||
|
||||
# 打开文件并读取内容
|
||||
with open(filepath, 'rb') as f:
|
||||
response = HttpResponse(f.read(), content_type=mime_type)
|
||||
return response
|
||||
|
||||
|
||||
urlpatterns = (
|
||||
[
|
||||
@@ -85,6 +115,11 @@ urlpatterns = (
|
||||
|
||||
# 仅用于开发,上线需关闭
|
||||
path("api/token/", LoginTokenView.as_view()),
|
||||
# 前端页面映射
|
||||
path('web/', web_view, name='web_view'),
|
||||
path('web/<path:filename>', serve_web_files, name='serve_web_files'),
|
||||
# sse
|
||||
path('sse/', sse_view, name='sse'),
|
||||
]
|
||||
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
+ static(settings.STATIC_URL, document_root=settings.STATIC_URL)
|
||||
|
||||
@@ -180,4 +180,4 @@ def create_message_push(title: str, content: str, target_type: int = 0, target_u
|
||||
"type": "push.message",
|
||||
"json": {**message, 'unread': unread_count}
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -4,4 +4,4 @@ from application.websocketConfig import MegCenter
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path('ws/<str:service_uid>/', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者
|
||||
]
|
||||
]
|
||||
@@ -21,13 +21,15 @@ DATABASE_PORT = 3306
|
||||
# # 数据库用户名
|
||||
DATABASE_USER = "root"
|
||||
# # 数据库密码
|
||||
DATABASE_PASSWORD = "DVADMIN3"
|
||||
DATABASE_PASSWORD = 'DVADMIN3'
|
||||
|
||||
# 表前缀
|
||||
TABLE_PREFIX = "dvadmin_"
|
||||
# ================================================= #
|
||||
# ******** redis配置,无redis 可不进行配置 ******** #
|
||||
# ================================================= #
|
||||
REDIS_DB = 1
|
||||
CELERY_BROKER_DB = 3
|
||||
REDIS_PASSWORD = 'DVADMIN3'
|
||||
REDIS_HOST = '127.0.0.1'
|
||||
REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6379'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
|
||||
exclude = ["venv"] # 需要排除的文件目录
|
||||
exclude = ["venv", ".venv"] # 需要排除的文件目录
|
||||
for root, dirs, files in os.walk('.'):
|
||||
dirs[:] = list(set(dirs) - set(exclude))
|
||||
if 'migrations' in dirs:
|
||||
|
||||
1
backend/dvadmin/__init__.py
Normal file
1
backend/dvadmin/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -4,3 +4,7 @@ from django.apps import AppConfig
|
||||
class SystemConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'dvadmin.system'
|
||||
|
||||
def ready(self):
|
||||
# 注册信号
|
||||
import dvadmin.system.signals # 确保路径正确
|
||||
|
||||
@@ -19,6 +19,20 @@ class UsersInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化获取数信息(用于生成初始化json文件)
|
||||
"""
|
||||
role_key = serializers.SerializerMethodField()
|
||||
dept_key = serializers.SerializerMethodField()
|
||||
|
||||
def get_dept_key(self, obj):
|
||||
if obj.dept:
|
||||
return obj.dept.key
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_role_key(self, obj):
|
||||
if obj.role.all():
|
||||
return [role.key for role in obj.role.all()]
|
||||
else:
|
||||
return []
|
||||
|
||||
def save(self, **kwargs):
|
||||
instance = super().save(**kwargs)
|
||||
@@ -35,7 +49,7 @@ class UsersInitSerializer(CustomModelSerializer):
|
||||
model = Users
|
||||
fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type',
|
||||
'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id',
|
||||
'password', 'last_login', 'is_superuser']
|
||||
'password', 'last_login', 'is_superuser', 'role_key' ,'dept_key']
|
||||
read_only_fields = ['id']
|
||||
extra_kwargs = {
|
||||
'creator': {'write_only': True},
|
||||
@@ -175,22 +189,30 @@ class RoleMenuInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化角色菜单(用于生成初始化json文件)
|
||||
"""
|
||||
role_key = serializers.CharField(max_length=100, required=True)
|
||||
menu_component_name = serializers.CharField(max_length=100, required=True)
|
||||
role__key = serializers.CharField(source='role.key')
|
||||
menu__web_path = serializers.CharField(source='menu.web_path')
|
||||
menu__component_name = serializers.CharField(source='menu.component_name', allow_blank=True)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
init_data = self.initial_data
|
||||
role_id = Role.objects.filter(key=init_data['role__key']).first()
|
||||
menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first()
|
||||
validated_data['role'] = role_id
|
||||
validated_data['menu'] = menu_id
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
def create(self, validated_data):
|
||||
init_data = self.initial_data
|
||||
validated_data.pop('menu_component_name')
|
||||
validated_data.pop('role_key')
|
||||
role_id = Role.objects.filter(key=init_data['role_key']).first()
|
||||
menu_id = Menu.objects.filter(component_name=init_data['menu_component_name']).first()
|
||||
role_id = Role.objects.filter(key=init_data['role__key']).first()
|
||||
menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first()
|
||||
validated_data['role'] = role_id
|
||||
validated_data['menu'] = menu_id
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuPermission
|
||||
fields = ['role_key', 'menu_component_name', 'creator', 'dept_belong_id']
|
||||
fields = ['role__key', 'menu__web_path', 'menu__component_name','creator', 'dept_belong_id']
|
||||
read_only_fields = ["id"]
|
||||
extra_kwargs = {
|
||||
'role': {'required': False},
|
||||
@@ -204,25 +226,38 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化角色菜单按钮(用于生成初始化json文件)
|
||||
"""
|
||||
role_key = serializers.CharField(max_length=100, required=True)
|
||||
menu_button_value = serializers.CharField(max_length=100, required=True)
|
||||
role__key = serializers.CharField(source='role.key')
|
||||
menu_button__value = serializers.CharField(source='menu_button.value')
|
||||
data_range = serializers.CharField(max_length=100, required=False)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
init_data = self.initial_data
|
||||
role_id = Role.objects.filter(key=init_data['role__key']).first()
|
||||
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
|
||||
validated_data['role'] = role_id
|
||||
validated_data['menu_button'] = menu_button_id
|
||||
instance = super().create(validated_data)
|
||||
instance.dept.set([])
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def create(self, validated_data):
|
||||
init_data = self.initial_data
|
||||
validated_data.pop('menu_button_value')
|
||||
validated_data.pop('role_key')
|
||||
role_id = Role.objects.filter(key=init_data['role_key']).first()
|
||||
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button_value']).first()
|
||||
role_id = Role.objects.filter(key=init_data['role__key']).first()
|
||||
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
|
||||
validated_data['role'] = role_id
|
||||
validated_data['menu_button'] = menu_button_id
|
||||
instance = super().create(validated_data)
|
||||
instance.dept.set([])
|
||||
return instance
|
||||
|
||||
def save(self, **kwargs):
|
||||
if not self.instance or self.initial_data.get('reset'):
|
||||
return super().save(**kwargs)
|
||||
return self.instance
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuButtonPermission
|
||||
fields = ['role_key', 'menu_button_value', 'data_range', 'dept', 'creator', 'dept_belong_id']
|
||||
fields = ['role__key', 'menu_button__value', 'data_range', 'dept', 'creator', 'dept_belong_id']
|
||||
read_only_fields = ["id"]
|
||||
extra_kwargs = {
|
||||
'role': {'required': False},
|
||||
|
||||
@@ -546,5 +546,50 @@
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "文件存储引擎",
|
||||
"value": "file_engine",
|
||||
"type": 0,
|
||||
"color": null,
|
||||
"is_value": false,
|
||||
"status": true,
|
||||
"sort": 9,
|
||||
"remark": null,
|
||||
"children": [
|
||||
{
|
||||
"label": "本地",
|
||||
"value": "local",
|
||||
"type": 0,
|
||||
"color": "primary",
|
||||
"is_value": true,
|
||||
"status": true,
|
||||
"sort": 1,
|
||||
"remark": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"label": "阿里云oss",
|
||||
"value": "oss",
|
||||
"type": 0,
|
||||
"color": "success",
|
||||
"is_value": true,
|
||||
"status": true,
|
||||
"sort": 2,
|
||||
"remark": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"label": "腾讯cos",
|
||||
"value": "cos",
|
||||
"type": 0,
|
||||
"color": "warning",
|
||||
"is_value": true,
|
||||
"status": true,
|
||||
"sort": 3,
|
||||
"remark": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -11,332 +11,11 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": null,
|
||||
"children": [
|
||||
{
|
||||
"name": "菜单管理",
|
||||
"icon": "iconfont icon-caidan",
|
||||
"sort": 1,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/menu",
|
||||
"component": "system/menu/index",
|
||||
"component_name": "menu",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 1,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "menu:Search",
|
||||
"api": "/api/system/menu/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "menu:Retrieve",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "查询所有",
|
||||
"value": "menu:SearchAll",
|
||||
"api": "/api/system/menu/get_all_menu/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "路由",
|
||||
"value": "menu:router",
|
||||
"api": "/api/system/menu/web_router/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "查询按钮权限",
|
||||
"value": "btn:Search",
|
||||
"api": "/api/system/menu_button/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "查询列权限",
|
||||
"value": "column:Search",
|
||||
"api": "/api/system/column/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "menu:Create",
|
||||
"api": "/api/system/menu/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "上移",
|
||||
"value": "menu:MoveUp",
|
||||
"api": "/api/system/menu/mode_up/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "下移",
|
||||
"value": "menu:MoveDown",
|
||||
"api": "/api/system/menu/mode_down/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "新增按钮权限",
|
||||
"value": "btn:Create",
|
||||
"api": "/api/system/menu_button/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "新增列权限",
|
||||
"value": "column:Create",
|
||||
"api": "/api/system/column/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "自动匹配列权限",
|
||||
"value": "column:Match",
|
||||
"api": "/api/system/column/auto_match_fields/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "menu:Update",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "修改按钮权限",
|
||||
"value": "btn:Update",
|
||||
"api": "/api/system/menu_button/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "编辑列权限",
|
||||
"value": "column:Update",
|
||||
"api": "/api/system/column/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "menu:Delete",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 3
|
||||
},
|
||||
{
|
||||
"name": "删除按钮权限",
|
||||
"value": "btn:Delete",
|
||||
"api": "/api/system/menu_button/{id}/",
|
||||
"method": 3
|
||||
},
|
||||
{
|
||||
"name": "删除列权限",
|
||||
"value": "column:Delete",
|
||||
"api": "/api/system/column/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
],
|
||||
"menu_field": []
|
||||
},
|
||||
{
|
||||
"name": "部门管理",
|
||||
"icon": "ele-OfficeBuilding",
|
||||
"sort": 3,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/dept",
|
||||
"component": "system/dept/index",
|
||||
"component_name": "dept",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 1,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "dept:Search",
|
||||
"api": "/api/system/dept/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "dept:Retrieve",
|
||||
"api": "/api/system/dept/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "查询所有",
|
||||
"value": "dept:SearchAll",
|
||||
"api": "/api/system/dept/all_dept/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "懒加载查询所有",
|
||||
"value": "dept:LazySearchAll",
|
||||
"api": "/api/system/dept/dept_lazy_tree/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "头信息",
|
||||
"value": "dept:HeaderInfo",
|
||||
"api": "/api/system/dept/dept_info/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "dept:Create",
|
||||
"api": "/api/system/dept/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "上移",
|
||||
"value": "dept:MoveUp",
|
||||
"api": "/api/system/dept/mode_up/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "下移",
|
||||
"value": "dept:MoveDown",
|
||||
"api": "/api/system/dept/mode_down/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "dept:Update",
|
||||
"api": "/api/system/dept/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "dept:Delete",
|
||||
"api": "/api/system/dept/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
],
|
||||
"menu_field": []
|
||||
},
|
||||
{
|
||||
"name": "角色管理",
|
||||
"icon": "ele-ColdDrink",
|
||||
"sort": 4,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/role",
|
||||
"component": "system/role/index",
|
||||
"component_name": "role",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 1,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "role:Search",
|
||||
"api": "/api/system/role/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "role:Retrieve",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "权限配置",
|
||||
"value": "role:Permission",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "role:Create",
|
||||
"api": "/api/system/role/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "role:Update",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "保存",
|
||||
"value": "role:Save",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "role:Delete",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
],
|
||||
"menu_field": [
|
||||
{
|
||||
"field_name": "create_datetime",
|
||||
"title": "创建时间",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "creator",
|
||||
"title": "创建人",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "dept_belong_id",
|
||||
"title": "数据归属部门",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "description",
|
||||
"title": "描述",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "id",
|
||||
"title": "Id",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "key",
|
||||
"title": "权限字符",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "modifier",
|
||||
"title": "修改人",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "name",
|
||||
"title": "角色名称",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "sort",
|
||||
"title": "角色顺序",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "status",
|
||||
"title": "角色状态",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "update_datetime",
|
||||
"title": "修改时间",
|
||||
"model": "Role"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "用户管理",
|
||||
"icon": "iconfont icon-icon-",
|
||||
"sort": 6,
|
||||
"sort": 1,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/user",
|
||||
@@ -345,7 +24,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 1,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -354,18 +32,24 @@
|
||||
"api": "/api/system/user/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "user:Retrieve",
|
||||
"api": "/api/system/user/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "user:Create",
|
||||
"api": "/api/system/user/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "user:Update",
|
||||
"api": "/api/system/user/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "user:Delete",
|
||||
"api": "/api/system/user/{id}/",
|
||||
"method": 3
|
||||
},
|
||||
{
|
||||
"name": "导出",
|
||||
"value": "user:Export",
|
||||
@@ -379,10 +63,16 @@
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "user:Update",
|
||||
"api": "/api/system/user/{id}/",
|
||||
"method": 2
|
||||
"name": "获取导入模板",
|
||||
"value": "user:ImportTemplate",
|
||||
"api": "/api/system/user/import/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "批量更新模板",
|
||||
"value": "user:BatchUpdateTemplate",
|
||||
"api": "/api/system/user/update_template/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "重设密码",
|
||||
@@ -392,15 +82,9 @@
|
||||
},
|
||||
{
|
||||
"name": "重置密码",
|
||||
"value": "user:DefaultPassword",
|
||||
"value": "user:ResetDefaultPassword",
|
||||
"api": "/api/system/user/{id}/reset_to_default_password/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "user:Delete",
|
||||
"api": "/api/system/user/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
],
|
||||
"menu_field": [
|
||||
@@ -481,6 +165,359 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "菜单管理",
|
||||
"icon": "iconfont icon-caidan",
|
||||
"sort": 2,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/menu",
|
||||
"component": "system/menu/index",
|
||||
"component_name": "menu",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "menu:Search",
|
||||
"api": "/api/system/menu/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "单例",
|
||||
"value": "menu:Retrieve",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "menu:Create",
|
||||
"api": "/api/system/menu/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "menu:Update",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "menu:Delete",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 3
|
||||
},
|
||||
{
|
||||
"name": "查询所有",
|
||||
"value": "menu:SearchAll",
|
||||
"api": "/api/system/menu/get_all_menu/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "路由",
|
||||
"value": "menu:router",
|
||||
"api": "/api/system/menu/web_router/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "查询按钮",
|
||||
"value": "menu:SearchButton",
|
||||
"api": "/api/system/menu_button/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增按钮",
|
||||
"value": "menu:CreateButton",
|
||||
"api": "/api/system/menu_button/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑按钮",
|
||||
"value": "menu:UpdateButton",
|
||||
"api": "/api/system/menu_button/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除按钮",
|
||||
"value": "menu:DeleteButton",
|
||||
"api": "/api/system/menu_button/{id}/",
|
||||
"method": 3
|
||||
},
|
||||
{
|
||||
"name": "上移",
|
||||
"value": "menu:MoveUp",
|
||||
"api": "/api/system/menu/mode_up/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "下移",
|
||||
"value": "menu:MoveDown",
|
||||
"api": "/api/system/menu/mode_down/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "查询列权限",
|
||||
"value": "column:Search",
|
||||
"api": "/api/system/column/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增列权限",
|
||||
"value": "column:Create",
|
||||
"api": "/api/system/column/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑列权限",
|
||||
"value": "column:Update",
|
||||
"api": "/api/system/column/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除列权限",
|
||||
"value": "column:Delete",
|
||||
"api": "/api/system/column/{id}/",
|
||||
"method": 3
|
||||
},
|
||||
{
|
||||
"name": "自动匹配列权限",
|
||||
"value": "column:Match",
|
||||
"api": "/api/system/column/auto_match_fields/",
|
||||
"method": 1
|
||||
}
|
||||
],
|
||||
"menu_field": []
|
||||
},
|
||||
{
|
||||
"name": "部门管理",
|
||||
"icon": "ele-OfficeBuilding",
|
||||
"sort": 3,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/dept",
|
||||
"component": "system/dept/index",
|
||||
"component_name": "dept",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "dept:Search",
|
||||
"api": "/api/system/dept/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "dept:Retrieve",
|
||||
"api": "/api/system/dept/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "获取所有部门",
|
||||
"value": "dept:SearchAll",
|
||||
"api": "/api/system/dept/all_dept/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "部门顶部信息",
|
||||
"value": "dept:HeaderInfo",
|
||||
"api": "/api/system/dept/dept_info/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "dept:Create",
|
||||
"api": "/api/system/dept/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "上移",
|
||||
"value": "dept:MoveUp",
|
||||
"api": "/api/system/dept/mode_up/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "下移",
|
||||
"value": "dept:MoveDown",
|
||||
"api": "/api/system/dept/mode_down/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "dept:Update",
|
||||
"api": "/api/system/dept/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "dept:Delete",
|
||||
"api": "/api/system/dept/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
],
|
||||
"menu_field": []
|
||||
},
|
||||
{
|
||||
"name": "角色管理",
|
||||
"icon": "ele-ColdDrink",
|
||||
"sort": 4,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/role",
|
||||
"component": "system/role/index",
|
||||
"component_name": "role",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "role:Search",
|
||||
"api": "/api/system/role/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "单例",
|
||||
"value": "role:Retrieve",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "role:Create",
|
||||
"api": "/api/system/role/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "role:Update",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "role:Delete",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 3
|
||||
},
|
||||
{
|
||||
"name": "获取所有可授权数据范围的部门",
|
||||
"value": "role:AllDataRangeDept",
|
||||
"api": "/api/system/role_menu_button_permision/role_to_dept_all/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "获取所有可授权菜单",
|
||||
"value": "role:AllCanMenu",
|
||||
"api": "/api/system/role_menu_button_permision/get_role_menu/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "获取所有已授权用户",
|
||||
"value": "role:AllAuthorizedUser",
|
||||
"api": "/api/system/role/get_role_users/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "获取菜单所有可授权按钮",
|
||||
"value": "role:AllMenuButton",
|
||||
"api": "/api/system/role_menu_button_permision/get_role_menu_btn_field/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "授权菜单",
|
||||
"value": "role:SetMenu",
|
||||
"api": "/api/system/role_menu_button_permision/set_role_menu/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "授权菜单按钮",
|
||||
"value": "role:SetMenuButton",
|
||||
"api": "/api/system/role_menu_button_permision/set_role_menu_btn/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "授权数据范围",
|
||||
"value": "role:SetDataRange",
|
||||
"api": "/api/system/role_menu_button_permision/set_role_menu_btn_data_range/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "获取所有用户",
|
||||
"value": "role:AllUser",
|
||||
"api": "/api/system/user/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "授权用户予角色",
|
||||
"value": "role:SetUserRole",
|
||||
"api": "/api/system/role/{id}/set_role_users/",
|
||||
"method": 2
|
||||
}
|
||||
],
|
||||
"menu_field": [
|
||||
{
|
||||
"field_name": "create_datetime",
|
||||
"title": "创建时间",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "creator",
|
||||
"title": "创建人",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "dept_belong_id",
|
||||
"title": "数据归属部门",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "description",
|
||||
"title": "描述",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "id",
|
||||
"title": "Id",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "key",
|
||||
"title": "权限字符",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "modifier",
|
||||
"title": "修改人",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "name",
|
||||
"title": "角色名称",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "sort",
|
||||
"title": "角色顺序",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "status",
|
||||
"title": "角色状态",
|
||||
"model": "Role"
|
||||
},
|
||||
{
|
||||
"field_name": "update_datetime",
|
||||
"title": "修改时间",
|
||||
"model": "Role"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "消息中心",
|
||||
"icon": "iconfont icon-xiaoxizhongxin",
|
||||
@@ -678,6 +715,29 @@
|
||||
"model": "ApiWhiteList"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "下载中心",
|
||||
"icon": "ele-Download",
|
||||
"sort": 9,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/downloadCenter",
|
||||
"component": "system/downloadCenter/index",
|
||||
"component_name": "downloadCenter",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 277,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "downloadCenter:Search",
|
||||
"api": "/api/system/download_center/"
|
||||
}
|
||||
],
|
||||
"menu_field": []
|
||||
}
|
||||
],
|
||||
"menu_button": [],
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
[
|
||||
{
|
||||
"role_key": "admin",
|
||||
"menu_button_value": "menu:Search",
|
||||
"data_range": 0
|
||||
},
|
||||
{
|
||||
"role_key": "public",
|
||||
"menu_button_value":"menu:Search",
|
||||
"role__key": "admin",
|
||||
"menu_button__value": "menu:Search",
|
||||
"data_range": 0
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
[
|
||||
{
|
||||
"role_key": "admin",
|
||||
"menu_component_name": "menu"
|
||||
"role__key": "admin",
|
||||
"menu__web_path": "/system",
|
||||
"menu__component_name": ""
|
||||
},
|
||||
{
|
||||
"role_key": "public",
|
||||
"menu_component_name": "menu"
|
||||
"role__key": "admin",
|
||||
"menu__web_path": "/menu",
|
||||
"menu__component_name": "menu"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -12,6 +12,34 @@
|
||||
"placeholder": null,
|
||||
"setting": null,
|
||||
"children": [
|
||||
{
|
||||
"parent": 10,
|
||||
"title": "网页标题",
|
||||
"key": "web_title",
|
||||
"value": "DVAdmin",
|
||||
"sort": 1,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [],
|
||||
"placeholder": "请输入网站标题",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"parent": 10,
|
||||
"title": "网站小图标",
|
||||
"key": "web_favicon",
|
||||
"value": "",
|
||||
"sort": 1,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [],
|
||||
"placeholder": "请输入网站小图标",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"parent": 10,
|
||||
"title": "开启验证码",
|
||||
@@ -207,5 +235,252 @@
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "文件存储配置",
|
||||
"key": "file_storage",
|
||||
"value": null,
|
||||
"sort": 0,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": null,
|
||||
"placeholder": null,
|
||||
"setting": null,
|
||||
"children": [
|
||||
{
|
||||
"title": "存储引擎",
|
||||
"key": "file_engine",
|
||||
"value": "local",
|
||||
"sort": 1,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 4,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请选择存储引擎",
|
||||
"setting": "file_engine",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "文件是否备份",
|
||||
"key": "file_backup",
|
||||
"value": false,
|
||||
"sort": 2,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 9,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "启用云存储时,文件是否备份到本地",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "阿里云-AccessKey",
|
||||
"key": "aliyun_access_key",
|
||||
"value": null,
|
||||
"sort": 3,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入AccessKey",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "阿里云-Secret",
|
||||
"key": "aliyun_access_secret",
|
||||
"value": null,
|
||||
"sort": 4,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入Secret",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "阿里云-Endpoint",
|
||||
"key": "aliyun_endpoint",
|
||||
"value": null,
|
||||
"sort": 5,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入Endpoint",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "阿里云-上传路径",
|
||||
"key": "aliyun_path",
|
||||
"value": "/media/",
|
||||
"sort": 5,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入上传路径",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "阿里云-Bucket",
|
||||
"key": "aliyun_bucket",
|
||||
"value": null,
|
||||
"sort": 7,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入Bucket",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},{
|
||||
"title": "阿里云-cdn地址",
|
||||
"key": "aliyun_cdn_url",
|
||||
"value": null,
|
||||
"sort": 7,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入cdn地址",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "腾讯云-SecretId",
|
||||
"key": "tencent_secret_id",
|
||||
"value": null,
|
||||
"sort": 8,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入SecretId",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "腾讯云-SecretKey",
|
||||
"key": "tencent_secret_key",
|
||||
"value": null,
|
||||
"sort": 9,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入SecretKey",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "腾讯云-Region",
|
||||
"key": "tencent_region",
|
||||
"value": null,
|
||||
"sort": 10,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入Region",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "腾讯云-Bucket",
|
||||
"key": "tencent_bucket",
|
||||
"value": null,
|
||||
"sort": 11,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入Bucket",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "腾讯云-上传路径",
|
||||
"key": "tencent_path",
|
||||
"value": "/media/",
|
||||
"sort": 12,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入上传路径",
|
||||
"setting": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -45,13 +45,13 @@ class Initialize(CoreInitialize):
|
||||
"""
|
||||
初始化角色菜单信息
|
||||
"""
|
||||
self.init_base(RoleMenuInitSerializer, unique_fields=['role', 'menu'])
|
||||
self.init_base(RoleMenuInitSerializer, unique_fields=['role__key', 'menu__web_path', 'menu__component_name'])
|
||||
|
||||
def init_role_menu_button(self):
|
||||
"""
|
||||
初始化角色菜单按钮信息
|
||||
"""
|
||||
self.init_base(RoleMenuButtonInitSerializer, unique_fields=['role', 'menu_button'])
|
||||
self.init_base(RoleMenuButtonInitSerializer, unique_fields=['role__key', 'menu_button__value'])
|
||||
|
||||
def init_api_white_list(self):
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,7 @@ django.setup()
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from application.settings import BASE_DIR
|
||||
from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig
|
||||
from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig, RoleMenuButtonPermission, RoleMenuPermission
|
||||
from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
|
||||
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
|
||||
RoleMenuInitSerializer, RoleMenuButtonInitSerializer
|
||||
@@ -29,7 +29,7 @@ class Command(BaseCommand):
|
||||
def serializer_data(self, serializer, query_set: QuerySet):
|
||||
serializer = serializer(query_set, many=True)
|
||||
data = json.loads(json.dumps(serializer.data, ensure_ascii=False))
|
||||
with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w') as f:
|
||||
with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w',encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
return
|
||||
|
||||
@@ -57,6 +57,12 @@ class Command(BaseCommand):
|
||||
def generate_system_config(self):
|
||||
self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True))
|
||||
|
||||
def generate_role_menu(self):
|
||||
self.serializer_data(RoleMenuInitSerializer, RoleMenuPermission.objects.all())
|
||||
|
||||
def generate_role_menu_button(self):
|
||||
self.serializer_data(RoleMenuButtonInitSerializer, RoleMenuButtonPermission.objects.all())
|
||||
|
||||
def handle(self, *args, **options):
|
||||
generate_name = options.get('generate_name')
|
||||
generate_name_dict = {
|
||||
@@ -67,6 +73,8 @@ class Command(BaseCommand):
|
||||
"api_white_list": self.generate_api_white_list,
|
||||
"dictionary": self.generate_dictionary,
|
||||
"system_config": self.generate_system_config,
|
||||
"role_menu": self.generate_role_menu,
|
||||
"role_menu_button": self.generate_role_menu_button,
|
||||
}
|
||||
if not generate_name:
|
||||
for ele in generate_name_dict.keys():
|
||||
|
||||
@@ -22,6 +22,8 @@ class Command(BaseCommand):
|
||||
parser.add_argument("-Y", nargs="*")
|
||||
parser.add_argument("-n", nargs="*")
|
||||
parser.add_argument("-N", nargs="*")
|
||||
parser.add_argument("-app", nargs="*")
|
||||
parser.add_argument("-A", nargs="*")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
reset = False
|
||||
@@ -29,9 +31,10 @@ class Command(BaseCommand):
|
||||
reset = True
|
||||
if isinstance(options.get("n"), list) or isinstance(options.get("N"), list):
|
||||
reset = False
|
||||
|
||||
assign_apps = options.get("app") or options.get("A") or []
|
||||
for app in settings.INSTALLED_APPS:
|
||||
|
||||
if assign_apps and app not in assign_apps:
|
||||
continue
|
||||
try:
|
||||
exec(
|
||||
f"""
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import hashlib
|
||||
import os
|
||||
from time import time
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
from django.contrib.auth.models import AbstractUser, UserManager
|
||||
from django.db import models
|
||||
@@ -61,6 +63,8 @@ class Users(CoreModel, AbstractUser):
|
||||
help_text="关联岗位")
|
||||
role = models.ManyToManyField(to="Role", blank=True, verbose_name="关联角色", db_constraint=False,
|
||||
help_text="关联角色")
|
||||
current_role = models.ForeignKey(to=Role, null=True, blank=True, db_constraint=False, on_delete=models.SET_NULL,
|
||||
verbose_name="当前登录角色", help_text="当前登录角色", related_name='current_role_set')
|
||||
dept = models.ForeignKey(
|
||||
to="Dept",
|
||||
verbose_name="所属部门",
|
||||
@@ -70,10 +74,26 @@ class Users(CoreModel, AbstractUser):
|
||||
blank=True,
|
||||
help_text="关联部门",
|
||||
)
|
||||
manage_dept = models.ManyToManyField(
|
||||
to="Dept",
|
||||
verbose_name="管理部门",
|
||||
db_constraint=False,
|
||||
blank=True,
|
||||
help_text="管理部门",
|
||||
related_name='manage_dept_set'
|
||||
)
|
||||
login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数")
|
||||
pwd_change_count = models.IntegerField(default=0,blank=True, verbose_name="密码修改次数", help_text="密码修改次数")
|
||||
objects = CustomUserManager()
|
||||
|
||||
def set_password(self, raw_password):
|
||||
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest())
|
||||
if raw_password:
|
||||
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest())
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.name == "":
|
||||
self.name = self.username
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_users"
|
||||
@@ -118,6 +138,27 @@ class Dept(CoreModel):
|
||||
help_text="上级部门",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _recursion(cls, instance, parent, result):
|
||||
new_instance = getattr(instance, parent, None)
|
||||
res = []
|
||||
data = getattr(instance, result, None)
|
||||
if data:
|
||||
res.append(data)
|
||||
if new_instance:
|
||||
array = cls._recursion(new_instance, parent, result)
|
||||
res += array
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_region_name(cls, obj):
|
||||
"""
|
||||
获取某个用户的递归所有部门名称
|
||||
"""
|
||||
dept_name_all = cls._recursion(obj, "parent", "name")
|
||||
dept_name_all.reverse()
|
||||
return "/".join(dept_name_all)
|
||||
|
||||
@classmethod
|
||||
def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None):
|
||||
"""
|
||||
@@ -137,6 +178,23 @@ class Dept(CoreModel):
|
||||
cls.recursion_all_dept(ele.get("id"), dept_all_list, dept_list)
|
||||
return list(set(dept_list))
|
||||
|
||||
@classmethod
|
||||
def recursion_all_parent_dept(cls, dept_id: int, dept_list=None):
|
||||
"""
|
||||
递归获取部门的所有上级部门
|
||||
:param dept_id: 需要获取的id
|
||||
:param dept_list: 递归list
|
||||
:return:
|
||||
"""
|
||||
if dept_list is None:
|
||||
dept_list = [dept_id]
|
||||
current_dept = Dept.objects.filter(id=dept_id).values('parent').first()
|
||||
if current_dept and current_dept.get('parent'):
|
||||
parent_id = current_dept.get('parent')
|
||||
dept_list.append(parent_id)
|
||||
cls.recursion_all_parent_dept(parent_id, dept_list)
|
||||
return list(set(dept_list))
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_dept"
|
||||
verbose_name = "部门表"
|
||||
@@ -404,6 +462,18 @@ class FileList(CoreModel):
|
||||
mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型")
|
||||
size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小")
|
||||
md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5")
|
||||
UPLOAD_METHOD_CHOIDES = (
|
||||
(0, '默认上传'),
|
||||
(1, '文件选择器上传'),
|
||||
)
|
||||
upload_method = models.SmallIntegerField(default=0, blank=True, null=True, choices=UPLOAD_METHOD_CHOIDES, verbose_name='上传方式', help_text='上传方式')
|
||||
FILE_TYPE_CHOIDES = (
|
||||
(0, '图片'),
|
||||
(1, '视频'),
|
||||
(2, '音频'),
|
||||
(3, '其他'),
|
||||
)
|
||||
file_type = models.SmallIntegerField(default=3, choices=FILE_TYPE_CHOIDES, blank=True, null=True, verbose_name='文件类型', help_text='文件类型')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.md5sum: # file is new
|
||||
@@ -485,7 +555,7 @@ class SystemConfig(CoreModel):
|
||||
help_text="父级",
|
||||
)
|
||||
title = models.CharField(max_length=50, verbose_name="标题", help_text="标题")
|
||||
key = models.CharField(max_length=20, verbose_name="键", help_text="键", db_index=True)
|
||||
key = models.CharField(max_length=100, verbose_name="键", help_text="键", db_index=True)
|
||||
value = models.JSONField(max_length=100, verbose_name="值", help_text="值", null=True, blank=True)
|
||||
sort = models.IntegerField(default=0, verbose_name="排序", help_text="排序", blank=True)
|
||||
status = models.BooleanField(default=True, verbose_name="启用状态", help_text="启用状态")
|
||||
@@ -594,3 +664,41 @@ class MessageCenterTargetUser(CoreModel):
|
||||
db_table = table_prefix + "message_center_target_user"
|
||||
verbose_name = "消息中心目标用户表"
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
|
||||
def media_file_name_downloadcenter(instance:'DownloadCenter', filename):
|
||||
h = instance.md5sum
|
||||
basename, ext = os.path.splitext(filename)
|
||||
return PurePosixPath("files", "dlct", h[:1], h[1:2], basename + '-' + str(time()).replace('.', '') + ext.lower())
|
||||
|
||||
|
||||
class DownloadCenter(CoreModel):
|
||||
TASK_STATUS_CHOICES = [
|
||||
(0, '任务已创建'),
|
||||
(1, '任务进行中'),
|
||||
(2, '任务完成'),
|
||||
(3, '任务失败'),
|
||||
]
|
||||
task_name = models.CharField(max_length=255, verbose_name="任务名称", help_text="任务名称")
|
||||
task_status = models.SmallIntegerField(default=0, choices=TASK_STATUS_CHOICES, verbose_name='是否可下载', help_text='是否可下载')
|
||||
file_name = models.CharField(max_length=255, null=True, blank=True, verbose_name="文件名", help_text="文件名")
|
||||
url = models.FileField(upload_to=media_file_name_downloadcenter, null=True, blank=True)
|
||||
size = models.BigIntegerField(default=0, verbose_name="文件大小", help_text="文件大小")
|
||||
md5sum = models.CharField(max_length=36, null=True, blank=True, verbose_name="文件md5", help_text="文件md5")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.url:
|
||||
if not self.md5sum: # file is new
|
||||
md5 = hashlib.md5()
|
||||
for chunk in self.url.chunks():
|
||||
md5.update(chunk)
|
||||
self.md5sum = md5.hexdigest()
|
||||
if not self.size:
|
||||
self.size = self.url.size
|
||||
super(DownloadCenter, self).save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "download_center"
|
||||
verbose_name = "下载中心"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("-create_datetime",)
|
||||
|
||||
27
backend/dvadmin/system/signals.py
Normal file
27
backend/dvadmin/system/signals.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import time
|
||||
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.core.cache import cache
|
||||
from dvadmin.system.models import MessageCenterTargetUser
|
||||
|
||||
# 初始化信号
|
||||
pre_init_complete = Signal()
|
||||
detail_init_complete = Signal()
|
||||
post_init_complete = Signal()
|
||||
# 租户初始化信号
|
||||
pre_tenants_init_complete = Signal()
|
||||
detail_tenants_init_complete = Signal()
|
||||
post_tenants_init_complete = Signal()
|
||||
post_tenants_all_init_complete = Signal()
|
||||
# 租户创建完成信号
|
||||
tenants_create_complete = Signal()
|
||||
|
||||
# 全局变量用于标记最后修改时间
|
||||
last_db_change_time = time.time()
|
||||
|
||||
|
||||
@receiver(post_save, sender=MessageCenterTargetUser)
|
||||
@receiver(post_delete, sender=MessageCenterTargetUser)
|
||||
def update_last_change_time(sender, **kwargs):
|
||||
cache.set('last_db_change_time', time.time(), timeout=None) # 设置永不超时的键值对
|
||||
107
backend/dvadmin/system/tasks.py
Normal file
107
backend/dvadmin/system/tasks.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from hashlib import md5
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.worksheet.table import Table, TableStyleInfo
|
||||
from openpyxl.utils import get_column_letter
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from application.celery import app
|
||||
from dvadmin.system.models import DownloadCenter
|
||||
|
||||
def is_number(num):
|
||||
try:
|
||||
float(num)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import unicodedata
|
||||
unicodedata.numeric(num)
|
||||
return True
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return False
|
||||
|
||||
def get_string_len(string):
|
||||
"""
|
||||
获取字符串最大长度
|
||||
:param string:
|
||||
:return:
|
||||
"""
|
||||
length = 4
|
||||
if string is None:
|
||||
return length
|
||||
if is_number(string):
|
||||
return length
|
||||
for char in string:
|
||||
length += 2.1 if ord(char) > 256 else 1
|
||||
return round(length, 1) if length <= 50 else 50
|
||||
|
||||
@app.task
|
||||
def async_export_data(data: list, filename: str, dcid: int, export_field_label: dict):
|
||||
instance = DownloadCenter.objects.get(pk=dcid)
|
||||
instance.task_status = 1
|
||||
instance.save()
|
||||
sleep(2)
|
||||
try:
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
header_data = ["序号", *export_field_label.values()]
|
||||
hidden_header = ["#", *export_field_label.keys()]
|
||||
df_len_max = [get_string_len(ele) for ele in header_data]
|
||||
row = get_column_letter(len(export_field_label) + 1)
|
||||
column = 1
|
||||
ws.append(header_data)
|
||||
for index, results in enumerate(data):
|
||||
results_list = []
|
||||
for h_index, h_item in enumerate(hidden_header):
|
||||
for key, val in results.items():
|
||||
if key == h_item:
|
||||
if val is None or val == "":
|
||||
results_list.append("")
|
||||
elif isinstance(val, datetime):
|
||||
val = val.strftime("%Y-%m-%d %H:%M:%S")
|
||||
results_list.append(val)
|
||||
else:
|
||||
results_list.append(val)
|
||||
# 计算最大列宽度
|
||||
result_column_width = get_string_len(val)
|
||||
if h_index != 0 and result_column_width > df_len_max[h_index]:
|
||||
df_len_max[h_index] = result_column_width
|
||||
ws.append([index + 1, *results_list])
|
||||
column += 1
|
||||
# 更新列宽
|
||||
for index, width in enumerate(df_len_max):
|
||||
ws.column_dimensions[get_column_letter(index + 1)].width = width
|
||||
tab = Table(displayName="Table", ref=f"A1:{row}{column}") # 名称管理器
|
||||
style = TableStyleInfo(
|
||||
name="TableStyleLight11",
|
||||
showFirstColumn=True,
|
||||
showLastColumn=True,
|
||||
showRowStripes=True,
|
||||
showColumnStripes=True,
|
||||
)
|
||||
tab.tableStyleInfo = style
|
||||
ws.add_table(tab)
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
stream.seek(0)
|
||||
s = md5()
|
||||
while True:
|
||||
chunk = stream.read(1024)
|
||||
if not chunk:
|
||||
break
|
||||
s.update(chunk)
|
||||
stream.seek(0)
|
||||
instance.md5sum = s.hexdigest()
|
||||
instance.file_name = filename
|
||||
instance.url.save(filename, ContentFile(stream.read()))
|
||||
instance.task_status = 2
|
||||
except Exception as e:
|
||||
instance.task_status = 3
|
||||
instance.description = str(e)[:250]
|
||||
instance.save()
|
||||
@@ -18,6 +18,7 @@ from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermi
|
||||
from dvadmin.system.views.system_config import SystemConfigViewSet
|
||||
from dvadmin.system.views.user import UserViewSet
|
||||
from dvadmin.system.views.menu_field import MenuFieldViewSet
|
||||
from dvadmin.system.views.download_center import DownloadCenterViewSet
|
||||
|
||||
system_url = routers.SimpleRouter()
|
||||
system_url.register(r'menu', MenuViewSet)
|
||||
@@ -35,6 +36,8 @@ system_url.register(r'message_center', MessageCenterViewSet)
|
||||
system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionViewSet)
|
||||
system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet)
|
||||
system_url.register(r'column', MenuFieldViewSet)
|
||||
system_url.register(r'login_log', LoginLogViewSet)
|
||||
system_url.register(r'download_center', DownloadCenterViewSet)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -44,9 +47,9 @@ urlpatterns = [
|
||||
path('system_config/get_association_table/', SystemConfigViewSet.as_view({'get': 'get_association_table'})),
|
||||
path('system_config/get_table_data/<int:pk>/', SystemConfigViewSet.as_view({'get': 'get_table_data'})),
|
||||
path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})),
|
||||
path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
|
||||
path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
|
||||
path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
|
||||
# path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
|
||||
# path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
|
||||
# path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
|
||||
path('clause/privacy.html', PrivacyView.as_view()),
|
||||
path('clause/terms_service.html', TermsServiceView.as_view()),
|
||||
]
|
||||
|
||||
0
backend/dvadmin/system/views/__init__.py
Normal file
0
backend/dvadmin/system/views/__init__.py
Normal file
@@ -1,8 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pypinyin
|
||||
from django.db.models import Q
|
||||
from rest_framework import serializers
|
||||
|
||||
from dvadmin.system.models import Area
|
||||
from dvadmin.utils.field_permission import FieldPermissionMixin
|
||||
from dvadmin.utils.json_response import SuccessResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
@@ -14,13 +16,21 @@ class AreaSerializer(CustomModelSerializer):
|
||||
"""
|
||||
pcode_count = serializers.SerializerMethodField(read_only=True)
|
||||
hasChild = serializers.SerializerMethodField()
|
||||
pcode_info = serializers.SerializerMethodField()
|
||||
|
||||
def get_pcode_info(self, instance):
|
||||
pcode = Area.objects.filter(code=instance.pcode_id).values("name", "code")
|
||||
return pcode
|
||||
|
||||
def get_pcode_count(self, instance: Area):
|
||||
return Area.objects.filter(pcode=instance).count()
|
||||
|
||||
def get_hasChild(self, instance):
|
||||
hasChild = Area.objects.filter(pcode=instance.code)
|
||||
if hasChild:
|
||||
return True
|
||||
return False
|
||||
|
||||
class Meta:
|
||||
model = Area
|
||||
fields = "__all__"
|
||||
@@ -32,12 +42,24 @@ class AreaCreateUpdateSerializer(CustomModelSerializer):
|
||||
地区管理 创建/更新时的列化器
|
||||
"""
|
||||
|
||||
def to_internal_value(self, data):
|
||||
pinyin = ''.join([''.join(i) for i in pypinyin.pinyin(data["name"], style=pypinyin.NORMAL)])
|
||||
data["level"] = 1
|
||||
data["pinyin"] = pinyin
|
||||
data["initials"] = pinyin[0].upper() if pinyin else "#"
|
||||
pcode = data["pcode"] if 'pcode' in data else None
|
||||
if pcode:
|
||||
pcode = Area.objects.get(pk=pcode)
|
||||
data["pcode"] = pcode.code
|
||||
data["level"] = pcode.level + 1
|
||||
return super().to_internal_value(data)
|
||||
|
||||
class Meta:
|
||||
model = Area
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class AreaViewSet(CustomModelViewSet):
|
||||
class AreaViewSet(CustomModelViewSet, FieldPermissionMixin):
|
||||
"""
|
||||
地区管理接口
|
||||
list:查询
|
||||
@@ -48,21 +70,28 @@ class AreaViewSet(CustomModelViewSet):
|
||||
"""
|
||||
queryset = Area.objects.all()
|
||||
serializer_class = AreaSerializer
|
||||
create_serializer_class = AreaCreateUpdateSerializer
|
||||
update_serializer_class = AreaCreateUpdateSerializer
|
||||
extra_filter_class = []
|
||||
|
||||
def get_queryset(self):
|
||||
def list(self, request, *args, **kwargs):
|
||||
self.request.query_params._mutable = True
|
||||
params = self.request.query_params
|
||||
pcode = params.get('pcode', None)
|
||||
page = params.get('page', None)
|
||||
limit = params.get('limit', None)
|
||||
if page:
|
||||
del params['page']
|
||||
if limit:
|
||||
del params['limit']
|
||||
if params and pcode:
|
||||
queryset = self.queryset.filter(enable=True, pcode=pcode)
|
||||
else:
|
||||
known_params = {'page', 'limit', 'pcode'}
|
||||
# 使用集合操作检查是否有未知参数
|
||||
other_params_exist = any(param not in known_params for param in params)
|
||||
if other_params_exist:
|
||||
queryset = self.queryset.filter(enable=True)
|
||||
return queryset
|
||||
|
||||
else:
|
||||
pcode = params.get('pcode', None)
|
||||
params['limit'] = 999
|
||||
if params and pcode:
|
||||
queryset = self.queryset.filter(enable=True, pcode=pcode)
|
||||
else:
|
||||
queryset = self.queryset.filter(enable=True, level=1)
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True, request=request)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = self.get_serializer(queryset, many=True, request=request)
|
||||
return SuccessResponse(data=serializer.data, msg="获取成功")
|
||||
|
||||
@@ -10,6 +10,7 @@ from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import Dept, RoleMenuButtonPermission, Users
|
||||
from dvadmin.utils.filters import DataLevelPermissionsFilter
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
@@ -124,33 +125,7 @@ class DeptViewSet(CustomModelViewSet):
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[])
|
||||
def dept_lazy_tree(self, request, *args, **kwargs):
|
||||
parent = self.request.query_params.get('parent')
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
queryset = Dept.objects.values('id', 'name', 'parent')
|
||||
else:
|
||||
role_ids = request.user.role.values_list('id', flat=True)
|
||||
data_range = RoleMenuButtonPermission.objects.filter(role__in=role_ids).values_list('data_range', flat=True)
|
||||
user_dept_id = request.user.dept.id
|
||||
dept_list = [user_dept_id]
|
||||
data_range_list = list(set(data_range))
|
||||
for item in data_range_list:
|
||||
if item in [0, 2]:
|
||||
dept_list = [user_dept_id]
|
||||
elif item == 1:
|
||||
dept_list = Dept.recursion_all_dept(dept_id=user_dept_id)
|
||||
elif item == 3:
|
||||
dept_list = Dept.objects.values_list('id', flat=True)
|
||||
elif item == 4:
|
||||
dept_list = request.user.role.values_list('dept', flat=True)
|
||||
else:
|
||||
dept_list = []
|
||||
queryset = Dept.objects.filter(id__in=dept_list).values('id', 'name', 'parent')
|
||||
return DetailResponse(data=queryset, msg="获取成功")
|
||||
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[])
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
|
||||
def all_dept(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent')
|
||||
|
||||
54
backend/dvadmin/system/views/download_center.py
Normal file
54
backend/dvadmin/system/views/download_center.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from rest_framework import serializers
|
||||
from django.conf import settings
|
||||
from django_filters.rest_framework import FilterSet, CharFilter
|
||||
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
from dvadmin.system.models import DownloadCenter
|
||||
|
||||
|
||||
class DownloadCenterSerializer(CustomModelSerializer):
|
||||
url = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_url(self, instance):
|
||||
if self.request.query_params.get('prefix'):
|
||||
if settings.ENVIRONMENT in ['local']:
|
||||
prefix = 'http://127.0.0.1:8000'
|
||||
elif settings.ENVIRONMENT in ['test']:
|
||||
prefix = 'http://{host}/api'.format(host=self.request.get_host())
|
||||
else:
|
||||
prefix = 'https://{host}/api'.format(host=self.request.get_host())
|
||||
return (f'{prefix}/media/{str(instance.url)}')
|
||||
return f'media/{str(instance.url)}'
|
||||
|
||||
class Meta:
|
||||
model = DownloadCenter
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class DownloadCenterFilterSet(FilterSet):
|
||||
task_name = CharFilter(field_name='task_name', lookup_expr='icontains')
|
||||
file_name = CharFilter(field_name='file_name', lookup_expr='icontains')
|
||||
|
||||
class Meta:
|
||||
model = DownloadCenter
|
||||
fields = ['task_status', 'task_name', 'file_name']
|
||||
|
||||
|
||||
class DownloadCenterViewSet(CustomModelViewSet):
|
||||
queryset = DownloadCenter.objects.all()
|
||||
serializer_class = DownloadCenterSerializer
|
||||
filter_class = DownloadCenterFilterSet
|
||||
permission_classes = []
|
||||
extra_filter_class = []
|
||||
|
||||
def get_queryset(self):
|
||||
# 判断是否是 Swagger 文档生成阶段,防止报错
|
||||
if getattr(self, 'swagger_fake_view', False):
|
||||
return self.queryset.model.objects.none()
|
||||
|
||||
# 正常请求下的逻辑
|
||||
if self.request.user.is_superuser:
|
||||
return super().get_queryset()
|
||||
return super().get_queryset().filter(creator=self.request.user)
|
||||
@@ -1,12 +1,16 @@
|
||||
import hashlib
|
||||
import mimetypes
|
||||
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from application import dispatch
|
||||
from dvadmin.system.models import FileList
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -15,7 +19,16 @@ class FileSerializer(CustomModelSerializer):
|
||||
url = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_url(self, instance):
|
||||
# return 'media/' + str(instance.url)
|
||||
if self.request.query_params.get('prefix'):
|
||||
if settings.ENVIRONMENT in ['local']:
|
||||
prefix = 'http://127.0.0.1:8000'
|
||||
elif settings.ENVIRONMENT in ['test']:
|
||||
prefix = 'http://{host}/api'.format(host=self.request.get_host())
|
||||
else:
|
||||
prefix = 'https://{host}/api'.format(host=self.request.get_host())
|
||||
if instance.file_url:
|
||||
return instance.file_url if instance.file_url.startswith('http') else f"{prefix}/{instance.file_url}"
|
||||
return (f'{prefix}/media/{str(instance.url)}')
|
||||
return instance.file_url or (f'media/{str(instance.url)}')
|
||||
|
||||
class Meta:
|
||||
@@ -23,8 +36,8 @@ class FileSerializer(CustomModelSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
def create(self, validated_data):
|
||||
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local'
|
||||
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup")
|
||||
file_engine = dispatch.get_system_config_values("file_storage.file_engine") or 'local'
|
||||
file_backup = dispatch.get_system_config_values("file_storage.file_backup")
|
||||
file = self.initial_data.get('file')
|
||||
file_size = file.size
|
||||
validated_data['name'] = str(file)
|
||||
@@ -35,18 +48,20 @@ class FileSerializer(CustomModelSerializer):
|
||||
validated_data['md5sum'] = md5.hexdigest()
|
||||
validated_data['engine'] = file_engine
|
||||
validated_data['mime_type'] = file.content_type
|
||||
ft = {'image':0,'video':1,'audio':2}.get(file.content_type.split('/')[0], None)
|
||||
validated_data['file_type'] = 3 if ft is None else ft
|
||||
if file_backup:
|
||||
validated_data['url'] = file
|
||||
if file_engine == 'oss':
|
||||
from dvadmin_cloud_storage.views.aliyun import ali_oss_upload
|
||||
file_path = ali_oss_upload(file)
|
||||
from dvadmin.utils.aliyunoss import ali_oss_upload
|
||||
file_path = ali_oss_upload(file, file_name=validated_data['name'])
|
||||
if file_path:
|
||||
validated_data['file_url'] = file_path
|
||||
else:
|
||||
raise ValueError("上传失败")
|
||||
elif file_engine == 'cos':
|
||||
from dvadmin_cloud_storage.views.tencent import tencent_cos_upload
|
||||
file_path = tencent_cos_upload(file)
|
||||
from dvadmin.utils.tencentcos import tencent_cos_upload
|
||||
file_path = tencent_cos_upload(file, file_name=validated_data['name'])
|
||||
if file_path:
|
||||
validated_data['file_url'] = file_path
|
||||
else:
|
||||
@@ -64,6 +79,22 @@ class FileSerializer(CustomModelSerializer):
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class FileAllSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = FileList
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
class FileFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(field_name="name", lookup_expr="icontains", help_text="文件名")
|
||||
mime_type = django_filters.CharFilter(field_name="mime_type", lookup_expr="icontains", help_text="文件类型")
|
||||
|
||||
class Meta:
|
||||
model = FileList
|
||||
fields = ['name', 'mime_type', 'upload_method', 'file_type']
|
||||
|
||||
|
||||
class FileViewSet(CustomModelViewSet):
|
||||
"""
|
||||
文件管理接口
|
||||
@@ -75,5 +106,22 @@ class FileViewSet(CustomModelViewSet):
|
||||
"""
|
||||
queryset = FileList.objects.all()
|
||||
serializer_class = FileSerializer
|
||||
filter_fields = ['name', ]
|
||||
permission_classes = []
|
||||
filter_class = FileFilter
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(methods=['GET'], detail=False)
|
||||
def get_all(self, request):
|
||||
data1 = self.get_serializer(self.get_queryset(), many=True).data
|
||||
data2 = []
|
||||
if dispatch.is_tenants_mode():
|
||||
from django_tenants.utils import schema_context
|
||||
with schema_context('public'):
|
||||
data2 = self.get_serializer(FileList.objects.all(), many=True).data
|
||||
return DetailResponse(data=data2+data1)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if self.request.query_params.get('system', 'False') == 'True' and dispatch.is_tenants_mode():
|
||||
from django_tenants.utils import schema_context
|
||||
with schema_context('public'):
|
||||
return super().list(request, *args, **kwargs)
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@@ -4,11 +4,15 @@ from datetime import datetime, timedelta
|
||||
from captcha.views import CaptchaStore, captcha_image
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
@@ -83,31 +87,50 @@ class LoginSerializer(TokenObtainPairSerializer):
|
||||
else:
|
||||
self.image_code and self.image_code.delete()
|
||||
raise CustomValidationError("图片验证码错误")
|
||||
|
||||
user = Users.objects.get(username=attrs['username'])
|
||||
try:
|
||||
user = Users.objects.get(
|
||||
Q(username=attrs['username']) | Q(email=attrs['username']) | Q(mobile=attrs['username']))
|
||||
except Users.DoesNotExist:
|
||||
raise CustomValidationError("您登录的账号不存在")
|
||||
except Users.MultipleObjectsReturned:
|
||||
raise CustomValidationError("您登录的账号存在多个,请联系管理员检查登录账号唯一性")
|
||||
if not user.is_active:
|
||||
raise CustomValidationError("账号被锁定")
|
||||
|
||||
data = super().validate(attrs)
|
||||
data["name"] = self.user.name
|
||||
data["userId"] = self.user.id
|
||||
data["avatar"] = self.user.avatar
|
||||
data['user_type'] = self.user.user_type
|
||||
dept = getattr(self.user, 'dept', None)
|
||||
if dept:
|
||||
data['dept_info'] = {
|
||||
'dept_id': dept.id,
|
||||
'dept_name': dept.name,
|
||||
|
||||
}
|
||||
role = getattr(self.user, 'role', None)
|
||||
if role:
|
||||
data['role_info'] = role.values('id', 'name', 'key')
|
||||
request = self.context.get("request")
|
||||
request.user = self.user
|
||||
# 记录登录日志
|
||||
save_login_log(request=request)
|
||||
return {"code": 2000, "msg": "请求成功", "data": data}
|
||||
raise CustomValidationError("账号已被锁定,联系管理员解锁")
|
||||
try:
|
||||
# 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误
|
||||
attrs['username'] = user.username
|
||||
data = super().validate(attrs)
|
||||
data["username"] = self.user.username
|
||||
data["name"] = self.user.name
|
||||
data["userId"] = self.user.id
|
||||
data["avatar"] = self.user.avatar
|
||||
data['user_type'] = self.user.user_type
|
||||
data['pwd_change_count'] = self.user.pwd_change_count
|
||||
dept = getattr(self.user, 'dept', None)
|
||||
if dept:
|
||||
data['dept_info'] = {
|
||||
'dept_id': dept.id,
|
||||
'dept_name': dept.name,
|
||||
}
|
||||
role = getattr(self.user, 'role', None)
|
||||
if role:
|
||||
data['role_info'] = role.values('id', 'name', 'key')
|
||||
request = self.context.get("request")
|
||||
request.user = self.user
|
||||
# 记录登录日志
|
||||
save_login_log(request=request)
|
||||
user.login_error_count = 0
|
||||
user.save()
|
||||
return {"code": 2000, "msg": "请求成功", "data": data}
|
||||
except Exception as e:
|
||||
user.login_error_count += 1
|
||||
if user.login_error_count >= 5:
|
||||
user.is_active = False
|
||||
user.save()
|
||||
raise CustomValidationError("账号已被锁定,联系管理员解锁")
|
||||
user.save()
|
||||
count = 5 - user.login_error_count
|
||||
raise CustomValidationError(f"账号/密码错误;重试{count}次后将被锁定~")
|
||||
|
||||
|
||||
class LoginView(TokenObtainPairView):
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@Remark: 按钮权限管理
|
||||
"""
|
||||
from dvadmin.system.models import LoginLog
|
||||
from dvadmin.utils.field_permission import FieldPermissionMixin
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -22,7 +23,7 @@ class LoginLogSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class LoginLogViewSet(CustomModelViewSet):
|
||||
class LoginLogViewSet(CustomModelViewSet, FieldPermissionMixin):
|
||||
"""
|
||||
登录日志接口
|
||||
list:查询
|
||||
@@ -33,4 +34,4 @@ class LoginLogViewSet(CustomModelViewSet):
|
||||
"""
|
||||
queryset = LoginLog.objects.all()
|
||||
serializer_class = LoginLogSerializer
|
||||
extra_filter_class = []
|
||||
# extra_filter_class = []
|
||||
|
||||
@@ -120,11 +120,11 @@ class MenuViewSet(CustomModelViewSet):
|
||||
"""用于前端获取当前角色的路由"""
|
||||
user = request.user
|
||||
if user.is_superuser:
|
||||
queryset = self.queryset.filter(status=1)
|
||||
queryset = self.queryset.filter(status=1).order_by("sort")
|
||||
else:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True)
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
queryset = Menu.objects.filter(id__in=menu_list).order_by("sort")
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@@ -10,12 +10,14 @@ from django.db.models import F
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import MenuButton, RoleMenuButtonPermission
|
||||
from dvadmin.system.models import MenuButton, RoleMenuButtonPermission, Menu
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
|
||||
|
||||
|
||||
class MenuButtonSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单按钮-序列化器
|
||||
@@ -80,4 +82,27 @@ class MenuButtonViewSet(CustomModelViewSet):
|
||||
else:
|
||||
role_id = request.user.role.values_list('id', flat=True)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_id).values_list('menu_button__value',flat=True).distinct()
|
||||
return DetailResponse(data=queryset)
|
||||
return DetailResponse(data=queryset)
|
||||
|
||||
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def batch_create(self, request, *args, **kwargs):
|
||||
"""
|
||||
批量创建菜单“增删改查查”权限
|
||||
创建的数据来源于菜单,需要规范创建菜单参数
|
||||
value:菜单的component_name:method
|
||||
api:菜单的web_path增加'/api'前缀,并根据method增加{id}
|
||||
"""
|
||||
menu_obj = Menu.objects.filter(id=request.data['menu']).first()
|
||||
result_list = [
|
||||
{'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api/{menu_obj.component_name}/', 'method': 1},
|
||||
{'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 3},
|
||||
{'menu': menu_obj.id, 'name': '编辑', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 2},
|
||||
{'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api/{menu_obj.component_name}/', 'method': 0},
|
||||
{'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 0},
|
||||
{'menu': menu_obj.id, 'name': '复制', 'value': f'{menu_obj.component_name}:Copy', 'api': f'/api/{menu_obj.component_name}/', 'method': 1},
|
||||
{'menu': menu_obj.id, 'name': '导入', 'value': f'{menu_obj.component_name}:Import', 'api': f'/api/{menu_obj.component_name}/import_data/', 'method': 1},
|
||||
{'menu': menu_obj.id, 'name': '导出', 'value': f'{menu_obj.component_name}:Export', 'api': f'/api{menu_obj.component_name}/export_data/', 'method': 1},]
|
||||
serializer = self.get_serializer(data=result_list, many=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return SuccessResponse(serializer.data, msg="批量创建成功")
|
||||
@@ -36,6 +36,8 @@ class MessageCenterSerializer(CustomModelSerializer):
|
||||
return serializer.data
|
||||
|
||||
def get_user_info(self, instance, parsed_query):
|
||||
if instance.target_type in (1, 2, 3):
|
||||
return []
|
||||
users = instance.target_user.all()
|
||||
# You can do what ever you want in here
|
||||
# `parsed_query` param is passed to BookSerializer to allow further querying
|
||||
@@ -80,6 +82,9 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
||||
"""
|
||||
目标用户序列化器-序列化器
|
||||
"""
|
||||
role_info = DynamicSerializerMethodField()
|
||||
user_info = DynamicSerializerMethodField()
|
||||
dept_info = DynamicSerializerMethodField()
|
||||
is_read = serializers.SerializerMethodField()
|
||||
|
||||
def get_is_read(self, instance):
|
||||
@@ -90,12 +95,49 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
||||
return queryset.is_read
|
||||
return False
|
||||
|
||||
def get_role_info(self, instance, parsed_query):
|
||||
roles = instance.target_role.all()
|
||||
# You can do what ever you want in here
|
||||
# `parsed_query` param is passed to BookSerializer to allow further querying
|
||||
from dvadmin.system.views.role import RoleSerializer
|
||||
serializer = RoleSerializer(
|
||||
roles,
|
||||
many=True,
|
||||
parsed_query=parsed_query
|
||||
)
|
||||
return serializer.data
|
||||
|
||||
def get_user_info(self, instance, parsed_query):
|
||||
if instance.target_type in (1, 2, 3):
|
||||
return []
|
||||
users = instance.target_user.all()
|
||||
# You can do what ever you want in here
|
||||
# `parsed_query` param is passed to BookSerializer to allow further querying
|
||||
from dvadmin.system.views.user import UserSerializer
|
||||
serializer = UserSerializer(
|
||||
users,
|
||||
many=True,
|
||||
parsed_query=parsed_query
|
||||
)
|
||||
return serializer.data
|
||||
|
||||
def get_dept_info(self, instance, parsed_query):
|
||||
dept = instance.target_dept.all()
|
||||
# You can do what ever you want in here
|
||||
# `parsed_query` param is passed to BookSerializer to allow further querying
|
||||
from dvadmin.system.views.dept import DeptSerializer
|
||||
serializer = DeptSerializer(
|
||||
dept,
|
||||
many=True,
|
||||
parsed_query=parsed_query
|
||||
)
|
||||
return serializer.data
|
||||
|
||||
class Meta:
|
||||
model = MessageCenter
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
def websocket_push(user_id, message):
|
||||
"""
|
||||
主动推送消息
|
||||
@@ -110,7 +152,6 @@ def websocket_push(user_id, message):
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MessageCenterCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
消息中心-新增-序列化器
|
||||
|
||||
@@ -10,22 +10,29 @@ from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import Role, Menu, MenuButton, Dept
|
||||
from dvadmin.system.models import Role, Menu, MenuButton, Dept, Users
|
||||
from dvadmin.system.views.dept import DeptSerializer
|
||||
from dvadmin.system.views.menu import MenuSerializer
|
||||
from dvadmin.system.views.menu_button import MenuButtonSerializer
|
||||
from dvadmin.utils.crud_mixin import FastCrudMixin
|
||||
from dvadmin.utils.field_permission import FieldPermissionMixin
|
||||
from dvadmin.utils.json_response import SuccessResponse, DetailResponse
|
||||
from dvadmin.utils.json_response import SuccessResponse, DetailResponse, ErrorResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.validator import CustomUniqueValidator
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
from dvadmin.utils.permission import CustomPermission
|
||||
|
||||
|
||||
class RoleSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色-序列化器
|
||||
"""
|
||||
users = serializers.SerializerMethodField()
|
||||
|
||||
@staticmethod
|
||||
def get_users(instance):
|
||||
users = instance.users_set.exclude(id=1).values('id', 'name', 'dept__name')
|
||||
return users
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
@@ -101,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer):
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
|
||||
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
|
||||
"""
|
||||
角色管理接口
|
||||
@@ -116,3 +122,82 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
|
||||
create_serializer_class = RoleCreateUpdateSerializer
|
||||
update_serializer_class = RoleCreateUpdateSerializer
|
||||
search_fields = ['name', 'key']
|
||||
|
||||
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
|
||||
def set_role_users(self, request, pk):
|
||||
"""
|
||||
设置 角色-用户
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
data = request.data
|
||||
direction = data.get('direction')
|
||||
movedKeys = data.get('movedKeys')
|
||||
role = Role.objects.get(pk=pk)
|
||||
if direction == "left":
|
||||
# left : 移除用户权限
|
||||
role.users_set.remove(*movedKeys)
|
||||
else:
|
||||
# right : 添加用户权限
|
||||
role.users_set.add(*movedKeys)
|
||||
serializer = RoleSerializer(role)
|
||||
return DetailResponse(data=serializer.data, msg="更新成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated, CustomPermission])
|
||||
def get_role_users(self, request):
|
||||
"""
|
||||
获取角色已授权、未授权的用户
|
||||
已授权的用户:1
|
||||
未授权的用户:0
|
||||
"""
|
||||
role_id = request.query_params.get('role_id', None)
|
||||
|
||||
if not role_id:
|
||||
return ErrorResponse(msg="请选择角色")
|
||||
|
||||
if request.query_params.get('authorized', 0) == "1":
|
||||
queryset = Users.objects.filter(role__id=role_id).exclude(is_superuser=True)
|
||||
else:
|
||||
queryset = Users.objects.exclude(role__id=role_id).exclude(is_superuser=True)
|
||||
|
||||
if name := request.query_params.get('name', None):
|
||||
queryset = queryset.filter(name__icontains=name)
|
||||
|
||||
if dept := request.query_params.get('dept', None):
|
||||
queryset = queryset.filter(dept=dept)
|
||||
|
||||
page = self.paginate_queryset(queryset.values('id', 'name', 'dept__name'))
|
||||
if page is not None:
|
||||
return self.get_paginated_response(page)
|
||||
|
||||
return SuccessResponse(data=page)
|
||||
|
||||
@action(methods=['DELETE'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
|
||||
def remove_role_user(self, request, pk):
|
||||
"""
|
||||
角色-删除用户
|
||||
"""
|
||||
user_id = request.data.get('user_id', None)
|
||||
|
||||
if not user_id:
|
||||
return ErrorResponse(msg="请选择用户")
|
||||
|
||||
role = self.get_object()
|
||||
role.users_set.remove(*user_id)
|
||||
|
||||
return SuccessResponse(msg="删除成功")
|
||||
|
||||
@action(methods=['POST'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
|
||||
def add_role_users(self, request, pk):
|
||||
"""
|
||||
角色-添加用户
|
||||
"""
|
||||
users_id = request.data.get('users_id', None)
|
||||
|
||||
if not users_id:
|
||||
return ErrorResponse(msg="请选择用户")
|
||||
|
||||
role = self.get_object()
|
||||
role.users_set.add(*users_id)
|
||||
|
||||
return DetailResponse(msg="添加成功")
|
||||
|
||||
@@ -6,33 +6,31 @@
|
||||
@Created on: 2021/6/3 003 0:30
|
||||
@Remark: 菜单按钮管理
|
||||
"""
|
||||
from django.db.models import F, Subquery, OuterRef, Exists
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission, FieldPermission, \
|
||||
MenuField
|
||||
from dvadmin.system.views.menu import MenuSerializer
|
||||
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
|
||||
from dvadmin.system.models import RoleMenuButtonPermission, Menu, Dept, MenuButton, RoleMenuPermission, \
|
||||
MenuField, FieldPermission
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
|
||||
class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单按钮-序列化器
|
||||
角色-菜单-按钮-权限 查询序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuButtonPermission
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
|
||||
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化菜单按钮-序列化器
|
||||
角色-菜单-按钮-权限 创建/修改序列化
|
||||
"""
|
||||
menu_button__name = serializers.CharField(source='menu_button.name', read_only=True)
|
||||
menu_button__value = serializers.CharField(source='menu_button.value', read_only=True)
|
||||
@@ -43,107 +41,108 @@ class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class RoleButtonPermissionSerializer(CustomModelSerializer):
|
||||
class RoleMenuSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色按钮权限
|
||||
角色-菜单 序列化
|
||||
"""
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
data_range = serializers.SerializerMethodField()
|
||||
|
||||
def get_isCheck(self, instance):
|
||||
params = self.request.query_params
|
||||
data = self.request.data
|
||||
return RoleMenuPermission.objects.filter(
|
||||
menu_id=instance.id,
|
||||
role_id=params.get('roleId', data.get('roleId')),
|
||||
).exists()
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ["id", "name", "parent", "is_catalog", "isCheck"]
|
||||
|
||||
|
||||
class RoleMenuButtonSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色-菜单-按钮 序列化
|
||||
"""
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
data_range = serializers.SerializerMethodField()
|
||||
role_menu_btn_perm_id = serializers.SerializerMethodField()
|
||||
dept = serializers.SerializerMethodField()
|
||||
|
||||
def get_isCheck(self, instance):
|
||||
params = self.request.query_params
|
||||
data = self.request.data
|
||||
return RoleMenuButtonPermission.objects.filter(
|
||||
menu_button__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
menu_button_id=instance.id,
|
||||
role_id=params.get('roleId', data.get('roleId')),
|
||||
).exists()
|
||||
|
||||
def get_data_range(self, instance):
|
||||
params = self.request.query_params
|
||||
obj = RoleMenuButtonPermission.objects.filter(
|
||||
menu_button__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
).first()
|
||||
obj = self.get_role_menu_btn_prem(instance)
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.data_range
|
||||
|
||||
def get_role_menu_btn_perm_id(self, instance):
|
||||
obj = self.get_role_menu_btn_prem(instance)
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.id
|
||||
|
||||
def get_dept(self, instance):
|
||||
obj = self.get_role_menu_btn_prem(instance)
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.dept.all().values_list('id', flat=True)
|
||||
|
||||
def get_role_menu_btn_prem(self, instance):
|
||||
params = self.request.query_params
|
||||
data = self.request.data
|
||||
obj = RoleMenuButtonPermission.objects.filter(
|
||||
menu_button_id=instance.id,
|
||||
role_id=params.get('roleId', data.get('roleId')),
|
||||
).first()
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
model = MenuButton
|
||||
fields = ['id','name','value','isCheck','data_range']
|
||||
fields = ['id', 'menu', 'name', 'isCheck', 'data_range', 'role_menu_btn_perm_id', 'dept']
|
||||
|
||||
class RoleFieldPermissionSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = FieldPermission
|
||||
fields = "__all__"
|
||||
|
||||
class RoleMenuFieldSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色-菜单-字段 序列化
|
||||
"""
|
||||
is_query = serializers.SerializerMethodField()
|
||||
is_create = serializers.SerializerMethodField()
|
||||
is_update = serializers.SerializerMethodField()
|
||||
|
||||
def get_is_query(self, instance):
|
||||
params = self.request.query_params
|
||||
queryset = instance.menu_field.filter(role=params.get('role')).first()
|
||||
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
|
||||
if queryset:
|
||||
return queryset.is_query
|
||||
return False
|
||||
|
||||
def get_is_create(self, instance):
|
||||
params = self.request.query_params
|
||||
queryset = instance.menu_field.filter(role=params.get('role')).first()
|
||||
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
|
||||
if queryset:
|
||||
return queryset.is_create
|
||||
return False
|
||||
|
||||
def get_is_update(self, instance):
|
||||
params = self.request.query_params
|
||||
queryset = instance.menu_field.filter(role=params.get('role')).first()
|
||||
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
|
||||
if queryset:
|
||||
return queryset.is_update
|
||||
return False
|
||||
|
||||
class Meta:
|
||||
model = MenuField
|
||||
fields = ['id','field_name','title','is_query','is_create','is_update']
|
||||
fields = ['id', 'field_name', 'title', 'is_query', 'is_create', 'is_update']
|
||||
|
||||
|
||||
class RoleMenuPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单和按钮权限
|
||||
"""
|
||||
name = serializers.SerializerMethodField()
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
btns = serializers.SerializerMethodField()
|
||||
columns = serializers.SerializerMethodField()
|
||||
|
||||
def get_name(self, instance):
|
||||
parent_list = Menu.get_all_parent(instance['id'])
|
||||
names = [d["name"] for d in parent_list]
|
||||
return "/".join(names)
|
||||
def get_isCheck(self, instance):
|
||||
params = self.request.query_params
|
||||
return RoleMenuPermission.objects.filter(
|
||||
menu__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
).exists()
|
||||
|
||||
def get_btns(self, instance):
|
||||
btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value')
|
||||
serializer = RoleButtonPermissionSerializer(btn_list,many=True,request=self.request)
|
||||
return serializer.data
|
||||
|
||||
def get_columns(self, instance):
|
||||
col_list = MenuField.objects.filter(menu=instance['id'])
|
||||
serializer = RoleMenuFieldSerializer(col_list,many=True,request=self.request)
|
||||
return serializer.data
|
||||
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ['id','name','isCheck','btns','columns']
|
||||
|
||||
class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
"""
|
||||
菜单按钮接口
|
||||
@@ -160,199 +159,110 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
extra_filter_class = []
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def get_role_premission(self, request):
|
||||
def get_role_menu(self, request):
|
||||
"""
|
||||
角色授权获取:
|
||||
:param request: role
|
||||
:return: menu,btns,columns
|
||||
获取 角色-菜单
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
menu_queryset = Menu.objects.all()
|
||||
serializer = RoleMenuSerializer(menu_queryset, many=True, request=request)
|
||||
return DetailResponse(data=serializer.data)
|
||||
|
||||
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def set_role_menu(self, request):
|
||||
"""
|
||||
设置 角色-菜单
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
data = request.data
|
||||
roleId = data.get('roleId')
|
||||
menuId = data.get('menuId')
|
||||
isCheck = data.get('isCheck')
|
||||
if isCheck:
|
||||
# 添加权限:创建关联记录
|
||||
instance = RoleMenuPermission.objects.create(role_id=roleId, menu_id=menuId)
|
||||
else:
|
||||
# 删除权限:移除关联记录
|
||||
RoleMenuPermission.objects.filter(role_id=roleId, menu_id=menuId).delete()
|
||||
menu_instance = Menu.objects.get(id=menuId)
|
||||
serializer = RoleMenuSerializer(menu_instance, request=request)
|
||||
return DetailResponse(data=serializer.data, msg="更新成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def get_role_menu_btn_field(self, request):
|
||||
"""
|
||||
获取 角色-菜单-按钮-列字段
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
role = params.get('role',None)
|
||||
if role is None:
|
||||
return ErrorResponse(msg="未获取到角色信息")
|
||||
is_superuser = request.user.is_superuser
|
||||
# if is_superuser:
|
||||
# queryset = Menu.objects.filter(status=1,is_catalog=False).values('name', 'id').all()
|
||||
# else:
|
||||
# role_id = request.user.role.values_list('id', flat=True)
|
||||
# menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id',flat=True)
|
||||
# queryset = Menu.objects.filter(status=1, is_catalog=False,id__in=menu_list).values('name', 'id').all()
|
||||
# serializer = RoleMenuPermissionSerializer(queryset,many=True,request=request)
|
||||
# data = serializer.data
|
||||
# return DetailResponse(data=data)
|
||||
data = []
|
||||
if is_superuser:
|
||||
queryset = Menu.objects.filter(status=1,is_catalog=False).values('name', 'id').all()
|
||||
else:
|
||||
role_id = request.user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id',flat=True)
|
||||
queryset = Menu.objects.filter(status=1, is_catalog=False,id__in=menu_list).values('name', 'id')
|
||||
for item in queryset:
|
||||
parent_list = Menu.get_all_parent(item['id'])
|
||||
names = [d["name"] for d in parent_list]
|
||||
completeName = "/".join(names)
|
||||
isCheck = RoleMenuPermission.objects.filter(
|
||||
menu__id=item['id'],
|
||||
role__id=role,
|
||||
).exists()
|
||||
mbCheck = RoleMenuButtonPermission.objects.filter(
|
||||
menu_button=OuterRef("pk"),
|
||||
role__id=role,
|
||||
)
|
||||
btns = MenuButton.objects.filter(
|
||||
menu__id=item['id'],
|
||||
).annotate(isCheck=Exists(mbCheck)).values('id', 'name', 'value', 'isCheck',
|
||||
data_range=F('menu_button_permission__data_range'))
|
||||
dicts = {
|
||||
'name': completeName,
|
||||
'id': item['id'],
|
||||
'isCheck': isCheck,
|
||||
'btns': btns,
|
||||
|
||||
}
|
||||
data.append(dicts)
|
||||
return DetailResponse(data=data)
|
||||
menuId = params.get('menuId', None)
|
||||
menu_btn_queryset = MenuButton.objects.filter(menu_id=menuId)
|
||||
menu_btn_serializer = RoleMenuButtonSerializer(menu_btn_queryset, many=True, request=request)
|
||||
menu_field_queryset = MenuField.objects.filter(menu_id=menuId)
|
||||
menu_field_serializer = RoleMenuFieldSerializer(menu_field_queryset, many=True, request=request)
|
||||
return DetailResponse(data={'menu_btn': menu_btn_serializer.data, 'menu_field': menu_field_serializer.data})
|
||||
|
||||
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
|
||||
def set_role_premission(self,request,pk):
|
||||
def set_role_menu_field(self, request, pk):
|
||||
"""
|
||||
对角色的菜单和按钮及按钮范围授权:
|
||||
:param request:
|
||||
:param pk: role
|
||||
:return:
|
||||
设置 角色-菜单-列字段
|
||||
"""
|
||||
body = request.data
|
||||
RoleMenuPermission.objects.filter(role=pk).delete()
|
||||
RoleMenuButtonPermission.objects.filter(role=pk).delete()
|
||||
for menu in body:
|
||||
if menu.get('isCheck'):
|
||||
menu_parent = Menu.get_all_parent(menu.get('id'))
|
||||
role_menu_permission_list = []
|
||||
for d in menu_parent:
|
||||
role_menu_permission_list.append(RoleMenuPermission(role_id=pk, menu_id=d["id"]))
|
||||
RoleMenuPermission.objects.bulk_create(role_menu_permission_list)
|
||||
# RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id'))
|
||||
for btn in menu.get('btns'):
|
||||
if btn.get('isCheck'):
|
||||
data_range = btn.get('data_range',0) or 0
|
||||
instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),data_range=data_range)
|
||||
instance.dept.set(btn.get('dept',[]))
|
||||
for col in menu.get('columns'):
|
||||
FieldPermission.objects.update_or_create(role_id=pk,field_id=col.get('id'),is_query=col.get('is_query'),is_create=col.get('is_create'),is_update=col.get('is_update'))
|
||||
return DetailResponse(msg="授权成功")
|
||||
data = request.data
|
||||
for col in data:
|
||||
FieldPermission.objects.update_or_create(
|
||||
role_id=pk, field_id=col.get('id'),
|
||||
defaults={
|
||||
'is_create': col.get('is_create'),
|
||||
'is_update': col.get('is_update'),
|
||||
'is_query': col.get('is_query'),
|
||||
})
|
||||
|
||||
return DetailResponse(data=[], msg="更新成功")
|
||||
|
||||
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def set_role_menu_btn(self, request):
|
||||
"""
|
||||
设置 角色-菜单-按钮
|
||||
"""
|
||||
data = request.data
|
||||
isCheck = data.get('isCheck', None)
|
||||
roleId = data.get('roleId', None)
|
||||
btnId = data.get('btnId', None)
|
||||
data_range = data.get('data_range', None) or 0 # 默认仅本人权限
|
||||
dept = data.get('dept', None) or [] # 默认空部门
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_menu_get_button(self, request):
|
||||
"""
|
||||
当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
if params := request.query_params:
|
||||
if menu_id := params.get('menu', None):
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
queryset = MenuButton.objects.filter(menu=menu_id).values('id', 'name')
|
||||
else:
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_list, menu_button__menu=menu_id
|
||||
).values(btn_id=F('menu_button__id'), name=F('menu_button__name'))
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="参数错误")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def data_scope(self, request):
|
||||
"""
|
||||
获取数据权限范围:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
data = [
|
||||
{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"label": '本部门及以下数据权限'
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": '本部门数据权限'
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"label": '全部数据权限'
|
||||
},
|
||||
{
|
||||
"value": 4,
|
||||
"label": '自定义数据权限'
|
||||
}
|
||||
]
|
||||
return DetailResponse(data=data)
|
||||
if isCheck:
|
||||
# 添加权限:创建关联记录
|
||||
instance = RoleMenuButtonPermission.objects.create(role_id=roleId,
|
||||
menu_button_id=btnId,
|
||||
data_range=data_range)
|
||||
# 自定义部门权限
|
||||
if data_range == 4 and dept:
|
||||
instance.dept.set(dept)
|
||||
else:
|
||||
data = []
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
if params := request.query_params:
|
||||
if menu_button_id := params.get('menu_button', None):
|
||||
role_queryset = RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_list, menu_button__id=menu_button_id
|
||||
).values_list('data_range', flat=True)
|
||||
data_range_list = list(set(role_queryset))
|
||||
for item in data_range_list:
|
||||
if item == 0:
|
||||
data = [{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
}]
|
||||
elif item == 1:
|
||||
data = [{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
}, {
|
||||
"value": 1,
|
||||
"label": '本部门及以下数据权限'
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": '本部门数据权限'
|
||||
}]
|
||||
elif item == 2:
|
||||
data = [{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": '本部门数据权限'
|
||||
}]
|
||||
elif item == 3:
|
||||
data = [{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"label": '全部数据权限'
|
||||
}, ]
|
||||
elif item == 4:
|
||||
data = [{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
},
|
||||
{
|
||||
"value": 4,
|
||||
"label": '自定义数据权限'
|
||||
}]
|
||||
else:
|
||||
data = []
|
||||
return DetailResponse(data=data)
|
||||
return ErrorResponse(msg="参数错误")
|
||||
# 删除权限:移除关联记录
|
||||
RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete()
|
||||
menu_btn_instance = MenuButton.objects.get(id=btnId)
|
||||
serializer = RoleMenuButtonSerializer(menu_btn_instance, request=request)
|
||||
return DetailResponse(data=serializer.data, msg="更新成功")
|
||||
|
||||
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def set_role_menu_btn_data_range(self, request):
|
||||
"""
|
||||
设置 角色-菜单-按钮-权限
|
||||
"""
|
||||
data = request.data
|
||||
instance = RoleMenuButtonPermission.objects.get(id=data.get('role_menu_btn_perm_id'))
|
||||
instance.data_range = data.get('data_range')
|
||||
instance.dept.add(*data.get('dept'))
|
||||
if not data.get('dept'):
|
||||
instance.dept.clear()
|
||||
instance.save()
|
||||
serializer = RoleMenuButtonPermissionSerializer(instance, request=request)
|
||||
return DetailResponse(data=serializer.data, msg="更新成功")
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_to_dept_all(self, request):
|
||||
@@ -361,72 +271,20 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
queryset = Dept.objects.values('id', 'name', 'parent')
|
||||
else:
|
||||
if not params:
|
||||
return ErrorResponse(msg="参数错误")
|
||||
menu_button = params.get('menu_button')
|
||||
if menu_button is None:
|
||||
return ErrorResponse(msg="参数错误")
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list, menu_button=None).values(
|
||||
dept_id=F('dept__id'),
|
||||
name=F('dept__name'),
|
||||
parent=F('dept__parent')
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def menu_to_button(self, request):
|
||||
"""
|
||||
根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
menu_id = params.get('menu', None)
|
||||
if menu_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values(
|
||||
'id',
|
||||
'data_range',
|
||||
'menu_button',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
else:
|
||||
if params:
|
||||
# 当前登录用户的角色
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
|
||||
role_id = params.get('role', None)
|
||||
if role_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role=role_id, menu_button__menu=menu_id).values(
|
||||
'id',
|
||||
'data_range',
|
||||
'menu_button',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
menu_button_id = params.get('menu_button')
|
||||
# 当前登录用户角色可以分配的自定义部门权限
|
||||
dept_checked_disabled = RoleMenuButtonPermission.objects.filter(
|
||||
role_id__in=role_list, menu_button_id=menu_button_id
|
||||
).values_list('dept', flat=True)
|
||||
dept_list = Dept.objects.values('id', 'name', 'parent')
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_to_menu(self, request):
|
||||
"""
|
||||
获取角色对应的按钮权限
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
role_id = params.get('role', None)
|
||||
if role_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
queryset = RoleMenuPermission.objects.filter(role_id=role_id).values_list('menu_id', flat=True).distinct()
|
||||
|
||||
return DetailResponse(data=queryset)
|
||||
data = []
|
||||
for dept in dept_list:
|
||||
dept["disabled"] = False if is_superuser else dept["id"] not in dept_checked_disabled
|
||||
data.append(dept)
|
||||
return DetailResponse(data=data)
|
||||
|
||||
@@ -90,6 +90,8 @@ class UserCreateSerializer(CustomModelSerializer):
|
||||
data = super().save(**kwargs)
|
||||
data.dept_belong_id = data.dept_id
|
||||
data.save()
|
||||
if not self.validated_data.get('manage_dept', None):
|
||||
data.manage_dept.add(data.dept_id)
|
||||
data.post.set(self.initial_data.get("post", []))
|
||||
return data
|
||||
|
||||
@@ -115,19 +117,20 @@ class UserUpdateSerializer(CustomModelSerializer):
|
||||
],
|
||||
)
|
||||
|
||||
# password = serializers.CharField(required=False, allow_blank=True)
|
||||
# mobile = serializers.CharField(
|
||||
# max_length=50,
|
||||
# validators=[
|
||||
# CustomUniqueValidator(queryset=Users.objects.all(), message="手机号必须唯一")
|
||||
# ],
|
||||
# allow_blank=True
|
||||
# )
|
||||
def validate_is_active(self, value):
|
||||
"""
|
||||
更改激活状态
|
||||
"""
|
||||
if value:
|
||||
self.initial_data["login_error_count"] = 0
|
||||
return value
|
||||
|
||||
def save(self, **kwargs):
|
||||
data = super().save(**kwargs)
|
||||
data.dept_belong_id = data.dept_id
|
||||
data.save()
|
||||
if not self.validated_data.get('manage_dept', None):
|
||||
data.manage_dept.add(data.dept_id)
|
||||
data.post.set(self.initial_data.get("post", []))
|
||||
return data
|
||||
|
||||
@@ -287,6 +290,7 @@ class UserViewSet(CustomModelViewSet):
|
||||
"dept": user.dept_id,
|
||||
"is_superuser": user.is_superuser,
|
||||
"role": user.role.values_list('id', flat=True),
|
||||
"pwd_change_count":user.pwd_change_count
|
||||
}
|
||||
if hasattr(connection, 'tenant'):
|
||||
result['tenant_id'] = connection.tenant and connection.tenant.id
|
||||
@@ -326,22 +330,47 @@ class UserViewSet(CustomModelViewSet):
|
||||
return ErrorResponse(msg="参数不能为空")
|
||||
if new_pwd != new_pwd2:
|
||||
return ErrorResponse(msg="两次密码不匹配")
|
||||
verify_password = check_password(old_pwd, self.request.user.password)
|
||||
verify_password = check_password(old_pwd, request.user.password)
|
||||
if not verify_password:
|
||||
verify_password = check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest(), self.request.user.password)
|
||||
old_pwd_md5 = hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest()
|
||||
verify_password = check_password(str(old_pwd_md5), request.user.password)
|
||||
# 创建用户时、自定义密码无法修改问题
|
||||
if not verify_password:
|
||||
old_pwd_md5 = hashlib.md5(old_pwd_md5.encode(encoding='UTF-8')).hexdigest()
|
||||
verify_password = check_password(str(old_pwd_md5), request.user.password)
|
||||
if verify_password:
|
||||
# request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
request.user.pwd_change_count += 1
|
||||
request.user.save()
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
else:
|
||||
return ErrorResponse(msg="旧密码不正确")
|
||||
|
||||
@action(methods=["post"], detail=False, permission_classes=[IsAuthenticated])
|
||||
def login_change_password(self, request, *args, **kwargs):
|
||||
"""初次登录进行密码修改"""
|
||||
data = request.data
|
||||
new_pwd = data.get("password")
|
||||
new_pwd2 = data.get("password_regain")
|
||||
if new_pwd != new_pwd2:
|
||||
return ErrorResponse(msg="两次密码不匹配")
|
||||
else:
|
||||
request.user.password = make_password(new_pwd)
|
||||
request.user.pwd_change_count += 1
|
||||
request.user.save()
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
|
||||
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
|
||||
def reset_to_default_password(self, request, *args, **kwargs):
|
||||
def reset_to_default_password(self, request,pk):
|
||||
"""恢复默认密码"""
|
||||
instance = Users.objects.filter(id=kwargs.get("pk")).first()
|
||||
if not self.request.user.is_superuser:
|
||||
return ErrorResponse(msg="只允许超级管理员对其进行密码重置")
|
||||
instance = Users.objects.filter(id=pk).first()
|
||||
if instance:
|
||||
instance.set_password(dispatch.get_system_config_values("base.default_password"))
|
||||
default_password = dispatch.get_system_config_values("base.default_password")
|
||||
md5_pwd = hashlib.md5(default_password.encode(encoding='UTF-8')).hexdigest()
|
||||
instance.password = make_password(md5_pwd)
|
||||
instance.save()
|
||||
return DetailResponse(data=None, msg="密码重置成功")
|
||||
else:
|
||||
|
||||
0
backend/dvadmin/utils/__init__.py
Normal file
0
backend/dvadmin/utils/__init__.py
Normal file
62
backend/dvadmin/utils/aliyunoss.py
Normal file
62
backend/dvadmin/utils/aliyunoss.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import oss2
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from application import dispatch
|
||||
|
||||
|
||||
# 进度条
|
||||
# 当无法确定待上传的数据长度时,total_bytes的值为None。
|
||||
def percentage(consumed_bytes, total_bytes):
|
||||
if total_bytes:
|
||||
rate = int(100 * (float(consumed_bytes) / float(total_bytes)))
|
||||
print('\r{0}% '.format(rate), end='')
|
||||
|
||||
|
||||
def ali_oss_upload(file, file_name):
|
||||
"""
|
||||
阿里云OSS上传
|
||||
"""
|
||||
try:
|
||||
file.seek(0)
|
||||
file_read = file.read()
|
||||
except Exception as e:
|
||||
file_read = file
|
||||
if not file:
|
||||
raise ValidationError('请上传文件')
|
||||
# 转存到oss
|
||||
path_prefix = dispatch.get_system_config_values("file_storage.aliyun_path")
|
||||
if not path_prefix.endswith('/'):
|
||||
path_prefix = path_prefix + '/'
|
||||
if path_prefix.startswith('/'):
|
||||
path_prefix = path_prefix[1:]
|
||||
base_fil_name = f'{path_prefix}{file_name}'
|
||||
# 获取OSS配置
|
||||
# 获取的AccessKey
|
||||
access_key_id = dispatch.get_system_config_values("file_storage.aliyun_access_key")
|
||||
access_key_secret = dispatch.get_system_config_values("file_storage.aliyun_access_secret")
|
||||
auth = oss2.Auth(access_key_id, access_key_secret)
|
||||
# 这个是需要用特定的地址,不同地域的服务器地址不同,不要弄错了
|
||||
# 参考官网给的地址配置https://www.alibabacloud.com/help/zh/object-storage-service/latest/regions-and-endpoints#concept-zt4-cvy-5db
|
||||
endpoint = dispatch.get_system_config_values("file_storage.aliyun_endpoint")
|
||||
bucket_name = dispatch.get_system_config_values("file_storage.aliyun_bucket")
|
||||
if bucket_name.endswith(endpoint):
|
||||
bucket_name = bucket_name.replace(f'.{endpoint}', '')
|
||||
# 你的项目名称,类似于不同的项目上传的图片前缀url不同
|
||||
bucket = oss2.Bucket(auth, endpoint, bucket_name) # 项目名称
|
||||
# 生成外网访问的文件路径
|
||||
aliyun_cdn_url = dispatch.get_system_config_values("file_storage.aliyun_cdn_url")
|
||||
if aliyun_cdn_url:
|
||||
if aliyun_cdn_url.endswith('/'):
|
||||
aliyun_cdn_url = aliyun_cdn_url[1:]
|
||||
file_path = f"{aliyun_cdn_url}/{base_fil_name}"
|
||||
else:
|
||||
file_path = f"https://{bucket_name}.{endpoint}/{base_fil_name}"
|
||||
# 这个是阿里提供的SDK方法
|
||||
res = bucket.put_object(base_fil_name, file_read, progress_callback=percentage)
|
||||
# 如果上传状态是200 代表成功 返回文件外网访问路径
|
||||
if res.status == 200:
|
||||
return file_path
|
||||
else:
|
||||
return None
|
||||
@@ -5,34 +5,39 @@ from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import FieldPermission, MenuField
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.models import get_custom_app_models
|
||||
|
||||
|
||||
def merge_permission(data):
|
||||
"""
|
||||
合并权限
|
||||
"""
|
||||
result = {}
|
||||
for item in data:
|
||||
field_name = item.pop('field_name')
|
||||
if field_name not in result:
|
||||
result[field_name] = item
|
||||
else:
|
||||
for key, value in item.items():
|
||||
result[field_name][key] = result[field_name][key] or value
|
||||
return result
|
||||
|
||||
|
||||
class FieldPermissionMixin:
|
||||
@action(methods=['get'], detail=False,permission_classes=[IsAuthenticated])
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def field_permission(self, request):
|
||||
"""
|
||||
获取字段权限
|
||||
"""
|
||||
finded = False
|
||||
for model in get_custom_app_models():
|
||||
if model['object'] is self.serializer_class.Meta.model:
|
||||
finded = True
|
||||
break
|
||||
if finded:
|
||||
break
|
||||
if finded is False:
|
||||
return []
|
||||
model = self.serializer_class.Meta.model.__name__
|
||||
user = request.user
|
||||
if user.is_superuser==1:
|
||||
data = MenuField.objects.filter( model=model['model']).values('field_name')
|
||||
for item in data:
|
||||
item['is_create'] = True
|
||||
item['is_query'] = True
|
||||
item['is_update'] = True
|
||||
# 创建一个默认字典来存储最终的结果
|
||||
if user.is_superuser == 1:
|
||||
data = MenuField.objects.filter(model=model).values('field_name')
|
||||
result = {item['field_name']: {"is_create": True, "is_query": True, "is_update": True} for item in data}
|
||||
else:
|
||||
roles = request.user.role.values_list('id', flat=True)
|
||||
data= FieldPermission.objects.filter(
|
||||
field__model=model['model'],role__in=roles
|
||||
).values( 'is_create', 'is_query', 'is_update',field_name=F('field__field_name'))
|
||||
return DetailResponse(data=data)
|
||||
data = FieldPermission.objects.filter(
|
||||
field__model=model, role__in=roles
|
||||
).values('is_create', 'is_query', 'is_update', field_name=F('field__field_name'))
|
||||
result = merge_permission(data)
|
||||
return DetailResponse(data=result)
|
||||
|
||||
@@ -15,15 +15,16 @@ import six
|
||||
from django.db import models
|
||||
from django.db.models import Q, F
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django_filters import utils, FilterSet
|
||||
from django_filters.constants import ALL_FIELDS
|
||||
from django_filters.filters import CharFilter, DateTimeFromToRangeFilter
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters.utils import get_model_field
|
||||
from django_filters.utils import get_model_field, translate_validation, deprecate
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
from django_filters.conf import settings
|
||||
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission
|
||||
from dvadmin.utils.models import CoreModel
|
||||
|
||||
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton, Users
|
||||
from util.currency import recursion_down_fast
|
||||
|
||||
|
||||
class CoreModelFilterBankend(BaseFilterBackend):
|
||||
"""
|
||||
@@ -33,15 +34,15 @@ class CoreModelFilterBankend(BaseFilterBackend):
|
||||
create_datetime_after = request.query_params.get('create_datetime_after', None)
|
||||
create_datetime_before = request.query_params.get('create_datetime_before', None)
|
||||
update_datetime_after = request.query_params.get('update_datetime_after', None)
|
||||
update_datetime_before = request.query_params.get('update_datetime_after', None)
|
||||
update_datetime_before = request.query_params.get('update_datetime_before', None)
|
||||
if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
|
||||
create_filter = Q()
|
||||
if create_datetime_after and create_datetime_before:
|
||||
create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=create_datetime_before)
|
||||
create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=f'{create_datetime_before} 23:59:59')
|
||||
elif create_datetime_after:
|
||||
create_filter &= Q(create_datetime__gte=create_datetime_after)
|
||||
elif create_datetime_before:
|
||||
create_filter &= Q(create_datetime__lte=create_datetime_before)
|
||||
create_filter &= Q(create_datetime__lte=f'{create_datetime_before} 23:59:59')
|
||||
|
||||
# 更新时间范围过滤条件
|
||||
update_filter = Q()
|
||||
@@ -149,13 +150,16 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
if _pk: # 判断是否是单例查询
|
||||
re_api = re.sub(_pk,'{id}', api)
|
||||
role_id_list = request.user.role.values_list('id', flat=True)
|
||||
role_permission_list=RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_id_list,
|
||||
role__status=1,
|
||||
menu_button__api=re_api,
|
||||
menu_button__method=method).values(
|
||||
'data_range'
|
||||
)
|
||||
# 修复权限获取bug
|
||||
menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True)
|
||||
role_permission_list = []
|
||||
if menu_button_ids:
|
||||
role_permission_list=RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_id_list,
|
||||
role__status=1,
|
||||
menu_button_id__in=menu_button_ids).values(
|
||||
'data_range'
|
||||
)
|
||||
dataScope_list = [] # 权限范围列表
|
||||
for ele in role_permission_list:
|
||||
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
|
||||
@@ -197,6 +201,86 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
return queryset.filter(dept_belong_id__in=list(set(dept_list)))
|
||||
|
||||
|
||||
class DataLevelPermissionsSubFilter(DataLevelPermissionsFilter):
|
||||
"""数据级权限过滤的子过滤器,过滤管理部门字段manage_dept"""
|
||||
|
||||
def _extracted_from_filter_queryset_33(self, request:Request, queryset, api, method):
|
||||
u:Users = request.user
|
||||
if u.is_superuser:
|
||||
return queryset
|
||||
# (0, "仅本人数据权限"),
|
||||
# (1, "本部门及以下数据权限"),
|
||||
# (2, "本部门数据权限"),
|
||||
# (3, "全部数据权限"),
|
||||
# (4, "自定数据权限")
|
||||
re_api = api
|
||||
_pk = request.parser_context["kwargs"].get('pk')
|
||||
if _pk: # 判断是否是单例查询
|
||||
re_api = re.sub(_pk,'{id}', api)
|
||||
role_id_list = request.user.role.values_list('id', flat=True)
|
||||
menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True)
|
||||
role_permission_list = []
|
||||
if menu_button_ids:
|
||||
role_permission_list=RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_id_list,
|
||||
role__status=1,
|
||||
menu_button_id__in=menu_button_ids).values(
|
||||
'data_range'
|
||||
)
|
||||
dataScope_list = [] # 权限范围列表
|
||||
for ele in role_permission_list:
|
||||
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
|
||||
if ele.get("data_range") == 3:
|
||||
return queryset
|
||||
dataScope_list.append(ele.get("data_range"))
|
||||
dataScope_list = list(set(dataScope_list))
|
||||
# 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据)
|
||||
if 0 in dataScope_list:
|
||||
return queryset.filter(
|
||||
creator=request.user, dept_belong_id=u.dept_id
|
||||
)
|
||||
dept_list = []
|
||||
# 5. 自定数据权限 获取部门,根据部门过滤
|
||||
for ele in dataScope_list:
|
||||
if ele == 1:
|
||||
dept_list.append(u.dept_id)
|
||||
dept_list.extend(
|
||||
get_dept(
|
||||
u.dept_id,
|
||||
)
|
||||
)
|
||||
elif ele == 2:
|
||||
dept_list.append(u.dept_id)
|
||||
elif ele == 4:
|
||||
dept_ids = RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_id_list,
|
||||
role__status=1,
|
||||
data_range=4).values_list(
|
||||
'dept__id',flat=True
|
||||
)
|
||||
dept_list.extend(
|
||||
dept_ids
|
||||
)
|
||||
# 自己部门 交 管理部门
|
||||
if u.manage_dept.exists(): # 兼容旧数据
|
||||
for dept in u.manage_dept.all():
|
||||
dept_list.extend(recursion_down_fast(dept, 'parent', 'id'))
|
||||
else:
|
||||
dept_list = recursion_down_fast(u.dept, 'parent', 'id')
|
||||
dept_list = set(recursion_down_fast(u.dept)) & set(dept_list)
|
||||
# 自己创建的数据要能看到
|
||||
# 应对归属a管b、c等情况,如果自己创建数据则是a,不显式指定自己的数据就查不到
|
||||
if queryset.model._meta.model_name == 'dept':
|
||||
return queryset.filter(Q(id__in=dept_list) | Q(creator=u))
|
||||
return queryset.filter(Q(dept_belong_id__in=dept_list) | Q(creator=u))
|
||||
|
||||
|
||||
class DataLevelPermissionMargeFilter(DataLevelPermissionsFilter):
|
||||
def _extracted_from_filter_queryset_33(self, request, queryset, api, method):
|
||||
queryset = super()._extracted_from_filter_queryset_33(request, queryset, api, method)
|
||||
return DataLevelPermissionsSubFilter._extracted_from_filter_queryset_33(self, request, queryset, api, method)
|
||||
|
||||
|
||||
class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
lookup_prefixes = {
|
||||
"^": "istartswith",
|
||||
@@ -237,14 +321,14 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
|
||||
# TODO: remove assertion in 2.1
|
||||
if filterset_class is None and hasattr(view, "filter_class"):
|
||||
utils.deprecate(
|
||||
deprecate(
|
||||
"`%s.filter_class` attribute should be renamed `filterset_class`." % view.__class__.__name__
|
||||
)
|
||||
filterset_class = getattr(view, "filter_class", None)
|
||||
|
||||
# TODO: remove assertion in 2.1
|
||||
if filterset_fields is None and hasattr(view, "filter_fields"):
|
||||
utils.deprecate(
|
||||
deprecate(
|
||||
"`%s.filter_fields` attribute should be renamed `filterset_fields`." % view.__class__.__name__
|
||||
)
|
||||
self.filter_fields = getattr(view, "filter_fields", None)
|
||||
@@ -340,7 +424,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
# 不进行 过滤的model 类
|
||||
if isinstance(field, (models.JSONField, TimeZoneField)):
|
||||
if isinstance(field, (models.JSONField, TimeZoneField, models.FileField)):
|
||||
continue
|
||||
# warn if the field doesn't exist.
|
||||
if field is None:
|
||||
@@ -424,5 +508,5 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
return queryset
|
||||
|
||||
if not filterset.is_valid() and self.raise_exception:
|
||||
raise utils.translate_validation(filterset.errors)
|
||||
raise translate_validation(filterset.errors)
|
||||
return filterset.qs
|
||||
|
||||
@@ -86,4 +86,5 @@ def import_to_data(file_url, field_data, m2m_fields=None):
|
||||
else:
|
||||
array[key] = cell_value
|
||||
tables.append(array)
|
||||
return tables
|
||||
data = [i for i in tables if len(i) != 0]
|
||||
return data
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.db import transaction
|
||||
@@ -11,8 +12,10 @@ from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
|
||||
from dvadmin.utils.import_export import import_to_data
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.request_util import get_verbose_name
|
||||
from dvadmin.system.tasks import async_export_data
|
||||
from dvadmin.system.models import DownloadCenter
|
||||
|
||||
|
||||
class ImportSerializerMixin:
|
||||
@@ -301,6 +304,16 @@ class ExportSerializerMixin:
|
||||
assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__
|
||||
assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__
|
||||
data = self.export_serializer_class(queryset, many=True, request=request).data
|
||||
try:
|
||||
async_export_data.delay(
|
||||
data,
|
||||
str(f"导出{get_verbose_name(queryset)}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx"),
|
||||
DownloadCenter.objects.create(creator=request.user, task_name=f'{get_verbose_name(queryset)}数据导出任务', dept_belong_id=request.user.dept_id).pk,
|
||||
self.export_field_label
|
||||
)
|
||||
return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载")
|
||||
except:
|
||||
pass
|
||||
# 导出excel 表
|
||||
response = HttpResponse(content_type="application/msexcel")
|
||||
response["Access-Control-Expose-Headers"] = f"Content-Disposition"
|
||||
|
||||
@@ -32,6 +32,14 @@ class ApiLoggingMiddleware(MiddlewareMixin):
|
||||
request.request_path = get_request_path(request)
|
||||
|
||||
def __handle_response(self, request, response):
|
||||
|
||||
# 判断有无log_id属性,使用All记录时,会出现此情况
|
||||
if request.request_data.get('log_id', None) is None:
|
||||
return
|
||||
|
||||
# 移除log_id,不记录此ID
|
||||
log_id = request.request_data.pop('log_id')
|
||||
|
||||
# request_data,request_ip由PermissionInterfaceMiddleware中间件中添加的属性
|
||||
body = getattr(request, 'request_data', {})
|
||||
# 请求含有password则用*替换掉(暂时先用于所有接口的password请求参数)
|
||||
@@ -60,7 +68,7 @@ class ApiLoggingMiddleware(MiddlewareMixin):
|
||||
'status': True if response.data.get('code') in [2000, ] else False,
|
||||
'json_result': {"code": response.data.get('code'), "msg": response.data.get('msg')},
|
||||
}
|
||||
operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=self.operation_log_id)
|
||||
operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=log_id)
|
||||
if not operation_log.request_modular and settings.API_MODEL_MAP.get(request.request_path, None):
|
||||
operation_log.request_modular = settings.API_MODEL_MAP[request.request_path]
|
||||
operation_log.save()
|
||||
@@ -71,7 +79,8 @@ class ApiLoggingMiddleware(MiddlewareMixin):
|
||||
if self.methods == 'ALL' or request.method in self.methods:
|
||||
log = OperationLog(request_modular=get_verbose_name(view_func.cls.queryset))
|
||||
log.save()
|
||||
self.operation_log_id = log.id
|
||||
# self.operation_log_id = log.id
|
||||
request.request_data['log_id'] = log.id
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
@Created on: 2021/5/31 031 22:08
|
||||
@Remark: 公共基础model类
|
||||
"""
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
from application import settings
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from rest_framework.request import Request
|
||||
|
||||
table_prefix = settings.TABLE_PREFIX # 数据库表名前缀
|
||||
|
||||
@@ -60,9 +61,45 @@ class SoftDeleteModel(models.Model):
|
||||
"""
|
||||
重写删除方法,直接开启软删除
|
||||
"""
|
||||
self.is_deleted = True
|
||||
self.save(using=using)
|
||||
if soft_delete:
|
||||
self.is_deleted = True
|
||||
self.save(using=using)
|
||||
# 级联软删除关联对象
|
||||
for related_object in self._meta.related_objects:
|
||||
related_model = getattr(self, related_object.get_accessor_name())
|
||||
# 处理一对多和多对多的关联对象
|
||||
if related_object.one_to_many or related_object.many_to_many:
|
||||
related_objects = related_model.all()
|
||||
elif related_object.one_to_one:
|
||||
related_objects = [related_model]
|
||||
else:
|
||||
continue
|
||||
|
||||
for obj in related_objects:
|
||||
obj.delete(soft_delete=True)
|
||||
else:
|
||||
super().delete(using=using, *args, **kwargs)
|
||||
|
||||
|
||||
class CoreModelManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
is_deleted = getattr(self.model, 'is_soft_delete', False)
|
||||
flow_work_status = getattr(self.model, 'flow_work_status', False)
|
||||
queryset = super().get_queryset()
|
||||
if flow_work_status:
|
||||
queryset = queryset.filter(flow_work_status=1)
|
||||
if is_deleted:
|
||||
queryset = queryset.filter(is_deleted=False)
|
||||
return queryset
|
||||
def create(self,request: Request=None, **kwargs):
|
||||
data = {**kwargs}
|
||||
if request:
|
||||
request_user = request.user
|
||||
data["creator"] = request_user
|
||||
data["modifier"] = request_user.id
|
||||
data["dept_belong_id"] = request_user.dept_id
|
||||
# 调用父类的create方法执行实际的创建操作
|
||||
return super().create(**data)
|
||||
|
||||
class CoreModel(models.Model):
|
||||
"""
|
||||
@@ -81,12 +118,118 @@ class CoreModel(models.Model):
|
||||
verbose_name="修改时间")
|
||||
create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
|
||||
verbose_name="创建时间")
|
||||
|
||||
objects = CoreModelManager()
|
||||
all_objects = models.Manager()
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = '核心模型'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def get_request_user(self, request: Request):
|
||||
if getattr(request, "user", None):
|
||||
return request.user
|
||||
return None
|
||||
|
||||
def get_request_user_id(self, request: Request):
|
||||
if getattr(request, "user", None):
|
||||
return getattr(request.user, "id", None)
|
||||
return None
|
||||
|
||||
def get_request_user_name(self, request: Request):
|
||||
if getattr(request, "user", None):
|
||||
return getattr(request.user, "name", None)
|
||||
return None
|
||||
|
||||
def get_request_user_username(self, request: Request):
|
||||
if getattr(request, "user", None):
|
||||
return getattr(request.user, "username", None)
|
||||
return None
|
||||
|
||||
def common_insert_data(self, request: Request):
|
||||
data = {
|
||||
'create_datetime': datetime.now(),
|
||||
'creator': self.get_request_user(request)
|
||||
}
|
||||
return {**data, **self.common_update_data(request)}
|
||||
|
||||
def common_update_data(self, request: Request):
|
||||
return {
|
||||
'update_datetime': datetime.now(),
|
||||
'modifier': self.get_request_user_username(request)
|
||||
}
|
||||
|
||||
exclude_fields = [
|
||||
'_state',
|
||||
'pk',
|
||||
'id',
|
||||
'create_datetime',
|
||||
'update_datetime',
|
||||
'creator',
|
||||
'creator_id',
|
||||
'creator_pk',
|
||||
'creator_name',
|
||||
'modifier',
|
||||
'modifier_id',
|
||||
'modifier_pk',
|
||||
'modifier_name',
|
||||
'dept_belong_id',
|
||||
]
|
||||
|
||||
def get_exclude_fields(self):
|
||||
return self.exclude_fields
|
||||
|
||||
def get_all_fields(self):
|
||||
return self._meta.fields
|
||||
|
||||
def get_all_fields_names(self):
|
||||
return [field.name for field in self.get_all_fields()]
|
||||
|
||||
def get_need_fields_names(self):
|
||||
return [field.name for field in self.get_all_fields() if field.name not in self.exclude_fields]
|
||||
|
||||
def to_data(self):
|
||||
"""将模型转化为字典(去除不包含字段)(注意与to_dict_data区分)。
|
||||
"""
|
||||
res = {}
|
||||
for field in self.get_need_fields_names():
|
||||
field_value = getattr(self, field)
|
||||
res[field] = field_value.id if (issubclass(field_value.__class__, CoreModel)) else field_value
|
||||
return res
|
||||
|
||||
@property
|
||||
def DATA(self):
|
||||
return self.to_data()
|
||||
|
||||
def to_dict_data(self):
|
||||
"""需要导出的字段(去除不包含字段)(注意与to_data区分)
|
||||
"""
|
||||
return {field: getattr(self, field) for field in self.get_need_fields_names()}
|
||||
|
||||
@property
|
||||
def DICT_DATA(self):
|
||||
return self.to_dict_data()
|
||||
|
||||
def insert(self, request):
|
||||
"""插入模型
|
||||
"""
|
||||
assert self.pk is None, f'模型{self.__class__.__name__}还没有保存到数据中,不能手动指定ID'
|
||||
validated_data = {**self.common_insert_data(request), **self.DICT_DATA}
|
||||
return self.__class__._default_manager.create(**validated_data)
|
||||
|
||||
def update(self, request, update_data: dict[str, any] = None):
|
||||
"""更新模型
|
||||
"""
|
||||
assert isinstance(update_data, dict), 'update_data必须为字典'
|
||||
validated_data = {**self.common_insert_data(request), **update_data}
|
||||
for key, value in validated_data.items():
|
||||
# 不允许修改id,pk,uuid字段
|
||||
if key in ['id', 'pk', 'uuid']:
|
||||
continue
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
self.save()
|
||||
return self
|
||||
|
||||
|
||||
def get_all_models_objects(model_name=None):
|
||||
"""
|
||||
@@ -97,16 +240,9 @@ def get_all_models_objects(model_name=None):
|
||||
if not settings.ALL_MODELS_OBJECTS:
|
||||
all_models = apps.get_models()
|
||||
for item in list(all_models):
|
||||
table = {
|
||||
"tableName": item._meta.verbose_name,
|
||||
"table": item.__name__,
|
||||
"tableFields": []
|
||||
}
|
||||
table = {"tableName": item._meta.verbose_name, "table": item.__name__, "tableFields": []}
|
||||
for field in item._meta.fields:
|
||||
fields = {
|
||||
"title": field.verbose_name,
|
||||
"field": field.name
|
||||
}
|
||||
fields = {"title": field.verbose_name, "field": field.name}
|
||||
table['tableFields'].append(fields)
|
||||
settings.ALL_MODELS_OBJECTS.setdefault(item.__name__, {"table": table, "object": item})
|
||||
if model_name:
|
||||
@@ -117,25 +253,20 @@ def get_all_models_objects(model_name=None):
|
||||
def get_model_from_app(app_name):
|
||||
"""获取模型里的字段"""
|
||||
model_module = import_module(app_name + '.models')
|
||||
exclude_models = getattr(model_module, 'exclude_models', [])
|
||||
filter_model = [
|
||||
getattr(model_module, item) for item in dir(model_module)
|
||||
if item != 'CoreModel' and issubclass(getattr(model_module, item).__class__, models.base.ModelBase)
|
||||
value for key, value in model_module.__dict__.items()
|
||||
if key != 'CoreModel'
|
||||
and isinstance(value, type)
|
||||
and issubclass(value, models.Model)
|
||||
and key not in exclude_models
|
||||
]
|
||||
model_list = []
|
||||
for model in filter_model:
|
||||
if model.__name__ == 'AbstractUser':
|
||||
continue
|
||||
fields = [
|
||||
{'title': field.verbose_name, 'name': field.name, 'object': field}
|
||||
for field in model._meta.fields
|
||||
]
|
||||
model_list.append({
|
||||
'app': app_name,
|
||||
'verbose': model._meta.verbose_name,
|
||||
'model': model.__name__,
|
||||
'object': model,
|
||||
'fields': fields
|
||||
})
|
||||
fields = [{'title': field.verbose_name, 'name': field.name, 'object': field} for field in model._meta.fields]
|
||||
model_list.append({'app': app_name, 'verbose': model._meta.verbose_name, 'model': model.__name__, 'object': model, 'fields': fields})
|
||||
return model_list
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,35 @@ class AnonymousUserPermission(BasePermission):
|
||||
return True
|
||||
|
||||
|
||||
class SuperuserPermission(BasePermission):
|
||||
"""
|
||||
超级管理员权限类
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if isinstance(request.user, AnonymousUser):
|
||||
return False
|
||||
# 判断是否是超级管理员
|
||||
if request.user.is_superuser:
|
||||
return True
|
||||
|
||||
|
||||
class AdminPermission(BasePermission):
|
||||
"""
|
||||
普通管理员权限类
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if isinstance(request.user, AnonymousUser):
|
||||
return False
|
||||
# 判断是否是超级管理员
|
||||
is_superuser = request.user.is_superuser
|
||||
# 判断是否是管理员角色
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
return True
|
||||
|
||||
|
||||
def ReUUID(api):
|
||||
"""
|
||||
将接口的uuid替换掉
|
||||
@@ -81,8 +110,9 @@ class CustomPermission(BasePermission):
|
||||
# ********#
|
||||
if not hasattr(request.user, "role"):
|
||||
return False
|
||||
role_id_list = request.user.role.values_list('id',flat=True)
|
||||
userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values(permission__api=F('menu_button__api'), permission__method=F('menu_button__method')) # 获取当前用户的角色拥有的所有接口
|
||||
role_id_list = request.user.role.values_list('id', flat=True)
|
||||
userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values(
|
||||
permission__api=F('menu_button__api'), permission__method=F('menu_button__method')) # 获取当前用户的角色拥有的所有接口
|
||||
ApiList = [
|
||||
str(item.get('permission__api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str(
|
||||
item.get('permission__method')) + '$' for item in userApiList if item.get('permission__api')]
|
||||
|
||||
@@ -26,7 +26,6 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
|
||||
# 修改人的审计字段名称, 默认modifier, 继承使用时可自定义覆盖
|
||||
modifier_field_id = "modifier"
|
||||
modifier_name = serializers.SerializerMethodField(read_only=True)
|
||||
dept_belong_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
def get_modifier_name(self, instance):
|
||||
if not hasattr(instance, "modifier"):
|
||||
@@ -52,7 +51,7 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
|
||||
format="%Y-%m-%d %H:%M:%S", required=False, read_only=True
|
||||
)
|
||||
update_datetime = serializers.DateTimeField(
|
||||
format="%Y-%m-%d %H:%M:%S", required=False
|
||||
format="%Y-%m-%d %H:%M:%S", required=False, read_only=True
|
||||
)
|
||||
|
||||
def __init__(self, instance=None, data=empty, request=None, **kwargs):
|
||||
@@ -71,11 +70,11 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
|
||||
validated_data[self.creator_field_id] = self.request.user
|
||||
|
||||
if (
|
||||
self.dept_belong_id_field_name in self.fields.fields
|
||||
and validated_data.get(self.dept_belong_id_field_name, None) is None
|
||||
self.dept_belong_id_field_name in self.fields.fields
|
||||
and validated_data.get(self.dept_belong_id_field_name, None) is None
|
||||
):
|
||||
validated_data[self.dept_belong_id_field_name] = getattr(
|
||||
self.request.user, "dept_id", None
|
||||
self.request.user, "dept_id", validated_data.get(self.dept_belong_id_field_name, None)
|
||||
)
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
56
backend/dvadmin/utils/tencentcos.py
Normal file
56
backend/dvadmin/utils/tencentcos.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from application import dispatch
|
||||
from qcloud_cos import CosConfig
|
||||
from qcloud_cos import CosS3Client
|
||||
|
||||
|
||||
# 进度条
|
||||
# 当无法确定待上传的数据长度时,total_bytes的值为None。
|
||||
def percentage(consumed_bytes, total_bytes):
|
||||
if total_bytes:
|
||||
rate = int(100 * (float(consumed_bytes) / float(total_bytes)))
|
||||
print('\r{0}% '.format(rate), end='')
|
||||
|
||||
def tencent_cos_upload(file, file_name):
|
||||
try:
|
||||
file.seek(0)
|
||||
file_read = file.read()
|
||||
except Exception as e:
|
||||
file_read = file
|
||||
if not file:
|
||||
raise ValidationError('请上传文件')
|
||||
# 生成文件名
|
||||
path_prefix = dispatch.get_system_config_values("file_storage.tencent_path")
|
||||
if not path_prefix.endswith('/'):
|
||||
path_prefix = path_prefix + '/'
|
||||
if path_prefix.startswith('/'):
|
||||
path_prefix = path_prefix[1:]
|
||||
base_fil_name = f'{path_prefix}{file_name}'
|
||||
# 获取cos配置
|
||||
# 1. 设置用户属性, 包括 secret_id, secret_key, region等。Appid 已在 CosConfig 中移除,请在参数 Bucket 中带上 Appid。Bucket 由 BucketName-Appid 组成
|
||||
secret_id = dispatch.get_system_config_values("file_storage.tencent_secret_id") # 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
|
||||
secret_key = dispatch.get_system_config_values("file_storage.tencent_secret_key") # 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
|
||||
region = dispatch.get_system_config_values("file_storage.tencent_region") # 替换为用户的 region,已创建桶归属的 region 可以在控制台查看,https://console.cloud.tencent.com/cos5/bucket # COS 支持的所有 region 列表参见https://cloud.tencent.com/document/product/436/6224
|
||||
bucket = dispatch.get_system_config_values("file_storage.tencent_bucket") # 要访问的桶名称
|
||||
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)
|
||||
client = CosS3Client(config)
|
||||
# 访问地址
|
||||
base_file_url = f'https://{bucket}.cos.{region}.myqcloud.com'
|
||||
# 生成外网访问的文件路径
|
||||
if base_file_url.endswith('/'):
|
||||
file_path = base_file_url + base_fil_name
|
||||
else:
|
||||
file_path = f'{base_file_url}/{base_fil_name}'
|
||||
# 这个是阿里提供的SDK方法 bucket是调用的4.1中配置的变量名
|
||||
try:
|
||||
response = client.put_object(
|
||||
Bucket=bucket,
|
||||
Body=file_read,
|
||||
Key=base_fil_name,
|
||||
EnableMD5=False
|
||||
)
|
||||
return file_path
|
||||
except:
|
||||
return None
|
||||
@@ -6,6 +6,8 @@
|
||||
@Created on: 2021/6/1 001 22:57
|
||||
@Remark: 自定义视图集
|
||||
"""
|
||||
import copy
|
||||
|
||||
from django.db import transaction
|
||||
from django_filters import DateTimeFromToRangeFilter
|
||||
from django_filters.rest_framework import FilterSet
|
||||
@@ -14,7 +16,7 @@ from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from dvadmin.utils.filters import DataLevelPermissionsFilter, CoreModelFilterBankend
|
||||
from dvadmin.utils.filters import CoreModelFilterBankend, DataLevelPermissionMargeFilter
|
||||
from dvadmin.utils.import_export_mixin import ExportSerializerMixin, ImportSerializerMixin
|
||||
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse
|
||||
from dvadmin.utils.permission import CustomPermission
|
||||
@@ -39,7 +41,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
update_serializer_class = None
|
||||
filter_fields = '__all__'
|
||||
search_fields = ()
|
||||
extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionsFilter]
|
||||
extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionMargeFilter]
|
||||
permission_classes = [CustomPermission]
|
||||
import_field_dict = {}
|
||||
export_field_label = {}
|
||||
@@ -67,12 +69,14 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
kwargs.setdefault('context', self.get_serializer_context())
|
||||
# 全部以可见字段为准
|
||||
can_see = self.get_menu_field(serializer_class)
|
||||
# 排除掉序列化器级的字段
|
||||
# sub_set = set(serializer_class._declared_fields.keys()) - set(can_see)
|
||||
# for field in sub_set:
|
||||
# serializer_class._declared_fields.pop(field)
|
||||
# 排除掉序列化器级的字段(排除字段权限中未授权的字段)
|
||||
# if not self.request.user.is_superuser:
|
||||
# serializer_class.Meta.fields = can_see
|
||||
# exclude_set = set(serializer_class._declared_fields.keys()) - set(can_see)
|
||||
# for field in exclude_set:
|
||||
# serializer_class._declared_fields.pop(field)
|
||||
# meta = copy.deepcopy(serializer_class.Meta)
|
||||
# meta.fields = list(can_see)
|
||||
# serializer_class.Meta = meta
|
||||
# 在分页器中使用
|
||||
self.request.permission_fields = can_see
|
||||
if isinstance(self.request.data, list):
|
||||
@@ -83,15 +87,17 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
|
||||
def get_menu_field(self, serializer_class):
|
||||
"""获取字段权限"""
|
||||
finded = False
|
||||
for model in get_custom_app_models():
|
||||
if model['object'] is serializer_class.Meta.model:
|
||||
finded = True
|
||||
break
|
||||
if finded is False:
|
||||
|
||||
if not any(model['object'] is serializer_class.Meta.model for model in get_custom_app_models()):
|
||||
return []
|
||||
return MenuField.objects.filter(model=model['model']
|
||||
).values('field_name', 'title')
|
||||
|
||||
# 匿名用户没有角色
|
||||
ret = FieldPermission.objects.filter(field__model=serializer_class.Meta.model.__name__)
|
||||
if hasattr(self.request.user, 'role'):
|
||||
roles = self.request.user.role.values_list('id', flat=True)
|
||||
ret = ret.filter(is_query=True, role__in=roles)
|
||||
|
||||
return ret.values_list('field__field_name', flat=True)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data, request=request)
|
||||
@@ -131,8 +137,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
instance.delete()
|
||||
return DetailResponse(data=[], msg="删除成功")
|
||||
|
||||
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.TYPE_STRING)
|
||||
|
||||
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_STRING))
|
||||
@swagger_auto_schema(request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['keys'],
|
||||
@@ -147,3 +152,13 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
return SuccessResponse(data=[], msg="删除成功")
|
||||
else:
|
||||
return ErrorResponse(msg="未获取到keys字段")
|
||||
|
||||
@action(methods=['post'], detail=False)
|
||||
def get_by_ids(self, request):
|
||||
"""通过IDS列表获取数据"""
|
||||
ids = request.data.get('ids', [])
|
||||
if ids and ids != ['']:
|
||||
queryset = self.get_queryset().filter(id__in=ids)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return DetailResponse(data=serializer.data)
|
||||
return DetailResponse(data=None)
|
||||
|
||||
17
backend/main.py
Normal file
17
backend/main.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
|
||||
root_path = os.getcwd()
|
||||
sys.path.append(root_path)
|
||||
import uvicorn
|
||||
from application.settings import LOGGING
|
||||
|
||||
if __name__ == '__main__':
|
||||
multiprocessing.freeze_support()
|
||||
workers = 4
|
||||
if os.sys.platform.startswith('win'):
|
||||
# Windows操作系统
|
||||
workers = None
|
||||
uvicorn.run("application.asgi:application", reload=False, host="0.0.0.0", port=8000, workers=workers,
|
||||
log_config=LOGGING)
|
||||
@@ -1,31 +1,33 @@
|
||||
Django==4.2.7
|
||||
Django==4.2.14
|
||||
django-comment-migrate==0.1.7
|
||||
django-cors-headers==4.3.0
|
||||
django-filter==23.3
|
||||
django-cors-headers==4.4.0
|
||||
django-filter==24.2
|
||||
django-ranged-response==0.2.0
|
||||
djangorestframework==3.14.0
|
||||
django-restql==0.15.3
|
||||
django-simple-captcha==0.5.20
|
||||
django-timezone-field==6.0.1
|
||||
djangorestframework-simplejwt==5.3.0
|
||||
djangorestframework==3.15.2
|
||||
django-restql==0.15.4
|
||||
django-simple-captcha==0.6.0
|
||||
django-timezone-field==7.0
|
||||
djangorestframework_simplejwt==5.4.0
|
||||
drf-yasg==1.21.7
|
||||
mysqlclient==2.2.0
|
||||
pypinyin==0.49.0
|
||||
pypinyin==0.51.0
|
||||
ua-parser==0.18.0
|
||||
pyparsing==3.1.1
|
||||
openpyxl==3.1.2
|
||||
requests==2.31.0
|
||||
typing-extensions==4.8.0
|
||||
tzlocal==5.1
|
||||
channels==3.0.5
|
||||
channels-redis==4.1.0
|
||||
websockets==11.0.3
|
||||
pyparsing==3.1.2
|
||||
openpyxl==3.1.5
|
||||
requests==2.32.4
|
||||
typing-extensions==4.12.2
|
||||
tzlocal==5.2
|
||||
channels==4.1.0
|
||||
channels-redis==4.2.0
|
||||
user-agents==2.2.0
|
||||
six==1.16.0
|
||||
whitenoise==6.6.0
|
||||
whitenoise==6.7.0
|
||||
psycopg2==2.9.9
|
||||
uvicorn==0.23.2
|
||||
gunicorn==21.2.0
|
||||
gevent==23.9.1
|
||||
Pillow==10.1.0
|
||||
dvadmin-celery==1.0.5
|
||||
uvicorn==0.30.3
|
||||
gunicorn==23.0.0
|
||||
gevent==24.2.1
|
||||
Pillow==10.4.0
|
||||
pyinstaller==6.9.0
|
||||
dvadmin3-celery==3.1.6
|
||||
oss2==2.19.1
|
||||
cos-python-sdk-v5==1.9.37
|
||||
123
backend/static/captcha/fonts/COPYRIGHT.TXT
Normal file
123
backend/static/captcha/fonts/COPYRIGHT.TXT
Normal file
@@ -0,0 +1,123 @@
|
||||
Bitstream Vera Fonts Copyright
|
||||
|
||||
The fonts have a generous copyright, allowing derivative works (as
|
||||
long as "Bitstream" or "Vera" are not in the names), and full
|
||||
redistribution (so long as they are not *sold* by themselves). They
|
||||
can be be bundled, redistributed and sold with any software.
|
||||
|
||||
The fonts are distributed under the following copyright:
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream
|
||||
Vera is a trademark of Bitstream, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the fonts accompanying this license ("Fonts") and associated
|
||||
documentation files (the "Font Software"), to reproduce and distribute
|
||||
the Font Software, including without limitation the rights to use,
|
||||
copy, merge, publish, distribute, and/or sell copies of the Font
|
||||
Software, and to permit persons to whom the Font Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright and trademark notices and this permission notice
|
||||
shall be included in all copies of one or more of the Font Software
|
||||
typefaces.
|
||||
|
||||
The Font Software may be modified, altered, or added to, and in
|
||||
particular the designs of glyphs or characters in the Fonts may be
|
||||
modified and additional glyphs or characters may be added to the
|
||||
Fonts, only if the fonts are renamed to names not containing either
|
||||
the words "Bitstream" or the word "Vera".
|
||||
|
||||
This License becomes null and void to the extent applicable to Fonts
|
||||
or Font Software that has been modified and is distributed under the
|
||||
"Bitstream Vera" names.
|
||||
|
||||
The Font Software may be sold as part of a larger software package but
|
||||
no copy of one or more of the Font Software typefaces may be sold by
|
||||
itself.
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
|
||||
BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL,
|
||||
OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT
|
||||
SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the names of Gnome, the Gnome
|
||||
Foundation, and Bitstream Inc., shall not be used in advertising or
|
||||
otherwise to promote the sale, use or other dealings in this Font
|
||||
Software without prior written authorization from the Gnome Foundation
|
||||
or Bitstream Inc., respectively. For further information, contact:
|
||||
fonts at gnome dot org.
|
||||
|
||||
Copyright FAQ
|
||||
=============
|
||||
|
||||
1. I don't understand the resale restriction... What gives?
|
||||
|
||||
Bitstream is giving away these fonts, but wishes to ensure its
|
||||
competitors can't just drop the fonts as is into a font sale system
|
||||
and sell them as is. It seems fair that if Bitstream can't make money
|
||||
from the Bitstream Vera fonts, their competitors should not be able to
|
||||
do so either. You can sell the fonts as part of any software package,
|
||||
however.
|
||||
|
||||
2. I want to package these fonts separately for distribution and
|
||||
sale as part of a larger software package or system. Can I do so?
|
||||
|
||||
Yes. A RPM or Debian package is a "larger software package" to begin
|
||||
with, and you aren't selling them independently by themselves.
|
||||
See 1. above.
|
||||
|
||||
3. Are derivative works allowed?
|
||||
Yes!
|
||||
|
||||
4. Can I change or add to the font(s)?
|
||||
Yes, but you must change the name(s) of the font(s).
|
||||
|
||||
5. Under what terms are derivative works allowed?
|
||||
|
||||
You must change the name(s) of the fonts. This is to ensure the
|
||||
quality of the fonts, both to protect Bitstream and Gnome. We want to
|
||||
ensure that if an application has opened a font specifically of these
|
||||
names, it gets what it expects (though of course, using fontconfig,
|
||||
substitutions could still could have occurred during font
|
||||
opening). You must include the Bitstream copyright. Additional
|
||||
copyrights can be added, as per copyright law. Happy Font Hacking!
|
||||
|
||||
6. If I have improvements for Bitstream Vera, is it possible they might get
|
||||
adopted in future versions?
|
||||
|
||||
Yes. The contract between the Gnome Foundation and Bitstream has
|
||||
provisions for working with Bitstream to ensure quality additions to
|
||||
the Bitstream Vera font family. Please contact us if you have such
|
||||
additions. Note, that in general, we will want such additions for the
|
||||
entire family, not just a single font, and that you'll have to keep
|
||||
both Gnome and Jim Lyles, Vera's designer, happy! To make sense to add
|
||||
glyphs to the font, they must be stylistically in keeping with Vera's
|
||||
design. Vera cannot become a "ransom note" font. Jim Lyles will be
|
||||
providing a document describing the design elements used in Vera, as a
|
||||
guide and aid for people interested in contributing to Vera.
|
||||
|
||||
7. I want to sell a software package that uses these fonts: Can I do so?
|
||||
|
||||
Sure. Bundle the fonts with your software and sell your software
|
||||
with the fonts. That is the intent of the copyright.
|
||||
|
||||
8. If applications have built the names "Bitstream Vera" into them,
|
||||
can I override this somehow to use fonts of my choosing?
|
||||
|
||||
This depends on exact details of the software. Most open source
|
||||
systems and software (e.g., Gnome, KDE, etc.) are now converting to
|
||||
use fontconfig (see www.fontconfig.org) to handle font configuration,
|
||||
selection and substitution; it has provisions for overriding font
|
||||
names and substituting alternatives. An example is provided by the
|
||||
supplied local.conf file, which chooses the family Bitstream Vera for
|
||||
"sans", "serif" and "monospace". Other software (e.g., the XFree86
|
||||
core server) has other mechanisms for font substitution.
|
||||
11
backend/static/captcha/fonts/README.TXT
Normal file
11
backend/static/captcha/fonts/README.TXT
Normal file
@@ -0,0 +1,11 @@
|
||||
Contained herin is the Bitstream Vera font family.
|
||||
|
||||
The Copyright information is found in the COPYRIGHT.TXT file (along
|
||||
with being incorporated into the fonts themselves).
|
||||
|
||||
The releases notes are found in the file "RELEASENOTES.TXT".
|
||||
|
||||
We hope you enjoy Vera!
|
||||
|
||||
Bitstream, Inc.
|
||||
The Gnome Project
|
||||
BIN
backend/static/captcha/fonts/Vera.ttf
Normal file
BIN
backend/static/captcha/fonts/Vera.ttf
Normal file
Binary file not shown.
18
backend/static/drf-yasg/README
Normal file
18
backend/static/drf-yasg/README
Normal file
@@ -0,0 +1,18 @@
|
||||
Information about external resources
|
||||
|
||||
The following files are taken from external resources or trees.
|
||||
|
||||
Files: insQ.js
|
||||
insQ.min.js
|
||||
License: MIT
|
||||
Copyright: Zbyszek Tenerowicz
|
||||
Eryk Napierała <eryk.piast@gmail.com>
|
||||
Askar Yusupov <devex.soft@gmail.com>
|
||||
Dan Dascalescu <ddascalescu+github@gmail.com>
|
||||
Source: https://github.com/naugtur/insertionQuery v1.0.3
|
||||
|
||||
Files: immutable.js
|
||||
immutable.min.js
|
||||
License: MIT
|
||||
Copyright: 2014-present, Facebook, Inc
|
||||
Source: https://github.com/immutable-js/immutable-js/releases/tag/v3.8.2
|
||||
BIN
backend/static/drf-yasg/README.gz
Normal file
BIN
backend/static/drf-yasg/README.gz
Normal file
Binary file not shown.
4977
backend/static/drf-yasg/immutable.js
Normal file
4977
backend/static/drf-yasg/immutable.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
backend/static/drf-yasg/immutable.js.gz
Normal file
BIN
backend/static/drf-yasg/immutable.js.gz
Normal file
Binary file not shown.
BIN
backend/static/drf-yasg/immutable.min.js.gz
Normal file
BIN
backend/static/drf-yasg/immutable.min.js.gz
Normal file
Binary file not shown.
163
backend/static/drf-yasg/insQ.js
Normal file
163
backend/static/drf-yasg/insQ.js
Normal file
@@ -0,0 +1,163 @@
|
||||
var insertionQ = (function () {
|
||||
"use strict";
|
||||
|
||||
var sequence = 100,
|
||||
isAnimationSupported = false,
|
||||
animation_string = 'animationName',
|
||||
keyframeprefix = '',
|
||||
domPrefixes = 'Webkit Moz O ms Khtml'.split(' '),
|
||||
pfx = '',
|
||||
elm = document.createElement('div'),
|
||||
options = {
|
||||
strictlyNew: true,
|
||||
timeout: 20
|
||||
};
|
||||
|
||||
if (elm.style.animationName) {
|
||||
isAnimationSupported = true;
|
||||
}
|
||||
|
||||
if (isAnimationSupported === false) {
|
||||
for (var i = 0; i < domPrefixes.length; i++) {
|
||||
if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) {
|
||||
pfx = domPrefixes[i];
|
||||
animation_string = pfx + 'AnimationName';
|
||||
keyframeprefix = '-' + pfx.toLowerCase() + '-';
|
||||
isAnimationSupported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function listen(selector, callback) {
|
||||
var styleAnimation, animationName = 'insQ_' + (sequence++);
|
||||
|
||||
var eventHandler = function (event) {
|
||||
if (event.animationName === animationName || event[animation_string] === animationName) {
|
||||
if (!isTagged(event.target)) {
|
||||
callback(event.target);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
styleAnimation = document.createElement('style');
|
||||
styleAnimation.innerHTML = '@' + keyframeprefix + 'keyframes ' + animationName + ' { from { outline: 1px solid transparent } to { outline: 0px solid transparent } }' +
|
||||
"\n" + selector + ' { animation-duration: 0.001s; animation-name: ' + animationName + '; ' +
|
||||
keyframeprefix + 'animation-duration: 0.001s; ' + keyframeprefix + 'animation-name: ' + animationName + '; ' +
|
||||
' } ';
|
||||
|
||||
document.head.appendChild(styleAnimation);
|
||||
|
||||
var bindAnimationLater = setTimeout(function () {
|
||||
document.addEventListener('animationstart', eventHandler, false);
|
||||
document.addEventListener('MSAnimationStart', eventHandler, false);
|
||||
document.addEventListener('webkitAnimationStart', eventHandler, false);
|
||||
//event support is not consistent with DOM prefixes
|
||||
}, options.timeout); //starts listening later to skip elements found on startup. this might need tweaking
|
||||
|
||||
return {
|
||||
destroy: function () {
|
||||
clearTimeout(bindAnimationLater);
|
||||
if (styleAnimation) {
|
||||
document.head.removeChild(styleAnimation);
|
||||
styleAnimation = null;
|
||||
}
|
||||
document.removeEventListener('animationstart', eventHandler);
|
||||
document.removeEventListener('MSAnimationStart', eventHandler);
|
||||
document.removeEventListener('webkitAnimationStart', eventHandler);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function tag(el) {
|
||||
el.QinsQ = true; //bug in V8 causes memory leaks when weird characters are used as field names. I don't want to risk leaking DOM trees so the key is not '-+-' anymore
|
||||
}
|
||||
|
||||
function isTagged(el) {
|
||||
return (options.strictlyNew && (el.QinsQ === true));
|
||||
}
|
||||
|
||||
function topmostUntaggedParent(el) {
|
||||
if (isTagged(el.parentNode)) {
|
||||
return el;
|
||||
} else {
|
||||
return topmostUntaggedParent(el.parentNode);
|
||||
}
|
||||
}
|
||||
|
||||
function tagAll(e) {
|
||||
tag(e);
|
||||
e = e.firstChild;
|
||||
for (; e; e = e.nextSibling) {
|
||||
if (e !== undefined && e.nodeType === 1) {
|
||||
tagAll(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//aggregates multiple insertion events into a common parent
|
||||
function catchInsertions(selector, callback) {
|
||||
var insertions = [];
|
||||
//throttle summary
|
||||
var sumUp = (function () {
|
||||
var to;
|
||||
return function () {
|
||||
clearTimeout(to);
|
||||
to = setTimeout(function () {
|
||||
insertions.forEach(tagAll);
|
||||
callback(insertions);
|
||||
insertions = [];
|
||||
}, 10);
|
||||
};
|
||||
})();
|
||||
|
||||
return listen(selector, function (el) {
|
||||
if (isTagged(el)) {
|
||||
return;
|
||||
}
|
||||
tag(el);
|
||||
var myparent = topmostUntaggedParent(el);
|
||||
if (insertions.indexOf(myparent) < 0) {
|
||||
insertions.push(myparent);
|
||||
}
|
||||
sumUp();
|
||||
});
|
||||
}
|
||||
|
||||
//insQ function
|
||||
var exports = function (selector) {
|
||||
if (isAnimationSupported && selector.match(/[^{}]/)) {
|
||||
|
||||
if (options.strictlyNew) {
|
||||
tagAll(document.body); //prevents from catching things on show
|
||||
}
|
||||
return {
|
||||
every: function (callback) {
|
||||
return listen(selector, callback);
|
||||
},
|
||||
summary: function (callback) {
|
||||
return catchInsertions(selector, callback);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
//allows overriding defaults
|
||||
exports.config = function (opt) {
|
||||
for (var o in opt) {
|
||||
if (opt.hasOwnProperty(o)) {
|
||||
options[o] = opt[o];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return exports;
|
||||
})();
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = insertionQ;
|
||||
}
|
||||
BIN
backend/static/drf-yasg/insQ.js.gz
Normal file
BIN
backend/static/drf-yasg/insQ.js.gz
Normal file
Binary file not shown.
BIN
backend/static/drf-yasg/insQ.min.js.gz
Normal file
BIN
backend/static/drf-yasg/insQ.min.js.gz
Normal file
Binary file not shown.
@@ -1,3 +1,9 @@
|
||||
// redoc-init.js
|
||||
// https://github.com/axnsan12/drf-yasg
|
||||
// Copyright 2017 - 2021, Cristian V. <cristi@cvjd.me>
|
||||
// This file is licensed under the BSD 3-Clause License.
|
||||
// License text available at https://opensource.org/licenses/BSD-3-Clause
|
||||
|
||||
"use strict";
|
||||
|
||||
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
|
||||
|
||||
BIN
backend/static/drf-yasg/redoc-init.js.gz
Normal file
BIN
backend/static/drf-yasg/redoc-init.js.gz
Normal file
Binary file not shown.
22
backend/static/drf-yasg/redoc-old/LICENSE
Normal file
22
backend/static/drf-yasg/redoc-old/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015, Rebilly, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
BIN
backend/static/drf-yasg/redoc-old/LICENSE.gz
Normal file
BIN
backend/static/drf-yasg/redoc-old/LICENSE.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
backend/static/drf-yasg/redoc-old/redoc.min.js.gz
Normal file
BIN
backend/static/drf-yasg/redoc-old/redoc.min.js.gz
Normal file
Binary file not shown.
23
backend/static/drf-yasg/redoc-old/redoc.min.js.map
Normal file
23
backend/static/drf-yasg/redoc-old/redoc.min.js.map
Normal file
@@ -0,0 +1,23 @@
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
// Map of projects that support handling 404s.
|
||||
var supportedProjects = {
|
||||
'ReDoc': 'https://redocly.github.io/redoc'
|
||||
};
|
||||
|
||||
var project = window.location.pathname.split('/')[1];
|
||||
|
||||
// Always fallback to rebilly.com redirect.
|
||||
var loc = 'https://www.rebilly.com/';
|
||||
|
||||
if (supportedProjects.hasOwnProperty(project)) {
|
||||
if (typeof supportedProjects[project] === 'string') {
|
||||
loc = supportedProjects[project];
|
||||
}
|
||||
}
|
||||
window.location.href = loc;
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
BIN
backend/static/drf-yasg/redoc-old/redoc.min.js.map.gz
Normal file
BIN
backend/static/drf-yasg/redoc-old/redoc.min.js.map.gz
Normal file
Binary file not shown.
22
backend/static/drf-yasg/redoc/LICENSE
Normal file
22
backend/static/drf-yasg/redoc/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present, Rebilly, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
BIN
backend/static/drf-yasg/redoc/LICENSE.gz
Normal file
BIN
backend/static/drf-yasg/redoc/LICENSE.gz
Normal file
Binary file not shown.
1906
backend/static/drf-yasg/redoc/redoc.min.js
vendored
1906
backend/static/drf-yasg/redoc/redoc.min.js
vendored
File diff suppressed because one or more lines are too long
BIN
backend/static/drf-yasg/redoc/redoc.min.js.gz
Normal file
BIN
backend/static/drf-yasg/redoc/redoc.min.js.gz
Normal file
Binary file not shown.
1
backend/static/drf-yasg/redoc/redoc.standalone.js.map
Normal file
1
backend/static/drf-yasg/redoc/redoc.standalone.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
backend/static/drf-yasg/redoc/redoc.standalone.js.map.gz
Normal file
BIN
backend/static/drf-yasg/redoc/redoc.standalone.js.map.gz
Normal file
Binary file not shown.
BIN
backend/static/drf-yasg/style.css.gz
Normal file
BIN
backend/static/drf-yasg/style.css.gz
Normal file
Binary file not shown.
202
backend/static/drf-yasg/swagger-ui-dist/LICENSE
Normal file
202
backend/static/drf-yasg/swagger-ui-dist/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
BIN
backend/static/drf-yasg/swagger-ui-dist/LICENSE.gz
Normal file
BIN
backend/static/drf-yasg/swagger-ui-dist/LICENSE.gz
Normal file
Binary file not shown.
2
backend/static/drf-yasg/swagger-ui-dist/NOTICE
Normal file
2
backend/static/drf-yasg/swagger-ui-dist/NOTICE
Normal file
@@ -0,0 +1,2 @@
|
||||
swagger-ui
|
||||
Copyright 2020-2021 SmartBear Software Inc.
|
||||
BIN
backend/static/drf-yasg/swagger-ui-dist/absolute-path.js.gz
Normal file
BIN
backend/static/drf-yasg/swagger-ui-dist/absolute-path.js.gz
Normal file
Binary file not shown.
16
backend/static/drf-yasg/swagger-ui-dist/index.css
Normal file
16
backend/static/drf-yasg/swagger-ui-dist/index.css
Normal file
@@ -0,0 +1,16 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
BIN
backend/static/drf-yasg/swagger-ui-dist/index.css.gz
Normal file
BIN
backend/static/drf-yasg/swagger-ui-dist/index.css.gz
Normal file
Binary file not shown.
BIN
backend/static/drf-yasg/swagger-ui-dist/index.js.gz
Normal file
BIN
backend/static/drf-yasg/swagger-ui-dist/index.js.gz
Normal file
Binary file not shown.
@@ -4,8 +4,6 @@
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
<script>
|
||||
'use strict';
|
||||
function run () {
|
||||
@@ -15,31 +13,32 @@
|
||||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1);
|
||||
qp = window.location.hash.substring(1).replace('?', '&');
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
||||
arr = qp.split("&")
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
|
||||
arr = qp.split("&");
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||
function (key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value)
|
||||
return key === "" ? value : decodeURIComponent(value);
|
||||
}
|
||||
) : {}
|
||||
) : {};
|
||||
|
||||
isValid = qp.state === sentState
|
||||
isValid = qp.state === sentState;
|
||||
|
||||
if ((
|
||||
oauth2.auth.schema.get("flow") === "accessCode"||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode"
|
||||
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||
) && !oauth2.auth.code) {
|
||||
if (!isValid) {
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
|
||||
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,7 +47,7 @@
|
||||
oauth2.auth.code = qp.code;
|
||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||
} else {
|
||||
let oauthErrorMsg
|
||||
let oauthErrorMsg;
|
||||
if (qp.error) {
|
||||
oauthErrorMsg = "["+qp.error+"]: " +
|
||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||
@@ -59,7 +58,7 @@
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -68,7 +67,13 @@
|
||||
window.close();
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
if (document.readyState !== 'loading') {
|
||||
run();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
backend/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html.gz
Normal file
BIN
backend/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html.gz
Normal file
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
window.onload = function() {
|
||||
//<editor-fold desc="Changeable Configuration Block">
|
||||
|
||||
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: "https://petstore.swagger.io/v2/swagger.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
|
||||
//</editor-fold>
|
||||
};
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
backend/static/drf-yasg/swagger-ui-dist/swagger-ui-bundle.js.gz
Normal file
BIN
backend/static/drf-yasg/swagger-ui-dist/swagger-ui-bundle.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user