初始化master
This commit is contained in:
commit
e2a20fe67a
|
@ -0,0 +1,16 @@
|
|||
.vscode/
|
||||
.vs/
|
||||
.idea/
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
media/*
|
||||
dist/*
|
||||
!media/default/
|
||||
celerybeat.pid
|
||||
celerybeat-schedule.bak
|
||||
celerybeat-schedule.dat
|
||||
celerybeat-schedule.dir
|
||||
db.sqlite3
|
||||
server/conf.py
|
||||
sh/*
|
|
@ -0,0 +1,2 @@
|
|||
from django.contrib import admin
|
||||
# Register your models here.
|
|
@ -0,0 +1,7 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthConfig(AppConfig):
|
||||
name = 'apps.auth1'
|
||||
verbose_name = "认证"
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class CustomBackend(ModelBackend):
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
if username is None:
|
||||
username = kwargs.get(UserModel.USERNAME_FIELD)
|
||||
if username is None or password is None:
|
||||
return
|
||||
try:
|
||||
user = UserModel._default_manager.get(
|
||||
Q(username=username) | Q(phone=username) | Q(email=username))
|
||||
except UserModel.DoesNotExist:
|
||||
# Run the default password hasher once to reduce the timing
|
||||
# difference between an existing and a nonexistent user (#20760).
|
||||
UserModel().set_password(password)
|
||||
else:
|
||||
if user.check_password(password) and self.user_can_authenticate(user):
|
||||
return user
|
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -0,0 +1,5 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(label="用户名")
|
||||
password = serializers.CharField(label="密码")
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,14 @@
|
|||
from django.urls import path
|
||||
from rest_framework_simplejwt.views import (TokenObtainPairView,
|
||||
TokenRefreshView)
|
||||
|
||||
from apps.auth1.views import LoginView, LogoutView, TokenBlackView
|
||||
|
||||
API_BASE_URL = 'api/auth/'
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL + 'token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path(API_BASE_URL + 'token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path(API_BASE_URL + 'token/black/', TokenBlackView.as_view(), name='token_black'),
|
||||
path(API_BASE_URL + 'login/', LoginView.as_view(), name='session_login'),
|
||||
path(API_BASE_URL + 'logout/', LogoutView.as_view(), name='session_logout')
|
||||
]
|
|
@ -0,0 +1,60 @@
|
|||
|
||||
from django.shortcuts import render
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from apps.auth1.serializers import LoginSerializer
|
||||
from apps.utils.response import FailResponse, SuccessResponse
|
||||
# Create your views here.
|
||||
|
||||
class TokenBlackView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Token拉黑
|
||||
|
||||
|
||||
Token拉黑
|
||||
"""
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
class LoginView(CreateAPIView):
|
||||
"""
|
||||
Session登录
|
||||
|
||||
|
||||
账户密码Session登录
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = LoginSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
vdata = serializer.validated_data
|
||||
user = authenticate(username = vdata.get('username'),
|
||||
password = vdata.get('password'))
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
return SuccessResponse()
|
||||
return FailResponse(msg='账户或密码错误')
|
||||
|
||||
class LogoutView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
退出登录
|
||||
|
||||
|
||||
退出登录
|
||||
"""
|
||||
logout(request)
|
||||
return SuccessResponse()
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MonitorConfig(AppConfig):
|
||||
name = 'apps.monitor'
|
||||
verbose_name = '系统监控'
|
|
@ -0,0 +1,46 @@
|
|||
import imp
|
||||
import json
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
|
||||
class MonitorConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
self.room_name = self.scope['url_route']['kwargs']['room_name']
|
||||
self.room_group_name = 'chat_%s' % self.room_name
|
||||
|
||||
# Join room group
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
# Leave room group
|
||||
await self.channel_layer.group_discard(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Receive message from WebSocket
|
||||
async def receive(self, text_data):
|
||||
text_data_json = json.loads(text_data)
|
||||
message = text_data_json['message']
|
||||
|
||||
# Send message to room group
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'chat_message',
|
||||
'message': message
|
||||
}
|
||||
)
|
||||
|
||||
# Receive message from room group
|
||||
async def chat_message(self, event):
|
||||
message = event['message']
|
||||
|
||||
# Send message to WebSocket
|
||||
await self.send(text_data=json.dumps({
|
||||
'message': message
|
||||
}))
|
|
@ -0,0 +1 @@
|
|||
from django.utils.deprecation import MiddlewareMixin
|
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -0,0 +1,8 @@
|
|||
from django.urls import path
|
||||
from apps.monitor import consumers
|
||||
|
||||
WS_BASE_URL = 'ws/monitor/'
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path(WS_BASE_URL + '<str:room_name>/', consumers.MonitorConsumer.as_asgi())
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
<!-- chat/templates/chat/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Chat Rooms</title>
|
||||
</head>
|
||||
<body>
|
||||
What chat room would you like to enter?<br>
|
||||
<input id="room-name-input" type="text" size="100"><br>
|
||||
<input id="room-name-submit" type="button" value="Enter">
|
||||
|
||||
<script>
|
||||
document.querySelector('#room-name-input').focus();
|
||||
document.querySelector('#room-name-input').onkeyup = function(e) {
|
||||
if (e.keyCode === 13) { // enter, return
|
||||
document.querySelector('#room-name-submit').click();
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('#room-name-submit').onclick = function(e) {
|
||||
var roomName = document.querySelector('#room-name-input').value;
|
||||
window.location.pathname = '/monitor/' + roomName + '/';
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,50 @@
|
|||
<!-- chat/templates/chat/room.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Chat Room</title>
|
||||
</head>
|
||||
<body>
|
||||
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
|
||||
<input id="chat-message-input" type="text" size="100"><br>
|
||||
<input id="chat-message-submit" type="button" value="Send">
|
||||
{{ room_name|json_script:"room-name" }}
|
||||
<script>
|
||||
const roomName = JSON.parse(document.getElementById('room-name').textContent);
|
||||
|
||||
const chatSocket = new WebSocket(
|
||||
'ws://'
|
||||
+ window.location.host
|
||||
+ '/ws/monitor/'
|
||||
+ roomName
|
||||
+ '/'
|
||||
);
|
||||
|
||||
chatSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
document.querySelector('#chat-log').value += (data.message + '\n');
|
||||
};
|
||||
|
||||
chatSocket.onclose = function(e) {
|
||||
console.error('Chat socket closed unexpectedly');
|
||||
};
|
||||
|
||||
document.querySelector('#chat-message-input').focus();
|
||||
document.querySelector('#chat-message-input').onkeyup = function(e) {
|
||||
if (e.keyCode === 13) { // enter, return
|
||||
document.querySelector('#chat-message-submit').click();
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('#chat-message-submit').onclick = function(e) {
|
||||
const messageInputDom = document.querySelector('#chat-message-input');
|
||||
const message = messageInputDom.value;
|
||||
chatSocket.send(JSON.stringify({
|
||||
'message': message
|
||||
}));
|
||||
messageInputDom.value = '';
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8 />
|
||||
<title>videojs-contrib-hls embed</title>
|
||||
|
||||
<link href="https://unpkg.com/video.js/dist/video-js.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/video.js/dist/video.js"></script>
|
||||
<script src="https://unpkg.com/videojs-contrib-hls/dist/videojs-contrib-hls.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>Video.js Example Embed</h1>
|
||||
|
||||
<video id="my_video_1" class="video-js vjs-default-skin" controls preload="auto" width="640" height="268"
|
||||
data-setup='{}'>
|
||||
<source src="http://60.191.94.122:20046/live/cameraid/1001339%240/substream/1.m3u8" type="application/x-mpegURL">
|
||||
</video>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,16 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework import routers
|
||||
from .views import ServerInfoView, LogView, LogDetailView, index, room, video
|
||||
|
||||
API_BASE_URL = 'api/monitor/'
|
||||
HTML_BASE_URL = 'monitor/'
|
||||
urlpatterns = [
|
||||
path(HTML_BASE_URL, index),
|
||||
path(HTML_BASE_URL + 'index/', index),
|
||||
path(HTML_BASE_URL + 'video/', video),
|
||||
path(HTML_BASE_URL + '<str:room_name>/', room, name='room'),
|
||||
|
||||
path(API_BASE_URL + 'log/', LogView.as_view()),
|
||||
path(API_BASE_URL + 'log/<str:name>/', LogDetailView.as_view()),
|
||||
path(API_BASE_URL + 'server/', ServerInfoView.as_view()),
|
||||
]
|
|
@ -0,0 +1,120 @@
|
|||
|
||||
from django.shortcuts import render
|
||||
import psutil
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.viewsets import ViewSet
|
||||
from django.conf import settings
|
||||
import os
|
||||
from rest_framework import serializers, status
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from apps.utils.response import SuccessResponse
|
||||
# Create your views here.
|
||||
|
||||
|
||||
def index(request):
|
||||
return render(request, 'monitor/index.html')
|
||||
|
||||
|
||||
def room(request, room_name):
|
||||
return render(request, 'monitor/room.html', {
|
||||
'room_name': room_name
|
||||
})
|
||||
|
||||
def video(request):
|
||||
return render(request, 'monitor/video.html')
|
||||
|
||||
|
||||
class ServerInfoView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
获取服务器当前状态
|
||||
|
||||
cpu/内存/硬盘
|
||||
"""
|
||||
ret = {'cpu': {}, 'memory': {}, 'disk': {}}
|
||||
ret['cpu']['count'] = psutil.cpu_count()
|
||||
ret['cpu']['lcount'] = psutil.cpu_count(logical=False)
|
||||
ret['cpu']['percent'] = psutil.cpu_percent(interval=1)
|
||||
memory = psutil.virtual_memory()
|
||||
ret['memory']['total'] = round(memory.total/1024/1024/1024, 2)
|
||||
ret['memory']['used'] = round(memory.used/1024/1024/1024, 2)
|
||||
ret['memory']['percent'] = memory.percent
|
||||
disk = psutil.disk_usage('/')
|
||||
ret['disk']['total'] = round(disk.total/1024/1024/1024, 2)
|
||||
ret['disk']['used'] = round(disk.used/1024/1024/1024, 2)
|
||||
ret['disk']['percent'] = disk.percent
|
||||
return SuccessResponse(ret)
|
||||
|
||||
|
||||
def get_file_list(file_path):
|
||||
dir_list = os.listdir(file_path)
|
||||
if not dir_list:
|
||||
return
|
||||
else:
|
||||
# 注意,这里使用lambda表达式,将文件按照最后修改时间顺序升序排列
|
||||
# os.path.getmtime() 函数是获取文件最后修改时间
|
||||
# os.path.getctime() 函数是获取文件最后创建时间
|
||||
dir_list = sorted(dir_list, key=lambda x: os.path.getmtime(
|
||||
os.path.join(file_path, x)), reverse=True)
|
||||
# print(dir_list)
|
||||
return dir_list
|
||||
|
||||
|
||||
class LogView(APIView):
|
||||
|
||||
@swagger_auto_schema(manual_parameters=[
|
||||
openapi.Parameter('name', openapi.IN_QUERY,
|
||||
description='日志文件名', type=openapi.TYPE_STRING)
|
||||
])
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
查看最近的日志列表
|
||||
|
||||
查看最近的日志列表
|
||||
"""
|
||||
logs = []
|
||||
name = request.GET.get('name', None)
|
||||
# for root, dirs, files in os.walk(settings.LOG_PATH):
|
||||
# files.reverse()
|
||||
for file in get_file_list(settings.LOG_PATH):
|
||||
if len(logs) > 50:
|
||||
break
|
||||
filepath = os.path.join(settings.LOG_PATH, file)
|
||||
if name:
|
||||
if name in filepath:
|
||||
fsize = os.path.getsize(filepath)
|
||||
if fsize:
|
||||
logs.append({
|
||||
"name": file,
|
||||
"filepath": filepath,
|
||||
"size": round(fsize/1000, 1)
|
||||
})
|
||||
else:
|
||||
fsize = os.path.getsize(filepath)
|
||||
if fsize:
|
||||
logs.append({
|
||||
"name": file,
|
||||
"filepath": filepath,
|
||||
"size": round(fsize/1000, 1)
|
||||
})
|
||||
return SuccessResponse(data=logs)
|
||||
|
||||
|
||||
class LogDetailView(APIView):
|
||||
|
||||
def get(self, request, name):
|
||||
"""
|
||||
查看日志详情
|
||||
|
||||
查看日志详情
|
||||
"""
|
||||
try:
|
||||
with open(os.path.join(settings.LOG_PATH, name)) as f:
|
||||
data = f.read()
|
||||
return Response(data)
|
||||
except:
|
||||
return Response('未找到', status=status.HTTP_404_NOT_FOUND)
|
|
@ -0,0 +1 @@
|
|||
default_app_config = 'apps.system.apps.SystemConfig'
|
|
@ -0,0 +1,11 @@
|
|||
from django.contrib import admin
|
||||
from simple_history.admin import SimpleHistoryAdmin
|
||||
from .models import User, Dept, Role, Permission, DictType, Dict, File
|
||||
# Register your models here.
|
||||
admin.site.register(User)
|
||||
admin.site.register(Dept)
|
||||
admin.site.register(Role)
|
||||
admin.site.register(Permission)
|
||||
admin.site.register(DictType)
|
||||
admin.site.register(Dict, SimpleHistoryAdmin)
|
||||
admin.site.register(File)
|
|
@ -0,0 +1,9 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SystemConfig(AppConfig):
|
||||
name = 'apps.system'
|
||||
verbose_name = '系统管理'
|
||||
|
||||
def ready(self):
|
||||
import apps.system.signals
|
|
@ -0,0 +1,11 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserFilter(filters.FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = {
|
||||
'name': ['exact', 'contains'],
|
||||
'is_active': ['exact'],
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
# Generated by Django 3.2.12 on 2022-04-02 01:21
|
||||
|
||||
import apps.utils.snowflake
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(blank=True, max_length=20, null=True, verbose_name='姓名')),
|
||||
('phone', models.CharField(blank=True, max_length=11, null=True, unique=True, verbose_name='手机号')),
|
||||
('avatar', models.CharField(blank=True, default='/media/default/avatar.png', max_length=100, null=True, verbose_name='头像')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户信息',
|
||||
'verbose_name_plural': '用户信息',
|
||||
'ordering': ['create_time'],
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Dept',
|
||||
fields=[
|
||||
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=60, verbose_name='名称')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(10, '公司'), (20, '部门')], default=20, verbose_name='类型')),
|
||||
('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序标记')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dept', verbose_name='父')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '部门',
|
||||
'verbose_name_plural': '部门',
|
||||
'ordering': ['sort'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Permission',
|
||||
fields=[
|
||||
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=30, verbose_name='名称')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(10, '目录'), (20, '菜单'), (30, '按钮')], default=30, verbose_name='类型')),
|
||||
('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序标记')),
|
||||
('codes', models.JSONField(blank=True, default=list, null=True, verbose_name='权限标识')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.permission', verbose_name='父')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '功能权限表',
|
||||
'verbose_name_plural': '功能权限表',
|
||||
'ordering': ['sort'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Post',
|
||||
fields=[
|
||||
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=32, unique=True, verbose_name='名称')),
|
||||
('code', models.CharField(blank=True, max_length=32, null=True, unique=True, verbose_name='岗位标识')),
|
||||
('description', models.CharField(blank=True, max_length=50, null=True, verbose_name='描述')),
|
||||
('data_range', models.PositiveSmallIntegerField(choices=[(10, '全部'), (30, '同级及以下'), (40, '本级及以下'), (50, '本级'), (60, '仅本人')], default=40, verbose_name='数据权限范围')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='post_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '职位/岗位',
|
||||
'verbose_name_plural': '职位/岗位',
|
||||
'ordering': ['create_time'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserPost',
|
||||
fields=[
|
||||
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(blank=True, max_length=20, null=True, verbose_name='名称')),
|
||||
('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序')),
|
||||
('dept', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.dept')),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.post')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户岗位关系表',
|
||||
'verbose_name_plural': '用户岗位关系表',
|
||||
'ordering': ['sort', 'create_time'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=32, unique=True, verbose_name='角色')),
|
||||
('code', models.CharField(blank=True, max_length=32, null=True, unique=True, verbose_name='角色标识')),
|
||||
('description', models.CharField(blank=True, max_length=50, null=True, verbose_name='描述')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='role_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('perms', models.ManyToManyField(blank=True, related_name='role_perms', to='system.Permission', verbose_name='功能权限')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='role_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '角色',
|
||||
'verbose_name_plural': '角色',
|
||||
'ordering': ['code'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='roles',
|
||||
field=models.ManyToManyField(related_name='post_roles', to='system.Role', verbose_name='关联角色'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='post_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='File',
|
||||
fields=[
|
||||
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(blank=True, max_length=100, null=True, verbose_name='名称')),
|
||||
('size', models.IntegerField(blank=True, default=1, null=True, verbose_name='文件大小')),
|
||||
('file', models.FileField(upload_to='%Y/%m/%d/', verbose_name='文件')),
|
||||
('mime', models.CharField(blank=True, max_length=120, null=True, verbose_name='文件格式')),
|
||||
('type', models.CharField(choices=[(10, '文档'), (20, '视频'), (30, '音频'), (40, '图片'), (50, '其它')], default='文档', max_length=50, verbose_name='文件类型')),
|
||||
('path', models.CharField(blank=True, max_length=200, null=True, verbose_name='地址')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='file_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='file_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '文件库',
|
||||
'verbose_name_plural': '文件库',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DictType',
|
||||
fields=[
|
||||
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=30, verbose_name='名称')),
|
||||
('code', models.CharField(max_length=30, unique=True, verbose_name='标识')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dicttype_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dicttype', verbose_name='父')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dicttype_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '字典类型',
|
||||
'verbose_name_plural': '字典类型',
|
||||
'ordering': ['-create_time'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='depts',
|
||||
field=models.ManyToManyField(through='system.UserPost', to='system.Dept'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='groups',
|
||||
field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='posts',
|
||||
field=models.ManyToManyField(related_name='user_posts', through='system.UserPost', to='system.Post'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='superior',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='上级主管'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='user_permissions',
|
||||
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Dict',
|
||||
fields=[
|
||||
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=60, verbose_name='名称')),
|
||||
('value', models.CharField(max_length=10, verbose_name='值')),
|
||||
('code', models.CharField(blank=True, max_length=30, null=True, verbose_name='标识')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='描述')),
|
||||
('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序')),
|
||||
('is_used', models.BooleanField(default=True, verbose_name='是否有效')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dict_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dict', verbose_name='父')),
|
||||
('type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.dicttype', verbose_name='类型')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dict_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '字典',
|
||||
'verbose_name_plural': '字典',
|
||||
'ordering': ['sort'],
|
||||
'unique_together': {('name', 'is_used', 'type')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,220 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from apps.utils.models import CommonAModel, CommonBModel, BaseModel
|
||||
|
||||
|
||||
class Permission(BaseModel):
|
||||
"""
|
||||
功能权限:目录,菜单,按钮
|
||||
"""
|
||||
PERM_TYPE_LIST = 10
|
||||
PERM_TYPE_MENU = 20
|
||||
PERM_TYPE_BUTTON = 30
|
||||
menu_type_choices = (
|
||||
(PERM_TYPE_LIST, '目录'),
|
||||
(PERM_TYPE_MENU, '菜单'),
|
||||
(PERM_TYPE_BUTTON, '按钮')
|
||||
)
|
||||
name = models.CharField('名称', max_length=30)
|
||||
type = models.PositiveSmallIntegerField('类型', choices=menu_type_choices, default=30)
|
||||
sort = models.PositiveSmallIntegerField('排序标记', default=1)
|
||||
parent = models.ForeignKey('self', null=True, blank=True,
|
||||
on_delete=models.SET_NULL, verbose_name='父')
|
||||
codes = models.JSONField('权限标识', default=list, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = '功能权限表'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['sort']
|
||||
|
||||
|
||||
class Dept(CommonAModel):
|
||||
"""
|
||||
部门
|
||||
"""
|
||||
DEPT_TYPE_COMPANY = 10
|
||||
DEPT_TYPE_DEPT = 20
|
||||
dept_type_choices = (
|
||||
(DEPT_TYPE_COMPANY, '公司'),
|
||||
(DEPT_TYPE_DEPT, '部门')
|
||||
)
|
||||
name = models.CharField('名称', max_length=60)
|
||||
type = models.PositiveSmallIntegerField('类型', choices=dept_type_choices, default=20)
|
||||
parent = models.ForeignKey('self', null=True, blank=True,
|
||||
on_delete=models.SET_NULL, verbose_name='父')
|
||||
sort = models.PositiveSmallIntegerField('排序标记', default=1)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '部门'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['sort']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Role(CommonAModel):
|
||||
"""
|
||||
角色
|
||||
"""
|
||||
name = models.CharField('角色', max_length=32, unique=True)
|
||||
code = models.CharField('角色标识', max_length=32, unique=True, null=True, blank=True)
|
||||
perms = models.ManyToManyField(Permission, blank=True, verbose_name='功能权限', related_name='role_perms')
|
||||
description = models.CharField('描述', max_length=50, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '角色'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['code']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Post(CommonAModel):
|
||||
"""
|
||||
职位/岗位
|
||||
"""
|
||||
name = models.CharField('名称', max_length=32, unique=True)
|
||||
code = models.CharField('岗位标识', max_length=32, unique=True, null=True, blank=True)
|
||||
description = models.CharField('描述', max_length=50, blank=True, null=True)
|
||||
roles = models.ManyToManyField(Role, verbose_name='关联角色', related_name='post_roles')
|
||||
POST_DATA_ALL = 10
|
||||
POST_DATA_CUSTOM = 20
|
||||
POST_DATA_SAMELEVE_AND_BELOW = 30
|
||||
POST_DATA_THISLEVEL_AND_BELOW = 40
|
||||
POST_DATA_THISLEVEL = 50
|
||||
POST_DATA_MYSELF = 60
|
||||
data_type_choices = (
|
||||
(POST_DATA_ALL, '全部'),
|
||||
(POST_DATA_SAMELEVE_AND_BELOW, '同级及以下'),
|
||||
(POST_DATA_THISLEVEL_AND_BELOW, '本级及以下'),
|
||||
(POST_DATA_THISLEVEL, '本级'),
|
||||
(POST_DATA_MYSELF, '仅本人')
|
||||
)
|
||||
data_range = models.PositiveSmallIntegerField('数据权限范围', choices=data_type_choices,
|
||||
default=POST_DATA_THISLEVEL_AND_BELOW)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '职位/岗位'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['create_time']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class User(AbstractUser, CommonBModel):
|
||||
"""
|
||||
用户
|
||||
"""
|
||||
name = models.CharField('姓名', max_length=20, null=True, blank=True)
|
||||
phone = models.CharField('手机号', max_length=11,
|
||||
null=True, blank=True, unique=True)
|
||||
avatar = models.CharField(
|
||||
'头像', default='/media/default/avatar.png', max_length=100, null=True, blank=True)
|
||||
superior = models.ForeignKey(
|
||||
'self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='上级主管')
|
||||
posts = models.ManyToManyField(Post, through='system.userpost', related_name='user_posts')
|
||||
depts = models.ManyToManyField(Dept, through='system.userpost')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '用户信息'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['create_time']
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
|
||||
class UserPost(BaseModel):
|
||||
"""
|
||||
用户岗位关系表
|
||||
"""
|
||||
name = models.CharField('名称', max_length=20, null=True, blank=True)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
post = models.ForeignKey(Post, on_delete=models.CASCADE)
|
||||
dept = models.ForeignKey(Dept, on_delete=models.CASCADE)
|
||||
sort = models.PositiveSmallIntegerField('排序', default=1)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '用户岗位关系表'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['sort', 'create_time']
|
||||
|
||||
|
||||
class DictType(CommonAModel):
|
||||
"""
|
||||
数据字典类型
|
||||
"""
|
||||
name = models.CharField('名称', max_length=30)
|
||||
code = models.CharField('标识', unique=True, max_length=30)
|
||||
parent = models.ForeignKey('self', null=True, blank=True,
|
||||
on_delete=models.SET_NULL, verbose_name='父')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '字典类型'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-create_time']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Dict(CommonAModel):
|
||||
"""
|
||||
数据字典
|
||||
"""
|
||||
name = models.CharField('名称', max_length=60)
|
||||
value = models.CharField('值', max_length=10)
|
||||
code = models.CharField('标识', max_length=30, null=True, blank=True)
|
||||
description = models.TextField('描述', blank=True, null=True)
|
||||
type = models.ForeignKey(
|
||||
DictType, on_delete=models.CASCADE, verbose_name='类型')
|
||||
sort = models.PositiveSmallIntegerField('排序', default=1)
|
||||
parent = models.ForeignKey('self', null=True, blank=True,
|
||||
on_delete=models.SET_NULL, verbose_name='父')
|
||||
is_used = models.BooleanField('是否有效', default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '字典'
|
||||
verbose_name_plural = verbose_name
|
||||
unique_together = ('name', 'is_used', 'type')
|
||||
ordering = ['sort']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class File(CommonAModel):
|
||||
"""
|
||||
文件存储表,业务表根据具体情况选择是否外键关联
|
||||
"""
|
||||
FILE_TYPE_DOC = 10
|
||||
FILE_TYPE_VIDEO = 20
|
||||
FILE_TYPE_AUDIO = 30
|
||||
FILE_TYPE_PIC = 40
|
||||
FILE_TYPE_OTHER = 50
|
||||
name = models.CharField('名称', max_length=100, null=True, blank=True)
|
||||
size = models.IntegerField('文件大小', default=1, null=True, blank=True)
|
||||
file = models.FileField('文件', upload_to='%Y/%m/%d/')
|
||||
type_choices = (
|
||||
(FILE_TYPE_DOC, '文档'),
|
||||
(FILE_TYPE_VIDEO, '视频'),
|
||||
(FILE_TYPE_AUDIO, '音频'),
|
||||
(FILE_TYPE_PIC, '图片'),
|
||||
(FILE_TYPE_OTHER, '其它')
|
||||
)
|
||||
mime = models.CharField('文件格式', max_length=120, null=True, blank=True)
|
||||
type = models.CharField('文件类型', max_length=50, choices=type_choices, default='文档')
|
||||
path = models.CharField('地址', max_length=200, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '文件库'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
|
@ -0,0 +1,286 @@
|
|||
import re
|
||||
from django_celery_beat.models import PeriodicTask, CrontabSchedule, IntervalSchedule
|
||||
from rest_framework import serializers
|
||||
from django_celery_results.models import TaskResult
|
||||
from apps.utils.serializers import CustomModelSerializer
|
||||
from apps.utils.vars import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE
|
||||
from .models import (Dict, DictType, File, Dept, Permission, Post,
|
||||
Role, User, UserPost)
|
||||
|
||||
|
||||
class IntervalSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = IntervalSchedule
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class CrontabSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = CrontabSchedule
|
||||
exclude = ['timezone']
|
||||
|
||||
|
||||
class PTaskCreateUpdateSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = PeriodicTask
|
||||
fields = ['name', 'task', 'interval', 'crontab', 'args', 'kwargs']
|
||||
|
||||
|
||||
class PTaskSerializer(CustomModelSerializer):
|
||||
interval_ = IntervalSerializer(source='interval', read_only=True)
|
||||
crontab_ = CrontabSerializer(source='crontab', read_only=True)
|
||||
schedule = serializers.SerializerMethodField()
|
||||
timetype = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = PeriodicTask
|
||||
fields = '__all__'
|
||||
|
||||
@staticmethod
|
||||
def setup_eager_loading(queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.select_related('interval', 'crontab')
|
||||
return queryset
|
||||
|
||||
def get_schedule(self, obj):
|
||||
if obj.interval:
|
||||
return obj.interval.__str__()
|
||||
elif obj.crontab:
|
||||
return obj.crontab.__str__()
|
||||
return ''
|
||||
|
||||
def get_timetype(self, obj):
|
||||
if obj.interval:
|
||||
return 'interval'
|
||||
elif obj.crontab:
|
||||
return 'crontab'
|
||||
return 'interval'
|
||||
|
||||
|
||||
class FileSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = File
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class DictTypeSerializer(CustomModelSerializer):
|
||||
"""
|
||||
数据字典类型序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = DictType
|
||||
fields = '__all__'
|
||||
|
||||
class DictTypeCreateUpdateSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = DictType
|
||||
fields = ['name', 'code', 'parent']
|
||||
|
||||
|
||||
class DictSerializer(CustomModelSerializer):
|
||||
"""
|
||||
数据字典序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Dict
|
||||
fields = '__all__'
|
||||
|
||||
class DictCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
数据字典序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Dict
|
||||
exclude = EXCLUDE_FIELDS
|
||||
|
||||
class PostSerializer(CustomModelSerializer):
|
||||
"""
|
||||
岗位序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = '__all__'
|
||||
|
||||
class PostCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
岗位序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Post
|
||||
exclude = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class PostSimpleSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = ['id', 'name', 'code']
|
||||
|
||||
|
||||
class RoleSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class RoleSimpleSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ['id', 'name', 'code']
|
||||
|
||||
|
||||
class RoleCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
exclude = EXCLUDE_FIELDS
|
||||
|
||||
class PermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
权限序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = '__all__'
|
||||
|
||||
class PermissionCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
权限序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
exclude = EXCLUDE_FIELDS_BASE
|
||||
|
||||
class DeptSimpleSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Dept
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class DeptSerializer(CustomModelSerializer):
|
||||
"""
|
||||
组织架构序列化
|
||||
"""
|
||||
type = serializers.ChoiceField(
|
||||
choices=Dept.dept_type_choices, default='部门')
|
||||
|
||||
class Meta:
|
||||
model = Dept
|
||||
fields = '__all__'
|
||||
|
||||
class DeptCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
部门序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Dept
|
||||
exclude = EXCLUDE_FIELDS
|
||||
|
||||
class UserSimpleSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'name']
|
||||
|
||||
|
||||
class UserListSerializer(CustomModelSerializer):
|
||||
"""
|
||||
用户列表序列化
|
||||
"""
|
||||
belong_dept_ = DeptSimpleSerializer(source='belong_dept', read_only=True)
|
||||
posts_ = PostSimpleSerializer(source='posts', many=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
exclude = ['password']
|
||||
|
||||
@staticmethod
|
||||
def setup_eager_loading(queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.select_related('superior', 'belong_dept')
|
||||
queryset = queryset.prefetch_related('posts')
|
||||
return queryset
|
||||
|
||||
|
||||
class UserUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
用户编辑序列化
|
||||
"""
|
||||
phone = serializers.CharField(max_length=11, required=False)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'name', 'phone', 'email', 'belong_dept',
|
||||
'avatar', 'is_active', 'is_superuser']
|
||||
|
||||
def validate_phone(self, phone):
|
||||
re_phone = '^1[358]\d{9}$|^147\d{8}$|^176\d{8}$'
|
||||
if not re.match(re_phone, phone):
|
||||
raise serializers.ValidationError('手机号码不合法')
|
||||
return phone
|
||||
|
||||
|
||||
class UserCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
创建用户序列化
|
||||
"""
|
||||
username = serializers.CharField(required=True)
|
||||
phone = serializers.CharField(max_length=11, required=False)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'name', 'phone', 'email', 'belong_dept',
|
||||
'avatar', 'is_active']
|
||||
|
||||
def validate_username(self, username):
|
||||
if User.objects.filter(username=username):
|
||||
raise serializers.ValidationError(username + ' 账号已存在')
|
||||
return username
|
||||
|
||||
def validate_phone(self, phone):
|
||||
re_phone = '^1[358]\d{9}$|^147\d{8}$|^176\d{8}$'
|
||||
if not re.match(re_phone, phone):
|
||||
raise serializers.ValidationError('手机号码不合法')
|
||||
if User.objects.filter(phone=phone):
|
||||
raise serializers.ValidationError('手机号已经被注册')
|
||||
return phone
|
||||
|
||||
|
||||
class PTaskResultSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = TaskResult
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class UserPostSerializer(CustomModelSerializer):
|
||||
"""
|
||||
用户-岗位序列化
|
||||
"""
|
||||
user_ = UserSimpleSerializer(source='user', read_only=True)
|
||||
post_ = PostSimpleSerializer(source='post', read_only=True)
|
||||
dept_ = DeptSimpleSerializer(source='dept', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = UserPost
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class UserInfoSerializer(CustomModelSerializer):
|
||||
posts_ = UserPostSerializer(source='post', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'name', 'posts_', 'avatar']
|
|
@ -0,0 +1,12 @@
|
|||
from django.db.models.signals import m2m_changed
|
||||
from .models import Role, Permission, User
|
||||
from django.dispatch import receiver
|
||||
from django.core.cache import cache
|
||||
from apps.utils.permission import get_user_perms_map
|
||||
|
||||
# 变更用户角色时动态更新权限或者前端刷新
|
||||
# @receiver(m2m_changed, sender=User.roles.through)
|
||||
# def update_perms_cache_user(sender, instance, action, **kwargs):
|
||||
# if action in ['post_remove', 'post_add']:
|
||||
# if cache.get('perms_' + instance.id, None):
|
||||
# get_user_perms_map(instance)
|
|
@ -0,0 +1,9 @@
|
|||
# Create your tasks here
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task
|
||||
def show():
|
||||
print('ok')
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,27 @@
|
|||
from django.urls import path, include
|
||||
from .views import FileViewSet, PTaskResultViewSet, TaskList, UserPostViewSet, UserViewSet, DeptViewSet, PermissionViewSet, RoleViewSet, PostViewSet, DictTypeViewSet, DictViewSet, PTaskViewSet
|
||||
from rest_framework import routers
|
||||
|
||||
API_BASE_URL = 'api/system/'
|
||||
HTML_BASE_URL = 'system/'
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register('user', UserViewSet, basename="user")
|
||||
router.register('dept', DeptViewSet, basename="dept")
|
||||
router.register('permission', PermissionViewSet, basename="permission")
|
||||
router.register('role', RoleViewSet, basename="role")
|
||||
router.register('post', PostViewSet, basename="post")
|
||||
router.register('dicttype', DictTypeViewSet, basename="dicttype")
|
||||
router.register('dict', DictViewSet, basename="dict")
|
||||
router.register('ptask', PTaskViewSet, basename="ptask")
|
||||
router.register('ptask_result', PTaskResultViewSet, basename="ptask_result")
|
||||
router.register('user_post', UserPostViewSet, basename='user_post')
|
||||
|
||||
router2 = routers.DefaultRouter()
|
||||
router2.register('file', FileViewSet, basename='file')
|
||||
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
path(API_BASE_URL + 'task/', TaskList.as_view()),
|
||||
path('api/', include(router2.urls)),
|
||||
]
|
|
@ -0,0 +1,390 @@
|
|||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
from django.db import transaction
|
||||
from django_celery_beat.models import (CrontabSchedule, IntervalSchedule,
|
||||
PeriodicTask)
|
||||
from django_celery_results.models import TaskResult
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
|
||||
ListModelMixin, RetrieveModelMixin)
|
||||
from rest_framework.parsers import (JSONParser,
|
||||
MultiPartParser)
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.utils.mixins import (CustomCreateModelMixin)
|
||||
from apps.utils.permission import get_user_perms_map
|
||||
from apps.utils.queryset import get_child_queryset2
|
||||
from apps.utils.response import FailResponse, SuccessResponse
|
||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||
from server.celery import app as celery_app
|
||||
from .filters import UserFilter
|
||||
from .models import (Dept, Dict, DictType, File, Permission, Post, Role, User,
|
||||
UserPost)
|
||||
from .serializers import (DeptCreateUpdateSerializer, DeptSerializer, DictCreateUpdateSerializer, DictSerializer, DictTypeCreateUpdateSerializer, DictTypeSerializer,
|
||||
FileSerializer, PermissionCreateUpdateSerializer, PermissionSerializer, PostCreateUpdateSerializer, PostSerializer,
|
||||
PTaskCreateUpdateSerializer, PTaskResultSerializer,
|
||||
PTaskSerializer, RoleCreateUpdateSerializer, RoleSerializer,
|
||||
UserCreateSerializer, UserListSerializer,
|
||||
UserPostSerializer, UserUpdateSerializer)
|
||||
|
||||
logger = logging.getLogger('log')
|
||||
|
||||
|
||||
# logger.info('请求成功! response_code:{};response_headers:{};
|
||||
# response_body:{}'.format(response_code, response_headers, response_body[:251]))
|
||||
# logger.error('请求出错-{}'.format(error))
|
||||
|
||||
|
||||
class TaskList(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, requests):
|
||||
"""获取注册任务列表
|
||||
|
||||
获取注册任务列表
|
||||
"""
|
||||
tasks = list(
|
||||
sorted(name for name in celery_app.tasks if not name.startswith('celery.')))
|
||||
return Response(tasks)
|
||||
|
||||
|
||||
class PTaskViewSet(CustomModelViewSet):
|
||||
"""
|
||||
list:定时任务列表
|
||||
|
||||
定时任务列表
|
||||
|
||||
retrieve:定时任务详情
|
||||
|
||||
定时任务详情
|
||||
"""
|
||||
queryset = PeriodicTask.objects.exclude(name__contains='celery.')
|
||||
serializer_class = PTaskSerializer
|
||||
create_serializer_class = PTaskCreateUpdateSerializer
|
||||
update_serializer_class = PTaskCreateUpdateSerializer
|
||||
search_fields = ['name', 'task']
|
||||
filterset_fields = ['enabled']
|
||||
ordering = ['-create_time']
|
||||
|
||||
@action(methods=['put'], detail=True, perms_map={'put': 'ptask_update'})
|
||||
def toggle(self, request, pk=None):
|
||||
"""修改启用禁用状态
|
||||
|
||||
修改启用禁用状态
|
||||
"""
|
||||
obj = self.get_object()
|
||||
obj.enabled = False if obj.enabled else True
|
||||
obj.save()
|
||||
return SuccessResponse()
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""创建定时任务
|
||||
|
||||
创建定时任务
|
||||
"""
|
||||
data = request.data
|
||||
timetype = data.get('timetype', None)
|
||||
interval_ = data.get('interval_', None)
|
||||
crontab_ = data.get('crontab_', None)
|
||||
if timetype == 'interval' and interval_:
|
||||
data['crontab'] = None
|
||||
try:
|
||||
interval, _ = IntervalSchedule.objects.get_or_create(
|
||||
**interval_, defaults=interval_)
|
||||
data['interval'] = interval.id
|
||||
except:
|
||||
raise ValidationError('时间策略有误')
|
||||
if timetype == 'crontab' and crontab_:
|
||||
data['interval'] = None
|
||||
try:
|
||||
crontab_['timezone'] = 'Asia/Shanghai'
|
||||
crontab, _ = CrontabSchedule.objects.get_or_create(
|
||||
**crontab_, defaults=crontab_)
|
||||
data['crontab'] = crontab.id
|
||||
except:
|
||||
raise ValidationError('时间策略有误')
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return SuccessResponse()
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""更新定时任务
|
||||
|
||||
更新定时任务
|
||||
"""
|
||||
data = request.data
|
||||
timetype = data.get('timetype', None)
|
||||
interval_ = data.get('interval_', None)
|
||||
crontab_ = data.get('crontab_', None)
|
||||
if timetype == 'interval' and interval_:
|
||||
data['crontab'] = None
|
||||
try:
|
||||
if 'id' in interval_:
|
||||
del interval_['id']
|
||||
interval, _ = IntervalSchedule.objects.get_or_create(
|
||||
**interval_, defaults=interval_)
|
||||
data['interval'] = interval.id
|
||||
except:
|
||||
raise ValidationError('时间策略有误')
|
||||
if timetype == 'crontab' and crontab_:
|
||||
data['interval'] = None
|
||||
try:
|
||||
crontab_['timezone'] = 'Asia/Shanghai'
|
||||
if 'id' in crontab_:
|
||||
del crontab_['id']
|
||||
crontab, _ = CrontabSchedule.objects.get_or_create(
|
||||
**crontab_, defaults=crontab_)
|
||||
data['crontab'] = crontab.id
|
||||
except:
|
||||
raise ValidationError('时间策略有误')
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
class PTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
list:任务执行结果列表
|
||||
|
||||
任务执行结果列表
|
||||
|
||||
retrieve:任务执行结果详情
|
||||
|
||||
任务执行结果详情
|
||||
"""
|
||||
perms_map = {'get': '*'}
|
||||
filterset_fields = ['task_name']
|
||||
queryset = TaskResult.objects.all()
|
||||
serializer_class = PTaskResultSerializer
|
||||
ordering = ['-date_created']
|
||||
|
||||
|
||||
class DictTypeViewSet(CustomModelViewSet):
|
||||
"""数据字典类型-增删改查
|
||||
|
||||
数据字典类型-增删改查
|
||||
"""
|
||||
queryset = DictType.objects.all()
|
||||
serializer_class = DictTypeSerializer
|
||||
create_serializer_class = DictTypeCreateUpdateSerializer
|
||||
update_serializer_class = DictTypeCreateUpdateSerializer
|
||||
search_fields = ['name']
|
||||
|
||||
|
||||
class DictViewSet(CustomModelViewSet):
|
||||
"""数据字典-增删改查
|
||||
|
||||
数据字典-增删改查
|
||||
"""
|
||||
# queryset = Dict.objects.get_queryset(all=True) # 获取全部的,包括软删除的
|
||||
queryset = Dict.objects.all()
|
||||
filterset_fields = ['type', 'is_used', 'type__code']
|
||||
serializer_class = DictSerializer
|
||||
create_serializer_class = DictCreateUpdateSerializer
|
||||
update_serializer_class = DictCreateUpdateSerializer
|
||||
search_fields = ['name']
|
||||
|
||||
|
||||
class PostViewSet(CustomModelViewSet):
|
||||
"""岗位-增删改查
|
||||
|
||||
岗位-增删改查
|
||||
"""
|
||||
queryset = Post.objects.all()
|
||||
serializer_class = PostSerializer
|
||||
create_serializer_class = PostCreateUpdateSerializer
|
||||
update_serializer_class = PostCreateUpdateSerializer
|
||||
search_fields = ['name', 'description']
|
||||
|
||||
|
||||
class PermissionViewSet(CustomModelViewSet):
|
||||
"""菜单权限-增删改查
|
||||
|
||||
菜单权限-增删改查
|
||||
"""
|
||||
queryset = Permission.objects.all()
|
||||
filterset_fields = ['type']
|
||||
serializer_class = PermissionSerializer
|
||||
create_serializer_class = PermissionCreateUpdateSerializer
|
||||
update_serializer_class = PermissionCreateUpdateSerializer
|
||||
search_fields = ['name', 'code']
|
||||
|
||||
|
||||
class DeptViewSet(CustomModelViewSet):
|
||||
"""部门-增删改查
|
||||
|
||||
部门-增删改查
|
||||
"""
|
||||
queryset = Dept.objects.all()
|
||||
serializer_class = DeptSerializer
|
||||
create_serializer_class = DeptCreateUpdateSerializer
|
||||
update_serializer_class = DeptCreateUpdateSerializer
|
||||
filterset_fields = ['type']
|
||||
search_fields = ['name']
|
||||
|
||||
|
||||
class RoleViewSet(CustomModelViewSet):
|
||||
"""角色-增删改查
|
||||
|
||||
角色-增删改查
|
||||
"""
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
create_serializer_class = RoleCreateUpdateSerializer
|
||||
update_serializer_class = RoleCreateUpdateSerializer
|
||||
search_fields = ['name', 'code']
|
||||
|
||||
|
||||
class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
|
||||
"""用户/岗位关系
|
||||
|
||||
用户/岗位关系
|
||||
"""
|
||||
perms_map = {'get': '*', 'post': 'user_update', 'delete': 'user_update'}
|
||||
queryset = UserPost.objects.select_related('user', 'post', 'dept').all()
|
||||
serializer_class = UserPostSerializer
|
||||
filterset_fields = ['user', 'post', 'dept']
|
||||
|
||||
def perform_create(self, serializer):
|
||||
instance = serializer.save()
|
||||
user = instance.user
|
||||
adept = UserPost.objects.filter(user=user).order_by('sort', 'create_time').first()
|
||||
if adept:
|
||||
user.belong_dept = adept
|
||||
user.update_by = self.request.user
|
||||
user.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
user = instance.user
|
||||
instance.delete()
|
||||
fdept = UserPost.objects.filter(user=user).order_by('sort', 'create_time').first()
|
||||
if fdept:
|
||||
user.belong_dept = fdept
|
||||
else:
|
||||
user.belong_dept = None
|
||||
user.update_by = self.request.user
|
||||
user.save()
|
||||
|
||||
|
||||
class UserViewSet(CustomModelViewSet):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserListSerializer
|
||||
create_serializer_class = UserCreateSerializer
|
||||
update_serializer_class = UserUpdateSerializer
|
||||
filterset_class = UserFilter
|
||||
search_fields = ['username', 'name', 'phone', 'email']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset
|
||||
if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
|
||||
queryset = self.get_serializer_class().setup_eager_loading(queryset) # 性能优化
|
||||
dept = self.request.query_params.get(
|
||||
'belong_dept', None) # 该部门及其子部门所有员工
|
||||
if dept:
|
||||
dept_queryset = get_child_queryset2(
|
||||
Dept.objects.get(pk=dept))
|
||||
queryset = queryset.filter(dept__in=dept_queryset)
|
||||
return queryset
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""创建用户
|
||||
|
||||
创建用户
|
||||
"""
|
||||
password = request.data.get('password', None)
|
||||
if password:
|
||||
password = make_password(password)
|
||||
else:
|
||||
password = make_password('0000')
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(password=password)
|
||||
return SuccessResponse(data=serializer.data)
|
||||
|
||||
@action(methods=['put'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def password(self, request, pk=None):
|
||||
"""修改密码
|
||||
|
||||
修改密码
|
||||
"""
|
||||
user = request.user
|
||||
old_password = request.data['old_password']
|
||||
if check_password(old_password, user.password):
|
||||
new_password1 = request.data['new_password1']
|
||||
new_password2 = request.data['new_password2']
|
||||
if new_password1 == new_password2:
|
||||
user.set_password(new_password2)
|
||||
user.save()
|
||||
return SuccessResponse()
|
||||
else:
|
||||
return FailResponse(msg='新密码两次输入不一致!')
|
||||
else:
|
||||
return FailResponse(msg='旧密码错误!')
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def info(self, request, pk=None):
|
||||
"""登录用户信息
|
||||
|
||||
获取登录用户信息
|
||||
"""
|
||||
user = request.user
|
||||
perms = get_user_perms_map(user)
|
||||
data = {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'name': user.name,
|
||||
'posts': user.posts.values_list('name', flat=True),
|
||||
'avatar': user.avatar,
|
||||
'perms': perms,
|
||||
}
|
||||
return SuccessResponse(data)
|
||||
|
||||
|
||||
class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, CustomGenericViewSet):
|
||||
"""文件上传
|
||||
|
||||
list:
|
||||
文件列表
|
||||
|
||||
文件列表
|
||||
|
||||
create:
|
||||
文件上传
|
||||
|
||||
文件上传
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [MultiPartParser, JSONParser]
|
||||
queryset = File.objects.all()
|
||||
serializer_class = FileSerializer
|
||||
filterset_fields = ['type']
|
||||
search_fields = ['name']
|
||||
|
||||
def perform_create(self, serializer):
|
||||
file_obj = self.request.data.get('file')
|
||||
name = file_obj._name
|
||||
size = file_obj.size
|
||||
mime = file_obj.content_type
|
||||
file_type = '其它'
|
||||
if 'image' in mime:
|
||||
file_type = '图片'
|
||||
elif 'video' in mime:
|
||||
file_type = '视频'
|
||||
elif 'audio' in mime:
|
||||
file_type = '音频'
|
||||
elif 'application' or 'text' in mime:
|
||||
file_type = '文档'
|
||||
instance = serializer.save(
|
||||
create_by=self.request.user, name=name, size=size, type=file_type, mime=mime)
|
||||
instance.path = settings.MEDIA_URL + instance.file.name
|
||||
instance.save()
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ThirdConfig(AppConfig):
|
||||
name = 'apps.third'
|
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -0,0 +1,14 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class RequestCommonSerializer(serializers.Serializer):
|
||||
method_choice = (
|
||||
('post','post'),
|
||||
('get','get'),
|
||||
('put','put'),
|
||||
('delete','delete')
|
||||
)
|
||||
url = serializers.CharField(label='请求地址')
|
||||
method = serializers.ChoiceField(label='请求方法', choices=method_choice)
|
||||
params = serializers.JSONField(label='请求参数', required=False, allow_null=True)
|
||||
json = serializers.JSONField(label='请求body', required=False, allow_null=True)
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,16 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework import routers
|
||||
from apps.third.views import DahuaTestView, DhCommonViewSet, XxCommonViewSet, XxTestView
|
||||
|
||||
API_BASE_URL = 'api/third/'
|
||||
HTML_BASE_URL = 'third/'
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register('xunxi', XxCommonViewSet, basename='api_xunxi')
|
||||
router.register('dahua', DhCommonViewSet, basename='api_dahua')
|
||||
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
path(API_BASE_URL + 'dahua/test/', DahuaTestView.as_view()),
|
||||
path(API_BASE_URL + 'xunxi/test/', XxTestView.as_view()),
|
||||
]
|
|
@ -0,0 +1,95 @@
|
|||
from django.shortcuts import render
|
||||
from apps.utils.dahua import dhClient
|
||||
from apps.utils.xunxi import xxClient
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from apps.utils.viewsets import CustomGenericViewSet
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
|
||||
from apps.third.serializers import RequestCommonSerializer
|
||||
# Create your views here.
|
||||
|
||||
|
||||
class DahuaTestView(APIView):
|
||||
"""
|
||||
大华测试接口
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
data = {
|
||||
"data":{
|
||||
"channelId": "1001339$1$0$0",
|
||||
"streamType": "1",
|
||||
"type": "hls"
|
||||
}
|
||||
}
|
||||
res = dhClient.request(
|
||||
url='/evo-apigw/admin/API/video/stream/realtime', method='post', json=data)
|
||||
# data = {
|
||||
# "pageNum":1,
|
||||
# "pageSize":100,
|
||||
# "isOnline":1,
|
||||
# "showChildNodeData":1,
|
||||
# "categorys":[8]
|
||||
|
||||
# }
|
||||
# res = dhClient.request(
|
||||
# url='/evo-apigw/evo-brm/1.0.0/device/subsystem/page', method='post', json=data)
|
||||
# data = {
|
||||
# "channelCodeList": ["1001382$7$0$0"]
|
||||
# }
|
||||
# res = dhClient.request(
|
||||
# url='/evo-apigw/evo-accesscontrol/1.0.0/card/accessControl/channelControl/closeDoor', method='post', json=data)
|
||||
return Response(res)
|
||||
|
||||
|
||||
class XxTestView(APIView):
|
||||
"""
|
||||
寻息测试接口
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
res = xxClient.request(
|
||||
url='/api/application/build/buildListV2', json={})
|
||||
return Response(res)
|
||||
|
||||
|
||||
class XxCommonViewSet(CreateModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
寻息通用调用接口
|
||||
"""
|
||||
perms_map = {'post': '*'}
|
||||
serializer_class = RequestCommonSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
vdata = serializer.validated_data
|
||||
res = xxClient.request(
|
||||
url=vdata['url'],
|
||||
method=vdata.get('method', 'post'),
|
||||
params=vdata.get('params', {}),
|
||||
json=vdata.get('data', {}))
|
||||
return Response(res)
|
||||
|
||||
|
||||
class DhCommonViewSet(CreateModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
大华通用调用接口
|
||||
"""
|
||||
perms_map = {'post': '*'}
|
||||
serializer_class = RequestCommonSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
vdata = serializer.validated_data
|
||||
res = dhClient.request(
|
||||
url=vdata['url'],
|
||||
method=vdata.get('method', 'post'),
|
||||
params=vdata.get('params', {}),
|
||||
json=vdata.get('data', {}))
|
||||
return Response(res)
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UtilsConfig(AppConfig):
|
||||
name = 'apps.utils'
|
|
@ -0,0 +1,99 @@
|
|||
from threading import Thread
|
||||
import traceback
|
||||
import requests
|
||||
from apps.utils.tools import print_roundtrip
|
||||
from server import settings
|
||||
import json
|
||||
import time
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
class DhClient:
|
||||
"""
|
||||
大华
|
||||
"""
|
||||
|
||||
def __init__(self, client_id= settings.DAHUA_CLIENTID
|
||||
, client_secret = settings.DAHUA_SECRET) -> None:
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.headers = {}
|
||||
self.isGetingToken = False
|
||||
self.isRuning = True
|
||||
self.t = None # 线程
|
||||
self.setup()
|
||||
|
||||
def _get_token_loop(self):
|
||||
while self.isRuning:
|
||||
params = {
|
||||
'grant_type': 'client_credentials',
|
||||
'client_id': self.client_id,
|
||||
'client_secret': self.client_secret
|
||||
}
|
||||
r = requests.post(params=params, url=settings.DAHUA_BASE_URL + '/evo-apigw/evo-oauth/oauth/token', verify=False)
|
||||
ret = r.json()
|
||||
if ret['success']:
|
||||
self.headers['Authorization'] = 'bearer ' + ret['data']['access_token']
|
||||
self.headers['User-Id'] = '1'
|
||||
time.sleep(3600)
|
||||
|
||||
def get_token(self):
|
||||
self.isGetingToken = True
|
||||
params = {
|
||||
'grant_type': 'client_credentials',
|
||||
'client_id': self.client_id,
|
||||
'client_secret': self.client_secret
|
||||
}
|
||||
r = requests.post(params=params, url=settings.DAHUA_BASE_URL + '/evo-apigw/evo-oauth/oauth/token', verify=False)
|
||||
ret = r.json()
|
||||
if ret['success']:
|
||||
self.headers['Authorization'] = 'bearer ' + ret['data']['access_token']
|
||||
self.headers['User-Id'] = '1'
|
||||
self.isGetingToken = False
|
||||
|
||||
def setup(self):
|
||||
t = Thread(target=self._get_token_loop, args=(), daemon=True)
|
||||
t.start()
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
自定义销毁
|
||||
"""
|
||||
self.isRuning = False
|
||||
self.t.join()
|
||||
|
||||
def request(self, url:str, method:str, params=dict(), json=dict(), timeout=20):
|
||||
if self.isGetingToken:
|
||||
req_num = 0
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
if not self.isGetingToken:
|
||||
self.request(url, method, params, json, timeout)
|
||||
req_num = req_num + 1
|
||||
if req_num > 4:
|
||||
break
|
||||
else:
|
||||
r = getattr(requests, method)('{}{}'.format(settings.DAHUA_BASE_URL, url)
|
||||
, headers = self.headers, params=params, json=json, verify=False)
|
||||
if settings.DEBUG:
|
||||
print_roundtrip(r)
|
||||
if r.status_code == 200:
|
||||
"""
|
||||
请求成功
|
||||
"""
|
||||
ret = r.json()
|
||||
if ret.get('code') == '27001007':
|
||||
self.get_token() # 重新获取token
|
||||
self.request(url, method, params, json, timeout) # 重新请求
|
||||
else:
|
||||
|
||||
msg = '{}|{}{}'.format(str(ret['code']), ret.get('errMsg',''), ret.get('desc', ''))
|
||||
|
||||
res = dict(success=True, code=200000, msg= msg, data=ret.get('data', None))
|
||||
if ret['code'] not in ['0', '100', '00000']:
|
||||
res['success'] = False
|
||||
res['code'] = 400000
|
||||
return res
|
||||
return dict(success=False, code=400901, msg='大华接口访问异常', data=None)
|
||||
|
||||
if settings.DAHUA_ENABLED:
|
||||
dhClient = DhClient()
|
|
@ -0,0 +1,65 @@
|
|||
from django.db.models.query import QuerySet
|
||||
from rest_framework.mixins import CreateModelMixin, UpdateModelMixin, ListModelMixin, RetrieveModelMixin, DestroyModelMixin
|
||||
class CreateUpdateModelAMixin:
|
||||
"""
|
||||
业务用基本表A用
|
||||
"""
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(create_by = self.request.user)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(update_by = self.request.user)
|
||||
|
||||
class CreateUpdateModelBMixin:
|
||||
"""
|
||||
业务用基本表B用
|
||||
"""
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(create_by = self.request.user, belong_dept=self.request.user.dept)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(update_by = self.request.user)
|
||||
|
||||
class CreateUpdateCustomMixin:
|
||||
"""
|
||||
整合
|
||||
"""
|
||||
def perform_create(self, serializer):
|
||||
if hasattr(self.queryset.model, 'belong_dept'):
|
||||
serializer.save(create_by = self.request.user, belong_dept=self.request.user.dept)
|
||||
else:
|
||||
serializer.save(create_by = self.request.user)
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(update_by = self.request.user)
|
||||
|
||||
class OptimizationMixin:
|
||||
"""
|
||||
性能优化,需要在序列化器里定义setup_eager_loading,可在必要的View下继承
|
||||
"""
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset
|
||||
if isinstance(queryset, QuerySet):
|
||||
# Ensure queryset is re-evaluated on each request.
|
||||
queryset = queryset.all()
|
||||
if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
|
||||
queryset = self.get_serializer_class().setup_eager_loading(queryset) # 性能优化
|
||||
return queryset
|
||||
|
||||
class CustomCreateModelMixin(CreateModelMixin):
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if hasattr(self.queryset.model, 'belong_dept'):
|
||||
serializer.save(create_by = self.request.user, belong_dept=self.request.user.dept)
|
||||
else:
|
||||
serializer.save(create_by = self.request.user)
|
||||
|
||||
class CustomUpdateModelMixin(UpdateModelMixin):
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(update_by = self.request.user)
|
||||
|
||||
class CustomDestoryModelMixin(DestroyModelMixin):
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.delete(update_by = self.request.user)
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
from django.db import models
|
||||
import django.utils.timezone as timezone
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
from apps.utils.snowflake import worker
|
||||
|
||||
# 自定义软删除查询基类
|
||||
|
||||
|
||||
class SoftDeletableQuerySetMixin(object):
|
||||
'''
|
||||
QuerySet for SoftDeletableModel. Instead of removing instance sets
|
||||
its ``is_deleted`` field to True.
|
||||
'''
|
||||
|
||||
def delete(self, soft=True, update_by=None):
|
||||
'''
|
||||
Soft delete objects from queryset (set their ``is_deleted``
|
||||
field to True)
|
||||
'''
|
||||
if soft:
|
||||
self.update(is_deleted=True, update_by=update_by)
|
||||
else:
|
||||
return super(SoftDeletableQuerySetMixin, self).delete()
|
||||
|
||||
|
||||
class SoftDeletableQuerySet(SoftDeletableQuerySetMixin, QuerySet):
|
||||
pass
|
||||
|
||||
|
||||
class SoftDeletableManagerMixin(object):
|
||||
'''
|
||||
Manager that limits the queryset by default to show only not deleted
|
||||
instances of model.
|
||||
'''
|
||||
_queryset_class = SoftDeletableQuerySet
|
||||
|
||||
def get_queryset(self, all=False):
|
||||
'''
|
||||
Return queryset limited to not deleted entries.
|
||||
'''
|
||||
kwargs = {'model': self.model, 'using': self._db}
|
||||
if hasattr(self, '_hints'):
|
||||
kwargs['hints'] = self._hints
|
||||
if all:
|
||||
return self._queryset_class(**kwargs)
|
||||
return self._queryset_class(**kwargs).filter(is_deleted=False)
|
||||
|
||||
|
||||
class SoftDeletableManager(SoftDeletableManagerMixin, models.Manager):
|
||||
pass
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
"""
|
||||
基本表
|
||||
"""
|
||||
id = models.CharField(max_length=20, primary_key=True, default=worker.get_id, editable=False, verbose_name='主键ID', help_text='主键ID')
|
||||
create_time = models.DateTimeField(
|
||||
default=timezone.now, verbose_name='创建时间', help_text='创建时间')
|
||||
update_time = models.DateTimeField(
|
||||
auto_now=True, verbose_name='修改时间', help_text='修改时间')
|
||||
is_deleted = models.BooleanField(
|
||||
default=False, verbose_name='删除标记', help_text='删除标记')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.pk:
|
||||
# If self.pk is not None then it's an update.
|
||||
cls = self.__class__
|
||||
old = cls.objects.filter(pk=self.pk).first()
|
||||
if old:
|
||||
# This will get the current model state since super().save() isn't called yet.
|
||||
new = self # This gets the newly instantiated Mode object with the new values.
|
||||
changed_fields = []
|
||||
for field in cls._meta.get_fields():
|
||||
field_name = field.name
|
||||
try:
|
||||
if getattr(old, field_name) != getattr(new, field_name):
|
||||
changed_fields.append(field_name)
|
||||
except Exception as ex: # Catch field does not exist exception
|
||||
pass
|
||||
kwargs['update_fields'] = changed_fields
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class SoftModel(BaseModel):
|
||||
"""
|
||||
软删除基本表
|
||||
"""
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
objects = SoftDeletableManager()
|
||||
|
||||
def delete(self, using=None, soft=True, update_by=None, *args, **kwargs):
|
||||
'''
|
||||
这里需要真删除的话soft=False即可
|
||||
'''
|
||||
if soft:
|
||||
self.is_deleted = True
|
||||
self.update_by = update_by
|
||||
self.save(using=using)
|
||||
else:
|
||||
|
||||
return super(SoftModel, self).delete(using=using, *args, **kwargs)
|
||||
|
||||
|
||||
class CommonAModel(SoftModel):
|
||||
"""
|
||||
业务用基本表A,包含create_by, update_by字段
|
||||
"""
|
||||
create_by = models.ForeignKey(
|
||||
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name= '%(class)s_create_by')
|
||||
update_by = models.ForeignKey(
|
||||
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name= '%(class)s_update_by')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
|
||||
class CommonBModel(SoftModel):
|
||||
"""
|
||||
业务用基本表B,包含create_by, update_by, belong_dept字段
|
||||
"""
|
||||
create_by = models.ForeignKey(
|
||||
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name = '%(class)s_create_by')
|
||||
update_by = models.ForeignKey(
|
||||
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name = '%(class)s_update_by')
|
||||
belong_dept = models.ForeignKey(
|
||||
'system.dept', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='所属部门', related_name= '%(class)s_belong_dept')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class CommonADModel(BaseModel):
|
||||
"""
|
||||
业务用基本表A, 物理删除, 包含create_by, update_by字段
|
||||
"""
|
||||
create_by = models.ForeignKey(
|
||||
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name= '%(class)s_create_by')
|
||||
update_by = models.ForeignKey(
|
||||
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name= '%(class)s_update_by')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class CommonBDModel(BaseModel):
|
||||
"""
|
||||
业务用基本表B, 物理删除, 包含create_by, update_by, belong_dept字段
|
||||
"""
|
||||
create_by = models.ForeignKey(
|
||||
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name = '%(class)s_create_by')
|
||||
update_by = models.ForeignKey(
|
||||
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name = '%(class)s_update_by')
|
||||
belong_dept = models.ForeignKey(
|
||||
'system.organzation', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='所属部门', related_name= '%(class)s_belong_dept')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
|
@ -0,0 +1,14 @@
|
|||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
class MyPagination(PageNumberPagination):
|
||||
page_size = 10
|
||||
page_size_query_param = 'page_size'
|
||||
|
||||
def paginate_queryset(self, queryset, request, view):
|
||||
if request.query_params.get('pageoff', None) or request.query_params.get('page', None)=='0':
|
||||
if queryset.count()<500:
|
||||
return None
|
||||
elif queryset.count()>=500:
|
||||
raise ParseError('单次请求数据量大,请分页获取')
|
||||
return super().paginate_queryset(queryset, request, view=view)
|
|
@ -0,0 +1,128 @@
|
|||
from django.core.cache import cache
|
||||
from rest_framework.permissions import BasePermission, DjangoModelPermissions
|
||||
from apps.utils.queryset import get_child_queryset2
|
||||
from apps.system.models import Dept, Permission, Post, Role, UserPost
|
||||
from django.db.models import Q
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
|
||||
def get_user_perms_map(user):
|
||||
"""
|
||||
获取权限列表,可用redis存取
|
||||
"""
|
||||
user_perms_map = {}
|
||||
if user.is_superuser:
|
||||
user_perms_map = {'superuser':None}
|
||||
else:
|
||||
objs = UserPost.objects.filter(user=user)
|
||||
for i in objs:
|
||||
dept_id = str(i.dept.id)
|
||||
if i.post.roles: # 岗位下有角色
|
||||
for perm in Permission.objects.filter(role__perms__in=i.post.roles):
|
||||
if perm.codes:
|
||||
for code in perm.codes:
|
||||
if code in user_perms_map:
|
||||
data_range = user_perms_map[code].get(dept_id, -1)
|
||||
if i.post.data_range < data_range:
|
||||
user_perms_map[code][dept_id] = data_range
|
||||
else:
|
||||
user_perms_map[code] = {dept_id:i.post.data_range}
|
||||
cache.set('perms_' + user.id, user_perms_map, 60*60)
|
||||
return user_perms_map
|
||||
|
||||
|
||||
class RbacPermission(BasePermission):
|
||||
"""
|
||||
基于角色的权限校验类
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""
|
||||
权限校验逻辑
|
||||
:param request:
|
||||
:param view:
|
||||
:return:
|
||||
"""
|
||||
if not hasattr(view, 'perms_map'):
|
||||
return False
|
||||
user_perms_map = cache.get('perms_' + request.user.id, None)
|
||||
if user_perms_map is None:
|
||||
user_perms_map = get_user_perms_map(self.request.user)
|
||||
if isinstance(user_perms_map, dict):
|
||||
if 'superuser' in user_perms_map:
|
||||
return True
|
||||
else:
|
||||
perms_map = view.perms_map
|
||||
_method = request._request.method.lower()
|
||||
if perms_map:
|
||||
for key in perms_map:
|
||||
if key == _method or key == '*':
|
||||
if perms_map[key] in user_perms_map or perms_map[key] == '*':
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
class RbacDataMixin:
|
||||
"""
|
||||
数据权限控权返回的queryset
|
||||
在必须的View下继承
|
||||
需要控数据权限的表需有belong_dept, create_by, update_by字段(部门, 创建人, 编辑人)
|
||||
带性能优化
|
||||
此处对性能有较大影响,根据业务需求进行修改或取舍
|
||||
"""
|
||||
def get_queryset(self):
|
||||
assert self.queryset is not None, (
|
||||
"'%s' should either include a `queryset` attribute, "
|
||||
"or override the `get_queryset()` method."
|
||||
% self.__class__.__name__
|
||||
)
|
||||
|
||||
queryset = self.queryset
|
||||
if isinstance(queryset, QuerySet):
|
||||
# Ensure queryset is re-evaluated on each request.
|
||||
queryset = queryset.all()
|
||||
|
||||
if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
|
||||
queryset = self.get_serializer_class().setup_eager_loading(queryset) # 性能优化
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
return queryset
|
||||
|
||||
if hasattr(queryset.model, 'belong_dept'):
|
||||
user = self.request.user
|
||||
user_perms_map = cache.get('perms_' + user.id, None)
|
||||
if user_perms_map is None:
|
||||
user_perms_map = get_user_perms_map(self.request.user)
|
||||
if isinstance(user_perms_map, dict):
|
||||
if hasattr(self.view, 'perms_map'):
|
||||
perms_map = self.view.perms_map
|
||||
action_str = perms_map.get(self.request._request.method.lower(), None)
|
||||
if '*' in perms_map:
|
||||
return queryset
|
||||
elif action_str == '*':
|
||||
return queryset
|
||||
elif action_str in user_perms_map:
|
||||
new_queryset = queryset.none()
|
||||
for dept_id, data_range in user_perms_map[action_str].items:
|
||||
dept = Dept.objects.get(id=dept_id)
|
||||
if data_range == Post.POST_DATA_ALL:
|
||||
return queryset
|
||||
elif data_range == Post.POST_DATA_SAMELEVE_AND_BELOW:
|
||||
if dept.parent:
|
||||
belong_depts = get_child_queryset2(dept.parent)
|
||||
else:
|
||||
belong_depts = get_child_queryset2(dept)
|
||||
queryset = queryset.filter(belong_dept__in = belong_depts)
|
||||
elif data_range == Post.POST_DATA_THISLEVEL_AND_BELOW:
|
||||
belong_depts = get_child_queryset2(dept)
|
||||
queryset = queryset.filter(belong_dept__in = belong_depts)
|
||||
elif data_range == Post.POST_DATA_THISLEVEL:
|
||||
queryset = queryset.filter(belong_dept = dept)
|
||||
elif data_range == Post.POST_DATA_THISLEVEL:
|
||||
queryset = queryset.filter(create_by = user)
|
||||
new_queryset = new_queryset | queryset
|
||||
return new_queryset
|
||||
else:
|
||||
return queryset.none()
|
||||
return queryset
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
from django.db import models
|
||||
from django.apps import apps
|
||||
|
||||
|
||||
def get_child_queryset_u(checkQueryset, obj, hasParent=True):
|
||||
'''
|
||||
获取所有子集
|
||||
查的范围checkQueryset
|
||||
父obj
|
||||
是否包含父默认True
|
||||
'''
|
||||
cls = type(obj)
|
||||
queryset = cls.objects.none()
|
||||
fatherQueryset = cls.objects.filter(pk=obj.id)
|
||||
if hasParent:
|
||||
queryset = queryset | fatherQueryset
|
||||
child_queryset = checkQueryset.filter(parent=obj)
|
||||
while child_queryset:
|
||||
queryset = queryset | child_queryset
|
||||
child_queryset = checkQueryset.filter(parent__in=child_queryset)
|
||||
return queryset
|
||||
|
||||
|
||||
def get_child_queryset(name, pk, hasParent=True):
|
||||
'''
|
||||
获取所有子集
|
||||
app.model名称
|
||||
Id
|
||||
是否包含父默认True
|
||||
'''
|
||||
app, model = name.split('.')
|
||||
cls = apps.get_model(app, model)
|
||||
queryset = cls.objects.none()
|
||||
fatherQueryset = cls.objects.filter(pk=pk)
|
||||
if fatherQueryset.exists():
|
||||
if hasParent:
|
||||
queryset = queryset | fatherQueryset
|
||||
child_queryset = cls.objects.filter(parent=fatherQueryset.first())
|
||||
while child_queryset:
|
||||
queryset = queryset | child_queryset
|
||||
child_queryset = cls.objects.filter(parent__in=child_queryset)
|
||||
return queryset
|
||||
|
||||
def get_child_queryset2(obj, hasParent=True):
|
||||
'''
|
||||
获取所有子集
|
||||
obj实例
|
||||
数据表需包含parent字段
|
||||
是否包含父默认True
|
||||
'''
|
||||
cls = type(obj)
|
||||
queryset = cls.objects.none()
|
||||
fatherQueryset = cls.objects.filter(pk=obj.id)
|
||||
if hasParent:
|
||||
queryset = queryset | fatherQueryset
|
||||
child_queryset = cls.objects.filter(parent=obj)
|
||||
while child_queryset:
|
||||
queryset = queryset | child_queryset
|
||||
child_queryset = cls.objects.filter(parent__in=child_queryset)
|
||||
return queryset
|
||||
|
||||
def get_parent_queryset(obj, hasSelf=True):
|
||||
cls = type(obj)
|
||||
ids = []
|
||||
if hasSelf:
|
||||
ids.append(obj.id)
|
||||
while obj.parent:
|
||||
obj = obj.parent
|
||||
ids.append(obj.id)
|
||||
return cls.objects.filter(id__in=ids)
|
|
@ -0,0 +1,104 @@
|
|||
import json
|
||||
from user_agents import parse
|
||||
def get_request_ip(request):
|
||||
"""
|
||||
获取请求IP
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
获取请求参数
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
获取请求路径
|
||||
"""
|
||||
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_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 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 ""
|
|
@ -0,0 +1,83 @@
|
|||
import traceback
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.views import exception_handler
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status as drf_status
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('log')
|
||||
|
||||
def custome_exception_hander(exc, context):
|
||||
"""
|
||||
自定义异常处理
|
||||
"""
|
||||
res = exception_handler(exc, context)
|
||||
if res:
|
||||
return res
|
||||
else:
|
||||
"""
|
||||
日志记录
|
||||
"""
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
class FitJSONRenderer(JSONRenderer):
|
||||
"""
|
||||
自行封装的渲染器
|
||||
"""
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
如果使用这个render,
|
||||
普通的response将会被包装成:
|
||||
{"code":200000,"data":"X","msg":"X"}
|
||||
这样的结果
|
||||
使用方法:
|
||||
- 全局
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_RENDERER_CLASSES': ('utils.response.FitJSONRenderer', ),
|
||||
}
|
||||
- 局部
|
||||
class UserCountView(APIView):
|
||||
renderer_classes = [FitJSONRenderer]
|
||||
|
||||
:param data:
|
||||
:param accepted_media_type:
|
||||
:param renderer_context:
|
||||
:return: {"code":200,"data":"X","msg":"X"}
|
||||
"""
|
||||
response_body = dict(success=True, code=200000, msg='请求成功', data=None)
|
||||
response = renderer_context.get("response")
|
||||
status_code = response.status_code
|
||||
if isinstance(data, dict) and data.keys() == response_body.keys():
|
||||
response_body = data
|
||||
else:
|
||||
# 处理drf格式的返回数据
|
||||
if status_code >= 400: # 如果http响应异常
|
||||
response_body = dict(success=False, code=400000, msg='请求失败', data=None)
|
||||
response_body['data'] = data # data里是详细异常信息
|
||||
response_body['code'] = status_code*1000
|
||||
prefix = ""
|
||||
if isinstance(data, dict):
|
||||
prefix = list(data.keys())[0]
|
||||
data = data[prefix]
|
||||
if isinstance(data, list):
|
||||
data = data[0]
|
||||
|
||||
if prefix != 'detail':
|
||||
response_body['msg'] = prefix + str(data) # 取一部分放入msg,方便前端alert
|
||||
else:
|
||||
response_body['data'] = data
|
||||
renderer_context.get("response").status_code = 200 # 统一成200响应, 可用body里code区分业务异常
|
||||
return super(FitJSONRenderer, self).render(response_body, accepted_media_type, renderer_context)
|
||||
|
||||
|
||||
class SuccessResponse(Response):
|
||||
def __init__(self, data=None, msg='请求成功', code=200000, status=None, template_name=None, headers=None, exception=False, content_type=None):
|
||||
std_data = dict(success=True, code=code, msg=msg, data=data)
|
||||
super().__init__(std_data, status, template_name, headers, exception, content_type)
|
||||
|
||||
class FailResponse(Response):
|
||||
def __init__(self, msg='请求失败', data=None, code=400000, status=400, template_name=None, headers=None, exception=False, content_type=None):
|
||||
std_data = dict(success=False, code=code, msg=msg, data=data)
|
||||
super().__init__(std_data, status, template_name, headers, exception, content_type)
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
from rest_framework import serializers
|
||||
from django_restql.mixins import DynamicFieldsMixin
|
||||
|
||||
class PkSerializer(serializers.Serializer):
|
||||
pks = serializers.ListField(child=serializers.IntegerField(min_value=1), label="主键ID列表")
|
||||
|
||||
class GenSignatureSerializer(serializers.Serializer):
|
||||
path = serializers.CharField(label="图片地址")
|
||||
|
||||
class CustomModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
|
||||
pass
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# Twitter's Snowflake algorithm implementation which is used to generate distributed IDs.
|
||||
# https://github.com/twitter-archive/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala
|
||||
|
||||
import time
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
# logger = logging.getLogger('log')
|
||||
|
||||
class InvalidSystemClock(Exception):
|
||||
"""
|
||||
时钟回拨异常
|
||||
"""
|
||||
pass
|
||||
|
||||
class Constant(object):
|
||||
# 64位ID的划分
|
||||
WORKER_ID_BITS = 5
|
||||
DATACENTER_ID_BITS = 5
|
||||
SEQUENCE_BITS = 12
|
||||
|
||||
# 最大取值计算
|
||||
MAX_WORKER_ID = -1 ^ (-1 << WORKER_ID_BITS) # 2**5-1 0b11111
|
||||
MAX_DATACENTER_ID = -1 ^ (-1 << DATACENTER_ID_BITS)
|
||||
|
||||
# 移位偏移计算
|
||||
WOKER_ID_SHIFT = SEQUENCE_BITS
|
||||
DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS
|
||||
TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS
|
||||
|
||||
# 序号循环掩码
|
||||
SEQUENCE_MASK = -1 ^ (-1 << SEQUENCE_BITS)
|
||||
|
||||
# Twitter元年时间戳
|
||||
TWEPOCH = 1288834974657
|
||||
|
||||
|
||||
class IdWorker(object):
|
||||
"""
|
||||
用于生成IDs
|
||||
"""
|
||||
|
||||
def __init__(self, datacenter_id, worker_id, sequence=0):
|
||||
"""
|
||||
初始化
|
||||
:param datacenter_id: 数据中心(机器区域)ID
|
||||
:param worker_id: 机器ID
|
||||
:param sequence: 起始序号
|
||||
"""
|
||||
# sanity check
|
||||
if worker_id > Constant.MAX_WORKER_ID or worker_id < 0:
|
||||
raise ValueError('worker_id值越界')
|
||||
|
||||
if datacenter_id > Constant.MAX_DATACENTER_ID or datacenter_id < 0:
|
||||
raise ValueError('datacenter_id值越界')
|
||||
|
||||
self.worker_id = worker_id
|
||||
self.datacenter_id = datacenter_id
|
||||
self.sequence = sequence
|
||||
|
||||
self.last_timestamp = -1 # 上次计算的时间戳
|
||||
|
||||
def _gen_timestamp(self):
|
||||
"""
|
||||
生成整数时间戳
|
||||
:return:int timestamp
|
||||
"""
|
||||
return int(time.time() * 1000)
|
||||
|
||||
def get_id(self):
|
||||
"""
|
||||
获取新ID
|
||||
:return:
|
||||
"""
|
||||
timestamp = self._gen_timestamp()
|
||||
|
||||
# 时钟回拨
|
||||
if timestamp < self.last_timestamp:
|
||||
# logger.error('clock is moving backwards. Rejecting requests until {}'.format(self.last_timestamp))
|
||||
raise InvalidSystemClock
|
||||
|
||||
if timestamp == self.last_timestamp:
|
||||
self.sequence = (self.sequence + 1) & Constant.SEQUENCE_MASK
|
||||
if self.sequence == 0:
|
||||
timestamp = self._til_next_millis(self.last_timestamp)
|
||||
else:
|
||||
self.sequence = 0
|
||||
|
||||
self.last_timestamp = timestamp
|
||||
|
||||
new_id = ((timestamp - Constant.TWEPOCH) << Constant.TIMESTAMP_LEFT_SHIFT) | (self.datacenter_id << Constant.DATACENTER_ID_SHIFT) | \
|
||||
(self.worker_id << Constant.WOKER_ID_SHIFT) | self.sequence
|
||||
return new_id
|
||||
|
||||
def _til_next_millis(self, last_timestamp):
|
||||
"""
|
||||
等到下一毫秒
|
||||
"""
|
||||
timestamp = self._gen_timestamp()
|
||||
while timestamp <= last_timestamp:
|
||||
timestamp = self._gen_timestamp()
|
||||
return timestamp
|
||||
|
||||
worker = IdWorker(settings.SNOW_DATACENTER_ID, settings.SNOW_WORKER_ID)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(worker.get_id())
|
|
@ -0,0 +1,22 @@
|
|||
import textwrap
|
||||
import requests
|
||||
|
||||
def print_roundtrip(response, *args, **kwargs):
|
||||
format_headers = lambda d: '\n'.join(f'{k}: {v}' for k, v in d.items())
|
||||
print(textwrap.dedent('''
|
||||
---------------- request ----------------
|
||||
{req.method} {req.url}
|
||||
{reqhdrs}
|
||||
|
||||
{req.body}
|
||||
---------------- response ----------------
|
||||
{res.status_code} {res.reason} {res.url}
|
||||
{reshdrs}
|
||||
|
||||
{res.text}
|
||||
''').format(
|
||||
req=response.request,
|
||||
res=response,
|
||||
reqhdrs=format_headers(response.request.headers),
|
||||
reshdrs=format_headers(response.headers),
|
||||
))
|
|
@ -0,0 +1,11 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework import routers
|
||||
from apps.utils.views import SignatureViewSet
|
||||
API_BASE_URL = 'api/utils/'
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register('signature', SignatureViewSet, basename='signature')
|
||||
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
]
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
|
||||
EXCLUDE_FIELDS_BASE = ['create_time', 'update_time', 'is_deleted']
|
||||
EXCLUDE_FIELDS = ['create_time', 'update_time', 'is_deleted', 'create_by', 'update_by']
|
|
@ -0,0 +1,47 @@
|
|||
from rest_framework.views import APIView
|
||||
import os
|
||||
import cv2
|
||||
from server.settings import BASE_DIR
|
||||
import numpy as np
|
||||
from .response import FailResponse, SuccessResponse
|
||||
from apps.utils.viewsets import CustomGenericViewSet
|
||||
from apps.utils.mixins import CustomCreateModelMixin
|
||||
from apps.utils.serializers import GenSignatureSerializer
|
||||
|
||||
class SignatureViewSet(CustomCreateModelMixin, CustomGenericViewSet):
|
||||
authentication_classes = ()
|
||||
permission_classes = ()
|
||||
create_serializer_class = GenSignatureSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
照片生成透明签名图片
|
||||
|
||||
照片生成透明签名图片
|
||||
"""
|
||||
path = (BASE_DIR + request.data['path']).replace('\\', '/')
|
||||
try:
|
||||
image = cv2.imread(path, cv2.IMREAD_UNCHANGED)
|
||||
size = image.shape
|
||||
width = size[0] # 宽度
|
||||
height = size[1] # 高度
|
||||
if size[2] != 4: # 判断
|
||||
background = np.zeros((size[0], size[1], 4))
|
||||
for yh in range(height):
|
||||
for xw in range(width):
|
||||
background[xw, yh, :3] = image[xw, yh]
|
||||
background[xw, yh, 3] = 255
|
||||
image = background
|
||||
size = image.shape
|
||||
for i in range(size[0]):
|
||||
for j in range(size[1]):
|
||||
if image[i][j][0] > 100 and image[i][j][1] > 100 and image[i][j][2] > 100:
|
||||
image[i][j][3] = 0
|
||||
else:
|
||||
image[i][j][0], image[i][j][1], image[i][j][2] = 0, 0, 0
|
||||
ext = os.path.splitext(path)
|
||||
new_path = ext[0] + '.png'
|
||||
cv2.imwrite(new_path, image)
|
||||
return SuccessResponse({'path': new_path.replace(BASE_DIR, '')})
|
||||
except:
|
||||
return FailResponse(msg='签名照处理失败,请重新上传')
|
|
@ -0,0 +1,71 @@
|
|||
|
||||
from rest_framework.viewsets import ModelViewSet, GenericViewSet
|
||||
from rest_framework.decorators import action
|
||||
from apps.utils.mixins import CustomCreateModelMixin, CustomDestoryModelMixin, CustomUpdateModelMixin, OptimizationMixin
|
||||
from apps.utils.permission import RbacDataMixin, RbacPermission
|
||||
from apps.utils.serializers import PkSerializer
|
||||
from apps.utils.response import FailResponse, SuccessResponse
|
||||
from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, ListModelMixin
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
|
||||
class CustomGenericViewSet(GenericViewSet):
|
||||
"""
|
||||
增强的GenericViewSet
|
||||
"""
|
||||
perms_map = {}
|
||||
ordering_fields = '__all__'
|
||||
filter_fields = '__all__'
|
||||
ordering = '-create_time'
|
||||
filterset_fields = '__all__'
|
||||
create_serializer_class = None
|
||||
update_serializer_class = None
|
||||
list_serializer_class = None
|
||||
retrieve_serializer_class = None
|
||||
permission_classes = [IsAuthenticated & RbacPermission]
|
||||
|
||||
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()
|
||||
|
||||
class CustomDataGenericViewSet(RbacDataMixin, CustomGenericViewSet):
|
||||
"""
|
||||
增强的GenericViewSet, 带数据权限过滤
|
||||
"""
|
||||
|
||||
class CustomModelViewSet(OptimizationMixin, CustomCreateModelMixin
|
||||
, CustomUpdateModelMixin, ListModelMixin, RetrieveModelMixin
|
||||
, CustomDestoryModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
增强的ModelViewSet
|
||||
"""
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
# 增加默认权限标识
|
||||
if not self.perms_map:
|
||||
basename = self.basename
|
||||
self.perms_map = {'get':'*', 'post':'{}_create'.format(basename)
|
||||
,'put':'{}_update'.format(basename)
|
||||
,'patch':'{}_update'.format(basename)
|
||||
,'delete':'{}_delete'.format(basename)
|
||||
,'deletes':'{}_delete'.format(basename)}
|
||||
|
||||
@action(methods=['post'], detail=False, serializer_class=PkSerializer)
|
||||
def deletes(self,request,*args,**kwargs):
|
||||
request_data = request.data
|
||||
pks = request_data.get('pks',None)
|
||||
if pks:
|
||||
self.get_queryset().filter(id__in=pks).delete(update_by=request.user)
|
||||
return SuccessResponse()
|
||||
else:
|
||||
return FailResponse(msg="未获取到pks字段")
|
||||
|
||||
|
||||
|
||||
class CustomDataModelViewSet(RbacDataMixin, CustomModelViewSet):
|
||||
"""
|
||||
增强的ModelViewSet,带数据权限过滤
|
||||
"""
|
|
@ -0,0 +1,88 @@
|
|||
from threading import Thread
|
||||
import requests
|
||||
import json
|
||||
from apps.utils.tools import print_roundtrip
|
||||
from server import settings
|
||||
import time
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
|
||||
|
||||
class XxClient:
|
||||
"""
|
||||
寻息
|
||||
"""
|
||||
def __init__(self, licence=settings.XX_LICENCE, username=settings.XX_USERNAME) -> None:
|
||||
self.licence = licence
|
||||
self.username = username
|
||||
self.isGetingToken = False
|
||||
self.isRuning = True
|
||||
self.token = ''
|
||||
self.t = None
|
||||
self.setup()
|
||||
|
||||
def _get_token_loop(self):
|
||||
while self.isRuning:
|
||||
json = {
|
||||
'licence':self.licence
|
||||
}
|
||||
r = requests.post(json=json, url=settings.XX_BASE_URL + '/getAccessTokenV2', verify=False, timeout=20)
|
||||
ret = r.json()
|
||||
if ret.get('errorCode', 1) == 0:
|
||||
self.token = ret['data']['token']
|
||||
time.sleep(3600)
|
||||
|
||||
def get_token(self):
|
||||
self.isGetingToken = True
|
||||
json = {
|
||||
'licence':self.licence
|
||||
}
|
||||
r = requests.post(json=json, url=settings.XX_BASE_URL + '/getAccessTokenV2', verify=False, timeout=20)
|
||||
ret = r.json()
|
||||
if ret.get('errorCode', 1) == 0:
|
||||
self.isGetingToken = False
|
||||
self.token = ret['data']['token']
|
||||
|
||||
def setup(self):
|
||||
self.t = Thread(target= self._get_token_loop, args=(), daemon=True)
|
||||
self.t.start()
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
自定义销毁
|
||||
"""
|
||||
self.isRuning = False
|
||||
self.t.join()
|
||||
|
||||
def request(self, url:str, method:str='post', params=dict(), json=dict(), timeout=20):
|
||||
params['accessToken'] = self.token
|
||||
json['username'] = self.username
|
||||
if self.isGetingToken:
|
||||
req_num = 0
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
if not self.isGetingToken:
|
||||
self.request(url, method, params, json, timeout)
|
||||
req_num = req_num + 1
|
||||
if req_num > 4:
|
||||
break
|
||||
else:
|
||||
r = getattr(requests, method)('{}{}'.format(settings.XX_BASE_URL, url)
|
||||
, params=params, json=json, verify=False)
|
||||
if settings.DEBUG:
|
||||
print_roundtrip(r)
|
||||
ret = r.json()
|
||||
if ret.get('errorCode') == '1060000':
|
||||
self.get_token() # 重新获取token
|
||||
self.request(url, method, params, json, timeout) # 重新请求
|
||||
else:
|
||||
msg = '{}|{}'.format(str(ret['errorCode']), '|'.join(ret['errorMsg']))
|
||||
res = dict(success=True, code=200000, msg= msg, data=ret['data'])
|
||||
if ret['errorCode'] != 0:
|
||||
res['success'] = False
|
||||
res['code'] = 400000
|
||||
return res
|
||||
return dict(success=False, code=400900, msg='寻息接口访问异常', data=None)
|
||||
|
||||
if settings.XX_ENABLED:
|
||||
xxClient = XxClient()
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,7 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
class WfConfig(AppConfig):
|
||||
name = 'apps.wf'
|
||||
verbose_name = '工作流管理'
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from .models import Ticket
|
||||
class TicketFilterSet(filters.FilterSet):
|
||||
start_create = filters.DateFilter(field_name="create_time", lookup_expr='gte')
|
||||
end_create = filters.DateFilter(field_name="create_time", lookup_expr='lte')
|
||||
category = filters.ChoiceFilter(choices = Ticket.category_choices, method='filter_category')
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = ['workflow', 'state', 'act_state', 'start_create', 'end_create', 'category']
|
||||
|
||||
def filter_category(self, queryset, name, value):
|
||||
user=self.request.user
|
||||
if value == 'owner': # 我的
|
||||
queryset = queryset.filter(create_by=user)
|
||||
elif value == 'duty': # 待办
|
||||
queryset = queryset.filter(participant__contains=user.id).exclude(act_state__in=[Ticket.TICKET_ACT_STATE_FINISH, Ticket.TICKET_ACT_STATE_CLOSED])
|
||||
elif value == 'worked': # 处理过的
|
||||
queryset = queryset.filter(ticketflow_ticket__participant=user).exclude(create_by=user).order_by('-update_time').distinct()
|
||||
elif value == 'cc': # 抄送我的
|
||||
queryset = queryset.filter(ticketflow_ticket__participant_cc__contains=user.id).exclude(create_by=user).order_by('-update_time').distinct()
|
||||
elif value == 'all':
|
||||
pass
|
||||
else:
|
||||
queryset = queryset.none()
|
||||
return queryset
|
|
@ -0,0 +1,237 @@
|
|||
from random import choice
|
||||
from django.db import models
|
||||
from django.db.models.base import Model
|
||||
import django.utils.timezone as timezone
|
||||
from django.db.models.query import QuerySet
|
||||
from apps.utils.models import CommonAModel
|
||||
from apps.system.models import Dept, User, Dict, File
|
||||
from apps.utils.models import SoftModel, BaseModel
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
|
||||
class Workflow(CommonAModel):
|
||||
"""
|
||||
工作流
|
||||
"""
|
||||
name = models.CharField('名称', max_length=50)
|
||||
key = models.CharField('工作流标识', unique=True, max_length=20, null=True, blank=True)
|
||||
sn_prefix = models.CharField('流水号前缀', max_length=50, default='hb')
|
||||
description = models.CharField('描述', max_length=200, null=True, blank=True)
|
||||
view_permission_check = models.BooleanField('查看权限校验', default=True, help_text='开启后,只允许工单的关联人(创建人、曾经的处理人)有权限查看工单')
|
||||
limit_expression = models.JSONField('限制表达式', default=dict, blank=True, help_text='限制周期({"period":24} 24小时), 限制次数({"count":1}在限制周期内只允许提交1次), 限制级别({"level":1} 针对(1单个用户 2全局)限制周期限制次数,默认特定用户);允许特定人员提交({"allow_persons":"zhangsan,lisi"}只允许张三提交工单,{"allow_depts":"1,2"}只允许部门id为1和2的用户提交工单,{"allow_roles":"1,2"}只允许角色id为1和2的用户提交工单)')
|
||||
display_form_str = models.JSONField('展现表单字段', default=list, blank=True, help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称')
|
||||
title_template = models.CharField('标题模板', max_length=50, default='{title}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}')
|
||||
content_template = models.CharField('内容模板', max_length=1000, default='标题:{title}, 创建时间:{create_time}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}')
|
||||
|
||||
class State(CommonAModel):
|
||||
"""
|
||||
状态记录
|
||||
"""
|
||||
STATE_TYPE_START = 1
|
||||
STATE_TYPE_END = 2
|
||||
type_choices = (
|
||||
(0, '普通'),
|
||||
(STATE_TYPE_START, '开始'),
|
||||
(STATE_TYPE_END, '结束')
|
||||
)
|
||||
PARTICIPANT_TYPE_PERSONAL = 1
|
||||
PARTICIPANT_TYPE_MULTI = 2
|
||||
PARTICIPANT_TYPE_DEPT = 3
|
||||
PARTICIPANT_TYPE_ROLE = 4
|
||||
PARTICIPANT_TYPE_VARIABLE = 5
|
||||
PARTICIPANT_TYPE_ROBOT = 6
|
||||
PARTICIPANT_TYPE_FIELD = 7
|
||||
PARTICIPANT_TYPE_PARENT_FIELD = 8
|
||||
PARTICIPANT_TYPE_FORMCODE = 9
|
||||
state_participanttype_choices = (
|
||||
(0, '无处理人'),
|
||||
(PARTICIPANT_TYPE_PERSONAL, '个人'),
|
||||
(PARTICIPANT_TYPE_MULTI, '多人'),
|
||||
# (PARTICIPANT_TYPE_DEPT, '部门'),
|
||||
(PARTICIPANT_TYPE_ROLE, '角色'),
|
||||
# (PARTICIPANT_TYPE_VARIABLE, '变量'),
|
||||
(PARTICIPANT_TYPE_ROBOT, '脚本'),
|
||||
(PARTICIPANT_TYPE_FIELD, '工单的字段'),
|
||||
# (PARTICIPANT_TYPE_PARENT_FIELD, '父工单的字段'),
|
||||
(PARTICIPANT_TYPE_FORMCODE, '代码获取')
|
||||
)
|
||||
STATE_DISTRIBUTE_TYPE_ACTIVE = 1 # 主动接单
|
||||
STATE_DISTRIBUTE_TYPE_DIRECT = 2 # 直接处理(当前为多人的情况,都可以处理,而不需要先接单)
|
||||
STATE_DISTRIBUTE_TYPE_RANDOM = 3 # 随机分配
|
||||
STATE_DISTRIBUTE_TYPE_ALL = 4 # 全部处理
|
||||
state_distribute_choices=(
|
||||
(STATE_DISTRIBUTE_TYPE_ACTIVE, '主动接单'),
|
||||
(STATE_DISTRIBUTE_TYPE_DIRECT, '直接处理'),
|
||||
(STATE_DISTRIBUTE_TYPE_RANDOM, '随机分配'),
|
||||
(STATE_DISTRIBUTE_TYPE_ALL, '全部处理'),
|
||||
)
|
||||
|
||||
STATE_FIELD_READONLY= 1 # 字段只读
|
||||
STATE_FIELD_REQUIRED = 2 # 字段必填
|
||||
STATE_FIELD_OPTIONAL = 3 # 字段可选
|
||||
STATE_FIELD_HIDDEN = 4 # 字段隐藏
|
||||
state_filter_choices=(
|
||||
(0, '无'),
|
||||
(1, '和工单同属一及上级部门'),
|
||||
(2, '和创建人同属一及上级部门'),
|
||||
(3, '和上步处理人同属一及上级部门'),
|
||||
)
|
||||
name = models.CharField('名称', max_length=50)
|
||||
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流')
|
||||
is_hidden = models.BooleanField('是否隐藏', default=False, help_text='设置为True时,获取工单步骤api中不显示此状态(当前处于此状态时除外)')
|
||||
sort = models.IntegerField('状态顺序', default=0, help_text='用于工单步骤接口时,step上状态的顺序(因为存在网状情况,所以需要人为设定顺序),值越小越靠前')
|
||||
type = models.IntegerField('状态类型', default=0, choices=type_choices, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理,即没有对应的transition)')
|
||||
enable_retreat = models.BooleanField('允许撤回', default=False, help_text='开启后允许工单创建人在此状态直接撤回工单到初始状态')
|
||||
participant_type = models.IntegerField('参与者类型', choices=state_participanttype_choices, default=1, blank=True, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5,参与人填create_by')
|
||||
participant = models.JSONField('参与者', default=list, blank=True, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表\部门id\角色id\变量(create_by,create_by_tl)\脚本记录的id等,包含子工作流的需要设置处理人为loonrobot')
|
||||
state_fields = models.JSONField('表单字段', default=dict, help_text='json格式字典存储,包括读写属性1:只读,2:必填,3:可选, 4:隐藏 示例:{"create_time":1,"title":2, "sn":1}, 内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称') # json格式存储,包括读写属性1:只读,2:必填,3:可选,4:不显示, 字典的字典
|
||||
distribute_type = models.IntegerField('分配方式', default=1, choices=state_distribute_choices, help_text='1.主动接单(如果当前处理人实际为多人的时候,需要先接单才能处理) 2.直接处理(即使当前处理人实际为多人,也可以直接处理) 3.随机分配(如果实际为多人,则系统会随机分配给其中一个人) 4.全部处理(要求所有参与人都要处理一遍,才能进入下一步)')
|
||||
filter_policy = models.IntegerField('参与人过滤策略', default=0, choices=state_filter_choices)
|
||||
participant_cc = models.JSONField('抄送给', default=list, blank=True, help_text='抄送给(userid列表)')
|
||||
|
||||
class Transition(CommonAModel):
|
||||
"""
|
||||
工作流流转,定时器,条件(允许跳过), 条件流转与定时器不可同时存在
|
||||
"""
|
||||
TRANSITION_ATTRIBUTE_TYPE_ACCEPT = 1 # 同意
|
||||
TRANSITION_ATTRIBUTE_TYPE_REFUSE = 2 # 拒绝
|
||||
TRANSITION_ATTRIBUTE_TYPE_OTHER = 3 # 其他
|
||||
attribute_type_choices = (
|
||||
(1, '同意'),
|
||||
(2, '拒绝'),
|
||||
(3, '其他')
|
||||
)
|
||||
TRANSITION_INTERVENE_TYPE_DELIVER = 1 # 转交操作
|
||||
TRANSITION_INTERVENE_TYPE_ADD_NODE = 2 # 加签操作
|
||||
TRANSITION_INTERVENE_TYPE_ADD_NODE_END = 3 # 加签处理完成
|
||||
TRANSITION_INTERVENE_TYPE_ACCEPT = 4 # 接单操作
|
||||
TRANSITION_INTERVENE_TYPE_COMMENT = 5 # 评论操作
|
||||
TRANSITION_INTERVENE_TYPE_DELETE = 6 # 删除操作
|
||||
TRANSITION_INTERVENE_TYPE_CLOSE = 7 # 强制关闭操作
|
||||
TRANSITION_INTERVENE_TYPE_ALTER_STATE = 8 # 强制修改状态操作
|
||||
TRANSITION_INTERVENE_TYPE_HOOK = 9 # hook操作
|
||||
TRANSITION_INTERVENE_TYPE_RETREAT = 10 # 撤回
|
||||
TRANSITION_INTERVENE_TYPE_CC = 11 # 抄送
|
||||
|
||||
intervene_type_choices = (
|
||||
(0, '正常处理'),
|
||||
(TRANSITION_INTERVENE_TYPE_DELIVER, '转交'),
|
||||
(TRANSITION_INTERVENE_TYPE_ADD_NODE, '加签'),
|
||||
(TRANSITION_INTERVENE_TYPE_ADD_NODE_END, '加签处理完成'),
|
||||
(TRANSITION_INTERVENE_TYPE_ACCEPT, '接单'),
|
||||
(TRANSITION_INTERVENE_TYPE_COMMENT, '评论'),
|
||||
(TRANSITION_INTERVENE_TYPE_DELETE, '删除'),
|
||||
(TRANSITION_INTERVENE_TYPE_CLOSE, '强制关闭'),
|
||||
(TRANSITION_INTERVENE_TYPE_ALTER_STATE, '强制修改状态'),
|
||||
(TRANSITION_INTERVENE_TYPE_HOOK, 'hook操作'),
|
||||
(TRANSITION_INTERVENE_TYPE_RETREAT, '撤回'),
|
||||
(TRANSITION_INTERVENE_TYPE_CC, '抄送')
|
||||
)
|
||||
|
||||
name = models.CharField('操作', max_length=50)
|
||||
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流')
|
||||
timer = models.IntegerField('定时器(单位秒)', default=0, help_text='单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态。设置时间有效')
|
||||
source_state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='源状态', related_name='sstate_transition')
|
||||
destination_state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='目的状态', related_name='dstate_transition')
|
||||
condition_expression = models.JSONField('条件表达式', max_length=1000, default=list, help_text='流转条件表达式,根据表达式中的条件来确定流转的下个状态,格式为[{"expression":"{days} > 3 and {days}<10", "target_state":11}] 其中{}用于填充工单的字段key,运算时会换算成实际的值,当符合条件下个状态将变为target_state_id中的值,表达式只支持简单的运算或datetime/time运算.loonflow会以首次匹配成功的条件为准,所以多个条件不要有冲突' )
|
||||
attribute_type = models.IntegerField('属性类型', default=1, choices=attribute_type_choices, help_text='属性类型,1.同意,2.拒绝,3.其他')
|
||||
field_require_check = models.BooleanField('是否校验必填项', default=True, help_text='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容')
|
||||
|
||||
|
||||
class CustomField(CommonAModel):
|
||||
"""自定义字段, 设定某个工作流有哪些自定义字段"""
|
||||
field_type_choices = (
|
||||
('string', '字符串'),
|
||||
('int', '整型'),
|
||||
('float', '浮点'),
|
||||
('boolean', '布尔'),
|
||||
('date', '日期'),
|
||||
('datetime', '日期时间'),
|
||||
('radio', '单选'),
|
||||
('checkbox', '多选'),
|
||||
('select', '单选下拉'),
|
||||
('selects', '多选下拉'),
|
||||
('cascader', '单选级联'),
|
||||
('cascaders', '多选级联'),
|
||||
('select_dg', '弹框单选'),
|
||||
('select_dgs', '弹框多选'),
|
||||
('textarea', '文本域'),
|
||||
('file', '附件')
|
||||
)
|
||||
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流')
|
||||
field_type = models.CharField('类型', max_length=50, choices=field_type_choices,
|
||||
help_text='string, int, float, date, datetime, radio, checkbox, select, selects, cascader, cascaders, select_dg, select_dgs,textarea, file')
|
||||
field_key = models.CharField('字段标识', max_length=50, help_text='字段类型请尽量特殊,避免与系统中关键字冲突')
|
||||
field_name = models.CharField('字段名称', max_length=50)
|
||||
sort = models.IntegerField('排序', default=0, help_text='工单基础字段在表单中排序为:流水号0,标题20,状态id40,状态名41,创建人80,创建时间100,更新时间120.前端展示工单信息的表单可以根据这个id顺序排列')
|
||||
default_value = models.CharField('默认值', null=True, blank=True, max_length=100, help_text='前端展示时,可以将此内容作为表单中的该字段的默认值')
|
||||
description = models.CharField('描述', max_length=100, blank=True, null=True, help_text='字段的描述信息,可用于显示在字段的下方对该字段的详细描述')
|
||||
placeholder = models.CharField('占位符', max_length=100, blank=True, null=True, help_text='用户工单详情表单中作为字段的占位符显示')
|
||||
field_template = models.TextField('文本域模板', null=True, blank=True, help_text='文本域类型字段前端显示时可以将此内容作为字段的placeholder')
|
||||
boolean_field_display = models.JSONField('布尔类型显示名', default=dict, blank=True,
|
||||
help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"是","0":"否"}或{"1":"需要","0":"不需要"},注意数字也需要引号')
|
||||
|
||||
field_choice = models.JSONField('选项值', default=list, blank=True,
|
||||
help_text='选项值,格式为list, 例["id":1, "name":"张三"]')
|
||||
|
||||
label = models.CharField('标签', max_length=1000, default='', help_text='处理特殊逻辑使用,比如sys_user用于获取用户作为选项')
|
||||
# hook = models.CharField('hook', max_length=1000, default='', help_text='获取下拉选项用于动态选项值')
|
||||
is_hidden = models.BooleanField('是否隐藏', default=False, help_text='可用于携带不需要用户查看的字段信息')
|
||||
|
||||
class Ticket(CommonAModel):
|
||||
"""
|
||||
工单
|
||||
"""
|
||||
TICKET_ACT_STATE_DRAFT = 0 # 草稿中
|
||||
TICKET_ACT_STATE_ONGOING = 1 # 进行中
|
||||
TICKET_ACT_STATE_BACK = 2 # 被退回
|
||||
TICKET_ACT_STATE_RETREAT = 3 # 被撤回
|
||||
TICKET_ACT_STATE_FINISH = 4 # 已完成
|
||||
TICKET_ACT_STATE_CLOSED = 5 # 已关闭
|
||||
|
||||
act_state_choices =(
|
||||
(TICKET_ACT_STATE_DRAFT, '草稿中'),
|
||||
(TICKET_ACT_STATE_ONGOING, '进行中'),
|
||||
(TICKET_ACT_STATE_BACK, '被退回'),
|
||||
(TICKET_ACT_STATE_RETREAT, '被撤回'),
|
||||
(TICKET_ACT_STATE_FINISH, '已完成'),
|
||||
(TICKET_ACT_STATE_CLOSED, '已关闭')
|
||||
)
|
||||
category_choices =(
|
||||
('all', '全部'),
|
||||
('owner', '我创建的'),
|
||||
('duty', '待办'),
|
||||
('worked', '我处理的'),
|
||||
('cc', '抄送我的')
|
||||
)
|
||||
title = models.CharField('标题', max_length=500, null=True, blank=True, help_text="工单标题")
|
||||
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='关联工作流')
|
||||
sn = models.CharField('流水号', max_length=25, help_text="工单的流水号")
|
||||
state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='当前状态', related_name='ticket_state')
|
||||
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, verbose_name='父工单')
|
||||
parent_state = models.ForeignKey(State, null=True, blank=True, on_delete=models.CASCADE, verbose_name='父工单状态', related_name='ticket_parent_state')
|
||||
ticket_data = models.JSONField('工单数据', default=dict, help_text='工单自定义字段内容')
|
||||
in_add_node = models.BooleanField('加签状态中', default=False, help_text='是否处于加签状态下')
|
||||
add_node_man = models.ForeignKey(User, verbose_name='加签人', on_delete=models.SET_NULL, null=True, blank=True, help_text='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效')
|
||||
script_run_last_result = models.BooleanField('脚本最后一次执行结果', default=True)
|
||||
participant_type = models.IntegerField('当前处理人类型', default=0, help_text='0.无处理人,1.个人,2.多人', choices=State.state_participanttype_choices)
|
||||
participant = models.JSONField('当前处理人', default=list, blank=True, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表')
|
||||
act_state = models.IntegerField('进行状态', default=1, help_text='当前工单的进行状态', choices=act_state_choices)
|
||||
multi_all_person = models.JSONField('全部处理的结果', default=dict, blank=True, help_text='需要当前状态处理人全部处理时实际的处理结果,json格式')
|
||||
|
||||
|
||||
class TicketFlow(BaseModel):
|
||||
"""
|
||||
工单流转日志
|
||||
"""
|
||||
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, verbose_name='关联工单', related_name='ticketflow_ticket')
|
||||
transition = models.ForeignKey(Transition, verbose_name='流转id', help_text='与worklow.Transition关联, 为空时表示认为干预的操作', on_delete=models.CASCADE, null=True, blank=True)
|
||||
suggestion = models.CharField('处理意见', max_length=10000, default='', blank=True)
|
||||
participant_type = models.IntegerField('处理人类型', default=0, help_text='0.无处理人,1.个人,2.多人等', choices=State.state_participanttype_choices)
|
||||
participant = models.ForeignKey(User, verbose_name='处理人', on_delete=models.SET_NULL, null=True, blank=True, related_name='ticketflow_participant')
|
||||
participant_str = models.CharField('处理人', max_length=200, null=True, blank=True, help_text='非人工处理的处理人相关信息')
|
||||
state = models.ForeignKey(State, verbose_name='当前状态', default=0, blank=True, on_delete=models.CASCADE)
|
||||
ticket_data = models.JSONField('工单数据', default=dict, blank=True, help_text='可以用于记录当前表单数据,json格式')
|
||||
intervene_type = models.IntegerField('干预类型', default=0, help_text='流转类型', choices=Transition.intervene_type_choices)
|
||||
participant_cc = models.JSONField('抄送给', default=list, blank=True, help_text='抄送给(userid列表)')
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
from apps.system.models import User
|
||||
from apps.wf.models import State, Ticket, TicketFlow, Transition
|
||||
|
||||
|
||||
class GetParticipants:
|
||||
"""
|
||||
获取处理人脚本
|
||||
"""
|
||||
all_funcs = [
|
||||
{'func':'get_create_by', 'name':'获取工单创建人'}
|
||||
]
|
||||
|
||||
# def all_funcs(self):
|
||||
# # return list(filter(lambda x: x.startswith('get_') and callable(getattr(self, x)), dir(self)))
|
||||
# return [(func, getattr(self, func).__doc__) for func in dir(self) if callable(getattr(self, func)) and func.startswith('get_')]
|
||||
|
||||
@classmethod
|
||||
def get_create_by(cls, state:dict={}, ticket:dict={}, new_ticket_data:dict={}, handler:User={}):
|
||||
"""工单创建人"""
|
||||
participant = ticket.create_by.id
|
||||
return participant
|
||||
|
||||
class HandleScripts:
|
||||
"""
|
||||
任务处理脚本
|
||||
"""
|
||||
all_funcs = [
|
||||
{'func': 'handle_something', 'name':'处理一些工作'}
|
||||
]
|
||||
|
||||
|
||||
@classmethod
|
||||
def to_next(cls, ticket:Ticket, by_timer:bool=False, by_task:bool=False, by_hook:bool=False, script_str:str=''):
|
||||
# 获取信息
|
||||
transition_obj = Transition.objects.filter(source_state=ticket.state, is_deleted=False).first()
|
||||
|
||||
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
||||
participant_type=State.PARTICIPANT_TYPE_ROBOT,
|
||||
participant_str='func:{}'.format(script_str),
|
||||
transition=transition_obj)
|
||||
from .services import WfService
|
||||
|
||||
# 自动执行流转
|
||||
WfService.handle_ticket(ticket=ticket, transition=transition_obj, new_ticket_data=ticket.ticket_data, by_task=True)
|
||||
|
||||
return ticket
|
||||
|
||||
@classmethod
|
||||
def handle_something(cls, ticket:Ticket):
|
||||
"""处理一些工作"""
|
||||
# 任务处理代码区
|
||||
|
||||
|
||||
# 调用自动流转
|
||||
ticket = cls.to_next(ticket=ticket, by_task=True, script_str= 'handle_something')
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
from apps.system.models import Dept, User
|
||||
from apps.system.serializers import UserSimpleSerializer
|
||||
import rest_framework
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import State, Ticket, TicketFlow, Workflow, Transition, CustomField
|
||||
|
||||
|
||||
class WorkflowSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Workflow
|
||||
fields = '__all__'
|
||||
|
||||
class StateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = State
|
||||
fields = '__all__'
|
||||
|
||||
class WorkflowSimpleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Workflow
|
||||
fields = ['id', 'name']
|
||||
|
||||
class StateSimpleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = State
|
||||
fields = ['id', 'name', 'type', 'distribute_type', 'enable_retreat']
|
||||
|
||||
class TransitionSerializer(serializers.ModelSerializer):
|
||||
source_state_ = StateSimpleSerializer(source='source_state', read_only=True)
|
||||
destination_state_ = StateSimpleSerializer(source='destination_state', read_only=True)
|
||||
class Meta:
|
||||
model = Transition
|
||||
fields = '__all__'
|
||||
@staticmethod
|
||||
def setup_eager_loading(queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.select_related('source_state','destination_state')
|
||||
return queryset
|
||||
|
||||
class AllField(serializers.Field):
|
||||
def to_representation(self, value):
|
||||
return value
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return data
|
||||
|
||||
class FieldChoiceSerializer(serializers.Serializer):
|
||||
id = AllField(label='ID')
|
||||
name = serializers.CharField(label='名称')
|
||||
|
||||
|
||||
class CustomFieldSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = '__all__'
|
||||
|
||||
class CustomFieldCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
|
||||
field_choice = FieldChoiceSerializer(label='选项列表', many=True, required=False)
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = ['workflow', 'field_type', 'field_key', 'field_name',
|
||||
'sort', 'default_value', 'description', 'placeholder', 'field_template',
|
||||
'boolean_field_display', 'field_choice', 'label', 'is_hidden']
|
||||
|
||||
|
||||
class TicketSimpleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = '__all__'
|
||||
|
||||
class TicketCreateSerializer(serializers.ModelSerializer):
|
||||
transition = serializers.PrimaryKeyRelatedField(queryset=Transition.objects.all(), write_only=True)
|
||||
title = serializers.CharField(allow_blank=True, required=False)
|
||||
class Meta:
|
||||
model=Ticket
|
||||
fields=['title','workflow', 'ticket_data', 'transition']
|
||||
|
||||
def create(self, validated_data):
|
||||
return super().create(validated_data)
|
||||
|
||||
class TicketSerializer(serializers.ModelSerializer):
|
||||
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
|
||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = '__all__'
|
||||
|
||||
@staticmethod
|
||||
def setup_eager_loading(queryset):
|
||||
queryset = queryset.select_related('workflow','state')
|
||||
return queryset
|
||||
|
||||
class TicketListSerializer(serializers.ModelSerializer):
|
||||
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
|
||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = ['id', 'title', 'sn', 'workflow', 'workflow_', 'state', 'state_', 'act_state', 'create_time', 'update_time', 'participant_type', 'create_by']
|
||||
|
||||
@staticmethod
|
||||
def setup_eager_loading(queryset):
|
||||
queryset = queryset.select_related('workflow','state')
|
||||
return queryset
|
||||
|
||||
class TicketDetailSerializer(serializers.ModelSerializer):
|
||||
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
|
||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||
ticket_data_ = serializers.SerializerMethodField()
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = '__all__'
|
||||
|
||||
@staticmethod
|
||||
def setup_eager_loading(queryset):
|
||||
queryset = queryset.select_related('workflow','state')
|
||||
return queryset
|
||||
|
||||
def get_ticket_data_(self, obj):
|
||||
ticket_data = obj.ticket_data
|
||||
state_fields = obj.state.state_fields
|
||||
all_fields = CustomField.objects.filter(workflow=obj.workflow).order_by('sort')
|
||||
all_fields_l = CustomFieldSerializer(instance=all_fields, many=True).data
|
||||
for i in all_fields_l:
|
||||
key = i['field_key']
|
||||
i['field_state'] = state_fields.get(key, 1)
|
||||
i['field_value'] = ticket_data.get(key, None)
|
||||
i['field_display'] = i['field_value'] # 该字段是用于查看详情直接展示
|
||||
if i['field_value']:
|
||||
if 'sys_user' in i['label']:
|
||||
if isinstance(i['field_value'], list):
|
||||
i['field_display'] = ','.join(list(User.objects.filter(id__in=i['field_value']).values_list('name', flat=True)))
|
||||
else:
|
||||
i['field_display'] = User.objects.get(id=i['field_value']).name
|
||||
elif 'deptSelect' in i['label']:
|
||||
if isinstance(i['field_value'], list):
|
||||
i['field_display'] = ','.join(list(Dept.objects.filter(id__in=i['field_value']).values_list('name', flat=True)))
|
||||
else:
|
||||
i['field_display'] = Dept.objects.get(id=i['field_value']).name
|
||||
elif i['field_type'] in ['radio', 'select']:
|
||||
for m in i['field_choice']:
|
||||
if m['id'] == i['field_value']:
|
||||
i['field_display'] = m['name']
|
||||
elif i['field_type'] in ['checkbox', 'selects']:
|
||||
d_list = []
|
||||
for m in i['field_choice']:
|
||||
if m['id'] in i['field_value']:
|
||||
d_list.append(m['name'])
|
||||
i['field_display'] = ','.join(d_list)
|
||||
return all_fields_l
|
||||
|
||||
def filter_display(self, item, field_value):
|
||||
if item['id'] == field_value:
|
||||
return
|
||||
|
||||
class TicketFlowSerializer(serializers.ModelSerializer):
|
||||
participant_ = UserSimpleSerializer(source='participant', read_only=True)
|
||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||
class Meta:
|
||||
model = TicketFlow
|
||||
fields = '__all__'
|
||||
|
||||
class TicketFlowSimpleSerializer(serializers.ModelSerializer):
|
||||
participant_ = UserSimpleSerializer(source='participant', read_only=True)
|
||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||
class Meta:
|
||||
model = TicketFlow
|
||||
exclude = ['ticket_data']
|
||||
|
||||
|
||||
class TicketHandleSerializer(serializers.Serializer):
|
||||
transition = serializers.PrimaryKeyRelatedField(queryset=Transition.objects.all(), label="流转id")
|
||||
ticket_data = serializers.JSONField(label="表单数据json")
|
||||
suggestion = serializers.CharField(label="处理意见", required = False, allow_blank=True)
|
||||
|
||||
class TicketRetreatSerializer(serializers.Serializer):
|
||||
suggestion = serializers.CharField(label="撤回原因", required = False)
|
||||
|
||||
class TicketCloseSerializer(serializers.Serializer):
|
||||
suggestion = serializers.CharField(label="关闭原因", required = False)
|
||||
|
||||
class TicketAddNodeSerializer(serializers.Serializer):
|
||||
suggestion = serializers.CharField(label="加签说明", required = False)
|
||||
toadd_user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), label='发送给谁去加签')
|
||||
|
||||
class TicketAddNodeEndSerializer(serializers.Serializer):
|
||||
suggestion = serializers.CharField(label="加签意见", required = False)
|
||||
|
||||
class TicketDestorySerializer(serializers.Serializer):
|
||||
ids = serializers.ListField(child=serializers.PrimaryKeyRelatedField(queryset=Ticket.objects.all()), label='工单ID列表')
|
|
@ -0,0 +1,353 @@
|
|||
from apps.wf.serializers import CustomFieldSerializer
|
||||
from apps.wf.serializers import TicketSerializer, TicketSimpleSerializer
|
||||
from typing import Tuple
|
||||
from apps.system.models import User
|
||||
from apps.wf.models import CustomField, State, Ticket, TicketFlow, Transition, Workflow
|
||||
from rest_framework.exceptions import APIException, PermissionDenied, ValidationError
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
import random
|
||||
from .scripts import GetParticipants, HandleScripts
|
||||
from apps.utils.queryset import get_parent_queryset
|
||||
|
||||
class WfService(object):
|
||||
@staticmethod
|
||||
def get_worlflow_states(workflow:Workflow):
|
||||
"""
|
||||
获取工作流状态列表
|
||||
"""
|
||||
return State.objects.filter(workflow=workflow, is_deleted=False).order_by('sort')
|
||||
|
||||
@staticmethod
|
||||
def get_workflow_transitions(workflow:Workflow):
|
||||
"""
|
||||
获取工作流流转列表
|
||||
"""
|
||||
return Transition.objects.filter(workflow=workflow, is_deleted=False)
|
||||
|
||||
@staticmethod
|
||||
def get_workflow_start_state(workflow:Workflow):
|
||||
"""
|
||||
获取工作流初始状态
|
||||
"""
|
||||
try:
|
||||
wf_state_obj = State.objects.get(workflow=workflow, type=State.STATE_TYPE_START, is_deleted=False)
|
||||
return wf_state_obj
|
||||
except:
|
||||
raise APIException('工作流状态配置错误')
|
||||
|
||||
@staticmethod
|
||||
def get_workflow_end_state(workflow:Workflow):
|
||||
"""
|
||||
获取工作流结束状态
|
||||
"""
|
||||
try:
|
||||
wf_state_obj = State.objects.get(workflow=workflow, type=State.STATE_TYPE_END, is_deleted=False)
|
||||
return wf_state_obj
|
||||
except:
|
||||
raise APIException('工作流状态配置错误')
|
||||
|
||||
@staticmethod
|
||||
def get_workflow_custom_fields(workflow:Workflow):
|
||||
"""
|
||||
获取工单字段
|
||||
"""
|
||||
return CustomField.objects.filter(is_deleted=False, workflow=workflow).order_by('sort')
|
||||
|
||||
@staticmethod
|
||||
def get_workflow_custom_fields_list(workflow:Workflow):
|
||||
"""
|
||||
获取工单字段key List
|
||||
"""
|
||||
return list(CustomField.objects.filter(is_deleted=False, workflow=workflow).order_by('sort').values_list('field_key', flat=True))
|
||||
|
||||
@classmethod
|
||||
def get_ticket_transitions(cls, ticket:Ticket):
|
||||
"""
|
||||
获取工单当前状态下可用的流转条件
|
||||
"""
|
||||
return cls.get_state_transitions(ticket.state)
|
||||
|
||||
@classmethod
|
||||
def get_state_transitions(cls, state:State):
|
||||
"""
|
||||
获取状态可执行的操作
|
||||
"""
|
||||
return Transition.objects.filter(is_deleted=False, source_state=state).all()
|
||||
|
||||
@classmethod
|
||||
def get_ticket_steps(cls, ticket:Ticket):
|
||||
steps = cls.get_worlflow_states(ticket.workflow)
|
||||
nsteps_list = []
|
||||
for i in steps:
|
||||
if ticket.state == i or (not i.is_hidden):
|
||||
nsteps_list.append(i)
|
||||
return nsteps_list
|
||||
|
||||
@classmethod
|
||||
def get_ticket_transitions(cls, ticket:Ticket):
|
||||
"""
|
||||
获取工单可执行的操作
|
||||
"""
|
||||
return cls.get_state_transitions(ticket.state)
|
||||
|
||||
@classmethod
|
||||
def get_transition_by_args(cls, kwargs:dict):
|
||||
"""
|
||||
查询并获取流转
|
||||
"""
|
||||
kwargs['is_deleted'] = False
|
||||
return Transition.objects.filter(**kwargs).all()
|
||||
|
||||
@classmethod
|
||||
def get_ticket_sn(cls, workflow:Workflow):
|
||||
"""
|
||||
生成工单流水号
|
||||
"""
|
||||
now = timezone.now()
|
||||
today = str(now)[:10]+' 00:00:00'
|
||||
next_day = str(now+timedelta(days=1))[:10]+' 00:00:00'
|
||||
ticket_day_count_new = Ticket.objects.filter(create_time__gte=today, create_time__lte=next_day, workflow=workflow).count()+1
|
||||
return '%s_%04d%02d%02d%04d' % (workflow.sn_prefix, now.year, now.month, now.day, ticket_day_count_new)
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_next_state_by_transition_and_ticket_info(cls, ticket:Ticket, transition: Transition, new_ticket_data:dict={})->object:
|
||||
"""
|
||||
获取下个节点状态
|
||||
"""
|
||||
source_state = ticket.state
|
||||
destination_state = transition.destination_state
|
||||
ticket_all_value = cls.get_ticket_all_field_value(ticket)
|
||||
ticket_all_value.update(**new_ticket_data)
|
||||
for key, value in ticket_all_value.items():
|
||||
if isinstance(ticket_all_value[key], str):
|
||||
ticket_all_value[key] = "'" + ticket_all_value[key] + "'"
|
||||
if transition.condition_expression:
|
||||
for i in transition.condition_expression:
|
||||
expression = i['expression'].format(**ticket_all_value)
|
||||
import datetime, time # 用于支持条件表达式中对时间的操作
|
||||
if eval(expression, {'__builtins__':None}, {'datetime':datetime, 'time':time}):
|
||||
destination_state = State.objects.get(pk=i['target_state'])
|
||||
return destination_state
|
||||
return destination_state
|
||||
|
||||
@classmethod
|
||||
def get_ticket_state_participant_info(cls, state:State, ticket:Ticket, new_ticket_data:dict={}, handler:User=None):
|
||||
"""
|
||||
获取工单目标状态实际的处理人, 处理人类型
|
||||
"""
|
||||
if state.type == State.STATE_TYPE_START:
|
||||
"""
|
||||
回到初始状态
|
||||
"""
|
||||
return dict(destination_participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
||||
destination_participant=ticket.create_by.id,
|
||||
multi_all_person={})
|
||||
elif state.type == State.STATE_TYPE_END:
|
||||
"""
|
||||
到达结束状态
|
||||
"""
|
||||
return dict(destination_participant_type=0,
|
||||
destination_participant=0,
|
||||
multi_all_person={})
|
||||
multi_all_person_dict = {}
|
||||
destination_participant_type, destination_participant = state.participant_type, state.participant
|
||||
if destination_participant_type == State.PARTICIPANT_TYPE_FIELD:
|
||||
destination_participant = new_ticket_data.get(destination_participant, 0) if destination_participant in new_ticket_data \
|
||||
else Ticket.ticket_data.get(destination_participant, 0)
|
||||
|
||||
elif destination_participant_type == State.PARTICIPANT_TYPE_FORMCODE:#代码获取
|
||||
destination_participant = getattr(GetParticipants, destination_participant)(
|
||||
state=state, ticket=ticket, new_ticket_data=new_ticket_data, hander=handler)
|
||||
|
||||
elif destination_participant_type == State.PARTICIPANT_TYPE_DEPT:#部门
|
||||
destination_participant = list(User.objects.filter(dept__in=destination_participant).values_list('id', flat=True))
|
||||
|
||||
elif destination_participant_type == State.PARTICIPANT_TYPE_ROLE:#角色
|
||||
user_queryset = User.objects.filter(roles__in=destination_participant)
|
||||
# 如果选择了角色, 需要走过滤策略
|
||||
if state.filter_policy == 1:
|
||||
depts = get_parent_queryset(ticket.belong_dept)
|
||||
user_queryset = user_queryset.filter(dept__in=depts)
|
||||
elif state.filter_policy == 2:
|
||||
depts = get_parent_queryset(ticket.create_by.dept)
|
||||
user_queryset = user_queryset.filter(dept__in=depts)
|
||||
elif state.filter_policy == 3:
|
||||
depts = get_parent_queryset(handler.dept)
|
||||
user_queryset = user_queryset.filter(dept__in=depts)
|
||||
destination_participant = list(user_queryset.values_list('id', flat=True))
|
||||
if type(destination_participant) == list:
|
||||
destination_participant_type = State.PARTICIPANT_TYPE_MULTI
|
||||
destination_participant = list(set(destination_participant))
|
||||
if len(destination_participant) == 1: # 如果只有一个人
|
||||
destination_participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
||||
destination_participant = destination_participant[0]
|
||||
else:
|
||||
destination_participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
||||
if destination_participant_type == State.PARTICIPANT_TYPE_MULTI:
|
||||
if state.distribute_type == State.STATE_DISTRIBUTE_TYPE_RANDOM:
|
||||
destination_participant = random.choice(destination_participant)
|
||||
elif state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ALL:
|
||||
for i in destination_participant:
|
||||
multi_all_person_dict[i]={}
|
||||
|
||||
return dict(destination_participant_type=destination_participant_type,
|
||||
destination_participant=destination_participant,
|
||||
multi_all_person=multi_all_person_dict)
|
||||
|
||||
@classmethod
|
||||
def ticket_handle_permission_check(cls, ticket:Ticket, user:User)-> dict:
|
||||
transitions = cls.get_state_transitions(ticket.state)
|
||||
if not transitions:
|
||||
return dict(permission=True, msg="工单当前状态无需操作")
|
||||
current_participant_count = 0
|
||||
participant_type = ticket.participant_type
|
||||
participant = ticket.participant
|
||||
state = ticket.stateF
|
||||
if participant_type == State.PARTICIPANT_TYPE_PERSONAL:
|
||||
if user.id != participant:
|
||||
return dict(permission=False, msg="非当前处理人", need_accept=False)
|
||||
elif participant_type in [State.PARTICIPANT_TYPE_MULTI, State.PARTICIPANT_TYPE_DEPT, State.PARTICIPANT_TYPE_ROLE]:
|
||||
if user.id not in participant:
|
||||
return dict(permission=False, msg="非当前处理人", need_accept=False)
|
||||
current_participant_count = len(participant)
|
||||
if current_participant_count == 1:
|
||||
if [user.id] == participant or user.id == participant:
|
||||
pass
|
||||
else:
|
||||
return dict(permission=False, msg="非当前处理人", need_accept=False)
|
||||
elif current_participant_count >1 and state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ACTIVE:
|
||||
if user.id not in participant:
|
||||
return dict(permission=False, msg="非当前处理人", need_accept=False)
|
||||
return dict(permission=False, msg="需要先接单再处理", need_accept=True)
|
||||
if ticket.in_add_node:
|
||||
return dict(permission=False, msg="工单当前处于加签中,请加签完成后操作", need_accept=False)
|
||||
return dict(permission=True, msg="", need_accept=False)
|
||||
|
||||
@classmethod
|
||||
def check_dict_has_all_same_value(cls, dict_obj: object)->tuple:
|
||||
"""
|
||||
check whether all key are equal in a dict
|
||||
:param dict_obj:
|
||||
:return:
|
||||
"""
|
||||
value_list = []
|
||||
for key, value in dict_obj.items():
|
||||
value_list.append(value)
|
||||
value_0 = value_list[0]
|
||||
for value in value_list:
|
||||
if value_0 != value:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_ticket_all_field_value(cls, ticket: Ticket)->dict:
|
||||
"""
|
||||
工单所有字段的值
|
||||
get ticket's all field value
|
||||
:param ticket:
|
||||
:return:
|
||||
"""
|
||||
# 获取工单基础表中的字段中的字段信息
|
||||
field_info_dict = TicketSimpleSerializer(instance=ticket).data
|
||||
# 获取自定义字段的值
|
||||
custom_fields_queryset = cls.get_workflow_custom_fields(ticket.workflow)
|
||||
for i in custom_fields_queryset:
|
||||
field_info_dict[i.field_key] = ticket.ticket_data.get(i.field_key, None)
|
||||
return field_info_dict
|
||||
|
||||
@classmethod
|
||||
def handle_ticket(cls, ticket:Ticket, transition: Transition, new_ticket_data:dict={}, handler:User=None,
|
||||
suggestion:str='', created:bool=False, by_timer:bool=False, by_task:bool=False, by_hook:bool=False):
|
||||
|
||||
source_state = ticket.state
|
||||
source_ticket_data = ticket.ticket_data
|
||||
|
||||
# 校验处理权限
|
||||
if not handler or not created: # 没有处理人意味着系统触发不校验处理权限
|
||||
result = WfService.ticket_handle_permission_check(ticket, handler)
|
||||
if result.get('permission') is False:
|
||||
raise PermissionDenied(result.get('msg'))
|
||||
|
||||
# 校验表单必填项目
|
||||
if transition.field_require_check or not created:
|
||||
for key, value in ticket.state.state_fields.items():
|
||||
if int(value) == State.STATE_FIELD_REQUIRED:
|
||||
if key not in new_ticket_data or not new_ticket_data[key]:
|
||||
raise ValidationError('字段{}必填'.format(key))
|
||||
|
||||
destination_state = cls.get_next_state_by_transition_and_ticket_info(ticket, transition, new_ticket_data)
|
||||
multi_all_person = ticket.multi_all_person
|
||||
if multi_all_person:
|
||||
multi_all_person[handler.id] =dict(transition=transition.id)
|
||||
# 判断所有人处理结果是否一致
|
||||
if WfService.check_dict_has_all_same_value(multi_all_person):
|
||||
participant_info = WfService.get_ticket_state_participant_info(destination_state, ticket, new_ticket_data)
|
||||
destination_participant_type = participant_info.get('destination_participant_type', 0)
|
||||
destination_participant = participant_info.get('destination_participant', 0)
|
||||
multi_all_person = {}
|
||||
else:
|
||||
# 处理人没有没有全部处理完成或者处理动作不一致
|
||||
destination_participant_type = ticket.participant_type
|
||||
destination_state = ticket.state # 保持原状态
|
||||
destination_participant = []
|
||||
for key, value in multi_all_person.items():
|
||||
if not value:
|
||||
destination_participant.append(key)
|
||||
else:
|
||||
# 当前处理人类型非全部处理
|
||||
participant_info = WfService.get_ticket_state_participant_info(destination_state, ticket, new_ticket_data)
|
||||
destination_participant_type = participant_info.get('destination_participant_type', 0)
|
||||
destination_participant = participant_info.get('destination_participant', 0)
|
||||
multi_all_person = participant_info.get('multi_all_person', {})
|
||||
|
||||
# 更新工单信息:基础字段及自定义字段, add_relation字段 需要下个处理人是部门、角色等的情况
|
||||
ticket.state = destination_state
|
||||
ticket.participant_type = destination_participant_type
|
||||
ticket.participant = destination_participant
|
||||
ticket.multi_all_person = multi_all_person
|
||||
if destination_state.type == State.STATE_TYPE_END:
|
||||
ticket.act_state = Ticket.TICKET_ACT_STATE_FINISH
|
||||
elif destination_state.type == State.STATE_TYPE_START:
|
||||
ticket.act_state = Ticket.TICKET_ACT_STATE_DRAFT
|
||||
else:
|
||||
ticket.act_state = Ticket.TICKET_ACT_STATE_ONGOING
|
||||
|
||||
if transition.attribute_type == Transition.TRANSITION_ATTRIBUTE_TYPE_REFUSE:
|
||||
ticket.act_state = Ticket.TICKET_ACT_STATE_BACK
|
||||
|
||||
# 只更新必填和可选的字段
|
||||
if not created:
|
||||
for key, value in source_state.state_fields.items():
|
||||
if value in (State.STATE_FIELD_REQUIRED, State.STATE_FIELD_OPTIONAL):
|
||||
if key in new_ticket_data:
|
||||
source_ticket_data[key] = new_ticket_data[key]
|
||||
ticket.ticket_data = source_ticket_data
|
||||
ticket.save()
|
||||
|
||||
# 更新工单流转记录
|
||||
if not by_task:
|
||||
TicketFlow.objects.create(ticket=ticket, state=source_state, ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
||||
participant=handler, transition=transition)
|
||||
|
||||
if created:
|
||||
if source_state.participant_cc:
|
||||
TicketFlow.objects.create(ticket=ticket, state=source_state,
|
||||
participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC,
|
||||
participant=None, participant_cc=source_state.participant_cc)
|
||||
|
||||
# 目标状态需要抄送
|
||||
if destination_state.participant_cc:
|
||||
TicketFlow.objects.create(ticket=ticket, state=destination_state,
|
||||
participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC,
|
||||
participant=None, participant_cc=destination_state.participant_cc)
|
||||
|
||||
# 如果目标状态是脚本则执行
|
||||
if destination_state.participant_type == State.PARTICIPANT_TYPE_ROBOT:
|
||||
getattr(HandleScripts, destination_state.participant)(ticket)
|
||||
|
||||
return ticket
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,21 @@
|
|||
from django.db.models import base
|
||||
from rest_framework import urlpatterns
|
||||
from apps.wf.views import CustomFieldViewSet, FromCodeListView, StateViewSet, TicketFlowViewSet, TicketViewSet, TransitionViewSet, WorkflowViewSet
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
API_BASE_URL = 'api/wf/'
|
||||
HTML_BASE_URL = 'wf/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('workflow', WorkflowViewSet, basename='wf')
|
||||
router.register('state', StateViewSet, basename='wf_state')
|
||||
router.register('transition', TransitionViewSet, basename='wf_transitions')
|
||||
router.register('customfield', CustomFieldViewSet, basename='wf_customfield')
|
||||
router.register('ticket', TicketViewSet, basename='wf_ticket')
|
||||
router.register('ticketflow', TicketFlowViewSet, basename='wf_ticketflow')
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL + 'participant_from_code', FromCodeListView.as_view()),
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
]
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
from django.db.models import query
|
||||
from rest_framework.utils import serializer_helpers
|
||||
from rest_framework.views import APIView
|
||||
from apps.system.models import User
|
||||
from apps.wf.filters import TicketFilterSet
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import serializers
|
||||
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin
|
||||
from apps.wf.serializers import CustomFieldCreateUpdateSerializer, CustomFieldSerializer, StateSerializer, TicketAddNodeEndSerializer, TicketAddNodeSerializer, TicketCloseSerializer, TicketCreateSerializer, TicketDestorySerializer, TicketFlowSerializer, TicketFlowSimpleSerializer, TicketHandleSerializer, TicketRetreatSerializer, TicketSerializer, TransitionSerializer, WorkflowSerializer, TicketListSerializer, TicketDetailSerializer
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
from rest_framework.decorators import action, api_view
|
||||
from apps.wf.models import CustomField, Ticket, Workflow, State, Transition, TicketFlow
|
||||
from apps.utils.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin, OptimizationMixin
|
||||
from apps.wf.services import WfService
|
||||
from rest_framework.exceptions import APIException, PermissionDenied
|
||||
from rest_framework import status
|
||||
from django.db.models import Count
|
||||
from .scripts import GetParticipants, HandleScripts
|
||||
|
||||
|
||||
# Create your views here.
|
||||
class FromCodeListView(APIView):
|
||||
def get(self, request, format=None):
|
||||
"""
|
||||
获取处理人代码列表
|
||||
"""
|
||||
return Response(GetParticipants.all_funcs)
|
||||
|
||||
class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet):
|
||||
perms_map = {'get': '*', 'post': 'workflow_create',
|
||||
'put': 'workflow_update', 'delete': 'workflow_delete'}
|
||||
queryset = Workflow.objects.all()
|
||||
serializer_class = WorkflowSerializer
|
||||
search_fields = ['name', 'description']
|
||||
filterset_fields = []
|
||||
ordering_fields = ['create_time']
|
||||
ordering = ['-create_time']
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get':'workflow_update'}, pagination_class=None, serializer_class=StateSerializer)
|
||||
def states(self, request, pk=None):
|
||||
"""
|
||||
工作流下的状态节点
|
||||
"""
|
||||
wf = self.get_object()
|
||||
serializer = self.serializer_class(instance=WfService.get_worlflow_states(wf), many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get':'workflow_update'}, pagination_class=None, serializer_class=TransitionSerializer)
|
||||
def transitions(self, request, pk=None):
|
||||
"""
|
||||
工作流下的流转规则
|
||||
"""
|
||||
wf = self.get_object()
|
||||
serializer = self.serializer_class(instance=WfService.get_workflow_transitions(wf), many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get':'workflow_update'}, pagination_class=None, serializer_class=CustomFieldSerializer)
|
||||
def customfields(self, request, pk=None):
|
||||
"""
|
||||
工作流下的自定义字段
|
||||
"""
|
||||
wf = self.get_object()
|
||||
serializer = self.serializer_class(instance=CustomField.objects.filter(workflow=wf, is_deleted=False).order_by('sort'), many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get':'workflow_init'})
|
||||
def init(self, request, pk=None):
|
||||
"""
|
||||
新建工单初始化
|
||||
"""
|
||||
ret={}
|
||||
wf = self.get_object()
|
||||
start_state = WfService.get_workflow_start_state(wf)
|
||||
transitions = WfService.get_state_transitions(start_state)
|
||||
ret['workflow'] = pk
|
||||
ret['transitions'] = TransitionSerializer(instance=transitions, many=True).data
|
||||
field_list = CustomFieldSerializer(instance=WfService.get_workflow_custom_fields(wf), many=True).data
|
||||
for i in field_list:
|
||||
if i['field_key'] in start_state.state_fields:
|
||||
i['field_attribute'] = start_state.state_fields[i['field_key']]
|
||||
else:
|
||||
i['field_attribute'] = State.STATE_FIELD_READONLY
|
||||
ret['field_list'] = field_list
|
||||
return Response(ret)
|
||||
|
||||
class StateViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet):
|
||||
perms_map = {'get':'*', 'post':'workflow_update',
|
||||
'put':'workflow_update', 'delete':'workflow_update'}
|
||||
queryset = State.objects.all()
|
||||
serializer_class = StateSerializer
|
||||
search_fields = ['name']
|
||||
filterset_fields = ['workflow']
|
||||
ordering = ['sort']
|
||||
|
||||
class TransitionViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet):
|
||||
perms_map = {'get':'*', 'post':'workflow_update',
|
||||
'put':'workflow_update', 'delete':'workflow_update'}
|
||||
queryset = Transition.objects.all()
|
||||
serializer_class = TransitionSerializer
|
||||
search_fields = ['name']
|
||||
filterset_fields = ['workflow']
|
||||
ordering = ['id']
|
||||
|
||||
class CustomFieldViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet):
|
||||
perms_map = {'get':'*', 'post':'workflow_update',
|
||||
'put':'workflow_update', 'delete':'workflow_update'}
|
||||
queryset = CustomField.objects.all()
|
||||
serializer_class = CustomFieldSerializer
|
||||
search_fields = ['field_name']
|
||||
filterset_fields = ['workflow', 'field_type']
|
||||
ordering = ['sort']
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action in ['create', 'update']:
|
||||
return CustomFieldCreateUpdateSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||
perms_map = {'get':'*', 'post':'ticket_create'}
|
||||
queryset = Ticket.objects.all()
|
||||
serializer_class = TicketSerializer
|
||||
search_fields = ['title']
|
||||
filterset_class = TicketFilterSet
|
||||
ordering = ['-create_time']
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create':
|
||||
return TicketCreateSerializer
|
||||
elif self.action == 'handle':
|
||||
return TicketHandleSerializer
|
||||
elif self.action == 'retreat':
|
||||
return TicketRetreatSerializer
|
||||
elif self.action == 'list':
|
||||
return TicketListSerializer
|
||||
elif self.action == 'retrieve':
|
||||
return TicketDetailSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if not self.detail and not self.request.query_params.get('category', None):
|
||||
raise APIException('请指定查询分类')
|
||||
return super().filter_queryset(queryset)
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
新建工单
|
||||
"""
|
||||
rdata = request.data
|
||||
serializer = self.get_serializer(data=rdata)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
vdata = serializer.validated_data #校验之后的数据
|
||||
start_state = WfService.get_workflow_start_state(vdata['workflow'])
|
||||
transition = vdata.pop('transition')
|
||||
ticket_data = vdata['ticket_data']
|
||||
|
||||
save_ticket_data = {}
|
||||
# 校验必填项
|
||||
if transition.field_require_check:
|
||||
for key, value in start_state.state_fields.items():
|
||||
if int(value) == State.STATE_FIELD_REQUIRED:
|
||||
if key not in ticket_data and not ticket_data[key]:
|
||||
raise APIException('字段{}必填'.format(key))
|
||||
save_ticket_data[key] = ticket_data[key]
|
||||
elif int(value) == State.STATE_FIELD_OPTIONAL:
|
||||
save_ticket_data[key] = ticket_data[key]
|
||||
|
||||
ticket = serializer.save(state=start_state,
|
||||
create_by=request.user,
|
||||
create_time=timezone.now(),
|
||||
act_state=Ticket.TICKET_ACT_STATE_DRAFT,
|
||||
belong_dept=request.user.dept,
|
||||
ticket_data=save_ticket_data) # 先创建出来
|
||||
# 更新title和sn
|
||||
title = vdata.get('title', '')
|
||||
title_template = ticket.workflow.title_template
|
||||
if title_template:
|
||||
all_ticket_data = {**rdata, **ticket_data}
|
||||
title = title_template.format(**all_ticket_data)
|
||||
sn = WfService.get_ticket_sn(ticket.workflow) # 流水号
|
||||
ticket.sn = sn
|
||||
ticket.title = title
|
||||
ticket.save()
|
||||
ticket = WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data,
|
||||
handler=request.user, created=True)
|
||||
return Response(TicketSerializer(instance=ticket).data)
|
||||
|
||||
@action(methods=['get'], detail=False, perms_map={'get':'*'})
|
||||
def duty_agg(self, request, pk=None):
|
||||
"""
|
||||
工单待办聚合
|
||||
"""
|
||||
ret = {}
|
||||
queryset = Ticket.objects.filter(participant__contains=request.user.id, is_deleted=False)\
|
||||
.exclude(act_state__in=[Ticket.TICKET_ACT_STATE_FINISH, Ticket.TICKET_ACT_STATE_CLOSED])
|
||||
ret['total_count'] = queryset.count()
|
||||
ret['details'] = list(queryset.values('workflow', 'workflow__name').annotate(count = Count('workflow')))
|
||||
return Response(ret)
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post':'*'})
|
||||
@transaction.atomic
|
||||
def handle(self, request, pk=None):
|
||||
"""
|
||||
处理工单
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
serializer = TicketHandleSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
vdata = serializer.validated_data
|
||||
new_ticket_data = ticket.ticket_data
|
||||
new_ticket_data.update(**vdata['ticket_data'])
|
||||
|
||||
ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'],
|
||||
new_ticket_data=new_ticket_data, handler=request.user, suggestion=vdata['suggestion'])
|
||||
return Response(TicketSerializer(instance=ticket).data)
|
||||
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get':'*'})
|
||||
def flowsteps(self, request, pk=None):
|
||||
"""
|
||||
工单流转step, 用于显示当前状态的step图(线性结构)
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
steps = WfService.get_ticket_steps(ticket)
|
||||
return Response(StateSerializer(instance=steps, many=True).data)
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get':'*'})
|
||||
def flowlogs(self, request, pk=None):
|
||||
"""
|
||||
工单流转记录
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
flowlogs = TicketFlow.objects.filter(ticket=ticket).order_by('-create_time')
|
||||
serializer = TicketFlowSerializer(instance=flowlogs, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get':'*'})
|
||||
def transitions(self, request, pk=None):
|
||||
"""
|
||||
获取工单可执行的操作
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
transitions = WfService.get_ticket_transitions(ticket)
|
||||
return Response(TransitionSerializer(instance=transitions, many=True).data)
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post':'*'})
|
||||
def accpet(self, request, pk=None):
|
||||
"""
|
||||
接单,当工单当前处理人实际为多个人时(角色、部门、多人都有可能, 注意角色和部门有可能实际只有一人)
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
result = WfService.ticket_handle_permission_check(ticket, request.user)
|
||||
if result.get('need_accept', False):
|
||||
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
||||
ticket.participant = request.user.id
|
||||
ticket.save()
|
||||
# 接单日志
|
||||
# 更新工单流转记录
|
||||
TicketFlow.objects.create(ticket=ticket, state=ticket.state, ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||
suggestion='', participant_type=State.PARTICIPANT_TYPE_PERSONAL, intervene_type=Transition.TRANSITION_ATTRIBUTE_TYPE_ACCEPT,
|
||||
participant=request.user, transition=None)
|
||||
return Response()
|
||||
else:
|
||||
raise APIException('无需接单')
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post':'*'})
|
||||
def retreat(self, request, pk=None):
|
||||
"""
|
||||
撤回工单,允许创建人在指定状态撤回工单至初始状态,状态设置中开启允许撤回
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
if ticket.create_by != request.user:
|
||||
raise APIException('非创建人不可撤回')
|
||||
if not ticket.state.enable_retreat:
|
||||
raise APIException('该状态不可撤回')
|
||||
start_state = WfService.get_workflow_start_state(ticket.workflow)
|
||||
ticket.state = start_state
|
||||
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
||||
ticket.participant = request.user.id
|
||||
ticket.act_state = Ticket.TICKET_ACT_STATE_RETREAT
|
||||
ticket.save()
|
||||
# 更新流转记录
|
||||
suggestion = request.data.get('suggestion', '') # 撤回原因
|
||||
TicketFlow.objects.create(ticket=ticket, state=ticket.state, ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_RETREAT,
|
||||
participant=request.user, transition=None)
|
||||
return Response()
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post':'*'}, serializer_class=TicketAddNodeSerializer)
|
||||
def add_node(self, request, pk=None):
|
||||
"""
|
||||
加签
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
data = request.data
|
||||
add_user = User.objects.get(pk=data['toadd_user'])
|
||||
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
||||
ticket.participant = add_user.id
|
||||
ticket.in_add_node = True
|
||||
ticket.add_node_man = request.user
|
||||
ticket.save()
|
||||
# 更新流转记录
|
||||
suggestion = request.data.get('suggestion', '') # 加签说明
|
||||
TicketFlow.objects.create(ticket=ticket, state=ticket.state, ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE,
|
||||
participant=request.user, transition=None)
|
||||
return Response()
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post':'*'}, serializer_class=TicketAddNodeEndSerializer)
|
||||
def add_node_end(self, request, pk=None):
|
||||
"""
|
||||
加签完成
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
||||
ticket.in_add_node = False
|
||||
ticket.participant = ticket.add_node_man.id
|
||||
ticket.add_node_man = None
|
||||
ticket.save()
|
||||
# 更新流转记录
|
||||
suggestion = request.data.get('suggestion', '') # 加签意见
|
||||
TicketFlow.objects.create(ticket=ticket, state=ticket.state, ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE_END,
|
||||
participant=request.user, transition=None)
|
||||
return Response()
|
||||
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post':'*'}, serializer_class=TicketCloseSerializer)
|
||||
def close(self, request, pk=None):
|
||||
"""
|
||||
关闭工单(创建人在初始状态)
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
if ticket.state.type == State.STATE_TYPE_START and ticket.create_by==request.user:
|
||||
end_state = WfService.get_workflow_end_state(ticket.workflow)
|
||||
ticket.state = end_state
|
||||
ticket.participant_type = 0
|
||||
ticket.participant = 0
|
||||
ticket.act_state = Ticket.TICKET_ACT_STATE_CLOSED
|
||||
ticket.save()
|
||||
# 更新流转记录
|
||||
suggestion = request.data.get('suggestion', '') # 关闭原因
|
||||
TicketFlow.objects.create(ticket=ticket, state=ticket.state, ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CLOSE,
|
||||
participant=request.user, transition=None)
|
||||
return Response()
|
||||
else:
|
||||
return Response('工单不可关闭', status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(methods=['post'], detail=False, perms_map={'post':'ticket_deletes'}, serializer_class=TicketDestorySerializer)
|
||||
def destory(self, request, pk=None):
|
||||
"""
|
||||
批量物理删除
|
||||
"""
|
||||
Ticket.objects.filter(id__in=request.data.get('ids', [])).delete(soft=False)
|
||||
return Response()
|
||||
|
||||
|
||||
|
||||
class TicketFlowViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||
"""
|
||||
工单日志
|
||||
"""
|
||||
perms_map = {'get':'*'}
|
||||
queryset = TicketFlow.objects.all()
|
||||
serializer_class = TicketFlowSerializer
|
||||
search_fields = ['suggestion']
|
||||
filterset_fields = ['ticket']
|
||||
ordering = ['-create_time']
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.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()
|
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
|
@ -0,0 +1,20 @@
|
|||
celery==5.2.3
|
||||
Django==3.2.12
|
||||
django-celery-beat==2.2.1
|
||||
django-celery-results==2.3.0
|
||||
django-cors-headers==3.11.0
|
||||
django-filter==21.1
|
||||
django-simple-history==3.0.0
|
||||
djangorestframework==3.13.1
|
||||
djangorestframework-simplejwt==5.1.0
|
||||
drf-yasg==1.20.0
|
||||
psutil==5.9.0
|
||||
pillow==9.0.1
|
||||
opencv-python==4.5.5.62
|
||||
gunicorn==20.1.0
|
||||
redis==4.1.4
|
||||
user-agents==2.2.0
|
||||
daphne==3.0.2
|
||||
channels==3.0.4
|
||||
channels-redis==3.4.0
|
||||
django-restql==0.15.2
|
|
@ -0,0 +1,5 @@
|
|||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
|
@ -0,0 +1,25 @@
|
|||
"""
|
||||
ASGI config for server 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.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from django.core.asgi import get_asgi_application
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
import apps.monitor.routing
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": get_asgi_application(),
|
||||
"websocket": AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
apps.monitor.routing.websocket_urlpatterns
|
||||
)
|
||||
)
|
||||
})
|
|
@ -0,0 +1,22 @@
|
|||
import os
|
||||
|
||||
from celery import Celery
|
||||
|
||||
# set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
||||
|
||||
app = Celery('server')
|
||||
|
||||
# Using a string here means the worker doesn't have to serialize
|
||||
# the configuration object to child processes.
|
||||
# - namespace='CELERY' means all celery-related configuration keys
|
||||
# should have a `CELERY_` prefix.
|
||||
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||
|
||||
# Load task modules from all registered Django app configs.
|
||||
app.autodiscover_tasks()
|
||||
|
||||
|
||||
@app.task(bind=True)
|
||||
def debug_task(self):
|
||||
print(f'Request: {self.request!r}')
|
|
@ -0,0 +1,12 @@
|
|||
SECRET_KEY = '48j$m*^$m0hf0c7&=+!i9&3$8k8^6^a18t9(c*e8^sp(1&3^z0'
|
||||
DEBUG = False
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'ehs',
|
||||
'USER': 'postgres',
|
||||
'PASSWORD': 'zcDsj2021',
|
||||
'HOST': '49.232.14.174',
|
||||
'PORT': '5432',
|
||||
}
|
||||
}
|
|
@ -0,0 +1,318 @@
|
|||
"""
|
||||
Django settings for server project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.0.3.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
from . import conf
|
||||
from server.conf import DATABASES, XX_ENABLED, XX_LICENCE
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = conf.SECRET_KEY
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = conf.DEBUG
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'channels',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'corsheaders',
|
||||
'django_celery_beat',
|
||||
'django_celery_results',
|
||||
'drf_yasg',
|
||||
'rest_framework',
|
||||
"django_filters",
|
||||
'simple_history',
|
||||
'apps.utils',
|
||||
'apps.third',
|
||||
'apps.system',
|
||||
'apps.auth1',
|
||||
'apps.monitor',
|
||||
'apps.wf'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'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',
|
||||
'simple_history.middleware.HistoryRequestMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'server.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': ['dist'],
|
||||
'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
|
||||
WSGI_APPLICATION = 'server.wsgi.application'
|
||||
|
||||
# ASGI
|
||||
ASGI_APPLICATION = 'server.asgi.application'
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
"hosts": [('127.0.0.1', 6379)],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||
DATABASES = conf.DATABASES
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.0/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.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'zh-hans'
|
||||
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'dist/static_collect')
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, 'dist/static'),
|
||||
)
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
# 默认主键
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
# 雪花ID生成配置
|
||||
SNOW_DATACENTER_ID = conf.SNOW_DATACENTER_ID
|
||||
SNOW_WORKER_ID = conf.SNOW_WORKER_ID
|
||||
|
||||
# restframework配置
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
'apps.utils.permission.RbacPermission'
|
||||
],
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'apps.utils.response.FitJSONRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer'
|
||||
],
|
||||
'DEFAULT_FILTER_BACKENDS': [
|
||||
'django_filters.rest_framework.DjangoFilterBackend',
|
||||
'rest_framework.filters.SearchFilter',
|
||||
'rest_framework.filters.OrderingFilter'
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'apps.utils.pagination.MyPagination',
|
||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S',
|
||||
'DATE_FORMAT': '%Y-%m-%d',
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
||||
'UNAUTHENTICATED_USER': None,
|
||||
'UNAUTHENTICATED_TOKEN': None,
|
||||
'EXCEPTION_HANDLER': 'apps.utils.response.custome_exception_hander',
|
||||
}
|
||||
# simplejwt配置
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
|
||||
}
|
||||
|
||||
# 跨域配置/可用nginx处理,无需引入corsheaders
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Auth配置
|
||||
AUTH_USER_MODEL = 'system.User'
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'apps.auth1.authentication.CustomBackend',
|
||||
)
|
||||
|
||||
# 缓存配置,有需要可更改为redis
|
||||
# CACHES = {
|
||||
# "default": {
|
||||
# "BACKEND": "django_redis.cache.RedisCache",
|
||||
# "LOCATION": "redis://redis:6379/1",
|
||||
# "OPTIONS": {
|
||||
# "CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
# "PICKLE_VERSION": -1
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
# celery配置,celery正常运行必须安装redis
|
||||
CELERY_BROKER_URL = "redis://redis:6379/0" # 任务存储
|
||||
CELERYD_MAX_TASKS_PER_CHILD = 100 # 每个worker最多执行100个任务就会被销毁,可防止内存泄露
|
||||
CELERY_TIMEZONE = 'Asia/Shanghai' # 设置时区
|
||||
CELERY_ENABLE_UTC = True # 启动时区设置
|
||||
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
|
||||
|
||||
# swagger配置
|
||||
SWAGGER_SETTINGS = {
|
||||
'LOGIN_URL':'/django/login/',
|
||||
'LOGOUT_URL':'/django/logout/',
|
||||
}
|
||||
|
||||
# 日志配置
|
||||
# 创建日志的路径
|
||||
LOG_PATH = os.path.join(BASE_DIR, 'log')
|
||||
# 如果地址不存在,则自动创建log文件夹
|
||||
if not os.path.exists(os.path.join(LOG_PATH)):
|
||||
os.mkdir(LOG_PATH)
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
# 日志格式
|
||||
'standard': {
|
||||
'format': '[%(asctime)s] [%(filename)s:%(lineno)d] [%(module)s:%(funcName)s] '
|
||||
'[%(levelname)s]- %(message)s'},
|
||||
'simple': { # 简单格式
|
||||
'format': '%(levelname)s %(message)s'
|
||||
},
|
||||
},
|
||||
# 过滤
|
||||
'filters': {
|
||||
'require_debug_true': {
|
||||
'()': 'django.utils.log.RequireDebugTrue',
|
||||
},
|
||||
},
|
||||
# 定义具体处理日志的方式
|
||||
'handlers': {
|
||||
# 默认记录所有日志
|
||||
'default': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': os.path.join(LOG_PATH, 'all-{}.log'.format(datetime.now().strftime('%Y-%m-%d'))),
|
||||
'maxBytes': 1024 * 1024 * 5, # 文件大小
|
||||
'backupCount': 5, # 备份数
|
||||
'formatter': 'standard', # 输出格式
|
||||
'encoding': 'utf-8', # 设置默认编码,否则打印出来汉字乱码
|
||||
},
|
||||
# 输出错误日志
|
||||
'error': {
|
||||
'level': 'ERROR',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': os.path.join(LOG_PATH, 'error-{}.log'.format(datetime.now().strftime('%Y-%m-%d'))),
|
||||
'maxBytes': 1024 * 1024 * 5, # 文件大小
|
||||
'backupCount': 5, # 备份数
|
||||
'formatter': 'standard', # 输出格式
|
||||
'encoding': 'utf-8', # 设置默认编码
|
||||
},
|
||||
# 控制台输出
|
||||
'console': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['require_debug_true'],
|
||||
'formatter': 'standard'
|
||||
},
|
||||
# 输出info日志
|
||||
'info': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': os.path.join(LOG_PATH, 'info-{}.log'.format(datetime.now().strftime('%Y-%m-%d'))),
|
||||
'maxBytes': 1024 * 1024 * 5,
|
||||
'backupCount': 5,
|
||||
'formatter': 'standard',
|
||||
'encoding': 'utf-8', # 设置默认编码
|
||||
},
|
||||
},
|
||||
# 配置用哪几种 handlers 来处理日志
|
||||
'loggers': {
|
||||
# 类型 为 django 处理所有类型的日志, 默认调用
|
||||
'django': {
|
||||
'handlers': ['default', 'console', 'error'],
|
||||
'level': 'INFO',
|
||||
'propagate': False
|
||||
},
|
||||
# log 调用时需要当作参数传入
|
||||
'log': {
|
||||
'handlers': ['error', 'info', 'console', 'default'],
|
||||
'level': 'INFO',
|
||||
'propagate': True
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# 大华ICC平台
|
||||
DAHUA_ENABLED = conf.DAHUA_ENABLED
|
||||
DAHUA_BASE_URL = conf.DAHUA_BASE_URL
|
||||
DAHUA_USERNAME = conf.DAHUA_USERNAME
|
||||
DAHUA_PASSWORD = conf.DAHUA_PASSWORD
|
||||
DAHUA_CLIENTID = conf.DAHUA_CLIENTID
|
||||
DAHUA_SECRET = conf.DAHUA_SECRET
|
||||
|
||||
# 寻息定位
|
||||
XX_ENABLED = conf.XX_ENABLED
|
||||
XX_BASE_URL = conf.XX_BASE_URL
|
||||
XX_LICENCE = conf.XX_LICENCE
|
||||
XX_USERNAME = conf.XX_USERNAME
|
|
@ -0,0 +1,60 @@
|
|||
"""server URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.0/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 import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
from rest_framework.documentation import include_docs_urls
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="EHS API",
|
||||
default_version='v1',
|
||||
contact=openapi.Contact(email="caoqianming@foxmail.com"),
|
||||
license=openapi.License(name="MIT License"),
|
||||
),
|
||||
public=True,
|
||||
permission_classes=[],
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# django后台
|
||||
path('django/doc/', include('django.contrib.admindocs.urls')),
|
||||
path('django/', admin.site.urls),
|
||||
|
||||
# api
|
||||
path('', include('apps.auth1.urls')),
|
||||
path('', include('apps.system.urls')),
|
||||
path('', include('apps.monitor.urls')),
|
||||
path('', include('apps.wf.urls')),
|
||||
path('', include('apps.third.urls')),
|
||||
path('', include('apps.utils.urls')),
|
||||
|
||||
|
||||
# api文档
|
||||
path('api/docs/', include_docs_urls(title="接口文档", authentication_classes=[], permission_classes=[])),
|
||||
path('api/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||
|
||||
# 前端页面入口
|
||||
path('',TemplateView.as_view(template_name="index.html"))
|
||||
] + \
|
||||
static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + \
|
||||
static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for server 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.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
||||
|
||||
application = get_wsgi_application()
|
Loading…
Reference in New Issue