1、初始化后端api和web项目
This commit is contained in:
		
							
								
								
									
										4
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| *.css linguist-language=Python | ||||
| *.less linguist-language=Python | ||||
| *.js linguist-language=Python | ||||
| *.html linguist-language=Python | ||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| /backend/venv | ||||
| /backend/.idea | ||||
| .idea | ||||
|  | ||||
| .history/ | ||||
| .vscode/ | ||||
| web/package-lock.json | ||||
							
								
								
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| # Django-Vue3-Admin 更新日志 | ||||
|  | ||||
| ## 正式发布v3.0.0版本 | ||||
| ### 1.新增:列权限管理与授权; | ||||
| ### 2.新增:代码新版本发布后,进行升级提醒; | ||||
| ### 3.优化:角色管理中按钮权限的操作; | ||||
| ### 4.优化:websocket 连接状态显示; | ||||
| ### 5.优化:初始化获取系统配置与字典配置,进行动态渲染登录页面; | ||||
| ### 6.修复:登录页面中系统配置不生效问题; | ||||
| ### 7.其他优化 | ||||
							
								
								
									
										201
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,201 @@ | ||||
|                                  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 [2022] [django-vue-admin] | ||||
|  | ||||
|    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. | ||||
							
								
								
									
										13
									
								
								NOTICE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								NOTICE
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| Copyright 2021 李强 | ||||
|  | ||||
| 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. | ||||
							
								
								
									
										175
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,175 @@ | ||||
| # Django-Vue3-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) | ||||
|  | ||||
| [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. | ||||
|  | ||||
| * 🧑🤝🧑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) | ||||
|  | ||||
| * demo account:superadmin | ||||
|  | ||||
| * demo password:admin123456 | ||||
|  | ||||
| 👩👦👦docs:[https://django-vue-admin.com](https://django-vue-admin.com) | ||||
|  | ||||
| ## 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: | ||||
|  | ||||
| gitee(Main push):[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)👩👦👦 | ||||
|  | ||||
| ## core function | ||||
|  | ||||
| 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 | ||||
|  | ||||
| ## before start project you need: | ||||
|  | ||||
| ~~~ | ||||
| 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 | ||||
| # clone code | ||||
| git clone https://gitee.com/huge-dream/django-vue3-admin.git | ||||
|  | ||||
| # enter code dir | ||||
| cd web | ||||
|  | ||||
| # install dependence | ||||
| npm install yarn | ||||
| yarn install --registry=https://registry.npm.taobao.org | ||||
|  | ||||
| # 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. 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 | ||||
|  | ||||
| * 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 | ||||
|  | ||||
| ~~~shell | ||||
| docker-compose up -d | ||||
| # 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 | ||||
|  | ||||
| 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 stop | ||||
| docker-compose down | ||||
| #  docker-compose restart | ||||
| docker-compose restart | ||||
| #  docker-compose on start build | ||||
| docker-compose up -d --build | ||||
| ~~~ | ||||
|  | ||||
| ## Demo screenshot✅ | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										214
									
								
								README.zh.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								README.zh.md
									
									
									
									
									
										Normal file
									
								
							| @ -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.npmmirror.com | ||||
|  | ||||
| # 启动服务 | ||||
| yarn build | ||||
| # 浏览器访问 http://localhost:8080 | ||||
| # .env.development 文件中可配置启动端口等参数 | ||||
| # 构建生产环境 | ||||
| # yarn run build | ||||
| ``` | ||||
|  | ||||
|  | ||||
|  | ||||
| ## 后端💈 | ||||
|  | ||||
| ~~~bash | ||||
| 1. 进入项目目录 cd backend | ||||
| 2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py | ||||
| 3. 在 env.py 中配置数据库信息 | ||||
| 	mysql数据库版本建议:8.0 | ||||
| 	mysql数据库字符集:utf8mb4 | ||||
| 4. 安装依赖环境 | ||||
| 	pip3 install -r requirements.txt | ||||
| 5. 执行迁移命令: | ||||
| 	python3 manage.py makemigrations | ||||
| 	python3 manage.py migrate | ||||
| 6. 初始化数据 | ||||
| 	python3 manage.py init | ||||
| 7. 初始化省市县数据: | ||||
| 	python3 manage.py init_area | ||||
| 8. 启动项目 | ||||
| 	python3 manage.py runserver 0.0.0.0:8000 | ||||
| 或使用 uvicorn : | ||||
|   uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8 | ||||
| ~~~ | ||||
| ## 开发建议 | ||||
| 前后端backend与web各自单独一个窗口打开进行开发 | ||||
|  | ||||
| ### 访问项目 | ||||
|  | ||||
| - 访问地址:[http://localhost:8080](http://localhost:8080) (默认为此地址,如有修改请按照配置文件) | ||||
| - 账号:`superadmin` 密码:`admin123456` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### docker-compose 运行 | ||||
|  | ||||
| ~~~shell | ||||
| # 先安装docker-compose (自行百度安装),执行此命令等待安装,如有使用celery插件请打开docker-compose.yml中celery 部分注释 | ||||
| docker-compose up -d | ||||
| # 初始化后端数据(第一次执行即可) | ||||
| docker exec -ti dvadmin3-django bash | ||||
| python manage.py makemigrations  | ||||
| python manage.py migrate | ||||
| python manage.py init_area | ||||
| python manage.py init | ||||
| exit | ||||
|  | ||||
| 前端地址:http://127.0.0.1:8080 | ||||
| 后端地址:http://127.0.0.1:8080/api | ||||
| # 在服务器上请把127.0.0.1 换成自己公网ip | ||||
| 账号:superadmin 密码:admin123456 | ||||
|  | ||||
| # docker-compose 停止 | ||||
| docker-compose down | ||||
| #  docker-compose 重启 | ||||
| docker-compose restart | ||||
| #  docker-compose 启动时重新进行 build | ||||
| docker-compose up -d --build | ||||
| ~~~ | ||||
|  | ||||
|  | ||||
|  | ||||
| ## 演示图✅ | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										102
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,102 @@ | ||||
| # Byte-compiled / optimized / DLL files | ||||
| *.py[cod] | ||||
| *$py.class | ||||
| __pycache__/ | ||||
| # C extensions | ||||
| *.so | ||||
|  | ||||
| # Distribution / packaging | ||||
| .Python | ||||
| env/ | ||||
| develop-eggs/ | ||||
| dist/ | ||||
| downloads/ | ||||
| eggs/ | ||||
| .eggs/ | ||||
| lib/ | ||||
| lib64/ | ||||
| parts/ | ||||
| sdist/ | ||||
| var/ | ||||
| *.egg-info/ | ||||
| .installed.cfg | ||||
| *.egg | ||||
|  | ||||
| # PyInstaller | ||||
| #  Usually these files are written by a python script from a template | ||||
| #  before PyInstaller builds the exe, so as to inject date/other infos into it. | ||||
| *.manifest | ||||
| *.spec | ||||
|  | ||||
| # Installer logs | ||||
| pip-log.txt | ||||
| pip-delete-this-directory.txt | ||||
|  | ||||
| # Unit test / coverage reports | ||||
| htmlcov/ | ||||
| .tox/ | ||||
| .coverage | ||||
| .coverage.* | ||||
| .cache | ||||
| nosetests.xml | ||||
| coverage.xml | ||||
| *,cover | ||||
| .hypothesis/ | ||||
|  | ||||
| # Translations | ||||
| *.mo | ||||
| *.pot | ||||
|  | ||||
| # Django stuff: | ||||
| *.log | ||||
| local_settings.py | ||||
|  | ||||
| # Flask stuff: | ||||
| instance/ | ||||
| .webassets-cache | ||||
|  | ||||
| # Scrapy stuff: | ||||
| .scrapy | ||||
|  | ||||
| # Sphinx documentation | ||||
| docs/_build/ | ||||
|  | ||||
| # PyBuilder | ||||
| target/ | ||||
|  | ||||
| # IPython Notebook | ||||
| .ipynb_checkpoints | ||||
|  | ||||
| # pyenv | ||||
| .python-version | ||||
|  | ||||
| # celery beat schedule file | ||||
| celerybeat-schedule | ||||
|  | ||||
| # dotenv | ||||
| .env | ||||
|  | ||||
| # virtualenv | ||||
| venv/ | ||||
| ENV/ | ||||
|  | ||||
| # Spyder project settings | ||||
| .spyderproject | ||||
|  | ||||
| # Rope project settings | ||||
| .ropeproject | ||||
| .idea/ | ||||
| *.db | ||||
| .DS_Store | ||||
| **/migrations/*.py | ||||
| !**/migrations/__init__.py | ||||
| *.pyc | ||||
| conf/* | ||||
| !conf/env.example.py | ||||
| db.sqlite3 | ||||
| media/ | ||||
| __pypackages__/ | ||||
| package-lock.json | ||||
| gunicorn.pid | ||||
| plugins/* | ||||
| !plugins/__init__.py | ||||
							
								
								
									
										1
									
								
								backend/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								backend/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
|  | ||||
							
								
								
									
										5
									
								
								backend/application/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								backend/application/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| # This will make sure the app is always imported when | ||||
| # Django starts so that shared_task will use this app. | ||||
| from .celery import app as celery_app | ||||
|  | ||||
| __all__ = ('celery_app',) | ||||
							
								
								
									
										32
									
								
								backend/application/asgi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								backend/application/asgi.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| """ | ||||
| ASGI config for application project. | ||||
|  | ||||
| It exposes the ASGI callable as a module-level variable named ``application``. | ||||
|  | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ | ||||
| """ | ||||
|  | ||||
| import os | ||||
| from channels.auth import AuthMiddlewareStack | ||||
| from channels.security.websocket import AllowedHostsOriginValidator | ||||
| from channels.routing import ProtocolTypeRouter, URLRouter | ||||
| from django.core.asgi import get_asgi_application | ||||
|  | ||||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') | ||||
| os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" | ||||
|  | ||||
| http_application = get_asgi_application() | ||||
|  | ||||
| from application.routing import websocket_urlpatterns | ||||
|  | ||||
| application = ProtocolTypeRouter({ | ||||
|     "http": http_application, | ||||
|     'websocket': AllowedHostsOriginValidator( | ||||
|         AuthMiddlewareStack( | ||||
|             URLRouter( | ||||
|                 websocket_urlpatterns  # 指明路由文件是devops/routing.py | ||||
|             ) | ||||
|         ) | ||||
|     ), | ||||
| }) | ||||
							
								
								
									
										40
									
								
								backend/application/celery.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								backend/application/celery.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| 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 | ||||
|  | ||||
|     app = TenantAwareCeleryApp() | ||||
| else: | ||||
|     from celery import Celery | ||||
|  | ||||
|     app = Celery(f"application") | ||||
| app.config_from_object('django.conf:settings', namespace='CELERY') | ||||
| 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 | ||||
							
								
								
									
										275
									
								
								backend/application/dispatch.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								backend/application/dispatch.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,275 @@ | ||||
| #!/usr/bin/env python | ||||
| # -*- coding: utf-8 -*- | ||||
| from django.conf import settings | ||||
| from django.db import connection | ||||
| from django.core.cache import cache | ||||
| from dvadmin.utils.validator import CustomValidationError | ||||
|  | ||||
| dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory')  # redis | ||||
|  | ||||
|  | ||||
| def is_tenants_mode(): | ||||
|     """ | ||||
|     判断是否为租户模式 | ||||
|     :return: | ||||
|     """ | ||||
|     return hasattr(connection, "tenant") and connection.tenant.schema_name | ||||
|  | ||||
|  | ||||
| # ================================================= # | ||||
| # ******************** 初始化 ******************** # | ||||
| # ================================================= # | ||||
| def _get_all_dictionary(): | ||||
|     from dvadmin.system.models import Dictionary | ||||
|  | ||||
|     queryset = Dictionary.objects.filter(status=True, is_value=False) | ||||
|     data = [] | ||||
|     for instance in queryset: | ||||
|         data.append( | ||||
|             { | ||||
|                 "id": instance.id, | ||||
|                 "value": instance.value, | ||||
|                 "children": list( | ||||
|                     Dictionary.objects.filter(parent=instance.id) | ||||
|                     .filter(status=1) | ||||
|                     .values("label", "value", "type", "color") | ||||
|                 ), | ||||
|             } | ||||
|         ) | ||||
|     return {ele.get("value"): ele for ele in data} | ||||
|  | ||||
|  | ||||
| def _get_all_system_config(): | ||||
|     data = {} | ||||
|     from dvadmin.system.models import SystemConfig | ||||
|  | ||||
|     system_config_obj = ( | ||||
|         SystemConfig.objects.filter(parent_id__isnull=False) | ||||
|         .values("parent__key", "key", "value", "form_item_type") | ||||
|         .order_by("sort") | ||||
|     ) | ||||
|     for system_config in system_config_obj: | ||||
|         value = system_config.get("value", "") | ||||
|         if value and system_config.get("form_item_type") == 7: | ||||
|             value = value[0].get("url") | ||||
|         if value and system_config.get("form_item_type") == 11: | ||||
|             new_value = [] | ||||
|             for ele in value: | ||||
|                 new_value.append({ | ||||
|                     "key": ele.get('key'), | ||||
|                     "title": ele.get('title'), | ||||
|                     "value": ele.get('value'), | ||||
|                 }) | ||||
|             new_value.sort(key=lambda s: s["key"]) | ||||
|             value = new_value | ||||
|         data[f"{system_config.get('parent__key')}.{system_config.get('key')}"] = value | ||||
|     return data | ||||
|  | ||||
|  | ||||
| def init_dictionary(): | ||||
|     """ | ||||
|     初始化字典配置 | ||||
|     :return: | ||||
|     """ | ||||
|     try: | ||||
|         if dispatch_db_type == 'redis': | ||||
|             cache.set(f"init_dictionary", _get_all_dictionary()) | ||||
|             return | ||||
|         if is_tenants_mode(): | ||||
|             from django_tenants.utils import tenant_context, get_tenant_model | ||||
|  | ||||
|             for tenant in get_tenant_model().objects.filter(): | ||||
|                 with tenant_context(tenant): | ||||
|                     settings.DICTIONARY_CONFIG[connection.tenant.schema_name] = _get_all_dictionary() | ||||
|         else: | ||||
|             settings.DICTIONARY_CONFIG = _get_all_dictionary() | ||||
|     except Exception as e: | ||||
|         print("请先进行数据库迁移!") | ||||
|     return | ||||
|  | ||||
|  | ||||
| def init_system_config(): | ||||
|     """ | ||||
|     初始化系统配置 | ||||
|     :param name: | ||||
|     :return: | ||||
|     """ | ||||
|     try: | ||||
|         if dispatch_db_type == 'redis': | ||||
|             cache.set(f"init_system_config", _get_all_system_config()) | ||||
|             return | ||||
|         if is_tenants_mode(): | ||||
|             from django_tenants.utils import tenant_context, get_tenant_model | ||||
|  | ||||
|             for tenant in get_tenant_model().objects.filter(): | ||||
|                 with tenant_context(tenant): | ||||
|                     settings.SYSTEM_CONFIG[connection.tenant.schema_name] = _get_all_system_config() | ||||
|         else: | ||||
|             settings.SYSTEM_CONFIG = _get_all_system_config() | ||||
|     except Exception as e: | ||||
|         print("请先进行数据库迁移!") | ||||
|     return | ||||
|  | ||||
|  | ||||
| def refresh_dictionary(): | ||||
|     """ | ||||
|     刷新字典配置 | ||||
|     :return: | ||||
|     """ | ||||
|     if dispatch_db_type == 'redis': | ||||
|         cache.set(f"init_dictionary", _get_all_dictionary()) | ||||
|         return | ||||
|     if is_tenants_mode(): | ||||
|         from django_tenants.utils import tenant_context, get_tenant_model | ||||
|  | ||||
|         for tenant in get_tenant_model().objects.filter(): | ||||
|             with tenant_context(tenant): | ||||
|                 settings.DICTIONARY_CONFIG[connection.tenant.schema_name] = _get_all_dictionary() | ||||
|     else: | ||||
|         settings.DICTIONARY_CONFIG = _get_all_dictionary() | ||||
|  | ||||
|  | ||||
| def refresh_system_config(): | ||||
|     """ | ||||
|     刷新系统配置 | ||||
|     :return: | ||||
|     """ | ||||
|     if dispatch_db_type == 'redis': | ||||
|         cache.set(f"init_system_config", _get_all_system_config()) | ||||
|         return | ||||
|     if is_tenants_mode(): | ||||
|         from django_tenants.utils import tenant_context, get_tenant_model | ||||
|  | ||||
|         for tenant in get_tenant_model().objects.filter(): | ||||
|             with tenant_context(tenant): | ||||
|                 settings.SYSTEM_CONFIG[connection.tenant.schema_name] = _get_all_system_config() | ||||
|     else: | ||||
|         settings.SYSTEM_CONFIG = _get_all_system_config() | ||||
|  | ||||
|  | ||||
| # ================================================= # | ||||
| # ******************** 字典管理 ******************** # | ||||
| # ================================================= # | ||||
| def get_dictionary_config(schema_name=None): | ||||
|     """ | ||||
|     获取字典所有配置 | ||||
|     :param schema_name: 对应字典配置的租户schema_name值 | ||||
|     :return: | ||||
|     """ | ||||
|     if dispatch_db_type == 'redis': | ||||
|         init_dictionary_data = cache.get(f"init_dictionary") | ||||
|         if not init_dictionary_data: | ||||
|             refresh_dictionary() | ||||
|         return cache.get(f"init_dictionary") or {} | ||||
|     if not settings.DICTIONARY_CONFIG: | ||||
|         refresh_dictionary() | ||||
|     if is_tenants_mode(): | ||||
|         dictionary_config = settings.DICTIONARY_CONFIG[schema_name or connection.tenant.schema_name] | ||||
|     else: | ||||
|         dictionary_config = settings.DICTIONARY_CONFIG | ||||
|     return dictionary_config or {} | ||||
|  | ||||
|  | ||||
| def get_dictionary_values(key, schema_name=None): | ||||
|     """ | ||||
|     获取字典数据数组 | ||||
|     :param key: 对应字典配置的key值(字典编号) | ||||
|     :param schema_name: 对应字典配置的租户schema_name值 | ||||
|     :return: | ||||
|     """ | ||||
|     if dispatch_db_type == 'redis': | ||||
|         dictionary_config = cache.get(f"init_dictionary") | ||||
|         if not dictionary_config: | ||||
|             refresh_dictionary() | ||||
|             dictionary_config = cache.get(f"init_dictionary") | ||||
|         return dictionary_config.get(key) | ||||
|     dictionary_config = get_dictionary_config(schema_name) | ||||
|     return dictionary_config.get(key) | ||||
|  | ||||
|  | ||||
| def get_dictionary_label(key, name, schema_name=None): | ||||
|     """ | ||||
|     获取获取字典label值 | ||||
|     :param key: 字典管理中的key值(字典编号) | ||||
|     :param name: 对应字典配置的value值 | ||||
|     :param schema_name: 对应字典配置的租户schema_name值 | ||||
|     :return: | ||||
|     """ | ||||
|     res = get_dictionary_values(key, schema_name) or [] | ||||
|     for ele in res.get('children'): | ||||
|         if ele.get("value") == str(name): | ||||
|             return ele.get("label") | ||||
|     return "" | ||||
|  | ||||
|  | ||||
| # ================================================= # | ||||
| # ******************** 系统配置 ******************** # | ||||
| # ================================================= # | ||||
| def get_system_config(schema_name=None): | ||||
|     """ | ||||
|     获取系统配置中所有配置 | ||||
|     1.只传父级的key,返回全部子级,{ "父级key.子级key" : "值" } | ||||
|     2."父级key.子级key",返回子级值 | ||||
|     :param schema_name: 对应字典配置的租户schema_name值 | ||||
|     :return: | ||||
|     """ | ||||
|     if dispatch_db_type == 'redis': | ||||
|         init_dictionary_data = cache.get(f"init_system_config") | ||||
|         if not init_dictionary_data: | ||||
|             refresh_system_config() | ||||
|         return cache.get(f"init_system_config") or {} | ||||
|     if not settings.SYSTEM_CONFIG: | ||||
|         refresh_system_config() | ||||
|     if is_tenants_mode(): | ||||
|         dictionary_config = settings.SYSTEM_CONFIG[schema_name or connection.tenant.schema_name] | ||||
|     else: | ||||
|         dictionary_config = settings.SYSTEM_CONFIG | ||||
|     return dictionary_config or {} | ||||
|  | ||||
|  | ||||
| def get_system_config_values(key, schema_name=None): | ||||
|     """ | ||||
|     获取系统配置数据数组 | ||||
|     :param key: 对应系统配置的key值(字典编号) | ||||
|     :param schema_name: 对应系统配置的租户schema_name值 | ||||
|     :return: | ||||
|     """ | ||||
|     if dispatch_db_type == 'redis': | ||||
|         system_config = cache.get(f"init_system_config") | ||||
|         if not system_config: | ||||
|             refresh_system_config() | ||||
|             system_config = cache.get(f"init_system_config") | ||||
|         return system_config.get(key) | ||||
|     system_config = get_system_config(schema_name) | ||||
|     return system_config.get(key) | ||||
|  | ||||
|  | ||||
| def get_system_config_values_to_dict(key, schema_name=None): | ||||
|     """ | ||||
|     获取系统配置数据并转换为字典 **仅限于数组类型系统配置 | ||||
|     :param key: 对应系统配置的key值(字典编号) | ||||
|     :param schema_name: 对应系统配置的租户schema_name值 | ||||
|     :return: | ||||
|     """ | ||||
|     values_dict = {} | ||||
|     config_values = get_system_config_values(key, schema_name) | ||||
|     if not isinstance(config_values, list): | ||||
|         raise CustomValidationError("该方式仅限于数组类型系统配置") | ||||
|     for ele in get_system_config_values(key, schema_name): | ||||
|         values_dict[ele.get('key')] = ele.get('value') | ||||
|     return values_dict | ||||
|  | ||||
|  | ||||
| def get_system_config_label(key, name, schema_name=None): | ||||
|     """ | ||||
|     获取获取系统配置label值 | ||||
|     :param key: 系统配置中的key值(字典编号) | ||||
|     :param name: 对应系统配置的value值 | ||||
|     :param schema_name: 对应系统配置的租户schema_name值 | ||||
|     :return: | ||||
|     """ | ||||
|     children = get_system_config_values(key, schema_name) or [] | ||||
|     for ele in children: | ||||
|         if ele.get("value") == str(name): | ||||
|             return ele.get("label") | ||||
|     return "" | ||||
							
								
								
									
										7
									
								
								backend/application/routing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								backend/application/routing.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| from django.urls import path | ||||
| from application.websocketConfig import MegCenter | ||||
|  | ||||
| websocket_urlpatterns = [ | ||||
|     path('ws/<str:service_uid>/', MegCenter.as_asgi()),  # consumers.DvadminWebSocket 是该路由的消费者 | ||||
| ] | ||||
							
								
								
									
										424
									
								
								backend/application/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										424
									
								
								backend/application/settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,424 @@ | ||||
| """ | ||||
| Django settings for application project. | ||||
|  | ||||
| Generated by 'django-admin startproject' using Django 3.2.3. | ||||
|  | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/4.1/topics/settings/ | ||||
|  | ||||
| For the full list of settings and their values, see | ||||
| 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 | ||||
|  | ||||
| # ================================================= # | ||||
| # ******************** 动态配置 ******************** # | ||||
| # ================================================= # | ||||
|  | ||||
| from conf.env import * | ||||
|  | ||||
| # Quick-start development settings - unsuitable for production | ||||
| # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ | ||||
|  | ||||
| # SECURITY WARNING: keep the secret key used in production secret! | ||||
| SECRET_KEY = "django-insecure--z8%exyzt7e_%i@1+#1mm=%lb5=^fx_57=1@a+_y7bg5-w%)sm" | ||||
| # 初始化plugins插件路径到环境变量中 | ||||
| PLUGINS_PATH = os.path.join(BASE_DIR, "plugins") | ||||
| sys.path.insert(0, os.path.join(PLUGINS_PATH)) | ||||
|  | ||||
| [ | ||||
|     sys.path.insert(0, os.path.join(PLUGINS_PATH, ele)) | ||||
|     for ele in os.listdir(PLUGINS_PATH) | ||||
|     if os.path.isdir(os.path.join(PLUGINS_PATH, ele)) and not ele.startswith("__") | ||||
| ] | ||||
|  | ||||
| # SECURITY WARNING: don't run with debug turned on in production! | ||||
| DEBUG = locals().get("DEBUG", True) | ||||
| ALLOWED_HOSTS = locals().get("ALLOWED_HOSTS", ["*"]) | ||||
|  | ||||
| # 列权限需要排除的App应用 | ||||
| COLUMN_EXCLUDE_APPS = ['channels', 'captcha'] + locals().get("COLUMN_EXCLUDE_APPS", []) | ||||
|  | ||||
| INSTALLED_APPS = [ | ||||
|     "django.contrib.auth", | ||||
|     "django.contrib.contenttypes", | ||||
|     "django.contrib.sessions", | ||||
|     "django.contrib.messages", | ||||
|     "django.contrib.staticfiles", | ||||
|     "django_comment_migrate", | ||||
|     "rest_framework", | ||||
|     "django_filters", | ||||
|     "corsheaders",  # 注册跨域app | ||||
|     "drf_yasg", | ||||
|     "captcha", | ||||
|     "channels", | ||||
|     "dvadmin.system", | ||||
| ] | ||||
| #主要添加如下代码 | ||||
| My_Apps = [ | ||||
| 	'translate',  #新的应用写在这里 | ||||
| ] | ||||
|  | ||||
| INSTALLED_APPS += My_Apps | ||||
| MIDDLEWARE = [ | ||||
|     "dvadmin.utils.middleware.HealthCheckMiddleware", | ||||
|     "django.middleware.security.SecurityMiddleware", | ||||
|     "whitenoise.middleware.WhiteNoiseMiddleware", | ||||
|     "django.contrib.sessions.middleware.SessionMiddleware", | ||||
|     "corsheaders.middleware.CorsMiddleware",  # 跨域中间件 | ||||
|     "django.middleware.common.CommonMiddleware", | ||||
|     "django.middleware.csrf.CsrfViewMiddleware", | ||||
|     "django.contrib.auth.middleware.AuthenticationMiddleware", | ||||
|     "django.contrib.messages.middleware.MessageMiddleware", | ||||
|     "django.middleware.clickjacking.XFrameOptionsMiddleware", | ||||
|     "dvadmin.utils.middleware.ApiLoggingMiddleware", | ||||
| ] | ||||
|  | ||||
| ROOT_URLCONF = "application.urls" | ||||
|  | ||||
| TEMPLATES = [ | ||||
|     { | ||||
|         "BACKEND": "django.template.backends.django.DjangoTemplates", | ||||
|         "DIRS": [os.path.join(BASE_DIR, "templates")], | ||||
|         "APP_DIRS": True, | ||||
|         "OPTIONS": { | ||||
|             "context_processors": [ | ||||
|                 "django.template.context_processors.debug", | ||||
|                 "django.template.context_processors.request", | ||||
|                 "django.contrib.auth.context_processors.auth", | ||||
|                 "django.contrib.messages.context_processors.messages", | ||||
|             ], | ||||
|         }, | ||||
|     }, | ||||
| ] | ||||
|  | ||||
| WSGI_APPLICATION = "application.wsgi.application" | ||||
|  | ||||
| # Database | ||||
| # https://docs.djangoproject.com/en/3.2/ref/settings/#databases | ||||
|  | ||||
| DATABASES = { | ||||
|     "default": { | ||||
|         "ENGINE": DATABASE_ENGINE, | ||||
|         "NAME": DATABASE_NAME, | ||||
|         "USER": DATABASE_USER, | ||||
|         "PASSWORD": DATABASE_PASSWORD, | ||||
|         "HOST": DATABASE_HOST, | ||||
|         "PORT": DATABASE_PORT, | ||||
|         'OPTIONS': { | ||||
|                     'charset': 'utf8mb4', | ||||
|                     'collation': 'utf8mb4_unicode_ci', | ||||
|                     'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",  # 严格模式 | ||||
|                 } | ||||
|     } | ||||
| } | ||||
| AUTH_USER_MODEL = "system.Users" | ||||
| USERNAME_FIELD = "username" | ||||
|  | ||||
| # Password validation | ||||
| # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators | ||||
|  | ||||
| AUTH_PASSWORD_VALIDATORS = [ | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", | ||||
|     }, | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", | ||||
|     }, | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", | ||||
|     }, | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", | ||||
|     }, | ||||
| ] | ||||
|  | ||||
| # Internationalization | ||||
| # https://docs.djangoproject.com/en/3.2/topics/i18n/ | ||||
|  | ||||
| LANGUAGE_CODE = "zh-hans" | ||||
|  | ||||
| TIME_ZONE = "Asia/Shanghai" | ||||
|  | ||||
| USE_I18N = True | ||||
|  | ||||
| USE_L10N = True | ||||
|  | ||||
| USE_TZ = False | ||||
|  | ||||
| # Static files (CSS, JavaScript, Images) | ||||
| # https://docs.djangoproject.com/en/3.2/howto/static-files/ | ||||
|  | ||||
| STATIC_URL = "/static/" | ||||
| # # 设置django的静态文件目录 | ||||
| STATICFILES_DIRS = [ | ||||
|     os.path.join(BASE_DIR, "static"), | ||||
| ] | ||||
|  | ||||
| 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') | ||||
|  | ||||
| # ================================================= # | ||||
| # ******************* 跨域的配置 ******************* # | ||||
| # ================================================= # | ||||
|  | ||||
| # 全部允许配置 | ||||
| CORS_ORIGIN_ALLOW_ALL = True | ||||
| # 允许cookie | ||||
| CORS_ALLOW_CREDENTIALS = True  # 指明在跨域访问中,后端是否支持对cookie的操作 | ||||
|  | ||||
| # ===================================================== # | ||||
| # ********************* channels配置 ******************* # | ||||
| # ===================================================== # | ||||
| ASGI_APPLICATION = 'application.asgi.application' | ||||
| CHANNEL_LAYERS = { | ||||
|     "default": { | ||||
|         "BACKEND": "channels.layers.InMemoryChannelLayer" | ||||
|     } | ||||
| } | ||||
| # CHANNEL_LAYERS = { | ||||
| #     'default': { | ||||
| #         'BACKEND': 'channels_redis.core.RedisChannelLayer', | ||||
| #         'CONFIG': { | ||||
| #             "hosts": [('127.0.0.1', 6379)], #需修改 | ||||
| #         }, | ||||
| #     }, | ||||
| # } | ||||
|  | ||||
|  | ||||
| # ================================================= # | ||||
| # ********************* 日志配置 ******************* # | ||||
| # ================================================= # | ||||
| # # 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": { | ||||
|         "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": { | ||||
|         "file": { | ||||
|             "level": "INFO", | ||||
|             "class": "logging.handlers.RotatingFileHandler", | ||||
|             "filename": SERVER_LOGS_FILE, | ||||
|             "maxBytes": 1024 * 1024 * 100,  # 100 MB | ||||
|             "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": { | ||||
|         "": { | ||||
|             "handlers": ["console", "error", "file"], | ||||
|             "level": "INFO", | ||||
|         }, | ||||
|         "django": { | ||||
|             "handlers": ["console", "error", "file"], | ||||
|             "level": "INFO", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         'django.db.backends': { | ||||
|             'handlers': ["console", "error", "file"], | ||||
|             'propagate': False, | ||||
|             'level': "INFO" | ||||
|         }, | ||||
|         "uvicorn.error": { | ||||
|             "level": "INFO", | ||||
|             "handlers": ["console", "error", "file"], | ||||
|         }, | ||||
|         "uvicorn.access": { | ||||
|             "handlers": ["console", "error", "file"], | ||||
|             "level": "INFO" | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| # ================================================= # | ||||
| # *************** REST_FRAMEWORK配置 *************** # | ||||
| # ================================================= # | ||||
|  | ||||
| REST_FRAMEWORK = { | ||||
|     'DEFAULT_PARSER_CLASSES': ( | ||||
|         'rest_framework.parsers.JSONParser', | ||||
|         'rest_framework.parsers.MultiPartParser', | ||||
|     ), | ||||
|     "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S",  # 日期时间格式配置 | ||||
|     "DATE_FORMAT": "%Y-%m-%d", | ||||
|     "DEFAULT_FILTER_BACKENDS": ( | ||||
|         # 'django_filters.rest_framework.DjangoFilterBackend', | ||||
|         "dvadmin.utils.filters.CustomDjangoFilterBackend", | ||||
|         "rest_framework.filters.SearchFilter", | ||||
|         "rest_framework.filters.OrderingFilter", | ||||
|     ), | ||||
|     "DEFAULT_PAGINATION_CLASS": "dvadmin.utils.pagination.CustomPagination",  # 自定义分页 | ||||
|     "DEFAULT_AUTHENTICATION_CLASSES": ( | ||||
|         "rest_framework_simplejwt.authentication.JWTAuthentication", | ||||
|         "rest_framework.authentication.SessionAuthentication", | ||||
|     ), | ||||
|     "DEFAULT_PERMISSION_CLASSES": [ | ||||
|         "rest_framework.permissions.IsAuthenticated",  # 只有经过身份认证确定用户身份才能访问 | ||||
|     ], | ||||
|     "EXCEPTION_HANDLER": "dvadmin.utils.exception.CustomExceptionHandler",  # 自定义的异常处理 | ||||
| } | ||||
| # ================================================= # | ||||
| # ******************** 登录方式配置 ******************** # | ||||
| # ================================================= # | ||||
|  | ||||
| AUTHENTICATION_BACKENDS = ["dvadmin.utils.backends.CustomBackend"] | ||||
| # ================================================= # | ||||
| # ****************** simplejwt配置 ***************** # | ||||
| # ================================================= # | ||||
| SIMPLE_JWT = { | ||||
|     # token有效时长 | ||||
|     "ACCESS_TOKEN_LIFETIME": timedelta(minutes=1440), | ||||
|     # token刷新后的有效时间 | ||||
|     "REFRESH_TOKEN_LIFETIME": timedelta(days=1), | ||||
|     # 设置前缀 | ||||
|     "AUTH_HEADER_TYPES": ("JWT",), | ||||
|     "ROTATE_REFRESH_TOKENS": True, | ||||
| } | ||||
|  | ||||
| # ====================================# | ||||
| # ****************swagger************# | ||||
| # ====================================# | ||||
| SWAGGER_SETTINGS = { | ||||
|     # 基础样式 | ||||
|     "SECURITY_DEFINITIONS": {"basic": {"type": "basic"}}, | ||||
|     # 如果需要登录才能够查看接口文档, 登录的链接使用restframework自带的. | ||||
|     "LOGIN_URL": "apiLogin/", | ||||
|     # 'LOGIN_URL': 'rest_framework:login', | ||||
|     "LOGOUT_URL": "rest_framework:logout", | ||||
|     # 'DOC_EXPANSION': None, | ||||
|     # 'SHOW_REQUEST_HEADERS':True, | ||||
|     # 'USE_SESSION_AUTH': True, | ||||
|     # 'DOC_EXPANSION': 'list', | ||||
|     # 接口文档中方法列表以首字母升序排列 | ||||
|     "APIS_SORTER": "alpha", | ||||
|     # 如果支持json提交, 则接口文档中包含json输入框 | ||||
|     "JSON_EDITOR": True, | ||||
|     # 方法列表字母排序 | ||||
|     "OPERATIONS_SORTER": "alpha", | ||||
|     "VALIDATOR_URL": None, | ||||
|     "AUTO_SCHEMA_TYPE": 2,  # 分组根据url层级分,0、1 或 2 层 | ||||
|     "DEFAULT_AUTO_SCHEMA_CLASS": "dvadmin.utils.swagger.CustomSwaggerAutoSchema", | ||||
| } | ||||
|  | ||||
| # ================================================= # | ||||
| # **************** 验证码配置  ******************* # | ||||
| # ================================================= # | ||||
| 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 = 36  # 字体大小 | ||||
| CAPTCHA_FOREGROUND_COLOR = "#64DAAA"  # 前景色 | ||||
| CAPTCHA_BACKGROUND_COLOR = "#F5F7F4"  # 背景色 | ||||
| CAPTCHA_NOISE_FUNCTIONS = ( | ||||
|     "captcha.helpers.noise_arcs",  # 线 | ||||
|     # "captcha.helpers.noise_dots",  # 点 | ||||
| ) | ||||
| # CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' #字母验证码 | ||||
| CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.math_challenge"  # 加减乘除验证码 | ||||
|  | ||||
| # ================================================= # | ||||
| # ******************** 其他配置 ******************** # | ||||
| # ================================================= # | ||||
|  | ||||
| DEFAULT_AUTO_FIELD = "django.db.models.AutoField" | ||||
| API_LOG_ENABLE = True | ||||
| # API_LOG_METHODS = 'ALL' # ['POST', 'DELETE'] | ||||
| API_LOG_METHODS = ["POST", "UPDATE", "DELETE", "PUT"]  # ['POST', 'DELETE'] | ||||
| API_MODEL_MAP = { | ||||
|     "/token/": "登录模块", | ||||
|     "/api/login/": "登录模块", | ||||
|     "/api/plugins_market/plugins/": "插件市场", | ||||
| } | ||||
|  | ||||
| DJANGO_CELERY_BEAT_TZ_AWARE = False | ||||
| CELERY_TIMEZONE = "Asia/Shanghai"  # celery 时区问题 | ||||
| # 静态页面压缩 | ||||
| STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage" | ||||
|  | ||||
| ALL_MODELS_OBJECTS = []  # 所有app models 对象 | ||||
|  | ||||
| # 初始化需要执行的列表,用来初始化后执行 | ||||
| INITIALIZE_LIST = [] | ||||
| INITIALIZE_RESET_LIST = [] | ||||
| # 表前缀 | ||||
| TABLE_PREFIX = locals().get('TABLE_PREFIX', "") | ||||
| # 系统配置 | ||||
| SYSTEM_CONFIG = {} | ||||
| # 字典配置 | ||||
| DICTIONARY_CONFIG = {} | ||||
|  | ||||
| # ================================================= # | ||||
| # ******************** 插件配置 ******************** # | ||||
| # ================================================= # | ||||
| # 租户共享app | ||||
| TENANT_SHARED_APPS = [] | ||||
| # 插件 urlpatterns | ||||
| PLUGINS_URL_PATTERNS = [] | ||||
| # ********** 一键导入插件配置开始 ********** | ||||
| # 例如: | ||||
| # from dvadmin_upgrade_center.settings import *    # 升级中心 | ||||
| # from dvadmin3_celery.settings import *            # celery 异步任务 | ||||
| # 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 * | ||||
| # ... | ||||
| # ********** 一键导入插件配置结束 ********** | ||||
							
								
								
									
										130
									
								
								backend/application/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								backend/application/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,130 @@ | ||||
| """backend URL Configuration | ||||
|  | ||||
| The `urlpatterns` list routes URLs to views. For more information please see: | ||||
|     https://docs.djangoproject.com/en/3.2/topics/http/urls/ | ||||
| Examples: | ||||
| Function views | ||||
|     1. Add an import:  from my_app import views | ||||
|     2. Add a URL to urlpatterns:  path('', views.home, name='home') | ||||
| Class-based views | ||||
|     1. Add an import:  from other_app.views import Home | ||||
|     2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home') | ||||
| Including another URLconf | ||||
|     1. Import the include() function: from django.urls import include, path | ||||
|     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls')) | ||||
| """ | ||||
| from django.conf.urls.static import static | ||||
| from django.urls import path, include, re_path | ||||
| from drf_yasg import openapi | ||||
| from drf_yasg.views import get_schema_view | ||||
| from rest_framework import permissions | ||||
| from rest_framework_simplejwt.views import ( | ||||
|     TokenRefreshView, | ||||
| ) | ||||
|  | ||||
| from application import dispatch | ||||
| from application import settings | ||||
| from dvadmin.system.views.dictionary import InitDictionaryViewSet | ||||
| from dvadmin.system.views.login import ( | ||||
|     LoginView, | ||||
|     CaptchaView, | ||||
|     ApiLogin, | ||||
|     LogoutView, | ||||
|     LoginTokenView | ||||
| ) | ||||
| from dvadmin.system.views.system_config import InitSettingsViewSet | ||||
| from dvadmin.utils.swagger import CustomOpenAPISchemaGenerator | ||||
|  | ||||
| # =========== 初始化系统配置 ================= | ||||
| dispatch.init_system_config() | ||||
| dispatch.init_dictionary() | ||||
| # =========== 初始化系统配置 ================= | ||||
|  | ||||
| schema_view = get_schema_view( | ||||
|     openapi.Info( | ||||
|         title="Snippets API", | ||||
|         default_version="v1", | ||||
|         description="Test description", | ||||
|         terms_of_service="https://www.google.com/policies/terms/", | ||||
|         contact=openapi.Contact(email="contact@snippets.local"), | ||||
|         license=openapi.License(name="BSD License"), | ||||
|     ), | ||||
|     public=True, | ||||
|     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 = ( | ||||
|         [ | ||||
|             re_path( | ||||
|                 r"^swagger(?P<format>\.json|\.yaml)$", | ||||
|                 schema_view.without_ui(cache_timeout=0), | ||||
|                 name="schema-json", | ||||
|             ), | ||||
|             path( | ||||
|                 "", | ||||
|                 schema_view.with_ui("swagger", cache_timeout=0), | ||||
|                 name="schema-swagger-ui", | ||||
|             ), | ||||
|             path( | ||||
|                 r"redoc/", | ||||
|                 schema_view.with_ui("redoc", cache_timeout=0), | ||||
|                 name="schema-redoc", | ||||
|             ), | ||||
|             path("api/system/", include("dvadmin.system.urls")), | ||||
|             path("api/login/", LoginView.as_view(), name="token_obtain_pair"), | ||||
|             path("api/logout/", LogoutView.as_view(), name="token_obtain_pair"), | ||||
|             path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), | ||||
|             re_path( | ||||
|                 r"^api-auth/", include("rest_framework.urls", namespace="rest_framework") | ||||
|             ), | ||||
|             path("api/captcha/", CaptchaView.as_view()), | ||||
|             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) | ||||
|         + [re_path(ele.get('re_path'), include(ele.get('include'))) for ele in settings.PLUGINS_URL_PATTERNS] | ||||
| ) | ||||
| #就是添加如下内容,把自己的路由单独写出来,这样方便与dvadmin3的官方路由作区分 | ||||
| My_Urls = ( | ||||
| 	[	#这里的crud_demo是指django创建的应用名称crud_demo | ||||
|         path('',include('translate.urls')),] | ||||
| ) | ||||
|  | ||||
| # 这里把自己的路径单独出来,后面再追加在一起 | ||||
| urlpatterns += My_Urls | ||||
							
								
								
									
										183
									
								
								backend/application/websocketConfig.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								backend/application/websocketConfig.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,183 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| import urllib | ||||
|  | ||||
| from asgiref.sync import sync_to_async, async_to_sync | ||||
| from channels.db import database_sync_to_async | ||||
| from channels.generic.websocket import AsyncJsonWebsocketConsumer, AsyncWebsocketConsumer | ||||
| import json | ||||
|  | ||||
| from channels.layers import get_channel_layer | ||||
| from jwt import InvalidSignatureError | ||||
| from rest_framework.request import Request | ||||
|  | ||||
| from application import settings | ||||
| from dvadmin.system.models import MessageCenter, Users, MessageCenterTargetUser | ||||
| from dvadmin.system.views.message_center import MessageCenterTargetUserSerializer | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
|  | ||||
| send_dict = {} | ||||
|  | ||||
|  | ||||
| # 发送消息结构体 | ||||
| def set_message(sender, msg_type, msg, unread=0): | ||||
|     text = { | ||||
|         'sender': sender, | ||||
|         'contentType': msg_type, | ||||
|         'content': msg, | ||||
|         'unread': unread | ||||
|     } | ||||
|     return text | ||||
|  | ||||
|  | ||||
| # 异步获取消息中心的目标用户 | ||||
| @database_sync_to_async | ||||
| def _get_message_center_instance(message_id): | ||||
|     from dvadmin.system.models import MessageCenter | ||||
|     _MessageCenter = MessageCenter.objects.filter(id=message_id).values_list('target_user', flat=True) | ||||
|     if _MessageCenter: | ||||
|         return _MessageCenter | ||||
|     else: | ||||
|         return [] | ||||
|  | ||||
|  | ||||
| @database_sync_to_async | ||||
| def _get_message_unread(user_id): | ||||
|     """获取用户的未读消息数量""" | ||||
|     from dvadmin.system.models import MessageCenterTargetUser | ||||
|     count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count() | ||||
|     return count or 0 | ||||
|  | ||||
|  | ||||
| def request_data(scope): | ||||
|     query_string = scope.get('query_string', b'').decode('utf-8') | ||||
|     qs = urllib.parse.parse_qs(query_string) | ||||
|     return qs | ||||
|  | ||||
|  | ||||
| class DvadminWebSocket(AsyncJsonWebsocketConsumer): | ||||
|     async def connect(self): | ||||
|         try: | ||||
|             import jwt | ||||
|             self.service_uid = self.scope["url_route"]["kwargs"]["service_uid"] | ||||
|             decoded_result = jwt.decode(self.service_uid, settings.SECRET_KEY, algorithms=["HS256"]) | ||||
|             if decoded_result: | ||||
|                 self.user_id = decoded_result.get('user_id') | ||||
|                 self.chat_group_name = "user_" + str(self.user_id) | ||||
|                 # 收到连接时候处理, | ||||
|                 await self.channel_layer.group_add( | ||||
|                     self.chat_group_name, | ||||
|                     self.channel_name | ||||
|                 ) | ||||
|                 await self.accept() | ||||
|                 # 主动推送消息 | ||||
|                 unread_count = await _get_message_unread(self.user_id) | ||||
|                 if unread_count == 0: | ||||
|                     # 发送连接成功 | ||||
|                     await self.send_json(set_message('system', 'SYSTEM', '您已上线')) | ||||
|                 else: | ||||
|                     await self.send_json( | ||||
|                         set_message('system', 'SYSTEM', "请查看您的未读消息~", | ||||
|                                     unread=unread_count)) | ||||
|         except InvalidSignatureError: | ||||
|             await self.disconnect(None) | ||||
|  | ||||
|     async def disconnect(self, close_code): | ||||
|         # Leave room group | ||||
|         await self.channel_layer.group_discard(self.chat_group_name, self.channel_name) | ||||
|         print("连接关闭") | ||||
|         try: | ||||
|             await self.close(close_code) | ||||
|         except Exception: | ||||
|             pass | ||||
|  | ||||
|  | ||||
| class MegCenter(DvadminWebSocket): | ||||
|     """ | ||||
|     消息中心 | ||||
|     """ | ||||
|  | ||||
|     async def receive(self, text_data): | ||||
|         # 接受客户端的信息,你处理的函数 | ||||
|         text_data_json = json.loads(text_data) | ||||
|         message_id = text_data_json.get('message_id', None) | ||||
|         user_list = await _get_message_center_instance(message_id) | ||||
|         for send_user in user_list: | ||||
|             await self.channel_layer.group_send( | ||||
|                 "user_" + str(send_user), | ||||
|                 {'type': 'push.message', 'json': text_data_json} | ||||
|             ) | ||||
|  | ||||
|     async def push_message(self, event): | ||||
|         """消息发送""" | ||||
|         message = event['json'] | ||||
|         await self.send(text_data=json.dumps(message)) | ||||
|  | ||||
|  | ||||
| class MessageCreateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     消息中心-新增-序列化器 | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = MessageCenter | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| def websocket_push(user_id, message): | ||||
|     username = "user_" + str(user_id) | ||||
|     channel_layer = get_channel_layer() | ||||
|     async_to_sync(channel_layer.group_send)( | ||||
|         username, | ||||
|         { | ||||
|             "type": "push.message", | ||||
|             "json": message | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|  | ||||
| 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: | ||||
|         target_role = [] | ||||
|     if target_dept is None: | ||||
|         target_dept = [] | ||||
|     data = { | ||||
|         "title": title, | ||||
|         "content": content, | ||||
|         "target_type": target_type, | ||||
|         "target_user": target_user, | ||||
|         "target_dept": target_dept, | ||||
|         "target_role": target_role | ||||
|     } | ||||
|     message_center_instance = MessageCreateSerializer(data=data, request=request) | ||||
|     message_center_instance.is_valid(raise_exception=True) | ||||
|     message_center_instance.save() | ||||
|     users = target_user or [] | ||||
|     if target_type in [1]:  # 按角色 | ||||
|         users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True) | ||||
|     if target_type in [2]:  # 按部门 | ||||
|         users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True) | ||||
|     if target_type in [3]:  # 系统通知 | ||||
|         users = Users.objects.values_list('id', flat=True) | ||||
|     targetuser_data = [] | ||||
|     for user in users: | ||||
|         targetuser_data.append({ | ||||
|             "messagecenter": message_center_instance.instance.id, | ||||
|             "users": user | ||||
|         }) | ||||
|     targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=request) | ||||
|     targetuser_instance.is_valid(raise_exception=True) | ||||
|     targetuser_instance.save() | ||||
|     for user in users: | ||||
|         username = "user_" + str(user) | ||||
|         unread_count = async_to_sync(_get_message_unread)(user) | ||||
|         channel_layer = get_channel_layer() | ||||
|         async_to_sync(channel_layer.group_send)( | ||||
|             username, | ||||
|             { | ||||
|                 "type": "push.message", | ||||
|                 "json": {**message, 'unread': unread_count} | ||||
|             } | ||||
|         ) | ||||
							
								
								
									
										17
									
								
								backend/application/wsgi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								backend/application/wsgi.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| """ | ||||
| WSGI config for backend project. | ||||
|  | ||||
| It exposes the WSGI callable as a module-level variable named ``application``. | ||||
|  | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ | ||||
| """ | ||||
|  | ||||
| import os | ||||
|  | ||||
| from django.core.wsgi import get_wsgi_application | ||||
|  | ||||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') | ||||
| os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" | ||||
|  | ||||
| application = get_wsgi_application() | ||||
							
								
								
									
										50
									
								
								backend/conf/env.example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								backend/conf/env.example.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| import os | ||||
|  | ||||
| from application.settings import BASE_DIR | ||||
|  | ||||
| # ================================================= # | ||||
| # *************** mysql数据库 配置  *************** # | ||||
| # ================================================= # | ||||
| # 数据库 ENGINE ,默认演示使用 sqlite3 数据库,正式环境建议使用 mysql 数据库 | ||||
| # 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 = 'fyapi' # mysql 时使用 | ||||
|  | ||||
| # 数据库地址 改为自己数据库地址 | ||||
| DATABASE_HOST = '127.0.0.1' | ||||
| # # 数据库端口 | ||||
| DATABASE_PORT = 3306 | ||||
| # # 数据库用户名 | ||||
| DATABASE_USER = "root" | ||||
| # # 数据库密码 | ||||
| DATABASE_PASSWORD = '123456' | ||||
|  | ||||
| # 表前缀 | ||||
| TABLE_PREFIX = "dvadmin_" | ||||
| # ================================================= # | ||||
| # ******** redis配置,无redis 可不进行配置  ******** # | ||||
| # ================================================= # | ||||
| REDIS_DB = 1 | ||||
| CELERY_BROKER_DB = 3 | ||||
| REDIS_PASSWORD = '' | ||||
| REDIS_HOST = '127.0.0.1' | ||||
| REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6379' | ||||
| # ================================================= # | ||||
| # ****************** 功能 启停  ******************* # | ||||
| # ================================================= # | ||||
| DEBUG = True | ||||
| # 启动登录详细概略获取(通过调用api获取ip详细地址。如果是内网,关闭即可) | ||||
| ENABLE_LOGIN_ANALYSIS_LOG = True | ||||
| # 登录接口 /api/token/ 是否需要验证码认证,用于测试,正式环境建议取消 | ||||
| LOGIN_NO_CAPTCHA_AUTH = True | ||||
| # ================================================= # | ||||
| # ****************** 其他 配置  ******************* # | ||||
| # ================================================= # | ||||
|  | ||||
| ALLOWED_HOSTS = ["*"] | ||||
| # 列权限中排除App应用 | ||||
| COLUMN_EXCLUDE_APPS = [] | ||||
							
								
								
									
										15
									
								
								backend/del_migrations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/del_migrations.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| import os | ||||
|  | ||||
| exclude = ["venv"] # 需要排除的文件目录 | ||||
| for root, dirs, files in os.walk('.'): | ||||
|     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)): | ||||
|             for file_k in files_j: | ||||
|                 if file_k != '__init__.py': | ||||
|                     dst_file = os.path.join(root_j, file_k) | ||||
|                     print('删除文件>>> ', dst_file) | ||||
|                     os.remove(dst_file) | ||||
							
								
								
									
										5
									
								
								backend/docker_start.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								backend/docker_start.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| #!/bin/bash | ||||
| # python manage.py makemigrations | ||||
| # python manage.py migrate | ||||
| # python manage.py init -y | ||||
| uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 4 | ||||
							
								
								
									
										1
									
								
								backend/dvadmin/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								backend/dvadmin/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
							
								
								
									
										0
									
								
								backend/dvadmin/system/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/dvadmin/system/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										3
									
								
								backend/dvadmin/system/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								backend/dvadmin/system/admin.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| from django.contrib import admin | ||||
|  | ||||
| # Register your models here. | ||||
							
								
								
									
										6
									
								
								backend/dvadmin/system/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								backend/dvadmin/system/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class SystemConfig(AppConfig): | ||||
|     default_auto_field = 'django.db.models.BigAutoField' | ||||
|     name = 'dvadmin.system' | ||||
							
								
								
									
										0
									
								
								backend/dvadmin/system/fixtures/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/dvadmin/system/fixtures/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										387
									
								
								backend/dvadmin/system/fixtures/initSerializer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										387
									
								
								backend/dvadmin/system/fixtures/initSerializer.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,387 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| import os | ||||
|  | ||||
| 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, MenuField | ||||
| ) | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
|  | ||||
|  | ||||
| class UsersInitSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化获取数信息(用于生成初始化json文件) | ||||
|     """ | ||||
|  | ||||
|     def save(self, **kwargs): | ||||
|         instance = super().save(**kwargs) | ||||
|         role_key = self.initial_data.get('role_key', []) | ||||
|         role_ids = Role.objects.filter(key__in=role_key).values_list('id', flat=True) | ||||
|         instance.role.set(role_ids) | ||||
|         dept_key = self.initial_data.get('dept_key', None) | ||||
|         dept_id = Dept.objects.filter(key=dept_key).first() | ||||
|         instance.dept = dept_id | ||||
|         instance.save() | ||||
|         return instance | ||||
|  | ||||
|     class Meta: | ||||
|         model = Users | ||||
|         fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type', | ||||
|                   'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id', | ||||
|                   'password', 'last_login', 'is_superuser'] | ||||
|         read_only_fields = ['id'] | ||||
|         extra_kwargs = { | ||||
|             'creator': {'write_only': True}, | ||||
|             'dept_belong_id': {'write_only': True} | ||||
|         } | ||||
|  | ||||
|  | ||||
| class MenuButtonInitSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化菜单按钮-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = MenuButton | ||||
|         fields = ['id', 'name', 'value', 'api', 'method', 'menu'] | ||||
|         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): | ||||
|     """ | ||||
|     递归深度获取数信息(用于生成初始化json文件) | ||||
|     """ | ||||
|     name = serializers.CharField(required=False) | ||||
|     children = serializers.SerializerMethodField() | ||||
|     menu_button = serializers.SerializerMethodField() | ||||
|     menu_field = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_children(self, obj: Menu): | ||||
|         data = [] | ||||
|         instance = Menu.objects.filter(parent_id=obj.id) | ||||
|         if instance: | ||||
|             serializer = MenuInitSerializer(instance=instance, many=True) | ||||
|             data = serializer.data | ||||
|         return data | ||||
|  | ||||
|     def get_menu_button(self, obj: Menu): | ||||
|         data = [] | ||||
|         instance = obj.menuPermission.order_by('method') | ||||
|         if instance: | ||||
|             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: | ||||
|                 menu_data['parent'] = instance.id | ||||
|                 filter_data = { | ||||
|                     "name": menu_data['name'], | ||||
|                     "web_path": menu_data['web_path'], | ||||
|                     "component": menu_data['component'], | ||||
|                     "component_name": menu_data['component_name'], | ||||
|                 } | ||||
|                 instance_obj = Menu.objects.filter(**filter_data).first() | ||||
|                 if instance_obj and not self.initial_data.get('reset'): | ||||
|                     continue | ||||
|                 serializer = MenuInitSerializer(instance_obj, data=menu_data, request=self.request) | ||||
|                 serializer.is_valid(raise_exception=True) | ||||
|                 serializer.save() | ||||
|         # 菜单按钮 | ||||
|         if menu_button: | ||||
|             for menu_button_data in menu_button: | ||||
|                 menu_button_data['menu'] = instance.id | ||||
|                 filter_data = { | ||||
|                     "menu": menu_button_data['menu'], | ||||
|                     "value": menu_button_data['value'] | ||||
|                 } | ||||
|                 instance_obj = MenuButton.objects.filter(**filter_data).first() | ||||
|                 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', 'menu_field', 'creator', 'dept_belong_id'] | ||||
|         extra_kwargs = { | ||||
|             'creator': {'write_only': True}, | ||||
|             'dept_belong_id': {'write_only': True} | ||||
|         } | ||||
|         read_only_fields = ['id', 'children'] | ||||
|  | ||||
|  | ||||
| class RoleInitSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化获取数信息(用于生成初始化json文件) | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = Role | ||||
|         fields = ['name', 'key', 'sort', 'status', | ||||
|                   'creator', 'dept_belong_id'] | ||||
|         read_only_fields = ["id"] | ||||
|         extra_kwargs = { | ||||
|             'creator': {'write_only': True}, | ||||
|             'dept_belong_id': {'write_only': True} | ||||
|         } | ||||
|  | ||||
|  | ||||
| class RoleMenuInitSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化角色菜单(用于生成初始化json文件) | ||||
|     """ | ||||
|     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__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__web_path', 'menu__component_name', 'creator', 'dept_belong_id'] | ||||
|         read_only_fields = ["id"] | ||||
|         extra_kwargs = { | ||||
|             'role': {'required': False}, | ||||
|             'menu': {'required': False}, | ||||
|             'creator': {'write_only': True}, | ||||
|             'dept_belong_id': {'write_only': True} | ||||
|         } | ||||
|  | ||||
|  | ||||
| class RoleMenuButtonInitSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化角色菜单按钮(用于生成初始化json文件) | ||||
|     """ | ||||
|     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['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'] | ||||
|         read_only_fields = ["id"] | ||||
|         extra_kwargs = { | ||||
|             'role': {'required': False}, | ||||
|             'menu': {'required': False}, | ||||
|             'creator': {'write_only': True}, | ||||
|             'dept_belong_id': {'write_only': True} | ||||
|         } | ||||
|  | ||||
|  | ||||
| class ApiWhiteListInitSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化获取数信息(用于生成初始化json文件) | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = ApiWhiteList | ||||
|         fields = ['url', 'method', 'enable_datasource', 'creator', 'dept_belong_id'] | ||||
|         read_only_fields = ["id"] | ||||
|         extra_kwargs = { | ||||
|             'creator': {'write_only': True}, | ||||
|             'dept_belong_id': {'write_only': True} | ||||
|         } | ||||
|  | ||||
|  | ||||
| class DeptInitSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     递归深度获取数信息(用于生成初始化json文件) | ||||
|     """ | ||||
|     children = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_children(self, obj: Dept): | ||||
|         data = [] | ||||
|         instance = Dept.objects.filter(parent_id=obj.id) | ||||
|         if instance: | ||||
|             serializer = DeptInitSerializer(instance=instance, many=True) | ||||
|             data = serializer.data | ||||
|         return data | ||||
|  | ||||
|     def save(self, **kwargs): | ||||
|         instance = super().save(**kwargs) | ||||
|         children = self.initial_data.get('children') | ||||
|         if children: | ||||
|             for menu_data in children: | ||||
|                 menu_data['parent'] = instance.id | ||||
|                 filter_data = { | ||||
|                     "name": menu_data['name'], | ||||
|                     "parent": menu_data['parent'], | ||||
|                     "key": menu_data['key'] | ||||
|                 } | ||||
|                 instance_obj = Dept.objects.filter(**filter_data).first() | ||||
|                 if instance_obj and not self.initial_data.get('reset'): | ||||
|                     continue | ||||
|                 serializer = DeptInitSerializer(instance_obj, data=menu_data, request=self.request) | ||||
|                 serializer.is_valid(raise_exception=True) | ||||
|                 serializer.save() | ||||
|  | ||||
|         return instance | ||||
|  | ||||
|     class Meta: | ||||
|         model = Dept | ||||
|         fields = ['name', 'sort', 'owner', 'phone', 'email', 'status', 'parent', 'creator', 'dept_belong_id', | ||||
|                   'children', 'key'] | ||||
|         extra_kwargs = { | ||||
|             'creator': {'write_only': True}, | ||||
|             'dept_belong_id': {'write_only': True} | ||||
|         } | ||||
|         read_only_fields = ['id', 'children'] | ||||
|  | ||||
|  | ||||
| class DictionaryInitSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化获取数信息(用于生成初始化json文件) | ||||
|     """ | ||||
|     children = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_children(self, obj: Dictionary): | ||||
|         data = [] | ||||
|         instance = Dictionary.objects.filter(parent_id=obj.id) | ||||
|         if instance: | ||||
|             serializer = DictionaryInitSerializer(instance=instance, many=True) | ||||
|             data = serializer.data | ||||
|         return data | ||||
|  | ||||
|     def save(self, **kwargs): | ||||
|         instance = super().save(**kwargs) | ||||
|         children = self.initial_data.get('children') | ||||
|         # 菜单表 | ||||
|         if children: | ||||
|             for data in children: | ||||
|                 data['parent'] = instance.id | ||||
|                 filter_data = { | ||||
|                     "value": data['value'], | ||||
|                     "parent": data['parent'] | ||||
|                 } | ||||
|                 instance_obj = Dictionary.objects.filter(**filter_data).first() | ||||
|                 if instance_obj and not self.initial_data.get('reset'): | ||||
|                     continue | ||||
|                 serializer = DictionaryInitSerializer(instance_obj, data=data, request=self.request) | ||||
|                 serializer.is_valid(raise_exception=True) | ||||
|                 serializer.save() | ||||
|         return instance | ||||
|  | ||||
|     class Meta: | ||||
|         model = Dictionary | ||||
|         fields = ['label', 'value', 'parent', 'type', 'color', 'is_value', 'status', 'sort', 'remark', 'creator', | ||||
|                   'dept_belong_id', 'children'] | ||||
|         read_only_fields = ["id"] | ||||
|         extra_kwargs = { | ||||
|             'creator': {'write_only': True}, | ||||
|             'dept_belong_id': {'write_only': True} | ||||
|         } | ||||
|  | ||||
|  | ||||
| class SystemConfigInitSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化获取数信息(用于生成初始化json文件) | ||||
|     """ | ||||
|     children = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_children(self, obj: SystemConfig): | ||||
|         data = [] | ||||
|         instance = SystemConfig.objects.filter(parent_id=obj.id) | ||||
|         if instance: | ||||
|             serializer = SystemConfigInitSerializer(instance=instance, many=True) | ||||
|             data = serializer.data | ||||
|         return data | ||||
|  | ||||
|     def save(self, **kwargs): | ||||
|         instance = super().save(**kwargs) | ||||
|         children = self.initial_data.get('children') | ||||
|         # 菜单表 | ||||
|         if children: | ||||
|             for data in children: | ||||
|                 data['parent'] = instance.id | ||||
|                 filter_data = { | ||||
|                     "key": data['key'], | ||||
|                     "parent": data['parent'] | ||||
|                 } | ||||
|                 instance_obj = SystemConfig.objects.filter(**filter_data).first() | ||||
|                 if instance_obj and not self.initial_data.get('reset'): | ||||
|                     continue | ||||
|                 serializer = SystemConfigInitSerializer(instance_obj, data=data, request=self.request) | ||||
|                 serializer.is_valid(raise_exception=True) | ||||
|                 serializer.save() | ||||
|         return instance | ||||
|  | ||||
|     class Meta: | ||||
|         model = SystemConfig | ||||
|         fields = ['parent', 'title', 'key', 'value', 'sort', 'status', 'data_options', 'form_item_type', 'rule', | ||||
|                   'placeholder', 'setting', 'creator', 'dept_belong_id', 'children'] | ||||
|         read_only_fields = ["id"] | ||||
|         extra_kwargs = { | ||||
|             'creator': {'write_only': True}, | ||||
|             'dept_belong_id': {'write_only': True} | ||||
|         } | ||||
							
								
								
									
										7
									
								
								backend/dvadmin/system/fixtures/init_apiwhitelist.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								backend/dvadmin/system/fixtures/init_apiwhitelist.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| [ | ||||
|     { | ||||
|         "url": "/api/system/dept_lazy_tree/", | ||||
|         "method": 0, | ||||
|         "enable_datasource": true | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										36
									
								
								backend/dvadmin/system/fixtures/init_dept.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								backend/dvadmin/system/fixtures/init_dept.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| [ | ||||
|     { | ||||
|         "name": "DVAdmin团队", | ||||
|         "key": "dvadmin", | ||||
|         "sort": 1, | ||||
|         "owner": "", | ||||
|         "phone": "", | ||||
|         "email": "", | ||||
|         "status": true, | ||||
|         "parent": null, | ||||
|         "children": [ | ||||
|             { | ||||
|                 "name": "运营部", | ||||
|                 "key": "", | ||||
|                 "sort": 2, | ||||
|                 "owner": "", | ||||
|                 "phone": "", | ||||
|                 "email": "", | ||||
|                 "status": true, | ||||
|                 "parent": 1, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "name": "技术部", | ||||
|                 "key": "technology", | ||||
|                 "sort": 1, | ||||
|                 "owner": "", | ||||
|                 "phone": "", | ||||
|                 "email": "", | ||||
|                 "status": true, | ||||
|                 "parent": 3, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										550
									
								
								backend/dvadmin/system/fixtures/init_dictionary.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										550
									
								
								backend/dvadmin/system/fixtures/init_dictionary.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,550 @@ | ||||
| [ | ||||
|     { | ||||
|         "label": "启用/禁用-布尔值", | ||||
|         "value": "button_status_bool", | ||||
|         "parent": null, | ||||
|         "type": 0, | ||||
|         "color": null, | ||||
|         "is_value": false, | ||||
|         "status": true, | ||||
|         "sort": 1, | ||||
|         "remark": null, | ||||
|         "children": [ | ||||
|             { | ||||
|                 "label": "启用", | ||||
|                 "value": "true", | ||||
|                 "parent": 1, | ||||
|                 "type": 6, | ||||
|                 "color": "success", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 1, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "禁用", | ||||
|                 "value": "false", | ||||
|                 "parent": 1, | ||||
|                 "type": 6, | ||||
|                 "color": "danger", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 2, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "label": "系统按钮", | ||||
|         "value": "system_button", | ||||
|         "parent": null, | ||||
|         "type": 0, | ||||
|         "color": null, | ||||
|         "is_value": false, | ||||
|         "status": true, | ||||
|         "sort": 2, | ||||
|         "remark": null, | ||||
|         "children": [ | ||||
|             { | ||||
|                 "label": "新增", | ||||
|                 "value": "Create", | ||||
|                 "parent": 4, | ||||
|                 "type": 0, | ||||
|                 "color": "success", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 1, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "编辑", | ||||
|                 "value": "Update", | ||||
|                 "parent": 4, | ||||
|                 "type": 0, | ||||
|                 "color": "primary", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 2, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "删除", | ||||
|                 "value": "Delete", | ||||
|                 "parent": 4, | ||||
|                 "type": 0, | ||||
|                 "color": "danger", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 3, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "详情", | ||||
|                 "value": "Retrieve", | ||||
|                 "parent": 4, | ||||
|                 "type": 0, | ||||
|                 "color": "info", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 4, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "查询", | ||||
|                 "value": "Search", | ||||
|                 "parent": 4, | ||||
|                 "type": 0, | ||||
|                 "color": "warning", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 5, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "保存", | ||||
|                 "value": "Save", | ||||
|                 "parent": 4, | ||||
|                 "type": 0, | ||||
|                 "color": "success", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 6, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "导入", | ||||
|                 "value": "Import", | ||||
|                 "parent": 4, | ||||
|                 "type": 0, | ||||
|                 "color": "primary", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 7, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "导出", | ||||
|                 "value": "Export", | ||||
|                 "parent": 4, | ||||
|                 "type": 0, | ||||
|                 "color": "warning", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 8, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "label": "启用/禁用-数字值", | ||||
|         "value": "button_status_number", | ||||
|         "parent": null, | ||||
|         "type": 0, | ||||
|         "color": null, | ||||
|         "is_value": false, | ||||
|         "status": true, | ||||
|         "sort": 3, | ||||
|         "remark": null, | ||||
|         "children": [ | ||||
|             { | ||||
|                 "label": "启用", | ||||
|                 "value": "1", | ||||
|                 "parent": 13, | ||||
|                 "type": 1, | ||||
|                 "color": "success", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 1, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "禁用", | ||||
|                 "value": "0", | ||||
|                 "parent": 13, | ||||
|                 "type": 1, | ||||
|                 "color": "danger", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 2, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "label": "是/否-布尔值", | ||||
|         "value": "button_whether_bool", | ||||
|         "parent": null, | ||||
|         "type": 0, | ||||
|         "color": null, | ||||
|         "is_value": false, | ||||
|         "status": true, | ||||
|         "sort": 4, | ||||
|         "remark": null, | ||||
|         "children": [ | ||||
|             { | ||||
|                 "label": "是", | ||||
|                 "value": "true", | ||||
|                 "parent": 16, | ||||
|                 "type": 6, | ||||
|                 "color": "success", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 1, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "否", | ||||
|                 "value": "false", | ||||
|                 "parent": 16, | ||||
|                 "type": 6, | ||||
|                 "color": "danger", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 2, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "label": "是/否-数字值", | ||||
|         "value": "button_whether_number", | ||||
|         "parent": null, | ||||
|         "type": 0, | ||||
|         "color": null, | ||||
|         "is_value": false, | ||||
|         "status": true, | ||||
|         "sort": 5, | ||||
|         "remark": null, | ||||
|         "children": [ | ||||
|             { | ||||
|                 "label": "是", | ||||
|                 "value": "1", | ||||
|                 "parent": 19, | ||||
|                 "type": 1, | ||||
|                 "color": "success", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 1, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "否", | ||||
|                 "value": "2", | ||||
|                 "parent": 19, | ||||
|                 "type": 1, | ||||
|                 "color": "danger", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 2, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "label": "用户类型", | ||||
|         "value": "user_type", | ||||
|         "parent": null, | ||||
|         "type": 0, | ||||
|         "color": null, | ||||
|         "is_value": false, | ||||
|         "status": true, | ||||
|         "sort": 6, | ||||
|         "remark": null, | ||||
|         "children": [ | ||||
|             { | ||||
|                 "label": "后台用户", | ||||
|                 "value": "0", | ||||
|                 "parent": 22, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 1, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "前台用户", | ||||
|                 "value": "1", | ||||
|                 "parent": 22, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 2, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "label": "表单类型", | ||||
|         "value": "config_form_type", | ||||
|         "parent": null, | ||||
|         "type": 0, | ||||
|         "color": null, | ||||
|         "is_value": false, | ||||
|         "status": true, | ||||
|         "sort": 7, | ||||
|         "remark": null, | ||||
|         "children": [ | ||||
|             { | ||||
|                 "label": "text", | ||||
|                 "value": "0", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 0, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "textarea", | ||||
|                 "value": "3", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": "", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 0, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "number", | ||||
|                 "value": "10", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": "", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 0, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "datetime", | ||||
|                 "value": "1", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 1, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "date", | ||||
|                 "value": "2", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 2, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "time", | ||||
|                 "value": "15", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": "", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 3, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "select", | ||||
|                 "value": "4", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 4, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "checkbox", | ||||
|                 "value": "5", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 5, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "radio", | ||||
|                 "value": "6", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 6, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "switch", | ||||
|                 "value": "9", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": "", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 6, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "文件附件", | ||||
|                 "value": "8", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": "", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 7, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "图片(单张)", | ||||
|                 "value": "7", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": "", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 8, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "图片(多张)", | ||||
|                 "value": "12", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": "", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 9, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "数组", | ||||
|                 "value": "11", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": "", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 11, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "关联表", | ||||
|                 "value": "13", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": "", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 13, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "关联表(多选)", | ||||
|                 "value": "14", | ||||
|                 "parent": 25, | ||||
|                 "type": 1, | ||||
|                 "color": "", | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 14, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "label": "性别", | ||||
|         "value": "gender", | ||||
|         "parent": null, | ||||
|         "type": 0, | ||||
|         "color": null, | ||||
|         "is_value": false, | ||||
|         "status": true, | ||||
|         "sort": 8, | ||||
|         "remark": null, | ||||
|         "children": [ | ||||
|             { | ||||
|                 "label": "未知", | ||||
|                 "value": "0", | ||||
|                 "parent": 42, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 0, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "男", | ||||
|                 "value": "1", | ||||
|                 "parent": 42, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 1, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "label": "女", | ||||
|                 "value": "2", | ||||
|                 "parent": 42, | ||||
|                 "type": 1, | ||||
|                 "color": null, | ||||
|                 "is_value": true, | ||||
|                 "status": true, | ||||
|                 "sort": 2, | ||||
|                 "remark": null, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										1444
									
								
								backend/dvadmin/system/fixtures/init_menu.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1444
									
								
								backend/dvadmin/system/fixtures/init_menu.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										16
									
								
								backend/dvadmin/system/fixtures/init_role.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								backend/dvadmin/system/fixtures/init_role.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| [ | ||||
|     { | ||||
|         "name": "管理员", | ||||
|         "key": "admin", | ||||
|         "sort": 1, | ||||
|         "status": true, | ||||
|         "remark": null | ||||
|     }, | ||||
|     { | ||||
|         "name": "用户", | ||||
|         "key": "public", | ||||
|         "sort": 2, | ||||
|         "status": true, | ||||
|         "remark": null | ||||
|     } | ||||
| ] | ||||
| @ -0,0 +1,7 @@ | ||||
| [ | ||||
|     { | ||||
|         "role__key": "admin", | ||||
|         "menu_button__value": "menu:Search", | ||||
|         "data_range": 0 | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										12
									
								
								backend/dvadmin/system/fixtures/init_rolemenupermission.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/dvadmin/system/fixtures/init_rolemenupermission.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| [ | ||||
|     { | ||||
|         "role__key": "admin", | ||||
|         "menu__web_path": "/system", | ||||
|         "menu__component_name": "" | ||||
|     }, | ||||
|     { | ||||
|         "role__key": "admin", | ||||
|         "menu__web_path": "/menu", | ||||
|         "menu__component_name": "menu" | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										239
									
								
								backend/dvadmin/system/fixtures/init_systemconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								backend/dvadmin/system/fixtures/init_systemconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,239 @@ | ||||
| [ | ||||
|     { | ||||
|         "parent": null, | ||||
|         "title": "基础配置", | ||||
|         "key": "base", | ||||
|         "value": null, | ||||
|         "sort": 0, | ||||
|         "status": true, | ||||
|         "data_options": null, | ||||
|         "form_item_type": 0, | ||||
|         "rule": null, | ||||
|         "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": "开启验证码", | ||||
|                 "key": "captcha_state", | ||||
|                 "value": true, | ||||
|                 "sort": 1, | ||||
|                 "status": true, | ||||
|                 "data_options": null, | ||||
|                 "form_item_type": 9, | ||||
|                 "rule": [ | ||||
|                     { | ||||
|                         "message": "必填项不能为空", | ||||
|                         "required": true | ||||
|                     } | ||||
|                 ], | ||||
|                 "placeholder": "请选择", | ||||
|                 "setting": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "parent": 10, | ||||
|                 "title": "创建用户默认密码", | ||||
|                 "key": "default_password", | ||||
|                 "value": "admin123456", | ||||
|                 "sort": 2, | ||||
|                 "status": true, | ||||
|                 "data_options": null, | ||||
|                 "form_item_type": 0, | ||||
|                 "rule": [ | ||||
|                     { | ||||
|                         "message": "必填项不能为空", | ||||
|                         "required": true | ||||
|                     } | ||||
|                 ], | ||||
|                 "placeholder": "请输入默认密码", | ||||
|                 "setting": null, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "parent": null, | ||||
|         "title": "登录页配置", | ||||
|         "key": "login", | ||||
|         "value": null, | ||||
|         "sort": 1, | ||||
|         "status": true, | ||||
|         "data_options": null, | ||||
|         "form_item_type": 0, | ||||
|         "rule": null, | ||||
|         "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": "网站名称", | ||||
|                 "key": "site_name", | ||||
|                 "value": "企业级后台管理系统", | ||||
|                 "sort": 1, | ||||
|                 "status": true, | ||||
|                 "data_options": null, | ||||
|                 "form_item_type": 0, | ||||
|                 "rule": [ | ||||
|                     { | ||||
|                         "message": "必填项不能为空", | ||||
|                         "required": true | ||||
|                     } | ||||
|                 ], | ||||
|                 "placeholder": "请输入网站名称", | ||||
|                 "setting": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "parent": 1, | ||||
|                 "title": "登录网站logo", | ||||
|                 "key": "site_logo", | ||||
|                 "value": null, | ||||
|                 "sort": 2, | ||||
|                 "status": true, | ||||
|                 "data_options": null, | ||||
|                 "form_item_type": 7, | ||||
|                 "rule": [], | ||||
|                 "placeholder": "请上传网站logo", | ||||
|                 "setting": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "parent": 1, | ||||
|                 "title": "登录页背景图", | ||||
|                 "key": "login_background", | ||||
|                 "value": null, | ||||
|                 "sort": 3, | ||||
|                 "status": true, | ||||
|                 "data_options": null, | ||||
|                 "form_item_type": 7, | ||||
|                 "rule": [], | ||||
|                 "placeholder": "请上传登录背景页", | ||||
|                 "setting": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "parent": 1, | ||||
|                 "title": "版权信息", | ||||
|                 "key": "copyright", | ||||
|                 "value": "2021-2024 django-vue-admin.com 版权所有", | ||||
|                 "sort": 4, | ||||
|                 "status": true, | ||||
|                 "data_options": null, | ||||
|                 "form_item_type": 0, | ||||
|                 "rule": [ | ||||
|                     { | ||||
|                         "message": "必填项不能为空", | ||||
|                         "required": true | ||||
|                     } | ||||
|                 ], | ||||
|                 "placeholder": "请输入版权信息", | ||||
|                 "setting": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "parent": 1, | ||||
|                 "title": "备案信息", | ||||
|                 "key": "keep_record", | ||||
|                 "value": "晋ICP备18005113号-3", | ||||
|                 "sort": 5, | ||||
|                 "status": true, | ||||
|                 "data_options": null, | ||||
|                 "form_item_type": 0, | ||||
|                 "rule": [ | ||||
|                     { | ||||
|                         "message": "必填项不能为空", | ||||
|                         "required": true | ||||
|                     } | ||||
|                 ], | ||||
|                 "placeholder": "请输入备案信息", | ||||
|                 "setting": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "parent": 1, | ||||
|                 "title": "帮助链接", | ||||
|                 "key": "help_url", | ||||
|                 "value": "https://django-vue-admin.com", | ||||
|                 "sort": 6, | ||||
|                 "status": true, | ||||
|                 "data_options": null, | ||||
|                 "form_item_type": 0, | ||||
|                 "rule": "", | ||||
|                 "placeholder": "请输入帮助信息", | ||||
|                 "setting": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "parent": 1, | ||||
|                 "title": "隐私链接", | ||||
|                 "key": "privacy_url", | ||||
|                 "value": "/api/system/clause/privacy.html", | ||||
|                 "sort": 7, | ||||
|                 "status": true, | ||||
|                 "data_options": null, | ||||
|                 "form_item_type": 0, | ||||
|                 "rule": [], | ||||
|                 "placeholder": "请填写隐私链接", | ||||
|                 "setting": null, | ||||
|                 "children": [] | ||||
|             }, | ||||
|             { | ||||
|                 "parent": 1, | ||||
|                 "title": "条款链接", | ||||
|                 "key": "clause_url", | ||||
|                 "value": "/api/system/clause/terms_service.html", | ||||
|                 "sort": 8, | ||||
|                 "status": true, | ||||
|                 "data_options": null, | ||||
|                 "form_item_type": 0, | ||||
|                 "rule": [], | ||||
|                 "placeholder": "请输入条款链接", | ||||
|                 "setting": null, | ||||
|                 "children": [] | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										60
									
								
								backend/dvadmin/system/fixtures/init_users.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								backend/dvadmin/system/fixtures/init_users.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| [ | ||||
|   { | ||||
|     "username": "superadmin", | ||||
|     "email": "dvadmin@django-vue-admin.com", | ||||
|     "mobile": "13333333333", | ||||
|     "avatar": null, | ||||
|     "name": "超级管理员", | ||||
|     "gender": 1, | ||||
|     "user_type": 0, | ||||
|     "role": [], | ||||
|     "role_key": [ | ||||
|      "admin" | ||||
|     ], | ||||
|     "dept_key": "dvadmin", | ||||
|     "first_name": "", | ||||
|     "last_name": "", | ||||
|     "is_staff": true, | ||||
|     "is_active": true, | ||||
|     "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", | ||||
|     "last_login": null, | ||||
|     "is_superuser": true | ||||
|   }, | ||||
|   { | ||||
|     "username": "admin", | ||||
|     "email": "dvadmin@django-vue-admin.com", | ||||
|     "mobile": "18888888888", | ||||
|     "avatar": "", | ||||
|     "name": "管理员", | ||||
|     "gender": 1, | ||||
|     "user_type": 0, | ||||
|     "role": [], | ||||
|     "dept_key": "dvadmin", | ||||
|     "first_name": "", | ||||
|     "last_name": "", | ||||
|     "is_staff": true, | ||||
|     "is_active": true, | ||||
|     "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", | ||||
|     "last_login": null, | ||||
|     "is_superuser": false | ||||
|   }, | ||||
|   { | ||||
|     "username": "test", | ||||
|     "email": "dvadmin@django-vue-admin.com", | ||||
|     "mobile": "18888888888", | ||||
|     "avatar": "", | ||||
|     "name": "测试人员", | ||||
|     "gender": 1, | ||||
|     "user_type": 0, | ||||
|     "role": [], | ||||
|     "role_key": ["public"], | ||||
|     "dept_key": "technology", | ||||
|     "first_name": "", | ||||
|     "last_name": "", | ||||
|     "is_staff": true, | ||||
|     "is_active": true, | ||||
|     "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", | ||||
|     "last_login": null, | ||||
|     "is_superuser": false | ||||
|   } | ||||
| ] | ||||
							
								
								
									
										87
									
								
								backend/dvadmin/system/fixtures/initialize.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								backend/dvadmin/system/fixtures/initialize.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| # 初始化 | ||||
| import os | ||||
|  | ||||
| import django | ||||
|  | ||||
|  | ||||
| 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 | ||||
| ) | ||||
|  | ||||
|  | ||||
| class Initialize(CoreInitialize): | ||||
|  | ||||
|     def init_dept(self): | ||||
|         """ | ||||
|         初始化部门信息 | ||||
|         """ | ||||
|         self.init_base(DeptInitSerializer, unique_fields=['name', 'parent','key']) | ||||
|  | ||||
|     def init_role(self): | ||||
|         """ | ||||
|         初始化角色信息 | ||||
|         """ | ||||
|         self.init_base(RoleInitSerializer, unique_fields=['key']) | ||||
|  | ||||
|     def init_users(self): | ||||
|         """ | ||||
|         初始化用户信息 | ||||
|         """ | ||||
|         self.init_base(UsersInitSerializer, unique_fields=['username']) | ||||
|  | ||||
|     def init_menu(self): | ||||
|         """ | ||||
|         初始化菜单信息 | ||||
|         """ | ||||
|         self.init_base(MenuInitSerializer, unique_fields=['name', 'web_path', 'component', 'component_name']) | ||||
|  | ||||
|     def init_role_menu(self): | ||||
|         """ | ||||
|         初始化角色菜单信息 | ||||
|         """ | ||||
|         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__key', 'menu_button__value']) | ||||
|  | ||||
|     def init_api_white_list(self): | ||||
|         """ | ||||
|         初始API白名单 | ||||
|         """ | ||||
|         self.init_base(ApiWhiteListInitSerializer, unique_fields=['url', 'method', ]) | ||||
|  | ||||
|     def init_dictionary(self): | ||||
|         """ | ||||
|         初始化字典表 | ||||
|         """ | ||||
|         self.init_base(DictionaryInitSerializer, unique_fields=['value', 'parent', ]) | ||||
|  | ||||
|     def init_system_config(self): | ||||
|         """ | ||||
|         初始化系统配置表 | ||||
|         """ | ||||
|         self.init_base(SystemConfigInitSerializer, unique_fields=['key', 'parent', ]) | ||||
|  | ||||
|     def run(self): | ||||
|         self.init_dept() | ||||
|         self.init_role() | ||||
|         self.init_users() | ||||
|         self.init_menu() | ||||
|         self.init_role_menu() | ||||
|         self.init_role_menu_button() | ||||
|         self.init_api_white_list() | ||||
|         self.init_dictionary() | ||||
|         self.init_system_config() | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     Initialize(app='dvadmin.system').run() | ||||
							
								
								
									
										0
									
								
								backend/dvadmin/system/management/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/dvadmin/system/management/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | ||||
| import json | ||||
| import logging | ||||
| import os | ||||
|  | ||||
| import django | ||||
| from django.db.models import QuerySet | ||||
|  | ||||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') | ||||
| django.setup() | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from application.settings import BASE_DIR | ||||
| from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig | ||||
| from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \ | ||||
|     MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \ | ||||
|     RoleMenuInitSerializer, RoleMenuButtonInitSerializer | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     """ | ||||
|     生产初始化菜单: python3 manage.py generate_init_json 生成初始化的model名 | ||||
|     例如: | ||||
|     全部生成:python3 manage.py generate_init_json | ||||
|     只生成某个model的: python3 manage.py generate_init_json users | ||||
|     """ | ||||
|  | ||||
|     def serializer_data(self, serializer, query_set: QuerySet): | ||||
|         serializer = serializer(query_set, many=True) | ||||
|         data = json.loads(json.dumps(serializer.data, ensure_ascii=False)) | ||||
|         with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w') as f: | ||||
|             json.dump(data, f, indent=4, ensure_ascii=False) | ||||
|         return | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument("generate_name", nargs="*", type=str, help="初始化生成的表名") | ||||
|  | ||||
|     def generate_users(self): | ||||
|         self.serializer_data(UsersInitSerializer, Users.objects.all()) | ||||
|  | ||||
|     def generate_role(self): | ||||
|         self.serializer_data(RoleInitSerializer, Role.objects.all()) | ||||
|  | ||||
|     def generate_dept(self): | ||||
|         self.serializer_data(DeptInitSerializer, Dept.objects.filter(parent_id__isnull=True)) | ||||
|  | ||||
|     def generate_menu(self): | ||||
|         self.serializer_data(MenuInitSerializer, Menu.objects.filter(parent_id__isnull=True)) | ||||
|  | ||||
|     def generate_api_white_list(self): | ||||
|         self.serializer_data(ApiWhiteListInitSerializer, ApiWhiteList.objects.all()) | ||||
|  | ||||
|     def generate_dictionary(self): | ||||
|         self.serializer_data(DictionaryInitSerializer, Dictionary.objects.filter(parent_id__isnull=True)) | ||||
|  | ||||
|     def generate_system_config(self): | ||||
|         self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True)) | ||||
|  | ||||
|     def handle(self, *args, **options): | ||||
|         generate_name = options.get('generate_name') | ||||
|         generate_name_dict = { | ||||
|             "users": self.generate_users, | ||||
|             "role": self.generate_role, | ||||
|             "dept": self.generate_dept, | ||||
|             "menu": self.generate_menu, | ||||
|             "api_white_list": self.generate_api_white_list, | ||||
|             "dictionary": self.generate_dictionary, | ||||
|             "system_config": self.generate_system_config, | ||||
|         } | ||||
|         if not generate_name: | ||||
|             for ele in generate_name_dict.keys(): | ||||
|                 generate_name_dict[ele]() | ||||
|             return | ||||
|  | ||||
|         for generate_name in generate_name: | ||||
|             if generate_name not in generate_name_dict: | ||||
|                 print(f"该初始化方法尚未配置\n{generate_name_dict}") | ||||
|                 raise Exception(f"该初始化方法尚未配置,已配置项:{list(generate_name_dict.keys())}") | ||||
|             generate_name_dict[generate_name]() | ||||
|             return | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     # with open(os.path.join(BASE_DIR, 'temp_init_menu.json')) as f: | ||||
|     #     for menu_data in json.load(f): | ||||
|     #         menu_data['creator'] = 1 | ||||
|     #         menu_data['modifier'] = 1 | ||||
|     #         menu_data['dept_belong_id'] = 1 | ||||
|     #         request.user = Users.objects.order_by('create_datetime').first() | ||||
|     #         serializer = MenuInitSerializer(data=menu_data, request=request) | ||||
|     #         serializer.is_valid(raise_exception=True) | ||||
|     #         serializer.save() | ||||
|     a = Users.objects.filter() | ||||
|     print(type(Users.objects.filter())) | ||||
							
								
								
									
										56
									
								
								backend/dvadmin/system/management/commands/init.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								backend/dvadmin/system/management/commands/init.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| import logging | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from application import settings | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     """ | ||||
|     项目初始化命令: python manage.py init | ||||
|     """ | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument( | ||||
|             "init_name", | ||||
|             nargs="*", | ||||
|             type=str, | ||||
|         ) | ||||
|         parser.add_argument("-y", nargs="*") | ||||
|         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 | ||||
|         if isinstance(options.get("y"), list) or isinstance(options.get("Y"), list): | ||||
|             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""" | ||||
| from {app}.fixtures.initialize import Initialize | ||||
| Initialize(reset={reset},app="{app}").run() | ||||
|                 """ | ||||
|                 ) | ||||
|             except ModuleNotFoundError: | ||||
|                 # 兼容之前版本初始化 | ||||
|                 try: | ||||
|                     exec( | ||||
|                         f""" | ||||
| from {app}.initialize import main | ||||
| main(reset={reset}) | ||||
|                 """ | ||||
|                     ) | ||||
|                 except ModuleNotFoundError: | ||||
|                     pass | ||||
|         print("初始化数据完成!") | ||||
							
								
								
									
										83
									
								
								backend/dvadmin/system/management/commands/init_area.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								backend/dvadmin/system/management/commands/init_area.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| # 城市联动 | ||||
| """ | ||||
| 到乡级 使用方法 | ||||
| 1. https://www.npmjs.com/package/china-division 下载数据,把对应的json放入对应目录 | ||||
| 2. 修改此文件中对应json名 | ||||
| 3. 右击执行此py文件进行初始化 | ||||
| """ | ||||
| import json | ||||
| import os | ||||
|  | ||||
| import django | ||||
| import pypinyin | ||||
| from django.core.management import BaseCommand | ||||
| from django.db import connection | ||||
|  | ||||
| from application import dispatch | ||||
|  | ||||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') | ||||
| django.setup() | ||||
| from application.settings import BASE_DIR | ||||
| from dvadmin.system.models import Area | ||||
|  | ||||
| area_code_list = [] | ||||
|  | ||||
|  | ||||
| def area_list(code_list, pcode=None, depth=1): | ||||
|     """ | ||||
|     递归获取所有列表 | ||||
|     """ | ||||
|     for code_dict in code_list: | ||||
|         code = code_dict.get('code', None) | ||||
|         name = code_dict.get('name', None) | ||||
|         children = code_dict.get('children', None) | ||||
|         pinyin = ''.join([''.join(i) for i in pypinyin.pinyin(name, style=pypinyin.NORMAL)]) | ||||
|         area_code_list.append( | ||||
|             { | ||||
|                 "name": name, | ||||
|                 "code": code, | ||||
|                 "level": depth, | ||||
|                 "pinyin": pinyin, | ||||
|                 "initials": pinyin[0].upper() if pinyin else "#", | ||||
|                 "pcode_id": pcode, | ||||
|             } | ||||
|         ) | ||||
|         if children: | ||||
|             area_list(code_list=children, pcode=code, depth=depth + 1) | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     with open(os.path.join(BASE_DIR, 'dvadmin', 'system', 'util', 'pca-code.json'), 'r', encoding="utf-8") as load_f: | ||||
|         code_list = json.load(load_f) | ||||
|     area_list(code_list) | ||||
|     if Area.objects.count() == 0: | ||||
|         Area.objects.bulk_create([Area(**ele) for ele in area_code_list]) | ||||
|     else: | ||||
|         for ele in area_code_list: | ||||
|             code = ele.pop("code") | ||||
|             Area.objects.update_or_create(code=code, defaults=ele) | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     """ | ||||
|     项目初始化命令: python manage.py init | ||||
|     """ | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         pass | ||||
|  | ||||
|     def handle(self, *args, **options): | ||||
|  | ||||
|         print(f"正在准备初始化省份数据...") | ||||
|  | ||||
|         if dispatch.is_tenants_mode(): | ||||
|             from django_tenants.utils import get_tenant_model | ||||
|             from django_tenants.utils import tenant_context | ||||
|             for tenant in get_tenant_model().objects.exclude(schema_name='public'): | ||||
|                 with tenant_context(tenant): | ||||
|                     print(f"租户[{connection.tenant.schema_name}]初始化数据开始...") | ||||
|                     main() | ||||
|                     print(f"租户[{connection.tenant.schema_name}]初始化数据完成!") | ||||
|         else: | ||||
|             main() | ||||
|         print("省份数据初始化数据完成!") | ||||
							
								
								
									
										0
									
								
								backend/dvadmin/system/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/dvadmin/system/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										720
									
								
								backend/dvadmin/system/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										720
									
								
								backend/dvadmin/system/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,720 @@ | ||||
| import hashlib | ||||
| import os | ||||
| import secrets | ||||
| from time import time | ||||
| from pathlib import PurePosixPath | ||||
|  | ||||
| from django.contrib.auth.models import AbstractUser, UserManager | ||||
| from django.core.validators import MinValueValidator | ||||
| from django.db import models, IntegrityError | ||||
| from django.core.exceptions import ObjectDoesNotExist, ValidationError | ||||
| from django.utils import timezone | ||||
|  | ||||
| from application import dispatch | ||||
| from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models | ||||
| import string | ||||
|  | ||||
| def generate_api_key(): | ||||
|     """生成16位随机字母数字组合的API密钥""" | ||||
|     alphabet = string.ascii_letters + string.digits  # 大小写字母 + 数字(共62种字符) | ||||
|     return ''.join(secrets.choice(alphabet) for _ in range(32)) | ||||
| class Role(CoreModel): | ||||
|     name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称") | ||||
|     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="角色状态") | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "system_role" | ||||
|         verbose_name = "角色表" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("sort",) | ||||
|  | ||||
|  | ||||
| class CustomUserManager(UserManager): | ||||
|  | ||||
|     def create_superuser(self, username, email=None, password=None, **extra_fields): | ||||
|         user = super(CustomUserManager, self).create_superuser(username, email, password, **extra_fields) | ||||
|         user.set_password(password) | ||||
|         try: | ||||
|             user.role.add(Role.objects.get(name="管理员")) | ||||
|             user.save(using=self._db) | ||||
|             return user | ||||
|         except ObjectDoesNotExist: | ||||
|             user.delete() | ||||
|             raise ValidationError("角色`管理员`不存在, 创建失败, 请先执行python manage.py init") | ||||
|  | ||||
|  | ||||
| class Users(CoreModel, AbstractUser): | ||||
|     username = models.CharField(max_length=150, unique=True, db_index=True, verbose_name="用户账号", | ||||
|                                 help_text="用户账号") | ||||
|     email = models.EmailField(max_length=255, verbose_name="邮箱", null=True, blank=True, help_text="邮箱") | ||||
|     mobile = models.CharField(max_length=255, verbose_name="电话", null=True, blank=True, help_text="电话") | ||||
|     avatar = models.CharField(max_length=255, verbose_name="头像", null=True, blank=True, help_text="头像") | ||||
|     name = models.CharField(max_length=40, verbose_name="姓名", help_text="姓名") | ||||
|     GENDER_CHOICES = ( | ||||
|         (0, "未知"), | ||||
|         (1, "男"), | ||||
|         (2, "女"), | ||||
|     ) | ||||
|     gender = models.IntegerField( | ||||
|         choices=GENDER_CHOICES, default=0, verbose_name="性别", null=True, blank=True, help_text="性别" | ||||
|     ) | ||||
|     USER_TYPE = ( | ||||
|         (0, "后台用户"), | ||||
|         (1, "前台用户"), | ||||
|     ) | ||||
|     user_type = models.IntegerField( | ||||
|         choices=USER_TYPE, default=0, verbose_name="用户类型", null=True, blank=True, help_text="用户类型" | ||||
|     ) | ||||
|     post = models.ManyToManyField(to="Post", blank=True, verbose_name="关联岗位", db_constraint=False, | ||||
|                                   help_text="关联岗位") | ||||
|     role = models.ManyToManyField(to="Role", blank=True, verbose_name="关联角色", db_constraint=False, | ||||
|                                   help_text="关联角色") | ||||
|     dept = models.ForeignKey( | ||||
|         to="Dept", | ||||
|         verbose_name="所属部门", | ||||
|         on_delete=models.PROTECT, | ||||
|         db_constraint=False, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         help_text="关联部门", | ||||
|     ) | ||||
|     login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数") | ||||
|     pwd_change_count = models.IntegerField(default=1,blank=True, verbose_name="密码修改次数", help_text="密码修改次数") | ||||
|     # 增加自定义 | ||||
|     # 账户余额(使用Decimal处理金额) | ||||
|     # 计费类型选项 | ||||
|     BILLING_TYPE_CHOICES = [ | ||||
|         (0, '端口/字符'), | ||||
|         (1, '端口'), | ||||
|         (2, '字符'), | ||||
|         (3, '无限制'), | ||||
|     ] | ||||
|     # 剩余翻译字符数(不可为负数) | ||||
|     user_remaining_chars = models.IntegerField( | ||||
|         default=0, | ||||
|         verbose_name="剩余字符数", | ||||
|         help_text="剩余可翻译的字符数量" | ||||
|     ) | ||||
|     user_balance = models.DecimalField( | ||||
|         max_digits=10, | ||||
|         decimal_places=2, | ||||
|         default=0.00, | ||||
|         verbose_name="账户余额", | ||||
|         help_text="账户余额(元)", | ||||
|         validators=[MinValueValidator(0)] | ||||
|     ) | ||||
|     # 密钥字段(加密存储) | ||||
|     user_api_key = models.CharField( | ||||
|         max_length=128, | ||||
|         unique=True, | ||||
|         default=generate_api_key,  # 关键点:绑定生成函数 | ||||
|         verbose_name="API密钥", | ||||
|         help_text="加密存储的API密钥" | ||||
|     ) | ||||
|     # 账户状态 | ||||
|     user_is_blocked = models.BooleanField( | ||||
|         default=False, | ||||
|         verbose_name="是否可用", | ||||
|         help_text="账户是否被可以使用翻译服务" | ||||
|     ) | ||||
|     # 计费类型 | ||||
|     user_billing_type = models.IntegerField( | ||||
|         choices=BILLING_TYPE_CHOICES, | ||||
|         default=2, | ||||
|         verbose_name="计费类型", | ||||
|         help_text="0=端口/字符 1=端口 2=字符 3=无限制" | ||||
|     ) | ||||
|     # 端口相关配置 | ||||
|     user_max_ports = models.PositiveIntegerField( | ||||
|         default=5, | ||||
|         verbose_name="授权端口数", | ||||
|         help_text="允许的最大并发端口数" | ||||
|     ) | ||||
|  | ||||
|     user_used_ports = models.PositiveIntegerField( | ||||
|         default=0, | ||||
|         verbose_name="已用端口数", | ||||
|         help_text="当前已使用的端口数量" | ||||
|     ) | ||||
|     objects = CustomUserManager() | ||||
|  | ||||
|     parent_id = models.IntegerField( | ||||
|         default=None, | ||||
|         verbose_name="子账户指向的主账户ID", | ||||
|     ) | ||||
|  | ||||
|     def set_password(self, raw_password): | ||||
|         super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest()) | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "system_users" | ||||
|         verbose_name = "用户表" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("-create_datetime",) | ||||
|  | ||||
|  | ||||
|  | ||||
| class Post(CoreModel): | ||||
|     name = models.CharField(null=False, max_length=64, verbose_name="岗位名称", help_text="岗位名称") | ||||
|     code = models.CharField(max_length=32, verbose_name="岗位编码", help_text="岗位编码") | ||||
|     sort = models.IntegerField(default=1, verbose_name="岗位顺序", help_text="岗位顺序") | ||||
|     STATUS_CHOICES = ( | ||||
|         (0, "离职"), | ||||
|         (1, "在职"), | ||||
|     ) | ||||
|     status = models.IntegerField(choices=STATUS_CHOICES, default=1, verbose_name="岗位状态", help_text="岗位状态") | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "system_post" | ||||
|         verbose_name = "岗位表" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("sort",) | ||||
|  | ||||
|  | ||||
| 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="关联字符") | ||||
|     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="联系电话") | ||||
|     email = models.EmailField(max_length=32, verbose_name="邮箱", null=True, blank=True, help_text="邮箱") | ||||
|     status = models.BooleanField(default=True, verbose_name="部门状态", null=True, blank=True, help_text="部门状态") | ||||
|     parent = models.ForeignKey( | ||||
|         to="Dept", | ||||
|         on_delete=models.CASCADE, | ||||
|         default=None, | ||||
|         verbose_name="上级部门", | ||||
|         db_constraint=False, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         help_text="上级部门", | ||||
|     ) | ||||
|  | ||||
|     @classmethod | ||||
|     def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None): | ||||
|         """ | ||||
|         递归获取部门的所有下级部门 | ||||
|         :param dept_id: 需要获取的id | ||||
|         :param dept_all_list: 所有列表 | ||||
|         :param dept_list: 递归list | ||||
|         :return: | ||||
|         """ | ||||
|         if not dept_all_list: | ||||
|             dept_all_list = Dept.objects.values("id", "parent") | ||||
|         if dept_list is None: | ||||
|             dept_list = [dept_id] | ||||
|         for ele in dept_all_list: | ||||
|             if ele.get("parent") == dept_id: | ||||
|                 dept_list.append(ele.get("id")) | ||||
|                 cls.recursion_all_dept(ele.get("id"), dept_all_list, dept_list) | ||||
|         return list(set(dept_list)) | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "system_dept" | ||||
|         verbose_name = "部门表" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("sort",) | ||||
|  | ||||
|  | ||||
| class Menu(CoreModel): | ||||
|     parent = models.ForeignKey( | ||||
|         to="Menu", | ||||
|         on_delete=models.CASCADE, | ||||
|         verbose_name="上级菜单", | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         db_constraint=False, | ||||
|         help_text="上级菜单", | ||||
|     ) | ||||
|     icon = models.CharField(max_length=64, verbose_name="菜单图标", null=True, blank=True, help_text="菜单图标") | ||||
|     name = models.CharField(max_length=64, verbose_name="菜单名称", help_text="菜单名称") | ||||
|     sort = models.IntegerField(default=1, verbose_name="显示排序", null=True, blank=True, help_text="显示排序") | ||||
|     ISLINK_CHOICES = ( | ||||
|         (0, "否"), | ||||
|         (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="组件地址") | ||||
|     component_name = models.CharField(max_length=50, verbose_name="组件名称", null=True, blank=True, | ||||
|                                       help_text="组件名称") | ||||
|     status = models.BooleanField(default=True, blank=True, verbose_name="菜单状态", help_text="菜单状态") | ||||
|     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( | ||||
|         to="Menu", | ||||
|         db_constraint=False, | ||||
|         related_name="menuPermission", | ||||
|         on_delete=models.CASCADE, | ||||
|         verbose_name="关联菜单", | ||||
|         help_text="关联菜单", | ||||
|     ) | ||||
|     name = models.CharField(max_length=64, verbose_name="名称", help_text="名称") | ||||
|     value = models.CharField(unique=True, max_length=64, verbose_name="权限值", help_text="权限值") | ||||
|     api = models.CharField(max_length=200, verbose_name="接口地址", help_text="接口地址") | ||||
|     METHOD_CHOICES = ( | ||||
|         (0, "GET"), | ||||
|         (1, "POST"), | ||||
|         (2, "PUT"), | ||||
|         (3, "DELETE"), | ||||
|     ) | ||||
|     method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True, | ||||
|                                  help_text="接口请求方法") | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "system_menu_button" | ||||
|         verbose_name = "菜单权限表" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("-name",) | ||||
|  | ||||
|  | ||||
| class RoleMenuPermission(CoreModel): | ||||
|     role = models.ForeignKey( | ||||
|         to="Role", | ||||
|         db_constraint=False, | ||||
|         related_name="role_menu", | ||||
|         on_delete=models.CASCADE, | ||||
|         verbose_name="关联角色", | ||||
|         help_text="关联角色", | ||||
|     ) | ||||
|     menu = models.ForeignKey( | ||||
|         to="Menu", | ||||
|         db_constraint=False, | ||||
|         related_name="role_menu", | ||||
|         on_delete=models.CASCADE, | ||||
|         verbose_name="关联菜单", | ||||
|         help_text="关联菜单", | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "role_menu_permission" | ||||
|         verbose_name = "角色菜单权限表" | ||||
|         verbose_name_plural = verbose_name | ||||
|         # ordering = ("-create_datetime",) | ||||
|  | ||||
|  | ||||
| class RoleMenuButtonPermission(CoreModel): | ||||
|     role = models.ForeignKey( | ||||
|         to="Role", | ||||
|         db_constraint=False, | ||||
|         related_name="role_menu_button", | ||||
|         on_delete=models.CASCADE, | ||||
|         verbose_name="关联角色", | ||||
|         help_text="关联角色", | ||||
|     ) | ||||
|     menu_button = models.ForeignKey( | ||||
|         to="MenuButton", | ||||
|         db_constraint=False, | ||||
|         related_name="menu_button_permission", | ||||
|         on_delete=models.CASCADE, | ||||
|         verbose_name="关联菜单按钮", | ||||
|         help_text="关联菜单按钮", | ||||
|         null=True, | ||||
|         blank=True | ||||
|     ) | ||||
|     DATASCOPE_CHOICES = ( | ||||
|         (0, "仅本人数据权限"), | ||||
|         (1, "本部门及以下数据权限"), | ||||
|         (2, "本部门数据权限"), | ||||
|         (3, "全部数据权限"), | ||||
|         (4, "自定数据权限"), | ||||
|     ) | ||||
|     data_range = models.IntegerField(default=0, choices=DATASCOPE_CHOICES, verbose_name="数据权限范围", | ||||
|                                      help_text="数据权限范围") | ||||
|     dept = models.ManyToManyField(to="Dept", blank=True, verbose_name="数据权限-关联部门", db_constraint=False, | ||||
|                                   help_text="数据权限-关联部门") | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "role_menu_button_permission" | ||||
|         verbose_name = "角色按钮权限表" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("-create_datetime",) | ||||
|  | ||||
|  | ||||
| class Dictionary(CoreModel): | ||||
|     TYPE_LIST = ( | ||||
|         (0, "text"), | ||||
|         (1, "number"), | ||||
|         (2, "date"), | ||||
|         (3, "datetime"), | ||||
|         (4, "time"), | ||||
|         (5, "files"), | ||||
|         (6, "boolean"), | ||||
|         (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="字典编号/实际值") | ||||
|     parent = models.ForeignKey( | ||||
|         to="self", | ||||
|         related_name="sublist", | ||||
|         db_constraint=False, | ||||
|         on_delete=models.PROTECT, | ||||
|         blank=True, | ||||
|         null=True, | ||||
|         verbose_name="父级", | ||||
|         help_text="父级", | ||||
|     ) | ||||
|     type = models.IntegerField(choices=TYPE_LIST, default=0, verbose_name="数据值类型", help_text="数据值类型") | ||||
|     color = models.CharField(max_length=20, blank=True, null=True, verbose_name="颜色", help_text="颜色") | ||||
|     is_value = models.BooleanField(default=False, verbose_name="是否为value值", | ||||
|                                    help_text="是否为value值,用来做具体值存放") | ||||
|     status = models.BooleanField(default=True, verbose_name="状态", help_text="状态") | ||||
|     sort = models.IntegerField(default=1, verbose_name="显示排序", null=True, blank=True, help_text="显示排序") | ||||
|     remark = models.CharField(max_length=2000, blank=True, null=True, verbose_name="备注", help_text="备注") | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "system_dictionary" | ||||
|         verbose_name = "字典表" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("sort",) | ||||
|  | ||||
|     def save(self, force_insert=False, force_update=False, using=None, update_fields=None): | ||||
|         super().save(force_insert, force_update, using, update_fields) | ||||
|         dispatch.refresh_dictionary()  # 有更新则刷新字典配置 | ||||
|  | ||||
|     def delete(self, using=None, keep_parents=False): | ||||
|         res = super().delete(using, keep_parents) | ||||
|         dispatch.refresh_dictionary() | ||||
|         return res | ||||
|  | ||||
|  | ||||
| class OperationLog(CoreModel): | ||||
|     request_modular = models.CharField(max_length=64, verbose_name="请求模块", null=True, blank=True, | ||||
|                                        help_text="请求模块") | ||||
|     request_path = models.CharField(max_length=400, verbose_name="请求地址", null=True, blank=True, | ||||
|                                     help_text="请求地址") | ||||
|     request_body = models.TextField(verbose_name="请求参数", null=True, blank=True, help_text="请求参数") | ||||
|     request_method = models.CharField(max_length=8, verbose_name="请求方式", null=True, blank=True, | ||||
|                                       help_text="请求方式") | ||||
|     request_msg = models.TextField(verbose_name="操作说明", null=True, blank=True, help_text="操作说明") | ||||
|     request_ip = models.CharField(max_length=32, verbose_name="请求ip地址", null=True, blank=True, | ||||
|                                   help_text="请求ip地址") | ||||
|     request_browser = models.CharField(max_length=64, verbose_name="请求浏览器", null=True, blank=True, | ||||
|                                        help_text="请求浏览器") | ||||
|     response_code = models.CharField(max_length=32, verbose_name="响应状态码", null=True, blank=True, | ||||
|                                      help_text="响应状态码") | ||||
|     request_os = models.CharField(max_length=64, verbose_name="操作系统", null=True, blank=True, help_text="操作系统") | ||||
|     json_result = models.TextField(verbose_name="返回信息", null=True, blank=True, help_text="返回信息") | ||||
|     status = models.BooleanField(default=False, verbose_name="响应状态", help_text="响应状态") | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "system_operation_log" | ||||
|         verbose_name = "操作日志" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("-create_datetime",) | ||||
|  | ||||
|  | ||||
| def media_file_name(instance, filename): | ||||
|     h = instance.md5sum | ||||
|     basename, ext = os.path.splitext(filename) | ||||
|     return os.path.join("files", h[:1], h[1:2], h + ext.lower()) | ||||
|  | ||||
|  | ||||
| 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, 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") | ||||
|     UPLOAD_METHOD_CHOIDES = ( | ||||
|         (0, '默认上传'), | ||||
|         (1, '文件选择器上传'), | ||||
|     ) | ||||
|     upload_method = models.SmallIntegerField(default=0, blank=True, null=True, choices=UPLOAD_METHOD_CHOIDES, verbose_name='上传方式', help_text='上传方式') | ||||
|     FILE_TYPE_CHOIDES = ( | ||||
|         (0, '图片'), | ||||
|         (1, '视频'), | ||||
|         (2, '音频'), | ||||
|         (3, '其他'), | ||||
|     ) | ||||
|     file_type = models.SmallIntegerField(default=3, choices=FILE_TYPE_CHOIDES, blank=True, null=True, verbose_name='文件类型', help_text='文件类型') | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if not self.md5sum:  # file is new | ||||
|             md5 = hashlib.md5() | ||||
|             for chunk in self.url.chunks(): | ||||
|                 md5.update(chunk) | ||||
|             self.md5sum = md5.hexdigest() | ||||
|         if not self.size: | ||||
|             self.size = self.url.size | ||||
|         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: | ||||
|         db_table = table_prefix + "system_file_list" | ||||
|         verbose_name = "文件管理" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("-create_datetime",) | ||||
|  | ||||
|  | ||||
| class Area(CoreModel): | ||||
|     name = models.CharField(max_length=100, verbose_name="名称", help_text="名称") | ||||
|     code = models.CharField(max_length=20, verbose_name="地区编码", help_text="地区编码", unique=True, db_index=True) | ||||
|     level = models.BigIntegerField(verbose_name="地区层级(1省份 2城市 3区县 4乡级)", | ||||
|                                    help_text="地区层级(1省份 2城市 3区县 4乡级)") | ||||
|     pinyin = models.CharField(max_length=255, verbose_name="拼音", help_text="拼音") | ||||
|     initials = models.CharField(max_length=20, verbose_name="首字母", help_text="首字母") | ||||
|     enable = models.BooleanField(default=True, verbose_name="是否启用", help_text="是否启用") | ||||
|     pcode = models.ForeignKey( | ||||
|         to="self", | ||||
|         verbose_name="父地区编码", | ||||
|         to_field="code", | ||||
|         on_delete=models.CASCADE, | ||||
|         db_constraint=False, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         help_text="父地区编码", | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "system_area" | ||||
|         verbose_name = "地区表" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("code",) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f"{self.name}" | ||||
|  | ||||
|  | ||||
| class ApiWhiteList(CoreModel): | ||||
|     url = models.CharField(max_length=200, help_text="url地址", verbose_name="url") | ||||
|     METHOD_CHOICES = ( | ||||
|         (0, "GET"), | ||||
|         (1, "POST"), | ||||
|         (2, "PUT"), | ||||
|         (3, "DELETE"), | ||||
|     ) | ||||
|     method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True, | ||||
|                                  help_text="接口请求方法") | ||||
|     enable_datasource = models.BooleanField(default=True, verbose_name="激活数据权限", help_text="激活数据权限", | ||||
|                                             blank=True) | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "api_white_list" | ||||
|         verbose_name = "接口白名单" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("-create_datetime",) | ||||
|  | ||||
|  | ||||
| class SystemConfig(CoreModel): | ||||
|     parent = models.ForeignKey( | ||||
|         to="self", | ||||
|         verbose_name="父级", | ||||
|         on_delete=models.CASCADE, | ||||
|         db_constraint=False, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         help_text="父级", | ||||
|     ) | ||||
|     title = models.CharField(max_length=50, verbose_name="标题", help_text="标题") | ||||
|     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="启用状态") | ||||
|     data_options = models.JSONField(verbose_name="数据options", help_text="数据options", null=True, blank=True) | ||||
|     FORM_ITEM_TYPE_LIST = ( | ||||
|         (0, "text"), | ||||
|         (1, "datetime"), | ||||
|         (2, "date"), | ||||
|         (3, "textarea"), | ||||
|         (4, "select"), | ||||
|         (5, "checkbox"), | ||||
|         (6, "radio"), | ||||
|         (7, "img"), | ||||
|         (8, "file"), | ||||
|         (9, "switch"), | ||||
|         (10, "number"), | ||||
|         (11, "array"), | ||||
|         (12, "imgs"), | ||||
|         (13, "foreignkey"), | ||||
|         (14, "manytomany"), | ||||
|         (15, "time"), | ||||
|     ) | ||||
|     form_item_type = models.IntegerField( | ||||
|         choices=FORM_ITEM_TYPE_LIST, verbose_name="表单类型", help_text="表单类型", default=0, blank=True | ||||
|     ) | ||||
|     rule = models.JSONField(null=True, blank=True, verbose_name="校验规则", help_text="校验规则") | ||||
|     placeholder = models.CharField(max_length=50, null=True, blank=True, verbose_name="提示信息", help_text="提示信息") | ||||
|     setting = models.JSONField(null=True, blank=True, verbose_name="配置", help_text="配置") | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "system_config" | ||||
|         verbose_name = "系统配置表" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("sort",) | ||||
|         unique_together = (("key", "parent_id"),) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f"{self.title}" | ||||
|  | ||||
|     def save(self, force_insert=False, force_update=False, using=None, update_fields=None): | ||||
|         super().save(force_insert, force_update, using, update_fields) | ||||
|         dispatch.refresh_system_config()  # 有更新则刷新系统配置 | ||||
|  | ||||
|     def delete(self, using=None, keep_parents=False): | ||||
|         res = super().delete(using, keep_parents) | ||||
|         dispatch.refresh_system_config() | ||||
|         return res | ||||
|  | ||||
|  | ||||
| class LoginLog(CoreModel): | ||||
|     LOGIN_TYPE_CHOICES = ((1, "普通登录"), (2, "微信扫码登录"),) | ||||
|     username = models.CharField(max_length=32, verbose_name="登录用户名", null=True, blank=True, help_text="登录用户名") | ||||
|     ip = models.CharField(max_length=32, verbose_name="登录ip", null=True, blank=True, help_text="登录ip") | ||||
|     agent = models.TextField(verbose_name="agent信息", null=True, blank=True, help_text="agent信息") | ||||
|     browser = models.CharField(max_length=200, verbose_name="浏览器名", null=True, blank=True, help_text="浏览器名") | ||||
|     os = models.CharField(max_length=200, verbose_name="操作系统", null=True, blank=True, help_text="操作系统") | ||||
|     continent = models.CharField(max_length=50, verbose_name="州", null=True, blank=True, help_text="州") | ||||
|     country = models.CharField(max_length=50, verbose_name="国家", null=True, blank=True, help_text="国家") | ||||
|     province = models.CharField(max_length=50, verbose_name="省份", null=True, blank=True, help_text="省份") | ||||
|     city = models.CharField(max_length=50, verbose_name="城市", null=True, blank=True, help_text="城市") | ||||
|     district = models.CharField(max_length=50, verbose_name="县区", null=True, blank=True, help_text="县区") | ||||
|     isp = models.CharField(max_length=50, verbose_name="运营商", null=True, blank=True, help_text="运营商") | ||||
|     area_code = models.CharField(max_length=50, verbose_name="区域代码", null=True, blank=True, help_text="区域代码") | ||||
|     country_english = models.CharField(max_length=50, verbose_name="英文全称", null=True, blank=True, | ||||
|                                        help_text="英文全称") | ||||
|     country_code = models.CharField(max_length=50, verbose_name="简称", null=True, blank=True, help_text="简称") | ||||
|     longitude = models.CharField(max_length=50, verbose_name="经度", null=True, blank=True, help_text="经度") | ||||
|     latitude = models.CharField(max_length=50, verbose_name="纬度", null=True, blank=True, help_text="纬度") | ||||
|     login_type = models.IntegerField(default=1, choices=LOGIN_TYPE_CHOICES, verbose_name="登录类型", | ||||
|                                      help_text="登录类型") | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "system_login_log" | ||||
|         verbose_name = "登录日志" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("-create_datetime",) | ||||
|  | ||||
|  | ||||
| class MessageCenter(CoreModel): | ||||
|     title = models.CharField(max_length=100, verbose_name="标题", help_text="标题") | ||||
|     content = models.TextField(verbose_name="内容", help_text="内容") | ||||
|     target_type = models.IntegerField(default=0, verbose_name="目标类型", help_text="目标类型") | ||||
|     target_user = models.ManyToManyField(to=Users, related_name='user', through='MessageCenterTargetUser', | ||||
|                                          through_fields=('messagecenter', 'users'), blank=True, verbose_name="目标用户", | ||||
|                                          help_text="目标用户") | ||||
|     target_dept = models.ManyToManyField(to=Dept, blank=True, db_constraint=False, | ||||
|                                          verbose_name="目标部门", help_text="目标部门") | ||||
|     target_role = models.ManyToManyField(to=Role, blank=True, db_constraint=False, | ||||
|                                          verbose_name="目标角色", help_text="目标角色") | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "message_center" | ||||
|         verbose_name = "消息中心" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("-create_datetime",) | ||||
|  | ||||
|  | ||||
| class MessageCenterTargetUser(CoreModel): | ||||
|     users = models.ForeignKey(Users, related_name="target_user", on_delete=models.CASCADE, db_constraint=False, | ||||
|                               verbose_name="关联用户表", help_text="关联用户表") | ||||
|     messagecenter = models.ForeignKey(MessageCenter, on_delete=models.CASCADE, db_constraint=False, | ||||
|                                       verbose_name="关联消息中心表", help_text="关联消息中心表") | ||||
|     is_read = models.BooleanField(default=False, blank=True, null=True, verbose_name="是否已读", help_text="是否已读") | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "message_center_target_user" | ||||
|         verbose_name = "消息中心目标用户表" | ||||
|         verbose_name_plural = verbose_name | ||||
|  | ||||
|  | ||||
| def media_file_name_downloadcenter(instance:'DownloadCenter', filename): | ||||
|     h = instance.md5sum | ||||
|     basename, ext = os.path.splitext(filename) | ||||
|     return PurePosixPath("files", "dlct", h[:1], h[1:2], basename + '-' + str(time()).replace('.', '') + ext.lower()) | ||||
|  | ||||
|  | ||||
| class DownloadCenter(CoreModel): | ||||
|     TASK_STATUS_CHOICES = [ | ||||
|         (0, '任务已创建'), | ||||
|         (1, '任务进行中'), | ||||
|         (2, '任务完成'), | ||||
|         (3, '任务失败'), | ||||
|     ] | ||||
|     task_name = models.CharField(max_length=255, verbose_name="任务名称", help_text="任务名称") | ||||
|     task_status = models.SmallIntegerField(default=0, choices=TASK_STATUS_CHOICES, verbose_name='是否可下载', help_text='是否可下载') | ||||
|     file_name = models.CharField(max_length=255, null=True, blank=True, verbose_name="文件名", help_text="文件名") | ||||
|     url = models.FileField(upload_to=media_file_name_downloadcenter, null=True, blank=True) | ||||
|     size = models.BigIntegerField(default=0, verbose_name="文件大小", help_text="文件大小") | ||||
|     md5sum = models.CharField(max_length=36, null=True, blank=True, verbose_name="文件md5", help_text="文件md5") | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if self.url: | ||||
|             if not self.md5sum:  # file is new | ||||
|                 md5 = hashlib.md5() | ||||
|                 for chunk in self.url.chunks(): | ||||
|                     md5.update(chunk) | ||||
|                 self.md5sum = md5.hexdigest() | ||||
|             if not self.size: | ||||
|                 self.size = self.url.size | ||||
|         super(DownloadCenter, self).save(*args, **kwargs) | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = table_prefix + "download_center" | ||||
|         verbose_name = "下载中心" | ||||
|         verbose_name_plural = verbose_name | ||||
|         ordering = ("-create_datetime",) | ||||
							
								
								
									
										12
									
								
								backend/dvadmin/system/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/dvadmin/system/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| from django.dispatch import Signal | ||||
| # 初始化信号 | ||||
| pre_init_complete = Signal() | ||||
| detail_init_complete = Signal() | ||||
| post_init_complete = Signal() | ||||
| # 租户初始化信号 | ||||
| pre_tenants_init_complete = Signal() | ||||
| detail_tenants_init_complete = Signal() | ||||
| post_tenants_init_complete = Signal() | ||||
| post_tenants_all_init_complete = Signal() | ||||
| # 租户创建完成信号 | ||||
| tenants_create_complete = Signal() | ||||
							
								
								
									
										107
									
								
								backend/dvadmin/system/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								backend/dvadmin/system/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| from hashlib import md5 | ||||
| from io import BytesIO | ||||
| from datetime import datetime | ||||
| from time import sleep | ||||
|  | ||||
| from openpyxl import Workbook | ||||
| from openpyxl.worksheet.table import Table, TableStyleInfo | ||||
| from openpyxl.utils import get_column_letter | ||||
| from django.core.files.base import ContentFile | ||||
|  | ||||
| from application.celery import app | ||||
| from dvadmin.system.models import DownloadCenter | ||||
|  | ||||
| def is_number(num): | ||||
|     try: | ||||
|         float(num) | ||||
|         return True | ||||
|     except ValueError: | ||||
|         pass | ||||
|  | ||||
|     try: | ||||
|         import unicodedata | ||||
|         unicodedata.numeric(num) | ||||
|         return True | ||||
|     except (TypeError, ValueError): | ||||
|         pass | ||||
|     return False | ||||
|  | ||||
| def get_string_len(string): | ||||
|     """ | ||||
|     获取字符串最大长度 | ||||
|     :param string: | ||||
|     :return: | ||||
|     """ | ||||
|     length = 4 | ||||
|     if string is None: | ||||
|         return length | ||||
|     if is_number(string): | ||||
|         return length | ||||
|     for char in string: | ||||
|         length += 2.1 if ord(char) > 256 else 1 | ||||
|     return round(length, 1) if length <= 50 else 50 | ||||
|  | ||||
| @app.task | ||||
| def async_export_data(data: list, filename: str, dcid: int, export_field_label: dict): | ||||
|     instance = DownloadCenter.objects.get(pk=dcid) | ||||
|     instance.task_status = 1 | ||||
|     instance.save() | ||||
|     sleep(2) | ||||
|     try: | ||||
|         wb = Workbook() | ||||
|         ws = wb.active | ||||
|         header_data = ["序号", *export_field_label.values()] | ||||
|         hidden_header = ["#", *export_field_label.keys()] | ||||
|         df_len_max = [get_string_len(ele) for ele in header_data] | ||||
|         row = get_column_letter(len(export_field_label) + 1) | ||||
|         column = 1 | ||||
|         ws.append(header_data) | ||||
|         for index, results in enumerate(data): | ||||
|             results_list = [] | ||||
|             for h_index, h_item in enumerate(hidden_header): | ||||
|                 for key, val in results.items(): | ||||
|                     if key == h_item: | ||||
|                         if val is None or val == "": | ||||
|                             results_list.append("") | ||||
|                         elif isinstance(val, datetime): | ||||
|                             val = val.strftime("%Y-%m-%d %H:%M:%S") | ||||
|                             results_list.append(val) | ||||
|                         else: | ||||
|                             results_list.append(val) | ||||
|                         # 计算最大列宽度 | ||||
|                         result_column_width = get_string_len(val) | ||||
|                         if h_index != 0 and result_column_width > df_len_max[h_index]: | ||||
|                             df_len_max[h_index] = result_column_width | ||||
|             ws.append([index + 1, *results_list]) | ||||
|             column += 1 | ||||
|         #  更新列宽 | ||||
|         for index, width in enumerate(df_len_max): | ||||
|             ws.column_dimensions[get_column_letter(index + 1)].width = width | ||||
|         tab = Table(displayName="Table", ref=f"A1:{row}{column}")  # 名称管理器 | ||||
|         style = TableStyleInfo( | ||||
|             name="TableStyleLight11", | ||||
|             showFirstColumn=True, | ||||
|             showLastColumn=True, | ||||
|             showRowStripes=True, | ||||
|             showColumnStripes=True, | ||||
|         ) | ||||
|         tab.tableStyleInfo = style | ||||
|         ws.add_table(tab) | ||||
|         stream = BytesIO() | ||||
|         wb.save(stream) | ||||
|         stream.seek(0) | ||||
|         s = md5() | ||||
|         while True: | ||||
|             chunk = stream.read(1024) | ||||
|             if not chunk: | ||||
|                 break | ||||
|             s.update(chunk) | ||||
|         stream.seek(0) | ||||
|         instance.md5sum = s.hexdigest() | ||||
|         instance.file_name = filename | ||||
|         instance.url.save(filename, ContentFile(stream.read())) | ||||
|         instance.task_status = 2 | ||||
|     except Exception as e: | ||||
|         instance.task_status = 3 | ||||
|         instance.description = str(e)[:250] | ||||
|     instance.save() | ||||
							
								
								
									
										56
									
								
								backend/dvadmin/system/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								backend/dvadmin/system/tests.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +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() | ||||
							
								
								
									
										56
									
								
								backend/dvadmin/system/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								backend/dvadmin/system/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| from django.urls import path | ||||
| 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 | ||||
| from dvadmin.system.views.login_log import LoginLogViewSet | ||||
| from dvadmin.system.views.menu import MenuViewSet | ||||
| from dvadmin.system.views.menu_button import MenuButtonViewSet | ||||
| from dvadmin.system.views.message_center import MessageCenterViewSet | ||||
| from dvadmin.system.views.operation_log import OperationLogViewSet | ||||
| from dvadmin.system.views.role import RoleViewSet | ||||
| 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 | ||||
| from dvadmin.system.views.download_center import DownloadCenterViewSet | ||||
|  | ||||
| system_url = routers.SimpleRouter() | ||||
| system_url.register(r'menu', MenuViewSet) | ||||
| system_url.register(r'menu_button', MenuButtonViewSet) | ||||
| system_url.register(r'role', RoleViewSet) | ||||
| system_url.register(r'dept', DeptViewSet) | ||||
| system_url.register(r'user', UserViewSet) | ||||
| system_url.register(r'operation_log', OperationLogViewSet) | ||||
| system_url.register(r'dictionary', DictionaryViewSet) | ||||
| 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'role_menu_button_permission', RoleMenuButtonPermissionViewSet) | ||||
| system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet) | ||||
| system_url.register(r'column', MenuFieldViewSet) | ||||
| system_url.register(r'login_log', LoginLogViewSet) | ||||
| system_url.register(r'download_center', DownloadCenterViewSet) | ||||
|  | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path('user/export/', UserViewSet.as_view({'post': 'export_data', })), | ||||
|     path('user/import/', UserViewSet.as_view({'get': 'import_data', 'post': 'import_data'})), | ||||
|     path('system_config/save_content/', SystemConfigViewSet.as_view({'put': 'save_content'})), | ||||
|     path('system_config/get_association_table/', SystemConfigViewSet.as_view({'get': 'get_association_table'})), | ||||
|     path('system_config/get_table_data/<int:pk>/', SystemConfigViewSet.as_view({'get': 'get_table_data'})), | ||||
|     path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})), | ||||
|     # path('login_log/', LoginLogViewSet.as_view({'get': 'list'})), | ||||
|     # path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})), | ||||
|     path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), | ||||
|     path('clause/privacy.html', PrivacyView.as_view()), | ||||
|     path('clause/terms_service.html', TermsServiceView.as_view()), | ||||
| ] | ||||
| urlpatterns += system_url.urls | ||||
							
								
								
									
										1
									
								
								backend/dvadmin/system/util/pca-code.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								backend/dvadmin/system/util/pca-code.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										0
									
								
								backend/dvadmin/system/views/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/dvadmin/system/views/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										39
									
								
								backend/dvadmin/system/views/api_white_list.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								backend/dvadmin/system/views/api_white_list.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2022/1/1 001 9:34 | ||||
| @Remark: | ||||
| """ | ||||
| from dvadmin.system.models import ApiWhiteList | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class ApiWhiteListSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     接口白名单-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = ApiWhiteList | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class ApiWhiteListViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     接口白名单 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = ApiWhiteList.objects.all() | ||||
|     serializer_class = ApiWhiteListSerializer | ||||
|     # permission_classes = [] | ||||
							
								
								
									
										97
									
								
								backend/dvadmin/system/views/area.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								backend/dvadmin/system/views/area.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| import pypinyin | ||||
| from django.db.models import Q | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from dvadmin.system.models import Area | ||||
| from dvadmin.utils.field_permission import FieldPermissionMixin | ||||
| from dvadmin.utils.json_response import SuccessResponse | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class AreaSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     地区-序列化器 | ||||
|     """ | ||||
|     pcode_count = serializers.SerializerMethodField(read_only=True) | ||||
|     hasChild = serializers.SerializerMethodField() | ||||
|     pcode_info = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_pcode_info(self, instance): | ||||
|         pcode = Area.objects.filter(code=instance.pcode_id).values("name", "code") | ||||
|         return pcode | ||||
|  | ||||
|     def get_pcode_count(self, instance: Area): | ||||
|         return Area.objects.filter(pcode=instance).count() | ||||
|  | ||||
|     def get_hasChild(self, instance): | ||||
|         hasChild = Area.objects.filter(pcode=instance.code) | ||||
|         if hasChild: | ||||
|             return True | ||||
|         return False | ||||
|  | ||||
|     class Meta: | ||||
|         model = Area | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class AreaCreateUpdateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     地区管理 创建/更新时的列化器 | ||||
|     """ | ||||
|  | ||||
|     def to_internal_value(self, data): | ||||
|         pinyin = ''.join([''.join(i) for i in pypinyin.pinyin(data["name"], style=pypinyin.NORMAL)]) | ||||
|         data["level"] = 1 | ||||
|         data["pinyin"] = pinyin | ||||
|         data["initials"] = pinyin[0].upper() if pinyin else "#" | ||||
|         pcode = data["pcode"] if 'pcode' in data else None | ||||
|         if pcode: | ||||
|             pcode = Area.objects.get(pk=pcode) | ||||
|             data["pcode"] = pcode.code | ||||
|             data["level"] = pcode.level + 1 | ||||
|         return super().to_internal_value(data) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Area | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class AreaViewSet(CustomModelViewSet, FieldPermissionMixin): | ||||
|     """ | ||||
|     地区管理接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = Area.objects.all() | ||||
|     serializer_class = AreaSerializer | ||||
|     create_serializer_class = AreaCreateUpdateSerializer | ||||
|     update_serializer_class = AreaCreateUpdateSerializer | ||||
|     extra_filter_class = [] | ||||
|  | ||||
|     def list(self, request, *args, **kwargs): | ||||
|         self.request.query_params._mutable = True | ||||
|         params = self.request.query_params | ||||
|         known_params = {'page', 'limit', 'pcode'} | ||||
|         # 使用集合操作检查是否有未知参数 | ||||
|         other_params_exist = any(param not in known_params for param in params) | ||||
|         if other_params_exist: | ||||
|             queryset = self.queryset.filter(enable=True) | ||||
|         else: | ||||
|             pcode = params.get('pcode', None) | ||||
|             params['limit'] = 999 | ||||
|             if params and pcode: | ||||
|                 queryset = self.queryset.filter(enable=True, pcode=pcode) | ||||
|             else: | ||||
|                 queryset = self.queryset.filter(enable=True, level=1) | ||||
|         page = self.paginate_queryset(queryset) | ||||
|         if page is not None: | ||||
|             serializer = self.get_serializer(page, many=True, request=request) | ||||
|             return self.get_paginated_response(serializer.data) | ||||
|         serializer = self.get_serializer(queryset, many=True, request=request) | ||||
|         return SuccessResponse(data=serializer.data, msg="获取成功") | ||||
							
								
								
									
										23
									
								
								backend/dvadmin/system/views/clause.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								backend/dvadmin/system/views/clause.py
									
									
									
									
									
										Normal file
									
								
							| @ -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') | ||||
							
								
								
									
										212
									
								
								backend/dvadmin/system/views/dept.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								backend/dvadmin/system/views/dept.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,212 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: H0nGzA1 | ||||
| @contact: QQ:2505811377 | ||||
| @Remark: 部门管理 | ||||
| """ | ||||
| from rest_framework import serializers | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
|  | ||||
| from dvadmin.system.models import Dept, RoleMenuButtonPermission, Users | ||||
| from dvadmin.utils.filters import DataLevelPermissionsFilter | ||||
| from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class DeptSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     部门-序列化器 | ||||
|     """ | ||||
|     parent_name = serializers.CharField(read_only=True, source='parent.name') | ||||
|     status_label = serializers.SerializerMethodField() | ||||
|     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: | ||||
|             return True | ||||
|         return False | ||||
|  | ||||
|     def get_status_label(self, obj: Dept): | ||||
|         if obj.status: | ||||
|             return "启用" | ||||
|         return "禁用" | ||||
|  | ||||
|     def get_has_children(self, obj: Dept): | ||||
|         return Dept.objects.filter(parent_id=obj.id).count() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Dept | ||||
|         fields = '__all__' | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class DeptImportSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     部门-导入-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = Dept | ||||
|         fields = '__all__' | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class DeptCreateUpdateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     部门管理 创建/更新时的列化器 | ||||
|     """ | ||||
|  | ||||
|     def create(self, validated_data): | ||||
|         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() | ||||
|         return instance | ||||
|  | ||||
|     class Meta: | ||||
|         model = Dept | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class DeptViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     部门管理接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = Dept.objects.all() | ||||
|     serializer_class = DeptSerializer | ||||
|     create_serializer_class = DeptCreateUpdateSerializer | ||||
|     update_serializer_class = DeptCreateUpdateSerializer | ||||
|     filter_fields = ['name', 'id', 'parent'] | ||||
|     search_fields = [] | ||||
|     # extra_filter_class = [] | ||||
|     import_serializer_class = DeptImportSerializer | ||||
|     import_field_dict = { | ||||
|         "name": "部门名称", | ||||
|         "key": "部门标识", | ||||
|     } | ||||
|  | ||||
|     def list(self, request, *args, **kwargs): | ||||
|         # 如果懒加载,则只返回父级 | ||||
|         request.query_params._mutable = True | ||||
|         params = request.query_params | ||||
|         parent = params.get('parent', None) | ||||
|         page = params.get('page', None) | ||||
|         limit = params.get('limit', None) | ||||
|         if page: | ||||
|             del params['page'] | ||||
|         if limit: | ||||
|             del params['limit'] | ||||
|         if params and parent: | ||||
|             queryset = self.queryset.filter(status=True, parent=parent) | ||||
|         else: | ||||
|             queryset = self.queryset.filter(status=True) | ||||
|         queryset = self.filter_queryset(queryset) | ||||
|         serializer = DeptSerializer(queryset, many=True, request=request) | ||||
|         data = serializer.data | ||||
|         return SuccessResponse(data=data) | ||||
|  | ||||
|     @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def all_dept(self, request, *args, **kwargs): | ||||
|         queryset = self.filter_queryset(self.get_queryset()) | ||||
|         data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent') | ||||
|         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) | ||||
							
								
								
									
										107
									
								
								backend/dvadmin/system/views/dictionary.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								backend/dvadmin/system/views/dictionary.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/3 003 0:30 | ||||
| @Remark: 字典管理 | ||||
| """ | ||||
| from rest_framework import serializers | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from application import dispatch | ||||
| from dvadmin.system.models import Dictionary | ||||
| from dvadmin.utils.json_response import SuccessResponse | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class DictionarySerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     字典-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = Dictionary | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| 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 | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class DictionaryViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     字典管理接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = Dictionary.objects.all() | ||||
|     serializer_class = DictionarySerializer | ||||
|     create_serializer_class = DictionaryCreateUpdateSerializer | ||||
|     extra_filter_class = [] | ||||
|     search_fields = ['label'] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         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(parent__isnull=True) | ||||
|             return queryset | ||||
|         else: | ||||
|             return self.queryset | ||||
|  | ||||
|  | ||||
| class InitDictionaryViewSet(APIView): | ||||
|     """ | ||||
|     获取初始化配置 | ||||
|     """ | ||||
|     authentication_classes = [] | ||||
|     permission_classes = [] | ||||
|     queryset = Dictionary.objects.all() | ||||
|  | ||||
|     def get(self, request): | ||||
|         dictionary_key = self.request.query_params.get('dictionary_key') | ||||
|         if dictionary_key: | ||||
|             if dictionary_key == 'all': | ||||
|                 data = [ele for ele in dispatch.get_dictionary_config().values()] | ||||
|                 if not data: | ||||
|                     dispatch.refresh_dictionary() | ||||
|                     data = [ele for ele in dispatch.get_dictionary_config().values()] | ||||
|             else: | ||||
|                 data = self.queryset.filter(parent__value=dictionary_key, status=True).values('label', 'value', 'type', | ||||
|                                                                                               'color') | ||||
|             return SuccessResponse(data=data, msg="获取成功") | ||||
|         return SuccessResponse(data=[], msg="获取成功") | ||||
							
								
								
									
										49
									
								
								backend/dvadmin/system/views/download_center.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								backend/dvadmin/system/views/download_center.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| from rest_framework import serializers | ||||
| from django.conf import settings | ||||
| from django_filters.rest_framework import FilterSet, CharFilter | ||||
|  | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
| from dvadmin.system.models import DownloadCenter | ||||
|  | ||||
|  | ||||
| class DownloadCenterSerializer(CustomModelSerializer): | ||||
|     url = serializers.SerializerMethodField(read_only=True) | ||||
|  | ||||
|     def get_url(self, instance): | ||||
|         if self.request.query_params.get('prefix'): | ||||
|             if settings.ENVIRONMENT in ['local']: | ||||
|                 prefix = 'http://127.0.0.1:8000' | ||||
|             elif settings.ENVIRONMENT in ['test']: | ||||
|                 prefix = 'http://{host}/api'.format(host=self.request.get_host()) | ||||
|             else: | ||||
|                 prefix = 'https://{host}/api'.format(host=self.request.get_host()) | ||||
|             return (f'{prefix}/media/{str(instance.url)}') | ||||
|         return f'media/{str(instance.url)}' | ||||
|  | ||||
|     class Meta: | ||||
|         model = DownloadCenter | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class DownloadCenterFilterSet(FilterSet): | ||||
|     task_name = CharFilter(field_name='task_name', lookup_expr='icontains') | ||||
|     file_name = CharFilter(field_name='file_name', lookup_expr='icontains') | ||||
|  | ||||
|     class Meta: | ||||
|         model = DownloadCenter | ||||
|         fields = ['task_status', 'task_name', 'file_name'] | ||||
|  | ||||
|  | ||||
| class DownloadCenterViewSet(CustomModelViewSet): | ||||
|     queryset = DownloadCenter.objects.all() | ||||
|     serializer_class = DownloadCenterSerializer | ||||
|     filter_class = DownloadCenterFilterSet | ||||
|     permission_classes = [] | ||||
|     extra_filter_class = [] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         if self.request.user.is_superuser: | ||||
|             return super().get_queryset() | ||||
|         return super().get_queryset().filter(creator=self.request.user) | ||||
							
								
								
									
										126
									
								
								backend/dvadmin/system/views/file_list.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								backend/dvadmin/system/views/file_list.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | ||||
| import hashlib | ||||
| import mimetypes | ||||
|  | ||||
| import django_filters | ||||
| from django.conf import settings | ||||
| from django.db import connection | ||||
| from rest_framework import serializers | ||||
| from rest_framework.decorators import action | ||||
|  | ||||
| from application import dispatch | ||||
| from dvadmin.system.models import FileList | ||||
| from dvadmin.utils.json_response import DetailResponse, SuccessResponse | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class FileSerializer(CustomModelSerializer): | ||||
|     url = serializers.SerializerMethodField(read_only=True) | ||||
|  | ||||
|     def get_url(self, instance): | ||||
|         if self.request.query_params.get('prefix'): | ||||
|             if settings.ENVIRONMENT in ['local']: | ||||
|                 prefix = 'http://127.0.0.1:8000' | ||||
|             elif settings.ENVIRONMENT in ['test']: | ||||
|                 prefix = 'http://{host}/api'.format(host=self.request.get_host()) | ||||
|             else: | ||||
|                 prefix = 'https://{host}/api'.format(host=self.request.get_host()) | ||||
|             if instance.file_url: | ||||
|                 return instance.file_url if instance.file_url.startswith('http') else f"{prefix}/{instance.file_url}" | ||||
|             return (f'{prefix}/media/{str(instance.url)}') | ||||
|         return instance.file_url or (f'media/{str(instance.url)}') | ||||
|  | ||||
|     class Meta: | ||||
|         model = FileList | ||||
|         fields = "__all__" | ||||
|  | ||||
|     def create(self, validated_data): | ||||
|         file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local' | ||||
|         file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup") | ||||
|         file = 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 | ||||
|         ft = {'image':0,'video':1,'audio':2}.get(file.content_type.split('/')[0], None) | ||||
|         validated_data['file_type'] = 3 if ft is None else ft | ||||
|         if file_backup: | ||||
|             validated_data['url'] = file | ||||
|         if file_engine == 'oss': | ||||
|             from dvadmin_cloud_storage.views.aliyun import ali_oss_upload | ||||
|             file_path = ali_oss_upload(file) | ||||
|             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) | ||||
|  | ||||
|  | ||||
| class FileAllSerializer(CustomModelSerializer): | ||||
|      | ||||
|     class Meta: | ||||
|         model = FileList | ||||
|         fields = ['id', 'name'] | ||||
|  | ||||
|  | ||||
| class FileFilter(django_filters.FilterSet): | ||||
|     name = django_filters.CharFilter(field_name="name", lookup_expr="icontains", help_text="文件名") | ||||
|     mime_type = django_filters.CharFilter(field_name="mime_type", lookup_expr="icontains", help_text="文件类型") | ||||
|  | ||||
|     class Meta: | ||||
|         model = FileList | ||||
|         fields = ['name', 'mime_type', 'upload_method', 'file_type'] | ||||
|  | ||||
|  | ||||
| class FileViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     文件管理接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = FileList.objects.all() | ||||
|     serializer_class = FileSerializer | ||||
|     filter_class = FileFilter | ||||
|     permission_classes = [] | ||||
|  | ||||
|     @action(methods=['GET'], detail=False) | ||||
|     def get_all(self, request): | ||||
|         data1 = self.get_serializer(self.get_queryset(), many=True).data | ||||
|         data2 = [] | ||||
|         if dispatch.is_tenants_mode(): | ||||
|             from django_tenants.utils import schema_context | ||||
|             with schema_context('public'): | ||||
|                 data2 = self.get_serializer(FileList.objects.all(), many=True).data | ||||
|         return DetailResponse(data=data2+data1) | ||||
|  | ||||
|     def list(self, request, *args, **kwargs): | ||||
|         if self.request.query_params.get('system', 'False') == 'True' and dispatch.is_tenants_mode(): | ||||
|             from django_tenants.utils import schema_context | ||||
|             with schema_context('public'): | ||||
|                 return super().list(request, *args, **kwargs) | ||||
|         return super().list(request, *args, **kwargs) | ||||
							
								
								
									
										274
									
								
								backend/dvadmin/system/views/login.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										274
									
								
								backend/dvadmin/system/views/login.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,274 @@ | ||||
| import base64 | ||||
| import hashlib | ||||
| from datetime import datetime, timedelta | ||||
| from captcha.views import CaptchaStore, captcha_image | ||||
| from django.contrib import auth | ||||
| from django.contrib.auth import login | ||||
| from django.contrib.auth.hashers import check_password, make_password | ||||
| from django.db.models import Q | ||||
| from django.shortcuts import redirect | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from drf_yasg import openapi | ||||
| from drf_yasg.utils import swagger_auto_schema | ||||
| from rest_framework import serializers | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.views import APIView | ||||
| from rest_framework_simplejwt.serializers import TokenObtainPairSerializer | ||||
| from rest_framework_simplejwt.views import TokenObtainPairView | ||||
| from django.conf import settings | ||||
| from application import dispatch | ||||
| from dvadmin.system.models import Users | ||||
| from dvadmin.utils.json_response import ErrorResponse, DetailResponse | ||||
| from dvadmin.utils.request_util import save_login_log | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.validator import CustomValidationError | ||||
|  | ||||
|  | ||||
| class CaptchaView(APIView): | ||||
|     authentication_classes = [] | ||||
|     permission_classes = [] | ||||
|  | ||||
|     @swagger_auto_schema( | ||||
|         responses={"200": openapi.Response("获取成功")}, | ||||
|         security=[], | ||||
|         operation_id="captcha-get", | ||||
|         operation_description="验证码获取", | ||||
|     ) | ||||
|     def get(self, request): | ||||
|         data = {} | ||||
|         if dispatch.get_system_config_values("base.captcha_state"): | ||||
|             hashkey = CaptchaStore.generate_key() | ||||
|             id = CaptchaStore.objects.filter(hashkey=hashkey).first().id | ||||
|             imgage = captcha_image(request, hashkey) | ||||
|             # 将图片转换为base64 | ||||
|             image_base = base64.b64encode(imgage.content) | ||||
|             data = { | ||||
|                 "key": id, | ||||
|                 "image_base": "data:image/png;base64," + image_base.decode("utf-8"), | ||||
|             } | ||||
|         return DetailResponse(data=data) | ||||
|  | ||||
|  | ||||
| class LoginSerializer(TokenObtainPairSerializer): | ||||
|     """ | ||||
|     登录的序列化器: | ||||
|     重写djangorestframework-simplejwt的序列化器 | ||||
|     """ | ||||
|     captcha = serializers.CharField( | ||||
|         max_length=6, required=False, allow_null=True, allow_blank=True | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Users | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|     default_error_messages = {"no_active_account": _("账号/密码错误")} | ||||
|  | ||||
|     def validate(self, attrs): | ||||
|         captcha = self.initial_data.get("captcha", None) | ||||
|         if dispatch.get_system_config_values("base.captcha_state"): | ||||
|             if captcha is None: | ||||
|                 raise CustomValidationError("验证码不能为空") | ||||
|             self.image_code = CaptchaStore.objects.filter( | ||||
|                 id=self.initial_data["captchaKey"] | ||||
|             ).first() | ||||
|             five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) | ||||
|             if self.image_code and five_minute_ago > self.image_code.expiration: | ||||
|                 self.image_code and self.image_code.delete() | ||||
|                 raise CustomValidationError("验证码过期") | ||||
|             else: | ||||
|                 if self.image_code and ( | ||||
|                     self.image_code.response == captcha | ||||
|                     or self.image_code.challenge == captcha | ||||
|                 ): | ||||
|                     self.image_code and self.image_code.delete() | ||||
|                 else: | ||||
|                     self.image_code and self.image_code.delete() | ||||
|                     raise CustomValidationError("图片验证码错误") | ||||
|         try: | ||||
|             user = Users.objects.get( | ||||
|                 Q(username=attrs['username']) | Q(email=attrs['username']) | Q(mobile=attrs['username'])) | ||||
|         except Users.DoesNotExist: | ||||
|             raise CustomValidationError("您登录的账号不存在") | ||||
|         except Users.MultipleObjectsReturned: | ||||
|             raise CustomValidationError("您登录的账号存在多个,请联系管理员检查登录账号唯一性") | ||||
|         if not user.is_active: | ||||
|             raise CustomValidationError("账号已被锁定,联系管理员解锁") | ||||
|         try: | ||||
|             # 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误 | ||||
|             attrs['username'] = user.username | ||||
|             data = super().validate(attrs) | ||||
|             data["username"] = self.user.username | ||||
|             data["name"] = self.user.name | ||||
|             data["userId"] = self.user.id | ||||
|             data["avatar"] = self.user.avatar | ||||
|             data['user_type'] = self.user.user_type | ||||
|             data['pwd_change_count'] = self.user.pwd_change_count | ||||
|             dept = getattr(self.user, 'dept', None) | ||||
|             if dept: | ||||
|                 data['dept_info'] = { | ||||
|                     'dept_id': dept.id, | ||||
|                     'dept_name': dept.name, | ||||
|                 } | ||||
|             role = getattr(self.user, 'role', None) | ||||
|             if role: | ||||
|                 data['role_info'] = role.values('id', 'name', 'key') | ||||
|             request = self.context.get("request") | ||||
|             request.user = self.user | ||||
|             # 记录登录日志 | ||||
|             save_login_log(request=request) | ||||
|             user.login_error_count = 0 | ||||
|             user.save() | ||||
|             return {"code": 2000, "msg": "请求成功", "data": data} | ||||
|         except Exception as e: | ||||
|             user.login_error_count += 1 | ||||
|             if user.login_error_count >= 5: | ||||
|                 user.is_active = False | ||||
|                 user.save() | ||||
|                 raise CustomValidationError("账号已被锁定,联系管理员解锁") | ||||
|             user.save() | ||||
|             count = 5 - user.login_error_count | ||||
|             raise CustomValidationError(f"账号/密码错误;重试{count}次后将被锁定~") | ||||
|  | ||||
|  | ||||
| class LoginView(TokenObtainPairView): | ||||
|     """ | ||||
|     登录接口 | ||||
|     """ | ||||
|     serializer_class = LoginSerializer | ||||
|     permission_classes = [] | ||||
|  | ||||
|     # def post(self, request, *args, **kwargs): | ||||
|     #     # username可能携带的不止是用户名,可能还是用户的其它唯一标识 手机号 邮箱 | ||||
|     #     username = request.data.get('username',None) | ||||
|     #     if username is None: | ||||
|     #         return ErrorResponse(msg="参数错误") | ||||
|     #     password = request.data.get('password',None) | ||||
|     #     if password is None: | ||||
|     #         return ErrorResponse(msg="参数错误") | ||||
|     #     captcha = request.data.get('captcha',None) | ||||
|     #     if captcha is None: | ||||
|     #         return ErrorResponse(msg="参数错误") | ||||
|     #     captchaKey = request.data.get('captchaKey',None) | ||||
|     #     if captchaKey is None: | ||||
|     #         return ErrorResponse(msg="参数错误") | ||||
|     #     if dispatch.get_system_config_values("base.captcha_state"): | ||||
|     #         if captcha is None: | ||||
|     #             raise CustomValidationError("验证码不能为空") | ||||
|     #         self.image_code = CaptchaStore.objects.filter( | ||||
|     #             id=captchaKey | ||||
|     #         ).first() | ||||
|     #         five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) | ||||
|     #         if self.image_code and five_minute_ago > self.image_code.expiration: | ||||
|     #             self.image_code and self.image_code.delete() | ||||
|     #             raise CustomValidationError("验证码过期") | ||||
|     #         else: | ||||
|     #             if self.image_code and ( | ||||
|     #                     self.image_code.response == captcha | ||||
|     #                     or self.image_code.challenge == captcha | ||||
|     #             ): | ||||
|     #                 self.image_code and self.image_code.delete() | ||||
|     #             else: | ||||
|     #                 self.image_code and self.image_code.delete() | ||||
|     #                 raise CustomValidationError("图片验证码错误") | ||||
|     #     try: | ||||
|     #         # 手动通过 user 签发 jwt-token | ||||
|     #         user = Users.objects.get(username=username) | ||||
|     #     except: | ||||
|     #         return DetailResponse(msg='该账号未注册') | ||||
|     #     # 获得用户后,校验密码并签发token | ||||
|     #     print(make_password(password),user.password) | ||||
|     #     if check_password(make_password(password),user.password): | ||||
|     #         return DetailResponse(msg='密码错误') | ||||
|     #     result = { | ||||
|     #        "name":user.name, | ||||
|     #         "userId":user.id, | ||||
|     #         "avatar":user.avatar, | ||||
|     #     } | ||||
|     #     dept = getattr(user, 'dept', None) | ||||
|     #     if dept: | ||||
|     #         result['dept_info'] = { | ||||
|     #             'dept_id': dept.id, | ||||
|     #             'dept_name': dept.name, | ||||
|     #             'dept_key': dept.key | ||||
|     #         } | ||||
|     #     role = getattr(user, 'role', None) | ||||
|     #     if role: | ||||
|     #         result['role_info'] = role.values('id', 'name', 'key') | ||||
|     #     refresh = LoginSerializer.get_token(user) | ||||
|     #     result["refresh"] = str(refresh) | ||||
|     #     result["access"] = str(refresh.access_token) | ||||
|     #     # 记录登录日志 | ||||
|     #     request.user = user | ||||
|     #     save_login_log(request=request) | ||||
|     #     return DetailResponse(data=result,msg="获取成功") | ||||
|  | ||||
|  | ||||
| class LoginTokenSerializer(TokenObtainPairSerializer): | ||||
|     """ | ||||
|     登录的序列化器: | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = Users | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|     default_error_messages = {"no_active_account": _("账号/密码不正确")} | ||||
|  | ||||
|     def validate(self, attrs): | ||||
|         if not getattr(settings, "LOGIN_NO_CAPTCHA_AUTH", False): | ||||
|             return {"code": 4000, "msg": "该接口暂未开通!", "data": None} | ||||
|         data = super().validate(attrs) | ||||
|         data["name"] = self.user.name | ||||
|         data["userId"] = self.user.id | ||||
|         return {"code": 2000, "msg": "请求成功", "data": data} | ||||
|  | ||||
|  | ||||
| class LoginTokenView(TokenObtainPairView): | ||||
|     """ | ||||
|     登录获取token接口 | ||||
|     """ | ||||
|  | ||||
|     serializer_class = LoginTokenSerializer | ||||
|     permission_classes = [] | ||||
|  | ||||
|  | ||||
| class LogoutView(APIView): | ||||
|     def post(self, request): | ||||
|         return DetailResponse(msg="注销成功") | ||||
|  | ||||
|  | ||||
| class ApiLoginSerializer(CustomModelSerializer): | ||||
|     """接口文档登录-序列化器""" | ||||
|  | ||||
|     username = serializers.CharField() | ||||
|     password = serializers.CharField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Users | ||||
|         fields = ["username", "password"] | ||||
|  | ||||
|  | ||||
| class ApiLogin(APIView): | ||||
|     """接口文档的登录接口""" | ||||
|  | ||||
|     serializer_class = ApiLoginSerializer | ||||
|     authentication_classes = [] | ||||
|     permission_classes = [] | ||||
|  | ||||
|     def post(self, request): | ||||
|         username = request.data.get("username") | ||||
|         password = request.data.get("password") | ||||
|         user_obj = auth.authenticate( | ||||
|             request, | ||||
|             username=username, | ||||
|             password=hashlib.md5(password.encode(encoding="UTF-8")).hexdigest(), | ||||
|         ) | ||||
|         if user_obj: | ||||
|             login(request, user_obj) | ||||
|             return redirect("/") | ||||
|         else: | ||||
|             return ErrorResponse(msg="账号/密码错误") | ||||
							
								
								
									
										37
									
								
								backend/dvadmin/system/views/login_log.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								backend/dvadmin/system/views/login_log.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/3 003 0:30 | ||||
| @Remark: 按钮权限管理 | ||||
| """ | ||||
| from dvadmin.system.models import LoginLog | ||||
| from dvadmin.utils.field_permission import FieldPermissionMixin | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class LoginLogSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     登录日志权限-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = LoginLog | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class LoginLogViewSet(CustomModelViewSet, FieldPermissionMixin): | ||||
|     """ | ||||
|     登录日志接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = LoginLog.objects.all() | ||||
|     serializer_class = LoginLogSerializer | ||||
|     # extra_filter_class = [] | ||||
							
								
								
									
										173
									
								
								backend/dvadmin/system/views/menu.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								backend/dvadmin/system/views/menu.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/1 001 22:38 | ||||
| @Remark: 菜单模块 | ||||
| """ | ||||
| from rest_framework import serializers | ||||
| from rest_framework.decorators import action | ||||
|  | ||||
| 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 | ||||
|  | ||||
|  | ||||
| class MenuSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     菜单表的简单序列化器 | ||||
|     """ | ||||
|     menuPermission = serializers.SerializerMethodField(read_only=True) | ||||
|     hasChild = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_menuPermission(self, instance): | ||||
|         queryset = instance.menuPermission.order_by('-name').values('id', 'name', 'value') | ||||
|         # MenuButtonSerializer(instance.menuPermission.all(), many=True) | ||||
|         if queryset: | ||||
|             return queryset | ||||
|         else: | ||||
|             return None | ||||
|  | ||||
|     def get_hasChild(self, instance): | ||||
|         hasChild = Menu.objects.filter(parent=instance.id) | ||||
|         if hasChild: | ||||
|             return True | ||||
|         return False | ||||
|  | ||||
|     class Meta: | ||||
|         model = Menu | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| 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__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class WebRouterSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     前端菜单路由的简单序列化器 | ||||
|     """ | ||||
|     path = serializers.CharField(source="web_path") | ||||
|     title = serializers.CharField(source="name") | ||||
|  | ||||
|     class Meta: | ||||
|         model = Menu | ||||
|         fields = ( | ||||
|             '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"] | ||||
|  | ||||
|  | ||||
| class MenuViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     菜单管理接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = Menu.objects.all() | ||||
|     serializer_class = MenuSerializer | ||||
|     create_serializer_class = MenuCreateSerializer | ||||
|     update_serializer_class = MenuCreateSerializer | ||||
|     search_fields = ['name', 'status'] | ||||
|     filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog'] | ||||
|  | ||||
|     def list(self, request): | ||||
|         """懒加载""" | ||||
|         request.query_params._mutable = True | ||||
|         params = request.query_params | ||||
|         parent = params.get('parent', None) | ||||
|         page = params.get('page', None) | ||||
|         limit = params.get('limit', None) | ||||
|         if page: | ||||
|             del params['page'] | ||||
|         if limit: | ||||
|             del params['limit'] | ||||
|         if params: | ||||
|             if parent: | ||||
|                 queryset = self.queryset.filter(parent=parent) | ||||
|             else: | ||||
|                 queryset = self.queryset.filter() | ||||
|         else: | ||||
|             queryset = self.queryset.filter(parent__isnull=True) | ||||
|         queryset = self.filter_queryset(queryset) | ||||
|         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).order_by("sort") | ||||
|         else: | ||||
|             role_list = user.role.values_list('id', flat=True) | ||||
|             menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True) | ||||
|             queryset = Menu.objects.filter(id__in=menu_list).order_by("sort") | ||||
|         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="下移成功") | ||||
							
								
								
									
										108
									
								
								backend/dvadmin/system/views/menu_button.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								backend/dvadmin/system/views/menu_button.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/3 003 0:30 | ||||
| @Remark: 菜单按钮管理 | ||||
| """ | ||||
| from django.db.models import F | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
|  | ||||
| from dvadmin.system.models import MenuButton, RoleMenuButtonPermission, Menu | ||||
| from dvadmin.utils.json_response import DetailResponse, SuccessResponse | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class MenuButtonSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     菜单按钮-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = MenuButton | ||||
|         fields = ['id', 'name', 'value', 'api', 'method','menu'] | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class MenuButtonCreateUpdateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化菜单按钮-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = MenuButton | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class MenuButtonViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     菜单按钮接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     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): | ||||
|         """ | ||||
|         获取所有的按钮权限 | ||||
|         :param request: | ||||
|         :return: | ||||
|         """ | ||||
|         is_superuser = request.user.is_superuser | ||||
|         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) | ||||
|  | ||||
|     @action(methods=['post'], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def batch_create(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         批量创建菜单“增删改查查”权限 | ||||
|         创建的数据来源于菜单,需要规范创建菜单参数 | ||||
|         value:菜单的component_name:method | ||||
|         api:菜单的web_path增加'/api'前缀,并根据method增加{id} | ||||
|         """ | ||||
|         menu_obj = Menu.objects.filter(id=request.data['menu']).first() | ||||
|         result_list = [ | ||||
|             {'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api/{menu_obj.component_name}/', 'method': 1}, | ||||
|             {'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 3}, | ||||
|             {'menu': menu_obj.id, 'name': '编辑', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 2}, | ||||
|             {'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api/{menu_obj.component_name}/', 'method': 0}, | ||||
|             {'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 0}, | ||||
|             {'menu': menu_obj.id, 'name': '复制', 'value': f'{menu_obj.component_name}:Copy', 'api': f'/api/{menu_obj.component_name}/', 'method': 1}, | ||||
|             {'menu': menu_obj.id, 'name': '导入', 'value': f'{menu_obj.component_name}:Import', 'api': f'/api/{menu_obj.component_name}/import_data/', 'method': 1}, | ||||
|             {'menu': menu_obj.id, 'name': '导出', 'value': f'{menu_obj.component_name}:Export', 'api': f'/api{menu_obj.component_name}/export_data/', 'method': 1},] | ||||
|         serializer = self.get_serializer(data=result_list, many=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return SuccessResponse(serializer.data, msg="批量创建成功") | ||||
							
								
								
									
										89
									
								
								backend/dvadmin/system/views/menu_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								backend/dvadmin/system/views/menu_field.py
									
									
									
									
									
										Normal file
									
								
							| @ -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='匹配成功') | ||||
							
								
								
									
										261
									
								
								backend/dvadmin/system/views/message_center.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								backend/dvadmin/system/views/message_center.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,261 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| import json | ||||
|  | ||||
| from asgiref.sync import async_to_sync | ||||
| from channels.layers import get_channel_layer | ||||
| from django_restql.fields import DynamicSerializerMethodField | ||||
| from rest_framework import serializers | ||||
| from rest_framework.decorators import action, permission_classes | ||||
| from rest_framework.permissions import IsAuthenticated, AllowAny | ||||
|  | ||||
| from dvadmin.system.models import MessageCenter, Users, MessageCenterTargetUser | ||||
| from dvadmin.utils.json_response import SuccessResponse, DetailResponse | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class MessageCenterSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     消息中心-序列化器 | ||||
|     """ | ||||
|     role_info = DynamicSerializerMethodField() | ||||
|     user_info = DynamicSerializerMethodField() | ||||
|     dept_info = DynamicSerializerMethodField() | ||||
|     is_read = serializers.BooleanField(read_only=True, source='target_user__is_read') | ||||
|  | ||||
|     def get_role_info(self, instance, parsed_query): | ||||
|         roles = instance.target_role.all() | ||||
|         # You can do what ever you want in here | ||||
|         # `parsed_query` param is passed to BookSerializer to allow further querying | ||||
|         from dvadmin.system.views.role import RoleSerializer | ||||
|         serializer = RoleSerializer( | ||||
|             roles, | ||||
|             many=True, | ||||
|             parsed_query=parsed_query | ||||
|         ) | ||||
|         return serializer.data | ||||
|  | ||||
|     def get_user_info(self, instance, parsed_query): | ||||
|         if instance.target_type  in (1,2,3): | ||||
|             return [] | ||||
|         users = instance.target_user.all() | ||||
|         # You can do what ever you want in here | ||||
|         # `parsed_query` param is passed to BookSerializer to allow further querying | ||||
|         from dvadmin.system.views.user import UserSerializer | ||||
|         serializer = UserSerializer( | ||||
|             users, | ||||
|             many=True, | ||||
|             parsed_query=parsed_query | ||||
|         ) | ||||
|         return serializer.data | ||||
|  | ||||
|     def get_dept_info(self, instance, parsed_query): | ||||
|         dept = instance.target_dept.all() | ||||
|         # You can do what ever you want in here | ||||
|         # `parsed_query` param is passed to BookSerializer to allow further querying | ||||
|         from dvadmin.system.views.dept import DeptSerializer | ||||
|         serializer = DeptSerializer( | ||||
|             dept, | ||||
|             many=True, | ||||
|             parsed_query=parsed_query | ||||
|         ) | ||||
|         return serializer.data | ||||
|  | ||||
|     class Meta: | ||||
|         model = MessageCenter | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class MessageCenterTargetUserSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     目标用户序列化器-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = MessageCenterTargetUser | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class MessageCenterTargetUserListSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     目标用户序列化器-序列化器 | ||||
|     """ | ||||
|     role_info = DynamicSerializerMethodField() | ||||
|     user_info = DynamicSerializerMethodField() | ||||
|     dept_info = DynamicSerializerMethodField() | ||||
|     is_read = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_is_read(self, instance): | ||||
|         user_id = self.request.user.id | ||||
|         message_center_id = instance.id | ||||
|         queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id, users_id=user_id).first() | ||||
|         if queryset: | ||||
|             return queryset.is_read | ||||
|         return False | ||||
|  | ||||
|     def get_role_info(self, instance, parsed_query): | ||||
|         roles = instance.target_role.all() | ||||
|         # You can do what ever you want in here | ||||
|         # `parsed_query` param is passed to BookSerializer to allow further querying | ||||
|         from dvadmin.system.views.role import RoleSerializer | ||||
|         serializer = RoleSerializer( | ||||
|             roles, | ||||
|             many=True, | ||||
|             parsed_query=parsed_query | ||||
|         ) | ||||
|         return serializer.data | ||||
|  | ||||
|     def get_user_info(self, instance, parsed_query): | ||||
|         if instance.target_type  in (1,2,3): | ||||
|             return [] | ||||
|         users = instance.target_user.all() | ||||
|         # You can do what ever you want in here | ||||
|         # `parsed_query` param is passed to BookSerializer to allow further querying | ||||
|         from dvadmin.system.views.user import UserSerializer | ||||
|         serializer = UserSerializer( | ||||
|             users, | ||||
|             many=True, | ||||
|             parsed_query=parsed_query | ||||
|         ) | ||||
|         return serializer.data | ||||
|  | ||||
|     def get_dept_info(self, instance, parsed_query): | ||||
|         dept = instance.target_dept.all() | ||||
|         # You can do what ever you want in here | ||||
|         # `parsed_query` param is passed to BookSerializer to allow further querying | ||||
|         from dvadmin.system.views.dept import DeptSerializer | ||||
|         serializer = DeptSerializer( | ||||
|             dept, | ||||
|             many=True, | ||||
|             parsed_query=parsed_query | ||||
|         ) | ||||
|         return serializer.data | ||||
|  | ||||
|     class Meta: | ||||
|         model = MessageCenter | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| def websocket_push(user_id, message): | ||||
|     """ | ||||
|     主动推送消息 | ||||
|     """ | ||||
|     username = "user_" + str(user_id) | ||||
|     channel_layer = get_channel_layer() | ||||
|     async_to_sync(channel_layer.group_send)( | ||||
|         username, | ||||
|         { | ||||
|             "type": "push.message", | ||||
|             "json": message | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class MessageCenterCreateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     消息中心-新增-序列化器 | ||||
|     """ | ||||
|  | ||||
|     def save(self, **kwargs): | ||||
|         data = super().save(**kwargs) | ||||
|         initial_data = self.initial_data | ||||
|         target_type = initial_data.get('target_type') | ||||
|         # 在保存之前,根据目标类型,把目标用户查询出来并保存 | ||||
|         users = initial_data.get('target_user', []) | ||||
|         if target_type in [1]:  # 按角色 | ||||
|             target_role = initial_data.get('target_role', []) | ||||
|             users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True) | ||||
|         if target_type in [2]:  # 按部门 | ||||
|             target_dept = initial_data.get('target_dept', []) | ||||
|             users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True) | ||||
|         if target_type in [3]:  # 系统通知 | ||||
|             users = Users.objects.values_list('id', flat=True) | ||||
|         targetuser_data = [] | ||||
|         for user in users: | ||||
|             targetuser_data.append({ | ||||
|                 "messagecenter": data.id, | ||||
|                 "users": user | ||||
|             }) | ||||
|         targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request) | ||||
|         targetuser_instance.is_valid(raise_exception=True) | ||||
|         targetuser_instance.save() | ||||
|         for user in users: | ||||
|             unread_count = MessageCenterTargetUser.objects.filter(users__id=user, is_read=False).count() | ||||
|             websocket_push(user, message={"sender": 'system', "contentType": 'SYSTEM', | ||||
|                                           "content": '您有一条新消息~', "unread": unread_count}) | ||||
|         return data | ||||
|  | ||||
|     class Meta: | ||||
|         model = MessageCenter | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class MessageCenterViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     消息中心接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = MessageCenter.objects.order_by('create_datetime') | ||||
|     serializer_class = MessageCenterSerializer | ||||
|     create_serializer_class = MessageCenterCreateSerializer | ||||
|     extra_filter_backends = [] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         if self.action == 'list': | ||||
|             return MessageCenter.objects.filter(creator=self.request.user.id).all() | ||||
|         return MessageCenter.objects.all() | ||||
|  | ||||
|     def retrieve(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         重写查看 | ||||
|         """ | ||||
|         pk = kwargs.get('pk') | ||||
|         user_id = self.request.user.id | ||||
|         queryset = MessageCenterTargetUser.objects.filter(users__id=user_id, messagecenter__id=pk).first() | ||||
|         if queryset: | ||||
|             queryset.is_read = True | ||||
|             queryset.save() | ||||
|         instance = self.get_object() | ||||
|         serializer = self.get_serializer(instance) | ||||
|         # 主动推送消息 | ||||
|         unread_count = MessageCenterTargetUser.objects.filter(users__id=user_id, is_read=False).count() | ||||
|         websocket_push(user_id, message={"sender": 'system', "contentType": 'TEXT', | ||||
|                                          "content": '您查看了一条消息~', "unread": unread_count}) | ||||
|         return DetailResponse(data=serializer.data, msg="获取成功") | ||||
|  | ||||
|     @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def get_self_receive(self, request): | ||||
|         """ | ||||
|         获取接收到的消息 | ||||
|         """ | ||||
|         self_user_id = self.request.user.id | ||||
|         # queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('-create_datetime') | ||||
|         queryset = MessageCenter.objects.filter(target_user__id=self_user_id) | ||||
|         # queryset = self.filter_queryset(queryset) | ||||
|         page = self.paginate_queryset(queryset) | ||||
|         if page is not None: | ||||
|             serializer = MessageCenterTargetUserListSerializer(page, many=True, request=request) | ||||
|             return self.get_paginated_response(serializer.data) | ||||
|         serializer = MessageCenterTargetUserListSerializer(queryset, many=True, request=request) | ||||
|         return SuccessResponse(data=serializer.data, msg="获取成功") | ||||
|  | ||||
|     @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def get_newest_msg(self, request): | ||||
|         """ | ||||
|         获取最新的一条消息 | ||||
|         """ | ||||
|         self_user_id = self.request.user.id | ||||
|         queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('create_datetime').last() | ||||
|         data = None | ||||
|         if queryset: | ||||
|             serializer = MessageCenterTargetUserListSerializer(queryset.messagecenter, many=False, request=request) | ||||
|             data = serializer.data | ||||
|         return DetailResponse(data=data, msg="获取成功") | ||||
							
								
								
									
										47
									
								
								backend/dvadmin/system/views/operation_log.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								backend/dvadmin/system/views/operation_log.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 李强 | ||||
| @contact: QQ:1206709430 | ||||
| @Created on: 2021/6/8 003 0:30 | ||||
| @Remark: 操作日志管理 | ||||
| """ | ||||
|  | ||||
| from dvadmin.system.models import OperationLog | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class OperationLogSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     日志-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = OperationLog | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class OperationLogCreateUpdateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     操作日志  创建/更新时的列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = OperationLog | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class OperationLogViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     操作日志接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = OperationLog.objects.order_by('-create_datetime') | ||||
|     serializer_class = OperationLogSerializer | ||||
|     # permission_classes = [] | ||||
							
								
								
									
										144
									
								
								backend/dvadmin/system/views/role.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								backend/dvadmin/system/views/role.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,144 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/3 003 0:30 | ||||
| @Remark: 角色管理 | ||||
| """ | ||||
| from rest_framework import serializers | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
|  | ||||
| from dvadmin.system.models import Role, Menu, MenuButton, Dept | ||||
| from dvadmin.system.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 | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class RoleSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     角色-序列化器 | ||||
|     """ | ||||
|     users = serializers.SerializerMethodField() | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_users(instance): | ||||
|         users = instance.users_set.exclude(id=1).values('id', 'name', 'dept__name') | ||||
|         return users | ||||
|  | ||||
|     class Meta: | ||||
|         model = Role | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class RoleCreateUpdateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     角色管理 创建/更新时的列化器 | ||||
|     """ | ||||
|     menu = MenuSerializer(many=True, read_only=True) | ||||
|     dept = DeptSerializer(many=True, read_only=True) | ||||
|     permission = MenuButtonSerializer(many=True, read_only=True) | ||||
|     key = serializers.CharField(max_length=50, | ||||
|                                 validators=[CustomUniqueValidator(queryset=Role.objects.all(), message="权限字符必须唯一")]) | ||||
|     name = serializers.CharField(max_length=50, validators=[CustomUniqueValidator(queryset=Role.objects.all())]) | ||||
|  | ||||
|     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 | ||||
|  | ||||
|     class Meta: | ||||
|         model = Role | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class MenuPermissionSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     菜单的按钮权限 | ||||
|     """ | ||||
|     menuPermission = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_menuPermission(self, instance): | ||||
|         is_superuser = self.request.user.is_superuser | ||||
|         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) | ||||
|         return serializer.data | ||||
|  | ||||
|     class Meta: | ||||
|         model = Menu | ||||
|         fields = ['id', 'parent', 'name', 'menuPermission'] | ||||
|  | ||||
|  | ||||
| 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:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = Role.objects.all() | ||||
|     serializer_class = RoleSerializer | ||||
|     create_serializer_class = RoleCreateUpdateSerializer | ||||
|     update_serializer_class = RoleCreateUpdateSerializer | ||||
|     search_fields = ['name', 'key'] | ||||
|  | ||||
|     @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) | ||||
|     def set_role_users(self, request, pk): | ||||
|         """ | ||||
|         设置 角色-用户 | ||||
|         :param request: | ||||
|         :return: | ||||
|         """ | ||||
|         data = request.data | ||||
|         direction = data.get('direction') | ||||
|         movedKeys = data.get('movedKeys') | ||||
|         role = Role.objects.get(pk=pk) | ||||
|         if direction == "left": | ||||
|             # left : 移除用户权限 | ||||
|             role.users_set.remove(*movedKeys) | ||||
|         else: | ||||
|             # right : 添加用户权限 | ||||
|             role.users_set.add(*movedKeys) | ||||
|         serializer = RoleSerializer(role) | ||||
|         return DetailResponse(data=serializer.data, msg="更新成功") | ||||
							
								
								
									
										86
									
								
								backend/dvadmin/system/views/role_menu.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								backend/dvadmin/system/views/role_menu.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,86 @@ | ||||
| # -*- 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 RoleMenuPermission, Menu, MenuButton | ||||
| from dvadmin.utils.json_response import DetailResponse, ErrorResponse | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class RoleMenuPermissionSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     菜单按钮-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = RoleMenuPermission | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class RoleMenuPermissionInitSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化菜单按钮-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = RoleMenuPermission | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
| class RoleMenuPermissionCreateUpdateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     初始化菜单按钮-序列化器 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = RoleMenuPermission | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class RoleMenuPermissionViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     菜单按钮接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = RoleMenuPermission.objects.all() | ||||
|     serializer_class = RoleMenuPermissionSerializer | ||||
|     create_serializer_class = RoleMenuPermissionCreateUpdateSerializer | ||||
|     update_serializer_class = RoleMenuPermissionCreateUpdateSerializer | ||||
|     extra_filter_class = [] | ||||
|  | ||||
|     @action(methods=['post'],detail=False) | ||||
|     def save_auth(self,request): | ||||
|         """ | ||||
|         保存页面菜单授权 | ||||
|         :param request: | ||||
|         :return: | ||||
|         """ | ||||
|         body = request.data | ||||
|         role_id = body.get('role',None) | ||||
|         if role_id is None: | ||||
|             return ErrorResponse(msg="未获取到角色参数") | ||||
|         menu_list = body.get('menu',None) | ||||
|         if menu_list is None: | ||||
|             return ErrorResponse(msg="未获取到菜单参数") | ||||
|         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() | ||||
|             return DetailResponse(msg="保存成功",data=serializer.data) | ||||
							
								
								
									
										282
									
								
								backend/dvadmin/system/views/role_menu_button_permission.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								backend/dvadmin/system/views/role_menu_button_permission.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,282 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/3 003 0:30 | ||||
| @Remark: 菜单按钮管理 | ||||
| """ | ||||
| from rest_framework import serializers | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
|  | ||||
| from dvadmin.system.models import RoleMenuButtonPermission, Menu, Dept, MenuButton, RoleMenuPermission, \ | ||||
|     MenuField, FieldPermission | ||||
| from dvadmin.utils.json_response import DetailResponse | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class RoleMenuButtonPermissionSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     角色-菜单-按钮-权限 查询序列化 | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = RoleMenuButtonPermission | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     角色-菜单-按钮-权限 创建/修改序列化 | ||||
|     """ | ||||
|     menu_button__name = serializers.CharField(source='menu_button.name', read_only=True) | ||||
|     menu_button__value = serializers.CharField(source='menu_button.value', read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = RoleMenuButtonPermission | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class RoleMenuSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     角色-菜单 序列化 | ||||
|     """ | ||||
|     isCheck = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_isCheck(self, instance): | ||||
|         params = self.request.query_params | ||||
|         data = self.request.data | ||||
|         return RoleMenuPermission.objects.filter( | ||||
|             menu_id=instance.id, | ||||
|             role_id=params.get('roleId', data.get('roleId')), | ||||
|         ).exists() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Menu | ||||
|         fields = ["id", "name", "parent", "is_catalog", "isCheck"] | ||||
|  | ||||
|  | ||||
| class RoleMenuButtonSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     角色-菜单-按钮 序列化 | ||||
|     """ | ||||
|     isCheck = serializers.SerializerMethodField() | ||||
|     data_range = serializers.SerializerMethodField() | ||||
|     role_menu_btn_perm_id = serializers.SerializerMethodField() | ||||
|     dept = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_isCheck(self, instance): | ||||
|         params = self.request.query_params | ||||
|         data = self.request.data | ||||
|         return RoleMenuButtonPermission.objects.filter( | ||||
|             menu_button_id=instance.id, | ||||
|             role_id=params.get('roleId', data.get('roleId')), | ||||
|         ).exists() | ||||
|  | ||||
|     def get_data_range(self, instance): | ||||
|         obj = self.get_role_menu_btn_prem(instance) | ||||
|         if obj is None: | ||||
|             return None | ||||
|         return obj.data_range | ||||
|  | ||||
|     def get_role_menu_btn_perm_id(self, instance): | ||||
|         obj = self.get_role_menu_btn_prem(instance) | ||||
|         if obj is None: | ||||
|             return None | ||||
|         return obj.id | ||||
|  | ||||
|     def get_dept(self, instance): | ||||
|         obj = self.get_role_menu_btn_prem(instance) | ||||
|         if obj is None: | ||||
|             return None | ||||
|         return obj.dept.all().values_list('id', flat=True) | ||||
|  | ||||
|     def get_role_menu_btn_prem(self, instance): | ||||
|         params = self.request.query_params | ||||
|         data = self.request.data | ||||
|         obj = RoleMenuButtonPermission.objects.filter( | ||||
|             menu_button_id=instance.id, | ||||
|             role_id=params.get('roleId', data.get('roleId')), | ||||
|         ).first() | ||||
|         return obj | ||||
|  | ||||
|     class Meta: | ||||
|         model = MenuButton | ||||
|         fields = ['id', 'menu', 'name', 'isCheck', 'data_range', 'role_menu_btn_perm_id', 'dept'] | ||||
|  | ||||
|  | ||||
| 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('roleId')).first() | ||||
|         if queryset: | ||||
|             return queryset.is_query | ||||
|         return False | ||||
|  | ||||
|     def get_is_create(self, instance): | ||||
|         params = self.request.query_params | ||||
|         queryset = instance.menu_field.filter(role=params.get('roleId')).first() | ||||
|         if queryset: | ||||
|             return queryset.is_create | ||||
|         return False | ||||
|  | ||||
|     def get_is_update(self, instance): | ||||
|         params = self.request.query_params | ||||
|         queryset = instance.menu_field.filter(role=params.get('roleId')).first() | ||||
|         if queryset: | ||||
|             return queryset.is_update | ||||
|         return False | ||||
|  | ||||
|     class Meta: | ||||
|         model = MenuField | ||||
|         fields = ['id', 'field_name', 'title', 'is_query', 'is_create', 'is_update'] | ||||
|  | ||||
|  | ||||
| class RoleMenuButtonPermissionViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     菜单按钮接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|     queryset = RoleMenuButtonPermission.objects.all() | ||||
|     serializer_class = RoleMenuButtonPermissionSerializer | ||||
|     create_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer | ||||
|     update_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer | ||||
|     extra_filter_class = [] | ||||
|  | ||||
|     @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def get_role_menu(self, request): | ||||
|         """ | ||||
|         获取 角色-菜单 | ||||
|         :param request: | ||||
|         :return: | ||||
|         """ | ||||
|         menu_queryset = Menu.objects.all() | ||||
|         serializer = RoleMenuSerializer(menu_queryset, many=True, request=request) | ||||
|         return DetailResponse(data=serializer.data) | ||||
|  | ||||
|     @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def set_role_menu(self, request): | ||||
|         """ | ||||
|         设置 角色-菜单 | ||||
|         :param request: | ||||
|         :return: | ||||
|         """ | ||||
|         data = request.data | ||||
|         roleId = data.get('roleId') | ||||
|         menuId = data.get('menuId') | ||||
|         isCheck = data.get('isCheck') | ||||
|         if isCheck: | ||||
|             # 添加权限:创建关联记录 | ||||
|             instance = RoleMenuPermission.objects.create(role_id=roleId, menu_id=menuId) | ||||
|         else: | ||||
|             # 删除权限:移除关联记录 | ||||
|             RoleMenuPermission.objects.filter(role_id=roleId, menu_id=menuId).delete() | ||||
|         menu_instance = Menu.objects.get(id=menuId) | ||||
|         serializer = RoleMenuSerializer(menu_instance, request=request) | ||||
|         return DetailResponse(data=serializer.data, msg="更新成功") | ||||
|  | ||||
|     @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def get_role_menu_btn_field(self, request): | ||||
|         """ | ||||
|         获取 角色-菜单-按钮-列字段 | ||||
|         :param request: | ||||
|         :return: | ||||
|         """ | ||||
|         params = request.query_params | ||||
|         menuId = params.get('menuId', None) | ||||
|         menu_btn_queryset = MenuButton.objects.filter(menu_id=menuId) | ||||
|         menu_btn_serializer = RoleMenuButtonSerializer(menu_btn_queryset, many=True, request=request) | ||||
|         menu_field_queryset = MenuField.objects.filter(menu_id=menuId) | ||||
|         menu_field_serializer = RoleMenuFieldSerializer(menu_field_queryset, many=True, request=request) | ||||
|         return DetailResponse(data={'menu_btn': menu_btn_serializer.data, 'menu_field': menu_field_serializer.data}) | ||||
|  | ||||
|     @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) | ||||
|     def set_role_menu_field(self, request, pk): | ||||
|         """ | ||||
|         设置 角色-菜单-列字段 | ||||
|         """ | ||||
|         data = request.data | ||||
|         for col in data: | ||||
|             FieldPermission.objects.update_or_create( | ||||
|                 role_id=pk, field_id=col.get('id'), | ||||
|                 defaults={ | ||||
|                     'is_create': col.get('is_create'), | ||||
|                     'is_update': col.get('is_update'), | ||||
|                     'is_query': col.get('is_query'), | ||||
|                 }) | ||||
|  | ||||
|         return DetailResponse(data=[], msg="更新成功") | ||||
|  | ||||
|     @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def set_role_menu_btn(self, request): | ||||
|         """ | ||||
|         设置 角色-菜单-按钮 | ||||
|         """ | ||||
|         data = request.data | ||||
|         isCheck = data.get('isCheck', None) | ||||
|         roleId = data.get('roleId', None) | ||||
|         btnId = data.get('btnId', None) | ||||
|         if isCheck: | ||||
|             # 添加权限:创建关联记录 | ||||
|             RoleMenuButtonPermission.objects.create(role_id=roleId, menu_button_id=btnId) | ||||
|         else: | ||||
|             # 删除权限:移除关联记录 | ||||
|             RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete() | ||||
|         menu_btn_instance = MenuButton.objects.get(id=btnId) | ||||
|         serializer = RoleMenuButtonSerializer(menu_btn_instance, request=request) | ||||
|         return DetailResponse(data=serializer.data, msg="更新成功") | ||||
|  | ||||
|     @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def set_role_menu_btn_data_range(self, request): | ||||
|         """ | ||||
|         设置 角色-菜单-按钮-权限 | ||||
|         """ | ||||
|         data = request.data | ||||
|         instance = RoleMenuButtonPermission.objects.get(id=data.get('role_menu_btn_perm_id')) | ||||
|         instance.data_range = data.get('data_range') | ||||
|         instance.dept.add(*data.get('dept')) | ||||
|         if not data.get('dept'): | ||||
|             instance.dept.clear() | ||||
|         instance.save() | ||||
|         serializer = RoleMenuButtonPermissionSerializer(instance, request=request) | ||||
|         return DetailResponse(data=serializer.data, msg="更新成功") | ||||
|  | ||||
|     @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def role_to_dept_all(self, request): | ||||
|         """ | ||||
|         当前用户角色下所能授权的部门:角色授权页面使用 | ||||
|         :param request: | ||||
|         :return: | ||||
|         """ | ||||
|         is_superuser = request.user.is_superuser | ||||
|         params = request.query_params | ||||
|         # 当前登录用户的角色 | ||||
|         role_list = request.user.role.values_list('id', flat=True) | ||||
|  | ||||
|         menu_button_id = params.get('menu_button') | ||||
|         # 当前登录用户角色可以分配的自定义部门权限 | ||||
|         dept_checked_disabled = RoleMenuButtonPermission.objects.filter( | ||||
|             role_id__in=role_list, menu_button_id=menu_button_id | ||||
|         ).values_list('dept', flat=True) | ||||
|         dept_list = Dept.objects.values('id', 'name', 'parent') | ||||
|  | ||||
|         data = [] | ||||
|         for dept in dept_list: | ||||
|             dept["disabled"] = False if is_superuser else dept["id"] not in dept_checked_disabled | ||||
|             data.append(dept) | ||||
|         return DetailResponse(data=data) | ||||
							
								
								
									
										234
									
								
								backend/dvadmin/system/views/system_config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								backend/dvadmin/system/views/system_config.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,234 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2022/1/21 003 0:30 | ||||
| @Remark: 系统配置 | ||||
| """ | ||||
| import django_filters | ||||
| from django.db.models import Q | ||||
| from django_filters.rest_framework import BooleanFilter | ||||
| from rest_framework import serializers | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from application import dispatch | ||||
| from dvadmin.system.models import SystemConfig | ||||
| from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse | ||||
| from dvadmin.utils.models import get_all_models_objects | ||||
| from dvadmin.utils.serializers import CustomModelSerializer | ||||
| from dvadmin.utils.validator import CustomValidationError | ||||
| from dvadmin.utils.viewset import CustomModelViewSet | ||||
|  | ||||
|  | ||||
| class SystemConfigCreateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     系统配置-新增时使用-序列化器 | ||||
|     """ | ||||
|     form_item_type_label = serializers.CharField(source='get_form_item_type_display', read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = SystemConfig | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|     def validate_key(self, value): | ||||
|         """ | ||||
|         验证key是否允许重复 | ||||
|         parent为空时不允许重复,反之允许 | ||||
|         """ | ||||
|         instance = SystemConfig.objects.filter(key=value, parent__isnull=True).exists() | ||||
|         if instance: | ||||
|             raise CustomValidationError('已存在相同变量名') | ||||
|         return value | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class SystemConfigSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     系统配置-序列化器 | ||||
|     """ | ||||
|     form_item_type_label = serializers.CharField(source='get_form_item_type_display', read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = SystemConfig | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class SystemConfigChinldernSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     系统配置子级-序列化器 | ||||
|     """ | ||||
|     children = serializers.SerializerMethodField() | ||||
|     form_item_type_label = serializers.CharField(source='get_form_item_type_display', read_only=True) | ||||
|  | ||||
|     def get_children(self, instance): | ||||
|         queryset = SystemConfig.objects.filter(parent=instance) | ||||
|         serializer = SystemConfigSerializer(queryset, many=True) | ||||
|         return serializer.data | ||||
|  | ||||
|     class Meta: | ||||
|         model = SystemConfig | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class SystemConfigListSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     系统配置下模块的保存-序列化器 | ||||
|     """ | ||||
|  | ||||
|     def update(self, instance, validated_data): | ||||
|         instance_mapping = {obj.id: obj for obj in instance} | ||||
|         data_mapping = {item['id']: item for item in validated_data} | ||||
|         for obj_id, data in data_mapping.items(): | ||||
|             instance_obj = instance_mapping.get(obj_id, None) | ||||
|             if instance_obj is None: | ||||
|                 return SystemConfig.objects.create(**data) | ||||
|             else: | ||||
|                 return instance_obj.objects.update(**data) | ||||
|  | ||||
|     class Meta: | ||||
|         model = SystemConfig | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|  | ||||
|  | ||||
| class SystemConfigSaveSerializer(serializers.Serializer): | ||||
|     class Meta: | ||||
|         read_only_fields = ["id"] | ||||
|         list_serializer_class = SystemConfigListSerializer | ||||
|  | ||||
|  | ||||
| class SystemConfigFilter(django_filters.rest_framework.FilterSet): | ||||
|     """ | ||||
|     过滤器 | ||||
|     """ | ||||
|     parent__isnull = BooleanFilter(field_name='parent', lookup_expr="isnull") | ||||
|  | ||||
|     class Meta: | ||||
|         model = SystemConfig | ||||
|         fields = ['id', 'parent', 'status', 'parent__isnull'] | ||||
|  | ||||
|  | ||||
| class SystemConfigViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     系统配置接口 | ||||
|     """ | ||||
|     queryset = SystemConfig.objects.order_by('sort', 'create_datetime') | ||||
|     serializer_class = SystemConfigChinldernSerializer | ||||
|     create_serializer_class = SystemConfigCreateSerializer | ||||
|     retrieve_serializer_class = SystemConfigChinldernSerializer | ||||
|     # filter_fields = ['id','parent'] | ||||
|     filter_class = SystemConfigFilter | ||||
|  | ||||
|     def save_content(self, request): | ||||
|         body = request.data | ||||
|         data_mapping = {item['id']: item for item in body} | ||||
|         for obj_id, data in data_mapping.items(): | ||||
|             instance_obj = SystemConfig.objects.filter(id=obj_id).first() | ||||
|             if instance_obj is None: | ||||
|                 # return SystemConfig.objects.create(**data) | ||||
|                 serializer = SystemConfigCreateSerializer(data=data) | ||||
|             else: | ||||
|                 serializer = SystemConfigCreateSerializer(instance_obj, data=data) | ||||
|             if serializer.is_valid(raise_exception=True): | ||||
|                 serializer.save() | ||||
|         return DetailResponse(msg="保存成功") | ||||
|  | ||||
|     def get_association_table(self, request): | ||||
|         """ | ||||
|         获取所有的model及字段信息 | ||||
|         """ | ||||
|         res = [ele.get('table') for ele in get_all_models_objects().values()] | ||||
|         return DetailResponse(msg="获取成功", data=res) | ||||
|  | ||||
|     def get_table_data(self, request, pk): | ||||
|         """ | ||||
|         动态获取关联表的数据 | ||||
|         """ | ||||
|         instance = SystemConfig.objects.filter(id=pk).first() | ||||
|         if instance is None: | ||||
|             return ErrorResponse(msg="查询出错了~") | ||||
|         setting = instance.setting | ||||
|         if setting is None: | ||||
|             return ErrorResponse(msg="查询出错了~") | ||||
|         table = setting.get('table')  # 获取model名 | ||||
|         model = get_all_models_objects(table).get("object", {}) | ||||
|         # 自己判断一下不存在 | ||||
|         queryset = model.objects.values() | ||||
|         body = request.query_params | ||||
|         search_value = body.get('search', None) | ||||
|         if search_value: | ||||
|             search_fields = setting.get('searchField') | ||||
|             filters = Q() | ||||
|             filters.connector = 'OR' | ||||
|             for item in search_fields: | ||||
|                 filed = '{0}__icontains'.format(item.get('field')) | ||||
|                 filters.children.append((filed, search_value)) | ||||
|             queryset = model.objects.filter(filters).values() | ||||
|         page = self.paginate_queryset(queryset) | ||||
|         if page is not None: | ||||
|             return self.get_paginated_response(queryset) | ||||
|         return SuccessResponse(msg="获取成功", data=queryset, total=len(queryset)) | ||||
|  | ||||
|     def get_relation_info(self, request): | ||||
|         """ | ||||
|         查询关联的模板信息 | ||||
|         """ | ||||
|         body = request.query_params | ||||
|         var_name = body.get('varName', None) | ||||
|         table = body.get('table', None) | ||||
|         instance = SystemConfig.objects.filter(key=var_name, setting__table=table).first() | ||||
|         if instance is None: | ||||
|             return ErrorResponse(msg="未获取到关联信息") | ||||
|         relation_id = body.get('relationIds', None) | ||||
|         relationIds = [] | ||||
|         if relation_id is None: | ||||
|             return ErrorResponse(msg="未获取到关联信息") | ||||
|         if instance.form_item_type in [13]: | ||||
|             relationIds = [relation_id] | ||||
|         elif instance.form_item_type in [14]: | ||||
|             relationIds = relation_id.split(',') | ||||
|         queryset = SystemConfig.objects.filter(value__in=relationIds).first() | ||||
|         if queryset is None: | ||||
|             return ErrorResponse(msg="未获取到关联信息") | ||||
|         serializer = SystemConfigChinldernSerializer(queryset.parent) | ||||
|         return DetailResponse(msg="查询成功", data=serializer.data) | ||||
|  | ||||
|  | ||||
| class InitSettingsViewSet(APIView): | ||||
|     """ | ||||
|     获取初始化配置 | ||||
|     """ | ||||
|     authentication_classes = [] | ||||
|     permission_classes = [] | ||||
|  | ||||
|     def filter_system_config_values(self, data: dict): | ||||
|         """ | ||||
|         过滤系统初始化配置 | ||||
|         :param data: | ||||
|         :return: | ||||
|         """ | ||||
|         if not self.request.query_params.get('key', ''): | ||||
|             return data | ||||
|         new_data = {} | ||||
|         for key in self.request.query_params.get('key', '').split('|'): | ||||
|             if key: | ||||
|                 new_data.update(**dict(filter(lambda x: x[0].startswith(key), data.items()))) | ||||
|         return new_data | ||||
|  | ||||
|     def get(self, request): | ||||
|         data = dispatch.get_system_config() | ||||
|         if not data: | ||||
|             dispatch.refresh_system_config() | ||||
|             data = dispatch.get_system_config() | ||||
|         # 不返回后端专用配置 | ||||
|         backend_config = [f"{ele.get('parent__key')}.{ele.get('key')}" for ele in | ||||
|                           SystemConfig.objects.filter(status=False, parent_id__isnull=False).values('parent__key', | ||||
|                                                                                                     'key')] | ||||
|         data = dict(filter(lambda x: x[0] not in backend_config, data.items())) | ||||
|         data = self.filter_system_config_values(data=data) | ||||
|         return DetailResponse(data=data) | ||||
							
								
								
									
										506
									
								
								backend/dvadmin/system/views/user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								backend/dvadmin/system/views/user.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,506 @@ | ||||
| import hashlib | ||||
|  | ||||
| from django.contrib.auth.hashers import make_password, check_password | ||||
| from django_restql.fields import DynamicSerializerMethodField | ||||
| from rest_framework import serializers | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from django.db import connection | ||||
| from django.db.models import Q | ||||
| from dvadmin.system.models import Users, Role, Dept | ||||
| from dvadmin.system.views.role import RoleSerializer | ||||
| 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 | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.response import Response | ||||
| from rest_framework import status | ||||
| from application import dispatch | ||||
|  | ||||
| from rest_framework.decorators import api_view, permission_classes,renderer_classes | ||||
| from django.db import IntegrityError | ||||
|  | ||||
| from rest_framework.permissions import AllowAny | ||||
| from rest_framework.renderers import JSONRenderer | ||||
|  | ||||
| def recursion(instance, parent, result): | ||||
|     new_instance = getattr(instance, parent, None) | ||||
|     res = [] | ||||
|     data = getattr(instance, result, None) | ||||
|     if data: | ||||
|         res.append(data) | ||||
|     if new_instance: | ||||
|         array = recursion(new_instance, parent, result) | ||||
|         res += array | ||||
|     return res | ||||
|  | ||||
|  | ||||
| class UserSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     用户管理-序列化器 | ||||
|     """ | ||||
|     dept_name = serializers.CharField(source='dept.name', read_only=True) | ||||
|     role_info = DynamicSerializerMethodField() | ||||
|     dept_name_all = serializers.SerializerMethodField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Users | ||||
|         read_only_fields = ["id"] | ||||
|         exclude = ["password"] | ||||
|         extra_kwargs = { | ||||
|             "post": {"required": False}, | ||||
|             "mobile": {"required": False}, | ||||
|         } | ||||
|  | ||||
|     def get_dept_name_all(self, instance): | ||||
|         dept_name_all = recursion(instance.dept, "parent", "name") | ||||
|         dept_name_all.reverse() | ||||
|         return "/".join(dept_name_all) | ||||
|  | ||||
|     def get_role_info(self, instance, parsed_query): | ||||
|         roles = instance.role.all() | ||||
|         # You can do what ever you want in here | ||||
|         # `parsed_query` param is passed to BookSerializer to allow further querying | ||||
|         serializer = RoleSerializer( | ||||
|             roles, | ||||
|             many=True, | ||||
|             parsed_query=parsed_query | ||||
|         ) | ||||
|         return serializer.data | ||||
|  | ||||
|  | ||||
| class UserCreateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     用户新增-序列化器 | ||||
|     """ | ||||
|  | ||||
|     username = serializers.CharField( | ||||
|         max_length=50, | ||||
|         validators=[ | ||||
|             CustomUniqueValidator(queryset=Users.objects.all(), message="账号必须唯一") | ||||
|         ], | ||||
|     ) | ||||
|     password = serializers.CharField( | ||||
|         required=False, | ||||
|     ) | ||||
|  | ||||
|     def validate_password(self, value): | ||||
|         """ | ||||
|         对密码进行验证 | ||||
|         """ | ||||
|         md5 = hashlib.md5() | ||||
|         md5.update(value.encode('utf-8')) | ||||
|         md5_password = md5.hexdigest() | ||||
|         return make_password(md5_password) | ||||
|  | ||||
|     def save(self, **kwargs): | ||||
|         data = super().save(**kwargs) | ||||
|         data.dept_belong_id = data.dept_id | ||||
|         data.save() | ||||
|         data.post.set(self.initial_data.get("post", [])) | ||||
|         return data | ||||
|  | ||||
|     class Meta: | ||||
|         model = Users | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ["id"] | ||||
|         extra_kwargs = { | ||||
|             "post": {"required": False}, | ||||
|             "mobile": {"required": False}, | ||||
|         } | ||||
|  | ||||
|  | ||||
| class UserUpdateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     用户修改-序列化器 | ||||
|     """ | ||||
|  | ||||
|     username = serializers.CharField( | ||||
|         max_length=50, | ||||
|         validators=[ | ||||
|             CustomUniqueValidator(queryset=Users.objects.all(), message="账号必须唯一") | ||||
|         ], | ||||
|     ) | ||||
|  | ||||
|     def validate_is_active(self, value): | ||||
|         """ | ||||
|         更改激活状态 | ||||
|         """ | ||||
|         if value: | ||||
|             self.initial_data["login_error_count"] = 0 | ||||
|         return value | ||||
|  | ||||
|     def save(self, **kwargs): | ||||
|         data = super().save(**kwargs) | ||||
|         data.dept_belong_id = data.dept_id | ||||
|         data.save() | ||||
|         data.post.set(self.initial_data.get("post", [])) | ||||
|         return data | ||||
|  | ||||
|     class Meta: | ||||
|         model = Users | ||||
|         read_only_fields = ["id", "password"] | ||||
|         fields = "__all__" | ||||
|         extra_kwargs = { | ||||
|             "post": {"required": False, "read_only": True}, | ||||
|             "mobile": {"required": False}, | ||||
|         } | ||||
|  | ||||
|  | ||||
| class UserInfoUpdateSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     用户修改-序列化器 | ||||
|     """ | ||||
|     mobile = serializers.CharField( | ||||
|         max_length=50, | ||||
|         validators=[ | ||||
|             CustomUniqueValidator(queryset=Users.objects.all(), message="手机号必须唯一") | ||||
|         ], | ||||
|         allow_blank=True | ||||
|     ) | ||||
|  | ||||
|     def update(self, instance, validated_data): | ||||
|         return super().update(instance, validated_data) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Users | ||||
|         fields = ['email', 'mobile', 'avatar', 'name', 'gender'] | ||||
|         extra_kwargs = { | ||||
|             "post": {"required": False, "read_only": True}, | ||||
|             "mobile": {"required": False}, | ||||
|         } | ||||
|  | ||||
|  | ||||
| class ExportUserProfileSerializer(CustomModelSerializer): | ||||
|     """ | ||||
|     用户导出 序列化器 | ||||
|     """ | ||||
|  | ||||
|     last_login = serializers.DateTimeField( | ||||
|         format="%Y-%m-%d %H:%M:%S", required=False, read_only=True | ||||
|     ) | ||||
|     is_active = serializers.SerializerMethodField(read_only=True) | ||||
|     dept_name = serializers.CharField(source="dept.name", default="") | ||||
|     dept_owner = serializers.CharField(source="dept.owner", default="") | ||||
|     gender = serializers.CharField(source="get_gender_display", read_only=True) | ||||
|  | ||||
|     def get_is_active(self, instance): | ||||
|         return "启用" if instance.is_active else "停用" | ||||
|  | ||||
|     class Meta: | ||||
|         model = Users | ||||
|         fields = ( | ||||
|             "username", | ||||
|             "name", | ||||
|             "email", | ||||
|             "mobile", | ||||
|             "gender", | ||||
|             "is_active", | ||||
|             "last_login", | ||||
|             "dept_name", | ||||
|             "dept_owner", | ||||
|         ) | ||||
|  | ||||
|  | ||||
| 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", "admin123456")).encode(encoding="UTF-8") | ||||
|         ).hexdigest() | ||||
|         data.set_password(password) | ||||
|         data.save() | ||||
|         return data | ||||
|  | ||||
|     class Meta: | ||||
|         model = Users | ||||
|         exclude = ( | ||||
|             "post", | ||||
|             "user_permissions", | ||||
|             "groups", | ||||
|             "is_superuser", | ||||
|             "date_joined", | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class UserViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     用户接口 | ||||
|     list:查询 | ||||
|     create:新增 | ||||
|     update:修改 | ||||
|     retrieve:单例 | ||||
|     destroy:删除 | ||||
|     """ | ||||
|  | ||||
|     queryset = Users.objects.exclude(is_superuser=1).all() | ||||
|     serializer_class = UserSerializer | ||||
|     create_serializer_class = UserCreateSerializer | ||||
|     update_serializer_class = UserUpdateSerializer | ||||
|     filter_fields = ["name", "username", "gender", "is_active", "dept", "user_type"] | ||||
|     search_fields = ["username", "name", "dept__name", "role__name"] | ||||
|     # 导出 | ||||
|     export_field_label = { | ||||
|         "username": "用户账号", | ||||
|         "name": "用户名称", | ||||
|         "email": "用户邮箱", | ||||
|         "mobile": "手机号码", | ||||
|         "gender": "用户性别", | ||||
|         "is_active": "帐号状态", | ||||
|         "last_login": "最后登录时间", | ||||
|         "dept_name": "部门名称", | ||||
|         "dept_owner": "部门负责人", | ||||
|     } | ||||
|     export_serializer_class = ExportUserProfileSerializer | ||||
|     # 导入 | ||||
|     import_serializer_class = UserProfileImportSerializer | ||||
|     import_field_dict = { | ||||
|         "username": "登录账号", | ||||
|         "name": "用户名称", | ||||
|         "email": "用户邮箱", | ||||
|         "mobile": "手机号码", | ||||
|         "gender": { | ||||
|             "title": "用户性别", | ||||
|             "choices": { | ||||
|                 "data": {"未知": 2, "男": 1, "女": 0}, | ||||
|             } | ||||
|         }, | ||||
|         "is_active": { | ||||
|             "title": "帐号状态", | ||||
|             "choices": { | ||||
|                 "data": {"启用": True, "禁用": False}, | ||||
|             } | ||||
|         }, | ||||
|         "dept": {"title": "部门", "choices": {"queryset": Dept.objects.filter(status=True), "values_name": "name"}}, | ||||
|         "role": {"title": "角色", "choices": {"queryset": Role.objects.filter(status=True), "values_name": "name"}}, | ||||
|     } | ||||
|  | ||||
|     @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def user_info(self, request): | ||||
|         """获取当前用户信息""" | ||||
|         user = request.user | ||||
|         result = { | ||||
|             "id": user.id, | ||||
|             "username": user.username, | ||||
|             "name": user.name, | ||||
|             "mobile": user.mobile, | ||||
|             "user_type": user.user_type, | ||||
|             "gender": user.gender, | ||||
|             "email": user.email, | ||||
|             "avatar": user.avatar, | ||||
|             "dept": user.dept_id, | ||||
|             "is_superuser": user.is_superuser, | ||||
|             "role": user.role.values_list('id', flat=True), | ||||
|             "pwd_change_count":user.pwd_change_count, | ||||
|             # 增加自定义字段 | ||||
|             # 剩余翻译字符数 | ||||
|             "user_remaining_chars":user.user_remaining_chars, | ||||
|             # 账户余额 | ||||
|             "user_balance":user.user_balance, | ||||
|             # API密钥 | ||||
|             "user_api_key":user.user_api_key, | ||||
|             # 账户状态 | ||||
|             "user_is_blocked":user.user_is_blocked, | ||||
|             # 计费类型 | ||||
|             "user_billing_type":user.user_billing_type, | ||||
|             # 授权端口数 | ||||
|             "user_max_ports":user.user_max_ports, | ||||
|             # 已用端口数 | ||||
|             "user_used_ports":user.user_used_ports | ||||
|         } | ||||
|         if hasattr(connection, 'tenant'): | ||||
|             result['tenant_id'] = connection.tenant and connection.tenant.id | ||||
|             result['tenant_name'] = connection.tenant and connection.tenant.name | ||||
|         dept = getattr(user, 'dept', None) | ||||
|         if dept: | ||||
|             result['dept_info'] = { | ||||
|                 'dept_id': dept.id, | ||||
|                 'dept_name': dept.name | ||||
|             } | ||||
|         else: | ||||
|             result['dept_info'] = { | ||||
|                 'dept_id': None, | ||||
|                 'dept_name': "暂无部门" | ||||
|             } | ||||
|         role = getattr(user, 'role', None) | ||||
|         if role: | ||||
|             result['role_info'] = role.values('id', 'name', 'key') | ||||
|         return DetailResponse(data=result, msg="获取成功") | ||||
|  | ||||
|     @action(methods=["PUT"], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def update_user_info(self, request): | ||||
|         """修改当前用户信息""" | ||||
|         serializer = UserInfoUpdateSerializer(request.user, data=request.data, request=request) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return DetailResponse(data=None, msg="修改成功") | ||||
|  | ||||
|     @action(methods=["PUT"], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def change_password(self, request, *args, **kwargs): | ||||
|         """密码修改""" | ||||
|         data = request.data | ||||
|         old_pwd = data.get("oldPassword") | ||||
|         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="两次密码不匹配") | ||||
|         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 not verify_password: | ||||
|                 old_pwd_md5 = hashlib.md5(old_pwd_md5.encode(encoding='UTF-8')).hexdigest() | ||||
|                 verify_password = check_password(str(old_pwd_md5), request.user.password) | ||||
|         if verify_password: | ||||
|             # request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()) | ||||
|             request.user.password = make_password(new_pwd) | ||||
|             request.user.pwd_change_count += 1 | ||||
|             request.user.save() | ||||
|             return DetailResponse(data=None, msg="修改成功") | ||||
|         else: | ||||
|             return ErrorResponse(msg="旧密码不正确") | ||||
|  | ||||
|     @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def login_change_password(self, request, *args, **kwargs): | ||||
|         """初次登录进行密码修改""" | ||||
|         data = request.data | ||||
|         new_pwd = data.get("password") | ||||
|         new_pwd2 = data.get("password_regain") | ||||
|         if new_pwd != new_pwd2: | ||||
|             return ErrorResponse(msg="两次密码不匹配") | ||||
|         else: | ||||
|             request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()) | ||||
|             request.user.pwd_change_count += 1 | ||||
|             request.user.save() | ||||
|             return DetailResponse(data=None, msg="修改成功") | ||||
|  | ||||
|     @action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated]) | ||||
|     def reset_to_default_password(self, request,pk): | ||||
|         """恢复默认密码""" | ||||
|         if not self.request.user.is_superuser: | ||||
|             return ErrorResponse(msg="只允许超级管理员对其进行密码重置") | ||||
|         instance = Users.objects.filter(id=pk).first() | ||||
|         if instance: | ||||
|             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: | ||||
|             return ErrorResponse(msg="未获取到用户") | ||||
|  | ||||
|     @action(methods=["PUT"], detail=True) | ||||
|     def reset_password(self, request, pk): | ||||
|         """ | ||||
|         密码重置 | ||||
|         """ | ||||
|         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") | ||||
|         new_pwd2 = data.get("newPassword2") | ||||
|         if instance: | ||||
|             if new_pwd != new_pwd2: | ||||
|                 return ErrorResponse(msg="两次密码不匹配") | ||||
|             else: | ||||
|                 instance.password = make_password(new_pwd) | ||||
|                 instance.save() | ||||
|                 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()) | ||||
|         # print(queryset.values('id','name','dept__id')) | ||||
|         page = self.paginate_queryset(queryset) | ||||
|         if page is not None: | ||||
|             serializer = self.get_serializer(page, many=True, request=request) | ||||
|             # print(serializer.data) | ||||
|             return self.get_paginated_response(serializer.data) | ||||
|         serializer = self.get_serializer(queryset, many=True, request=request) | ||||
|  | ||||
|         return SuccessResponse(data=serializer.data, msg="获取成功") | ||||
|  | ||||
|  | ||||
| @api_view(['POST']) | ||||
| @permission_classes([AllowAny]) | ||||
| @renderer_classes([JSONRenderer]) | ||||
| def create_sub(request): | ||||
|     print('here') | ||||
|     data = request.data | ||||
|     username = data.get('username') | ||||
|     password = data.get('password') | ||||
|     parent_id = data.get('parent_id') | ||||
|     if not username or not password or not parent_id: | ||||
|         return Response({'code': 400, 'msg': '参数缺失'}, status=400) | ||||
|     if Users.objects.filter(username=username).exists(): | ||||
|         return Response({'code': 409, 'msg': 'USERNAME_EXISTS'}, status=200) | ||||
|     try: | ||||
|         user = Users( | ||||
|             username=username, | ||||
|             parent_id=parent_id, | ||||
|         ) | ||||
|         user.set_password(password) | ||||
|         user.save() | ||||
|         return Response({'code': 200, 'msg': 'success'}) | ||||
|     except IntegrityError: | ||||
|         return Response({'code': 500, 'msg': 'CREATE_FAILED'}, status=500) | ||||
|  | ||||
| @api_view(['GET']) | ||||
| @permission_classes([AllowAny]) | ||||
| @renderer_classes([JSONRenderer]) | ||||
| def list_sub(request): | ||||
|     parent_id = request.GET.get('parentId') | ||||
|     if not parent_id: | ||||
|         return Response({'code': 400, 'msg': '参数缺失'}, status=400) | ||||
|     users = Users.objects.filter(parent_id=parent_id) | ||||
|     serializer = UserSerializer(users, many=True, request=request) | ||||
|     return SuccessResponse(data=serializer.data, msg="获取成功") | ||||
|  | ||||
| @api_view(['GET']) | ||||
| @permission_classes([AllowAny]) | ||||
| @renderer_classes([JSONRenderer]) | ||||
| def delete_sub(request, id): | ||||
|     try: | ||||
|         user = Users.objects.get(id=id) | ||||
|         user.delete() | ||||
|         return Response({'code': 200, 'msg': 'success'}) | ||||
|     except Users.DoesNotExist: | ||||
|         return Response({'code': 404, 'msg': 'NOT_FOUND'}, status=404) | ||||
							
								
								
									
										0
									
								
								backend/dvadmin/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/dvadmin/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										39
									
								
								backend/dvadmin/utils/backends.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								backend/dvadmin/utils/backends.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| import hashlib | ||||
| 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() | ||||
|  | ||||
|  | ||||
| class CustomBackend(ModelBackend): | ||||
|     """ | ||||
|     Django原生认证方式 | ||||
|     """ | ||||
|  | ||||
|     def authenticate(self, request, username=None, password=None, **kwargs): | ||||
|         msg = '%s 正在使用本地登录...' % username | ||||
|         logger.info(msg) | ||||
|         if username is None: | ||||
|             username = kwargs.get(UserModel.USERNAME_FIELD) | ||||
|         try: | ||||
|             user = UserModel._default_manager.get_by_natural_key(username) | ||||
|         except UserModel.DoesNotExist: | ||||
|             UserModel().set_password(password) | ||||
|         else: | ||||
|             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("当前用户已被禁用,请联系管理员!") | ||||
							
								
								
									
										89
									
								
								backend/dvadmin/utils/core_initialize.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								backend/dvadmin/utils/core_initialize.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | ||||
| # 初始化基类 | ||||
| import json | ||||
| import os | ||||
|  | ||||
| from django.apps import apps | ||||
| from rest_framework import request | ||||
|  | ||||
| from application import settings | ||||
| from dvadmin.system.models import Users | ||||
|  | ||||
|  | ||||
| class CoreInitialize: | ||||
|     """ | ||||
|     使用方法:继承此类,重写 run方法,在 run 中调用 save 进行数据初始化 | ||||
|     """ | ||||
|     creator_id = None | ||||
|     reset = False | ||||
|     request = request | ||||
|     file_path = None | ||||
|  | ||||
|     def __init__(self, reset=False, creator_id=None, app=None): | ||||
|         """ | ||||
|         reset: 是否重置初始化数据 | ||||
|         creator_id: 创建人id | ||||
|         """ | ||||
|         self.reset = reset or self.reset | ||||
|         self.creator_id = creator_id or self.creator_id | ||||
|         self.app = app or '' | ||||
|         self.request.user = Users.objects.order_by('create_datetime').first() | ||||
|  | ||||
|     def init_base(self, Serializer, unique_fields=None): | ||||
|         model = Serializer.Meta.model | ||||
|         path_file = os.path.join(apps.get_app_config(self.app.split('.')[-1]).path, 'fixtures', | ||||
|                                  f'init_{Serializer.Meta.model._meta.model_name}.json') | ||||
|         if not os.path.isfile(path_file): | ||||
|             print("文件不存在,跳过初始化") | ||||
|             return | ||||
|         with open(path_file,encoding="utf-8") as f: | ||||
|             for data in json.load(f): | ||||
|                 filter_data = {} | ||||
|                 # 配置过滤条件,如果有唯一标识字段则使用唯一标识字段,否则使用全部字段 | ||||
|                 if unique_fields: | ||||
|                     for field in unique_fields: | ||||
|                         if field in data: | ||||
|                             filter_data[field] = data[field] | ||||
|                 else: | ||||
|                     for key, value in data.items(): | ||||
|                         if isinstance(value, list) or value == None or value == '': | ||||
|                             continue | ||||
|                         filter_data[key] = value | ||||
|                 instance = model.objects.filter(**filter_data).first() | ||||
|                 data["reset"] = self.reset | ||||
|                 serializer = Serializer(instance, data=data, request=self.request) | ||||
|                 serializer.is_valid(raise_exception=True) | ||||
|                 serializer.save() | ||||
|         print(f"[{self.app}][{model._meta.model_name}]初始化完成") | ||||
|  | ||||
|     def save(self, obj, data: list, name=None, no_reset=False): | ||||
|         name = name or obj._meta.verbose_name | ||||
|         print(f"正在初始化[{obj._meta.label} => {name}]") | ||||
|         if not no_reset and self.reset and obj not in settings.INITIALIZE_RESET_LIST: | ||||
|             try: | ||||
|                 obj.objects.all().delete() | ||||
|                 settings.INITIALIZE_RESET_LIST.append(obj) | ||||
|             except Exception: | ||||
|                 pass | ||||
|         for ele in data: | ||||
|             m2m_dict = {} | ||||
|             new_data = {} | ||||
|             for key, value in ele.items(): | ||||
|                 # 判断传的 value 为 list 的多对多进行抽离,使用set 进行更新 | ||||
|                 if isinstance(value, list) and value and isinstance(value[0], int): | ||||
|                     m2m_dict[key] = value | ||||
|                 else: | ||||
|                     new_data[key] = value | ||||
|             object, _ = obj.objects.get_or_create(id=ele.get("id"), defaults=new_data) | ||||
|             for key, m2m in m2m_dict.items(): | ||||
|                 m2m = list(set(m2m)) | ||||
|                 if m2m and len(m2m) > 0 and m2m[0]: | ||||
|                     exec(f""" | ||||
| if object.{key}: | ||||
|     values_list = object.{key}.all().values_list('id', flat=True) | ||||
|     values_list = list(set(list(values_list) + {m2m})) | ||||
|     object.{key}.set(values_list) | ||||
| """) | ||||
|         print(f"初始化完成[{obj._meta.label} => {name}]") | ||||
|  | ||||
|     def run(self): | ||||
|         raise NotImplementedError('.run() must be overridden') | ||||
							
								
								
									
										155
									
								
								backend/dvadmin/utils/crud_mixin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								backend/dvadmin/utils/crud_mixin.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,155 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.permissions import AllowAny | ||||
|  | ||||
| from dvadmin.utils.json_response import DetailResponse | ||||
|  | ||||
|  | ||||
| class FastCrudMixin: | ||||
|     """ | ||||
|     定义快速CRUD数据操作的通用方法 | ||||
|     """ | ||||
|     # 需要CRUD的字段 | ||||
|     crud_fields = None | ||||
|     # 排除CRUD的字段 | ||||
|     exclude_fields = None | ||||
|     # 自定义CRUD的JSON | ||||
|     custom_crud_json = None | ||||
|     # 需要修改的CRUD键值对 | ||||
|     crud_update_key_value = None | ||||
|  | ||||
|     # 将Django的字段类型处理为JS类型 | ||||
|     def __handle_type(self, type): | ||||
|         if type in ['BigAutoField', 'CharField']: | ||||
|             return "input" | ||||
|         if type == 'DateTimeField': | ||||
|             return "datetime" | ||||
|         if type == 'DateField': | ||||
|             return "date" | ||||
|         if type == 'IntegerField': | ||||
|             return "number" | ||||
|         if type == 'BooleanField': | ||||
|             return "dict-switch" | ||||
|  | ||||
|     # 获取字段属性信息 | ||||
|     def __get_field_attribute(self): | ||||
|         result = [] | ||||
|         queryset = self.get_queryset() | ||||
|         __name = "" | ||||
|         __verbose_name = "" | ||||
|         __type = "text" | ||||
|         # 判断指定CRUD字段 | ||||
|         if self.crud_fields and type(self.crud_fields == list): | ||||
|             for item in self.crud_fields: | ||||
|                 try: | ||||
|                     field = queryset.model._meta.get_field(item) | ||||
|                     field_type = field.get_internal_type() | ||||
|                     __name = field.name | ||||
|                     # 判断类型是否为外键类型,外键类型需要特殊方式获取verbose_name | ||||
|                     if field_type in ['ForeignKey', 'OneToOneField', 'ManyToManyField']: | ||||
|                         continue | ||||
|                         # try: | ||||
|                         #     verbose_name = Users._meta.get_field(str(field.name)).verbose_name | ||||
|                         # except: | ||||
|                         #     pass | ||||
|                     else: | ||||
|                         __verbose_name = field.verbose_name | ||||
|                         __type = self.__handle_type(field_type) | ||||
|                 except: | ||||
|                     continue | ||||
|                 result.append({"key": __name, "title": __verbose_name, "type": __type}) | ||||
|         else: | ||||
|             # 获取model的所有字段及属性 | ||||
|             model_fields = queryset.model._meta.get_fields() | ||||
|             # 遍历所有字段属性 | ||||
|             for field in model_fields: | ||||
|                 field_type = field.get_internal_type() | ||||
|                 __name = field.name | ||||
|                 # 判断需要排除的CRUD字段 | ||||
|                 if self.exclude_fields and type(self.exclude_fields == list): | ||||
|                     if __name in self.exclude_fields: | ||||
|                         continue | ||||
|                 # 判断类型是否为外键类型,外键类型需要特殊方式获取verbose_name | ||||
|                 if field_type in ['ForeignKey', 'OneToOneField', 'ManyToManyField']: | ||||
|                     continue | ||||
|                     # try: | ||||
|                     #     verbose_name = Users._meta.get_field(str(field.name)).verbose_name | ||||
|                     # except: | ||||
|                     #     pass | ||||
|                 else: | ||||
|                     __verbose_name = field.verbose_name | ||||
|                     __type = self.__handle_type(field_type) | ||||
|                 result.append({"key": __name, "title": __verbose_name, "type": __type}) | ||||
|         return result | ||||
|  | ||||
|     #获取key | ||||
|     def __find_key(self,dct: dict, | ||||
|                  target_key: str, | ||||
|                  level: int = -1, | ||||
|                  index: int = -1) -> tuple: | ||||
|         """Find a key within a nested dictionary and return its level and index.""" | ||||
|         for k, v in dct.items(): | ||||
|             level += 1 | ||||
|             index += 1 | ||||
|             if k == target_key: | ||||
|                 return level, index | ||||
|             elif isinstance(v, list): | ||||
|                 for i, dct_ in enumerate(v): | ||||
|                     if isinstance(dct_, dict): | ||||
|                         result = self.__find_key(dct_, target_key) | ||||
|                         if result is not None: | ||||
|                             return result | ||||
|                     else: | ||||
|                         continue | ||||
|             elif isinstance(v, str) or isinstance(v, int) or isinstance(v, float): | ||||
|                 continue | ||||
|  | ||||
|     # 修改字典中key的value | ||||
|     def __update_nested_dict(self,nested_dict: dict, | ||||
|                            target_key: str, | ||||
|                            new_value) -> dict: | ||||
|         """Update a nested dictionary with a new value.""" | ||||
|         split_target_key = target_key.split('.') | ||||
|         if len(split_target_key) > 1: | ||||
|             new_dict = nested_dict[split_target_key[0]] | ||||
|             for item in split_target_key[1:-1]: | ||||
|                 new_dict = new_dict[item] | ||||
|             self.__update_nested_dict(new_dict, split_target_key[-1], new_value) | ||||
|         else: | ||||
|             nested_dict[target_key] = new_value | ||||
|         return nested_dict | ||||
|  | ||||
|     # 处理crud,返回columns | ||||
|     def __handle_crud(self): | ||||
|         result = self.__get_field_attribute() | ||||
|         columns = dict() | ||||
|         for item in result: | ||||
|             key = item.get('key') | ||||
|             title = item.get('title') | ||||
|             type = item.get('type') | ||||
|             columns[key] = { | ||||
|                 "title": title, | ||||
|                 "key": key, | ||||
|                 "type": type | ||||
|             } | ||||
|         # 对自定义的crud配置合并 | ||||
|         if self.custom_crud_json and isinstance(self.custom_crud_json,dict): | ||||
|             columns = columns | self.custom_crud_json | ||||
|         # 对curd进行修改配置 | ||||
|         if self.crud_update_key_value and isinstance(self.crud_update_key_value,dict): | ||||
|             for key, value in self.crud_update_key_value.items(): | ||||
|                 columns = self.__update_nested_dict(columns,key,value) | ||||
|         return columns | ||||
|     @action(methods=['get'], detail=False,permission_classes=[AllowAny]) | ||||
|     def init_crud(self, request): | ||||
|         self.permission_classes = [AllowAny] | ||||
|         columns = self.__handle_crud() | ||||
|         expose = "({expose,dict})=>{" | ||||
|         ret = "return {" | ||||
|         res = "}}" | ||||
|         data = f"""{expose} | ||||
|         {ret} | ||||
|         columns:{columns} | ||||
|         {res} | ||||
|         """ | ||||
|         return DetailResponse(data=data) | ||||
							
								
								
									
										70
									
								
								backend/dvadmin/utils/exception.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								backend/dvadmin/utils/exception.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/2 002 16:06 | ||||
| @Remark: 自定义异常处理 | ||||
| """ | ||||
| import logging | ||||
| import traceback | ||||
|  | ||||
| from django.db.models import ProtectedError | ||||
| from django.http import Http404 | ||||
| from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed, NotAuthenticated | ||||
| from rest_framework.status import HTTP_401_UNAUTHORIZED | ||||
| from rest_framework.views import set_rollback, exception_handler | ||||
|  | ||||
| from dvadmin.utils.json_response import ErrorResponse | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class CustomAuthenticationFailed(NotAuthenticated): | ||||
|     # 设置 status_code 属性为 400 | ||||
|     status_code = 400 | ||||
|  | ||||
| def CustomExceptionHandler(ex, context): | ||||
|     """ | ||||
|     统一异常拦截处理 | ||||
|     目的:(1)取消所有的500异常响应,统一响应为标准错误返回 | ||||
|         (2)准确显示错误信息 | ||||
|     :param ex: | ||||
|     :param context: | ||||
|     :return: | ||||
|     """ | ||||
|     msg = '' | ||||
|     code = 4000 | ||||
|     # 调用默认的异常处理函数 | ||||
|     response = exception_handler(ex, context) | ||||
|     if isinstance(ex, AuthenticationFailed): | ||||
|         # 如果是身份验证错误 | ||||
|         if response and response.data.get('detail') == "Given token not valid for any token type": | ||||
|             code = 401 | ||||
|             msg = ex.detail | ||||
|         elif response and response.data.get('detail') == "Token is blacklisted": | ||||
|             # token在黑名单 | ||||
|             return ErrorResponse(status=HTTP_401_UNAUTHORIZED) | ||||
|         else: | ||||
|             code = 401 | ||||
|             msg = ex.detail | ||||
|     elif isinstance(ex,Http404): | ||||
|         code = 400 | ||||
|         msg = "接口地址不正确" | ||||
|     elif isinstance(ex, DRFAPIException): | ||||
|         set_rollback() | ||||
|         msg = ex.detail | ||||
|         if isinstance(msg,dict): | ||||
|             for k, v in msg.items(): | ||||
|                 for i in v: | ||||
|                     msg = "%s:%s" % (k, i) | ||||
|     elif isinstance(ex, ProtectedError): | ||||
|         set_rollback() | ||||
|         msg = "删除失败:该条数据与其他数据有相关绑定" | ||||
|     # elif isinstance(ex, DatabaseError): | ||||
|     #     set_rollback() | ||||
|     #     msg = "接口服务器异常,请联系管理员" | ||||
|     elif isinstance(ex, Exception): | ||||
|         logger.exception(traceback.format_exc()) | ||||
|         msg = str(ex) | ||||
|     return ErrorResponse(msg=msg, code=code) | ||||
							
								
								
									
										43
									
								
								backend/dvadmin/utils/field_permission.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								backend/dvadmin/utils/field_permission.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| # -*- 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 | ||||
|  | ||||
|  | ||||
| def merge_permission(data): | ||||
|     """ | ||||
|     合并权限 | ||||
|     """ | ||||
|     result = {} | ||||
|     for item in data: | ||||
|         field_name = item.pop('field_name') | ||||
|         if field_name not in result: | ||||
|             result[field_name] = item | ||||
|         else: | ||||
|             for key, value in item.items(): | ||||
|                 result[field_name][key] = result[field_name][key] or value | ||||
|     return result | ||||
|  | ||||
|  | ||||
| class FieldPermissionMixin: | ||||
|     @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) | ||||
|     def field_permission(self, request): | ||||
|         """ | ||||
|         获取字段权限 | ||||
|         """ | ||||
|         model = self.serializer_class.Meta.model.__name__ | ||||
|         user = request.user | ||||
|         # 创建一个默认字典来存储最终的结果 | ||||
|         if user.is_superuser == 1: | ||||
|             data = MenuField.objects.filter(model=model).values('field_name') | ||||
|             result = {item['field_name']: {"is_create": True, "is_query": True, "is_update": True} for item in data} | ||||
|         else: | ||||
|             roles = request.user.role.values_list('id', flat=True) | ||||
|             data = FieldPermission.objects.filter( | ||||
|                 field__model=model, role__in=roles | ||||
|             ).values('is_create', 'is_query', 'is_update', field_name=F('field__field_name')) | ||||
|             result = merge_permission(data) | ||||
|         return DetailResponse(data=result) | ||||
							
								
								
									
										428
									
								
								backend/dvadmin/utils/filters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										428
									
								
								backend/dvadmin/utils/filters.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,428 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/6 006 12:39 | ||||
| @Remark: 自定义过滤器 | ||||
| """ | ||||
| import operator | ||||
| import re | ||||
| 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, 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=f'{create_datetime_before} 23:59:59') | ||||
|             elif create_datetime_after: | ||||
|                 create_filter &= Q(create_datetime__gte=create_datetime_after) | ||||
|             elif create_datetime_before: | ||||
|                 create_filter &= Q(create_datetime__lte=f'{create_datetime_before} 23:59:59') | ||||
|  | ||||
|             # 更新时间范围过滤条件 | ||||
|             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): | ||||
|     """ | ||||
|     递归获取部门的所有下级部门 | ||||
|     :param dept_id: 需要获取的部门id | ||||
|     :param dept_all_list: 所有部门列表 | ||||
|     :param dept_list: 递归部门list | ||||
|     :return: | ||||
|     """ | ||||
|     if not dept_all_list: | ||||
|         dept_all_list = Dept.objects.all().values("id", "parent") | ||||
|     if dept_list is None: | ||||
|         dept_list = [dept_id] | ||||
|     for ele in dept_all_list: | ||||
|         if ele.get("parent") == dept_id: | ||||
|             dept_list.append(ele.get("id")) | ||||
|             get_dept(ele.get("id"), dept_all_list, dept_list) | ||||
|     return list(set(dept_list)) | ||||
|  | ||||
|  | ||||
| class DataLevelPermissionsFilter(BaseFilterBackend): | ||||
|     """ | ||||
|     数据 级权限过滤器 | ||||
|     0. 获取用户的部门id,没有部门则返回空 | ||||
|     1. 判断过滤的数据是否有创建人所在部门 "creator" 字段,没有则返回全部 | ||||
|     2. 如果用户没有关联角色则返回本部门数据 | ||||
|     3. 根据角色的最大权限进行数据过滤(会有多个角色,进行去重取最大权限) | ||||
|     3.1 判断用户是否为超级管理员角色/如果有1(所有数据) 则返回所有数据 | ||||
|  | ||||
|     4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据) | ||||
|     5. 自定数据权限 获取部门,根据部门过滤 | ||||
|     """ | ||||
|  | ||||
|     def filter_queryset(self, request, queryset, view): | ||||
|         """ | ||||
|         接口白名单是否认证数据权限 | ||||
|         """ | ||||
|         api = request.path  # 当前请求接口 | ||||
|         method = request.method  # 当前请求方法 | ||||
|         methodList = ["GET", "POST", "PUT", "DELETE", "OPTIONS"] | ||||
|         method = methodList.index(method) | ||||
|         # ***接口白名单*** | ||||
|         api_white_list = ApiWhiteList.objects.filter(enable_datasource=False).values( | ||||
|             permission__api=F("url"), permission__method=F("method") | ||||
|         ) | ||||
|         api_white_list = [ | ||||
|             str(item.get("permission__api").replace("{id}", ".*?")) | ||||
|             + ":" | ||||
|             + str(item.get("permission__method")) | ||||
|             for item in api_white_list | ||||
|             if item.get("permission__api") | ||||
|         ] | ||||
|         for item in api_white_list: | ||||
|             new_api = f"{api}:{method}" | ||||
|             matchObj = re.match(item, new_api, re.M | re.I) | ||||
|             if matchObj is None: | ||||
|                 continue | ||||
|             else: | ||||
|                 return queryset | ||||
|         """ | ||||
|         判断是否为超级管理员: | ||||
|         如果不是超级管理员,则进入下一步权限判断 | ||||
|         """ | ||||
|         if request.user.is_superuser == 0: | ||||
|             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 = { | ||||
|         "^": "istartswith", | ||||
|         "=": "iexact", | ||||
|         "@": "search", | ||||
|         "$": "iregex", | ||||
|         "~": "icontains", | ||||
|     } | ||||
|     filter_fields = "__all__" | ||||
|  | ||||
|     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 = 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_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 | ||||
|         return None | ||||
|  | ||||
|     def get_filterset_class(self, view, queryset=None): | ||||
|         """ | ||||
|         Return the `FilterSet` class used to filter the queryset. | ||||
|         """ | ||||
|         filterset_class = getattr(view, "filterset_class", None) | ||||
|         filterset_fields = getattr(view, "filterset_fields", None) | ||||
|  | ||||
|         # 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__ | ||||
|             ) | ||||
|             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__ | ||||
|             ) | ||||
|             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 | ||||
|  | ||||
|             # FilterSets do not need to specify a Meta class | ||||
|             if filterset_model and queryset is not None: | ||||
|                 assert issubclass( | ||||
|                     queryset.model, filterset_model | ||||
|                 ), "FilterSet model %s does not match queryset model %s" % ( | ||||
|                     filterset_model, | ||||
|                     queryset.model, | ||||
|                 ) | ||||
|  | ||||
|             return filterset_class | ||||
|  | ||||
|         if filterset_fields and queryset is not None: | ||||
|             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): | ||||
|                     """ | ||||
|                     Get all filters for the filterset. This is the combination of declared and | ||||
|                     generated filters. | ||||
|                     """ | ||||
|  | ||||
|                     # No model specified - skip filter generation | ||||
|                     if not cls._meta.model: | ||||
|                         return cls.declared_filters.copy() | ||||
|  | ||||
|                     # Determine the filters that should be included on the filterset. | ||||
|                     filters = OrderedDict() | ||||
|                     fields = cls.get_fields() | ||||
|                     undefined = [] | ||||
|  | ||||
|                     for field_name, lookups in fields.items(): | ||||
|                         field = get_model_field(cls._meta.model, field_name) | ||||
|                         from django.db import models | ||||
|                         from timezone_field import TimeZoneField | ||||
|  | ||||
|                         # 不进行 过滤的model 类 | ||||
|                         if isinstance(field, (models.JSONField, TimeZoneField, models.FileField)): | ||||
|                             continue | ||||
|                         # 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) | ||||
|  | ||||
|                             # If the filter is explicitly declared on the class, skip generation | ||||
|                             if filter_name in cls.declared_filters: | ||||
|                                 filters[filter_name] = cls.declared_filters[filter_name] | ||||
|                                 continue | ||||
|  | ||||
|                             if field is not None: | ||||
|                                 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] | ||||
|  | ||||
|                     if undefined: | ||||
|                         raise TypeError( | ||||
|                             "'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 | ||||
|                     # declared filters to the 'Meta.fields' option | ||||
|                     filters.update(cls.declared_filters) | ||||
|                     return filters | ||||
|  | ||||
|                 class Meta(MetaBase): | ||||
|                     model = queryset.model | ||||
|                     fields = filterset_fields | ||||
|  | ||||
|             return AutoFilterSet | ||||
|  | ||||
|         return None | ||||
|  | ||||
|     def filter_queryset(self, request, queryset, view): | ||||
|         filterset = self.get_filterset(request, queryset, view) | ||||
|         if filterset is None: | ||||
|             return queryset | ||||
|         if filterset.__class__.__name__ == "AutoFilterSet": | ||||
|             queryset = filterset.queryset | ||||
|             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 or filterset.data.get(search_term_key) == '': | ||||
|                     continue | ||||
|                 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)) | ||||
|                 return queryset | ||||
|             else: | ||||
|                 return queryset | ||||
|  | ||||
|         if not filterset.is_valid() and self.raise_exception: | ||||
|             raise utils.translate_validation(filterset.errors) | ||||
|         return filterset.qs | ||||
							
								
								
									
										104
									
								
								backend/dvadmin/utils/git_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								backend/dvadmin/utils/git_utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,104 @@ | ||||
| import os | ||||
| from git.repo import Repo | ||||
| from git.repo.fun import is_git_dir | ||||
|  | ||||
|  | ||||
| class GitRepository(object): | ||||
|     """ | ||||
|     git仓库管理 | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, local_path, repo_url, branch='master'): | ||||
|         self.local_path = local_path | ||||
|         self.repo_url = repo_url | ||||
|         self.repo = None | ||||
|         self.initial(self.repo_url, branch) | ||||
|  | ||||
|     def initial(self, repo_url, branch): | ||||
|         """ | ||||
|         初始化git仓库 | ||||
|         :param repo_url: | ||||
|         :param branch: | ||||
|         :return: | ||||
|         """ | ||||
|         if not os.path.exists(self.local_path): | ||||
|             os.makedirs(self.local_path) | ||||
|         git_local_path = os.path.join(self.local_path, '.git') | ||||
|         if not is_git_dir(git_local_path): | ||||
|             self.repo = Repo.clone_from(repo_url, to_path=self.local_path, branch=branch) | ||||
|         else: | ||||
|             self.repo = Repo(self.local_path) | ||||
|  | ||||
|     def pull(self): | ||||
|         """ | ||||
|         从线上拉最新代码 | ||||
|         :return: | ||||
|         """ | ||||
|         self.repo.git.pull() | ||||
|  | ||||
|     def branches(self): | ||||
|         """ | ||||
|         获取所有分支 | ||||
|         :return: | ||||
|         """ | ||||
|         branches = self.repo.remote().refs | ||||
|         return [item.remote_head for item in branches if item.remote_head not in ['HEAD', ]] | ||||
|  | ||||
|     def commits(self): | ||||
|         """ | ||||
|         获取所有提交记录 | ||||
|         :return: | ||||
|         """ | ||||
|         commit_log = self.repo.git.log('--pretty={"commit":"%h","author":"%an","summary":"%s","date":"%cd"}', | ||||
|                                        max_count=50, | ||||
|                                        date='format:%Y-%m-%d %H:%M') | ||||
|         log_list = commit_log.split("\n") | ||||
|         return [eval(item) for item in log_list] | ||||
|  | ||||
|     def tags(self): | ||||
|         """ | ||||
|         获取所有tag | ||||
|         :return: | ||||
|         """ | ||||
|         return [tag.name for tag in self.repo.tags] | ||||
|  | ||||
|     def tags_exists(self, tag): | ||||
|         """ | ||||
|         tag是否存在 | ||||
|         :return: | ||||
|         """ | ||||
|         return tag in self.tags() | ||||
|  | ||||
|     def change_to_branch(self, branch): | ||||
|         """ | ||||
|         切换分支 | ||||
|         :param branch: | ||||
|         :return: | ||||
|         """ | ||||
|         self.repo.git.checkout(branch) | ||||
|  | ||||
|     def change_to_commit(self, branch, commit): | ||||
|         """ | ||||
|         切换commit | ||||
|         :param branch: | ||||
|         :param commit: | ||||
|         :return: | ||||
|         """ | ||||
|         self.change_to_branch(branch=branch) | ||||
|         self.repo.git.reset('--hard', commit) | ||||
|  | ||||
|     def change_to_tag(self, tag): | ||||
|         """ | ||||
|         切换tag | ||||
|         :param tag: | ||||
|         :return: | ||||
|         """ | ||||
|         self.repo.git.checkout(tag) | ||||
|  | ||||
| # if __name__ == '__main__': | ||||
| # local_path = os.path.join('codes', 't1') | ||||
| # repo = GitRepository(local_path, remote_path) | ||||
| # branch_list = repo.branches() | ||||
| # print(branch_list) | ||||
| # repo.change_to_branch('dev') | ||||
| # repo.pull() | ||||
							
								
								
									
										90
									
								
								backend/dvadmin/utils/import_export.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								backend/dvadmin/utils/import_export.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| import os | ||||
| import re | ||||
| from datetime import datetime | ||||
|  | ||||
| import openpyxl | ||||
| from django.conf import settings | ||||
|  | ||||
| from dvadmin.utils.validator import CustomValidationError | ||||
|  | ||||
|  | ||||
| def import_to_data(file_url, field_data, m2m_fields=None): | ||||
|     """ | ||||
|     读取导入的excel文件 | ||||
|     :param file_url: | ||||
|     :param field_data: 首行数据源 | ||||
|     :param m2m_fields: 多对多字段 | ||||
|     :return: | ||||
|     """ | ||||
|     # 读取excel 文件 | ||||
|     file_path_dir = os.path.join(settings.BASE_DIR, file_url) | ||||
|     workbook = openpyxl.load_workbook(file_path_dir) | ||||
|     table = workbook[workbook.sheetnames[0]] | ||||
|     theader = tuple(table.values)[0] #Excel的表头 | ||||
|     is_update = '更新主键(勿改)' in theader #是否导入更新 | ||||
|     if is_update is False: #不是更新时,删除id列 | ||||
|         field_data.pop('id') | ||||
|     # 获取参数映射 | ||||
|     validation_data_dict = {} | ||||
|     for key, value in field_data.items(): | ||||
|         if isinstance(value, dict): | ||||
|             choices = value.get("choices", {}) | ||||
|             data_dict = {} | ||||
|             if choices.get("data"): | ||||
|                 for k, v in choices.get("data").items(): | ||||
|                     data_dict[k] = v | ||||
|             elif choices.get("queryset") and choices.get("values_name"): | ||||
|                 data_list = choices.get("queryset").values(choices.get("values_name"), "id") | ||||
|                 for ele in data_list: | ||||
|                     data_dict[ele.get(choices.get("values_name"))] = ele.get("id") | ||||
|             else: | ||||
|                 continue | ||||
|             validation_data_dict[key] = data_dict | ||||
|     # 创建一个空列表,存储Excel的数据 | ||||
|     tables = [] | ||||
|     for i, row in enumerate(range(table.max_row)): | ||||
|         if i == 0: | ||||
|             continue | ||||
|         array = {} | ||||
|         for index, item in enumerate(field_data.items()): | ||||
|             items = list(item) | ||||
|             key = items[0] | ||||
|             values = items[1] | ||||
|             value_type = 'str' | ||||
|             if isinstance(values, dict): | ||||
|                 value_type = values.get('type','str') | ||||
|             cell_value = table.cell(row=row + 1, column=index + 2).value | ||||
|             if cell_value is None or cell_value=='': | ||||
|                 continue | ||||
|             elif value_type == 'date': | ||||
|                 print(61, datetime.strptime(str(cell_value), '%Y-%m-%d %H:%M:%S').date()) | ||||
|                 try: | ||||
|                     cell_value = datetime.strptime(str(cell_value), '%Y-%m-%d %H:%M:%S').date() | ||||
|                 except: | ||||
|                     raise CustomValidationError('日期格式不正确') | ||||
|             elif value_type == 'datetime': | ||||
|                 cell_value = datetime.strptime(str(cell_value), '%Y-%m-%d %H:%M:%S') | ||||
|             else: | ||||
|             # 由于excel导入数字类型后,会出现数字加 .0 的,进行处理 | ||||
|                 if type(cell_value) is float and str(cell_value).split(".")[1] == "0": | ||||
|                     cell_value = int(str(cell_value).split(".")[0]) | ||||
|                 elif type(cell_value) is str: | ||||
|                     cell_value = cell_value.strip(" \t\n\r") | ||||
|             if key in validation_data_dict: | ||||
|                 array[key] = validation_data_dict.get(key, {}).get(cell_value, None) | ||||
|                 if key in m2m_fields: | ||||
|                     array[key] = list( | ||||
|                         filter( | ||||
|                             lambda x: x, | ||||
|                             [ | ||||
|                                 validation_data_dict.get(key, {}).get(value, None) | ||||
|                                 for value in re.split(r"[,;:|.,;:\s]\s*", cell_value) | ||||
|                             ], | ||||
|                         ) | ||||
|                     ) | ||||
|             else: | ||||
|                 array[key] = cell_value | ||||
|         tables.append(array) | ||||
|     data = [i for i in tables if len(i) != 0] | ||||
|     return data | ||||
							
								
								
									
										358
									
								
								backend/dvadmin/utils/import_export_mixin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										358
									
								
								backend/dvadmin/utils/import_export_mixin.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,358 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| import datetime | ||||
| from urllib.parse import quote | ||||
|  | ||||
| from django.db import transaction | ||||
| from django.http import HttpResponse | ||||
| from openpyxl import Workbook | ||||
| from openpyxl.worksheet.datavalidation import DataValidation | ||||
| from openpyxl.utils import get_column_letter, quote_sheetname | ||||
| from openpyxl.worksheet.table import Table, TableStyleInfo | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.request import Request | ||||
|  | ||||
| from dvadmin.utils.import_export import import_to_data | ||||
| from dvadmin.utils.json_response import DetailResponse, SuccessResponse | ||||
| from dvadmin.utils.request_util import get_verbose_name | ||||
| from dvadmin.system.tasks import async_export_data | ||||
| from dvadmin.system.models import DownloadCenter | ||||
|  | ||||
|  | ||||
| class ImportSerializerMixin: | ||||
|     """ | ||||
|     自定义导入模板、导入功能 | ||||
|     """ | ||||
|  | ||||
|     # 导入字段 | ||||
|     import_field_dict = {} | ||||
|     # 导入序列化器 | ||||
|     import_serializer_class = None | ||||
|     # 表格表头最大宽度,默认50个字符 | ||||
|     export_column_width = 50 | ||||
|  | ||||
|     def is_number(self,num): | ||||
|         try: | ||||
|             float(num) | ||||
|             return True | ||||
|         except ValueError: | ||||
|             pass | ||||
|  | ||||
|         try: | ||||
|             import unicodedata | ||||
|             unicodedata.numeric(num) | ||||
|             return True | ||||
|         except (TypeError, ValueError): | ||||
|             pass | ||||
|         return False | ||||
|  | ||||
|     def get_string_len(self, string): | ||||
|         """ | ||||
|         获取字符串最大长度 | ||||
|         :param string: | ||||
|         :return: | ||||
|         """ | ||||
|         length = 4 | ||||
|         if string is None: | ||||
|             return length | ||||
|         if self.is_number(string): | ||||
|             return length | ||||
|         for char in string: | ||||
|             length += 2.1 if ord(char) > 256 else 1 | ||||
|         return round(length, 1) if length <= self.export_column_width else self.export_column_width | ||||
|  | ||||
|     @action(methods=['get','post'],detail=False) | ||||
|     @transaction.atomic  # Django 事务,防止出错 | ||||
|     def import_data(self, request: Request, *args, **kwargs): | ||||
|         """ | ||||
|         导入模板 | ||||
|         :param request: | ||||
|         :param args: | ||||
|         :param kwargs: | ||||
|         :return: | ||||
|         """ | ||||
|         assert self.import_field_dict, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__ | ||||
|         # 导出模板 | ||||
|         if request.method == "GET": | ||||
|             # 示例数据 | ||||
|             queryset = self.filter_queryset(self.get_queryset()) | ||||
|             # 导出excel 表 | ||||
|             response = HttpResponse(content_type="application/msexcel") | ||||
|             response["Access-Control-Expose-Headers"] = f"Content-Disposition" | ||||
|             response[ | ||||
|                 "Content-Disposition" | ||||
|             ] = f'attachment;filename={quote(str(f"导入{get_verbose_name(queryset)}模板.xlsx"))}' | ||||
|             wb = Workbook() | ||||
|             ws1 = wb.create_sheet("data", 1) | ||||
|             ws1.sheet_state = "hidden" | ||||
|             ws = wb.active | ||||
|             row = get_column_letter(len(self.import_field_dict) + 1) | ||||
|             column = 10 | ||||
|             header_data = [ | ||||
|                 "序号", | ||||
|             ] | ||||
|             validation_data_dict = {} | ||||
|             for index, ele in enumerate(self.import_field_dict.values()): | ||||
|                 if isinstance(ele, dict): | ||||
|                     header_data.append(ele.get("title")) | ||||
|                     choices = ele.get("choices", {}) | ||||
|                     if choices.get("data"): | ||||
|                         data_list = [] | ||||
|                         data_list.extend(choices.get("data").keys()) | ||||
|                         validation_data_dict[ele.get("title")] = data_list | ||||
|                     elif choices.get("queryset") and choices.get("values_name"): | ||||
|                         data_list = choices.get("queryset").values_list(choices.get("values_name"), flat=True) | ||||
|                         validation_data_dict[ele.get("title")] = list(data_list) | ||||
|                     else: | ||||
|                         continue | ||||
|                     column_letter = get_column_letter(len(validation_data_dict)) | ||||
|                     dv = DataValidation( | ||||
|                         type="list", | ||||
|                         formula1=f"{quote_sheetname('data')}!${column_letter}$2:${column_letter}${len(validation_data_dict[ele.get('title')]) + 1}", | ||||
|                         allow_blank=True, | ||||
|                     ) | ||||
|                     ws.add_data_validation(dv) | ||||
|                     dv.add(f"{get_column_letter(index + 2)}2:{get_column_letter(index + 2)}1048576") | ||||
|                 else: | ||||
|                     header_data.append(ele) | ||||
|             # 添加数据列 | ||||
|             ws1.append(list(validation_data_dict.keys())) | ||||
|             for index, validation_data in enumerate(validation_data_dict.values()): | ||||
|                 for inx, ele in enumerate(validation_data): | ||||
|                     ws1[f"{get_column_letter(index + 1)}{inx + 2}"] = ele | ||||
|             # 插入导出模板正式数据 | ||||
|             df_len_max = [self.get_string_len(ele) for ele in header_data] | ||||
|             ws.append(header_data) | ||||
|             #  更新列宽 | ||||
|             for index, width in enumerate(df_len_max): | ||||
|                 ws.column_dimensions[get_column_letter(index + 1)].width = width | ||||
|             tab = Table(displayName="Table1", ref=f"A1:{row}{column}")  # 名称管理器 | ||||
|             style = TableStyleInfo( | ||||
|                 name="TableStyleLight11", | ||||
|                 showFirstColumn=True, | ||||
|                 showLastColumn=True, | ||||
|                 showRowStripes=True, | ||||
|                 showColumnStripes=True, | ||||
|             ) | ||||
|             tab.tableStyleInfo = style | ||||
|             ws.add_table(tab) | ||||
|             wb.save(response) | ||||
|             return response | ||||
|         else: | ||||
|             # 从excel中组织对应的数据结构,然后使用序列化器保存 | ||||
|             queryset = self.filter_queryset(self.get_queryset()) | ||||
|             # 获取多对多字段 | ||||
|             m2m_fields = [ | ||||
|                 ele.name | ||||
|                 for ele in queryset.model._meta.get_fields() | ||||
|                 if hasattr(ele, "many_to_many") and ele.many_to_many == True | ||||
|             ] | ||||
|             import_field_dict = {'id':'更新主键(勿改)',**self.import_field_dict} | ||||
|             data = import_to_data(request.data.get("url"), import_field_dict, m2m_fields) | ||||
|             for ele in data: | ||||
|                 filter_dic = {'id':ele.get('id')} | ||||
|                 instance = filter_dic and queryset.filter(**filter_dic).first() | ||||
|                 # print(156,ele) | ||||
|                 serializer = self.import_serializer_class(instance, data=ele, request=request) | ||||
|                 serializer.is_valid(raise_exception=True) | ||||
|                 serializer.save() | ||||
|             return DetailResponse(msg=f"导入成功!") | ||||
|  | ||||
|     @action(methods=['get'],detail=False) | ||||
|     def update_template(self,request): | ||||
|         queryset = self.filter_queryset(self.get_queryset()) | ||||
|         assert self.import_field_dict, "'%s' 请配置对应的导入模板字段。" % self.__class__.__name__ | ||||
|         assert self.import_serializer_class, "'%s' 请配置对应的导入序列化器。" % self.__class__.__name__ | ||||
|         data = self.import_serializer_class(queryset, many=True, request=request).data | ||||
|         # 导出excel 表 | ||||
|         response = HttpResponse(content_type="application/msexcel") | ||||
|         response["Access-Control-Expose-Headers"] = f"Content-Disposition" | ||||
|         response["content-disposition"] = f'attachment;filename={quote(str(f"导出{get_verbose_name(queryset)}.xlsx"))}' | ||||
|         wb = Workbook() | ||||
|         ws1 = wb.create_sheet("data", 1) | ||||
|         ws1.sheet_state = "hidden" | ||||
|         ws = wb.active | ||||
|         import_field_dict = {} | ||||
|         header_data = ["序号","更新主键(勿改)"] | ||||
|         hidden_header = ["#","id"] | ||||
|         #----设置选项---- | ||||
|         validation_data_dict = {} | ||||
|         for index, item in enumerate(self.import_field_dict.items()): | ||||
|             items = list(item) | ||||
|             key = items[0] | ||||
|             value = items[1] | ||||
|             if isinstance(value, dict): | ||||
|                 header_data.append(value.get("title")) | ||||
|                 hidden_header.append(value.get('display')) | ||||
|                 choices = value.get("choices", {}) | ||||
|                 if choices.get("data"): | ||||
|                     data_list = [] | ||||
|                     data_list.extend(choices.get("data").keys()) | ||||
|                     validation_data_dict[value.get("title")] = data_list | ||||
|                 elif choices.get("queryset") and choices.get("values_name"): | ||||
|                     data_list = choices.get("queryset").values_list(choices.get("values_name"), flat=True) | ||||
|                     validation_data_dict[value.get("title")] = list(data_list) | ||||
|                 else: | ||||
|                     continue | ||||
|                 column_letter = get_column_letter(len(validation_data_dict)) | ||||
|                 dv = DataValidation( | ||||
|                     type="list", | ||||
|                     formula1=f"{quote_sheetname('data')}!${column_letter}$2:${column_letter}${len(validation_data_dict[value.get('title')]) + 1}", | ||||
|                     allow_blank=True, | ||||
|                 ) | ||||
|                 ws.add_data_validation(dv) | ||||
|                 dv.add(f"{get_column_letter(index + 3)}2:{get_column_letter(index + 3)}1048576") | ||||
|             else: | ||||
|                 header_data.append(value) | ||||
|                 hidden_header.append(key) | ||||
|         # 添加数据列 | ||||
|         ws1.append(list(validation_data_dict.keys())) | ||||
|         for index, validation_data in enumerate(validation_data_dict.values()): | ||||
|             for inx, ele in enumerate(validation_data): | ||||
|                 ws1[f"{get_column_letter(index + 1)}{inx + 2}"] = ele | ||||
|         #-------- | ||||
|         df_len_max = [self.get_string_len(ele) for ele in header_data] | ||||
|         row = get_column_letter(len(hidden_header) + 1) | ||||
|         column = 1 | ||||
|         ws.append(header_data) | ||||
|         for index, results in enumerate(data): | ||||
|             results_list = [] | ||||
|             for h_index, h_item in enumerate(hidden_header): | ||||
|                 for key, val in results.items(): | ||||
|                     if key == h_item: | ||||
|                         if val is None or val == "": | ||||
|                             results_list.append("") | ||||
|                         elif isinstance(val,list): | ||||
|                             results_list.append(str(val)) | ||||
|                         else: | ||||
|                             results_list.append(val) | ||||
|                         # 计算最大列宽度 | ||||
|                         if isinstance(val,str): | ||||
|                             result_column_width = self.get_string_len(val) | ||||
|                             if h_index != 0 and result_column_width > df_len_max[h_index]: | ||||
|                                 df_len_max[h_index] = result_column_width | ||||
|             ws.append([index+1,*results_list]) | ||||
|             column += 1 | ||||
|         #  更新列宽 | ||||
|         for index, width in enumerate(df_len_max): | ||||
|             ws.column_dimensions[get_column_letter(index + 1)].width = width | ||||
|         tab = Table(displayName="Table", ref=f"A1:{row}{column}")  # 名称管理器 | ||||
|         style = TableStyleInfo( | ||||
|             name="TableStyleLight11", | ||||
|             showFirstColumn=True, | ||||
|             showLastColumn=True, | ||||
|             showRowStripes=True, | ||||
|             showColumnStripes=True, | ||||
|         ) | ||||
|         tab.tableStyleInfo = style | ||||
|         ws.add_table(tab) | ||||
|         wb.save(response) | ||||
|         return response | ||||
|  | ||||
|  | ||||
| class ExportSerializerMixin: | ||||
|     """ | ||||
|     自定义导出功能 | ||||
|     """ | ||||
|  | ||||
|     # 导出字段 | ||||
|     export_field_label = [] | ||||
|     # 导出序列化器 | ||||
|     export_serializer_class = None | ||||
|     # 表格表头最大宽度,默认50个字符 | ||||
|     export_column_width = 50 | ||||
|  | ||||
|     def is_number(self,num): | ||||
|         try: | ||||
|             float(num) | ||||
|             return True | ||||
|         except ValueError: | ||||
|             pass | ||||
|  | ||||
|         try: | ||||
|             import unicodedata | ||||
|             unicodedata.numeric(num) | ||||
|             return True | ||||
|         except (TypeError, ValueError): | ||||
|             pass | ||||
|         return False | ||||
|  | ||||
|     def get_string_len(self, string): | ||||
|         """ | ||||
|         获取字符串最大长度 | ||||
|         :param string: | ||||
|         :return: | ||||
|         """ | ||||
|         length = 4 | ||||
|         if string is None: | ||||
|             return length | ||||
|         if self.is_number(string): | ||||
|             return length | ||||
|         for char in string: | ||||
|             length += 2.1 if ord(char) > 256 else 1 | ||||
|         return round(length, 1) if length <= self.export_column_width else self.export_column_width | ||||
|  | ||||
|     @action(methods=['get'],detail=False) | ||||
|     def export_data(self, request: Request, *args, **kwargs): | ||||
|         """ | ||||
|         导出功能 | ||||
|         :param request: | ||||
|         :param args: | ||||
|         :param kwargs: | ||||
|         :return: | ||||
|         """ | ||||
|         queryset = self.filter_queryset(self.get_queryset()) | ||||
|         assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__ | ||||
|         assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__ | ||||
|         data = self.export_serializer_class(queryset, many=True, request=request).data | ||||
|         try: | ||||
|             async_export_data.delay( | ||||
|                 data, | ||||
|                 str(f"导出{get_verbose_name(queryset)}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx"), | ||||
|                 DownloadCenter.objects.create(creator=request.user, task_name=f'{get_verbose_name(queryset)}数据导出任务', dept_belong_id=request.user.dept_id).pk, | ||||
|                 self.export_field_label | ||||
|             ) | ||||
|             return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载") | ||||
|         except: | ||||
|             pass | ||||
|         # 导出excel 表 | ||||
|         response = HttpResponse(content_type="application/msexcel") | ||||
|         response["Access-Control-Expose-Headers"] = f"Content-Disposition" | ||||
|         response["content-disposition"] = f'attachment;filename={quote(str(f"导出{get_verbose_name(queryset)}.xlsx"))}' | ||||
|         wb = Workbook() | ||||
|         ws = wb.active | ||||
|         header_data = ["序号", *self.export_field_label.values()] | ||||
|         hidden_header = ["#", *self.export_field_label.keys()] | ||||
|         df_len_max = [self.get_string_len(ele) for ele in header_data] | ||||
|         row = get_column_letter(len(self.export_field_label) + 1) | ||||
|         column = 1 | ||||
|         ws.append(header_data) | ||||
|         for index, results in enumerate(data): | ||||
|             results_list = [] | ||||
|             for h_index, h_item in enumerate(hidden_header): | ||||
|                 for key,val in results.items(): | ||||
|                     if key == h_item: | ||||
|                         if val is None or val=="": | ||||
|                             results_list.append("") | ||||
|                         else: | ||||
|                             results_list.append(val) | ||||
|                         # 计算最大列宽度 | ||||
|                         result_column_width = self.get_string_len(val) | ||||
|                         if h_index !=0 and result_column_width > df_len_max[h_index]: | ||||
|                             df_len_max[h_index] = result_column_width | ||||
|             ws.append([index + 1, *results_list]) | ||||
|             column += 1 | ||||
|         #  更新列宽 | ||||
|         for index, width in enumerate(df_len_max): | ||||
|             ws.column_dimensions[get_column_letter(index + 1)].width = width | ||||
|         tab = Table(displayName="Table", ref=f"A1:{row}{column}")  # 名称管理器 | ||||
|         style = TableStyleInfo( | ||||
|             name="TableStyleLight11", | ||||
|             showFirstColumn=True, | ||||
|             showLastColumn=True, | ||||
|             showRowStripes=True, | ||||
|             showColumnStripes=True, | ||||
|         ) | ||||
|         tab.tableStyleInfo = style | ||||
|         ws.add_table(tab) | ||||
|         wb.save(response) | ||||
|         return response | ||||
							
								
								
									
										61
									
								
								backend/dvadmin/utils/json_response.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								backend/dvadmin/utils/json_response.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/2 002 14:43 | ||||
| @Remark: 自定义的JsonResonpse文件 | ||||
| """ | ||||
|  | ||||
| from rest_framework.response import Response | ||||
|  | ||||
|  | ||||
| class SuccessResponse(Response): | ||||
|     """ | ||||
|     标准响应成功的返回, SuccessResponse(data)或者SuccessResponse(data=data) | ||||
|     (1)默认code返回2000, 不支持指定其他返回码 | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, data=None, msg='success', status=None, template_name=None, headers=None, exception=False, | ||||
|                  content_type=None,page=1,limit=1,total=1): | ||||
|         std_data = { | ||||
|             "code": 2000, | ||||
|             "page": page, | ||||
|             "limit": limit, | ||||
|             "total": total, | ||||
|             "data": data, | ||||
|             "msg": msg | ||||
|         } | ||||
|         super().__init__(std_data, status, template_name, headers, exception, content_type) | ||||
|  | ||||
|  | ||||
| class DetailResponse(Response): | ||||
|     """ | ||||
|     不包含分页信息的接口返回,主要用于单条数据查询 | ||||
|     (1)默认code返回2000, 不支持指定其他返回码 | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, data=None, msg='success', status=None, template_name=None, headers=None, exception=False, | ||||
|                  content_type=None,): | ||||
|         std_data = { | ||||
|             "code": 2000, | ||||
|             "data": data, | ||||
|             "msg": msg | ||||
|         } | ||||
|         super().__init__(std_data, status, template_name, headers, exception, content_type) | ||||
|  | ||||
|  | ||||
| class ErrorResponse(Response): | ||||
|     """ | ||||
|     标准响应错误的返回,ErrorResponse(msg='xxx') | ||||
|     (1)默认错误码返回400, 也可以指定其他返回码:ErrorResponse(code=xxx) | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, data=None, msg='error', code=400, status=None, template_name=None, headers=None, | ||||
|                  exception=False, content_type=None): | ||||
|         std_data = { | ||||
|             "code": code, | ||||
|             "data": data, | ||||
|             "msg": msg | ||||
|         } | ||||
|         super().__init__(std_data, status, template_name, headers, exception, content_type) | ||||
							
								
								
									
										155
									
								
								backend/dvadmin/utils/middleware.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								backend/dvadmin/utils/middleware.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,155 @@ | ||||
| """ | ||||
| 日志 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 | ||||
| from dvadmin.utils.request_util import get_request_user, get_request_ip, get_request_data, get_request_path, get_os, \ | ||||
|     get_browser, get_verbose_name | ||||
|  | ||||
|  | ||||
| class ApiLoggingMiddleware(MiddlewareMixin): | ||||
|     """ | ||||
|     用于记录API访问日志中间件 | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, get_response=None): | ||||
|         super().__init__(get_response) | ||||
|         self.enable = getattr(settings, 'API_LOG_ENABLE', None) or False | ||||
|         self.methods = getattr(settings, 'API_LOG_METHODS', None) or set() | ||||
|         self.operation_log_id = None | ||||
|  | ||||
|     @classmethod | ||||
|     def __handle_request(cls, request): | ||||
|         request.request_ip = get_request_ip(request) | ||||
|         request.request_data = get_request_data(request) | ||||
|         request.request_path = get_request_path(request) | ||||
|  | ||||
|     def __handle_response(self, request, response): | ||||
|  | ||||
|         # 判断有无log_id属性,使用All记录时,会出现此情况 | ||||
|         if request.request_data.get('log_id', None) is None: | ||||
|             return | ||||
|          | ||||
|         # 移除log_id,不记录此ID | ||||
|         log_id = request.request_data.pop('log_id') | ||||
|  | ||||
|         # request_data,request_ip由PermissionInterfaceMiddleware中间件中添加的属性 | ||||
|         body = getattr(request, 'request_data', {}) | ||||
|         # 请求含有password则用*替换掉(暂时先用于所有接口的password请求参数) | ||||
|         if isinstance(body, dict) and body.get('password', ''): | ||||
|             body['password'] = '*' * len(body['password']) | ||||
|         if not hasattr(response, 'data') or not isinstance(response.data, dict): | ||||
|             response.data = {} | ||||
|         try: | ||||
|             if not response.data and response.content: | ||||
|                 content = json.loads(response.content.decode()) | ||||
|                 response.data = content if isinstance(content, dict) else {} | ||||
|         except Exception: | ||||
|             return | ||||
|         user = get_request_user(request) | ||||
|         info = { | ||||
|             'request_ip': getattr(request, 'request_ip', 'unknown'), | ||||
|             'creator': user if not isinstance(user, AnonymousUser) else None, | ||||
|             'dept_belong_id': getattr(request.user, 'dept_id', None), | ||||
|             'request_method': request.method, | ||||
|             'request_path': request.request_path, | ||||
|             'request_body': body, | ||||
|             'response_code': response.data.get('code'), | ||||
|             'request_os': get_os(request), | ||||
|             'request_browser': get_browser(request), | ||||
|             'request_msg': request.session.get('request_msg'), | ||||
|             'status': True if response.data.get('code') in [2000, ] else False, | ||||
|             'json_result': {"code": response.data.get('code'), "msg": response.data.get('msg')}, | ||||
|         } | ||||
|         operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=log_id) | ||||
|         if not operation_log.request_modular and settings.API_MODEL_MAP.get(request.request_path, None): | ||||
|             operation_log.request_modular = settings.API_MODEL_MAP[request.request_path] | ||||
|             operation_log.save() | ||||
|  | ||||
|     def process_view(self, request, view_func, view_args, view_kwargs): | ||||
|         if hasattr(view_func, 'cls') and hasattr(view_func.cls, 'queryset'): | ||||
|             if self.enable: | ||||
|                 if self.methods == 'ALL' or request.method in self.methods: | ||||
|                     log = OperationLog(request_modular=get_verbose_name(view_func.cls.queryset)) | ||||
|                     log.save() | ||||
|                     # self.operation_log_id = log.id | ||||
|                     request.request_data['log_id'] = log.id | ||||
|  | ||||
|         return | ||||
|  | ||||
|     def process_request(self, request): | ||||
|         self.__handle_request(request) | ||||
|  | ||||
|     def process_response(self, request, response): | ||||
|         """ | ||||
|         主要请求处理完之后记录 | ||||
|         :param request: | ||||
|         :param response: | ||||
|         :return: | ||||
|         """ | ||||
|         if self.enable: | ||||
|             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") | ||||
							
								
								
									
										272
									
								
								backend/dvadmin/utils/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								backend/dvadmin/utils/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,272 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/5/31 031 22:08 | ||||
| @Remark: 公共基础model类 | ||||
| """ | ||||
| from datetime import datetime | ||||
| from importlib import import_module | ||||
|  | ||||
| from application import settings | ||||
| from django.apps import apps | ||||
| from django.conf import settings | ||||
| from django.db import models | ||||
| from rest_framework.request import Request | ||||
|  | ||||
| table_prefix = settings.TABLE_PREFIX  # 数据库表名前缀 | ||||
|  | ||||
|  | ||||
| class SoftDeleteQuerySet(models.QuerySet): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class SoftDeleteManager(models.Manager): | ||||
|     """支持软删除""" | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         self.__add_is_del_filter = False | ||||
|         super(SoftDeleteManager, self).__init__(*args, **kwargs) | ||||
|  | ||||
|     def filter(self, *args, **kwargs): | ||||
|         # 考虑是否主动传入is_deleted | ||||
|         if not kwargs.get('is_deleted') is None: | ||||
|             self.__add_is_del_filter = True | ||||
|         return super(SoftDeleteManager, self).filter(*args, **kwargs) | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         if self.__add_is_del_filter: | ||||
|             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): | ||||
|         return SoftDeleteQuerySet(self.model).get(username=name) | ||||
|  | ||||
|  | ||||
| class SoftDeleteModel(models.Model): | ||||
|     """ | ||||
|     软删除模型 | ||||
|     一旦继承,就将开启软删除 | ||||
|     """ | ||||
|     is_deleted = models.BooleanField(verbose_name="是否软删除", help_text='是否软删除', default=False, db_index=True) | ||||
|     objects = SoftDeleteManager() | ||||
|  | ||||
|     class Meta: | ||||
|         abstract = True | ||||
|         verbose_name = '软删除模型' | ||||
|         verbose_name_plural = verbose_name | ||||
|  | ||||
|     def delete(self, using=None, soft_delete=True, *args, **kwargs): | ||||
|         """ | ||||
|         重写删除方法,直接开启软删除 | ||||
|         """ | ||||
|         if soft_delete: | ||||
|             self.is_deleted = True | ||||
|             self.save(using=using) | ||||
|             # 级联软删除关联对象 | ||||
|             for related_object in self._meta.related_objects: | ||||
|                 related_model = getattr(self, related_object.get_accessor_name()) | ||||
|                 # 处理一对多和多对多的关联对象 | ||||
|                 if related_object.one_to_many or related_object.many_to_many: | ||||
|                     related_objects = related_model.all() | ||||
|                 elif related_object.one_to_one: | ||||
|                     related_objects = [related_model] | ||||
|                 else: | ||||
|                     continue | ||||
|  | ||||
|                 for obj in related_objects: | ||||
|                     obj.delete(soft_delete=True) | ||||
|         else: | ||||
|             super().delete(using=using, *args, **kwargs) | ||||
|  | ||||
|  | ||||
| class 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) | ||||
|     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="修改时间") | ||||
|     create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间", | ||||
|                                            verbose_name="创建时间") | ||||
|  | ||||
|     class Meta: | ||||
|         abstract = True | ||||
|         verbose_name = '核心模型' | ||||
|         verbose_name_plural = verbose_name | ||||
|  | ||||
|     def get_request_user(self, request: Request): | ||||
|         if getattr(request, "user", None): | ||||
|             return request.user | ||||
|         return None | ||||
|  | ||||
|     def get_request_user_id(self, request: Request): | ||||
|         if getattr(request, "user", None): | ||||
|             return getattr(request.user, "id", None) | ||||
|         return None | ||||
|  | ||||
|     def get_request_user_name(self, request: Request): | ||||
|         if getattr(request, "user", None): | ||||
|             return getattr(request.user, "name", None) | ||||
|         return None | ||||
|  | ||||
|     def get_request_user_username(self, request: Request): | ||||
|         if getattr(request, "user", None): | ||||
|             return getattr(request.user, "username", None) | ||||
|         return None | ||||
|  | ||||
|     def common_insert_data(self, request: Request): | ||||
|         data = { | ||||
|             'create_datetime': datetime.now(), | ||||
|             'creator': self.get_request_user(request) | ||||
|         } | ||||
|         return {**data, **self.common_update_data(request)} | ||||
|  | ||||
|     def common_update_data(self, request: Request): | ||||
|         return { | ||||
|             'update_datetime': datetime.now(), | ||||
|             'modifier': self.get_request_user_username(request) | ||||
|         } | ||||
|  | ||||
|     exclude_fields = [ | ||||
|         '_state', | ||||
|         'pk', | ||||
|         'id', | ||||
|         'create_datetime', | ||||
|         'update_datetime', | ||||
|         'creator', | ||||
|         'creator_id', | ||||
|         'creator_pk', | ||||
|         'creator_name', | ||||
|         'modifier', | ||||
|         'modifier_id', | ||||
|         'modifier_pk', | ||||
|         'modifier_name', | ||||
|         'dept_belong_id', | ||||
|     ] | ||||
|  | ||||
|     def get_exclude_fields(self): | ||||
|         return self.exclude_fields | ||||
|  | ||||
|     def get_all_fields(self): | ||||
|         return self._meta.fields | ||||
|  | ||||
|     def get_all_fields_names(self): | ||||
|         return [field.name for field in self.get_all_fields()] | ||||
|  | ||||
|     def get_need_fields_names(self): | ||||
|         return [field.name for field in self.get_all_fields() if field.name not in self.exclude_fields] | ||||
|  | ||||
|     def to_data(self): | ||||
|         """将模型转化为字典(去除不包含字段)(注意与to_dict_data区分)。 | ||||
|         """ | ||||
|         res = {} | ||||
|         for field in self.get_need_fields_names(): | ||||
|             field_value = getattr(self, field) | ||||
|             res[field] = field_value.id if (issubclass(field_value.__class__, CoreModel)) else field_value | ||||
|         return res | ||||
|  | ||||
|     @property | ||||
|     def DATA(self): | ||||
|         return self.to_data() | ||||
|  | ||||
|     def to_dict_data(self): | ||||
|         """需要导出的字段(去除不包含字段)(注意与to_data区分) | ||||
|         """ | ||||
|         return {field: getattr(self, field) for field in self.get_need_fields_names()} | ||||
|  | ||||
|     @property | ||||
|     def DICT_DATA(self): | ||||
|         return self.to_dict_data() | ||||
|  | ||||
|     def insert(self, request): | ||||
|         """插入模型 | ||||
|         """ | ||||
|         assert self.pk is None, f'模型{self.__class__.__name__}还没有保存到数据中,不能手动指定ID' | ||||
|         validated_data = {**self.common_insert_data(request), **self.DICT_DATA} | ||||
|         return self.__class__._default_manager.create(**validated_data) | ||||
|  | ||||
|     def update(self, request, update_data: dict[str, any] = None): | ||||
|         """更新模型 | ||||
|         """ | ||||
|         assert isinstance(update_data, dict), 'update_data必须为字典' | ||||
|         validated_data = {**self.common_insert_data(request), **update_data} | ||||
|         for key, value in validated_data.items(): | ||||
|             # 不允许修改id,pk,uuid字段 | ||||
|             if key in ['id', 'pk', 'uuid']: | ||||
|                 continue | ||||
|             if hasattr(self, key): | ||||
|                 setattr(self, key, value) | ||||
|         self.save() | ||||
|         return self | ||||
|  | ||||
|  | ||||
| def get_all_models_objects(model_name=None): | ||||
|     """ | ||||
|     获取所有 models 对象 | ||||
|     :return: {} | ||||
|     """ | ||||
|     settings.ALL_MODELS_OBJECTS = {} | ||||
|     if not settings.ALL_MODELS_OBJECTS: | ||||
|         all_models = apps.get_models() | ||||
|         for item in list(all_models): | ||||
|             table = {"tableName": item._meta.verbose_name, "table": item.__name__, "tableFields": []} | ||||
|             for field in item._meta.fields: | ||||
|                 fields = {"title": field.verbose_name, "field": field.name} | ||||
|                 table['tableFields'].append(fields) | ||||
|             settings.ALL_MODELS_OBJECTS.setdefault(item.__name__, {"table": table, "object": item}) | ||||
|     if model_name: | ||||
|         return settings.ALL_MODELS_OBJECTS[model_name] or {} | ||||
|     return settings.ALL_MODELS_OBJECTS or {} | ||||
|  | ||||
|  | ||||
| def get_model_from_app(app_name): | ||||
|     """获取模型里的字段""" | ||||
|     model_module = import_module(app_name + '.models') | ||||
|     exclude_models = getattr(model_module, 'exclude_models', []) | ||||
|     filter_model = [ | ||||
|         value for key, value in model_module.__dict__.items() | ||||
|         if key != 'CoreModel' | ||||
|         and isinstance(value, type) | ||||
|         and issubclass(value, models.Model) | ||||
|         and key not in exclude_models | ||||
|     ] | ||||
|     model_list = [] | ||||
|     for model in filter_model: | ||||
|         if model.__name__ == 'AbstractUser': | ||||
|             continue | ||||
|         fields = [{'title': field.verbose_name, 'name': field.name, 'object': field} for field in model._meta.fields] | ||||
|         model_list.append({'app': app_name, 'verbose': model._meta.verbose_name, 'model': model.__name__, 'object': model, 'fields': fields}) | ||||
|     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 | ||||
							
								
								
									
										83
									
								
								backend/dvadmin/utils/pagination.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								backend/dvadmin/utils/pagination.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
|  | ||||
| @contact: QQ:1638245306 | ||||
|  | ||||
| @Created on: 2020/4/16 23:35 | ||||
| """ | ||||
| from collections import OrderedDict | ||||
|  | ||||
| from django.core import paginator | ||||
| from django.core.paginator import Paginator as DjangoPaginator, InvalidPage | ||||
| from rest_framework.pagination import PageNumberPagination | ||||
| from rest_framework.response import Response | ||||
|  | ||||
|  | ||||
| class CustomPagination(PageNumberPagination): | ||||
|     page_size = 10 | ||||
|     page_size_query_param = "limit" | ||||
|     max_page_size = 999 | ||||
|     django_paginator_class = DjangoPaginator | ||||
|  | ||||
|     def paginate_queryset(self, queryset, request, view=None): | ||||
|         """ | ||||
|         Paginate a queryset if required, either returning a | ||||
|         page object, or `None` if pagination is not configured for this view. | ||||
|         """ | ||||
|         empty = True | ||||
|  | ||||
|         page_size = self.get_page_size(request) | ||||
|         if not page_size: | ||||
|             return None | ||||
|  | ||||
|         paginator = self.django_paginator_class(queryset, page_size) | ||||
|         page_number = request.query_params.get(self.page_query_param, 1) | ||||
|         if page_number in self.last_page_strings: | ||||
|             page_number = paginator.num_pages | ||||
|  | ||||
|         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 | ||||
|  | ||||
|         if paginator.num_pages > 1 and self.template is not None: | ||||
|             # The browsable API should display pagination controls. | ||||
|             self.display_page_controls = True | ||||
|  | ||||
|         self.request = request | ||||
|  | ||||
|         if not empty: | ||||
|             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() if self.page else False | ||||
|         is_previous = self.page.has_previous() if self.page else False | ||||
|  | ||||
|         if not data: | ||||
|             code = 2000 | ||||
|             msg = "暂无数据" | ||||
|             data = [] | ||||
|  | ||||
|         return Response(OrderedDict([ | ||||
|             ('code', code), | ||||
|             ('msg', msg), | ||||
|             ('page', page), | ||||
|             ('limit', limit), | ||||
|             ('total', total), | ||||
|             ('is_next', is_next), | ||||
|             ('is_previous', is_previous), | ||||
|             ('data', data) | ||||
|         ])) | ||||
							
								
								
									
										128
									
								
								backend/dvadmin/utils/permission.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								backend/dvadmin/utils/permission.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,128 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/6 006 10:30 | ||||
| @Remark: 自定义权限 | ||||
| """ | ||||
| import re | ||||
|  | ||||
| from django.contrib.auth.models import AnonymousUser | ||||
| from django.db.models import F | ||||
| from rest_framework.permissions import BasePermission | ||||
|  | ||||
| from dvadmin.system.models import ApiWhiteList, RoleMenuButtonPermission | ||||
|  | ||||
|  | ||||
| def ValidationApi(reqApi, validApi): | ||||
|     """ | ||||
|     验证当前用户是否有接口权限 | ||||
|     :param reqApi: 当前请求的接口 | ||||
|     :param validApi: 用于验证的接口 | ||||
|     :return: True或者False | ||||
|     """ | ||||
|     if validApi is not None: | ||||
|         valid_api = validApi.replace('{id}', '.*?') | ||||
|         matchObj = re.match(valid_api, reqApi, re.M | re.I) | ||||
|         if matchObj: | ||||
|             return True | ||||
|         else: | ||||
|             return False | ||||
|     else: | ||||
|         return False | ||||
|  | ||||
|  | ||||
| class AnonymousUserPermission(BasePermission): | ||||
|     """ | ||||
|     匿名用户权限 | ||||
|     """ | ||||
|  | ||||
|     def has_permission(self, request, view): | ||||
|         if isinstance(request.user, AnonymousUser): | ||||
|             return False | ||||
|         return True | ||||
|  | ||||
|  | ||||
| class SuperuserPermission(BasePermission): | ||||
|     """ | ||||
|     超级管理员权限类 | ||||
|     """ | ||||
|  | ||||
|     def has_permission(self, request, view): | ||||
|         if isinstance(request.user, AnonymousUser): | ||||
|             return False | ||||
|         # 判断是否是超级管理员 | ||||
|         if request.user.is_superuser: | ||||
|             return True | ||||
|  | ||||
|  | ||||
| class AdminPermission(BasePermission): | ||||
|     """ | ||||
|     普通管理员权限类 | ||||
|     """ | ||||
|  | ||||
|     def has_permission(self, request, view): | ||||
|         if isinstance(request.user, AnonymousUser): | ||||
|             return False | ||||
|         # 判断是否是超级管理员 | ||||
|         is_superuser = request.user.is_superuser | ||||
|         # 判断是否是管理员角色 | ||||
|         is_admin = request.user.role.values_list('admin', flat=True) | ||||
|         if is_superuser or True in is_admin: | ||||
|             return True | ||||
|  | ||||
|  | ||||
| def ReUUID(api): | ||||
|     """ | ||||
|     将接口的uuid替换掉 | ||||
|     :param api: | ||||
|     :return: | ||||
|     """ | ||||
|     pattern = re.compile(r'[a-f\d]{4}(?:[a-f\d]{4}-){4}[a-f\d]{12}/$') | ||||
|     m = pattern.search(api) | ||||
|     if m: | ||||
|         res = api.replace(m.group(0), ".*/") | ||||
|         return res | ||||
|     else: | ||||
|         return None | ||||
|  | ||||
|  | ||||
| class CustomPermission(BasePermission): | ||||
|     """自定义权限""" | ||||
|  | ||||
|     def has_permission(self, request, view): | ||||
|         if isinstance(request.user, AnonymousUser): | ||||
|             return False | ||||
|         # 判断是否是超级管理员 | ||||
|         if request.user.is_superuser: | ||||
|             return True | ||||
|         else: | ||||
|             api = request.path  # 当前请求接口 | ||||
|             method = request.method  # 当前请求方法 | ||||
|             methodList = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'] | ||||
|             method = methodList.index(method) | ||||
|             # ***接口白名单*** | ||||
|             api_white_list = ApiWhiteList.objects.values(permission__api=F('url'), permission__method=F('method')) | ||||
|             api_white_list = [ | ||||
|                 str(item.get('permission__api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str( | ||||
|                     item.get('permission__method')) + '$' for item in api_white_list if item.get('permission__api')] | ||||
|             # ********# | ||||
|             if not hasattr(request.user, "role"): | ||||
|                 return False | ||||
|             role_id_list = request.user.role.values_list('id', flat=True) | ||||
|             userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values( | ||||
|                 permission__api=F('menu_button__api'), permission__method=F('menu_button__method'))  # 获取当前用户的角色拥有的所有接口 | ||||
|             ApiList = [ | ||||
|                 str(item.get('permission__api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str( | ||||
|                     item.get('permission__method')) + '$' for item in userApiList if item.get('permission__api')] | ||||
|             new_api_ist = api_white_list + ApiList | ||||
|             new_api = api + ":" + str(method) | ||||
|             for item in new_api_ist: | ||||
|                 matchObj = re.match(item, new_api, re.M | re.I) | ||||
|                 if matchObj is None: | ||||
|                     continue | ||||
|                 else: | ||||
|                     return True | ||||
|             else: | ||||
|                 return False | ||||
							
								
								
									
										219
									
								
								backend/dvadmin/utils/request_util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								backend/dvadmin/utils/request_util.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,219 @@ | ||||
| """ | ||||
| Request工具类 | ||||
| """ | ||||
| import json | ||||
|  | ||||
| import requests | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import AbstractBaseUser | ||||
| from django.contrib.auth.models import AnonymousUser | ||||
| from django.urls.resolvers import ResolverMatch | ||||
| from rest_framework_simplejwt.authentication import JWTAuthentication | ||||
| from user_agents import parse | ||||
|  | ||||
| from dvadmin.system.models import LoginLog | ||||
|  | ||||
|  | ||||
| def get_request_user(request): | ||||
|     """ | ||||
|     获取请求user | ||||
|     (1)如果request里的user没有认证,那么则手动认证一次 | ||||
|     :param request: | ||||
|     :return: | ||||
|     """ | ||||
|     user: AbstractBaseUser = getattr(request, 'user', None) | ||||
|     if user and user.is_authenticated: | ||||
|         return user | ||||
|     try: | ||||
|         user, tokrn = JWTAuthentication().authenticate(request) | ||||
|     except Exception as e: | ||||
|         pass | ||||
|     return user or AnonymousUser() | ||||
|  | ||||
|  | ||||
| def get_request_ip(request): | ||||
|     """ | ||||
|     获取请求IP | ||||
|     :param request: | ||||
|     :return: | ||||
|     """ | ||||
|     x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '') | ||||
|     if x_forwarded_for: | ||||
|         ip = x_forwarded_for.split(',')[-1].strip() | ||||
|         return ip | ||||
|     ip = request.META.get('REMOTE_ADDR', '') or getattr(request, 'request_ip', None) | ||||
|     return ip or 'unknown' | ||||
|  | ||||
|  | ||||
| def get_request_data(request): | ||||
|     """ | ||||
|     获取请求参数 | ||||
|     :param request: | ||||
|     :return: | ||||
|     """ | ||||
|     request_data = getattr(request, 'request_data', None) | ||||
|     if request_data: | ||||
|         return request_data | ||||
|     data: dict = {**request.GET.dict(), **request.POST.dict()} | ||||
|     if not data: | ||||
|         try: | ||||
|             body = request.body | ||||
|             if body: | ||||
|                 data = json.loads(body) | ||||
|         except Exception as e: | ||||
|             pass | ||||
|         if not isinstance(data, dict): | ||||
|             data = {'data': data} | ||||
|     return data | ||||
|  | ||||
|  | ||||
| def get_request_path(request, *args, **kwargs): | ||||
|     """ | ||||
|     获取请求路径 | ||||
|     :param request: | ||||
|     :param args: | ||||
|     :param kwargs: | ||||
|     :return: | ||||
|     """ | ||||
|     request_path = getattr(request, 'request_path', None) | ||||
|     if request_path: | ||||
|         return request_path | ||||
|     values = [] | ||||
|     for arg in args: | ||||
|         if len(arg) == 0: | ||||
|             continue | ||||
|         if isinstance(arg, str): | ||||
|             values.append(arg) | ||||
|         elif isinstance(arg, (tuple, set, list)): | ||||
|             values.extend(arg) | ||||
|         elif isinstance(arg, dict): | ||||
|             values.extend(arg.values()) | ||||
|     if len(values) == 0: | ||||
|         return request.path | ||||
|     path: str = request.path | ||||
|     for value in values: | ||||
|         path = path.replace('/' + value, '/' + '{id}') | ||||
|     return path | ||||
|  | ||||
|  | ||||
| def get_request_canonical_path(request, ): | ||||
|     """ | ||||
|     获取请求路径 | ||||
|     :param request: | ||||
|     :param args: | ||||
|     :param kwargs: | ||||
|     :return: | ||||
|     """ | ||||
|     request_path = getattr(request, 'request_canonical_path', None) | ||||
|     if request_path: | ||||
|         return request_path | ||||
|     path: str = request.path | ||||
|     resolver_match: ResolverMatch = request.resolver_match | ||||
|     for value in resolver_match.args: | ||||
|         path = path.replace(f"/{value}", "/{id}") | ||||
|     for key, value in resolver_match.kwargs.items(): | ||||
|         if key == 'pk': | ||||
|             path = path.replace(f"/{value}", f"/{{id}}") | ||||
|             continue | ||||
|         path = path.replace(f"/{value}", f"/{{{key}}}") | ||||
|  | ||||
|     return path | ||||
|  | ||||
|  | ||||
| def get_browser(request, ): | ||||
|     """ | ||||
|     获取浏览器名 | ||||
|     :param request: | ||||
|     :param args: | ||||
|     :param kwargs: | ||||
|     :return: | ||||
|     """ | ||||
|     ua_string = request.META['HTTP_USER_AGENT'] | ||||
|     user_agent = parse(ua_string) | ||||
|     return user_agent.get_browser() | ||||
|  | ||||
|  | ||||
| def get_os(request, ): | ||||
|     """ | ||||
|     获取操作系统 | ||||
|     :param request: | ||||
|     :param args: | ||||
|     :param kwargs: | ||||
|     :return: | ||||
|     """ | ||||
|     ua_string = request.META['HTTP_USER_AGENT'] | ||||
|     user_agent = parse(ua_string) | ||||
|     return user_agent.get_os() | ||||
|  | ||||
|  | ||||
| def get_verbose_name(queryset=None, view=None, model=None): | ||||
|     """ | ||||
|     获取 verbose_name | ||||
|     :param request: | ||||
|     :param view: | ||||
|     :return: | ||||
|     """ | ||||
|     try: | ||||
|         if queryset is not None and hasattr(queryset, 'model'): | ||||
|             model = queryset.model | ||||
|         elif view and hasattr(view.get_queryset(), 'model'): | ||||
|             model = view.get_queryset().model | ||||
|         elif view and hasattr(view.get_serializer(), 'Meta') and hasattr(view.get_serializer().Meta, 'model'): | ||||
|             model = view.get_serializer().Meta.model | ||||
|         if model: | ||||
|             return getattr(model, '_meta').verbose_name | ||||
|         else: | ||||
|             model = queryset.model._meta.verbose_name | ||||
|     except Exception as e: | ||||
|         pass | ||||
|     return model if model else "" | ||||
|  | ||||
|  | ||||
| def get_ip_analysis(ip): | ||||
|     """ | ||||
|     获取ip详细概略 | ||||
|     :param ip: ip地址 | ||||
|     :return: | ||||
|     """ | ||||
|     data = { | ||||
|         "continent": "", | ||||
|         "country": "", | ||||
|         "province": "", | ||||
|         "city": "", | ||||
|         "district": "", | ||||
|         "isp": "", | ||||
|         "area_code": "", | ||||
|         "country_english": "", | ||||
|         "country_code": "", | ||||
|         "longitude": "", | ||||
|         "latitude": "" | ||||
|     } | ||||
|     if ip != 'unknown' and ip: | ||||
|         if getattr(settings, 'ENABLE_LOGIN_ANALYSIS_LOG', True): | ||||
|             try: | ||||
|                 res = requests.get(url='https://ip.django-vue-admin.com/ip/analysis', params={"ip": ip}, timeout=5) | ||||
|                 if res.status_code == 200: | ||||
|                     res_data = res.json() | ||||
|                     if res_data.get('code') == 0: | ||||
|                         data = res_data.get('data') | ||||
|                 return data | ||||
|             except Exception as e: | ||||
|                 print(e) | ||||
|     return data | ||||
|  | ||||
|  | ||||
| def save_login_log(request): | ||||
|     """ | ||||
|     保存登录日志 | ||||
|     :return: | ||||
|     """ | ||||
|     ip = get_request_ip(request=request) | ||||
|     analysis_data = get_ip_analysis(ip) | ||||
|     analysis_data['username'] = request.user.username | ||||
|     analysis_data['ip'] = ip | ||||
|     analysis_data['agent'] = str(parse(request.META['HTTP_USER_AGENT'])) | ||||
|     analysis_data['browser'] = get_browser(request) | ||||
|     analysis_data['os'] = get_os(request) | ||||
|     analysis_data['creator_id'] = request.user.id | ||||
|     analysis_data['dept_belong_id'] = getattr(request.user, 'dept_id', '') | ||||
|     LoginLog.objects.create(**analysis_data) | ||||
							
								
								
									
										170
									
								
								backend/dvadmin/utils/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								backend/dvadmin/utils/serializers.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,170 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/1 001 22:47 | ||||
| @Remark: 自定义序列化器 | ||||
| """ | ||||
| from rest_framework import serializers | ||||
| from rest_framework.fields import empty | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from django.utils.functional import cached_property | ||||
| from rest_framework.utils.serializer_helpers import BindingDict | ||||
|  | ||||
| from dvadmin.system.models import Users | ||||
| from django_restql.mixins import DynamicFieldsMixin | ||||
|  | ||||
|  | ||||
| class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer): | ||||
|     """ | ||||
|     增强DRF的ModelSerializer,可自动更新模型的审计字段记录 | ||||
|     (1)self.request能获取到rest_framework.request.Request对象 | ||||
|     """ | ||||
|  | ||||
|     # 修改人的审计字段名称, 默认modifier, 继承使用时可自定义覆盖 | ||||
|     modifier_field_id = "modifier" | ||||
|     modifier_name = serializers.SerializerMethodField(read_only=True) | ||||
|  | ||||
|     def get_modifier_name(self, instance): | ||||
|         if not hasattr(instance, "modifier"): | ||||
|             return None | ||||
|         queryset = ( | ||||
|             Users.objects.filter(id=instance.modifier) | ||||
|             .values_list("name", flat=True) | ||||
|             .first() | ||||
|         ) | ||||
|         if queryset: | ||||
|             return queryset | ||||
|         return None | ||||
|  | ||||
|     # 创建人的审计字段名称, 默认creator, 继承使用时可自定义覆盖 | ||||
|     creator_field_id = "creator" | ||||
|     creator_name = serializers.SlugRelatedField( | ||||
|         slug_field="name", source="creator", read_only=True | ||||
|     ) | ||||
|     # 数据所属部门字段 | ||||
|     dept_belong_id_field_name = "dept_belong_id" | ||||
|     # 添加默认时间返回格式 | ||||
|     create_datetime = serializers.DateTimeField( | ||||
|         format="%Y-%m-%d %H:%M:%S", required=False, read_only=True | ||||
|     ) | ||||
|     update_datetime = serializers.DateTimeField( | ||||
|         format="%Y-%m-%d %H:%M:%S", required=False, read_only=True | ||||
|     ) | ||||
|  | ||||
|     def __init__(self, instance=None, data=empty, request=None, **kwargs): | ||||
|         super().__init__(instance, data, **kwargs) | ||||
|         self.request: Request = request or self.context.get("request", None) | ||||
|  | ||||
|     def save(self, **kwargs): | ||||
|         return super().save(**kwargs) | ||||
|  | ||||
|     def create(self, validated_data): | ||||
|         if self.request: | ||||
|             if str(self.request.user) != "AnonymousUser": | ||||
|                 if self.modifier_field_id in self.fields.fields: | ||||
|                     validated_data[self.modifier_field_id] = self.get_request_user_id() | ||||
|                 if self.creator_field_id in self.fields.fields: | ||||
|                     validated_data[self.creator_field_id] = self.request.user | ||||
|  | ||||
|                 if ( | ||||
|                         self.dept_belong_id_field_name in self.fields.fields | ||||
|                         and validated_data.get(self.dept_belong_id_field_name, None) is None | ||||
|                 ): | ||||
|                     validated_data[self.dept_belong_id_field_name] = getattr( | ||||
|                         self.request.user, "dept_id", validated_data.get(self.dept_belong_id_field_name, None) | ||||
|                     ) | ||||
|         return super().create(validated_data) | ||||
|  | ||||
|     def update(self, instance, validated_data): | ||||
|         if self.request: | ||||
|             if str(self.request.user) != "AnonymousUser": | ||||
|                 if self.modifier_field_id in self.fields.fields: | ||||
|                     validated_data[self.modifier_field_id] = self.get_request_user_id() | ||||
|             if hasattr(self.instance, self.modifier_field_id): | ||||
|                 setattr( | ||||
|                     self.instance, self.modifier_field_id, self.get_request_user_id() | ||||
|                 ) | ||||
|         return super().update(instance, validated_data) | ||||
|  | ||||
|     def get_request_username(self): | ||||
|         if getattr(self.request, "user", None): | ||||
|             return getattr(self.request.user, "username", None) | ||||
|         return None | ||||
|  | ||||
|     def get_request_name(self): | ||||
|         if getattr(self.request, "user", None): | ||||
|             return getattr(self.request.user, "name", None) | ||||
|         return None | ||||
|  | ||||
|     def get_request_user_id(self): | ||||
|         if getattr(self.request, "user", None): | ||||
|             return getattr(self.request.user, "id", None) | ||||
|         return None | ||||
|  | ||||
|     @property | ||||
|     def errors(self): | ||||
|         # get errors | ||||
|         errors = super().errors | ||||
|         verbose_errors = {} | ||||
|  | ||||
|         # fields = { field.name: field.verbose_name } for each field in model | ||||
|         fields = {field.name: field.verbose_name for field in | ||||
|                   self.Meta.model._meta.get_fields() if hasattr(field, 'verbose_name')} | ||||
|  | ||||
|         # iterate over errors and replace error key with verbose name if exists | ||||
|         for field_name, error in errors.items(): | ||||
|             if field_name in fields: | ||||
|                 verbose_errors[str(fields[field_name])] = error | ||||
|             else: | ||||
|                 verbose_errors[field_name] = error | ||||
|         return verbose_errors | ||||
|  | ||||
|     # @cached_property | ||||
|     # def fields(self): | ||||
|     #     fields = BindingDict(self) | ||||
|     #     for key, value in self.get_fields().items(): | ||||
|     #         fields[key] = value | ||||
|     # | ||||
|     #     if not hasattr(self, '_context'): | ||||
|     #         return fields | ||||
|     #     is_root = self.root == self | ||||
|     #     parent_is_list_root = self.parent == self.root and getattr(self.parent, 'many', False) | ||||
|     #     if not (is_root or parent_is_list_root): | ||||
|     #         return fields | ||||
|     # | ||||
|     #     try: | ||||
|     #         request = self.request or self.context['request'] | ||||
|     #     except KeyError: | ||||
|     #         return fields | ||||
|     #     params = getattr( | ||||
|     #         request, 'query_params', getattr(request, 'GET', None) | ||||
|     #     ) | ||||
|     #     if params is None: | ||||
|     #         pass | ||||
|     #     try: | ||||
|     #         filter_fields = params.get('_fields', None).split(',') | ||||
|     #     except AttributeError: | ||||
|     #         filter_fields = None | ||||
|     # | ||||
|     #     try: | ||||
|     #         omit_fields = params.get('_exclude', None).split(',') | ||||
|     #     except AttributeError: | ||||
|     #         omit_fields = [] | ||||
|     # | ||||
|     #     existing = set(fields.keys()) | ||||
|     #     if filter_fields is None: | ||||
|     #         allowed = existing | ||||
|     #     else: | ||||
|     #         allowed = set(filter(None, filter_fields)) | ||||
|     # | ||||
|     #     omitted = set(filter(None, omit_fields)) | ||||
|     #     for field in existing: | ||||
|     #         if field not in allowed: | ||||
|     #             fields.pop(field, None) | ||||
|     #         if field in omitted: | ||||
|     #             fields.pop(field, None) | ||||
|     # | ||||
|     #     return fields | ||||
							
								
								
									
										42
									
								
								backend/dvadmin/utils/string_util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								backend/dvadmin/utils/string_util.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/8/21 021 9:48 | ||||
| @Remark: | ||||
| """ | ||||
| import hashlib | ||||
| import random | ||||
|  | ||||
| CHAR_SET = ("2", "3", "4", "5", | ||||
|             "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", | ||||
|             "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", | ||||
|             "W", "X", "Y", "Z") | ||||
|  | ||||
|  | ||||
| def random_str(number=16): | ||||
|     """ | ||||
|     返回特定长度的随机字符串(非进制) | ||||
|     :return: | ||||
|     """ | ||||
|     result = "" | ||||
|     for i in range(0, number): | ||||
|         inx = random.randint(0, len(CHAR_SET) - 1) | ||||
|         result += CHAR_SET[inx] | ||||
|     return result | ||||
|  | ||||
|  | ||||
| def has_md5(str, salt='123456'): | ||||
|     """ | ||||
|     md5 加密 | ||||
|     :param str: | ||||
|     :param salt: | ||||
|     :return: | ||||
|     """ | ||||
|     # satl是盐值,默认是123456 | ||||
|     str = str + salt | ||||
|     md = hashlib.md5()  # 构造一个md5对象 | ||||
|     md.update(str.encode()) | ||||
|     res = md.hexdigest() | ||||
|     return res | ||||
							
								
								
									
										46
									
								
								backend/dvadmin/utils/swagger.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								backend/dvadmin/utils/swagger.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/8/12 012 10:25 | ||||
| @Remark: swagger配置 | ||||
| """ | ||||
| from drf_yasg.generators import OpenAPISchemaGenerator | ||||
| from drf_yasg.inspectors import SwaggerAutoSchema | ||||
|  | ||||
| from application.settings import SWAGGER_SETTINGS | ||||
|  | ||||
|  | ||||
| def get_summary(string): | ||||
|     if string is not None: | ||||
|         result = string.strip().replace(" ","").split("\n") | ||||
|         return result[0] | ||||
|  | ||||
| class CustomSwaggerAutoSchema(SwaggerAutoSchema): | ||||
|     def get_tags(self, operation_keys=None): | ||||
|         tags = super().get_tags(operation_keys) | ||||
|         if "api" in tags and operation_keys: | ||||
|             #  `operation_keys` 内容像这样 ['v1', 'prize_join_log', 'create'] | ||||
|             tags[0] = operation_keys[SWAGGER_SETTINGS.get('AUTO_SCHEMA_TYPE', 2)] | ||||
|         return tags | ||||
|  | ||||
|     def get_summary_and_description(self): | ||||
|         summary_and_description = super().get_summary_and_description() | ||||
|         summary = get_summary(self.__dict__.get('view').__doc__) | ||||
|         description = summary_and_description[1] | ||||
|         return summary,description | ||||
|  | ||||
|  | ||||
| class CustomOpenAPISchemaGenerator(OpenAPISchemaGenerator): | ||||
|     def get_schema(self, request=None, public=False): | ||||
|         """Generate a :class:`.Swagger` object with custom tags""" | ||||
|  | ||||
|         swagger = super().get_schema(request, public) | ||||
|         swagger.tags = [ | ||||
|             { | ||||
|                 "name": "token", | ||||
|                 "description": "认证相关" | ||||
|             }, | ||||
|         ] | ||||
|         return swagger | ||||
							
								
								
									
										73
									
								
								backend/dvadmin/utils/validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								backend/dvadmin/utils/validator.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/2 002 17:03 | ||||
| @Remark: 自定义验证器 | ||||
| """ | ||||
|  | ||||
| from django.db import DataError | ||||
| from rest_framework.exceptions import APIException | ||||
| from rest_framework.validators import UniqueValidator | ||||
|  | ||||
|  | ||||
| class CustomValidationError(APIException): | ||||
|     """ | ||||
|     继承并重写验证器返回的结果,避免暴露字段 | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, detail): | ||||
|         self.detail = detail | ||||
|  | ||||
|  | ||||
| def qs_exists(queryset): | ||||
|     try: | ||||
|         return queryset.exists() | ||||
|     except (TypeError, ValueError, DataError): | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def qs_filter(queryset, **kwargs): | ||||
|     try: | ||||
|         return queryset.filter(**kwargs) | ||||
|     except (TypeError, ValueError, DataError): | ||||
|         return queryset.none() | ||||
|  | ||||
|  | ||||
| class CustomUniqueValidator(UniqueValidator): | ||||
|     """ | ||||
|     继承,重写必填字段的验证器结果,防止字段暴露 | ||||
|     """ | ||||
|  | ||||
|     def filter_queryset(self, value, queryset, field_name): | ||||
|         """ | ||||
|         Filter the queryset to all instances matching the given attribute. | ||||
|         """ | ||||
|         filter_kwargs = {'%s__%s' % (field_name, self.lookup): value} | ||||
|         return qs_filter(queryset, **filter_kwargs) | ||||
|  | ||||
|     def exclude_current_instance(self, queryset, instance): | ||||
|         """ | ||||
|         If an instance is being updated, then do not include | ||||
|         that instance itself as a uniqueness conflict. | ||||
|         """ | ||||
|         if instance is not None: | ||||
|             return queryset.exclude(pk=instance.pk) | ||||
|         return queryset | ||||
|  | ||||
|     def __call__(self, value, serializer_field): | ||||
|         # Determine the underlying model field name. This may not be the | ||||
|         # same as the serializer field name if `source=<>` is set. | ||||
|         field_name = serializer_field.source_attrs[-1] | ||||
|         # Determine the existing instance, if this is an update operation. | ||||
|         instance = getattr(serializer_field.parent, 'instance', None) | ||||
|  | ||||
|         queryset = self.queryset | ||||
|         queryset = self.filter_queryset(value, queryset, field_name) | ||||
|         queryset = self.exclude_current_instance(queryset, instance) | ||||
|         if qs_exists(queryset): | ||||
|             raise CustomValidationError(self.message) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return super().__repr__() | ||||
							
								
								
									
										149
									
								
								backend/dvadmin/utils/viewset.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								backend/dvadmin/utils/viewset.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,149 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @author: 猿小天 | ||||
| @contact: QQ:1638245306 | ||||
| @Created on: 2021/6/1 001 22:57 | ||||
| @Remark: 自定义视图集 | ||||
| """ | ||||
| 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, 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 | ||||
|  | ||||
|  | ||||
| class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMixin, QueryArgumentsMixin): | ||||
|     """ | ||||
|     自定义的ModelViewSet: | ||||
|     统一标准的返回格式;新增,查询,修改可使用不同序列化器 | ||||
|     (1)ORM性能优化, 尽可能使用values_queryset形式 | ||||
|     (2)xxx_serializer_class 某个方法下使用的序列化器(xxx=create|update|list|retrieve|destroy) | ||||
|     (3)filter_fields = '__all__' 默认支持全部model中的字段查询(除json字段外) | ||||
|     (4)import_field_dict={} 导入时的字段字典 {model值: model的label} | ||||
|     (5)export_field_label = [] 导出时的字段 | ||||
|     """ | ||||
|     values_queryset = None | ||||
|     ordering_fields = '__all__' | ||||
|     create_serializer_class = None | ||||
|     update_serializer_class = None | ||||
|     filter_fields = '__all__' | ||||
|     search_fields = () | ||||
|     extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionsFilter] | ||||
|     permission_classes = [CustomPermission] | ||||
|     import_field_dict = {} | ||||
|     export_field_label = {} | ||||
|  | ||||
|     def filter_queryset(self, queryset): | ||||
|         for backend in set(set(self.filter_backends) | set(self.extra_filter_class or [])): | ||||
|             queryset = backend().filter_queryset(self.request, queryset, self) | ||||
|         return queryset | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         if getattr(self, 'values_queryset', None): | ||||
|             return self.values_queryset | ||||
|         return super().get_queryset() | ||||
|  | ||||
|     def get_serializer_class(self): | ||||
|         action_serializer_name = f"{self.action}_serializer_class" | ||||
|         action_serializer_class = getattr(self, action_serializer_name, None) | ||||
|         if action_serializer_class: | ||||
|             return action_serializer_class | ||||
|         return super().get_serializer_class() | ||||
|  | ||||
|     # 通过many=True直接改造原有的API,使其可以批量创建 | ||||
|     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) | ||||
|         self.perform_create(serializer) | ||||
|         return DetailResponse(data=serializer.data, msg="新增成功") | ||||
|  | ||||
|     def list(self, request, *args, **kwargs): | ||||
|         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="获取成功") | ||||
|  | ||||
|     def retrieve(self, request, *args, **kwargs): | ||||
|         instance = self.get_object() | ||||
|         serializer = self.get_serializer(instance) | ||||
|         return DetailResponse(data=serializer.data, msg="获取成功") | ||||
|  | ||||
|     def update(self, request, *args, **kwargs): | ||||
|         partial = kwargs.pop('partial', False) | ||||
|         instance = self.get_object() | ||||
|         serializer = self.get_serializer(instance, data=request.data, request=request, partial=partial) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         self.perform_update(serializer) | ||||
|  | ||||
|         if getattr(instance, '_prefetched_objects_cache', None): | ||||
|             # If 'prefetch_related' has been applied to a queryset, we need to | ||||
|             # forcibly invalidate the prefetch cache on the instance. | ||||
|             instance._prefetched_objects_cache = {} | ||||
|         return DetailResponse(data=serializer.data, msg="更新成功") | ||||
|  | ||||
|     def destroy(self, request, *args, **kwargs): | ||||
|         instance = self.get_object() | ||||
|         instance.delete() | ||||
|         return DetailResponse(data=[], msg="删除成功") | ||||
|  | ||||
|     keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.TYPE_STRING) | ||||
|  | ||||
|     @swagger_auto_schema(request_body=openapi.Schema( | ||||
|         type=openapi.TYPE_OBJECT, | ||||
|         required=['keys'], | ||||
|         properties={'keys': keys} | ||||
|     ), operation_summary='批量删除') | ||||
|     @action(methods=['delete'], detail=False) | ||||
|     def multiple_delete(self, request, *args, **kwargs): | ||||
|         request_data = request.data | ||||
|         keys = request_data.get('keys', None) | ||||
|         if keys: | ||||
|             self.get_queryset().filter(id__in=keys).delete() | ||||
|             return SuccessResponse(data=[], msg="删除成功") | ||||
|         else: | ||||
|             return ErrorResponse(msg="未获取到keys字段") | ||||
							
								
								
									
										6419
									
								
								backend/fyapi.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6419
									
								
								backend/fyapi.sql
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										48
									
								
								backend/gunicorn_conf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								backend/gunicorn_conf.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| # gunicorn.conf | ||||
| # coding:utf-8 | ||||
| # 启动命令:gunicorn -c gunicorn.py application.asgi:application | ||||
| import multiprocessing | ||||
| # 并行工作进程数, int,cpu数量*2+1 推荐进程数 | ||||
| workers = multiprocessing.cpu_count() * 2 + 1 | ||||
| # 指定每个进程开启的线程数 | ||||
| threads = 3 | ||||
| # 绑定的ip与端口 | ||||
| bind = '0.0.0.0:8000' | ||||
| # 设置守护进程,将进程交给第三方管理 | ||||
| daemon = 'false' | ||||
| # 工作模式协程,默认的是sync模式,推荐使用 gevent,此处使用与uvicorn配合使用 uvicorn.workers.UvicornWorker | ||||
| worker_class = 'uvicorn.workers.UvicornWorker' | ||||
| # 设置最大并发量(每个worker处理请求的工作线程数,正整数,默认为1) | ||||
| worker_connections = 10000 | ||||
| # 最大客户端并发数量,默认情况下这个值为1000。此设置将影响gevent和eventlet工作模式 | ||||
| # 每个工作进程将在处理max_requests请求后自动重新启动该进程 | ||||
| max_requests = 10000 | ||||
| max_requests_jitter = 200 | ||||
| # 设置进程文件目录 | ||||
| pidfile = './gunicorn.pid' | ||||
| # 日志级别,这个日志级别指的是错误日志的级别,而访问日志的级别无法设置 | ||||
| loglevel = 'info' | ||||
| # 设置gunicorn访问日志格式,错误日志无法设置 | ||||
| access_log_format = '' # worker_class 为 uvicorn.workers.UvicornWorker 时,日志格式为Django的loggers | ||||
| # 监听队列 | ||||
| backlog = 512 | ||||
| #进程名 | ||||
| proc_name = 'gunicorn_process' | ||||
| # 设置超时时间120s,默认为30s。按自己的需求进行设置timeout = 120 | ||||
| timeout = 120 | ||||
| # 超时重启 | ||||
| graceful_timeout = 300 | ||||
| # 在keep-alive连接上等待请求的秒数,默认情况下值为2。一般设定在1~5秒之间。 | ||||
| keepalive = 3 | ||||
| # HTTP请求行的最大大小,此参数用于限制HTTP请求行的允许大小,默认情况下,这个值为4094。 | ||||
| # 值是0~8190的数字。此参数可以防止任何DDOS攻击 | ||||
| limit_request_line = 5120 | ||||
| # 限制HTTP请求中请求头字段的数量。 | ||||
| #  此字段用于限制请求头字段的数量以防止DDOS攻击,与limit-request-field-size一起使用可以提高安全性。 | ||||
| # 默认情况下,这个值为100,这个值不能超过32768 | ||||
| limit_request_fields = 101 | ||||
| # 限制HTTP请求中请求头的大小,默认情况下这个值为8190。 | ||||
| # 值是一个整数或者0,当该值为0时,表示将对请求头大小不做限制 | ||||
| limit_request_field_size = 0 | ||||
| # 记录到标准输出 | ||||
| accesslog = '-' | ||||
							
								
								
									
										0
									
								
								backend/logs/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/logs/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										17
									
								
								backend/main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								backend/main.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| import multiprocessing | ||||
| import os | ||||
| import sys | ||||
|  | ||||
| root_path = os.getcwd() | ||||
| sys.path.append(root_path) | ||||
| import uvicorn | ||||
| from application.settings import LOGGING | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     multiprocessing.freeze_support() | ||||
|     workers = 4 | ||||
|     if os.sys.platform.startswith('win'): | ||||
|         # Windows操作系统 | ||||
|         workers = None | ||||
|     uvicorn.run("application.asgi:application", reload=False, host="0.0.0.0", port=8000, workers=workers, | ||||
|                 log_config=LOGGING) | ||||
							
								
								
									
										22
									
								
								backend/manage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								backend/manage.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| #!/usr/bin/env python | ||||
| """Django's command-line utility for administrative tasks.""" | ||||
| import os | ||||
| import sys | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     """Run administrative tasks.""" | ||||
|     os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') | ||||
|     try: | ||||
|         from django.core.management import execute_from_command_line | ||||
|     except ImportError as exc: | ||||
|         raise ImportError( | ||||
|             "Couldn't import Django. Are you sure it's installed and " | ||||
|             "available on your PYTHONPATH environment variable? Did you " | ||||
|             "forget to activate a virtual environment?" | ||||
|         ) from exc | ||||
|     execute_from_command_line(sys.argv) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
							
								
								
									
										1
									
								
								backend/plugins/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								backend/plugins/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
							
								
								
									
										34
									
								
								backend/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								backend/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| Django==4.2.14 | ||||
| django-comment-migrate==0.1.7 | ||||
| django-cors-headers==4.4.0 | ||||
| django-filter==24.2 | ||||
| django-ranged-response==0.2.0 | ||||
| djangorestframework==3.15.2 | ||||
| django-restql==0.15.4 | ||||
| django-simple-captcha==0.6.0 | ||||
| django-timezone-field==7.0 | ||||
| djangorestframework-simplejwt==5.3.1 | ||||
| drf-yasg==1.21.7 | ||||
| mysqlclient==2.2.0 | ||||
| pypinyin==0.51.0 | ||||
| ua-parser==0.18.0 | ||||
| pyparsing==3.1.2 | ||||
| openpyxl==3.1.5 | ||||
| requests==2.32.3 | ||||
| typing-extensions==4.12.2 | ||||
| tzlocal==5.2 | ||||
| channels==4.1.0 | ||||
| channels-redis==4.2.0 | ||||
| websockets==11.0.3 | ||||
| user-agents==2.2.0 | ||||
| six==1.16.0 | ||||
| whitenoise==6.7.0 | ||||
| uvicorn==0.30.3 | ||||
| gunicorn==22.0.0 | ||||
| gevent==24.2.1 | ||||
| Pillow==10.4.0 | ||||
| pyinstaller==6.9.0 | ||||
| asgiref~=3.8.1 | ||||
| volcengine | ||||
| celery~=5.4.0 | ||||
| PyJWT~=2.10.1 | ||||
							
								
								
									
										123
									
								
								backend/static/captcha/fonts/COPYRIGHT.TXT
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								backend/static/captcha/fonts/COPYRIGHT.TXT
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | ||||
| Bitstream Vera Fonts Copyright | ||||
|  | ||||
| The fonts have a generous copyright, allowing derivative works (as | ||||
| long as "Bitstream" or "Vera" are not in the names), and full | ||||
| redistribution (so long as they are not *sold* by themselves). They | ||||
| can be be bundled, redistributed and sold with any software. | ||||
|  | ||||
| The fonts are distributed under the following copyright: | ||||
|  | ||||
| Copyright | ||||
| ========= | ||||
|  | ||||
| Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream | ||||
| Vera is a trademark of Bitstream, Inc. | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining | ||||
| a copy of the fonts accompanying this license ("Fonts") and associated | ||||
| documentation files (the "Font Software"), to reproduce and distribute | ||||
| the Font Software, including without limitation the rights to use, | ||||
| copy, merge, publish, distribute, and/or sell copies of the Font | ||||
| Software, and to permit persons to whom the Font Software is furnished | ||||
| to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright and trademark notices and this permission notice | ||||
| shall be included in all copies of one or more of the Font Software | ||||
| typefaces. | ||||
|  | ||||
| The Font Software may be modified, altered, or added to, and in | ||||
| particular the designs of glyphs or characters in the Fonts may be | ||||
| modified and additional glyphs or characters may be added to the | ||||
| Fonts, only if the fonts are renamed to names not containing either | ||||
| the words "Bitstream" or the word "Vera". | ||||
|  | ||||
| This License becomes null and void to the extent applicable to Fonts | ||||
| or Font Software that has been modified and is distributed under the | ||||
| "Bitstream Vera" names. | ||||
|  | ||||
| The Font Software may be sold as part of a larger software package but | ||||
| no copy of one or more of the Font Software typefaces may be sold by | ||||
| itself. | ||||
|  | ||||
| THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | ||||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | ||||
| OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL | ||||
| BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR | ||||
| OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, | ||||
| OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||||
| OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT | ||||
| SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. | ||||
|  | ||||
| Except as contained in this notice, the names of Gnome, the Gnome | ||||
| Foundation, and Bitstream Inc., shall not be used in advertising or | ||||
| otherwise to promote the sale, use or other dealings in this Font | ||||
| Software without prior written authorization from the Gnome Foundation | ||||
| or Bitstream Inc., respectively. For further information, contact: | ||||
| fonts at gnome dot org. | ||||
|  | ||||
| Copyright FAQ | ||||
| ============= | ||||
|  | ||||
|    1. I don't understand the resale restriction... What gives? | ||||
|  | ||||
|       Bitstream is giving away these fonts, but wishes to ensure its | ||||
|       competitors can't just drop the fonts as is into a font sale system | ||||
|       and sell them as is. It seems fair that if Bitstream can't make money | ||||
|       from the Bitstream Vera fonts, their competitors should not be able to | ||||
|       do so either. You can sell the fonts as part of any software package, | ||||
|       however. | ||||
|  | ||||
|    2. I want to package these fonts separately for distribution and | ||||
|       sale as part of a larger software package or system.  Can I do so? | ||||
|  | ||||
|       Yes. A RPM or Debian package is a "larger software package" to begin | ||||
|       with, and you aren't selling them independently by themselves. | ||||
|       See 1. above. | ||||
|  | ||||
|    3. Are derivative works allowed? | ||||
|       Yes! | ||||
|  | ||||
|    4. Can I change or add to the font(s)? | ||||
|       Yes, but you must change the name(s) of the font(s). | ||||
|  | ||||
|    5. Under what terms are derivative works allowed? | ||||
|  | ||||
|       You must change the name(s) of the fonts. This is to ensure the | ||||
|       quality of the fonts, both to protect Bitstream and Gnome. We want to | ||||
|       ensure that if an application has opened a font specifically of these | ||||
|       names, it gets what it expects (though of course, using fontconfig, | ||||
|       substitutions could still could have occurred during font | ||||
|       opening). You must include the Bitstream copyright. Additional | ||||
|       copyrights can be added, as per copyright law. Happy Font Hacking! | ||||
|  | ||||
|    6. If I have improvements for Bitstream Vera, is it possible they might get | ||||
|        adopted in future versions? | ||||
|  | ||||
|       Yes. The contract between the Gnome Foundation and Bitstream has | ||||
|       provisions for working with Bitstream to ensure quality additions to | ||||
|       the Bitstream Vera font family. Please contact us if you have such | ||||
|       additions. Note, that in general, we will want such additions for the | ||||
|       entire family, not just a single font, and that you'll have to keep | ||||
|       both Gnome and Jim Lyles, Vera's designer, happy! To make sense to add | ||||
|       glyphs to the font, they must be stylistically in keeping with Vera's | ||||
|       design. Vera cannot become a "ransom note" font. Jim Lyles will be | ||||
|       providing a document describing the design elements used in Vera, as a | ||||
|       guide and aid for people interested in contributing to Vera. | ||||
|  | ||||
|    7. I want to sell a software package that uses these fonts: Can I do so? | ||||
|  | ||||
|       Sure. Bundle the fonts with your software and sell your software | ||||
|       with the fonts. That is the intent of the copyright. | ||||
|  | ||||
|    8. If applications have built the names "Bitstream Vera" into them, | ||||
|       can I override this somehow to use fonts of my choosing? | ||||
|  | ||||
|       This depends on exact details of the software. Most open source | ||||
|       systems and software (e.g., Gnome, KDE, etc.) are now converting to | ||||
|       use fontconfig (see www.fontconfig.org) to handle font configuration, | ||||
|       selection and substitution; it has provisions for overriding font | ||||
|       names and substituting alternatives. An example is provided by the | ||||
|       supplied local.conf file, which chooses the family Bitstream Vera for | ||||
|       "sans", "serif" and "monospace".  Other software (e.g., the XFree86 | ||||
|       core server) has other mechanisms for font substitution. | ||||
							
								
								
									
										11
									
								
								backend/static/captcha/fonts/README.TXT
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/static/captcha/fonts/README.TXT
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| Contained herin is the Bitstream Vera font family. | ||||
|  | ||||
| The Copyright information is found in the COPYRIGHT.TXT file (along | ||||
| with being incorporated into the fonts themselves). | ||||
|  | ||||
| The releases notes are found in the file "RELEASENOTES.TXT". | ||||
|  | ||||
| We hope you enjoy Vera! | ||||
|  | ||||
|                         Bitstream, Inc. | ||||
| 			The Gnome Project | ||||
							
								
								
									
										
											BIN
										
									
								
								backend/static/captcha/fonts/Vera.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/static/captcha/fonts/Vera.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user