init
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
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/backend/venv
|
||||
/backend/.idea
|
||||
.idea
|
||||
|
||||
.history/
|
||||
.vscode/
|
||||
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.
|
||||
100
backend/.gitignore
vendored
Normal file
100
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
# 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
|
||||
0
backend/application/__init__.py
Normal file
0
backend/application/__init__.py
Normal file
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
|
||||
)
|
||||
)
|
||||
),
|
||||
})
|
||||
18
backend/application/celery.py
Normal file
18
backend/application/celery.py
Normal file
@@ -0,0 +1,18 @@
|
||||
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')
|
||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
||||
platforms.C_FORCE_ROOT = True
|
||||
222
backend/application/dispatch.py
Normal file
222
backend/application/dispatch.py
Normal file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
|
||||
|
||||
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 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 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 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 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 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:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
children = get_dictionary_values(key, schema_name) or []
|
||||
for ele in 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 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:
|
||||
"""
|
||||
system_config = get_system_config(schema_name)
|
||||
return system_config.get(key)
|
||||
|
||||
|
||||
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 ""
|
||||
8
backend/application/routing.py
Normal file
8
backend/application/routing.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# -*- 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 是该路由的消费者
|
||||
]
|
||||
|
||||
406
backend/application/settings.py
Normal file
406
backend/application/settings.py
Normal file
@@ -0,0 +1,406 @@
|
||||
"""
|
||||
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
|
||||
|
||||
# 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", ["*"])
|
||||
|
||||
# Application definition
|
||||
|
||||
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
|
||||
"dvadmin.system",
|
||||
"drf_yasg",
|
||||
"captcha",
|
||||
'channels',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"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,
|
||||
}
|
||||
}
|
||||
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找到文件
|
||||
|
||||
# 收集静态文件,必须将 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配置 ***************** #
|
||||
# ================================================= #
|
||||
from datetime import timedelta
|
||||
|
||||
SIMPLE_JWT = {
|
||||
# token有效时长
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=120),
|
||||
# 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, 60) # 设置 captcha 图片大小
|
||||
CAPTCHA_LENGTH = 4 # 字符个数
|
||||
CAPTCHA_TIMEOUT = 1 # 超时(minutes)
|
||||
CAPTCHA_OUTPUT_FORMAT = "%(image)s %(text_field)s %(hidden_field)s "
|
||||
CAPTCHA_FONT_SIZE = 40 # 字体大小
|
||||
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 dvadmin_celery.settings import * # celery 异步任务
|
||||
# from dvadmin_third.settings import * # 第三方用户管理
|
||||
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
|
||||
# from dvadmin_tenants.settings import * # 租户管理
|
||||
# ...
|
||||
# ********** 一键导入插件配置结束 **********
|
||||
88
backend/application/urls.py
Normal file
88
backend/application/urls.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
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()),
|
||||
]
|
||||
+ 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]
|
||||
)
|
||||
181
backend/application/websocketConfig.py
Normal file
181
backend/application/websocketConfig.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# -*- 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=[], target_dept=None, target_role=None,
|
||||
message: dict = {'contentType': 'INFO', 'content': '测试~'}, 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()
|
||||
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
|
||||
gunicorn -c gunicorn_conf.py application.asgi:application
|
||||
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
345
backend/dvadmin/system/fixtures/initSerializer.py
Normal file
345
backend/dvadmin/system/fixtures/initSerializer.py
Normal file
@@ -0,0 +1,345 @@
|
||||
# -*- 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
|
||||
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 MenuInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
递归深度获取数信息(用于生成初始化json文件)
|
||||
"""
|
||||
name = serializers.CharField(required=False)
|
||||
children = serializers.SerializerMethodField()
|
||||
menu_button = 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 save(self, **kwargs):
|
||||
instance = super().save(**kwargs)
|
||||
children = self.initial_data.get('children')
|
||||
menu_button = self.initial_data.get('menu_button')
|
||||
# 菜单表
|
||||
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()
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ['name', 'icon', 'sort', 'is_link', 'is_catalog', 'web_path', 'component', 'component_name', 'status',
|
||||
'cache', 'visible', 'parent', 'children', 'menu_button', 'creator', 'dept_belong_id']
|
||||
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', 'admin',
|
||||
'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_component_name = serializers.CharField(max_length=100,required=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
init_data = self.initial_data
|
||||
validated_data.pop('menu_component_name')
|
||||
validated_data.pop('role_key')
|
||||
role_id = Role.objects.filter(key=init_data['role_key']).first()
|
||||
menu_id = Menu.objects.filter(component_name=init_data['menu_component_name']).first()
|
||||
validated_data['role'] = role_id
|
||||
validated_data['menu'] = menu_id
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuPermission
|
||||
fields = ['role_key','menu_component_name','creator', 'dept_belong_id']
|
||||
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
|
||||
|
||||
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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
646
backend/dvadmin/system/fixtures/init_menu.json
Normal file
646
backend/dvadmin/system/fixtures/init_menu.json
Normal file
@@ -0,0 +1,646 @@
|
||||
[
|
||||
{
|
||||
"name": "系统管理",
|
||||
"icon": "iconfont icon-xitongshezhi",
|
||||
"sort": 1,
|
||||
"is_link": false,
|
||||
"is_catalog": true,
|
||||
"web_path": "/system",
|
||||
"component": "",
|
||||
"component_name": "",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": null,
|
||||
"children": [
|
||||
{
|
||||
"name": "菜单管理",
|
||||
"icon": "iconfont icon-caidan",
|
||||
"sort": 1,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/menu",
|
||||
"component": "system/menu/index",
|
||||
"component_name": "menu",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 41,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "menu:Search",
|
||||
"api": "/api/system/menu/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "menu:Retrieve",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "menu:Create",
|
||||
"api": "/api/system/menu/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "menu:Update",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "menu:Delete",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "菜单按钮",
|
||||
"icon": "dot-circle-o",
|
||||
"sort": 2,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/menuButton",
|
||||
"component": "system/menuButton/index",
|
||||
"component_name": "menuButton",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": false,
|
||||
"parent": 41,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "menu_button:Search",
|
||||
"api": "/api/system/menu_button/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "menu_button:Create",
|
||||
"api": "/api/system/menu_button/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "menu_button:Update",
|
||||
"api": "/api/system/menu_button/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "menu_button:Delete",
|
||||
"api": "/api/system/menu_button/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "部门管理",
|
||||
"icon": "ele-OfficeBuilding",
|
||||
"sort": 3,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/dept",
|
||||
"component": "system/dept/index",
|
||||
"component_name": "dept",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 41,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "dept:Search",
|
||||
"api": "/api/system/dept/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "dept:Retrieve",
|
||||
"api": "/api/system/dept/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "dept:Create",
|
||||
"api": "/api/system/dept/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "dept:Update",
|
||||
"api": "/api/system/dept/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "dept:Delete",
|
||||
"api": "/api/system/dept/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "角色管理",
|
||||
"icon": "ele-ColdDrink",
|
||||
"sort": 4,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/role",
|
||||
"component": "system/role/index",
|
||||
"component_name": "role",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 41,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "role:Search",
|
||||
"api": "/api/system/role/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "role:Retrieve",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "role:Create",
|
||||
"api": "/api/system/role/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "role:Update",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "保存",
|
||||
"value": "role:Save",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "role:Delete",
|
||||
"api": "/api/system/role/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "用户管理",
|
||||
"icon": "iconfont icon-icon-",
|
||||
"sort": 6,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/user",
|
||||
"component": "system/user/index",
|
||||
"component_name": "user",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 41,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "user:Search",
|
||||
"api": "/api/system/user/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "user:Retrieve",
|
||||
"api": "/api/system/user/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "user:Create",
|
||||
"api": "/api/system/user/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "导出",
|
||||
"value": "user:Export",
|
||||
"api": "/api/system/user/export/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "导入",
|
||||
"value": "user:Import",
|
||||
"api": "/api/system/user/import/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "user:Update",
|
||||
"api": "/api/system/user/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "重设密码",
|
||||
"value": "user:ResetPassword",
|
||||
"api": "/api/system/user/{id}/reset_password/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "重置密码",
|
||||
"value": "user:DefaultPassword",
|
||||
"api": "/api/system/user/{id}/reset_to_default_password/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "user:Delete",
|
||||
"api": "/api/system/user/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "消息中心",
|
||||
"icon": "iconfont icon-xiaoxizhongxin",
|
||||
"sort": 7,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/messageCenter",
|
||||
"component": "system/messageCenter/index",
|
||||
"component_name": "messageCenter",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 41,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "messageCenter:Search",
|
||||
"api": "/api/system/message_center/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "messageCenter:Retrieve",
|
||||
"api": "/api/system/message_center/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "messageCenter:Create",
|
||||
"api": "/api/system/message_center/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "messageCenter:Update",
|
||||
"api": "/api/system/message_center/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "messageCenter:Delete",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "接口白名单",
|
||||
"icon": "ele-SetUp",
|
||||
"sort": 8,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/apiWhiteList",
|
||||
"component": "system/whiteList/index",
|
||||
"component_name": "whiteList",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 41,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "api_white_list:Search",
|
||||
"api": "/api/system/api_white_list/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "api_white_list:Retrieve",
|
||||
"api": "/api/system/api_white_list/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "api_white_list:Create",
|
||||
"api": "/api/system/api_white_list/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "api_white_list:Update",
|
||||
"api": "/api/system/api_white_list/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "api_white_list:Delete",
|
||||
"api": "/api/system/api_white_list/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"menu_button": []
|
||||
},
|
||||
{
|
||||
"name": "常规配置",
|
||||
"icon": "iconfont icon-configure",
|
||||
"sort": 2,
|
||||
"is_link": false,
|
||||
"is_catalog": true,
|
||||
"web_path": "/generalConfig",
|
||||
"component": "",
|
||||
"component_name": "",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": null,
|
||||
"children": [
|
||||
{
|
||||
"name": "系统配置",
|
||||
"icon": "iconfont icon-system",
|
||||
"sort": 0,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/config",
|
||||
"component": "system/config/index",
|
||||
"component_name": "config",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 49,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "system_config:Search",
|
||||
"api": "/api/system/system_config/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "system_config:Retrieve",
|
||||
"api": "/api/system/system_config/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "system_config:Create",
|
||||
"api": "/api/system/system_config/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "system_config:Update",
|
||||
"api": "/api/system/system_config/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "system_config:Delete",
|
||||
"api": "/api/system/system_config/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "字典管理",
|
||||
"icon": "iconfont icon-dict",
|
||||
"sort": 1,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/dictionary",
|
||||
"component": "system/dictionary/index",
|
||||
"component_name": "dictionary",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 49,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "dictionary:Search",
|
||||
"api": "/api/system/dictionary/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "dictionary:Retrieve",
|
||||
"api": "/api/system/dictionary/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "dictionary:Create",
|
||||
"api": "/api/system/dictionary/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "dictionary:Update",
|
||||
"api": "/api/system/dictionary/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "dictionary:Delete",
|
||||
"api": "/api/system/dictionary/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "地区管理",
|
||||
"icon": "iconfont icon-Area",
|
||||
"sort": 2,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/areas",
|
||||
"component": "system/areas/index",
|
||||
"component_name": "areas",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 49,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "area:Search",
|
||||
"api": "/api/system/area/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "area:Retrieve",
|
||||
"api": "/api/system/area/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "area:Create",
|
||||
"api": "/api/system/area/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "area:Update",
|
||||
"api": "/api/system/area/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "area:Delete",
|
||||
"api": "/api/system/area/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "附件管理",
|
||||
"icon": "iconfont icon-file",
|
||||
"sort": 3,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/file",
|
||||
"component": "system/fileList/index",
|
||||
"component_name": "file",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 49,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "file:Retrieve",
|
||||
"api": "/api/system/file/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "file:Search",
|
||||
"api": "/api/system/file/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "file:Update",
|
||||
"api": "/api/system/file/{id}/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "file:Delete",
|
||||
"api": "/api/system/file/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"menu_button": []
|
||||
},
|
||||
{
|
||||
"name": "日志管理",
|
||||
"icon": "iconfont icon-rizhi",
|
||||
"sort": 3,
|
||||
"is_link": false,
|
||||
"is_catalog": true,
|
||||
"web_path": "/log",
|
||||
"component": "",
|
||||
"component_name": "",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": null,
|
||||
"children": [
|
||||
{
|
||||
"name": "登录日志",
|
||||
"icon": "iconfont icon-guanlidenglurizhi",
|
||||
"sort": 1,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/loginLog",
|
||||
"component": "system/log/loginLog/index",
|
||||
"component_name": "loginLog",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 54,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "login_log:Search",
|
||||
"api": "/api/system/login_log/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "login_log:Retrieve",
|
||||
"api": "/api/system/login_log/{id}/",
|
||||
"method": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "操作日志",
|
||||
"icon": "iconfont icon-caozuorizhi",
|
||||
"sort": 2,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/operationLog",
|
||||
"component": "system/log/operationLog/index",
|
||||
"component_name": "operationLog",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 54,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "operation_log:Retrieve",
|
||||
"api": "/api/system/operation_log/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "operation_log:Search",
|
||||
"api": "/api/system/operation_log/",
|
||||
"method": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"menu_button": []
|
||||
}
|
||||
]
|
||||
18
backend/dvadmin/system/fixtures/init_role.json
Normal file
18
backend/dvadmin/system/fixtures/init_role.json
Normal file
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"name": "管理员",
|
||||
"key": "admin",
|
||||
"sort": 1,
|
||||
"status": true,
|
||||
"admin": true,
|
||||
"remark": null
|
||||
},
|
||||
{
|
||||
"name": "用户",
|
||||
"key": "public",
|
||||
"sort": 2,
|
||||
"status": true,
|
||||
"admin": true,
|
||||
"remark": null
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"role_key": "admin",
|
||||
"menu_button_value": "menu:Search",
|
||||
"data_range": 0
|
||||
},
|
||||
{
|
||||
"role_key": "public",
|
||||
"menu_button_value":"menu:Search",
|
||||
"data_range": 0
|
||||
}
|
||||
]
|
||||
10
backend/dvadmin/system/fixtures/init_rolemenupermission.json
Normal file
10
backend/dvadmin/system/fixtures/init_rolemenupermission.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"role_key": "admin",
|
||||
"menu_component_name": "menu"
|
||||
},
|
||||
{
|
||||
"role_key": "public",
|
||||
"menu_component_name": "menu"
|
||||
}
|
||||
]
|
||||
197
backend/dvadmin/system/fixtures/init_systemconfig.json
Normal file
197
backend/dvadmin/system/fixtures/init_systemconfig.json
Normal file
@@ -0,0 +1,197 @@
|
||||
[
|
||||
{
|
||||
"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": "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_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-2022 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": "#",
|
||||
"sort": 7,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [],
|
||||
"placeholder": "请填写隐私链接",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"parent": 1,
|
||||
"title": "条款链接",
|
||||
"key": "clause_url",
|
||||
"value": "#",
|
||||
"sort": 8,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [],
|
||||
"placeholder": "请输入条款链接",
|
||||
"setting": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
59
backend/dvadmin/system/fixtures/init_users.json
Normal file
59
backend/dvadmin/system/fixtures/init_users.json
Normal file
@@ -0,0 +1,59 @@
|
||||
[
|
||||
{
|
||||
"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": [],
|
||||
"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
|
||||
}
|
||||
]
|
||||
86
backend/dvadmin/system/fixtures/initialize.py
Normal file
86
backend/dvadmin/system/fixtures/initialize.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# 初始化
|
||||
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', 'menu'])
|
||||
|
||||
def init_role_menu_button(self):
|
||||
"""
|
||||
初始化角色菜单按钮信息
|
||||
"""
|
||||
self.init_base(RoleMenuButtonInitSerializer, unique_fields=['role', 'menu_button'])
|
||||
|
||||
|
||||
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()))
|
||||
53
backend/dvadmin/system/management/commands/init.py
Normal file
53
backend/dvadmin/system/management/commands/init.py
Normal file
@@ -0,0 +1,53 @@
|
||||
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="*")
|
||||
|
||||
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
|
||||
|
||||
for app in settings.INSTALLED_APPS:
|
||||
|
||||
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
556
backend/dvadmin/system/models.py
Normal file
556
backend/dvadmin/system/models.py
Normal file
@@ -0,0 +1,556 @@
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from django.contrib.auth.models import AbstractUser, UserManager
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from application import dispatch
|
||||
from dvadmin.utils.models import CoreModel, table_prefix
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(0, "禁用"),
|
||||
(1, "启用"),
|
||||
)
|
||||
|
||||
|
||||
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="角色状态")
|
||||
admin = models.BooleanField(default=False, verbose_name="是否为admin", help_text="是否为admin")
|
||||
|
||||
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="关联部门",
|
||||
)
|
||||
objects = CustomUserManager()
|
||||
|
||||
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="是否外链")
|
||||
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="侧边栏中是否显示")
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_menu"
|
||||
verbose_name = "菜单表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("sort",)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
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=20, 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
|
||||
1
backend/dvadmin/system/tests.py
Normal file
1
backend/dvadmin/system/tests.py
Normal file
@@ -0,0 +1 @@
|
||||
from django.test import TestCase
|
||||
50
backend/dvadmin/system/urls.py
Normal file
50
backend/dvadmin/system/urls.py
Normal file
@@ -0,0 +1,50 @@
|
||||
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.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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
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'})),
|
||||
]
|
||||
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
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 = []
|
||||
71
backend/dvadmin/system/views/area.py
Normal file
71
backend/dvadmin/system/views/area.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.db.models import Q
|
||||
from rest_framework import serializers
|
||||
|
||||
from dvadmin.system.models import Area
|
||||
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()
|
||||
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):
|
||||
"""
|
||||
地区管理 创建/更新时的列化器
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Area
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class AreaViewSet(CustomModelViewSet):
|
||||
"""
|
||||
地区管理接口
|
||||
list:查询
|
||||
create:新增
|
||||
update:修改
|
||||
retrieve:单例
|
||||
destroy:删除
|
||||
"""
|
||||
queryset = Area.objects.all()
|
||||
serializer_class = AreaSerializer
|
||||
extra_filter_class = []
|
||||
|
||||
def get_queryset(self):
|
||||
self.request.query_params._mutable = True
|
||||
params = self.request.query_params
|
||||
pcode = params.get('pcode', None)
|
||||
page = params.get('page', None)
|
||||
limit = params.get('limit', None)
|
||||
if page:
|
||||
del params['page']
|
||||
if limit:
|
||||
del params['limit']
|
||||
if params:
|
||||
if pcode:
|
||||
queryset = self.queryset.filter(enable=True, pcode=pcode)
|
||||
else:
|
||||
queryset = self.queryset.filter(enable=True)
|
||||
else:
|
||||
queryset = self.queryset.filter(enable=True, pcode__isnull=True)
|
||||
return queryset
|
||||
|
||||
157
backend/dvadmin/system/views/dept.py
Normal file
157
backend/dvadmin/system/views/dept.py
Normal file
@@ -0,0 +1,157 @@
|
||||
# -*- 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
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.permission import AnonymousUserPermission
|
||||
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()
|
||||
|
||||
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
|
||||
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:
|
||||
if parent:
|
||||
queryset = self.queryset.filter(status=True, parent=parent)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=True)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=True, parent__isnull=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], extra_filter_class=[])
|
||||
def dept_lazy_tree(self, request, *args, **kwargs):
|
||||
parent = self.request.query_params.get('parent')
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
queryset = Dept.objects.values('id', 'name', 'parent')
|
||||
else:
|
||||
role_ids = request.user.role.values_list('id',flat=True)
|
||||
data_range = RoleMenuButtonPermission.objects.filter(role__in=role_ids).values_list('data_range', flat=True)
|
||||
user_dept_id = request.user.dept.id
|
||||
dept_list = [user_dept_id]
|
||||
data_range_list = list(set(data_range))
|
||||
for item in data_range_list:
|
||||
if item in [0,2]:
|
||||
dept_list = [user_dept_id]
|
||||
elif item == 1:
|
||||
dept_list = Dept.recursion_dept_info(dept_id=user_dept_id)
|
||||
elif item == 3:
|
||||
dept_list = Dept.objects.values_list('id',flat=True)
|
||||
elif item == 4:
|
||||
dept_list = request.user.role.values_list('dept',flat=True)
|
||||
else:
|
||||
dept_list = []
|
||||
queryset = Dept.objects.filter(id__in=dept_list).values('id', 'name', 'parent')
|
||||
return DetailResponse(data=queryset, msg="获取成功")
|
||||
|
||||
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated],extra_filter_class=[])
|
||||
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="获取成功")
|
||||
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="获取成功")
|
||||
79
backend/dvadmin/system/views/file_list.py
Normal file
79
backend/dvadmin/system/views/file_list.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import hashlib
|
||||
import mimetypes
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from application import dispatch
|
||||
from dvadmin.system.models import FileList
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
|
||||
class FileSerializer(CustomModelSerializer):
|
||||
url = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_url(self, instance):
|
||||
# return 'media/' + str(instance.url)
|
||||
return instance.file_url or (f'media/{str(instance.url)}')
|
||||
|
||||
class Meta:
|
||||
model = FileList
|
||||
fields = "__all__"
|
||||
|
||||
def create(self, validated_data):
|
||||
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local'
|
||||
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup")
|
||||
file = self.initial_data.get('file')
|
||||
file_size = file.size
|
||||
validated_data['name'] = str(file)
|
||||
validated_data['size'] = file_size
|
||||
md5 = hashlib.md5()
|
||||
for chunk in file.chunks():
|
||||
md5.update(chunk)
|
||||
validated_data['md5sum'] = md5.hexdigest()
|
||||
validated_data['engine'] = file_engine
|
||||
validated_data['mime_type'] = file.content_type
|
||||
if file_backup:
|
||||
validated_data['url'] = file
|
||||
if file_engine == 'oss':
|
||||
from dvadmin_cloud_storage.views.aliyun import ali_oss_upload
|
||||
file_path = ali_oss_upload(file)
|
||||
if file_path:
|
||||
validated_data['file_url'] = file_path
|
||||
else:
|
||||
raise ValueError("上传失败")
|
||||
elif file_engine == 'cos':
|
||||
from dvadmin_cloud_storage.views.tencent import tencent_cos_upload
|
||||
file_path = tencent_cos_upload(file)
|
||||
if file_path:
|
||||
validated_data['file_url'] = file_path
|
||||
else:
|
||||
raise ValueError("上传失败")
|
||||
else:
|
||||
validated_data['url'] = file
|
||||
# 审计字段
|
||||
try:
|
||||
request_user = self.request.user
|
||||
validated_data['dept_belong_id'] = request_user.dept.id
|
||||
validated_data['creator'] = request_user.id
|
||||
validated_data['modifier'] = request_user.id
|
||||
except:
|
||||
pass
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class FileViewSet(CustomModelViewSet):
|
||||
"""
|
||||
文件管理接口
|
||||
list:查询
|
||||
create:新增
|
||||
update:修改
|
||||
retrieve:单例
|
||||
destroy:删除
|
||||
"""
|
||||
queryset = FileList.objects.all()
|
||||
serializer_class = FileSerializer
|
||||
filter_fields = ['name', ]
|
||||
permission_classes = []
|
||||
251
backend/dvadmin/system/views/login.py
Normal file
251
backend/dvadmin/system/views/login.py
Normal file
@@ -0,0 +1,251 @@
|
||||
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.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.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("图片验证码错误")
|
||||
|
||||
user = Users.objects.get(username=attrs['username'])
|
||||
if not user.is_active:
|
||||
raise CustomValidationError("账号被锁定")
|
||||
|
||||
data = super().validate(attrs)
|
||||
data["name"] = self.user.name
|
||||
data["userId"] = self.user.id
|
||||
data["avatar"] = self.user.avatar
|
||||
data['user_type'] = self.user.user_type
|
||||
dept = getattr(self.user, 'dept', None)
|
||||
if dept:
|
||||
data['dept_info'] = {
|
||||
'dept_id': dept.id,
|
||||
'dept_name': dept.name,
|
||||
|
||||
}
|
||||
role = getattr(self.user, 'role', None)
|
||||
if role:
|
||||
data['role_info'] = role.values('id', 'name', 'key')
|
||||
request = self.context.get("request")
|
||||
request.user = self.user
|
||||
# 记录登录日志
|
||||
save_login_log(request=request)
|
||||
return {"code": 2000, "msg": "请求成功", "data": data}
|
||||
|
||||
|
||||
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="账号/密码错误")
|
||||
36
backend/dvadmin/system/views/login_log.py
Normal file
36
backend/dvadmin/system/views/login_log.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- 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.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):
|
||||
"""
|
||||
登录日志接口
|
||||
list:查询
|
||||
create:新增
|
||||
update:修改
|
||||
retrieve:单例
|
||||
destroy:删除
|
||||
"""
|
||||
queryset = LoginLog.objects.all()
|
||||
serializer_class = LoginLogSerializer
|
||||
extra_filter_class = []
|
||||
152
backend/dvadmin/system/views/menu.py
Normal file
152
backend/dvadmin/system/views/menu.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# -*- 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, MenuButton, RoleMenuPermission
|
||||
from dvadmin.utils.json_response import SuccessResponse
|
||||
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_list('name', flat=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)
|
||||
|
||||
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', 'is_catalog', 'web_path', 'component',
|
||||
'component_name', 'cache', 'visible', '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']
|
||||
|
||||
# extra_filter_class = []
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def web_router(self, request):
|
||||
"""用于前端获取当前角色的路由"""
|
||||
user = request.user
|
||||
is_admin = user.role.values_list('admin', flat=True)
|
||||
if user.is_superuser or True in is_admin:
|
||||
queryset = self.queryset.filter(status=1)
|
||||
else:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id',flat=True)
|
||||
print("role_list", role_list)
|
||||
print("menu_list",menu_list)
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
print(queryset)
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def get_all_menu(self, request):
|
||||
"""用于菜单管理获取所有的菜单"""
|
||||
user = request.user
|
||||
queryset = self.queryset.all()
|
||||
if not user.is_superuser:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id')
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[])
|
||||
def drag_menu(self, request):
|
||||
"""前端拖拽菜单之后重写parent"""
|
||||
menu_id = request.data['menu_id']
|
||||
parent_id = request.data['parent_id']
|
||||
menu = Menu.objects.get(id=menu_id)
|
||||
menu.parent_id = parent_id
|
||||
menu.save()
|
||||
|
||||
return SuccessResponse(data=[], msg="拖拽菜单成功")
|
||||
|
||||
def list(self, request):
|
||||
"""懒加载"""
|
||||
request.query_params._mutable = True
|
||||
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)
|
||||
72
backend/dvadmin/system/views/menu_button.py
Normal file
72
backend/dvadmin/system/views/menu_button.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- 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
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
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.all()
|
||||
serializer_class = MenuButtonSerializer
|
||||
create_serializer_class = MenuButtonCreateUpdateSerializer
|
||||
update_serializer_class = MenuButtonCreateUpdateSerializer
|
||||
extra_filter_class = []
|
||||
|
||||
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
|
||||
def menu_button_all_permission(self,request):
|
||||
"""
|
||||
获取所有的按钮权限
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
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)
|
||||
218
backend/dvadmin/system/views/message_center.py
Normal file
218
backend/dvadmin/system/views/message_center.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# -*- 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):
|
||||
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):
|
||||
"""
|
||||
目标用户序列化器-序列化器
|
||||
"""
|
||||
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
|
||||
|
||||
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 = []
|
||||
98
backend/dvadmin/system/views/role.py
Normal file
98
backend/dvadmin/system/views/role.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# -*- 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.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):
|
||||
"""
|
||||
角色-序列化器
|
||||
"""
|
||||
|
||||
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 MenuPermissonSerializer(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 RoleViewSet(CustomModelViewSet,FastCrudMixin):
|
||||
"""
|
||||
角色管理接口
|
||||
list:查询
|
||||
create:新增
|
||||
update:修改
|
||||
retrieve:单例
|
||||
destroy:删除
|
||||
"""
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
create_serializer_class = RoleCreateUpdateSerializer
|
||||
update_serializer_class = RoleCreateUpdateSerializer
|
||||
search_fields = ['name', 'key']
|
||||
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)
|
||||
280
backend/dvadmin/system/views/role_menu_button_permission.py
Normal file
280
backend/dvadmin/system/views/role_menu_button_permission.py
Normal file
@@ -0,0 +1,280 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@author: 猿小天
|
||||
@contact: QQ:1638245306
|
||||
@Created on: 2021/6/3 003 0:30
|
||||
@Remark: 菜单按钮管理
|
||||
"""
|
||||
from django.db.models import F, Subquery, OuterRef, Exists
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission
|
||||
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
|
||||
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 RoleMenuButtonPermissionInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化菜单按钮-序列化器
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuButtonPermission
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化菜单按钮-序列化器
|
||||
"""
|
||||
menu_button__name = serializers.CharField(source='menu_button.name', read_only=True)
|
||||
menu_button__value= serializers.CharField(source='menu_button.value', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuButtonPermission
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
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 role_get_menu(self, request):
|
||||
"""根据当前用户的角色返回角色拥有的菜单"""
|
||||
data = []
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
queryset = Menu.objects.filter(status=1).values('name','parent','is_catalog',menu_id=F('id'))
|
||||
for item in queryset:
|
||||
btn_name = MenuButton.objects.filter(menu=item['menu_id']).values_list(
|
||||
'name', flat=True)
|
||||
data.append({'menu_id': item['menu_id'], 'name': item['name'], 'parent': item['parent'],
|
||||
'permission': ','.join(btn_name), 'is_catalog': item['is_catalog']})
|
||||
else:
|
||||
role_id = request.user.role.values_list('id',flat=True)
|
||||
queryset = RoleMenuPermission.objects.filter(role__in=role_id).values('menu_id',name=F('menu__name'),parent=F('menu__parent'),is_catalog=F('menu__is_catalog')).distinct()
|
||||
for item in queryset:
|
||||
btn_name = RoleMenuButtonPermission.objects.filter(menu_button__menu=item['menu_id']).values_list('menu_button__name',flat=True)
|
||||
data.append({'menu_id':item['menu_id'], 'name':item['name'], 'parent':item['parent'],'permission':','.join(btn_name),'is_catalog':item['is_catalog']})
|
||||
return DetailResponse(data=data)
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_menu_get_button(self,request):
|
||||
"""
|
||||
当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
if params := request.query_params:
|
||||
if menu_id := params.get('menu', None):
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
queryset = MenuButton.objects.filter(menu=menu_id).values('id', 'name')
|
||||
else:
|
||||
role_list = request.user.role.values_list('id',flat=True)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list,menu_button__menu=menu_id).values(
|
||||
btn_id=F('menu_button__id'),
|
||||
name=F('menu_button__name')
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="参数错误")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def data_scope(self, request):
|
||||
"""
|
||||
获取数据权限范围:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
data = [
|
||||
{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"label": '本部门及以下数据权限'
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": '本部门数据权限'
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"label": '全部数据权限'
|
||||
},
|
||||
{
|
||||
"value": 4,
|
||||
"label": '自定义数据权限'
|
||||
}
|
||||
]
|
||||
return DetailResponse(data=data)
|
||||
else:
|
||||
data = []
|
||||
role_list = request.user.role.values_list('id',flat=True)
|
||||
if params := request.query_params:
|
||||
if menu_button_id := params.get('menu_button', None):
|
||||
role_queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list,menu_button__id=menu_button_id).values_list('data_range',flat=True)
|
||||
data_range_list = list(set(role_queryset))
|
||||
for item in data_range_list:
|
||||
if item == 0:
|
||||
data = [{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
}]
|
||||
elif item == 1:
|
||||
data = [{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
}, {
|
||||
"value": 1,
|
||||
"label": '本部门及以下数据权限'
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": '本部门数据权限'
|
||||
}]
|
||||
elif item == 2:
|
||||
data = [{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": '本部门数据权限'
|
||||
}]
|
||||
elif item == 3:
|
||||
data = [{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"label": '全部数据权限'
|
||||
}, ]
|
||||
elif item == 4:
|
||||
data = [{
|
||||
"value": 0,
|
||||
"label": '仅本人数据权限'
|
||||
},
|
||||
{
|
||||
"value": 4,
|
||||
"label": '自定义数据权限'
|
||||
}]
|
||||
else:
|
||||
data = []
|
||||
return DetailResponse(data=data)
|
||||
return ErrorResponse(msg="参数错误")
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_to_dept_all(self, request):
|
||||
"""
|
||||
当前用户角色下所能授权的部门:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
queryset = Dept.objects.values('id','name','parent')
|
||||
else:
|
||||
if not params:
|
||||
return ErrorResponse(msg="参数错误")
|
||||
menu_button = params.get('menu_button')
|
||||
if menu_button is None:
|
||||
return ErrorResponse(msg="参数错误")
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list,menu_button=None).values(
|
||||
dept_id=F('dept__id'),
|
||||
name=F('dept__name'),
|
||||
parent=F('dept__parent')
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
|
||||
|
||||
|
||||
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
|
||||
def menu_to_button(self,request):
|
||||
"""
|
||||
根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
menu_id = params.get('menu', None)
|
||||
if menu_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values(
|
||||
'id',
|
||||
'data_range',
|
||||
'menu_button',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
else:
|
||||
if params:
|
||||
|
||||
role_id = params.get('role', None)
|
||||
if role_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role=role_id,menu_button__menu=menu_id).values(
|
||||
'id',
|
||||
'data_range',
|
||||
'menu_button',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_to_menu(self, request):
|
||||
"""
|
||||
获取角色对应的按钮权限
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
role_id = params.get('role', None)
|
||||
if role_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
queryset = RoleMenuPermission.objects.filter(role_id=role_id).values_list('menu_id',flat=True).distinct()
|
||||
|
||||
return DetailResponse(data=queryset)
|
||||
|
||||
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)
|
||||
365
backend/dvadmin/system/views/user.py
Normal file
365
backend/dvadmin/system/views/user.py
Normal file
@@ -0,0 +1,365 @@
|
||||
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.decorators import action, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.db import connection
|
||||
from application import dispatch
|
||||
from dvadmin.system.models import Users, Role, Dept
|
||||
from dvadmin.system.views.role import RoleSerializer
|
||||
from dvadmin.utils.json_response import ErrorResponse, DetailResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.validator import CustomUniqueValidator
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
|
||||
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="账号必须唯一")
|
||||
],
|
||||
)
|
||||
|
||||
# password = serializers.CharField(required=False, allow_blank=True)
|
||||
# mobile = serializers.CharField(
|
||||
# max_length=50,
|
||||
# validators=[
|
||||
# CustomUniqueValidator(queryset=Users.objects.all(), message="手机号必须唯一")
|
||||
# ],
|
||||
# allow_blank=True
|
||||
# )
|
||||
|
||||
def 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", "gender", "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),
|
||||
}
|
||||
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=True, 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, self.request.user.password)
|
||||
if not verify_password:
|
||||
verify_password = check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest(), self.request.user.password)
|
||||
if verify_password:
|
||||
request.user.password = make_password(new_pwd)
|
||||
request.user.save()
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
else:
|
||||
return ErrorResponse(msg="旧密码不正确")
|
||||
|
||||
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
|
||||
def reset_to_default_password(self, request, *args, **kwargs):
|
||||
"""恢复默认密码"""
|
||||
instance = Users.objects.filter(id=kwargs.get("pk")).first()
|
||||
if instance:
|
||||
instance.set_password(dispatch.get_system_config_values("base.default_password"))
|
||||
instance.save()
|
||||
return DetailResponse(data=None, msg="密码重置成功")
|
||||
else:
|
||||
return ErrorResponse(msg="未获取到用户")
|
||||
|
||||
@action(methods=["PUT"], detail=True)
|
||||
def reset_password(self, request, pk):
|
||||
"""
|
||||
密码重置
|
||||
"""
|
||||
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="未获取到用户")
|
||||
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)
|
||||
331
backend/dvadmin/utils/filters.py
Normal file
331
backend/dvadmin/utils/filters.py
Normal file
@@ -0,0 +1,331 @@
|
||||
# -*- 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.models import Q, F
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django_filters import utils
|
||||
from django_filters.filters import CharFilter
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters.utils import get_model_field
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
|
||||
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission
|
||||
|
||||
|
||||
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, "自定数据权限")
|
||||
replace_str = re.compile('\d')
|
||||
re_api = replace_str.sub('{id}', api)
|
||||
role_id_list = request.user.role.values_list('id', flat=True)
|
||||
role_permission_list=RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_id_list,
|
||||
role__status=1,
|
||||
menu_button__api=re_api,
|
||||
menu_button__method=method).values(
|
||||
'data_range',
|
||||
role_admin=F('role__admin')
|
||||
)
|
||||
dataScope_list = [] # 权限范围列表
|
||||
for ele in role_permission_list:
|
||||
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
|
||||
if ele.get("data_range") == 3 or ele.get("role_admin") == True:
|
||||
return queryset
|
||||
dataScope_list.append(ele.get("data_range"))
|
||||
dataScope_list = list(set(dataScope_list))
|
||||
|
||||
# 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据)
|
||||
if 0 in dataScope_list:
|
||||
return queryset.filter(
|
||||
creator=request.user, dept_belong_id=user_dept_id
|
||||
)
|
||||
|
||||
# 5. 自定数据权限 获取部门,根据部门过滤
|
||||
dept_list = []
|
||||
for ele in dataScope_list:
|
||||
if ele == 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_list.extend(
|
||||
request.user.role.filter(status=1).values_list(
|
||||
"dept__id", flat=True
|
||||
)
|
||||
)
|
||||
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",
|
||||
}
|
||||
|
||||
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 field_name.endswith(lookup):
|
||||
return field_name
|
||||
return LOOKUP_SEP.join([field_name, lookup])
|
||||
|
||||
def find_filter_lookups(self, orm_lookups, search_term_key):
|
||||
for lookup in orm_lookups:
|
||||
# if lookup.find(search_term_key) >= 0:
|
||||
new_lookup = lookup.split("__")[0]
|
||||
# 修复条件搜索错误 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__
|
||||
)
|
||||
filterset_fields = getattr(view, "filter_fields", None)
|
||||
|
||||
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_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)):
|
||||
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
|
||||
orm_lookups = []
|
||||
for search_field in filterset.filters:
|
||||
if isinstance(filterset.filters[search_field], CharFilter):
|
||||
orm_lookups.append(
|
||||
self.construct_search(six.text_type(search_field), filterset.filters[search_field].lookup_expr)
|
||||
)
|
||||
else:
|
||||
orm_lookups.append(search_field)
|
||||
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:
|
||||
continue
|
||||
query = Q(**{orm_lookup: filterset.data[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()
|
||||
89
backend/dvadmin/utils/import_export.py
Normal file
89
backend/dvadmin/utils/import_export.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# -*- 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)
|
||||
return tables
|
||||
345
backend/dvadmin/utils/import_export_mixin.py
Normal file
345
backend/dvadmin/utils/import_export_mixin.py
Normal file
@@ -0,0 +1,345 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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
|
||||
from dvadmin.utils.request_util import get_verbose_name
|
||||
|
||||
|
||||
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
|
||||
# 导出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)
|
||||
89
backend/dvadmin/utils/middleware.py
Normal file
89
backend/dvadmin/utils/middleware.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
日志 django中间件
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
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):
|
||||
# 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=self.operation_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
|
||||
|
||||
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
|
||||
114
backend/dvadmin/utils/models.py
Normal file
114
backend/dvadmin/utils/models.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@author: 猿小天
|
||||
@contact: QQ:1638245306
|
||||
@Created on: 2021/5/31 031 22:08
|
||||
@Remark: 公共基础model类
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from application import settings
|
||||
table_prefix = settings.TABLE_PREFIX # 数据库表名前缀
|
||||
|
||||
|
||||
class SoftDeleteQuerySet(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):
|
||||
"""
|
||||
重写删除方法,直接开启软删除
|
||||
"""
|
||||
self.is_deleted = True
|
||||
self.save(using=using)
|
||||
|
||||
|
||||
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_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 {}
|
||||
85
backend/dvadmin/utils/pagination.py
Normal file
85
backend/dvadmin/utils/pagination.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- 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
|
||||
pass
|
||||
|
||||
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()
|
||||
is_previous= self.page.has_previous()
|
||||
data=data
|
||||
|
||||
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)
|
||||
]))
|
||||
98
backend/dvadmin/utils/permission.py
Normal file
98
backend/dvadmin/utils/permission.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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", 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__()
|
||||
125
backend/dvadmin/utils/viewset.py
Normal file
125
backend/dvadmin/utils/viewset.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@author: 猿小天
|
||||
@contact: QQ:1638245306
|
||||
@Created on: 2021/6/1 001 22:57
|
||||
@Remark: 自定义视图集
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from django.db import transaction
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from dvadmin.utils.filters import DataLevelPermissionsFilter
|
||||
from dvadmin.utils.import_export_mixin import ExportSerializerMixin, ImportSerializerMixin
|
||||
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse
|
||||
from dvadmin.utils.permission import CustomPermission
|
||||
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 = [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())
|
||||
if isinstance(self.request.data, list):
|
||||
with transaction.atomic():
|
||||
return serializer_class(many=True, *args, **kwargs)
|
||||
else:
|
||||
return serializer_class(*args, **kwargs)
|
||||
|
||||
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字段")
|
||||
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 = '-'
|
||||
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 -*-
|
||||
30
backend/requirements.txt
Normal file
30
backend/requirements.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
Django==4.1.5
|
||||
django-comment-migrate==0.1.7
|
||||
django-cors-headers==3.13.0
|
||||
django-filter==21.1
|
||||
django-ranged-response==0.2.0
|
||||
djangorestframework==3.14.0
|
||||
django-restql==0.15.3
|
||||
django-simple-captcha==0.5.17
|
||||
django-timezone-field==5.0
|
||||
djangorestframework-simplejwt==5.2.2
|
||||
drf-yasg==1.21.4
|
||||
mysqlclient==2.1.1
|
||||
pypinyin==0.48.0
|
||||
ua-parser==0.16.1
|
||||
pyparsing==3.0.9
|
||||
openpyxl==3.0.10
|
||||
requests==2.28.2
|
||||
typing-extensions==4.4.0
|
||||
smmap==5.0.0
|
||||
tzlocal==4.1
|
||||
channels==4.0.0
|
||||
channels-redis==4.0.0
|
||||
websockets==10.4
|
||||
user-agents==2.2.0
|
||||
six==1.16.0
|
||||
whitenoise==6.3.0
|
||||
psycopg2==2.9.5
|
||||
uvicorn==0.20.0
|
||||
gunicorn==20.1.0
|
||||
gevent==22.10.2
|
||||
34
backend/static/drf-yasg/immutable.min.js
vendored
Normal file
34
backend/static/drf-yasg/immutable.min.js
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2014-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Immutable=e()}(this,function(){"use strict";function t(t,e){e&&(t.prototype=Object.create(e.prototype)),t.prototype.constructor=t}function e(t){return o(t)?t:O(t)}function r(t){return u(t)?t:x(t)}function n(t){return s(t)?t:k(t)}function i(t){return o(t)&&!a(t)?t:A(t)}function o(t){return!(!t||!t[ar])}function u(t){return!(!t||!t[hr])}function s(t){return!(!t||!t[fr])}function a(t){return u(t)||s(t)}function h(t){return!(!t||!t[cr])}function f(t){return t.value=!1,t}function c(t){t&&(t.value=!0)}function _(){}function p(t,e){e=e||0;for(var r=Math.max(0,t.length-e),n=Array(r),i=0;r>i;i++)n[i]=t[i+e];return n}function v(t){return void 0===t.size&&(t.size=t.__iterate(y)),t.size}function l(t,e){if("number"!=typeof e){var r=e>>>0;if(""+r!==e||4294967295===r)return NaN;e=r}return 0>e?v(t)+e:e}function y(){return!0}function d(t,e,r){return(0===t||void 0!==r&&-r>=t)&&(void 0===e||void 0!==r&&e>=r)}function m(t,e){return w(t,e,0)}function g(t,e){return w(t,e,e)}function w(t,e,r){return void 0===t?r:0>t?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function S(t){this.next=t}function z(t,e,r,n){var i=0===t?e:1===t?r:[e,r];return n?n.value=i:n={value:i,done:!1},n}function I(){return{value:void 0,done:!0}}function b(t){return!!M(t)}function q(t){return t&&"function"==typeof t.next}function D(t){var e=M(t);return e&&e.call(t)}function M(t){var e=t&&(zr&&t[zr]||t[Ir]);return"function"==typeof e?e:void 0}function E(t){return t&&"number"==typeof t.length}function O(t){return null===t||void 0===t?T():o(t)?t.toSeq():C(t)}function x(t){return null===t||void 0===t?T().toKeyedSeq():o(t)?u(t)?t.toSeq():t.fromEntrySeq():B(t)}function k(t){return null===t||void 0===t?T():o(t)?u(t)?t.entrySeq():t.toIndexedSeq():W(t)}function A(t){return(null===t||void 0===t?T():o(t)?u(t)?t.entrySeq():t:W(t)).toSetSeq()}function j(t){this._array=t,this.size=t.length}function R(t){var e=Object.keys(t);this._object=t,this._keys=e,
|
||||
this.size=e.length}function U(t){this._iterable=t,this.size=t.length||t.size}function K(t){this._iterator=t,this._iteratorCache=[]}function L(t){return!(!t||!t[qr])}function T(){return Dr||(Dr=new j([]))}function B(t){var e=Array.isArray(t)?new j(t).fromEntrySeq():q(t)?new K(t).fromEntrySeq():b(t)?new U(t).fromEntrySeq():"object"==typeof t?new R(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function W(t){var e=J(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function C(t){var e=J(t)||"object"==typeof t&&new R(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function J(t){return E(t)?new j(t):q(t)?new K(t):b(t)?new U(t):void 0}function N(t,e,r,n){var i=t._cache;if(i){for(var o=i.length-1,u=0;o>=u;u++){var s=i[r?o-u:u];if(e(s[1],n?s[0]:u,t)===!1)return u+1}return u}return t.__iterateUncached(e,r)}function P(t,e,r,n){var i=t._cache;if(i){var o=i.length-1,u=0;return new S(function(){var t=i[r?o-u:u];return u++>o?I():z(e,n?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,r)}function H(t,e){return e?V(e,t,"",{"":t}):Y(t)}function V(t,e,r,n){return Array.isArray(e)?t.call(n,r,k(e).map(function(r,n){return V(t,r,n,e)})):Q(e)?t.call(n,r,x(e).map(function(r,n){return V(t,r,n,e)})):e}function Y(t){return Array.isArray(t)?k(t).map(Y).toList():Q(t)?x(t).map(Y).toMap():t}function Q(t){return t&&(t.constructor===Object||void 0===t.constructor)}function X(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return"function"==typeof t.equals&&"function"==typeof e.equals&&t.equals(e)?!0:!1}function F(t,e){if(t===e)return!0;if(!o(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||u(t)!==u(e)||s(t)!==s(e)||h(t)!==h(e))return!1;if(0===t.size&&0===e.size)return!0;
|
||||
var r=!a(t);if(h(t)){var n=t.entries();return e.every(function(t,e){var i=n.next().value;return i&&X(i[1],t)&&(r||X(i[0],e))})&&n.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var f=t;t=e,e=f}var c=!0,_=e.__iterate(function(e,n){return(r?t.has(e):i?X(e,t.get(n,yr)):X(t.get(n,yr),e))?void 0:(c=!1,!1)});return c&&t.size===_}function G(t,e){if(!(this instanceof G))return new G(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(Mr)return Mr;Mr=this}}function Z(t,e){if(!t)throw Error(e)}function $(t,e,r){if(!(this instanceof $))return new $(t,e,r);if(Z(0!==r,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),r=void 0===r?1:Math.abs(r),t>e&&(r=-r),this._start=t,this._end=e,this._step=r,this.size=Math.max(0,Math.ceil((e-t)/r-1)+1),0===this.size){if(Er)return Er;Er=this}}function tt(){throw TypeError("Abstract")}function et(){}function rt(){}function nt(){}function it(t){return t>>>1&1073741824|3221225471&t}function ot(t){if(t===!1||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(t=t.valueOf(),t===!1||null===t||void 0===t))return 0;if(t===!0)return 1;var e=typeof t;if("number"===e){if(t!==t||t===1/0)return 0;var r=0|t;for(r!==t&&(r^=4294967295*t);t>4294967295;)t/=4294967295,r^=t;return it(r)}if("string"===e)return t.length>Kr?ut(t):st(t);if("function"==typeof t.hashCode)return t.hashCode();if("object"===e)return at(t);if("function"==typeof t.toString)return st(""+t);throw Error("Value type "+e+" cannot be hashed.")}function ut(t){var e=Br[t];return void 0===e&&(e=st(t),Tr===Lr&&(Tr=0,Br={}),Tr++,Br[t]=e),e}function st(t){for(var e=0,r=0;t.length>r;r++)e=31*e+t.charCodeAt(r)|0;return it(e)}function at(t){var e;if(jr&&(e=Or.get(t),void 0!==e))return e;if(e=t[Ur],void 0!==e)return e;if(!Ar){if(e=t.propertyIsEnumerable&&t.propertyIsEnumerable[Ur],void 0!==e)return e;if(e=ht(t),void 0!==e)return e}if(e=++Rr,1073741824&Rr&&(Rr=0),jr)Or.set(t,e);else{if(void 0!==kr&&kr(t)===!1)throw Error("Non-extensible objects are not allowed as keys.");
|
||||
if(Ar)Object.defineProperty(t,Ur,{enumerable:!1,configurable:!1,writable:!1,value:e});else if(void 0!==t.propertyIsEnumerable&&t.propertyIsEnumerable===t.constructor.prototype.propertyIsEnumerable)t.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},t.propertyIsEnumerable[Ur]=e;else{if(void 0===t.nodeType)throw Error("Unable to set a non-enumerable property on object.");t[Ur]=e}}return e}function ht(t){if(t&&t.nodeType>0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ft(t){Z(t!==1/0,"Cannot perform this action with an infinite size.")}function ct(t){return null===t||void 0===t?zt():_t(t)&&!h(t)?t:zt().withMutations(function(e){var n=r(t);ft(n.size),n.forEach(function(t,r){return e.set(r,t)})})}function _t(t){return!(!t||!t[Wr])}function pt(t,e){this.ownerID=t,this.entries=e}function vt(t,e,r){this.ownerID=t,this.bitmap=e,this.nodes=r}function lt(t,e,r){this.ownerID=t,this.count=e,this.nodes=r}function yt(t,e,r){this.ownerID=t,this.keyHash=e,this.entries=r}function dt(t,e,r){this.ownerID=t,this.keyHash=e,this.entry=r}function mt(t,e,r){this._type=e,this._reverse=r,this._stack=t._root&&wt(t._root)}function gt(t,e){return z(t,e[0],e[1])}function wt(t,e){return{node:t,index:0,__prev:e}}function St(t,e,r,n){var i=Object.create(Cr);return i.size=t,i._root=e,i.__ownerID=r,i.__hash=n,i.__altered=!1,i}function zt(){return Jr||(Jr=St(0))}function It(t,e,r){var n,i;if(t._root){var o=f(dr),u=f(mr);if(n=bt(t._root,t.__ownerID,0,void 0,e,r,o,u),!u.value)return t;i=t.size+(o.value?r===yr?-1:1:0)}else{if(r===yr)return t;i=1,n=new pt(t.__ownerID,[[e,r]])}return t.__ownerID?(t.size=i,t._root=n,t.__hash=void 0,t.__altered=!0,t):n?St(i,n):zt()}function bt(t,e,r,n,i,o,u,s){return t?t.update(e,r,n,i,o,u,s):o===yr?t:(c(s),c(u),new dt(e,n,[i,o]))}function qt(t){return t.constructor===dt||t.constructor===yt}function Dt(t,e,r,n,i){if(t.keyHash===n)return new yt(e,n,[t.entry,i]);var o,u=(0===r?t.keyHash:t.keyHash>>>r)&lr,s=(0===r?n:n>>>r)&lr,a=u===s?[Dt(t,e,r+pr,n,i)]:(o=new dt(e,n,i),
|
||||
s>u?[t,o]:[o,t]);return new vt(e,1<<u|1<<s,a)}function Mt(t,e,r,n){t||(t=new _);for(var i=new dt(t,ot(r),[r,n]),o=0;e.length>o;o++){var u=e[o];i=i.update(t,0,void 0,u[0],u[1])}return i}function Et(t,e,r,n){for(var i=0,o=0,u=Array(r),s=0,a=1,h=e.length;h>s;s++,a<<=1){var f=e[s];void 0!==f&&s!==n&&(i|=a,u[o++]=f)}return new vt(t,i,u)}function Ot(t,e,r,n,i){for(var o=0,u=Array(vr),s=0;0!==r;s++,r>>>=1)u[s]=1&r?e[o++]:void 0;return u[n]=i,new lt(t,o+1,u)}function xt(t,e,n){for(var i=[],u=0;n.length>u;u++){var s=n[u],a=r(s);o(s)||(a=a.map(function(t){return H(t)})),i.push(a)}return jt(t,e,i)}function kt(t,e,r){return t&&t.mergeDeep&&o(e)?t.mergeDeep(e):X(t,e)?t:e}function At(t){return function(e,r,n){if(e&&e.mergeDeepWith&&o(r))return e.mergeDeepWith(t,r);var i=t(e,r,n);return X(e,i)?e:i}}function jt(t,e,r){return r=r.filter(function(t){return 0!==t.size}),0===r.length?t:0!==t.size||t.__ownerID||1!==r.length?t.withMutations(function(t){for(var n=e?function(r,n){t.update(n,yr,function(t){return t===yr?r:e(t,r,n)})}:function(e,r){t.set(r,e)},i=0;r.length>i;i++)r[i].forEach(n)}):t.constructor(r[0])}function Rt(t,e,r,n){var i=t===yr,o=e.next();if(o.done){var u=i?r:t,s=n(u);return s===u?t:s}Z(i||t&&t.set,"invalid keyPath");var a=o.value,h=i?yr:t.get(a,yr),f=Rt(h,e,r,n);return f===h?t:f===yr?t.remove(a):(i?zt():t).set(a,f)}function Ut(t){return t-=t>>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function Kt(t,e,r,n){var i=n?t:p(t);return i[e]=r,i}function Lt(t,e,r,n){var i=t.length+1;if(n&&e+1===i)return t[e]=r,t;for(var o=Array(i),u=0,s=0;i>s;s++)s===e?(o[s]=r,u=-1):o[s]=t[s+u];return o}function Tt(t,e,r){var n=t.length-1;if(r&&e===n)return t.pop(),t;for(var i=Array(n),o=0,u=0;n>u;u++)u===e&&(o=1),i[u]=t[u+o];return i}function Bt(t){var e=Pt();if(null===t||void 0===t)return e;if(Wt(t))return t;var r=n(t),i=r.size;return 0===i?e:(ft(i),i>0&&vr>i?Nt(0,i,pr,null,new Ct(r.toArray())):e.withMutations(function(t){t.setSize(i),r.forEach(function(e,r){return t.set(r,e)})}))}function Wt(t){
|
||||
return!(!t||!t[Vr])}function Ct(t,e){this.array=t,this.ownerID=e}function Jt(t,e){function r(t,e,r){return 0===e?n(t,r):i(t,e,r)}function n(t,r){var n=r===s?a&&a.array:t&&t.array,i=r>o?0:o-r,h=u-r;return h>vr&&(h=vr),function(){if(i===h)return Xr;var t=e?--h:i++;return n&&n[t]}}function i(t,n,i){var s,a=t&&t.array,h=i>o?0:o-i>>n,f=(u-i>>n)+1;return f>vr&&(f=vr),function(){for(;;){if(s){var t=s();if(t!==Xr)return t;s=null}if(h===f)return Xr;var o=e?--f:h++;s=r(a&&a[o],n-pr,i+(o<<n))}}}var o=t._origin,u=t._capacity,s=Gt(u),a=t._tail;return r(t._root,t._level,0)}function Nt(t,e,r,n,i,o,u){var s=Object.create(Yr);return s.size=e-t,s._origin=t,s._capacity=e,s._level=r,s._root=n,s._tail=i,s.__ownerID=o,s.__hash=u,s.__altered=!1,s}function Pt(){return Qr||(Qr=Nt(0,0,pr))}function Ht(t,e,r){if(e=l(t,e),e!==e)return t;if(e>=t.size||0>e)return t.withMutations(function(t){0>e?Xt(t,e).set(0,r):Xt(t,0,e+1).set(e,r)});e+=t._origin;var n=t._tail,i=t._root,o=f(mr);return e>=Gt(t._capacity)?n=Vt(n,t.__ownerID,0,e,r,o):i=Vt(i,t.__ownerID,t._level,e,r,o),o.value?t.__ownerID?(t._root=i,t._tail=n,t.__hash=void 0,t.__altered=!0,t):Nt(t._origin,t._capacity,t._level,i,n):t}function Vt(t,e,r,n,i,o){var u=n>>>r&lr,s=t&&t.array.length>u;if(!s&&void 0===i)return t;var a;if(r>0){var h=t&&t.array[u],f=Vt(h,e,r-pr,n,i,o);return f===h?t:(a=Yt(t,e),a.array[u]=f,a)}return s&&t.array[u]===i?t:(c(o),a=Yt(t,e),void 0===i&&u===a.array.length-1?a.array.pop():a.array[u]=i,a)}function Yt(t,e){return e&&t&&e===t.ownerID?t:new Ct(t?t.array.slice():[],e)}function Qt(t,e){if(e>=Gt(t._capacity))return t._tail;if(1<<t._level+pr>e){for(var r=t._root,n=t._level;r&&n>0;)r=r.array[e>>>n&lr],n-=pr;return r}}function Xt(t,e,r){void 0!==e&&(e=0|e),void 0!==r&&(r=0|r);var n=t.__ownerID||new _,i=t._origin,o=t._capacity,u=i+e,s=void 0===r?o:0>r?o+r:i+r;if(u===i&&s===o)return t;if(u>=s)return t.clear();for(var a=t._level,h=t._root,f=0;0>u+f;)h=new Ct(h&&h.array.length?[void 0,h]:[],n),a+=pr,f+=1<<a;f&&(u+=f,i+=f,s+=f,o+=f);for(var c=Gt(o),p=Gt(s);p>=1<<a+pr;)h=new Ct(h&&h.array.length?[h]:[],n),
|
||||
a+=pr;var v=t._tail,l=c>p?Qt(t,s-1):p>c?new Ct([],n):v;if(v&&p>c&&o>u&&v.array.length){h=Yt(h,n);for(var y=h,d=a;d>pr;d-=pr){var m=c>>>d&lr;y=y.array[m]=Yt(y.array[m],n)}y.array[c>>>pr&lr]=v}if(o>s&&(l=l&&l.removeAfter(n,0,s)),u>=p)u-=p,s-=p,a=pr,h=null,l=l&&l.removeBefore(n,0,u);else if(u>i||c>p){for(f=0;h;){var g=u>>>a&lr;if(g!==p>>>a&lr)break;g&&(f+=(1<<a)*g),a-=pr,h=h.array[g]}h&&u>i&&(h=h.removeBefore(n,a,u-f)),h&&c>p&&(h=h.removeAfter(n,a,p-f)),f&&(u-=f,s-=f)}return t.__ownerID?(t.size=s-u,t._origin=u,t._capacity=s,t._level=a,t._root=h,t._tail=l,t.__hash=void 0,t.__altered=!0,t):Nt(u,s,a,h,l)}function Ft(t,e,r){for(var i=[],u=0,s=0;r.length>s;s++){var a=r[s],h=n(a);h.size>u&&(u=h.size),o(a)||(h=h.map(function(t){return H(t)})),i.push(h)}return u>t.size&&(t=t.setSize(u)),jt(t,e,i)}function Gt(t){return vr>t?0:t-1>>>pr<<pr}function Zt(t){return null===t||void 0===t?ee():$t(t)?t:ee().withMutations(function(e){var n=r(t);ft(n.size),n.forEach(function(t,r){return e.set(r,t)})})}function $t(t){return _t(t)&&h(t)}function te(t,e,r,n){var i=Object.create(Zt.prototype);return i.size=t?t.size:0,i._map=t,i._list=e,i.__ownerID=r,i.__hash=n,i}function ee(){return Fr||(Fr=te(zt(),Pt()))}function re(t,e,r){var n,i,o=t._map,u=t._list,s=o.get(e),a=void 0!==s;if(r===yr){if(!a)return t;u.size>=vr&&u.size>=2*o.size?(i=u.filter(function(t,e){return void 0!==t&&s!==e}),n=i.toKeyedSeq().map(function(t){return t[0]}).flip().toMap(),t.__ownerID&&(n.__ownerID=i.__ownerID=t.__ownerID)):(n=o.remove(e),i=s===u.size-1?u.pop():u.set(s,void 0))}else if(a){if(r===u.get(s)[1])return t;n=o,i=u.set(s,[e,r])}else n=o.set(e,u.size),i=u.set(u.size,[e,r]);return t.__ownerID?(t.size=n.size,t._map=n,t._list=i,t.__hash=void 0,t):te(n,i)}function ne(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ie(t){this._iter=t,this.size=t.size}function oe(t){this._iter=t,this.size=t.size}function ue(t){this._iter=t,this.size=t.size}function se(t){var e=Ee(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);
|
||||
return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=Oe,e.__iterateUncached=function(e,r){var n=this;return t.__iterate(function(t,r){return e(r,t,n)!==!1},r)},e.__iteratorUncached=function(e,r){if(e===Sr){var n=t.__iterator(e,r);return new S(function(){var t=n.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===wr?gr:wr,r)},e}function ae(t,e,r){var n=Ee(t);return n.size=t.size,n.has=function(e){return t.has(e)},n.get=function(n,i){var o=t.get(n,yr);return o===yr?i:e.call(r,o,n,t)},n.__iterateUncached=function(n,i){var o=this;return t.__iterate(function(t,i,u){return n(e.call(r,t,i,u),i,o)!==!1},i)},n.__iteratorUncached=function(n,i){var o=t.__iterator(Sr,i);return new S(function(){var i=o.next();if(i.done)return i;var u=i.value,s=u[0];return z(n,s,e.call(r,u[1],s,t),i)})},n}function he(t,e){var r=Ee(t);return r._iter=t,r.size=t.size,r.reverse=function(){return t},t.flip&&(r.flip=function(){var e=se(t);return e.reverse=function(){return t.flip()},e}),r.get=function(r,n){return t.get(e?r:-1-r,n)},r.has=function(r){return t.has(e?r:-1-r)},r.includes=function(e){return t.includes(e)},r.cacheResult=Oe,r.__iterate=function(e,r){var n=this;return t.__iterate(function(t,r){return e(t,r,n)},!r)},r.__iterator=function(e,r){return t.__iterator(e,!r)},r}function fe(t,e,r,n){var i=Ee(t);return n&&(i.has=function(n){var i=t.get(n,yr);return i!==yr&&!!e.call(r,i,n,t)},i.get=function(n,i){var o=t.get(n,yr);return o!==yr&&e.call(r,o,n,t)?o:i}),i.__iterateUncached=function(i,o){var u=this,s=0;return t.__iterate(function(t,o,a){return e.call(r,t,o,a)?(s++,i(t,n?o:s-1,u)):void 0},o),s},i.__iteratorUncached=function(i,o){var u=t.__iterator(Sr,o),s=0;return new S(function(){for(;;){var o=u.next();if(o.done)return o;var a=o.value,h=a[0],f=a[1];if(e.call(r,f,h,t))return z(i,n?h:s++,f,o)}})},i}function ce(t,e,r){var n=ct().asMutable();return t.__iterate(function(i,o){n.update(e.call(r,i,o,t),0,function(t){
|
||||
return t+1})}),n.asImmutable()}function _e(t,e,r){var n=u(t),i=(h(t)?Zt():ct()).asMutable();t.__iterate(function(o,u){i.update(e.call(r,o,u,t),function(t){return t=t||[],t.push(n?[u,o]:o),t})});var o=Me(t);return i.map(function(e){return be(t,o(e))})}function pe(t,e,r,n){var i=t.size;if(void 0!==e&&(e=0|e),void 0!==r&&(r=r===1/0?i:0|r),d(e,r,i))return t;var o=m(e,i),u=g(r,i);if(o!==o||u!==u)return pe(t.toSeq().cacheResult(),e,r,n);var s,a=u-o;a===a&&(s=0>a?0:a);var h=Ee(t);return h.size=0===s?s:t.size&&s||void 0,!n&&L(t)&&s>=0&&(h.get=function(e,r){return e=l(this,e),e>=0&&s>e?t.get(e+o,r):r}),h.__iterateUncached=function(e,r){var i=this;if(0===s)return 0;if(r)return this.cacheResult().__iterate(e,r);var u=0,a=!0,h=0;return t.__iterate(function(t,r){return a&&(a=u++<o)?void 0:(h++,e(t,n?r:h-1,i)!==!1&&h!==s)}),h},h.__iteratorUncached=function(e,r){if(0!==s&&r)return this.cacheResult().__iterator(e,r);var i=0!==s&&t.__iterator(e,r),u=0,a=0;return new S(function(){for(;u++<o;)i.next();if(++a>s)return I();var t=i.next();return n||e===wr?t:e===gr?z(e,a-1,void 0,t):z(e,a-1,t.value[1],t)})},h}function ve(t,e,r){var n=Ee(t);return n.__iterateUncached=function(n,i){var o=this;if(i)return this.cacheResult().__iterate(n,i);var u=0;return t.__iterate(function(t,i,s){return e.call(r,t,i,s)&&++u&&n(t,i,o)}),u},n.__iteratorUncached=function(n,i){var o=this;if(i)return this.cacheResult().__iterator(n,i);var u=t.__iterator(Sr,i),s=!0;return new S(function(){if(!s)return I();var t=u.next();if(t.done)return t;var i=t.value,a=i[0],h=i[1];return e.call(r,h,a,o)?n===Sr?t:z(n,a,h,t):(s=!1,I())})},n}function le(t,e,r,n){var i=Ee(t);return i.__iterateUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterate(i,o);var s=!0,a=0;return t.__iterate(function(t,o,h){return s&&(s=e.call(r,t,o,h))?void 0:(a++,i(t,n?o:a-1,u))}),a},i.__iteratorUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterator(i,o);var s=t.__iterator(Sr,o),a=!0,h=0;return new S(function(){var t,o,f;do{if(t=s.next(),t.done)return n||i===wr?t:i===gr?z(i,h++,void 0,t):z(i,h++,t.value[1],t);
|
||||
var c=t.value;o=c[0],f=c[1],a&&(a=e.call(r,f,o,u))}while(a);return i===Sr?t:z(i,o,f,t)})},i}function ye(t,e){var n=u(t),i=[t].concat(e).map(function(t){return o(t)?n&&(t=r(t)):t=n?B(t):W(Array.isArray(t)?t:[t]),t}).filter(function(t){return 0!==t.size});if(0===i.length)return t;if(1===i.length){var a=i[0];if(a===t||n&&u(a)||s(t)&&s(a))return a}var h=new j(i);return n?h=h.toKeyedSeq():s(t)||(h=h.toSetSeq()),h=h.flatten(!0),h.size=i.reduce(function(t,e){if(void 0!==t){var r=e.size;if(void 0!==r)return t+r}},0),h}function de(t,e,r){var n=Ee(t);return n.__iterateUncached=function(n,i){function u(t,h){var f=this;t.__iterate(function(t,i){return(!e||e>h)&&o(t)?u(t,h+1):n(t,r?i:s++,f)===!1&&(a=!0),!a},i)}var s=0,a=!1;return u(t,0),s},n.__iteratorUncached=function(n,i){var u=t.__iterator(n,i),s=[],a=0;return new S(function(){for(;u;){var t=u.next();if(t.done===!1){var h=t.value;if(n===Sr&&(h=h[1]),e&&!(e>s.length)||!o(h))return r?t:z(n,a++,h,t);s.push(u),u=h.__iterator(n,i)}else u=s.pop()}return I()})},n}function me(t,e,r){var n=Me(t);return t.toSeq().map(function(i,o){return n(e.call(r,i,o,t))}).flatten(!0)}function ge(t,e){var r=Ee(t);return r.size=t.size&&2*t.size-1,r.__iterateUncached=function(r,n){var i=this,o=0;return t.__iterate(function(t,n){return(!o||r(e,o++,i)!==!1)&&r(t,o++,i)!==!1},n),o},r.__iteratorUncached=function(r,n){var i,o=t.__iterator(wr,n),u=0;return new S(function(){return(!i||u%2)&&(i=o.next(),i.done)?i:u%2?z(r,u++,e):z(r,u++,i.value,i)})},r}function we(t,e,r){e||(e=xe);var n=u(t),i=0,o=t.toSeq().map(function(e,n){return[n,e,i++,r?r(e,n,t):e]}).toArray();return o.sort(function(t,r){return e(t[3],r[3])||t[2]-r[2]}).forEach(n?function(t,e){o[e].length=2}:function(t,e){o[e]=t[1]}),n?x(o):s(t)?k(o):A(o)}function Se(t,e,r){if(e||(e=xe),r){var n=t.toSeq().map(function(e,n){return[e,r(e,n,t)]}).reduce(function(t,r){return ze(e,t[1],r[1])?r:t});return n&&n[0]}return t.reduce(function(t,r){return ze(e,t,r)?r:t})}function ze(t,e,r){var n=t(r,e);return 0===n&&r!==e&&(void 0===r||null===r||r!==r)||n>0}function Ie(t,r,n){
|
||||
var i=Ee(t);return i.size=new j(n).map(function(t){return t.size}).min(),i.__iterate=function(t,e){for(var r,n=this.__iterator(wr,e),i=0;!(r=n.next()).done&&t(r.value,i++,this)!==!1;);return i},i.__iteratorUncached=function(t,i){var o=n.map(function(t){return t=e(t),D(i?t.reverse():t)}),u=0,s=!1;return new S(function(){var e;return s||(e=o.map(function(t){return t.next()}),s=e.some(function(t){return t.done})),s?I():z(t,u++,r.apply(null,e.map(function(t){return t.value})))})},i}function be(t,e){return L(t)?e:t.constructor(e)}function qe(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function De(t){return ft(t.size),v(t)}function Me(t){return u(t)?r:s(t)?n:i}function Ee(t){return Object.create((u(t)?x:s(t)?k:A).prototype)}function Oe(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):O.prototype.cacheResult.call(this)}function xe(t,e){return t>e?1:e>t?-1:0}function ke(t){var r=D(t);if(!r){if(!E(t))throw new TypeError("Expected iterable or array-like: "+t);r=D(e(t))}return r}function Ae(t,e){var r,n=function(o){if(o instanceof n)return o;if(!(this instanceof n))return new n(o);if(!r){r=!0;var u=Object.keys(t);Ue(i,u),i.size=u.length,i._name=e,i._keys=u,i._defaultValues=t}this._map=ct(o)},i=n.prototype=Object.create(Gr);return i.constructor=n,n}function je(t,e,r){var n=Object.create(Object.getPrototypeOf(t));return n._map=e,n.__ownerID=r,n}function Re(t){return t._name||t.constructor.name||"Record"}function Ue(t,e){try{e.forEach(Ke.bind(void 0,t))}catch(r){}}function Ke(t,e){Object.defineProperty(t,e,{get:function(){return this.get(e)},set:function(t){Z(this.__ownerID,"Cannot set on an immutable record."),this.set(e,t)}})}function Le(t){return null===t||void 0===t?Ce():Te(t)&&!h(t)?t:Ce().withMutations(function(e){var r=i(t);ft(r.size),r.forEach(function(t){return e.add(t)})})}function Te(t){return!(!t||!t[Zr])}function Be(t,e){return t.__ownerID?(t.size=e.size,t._map=e,t):e===t._map?t:0===e.size?t.__empty():t.__make(e)}function We(t,e){var r=Object.create($r);
|
||||
return r.size=t?t.size:0,r._map=t,r.__ownerID=e,r}function Ce(){return tn||(tn=We(zt()))}function Je(t){return null===t||void 0===t?He():Ne(t)?t:He().withMutations(function(e){var r=i(t);ft(r.size),r.forEach(function(t){return e.add(t)})})}function Ne(t){return Te(t)&&h(t)}function Pe(t,e){var r=Object.create(en);return r.size=t?t.size:0,r._map=t,r.__ownerID=e,r}function He(){return rn||(rn=Pe(ee()))}function Ve(t){return null===t||void 0===t?Xe():Ye(t)?t:Xe().unshiftAll(t)}function Ye(t){return!(!t||!t[nn])}function Qe(t,e,r,n){var i=Object.create(on);return i.size=t,i._head=e,i.__ownerID=r,i.__hash=n,i.__altered=!1,i}function Xe(){return un||(un=Qe(0))}function Fe(t,e){var r=function(r){t.prototype[r]=e[r]};return Object.keys(e).forEach(r),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(e).forEach(r),t}function Ge(t,e){return e}function Ze(t,e){return[e,t]}function $e(t){return function(){return!t.apply(this,arguments)}}function tr(t){return function(){return-t.apply(this,arguments)}}function er(t){return"string"==typeof t?JSON.stringify(t):t+""}function rr(){return p(arguments)}function nr(t,e){return e>t?1:t>e?-1:0}function ir(t){if(t.size===1/0)return 0;var e=h(t),r=u(t),n=e?1:0,i=t.__iterate(r?e?function(t,e){n=31*n+ur(ot(t),ot(e))|0}:function(t,e){n=n+ur(ot(t),ot(e))|0}:e?function(t){n=31*n+ot(t)|0}:function(t){n=n+ot(t)|0});return or(i,n)}function or(t,e){return e=xr(e,3432918353),e=xr(e<<15|e>>>-15,461845907),e=xr(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=xr(e^e>>>16,2246822507),e=xr(e^e>>>13,3266489909),e=it(e^e>>>16)}function ur(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var sr=Array.prototype.slice;t(r,e),t(n,e),t(i,e),e.isIterable=o,e.isKeyed=u,e.isIndexed=s,e.isAssociative=a,e.isOrdered=h,e.Keyed=r,e.Indexed=n,e.Set=i;var ar="@@__IMMUTABLE_ITERABLE__@@",hr="@@__IMMUTABLE_KEYED__@@",fr="@@__IMMUTABLE_INDEXED__@@",cr="@@__IMMUTABLE_ORDERED__@@",_r="delete",pr=5,vr=1<<pr,lr=vr-1,yr={},dr={value:!1},mr={value:!1},gr=0,wr=1,Sr=2,zr="function"==typeof Symbol&&Symbol.iterator,Ir="@@iterator",br=zr||Ir;
|
||||
S.prototype.toString=function(){return"[Iterator]"},S.KEYS=gr,S.VALUES=wr,S.ENTRIES=Sr,S.prototype.inspect=S.prototype.toSource=function(){return""+this},S.prototype[br]=function(){return this},t(O,e),O.of=function(){return O(arguments)},O.prototype.toSeq=function(){return this},O.prototype.toString=function(){return this.__toString("Seq {","}")},O.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},O.prototype.__iterate=function(t,e){return N(this,t,e,!0)},O.prototype.__iterator=function(t,e){return P(this,t,e,!0)},t(x,O),x.prototype.toKeyedSeq=function(){return this},t(k,O),k.of=function(){return k(arguments)},k.prototype.toIndexedSeq=function(){return this},k.prototype.toString=function(){return this.__toString("Seq [","]")},k.prototype.__iterate=function(t,e){return N(this,t,e,!1)},k.prototype.__iterator=function(t,e){return P(this,t,e,!1)},t(A,O),A.of=function(){return A(arguments)},A.prototype.toSetSeq=function(){return this},O.isSeq=L,O.Keyed=x,O.Set=A,O.Indexed=k;var qr="@@__IMMUTABLE_SEQ__@@";O.prototype[qr]=!0,t(j,k),j.prototype.get=function(t,e){return this.has(t)?this._array[l(this,t)]:e},j.prototype.__iterate=function(t,e){for(var r=this._array,n=r.length-1,i=0;n>=i;i++)if(t(r[e?n-i:i],i,this)===!1)return i+1;return i},j.prototype.__iterator=function(t,e){var r=this._array,n=r.length-1,i=0;return new S(function(){return i>n?I():z(t,i,r[e?n-i++:i++])})},t(R,x),R.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},R.prototype.has=function(t){return this._object.hasOwnProperty(t)},R.prototype.__iterate=function(t,e){for(var r=this._object,n=this._keys,i=n.length-1,o=0;i>=o;o++){var u=n[e?i-o:o];if(t(r[u],u,this)===!1)return o+1}return o},R.prototype.__iterator=function(t,e){var r=this._object,n=this._keys,i=n.length-1,o=0;return new S(function(){var u=n[e?i-o:o];return o++>i?I():z(t,u,r[u])})},R.prototype[cr]=!0,t(U,k),U.prototype.__iterateUncached=function(t,e){if(e)return this.cacheResult().__iterate(t,e);
|
||||
var r=this._iterable,n=D(r),i=0;if(q(n))for(var o;!(o=n.next()).done&&t(o.value,i++,this)!==!1;);return i},U.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var r=this._iterable,n=D(r);if(!q(n))return new S(I);var i=0;return new S(function(){var e=n.next();return e.done?e:z(t,i++,e.value)})},t(K,k),K.prototype.__iterateUncached=function(t,e){if(e)return this.cacheResult().__iterate(t,e);for(var r=this._iterator,n=this._iteratorCache,i=0;n.length>i;)if(t(n[i],i++,this)===!1)return i;for(var o;!(o=r.next()).done;){var u=o.value;if(n[i]=u,t(u,i++,this)===!1)break}return i},K.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var r=this._iterator,n=this._iteratorCache,i=0;return new S(function(){if(i>=n.length){var e=r.next();if(e.done)return e;n[i]=e.value}return z(t,i,n[i++])})};var Dr;t(G,k),G.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},G.prototype.get=function(t,e){return this.has(t)?this._value:e},G.prototype.includes=function(t){return X(this._value,t)},G.prototype.slice=function(t,e){var r=this.size;return d(t,e,r)?this:new G(this._value,g(e,r)-m(t,r))},G.prototype.reverse=function(){return this},G.prototype.indexOf=function(t){return X(this._value,t)?0:-1},G.prototype.lastIndexOf=function(t){return X(this._value,t)?this.size:-1},G.prototype.__iterate=function(t,e){for(var r=0;this.size>r;r++)if(t(this._value,r,this)===!1)return r+1;return r},G.prototype.__iterator=function(t,e){var r=this,n=0;return new S(function(){return r.size>n?z(t,n++,r._value):I()})},G.prototype.equals=function(t){return t instanceof G?X(this._value,t._value):F(t)};var Mr;t($,k),$.prototype.toString=function(){return 0===this.size?"Range []":"Range [ "+this._start+"..."+this._end+(1!==this._step?" by "+this._step:"")+" ]"},$.prototype.get=function(t,e){return this.has(t)?this._start+l(this,t)*this._step:e},$.prototype.includes=function(t){var e=(t-this._start)/this._step;return e>=0&&this.size>e&&e===Math.floor(e);
|
||||
},$.prototype.slice=function(t,e){return d(t,e,this.size)?this:(t=m(t,this.size),e=g(e,this.size),t>=e?new $(0,0):new $(this.get(t,this._end),this.get(e,this._end),this._step))},$.prototype.indexOf=function(t){var e=t-this._start;if(e%this._step===0){var r=e/this._step;if(r>=0&&this.size>r)return r}return-1},$.prototype.lastIndexOf=function(t){return this.indexOf(t)},$.prototype.__iterate=function(t,e){for(var r=this.size-1,n=this._step,i=e?this._start+r*n:this._start,o=0;r>=o;o++){if(t(i,o,this)===!1)return o+1;i+=e?-n:n}return o},$.prototype.__iterator=function(t,e){var r=this.size-1,n=this._step,i=e?this._start+r*n:this._start,o=0;return new S(function(){var u=i;return i+=e?-n:n,o>r?I():z(t,o++,u)})},$.prototype.equals=function(t){return t instanceof $?this._start===t._start&&this._end===t._end&&this._step===t._step:F(this,t)};var Er;t(tt,e),t(et,tt),t(rt,tt),t(nt,tt),tt.Keyed=et,tt.Indexed=rt,tt.Set=nt;var Or,xr="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(t,e){t=0|t,e=0|e;var r=65535&t,n=65535&e;return r*n+((t>>>16)*n+r*(e>>>16)<<16>>>0)|0},kr=Object.isExtensible,Ar=function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}}(),jr="function"==typeof WeakMap;jr&&(Or=new WeakMap);var Rr=0,Ur="__immutablehash__";"function"==typeof Symbol&&(Ur=Symbol(Ur));var Kr=16,Lr=255,Tr=0,Br={};t(ct,et),ct.of=function(){var t=sr.call(arguments,0);return zt().withMutations(function(e){for(var r=0;t.length>r;r+=2){if(r+1>=t.length)throw Error("Missing value for key: "+t[r]);e.set(t[r],t[r+1])}})},ct.prototype.toString=function(){return this.__toString("Map {","}")},ct.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},ct.prototype.set=function(t,e){return It(this,t,e)},ct.prototype.setIn=function(t,e){return this.updateIn(t,yr,function(){return e})},ct.prototype.remove=function(t){return It(this,t,yr)},ct.prototype.deleteIn=function(t){return this.updateIn(t,function(){return yr})},ct.prototype.update=function(t,e,r){return 1===arguments.length?t(this):this.updateIn([t],e,r);
|
||||
},ct.prototype.updateIn=function(t,e,r){r||(r=e,e=void 0);var n=Rt(this,ke(t),e,r);return n===yr?void 0:n},ct.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):zt()},ct.prototype.merge=function(){return xt(this,void 0,arguments)},ct.prototype.mergeWith=function(t){var e=sr.call(arguments,1);return xt(this,t,e)},ct.prototype.mergeIn=function(t){var e=sr.call(arguments,1);return this.updateIn(t,zt(),function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]})},ct.prototype.mergeDeep=function(){return xt(this,kt,arguments)},ct.prototype.mergeDeepWith=function(t){var e=sr.call(arguments,1);return xt(this,At(t),e)},ct.prototype.mergeDeepIn=function(t){var e=sr.call(arguments,1);return this.updateIn(t,zt(),function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]})},ct.prototype.sort=function(t){return Zt(we(this,t))},ct.prototype.sortBy=function(t,e){return Zt(we(this,e,t))},ct.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},ct.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new _)},ct.prototype.asImmutable=function(){return this.__ensureOwner()},ct.prototype.wasAltered=function(){return this.__altered},ct.prototype.__iterator=function(t,e){return new mt(this,t,e)},ct.prototype.__iterate=function(t,e){var r=this,n=0;return this._root&&this._root.iterate(function(e){return n++,t(e[1],e[0],r)},e),n},ct.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?St(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},ct.isMap=_t;var Wr="@@__IMMUTABLE_MAP__@@",Cr=ct.prototype;Cr[Wr]=!0,Cr[_r]=Cr.remove,Cr.removeIn=Cr.deleteIn,pt.prototype.get=function(t,e,r,n){for(var i=this.entries,o=0,u=i.length;u>o;o++)if(X(r,i[o][0]))return i[o][1];return n},pt.prototype.update=function(t,e,r,n,i,o,u){for(var s=i===yr,a=this.entries,h=0,f=a.length;f>h&&!X(n,a[h][0]);h++);
|
||||
var _=f>h;if(_?a[h][1]===i:s)return this;if(c(u),(s||!_)&&c(o),!s||1!==a.length){if(!_&&!s&&a.length>=Nr)return Mt(t,a,n,i);var v=t&&t===this.ownerID,l=v?a:p(a);return _?s?h===f-1?l.pop():l[h]=l.pop():l[h]=[n,i]:l.push([n,i]),v?(this.entries=l,this):new pt(t,l)}},vt.prototype.get=function(t,e,r,n){void 0===e&&(e=ot(r));var i=1<<((0===t?e:e>>>t)&lr),o=this.bitmap;return 0===(o&i)?n:this.nodes[Ut(o&i-1)].get(t+pr,e,r,n)},vt.prototype.update=function(t,e,r,n,i,o,u){void 0===r&&(r=ot(n));var s=(0===e?r:r>>>e)&lr,a=1<<s,h=this.bitmap,f=0!==(h&a);if(!f&&i===yr)return this;var c=Ut(h&a-1),_=this.nodes,p=f?_[c]:void 0,v=bt(p,t,e+pr,r,n,i,o,u);if(v===p)return this;if(!f&&v&&_.length>=Pr)return Ot(t,_,h,s,v);if(f&&!v&&2===_.length&&qt(_[1^c]))return _[1^c];if(f&&v&&1===_.length&&qt(v))return v;var l=t&&t===this.ownerID,y=f?v?h:h^a:h|a,d=f?v?Kt(_,c,v,l):Tt(_,c,l):Lt(_,c,v,l);return l?(this.bitmap=y,this.nodes=d,this):new vt(t,y,d)},lt.prototype.get=function(t,e,r,n){void 0===e&&(e=ot(r));var i=(0===t?e:e>>>t)&lr,o=this.nodes[i];return o?o.get(t+pr,e,r,n):n},lt.prototype.update=function(t,e,r,n,i,o,u){void 0===r&&(r=ot(n));var s=(0===e?r:r>>>e)&lr,a=i===yr,h=this.nodes,f=h[s];if(a&&!f)return this;var c=bt(f,t,e+pr,r,n,i,o,u);if(c===f)return this;var _=this.count;if(f){if(!c&&(_--,Hr>_))return Et(t,h,_,s)}else _++;var p=t&&t===this.ownerID,v=Kt(h,s,c,p);return p?(this.count=_,this.nodes=v,this):new lt(t,_,v)},yt.prototype.get=function(t,e,r,n){for(var i=this.entries,o=0,u=i.length;u>o;o++)if(X(r,i[o][0]))return i[o][1];return n},yt.prototype.update=function(t,e,r,n,i,o,u){void 0===r&&(r=ot(n));var s=i===yr;if(r!==this.keyHash)return s?this:(c(u),c(o),Dt(this,t,e,r,[n,i]));for(var a=this.entries,h=0,f=a.length;f>h&&!X(n,a[h][0]);h++);var _=f>h;if(_?a[h][1]===i:s)return this;if(c(u),(s||!_)&&c(o),s&&2===f)return new dt(t,this.keyHash,a[1^h]);var v=t&&t===this.ownerID,l=v?a:p(a);return _?s?h===f-1?l.pop():l[h]=l.pop():l[h]=[n,i]:l.push([n,i]),v?(this.entries=l,this):new yt(t,this.keyHash,l)},dt.prototype.get=function(t,e,r,n){return X(r,this.entry[0])?this.entry[1]:n;
|
||||
},dt.prototype.update=function(t,e,r,n,i,o,u){var s=i===yr,a=X(n,this.entry[0]);return(a?i===this.entry[1]:s)?this:(c(u),s?void c(o):a?t&&t===this.ownerID?(this.entry[1]=i,this):new dt(t,this.keyHash,[n,i]):(c(o),Dt(this,t,e,ot(n),[n,i])))},pt.prototype.iterate=yt.prototype.iterate=function(t,e){for(var r=this.entries,n=0,i=r.length-1;i>=n;n++)if(t(r[e?i-n:n])===!1)return!1},vt.prototype.iterate=lt.prototype.iterate=function(t,e){for(var r=this.nodes,n=0,i=r.length-1;i>=n;n++){var o=r[e?i-n:n];if(o&&o.iterate(t,e)===!1)return!1}},dt.prototype.iterate=function(t,e){return t(this.entry)},t(mt,S),mt.prototype.next=function(){for(var t=this._type,e=this._stack;e;){var r,n=e.node,i=e.index++;if(n.entry){if(0===i)return gt(t,n.entry)}else if(n.entries){if(r=n.entries.length-1,r>=i)return gt(t,n.entries[this._reverse?r-i:i])}else if(r=n.nodes.length-1,r>=i){var o=n.nodes[this._reverse?r-i:i];if(o){if(o.entry)return gt(t,o.entry);e=this._stack=wt(o,e)}continue}e=this._stack=this._stack.__prev}return I()};var Jr,Nr=vr/4,Pr=vr/2,Hr=vr/4;t(Bt,rt),Bt.of=function(){return this(arguments)},Bt.prototype.toString=function(){return this.__toString("List [","]")},Bt.prototype.get=function(t,e){if(t=l(this,t),t>=0&&this.size>t){t+=this._origin;var r=Qt(this,t);return r&&r.array[t&lr]}return e},Bt.prototype.set=function(t,e){return Ht(this,t,e)},Bt.prototype.remove=function(t){return this.has(t)?0===t?this.shift():t===this.size-1?this.pop():this.splice(t,1):this},Bt.prototype.insert=function(t,e){return this.splice(t,0,e)},Bt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=pr,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):Pt()},Bt.prototype.push=function(){var t=arguments,e=this.size;return this.withMutations(function(r){Xt(r,0,e+t.length);for(var n=0;t.length>n;n++)r.set(e+n,t[n])})},Bt.prototype.pop=function(){return Xt(this,0,-1)},Bt.prototype.unshift=function(){var t=arguments;return this.withMutations(function(e){Xt(e,-t.length);for(var r=0;t.length>r;r++)e.set(r,t[r]);
|
||||
})},Bt.prototype.shift=function(){return Xt(this,1)},Bt.prototype.merge=function(){return Ft(this,void 0,arguments)},Bt.prototype.mergeWith=function(t){var e=sr.call(arguments,1);return Ft(this,t,e)},Bt.prototype.mergeDeep=function(){return Ft(this,kt,arguments)},Bt.prototype.mergeDeepWith=function(t){var e=sr.call(arguments,1);return Ft(this,At(t),e)},Bt.prototype.setSize=function(t){return Xt(this,0,t)},Bt.prototype.slice=function(t,e){var r=this.size;return d(t,e,r)?this:Xt(this,m(t,r),g(e,r))},Bt.prototype.__iterator=function(t,e){var r=0,n=Jt(this,e);return new S(function(){var e=n();return e===Xr?I():z(t,r++,e)})},Bt.prototype.__iterate=function(t,e){for(var r,n=0,i=Jt(this,e);(r=i())!==Xr&&t(r,n++,this)!==!1;);return n},Bt.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Nt(this._origin,this._capacity,this._level,this._root,this._tail,t,this.__hash):(this.__ownerID=t,this)},Bt.isList=Wt;var Vr="@@__IMMUTABLE_LIST__@@",Yr=Bt.prototype;Yr[Vr]=!0,Yr[_r]=Yr.remove,Yr.setIn=Cr.setIn,Yr.deleteIn=Yr.removeIn=Cr.removeIn,Yr.update=Cr.update,Yr.updateIn=Cr.updateIn,Yr.mergeIn=Cr.mergeIn,Yr.mergeDeepIn=Cr.mergeDeepIn,Yr.withMutations=Cr.withMutations,Yr.asMutable=Cr.asMutable,Yr.asImmutable=Cr.asImmutable,Yr.wasAltered=Cr.wasAltered,Ct.prototype.removeBefore=function(t,e,r){if(r===e?1<<e:0===this.array.length)return this;var n=r>>>e&lr;if(n>=this.array.length)return new Ct([],t);var i,o=0===n;if(e>0){var u=this.array[n];if(i=u&&u.removeBefore(t,e-pr,r),i===u&&o)return this}if(o&&!i)return this;var s=Yt(this,t);if(!o)for(var a=0;n>a;a++)s.array[a]=void 0;return i&&(s.array[n]=i),s},Ct.prototype.removeAfter=function(t,e,r){if(r===(e?1<<e:0)||0===this.array.length)return this;var n=r-1>>>e&lr;if(n>=this.array.length)return this;var i;if(e>0){var o=this.array[n];if(i=o&&o.removeAfter(t,e-pr,r),i===o&&n===this.array.length-1)return this}var u=Yt(this,t);return u.array.splice(n+1),i&&(u.array[n]=i),u};var Qr,Xr={};t(Zt,ct),Zt.of=function(){return this(arguments)},Zt.prototype.toString=function(){return this.__toString("OrderedMap {","}");
|
||||
},Zt.prototype.get=function(t,e){var r=this._map.get(t);return void 0!==r?this._list.get(r)[1]:e},Zt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):ee()},Zt.prototype.set=function(t,e){return re(this,t,e)},Zt.prototype.remove=function(t){return re(this,t,yr)},Zt.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Zt.prototype.__iterate=function(t,e){var r=this;return this._list.__iterate(function(e){return e&&t(e[1],e[0],r)},e)},Zt.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Zt.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),r=this._list.__ensureOwner(t);return t?te(e,r,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=r,this)},Zt.isOrderedMap=$t,Zt.prototype[cr]=!0,Zt.prototype[_r]=Zt.prototype.remove;var Fr;t(ne,x),ne.prototype.get=function(t,e){return this._iter.get(t,e)},ne.prototype.has=function(t){return this._iter.has(t)},ne.prototype.valueSeq=function(){return this._iter.valueSeq()},ne.prototype.reverse=function(){var t=this,e=he(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},ne.prototype.map=function(t,e){var r=this,n=ae(this,t,e);return this._useKeys||(n.valueSeq=function(){return r._iter.toSeq().map(t,e)}),n},ne.prototype.__iterate=function(t,e){var r,n=this;return this._iter.__iterate(this._useKeys?function(e,r){return t(e,r,n)}:(r=e?De(this):0,function(i){return t(i,e?--r:r++,n)}),e)},ne.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var r=this._iter.__iterator(wr,e),n=e?De(this):0;return new S(function(){var i=r.next();return i.done?i:z(t,e?--n:n++,i.value,i)})},ne.prototype[cr]=!0,t(ie,k),ie.prototype.includes=function(t){return this._iter.includes(t)},ie.prototype.__iterate=function(t,e){var r=this,n=0;return this._iter.__iterate(function(e){return t(e,n++,r)},e)},ie.prototype.__iterator=function(t,e){var r=this._iter.__iterator(wr,e),n=0;
|
||||
return new S(function(){var e=r.next();return e.done?e:z(t,n++,e.value,e)})},t(oe,A),oe.prototype.has=function(t){return this._iter.includes(t)},oe.prototype.__iterate=function(t,e){var r=this;return this._iter.__iterate(function(e){return t(e,e,r)},e)},oe.prototype.__iterator=function(t,e){var r=this._iter.__iterator(wr,e);return new S(function(){var e=r.next();return e.done?e:z(t,e.value,e.value,e)})},t(ue,x),ue.prototype.entrySeq=function(){return this._iter.toSeq()},ue.prototype.__iterate=function(t,e){var r=this;return this._iter.__iterate(function(e){if(e){qe(e);var n=o(e);return t(n?e.get(1):e[1],n?e.get(0):e[0],r)}},e)},ue.prototype.__iterator=function(t,e){var r=this._iter.__iterator(wr,e);return new S(function(){for(;;){var e=r.next();if(e.done)return e;var n=e.value;if(n){qe(n);var i=o(n);return z(t,i?n.get(0):n[0],i?n.get(1):n[1],e)}}})},ie.prototype.cacheResult=ne.prototype.cacheResult=oe.prototype.cacheResult=ue.prototype.cacheResult=Oe,t(Ae,et),Ae.prototype.toString=function(){return this.__toString(Re(this)+" {","}")},Ae.prototype.has=function(t){return this._defaultValues.hasOwnProperty(t)},Ae.prototype.get=function(t,e){if(!this.has(t))return e;var r=this._defaultValues[t];return this._map?this._map.get(t,r):r},Ae.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var t=this.constructor;return t._empty||(t._empty=je(this,zt()))},Ae.prototype.set=function(t,e){if(!this.has(t))throw Error('Cannot set unknown key "'+t+'" on '+Re(this));if(this._map&&!this._map.has(t)){var r=this._defaultValues[t];if(e===r)return this}var n=this._map&&this._map.set(t,e);return this.__ownerID||n===this._map?this:je(this,n)},Ae.prototype.remove=function(t){if(!this.has(t))return this;var e=this._map&&this._map.remove(t);return this.__ownerID||e===this._map?this:je(this,e)},Ae.prototype.wasAltered=function(){return this._map.wasAltered()},Ae.prototype.__iterator=function(t,e){var n=this;return r(this._defaultValues).map(function(t,e){return n.get(e)}).__iterator(t,e)},Ae.prototype.__iterate=function(t,e){
|
||||
var n=this;return r(this._defaultValues).map(function(t,e){return n.get(e)}).__iterate(t,e)},Ae.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map&&this._map.__ensureOwner(t);return t?je(this,e,t):(this.__ownerID=t,this._map=e,this)};var Gr=Ae.prototype;Gr[_r]=Gr.remove,Gr.deleteIn=Gr.removeIn=Cr.removeIn,Gr.merge=Cr.merge,Gr.mergeWith=Cr.mergeWith,Gr.mergeIn=Cr.mergeIn,Gr.mergeDeep=Cr.mergeDeep,Gr.mergeDeepWith=Cr.mergeDeepWith,Gr.mergeDeepIn=Cr.mergeDeepIn,Gr.setIn=Cr.setIn,Gr.update=Cr.update,Gr.updateIn=Cr.updateIn,Gr.withMutations=Cr.withMutations,Gr.asMutable=Cr.asMutable,Gr.asImmutable=Cr.asImmutable,t(Le,nt),Le.of=function(){return this(arguments)},Le.fromKeys=function(t){return this(r(t).keySeq())},Le.prototype.toString=function(){return this.__toString("Set {","}")},Le.prototype.has=function(t){return this._map.has(t)},Le.prototype.add=function(t){return Be(this,this._map.set(t,!0))},Le.prototype.remove=function(t){return Be(this,this._map.remove(t))},Le.prototype.clear=function(){return Be(this,this._map.clear())},Le.prototype.union=function(){var t=sr.call(arguments,0);return t=t.filter(function(t){return 0!==t.size}),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations(function(e){for(var r=0;t.length>r;r++)i(t[r]).forEach(function(t){return e.add(t)})}):this.constructor(t[0])},Le.prototype.intersect=function(){var t=sr.call(arguments,0);if(0===t.length)return this;t=t.map(function(t){return i(t)});var e=this;return this.withMutations(function(r){e.forEach(function(e){t.every(function(t){return t.includes(e)})||r.remove(e)})})},Le.prototype.subtract=function(){var t=sr.call(arguments,0);if(0===t.length)return this;t=t.map(function(t){return i(t)});var e=this;return this.withMutations(function(r){e.forEach(function(e){t.some(function(t){return t.includes(e)})&&r.remove(e)})})},Le.prototype.merge=function(){return this.union.apply(this,arguments)},Le.prototype.mergeWith=function(t){var e=sr.call(arguments,1);return this.union.apply(this,e)},
|
||||
Le.prototype.sort=function(t){return Je(we(this,t))},Le.prototype.sortBy=function(t,e){return Je(we(this,e,t))},Le.prototype.wasAltered=function(){return this._map.wasAltered()},Le.prototype.__iterate=function(t,e){var r=this;return this._map.__iterate(function(e,n){return t(n,n,r)},e)},Le.prototype.__iterator=function(t,e){return this._map.map(function(t,e){return e}).__iterator(t,e)},Le.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t);return t?this.__make(e,t):(this.__ownerID=t,this._map=e,this)},Le.isSet=Te;var Zr="@@__IMMUTABLE_SET__@@",$r=Le.prototype;$r[Zr]=!0,$r[_r]=$r.remove,$r.mergeDeep=$r.merge,$r.mergeDeepWith=$r.mergeWith,$r.withMutations=Cr.withMutations,$r.asMutable=Cr.asMutable,$r.asImmutable=Cr.asImmutable,$r.__empty=Ce,$r.__make=We;var tn;t(Je,Le),Je.of=function(){return this(arguments)},Je.fromKeys=function(t){return this(r(t).keySeq())},Je.prototype.toString=function(){return this.__toString("OrderedSet {","}")},Je.isOrderedSet=Ne;var en=Je.prototype;en[cr]=!0,en.__empty=He,en.__make=Pe;var rn;t(Ve,rt),Ve.of=function(){return this(arguments)},Ve.prototype.toString=function(){return this.__toString("Stack [","]")},Ve.prototype.get=function(t,e){var r=this._head;for(t=l(this,t);r&&t--;)r=r.next;return r?r.value:e},Ve.prototype.peek=function(){return this._head&&this._head.value},Ve.prototype.push=function(){if(0===arguments.length)return this;for(var t=this.size+arguments.length,e=this._head,r=arguments.length-1;r>=0;r--)e={value:arguments[r],next:e};return this.__ownerID?(this.size=t,this._head=e,this.__hash=void 0,this.__altered=!0,this):Qe(t,e)},Ve.prototype.pushAll=function(t){if(t=n(t),0===t.size)return this;ft(t.size);var e=this.size,r=this._head;return t.reverse().forEach(function(t){e++,r={value:t,next:r}}),this.__ownerID?(this.size=e,this._head=r,this.__hash=void 0,this.__altered=!0,this):Qe(e,r)},Ve.prototype.pop=function(){return this.slice(1)},Ve.prototype.unshift=function(){return this.push.apply(this,arguments)},Ve.prototype.unshiftAll=function(t){
|
||||
return this.pushAll(t)},Ve.prototype.shift=function(){return this.pop.apply(this,arguments)},Ve.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Xe()},Ve.prototype.slice=function(t,e){if(d(t,e,this.size))return this;var r=m(t,this.size),n=g(e,this.size);if(n!==this.size)return rt.prototype.slice.call(this,t,e);for(var i=this.size-r,o=this._head;r--;)o=o.next;return this.__ownerID?(this.size=i,this._head=o,this.__hash=void 0,this.__altered=!0,this):Qe(i,o)},Ve.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Qe(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Ve.prototype.__iterate=function(t,e){if(e)return this.reverse().__iterate(t);for(var r=0,n=this._head;n&&t(n.value,r++,this)!==!1;)n=n.next;return r},Ve.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var r=0,n=this._head;return new S(function(){if(n){var e=n.value;return n=n.next,z(t,r++,e)}return I()})},Ve.isStack=Ye;var nn="@@__IMMUTABLE_STACK__@@",on=Ve.prototype;on[nn]=!0,on.withMutations=Cr.withMutations,on.asMutable=Cr.asMutable,on.asImmutable=Cr.asImmutable,on.wasAltered=Cr.wasAltered;var un;e.Iterator=S,Fe(e,{toArray:function(){ft(this.size);var t=Array(this.size||0);return this.valueSeq().__iterate(function(e,r){t[r]=e}),t},toIndexedSeq:function(){return new ie(this)},toJS:function(){return this.toSeq().map(function(t){return t&&"function"==typeof t.toJS?t.toJS():t}).__toJS()},toJSON:function(){return this.toSeq().map(function(t){return t&&"function"==typeof t.toJSON?t.toJSON():t}).__toJS()},toKeyedSeq:function(){return new ne(this,!0)},toMap:function(){return ct(this.toKeyedSeq())},toObject:function(){ft(this.size);var t={};return this.__iterate(function(e,r){t[r]=e}),t},toOrderedMap:function(){return Zt(this.toKeyedSeq())},toOrderedSet:function(){return Je(u(this)?this.valueSeq():this)},toSet:function(){return Le(u(this)?this.valueSeq():this)},toSetSeq:function(){return new oe(this);
|
||||
},toSeq:function(){return s(this)?this.toIndexedSeq():u(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Ve(u(this)?this.valueSeq():this)},toList:function(){return Bt(u(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(t,e){return 0===this.size?t+e:t+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+e},concat:function(){var t=sr.call(arguments,0);return be(this,ye(this,t))},includes:function(t){return this.some(function(e){return X(e,t)})},entries:function(){return this.__iterator(Sr)},every:function(t,e){ft(this.size);var r=!0;return this.__iterate(function(n,i,o){return t.call(e,n,i,o)?void 0:(r=!1,!1)}),r},filter:function(t,e){return be(this,fe(this,t,e,!0))},find:function(t,e,r){var n=this.findEntry(t,e);return n?n[1]:r},forEach:function(t,e){return ft(this.size),this.__iterate(e?t.bind(e):t)},join:function(t){ft(this.size),t=void 0!==t?""+t:",";var e="",r=!0;return this.__iterate(function(n){r?r=!1:e+=t,e+=null!==n&&void 0!==n?""+n:""}),e},keys:function(){return this.__iterator(gr)},map:function(t,e){return be(this,ae(this,t,e))},reduce:function(t,e,r){ft(this.size);var n,i;return arguments.length<2?i=!0:n=e,this.__iterate(function(e,o,u){i?(i=!1,n=e):n=t.call(r,n,e,o,u)}),n},reduceRight:function(t,e,r){var n=this.toKeyedSeq().reverse();return n.reduce.apply(n,arguments)},reverse:function(){return be(this,he(this,!0))},slice:function(t,e){return be(this,pe(this,t,e,!0))},some:function(t,e){return!this.every($e(t),e)},sort:function(t){return be(this,we(this,t))},values:function(){return this.__iterator(wr)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some(function(){return!0})},count:function(t,e){return v(t?this.toSeq().filter(t,e):this)},countBy:function(t,e){return ce(this,t,e)},equals:function(t){return F(this,t)},entrySeq:function(){var t=this;if(t._cache)return new j(t._cache);var e=t.toSeq().map(Ze).toIndexedSeq();return e.fromEntrySeq=function(){return t.toSeq()},e},filterNot:function(t,e){
|
||||
return this.filter($e(t),e)},findEntry:function(t,e,r){var n=r;return this.__iterate(function(r,i,o){return t.call(e,r,i,o)?(n=[i,r],!1):void 0}),n},findKey:function(t,e){var r=this.findEntry(t,e);return r&&r[0]},findLast:function(t,e,r){return this.toKeyedSeq().reverse().find(t,e,r)},findLastEntry:function(t,e,r){return this.toKeyedSeq().reverse().findEntry(t,e,r)},findLastKey:function(t,e){return this.toKeyedSeq().reverse().findKey(t,e)},first:function(){return this.find(y)},flatMap:function(t,e){return be(this,me(this,t,e))},flatten:function(t){return be(this,de(this,t,!0))},fromEntrySeq:function(){return new ue(this)},get:function(t,e){return this.find(function(e,r){return X(r,t)},void 0,e)},getIn:function(t,e){for(var r,n=this,i=ke(t);!(r=i.next()).done;){var o=r.value;if(n=n&&n.get?n.get(o,yr):yr,n===yr)return e}return n},groupBy:function(t,e){return _e(this,t,e)},has:function(t){return this.get(t,yr)!==yr},hasIn:function(t){return this.getIn(t,yr)!==yr},isSubset:function(t){return t="function"==typeof t.includes?t:e(t),this.every(function(e){return t.includes(e)})},isSuperset:function(t){return t="function"==typeof t.isSubset?t:e(t),t.isSubset(this)},keyOf:function(t){return this.findKey(function(e){return X(e,t)})},keySeq:function(){return this.toSeq().map(Ge).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(t){return this.toKeyedSeq().reverse().keyOf(t)},max:function(t){return Se(this,t)},maxBy:function(t,e){return Se(this,e,t)},min:function(t){return Se(this,t?tr(t):nr)},minBy:function(t,e){return Se(this,e?tr(e):nr,t)},rest:function(){return this.slice(1)},skip:function(t){return this.slice(Math.max(0,t))},skipLast:function(t){return be(this,this.toSeq().reverse().skip(t).reverse())},skipWhile:function(t,e){return be(this,le(this,t,e,!0))},skipUntil:function(t,e){return this.skipWhile($e(t),e)},sortBy:function(t,e){return be(this,we(this,e,t))},take:function(t){return this.slice(0,Math.max(0,t))},takeLast:function(t){return be(this,this.toSeq().reverse().take(t).reverse());
|
||||
},takeWhile:function(t,e){return be(this,ve(this,t,e))},takeUntil:function(t,e){return this.takeWhile($e(t),e)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=ir(this))}});var sn=e.prototype;sn[ar]=!0,sn[br]=sn.values,sn.__toJS=sn.toArray,sn.__toStringMapper=er,sn.inspect=sn.toSource=function(){return""+this},sn.chain=sn.flatMap,sn.contains=sn.includes,Fe(r,{flip:function(){return be(this,se(this))},mapEntries:function(t,e){var r=this,n=0;return be(this,this.toSeq().map(function(i,o){return t.call(e,[o,i],n++,r)}).fromEntrySeq())},mapKeys:function(t,e){var r=this;return be(this,this.toSeq().flip().map(function(n,i){return t.call(e,n,i,r)}).flip())}});var an=r.prototype;an[hr]=!0,an[br]=sn.entries,an.__toJS=sn.toObject,an.__toStringMapper=function(t,e){return JSON.stringify(e)+": "+er(t)},Fe(n,{toKeyedSeq:function(){return new ne(this,!1)},filter:function(t,e){return be(this,fe(this,t,e,!1))},findIndex:function(t,e){var r=this.findEntry(t,e);return r?r[0]:-1},indexOf:function(t){var e=this.keyOf(t);return void 0===e?-1:e},lastIndexOf:function(t){var e=this.lastKeyOf(t);return void 0===e?-1:e},reverse:function(){return be(this,he(this,!1))},slice:function(t,e){return be(this,pe(this,t,e,!1))},splice:function(t,e){var r=arguments.length;if(e=Math.max(0|e,0),0===r||2===r&&!e)return this;t=m(t,0>t?this.count():this.size);var n=this.slice(0,t);return be(this,1===r?n:n.concat(p(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var r=this.findLastEntry(t,e);return r?r[0]:-1},first:function(){return this.get(0)},flatten:function(t){return be(this,de(this,t,!1))},get:function(t,e){return t=l(this,t),0>t||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find(function(e,r){return r===t},void 0,e)},has:function(t){return t=l(this,t),t>=0&&(void 0!==this.size?this.size===1/0||this.size>t:-1!==this.indexOf(t))},interpose:function(t){return be(this,ge(this,t))},interleave:function(){var t=[this].concat(p(arguments)),e=Ie(this.toSeq(),k.of,t),r=e.flatten(!0);return e.size&&(r.size=e.size*t.length),
|
||||
be(this,r)},keySeq:function(){return $(0,this.size)},last:function(){return this.get(-1)},skipWhile:function(t,e){return be(this,le(this,t,e,!1))},zip:function(){var t=[this].concat(p(arguments));return be(this,Ie(this,rr,t))},zipWith:function(t){var e=p(arguments);return e[0]=this,be(this,Ie(this,t,e))}}),n.prototype[fr]=!0,n.prototype[cr]=!0,Fe(i,{get:function(t,e){return this.has(t)?t:e},includes:function(t){return this.has(t)},keySeq:function(){return this.valueSeq()}}),i.prototype.has=sn.includes,i.prototype.contains=i.prototype.includes,Fe(x,r.prototype),Fe(k,n.prototype),Fe(A,i.prototype),Fe(et,r.prototype),Fe(rt,n.prototype),Fe(nt,i.prototype);var hn={Iterable:e,Seq:O,Collection:tt,Map:ct,OrderedMap:Zt,List:Bt,Stack:Ve,Set:Le,OrderedSet:Je,Record:Ae,Range:$,Repeat:G,is:X,fromJS:H};return hn});
|
||||
4
backend/static/drf-yasg/insQ.min.js
vendored
Normal file
4
backend/static/drf-yasg/insQ.min.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
// insertion-query v1.0.3 (2016-01-20)
|
||||
// license:MIT
|
||||
// Zbyszek Tenerowicz <naugtur@gmail.com> (http://naugtur.pl/)
|
||||
var insertionQ=function(){"use strict";function a(a,b){var d,e="insQ_"+g++,f=function(a){(a.animationName===e||a[i]===e)&&(c(a.target)||b(a.target))};d=document.createElement("style"),d.innerHTML="@"+j+"keyframes "+e+" { from { outline: 1px solid transparent } to { outline: 0px solid transparent } }\n"+a+" { animation-duration: 0.001s; animation-name: "+e+"; "+j+"animation-duration: 0.001s; "+j+"animation-name: "+e+"; } ",document.head.appendChild(d);var h=setTimeout(function(){document.addEventListener("animationstart",f,!1),document.addEventListener("MSAnimationStart",f,!1),document.addEventListener("webkitAnimationStart",f,!1)},n.timeout);return{destroy:function(){clearTimeout(h),d&&(document.head.removeChild(d),d=null),document.removeEventListener("animationstart",f),document.removeEventListener("MSAnimationStart",f),document.removeEventListener("webkitAnimationStart",f)}}}function b(a){a.QinsQ=!0}function c(a){return n.strictlyNew&&a.QinsQ===!0}function d(a){return c(a.parentNode)?a:d(a.parentNode)}function e(a){for(b(a),a=a.firstChild;a;a=a.nextSibling)void 0!==a&&1===a.nodeType&&e(a)}function f(f,g){var h=[],i=function(){var a;return function(){clearTimeout(a),a=setTimeout(function(){h.forEach(e),g(h),h=[]},10)}}();return a(f,function(a){if(!c(a)){b(a);var e=d(a);h.indexOf(e)<0&&h.push(e),i()}})}var g=100,h=!1,i="animationName",j="",k="Webkit Moz O ms Khtml".split(" "),l="",m=document.createElement("div"),n={strictlyNew:!0,timeout:20};if(m.style.animationName&&(h=!0),h===!1)for(var o=0;o<k.length;o++)if(void 0!==m.style[k[o]+"AnimationName"]){l=k[o],i=l+"AnimationName",j="-"+l.toLowerCase()+"-",h=!0;break}var p=function(b){return h&&b.match(/[^{}]/)?(n.strictlyNew&&e(document.body),{every:function(c){return a(b,c)},summary:function(a){return f(b,a)}}):!1};return p.config=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b])},p}();"undefined"!=typeof module&&"undefined"!=typeof module.exports&&(module.exports=insertionQ);
|
||||
67
backend/static/drf-yasg/redoc-init.js
Normal file
67
backend/static/drf-yasg/redoc-init.js
Normal file
@@ -0,0 +1,67 @@
|
||||
"use strict";
|
||||
|
||||
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
|
||||
var specURL = currentPath + '?format=openapi';
|
||||
var redoc = document.createElement("redoc");
|
||||
|
||||
var redocSettings = JSON.parse(document.getElementById('redoc-settings').innerHTML);
|
||||
if (redocSettings.url) {
|
||||
specURL = redocSettings.url;
|
||||
}
|
||||
delete redocSettings.url;
|
||||
if (redocSettings.fetchSchemaWithQuery) {
|
||||
var query = new URLSearchParams(window.location.search || '').entries();
|
||||
var url = specURL.split('?');
|
||||
var usp = new URLSearchParams(url[1] || '');
|
||||
for (var it = query.next(); !it.done; it = query.next()) {
|
||||
usp.set(it.value[0], it.value[1]);
|
||||
}
|
||||
url[1] = usp.toString();
|
||||
specURL = url[1] ? url.join('?') : url[0];
|
||||
}
|
||||
delete redocSettings.fetchSchemaWithQuery;
|
||||
|
||||
redoc.setAttribute("spec-url", specURL);
|
||||
|
||||
function camelToKebab(str) {
|
||||
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
}
|
||||
|
||||
for (var p in redocSettings) {
|
||||
if (redocSettings.hasOwnProperty(p)) {
|
||||
if (redocSettings[p] !== null && redocSettings[p] !== undefined && redocSettings[p] !== false) {
|
||||
redoc.setAttribute(camelToKebab(p), redocSettings[p].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.body.replaceChild(redoc, document.getElementById('redoc-placeholder'));
|
||||
|
||||
function hideEmptyVersion() {
|
||||
// 'span.api-info-version' is for redoc 1.x, 'div.api-info span' is for redoc 2-alpha
|
||||
var apiVersion = document.querySelector('span.api-info-version') || document.querySelector('div.api-info span');
|
||||
if (!apiVersion) {
|
||||
console.log("WARNING: could not find API versionString element (span.api-info-version)");
|
||||
return;
|
||||
}
|
||||
|
||||
var versionString = apiVersion.innerText;
|
||||
if (versionString) {
|
||||
// trim spaces and surrounding ()
|
||||
versionString = versionString.replace(/ /g, '');
|
||||
versionString = versionString.replace(/(^\()|(\)$)/g, '');
|
||||
}
|
||||
|
||||
if (!versionString) {
|
||||
// hide version element if empty
|
||||
apiVersion.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
if (document.querySelector('span.api-info-version') || document.querySelector('div.api-info span')) {
|
||||
hideEmptyVersion();
|
||||
}
|
||||
else {
|
||||
insertionQ('span.api-info-version').every(hideEmptyVersion);
|
||||
insertionQ('div.api-info span').every(hideEmptyVersion);
|
||||
}
|
||||
8
backend/static/drf-yasg/redoc-old/redoc.min.js
vendored
Normal file
8
backend/static/drf-yasg/redoc-old/redoc.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
backend/static/drf-yasg/redoc/redoc-logo.png
Normal file
BIN
backend/static/drf-yasg/redoc/redoc-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
110
backend/static/drf-yasg/redoc/redoc.min.js
vendored
Normal file
110
backend/static/drf-yasg/redoc/redoc.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
73
backend/static/drf-yasg/style.css
Normal file
73
backend/static/drf-yasg/style.css
Normal file
@@ -0,0 +1,73 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.swagger-body {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#django-session-auth > div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#django-session-auth .btn.authorize {
|
||||
padding: 10px 23px;
|
||||
}
|
||||
|
||||
#django-session-auth .btn.authorize a {
|
||||
color: #49cc90;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#django-session-auth .hello {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#django-session-auth .hello .django-session {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline;
|
||||
padding: .2em .6em .3em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: .25em;
|
||||
}
|
||||
|
||||
.label-primary {
|
||||
background-color: #337ab7;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-right: 8px;
|
||||
background: #16222c44;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
svg.swagger-defs {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
14
backend/static/drf-yasg/swagger-ui-dist/absolute-path.js
Normal file
14
backend/static/drf-yasg/swagger-ui-dist/absolute-path.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* getAbsoluteFSPath
|
||||
* @return {string} When run in NodeJS env, returns the absolute path to the current directory
|
||||
* When run outside of NodeJS, will return an error message
|
||||
*/
|
||||
const getAbsoluteFSPath = function () {
|
||||
// detect whether we are running in a browser or nodejs
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
return require("path").resolve(__dirname)
|
||||
}
|
||||
throw new Error('getAbsoluteFSPath can only be called within a Nodejs environment');
|
||||
}
|
||||
|
||||
module.exports = getAbsoluteFSPath
|
||||
BIN
backend/static/drf-yasg/swagger-ui-dist/favicon-32x32.png
Normal file
BIN
backend/static/drf-yasg/swagger-ui-dist/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
17
backend/static/drf-yasg/swagger-ui-dist/index.js
Normal file
17
backend/static/drf-yasg/swagger-ui-dist/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
try {
|
||||
module.exports.SwaggerUIBundle = require("./swagger-ui-bundle.js")
|
||||
module.exports.SwaggerUIStandalonePreset = require("./swagger-ui-standalone-preset.js")
|
||||
} catch(e) {
|
||||
// swallow the error if there's a problem loading the assets.
|
||||
// allows this module to support providing the assets for browserish contexts,
|
||||
// without exploding in a Node context.
|
||||
//
|
||||
// see https://github.com/swagger-api/swagger-ui/issues/3291#issuecomment-311195388
|
||||
// for more information.
|
||||
}
|
||||
|
||||
// `absolutePath` and `getAbsoluteFSPath` are both here because at one point,
|
||||
// we documented having one and actually implemented the other.
|
||||
// They were both retained so we don't break anyone's code.
|
||||
module.exports.absolutePath = require("./absolute-path.js")
|
||||
module.exports.getAbsoluteFSPath = require("./absolute-path.js")
|
||||
74
backend/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html
Normal file
74
backend/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
<script>
|
||||
'use strict';
|
||||
function run () {
|
||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||
var sentState = oauth2.state;
|
||||
var redirectUrl = oauth2.redirectUrl;
|
||||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1);
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
||||
arr = qp.split("&")
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
|
||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||
function (key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value)
|
||||
}
|
||||
) : {}
|
||||
|
||||
isValid = qp.state === sentState
|
||||
|
||||
if ((
|
||||
oauth2.auth.schema.get("flow") === "accessCode"||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode"
|
||||
) && !oauth2.auth.code) {
|
||||
if (!isValid) {
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
|
||||
});
|
||||
}
|
||||
|
||||
if (qp.code) {
|
||||
delete oauth2.state;
|
||||
oauth2.auth.code = qp.code;
|
||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||
} else {
|
||||
let oauthErrorMsg
|
||||
if (qp.error) {
|
||||
oauthErrorMsg = "["+qp.error+"]: " +
|
||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||
}
|
||||
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
backend/static/drf-yasg/swagger-ui-dist/swagger-ui.css
Normal file
4
backend/static/drf-yasg/swagger-ui-dist/swagger-ui.css
Normal file
File diff suppressed because one or more lines are too long
385
backend/static/drf-yasg/swagger-ui-init.js
Normal file
385
backend/static/drf-yasg/swagger-ui-init.js
Normal file
@@ -0,0 +1,385 @@
|
||||
"use strict";
|
||||
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
|
||||
var defaultSpecUrl = currentPath + '?format=openapi';
|
||||
|
||||
function slugify(text) {
|
||||
return text.toString().toLowerCase()
|
||||
.replace(/\s+/g, '-') // Replace spaces with -
|
||||
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
|
||||
.replace(/--+/g, '-') // Replace multiple - with single -
|
||||
.replace(/^-+/, '') // Trim - from start of text
|
||||
.replace(/-+$/, ''); // Trim - from end of text
|
||||
}
|
||||
|
||||
var KEY_AUTH = slugify(window.location.pathname) + "-drf-yasg-auth";
|
||||
|
||||
// load the saved authorization state from localStorage; ImmutableJS is used for consistency with swagger-ui state
|
||||
var savedAuth = Immutable.fromJS({});
|
||||
|
||||
// global SwaggerUI config object; can be changed directly or by hooking initSwaggerUiConfig
|
||||
var swaggerUiConfig = {
|
||||
url: defaultSpecUrl,
|
||||
dom_id: '#swagger-ui',
|
||||
displayRequestDuration: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout",
|
||||
filter: true,
|
||||
requestInterceptor: function (request) {
|
||||
var headers = request.headers || {};
|
||||
var csrftoken = document.querySelector("[name=csrfmiddlewaretoken]");
|
||||
if (csrftoken) {
|
||||
headers["X-CSRFToken"] = csrftoken.value;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
};
|
||||
|
||||
function patchSwaggerUi() {
|
||||
if (document.querySelector('.auth-wrapper #django-session-auth')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var authWrapper = document.querySelector('.auth-wrapper');
|
||||
var authorizeButton = document.querySelector('.auth-wrapper .authorize');
|
||||
var djangoSessionAuth = document.querySelector('#django-session-auth');
|
||||
|
||||
if (!djangoSessionAuth) {
|
||||
console.log("WARNING: session auth disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
djangoSessionAuth = djangoSessionAuth.cloneNode(true);
|
||||
authWrapper.insertBefore(djangoSessionAuth, authorizeButton);
|
||||
djangoSessionAuth.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function initSwaggerUi() {
|
||||
if (window.ui) {
|
||||
console.log("WARNING: skipping initSwaggerUi() because window.ui is already defined");
|
||||
return;
|
||||
}
|
||||
if (document.querySelector('.auth-wrapper .authorize')) {
|
||||
patchSwaggerUi();
|
||||
} else {
|
||||
insertionQ('.auth-wrapper .authorize').every(patchSwaggerUi);
|
||||
}
|
||||
|
||||
var swaggerSettings = JSON.parse(document.getElementById('swagger-settings').innerHTML);
|
||||
|
||||
var oauth2RedirectUrl = document.getElementById('oauth2-redirect-url');
|
||||
if (oauth2RedirectUrl) {
|
||||
if (!('oauth2RedirectUrl' in swaggerSettings)) {
|
||||
if (oauth2RedirectUrl) {
|
||||
swaggerSettings['oauth2RedirectUrl'] = oauth2RedirectUrl.href;
|
||||
}
|
||||
}
|
||||
oauth2RedirectUrl.parentNode.removeChild(oauth2RedirectUrl);
|
||||
}
|
||||
|
||||
console.log('swaggerSettings', swaggerSettings);
|
||||
var oauth2Config = JSON.parse(document.getElementById('oauth2-config').innerHTML);
|
||||
console.log('oauth2Config', oauth2Config);
|
||||
|
||||
initSwaggerUiConfig(swaggerSettings, oauth2Config);
|
||||
window.ui = SwaggerUIBundle(swaggerUiConfig);
|
||||
window.ui.initOAuth(oauth2Config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the global swaggerUiConfig with any given additional settings.
|
||||
* @param swaggerSettings SWAGGER_SETTINGS from Django settings
|
||||
* @param oauth2Settings OAUTH2_CONFIG from Django settings
|
||||
*/
|
||||
function initSwaggerUiConfig(swaggerSettings, oauth2Settings) {
|
||||
var persistAuth = swaggerSettings.persistAuth;
|
||||
var refetchWithAuth = swaggerSettings.refetchWithAuth;
|
||||
var refetchOnLogout = swaggerSettings.refetchOnLogout;
|
||||
var fetchSchemaWithQuery = swaggerSettings.fetchSchemaWithQuery;
|
||||
delete swaggerSettings['persistAuth'];
|
||||
delete swaggerSettings['refetchWithAuth'];
|
||||
delete swaggerSettings['refetchOnLogout'];
|
||||
delete swaggerSettings['fetchSchemaWithQuery'];
|
||||
|
||||
for (var p in swaggerSettings) {
|
||||
if (swaggerSettings.hasOwnProperty(p)) {
|
||||
swaggerUiConfig[p] = swaggerSettings[p];
|
||||
}
|
||||
}
|
||||
|
||||
var specURL = swaggerUiConfig.url;
|
||||
if (fetchSchemaWithQuery) {
|
||||
// only add query params from document for the first spec request
|
||||
// this ensures we otherwise honor the spec selector box which might be manually modified
|
||||
var query = new URLSearchParams(window.location.search || '').entries();
|
||||
for (var it = query.next(); !it.done; it = query.next()) {
|
||||
specURL = setQueryParam(specURL, it.value[0], it.value[1]);
|
||||
}
|
||||
}
|
||||
if (persistAuth) {
|
||||
try {
|
||||
savedAuth = Immutable.fromJS(JSON.parse(localStorage.getItem(KEY_AUTH)) || {});
|
||||
} catch (e) {
|
||||
localStorage.removeItem(KEY_AUTH);
|
||||
}
|
||||
}
|
||||
if (refetchWithAuth) {
|
||||
specURL = applyAuth(savedAuth, specURL) || specURL;
|
||||
}
|
||||
swaggerUiConfig.url = specURL;
|
||||
|
||||
if (persistAuth || refetchWithAuth) {
|
||||
var hookedAuth = false;
|
||||
|
||||
var oldOnComplete = swaggerUiConfig.onComplete;
|
||||
swaggerUiConfig.onComplete = function () {
|
||||
if (persistAuth) {
|
||||
preauthorizeAll(savedAuth, window.ui);
|
||||
}
|
||||
|
||||
if (!hookedAuth) {
|
||||
hookAuthActions(window.ui, persistAuth, refetchWithAuth, refetchOnLogout);
|
||||
hookedAuth = true;
|
||||
}
|
||||
if (oldOnComplete) {
|
||||
oldOnComplete();
|
||||
}
|
||||
};
|
||||
|
||||
var specRequestsInFlight = {};
|
||||
var oldRequestInterceptor = swaggerUiConfig.requestInterceptor;
|
||||
swaggerUiConfig.requestInterceptor = function (request) {
|
||||
var headers = request.headers || {};
|
||||
if (request.loadSpec) {
|
||||
var newUrl = request.url;
|
||||
if (refetchWithAuth) {
|
||||
newUrl = applyAuth(savedAuth, newUrl, headers) || newUrl;
|
||||
}
|
||||
|
||||
if (newUrl !== request.url) {
|
||||
request.url = newUrl;
|
||||
|
||||
if (window.ui) {
|
||||
// this visually updates the spec url before the request is done, i.e. while loading
|
||||
window.ui.specActions.updateUrl(request.url);
|
||||
} else {
|
||||
// setTimeout is needed here because the request interceptor can be called *during*
|
||||
// window.ui initialization (by the SwaggerUIBundle constructor)
|
||||
setTimeout(function () {
|
||||
window.ui.specActions.updateUrl(request.url);
|
||||
});
|
||||
}
|
||||
|
||||
// need to manually remember requests for spec urls because
|
||||
// responseInterceptor has no reference to the request...
|
||||
var absUrl = new URL(request.url, currentPath);
|
||||
specRequestsInFlight[absUrl.href] = request.url;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldRequestInterceptor) {
|
||||
request = oldRequestInterceptor(request);
|
||||
}
|
||||
return request;
|
||||
};
|
||||
|
||||
var oldResponseInterceptor = swaggerUiConfig.responseInterceptor;
|
||||
swaggerUiConfig.responseInterceptor = function (response) {
|
||||
var absUrl = new URL(response.url, currentPath);
|
||||
if (absUrl.href in specRequestsInFlight) {
|
||||
var setToUrl = specRequestsInFlight[absUrl.href];
|
||||
delete specRequestsInFlight[absUrl.href];
|
||||
if (response.ok) {
|
||||
// need setTimeout here because swagger-ui insists to call updateUrl
|
||||
// with the initial request url after the response...
|
||||
setTimeout(function () {
|
||||
var currentUrl = new URL(window.ui.specSelectors.url(), currentPath);
|
||||
if (currentUrl.href !== absUrl.href) {
|
||||
window.ui.specActions.updateUrl(setToUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (oldResponseInterceptor) {
|
||||
response = oldResponseInterceptor(response);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _usp(url, fn) {
|
||||
url = url.split('?');
|
||||
var usp = new URLSearchParams(url[1] || '');
|
||||
fn(usp);
|
||||
url[1] = usp.toString();
|
||||
return url[1] ? url.join('?') : url[0];
|
||||
}
|
||||
|
||||
function setQueryParam(url, key, value) {
|
||||
return _usp(url, function (usp) {
|
||||
usp.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
function removeQueryParam(url, key) {
|
||||
return _usp(url, function (usp) {
|
||||
usp.delete(key);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Call sui.preauthorize### for all authorizations in authorization.
|
||||
* @param authorization authorization object {key => authScheme} saved from authActions.authorize
|
||||
* @param sui SwaggerUI or SwaggerUIBundle instance
|
||||
*/
|
||||
function preauthorizeAll(authorization, sui) {
|
||||
authorization.valueSeq().forEach(function (authScheme) {
|
||||
var schemeName = authScheme.get("name"), schemeType = authScheme.getIn(["schema", "type"]);
|
||||
if (schemeType === "basic" && schemeName) {
|
||||
var username = authScheme.getIn(["value", "username"]);
|
||||
var password = authScheme.getIn(["value", "password"]);
|
||||
if (username && password) {
|
||||
sui.preauthorizeBasic(schemeName, username, password);
|
||||
}
|
||||
} else if (schemeType === "apiKey" && schemeName) {
|
||||
var key = authScheme.get("value");
|
||||
if (key) {
|
||||
sui.preauthorizeApiKey(schemeName, key);
|
||||
}
|
||||
} else {
|
||||
// TODO: OAuth2
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually apply auth headers from the given auth object.
|
||||
* @param {object} authorization authorization object {key => authScheme} saved from authActions.authorize
|
||||
* @param {string} requestUrl the request url
|
||||
* @param {object} requestHeaders target headers, modified in place by the function
|
||||
* @return string new request url
|
||||
*/
|
||||
function applyAuth(authorization, requestUrl, requestHeaders) {
|
||||
authorization.valueSeq().forEach(function (authScheme) {
|
||||
requestHeaders = requestHeaders || {};
|
||||
var schemeName = authScheme.get("name"), schemeType = authScheme.getIn(["schema", "type"]);
|
||||
if (schemeType === "basic" && schemeName) {
|
||||
var username = authScheme.getIn(["value", "username"]);
|
||||
var password = authScheme.getIn(["value", "password"]);
|
||||
if (username && password) {
|
||||
requestHeaders["Authorization"] = "Basic " + btoa(username + ":" + password);
|
||||
}
|
||||
} else if (schemeType === "apiKey" && schemeName) {
|
||||
var _in = authScheme.getIn(["schema", "in"]), paramName = authScheme.getIn(["schema", "name"]);
|
||||
var key = authScheme.get("value");
|
||||
if (key && paramName) {
|
||||
if (_in === "header") {
|
||||
requestHeaders[paramName] = key;
|
||||
}
|
||||
if (_in === "query") {
|
||||
if (requestUrl) {
|
||||
requestUrl = setQueryParam(requestUrl, paramName, key);
|
||||
} else {
|
||||
console.warn("WARNING: cannot apply apiKey query parameter via interceptor");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: OAuth2
|
||||
}
|
||||
});
|
||||
|
||||
return requestUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given authorization scheme from the url.
|
||||
* @param {object} authorization authorization object {key => authScheme} containing schemes to deauthorize
|
||||
* @param {string} requestUrl request url
|
||||
* @return string new request url
|
||||
*/
|
||||
function deauthUrl(authorization, requestUrl) {
|
||||
authorization.valueSeq().forEach(function (authScheme) {
|
||||
var schemeType = authScheme.getIn(["schema", "type"]);
|
||||
if (schemeType === "apiKey") {
|
||||
var _in = authScheme.getIn(["schema", "in"]), paramName = authScheme.getIn(["schema", "name"]);
|
||||
if (_in === "query" && requestUrl && paramName) {
|
||||
requestUrl = removeQueryParam(requestUrl, paramName);
|
||||
}
|
||||
} else {
|
||||
// TODO: OAuth2?
|
||||
}
|
||||
});
|
||||
return requestUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook the authorize and logout actions of SwaggerUI.
|
||||
* The hooks are used to persist authorization data and trigger schema refetch.
|
||||
* @param sui SwaggerUI or SwaggerUIBundle instance
|
||||
* @param {boolean} persistAuth true to save auth to local storage
|
||||
* @param {boolean} refetchWithAuth true to trigger schema fetch on login
|
||||
* @param {boolean} refetchOnLogout true to trigger schema fetch on logout
|
||||
*/
|
||||
function hookAuthActions(sui, persistAuth, refetchWithAuth, refetchOnLogout) {
|
||||
if (!persistAuth && !refetchWithAuth) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
var originalAuthorize = sui.authActions.authorize;
|
||||
sui.authActions.authorize = function (authorization) {
|
||||
originalAuthorize(authorization);
|
||||
// authorization is map of scheme name to scheme object
|
||||
// need to use ImmutableJS because schema is already an ImmutableJS object
|
||||
var newAuths = Immutable.fromJS(authorization);
|
||||
savedAuth = savedAuth.merge(newAuths);
|
||||
|
||||
if (refetchWithAuth) {
|
||||
var url = sui.specSelectors.url();
|
||||
url = applyAuth(savedAuth, url) || url;
|
||||
sui.specActions.updateUrl(url);
|
||||
sui.specActions.download();
|
||||
sui.authActions.showDefinitions(); // hide authorize dialog
|
||||
}
|
||||
if (persistAuth) {
|
||||
localStorage.setItem(KEY_AUTH, JSON.stringify(savedAuth.toJSON()));
|
||||
}
|
||||
};
|
||||
|
||||
var originalLogout = sui.authActions.logout;
|
||||
sui.authActions.logout = function (authorization) {
|
||||
// stash logged out methods for use with deauthUrl
|
||||
var loggedOut = savedAuth.filter(function (val, key) {
|
||||
return authorization.indexOf(key) !== -1;
|
||||
}).mapEntries(function (entry) {
|
||||
return [entry[0], entry[1].set("value", null)]
|
||||
});
|
||||
// remove logged out methods from savedAuth
|
||||
savedAuth = savedAuth.filter(function (val, key) {
|
||||
return authorization.indexOf(key) === -1;
|
||||
});
|
||||
|
||||
if (refetchWithAuth) {
|
||||
var url = sui.specSelectors.url();
|
||||
url = deauthUrl(loggedOut, url) || url;
|
||||
sui.specActions.updateUrl(url);
|
||||
sui.specActions.download(url);
|
||||
sui.authActions.showDefinitions(); // hide authorize dialog
|
||||
}
|
||||
if (persistAuth) {
|
||||
localStorage.setItem(KEY_AUTH, JSON.stringify(savedAuth.toJSON()));
|
||||
}
|
||||
originalLogout(authorization);
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener('load', initSwaggerUi);
|
||||
1
backend/static/drf-yasg/url-polyfill.min.js
vendored
Normal file
1
backend/static/drf-yasg/url-polyfill.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
backend/static/rest_framework/css/bootstrap-theme.min.css
vendored
Normal file
6
backend/static/rest_framework/css/bootstrap-theme.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user