mirror of
https://github.com/huge-dream/django-vue3-admin.git
synced 2026-09-21 12:52:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
82a16545b4 | ||
|
|
39638e2e6a | ||
|
|
d3e5f258e5 | ||
|
|
77a27cba14 | ||
|
|
26bbf67da8 | ||
|
|
d4f754976e | ||
|
|
85805e0d4b | ||
|
|
8a646e5ef7 | ||
|
|
71eec9cfbd | ||
|
|
9bd79b9dc6 | ||
|
|
e210b7dd17 | ||
|
|
4d00661163 | ||
|
|
6798fca362 | ||
|
|
6f5bbb045d | ||
|
|
bbf3018b20 | ||
|
|
5510a18280 | ||
|
|
97737c3ef1 | ||
|
|
0b554f3669 | ||
|
|
63453450ad | ||
|
|
fea6ebd98e | ||
|
|
d4e43e28e8 | ||
|
|
dffe47e79c | ||
|
|
9973688675 | ||
|
|
98a5bbd60a | ||
|
|
ac7e8a7764 | ||
|
|
26ed59d1ff | ||
|
|
369157fa4f | ||
|
|
8961733025 | ||
|
|
577b88332f | ||
|
|
411f065bfc | ||
|
|
8d37d929b2 | ||
|
|
c0d6cd54c3 | ||
|
|
ae8366f097 | ||
|
|
4baf5da0ff | ||
|
|
c00f5f2beb | ||
|
|
5143afdd85 | ||
|
|
b4153d1848 | ||
|
|
d2c0c54080 | ||
|
|
7f184b2a9a | ||
|
|
87f9784445 | ||
|
|
e900cc2280 | ||
|
|
4a26257a61 | ||
|
|
06476c182e | ||
|
|
6146e6edb6 | ||
|
|
85f5ff1935 | ||
|
|
86e33e44e1 | ||
|
|
217f3c8e14 | ||
|
|
d0feaa23e5 | ||
|
|
fa67a27908 | ||
|
|
56145dfe61 | ||
|
|
3def205e6e | ||
|
|
873400bcce | ||
|
|
0c1cf1218b | ||
|
|
d8e5ad9057 | ||
|
|
3639cad58c | ||
|
|
c99eb1a9bc | ||
|
|
7ae15022c0 | ||
|
|
68b3d479ca | ||
|
|
2826cafa75 | ||
|
|
ba4a580bb0 | ||
|
|
f322d38ab8 | ||
|
|
7d315a7d28 | ||
|
|
916071814a | ||
|
|
acf113e5e6 | ||
|
|
083eef9df1 | ||
|
|
e8c64d6d11 | ||
|
|
d1abc58b40 | ||
|
|
0c8ce0ce27 | ||
|
|
23d320434e | ||
|
|
14907e140d | ||
|
|
1132e04080 | ||
|
|
39fee0875d | ||
|
|
5898ae85de | ||
|
|
172a867bad | ||
|
|
5a308695fd | ||
|
|
0c5c3c4ecd | ||
|
|
9a8241023b | ||
|
|
27be35616b | ||
|
|
e6f0a5e0ea | ||
|
|
cbebd8305c | ||
|
|
66b68523f5 | ||
|
|
95b14e5a42 | ||
|
|
583b172a37 | ||
|
|
2227564c6f | ||
|
|
5e3ecfc0a6 | ||
|
|
d467915f97 | ||
|
|
197714e845 | ||
|
|
fd3adeed57 | ||
|
|
bc15ed15ca | ||
|
|
349986f14f | ||
|
|
fb4bba9000 | ||
|
|
e0d46871c4 | ||
|
|
7691481006 | ||
|
|
120c737de7 | ||
|
|
5729aeb521 | ||
|
|
205cfcca2e | ||
|
|
0fdc108ebf | ||
|
|
064cbee8a2 | ||
|
|
a2d4d6e07e | ||
|
|
d721405ee4 | ||
|
|
0650c95745 | ||
|
|
f291885d57 | ||
|
|
4b77fb903c | ||
|
|
fca82c093a | ||
|
|
49300fb17e | ||
|
|
c763333024 | ||
|
|
42e6c7b600 | ||
|
|
6172dea399 | ||
|
|
2baba8e36f | ||
|
|
0131d31808 | ||
|
|
dd2dcd4ad1 | ||
|
|
fb8b0a5ac6 | ||
|
|
6045312f7e | ||
|
|
4641d9c774 | ||
|
|
c04b33ed31 | ||
|
|
0286ca003f | ||
|
|
54e4d23cf7 | ||
|
|
645f43c887 | ||
|
|
4ac8ed7627 | ||
|
|
94ad6b1bae | ||
|
|
96748da99d | ||
|
|
b74a196c94 | ||
|
|
dd98b98775 | ||
|
|
8e107d9944 | ||
|
|
7159253c6b | ||
|
|
075d457dc7 | ||
|
|
564e9a112b | ||
|
|
3b84773a7d | ||
|
|
996d644d9b | ||
|
|
97f7bfd3f0 | ||
|
|
d1fdbd7e51 | ||
|
|
802ee7f50e | ||
|
|
ddf9fcd8e8 | ||
|
|
6f15c2f669 | ||
|
|
084d1c0c3e | ||
|
|
6ded198206 | ||
|
|
e549d33a48 | ||
|
|
cbba5a60f4 | ||
|
|
773b56a5e6 | ||
|
|
fb29929f55 | ||
|
|
0772a605bf | ||
|
|
496c453615 | ||
|
|
58b59c2ed0 | ||
|
|
94815f4b20 | ||
|
|
ad6ec5ca58 | ||
|
|
4bcaf8d45f | ||
|
|
be5db33a55 | ||
|
|
cfc5f8597a | ||
|
|
209e2ecced | ||
|
|
4b948fe5c3 | ||
|
|
ea335922b2 | ||
|
|
d290836f4f | ||
|
|
9a426dbd72 | ||
|
|
a0b6e57a3c | ||
|
|
5a89cdf889 | ||
|
|
b6b970f7a2 | ||
|
|
2722631276 | ||
|
|
478aea32f1 | ||
|
|
cf55897552 | ||
|
|
8020da03e2 | ||
|
|
c646563d26 | ||
|
|
43a4572a71 | ||
|
|
1582353279 | ||
|
|
1df2101658 | ||
|
|
0679198d0f | ||
|
|
e3c478a263 | ||
|
|
07cf81c9fe | ||
|
|
1b98fbc982 | ||
|
|
85040c808f | ||
|
|
00b2cac7a5 | ||
|
|
7c27f95353 | ||
|
|
d59df6db7c | ||
|
|
34c55cfd7e | ||
|
|
191e57dcd9 | ||
|
|
9be50863d9 | ||
|
|
9c218a856c | ||
|
|
b4ebb14843 | ||
|
|
17d80b0b00 | ||
|
|
01394c6904 | ||
|
|
59b1b54da1 | ||
|
|
8084ca7b0b | ||
|
|
427b6f8478 | ||
|
|
41a9888441 | ||
|
|
5139d37007 | ||
|
|
44bceb6e22 | ||
|
|
c2f8c43905 | ||
|
|
9f479e476d | ||
|
|
f5a7b157f9 | ||
|
|
7110d10432 | ||
|
|
05f4e2970d | ||
|
|
5353a27187 | ||
|
|
f4ff5d87af | ||
|
|
ceac44813f | ||
|
|
a490ced370 | ||
|
|
20188261c5 | ||
|
|
15ef01aed9 | ||
|
|
1dea6ed29c | ||
|
|
2bad36c982 | ||
|
|
941dbbc783 | ||
|
|
14881707e2 | ||
|
|
d60c795ea2 | ||
|
|
9cf5d13f1c | ||
|
|
e1618359d7 | ||
|
|
abe7972ab4 | ||
|
|
0709e72ae1 | ||
|
|
2e676d64bb | ||
|
|
98e974b55e | ||
|
|
e7f78fbae0 | ||
|
|
111edb3f4a | ||
|
|
86f539a193 | ||
|
|
3b9dc7c171 | ||
|
|
8175c620ed | ||
|
|
de5884d28d | ||
|
|
e63a7cab6f | ||
|
|
cdfdf0a483 | ||
|
|
515f720a59 | ||
|
|
5524446dbc | ||
|
|
9c32c7f60b | ||
|
|
6eed2814f8 | ||
|
|
f03e61ce63 | ||
|
|
5450102275 | ||
|
|
689ce4b43b | ||
|
|
65af240757 | ||
|
|
f61025c6db | ||
|
|
516f0ac675 | ||
|
|
b1b5ae5fcd | ||
|
|
9a847d078f | ||
|
|
dcac5c759a | ||
|
|
13999142aa | ||
|
|
cd6318c6fd | ||
|
|
3c58bcf467 | ||
|
|
b9bdf878de | ||
|
|
db989671c0 | ||
|
|
f67869d428 | ||
|
|
78c693718b | ||
|
|
653483e5de | ||
|
|
89ba768932 | ||
|
|
e4326b6d4e | ||
|
|
97020ba4e8 | ||
|
|
945db3344c | ||
|
|
19df23ce5f | ||
|
|
97c76b6afe | ||
|
|
6597826fdd | ||
|
|
ee419e282e | ||
|
|
3d12d7a5c5 | ||
|
|
b2aa22ce84 | ||
|
|
06d7d718e1 | ||
|
|
6067dfac2f | ||
|
|
637e45ef83 | ||
|
|
202f67460e | ||
|
|
7da23ab6a6 | ||
|
|
be51124bb6 | ||
|
|
7e7637f10b | ||
|
|
dbb94fb9ff | ||
|
|
fea61e35ef | ||
|
|
8ecfc1143b | ||
|
|
7c72cc74df | ||
|
|
12f5125207 | ||
|
|
770c5f73c0 | ||
|
|
98b81a789d | ||
|
|
c13a5f0dda | ||
|
|
a524137e18 | ||
|
|
2c9d8766e3 | ||
|
|
8a0f18eab1 | ||
|
|
d8511eeb7b | ||
|
|
52a91f1703 | ||
|
|
14395621c0 | ||
|
|
c4e2451ac8 | ||
|
|
936928e854 | ||
|
|
619ebeacf1 | ||
|
|
b95679a986 | ||
|
|
8e930c6a10 | ||
|
|
d2811a1f87 | ||
|
|
7ac5c86ed3 | ||
|
|
eea5cdadfe | ||
|
|
4e5f9ce946 | ||
|
|
e790882ab6 | ||
|
|
0f86f5c6fc | ||
|
|
4018113161 | ||
|
|
252b6a8328 | ||
|
|
c26abef364 | ||
|
|
a40dc9afb1 | ||
|
|
9114c6bf81 | ||
|
|
7b0a6bc2be | ||
|
|
e4ca462153 | ||
|
|
840323ff07 | ||
|
|
ed0dca7346 | ||
|
|
2b63966ee2 | ||
|
|
55a2f73420 | ||
|
|
7ccbcebe77 | ||
|
|
8f57a240db | ||
|
|
1ccfd619f3 | ||
|
|
dcdc7bbe3b | ||
|
|
90557ea039 | ||
|
|
a462e15643 | ||
|
|
6a24683769 | ||
|
|
9fa1775b77 | ||
|
|
16605779ed | ||
|
|
186658858d | ||
|
|
996cb1447d | ||
|
|
e8b84ad71f | ||
|
|
e7065ab89e | ||
|
|
e962ca6559 | ||
|
|
4bdc256311 | ||
|
|
f7b94e496c | ||
|
|
8c0a04ce61 | ||
|
|
31f20a766f | ||
|
|
65a34a683c | ||
|
|
66effde1d9 | ||
|
|
f4f958d2e9 | ||
|
|
d10127eafc | ||
|
|
60035767ad | ||
|
|
adc522c180 | ||
|
|
17b67e43c9 | ||
|
|
3cccfaa7f4 | ||
|
|
ce4d410e53 | ||
|
|
7b0341b527 | ||
|
|
31f7175186 | ||
|
|
2c22ecbac5 | ||
|
|
ad59bc68aa | ||
|
|
8d7d775164 | ||
|
|
9c2dea9db5 | ||
|
|
8c31bda3da | ||
|
|
7f7d88fe5b | ||
|
|
19e18a7b6f | ||
|
|
cd6cd775e7 | ||
|
|
111ca9554c | ||
|
|
c9fddbd4a7 | ||
|
|
d3b057f75a | ||
|
|
096f5919af | ||
|
|
25cc2c83c1 | ||
|
|
054bcf3eea | ||
|
|
9c24e61123 | ||
|
|
cc36ff3d8f | ||
|
|
93e74dbaa2 | ||
|
|
5ab3ad4ed9 | ||
|
|
9468abd4a2 | ||
|
|
1080af6413 | ||
|
|
c4c9a81ac8 | ||
|
|
d1223dddd3 | ||
|
|
3dd68e5d21 | ||
|
|
0668cb087f | ||
|
|
38494edea5 | ||
|
|
2f04f22904 | ||
|
|
0ccf2e3725 | ||
|
|
68cd42246d | ||
|
|
dc6ddda4ab | ||
|
|
08b56ee663 | ||
|
|
82d7193dcb | ||
|
|
4b609ed7e4 | ||
|
|
61066b8caa | ||
|
|
160c376f81 | ||
|
|
b96c220f1d | ||
|
|
42884685ff | ||
|
|
58e611cb22 | ||
|
|
0b2fa1e92c | ||
|
|
125dfd4dd5 | ||
|
|
af7bfcd215 | ||
|
|
6917786864 | ||
|
|
cd60b3d9be | ||
|
|
cfae363f53 | ||
|
|
4f8686b35e | ||
|
|
1407a25b6a | ||
|
|
7c78c7e53f | ||
|
|
f1da0507dd | ||
|
|
df74c792f6 | ||
|
|
4c697ef9d4 | ||
|
|
ab1d1c87e2 | ||
|
|
b6b3b4a39a | ||
|
|
581e4cd92f | ||
|
|
fb76d4271b | ||
|
|
4f176cf88a | ||
|
|
af5f3b7137 | ||
|
|
b03037b866 |
+2
-1
@@ -3,4 +3,5 @@
|
||||
.idea
|
||||
|
||||
.history/
|
||||
.vscode/
|
||||
.vscode/
|
||||
web/package-lock.json
|
||||
@@ -0,0 +1,10 @@
|
||||
# Django-Vue3-Admin 更新日志
|
||||
|
||||
## 正式发布v3.0.0版本
|
||||
### 1.新增:列权限管理与授权;
|
||||
### 2.新增:代码新版本发布后,进行升级提醒;
|
||||
### 3.优化:角色管理中按钮权限的操作;
|
||||
### 4.优化:websocket 连接状态显示;
|
||||
### 5.优化:初始化获取系统配置与字典配置,进行动态渲染登录页面;
|
||||
### 6.修复:登录页面中系统配置不生效问题;
|
||||
### 7.其他优化
|
||||
@@ -1,196 +1,175 @@
|
||||
# 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://gitee.com/huge-dream/django-vue3-admin/blob/master/LICENSE) [](https://python.org/) [](https://docs.djangoproject.com/zh-hans/3.2/) [](https://nodejs.org/zh-cn/) [](https://gitee.com/huge-dream/django-vue3-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)
|
||||
[preview](https://demo.dvadmin.com) | [Official website document](https://www.django-vue-admin.com) | [qq group](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [community](https://bbs.django-vue-admin.com) | [plugins market](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
|
||||
|
||||
💡 **「About」**
|
||||
|
||||
It is a completely open-source rapid development platform, provided free for personal use and authorized for group use.
|
||||
Django-Vue3-Admin is a comprehensive basic development platform based on the RBAC (Role-Based Access Control) model for permission control, with column-level granularity. It follows a frontend-backend separation architecture, with Django and Django Rest Framework used for the backend, and Vue3, Composition API, TypeScript, Vite, and Element Plus used for the frontend.
|
||||
|
||||
|
||||
## framework introduction
|
||||
|
||||
💡 **「关于」**
|
||||
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) Is a set of all open source rapid development platform, no reservation for individuals and enterprises free use.
|
||||
|
||||
我们是一群热爱代码的青年,在这个炙热的时代下,我们希望静下心来通过Code带来一点我们的色彩和颜色。
|
||||
* 🧑🤝🧑Front-end adoption Vue3+TS+pinia+fastcrud。
|
||||
* 👭The backend uses the Python language Django framework as well as the powerful[Django REST Framework](https://pypi.org/project/djangorestframework)。
|
||||
* 👫Permission authentication use[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),Supports the multi-terminal authentication system.
|
||||
* 👬Support loading dynamic permission menu, multi - way easy permission control.
|
||||
* 👬Enhanced Column Permission Control, with granularity down to each column.
|
||||
* 💏Special thanks: [vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/).
|
||||
* 💡Special thanks:[jetbrains](https://www.jetbrains.com/) To provide a free IntelliJ IDEA license for this open source project.
|
||||
|
||||
因为热爱,所以拥抱未来
|
||||
## Online experience
|
||||
|
||||
## 平台简介
|
||||
👩👧👦👩👧👦 demo address:[https://demo.dvadmin.com](https://demo.dvadmin.com)
|
||||
|
||||
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
|
||||
* demo account:superadmin
|
||||
|
||||
* demo password:admin123456
|
||||
|
||||
👩👦👦docs:[https://django-vue-admin.com](https://django-vue-admin.com)
|
||||
|
||||
* 🧑🤝🧑前端采用 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 授权。
|
||||
## communication
|
||||
|
||||
* Communication community:[click here](https://bbs.django-vue-admin.com)👩👦👦
|
||||
|
||||
* plugins market:[click here](https://bbs.django-vue-admin.com/plugMarket.html)👩👦👦
|
||||
|
||||
## 在线体验
|
||||
## source code url:
|
||||
|
||||
👩👧👦演示地址:[http://demo.django-vue-admin.com](http://demo.django-vue-admin.com)
|
||||
gitee(Main push):[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩👦👦
|
||||
|
||||
- 账号:superadmin
|
||||
github:[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩👦👦
|
||||
|
||||
- 密码:admin123456
|
||||
## core function
|
||||
|
||||
👩👦👦文档地址:[https://django-vue-admin.com](https://django-vue-admin.com)
|
||||
1. 👨⚕️Menu Management: Configure system menus, operation permissions, button permission flags, backend interface permissions, etc.
|
||||
2. 🧑⚕️Department Management: Configure system organizational structure (company, department, role).
|
||||
3. 👩⚕️Role Management: Role menu permission assignment, data permission assignment, set role-based data scope permissions by department.
|
||||
4. 🧑🎓Button Permission Control: Authorize role-specific button permissions and interface permissions, enabling authorization of data scope for each interface.
|
||||
5. 🧑🎓Field Column Permission Control: Authorize page field display permissions, specifically for the display permissions of a certain column.
|
||||
6. 👨🎓User Management: Users are system operators, and this function is mainly used for system user configuration.
|
||||
7. 👬API Whitelist: Configure interfaces that do not require permission verification.
|
||||
8. 🧑🔧Dictionary Management: Maintain frequently used and relatively fixed data in the system.
|
||||
9. 🧑🔧Region Management: Manage provinces, cities, counties, and districts.
|
||||
10. 📁File Management: Unified management of all files, images, etc., on the platform.
|
||||
11. 🗓️Operation Logs: Record and query logs for normal system operations and exceptional system information.
|
||||
12. 🔌[Plugin Market](https://bbs.django-vue-admin.com/plugMarket.html): Applications and plugins developed based on the Django-Vue-Admin framework.
|
||||
|
||||
## plugins market 🔌
|
||||
|
||||
Updating...
|
||||
|
||||
## 交流
|
||||
## Repository Branch Explanation 💈
|
||||
Main Branch: master (stable version)
|
||||
Development Branch: develop
|
||||
|
||||
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩👦👦
|
||||
## before start project you need:
|
||||
|
||||
- 插件市场:[戳我](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)
|
||||
|
||||
- 二维码
|
||||
|
||||
<img src='https://images.gitee.com/uploads/images/2022/0530/233203_5fb11883_5074988.jpeg' width='200'>
|
||||
|
||||
## 源码地址
|
||||
|
||||
gitee地址(主推):[https://gitee.com/liqianglog/django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)👩👦👦
|
||||
|
||||
github地址:[https://github.com/liqianglog/django-vue-admin](https://github.com/liqianglog/django-vue-admin)👩👦👦
|
||||
|
||||
|
||||
|
||||
## 内置功能
|
||||
|
||||
1. 👨⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
|
||||
2. 🧑⚕️部门管理:配置系统组织机构(公司、部门、角色)。
|
||||
3. 👩⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
|
||||
4. 🧑🎓权限权限:授权角色的权限范围。
|
||||
5. 👨🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
6. 👬接口白名单:配置不需要进行权限校验的接口。
|
||||
7. 🧑🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
8. 🧑🔧地区管理:对省市县区域进行管理。
|
||||
9. 📁附件管理:对平台上所有文件、图片等进行统一管理。
|
||||
10. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
11. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html):基于Django-Vue-Admin框架开发的应用和插件。
|
||||
|
||||
## 插件市场 🔌
|
||||
|
||||
- Celery异步任务:[dvadmin-celery](https://gitee.com/huge-dream/dvadmin-celery)
|
||||
- 升级中心后端:[dvadmin-upgrade-center](https://gitee.com/huge-dream/dvadmin-upgrade-center)
|
||||
- 升级中心前端:[dvadmin-upgrade-center-web](https://gitee.com/huge-dream/dvadmin-upgrade-center-web)
|
||||
|
||||
## 准备工作
|
||||
~~~
|
||||
Python >= 3.8.0 (推荐3.8+版本)
|
||||
nodejs >= 14.0 (推荐最新)
|
||||
Mysql >= 5.7.0 (可选,默认数据库sqlite3,推荐8.0版本)
|
||||
Redis(可选,最新版)
|
||||
Python >= 3.11.0 (Minimum version 3.9+)
|
||||
Node.js >= 16.0
|
||||
Mysql >= 8.0 (Optional, default database: SQLite3, supports 5.7+, recommended version: 8.0)
|
||||
Redis (Optional, latest version)
|
||||
~~~
|
||||
|
||||
## 前端♝
|
||||
## frontend♝
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://gitee.com/liqianglog/django-vue-admin.git
|
||||
# clone code
|
||||
git clone https://gitee.com/huge-dream/django-vue3-admin.git
|
||||
|
||||
# 进入项目目录
|
||||
# enter code dir
|
||||
cd web
|
||||
|
||||
# 安装依赖
|
||||
npm install --registry=https://registry.npm.taobao.org
|
||||
# install dependence
|
||||
npm install yarn
|
||||
yarn install --registry=https://registry.npm.taobao.org
|
||||
|
||||
# 启动服务
|
||||
npm run dev
|
||||
# 浏览器访问 http://localhost:8080
|
||||
# .env.development 文件中可配置启动端口等参数
|
||||
# 构建生产环境
|
||||
# npm run build
|
||||
# Start service
|
||||
yarn run dev
|
||||
# Visit http://localhost:8080 in your browser
|
||||
# Parameters such as boot port can be configured in the #.env.development file
|
||||
# Build the production environment
|
||||
# yarn run build
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 后端💈
|
||||
## backend💈
|
||||
|
||||
~~~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
|
||||
或使用 daphne :
|
||||
daphne -b 0.0.0.0 -p 8000 application.asgi:application
|
||||
1. enter code dir cd backend
|
||||
2. copy ./conf/env.example.py to ./conf dir,rename as env.py
|
||||
3. in env.py configure database information
|
||||
mysql database recommended version: 8.0
|
||||
mysql database character set: utf8mb4
|
||||
4. install pip dependence
|
||||
pip3 install -r requirements.txt
|
||||
5. Execute the migration command:
|
||||
python3 manage.py makemigrations
|
||||
python3 manage.py migrate
|
||||
6. Initialization data
|
||||
python3 manage.py init
|
||||
7. Initialize provincial, municipal and county data:
|
||||
python3 manage.py init_area
|
||||
8. start backend
|
||||
python3 manage.py runserver 0.0.0.0:8000
|
||||
or uvicorn :
|
||||
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8
|
||||
~~~
|
||||
|
||||
### 访问项目
|
||||
### visit backend swagger
|
||||
|
||||
- 访问地址:[http://localhost:8080](http://localhost:8080) (默认为此地址,如有修改请按照配置文件)
|
||||
- 账号:`superadmin` 密码:`admin123456`
|
||||
* visit url:[http://localhost:8080](http://localhost:8080) (The default address is this one. If you want to change it, follow the configuration file)
|
||||
* account:`superadmin` password:`admin123456`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### docker-compose 运行
|
||||
### docker-compose
|
||||
|
||||
~~~shell
|
||||
# 先安装docker-compose (自行百度安装),执行此命令等待安装,如有使用celery插件请打开docker-compose.yml中celery 部分注释
|
||||
docker-compose up -d
|
||||
# 初始化后端数据(第一次执行即可)
|
||||
docker exec -ti dvadmin-django bash
|
||||
# Initialize backend data (first execution only)
|
||||
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
|
||||
frontend url:http://127.0.0.1:8080
|
||||
backend url:http://127.0.0.1:8080/api
|
||||
# Change 127.0.0.1 to your own public ip address on the server
|
||||
account:`superadmin` password:`admin123456`
|
||||
|
||||
# docker-compose 停止
|
||||
# docker-compose stop
|
||||
docker-compose down
|
||||
# docker-compose 重启
|
||||
# docker-compose restart
|
||||
docker-compose restart
|
||||
# docker-compose 启动时重新进行 build
|
||||
# docker-compose on start build
|
||||
docker-compose up -d --build
|
||||
~~~
|
||||
|
||||
## Demo screenshot✅
|
||||
|
||||

|
||||
|
||||
## 演示图✅
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
# 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)
|
||||
|
||||
- 二维码
|
||||
|
||||
<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
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
## 演示图✅
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -91,10 +91,11 @@ ENV/
|
||||
**/migrations/*.py
|
||||
!**/migrations/__init__.py
|
||||
*.pyc
|
||||
conf/
|
||||
conf/*
|
||||
!conf/env.example.py
|
||||
db.sqlite3
|
||||
media/
|
||||
__pypackages__/
|
||||
package-lock.json
|
||||
gunicorn.pid
|
||||
!plugins/__init__.py
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import functools
|
||||
import os
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||
|
||||
from django.conf import settings
|
||||
from celery import platforms
|
||||
|
||||
# 租户模式
|
||||
if "django_tenants" in settings.INSTALLED_APPS:
|
||||
from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
|
||||
@@ -16,3 +18,23 @@ else:
|
||||
app.config_from_object('django.conf:settings')
|
||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
||||
platforms.C_FORCE_ROOT = True
|
||||
|
||||
|
||||
def retry_base_task_error():
|
||||
"""
|
||||
celery 失败重试装饰器
|
||||
:return:
|
||||
"""
|
||||
|
||||
def wraps(func):
|
||||
@app.task(bind=True, retry_delay=180, max_retries=3)
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as exc:
|
||||
raise self.retry(exc=exc)
|
||||
|
||||
return wrapper
|
||||
|
||||
return wraps
|
||||
|
||||
@@ -3,6 +3,5 @@ from django.urls import path
|
||||
from application.websocketConfig import MegCenter
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path('ws/<str:service_uid>/', MegCenter.as_asgi()), #consumers.DvadminWebSocket 是该路由的消费者
|
||||
path('ws/<str:service_uid>/', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者
|
||||
]
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
@@ -42,7 +43,8 @@ sys.path.insert(0, os.path.join(PLUGINS_PATH))
|
||||
DEBUG = locals().get("DEBUG", True)
|
||||
ALLOWED_HOSTS = locals().get("ALLOWED_HOSTS", ["*"])
|
||||
|
||||
# Application definition
|
||||
# 列权限需要排除的App应用
|
||||
COLUMN_EXCLUDE_APPS = ['channels', 'captcha'] + locals().get("COLUMN_EXCLUDE_APPS", [])
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.auth",
|
||||
@@ -54,13 +56,14 @@ INSTALLED_APPS = [
|
||||
"rest_framework",
|
||||
"django_filters",
|
||||
"corsheaders", # 注册跨域app
|
||||
"dvadmin.system",
|
||||
"drf_yasg",
|
||||
"captcha",
|
||||
'channels',
|
||||
"channels",
|
||||
"dvadmin.system",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"dvadmin.utils.middleware.HealthCheckMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
@@ -152,6 +155,11 @@ STATICFILES_DIRS = [
|
||||
MEDIA_ROOT = "media" # 项目下的目录
|
||||
MEDIA_URL = "/media/" # 跟STATIC_URL类似,指定用户可以通过这个url找到文件
|
||||
|
||||
#添加以下代码以后就不用写{% load staticfiles %},可以直接引用
|
||||
STATICFILES_FINDERS = (
|
||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder"
|
||||
)
|
||||
# 收集静态文件,必须将 MEDIA_ROOT,STATICFILES_DIRS先注释
|
||||
# python manage.py collectstatic
|
||||
# STATIC_ROOT=os.path.join(BASE_DIR,'static')
|
||||
@@ -165,9 +173,9 @@ CORS_ORIGIN_ALLOW_ALL = True
|
||||
# 允许cookie
|
||||
CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持对cookie的操作
|
||||
|
||||
# ================================================= #
|
||||
# ===================================================== #
|
||||
# ********************* channels配置 ******************* #
|
||||
# ================================================= #
|
||||
# ===================================================== #
|
||||
ASGI_APPLICATION = 'application.asgi.application'
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
@@ -187,69 +195,83 @@ CHANNEL_LAYERS = {
|
||||
# ================================================= #
|
||||
# ********************* 日志配置 ******************* #
|
||||
# ================================================= #
|
||||
|
||||
# log 配置部分BEGIN #
|
||||
# # log 配置部分BEGIN #
|
||||
SERVER_LOGS_FILE = os.path.join(BASE_DIR, "logs", "server.log")
|
||||
ERROR_LOGS_FILE = os.path.join(BASE_DIR, "logs", "error.log")
|
||||
LOGS_FILE = os.path.join(BASE_DIR, "logs")
|
||||
if not os.path.exists(os.path.join(BASE_DIR, "logs")):
|
||||
os.makedirs(os.path.join(BASE_DIR, "logs"))
|
||||
|
||||
# 格式:[2020-04-22 23:33:01][micoservice.apps.ready():16] [INFO] 这是一条日志:
|
||||
# 格式:[日期][模块.函数名称():行号] [级别] 信息
|
||||
STANDARD_LOG_FORMAT = (
|
||||
"[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s"
|
||||
)
|
||||
CONSOLE_LOG_FORMAT = (
|
||||
"[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s"
|
||||
)
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"servers": {
|
||||
"format": "%(message)s",
|
||||
"standard": {"format": STANDARD_LOG_FORMAT},
|
||||
"console": {
|
||||
"format": CONSOLE_LOG_FORMAT,
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"format": CONSOLE_LOG_FORMAT,
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"servers": {
|
||||
"logging_levels": ["info", "error", "warning"],
|
||||
"class": "dvadmin.utils.log.InterceptTimedRotatingFileHandler", # 这个路径看你本地放在哪里(下面的log文件)
|
||||
"filename": os.path.join(LOGS_FILE, "server.log"),
|
||||
"when": "D",
|
||||
"interval": 1,
|
||||
"file": {
|
||||
"level": "INFO",
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"filename": SERVER_LOGS_FILE,
|
||||
"maxBytes": 1024 * 1024 * 100, # 100 MB
|
||||
"backupCount": 1,
|
||||
"formatter": "servers",
|
||||
"backupCount": 5, # 最多备份5个
|
||||
"formatter": "standard",
|
||||
"encoding": "utf-8",
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"level": "ERROR",
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"filename": ERROR_LOGS_FILE,
|
||||
"maxBytes": 1024 * 1024 * 100, # 100 MB
|
||||
"backupCount": 3, # 最多备份3个
|
||||
"formatter": "standard",
|
||||
"encoding": "utf-8",
|
||||
},
|
||||
"console": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "console",
|
||||
},
|
||||
|
||||
},
|
||||
"loggers": {
|
||||
# default日志
|
||||
'django': {
|
||||
'handlers': ['servers'],
|
||||
'propagate': False,
|
||||
'level': "INFO"
|
||||
"": {
|
||||
"handlers": ["console", "error", "file"],
|
||||
"level": "INFO",
|
||||
},
|
||||
'': {
|
||||
'handlers': ['servers'],
|
||||
'propagate': False,
|
||||
'level': "ERROR"
|
||||
},
|
||||
'celery': {
|
||||
'handlers': ['servers'],
|
||||
'propagate': False,
|
||||
'level': "INFO"
|
||||
"django": {
|
||||
"handlers": ["console", "error", "file"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
'django.db.backends': {
|
||||
'handlers': ['servers'],
|
||||
'handlers': ["console", "error", "file"],
|
||||
'propagate': False,
|
||||
'level': "INFO"
|
||||
},
|
||||
'django.request': {
|
||||
'handlers': ['servers'],
|
||||
'propagate': False,
|
||||
'level': "DEBUG"
|
||||
},
|
||||
"uvicorn.error": {
|
||||
"level": "INFO",
|
||||
"handlers": ["servers"],
|
||||
"handlers": ["console", "error", "file"],
|
||||
},
|
||||
"uvicorn.access": {
|
||||
"handlers": ["servers"],
|
||||
"level": "INFO",
|
||||
"propagate": False
|
||||
"handlers": ["console", "error", "file"],
|
||||
"level": "INFO"
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -289,11 +311,9 @@ AUTHENTICATION_BACKENDS = ["dvadmin.utils.backends.CustomBackend"]
|
||||
# ================================================= #
|
||||
# ****************** simplejwt配置 ***************** #
|
||||
# ================================================= #
|
||||
from datetime import timedelta
|
||||
|
||||
SIMPLE_JWT = {
|
||||
# token有效时长
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=120),
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=1440),
|
||||
# token刷新后的有效时间
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
|
||||
# 设置前缀
|
||||
@@ -329,11 +349,11 @@ SWAGGER_SETTINGS = {
|
||||
# ================================================= #
|
||||
# **************** 验证码配置 ******************* #
|
||||
# ================================================= #
|
||||
CAPTCHA_IMAGE_SIZE = (160, 60) # 设置 captcha 图片大小
|
||||
CAPTCHA_IMAGE_SIZE = (160, 46) # 设置 captcha 图片大小
|
||||
CAPTCHA_LENGTH = 4 # 字符个数
|
||||
CAPTCHA_TIMEOUT = 1 # 超时(minutes)
|
||||
CAPTCHA_OUTPUT_FORMAT = "%(image)s %(text_field)s %(hidden_field)s "
|
||||
CAPTCHA_FONT_SIZE = 40 # 字体大小
|
||||
CAPTCHA_FONT_SIZE = 36 # 字体大小
|
||||
CAPTCHA_FOREGROUND_COLOR = "#64DAAA" # 前景色
|
||||
CAPTCHA_BACKGROUND_COLOR = "#F5F7F4" # 背景色
|
||||
CAPTCHA_NOISE_FUNCTIONS = (
|
||||
@@ -388,5 +408,7 @@ PLUGINS_URL_PATTERNS = []
|
||||
# from dvadmin_third.settings import * # 第三方用户管理
|
||||
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
|
||||
# from dvadmin_tenants.settings import * # 租户管理
|
||||
#from dvadmin_social_auth.settings import *
|
||||
#from dvadmin_uniapp.settings import *
|
||||
# ...
|
||||
# ********** 一键导入插件配置结束 **********
|
||||
|
||||
@@ -30,6 +30,7 @@ from dvadmin.system.views.login import (
|
||||
CaptchaView,
|
||||
ApiLogin,
|
||||
LogoutView,
|
||||
LoginTokenView
|
||||
)
|
||||
from dvadmin.system.views.system_config import InitSettingsViewSet
|
||||
from dvadmin.utils.swagger import CustomOpenAPISchemaGenerator
|
||||
@@ -52,6 +53,33 @@ schema_view = get_schema_view(
|
||||
permission_classes=(permissions.AllowAny,),
|
||||
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 = (
|
||||
[
|
||||
@@ -81,6 +109,12 @@ urlpatterns = (
|
||||
path("api/init/dictionary/", InitDictionaryViewSet.as_view()),
|
||||
path("api/init/settings/", InitSettingsViewSet.as_view()),
|
||||
path("apiLogin/", ApiLogin.as_view()),
|
||||
|
||||
# 仅用于开发,上线需关闭
|
||||
path("api/token/", LoginTokenView.as_view()),
|
||||
# 前端页面映射
|
||||
path('web/', web_view, name='web_view'),
|
||||
path('web/<path:filename>', serve_web_files, name='serve_web_files'),
|
||||
]
|
||||
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
+ static(settings.STATIC_URL, document_root=settings.STATIC_URL)
|
||||
|
||||
@@ -73,7 +73,7 @@ class DvadminWebSocket(AsyncJsonWebsocketConsumer):
|
||||
unread_count = await _get_message_unread(self.user_id)
|
||||
if unread_count == 0:
|
||||
# 发送连接成功
|
||||
await self.send_json(set_message('system', 'SYSTEM', '连接成功'))
|
||||
await self.send_json(set_message('system', 'SYSTEM', '您已上线'))
|
||||
else:
|
||||
await self.send_json(
|
||||
set_message('system', 'SYSTEM', "请查看您的未读消息~",
|
||||
@@ -122,7 +122,8 @@ class MessageCreateSerializer(CustomModelSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
def websocket_push(user_id,message):
|
||||
|
||||
def websocket_push(user_id, message):
|
||||
username = "user_" + str(user_id)
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
@@ -133,8 +134,9 @@ def websocket_push(user_id,message):
|
||||
}
|
||||
)
|
||||
|
||||
def create_message_push(title: str, content: str, target_type: int=0, target_user: list=[], target_dept=None, target_role=None,
|
||||
message: dict = {'contentType': 'INFO', 'content': '测试~'}, request= Request):
|
||||
|
||||
def create_message_push(title: str, content: str, target_type: int = 0, target_user: list = None, target_dept=None,
|
||||
target_role=None, message: dict = None, request=Request):
|
||||
if message is None:
|
||||
message = {"contentType": "INFO", "content": None}
|
||||
if target_role is None:
|
||||
@@ -145,11 +147,11 @@ def create_message_push(title: str, content: str, target_type: int=0, target_use
|
||||
"title": title,
|
||||
"content": content,
|
||||
"target_type": target_type,
|
||||
"target_user":target_user,
|
||||
"target_dept":target_dept,
|
||||
"target_role":target_role
|
||||
"target_user": target_user,
|
||||
"target_dept": target_dept,
|
||||
"target_role": target_role
|
||||
}
|
||||
message_center_instance = MessageCreateSerializer(data=data,request=request)
|
||||
message_center_instance = MessageCreateSerializer(data=data, request=request)
|
||||
message_center_instance.is_valid(raise_exception=True)
|
||||
message_center_instance.save()
|
||||
users = target_user or []
|
||||
@@ -176,6 +178,6 @@ def create_message_push(title: str, content: str, target_type: int=0, target_use
|
||||
username,
|
||||
{
|
||||
"type": "push.message",
|
||||
"json": {**message,'unread':unread_count}
|
||||
"json": {**message, 'unread': unread_count}
|
||||
}
|
||||
)
|
||||
|
||||
+13
-12
@@ -7,30 +7,32 @@ from application.settings import BASE_DIR
|
||||
# ================================================= #
|
||||
# 数据库 ENGINE ,默认演示使用 sqlite3 数据库,正式环境建议使用 mysql 数据库
|
||||
# sqlite3 设置
|
||||
DATABASE_ENGINE = "django.db.backends.sqlite3"
|
||||
DATABASE_NAME = os.path.join(BASE_DIR, "db.sqlite3")
|
||||
# DATABASE_ENGINE = "django.db.backends.sqlite3"
|
||||
# DATABASE_NAME = os.path.join(BASE_DIR, "db.sqlite3")
|
||||
|
||||
# 使用mysql时,改为此配置
|
||||
# DATABASE_ENGINE = "django.db.backends.mysql"
|
||||
# DATABASE_NAME = 'django-vue-admin' # mysql 时使用
|
||||
DATABASE_ENGINE = "django.db.backends.mysql"
|
||||
DATABASE_NAME = 'django-vue3-admin' # mysql 时使用
|
||||
|
||||
# 数据库地址 改为自己数据库地址
|
||||
DATABASE_HOST = "127.0.0.1"
|
||||
DATABASE_HOST = '127.0.0.1'
|
||||
# # 数据库端口
|
||||
DATABASE_PORT = 3306
|
||||
# # 数据库用户名
|
||||
DATABASE_USER = "root"
|
||||
# # 数据库密码
|
||||
DATABASE_PASSWORD = "123456"
|
||||
DATABASE_PASSWORD = 'DVADMIN3'
|
||||
|
||||
# 表前缀
|
||||
TABLE_PREFIX = "dvadmin_"
|
||||
# ================================================= #
|
||||
# ******** redis配置,无redis 可不进行配置 ******** #
|
||||
# ================================================= #
|
||||
# REDIS_PASSWORD = ''
|
||||
# REDIS_HOST = '127.0.0.1'
|
||||
# REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6380'
|
||||
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'
|
||||
# ================================================= #
|
||||
# ****************** 功能 启停 ******************* #
|
||||
# ================================================= #
|
||||
@@ -44,6 +46,5 @@ LOGIN_NO_CAPTCHA_AUTH = True
|
||||
# ================================================= #
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# daphne启动命令
|
||||
#daphne application.asgi:application -b 0.0.0.0 -p 8000
|
||||
# 列权限中排除App应用
|
||||
COLUMN_EXCLUDE_APPS = []
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
exclude = ["venv"] # 需要排除的文件目录
|
||||
for root, dirs, files in os.walk('.'):
|
||||
dirs[:] = [d for d in set(dirs) - set(exclude)]
|
||||
dirs[:] = list(set(dirs) - set(exclude))
|
||||
if 'migrations' in dirs:
|
||||
dir = dirs[dirs.index('migrations')]
|
||||
for root_j, dirs_j, files_j in os.walk(os.path.join(root, dir)):
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
# python manage.py makemigrations
|
||||
# python manage.py migrate
|
||||
# python manage.py init -y
|
||||
gunicorn -c gunicorn_conf.py application.asgi:application
|
||||
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 4
|
||||
|
||||
@@ -5,9 +5,13 @@ from rest_framework import serializers
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
from dvadmin.system.models import Role, Dept, Users, Menu, MenuButton, ApiWhiteList, Dictionary, SystemConfig, \
|
||||
RoleMenuPermission, RoleMenuButtonPermission
|
||||
from dvadmin.system.models import (
|
||||
Role, Dept, Users, Menu, MenuButton,
|
||||
ApiWhiteList, Dictionary, SystemConfig,
|
||||
RoleMenuPermission, RoleMenuButtonPermission, MenuField
|
||||
)
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
|
||||
|
||||
@@ -50,6 +54,16 @@ class MenuButtonInitSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class MenuFieldInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化列权限-序列化器
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = MenuField
|
||||
fields = ['id', 'menu', 'field_name', 'title', 'model']
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class MenuInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
@@ -58,6 +72,7 @@ class MenuInitSerializer(CustomModelSerializer):
|
||||
name = serializers.CharField(required=False)
|
||||
children = serializers.SerializerMethodField()
|
||||
menu_button = serializers.SerializerMethodField()
|
||||
menu_field = serializers.SerializerMethodField()
|
||||
|
||||
def get_children(self, obj: Menu):
|
||||
data = []
|
||||
@@ -74,10 +89,18 @@ class MenuInitSerializer(CustomModelSerializer):
|
||||
data = list(instance.values('name', 'value', 'api', 'method'))
|
||||
return data
|
||||
|
||||
def get_menu_field(self, obj: Menu):
|
||||
data = []
|
||||
instance = obj.menufield_set.order_by('field_name')
|
||||
if instance:
|
||||
data = list(instance.values('field_name', 'title', 'model'))
|
||||
return data
|
||||
|
||||
def save(self, **kwargs):
|
||||
instance = super().save(**kwargs)
|
||||
children = self.initial_data.get('children')
|
||||
menu_button = self.initial_data.get('menu_button')
|
||||
menu_field = self.initial_data.get('menu_field')
|
||||
# 菜单表
|
||||
if children:
|
||||
for menu_data in children:
|
||||
@@ -106,12 +129,25 @@ class MenuInitSerializer(CustomModelSerializer):
|
||||
serializer = MenuButtonInitSerializer(instance_obj, data=menu_button_data, request=self.request)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
# 列权限
|
||||
if menu_field:
|
||||
for field_data in menu_field:
|
||||
field_data['menu'] = instance.id
|
||||
filter_data = {
|
||||
'menu': field_data['menu'],
|
||||
'field_name': field_data['field_name'],
|
||||
'model': field_data['model']
|
||||
}
|
||||
instance_obj = MenuField.objects.filter(**filter_data).first()
|
||||
serializer = MenuFieldInitSerializer(instance_obj, data=field_data, request=self.request)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ['name', 'icon', 'sort', 'is_link', 'is_catalog', 'web_path', 'component', 'component_name', 'status',
|
||||
'cache', 'visible', 'parent', 'children', 'menu_button', 'creator', 'dept_belong_id']
|
||||
'cache', 'visible', 'parent', 'children', 'menu_button', 'menu_field', 'creator', 'dept_belong_id']
|
||||
extra_kwargs = {
|
||||
'creator': {'write_only': True},
|
||||
'dept_belong_id': {'write_only': True}
|
||||
@@ -126,7 +162,7 @@ class RoleInitSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ['name', 'key', 'sort', 'status', 'admin',
|
||||
fields = ['name', 'key', 'sort', 'status',
|
||||
'creator', 'dept_belong_id']
|
||||
read_only_fields = ["id"]
|
||||
extra_kwargs = {
|
||||
@@ -139,22 +175,24 @@ 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(max_length=100, required=True)
|
||||
menu__web_path = serializers.CharField(max_length=100, required=True)
|
||||
menu__component_name = serializers.CharField(max_length=100, required=True, allow_blank=True)
|
||||
|
||||
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()
|
||||
validated_data.pop('menu__web_path')
|
||||
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(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},
|
||||
@@ -168,25 +206,30 @@ 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(max_length=100, required=True)
|
||||
menu_button__value = serializers.CharField(max_length=100, required=True)
|
||||
data_range = serializers.CharField(max_length=100, required=False)
|
||||
|
||||
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()
|
||||
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()
|
||||
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 self.instance and 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},
|
||||
@@ -196,7 +239,6 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ApiWhiteListInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化获取数信息(用于生成初始化json文件)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@
|
||||
"key": "admin",
|
||||
"sort": 1,
|
||||
"status": true,
|
||||
"admin": true,
|
||||
"remark": null
|
||||
},
|
||||
{
|
||||
@@ -12,7 +11,6 @@
|
||||
"key": "public",
|
||||
"sort": 2,
|
||||
"status": true,
|
||||
"admin": true,
|
||||
"remark": null
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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": "开启验证码",
|
||||
@@ -65,6 +93,20 @@
|
||||
"placeholder": null,
|
||||
"setting": null,
|
||||
"children": [
|
||||
{
|
||||
"parent": 1,
|
||||
"title": "网站标题",
|
||||
"key": "site_title",
|
||||
"value": "Dvadmin",
|
||||
"sort": 1,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [],
|
||||
"placeholder": "请输入网站标题",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"parent": 1,
|
||||
"title": "网站名称",
|
||||
@@ -116,7 +158,7 @@
|
||||
"parent": 1,
|
||||
"title": "版权信息",
|
||||
"key": "copyright",
|
||||
"value": "2021-2022 django-vue-admin.com 版权所有",
|
||||
"value": "2021-2024 django-vue-admin.com 版权所有",
|
||||
"sort": 4,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
@@ -168,7 +210,7 @@
|
||||
"parent": 1,
|
||||
"title": "隐私链接",
|
||||
"key": "privacy_url",
|
||||
"value": "#",
|
||||
"value": "/api/system/clause/privacy.html",
|
||||
"sort": 7,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
@@ -182,7 +224,7 @@
|
||||
"parent": 1,
|
||||
"title": "条款链接",
|
||||
"key": "clause_url",
|
||||
"value": "#",
|
||||
"value": "/api/system/clause/terms_service.html",
|
||||
"sort": 8,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"gender": 1,
|
||||
"user_type": 0,
|
||||
"role": [],
|
||||
"dept_key": "dvadmin",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"is_staff": true,
|
||||
|
||||
@@ -8,9 +8,11 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "application.settings")
|
||||
django.setup()
|
||||
|
||||
from dvadmin.utils.core_initialize import CoreInitialize
|
||||
from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
|
||||
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
|
||||
RoleMenuInitSerializer, RoleMenuButtonInitSerializer
|
||||
from dvadmin.system.fixtures.initSerializer import (
|
||||
UsersInitSerializer, DeptInitSerializer, RoleInitSerializer,
|
||||
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer,
|
||||
SystemConfigInitSerializer, RoleMenuInitSerializer, RoleMenuButtonInitSerializer
|
||||
)
|
||||
|
||||
|
||||
class Initialize(CoreInitialize):
|
||||
@@ -43,14 +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):
|
||||
"""
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -5,12 +5,7 @@ from django.contrib.auth.models import AbstractUser, UserManager
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from application import dispatch
|
||||
from dvadmin.utils.models import CoreModel, table_prefix
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(0, "禁用"),
|
||||
(1, "启用"),
|
||||
)
|
||||
from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models
|
||||
|
||||
|
||||
class Role(CoreModel):
|
||||
@@ -18,7 +13,6 @@ class Role(CoreModel):
|
||||
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
|
||||
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
|
||||
status = models.BooleanField(default=True, verbose_name="角色状态", help_text="角色状态")
|
||||
admin = models.BooleanField(default=False, verbose_name="是否为admin", help_text="是否为admin")
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_role"
|
||||
@@ -76,6 +70,7 @@ class Users(CoreModel, AbstractUser):
|
||||
blank=True,
|
||||
help_text="关联部门",
|
||||
)
|
||||
login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数")
|
||||
objects = CustomUserManager()
|
||||
|
||||
def set_password(self, raw_password):
|
||||
@@ -107,8 +102,7 @@ class Post(CoreModel):
|
||||
|
||||
class Dept(CoreModel):
|
||||
name = models.CharField(max_length=64, verbose_name="部门名称", help_text="部门名称")
|
||||
key = models.CharField(max_length=64, unique=True, null=True, blank=True, verbose_name="关联字符",
|
||||
help_text="关联字符")
|
||||
key = models.CharField(max_length=64, unique=True, null=True, blank=True, verbose_name="关联字符", help_text="关联字符")
|
||||
sort = models.IntegerField(default=1, verbose_name="显示排序", help_text="显示排序")
|
||||
owner = models.CharField(max_length=32, verbose_name="负责人", null=True, blank=True, help_text="负责人")
|
||||
phone = models.CharField(max_length=32, verbose_name="联系电话", null=True, blank=True, help_text="联系电话")
|
||||
@@ -169,6 +163,7 @@ class Menu(CoreModel):
|
||||
(1, "是"),
|
||||
)
|
||||
is_link = models.BooleanField(default=False, verbose_name="是否外链", help_text="是否外链")
|
||||
link_url = models.CharField(max_length=255, verbose_name="链接地址", null=True, blank=True, help_text="链接地址")
|
||||
is_catalog = models.BooleanField(default=False, verbose_name="是否目录", help_text="是否目录")
|
||||
web_path = models.CharField(max_length=128, verbose_name="路由地址", null=True, blank=True, help_text="路由地址")
|
||||
component = models.CharField(max_length=128, verbose_name="组件地址", null=True, blank=True, help_text="组件地址")
|
||||
@@ -178,13 +173,59 @@ class Menu(CoreModel):
|
||||
cache = models.BooleanField(default=False, blank=True, verbose_name="是否页面缓存", help_text="是否页面缓存")
|
||||
visible = models.BooleanField(default=True, blank=True, verbose_name="侧边栏中是否显示",
|
||||
help_text="侧边栏中是否显示")
|
||||
is_iframe = models.BooleanField(default=False, blank=True, verbose_name="框架外显示", help_text="框架外显示")
|
||||
is_affix = models.BooleanField(default=False, blank=True, verbose_name="是否固定", help_text="是否固定")
|
||||
|
||||
@classmethod
|
||||
def get_all_parent(cls, id: int, all_list=None, nodes=None):
|
||||
"""
|
||||
递归获取给定ID的所有层级
|
||||
:param id: 参数ID
|
||||
:param all_list: 所有列表
|
||||
:param nodes: 递归列表
|
||||
:return: nodes
|
||||
"""
|
||||
if not all_list:
|
||||
all_list = Menu.objects.values("id", "name", "parent")
|
||||
if nodes is None:
|
||||
nodes = []
|
||||
for ele in all_list:
|
||||
if ele.get("id") == id:
|
||||
parent_id = ele.get("parent")
|
||||
if parent_id is not None:
|
||||
cls.get_all_parent(parent_id, all_list, nodes)
|
||||
nodes.append(ele)
|
||||
return nodes
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_menu"
|
||||
verbose_name = "菜单表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("sort",)
|
||||
|
||||
class MenuField(CoreModel):
|
||||
model = models.CharField(max_length=64, verbose_name='表名')
|
||||
menu = models.ForeignKey(to='Menu', on_delete=models.CASCADE, verbose_name='菜单', db_constraint=False)
|
||||
field_name = models.CharField(max_length=64, verbose_name='模型表字段名')
|
||||
title = models.CharField(max_length=64, verbose_name='字段显示名')
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_menu_field"
|
||||
verbose_name = "菜单字段表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("id",)
|
||||
|
||||
class FieldPermission(CoreModel):
|
||||
role = models.ForeignKey(to='Role', on_delete=models.CASCADE, verbose_name='角色', db_constraint=False)
|
||||
field = models.ForeignKey(to='MenuField', on_delete=models.CASCADE,related_name='menu_field', verbose_name='字段', db_constraint=False)
|
||||
is_query = models.BooleanField(default=1, verbose_name='是否可查询')
|
||||
is_create = models.BooleanField(default=1, verbose_name='是否可创建')
|
||||
is_update = models.BooleanField(default=1, verbose_name='是否可更新')
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_field_permission"
|
||||
verbose_name = "字段权限表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("id",)
|
||||
|
||||
|
||||
class MenuButton(CoreModel):
|
||||
menu = models.ForeignKey(
|
||||
@@ -236,7 +277,7 @@ class RoleMenuPermission(CoreModel):
|
||||
db_table = table_prefix + "role_menu_permission"
|
||||
verbose_name = "角色菜单权限表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("-create_datetime",)
|
||||
# ordering = ("-create_datetime",)
|
||||
|
||||
|
||||
class RoleMenuButtonPermission(CoreModel):
|
||||
@@ -289,8 +330,7 @@ class Dictionary(CoreModel):
|
||||
(7, "images"),
|
||||
)
|
||||
label = models.CharField(max_length=100, blank=True, null=True, verbose_name="字典名称", help_text="字典名称")
|
||||
value = models.CharField(max_length=200, blank=True, null=True, verbose_name="字典编号",
|
||||
help_text="字典编号/实际值")
|
||||
value = models.CharField(max_length=200, blank=True, null=True, verbose_name="字典编号", help_text="字典编号/实际值")
|
||||
parent = models.ForeignKey(
|
||||
to="self",
|
||||
related_name="sublist",
|
||||
@@ -359,7 +399,11 @@ def media_file_name(instance, filename):
|
||||
|
||||
class FileList(CoreModel):
|
||||
name = models.CharField(max_length=200, null=True, blank=True, verbose_name="名称", help_text="名称")
|
||||
url = models.FileField(upload_to=media_file_name)
|
||||
url = models.FileField(upload_to=media_file_name, null=True, blank=True,)
|
||||
file_url = models.CharField(max_length=255, blank=True, verbose_name="文件地址", help_text="文件地址")
|
||||
engine = models.CharField(max_length=100, default='local', blank=True, verbose_name="引擎", help_text="引擎")
|
||||
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")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -368,6 +412,11 @@ class FileList(CoreModel):
|
||||
for chunk in self.url.chunks():
|
||||
md5.update(chunk)
|
||||
self.md5sum = md5.hexdigest()
|
||||
if not self.size:
|
||||
self.size = self.url.size
|
||||
if not self.file_url:
|
||||
url = media_file_name(self, self.name)
|
||||
self.file_url = f'media/{url}'
|
||||
super(FileList, self).save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
@@ -437,7 +486,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="启用状态")
|
||||
|
||||
@@ -1 +1,56 @@
|
||||
from functools import wraps
|
||||
|
||||
from django.db.models import Func, F, OuterRef, Exists
|
||||
from django.test import TestCase
|
||||
import django
|
||||
import os
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "application.settings")
|
||||
django.setup()
|
||||
from dvadmin.system.models import Menu, RoleMenuPermission, RoleMenuButtonPermission, MenuButton
|
||||
|
||||
|
||||
import time
|
||||
|
||||
def timing_decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
result = func(*args, **kwargs)
|
||||
end_time = time.time()
|
||||
run_time = end_time - start_time
|
||||
print(f"{func.__name__} ran in {run_time:.6f} seconds")
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
@timing_decorator
|
||||
def getMenu():
|
||||
data = []
|
||||
queryset = Menu.objects.filter(status=1, is_catalog=False).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=1,
|
||||
).exists()
|
||||
mbCheck = RoleMenuButtonPermission.objects.filter(
|
||||
menu_button = OuterRef("pk"),
|
||||
role__id=1,
|
||||
)
|
||||
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'))
|
||||
# print(b)
|
||||
dicts = {
|
||||
'name': completeName,
|
||||
'id': item['id'],
|
||||
'isCheck': isCheck,
|
||||
'btns':btns
|
||||
}
|
||||
print(dicts)
|
||||
data.append(dicts)
|
||||
# print(data)
|
||||
|
||||
if __name__ == '__main__':
|
||||
getMenu()
|
||||
@@ -3,6 +3,7 @@ from rest_framework import routers
|
||||
|
||||
from dvadmin.system.views.api_white_list import ApiWhiteListViewSet
|
||||
from dvadmin.system.views.area import AreaViewSet
|
||||
from dvadmin.system.views.clause import PrivacyView, TermsServiceView
|
||||
from dvadmin.system.views.dept import DeptViewSet
|
||||
from dvadmin.system.views.dictionary import DictionaryViewSet
|
||||
from dvadmin.system.views.file_list import FileViewSet
|
||||
@@ -16,6 +17,7 @@ from dvadmin.system.views.role_menu import RoleMenuPermissionViewSet
|
||||
from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermissionViewSet
|
||||
from dvadmin.system.views.system_config import SystemConfigViewSet
|
||||
from dvadmin.system.views.user import UserViewSet
|
||||
from dvadmin.system.views.menu_field import MenuFieldViewSet
|
||||
|
||||
system_url = routers.SimpleRouter()
|
||||
system_url.register(r'menu', MenuViewSet)
|
||||
@@ -29,11 +31,10 @@ system_url.register(r'area', AreaViewSet)
|
||||
system_url.register(r'file', FileViewSet)
|
||||
system_url.register(r'api_white_list', ApiWhiteListViewSet)
|
||||
system_url.register(r'system_config', SystemConfigViewSet)
|
||||
system_url.register(r'message_center',MessageCenterViewSet)
|
||||
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)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -46,5 +47,7 @@ urlpatterns = [
|
||||
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()),
|
||||
]
|
||||
urlpatterns += system_url.urls
|
||||
|
||||
@@ -60,12 +60,9 @@ class AreaViewSet(CustomModelViewSet):
|
||||
del params['page']
|
||||
if limit:
|
||||
del params['limit']
|
||||
if params:
|
||||
if pcode:
|
||||
queryset = self.queryset.filter(enable=True, pcode=pcode)
|
||||
else:
|
||||
queryset = self.queryset.filter(enable=True)
|
||||
if params and pcode:
|
||||
queryset = self.queryset.filter(enable=True, pcode=pcode)
|
||||
else:
|
||||
queryset = self.queryset.filter(enable=True, pcode__isnull=True)
|
||||
queryset = self.queryset.filter(enable=True)
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from rest_framework.views import APIView
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
class PrivacyView(APIView):
|
||||
"""
|
||||
后台隐私政策
|
||||
"""
|
||||
permission_classes = []
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return render(request, 'privacy.html')
|
||||
|
||||
|
||||
|
||||
class TermsServiceView(APIView):
|
||||
"""
|
||||
后台服务条款
|
||||
"""
|
||||
permission_classes = []
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return render(request, 'terms_service.html')
|
||||
@@ -9,9 +9,8 @@ from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import Dept
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.permission import AnonymousUserPermission
|
||||
from dvadmin.system.models import Dept, RoleMenuButtonPermission, Users
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -25,6 +24,11 @@ class DeptSerializer(CustomModelSerializer):
|
||||
has_children = serializers.SerializerMethodField()
|
||||
hasChild = serializers.SerializerMethodField()
|
||||
|
||||
dept_user_count = serializers.SerializerMethodField()
|
||||
|
||||
def get_dept_user_count(self, obj: Dept):
|
||||
return Users.objects.filter(dept=obj).count()
|
||||
|
||||
def get_hasChild(self, instance):
|
||||
hasChild = Dept.objects.filter(parent=instance.id)
|
||||
if hasChild:
|
||||
@@ -56,18 +60,18 @@ class DeptImportSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class DeptCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
部门管理 创建/更新时的列化器
|
||||
"""
|
||||
|
||||
def create(self, validated_data):
|
||||
value = validated_data.get('parent',None)
|
||||
value = validated_data.get('parent', None)
|
||||
if value is None:
|
||||
validated_data['parent'] = self.request.user.dept
|
||||
dept_obj = Dept.objects.filter(parent=self.request.user.dept).order_by('-sort').first()
|
||||
last_sort = dept_obj.sort if dept_obj else 0
|
||||
validated_data['sort'] = last_sort + 1
|
||||
instance = super().create(validated_data)
|
||||
instance.dept_belong_id = instance.id
|
||||
instance.save()
|
||||
@@ -111,13 +115,10 @@ class DeptViewSet(CustomModelViewSet):
|
||||
del params['page']
|
||||
if limit:
|
||||
del params['limit']
|
||||
if params:
|
||||
if parent:
|
||||
queryset = self.queryset.filter(status=True, parent=parent)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=True)
|
||||
if params and parent:
|
||||
queryset = self.queryset.filter(status=True, parent=parent)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=True, parent__isnull=True)
|
||||
queryset = self.queryset.filter(status=True)
|
||||
queryset = self.filter_queryset(queryset)
|
||||
serializer = DeptSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
@@ -130,27 +131,107 @@ class DeptViewSet(CustomModelViewSet):
|
||||
if is_superuser:
|
||||
queryset = Dept.objects.values('id', 'name', 'parent')
|
||||
else:
|
||||
data_range = request.user.role.values_list('data_range', flat=True)
|
||||
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]:
|
||||
if item in [0, 2]:
|
||||
dept_list = [user_dept_id]
|
||||
elif item == 1:
|
||||
dept_list = Dept.recursion_dept_info(dept_id=user_dept_id)
|
||||
dept_list = Dept.recursion_all_dept(dept_id=user_dept_id)
|
||||
elif item == 3:
|
||||
dept_list = Dept.objects.values_list('id',flat=True)
|
||||
dept_list = Dept.objects.values_list('id', flat=True)
|
||||
elif item == 4:
|
||||
dept_list = request.user.role.values_list('dept',flat=True)
|
||||
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], extra_filter_class=[])
|
||||
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')
|
||||
return DetailResponse(data=data, msg="获取成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def move_up(self, request):
|
||||
"""部门上移"""
|
||||
dept_id = request.data.get('dept_id')
|
||||
try:
|
||||
dept = Dept.objects.get(id=dept_id)
|
||||
except Dept.DoesNotExist:
|
||||
return ErrorResponse(msg="部门不存在")
|
||||
previous_menu = Dept.objects.filter(sort__lt=dept.sort, parent=dept.parent).order_by('-sort').first()
|
||||
if previous_menu:
|
||||
previous_menu.sort, dept.sort = dept.sort, previous_menu.sort
|
||||
previous_menu.save()
|
||||
dept.save()
|
||||
return SuccessResponse(data=[], msg="上移成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def move_down(self, request):
|
||||
"""部门下移"""
|
||||
dept_id = request.data['dept_id']
|
||||
try:
|
||||
dept = Dept.objects.get(id=dept_id)
|
||||
except Dept.DoesNotExist:
|
||||
return ErrorResponse(msg="部门不存在")
|
||||
next_menu = Dept.objects.filter(sort__gt=dept.sort, parent=dept.parent).order_by('sort').first()
|
||||
if next_menu:
|
||||
next_menu.sort, dept.sort = dept.sort, next_menu.sort
|
||||
next_menu.save()
|
||||
dept.save()
|
||||
return SuccessResponse(data=[], msg="下移成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def dept_info(self, request):
|
||||
"""部门信息"""
|
||||
def inner(did, li):
|
||||
sub = Dept.objects.filter(parent_id=did)
|
||||
if not sub.exists():
|
||||
return li
|
||||
for i in sub:
|
||||
li.append(i.pk)
|
||||
inner(i, li)
|
||||
return li
|
||||
dept_id = request.query_params.get('dept_id')
|
||||
show_all = request.query_params.get('show_all')
|
||||
if dept_id is None:
|
||||
return ErrorResponse(msg="部门不存在")
|
||||
if not show_all:
|
||||
show_all = 0
|
||||
if int(show_all): # 递归当前部门下的所有部门,查询用户
|
||||
all_did = [dept_id]
|
||||
inner(dept_id, all_did)
|
||||
users = Users.objects.filter(dept_id__in=all_did)
|
||||
else:
|
||||
if dept_id != '':
|
||||
users = Users.objects.filter(dept_id=dept_id)
|
||||
else:
|
||||
users = Users.objects.none()
|
||||
dept_obj = Dept.objects.get(id=dept_id) if dept_id != '' else None
|
||||
sub_dept = Dept.objects.filter(parent_id=dept_obj.pk) if dept_id != '' else []
|
||||
data = {
|
||||
'dept_name': dept_obj and dept_obj.name,
|
||||
'dept_user': users.count(),
|
||||
'owner': dept_obj and dept_obj.owner,
|
||||
'description': dept_obj and dept_obj.description,
|
||||
'gender': {
|
||||
'male': users.filter(gender=1).count(),
|
||||
'female': users.filter(gender=2).count(),
|
||||
'unknown': users.filter(gender=0).count(),
|
||||
},
|
||||
'sub_dept_map': []
|
||||
}
|
||||
for dept in sub_dept:
|
||||
all_did = [dept.pk]
|
||||
inner(dept.pk, all_did)
|
||||
sub_data = {
|
||||
'name': dept.name,
|
||||
'count': Users.objects.filter(dept_id__in=all_did).count()
|
||||
}
|
||||
data['sub_dept_map'].append(sub_data)
|
||||
return SuccessResponse(data)
|
||||
|
||||
@@ -34,6 +34,19 @@ class DictionaryCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
字典管理 创建/更新时的列化器
|
||||
"""
|
||||
value = serializers.CharField(max_length=100)
|
||||
|
||||
def validate_value(self, value):
|
||||
"""
|
||||
在父级的字典编号验证重复性
|
||||
"""
|
||||
initial_data = self.initial_data
|
||||
parent = initial_data.get('parent',None)
|
||||
if parent is None:
|
||||
unique = Dictionary.objects.filter(value=value).exists()
|
||||
if unique:
|
||||
raise serializers.ValidationError("字典编号不能重复")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = Dictionary
|
||||
@@ -51,20 +64,24 @@ class DictionaryViewSet(CustomModelViewSet):
|
||||
"""
|
||||
queryset = Dictionary.objects.all()
|
||||
serializer_class = DictionarySerializer
|
||||
create_serializer_class = DictionaryCreateUpdateSerializer
|
||||
extra_filter_class = []
|
||||
search_fields = ['label']
|
||||
|
||||
def get_queryset(self):
|
||||
params = self.request.query_params
|
||||
parent = params.get('parent', None)
|
||||
if params:
|
||||
if parent:
|
||||
queryset = self.queryset.filter(status=1, parent=parent)
|
||||
if self.action =='list':
|
||||
params = self.request.query_params
|
||||
parent = params.get('parent', None)
|
||||
if params:
|
||||
if parent:
|
||||
queryset = self.queryset.filter(parent=parent)
|
||||
else:
|
||||
queryset = self.queryset.filter(parent__isnull=True)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=1, parent__isnull=True)
|
||||
queryset = self.queryset.filter(parent__isnull=True)
|
||||
return queryset
|
||||
else:
|
||||
queryset = self.queryset.filter(status=1, parent__isnull=True)
|
||||
return queryset
|
||||
return self.queryset
|
||||
|
||||
|
||||
class InitDictionaryViewSet(APIView):
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import hashlib
|
||||
import mimetypes
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from application import dispatch
|
||||
from dvadmin.system.models import FileList
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -8,15 +15,52 @@ class FileSerializer(CustomModelSerializer):
|
||||
url = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_url(self, instance):
|
||||
return f'media/{str(instance.url)}'
|
||||
# return 'media/' + str(instance.url)
|
||||
return instance.file_url or (f'media/{str(instance.url)}')
|
||||
|
||||
class Meta:
|
||||
model = FileList
|
||||
fields = "__all__"
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['name'] = str(self.initial_data.get('file'))
|
||||
validated_data['url'] = self.initial_data.get('file')
|
||||
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local'
|
||||
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup")
|
||||
file = self.initial_data.get('file')
|
||||
file_size = file.size
|
||||
validated_data['name'] = str(file)
|
||||
validated_data['size'] = file_size
|
||||
md5 = hashlib.md5()
|
||||
for chunk in file.chunks():
|
||||
md5.update(chunk)
|
||||
validated_data['md5sum'] = md5.hexdigest()
|
||||
validated_data['engine'] = file_engine
|
||||
validated_data['mime_type'] = file.content_type
|
||||
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)
|
||||
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)
|
||||
if file_path:
|
||||
validated_data['file_url'] = file_path
|
||||
else:
|
||||
raise ValueError("上传失败")
|
||||
else:
|
||||
validated_data['url'] = file
|
||||
# 审计字段
|
||||
try:
|
||||
request_user = self.request.user
|
||||
validated_data['dept_belong_id'] = request_user.dept.id
|
||||
validated_data['creator'] = request_user.id
|
||||
validated_data['modifier'] = request_user.id
|
||||
except:
|
||||
pass
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
@@ -32,4 +76,4 @@ class FileViewSet(CustomModelViewSet):
|
||||
queryset = FileList.objects.all()
|
||||
serializer_class = FileSerializer
|
||||
filter_fields = ['name', ]
|
||||
permission_classes = []
|
||||
permission_classes = []
|
||||
@@ -86,28 +86,38 @@ class LoginSerializer(TokenObtainPairSerializer):
|
||||
|
||||
user = Users.objects.get(username=attrs['username'])
|
||||
if not user.is_active:
|
||||
raise CustomValidationError("账号被锁定")
|
||||
raise CustomValidationError("账号已被锁定,联系管理员解锁")
|
||||
try:
|
||||
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,
|
||||
|
||||
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}
|
||||
}
|
||||
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
|
||||
raise CustomValidationError("账号已被锁定,联系管理员解锁")
|
||||
user.save()
|
||||
count = 5 - user.login_error_count
|
||||
raise CustomValidationError(f"账号/密码错误;重试{count}次后将被锁定~")
|
||||
|
||||
|
||||
class LoginView(TokenObtainPairView):
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from dvadmin.system.models import Menu, MenuButton, RoleMenuPermission
|
||||
from dvadmin.utils.json_response import SuccessResponse
|
||||
from dvadmin.system.models import Menu, RoleMenuPermission
|
||||
from dvadmin.system.views.menu_button import MenuButtonSerializer
|
||||
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -23,7 +24,8 @@ class MenuSerializer(CustomModelSerializer):
|
||||
hasChild = serializers.SerializerMethodField()
|
||||
|
||||
def get_menuPermission(self, instance):
|
||||
queryset = instance.menuPermission.order_by('-name').values_list('name', flat=True)
|
||||
queryset = instance.menuPermission.order_by('-name').values('id', 'name', 'value')
|
||||
# MenuButtonSerializer(instance.menuPermission.all(), many=True)
|
||||
if queryset:
|
||||
return queryset
|
||||
else:
|
||||
@@ -47,6 +49,12 @@ class MenuCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
name = serializers.CharField(required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
menu_obj = Menu.objects.filter(parent_id=validated_data.get('parent', None)).order_by('-sort').first()
|
||||
last_sort = menu_obj.sort if menu_obj else 0
|
||||
validated_data['sort'] = last_sort + 1
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = "__all__"
|
||||
@@ -63,8 +71,8 @@ class WebRouterSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = (
|
||||
'id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
|
||||
'component_name', 'cache', 'visible', 'status')
|
||||
'id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link','link_url', 'is_catalog', 'web_path', 'component',
|
||||
'component_name', 'cache', 'visible','is_iframe','is_affix', 'status')
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
@@ -84,45 +92,6 @@ class MenuViewSet(CustomModelViewSet):
|
||||
search_fields = ['name', 'status']
|
||||
filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog']
|
||||
|
||||
# extra_filter_class = []
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def web_router(self, request):
|
||||
"""用于前端获取当前角色的路由"""
|
||||
user = request.user
|
||||
queryset = self.queryset.filter(status=1)
|
||||
if not user.is_superuser:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id')
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def get_all_menu(self, request):
|
||||
"""用于菜单管理获取所有的菜单"""
|
||||
user = request.user
|
||||
queryset = self.queryset.all()
|
||||
if not user.is_superuser:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id')
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[])
|
||||
def drag_menu(self, request):
|
||||
"""前端拖拽菜单之后重写parent"""
|
||||
menu_id = request.data['menu_id']
|
||||
parent_id = request.data['parent_id']
|
||||
menu = Menu.objects.get(id=menu_id)
|
||||
menu.parent_id = parent_id
|
||||
menu.save()
|
||||
|
||||
return SuccessResponse(data=[], msg="拖拽菜单成功")
|
||||
|
||||
def list(self, request):
|
||||
"""懒加载"""
|
||||
request.query_params._mutable = True
|
||||
@@ -145,3 +114,60 @@ class MenuViewSet(CustomModelViewSet):
|
||||
serializer = MenuSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def web_router(self, request):
|
||||
"""用于前端获取当前角色的路由"""
|
||||
user = request.user
|
||||
if user.is_superuser:
|
||||
queryset = self.queryset.filter(status=1)
|
||||
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)
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def get_all_menu(self, request):
|
||||
"""用于菜单管理获取所有的菜单"""
|
||||
user = request.user
|
||||
queryset = self.queryset.all()
|
||||
if not user.is_superuser:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id')
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[])
|
||||
def move_up(self, request):
|
||||
"""菜单上移"""
|
||||
menu_id = request.data.get('menu_id')
|
||||
try:
|
||||
menu = Menu.objects.get(id=menu_id)
|
||||
except Menu.DoesNotExist:
|
||||
return ErrorResponse(msg="菜单不存在")
|
||||
previous_menu = Menu.objects.filter(sort__lt=menu.sort, parent=menu.parent).order_by('-sort').first()
|
||||
if previous_menu:
|
||||
previous_menu.sort, menu.sort = menu.sort, previous_menu.sort
|
||||
previous_menu.save()
|
||||
menu.save()
|
||||
return SuccessResponse(data=[], msg="上移成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[])
|
||||
def move_down(self, request):
|
||||
"""菜单下移"""
|
||||
menu_id = request.data['menu_id']
|
||||
try:
|
||||
menu = Menu.objects.get(id=menu_id)
|
||||
except Menu.DoesNotExist:
|
||||
return ErrorResponse(msg="菜单不存在")
|
||||
next_menu = Menu.objects.filter(sort__gt=menu.sort, parent=menu.parent).order_by('sort').first()
|
||||
if next_menu:
|
||||
next_menu.sort, menu.sort = menu.sort, next_menu.sort
|
||||
next_menu.save()
|
||||
menu.save()
|
||||
return SuccessResponse(data=[], msg="下移成功")
|
||||
|
||||
@@ -11,7 +11,7 @@ from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import MenuButton, RoleMenuButtonPermission
|
||||
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
|
||||
|
||||
@@ -23,7 +23,7 @@ class MenuButtonSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = MenuButton
|
||||
fields = ['id', 'name', 'value', 'api', 'method']
|
||||
fields = ['id', 'name', 'value', 'api', 'method','menu']
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
@@ -49,12 +49,24 @@ class MenuButtonViewSet(CustomModelViewSet):
|
||||
retrieve:单例
|
||||
destroy:删除
|
||||
"""
|
||||
queryset = MenuButton.objects.all()
|
||||
queryset = MenuButton.objects.order_by('create_datetime')
|
||||
serializer_class = MenuButtonSerializer
|
||||
create_serializer_class = MenuButtonCreateUpdateSerializer
|
||||
update_serializer_class = MenuButtonCreateUpdateSerializer
|
||||
extra_filter_class = []
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
重写list方法
|
||||
:param request:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
queryset = self.filter_queryset(self.get_queryset()).order_by('name')
|
||||
serializer = self.get_serializer(queryset, many=True, request=request)
|
||||
return SuccessResponse(serializer.data,msg="获取成功")
|
||||
|
||||
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
|
||||
def menu_button_all_permission(self,request):
|
||||
"""
|
||||
@@ -63,10 +75,34 @@ class MenuButtonViewSet(CustomModelViewSet):
|
||||
:return:
|
||||
"""
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
if is_superuser:
|
||||
queryset = MenuButton.objects.values_list('value',flat=True)
|
||||
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.web_path}/',
|
||||
'method': 1},
|
||||
{'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api{menu_obj.web_path}/{{id}}/',
|
||||
'method': 3},
|
||||
{'menu': menu_obj.id, 'name': '修改', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api{menu_obj.web_path}/{{id}}/',
|
||||
'method': 2},
|
||||
{'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api{menu_obj.web_path}/',
|
||||
'method': 0},
|
||||
{'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api{menu_obj.web_path}/{{id}}/',
|
||||
'method': 0}]
|
||||
serializer = self.get_serializer(data=result_list, many=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return SuccessResponse(serializer.data, msg="批量创建成功")
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.apps import apps
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import Role, MenuField
|
||||
from dvadmin.utils.models import get_custom_app_models
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.json_response import DetailResponse, ErrorResponse, SuccessResponse
|
||||
|
||||
|
||||
class MenuFieldSerializer(CustomModelSerializer):
|
||||
"""
|
||||
列权限序列化器
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = MenuField
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id']
|
||||
|
||||
|
||||
class MenuFieldViewSet(CustomModelViewSet):
|
||||
"""
|
||||
列权限视图集
|
||||
"""
|
||||
queryset = MenuField.objects.order_by('-model')
|
||||
serializer_class = MenuFieldSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
menu = request.query_params.get('menu')
|
||||
if not menu:
|
||||
return SuccessResponse([])
|
||||
queryset = self.filter_queryset(self.get_queryset().filter(menu=menu))
|
||||
serializer = self.get_serializer(queryset, many=True, request=request)
|
||||
return SuccessResponse(data=serializer.data, msg="获取成功")
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
payload = request.data
|
||||
for model in apps.get_models():
|
||||
if payload.get('model') == model.__name__:
|
||||
break
|
||||
else:
|
||||
return ErrorResponse(msg='模型表不存在')
|
||||
|
||||
if MenuField.objects.filter(menu=payload.get('menu'),model=model.__name__, field_name=payload.get('field_name')).exists():
|
||||
return ErrorResponse(msg='‘%s’ 字段权限已有,不可重复创建' % payload.get('title'))
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def get_models(self, request):
|
||||
"""获取所有项目app下的model"""
|
||||
res = []
|
||||
for model in get_custom_app_models():
|
||||
res.append({
|
||||
'app': model['app'],
|
||||
'title': model['verbose'],
|
||||
'key': model['model']
|
||||
})
|
||||
return DetailResponse(res)
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def auto_match_fields(self, request):
|
||||
"""自动匹配已有的字段"""
|
||||
menu_id = request.data.get('menu')
|
||||
model_name = request.data.get('model')
|
||||
if not menu_id or not model_name:
|
||||
return ErrorResponse( msg='参数错误')
|
||||
for model in get_custom_app_models():
|
||||
if model['model'] != model_name:
|
||||
continue
|
||||
for field in model['fields']:
|
||||
if MenuField.objects.filter(
|
||||
menu_id=menu_id, model=model_name, field_name=field['name']
|
||||
).exists():
|
||||
continue
|
||||
data = {
|
||||
'menu': menu_id,
|
||||
'model': model_name,
|
||||
'field_name': field['name'],
|
||||
'title': str(field['title']),
|
||||
}
|
||||
serializer = self.get_serializer(data=data, request=request)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return SuccessResponse(msg='匹配成功')
|
||||
@@ -15,6 +15,7 @@ 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.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.validator import CustomUniqueValidator
|
||||
@@ -32,9 +33,6 @@ class RoleSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class RoleCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色管理 创建/更新时的列化器
|
||||
@@ -49,19 +47,19 @@ class RoleCreateUpdateSerializer(CustomModelSerializer):
|
||||
def validate(self, attrs: dict):
|
||||
return super().validate(attrs)
|
||||
|
||||
def save(self, **kwargs):
|
||||
is_superuser = self.request.user.is_superuser
|
||||
if not is_superuser:
|
||||
self.validated_data.pop('admin')
|
||||
data = super().save(**kwargs)
|
||||
return data
|
||||
# def save(self, **kwargs):
|
||||
# is_superuser = self.request.user.is_superuser
|
||||
# if not is_superuser:
|
||||
# self.validated_data.pop('admin')
|
||||
# data = super().save(**kwargs)
|
||||
# return data
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class MenuPermissonSerializer(CustomModelSerializer):
|
||||
class MenuPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单的按钮权限
|
||||
"""
|
||||
@@ -72,9 +70,9 @@ class MenuPermissonSerializer(CustomModelSerializer):
|
||||
if is_superuser:
|
||||
queryset = MenuButton.objects.filter(menu__id=instance.id)
|
||||
else:
|
||||
menu_permission_id_list = self.request.user.role.values_list('permission',flat=True)
|
||||
queryset = MenuButton.objects.filter(id__in=menu_permission_id_list,menu__id=instance.id)
|
||||
serializer = MenuButtonSerializer(queryset,many=True, read_only=True)
|
||||
menu_permission_id_list = self.request.user.role.values_list('permission', flat=True)
|
||||
queryset = MenuButton.objects.filter(id__in=menu_permission_id_list, menu__id=instance.id)
|
||||
serializer = MenuButtonSerializer(queryset, many=True, read_only=True)
|
||||
return serializer.data
|
||||
|
||||
class Meta:
|
||||
@@ -82,7 +80,29 @@ class MenuPermissonSerializer(CustomModelSerializer):
|
||||
fields = ['id', 'parent', 'name', 'menuPermission']
|
||||
|
||||
|
||||
class RoleViewSet(CustomModelViewSet,FastCrudMixin):
|
||||
class MenuButtonPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单和按钮权限
|
||||
"""
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
|
||||
def get_isCheck(self, instance):
|
||||
is_superuser = self.request.user.is_superuser
|
||||
if is_superuser:
|
||||
return True
|
||||
else:
|
||||
return MenuButton.objects.filter(
|
||||
menu__id=instance.id,
|
||||
role__id__in=self.request.user.role.values_list('id', flat=True),
|
||||
).exists()
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
|
||||
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
|
||||
"""
|
||||
角色管理接口
|
||||
list:查询
|
||||
|
||||
@@ -72,7 +72,14 @@ class RoleMenuPermissionViewSet(CustomModelViewSet):
|
||||
menu_list = body.get('menu',None)
|
||||
if menu_list is None:
|
||||
return ErrorResponse(msg="未获取到菜单参数")
|
||||
data = [{"role":role_id,"menu":item} for item in menu_list]
|
||||
obj_list = RoleMenuPermission.objects.filter(role__id=role_id).values_list('menu__id',flat=True)
|
||||
old_set = set(obj_list)
|
||||
new_set = set(menu_list)
|
||||
#need_update = old_set.intersection(new_set) # 需要更新的
|
||||
need_del = old_set.difference(new_set) # 需要删除的
|
||||
need_add = new_set.difference(old_set) # 需要新增的
|
||||
RoleMenuPermission.objects.filter(role__id=role_id,menu__in=list(need_del)).delete()
|
||||
data = [{"role": role_id, "menu": item} for item in list(need_add)]
|
||||
serializer = RoleMenuPermissionSerializer(data=data,many=True,request=request)
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save()
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
@Created on: 2021/6/3 003 0:30
|
||||
@Remark: 菜单按钮管理
|
||||
"""
|
||||
from django.db.models import F
|
||||
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
|
||||
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.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
@@ -27,20 +30,12 @@ class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class RoleMenuButtonPermissionInitSerializer(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)
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuButtonPermission
|
||||
@@ -48,6 +43,120 @@ class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class RoleButtonPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色按钮权限
|
||||
"""
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
data_range = serializers.SerializerMethodField()
|
||||
|
||||
def get_isCheck(self, instance):
|
||||
params = self.request.query_params
|
||||
return RoleMenuButtonPermission.objects.filter(
|
||||
menu_button__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
).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()
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.data_range
|
||||
|
||||
class Meta:
|
||||
model = MenuButton
|
||||
fields = ['id', 'name', 'value', 'isCheck', 'data_range']
|
||||
|
||||
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
if queryset:
|
||||
return queryset.is_update
|
||||
return False
|
||||
|
||||
class Meta:
|
||||
model = MenuField
|
||||
fields = ['id', 'field_name', 'title', 'is_query', 'is_create', 'is_update']
|
||||
|
||||
|
||||
class RoleMenuSerializer(CustomModelSerializer):
|
||||
menus = serializers.SerializerMethodField()
|
||||
|
||||
def get_menus(self, instance):
|
||||
menu_list = Menu.objects.filter(parent=instance['id']).values('id', 'name')
|
||||
serializer = RoleMenuPermissionSerializer(menu_list, many=True, request=self.request)
|
||||
return serializer.data
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ['id', 'name', 'menus']
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
菜单按钮接口
|
||||
@@ -64,38 +173,111 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
extra_filter_class = []
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_get_menu(self, request):
|
||||
"""根据当前用户的角色返回角色拥有的菜单"""
|
||||
def get_role_premission(self, request):
|
||||
"""
|
||||
角色授权获取:
|
||||
:param request: role
|
||||
:return: menu,btns,columns
|
||||
"""
|
||||
params = request.query_params
|
||||
role = params.get('role', None)
|
||||
if role is None:
|
||||
return ErrorResponse(msg="未获取到角色信息")
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
queryset = Menu.objects.filter(status=1).values('id','name','parent','is_catalog')
|
||||
if is_superuser:
|
||||
queryset = Menu.objects.filter(status=1, is_catalog=True).values('name', 'id').all()
|
||||
else:
|
||||
role_id = request.user.role.values_list('id',flat=True)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_id).values(id=F('menu__id'),name=F('menu__name'),parent=F('menu__parent'),is_catalog=F('menu__is_catalog'))
|
||||
return DetailResponse(data=queryset)
|
||||
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=True, id__in=menu_list).values('name', 'id').all()
|
||||
serializer = RoleMenuSerializer(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)
|
||||
|
||||
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
|
||||
def set_role_premission(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 item in body:
|
||||
for menu in item["menus"]:
|
||||
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'),
|
||||
defaults={
|
||||
'is_query': col.get('is_query'),
|
||||
'is_create': col.get('is_create'),
|
||||
'is_update': col.get('is_update')
|
||||
})
|
||||
return DetailResponse(msg="授权成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_menu_get_button(self,request):
|
||||
def role_menu_get_button(self, request):
|
||||
"""
|
||||
当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
if params:
|
||||
menu_id = params.get('menu',None)
|
||||
if menu_id:
|
||||
if params := request.query_params:
|
||||
if menu_id := params.get('menu', None):
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
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(
|
||||
id=F('menu_button__id'),
|
||||
name=F('menu_button__name')
|
||||
)
|
||||
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="参数错误")
|
||||
|
||||
@@ -133,12 +315,12 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
return DetailResponse(data=data)
|
||||
else:
|
||||
data = []
|
||||
role_id = request.user.role.id
|
||||
params = request.query_params
|
||||
if params:
|
||||
menu_button_id = params.get('menu_button', None)
|
||||
if menu_button_id:
|
||||
role_queryset = RoleMenuButtonPermission.objects.filter(role=role_id,menu_button=menu_button_id).values_list('data_range',flat=True)
|
||||
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:
|
||||
@@ -199,46 +381,70 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
"""
|
||||
params = request.query_params
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
queryset = Dept.objects.values('id','name','parent')
|
||||
return DetailResponse(data=queryset)
|
||||
if is_superuser:
|
||||
queryset = Dept.objects.values('id', 'name', 'parent')
|
||||
else:
|
||||
if params:
|
||||
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(
|
||||
id=F('dept__id'),
|
||||
name=F('dept__name'),
|
||||
parent=F('dept__parent')
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
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):
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def menu_to_button(self, request):
|
||||
"""
|
||||
根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
if params:
|
||||
menu_id = params.get('menu',None)
|
||||
if menu_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
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(
|
||||
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',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
else:
|
||||
if params:
|
||||
|
||||
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="未获取到参数")
|
||||
|
||||
@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)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import hashlib
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.hashers import make_password, check_password
|
||||
from django_restql.fields import DynamicSerializerMethodField
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action, permission_classes
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.db import connection
|
||||
from django.db.models import Q
|
||||
from application import dispatch
|
||||
from dvadmin.system.models import Users, Role, Dept
|
||||
from dvadmin.system.views.role import RoleSerializer
|
||||
from dvadmin.utils.json_response import ErrorResponse, DetailResponse
|
||||
from dvadmin.utils.json_response import ErrorResponse, DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.validator import CustomUniqueValidator
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
@@ -23,7 +24,7 @@ def recursion(instance, parent, result):
|
||||
res.append(data)
|
||||
if new_instance:
|
||||
array = recursion(new_instance, parent, result)
|
||||
res += (array)
|
||||
res += array
|
||||
return res
|
||||
|
||||
|
||||
@@ -114,14 +115,14 @@ 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):
|
||||
"""
|
||||
更改激活状态
|
||||
"""
|
||||
print(111, value)
|
||||
if value:
|
||||
self.initial_data["login_error_count"] = 0
|
||||
return value
|
||||
|
||||
def save(self, **kwargs):
|
||||
data = super().save(**kwargs)
|
||||
@@ -196,10 +197,12 @@ class ExportUserProfileSerializer(CustomModelSerializer):
|
||||
|
||||
|
||||
class UserProfileImportSerializer(CustomModelSerializer):
|
||||
password = serializers.CharField(read_only=True, required=False)
|
||||
|
||||
def save(self, **kwargs):
|
||||
data = super().save(**kwargs)
|
||||
password = hashlib.new(
|
||||
"md5", str(self.initial_data.get("password", "")).encode(encoding="UTF-8")
|
||||
"md5", str(self.initial_data.get("password", "admin123456")).encode(encoding="UTF-8")
|
||||
).hexdigest()
|
||||
data.set_password(password)
|
||||
data.save()
|
||||
@@ -231,7 +234,7 @@ class UserViewSet(CustomModelViewSet):
|
||||
create_serializer_class = UserCreateSerializer
|
||||
update_serializer_class = UserUpdateSerializer
|
||||
filter_fields = ["name", "username", "gender", "is_active", "dept", "user_type"]
|
||||
search_fields = ["username", "name", "gender", "dept__name", "role__name"]
|
||||
search_fields = ["username", "name", "dept__name", "role__name"]
|
||||
# 导出
|
||||
export_field_label = {
|
||||
"username": "用户账号",
|
||||
@@ -264,7 +267,6 @@ class UserViewSet(CustomModelViewSet):
|
||||
"data": {"启用": True, "禁用": False},
|
||||
}
|
||||
},
|
||||
"password": "登录密码",
|
||||
"dept": {"title": "部门", "choices": {"queryset": Dept.objects.filter(status=True), "values_name": "name"}},
|
||||
"role": {"title": "角色", "choices": {"queryset": Role.objects.filter(status=True), "values_name": "name"}},
|
||||
}
|
||||
@@ -313,34 +315,39 @@ class UserViewSet(CustomModelViewSet):
|
||||
serializer.save()
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
|
||||
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
|
||||
@action(methods=["PUT"], detail=False, permission_classes=[IsAuthenticated])
|
||||
def change_password(self, request, *args, **kwargs):
|
||||
"""密码修改"""
|
||||
data = request.data
|
||||
old_pwd = data.get("oldPassword")
|
||||
print(old_pwd)
|
||||
new_pwd = data.get("newPassword")
|
||||
new_pwd2 = data.get("newPassword2")
|
||||
if old_pwd is None or new_pwd is None or new_pwd2 is None:
|
||||
return ErrorResponse(msg="参数不能为空")
|
||||
if new_pwd != new_pwd2:
|
||||
return ErrorResponse(msg="两次密码不匹配")
|
||||
check_password = request.user.check_password(old_pwd)
|
||||
if not check_password:
|
||||
check_password = request.user.check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
if check_password:
|
||||
new_pwd = hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()
|
||||
request.user.password = make_password(new_pwd)
|
||||
verify_password = check_password(old_pwd, request.user.password)
|
||||
if not verify_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 verify_password:
|
||||
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
request.user.save()
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
else:
|
||||
return ErrorResponse(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:
|
||||
@@ -351,6 +358,8 @@ class UserViewSet(CustomModelViewSet):
|
||||
"""
|
||||
密码重置
|
||||
"""
|
||||
if not self.request.user.is_superuser:
|
||||
return ErrorResponse(msg="只允许超级管理员对其进行密码重置")
|
||||
instance = Users.objects.filter(id=pk).first()
|
||||
data = request.data
|
||||
new_pwd = data.get("newPassword")
|
||||
@@ -364,3 +373,43 @@ class UserViewSet(CustomModelViewSet):
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
else:
|
||||
return ErrorResponse(msg="未获取到用户")
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
dept_id = request.query_params.get('dept')
|
||||
show_all = request.query_params.get('show_all')
|
||||
if not dept_id:
|
||||
dept_id = ''
|
||||
if not show_all:
|
||||
show_all = 0
|
||||
if int(show_all):
|
||||
all_did = [dept_id]
|
||||
def inner(did):
|
||||
sub = Dept.objects.filter(parent_id=did)
|
||||
if not sub.exists():
|
||||
return
|
||||
for i in sub:
|
||||
all_did.append(i.pk)
|
||||
inner(i)
|
||||
if dept_id != '':
|
||||
inner(dept_id)
|
||||
searchs = [
|
||||
Q(**{f+'__icontains':i})
|
||||
for f in self.search_fields
|
||||
] if (i:=request.query_params.get('search')) else []
|
||||
q_obj = []
|
||||
if searchs:
|
||||
q = searchs[0]
|
||||
for i in searchs[1:]:
|
||||
q |= i
|
||||
q_obj.append(Q(q))
|
||||
queryset = Users.objects.filter(*q_obj, dept_id__in=all_did)
|
||||
else:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
else:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
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="获取成功")
|
||||
|
||||
@@ -3,8 +3,11 @@ import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.utils import timezone
|
||||
|
||||
from dvadmin.utils.validator import CustomValidationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
UserModel = get_user_model()
|
||||
|
||||
@@ -24,10 +27,13 @@ class CustomBackend(ModelBackend):
|
||||
except UserModel.DoesNotExist:
|
||||
UserModel().set_password(password)
|
||||
else:
|
||||
check_password = user.check_password(password)
|
||||
if not check_password:
|
||||
check_password = user.check_password(hashlib.md5(password.encode(encoding='UTF-8')).hexdigest())
|
||||
if check_password and self.user_can_authenticate(user):
|
||||
user.last_login = timezone.now()
|
||||
user.save()
|
||||
return user
|
||||
verify_password = check_password(password, user.password)
|
||||
if not verify_password:
|
||||
password = hashlib.md5(password.encode(encoding='UTF-8')).hexdigest()
|
||||
verify_password = check_password(password, user.password)
|
||||
if verify_password:
|
||||
if self.user_can_authenticate(user):
|
||||
user.last_login = timezone.now()
|
||||
user.save()
|
||||
return user
|
||||
raise CustomValidationError("当前用户已被禁用,请联系管理员!")
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.db.models import F
|
||||
from rest_framework.decorators import action
|
||||
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
|
||||
|
||||
|
||||
class FieldPermissionMixin:
|
||||
@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 []
|
||||
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
|
||||
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)
|
||||
@@ -12,16 +12,49 @@ from collections import OrderedDict
|
||||
from functools import reduce
|
||||
|
||||
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
|
||||
from django_filters.filters import CharFilter
|
||||
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 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
|
||||
|
||||
class CoreModelFilterBankend(BaseFilterBackend):
|
||||
"""
|
||||
自定义时间范围过滤器
|
||||
"""
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
# 更新时间范围过滤条件
|
||||
update_filter = Q()
|
||||
if update_datetime_after and update_datetime_before:
|
||||
update_filter &= Q(update_datetime__gte=update_datetime_after) & Q(update_datetime__lte=update_datetime_before)
|
||||
elif update_datetime_after:
|
||||
update_filter &= Q(update_datetime__gte=update_datetime_after)
|
||||
elif update_datetime_before:
|
||||
update_filter &= Q(update_datetime__lte=update_datetime_before)
|
||||
# 结合两个时间范围过滤条件
|
||||
queryset = queryset.filter(create_filter & update_filter)
|
||||
return queryset
|
||||
return queryset
|
||||
|
||||
def get_dept(dept_id: int, dept_all_list=None, dept_list=None):
|
||||
"""
|
||||
@@ -75,7 +108,7 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
if item.get("permission__api")
|
||||
]
|
||||
for item in api_white_list:
|
||||
new_api = api + ":" + str(method)
|
||||
new_api = f"{api}:{method}"
|
||||
matchObj = re.match(item, new_api, re.M | re.I)
|
||||
if matchObj is None:
|
||||
continue
|
||||
@@ -86,74 +119,83 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
如果不是超级管理员,则进入下一步权限判断
|
||||
"""
|
||||
if request.user.is_superuser == 0:
|
||||
# 0. 获取用户的部门id,没有部门则返回空
|
||||
user_dept_id = getattr(request.user, "dept_id", None)
|
||||
if not user_dept_id:
|
||||
return queryset.none()
|
||||
|
||||
# 1. 判断过滤的数据是否有创建人所在部门 "dept_belong_id" 字段
|
||||
if not getattr(queryset.model, "dept_belong_id", None):
|
||||
return queryset
|
||||
|
||||
# 2. 如果用户没有关联角色则返回本部门数据
|
||||
if not hasattr(request.user, "role"):
|
||||
return queryset.filter(dept_belong_id=user_dept_id)
|
||||
|
||||
# 3. 根据所有角色 获取所有权限范围
|
||||
# (0, "仅本人数据权限"),
|
||||
# (1, "本部门及以下数据权限"),
|
||||
# (2, "本部门数据权限"),
|
||||
# (3, "全部数据权限"),
|
||||
# (4, "自定数据权限")
|
||||
replace_str = re.compile('\d')
|
||||
re_api = replace_str.sub('{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',
|
||||
role_admin=F('role__admin')
|
||||
)
|
||||
dataScope_list = [] # 权限范围列表
|
||||
for ele in role_permission_list:
|
||||
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
|
||||
if 3 == ele.get("data_range") or ele.get("role_admin") == True:
|
||||
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=user_dept_id
|
||||
)
|
||||
|
||||
# 5. 自定数据权限 获取部门,根据部门过滤
|
||||
dept_list = []
|
||||
for ele in dataScope_list:
|
||||
if ele == 4:
|
||||
dept_list.extend(
|
||||
request.user.role.filter(status=1).values_list(
|
||||
"dept__id", flat=True
|
||||
)
|
||||
)
|
||||
elif ele == 2:
|
||||
dept_list.append(user_dept_id)
|
||||
elif ele == 1:
|
||||
dept_list.append(user_dept_id)
|
||||
dept_list.extend(
|
||||
get_dept(
|
||||
user_dept_id,
|
||||
)
|
||||
)
|
||||
if queryset.model._meta.model_name == 'dept':
|
||||
return queryset.filter(id__in=list(set(dept_list)))
|
||||
return queryset.filter(dept_belong_id__in=list(set(dept_list)))
|
||||
return self._extracted_from_filter_queryset_33(request, queryset, api, method)
|
||||
else:
|
||||
return queryset
|
||||
|
||||
# TODO Rename this here and in `filter_queryset`
|
||||
def _extracted_from_filter_queryset_33(self, request, queryset, api, method):
|
||||
# 0. 获取用户的部门id,没有部门则返回空
|
||||
user_dept_id = getattr(request.user, "dept_id", None)
|
||||
if not user_dept_id:
|
||||
return queryset.none()
|
||||
|
||||
# 1. 判断过滤的数据是否有创建人所在部门 "dept_belong_id" 字段
|
||||
if not getattr(queryset.model, "dept_belong_id", None):
|
||||
return queryset
|
||||
|
||||
# 2. 如果用户没有关联角色则返回本部门数据
|
||||
if not hasattr(request.user, "role"):
|
||||
return queryset.filter(dept_belong_id=user_dept_id)
|
||||
|
||||
# 3. 根据所有角色 获取所有权限范围
|
||||
# (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)
|
||||
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'
|
||||
)
|
||||
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=user_dept_id
|
||||
)
|
||||
|
||||
# 5. 自定数据权限 获取部门,根据部门过滤
|
||||
dept_list = []
|
||||
for ele in dataScope_list:
|
||||
if ele == 1:
|
||||
dept_list.append(user_dept_id)
|
||||
dept_list.extend(
|
||||
get_dept(
|
||||
user_dept_id,
|
||||
)
|
||||
)
|
||||
elif ele == 2:
|
||||
dept_list.append(user_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 queryset.model._meta.model_name == 'dept':
|
||||
return queryset.filter(id__in=list(set(dept_list)))
|
||||
return queryset.filter(dept_belong_id__in=list(set(dept_list)))
|
||||
|
||||
|
||||
class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
lookup_prefixes = {
|
||||
@@ -163,19 +205,24 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
"$": "iregex",
|
||||
"~": "icontains",
|
||||
}
|
||||
filter_fields = "__all__"
|
||||
|
||||
def construct_search(self, field_name):
|
||||
def construct_search(self, field_name, lookup_expr=None):
|
||||
lookup = self.lookup_prefixes.get(field_name[0])
|
||||
if lookup:
|
||||
field_name = field_name[1:]
|
||||
else:
|
||||
lookup = "icontains"
|
||||
return LOOKUP_SEP.join([field_name, lookup])
|
||||
lookup = lookup_expr
|
||||
if lookup:
|
||||
if field_name.endswith(lookup):
|
||||
return field_name
|
||||
return LOOKUP_SEP.join([field_name, lookup])
|
||||
return field_name
|
||||
|
||||
def find_filter_lookups(self, orm_lookups, search_term_key):
|
||||
for lookup in orm_lookups:
|
||||
# if lookup.find(search_term_key) >= 0:
|
||||
new_lookup = lookup.split("__")[0]
|
||||
new_lookup = LOOKUP_SEP.join(lookup.split(LOOKUP_SEP)[:-1]) if len(lookup.split(LOOKUP_SEP)) > 1 else lookup
|
||||
# 修复条件搜索错误 bug
|
||||
if new_lookup == search_term_key:
|
||||
return lookup
|
||||
@@ -191,18 +238,22 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
# TODO: remove assertion in 2.1
|
||||
if filterset_class is None and hasattr(view, "filter_class"):
|
||||
utils.deprecate(
|
||||
"`%s.filter_class` attribute should be renamed `filterset_class`."
|
||||
% view.__class__.__name__
|
||||
"`%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(
|
||||
"`%s.filter_fields` attribute should be renamed `filterset_fields`."
|
||||
% view.__class__.__name__
|
||||
"`%s.filter_fields` attribute should be renamed `filterset_fields`." % view.__class__.__name__
|
||||
)
|
||||
filterset_fields = getattr(view, "filter_fields", None)
|
||||
self.filter_fields = getattr(view, "filter_fields", None)
|
||||
if isinstance(self.filter_fields, (list, tuple)):
|
||||
filterset_fields = [
|
||||
field[1:] if field[0] in self.lookup_prefixes.keys() else field for field in self.filter_fields
|
||||
]
|
||||
else:
|
||||
filterset_fields = self.filter_fields
|
||||
|
||||
if filterset_class:
|
||||
filterset_model = filterset_class._meta.model
|
||||
@@ -222,6 +273,51 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
MetaBase = getattr(self.filterset_base, "Meta", object)
|
||||
|
||||
class AutoFilterSet(self.filterset_base):
|
||||
@classmethod
|
||||
def get_all_model_fields(cls, model):
|
||||
opts = model._meta
|
||||
|
||||
return [
|
||||
f.name
|
||||
for f in sorted(opts.fields + opts.many_to_many)
|
||||
if (f.name == "id")
|
||||
or not isinstance(f, models.AutoField)
|
||||
and not (getattr(f.remote_field, "parent_link", False))
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls):
|
||||
"""
|
||||
Resolve the 'fields' argument that should be used for generating filters on the
|
||||
filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'.
|
||||
"""
|
||||
model = cls._meta.model
|
||||
fields = cls._meta.fields
|
||||
exclude = cls._meta.exclude
|
||||
|
||||
assert not (fields is None and exclude is None), (
|
||||
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' "
|
||||
"has been deprecated since 0.15.0 and is now disallowed. Add an explicit "
|
||||
"'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__
|
||||
)
|
||||
|
||||
# Setting exclude with no fields implies all other fields.
|
||||
if exclude is not None and fields is None:
|
||||
fields = ALL_FIELDS
|
||||
|
||||
# Resolve ALL_FIELDS into all fields for the filterset's model.
|
||||
if fields == ALL_FIELDS:
|
||||
fields = cls.get_all_model_fields(model)
|
||||
|
||||
# Remove excluded fields
|
||||
exclude = exclude or []
|
||||
if not isinstance(fields, dict):
|
||||
fields = [(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude]
|
||||
else:
|
||||
fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]
|
||||
|
||||
return OrderedDict(fields)
|
||||
|
||||
@classmethod
|
||||
def get_filters(cls):
|
||||
"""
|
||||
@@ -249,7 +345,13 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
# warn if the field doesn't exist.
|
||||
if field is None:
|
||||
undefined.append(field_name)
|
||||
|
||||
# 更新默认字符串搜索为模糊搜索
|
||||
if (
|
||||
isinstance(field, (models.CharField))
|
||||
and filterset_fields == "__all__"
|
||||
and lookups == ["exact"]
|
||||
):
|
||||
lookups = ["icontains"]
|
||||
for lookup_expr in lookups:
|
||||
filter_name = cls.get_filter_name(field_name, lookup_expr)
|
||||
|
||||
@@ -259,20 +361,15 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
continue
|
||||
|
||||
if field is not None:
|
||||
filters[filter_name] = cls.filter_for_field(
|
||||
field, field_name, lookup_expr
|
||||
)
|
||||
filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
|
||||
|
||||
# Allow Meta.fields to contain declared filters *only* when a list/tuple
|
||||
if isinstance(cls._meta.fields, (list, tuple)):
|
||||
undefined = [
|
||||
f for f in undefined if f not in cls.declared_filters
|
||||
]
|
||||
undefined = [f for f in undefined if f not in cls.declared_filters]
|
||||
|
||||
if undefined:
|
||||
raise TypeError(
|
||||
"'Meta.fields' must not contain non-model field names: %s"
|
||||
% ", ".join(undefined)
|
||||
"'Meta.fields' must not contain non-model field names: %s" % ", ".join(undefined)
|
||||
)
|
||||
|
||||
# Add in declared filters. This is necessary since we don't enforce adding
|
||||
@@ -294,22 +391,31 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
return queryset
|
||||
if filterset.__class__.__name__ == "AutoFilterSet":
|
||||
queryset = filterset.queryset
|
||||
orm_lookups = []
|
||||
for search_field in filterset.filters:
|
||||
if isinstance(filterset.filters[search_field], CharFilter):
|
||||
orm_lookups.append(
|
||||
self.construct_search(six.text_type(search_field))
|
||||
)
|
||||
else:
|
||||
orm_lookups.append(search_field)
|
||||
filter_fields = filterset.filters if self.filter_fields == "__all__" else self.filter_fields
|
||||
orm_lookup_dict = dict(
|
||||
zip(
|
||||
[field for field in filter_fields],
|
||||
[filterset.filters[lookup].lookup_expr for lookup in filterset.filters.keys()],
|
||||
)
|
||||
)
|
||||
orm_lookups = [
|
||||
self.construct_search(lookup, lookup_expr) for lookup, lookup_expr in orm_lookup_dict.items()
|
||||
]
|
||||
# print(orm_lookups)
|
||||
conditions = []
|
||||
queries = []
|
||||
for search_term_key in filterset.data.keys():
|
||||
orm_lookup = self.find_filter_lookups(orm_lookups, search_term_key)
|
||||
if not orm_lookup:
|
||||
if not orm_lookup or filterset.data.get(search_term_key) == '':
|
||||
continue
|
||||
query = Q(**{orm_lookup: filterset.data[search_term_key]})
|
||||
queries.append(query)
|
||||
filterset_data_len = len(filterset.data.getlist(search_term_key))
|
||||
if filterset_data_len == 1:
|
||||
query = Q(**{orm_lookup: filterset.data[search_term_key]})
|
||||
queries.append(query)
|
||||
elif filterset_data_len == 2:
|
||||
orm_lookup += '__range'
|
||||
query = Q(**{orm_lookup: filterset.data.getlist(search_term_key)})
|
||||
queries.append(query)
|
||||
if len(queries) > 0:
|
||||
conditions.append(reduce(operator.and_, queries))
|
||||
queryset = queryset.filter(reduce(operator.and_, conditions))
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from django.db import connection
|
||||
from loguru import logger
|
||||
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
# 1.🎖️先声明一个类继承logging.Handler(制作一件品如的衣服)
|
||||
|
||||
from application.dispatch import is_tenants_mode
|
||||
|
||||
|
||||
class InterceptTimedRotatingFileHandler(RotatingFileHandler, logging.Filter):
|
||||
"""
|
||||
自定义反射时间回滚日志记录器
|
||||
缺少命名空间
|
||||
"""
|
||||
|
||||
def __init__(self, filename, when='d', interval=1, backupCount=5, encoding="utf-8", delay=False, utc=False,
|
||||
maxBytes=1024 * 1024 * 100, atTime=None, logging_levels="all", format=None):
|
||||
super(InterceptTimedRotatingFileHandler, self).__init__(filename)
|
||||
filename = os.path.abspath(filename)
|
||||
# 定义默认格式
|
||||
if not format:
|
||||
format = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <green>{extra[ip]}:{extra[port]}</green> | <level>{level: <8}</level>| <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"
|
||||
when = when.lower()
|
||||
# 2.🎖️需要本地用不同的文件名做为不同日志的筛选器
|
||||
logger.configure(
|
||||
handlers=[
|
||||
dict(sink=sys.stderr, format=format),
|
||||
],
|
||||
)
|
||||
self.logger_ = logger.bind(sime=filename, ip="-", port="-", username="张三")
|
||||
self.filename = filename
|
||||
key_map = {
|
||||
'h': 'hour',
|
||||
'w': 'week',
|
||||
's': 'second',
|
||||
'm': 'minute',
|
||||
'd': 'day',
|
||||
}
|
||||
# 根据输入文件格式及时间回滚设立文件名称
|
||||
rotation = f"{maxBytes / 1024 / 1024}MB"
|
||||
retention = "%d %ss" % (backupCount, key_map[when])
|
||||
time_format = "{time:%Y-%m-%d_%H-%M-%S}"
|
||||
if when == "s":
|
||||
time_format = "{time:%Y-%m-%d_%H-%M-%S}"
|
||||
elif when == "m":
|
||||
time_format = "{time:%Y-%m-%d_%H-%M}"
|
||||
elif when == "h":
|
||||
time_format = "{time:%Y-%m-%d_%H}"
|
||||
elif when == "d":
|
||||
time_format = "{time:%Y-%m-%d}"
|
||||
elif when == "w":
|
||||
time_format = "{time:%Y-%m-%d}"
|
||||
level_keys = ["info"]
|
||||
# 3.🎖️构建一个筛选器
|
||||
levels = {
|
||||
"debug": lambda x: "DEBUG" == x['level'].name.upper() and x['extra'].get('sime') == filename,
|
||||
"error": lambda x: "ERROR" == x['level'].name.upper() and x['extra'].get('sime') == filename,
|
||||
"info": lambda x: "INFO" == x['level'].name.upper() and x['extra'].get('sime') == filename,
|
||||
"warning": lambda x: "WARNING" == x['level'].name.upper() and x['extra'].get('sime') == filename
|
||||
}
|
||||
# 4. 🎖️根据输出构建筛选器
|
||||
if isinstance(logging_levels, str):
|
||||
if logging_levels.lower() == "all":
|
||||
level_keys = levels.keys()
|
||||
elif logging_levels.lower() in levels:
|
||||
level_keys = [logging_levels]
|
||||
elif isinstance(logging_levels, (list, tuple)):
|
||||
level_keys = logging_levels
|
||||
for k, f in {_: levels[_] for _ in level_keys}.items():
|
||||
|
||||
# 5.🎖️为防止重复添加sink,而重复写入日志,需要判断是否已经装载了对应sink,防止其使用秘技:反复横跳。
|
||||
filename_fmt = filename.replace(".log", "_%s_%s.log" % (time_format, k))
|
||||
# noinspection PyUnresolvedReferences,PyProtectedMember
|
||||
file_key = {_._name: han_id for han_id, _ in self.logger_._core.handlers.items()}
|
||||
filename_fmt_key = "'{}'".format(filename_fmt)
|
||||
if filename_fmt_key in file_key:
|
||||
continue
|
||||
# self.logger_.remove(file_key[filename_fmt_key])
|
||||
self.logger_.add(
|
||||
filename_fmt,
|
||||
format=format,
|
||||
retention=retention,
|
||||
encoding=encoding,
|
||||
level=self.level,
|
||||
rotation=rotation,
|
||||
compression="zip", # 日志归档自行压缩文件
|
||||
delay=delay,
|
||||
enqueue=True,
|
||||
backtrace=True,
|
||||
filter=f
|
||||
)
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
level = self.logger_.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
frame, depth = logging.currentframe(), 2
|
||||
# 6.🎖️把当前帧的栈深度回到发生异常的堆栈深度,不然就是当前帧发生异常而无法回溯
|
||||
while frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
# 设置自定义属性
|
||||
port = "-"
|
||||
ip = "-"
|
||||
details = frame.f_locals.get('details', None)
|
||||
msg = self.format(record)
|
||||
bind = {}
|
||||
if details and details.get('client'):
|
||||
ip, port = details.get('client').split(':')
|
||||
if is_tenants_mode():
|
||||
bind["schema_name"] = connection.tenant.schema_name
|
||||
bind["domain_url"] = getattr(connection.tenant, 'domain_url', None)
|
||||
bind["ip"] = ip
|
||||
bind["port"] = port
|
||||
self.logger_ \
|
||||
.opt(depth=depth, exception=record.exc_info, colors=True) \
|
||||
.bind(**bind) \
|
||||
.log(level, msg)
|
||||
@@ -2,9 +2,11 @@
|
||||
日志 django中间件
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.http import HttpResponse, HttpResponseServerError
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
from dvadmin.system.models import OperationLog
|
||||
@@ -87,3 +89,58 @@ class ApiLoggingMiddleware(MiddlewareMixin):
|
||||
if self.methods == 'ALL' or request.method in self.methods:
|
||||
self.__handle_response(request, response)
|
||||
return response
|
||||
|
||||
logger = logging.getLogger("healthz")
|
||||
class HealthCheckMiddleware(object):
|
||||
"""
|
||||
存活检查中间件
|
||||
"""
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
# One-time configuration and initialization.
|
||||
|
||||
def __call__(self, request):
|
||||
if request.method == "GET":
|
||||
if request.path == "/readiness":
|
||||
return self.readiness(request)
|
||||
elif request.path == "/healthz":
|
||||
return self.healthz(request)
|
||||
return self.get_response(request)
|
||||
|
||||
def healthz(self, request):
|
||||
"""
|
||||
Returns that the server is alive.
|
||||
"""
|
||||
return HttpResponse("OK")
|
||||
|
||||
def readiness(self, request):
|
||||
# Connect to each database and do a generic standard SQL query
|
||||
# that doesn't write any data and doesn't depend on any tables
|
||||
# being present.
|
||||
try:
|
||||
from django.db import connections
|
||||
for name in connections:
|
||||
cursor = connections[name].cursor()
|
||||
cursor.execute("SELECT 1;")
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
return HttpResponseServerError("db: invalid response")
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return HttpResponseServerError("db: cannot connect to database.")
|
||||
|
||||
# Call get_stats() to connect to each memcached instance and get it's stats.
|
||||
# This can effectively check if each is online.
|
||||
try:
|
||||
from django.core.cache import caches
|
||||
from django.core.cache.backends.memcached import BaseMemcachedCache
|
||||
for cache in caches.all():
|
||||
if isinstance(cache, BaseMemcachedCache):
|
||||
stats = cache._cache.get_stats()
|
||||
if len(stats) != len(cache._servers):
|
||||
return HttpResponseServerError("cache: cannot connect to cache.")
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return HttpResponseServerError("cache: cannot connect to cache.")
|
||||
|
||||
return HttpResponse("OK")
|
||||
|
||||
@@ -6,22 +6,21 @@
|
||||
@Created on: 2021/5/31 031 22:08
|
||||
@Remark: 公共基础model类
|
||||
"""
|
||||
import uuid
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.conf import settings
|
||||
|
||||
from application import settings
|
||||
|
||||
table_prefix = settings.TABLE_PREFIX # 数据库表名前缀
|
||||
|
||||
|
||||
class SoftDeleteQuerySet(QuerySet):
|
||||
class SoftDeleteQuerySet(models.QuerySet):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
class SoftDeleteManager(models.Manager):
|
||||
"""支持软删除"""
|
||||
|
||||
@@ -40,7 +39,7 @@ class SoftDeleteManager(models.Manager):
|
||||
return SoftDeleteQuerySet(self.model, using=self._db).exclude(is_deleted=False)
|
||||
return SoftDeleteQuerySet(self.model).exclude(is_deleted=True)
|
||||
|
||||
def get_by_natural_key(self,name):
|
||||
def get_by_natural_key(self, name):
|
||||
return SoftDeleteQuerySet(self.model).get(username=name)
|
||||
|
||||
|
||||
@@ -73,10 +72,13 @@ class CoreModel(models.Model):
|
||||
id = models.BigAutoField(primary_key=True, help_text="Id", verbose_name="Id")
|
||||
description = models.CharField(max_length=255, verbose_name="描述", null=True, blank=True, help_text="描述")
|
||||
creator = models.ForeignKey(to=settings.AUTH_USER_MODEL, related_query_name='creator_query', null=True,
|
||||
verbose_name='创建人', help_text="创建人", on_delete=models.SET_NULL, db_constraint=False)
|
||||
verbose_name='创建人', help_text="创建人", on_delete=models.SET_NULL,
|
||||
db_constraint=False)
|
||||
modifier = models.CharField(max_length=255, null=True, blank=True, help_text="修改人", verbose_name="修改人")
|
||||
dept_belong_id = models.CharField(max_length=255, help_text="数据归属部门", null=True, blank=True, verbose_name="数据归属部门")
|
||||
update_datetime = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间", verbose_name="修改时间")
|
||||
dept_belong_id = models.CharField(max_length=255, help_text="数据归属部门", null=True, blank=True,
|
||||
verbose_name="数据归属部门")
|
||||
update_datetime = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间",
|
||||
verbose_name="修改时间")
|
||||
create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
|
||||
verbose_name="创建时间")
|
||||
|
||||
@@ -86,8 +88,6 @@ class CoreModel(models.Model):
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
|
||||
|
||||
|
||||
def get_all_models_objects(model_name=None):
|
||||
"""
|
||||
获取所有 models 对象
|
||||
@@ -111,4 +111,52 @@ def get_all_models_objects(model_name=None):
|
||||
settings.ALL_MODELS_OBJECTS.setdefault(item.__name__, {"table": table, "object": item})
|
||||
if model_name:
|
||||
return settings.ALL_MODELS_OBJECTS[model_name] or {}
|
||||
return settings.ALL_MODELS_OBJECTS or {}
|
||||
return settings.ALL_MODELS_OBJECTS or {}
|
||||
|
||||
|
||||
def get_model_from_app(app_name):
|
||||
"""获取模型里的字段"""
|
||||
model_module = import_module(app_name + '.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)
|
||||
]
|
||||
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
|
||||
})
|
||||
return model_list
|
||||
|
||||
|
||||
def get_custom_app_models(app_name=None):
|
||||
"""
|
||||
获取所有项目下的app里的models
|
||||
"""
|
||||
if app_name:
|
||||
return get_model_from_app(app_name)
|
||||
all_apps = apps.get_app_configs()
|
||||
res = []
|
||||
for app in all_apps:
|
||||
if app.name.startswith('django'):
|
||||
continue
|
||||
if app.name in settings.COLUMN_EXCLUDE_APPS:
|
||||
continue
|
||||
try:
|
||||
all_models = get_model_from_app(app.name)
|
||||
if all_models:
|
||||
for model in all_models:
|
||||
res.append(model)
|
||||
except Exception as e:
|
||||
pass
|
||||
return res
|
||||
|
||||
@@ -40,13 +40,11 @@ class CustomPagination(PageNumberPagination):
|
||||
try:
|
||||
self.page = paginator.page(page_number)
|
||||
except InvalidPage as exc:
|
||||
|
||||
# msg = self.invalid_page_message.format(
|
||||
# page_number=page_number, message=str(exc)
|
||||
# )
|
||||
# raise NotFound(msg)
|
||||
empty = False
|
||||
pass
|
||||
|
||||
if paginator.num_pages > 1 and self.template is not None:
|
||||
# The browsable API should display pagination controls.
|
||||
@@ -58,15 +56,15 @@ class CustomPagination(PageNumberPagination):
|
||||
self.page = []
|
||||
|
||||
return list(self.page)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
code = 2000
|
||||
msg = 'success'
|
||||
page =int(self.get_page_number(self.request, paginator)) or 1
|
||||
total=self.page.paginator.count if self.page else 0
|
||||
limit= int(self.get_page_size(self.request)) or 10
|
||||
is_next= self.page.has_next()
|
||||
is_previous= self.page.has_previous()
|
||||
data=data
|
||||
page = int(self.get_page_number(self.request, paginator)) or 1
|
||||
total = self.page.paginator.count if self.page else 0
|
||||
limit = int(self.get_page_size(self.request)) or 10
|
||||
is_next = self.page.has_next() if self.page else False
|
||||
is_previous = self.page.has_previous() if self.page else False
|
||||
|
||||
if not data:
|
||||
code = 2000
|
||||
@@ -78,8 +76,8 @@ class CustomPagination(PageNumberPagination):
|
||||
('msg', msg),
|
||||
('page', page),
|
||||
('limit', limit),
|
||||
('total',total),
|
||||
('is_next',is_next),
|
||||
('total', total),
|
||||
('is_next', is_next),
|
||||
('is_previous', is_previous),
|
||||
('data', data)
|
||||
]))
|
||||
|
||||
@@ -26,6 +26,7 @@ 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"):
|
||||
|
||||
@@ -6,18 +6,20 @@
|
||||
@Created on: 2021/6/1 001 22:57
|
||||
@Remark: 自定义视图集
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from django.db import transaction
|
||||
from django_filters import DateTimeFromToRangeFilter
|
||||
from django_filters.rest_framework import FilterSet
|
||||
from drf_yasg import openapi
|
||||
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
|
||||
from dvadmin.utils.filters import DataLevelPermissionsFilter, CoreModelFilterBankend
|
||||
from dvadmin.utils.import_export_mixin import ExportSerializerMixin, ImportSerializerMixin
|
||||
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse
|
||||
from dvadmin.utils.permission import CustomPermission
|
||||
from dvadmin.utils.models import get_custom_app_models, CoreModel
|
||||
from dvadmin.system.models import FieldPermission, MenuField
|
||||
from django_restql.mixins import QueryArgumentsMixin
|
||||
|
||||
|
||||
@@ -37,7 +39,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
update_serializer_class = None
|
||||
filter_fields = '__all__'
|
||||
search_fields = ()
|
||||
extra_filter_class = [DataLevelPermissionsFilter]
|
||||
extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionsFilter]
|
||||
permission_classes = [CustomPermission]
|
||||
import_field_dict = {}
|
||||
export_field_label = {}
|
||||
@@ -63,12 +65,34 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
serializer_class = self.get_serializer_class()
|
||||
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
|
||||
# 在分页器中使用
|
||||
self.request.permission_fields = can_see
|
||||
if isinstance(self.request.data, list):
|
||||
with transaction.atomic():
|
||||
return serializer_class(many=True, *args, **kwargs)
|
||||
else:
|
||||
return serializer_class(*args, **kwargs)
|
||||
|
||||
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:
|
||||
return []
|
||||
return MenuField.objects.filter(model=model['model']
|
||||
).values('field_name', 'title')
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data, request=request)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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()
|
||||
uvicorn.run("application.asgi:application", reload=False, host="0.0.0.0", port=8000, workers=4,
|
||||
log_config=LOGGING)
|
||||
+26
-25
@@ -1,31 +1,32 @@
|
||||
Django==4.1.5
|
||||
Django==4.2.7
|
||||
django-comment-migrate==0.1.7
|
||||
django-cors-headers==3.13.0
|
||||
django-filter==21.1
|
||||
django-cors-headers==4.3.0
|
||||
django-filter==23.3
|
||||
django-ranged-response==0.2.0
|
||||
djangorestframework==3.14.0
|
||||
django-restql==0.15.3
|
||||
django-simple-captcha==0.5.17
|
||||
django-timezone-field==5.0
|
||||
djangorestframework-simplejwt==5.2.2
|
||||
drf-yasg==1.21.4
|
||||
mysqlclient==2.1.1
|
||||
pypinyin==0.48.0
|
||||
ua-parser==0.16.1
|
||||
pyparsing==3.0.9
|
||||
openpyxl==3.0.10
|
||||
requests==2.28.2
|
||||
loguru==0.6.0
|
||||
typing-extensions==4.4.0
|
||||
smmap==5.0.0
|
||||
tzlocal==4.1
|
||||
channels==4.0.0
|
||||
channels-redis==4.0.0
|
||||
websockets==10.4
|
||||
django-simple-captcha==0.5.20
|
||||
django-timezone-field==6.0.1
|
||||
djangorestframework-simplejwt==5.3.0
|
||||
drf-yasg==1.21.7
|
||||
mysqlclient==2.2.0
|
||||
pypinyin==0.49.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
|
||||
user-agents==2.2.0
|
||||
six==1.16.0
|
||||
whitenoise==6.3.0
|
||||
psycopg2==2.9.5
|
||||
uvicorn==0.20.0
|
||||
gunicorn==20.1.0
|
||||
gevent==22.10.2
|
||||
whitenoise==6.6.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
|
||||
pyinstaller==6.8.0
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -0,0 +1,34 @@
|
||||
html,body{width:100%;overflow-x: hidden;margin: 0;padding: 0;font-size:1rem;font-family: "PingFang SC","SF Pro SC","SF Pro Text","SF Pro Icons","Helvetica Neue","Helvetica","Arial",sans-serif;
|
||||
-webkit-font-smoothing: antialiased;}
|
||||
a{text-decoration:none;}
|
||||
ul,li{list-style:none;padding:0;margin:0;}
|
||||
.bg{background-repeat:no-repeat;background-position:center center;background-size:100%;}
|
||||
.center{margin:0 auto;position:relative;}
|
||||
.abs_center{position:absolute;left: 50%;transform: translateX(-50%);-webkit-transform:translateX(-50%);}
|
||||
/*滚动条统一样式*/
|
||||
.scrollbar{overflow-x:hidden;scrollbar-track-color:none;}
|
||||
.scrollbar::-webkit-scrollbar{width:8px;background-color:transparent;} /*滚动条整体部分*/
|
||||
.scrollbar::-webkit-scrollbar-track{border-radius:4px;-webkit-box-shadow: inset 0 0 0 rgba(0,0,0,0);background-color:transparent;display:none;} /* 滚动条的轨道*/
|
||||
.scrollbar::-webkit-scrollbar-track-piece{background-color:transparent;display:none;}/* 滚动条的内层轨道*/
|
||||
.scrollbar::-webkit-scrollbar-thumb{border-radius:4px;background: #313131;}/*滚动条里面的小方块,能向上向下移动(或往左往右移动,取决于是垂直滚动条还是水平滚动条)*/
|
||||
|
||||
h3,h4,h5{ font-weight: bold;}
|
||||
h3{ font-size: .4rem; color:#000;}
|
||||
h4{ font-size: .35rem; color: #000; margin:.2rem 0 0 0;}
|
||||
p{ line-height: .5rem; margin:.2rem 0;word-break: break-all;text-align:justify;}
|
||||
.app .main{width:90%;overflow:hidden;font-size:.30rem;box-sizing: border-box; margin: 0 auto; color: #484d57;}
|
||||
.app ul { padding-left: .1rem; font-size: .26rem;}
|
||||
.app ul p{ line-height: .4rem;}
|
||||
.app .mainbg{position:absolute;left:50%;transform:translateX(-50%);background-repeat:no-repeat;background-size:100%;}
|
||||
|
||||
.app .poster{width:100%;height:31.85rem;background-image:url(../images/poster_bg.jpg);}
|
||||
|
||||
.pc .main{font-size:.16rem;}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
}
|
||||
Binary file not shown.
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;
|
||||
|
||||
Binary file not shown.
@@ -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.
|
||||
|
||||
Binary file not shown.
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -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>
|
||||
Binary file not shown.
@@ -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.
|
||||
|
||||
Binary file not shown.
+1801
-105
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.
Binary file not shown.
@@ -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.
|
||||
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
swagger-ui
|
||||
Copyright 2020-2021 SmartBear Software Inc.
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
Binary file not shown.
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>
|
||||
|
||||
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
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.
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user