语法pep8修正

This commit is contained in:
caoqianming 2022-05-04 16:06:02 +08:00
parent d1c79e516a
commit 2a9c70eee5
65 changed files with 821 additions and 703 deletions

0
apps/__init__.py Normal file
View File

View File

@ -4,4 +4,3 @@ from django.apps import AppConfig
class AuthConfig(AppConfig):
name = 'apps.auth1'
verbose_name = "认证"

View File

@ -1,2 +1,2 @@
USERNAME_OR_PASSWORD_WRONG = {"code":"username_or_password_wrong", "detail":"账户名或密码错误"}
USERNAME_OR_PASSWORD_WRONG = {"code": "username_or_password_wrong", "detail": "账户名或密码错误"}

View File

@ -1,5 +1,6 @@
from rest_framework import serializers
class LoginSerializer(serializers.Serializer):
username = serializers.CharField(label="用户名")
password = serializers.CharField(label="密码")

View File

@ -1,6 +1,5 @@
from rest_framework.exceptions import ParseError, ValidationError
from django.shortcuts import render
from rest_framework.exceptions import ParseError
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
@ -14,6 +13,7 @@ from apps.auth1.serializers import LoginSerializer
# Create your views here.
class TokenBlackView(APIView):
permission_classes = [IsAuthenticated]
@ -26,6 +26,7 @@ class TokenBlackView(APIView):
"""
return Response(status=status.HTTP_200_OK)
class LoginView(CreateAPIView):
"""
Session登录
@ -41,13 +42,14 @@ class LoginView(CreateAPIView):
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'))
user = authenticate(username=vdata.get('username'),
password=vdata.get('password'))
if user is not None:
login(request, user)
return Response()
raise ParseError(**USERNAME_OR_PASSWORD_WRONG)
class LogoutView(APIView):
authentication_classes = []
permission_classes = []

View File

@ -1,11 +1,9 @@
from __future__ import absolute_import, unicode_literals
from distutils import command
from rest_framework.response import Response
from celery import shared_task
import os
import subprocess
from server.settings import DATABASES, BACKUP_PATH, SH_PATH, SD_PWD
@shared_task
def backup_database():
"""
@ -23,22 +21,23 @@ def backup_database():
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
return completed
@shared_task
def reload_server_git():
command = 'bash {}/git_server.sh'.format(SH_PATH)
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
return completed
@shared_task
def reload_server_only():
command = 'echo "{}" | sudo -S service supervisor reload'.format(SD_PWD)
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
return completed
@shared_task
def backup_media():
command = 'bash {}/backup_media.sh'.format(SH_PATH)
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
return completed

View File

@ -1,7 +1,4 @@
from django.db.models import base
from rest_framework import urlpatterns
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from django.urls import path
from apps.develop.views import BackupDatabase, BackupMedia, ReloadServerGit, ReloadServerOnly
API_BASE_URL = 'api/develop/'
@ -13,4 +10,3 @@ urlpatterns = [
path(API_BASE_URL + 'backup_database/', BackupDatabase.as_view()),
path(API_BASE_URL + 'backup_media/', BackupMedia.as_view())
]

View File

@ -1,8 +1,5 @@
from django.db import transaction
from django.shortcuts import render
from rest_framework.decorators import permission_classes
from rest_framework.views import APIView
from rest_framework.permissions import IsAdminUser, AllowAny
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from apps.develop.tasks import backup_database, backup_media, reload_server_git, reload_server_only
from rest_framework.exceptions import APIException
@ -11,6 +8,7 @@ from rest_framework.exceptions import APIException
class ReloadServerGit(APIView):
permission_classes = [IsAdminUser]
def post(self, request):
"""
拉取后端代码并重启服务
@ -23,8 +21,10 @@ class ReloadServerGit(APIView):
else:
raise APIException(completed.stdout)
class ReloadServerOnly(APIView):
permission_classes = [IsAdminUser]
def post(self, request):
"""
仅重启服务
@ -40,6 +40,7 @@ class ReloadServerOnly(APIView):
class BackupDatabase(APIView):
permission_classes = [IsAdminUser]
def post(self, request):
"""
备份数据库到指定位置
@ -52,8 +53,10 @@ class BackupDatabase(APIView):
else:
raise APIException(completed.stdout)
class BackupMedia(APIView):
permission_classes = [IsAdminUser]
def post(self, request):
"""
备份资源到指定位置

View File

@ -1,9 +1,9 @@
from django.apps import AppConfig
class HrmConfig(AppConfig):
name = 'apps.hrm'
verbose_name = '人力资源管理'
def ready(self):
import apps.hrm.signals

View File

@ -1,7 +1,5 @@
from enum import Enum
NO_NEED_LEVEL_REMARK = {"code":"no_need_level_remark", "detail":"无需填写离岗说明"}
PHONE_F_WRONG = {"code":"phone_f_wrong", "detail":"手机号格式错误"}
PHONE_EXIST = {"code":"phone_exist", "detail":"手机号已存在"}
DH_PHOTO_FALI = {"code":"dh_photo_fail", "detail":"大华照片人脸提取失败"}
NO_NEED_LEVEL_REMARK = {"code": "no_need_level_remark", "detail": "无需填写离岗说明"}
PHONE_F_WRONG = {"code": "phone_f_wrong", "detail": "手机号格式错误"}
PHONE_EXIST = {"code": "phone_exist", "detail": "手机号已存在"}
DH_PHOTO_FALI = {"code": "dh_photo_fail", "detail": "大华照片人脸提取失败"}

View File

@ -1,11 +1,13 @@
from django_filters import rest_framework as filters
from apps.hrm.models import ClockRecord, Employee, NotWorkRemark
class ClockRecordFilterSet(filters.FilterSet):
create_time_start = filters.DateFilter(field_name="create_time", lookup_expr='gte')
create_time_end = filters.DateFilter(field_name="create_time", lookup_expr='lte')
year = filters.NumberFilter(method='filter_year')
month = filters.NumberFilter(method='filter_month')
class Meta:
model = ClockRecord
fields = ['create_by', 'create_time_start', 'create_time_end', 'year', 'month']
@ -16,6 +18,7 @@ class ClockRecordFilterSet(filters.FilterSet):
def filter_month(self, queryset, name, value):
return queryset.filter(create_time_date__month=value)
class EmployeeFilterSet(filters.FilterSet):
class Meta:
@ -26,6 +29,7 @@ class EmployeeFilterSet(filters.FilterSet):
class NotWorkRemarkFilterSet(filters.FilterSet):
year = filters.NumberFilter(method='filter_year')
month = filters.NumberFilter(method='filter_month')
class Meta:
model = NotWorkRemark
fields = ['year', 'month', 'user']

View File

@ -1,12 +1,9 @@
from django.db import models
from django.db.models.query import QuerySet
from apps.system.models import User
from apps.utils.models import CommonADModel, CommonAModel, CommonBModel
class Employee(CommonBModel):
"""
员工信息
@ -14,12 +11,12 @@ class Employee(CommonBModel):
JOB_ON = 10
JOB_OFF = 20
jobstate_choices = (
(JOB_ON, '在职'),
(JOB_OFF, '离职'),
(JOB_ON, '在职'),
(JOB_OFF, '离职'),
)
user = models.OneToOneField(User,
verbose_name='系统账号',
on_delete=models.PROTECT, related_name='employee_user', null=True, blank=True)
verbose_name='系统账号',
on_delete=models.PROTECT, related_name='employee_user', null=True, blank=True)
name = models.CharField('姓名', max_length=20)
phone = models.CharField('手机号', max_length=11, null=True, blank=True)
email = models.EmailField('邮箱号', null=True, blank=True)
@ -30,13 +27,14 @@ class Employee(CommonBModel):
signature = models.CharField('签名图片', max_length=200, null=True, blank=True)
birthday = models.DateField('出生年月日', null=True, blank=True)
qualification = models.CharField('学历', max_length=50, null=True, blank=True)
job_state = models.IntegerField('在职状态', choices=jobstate_choices, default=1)
job_state = models.IntegerField('在职状态', choices=jobstate_choices, default=1)
face_data = models.JSONField('人脸识别数据', null=True, blank=True)
is_atwork = models.BooleanField('当前在岗', default=False)
show_atwork = models.BooleanField('是否展示在岗状态', default=True)
last_check_time = models.DateTimeField('打卡时间', null=True, blank=True)
not_work_remark = models.CharField('当前未打卡说明', null=True, blank=True, max_length=200)
third_info = models.JSONField('三方信息', default=dict, null=False, blank=True)
class Meta:
verbose_name = '员工补充信息'
verbose_name_plural = verbose_name
@ -53,7 +51,6 @@ class Employee(CommonBModel):
# CARD_LOCATION = 20
class NotWorkRemark(CommonAModel):
"""
离岗说明
@ -62,6 +59,7 @@ class NotWorkRemark(CommonAModel):
user = models.ForeignKey(User, verbose_name='用户', on_delete=models.CASCADE)
remark = models.CharField('未打卡说明', null=True, blank=True, max_length=200)
class ClockRecord(CommonADModel):
"""
打卡记录
@ -71,5 +69,3 @@ class ClockRecord(CommonADModel):
(ClOCK_WORK1, '上班打卡'),
)
type = models.PositiveSmallIntegerField('打卡类型', choices=type_choice, default=ClOCK_WORK1)

View File

@ -1,20 +1,18 @@
from apps.hrm.errors import DH_PHOTO_FALI, PHONE_EXIST, PHONE_F_WRONG
from apps.system.models import Dept, User
from apps.hrm.errors import PHONE_F_WRONG
from rest_framework.serializers import ModelSerializer
from rest_framework import serializers
from apps.utils.serializers import CustomModelSerializer
from apps.utils.constants import EXCLUDE_FIELDS
from apps.utils.tools import rannum, ranstr
from .models import ClockRecord, Employee, NotWorkRemark
from apps.system.serializers import DeptSimpleSerializer,UserSimpleSerializer
from apps.utils.tools import rannum
from apps.hrm.models import ClockRecord, Employee, NotWorkRemark
from apps.system.serializers import DeptSimpleSerializer, UserSimpleSerializer
from django.db import transaction
from apps.third.clients import dhClient
from apps.third.tapis import dhapis
import re
from server.settings import DEBUG
from rest_framework.exceptions import ParseError
from datetime import datetime, timedelta
from datetime import datetime
class EmployeeBaseSerializer(CustomModelSerializer):
def save(self, **kwargs):
@ -26,24 +24,26 @@ class EmployeeBaseSerializer(CustomModelSerializer):
def phone_check(phone):
re_phone = '^1[358]\d{9}$|^147\d{8}$|^176\d{8}$'
re_phone = r'^1[358]\d{9}$|^147\d{8}$|^176\d{8}$'
if not re.match(re_phone, phone):
raise serializers.ValidationError(**PHONE_F_WRONG)
return phone
class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer):
phone = serializers.CharField(label="手机号", validators=[phone_check])
class Meta:
model = Employee
exclude = EXCLUDE_FIELDS + ['face_data',
'is_atwork', 'last_check_time',
'not_work_remark', 'third_info']
exclude = EXCLUDE_FIELDS + ['face_data',
'is_atwork', 'last_check_time',
'not_work_remark', 'third_info']
extra_kwargs = {
'phone': {'required': True},
'number': {'required': True},
'photo': {'required': True},
'id_number': {'required': True},
}
}
@transaction.atomic
def create(self, validated_data):
@ -59,7 +59,7 @@ class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer):
except:
pass
json_data = {
"service":"ehs",
"service": "ehs",
"id": personId,
"name": instance.name,
"code": instance.number,
@ -75,11 +75,11 @@ class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer):
dh_photo = res["fileUrl"]
json_data.update(
{
"biosignatureTypeList":[3],
"personBiosignatures":[{
"type":3,
"index":1,
"path": dh_photo
"biosignatureTypeList": [3],
"personBiosignatures": [{
"type": 3,
"index": 1,
"path": dh_photo
}]
}
)
@ -91,20 +91,20 @@ class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer):
now = datetime.now()
startDate = now.strftime("%Y-%m-%d %H:%M:%S")
endDate = (datetime(year=now.year+50,
month=now.month, day=1)).strftime("%Y-%m-%d %H:%M:%S")
month=now.month, day=1)).strftime("%Y-%m-%d %H:%M:%S")
json_data = {
"id": cardId,
"cardNumber": cardNumber,
"category": 0,
"cardType": 0,
"personId": personId,
"departmentId":departmentId,
"departmentId": departmentId,
"startDate": startDate,
"endDate": endDate
}
_, res = dhClient.request(**dhapis['card_add'], json=json_data)
instance.third_info = {'dh_id':personId,
'dh_photo':dh_photo, 'dh_face_card':res['id']}
instance.third_info = {'dh_id': personId,
'dh_photo': dh_photo, 'dh_face_card': res['id']}
instance.save()
return instance
@ -123,7 +123,7 @@ class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer):
dh_id = instance.third_info['dh_id']
dh_photo = third_info['dh_photo']
json_data = {
"service":"ehs",
"service": "ehs",
"id": dh_id,
"name": instance.name,
"code": instance.number,
@ -134,23 +134,23 @@ class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer):
"phone": instance.phone,
"email": instance.email,
"sex": 1 if instance.gender == '' else 2,
"biosignatureTypeList":[3],
"personBiosignatures":[{
"type":3,
"index":1,
"biosignatureTypeList": [3],
"personBiosignatures": [{
"type": 3,
"index": 1,
"path": third_info['dh_photo']
}]
}]
}
if instance.photo != old_photo:
_, res = dhClient.request(**dhapis['person_img_upload'], file_path_rela=instance.photo)
dh_photo = res["fileUrl"]
json_data.update(
{
"biosignatureTypeList":[3],
"personBiosignatures":[{
"type":3,
"index":1,
"path": dh_photo
"biosignatureTypeList": [3],
"personBiosignatures": [{
"type": 3,
"index": 1,
"path": dh_photo
}]
}
)
@ -165,14 +165,14 @@ class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer):
now = datetime.now()
startDate = now.strftime("%Y-%m-%d %H:%M:%S")
endDate = (datetime(year=now.year+50,
month=now.month, day=1)).strftime("%Y-%m-%d %H:%M:%S")
month=now.month, day=1)).strftime("%Y-%m-%d %H:%M:%S")
json_data = {
"id": cardId,
"cardNumber": cardNumber,
"category": 0,
"cardType": 0,
"personId": third_info['dh_id'],
"departmentId":departmentId,
"departmentId": departmentId,
"startDate": startDate,
"endDate": endDate
}
@ -181,31 +181,35 @@ class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer):
instance.save()
return instance
class ChannelAuthoritySerializer(serializers.Serializer):
pks = serializers.ListField(child=serializers.CharField(max_length=20), label="员工ID列表")
channels = serializers.ListField(child=serializers.CharField(max_length=20), label="门通道ID列表")
class EmployeeSerializer(EmployeeBaseSerializer):
belong_dept_ = DeptSimpleSerializer(source='belong_dept', read_only=True)
class Meta:
model = Employee
exclude = ['face_data']
exclude = ['face_data']
read_only_fields = ['is_atwork', 'last_check_time', 'not_work_remark']
class EmployeeNotWorkRemarkSerializer(ModelSerializer):
class Meta:
model = Employee
fields = ['not_work_remark']
class ClockRecordListSerializer(serializers.ModelSerializer):
create_by_ = UserSimpleSerializer(source='create_by', read_only=True)
class Meta:
model = ClockRecord
fields = '__all__'
class NotWorkRemarkListSerializer(serializers.ModelSerializer):
class Meta:
model = NotWorkRemark

View File

@ -2,7 +2,7 @@ from django.db.models.signals import post_save
from apps.system.models import User
from django.dispatch import receiver
from apps.hrm.models import Employee
from django.conf import settings
@receiver(post_save, sender=User)
def updateEmployee(sender, instance, created, **kwargs):
@ -13,4 +13,3 @@ def updateEmployee(sender, instance, created, **kwargs):
if ep.belong_dept and ep.belong_dept != instance.belong_dept:
ep.belong_dept = instance.belong_dept
ep.save()

View File

@ -9,6 +9,4 @@ def update_all_employee_not_atwork():
"""
将所有员工设为非在岗状态
"""
Employee.objects.all().update(is_atwork=False, last_check_time = None, not_work_remark=None)
Employee.objects.all().update(is_atwork=False, last_check_time=None, not_work_remark=None)

View File

@ -1,5 +1,3 @@
from rest_framework import urlpatterns
from apps.hrm.views import ClockRecordViewSet, EmployeeViewSet, NotWorkRemarkViewSet
from django.urls import path, include
from rest_framework.routers import DefaultRouter
@ -14,4 +12,3 @@ router.register('not_work_remark', NotWorkRemarkViewSet, basename='not_work_ream
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
]

View File

@ -1,18 +1,13 @@
from datetime import datetime
from django.conf import settings
from django.db import transaction
from django.shortcuts import render
from django.utils import timezone
from rest_framework import exceptions, serializers, status
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.exceptions import ParseError
from rest_framework.generics import CreateAPIView
from rest_framework.mixins import (CreateModelMixin, ListModelMixin,
RetrieveModelMixin, UpdateModelMixin)
from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from apps.hrm.errors import NO_NEED_LEVEL_REMARK
from apps.hrm.filters import (ClockRecordFilterSet, EmployeeFilterSet,
@ -24,11 +19,9 @@ from apps.hrm.serializers import (ChannelAuthoritySerializer,
EmployeeNotWorkRemarkSerializer,
EmployeeSerializer,
NotWorkRemarkListSerializer)
from apps.system.models import User
from apps.system.serializers import UserSimpleSerializer
from apps.third.clients import dhClient
from apps.third.tapis import dhapis
from apps.utils.serializers import PkSerializer
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
@ -47,8 +40,8 @@ class EmployeeViewSet(CustomModelViewSet):
search_fields = ['name', 'number', 'user__username']
ordering = ['-pk']
@action(methods=['post'], detail=True, perms_map={'post': 'employee:notworkremark'}
, serializer_class=EmployeeNotWorkRemarkSerializer)
@action(methods=['post'], detail=True, perms_map={'post': 'employee:notworkremark'},
serializer_class=EmployeeNotWorkRemarkSerializer)
def not_work_remark(self, request, pk=None):
"""
填写离岗说明
@ -62,13 +55,13 @@ class EmployeeViewSet(CustomModelViewSet):
obj.save()
now = timezone.now()
instance, created = NotWorkRemark.objects.get_or_create(
not_work_date = now.date(),
user = obj.user,
not_work_date=now.date(),
user=obj.user,
defaults={
"not_work_date":now.date(),
"user":obj.user,
"remark":remark,
"create_by":request.user,
"not_work_date": now.date(),
"user": obj.user,
"remark": remark,
"create_by": request.user,
}
)
if not created:
@ -79,8 +72,8 @@ class EmployeeViewSet(CustomModelViewSet):
raise ParseError(**NO_NEED_LEVEL_REMARK)
@transaction.atomic
@action(methods=['post'], detail=False, perms_map={'post': 'employee:channel_authority'}
, serializer_class=ChannelAuthoritySerializer)
@action(methods=['post'], detail=False, perms_map={'post': 'employee:channel_authority'},
serializer_class=ChannelAuthoritySerializer)
def channel_authority(self, request, pk=None):
"""门通道授权
@ -110,12 +103,12 @@ class EmployeeViewSet(CustomModelViewSet):
dhClient.request(**dhapis['card_door_authority'], json=json_data)
for i in objs:
i.third_info['dh_channels'] = vdata['channels']
Employee.objects.bulk_update(objs, fields = ['third_info'])
Employee.objects.bulk_update(objs, fields=['third_info'])
return Response()
@transaction.atomic
@action(methods=['post'], detail=False, perms_map={'post': 'employee:face_bind_1'}
, serializer_class=serializers.Serializer)
@action(methods=['post'], detail=False, perms_map={'post': 'employee:face_bind_1'},
serializer_class=serializers.Serializer)
def face_bind_1(self, request, pk=None):
"""同步人脸库1
@ -123,10 +116,10 @@ class EmployeeViewSet(CustomModelViewSet):
"""
# 获取设备
json_data = {
"pageNum":1,
"pageSize":1000,
"ownerCodes":['001'],
"showChildNodeData":1,
"pageNum": 1,
"pageSize": 1000,
"ownerCodes": ['001'],
"showChildNodeData": 1,
# "isOnline":1
}
_, res = dhClient.request(**dhapis['dev_page'], json=json_data)
@ -136,41 +129,40 @@ class EmployeeViewSet(CustomModelViewSet):
devs.append(i['deviceCode'])
# 编辑人像库
json_data = {
"groupid":settings.DAHUA_FACEGROUPID_1,
"groupname":"全体人员",
"groupdetail":"全体人员",
"grouptype":3,
"deviceCodeList":devs,
"syncState":0
"groupid": settings.DAHUA_FACEGROUPID_1,
"groupname": "全体人员",
"groupdetail": "全体人员",
"grouptype": 3,
"deviceCodeList": devs,
"syncState": 0
}
dhClient.request(**dhapis['face_group_update'], json=json_data)
# 人像绑定
json_data = {
"deptId":1,
"groupIdList":[settings.DAHUA_FACEGROUPID_1],
"cascade":True
"deptId": 1,
"groupIdList": [settings.DAHUA_FACEGROUPID_1],
"cascade": True
}
dhClient.request(**dhapis['face_bind'], json=json_data)
return Response()
@action(methods=['get'], detail=False, perms_map={'get': 'employee:face_bind_1'}
, serializer_class=serializers.Serializer)
@action(methods=['get'], detail=False, perms_map={'get': 'employee:face_bind_1'},
serializer_class=serializers.Serializer)
def face_status_1(self, request, pk=None):
"""人像下发状态
人像下发状态
"""
params = {'id':settings.DAHUA_FACEGROUPID_1}
params = {'id': settings.DAHUA_FACEGROUPID_1}
_, res = dhClient.request(**dhapis['face_group_info'], params=params)
return Response(res)
class ClockRecordViewSet(ListModelMixin, CustomGenericViewSet):
"""
打卡记录
"""
perms_map = {'get':'*', 'post':'*'}
perms_map = {'get': '*', 'post': '*'}
authentication_classes = []
permission_classes = [AllowAny]
queryset = ClockRecord.objects.all()
@ -184,7 +176,7 @@ class NotWorkRemarkViewSet(ListModelMixin, CustomGenericViewSet):
"""
离岗说明
"""
perms_map = {'get':'*'}
perms_map = {'get': '*'}
queryset = NotWorkRemark.objects.all()
select_related_fields = ['user']
serializer_class = NotWorkRemarkListSerializer

View File

@ -1,7 +1,7 @@
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']

View File

@ -1 +1 @@
LOG_NOT_FONED = {"code":"log_not_found", "detail":"日志不存在"}
LOG_NOT_FONED = {"code": "log_not_found", "detail": "日志不存在"}

View File

@ -3,6 +3,7 @@ from django.db import models
from apps.utils.models import BaseModel
class DrfRequestLog(BaseModel):
"""Logs Django rest framework API requests"""

View File

@ -1,5 +1,4 @@
from django.urls import path, include
from rest_framework import routers
from django.urls import path
from .views import DrfRequestLogViewSet, ServerInfoView, LogView, LogDetailView, index, room, video
API_BASE_URL = 'api/monitor/'
@ -13,5 +12,5 @@ urlpatterns = [
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()),
path(API_BASE_URL + 'request_log/', DrfRequestLogViewSet.as_view({'get':'list'}), name='requestlog_view')
path(API_BASE_URL + 'request_log/', DrfRequestLogViewSet.as_view({'get': 'list'}), name='requestlog_view')
]

View File

@ -4,10 +4,9 @@ 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 rest_framework import serializers
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework.exceptions import NotFound
@ -28,12 +27,14 @@ def room(request, room_name):
'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):
"""
获取服务器当前状态
@ -124,19 +125,19 @@ class LogDetailView(APIView):
except:
raise NotFound(**LOG_NOT_FONED)
class DrfRequestLogSerializer(serializers.ModelSerializer):
class Meta:
model = DrfRequestLog
fields = '__all__'
class DrfRequestLogViewSet(ListModelMixin, CustomGenericViewSet):
"""请求日志
请求日志
"""
perms_map = {'get':'requestlog.view'}
perms_map = {'get': 'requestlog.view'}
queryset = DrfRequestLog.objects.all()
list_serializer_class = DrfRequestLogSerializer
ordering = ['-requested_at']

View File

@ -1,7 +1,7 @@
SCHEDULE_WRONG = {"code":"schedule_wrong", "detail":"时间策略有误"}
PASSWORD_NOT_SAME = {"code":"password_not_same", "detail":"新旧密码不一致"}
OLD_PASSWORD_WRONG = {"code":"old_password_wrong", "detail":"旧密码错误"}
SCHEDULE_WRONG = {"code": "schedule_wrong", "detail": "时间策略有误"}
PASSWORD_NOT_SAME = {"code": "password_not_same", "detail": "新旧密码不一致"}
OLD_PASSWORD_WRONG = {"code": "old_password_wrong", "detail": "旧密码错误"}
USERNAME_EXIST = {"code":"username_exist", "detail":"账户已存在"}
ROLE_NAME_EXIST = {"code":"role_name_exist", "detail":"角色名已存在"}
ROLE_CODE_EXIST = {"code":"role_code_exist", "detail":"角色标识已存在"}
USERNAME_EXIST = {"code": "username_exist", "detail": "账户已存在"}
ROLE_NAME_EXIST = {"code": "role_name_exist", "detail": "角色名已存在"}
ROLE_CODE_EXIST = {"code": "role_code_exist", "detail": "角色标识已存在"}

View File

@ -1,18 +1,17 @@
import json
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.system.errors import ROLE_CODE_EXIST, ROLE_NAME_EXIST, USERNAME_EXIST
from apps.system.errors import USERNAME_EXIST
from apps.utils.serializers import CustomModelSerializer
from apps.utils.constants import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE
from .models import (Dictionary, DictType, File, Dept, Permission, Post,
Role, User, UserPost)
from rest_framework.exceptions import ParseError, APIException
from rest_framework.exceptions import ParseError
from django.db import transaction
from apps.third.tapis import dhapis
from rest_framework.validators import UniqueValidator
from django.contrib.auth.hashers import make_password
class IntervalSerializer(CustomModelSerializer):
class Meta:
@ -72,6 +71,7 @@ class DictTypeSerializer(CustomModelSerializer):
model = DictType
fields = '__all__'
class DictTypeCreateUpdateSerializer(CustomModelSerializer):
class Meta:
model = DictType
@ -87,6 +87,7 @@ class DictSerializer(CustomModelSerializer):
model = Dictionary
fields = '__all__'
class DictCreateUpdateSerializer(CustomModelSerializer):
"""
数据字典序列化
@ -96,6 +97,7 @@ class DictCreateUpdateSerializer(CustomModelSerializer):
model = Dictionary
exclude = EXCLUDE_FIELDS
class PostSerializer(CustomModelSerializer):
"""
岗位序列化
@ -105,6 +107,7 @@ class PostSerializer(CustomModelSerializer):
model = Post
fields = '__all__'
class PostCreateUpdateSerializer(CustomModelSerializer):
"""
岗位序列化
@ -145,10 +148,12 @@ class RoleCreateUpdateSerializer(CustomModelSerializer):
UniqueValidator(queryset=Role.objects.all(), message='已存在相同名称的角色')])
code = serializers.CharField(label="标识", validators=[
UniqueValidator(queryset=Role.objects.all(), message='已存在相同标识的角色')])
class Meta:
model = Role
exclude = EXCLUDE_FIELDS
class PermissionSerializer(serializers.ModelSerializer):
"""
权限序列化
@ -158,6 +163,7 @@ class PermissionSerializer(serializers.ModelSerializer):
model = Permission
fields = '__all__'
class PermissionCreateUpdateSerializer(serializers.ModelSerializer):
"""
权限序列化
@ -167,6 +173,7 @@ class PermissionCreateUpdateSerializer(serializers.ModelSerializer):
model = Permission
exclude = EXCLUDE_FIELDS_BASE
class DeptSimpleSerializer(CustomModelSerializer):
class Meta:
model = Dept
@ -184,11 +191,13 @@ class DeptSerializer(CustomModelSerializer):
model = Dept
fields = '__all__'
class DeptCreateUpdateSerializer(CustomModelSerializer):
"""
部门序列化
"""
parent = serializers.PrimaryKeyRelatedField(queryset = Dept.objects.all(), required=True)
parent = serializers.PrimaryKeyRelatedField(queryset=Dept.objects.all(), required=True)
class Meta:
model = Dept
exclude = EXCLUDE_FIELDS + ['third_info']
@ -198,16 +207,15 @@ class DeptCreateUpdateSerializer(CustomModelSerializer):
from apps.third.clients import dhClient
if dhClient:
data = {
"parentId":1,
"name":validated_data['name'],
"service":"ehs"
"parentId": 1,
"name": validated_data['name'],
"service": "ehs"
}
_, res = dhClient.request(**dhapis['dept_create'],json=data)
third_info = {'dh_id':str(res['id'])}
_, res = dhClient.request(**dhapis['dept_create'], json=data)
third_info = {'dh_id': str(res['id'])}
validated_data['third_info'] = third_info
return super().create(validated_data)
@transaction.atomic
def update(self, instance, validated_data):
from apps.third.clients import dhClient
@ -215,24 +223,25 @@ class DeptCreateUpdateSerializer(CustomModelSerializer):
if dhClient and not third_info.get('dh_id', False):
# 如果dh_id 不存在
data = {
"parentId":1,
"name":validated_data['name'],
"service":"ehs"
"parentId": 1,
"name": validated_data['name'],
"service": "ehs"
}
_, res = dhClient.request(**dhapis['dept_create'],json=data)
_, res = dhClient.request(**dhapis['dept_create'], json=data)
third_info['dh_id'] = res['id']
instance.third_info = third_info
instance.save()
elif instance.name != validated_data.get('name', ''):
if dhClient and instance.third_info.get('dh_id', False):
data = {
"id":instance.third_info['dh_id'],
"parentId":1,
"name":validated_data['name']
"id": instance.third_info['dh_id'],
"parentId": 1,
"name": validated_data['name']
}
dhClient.request(**dhapis['dept_update'],json=data)
dhClient.request(**dhapis['dept_update'], json=data)
return super().update(instance, validated_data)
class UserSimpleSerializer(CustomModelSerializer):
class Meta:
model = User
@ -260,11 +269,13 @@ class UserListSerializer(CustomModelSerializer):
# if User.objects.filter(phone=phone).exists():
# raise serializers.ValidationError(**PHONE_EXIST)
def user_exist(username):
if User.objects.filter(username=username).exists():
raise serializers.ValidationError(**USERNAME_EXIST)
return username
class UserUpdateSerializer(CustomModelSerializer):
"""
用户编辑序列化
@ -275,13 +286,11 @@ class UserUpdateSerializer(CustomModelSerializer):
def update(self, instance, validated_data):
if User.objects.filter(username=validated_data['username']
).exclude(id=instance.id).exists():
).exclude(id=instance.id).exists():
raise ParseError(**USERNAME_EXIST)
return super().update(instance, validated_data)
class UserCreateSerializer(CustomModelSerializer):
"""
创建用户序列化
@ -293,12 +302,12 @@ class UserCreateSerializer(CustomModelSerializer):
fields = ['username', 'name', 'avatar', 'is_active']
class PasswordChangeSerializer(serializers.Serializer):
old_password = serializers.CharField(label="原密码")
new_password1 = serializers.CharField(label="新密码1")
new_password2 = serializers.CharField(label="新密码2")
class PTaskResultSerializer(CustomModelSerializer):
class Meta:
model = TaskResult
@ -317,6 +326,7 @@ class UserPostSerializer(CustomModelSerializer):
model = UserPost
fields = '__all__'
class UserPostCreateSerializer(CustomModelSerializer):
class Meta:
model = UserPost

View File

@ -1,5 +1,8 @@
from django.urls import path, include
from .views import FileViewSet, PTaskResultViewSet, TaskList, UserPostViewSet, UserViewSet, DeptViewSet, PermissionViewSet, RoleViewSet, PostViewSet, DictTypeViewSet, DictViewSet, PTaskViewSet
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/'

View File

@ -17,14 +17,15 @@ from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_W
from apps.utils.mixins import (CustomCreateModelMixin)
from django.conf import settings
from apps.utils.permission import ALL_PERMS, get_user_perms_map
from apps.utils.queryset import get_child_queryset2
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
from server.celery import app as celery_app
from .filters import UserFilter
from .models import (Dept, Dictionary, DictType, File, Permission, Post, Role, User,
UserPost)
from .serializers import (DeptCreateUpdateSerializer, DeptSerializer, DictCreateUpdateSerializer, DictSerializer, DictTypeCreateUpdateSerializer, DictTypeSerializer,
FileSerializer, PasswordChangeSerializer, PermissionCreateUpdateSerializer, PermissionSerializer, PostCreateUpdateSerializer, PostSerializer,
from .serializers import (DeptCreateUpdateSerializer, DeptSerializer, DictCreateUpdateSerializer,
DictSerializer, DictTypeCreateUpdateSerializer, DictTypeSerializer,
FileSerializer, PasswordChangeSerializer, PermissionCreateUpdateSerializer,
PermissionSerializer, PostCreateUpdateSerializer, PostSerializer,
PTaskCreateUpdateSerializer, PTaskResultSerializer,
PTaskSerializer, RoleCreateUpdateSerializer, RoleSerializer,
UserCreateSerializer, UserListSerializer, UserPostCreateSerializer,
@ -280,7 +281,7 @@ class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo
def perform_destroy(self, instance):
user = instance.user
instance.delete(update_by = self.request.user)
instance.delete(update_by=self.request.user)
fdept = UserPost.objects.filter(user=user).order_by('sort', 'create_time').first()
if fdept:
user.belong_dept = fdept
@ -312,8 +313,8 @@ class UserViewSet(CustomModelViewSet):
return Response(data=serializer.data)
@action(methods=['put'], detail=False,
permission_classes=[IsAuthenticated],
serializer_class = PasswordChangeSerializer)
permission_classes=[IsAuthenticated],
serializer_class=PasswordChangeSerializer)
def password(self, request, pk=None):
"""修改密码

View File

@ -1 +1 @@
TAPI_CODE_WRONG = {"code":"tapi_code_wrong", "detail":"三方接口请求标识有误"}
TAPI_CODE_WRONG = {"code": "tapi_code_wrong", "detail": "三方接口请求标识有误"}

View File

@ -3,10 +3,10 @@ from rest_framework import serializers
class RequestCommonSerializer(serializers.Serializer):
method_choice = (
('post','post'),
('get','get'),
('put','put'),
('delete','delete')
('post', 'post'),
('get', 'get'),
('put', 'put'),
('delete', 'delete')
)
url = serializers.CharField(label='请求地址', required=False)
method = serializers.ChoiceField(label='请求方法', choices=method_choice, required=False)

View File

@ -1,88 +1,88 @@
# 大华API接口
dhapis = {
"dept_tree":{
"url":"/evo-apigw/evo-brm/1.2.0/department/tree",
"method":"post"
"dept_tree": {
"url": "/evo-apigw/evo-brm/1.2.0/department/tree",
"method": "post"
},
"dept_create":{
"url":"/evo-apigw/evo-brm/1.0.0/department/add",
"method":"post"
"dept_create": {
"url": "/evo-apigw/evo-brm/1.0.0/department/add",
"method": "post"
},
"dept_update":{
"url":"/evo-apigw/evo-brm/1.0.0/department/update",
"method":"put"
"dept_update": {
"url": "/evo-apigw/evo-brm/1.0.0/department/update",
"method": "put"
},
"person_gen_id":{
"url":"/evo-apigw/evo-brm/1.0.0/person/generate-id",
"method":"get"
"person_gen_id": {
"url": "/evo-apigw/evo-brm/1.0.0/person/generate-id",
"method": "get"
},
"person_add": {
"url":"/evo-apigw/evo-brm/1.2.0/person/subsystem/add",
"method":"post"
"url": "/evo-apigw/evo-brm/1.2.0/person/subsystem/add",
"method": "post"
},
"person_update": {
"url":"/evo-apigw/evo-brm/1.2.0/person/subsystem/update",
"method":"put"
"url": "/evo-apigw/evo-brm/1.2.0/person/subsystem/update",
"method": "put"
},
"person_img_upload": {
"url":"/evo-apigw/evo-brm/1.2.0/person/upload/img",
"method":"post"
"url": "/evo-apigw/evo-brm/1.2.0/person/upload/img",
"method": "post"
},
"person_detail": {
"url":"/evo-apigw/evo-brm/1.0.0/person/subsystem/{id}",
"method":"get"
"url": "/evo-apigw/evo-brm/1.0.0/person/subsystem/{id}",
"method": "get"
},
"device_list": {
"url":"/evo-apigw/evo-brm/1.2.0/device/subsystem/page",
"method":"get"
"url": "/evo-apigw/evo-brm/1.2.0/device/subsystem/page",
"method": "get"
},
"device_detail": {
"url":"/evo-apigw/evo-brm/1.0.0/device/{deviceCode}",
"method":"get"
"url": "/evo-apigw/evo-brm/1.0.0/device/{deviceCode}",
"method": "get"
},
"channel_list": {
"url":"/evo-apigw/evo-brm/1.2.0/device/channel/subsystem/page",
"method":"post"
"url": "/evo-apigw/evo-brm/1.2.0/device/channel/subsystem/page",
"method": "post"
},
"card_add": {
"url":"/evo-apigw/evo-brm/1.2.0/card/add",
"method":"post"
"url": "/evo-apigw/evo-brm/1.2.0/card/add",
"method": "post"
},
"card_gen_id": {
"url":"/evo-apigw/evo-brm/1.0.0/card/generate-id",
"method":"get"
"url": "/evo-apigw/evo-brm/1.0.0/card/generate-id",
"method": "get"
},
"card_door_authority": {
"url":"/evo-apigw/evo-accesscontrol/1.0.0/card/accessControl/doorAuthority",
"method":"post"
"url": "/evo-apigw/evo-accesscontrol/1.0.0/card/accessControl/doorAuthority",
"method": "post"
},
"mq_subscribe": {
"url":"/evo-apigw/evo-event/1.0.0/subscribe/mqinfo",
"method":"post"
"url": "/evo-apigw/evo-event/1.0.0/subscribe/mqinfo",
"method": "post"
},
"mq_unsubscribe": {
"url":"/evo-apigw/evo-event/1.0.0/subscribe/mqinfo",
"method":"delete"
"url": "/evo-apigw/evo-event/1.0.0/subscribe/mqinfo",
"method": "delete"
},
"face_bind":{
"url":"/evo-apigw/evo-face/deptGroup/sync/bindPerson",
"method":"post"
"face_bind": {
"url": "/evo-apigw/evo-face/deptGroup/sync/bindPerson",
"method": "post"
},
"dev_tree":{
"url":"/evo-apigw/evo-face/tree/getDevChnIdsAndName",
"method":"get"
"dev_tree": {
"url": "/evo-apigw/evo-face/tree/getDevChnIdsAndName",
"method": "get"
},
"dev_page":{
"url":"/evo-apigw/evo-brm/1.2.0/device/subsystem/page",
"method":"post"
"dev_page": {
"url": "/evo-apigw/evo-brm/1.2.0/device/subsystem/page",
"method": "post"
},
"face_group_update":{
"url":"/evo-apigw/evo-face/groupInfo/update",
"method":"post"
"face_group_update": {
"url": "/evo-apigw/evo-face/groupInfo/update",
"method": "post"
},
"face_group_info":{
"url":"/evo-apigw/evo-face/groupInfo/view/{id}",
"method":"get"
"face_group_info": {
"url": "/evo-apigw/evo-face/groupInfo/view/{id}",
"method": "get"
}
}

View File

@ -2,7 +2,6 @@ from rest_framework.exceptions import ParseError, APIException
from apps.third.tapis import dhapis, xxapis
from apps.third.erros import TAPI_CODE_WRONG
from apps.third.clients import dhClient
from apps.utils.errors import XX_REQUEST_ERROR
from apps.utils.mixins import MyLoggingMixin
from apps.third.clients import xxClient
from rest_framework.response import Response
@ -51,6 +50,8 @@ class XxTestView(APIView):
raise APIException(**res)
# 寻息事件订阅
class XxListener(stomp.ConnectionListener):
def on_error(self, frame):
print('received an error "%s"' % frame.body)
@ -65,6 +66,7 @@ if settings.XX_ENABLED:
c.connect(settings.XX_USERNAME, settings.XX_LICENCE)
c.subscribe(settings.XX_QUEUE, id='')
class XxCommonViewSet(CreateModelMixin, CustomGenericViewSet):
"""
寻息通用调用接口
@ -175,7 +177,7 @@ class DhCommonViewSet(CreateModelMixin, CustomGenericViewSet):
取消事件订阅
"""
dhClient.request(**dhapis['mq_unsubscribe'], params={'name':'127.0.0.1_8000'})
dhClient.request(**dhapis['mq_unsubscribe'], params={'name': '127.0.0.1_8000'})
return Response()
@action(methods=['post'], detail=False,
@ -189,7 +191,7 @@ class DhCommonViewSet(CreateModelMixin, CustomGenericViewSet):
data = request.data
method = data['method']
subsystem = data.get('subsystem', None)
info = data.get('info', {})
# info = data.get('info', {})
if method == 'department.update':
pass
elif method == 'person.update':

View File

@ -1,22 +1,22 @@
import time
from threading import Thread
import traceback
import requests
from django.conf import settings
from rest_framework.exceptions import APIException, ParseError
from apps.utils.errors import DH_REQUEST_ERROR
from apps.utils.tools import print_roundtrip
from django.conf import settings
import json
import time
from rest_framework.exceptions import APIException, ParseError
import os
requests.packages.urllib3.disable_warnings()
class DhClient:
"""
大华
"""
def __init__(self, client_id= settings.DAHUA_CLIENTID
, client_secret = settings.DAHUA_SECRET) -> None:
def __init__(self, client_id=settings.DAHUA_CLIENTID,
client_secret=settings.DAHUA_SECRET) -> None:
if not settings.DAHUA_ENABLED:
return None
self.client_id = client_id
@ -34,7 +34,8 @@ class DhClient:
'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)
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']
@ -66,7 +67,8 @@ class DhClient:
self.isRuning = False
# self.t.join()
def request(self, url:str, method:str, params=dict(), json=dict(), timeout=10, file_path_rela=None, raise_exception=True):
def request(self, url: str, method: str, params=dict(), json=dict(), timeout=10,
file_path_rela=None, raise_exception=True):
if self.isGetingToken:
req_num = 0
while True:
@ -78,13 +80,14 @@ class DhClient:
break
else:
files = None
if file_path_rela: # 相对路径
if file_path_rela: # 相对路径
files = {'file': open(settings.BASE_DIR + file_path_rela, 'rb')}
try:
if params:
url = url.format(**params)
r = getattr(requests, method)('{}{}'.format(settings.DAHUA_BASE_URL, url)
, headers = self.headers, params=params, json=json, timeout=timeout, files=files, verify=False)
r = getattr(requests, method)('{}{}'.format(settings.DAHUA_BASE_URL, url),
headers=self.headers, params=params, json=json,
timeout=timeout, files=files, verify=False)
except:
if raise_exception:
raise APIException(**DH_REQUEST_ERROR)
@ -92,16 +95,15 @@ class DhClient:
# if settings.DEBUG:
# print_roundtrip(r)
if r.status_code == 200:
"""
请求成功
"""
ret = r.json()
if ret.get('code') == '27001007':
self.get_token() # 重新获取token
self.get_token() # 重新获取token
self.request(url, method, params, json, timeout, file_path_rela, raise_exception)
else:
if ret['code'] not in ['0', '100', '00000', '1000', 0, 100, 1000]:
detail = '大华错误:' + '{}|{}{}{}'.format(str(ret['code']), ret.get('errMsg',''), ret.get('desc', ''), str(ret.get('data', '')))
detail = '大华错误:' + \
'{}|{}{}{}'.format(str(ret['code']), ret.get('errMsg', ''),
ret.get('desc', ''), str(ret.get('data', '')))
err_detail = dict(detail=detail, code='dh_'+str(ret['code']))
if raise_exception:
raise ParseError(**err_detail)
@ -111,4 +113,5 @@ class DhClient:
raise APIException(**DH_REQUEST_ERROR)
return 'error', DH_REQUEST_ERROR
dhClient = DhClient()

View File

@ -1,4 +1,4 @@
XX_REQUEST_ERROR = {"code":"xx_request_error", "detail":"寻息接口访问异常"}
DH_REQUEST_ERROR = {"code":"dh_request_error", "detail":"大华接口访问异常"}
SIGN_MAKE_FAIL = {"code":"sign_make_fail", "detail":"签名照生成失败,请重新上传"}
PKS_ERROR = {"code":"pks_error", "detail":"未获取到主键列表"}
XX_REQUEST_ERROR = {"code": "xx_request_error", "detail": "寻息接口访问异常"}
DH_REQUEST_ERROR = {"code": "dh_request_error", "detail": "大华接口访问异常"}
SIGN_MAKE_FAIL = {"code": "sign_make_fail", "detail": "签名照生成失败,请重新上传"}
PKS_ERROR = {"code": "pks_error", "detail": "未获取到主键列表"}

View File

@ -1,12 +1,13 @@
import traceback
from django.core.exceptions import PermissionDenied
from django.http import Http404
from rest_framework.response import Response
from rest_framework import exceptions
from rest_framework.response import Response
from rest_framework.views import set_rollback
from django.utils.translation import gettext_lazy as _
import traceback
from server.settings import myLogger
def custom_exception_hander(exc, context):
"""
自定义异常处理
@ -25,15 +26,15 @@ def custom_exception_hander(exc, context):
headers['Retry-After'] = '%d' % exc.wait
if isinstance(exc.detail, (list, dict)):
data = {'err_detail':exc.detail}
data = {'err_detail': exc.detail}
data['err_code'] = exc.default_code
data['err_msg'] = exc.default_detail
else:
data = {'err_msg': exc.detail, 'err_code':exc.get_codes()}
data = {'err_msg': exc.detail, 'err_code': exc.get_codes()}
set_rollback()
data['request_id'] = request_id
return Response(data, status=exc.status_code, headers=headers)
err_str = traceback.format_exc()
myLogger.error('{}-{}'.format(request_id, err_str))
return Response(data={'err_code':'server_error', 'err_msg':'服务器错误', 'request_id': request_id}, status=500)
args = (request_id, traceback.format_exc())
myLogger.error(f"{args[0]}-{args[1]}")
return Response(data={'err_code': 'server_error', 'err_msg': '服务器错误', 'request_id': request_id}, status=500)

View File

@ -1,6 +1,5 @@
import uuid
from django.db.models.query import QuerySet
from rest_framework.mixins import CreateModelMixin, UpdateModelMixin, ListModelMixin, RetrieveModelMixin, DestroyModelMixin
from rest_framework.mixins import CreateModelMixin, UpdateModelMixin, DestroyModelMixin
import ast
import ipaddress
import traceback
@ -8,58 +7,67 @@ from apps.monitor.models import DrfRequestLog
from server.settings import myLogger
from django.db import connection
from django.utils.timezone import now
from apps.utils.snowflake import idWorker
from user_agents import parse
class CreateUpdateModelAMixin:
"""
业务用基本表A用
"""
def perform_create(self, serializer):
serializer.save(create_by = self.request.user)
serializer.save(create_by=self.request.user)
def perform_update(self, serializer):
serializer.save(update_by = self.request.user)
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)
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)
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)
serializer.save(create_by=self.request.user, belong_dept=self.request.user.dept)
else:
serializer.save(create_by = self.request.user)
serializer.save(create_by=self.request.user)
def perform_update(self, serializer):
serializer.save(update_by = self.request.user)
serializer.save(update_by=self.request.user)
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)
serializer.save(create_by=self.request.user, belong_dept=self.request.user.dept)
else:
serializer.save(create_by = self.request.user)
serializer.save(create_by=self.request.user)
class CustomUpdateModelMixin(UpdateModelMixin):
def perform_update(self, serializer):
serializer.save(update_by = self.request.user)
serializer.save(update_by=self.request.user)
class CustomDestoryModelMixin(DestroyModelMixin):
def perform_destroy(self, instance):
instance.delete(update_by = self.request.user)
instance.delete(update_by=self.request.user)
class MyLoggingMixin(object):
@ -79,7 +87,7 @@ class MyLoggingMixin(object):
def initial(self, request, *args, **kwargs):
request_id = uuid.uuid4()
self.log = {"requested_at": now(), "id":request_id}
self.log = {"requested_at": now(), "id": request_id}
setattr(request, 'request_id', request_id)
if not getattr(self, "decode_request_body", False):
self.log["data"] = ""
@ -114,7 +122,8 @@ class MyLoggingMixin(object):
)
if should_log(request, response):
if (connection.settings_dict.get("ATOMIC_REQUESTS") and getattr(response, "exception", None) and connection.in_atomic_block):
if (connection.settings_dict.get("ATOMIC_REQUESTS") and
getattr(response, "exception", None) and connection.in_atomic_block):
# response with exception (HTTP status like: 401, 404, etc)
# pointwise disable atomic block for handle log (TransactionManagementError)
connection.set_rollback(True)
@ -277,4 +286,3 @@ class MyLoggingMixin(object):
if key.lower() in SENSITIVE_FIELDS:
data[key] = self.CLEANED_SUBSTITUTE
return data

View File

@ -1,10 +1,7 @@
from copy import copy
import django.utils.timezone as timezone
from django.db import models
from django.db.models.query import QuerySet
from django.core.exceptions import ObjectDoesNotExist
from apps.utils.snowflake import IdWorker, idWorker
from typing import Iterable, Optional
from apps.utils.snowflake import idWorker
# 自定义软删除查询基类
@ -101,9 +98,11 @@ 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')
'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')
'system.user', null=True, blank=True, on_delete=models.SET_NULL,
verbose_name='最后编辑人', related_name='%(class)s_update_by')
class Meta:
abstract = True
@ -114,11 +113,14 @@ 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')
'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')
'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')
'system.dept', null=True, blank=True, on_delete=models.SET_NULL,
verbose_name='所属部门', related_name='%(class)s_belong_dept')
class Meta:
abstract = True
@ -129,9 +131,11 @@ 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')
'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')
'system.user', null=True, blank=True, on_delete=models.SET_NULL,
verbose_name='最后编辑人', related_name='%(class)s_update_by')
class Meta:
abstract = True
@ -142,11 +146,14 @@ 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')
'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')
'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')
'system.organzation', null=True, blank=True, on_delete=models.SET_NULL,
verbose_name='所属部门', related_name='%(class)s_belong_dept')
class Meta:
abstract = True

View File

@ -1,14 +1,17 @@
from rest_framework.pagination import PageNumberPagination
from rest_framework.exceptions import ParseError
class MyPagination(PageNumberPagination):
"""
自定义分页/传入page为0则不分页
"""
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:
def paginate_queryset(self, queryset, request, view=None):
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('单次请求数据量大,请分页获取')
raise ParseError('单次请求数据量大,请分页获取')
return super().paginate_queryset(queryset, request, view=view)

View File

@ -1,26 +1,26 @@
from django.core.cache import cache
from rest_framework.permissions import BasePermission, DjangoModelPermissions
from rest_framework.permissions import BasePermission
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 apps.system.models import Dept, Permission, Post, UserPost
from django.db.models.query import QuerySet
ALL_PERMS = [
]
def get_user_perms_map(user):
"""
获取权限字典,可用redis存取
"""
user_perms_map = {}
if user.is_superuser:
user_perms_map = {'superuser':None}
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: # 岗位下有角色
if i.post.roles: # 岗位下有角色
for perm in Permission.objects.filter(role__perms__in=i.post.roles):
if perm.codes:
for code in perm.codes:
@ -29,7 +29,7 @@ def get_user_perms_map(user):
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}
user_perms_map[code] = {dept_id: i.post.data_range}
cache.set('perms_' + user.id, user_perms_map, 60*60)
return user_perms_map
@ -65,6 +65,7 @@ class RbacPermission(BasePermission):
return False
return False
class RbacDataMixin:
"""
数据权限控权返回的queryset
@ -115,17 +116,16 @@ class RbacDataMixin:
belong_depts = get_child_queryset2(dept.parent)
else:
belong_depts = get_child_queryset2(dept)
queryset = queryset.filter(belong_dept__in = belong_depts)
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)
queryset = queryset.filter(belong_dept__in=belong_depts)
elif data_range == Post.POST_DATA_THISLEVEL:
queryset = queryset.filter(belong_dept = dept)
queryset = queryset.filter(belong_dept=dept)
elif data_range == Post.POST_DATA_THISLEVEL:
queryset = queryset.filter(create_by = user)
queryset = queryset.filter(create_by=user)
new_queryset = new_queryset | queryset
return new_queryset
else:
return queryset.none()
return queryset

View File

@ -1,4 +1,3 @@
from django.db import models
from django.apps import apps
@ -41,6 +40,7 @@ def get_child_queryset(name, pk, hasParent=True):
child_queryset = cls.objects.filter(parent__in=child_queryset)
return queryset
def get_child_queryset2(obj, hasParent=True):
'''
获取所有子集
@ -59,6 +59,7 @@ def get_child_queryset2(obj, hasParent=True):
child_queryset = cls.objects.filter(parent__in=child_queryset)
return queryset
def get_parent_queryset(obj, hasSelf=True):
cls = type(obj)
ids = []

View File

@ -1,5 +1,7 @@
import json
from user_agents import parse
def get_request_ip(request):
"""
获取请求IP
@ -11,6 +13,7 @@ def get_request_ip(request):
ip = request.META.get('REMOTE_ADDR', '') or getattr(request, 'request_ip', None)
return ip or 'unknown'
def get_request_data(request):
"""
获取请求参数
@ -55,6 +58,7 @@ def get_request_path(request, *args, **kwargs):
path = path.replace('/' + value, '/' + '{id}')
return path
def get_browser(request, ):
"""
获取浏览器名

View File

@ -3,27 +3,33 @@ from rest_framework import serializers
from django_restql.mixins import DynamicFieldsMixin
from rest_framework.fields import empty
from rest_framework.request import Request
class PkSerializer(serializers.Serializer):
pks = serializers.ListField(child=serializers.CharField(max_length=20), label="主键ID列表")
class GenSignatureSerializer(serializers.Serializer):
path = serializers.CharField(label="图片地址")
class CustomModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
class CustomModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
"""
自定义serializer/包含创建和新增字段处理
"""
def __init__(self, instance=None, data=empty, request=None, **kwargs):
super().__init__(instance, data, **kwargs)
self.request: Request = request or self.context.get('request', None)
def create(self, validated_data):
if self.request:
if getattr(self.request, 'user', None):
validated_data['create_by'] = self.request.user
if 'belong_dept' in validated_data: # 如果指定了数据归属部门
pass
elif getattr(self.request.user, 'belong_dept', None):
if hasattr(self.Meta.model, 'belong_dept'):
validated_data['belong_dept'] = self.request.user.belong_dept
if getattr(self.request, 'user', None):
validated_data['create_by'] = self.request.user
if 'belong_dept' in validated_data:
pass
elif getattr(self.request.user, 'belong_dept', None):
if hasattr(self.Meta.model, 'belong_dept'):
validated_data['belong_dept'] = self.request.user.belong_dept
return super().create(validated_data)
def update(self, instance, validated_data):

View File

@ -4,12 +4,14 @@
import time
from server.settings import SNOW_DATACENTER_ID, SNOW_WORKER_ID
class InvalidSystemClock(Exception):
"""
时钟回拨异常
"""
pass
class Constant(object):
# 64位ID的划分
WORKER_ID_BITS = 5
@ -17,7 +19,7 @@ class Constant(object):
SEQUENCE_BITS = 12
# 最大取值计算
MAX_WORKER_ID = -1 ^ (-1 << WORKER_ID_BITS) # 2**5-1 0b11111
MAX_WORKER_ID = -1 ^ (-1 << WORKER_ID_BITS) # 2**5-1 0b11111
MAX_DATACENTER_ID = -1 ^ (-1 << DATACENTER_ID_BITS)
# 移位偏移计算
@ -55,7 +57,7 @@ class IdWorker(object):
self.datacenter_id = datacenter_id
self.sequence = sequence
self.last_timestamp = -1 # 上次计算的时间戳
self.last_timestamp = -1 # 上次计算的时间戳
def _gen_timestamp(self):
"""
@ -84,8 +86,9 @@ class IdWorker(object):
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
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):
@ -97,6 +100,7 @@ class IdWorker(object):
timestamp = self._gen_timestamp()
return timestamp
idWorker = IdWorker(SNOW_DATACENTER_ID, SNOW_WORKER_ID)
if __name__ == '__main__':

View File

@ -2,8 +2,9 @@ import textwrap
import random
import string
def print_roundtrip(response, *args, **kwargs):
format_headers = lambda d: '\n'.join(f'{k}: {v}' for k, v in d.items())
def format_headers(d): return '\n'.join(f'{k}: {v}' for k, v in d.items())
print(textwrap.dedent('''
---------------- request ----------------
{req.method} {req.url}
@ -22,10 +23,12 @@ def print_roundtrip(response, *args, **kwargs):
reshdrs=format_headers(response.headers),
))
def ranstr(num):
salt = ''.join(random.sample(string.ascii_lowercase + string.digits, num))
return salt
def rannum(num):
salt = ''.join(random.sample(string.digits, num))
return salt

View File

@ -1,4 +1,4 @@
from rest_framework.views import APIView
import os
import cv2
from apps.utils.errors import SIGN_MAKE_FAIL
@ -10,6 +10,7 @@ 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 = ()

View File

@ -1,24 +1,26 @@
from rest_framework.viewsets import GenericViewSet
from django.core.cache import cache
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.mixins import (CreateModelMixin, ListModelMixin,
RetrieveModelMixin, UpdateModelMixin)
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from apps.system.models import Dept, Post
from apps.utils.errors import PKS_ERROR
from apps.utils.mixins import CustomDestoryModelMixin, MyLoggingMixin
from apps.utils.permission import ALL_PERMS, RbacPermission, get_user_perms_map
from apps.utils.queryset import get_child_queryset2
from apps.utils.serializers import PkSerializer
from rest_framework.response import Response
from rest_framework.mixins import RetrieveModelMixin, ListModelMixin, CreateModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.exceptions import ValidationError
from django.core.cache import cache
class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
"""
增强的GenericViewSet
"""
perms_map = {} # 权限标识
perms_map = {} # 权限标识
logging_methods = ['POST', 'PUT', 'PATCH', 'DELETE']
ordering_fields = '__all__'
filter_fields = '__all__'
@ -32,7 +34,7 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
select_related_fields = []
prefetch_related_fields = []
permission_classes = [IsAuthenticated & RbacPermission]
data_filter = False # 数据权限过滤是否开启(需要RbacPermission)
data_filter = False # 数据权限过滤是否开启(需要RbacPermission)
def get_serializer_class(self):
action_serializer_name = f"{self.action}_serializer_class"
@ -45,7 +47,7 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
super().__init__(*args, **kwargs)
if self.perms_map:
for k, v in self.perms_map.items():
if v not in ALL_PERMS and v!='*':
if v not in ALL_PERMS and v != '*':
ALL_PERMS.append(v)
def get_queryset(self):
@ -81,43 +83,45 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
belong_depts = get_child_queryset2(dept.parent)
else:
belong_depts = get_child_queryset2(dept)
queryset = queryset.filter(belong_dept__in = belong_depts)
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)
queryset = queryset.filter(belong_dept__in=belong_depts)
elif data_range == Post.POST_DATA_THISLEVEL:
queryset = queryset.filter(belong_dept = dept)
queryset = queryset.filter(belong_dept=dept)
elif data_range == Post.POST_DATA_THISLEVEL:
queryset = queryset.filter(create_by = user)
queryset = queryset.filter(create_by=user)
new_queryset = new_queryset | queryset
return new_queryset
else:
return queryset.none()
return queryset
class CustomModelViewSet(CreateModelMixin
, UpdateModelMixin, ListModelMixin, RetrieveModelMixin
, CustomDestoryModelMixin, CustomGenericViewSet):
class CustomModelViewSet(CreateModelMixin, UpdateModelMixin, 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)}
self.perms_map = {'get': '*', 'post': '{}:create'.format(basename), 'put': '{}:update'.format(
basename), 'patch': '{}:update'.format(basename), 'delete': '{}:delete'.format(basename)}
for k, v in self.perms_map.items():
if v not in ALL_PERMS and v!='*':
if v not in ALL_PERMS and v != '*':
ALL_PERMS.append(v)
@action(methods=['post'], detail=False, serializer_class=PkSerializer)
def deletes(self,request,*args,**kwargs):
def deletes(self, request, *args, **kwargs):
"""
批量删除
"""
request_data = request.data
pks = request_data.get('pks',None)
pks = request_data.get('pks', None)
if pks:
self.get_queryset().filter(id__in=pks).delete(update_by=request.user)
return Response()

View File

@ -1,6 +1,5 @@
from threading import Thread
import requests
import json
from apps.utils.errors import XX_REQUEST_ERROR
from apps.utils.tools import print_roundtrip
from django.conf import settings
@ -9,11 +8,11 @@ from rest_framework.exceptions import APIException, ParseError
requests.packages.urllib3.disable_warnings()
class XxClient:
"""
寻息
"""
def __init__(self, licence=settings.XX_LICENCE, username=settings.XX_USERNAME) -> None:
if not settings.XX_ENABLED:
return None
@ -28,7 +27,7 @@ class XxClient:
def _get_token_loop(self):
while self.isRuning:
json = {
'licence':self.licence
'licence': self.licence
}
r = requests.post(json=json, url=settings.XX_BASE_URL + '/getAccessTokenV2', verify=False, timeout=20)
ret = r.json()
@ -39,7 +38,7 @@ class XxClient:
def get_token(self):
self.isGetingToken = True
json = {
'licence':self.licence
'licence': self.licence
}
r = requests.post(json=json, url=settings.XX_BASE_URL + '/getAccessTokenV2', verify=False, timeout=20)
ret = r.json()
@ -48,7 +47,7 @@ class XxClient:
self.token = ret['data']['token']
def setup(self):
self.t = Thread(target= self._get_token_loop, args=(), daemon=True)
self.t = Thread(target=self._get_token_loop, args=(), daemon=True)
self.t.start()
def __del__(self):
@ -58,7 +57,7 @@ class XxClient:
self.isRuning = False
self.t.join()
def request(self, url:str, method:str='post', params=dict(), json=dict(), timeout=4, raise_exception=True):
def request(self, url: str, method: str = 'post', params=dict(), json=dict(), timeout=4, raise_exception=True):
params['accessToken'] = self.token
json['username'] = self.username
json['buildId'] = settings.XX_BUILDID
@ -72,20 +71,20 @@ class XxClient:
if req_num > 4:
break
else:
r = getattr(requests, method)('{}{}'.format(settings.XX_BASE_URL, url)
, params=params, json=json, timeout=timeout, verify=False)
r = getattr(requests, method)('{}{}'.format(settings.XX_BASE_URL, url),
params=params, json=json, timeout=timeout, 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, raise_exception) # 重新请求
self.get_token() # 重新获取token
self.request(url, method, params, json, timeout, raise_exception) # 重新请求
else:
if ret['errorCode'] != 0:
err_detail = dict(detail='寻息错误:' + '|'.join(ret['errorMsg']),
code='xx_' + str(ret['errorCode']))
code='xx_' + str(ret['errorCode']))
if raise_exception:
raise ParseError(**err_detail)
raise ParseError(**err_detail)
return 'fail', err_detail
return 'success', ret['data']
if raise_exception:

View File

@ -1,7 +1,6 @@
from django.apps import AppConfig
class WfConfig(AppConfig):
name = 'apps.wf'
verbose_name = '工作流管理'

View File

@ -1,24 +1,29 @@
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')
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': # 我的
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 == '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:

View File

@ -1,12 +1,7 @@
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, File
from apps.utils.models import SoftModel, BaseModel
from simple_history.models import HistoricalRecords
from apps.system.models import User
from apps.utils.models import BaseModel
class Workflow(CommonAModel):
@ -18,10 +13,15 @@ class Workflow(CommonAModel):
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}')
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):
"""
@ -55,22 +55,22 @@ class State(CommonAModel):
# (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 = 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=(
STATE_FIELD_READONLY = 1 # 字段只读
STATE_FIELD_REQUIRED = 2 # 字段必填
STATE_FIELD_OPTIONAL = 3 # 字段可选
STATE_FIELD_HIDDEN = 4 # 字段隐藏
state_filter_choices = (
(0, ''),
(1, '和工单同属一及上级部门'),
(2, '和创建人同属一及上级部门'),
@ -80,15 +80,22 @@ class State(CommonAModel):
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)')
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.全部处理(要求所有参与人都要处理一遍,才能进入下一步)')
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')
# json格式存储,包括读写属性1只读2必填3可选4不显示, 字典的字典
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:工作流名称')
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):
"""
工作流流转定时器条件(允许跳过) 条件流转与定时器不可同时存在
@ -111,7 +118,7 @@ class Transition(CommonAModel):
TRANSITION_INTERVENE_TYPE_ALTER_STATE = 8 # 强制修改状态操作
TRANSITION_INTERVENE_TYPE_HOOK = 9 # hook操作
TRANSITION_INTERVENE_TYPE_RETREAT = 10 # 撤回
TRANSITION_INTERVENE_TYPE_CC = 11 # 抄送
TRANSITION_INTERVENE_TYPE_CC = 11 # 抄送
intervene_type_choices = (
(0, '正常处理'),
@ -131,11 +138,16 @@ class Transition(CommonAModel):
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='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容')
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):
@ -160,12 +172,15 @@ class CustomField(CommonAModel):
)
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')
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='字段的描述信息,可用于显示在字段的下方对该字段的详细描述')
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,
@ -178,6 +193,7 @@ class CustomField(CommonAModel):
# hook = models.CharField('hook', max_length=1000, default='', help_text='获取下拉选项用于动态选项值')
is_hidden = models.BooleanField('是否隐藏', default=False, help_text='可用于携带不需要用户查看的字段信息')
class Ticket(CommonAModel):
"""
工单
@ -189,7 +205,7 @@ class Ticket(CommonAModel):
TICKET_ACT_STATE_FINISH = 4 # 已完成
TICKET_ACT_STATE_CLOSED = 5 # 已关闭
act_state_choices =(
act_state_choices = (
(TICKET_ACT_STATE_DRAFT, '草稿中'),
(TICKET_ACT_STATE_ONGOING, '进行中'),
(TICKET_ACT_STATE_BACK, '被退回'),
@ -197,7 +213,7 @@ class Ticket(CommonAModel):
(TICKET_ACT_STATE_FINISH, '已完成'),
(TICKET_ACT_STATE_CLOSED, '已关闭')
)
category_choices =(
category_choices = (
('all', '全部'),
('owner', '我创建的'),
('duty', '待办'),
@ -209,12 +225,15 @@ class Ticket(CommonAModel):
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')
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='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效')
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_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格式')
@ -225,13 +244,15 @@ 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)
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_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列表)')

View File

@ -7,50 +7,46 @@ class GetParticipants:
获取处理人脚本
"""
all_funcs = [
{'func':'get_create_by', 'name':'获取工单创建人'}
{'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={}):
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':'处理一些工作'}
{'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=''):
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)
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)
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):
def handle_something(cls, ticket: Ticket):
"""处理一些工作"""
# 任务处理代码区
# 调用自动流转
ticket = cls.to_next(ticket=ticket, by_task=True, script_str= 'handle_something')
ticket = cls.to_next(ticket=ticket, by_task=True, script_str='handle_something')

View File

@ -1,6 +1,5 @@
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
@ -11,33 +10,40 @@ class WorkflowSerializer(serializers.ModelSerializer):
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')
queryset = queryset.select_related('source_state', 'destination_state')
return queryset
class AllField(serializers.Field):
def to_representation(self, value):
return value
@ -45,6 +51,7 @@ class AllField(serializers.Field):
def to_internal_value(self, data):
return data
class FieldChoiceSerializer(serializers.Serializer):
id = AllField(label='ID')
name = serializers.CharField(label='名称')
@ -55,14 +62,16 @@ class CustomFieldSerializer(serializers.ModelSerializer):
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']
'sort', 'default_value', 'description', 'placeholder', 'field_template',
'boolean_field_display', 'field_choice', 'label', 'is_hidden']
class TicketSimpleSerializer(serializers.ModelSerializer):
@ -70,16 +79,19 @@ class TicketSimpleSerializer(serializers.ModelSerializer):
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']
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)
@ -90,33 +102,37 @@ class TicketSerializer(serializers.ModelSerializer):
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('workflow','state')
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']
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')
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')
queryset = queryset.select_related('workflow', 'state')
return queryset
def get_ticket_data_(self, obj):
@ -128,16 +144,18 @@ class TicketDetailSerializer(serializers.ModelSerializer):
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'] # 该字段是用于查看详情直接展示
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)))
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)))
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']:
@ -156,16 +174,20 @@ class TicketDetailSerializer(serializers.ModelSerializer):
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']
@ -174,20 +196,25 @@ class TicketFlowSimpleSerializer(serializers.ModelSerializer):
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)
suggestion = serializers.CharField(label="处理意见", required=False, allow_blank=True)
class TicketRetreatSerializer(serializers.Serializer):
suggestion = serializers.CharField(label="撤回原因", required = False)
suggestion = serializers.CharField(label="撤回原因", required=False)
class TicketCloseSerializer(serializers.Serializer):
suggestion = serializers.CharField(label="关闭原因", required = False)
suggestion = serializers.CharField(label="关闭原因", required=False)
class TicketAddNodeSerializer(serializers.Serializer):
suggestion = serializers.CharField(label="加签说明", required = False)
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)
suggestion = serializers.CharField(label="加签意见", required=False)
class TicketDestorySerializer(serializers.Serializer):
ids = serializers.ListField(child=serializers.PrimaryKeyRelatedField(queryset=Ticket.objects.all()), label='工单ID列表')

View File

@ -1,6 +1,4 @@
from apps.wf.serializers import CustomFieldSerializer
from apps.wf.serializers import TicketSerializer, TicketSimpleSerializer
from typing import Tuple
from apps.wf.serializers import TicketSimpleSerializer
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
@ -10,23 +8,24 @@ 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):
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):
def get_workflow_transitions(workflow: Workflow):
"""
获取工作流流转列表
"""
return Transition.objects.filter(workflow=workflow, is_deleted=False)
@staticmethod
def get_workflow_start_state(workflow:Workflow):
def get_workflow_start_state(workflow: Workflow):
"""
获取工作流初始状态
"""
@ -37,7 +36,7 @@ class WfService(object):
raise APIException('工作流状态配置错误')
@staticmethod
def get_workflow_end_state(workflow:Workflow):
def get_workflow_end_state(workflow: Workflow):
"""
获取工作流结束状态
"""
@ -48,35 +47,36 @@ class WfService(object):
raise APIException('工作流状态配置错误')
@staticmethod
def get_workflow_custom_fields(workflow:Workflow):
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):
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))
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):
def get_ticket_transitions(cls, ticket: Ticket):
"""
获取工单当前状态下可用的流转条件
"""
return cls.get_state_transitions(ticket.state)
@classmethod
def get_state_transitions(cls, state:State):
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):
def get_ticket_steps(cls, ticket: Ticket):
steps = cls.get_worlflow_states(ticket.workflow)
nsteps_list = []
for i in steps:
@ -85,14 +85,14 @@ class WfService(object):
return nsteps_list
@classmethod
def get_ticket_transitions(cls, ticket:Ticket):
def get_ticket_transitions(cls, ticket: Ticket):
"""
获取工单可执行的操作
"""
return cls.get_state_transitions(ticket.state)
@classmethod
def get_transition_by_args(cls, kwargs:dict):
def get_transition_by_args(cls, kwargs: dict):
"""
查询并获取流转
"""
@ -100,41 +100,42 @@ class WfService(object):
return Transition.objects.filter(**kwargs).all()
@classmethod
def get_ticket_sn(cls, workflow:Workflow):
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
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:
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 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}):
import datetime
import 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):
def get_ticket_state_participant_info(cls, state: State,
ticket: Ticket, new_ticket_data: dict = {}, handler: User = None):
"""
获取工单目标状态实际的处理人, 处理人类型
"""
@ -143,29 +144,30 @@ class WfService(object):
回到初始状态
"""
return dict(destination_participant_type=State.PARTICIPANT_TYPE_PERSONAL,
destination_participant=ticket.create_by.id,
multi_all_person={})
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={})
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)
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:#代码获取
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_DEPT: # 部门
destination_participant = list(User.objects.filter(
dept__in=destination_participant).values_list('id', flat=True))
elif destination_participant_type == State.PARTICIPANT_TYPE_ROLE:#角色
elif destination_participant_type == State.PARTICIPANT_TYPE_ROLE: # 角色
user_queryset = User.objects.filter(roles__in=destination_participant)
# 如果选择了角色, 需要走过滤策略
if state.filter_policy == 1:
@ -181,7 +183,7 @@ class WfService(object):
if type(destination_participant) == list:
destination_participant_type = State.PARTICIPANT_TYPE_MULTI
destination_participant = list(set(destination_participant))
if len(destination_participant) == 1: # 如果只有一个人
if len(destination_participant) == 1: # 如果只有一个人
destination_participant_type = State.PARTICIPANT_TYPE_PERSONAL
destination_participant = destination_participant[0]
else:
@ -191,14 +193,14 @@ class WfService(object):
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]={}
multi_all_person_dict[i] = {}
return dict(destination_participant_type=destination_participant_type,
destination_participant=destination_participant,
multi_all_person=multi_all_person_dict)
destination_participant=destination_participant,
multi_all_person=multi_all_person_dict)
@classmethod
def ticket_handle_permission_check(cls, ticket:Ticket, user:User)-> dict:
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="工单当前状态无需操作")
@ -209,7 +211,8 @@ class WfService(object):
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]:
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)
@ -218,7 +221,7 @@ class WfService(object):
pass
else:
return dict(permission=False, msg="非当前处理人", need_accept=False)
elif current_participant_count >1 and state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ACTIVE:
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)
@ -227,7 +230,7 @@ class WfService(object):
return dict(permission=True, msg="", need_accept=False)
@classmethod
def check_dict_has_all_same_value(cls, dict_obj: object)->tuple:
def check_dict_has_all_same_value(cls, dict_obj: object) -> tuple:
"""
check whether all key are equal in a dict
:param dict_obj:
@ -243,7 +246,7 @@ class WfService(object):
return True
@classmethod
def get_ticket_all_field_value(cls, ticket: Ticket)->dict:
def get_ticket_all_field_value(cls, ticket: Ticket) -> dict:
"""
工单所有字段的值
get ticket's all field value
@ -259,14 +262,15 @@ class WfService(object):
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):
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: # 没有处理人意味着系统触发不校验处理权限
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'))
@ -281,10 +285,11 @@ class WfService(object):
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)
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)
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 = {}
@ -321,33 +326,33 @@ class WfService(object):
# 只更新必填和可选的字段
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]
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)
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)
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)
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

View File

@ -1,6 +1,5 @@
from django.db.models import base
from rest_framework import urlpatterns
from apps.wf.views import CustomFieldViewSet, FromCodeListView, StateViewSet, TicketFlowViewSet, TicketViewSet, TransitionViewSet, WorkflowViewSet
from apps.wf.views import CustomFieldViewSet, FromCodeListView, StateViewSet, TicketFlowViewSet, \
TicketViewSet, TransitionViewSet, WorkflowViewSet
from django.urls import path, include
from rest_framework.routers import DefaultRouter
@ -18,4 +17,3 @@ urlpatterns = [
path(API_BASE_URL + 'participant_from_code', FromCodeListView.as_view()),
path(API_BASE_URL, include(router.urls)),
]

View File

@ -1,26 +1,26 @@
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.utils.viewsets import CustomModelViewSet
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.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, \
RetrieveModelMixin, UpdateModelMixin
from apps.wf.serializers import CustomFieldCreateUpdateSerializer, CustomFieldSerializer, StateSerializer, \
TicketAddNodeEndSerializer, TicketAddNodeSerializer, TicketCloseSerializer, \
TicketCreateSerializer, TicketDestorySerializer, TicketFlowSerializer, \
TicketHandleSerializer, TicketRetreatSerializer, \
TicketSerializer, TransitionSerializer, WorkflowSerializer, \
TicketListSerializer, TicketDetailSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.decorators import action, api_view
from rest_framework.decorators import action
from apps.wf.models import CustomField, Ticket, Workflow, State, Transition, TicketFlow
from apps.utils.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin
from apps.wf.services import WfService
from rest_framework.exceptions import APIException, PermissionDenied
from rest_framework.exceptions import APIException
from rest_framework import status
from django.db.models import Count
from .scripts import GetParticipants, HandleScripts
from .scripts import GetParticipants
# Create your views here.
@ -31,6 +31,7 @@ class FromCodeListView(APIView):
"""
return Response(GetParticipants.all_funcs)
class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet):
perms_map = {'get': '*', 'post': 'workflow:create',
'put': 'workflow:update', 'delete': 'workflow:delete'}
@ -41,7 +42,8 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet):
ordering_fields = ['create_time']
ordering = ['-create_time']
@action(methods=['get'], detail=True, perms_map={'get':'workflow:update'}, pagination_class=None, serializer_class=StateSerializer)
@action(methods=['get'], detail=True, perms_map={'get': 'workflow:update'},
pagination_class=None, serializer_class=StateSerializer)
def states(self, request, pk=None):
"""
工作流下的状态节点
@ -50,7 +52,8 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet):
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)
@action(methods=['get'], detail=True, perms_map={'get': 'workflow:update'},
pagination_class=None, serializer_class=TransitionSerializer)
def transitions(self, request, pk=None):
"""
工作流下的流转规则
@ -59,21 +62,23 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet):
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)
@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)
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'})
@action(methods=['get'], detail=True, perms_map={'get': 'workflow:init'})
def init(self, request, pk=None):
"""
新建工单初始化
"""
ret={}
ret = {}
wf = self.get_object()
start_state = WfService.get_workflow_start_state(wf)
transitions = WfService.get_state_transitions(start_state)
@ -88,27 +93,30 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet):
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'}
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'}
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'}
perms_map = {'get': '*', 'post': 'workflow:update',
'put': 'workflow:update', 'delete': 'workflow:update'}
queryset = CustomField.objects.all()
serializer_class = CustomFieldSerializer
search_fields = ['field_name']
@ -120,8 +128,9 @@ class CustomFieldViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin,
return CustomFieldCreateUpdateSerializer
return super().get_serializer_class()
class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet):
perms_map = {'get':'*', 'post':'ticket:create'}
perms_map = {'get': '*', 'post': 'ticket:create'}
queryset = Ticket.objects.all()
serializer_class = TicketSerializer
search_fields = ['title']
@ -154,7 +163,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
rdata = request.data
serializer = self.get_serializer(data=rdata)
serializer.is_valid(raise_exception=True)
vdata = serializer.validated_data #校验之后的数据
vdata = serializer.validated_data # 校验之后的数据
start_state = WfService.get_workflow_start_state(vdata['workflow'])
transition = vdata.pop('transition')
ticket_data = vdata['ticket_data']
@ -171,26 +180,26 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
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) # 先创建出来
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) # 流水号
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)
handler=request.user, created=True)
return Response(TicketSerializer(instance=ticket).data)
@action(methods=['get'], detail=False, perms_map={'get':'*'})
@action(methods=['get'], detail=False, perms_map={'get': '*'})
def duty_agg(self, request, pk=None):
"""
工单待办聚合
@ -199,10 +208,10 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
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')))
ret['details'] = list(queryset.values('workflow', 'workflow__name').annotate(count=Count('workflow')))
return Response(ret)
@action(methods=['post'], detail=True, perms_map={'post':'*'})
@action(methods=['post'], detail=True, perms_map={'post': '*'})
@transaction.atomic
def handle(self, request, pk=None):
"""
@ -216,11 +225,11 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
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'])
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':'*'})
@action(methods=['get'], detail=True, perms_map={'get': '*'})
def flowsteps(self, request, pk=None):
"""
工单流转step, 用于显示当前状态的step图(线性结构)
@ -229,7 +238,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
steps = WfService.get_ticket_steps(ticket)
return Response(StateSerializer(instance=steps, many=True).data)
@action(methods=['get'], detail=True, perms_map={'get':'*'})
@action(methods=['get'], detail=True, perms_map={'get': '*'})
def flowlogs(self, request, pk=None):
"""
工单流转记录
@ -239,7 +248,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
serializer = TicketFlowSerializer(instance=flowlogs, many=True)
return Response(serializer.data)
@action(methods=['get'], detail=True, perms_map={'get':'*'})
@action(methods=['get'], detail=True, perms_map={'get': '*'})
def transitions(self, request, pk=None):
"""
获取工单可执行的操作
@ -248,7 +257,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
transitions = WfService.get_ticket_transitions(ticket)
return Response(TransitionSerializer(instance=transitions, many=True).data)
@action(methods=['post'], detail=True, perms_map={'post':'*'})
@action(methods=['post'], detail=True, perms_map={'post': '*'})
def accpet(self, request, pk=None):
"""
接单,当工单当前处理人实际为多个人时(角色部门多人都有可能 注意角色和部门有可能实际只有一人)
@ -261,14 +270,16 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
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)
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':'*'})
@action(methods=['post'], detail=True, perms_map={'post': '*'})
def retreat(self, request, pk=None):
"""
撤回工单允许创建人在指定状态撤回工单至初始状态状态设置中开启允许撤回
@ -285,13 +296,15 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
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)
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)
@action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=TicketAddNodeSerializer)
def add_node(self, request, pk=None):
"""
加签
@ -305,13 +318,15 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
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)
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)
@action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=TicketAddNodeEndSerializer)
def add_node_end(self, request, pk=None):
"""
加签完成
@ -323,20 +338,22 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
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)
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)
@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:
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
@ -344,15 +361,18 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
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)
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:destorys'}, serializer_class=TicketDestorySerializer)
@action(methods=['post'], detail=False, perms_map={'post': 'ticket:destorys'},
serializer_class=TicketDestorySerializer)
def destorys(self, request, pk=None):
"""
批量物理删除
@ -361,12 +381,11 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
return Response()
class TicketFlowViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
"""
工单日志
"""
perms_map = {'get':'*'}
perms_map = {'get': '*'}
queryset = TicketFlow.objects.all()
serializer_class = TicketFlowSerializer
search_fields = ['suggestion']

View File

@ -217,8 +217,8 @@ CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
# swagger配置
SWAGGER_SETTINGS = {
'LOGIN_URL':'/django/login/',
'LOGOUT_URL':'/django/logout/',
'LOGIN_URL': '/django/login/',
'LOGOUT_URL': '/django/logout/',
}
# 日志配置

View File

@ -23,14 +23,14 @@ 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=[],
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 = [
@ -57,8 +57,7 @@ urlpatterns = [
path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
# 前端页面入口
path('',TemplateView.as_view(template_name="index.html")),
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)
static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + \
static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)