diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/auth1/apps.py b/apps/auth1/apps.py index 74a0184f..c7782e0a 100755 --- a/apps/auth1/apps.py +++ b/apps/auth1/apps.py @@ -4,4 +4,3 @@ from django.apps import AppConfig class AuthConfig(AppConfig): name = 'apps.auth1' verbose_name = "认证" - diff --git a/apps/auth1/errors.py b/apps/auth1/errors.py index 7d2f78f8..58ec2be7 100755 --- a/apps/auth1/errors.py +++ b/apps/auth1/errors.py @@ -1,2 +1,2 @@ -USERNAME_OR_PASSWORD_WRONG = {"code":"username_or_password_wrong", "detail":"账户名或密码错误"} \ No newline at end of file +USERNAME_OR_PASSWORD_WRONG = {"code": "username_or_password_wrong", "detail": "账户名或密码错误"} diff --git a/apps/auth1/serializers.py b/apps/auth1/serializers.py index f318bee0..a488f133 100755 --- a/apps/auth1/serializers.py +++ b/apps/auth1/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers + class LoginSerializer(serializers.Serializer): username = serializers.CharField(label="用户名") - password = serializers.CharField(label="密码") \ No newline at end of file + password = serializers.CharField(label="密码") diff --git a/apps/auth1/urls.py b/apps/auth1/urls.py index 7a7cf1cb..22c06a89 100755 --- a/apps/auth1/urls.py +++ b/apps/auth1/urls.py @@ -11,4 +11,4 @@ urlpatterns = [ path(API_BASE_URL + 'token/black/', TokenBlackView.as_view(), name='token_black'), path(API_BASE_URL + 'login/', LoginView.as_view(), name='session_login'), path(API_BASE_URL + 'logout/', LogoutView.as_view(), name='session_logout') -] \ No newline at end of file +] diff --git a/apps/auth1/views.py b/apps/auth1/views.py index 0b70f055..7d9b85ec 100755 --- a/apps/auth1/views.py +++ b/apps/auth1/views.py @@ -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] @@ -25,7 +25,8 @@ class TokenBlackView(APIView): Token拉黑 """ return Response(status=status.HTTP_200_OK) - + + class LoginView(CreateAPIView): """ Session登录 @@ -36,22 +37,23 @@ class LoginView(CreateAPIView): authentication_classes = [] permission_classes = [] serializer_class = LoginSerializer - + def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) vdata = serializer.validated_data - user = authenticate(username = vdata.get('username'), - password = vdata.get('password')) + 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 = [] - + def post(self, request, *args, **kwargs): """ 退出登录 @@ -60,4 +62,4 @@ class LogoutView(APIView): 退出登录 """ logout(request) - return Response() \ No newline at end of file + return Response() diff --git a/apps/develop/tasks.py b/apps/develop/tasks.py index 43d9e113..c4610ae8 100755 --- a/apps/develop/tasks.py +++ b/apps/develop/tasks.py @@ -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 - - \ No newline at end of file diff --git a/apps/develop/urls.py b/apps/develop/urls.py index 90caa4e4..5f7d21db 100755 --- a/apps/develop/urls.py +++ b/apps/develop/urls.py @@ -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()) ] - diff --git a/apps/develop/views.py b/apps/develop/views.py index 427672a4..83262db3 100755 --- a/apps/develop/views.py +++ b/apps/develop/views.py @@ -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): """ 备份资源到指定位置 @@ -64,4 +67,4 @@ class BackupMedia(APIView): if completed.returncode == 0: return Response() else: - raise APIException(completed.stdout) \ No newline at end of file + raise APIException(completed.stdout) diff --git a/apps/hrm/apps.py b/apps/hrm/apps.py index 852f1e7f..5128c915 100755 --- a/apps/hrm/apps.py +++ b/apps/hrm/apps.py @@ -1,9 +1,9 @@ from django.apps import AppConfig + class HrmConfig(AppConfig): name = 'apps.hrm' verbose_name = '人力资源管理' def ready(self): import apps.hrm.signals - diff --git a/apps/hrm/errors.py b/apps/hrm/errors.py index 81df79ab..53e874e8 100755 --- a/apps/hrm/errors.py +++ b/apps/hrm/errors.py @@ -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": "大华照片人脸提取失败"} diff --git a/apps/hrm/filters.py b/apps/hrm/filters.py index c377d119..f8e9dd66 100755 --- a/apps/hrm/filters.py +++ b/apps/hrm/filters.py @@ -1,21 +1,24 @@ 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'] def filter_year(self, queryset, name, value): return queryset.filter(create_time_date__year=value) - + def filter_month(self, queryset, name, value): return queryset.filter(create_time_date__month=value) + class EmployeeFilterSet(filters.FilterSet): class Meta: @@ -26,12 +29,13 @@ 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'] def filter_year(self, queryset, name, value): return queryset.filter(not_work_date__year=value) - + def filter_month(self, queryset, name, value): - return queryset.filter(not_work_date__month=value) \ No newline at end of file + return queryset.filter(not_work_date__month=value) diff --git a/apps/hrm/models.py b/apps/hrm/models.py index 7ca25190..1f7fa7b6 100755 --- a/apps/hrm/models.py +++ b/apps/hrm/models.py @@ -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) + user = models.OneToOneField(User, + 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 @@ -51,8 +49,7 @@ class Employee(CommonBModel): # """ # CARD_FACE = 10 # 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) - - diff --git a/apps/hrm/serializers.py b/apps/hrm/serializers.py index 2538e343..4286d1f2 100755 --- a/apps/hrm/serializers.py +++ b/apps/hrm/serializers.py @@ -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 }] } ) @@ -90,24 +90,24 @@ class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer): cardNumber = instance.id[:8] + rannum(2) 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") + endDate = (datetime(year=now.year+50, + 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 - + @transaction.atomic def update(self, instance, validated_data): old_photo = instance.photo @@ -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 }] } ) @@ -164,15 +164,15 @@ class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer): cardNumber = instance.id[3:8] + rannum(5) 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") + endDate = (datetime(year=now.year+50, + 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,32 +181,36 @@ 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 - fields = '__all__' \ No newline at end of file + fields = '__all__' diff --git a/apps/hrm/signals.py b/apps/hrm/signals.py index 65a6b89b..6c1013b0 100755 --- a/apps/hrm/signals.py +++ b/apps/hrm/signals.py @@ -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() - diff --git a/apps/hrm/tasks.py b/apps/hrm/tasks.py index 4fb60923..0b6103af 100755 --- a/apps/hrm/tasks.py +++ b/apps/hrm/tasks.py @@ -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) - - \ No newline at end of file + Employee.objects.all().update(is_atwork=False, last_check_time=None, not_work_remark=None) diff --git a/apps/hrm/urls.py b/apps/hrm/urls.py index d123fd41..dabe4d1f 100755 --- a/apps/hrm/urls.py +++ b/apps/hrm/urls.py @@ -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)), ] - diff --git a/apps/hrm/views.py b/apps/hrm/views.py index 197bf7b7..7dcbf693 100755 --- a/apps/hrm/views.py +++ b/apps/hrm/views.py @@ -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() @@ -178,13 +170,13 @@ class ClockRecordViewSet(ListModelMixin, CustomGenericViewSet): serializer_class = ClockRecordListSerializer filterset_class = ClockRecordFilterSet ordering = ['-pk'] - + class NotWorkRemarkViewSet(ListModelMixin, CustomGenericViewSet): """ 离岗说明 """ - perms_map = {'get':'*'} + perms_map = {'get': '*'} queryset = NotWorkRemark.objects.all() select_related_fields = ['user'] serializer_class = NotWorkRemarkListSerializer diff --git a/apps/monitor/consumers.py b/apps/monitor/consumers.py index a84df0cc..8b69ce28 100755 --- a/apps/monitor/consumers.py +++ b/apps/monitor/consumers.py @@ -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'] @@ -43,4 +43,4 @@ class MonitorConsumer(AsyncWebsocketConsumer): # Send message to WebSocket await self.send(text_data=json.dumps({ 'message': message - })) \ No newline at end of file + })) diff --git a/apps/monitor/errors.py b/apps/monitor/errors.py index 49abfed6..3bd94a2e 100755 --- a/apps/monitor/errors.py +++ b/apps/monitor/errors.py @@ -1 +1 @@ -LOG_NOT_FONED = {"code":"log_not_found", "detail":"日志不存在"} \ No newline at end of file +LOG_NOT_FONED = {"code": "log_not_found", "detail": "日志不存在"} diff --git a/apps/monitor/models.py b/apps/monitor/models.py index 057a1f8c..a3d8c30e 100755 --- a/apps/monitor/models.py +++ b/apps/monitor/models.py @@ -3,6 +3,7 @@ from django.db import models from apps.utils.models import BaseModel + class DrfRequestLog(BaseModel): """Logs Django rest framework API requests""" diff --git a/apps/monitor/routing.py b/apps/monitor/routing.py index 5a232d7b..777975d5 100755 --- a/apps/monitor/routing.py +++ b/apps/monitor/routing.py @@ -5,4 +5,4 @@ WS_BASE_URL = 'ws/monitor/' websocket_urlpatterns = [ path(WS_BASE_URL + '/', consumers.MonitorConsumer.as_asgi()) -] \ No newline at end of file +] diff --git a/apps/monitor/urls.py b/apps/monitor/urls.py index fbe38d2f..a43d42a1 100755 --- a/apps/monitor/urls.py +++ b/apps/monitor/urls.py @@ -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/' @@ -9,9 +8,9 @@ urlpatterns = [ path(HTML_BASE_URL + 'index/', index), path(HTML_BASE_URL + 'video/', video), path(HTML_BASE_URL + '/', room, name='room'), - + path(API_BASE_URL + 'log/', LogView.as_view()), path(API_BASE_URL + 'log//', 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') ] diff --git a/apps/monitor/views.py b/apps/monitor/views.py index fcfadd8e..df636ded 100755 --- a/apps/monitor/views.py +++ b/apps/monitor/views.py @@ -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): """ 获取服务器当前状态 @@ -78,7 +79,7 @@ class LogView(APIView): def get(self, request, *args, **kwargs): """ 查看最近的日志列表 - + 查看最近的日志列表 """ logs = [] @@ -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'] - - diff --git a/apps/system/admin.py b/apps/system/admin.py index 19fbdabd..5a5edbed 100755 --- a/apps/system/admin.py +++ b/apps/system/admin.py @@ -8,4 +8,4 @@ admin.site.register(Role) admin.site.register(Permission) admin.site.register(DictType) admin.site.register(Dictionary, SimpleHistoryAdmin) -admin.site.register(File) \ No newline at end of file +admin.site.register(File) diff --git a/apps/system/apps.py b/apps/system/apps.py index dfd43d16..7ef85c89 100755 --- a/apps/system/apps.py +++ b/apps/system/apps.py @@ -6,4 +6,4 @@ class SystemConfig(AppConfig): verbose_name = '系统管理' def ready(self): - import apps.system.signals \ No newline at end of file + import apps.system.signals diff --git a/apps/system/errors.py b/apps/system/errors.py index bfac3206..afdbbd7a 100755 --- a/apps/system/errors.py +++ b/apps/system/errors.py @@ -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":"角色标识已存在"} \ No newline at end of file +USERNAME_EXIST = {"code": "username_exist", "detail": "账户已存在"} +ROLE_NAME_EXIST = {"code": "role_name_exist", "detail": "角色名已存在"} +ROLE_CODE_EXIST = {"code": "role_code_exist", "detail": "角色标识已存在"} diff --git a/apps/system/serializers.py b/apps/system/serializers.py index b7ec20c3..3414a6d9 100755 --- a/apps/system/serializers.py +++ b/apps/system/serializers.py @@ -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): """ 用户编辑序列化 @@ -272,16 +283,14 @@ class UserUpdateSerializer(CustomModelSerializer): class Meta: model = User fields = ['username', 'name', 'avatar', 'is_active'] - + 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): """ 创建用户序列化 @@ -291,7 +300,6 @@ class UserCreateSerializer(CustomModelSerializer): class Meta: model = User fields = ['username', 'name', 'avatar', 'is_active'] - class PasswordChangeSerializer(serializers.Serializer): @@ -299,6 +307,7 @@ class PasswordChangeSerializer(serializers.Serializer): 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 diff --git a/apps/system/tasks.py b/apps/system/tasks.py index 6d2adaec..3b23e547 100755 --- a/apps/system/tasks.py +++ b/apps/system/tasks.py @@ -6,4 +6,4 @@ from celery import shared_task @shared_task def show(): - print('ok') \ No newline at end of file + print('ok') diff --git a/apps/system/urls.py b/apps/system/urls.py index b5fe81a6..6d0bf054 100755 --- a/apps/system/urls.py +++ b/apps/system/urls.py @@ -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/' diff --git a/apps/system/views.py b/apps/system/views.py index 1ec5758e..54177998 100755 --- a/apps/system/views.py +++ b/apps/system/views.py @@ -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 @@ -311,9 +312,9 @@ class UserViewSet(CustomModelViewSet): serializer.save(password=password, belong_dept=None) return Response(data=serializer.data) - @action(methods=['put'], detail=False, - permission_classes=[IsAuthenticated], - serializer_class = PasswordChangeSerializer) + @action(methods=['put'], detail=False, + permission_classes=[IsAuthenticated], + serializer_class=PasswordChangeSerializer) def password(self, request, pk=None): """修改密码 diff --git a/apps/third/clients.py b/apps/third/clients.py index a282ce62..ad5dd6f2 100755 --- a/apps/third/clients.py +++ b/apps/third/clients.py @@ -2,4 +2,4 @@ from apps.utils.xunxi import XxClient from apps.utils.dahua import DhClient dhClient = DhClient() -xxClient = XxClient() \ No newline at end of file +xxClient = XxClient() diff --git a/apps/third/erros.py b/apps/third/erros.py index f60c82a0..c58b5d49 100755 --- a/apps/third/erros.py +++ b/apps/third/erros.py @@ -1 +1 @@ -TAPI_CODE_WRONG = {"code":"tapi_code_wrong", "detail":"三方接口请求标识有误"} \ No newline at end of file +TAPI_CODE_WRONG = {"code": "tapi_code_wrong", "detail": "三方接口请求标识有误"} diff --git a/apps/third/serializers.py b/apps/third/serializers.py index c76827e5..e4585089 100755 --- a/apps/third/serializers.py +++ b/apps/third/serializers.py @@ -3,13 +3,13 @@ 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) params = serializers.JSONField(label='请求参数', required=False, allow_null=True) json = serializers.JSONField(label='请求body(json格式)', required=False, allow_null=True) - code = serializers.CharField(label='请求短标识', required=False, allow_null=True) \ No newline at end of file + code = serializers.CharField(label='请求短标识', required=False, allow_null=True) diff --git a/apps/third/tapis.py b/apps/third/tapis.py index e4a2b3ac..3d644c0b 100755 --- a/apps/third/tapis.py +++ b/apps/third/tapis.py @@ -1,92 +1,92 @@ # 大华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" } } # 寻息API接口 xxapis = { -} \ No newline at end of file +} diff --git a/apps/third/urls.py b/apps/third/urls.py index bc7492ec..fa2a1548 100755 --- a/apps/third/urls.py +++ b/apps/third/urls.py @@ -13,4 +13,4 @@ urlpatterns = [ path(API_BASE_URL, include(router.urls)), path(API_BASE_URL + 'dahua/test/', DahuaTestView.as_view()), path(API_BASE_URL + 'xunxi/test/', XxTestView.as_view()), -] \ No newline at end of file +] diff --git a/apps/third/views.py b/apps/third/views.py index 7107555a..aa61ad09 100755 --- a/apps/third/views.py +++ b/apps/third/views.py @@ -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): """ 寻息通用调用接口 @@ -167,7 +169,7 @@ class DhCommonViewSet(CreateModelMixin, CustomGenericViewSet): } dhClient.request(**dhapis['mq_subscribe'], json=json_data) return Response() - + @action(methods=['delete'], detail=False, permission_classes=[IsAdminUser], serializer_class=serializers.Serializer) def unsubscribe(self, request, pk=None): @@ -175,10 +177,10 @@ 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, + @action(methods=['post'], detail=False, authentication_classes=[], permission_classes=[], serializer_class=serializers.Serializer) def mq(self, request, pk=None): @@ -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': @@ -198,4 +200,4 @@ class DhCommonViewSet(CreateModelMixin, CustomGenericViewSet): """ 刷卡事件 """ - return Response() \ No newline at end of file + return Response() diff --git a/apps/utils/constants.py b/apps/utils/constants.py index de5fb199..c086c97a 100755 --- a/apps/utils/constants.py +++ b/apps/utils/constants.py @@ -1,4 +1,4 @@ EXCLUDE_FIELDS_BASE = ['create_time', 'update_time', 'is_deleted'] -EXCLUDE_FIELDS = ['create_time', 'update_time', 'is_deleted', 'create_by', 'update_by'] \ No newline at end of file +EXCLUDE_FIELDS = ['create_time', 'update_time', 'is_deleted', 'create_by', 'update_by'] diff --git a/apps/utils/dahua.py b/apps/utils/dahua.py index f1ddd3f5..cad9f141 100755 --- a/apps/utils/dahua.py +++ b/apps/utils/dahua.py @@ -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: @@ -75,16 +77,17 @@ class DhClient: self.request(url, method, params, json, timeout, file_path_rela, raise_exception) req_num = req_num + 1 if req_num > 4: - break + 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() \ No newline at end of file + +dhClient = DhClient() diff --git a/apps/utils/errors.py b/apps/utils/errors.py index cb160605..64235130 100755 --- a/apps/utils/errors.py +++ b/apps/utils/errors.py @@ -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":"未获取到主键列表"} \ No newline at end of file +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": "未获取到主键列表"} diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index 249d45c5..66d5b085 100755 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -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) \ No newline at end of file + 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) diff --git a/apps/utils/mixins.py b/apps/utils/mixins.py index eeeea018..419816fe 100755 --- a/apps/utils/mixins.py +++ b/apps/utils/mixins.py @@ -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) @@ -210,7 +219,7 @@ class MyLoggingMixin(object): if user.is_anonymous: return None return user - + def _get_agent(self, request): """Get os string""" return str(parse(request.META['HTTP_USER_AGENT'])) @@ -277,4 +286,3 @@ class MyLoggingMixin(object): if key.lower() in SENSITIVE_FIELDS: data[key] = self.CLEANED_SUBSTITUTE return data - diff --git a/apps/utils/models.py b/apps/utils/models.py index f2b3ec7d..36a61a6c 100755 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -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 # 自定义软删除查询基类 @@ -67,7 +64,7 @@ class BaseModel(models.Model): class Meta: abstract = True - + def save(self, *args, **kwargs) -> None: if not self.id: self.id = idWorker.get_id() @@ -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 diff --git a/apps/utils/pagination.py b/apps/utils/pagination.py index 3c6d530a..88dcf151 100755 --- a/apps/utils/pagination.py +++ b/apps/utils/pagination.py @@ -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('单次请求数据量大,请分页获取') - return super().paginate_queryset(queryset, request, view=view) \ No newline at end of file + raise ParseError('单次请求数据量大,请分页获取') + return super().paginate_queryset(queryset, request, view=view) diff --git a/apps/utils/permission.py b/apps/utils/permission.py index 7ce672be..c3ea74f1 100755 --- a/apps/utils/permission.py +++ b/apps/utils/permission.py @@ -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 @@ -79,7 +80,7 @@ class RbacDataMixin: "or override the `get_queryset()` method." % self.__class__.__name__ ) - + queryset = self.queryset if isinstance(queryset, QuerySet): # Ensure queryset is re-evaluated on each request. @@ -87,7 +88,7 @@ class RbacDataMixin: if hasattr(self.get_serializer_class(), 'setup_eager_loading'): queryset = self.get_serializer_class().setup_eager_loading(queryset) # 性能优化 - + if self.request.user.is_superuser: return queryset @@ -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 - diff --git a/apps/utils/queryset.py b/apps/utils/queryset.py index 73a6bb12..612d486c 100755 --- a/apps/utils/queryset.py +++ b/apps/utils/queryset.py @@ -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 = [] @@ -67,4 +68,4 @@ def get_parent_queryset(obj, hasSelf=True): while obj.parent: obj = obj.parent ids.append(obj.id) - return cls.objects.filter(id__in=ids) \ No newline at end of file + return cls.objects.filter(id__in=ids) diff --git a/apps/utils/request.py b/apps/utils/request.py index 8591cbbe..8e7d3656 100755 --- a/apps/utils/request.py +++ b/apps/utils/request.py @@ -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, ): """ 获取浏览器名 diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index 54003345..4ea620da 100755 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -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): - + """ + 自定义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): diff --git a/apps/utils/snowflake.py b/apps/utils/snowflake.py index 8b6f89f6..5ab55b47 100755 --- a/apps/utils/snowflake.py +++ b/apps/utils/snowflake.py @@ -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,7 +100,8 @@ class IdWorker(object): timestamp = self._gen_timestamp() return timestamp + idWorker = IdWorker(SNOW_DATACENTER_ID, SNOW_WORKER_ID) if __name__ == '__main__': - print(idWorker.get_id()) \ No newline at end of file + print(idWorker.get_id()) diff --git a/apps/utils/tools.py b/apps/utils/tools.py index 18db1b17..ced0caf5 100755 --- a/apps/utils/tools.py +++ b/apps/utils/tools.py @@ -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} @@ -16,16 +17,18 @@ def print_roundtrip(response, *args, **kwargs): {res.text} ''').format( - req=response.request, - res=response, - reqhdrs=format_headers(response.request.headers), - reshdrs=format_headers(response.headers), + req=response.request, + res=response, + reqhdrs=format_headers(response.request.headers), + 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 \ No newline at end of file + return salt diff --git a/apps/utils/urls.py b/apps/utils/urls.py index 5a03b06a..325c391f 100755 --- a/apps/utils/urls.py +++ b/apps/utils/urls.py @@ -8,4 +8,4 @@ router.register('signature', SignatureViewSet, basename='signature') urlpatterns = [ path(API_BASE_URL, include(router.urls)), -] \ No newline at end of file +] diff --git a/apps/utils/views.py b/apps/utils/views.py index 1739977b..db4b3dd4 100755 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -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 = () diff --git a/apps/utils/viewsets.py b/apps/utils/viewsets.py index af696e07..63258493 100755 --- a/apps/utils/viewsets.py +++ b/apps/utils/viewsets.py @@ -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,9 +47,9 @@ 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): queryset = super().get_queryset() if self.select_related_fields: @@ -81,45 +83,47 @@ 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() else: - raise ValidationError(**PKS_ERROR) \ No newline at end of file + raise ValidationError(**PKS_ERROR) diff --git a/apps/utils/xunxi.py b/apps/utils/xunxi.py index 9c0fadfa..1e29d8f0 100755 --- a/apps/utils/xunxi.py +++ b/apps/utils/xunxi.py @@ -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,9 +47,9 @@ 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,22 +71,22 @@ 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'])) + err_detail = dict(detail='寻息错误:' + '|'.join(ret['errorMsg']), + 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: raise APIException(**XX_REQUEST_ERROR) - return 'error', XX_REQUEST_ERROR \ No newline at end of file + return 'error', XX_REQUEST_ERROR diff --git a/apps/wf/apps.py b/apps/wf/apps.py index f0709ed4..cefdb021 100755 --- a/apps/wf/apps.py +++ b/apps/wf/apps.py @@ -1,7 +1,6 @@ from django.apps import AppConfig + class WfConfig(AppConfig): name = 'apps.wf' verbose_name = '工作流管理' - - diff --git a/apps/wf/filters.py b/apps/wf/filters.py index 02c90661..76cd7b1e 100755 --- a/apps/wf/filters.py +++ b/apps/wf/filters.py @@ -1,26 +1,31 @@ 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: queryset = queryset.none() - return queryset \ No newline at end of file + return queryset diff --git a/apps/wf/models.py b/apps/wf/models.py index fd059949..4b7871db 100755 --- a/apps/wf/models.py +++ b/apps/wf/models.py @@ -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): @@ -159,25 +171,29 @@ class CustomField(CommonAModel): ('file', '附件') ) workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流') - field_type = models.CharField('类型', max_length=50, choices=field_type_choices, - help_text='string, int, float, date, datetime, radio, checkbox, select, selects, cascader, cascaders, select_dg, select_dgs,textarea, file') + field_type = models.CharField('类型', max_length=50, choices=field_type_choices, + help_text='string, int, float, date, datetime, radio, checkbox, select, selects, cascader, cascaders, select_dg, select_dgs,textarea, file') field_key = models.CharField('字段标识', max_length=50, help_text='字段类型请尽量特殊,避免与系统中关键字冲突') field_name = models.CharField('字段名称', max_length=50) - sort = models.IntegerField('排序', default=0, help_text='工单基础字段在表单中排序为:流水号0,标题20,状态id40,状态名41,创建人80,创建时间100,更新时间120.前端展示工单信息的表单可以根据这个id顺序排列') - default_value = models.CharField('默认值', null=True, blank=True, max_length=100, help_text='前端展示时,可以将此内容作为表单中的该字段的默认值') - description = models.CharField('描述', max_length=100, blank=True, null=True, help_text='字段的描述信息,可用于显示在字段的下方对该字段的详细描述') + sort = models.IntegerField( + '排序', default=0, help_text='工单基础字段在表单中排序为:流水号0,标题20,状态id40,状态名41,创建人80,创建时间100,更新时间120.前端展示工单信息的表单可以根据这个id顺序排列') + default_value = models.CharField('默认值', null=True, blank=True, max_length=100, + help_text='前端展示时,可以将此内容作为表单中的该字段的默认值') + description = models.CharField('描述', max_length=100, blank=True, null=True, + help_text='字段的描述信息,可用于显示在字段的下方对该字段的详细描述') placeholder = models.CharField('占位符', max_length=100, blank=True, null=True, help_text='用户工单详情表单中作为字段的占位符显示') field_template = models.TextField('文本域模板', null=True, blank=True, help_text='文本域类型字段前端显示时可以将此内容作为字段的placeholder') boolean_field_display = models.JSONField('布尔类型显示名', default=dict, blank=True, help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"是","0":"否"}或{"1":"需要","0":"不需要"},注意数字也需要引号') - + field_choice = models.JSONField('选项值', default=list, blank=True, help_text='选项值,格式为list, 例["id":1, "name":"张三"]') - + label = models.CharField('标签', max_length=1000, default='', help_text='处理特殊逻辑使用,比如sys_user用于获取用户作为选项') # hook = models.CharField('hook', max_length=1000, default='', help_text='获取下拉选项用于动态选项值') is_hidden = models.BooleanField('是否隐藏', default=False, help_text='可用于携带不需要用户查看的字段信息') + class Ticket(CommonAModel): """ 工单 @@ -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列表)') - diff --git a/apps/wf/scripts.py b/apps/wf/scripts.py index eda2694b..bc48d1b8 100755 --- a/apps/wf/scripts.py +++ b/apps/wf/scripts.py @@ -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') diff --git a/apps/wf/serializers.py b/apps/wf/serializers.py index e425284b..be930900 100755 --- a/apps/wf/serializers.py +++ b/apps/wf/serializers.py @@ -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,31 +62,36 @@ 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'] - + fields = ['workflow', 'field_type', 'field_key', 'field_name', + 'sort', 'default_value', 'description', 'placeholder', 'field_template', + 'boolean_field_display', 'field_choice', 'label', 'is_hidden'] + class TicketSimpleSerializer(serializers.ModelSerializer): class Meta: model = Ticket fields = '__all__' + class TicketCreateSerializer(serializers.ModelSerializer): transition = serializers.PrimaryKeyRelatedField(queryset=Transition.objects.all(), write_only=True) title = serializers.CharField(allow_blank=True, required=False) + class Meta: - model=Ticket - fields=['title','workflow', 'ticket_data', 'transition'] - + 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) @@ -87,36 +99,40 @@ class TicketSerializer(serializers.ModelSerializer): 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 + 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']: @@ -154,40 +172,49 @@ class TicketDetailSerializer(serializers.ModelSerializer): def filter_display(self, item, field_value): if item['id'] == field_value: - return + return + class TicketFlowSerializer(serializers.ModelSerializer): participant_ = UserSimpleSerializer(source='participant', read_only=True) state_ = StateSimpleSerializer(source='state', read_only=True) + class Meta: model = TicketFlow fields = '__all__' + class TicketFlowSimpleSerializer(serializers.ModelSerializer): participant_ = UserSimpleSerializer(source='participant', read_only=True) state_ = StateSimpleSerializer(source='state', read_only=True) + class Meta: model = TicketFlow exclude = ['ticket_data'] - + class TicketHandleSerializer(serializers.Serializer): transition = serializers.PrimaryKeyRelatedField(queryset=Transition.objects.all(), label="流转id") ticket_data = serializers.JSONField(label="表单数据json") - suggestion = serializers.CharField(label="处理意见", required = False, allow_blank=True) + 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列表') \ No newline at end of file + ids = serializers.ListField(child=serializers.PrimaryKeyRelatedField(queryset=Ticket.objects.all()), label='工单ID列表') diff --git a/apps/wf/services.py b/apps/wf/services.py index 8dad0a6b..a4538f5b 100755 --- a/apps/wf/services.py +++ b/apps/wf/services.py @@ -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): + @classmethod + def get_state_transitions(cls, state: State): """ 获取状态可执行的操作 """ return Transition.objects.filter(is_deleted=False, source_state=state).all() @classmethod - def get_ticket_steps(cls, ticket:Ticket): + 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: @@ -241,9 +244,9 @@ class WfService(object): if value_0 != value: return False 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,21 +262,22 @@ 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')) # 校验表单必填项目 if transition.field_require_check or not created: - for key, value in ticket.state.state_fields.items(): + for key, value in ticket.state.state_fields.items(): if int(value) == State.STATE_FIELD_REQUIRED: if key not in new_ticket_data or not new_ticket_data[key]: raise ValidationError('字段{}必填'.format(key)) @@ -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 = {} @@ -320,34 +325,34 @@ 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] + for key, value in source_state.state_fields.items(): + if value in (State.STATE_FIELD_REQUIRED, State.STATE_FIELD_OPTIONAL): + if key in new_ticket_data: + source_ticket_data[key] = new_ticket_data[key] ticket.ticket_data = source_ticket_data ticket.save() # 更新工单流转记录 if not by_task: - TicketFlow.objects.create(ticket=ticket, state=source_state, ticket_data=WfService.get_ticket_all_field_value(ticket), - suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL, - participant=handler, transition=transition) + 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) + TicketFlow.objects.create(ticket=ticket, state=source_state, + participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC, + participant=None, participant_cc=source_state.participant_cc) # 目标状态需要抄送 if destination_state.participant_cc: - TicketFlow.objects.create(ticket=ticket, state=destination_state, - participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC, - participant=None, participant_cc=destination_state.participant_cc) - + TicketFlow.objects.create(ticket=ticket, state=destination_state, + participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC, + participant=None, participant_cc=destination_state.participant_cc) + # 如果目标状态是脚本则执行 if destination_state.participant_type == State.PARTICIPANT_TYPE_ROBOT: getattr(HandleScripts, destination_state.participant)(ticket) - - return ticket + return ticket diff --git a/apps/wf/urls.py b/apps/wf/urls.py index a631443d..e5851648 100755 --- a/apps/wf/urls.py +++ b/apps/wf/urls.py @@ -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)), ] - diff --git a/apps/wf/views.py b/apps/wf/views.py index d1df1a35..8de134be 100755 --- a/apps/wf/views.py +++ b/apps/wf/views.py @@ -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'} @@ -39,9 +40,10 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet): search_fields = ['name', 'description'] filterset_fields = [] ordering_fields = ['create_time'] - ordering = ['-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): """ 工作流下的状态节点 @@ -49,8 +51,9 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet): wf = self.get_object() serializer = self.serializer_class(instance=WfService.get_worlflow_states(wf), many=True) return Response(serializer.data) - - @action(methods=['get'], detail=True, perms_map={'get':'workflow:update'}, pagination_class=None, serializer_class=TransitionSerializer) + + @action(methods=['get'], detail=True, perms_map={'get': 'workflow:update'}, + pagination_class=None, serializer_class=TransitionSerializer) def transitions(self, request, pk=None): """ 工作流下的流转规则 @@ -58,22 +61,24 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet): wf = self.get_object() serializer = self.serializer_class(instance=WfService.get_workflow_transitions(wf), many=True) return Response(serializer.data) - - @action(methods=['get'], detail=True, perms_map={'get':'workflow:update'}, pagination_class=None, serializer_class=CustomFieldSerializer) + + @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'] @@ -140,7 +149,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R elif self.action == 'retrieve': return TicketDetailSerializer return super().get_serializer_class() - + def filter_queryset(self, queryset): if not self.detail and not self.request.query_params.get('category', None): raise APIException('请指定查询分类') @@ -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'] @@ -162,7 +171,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R save_ticket_data = {} # 校验必填项 if transition.field_require_check: - for key, value in start_state.state_fields.items(): + for key, value in start_state.state_fields.items(): if int(value) == State.STATE_FIELD_REQUIRED: if key not in ticket_data and not ticket_data[key]: raise APIException('字段{}必填'.format(key)) @@ -170,27 +179,27 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R elif int(value) == State.STATE_FIELD_OPTIONAL: save_ticket_data[key] = ticket_data[key] - ticket = serializer.save(state=start_state, - create_by=request.user, - create_time=timezone.now(), - act_state=Ticket.TICKET_ACT_STATE_DRAFT, - belong_dept=request.user.dept, - ticket_data=save_ticket_data) # 先创建出来 + ticket = serializer.save(state=start_state, + create_by=request.user, + create_time=timezone.now(), + act_state=Ticket.TICKET_ACT_STATE_DRAFT, + belong_dept=request.user.dept, + ticket_data=save_ticket_data) # 先创建出来 # 更新title和sn title = vdata.get('title', '') title_template = ticket.workflow.title_template if title_template: all_ticket_data = {**rdata, **ticket_data} title = title_template.format(**all_ticket_data) - sn = WfService.get_ticket_sn(ticket.workflow) # 流水号 + 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) + ticket = WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data, + handler=request.user, created=True) return Response(TicketSerializer(instance=ticket).data) - @action(methods=['get'], detail=False, perms_map={'get':'*'}) + @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): """ @@ -215,12 +224,12 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R new_ticket_data = ticket.ticket_data new_ticket_data.update(**vdata['ticket_data']) - ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'], - new_ticket_data=new_ticket_data, handler=request.user, suggestion=vdata['suggestion']) + ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'], + new_ticket_data=new_ticket_data, handler=request.user, + suggestion=vdata['suggestion']) return Response(TicketSerializer(instance=ticket).data) - - @action(methods=['get'], detail=True, perms_map={'get':'*'}) + @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): """ 工单流转记录 @@ -238,8 +247,8 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R flowlogs = TicketFlow.objects.filter(ticket=ticket).order_by('-create_time') serializer = TicketFlowSerializer(instance=flowlogs, many=True) return Response(serializer.data) - - @action(methods=['get'], detail=True, perms_map={'get':'*'}) + + @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,14 +381,13 @@ 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'] filterset_fields = ['ticket'] - ordering = ['-create_time'] \ No newline at end of file + ordering = ['-create_time'] diff --git a/server/conf.example.py b/server/conf.example.py index acade302..54e3b0c6 100755 --- a/server/conf.example.py +++ b/server/conf.example.py @@ -9,4 +9,4 @@ DATABASES = { 'HOST': '49.232.14.174', 'PORT': '5432', } -} \ No newline at end of file +} diff --git a/server/settings.py b/server/settings.py index ee64fff8..4c0f9c76 100755 --- a/server/settings.py +++ b/server/settings.py @@ -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/', } # 日志配置 @@ -330,4 +330,4 @@ XX_QUEUE = conf.XX_QUEUE # 运维相关 SD_PWD = conf.SD_PWD BACKUP_PATH = conf.BACKUP_PATH -SH_PATH = conf.SH_PATH \ No newline at end of file +SH_PATH = conf.SH_PATH diff --git a/server/urls.py b/server/urls.py index d9660c97..caa05cb0 100755 --- a/server/urls.py +++ b/server/urls.py @@ -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 = [ @@ -48,17 +48,16 @@ urlpatterns = [ path('', include('apps.utils.urls')), path('', include('apps.develop.urls')), path('', include('apps.hrm.urls')), - - + + # api文档 path('api/docs/', include_docs_urls(title="接口文档", authentication_classes=[], permission_classes=[])), path('api/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), - - # 前端页面入口 - path('',TemplateView.as_view(template_name="index.html")), -] + \ -static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + \ -static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + # 前端页面入口 + 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)