From cd37d534594dbe41de9f3086febe3e482c789065 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 26 Aug 2021 10:01:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E5=BA=8F=E8=BF=94=E5=9B=9E=E6=8C=87?= =?UTF-8?q?=E5=AF=BC=E4=B9=A6=E5=B5=8C=E5=A5=97=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hb_server/apps/mtm/serializers.py | 3 + hb_server/apps/mtm/views.py | 2 +- hb_server/apps/system/serializers.py | 5 ++ .../wf/migrations/0006_auto_20210825_1542.py | 23 +++++ hb_server/apps/wf/models.py | 35 +++++--- hb_server/apps/wf/serializers.py | 24 +++--- hb_server/apps/wf/services.py | 86 +++++++++++++++---- hb_server/apps/wf/views.py | 45 ++++++++-- 8 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 hb_server/apps/wf/migrations/0006_auto_20210825_1542.py diff --git a/hb_server/apps/mtm/serializers.py b/hb_server/apps/mtm/serializers.py index a2aa192..928c0a3 100644 --- a/hb_server/apps/mtm/serializers.py +++ b/hb_server/apps/mtm/serializers.py @@ -1,6 +1,7 @@ from rest_framework.serializers import ModelSerializer from .models import Material, Process, Step +from apps.system.serializers import FileSimpleSerializer class MaterialSerializer(ModelSerializer): @@ -9,9 +10,11 @@ class MaterialSerializer(ModelSerializer): fields = '__all__' class ProcessSerializer(ModelSerializer): + instruction_ = FileSimpleSerializer(source='instruction', read_only=True) class Meta: model = Process fields = '__all__' + class StepSerializer(ModelSerializer): class Meta: diff --git a/hb_server/apps/mtm/views.py b/hb_server/apps/mtm/views.py index 08b132b..95557cd 100644 --- a/hb_server/apps/mtm/views.py +++ b/hb_server/apps/mtm/views.py @@ -29,7 +29,7 @@ class ProcessViewSet(CreateUpdateModelAMixin, ModelViewSet): """ perms_map = {'get': '*', 'post': 'process_create', 'put': 'process_update', 'delete': 'process_delete'} - queryset = Process.objects.all() + queryset = Process.objects.select_related('instruction').all() serializer_class = ProcessSerializer search_fields = ['name', 'number'] filterset_fields = ['number'] diff --git a/hb_server/apps/system/serializers.py b/hb_server/apps/system/serializers.py index 576be89..a474a42 100644 --- a/hb_server/apps/system/serializers.py +++ b/hb_server/apps/system/serializers.py @@ -49,6 +49,11 @@ class PTaskSerializer(serializers.ModelSerializer): return 'crontab' return 'interval' +class FileSimpleSerializer(serializers.ModelSerializer): + class Meta: + model =File + fields = ['id', 'name', 'file', 'path'] + class FileSerializer(serializers.ModelSerializer): class Meta: model = File diff --git a/hb_server/apps/wf/migrations/0006_auto_20210825_1542.py b/hb_server/apps/wf/migrations/0006_auto_20210825_1542.py new file mode 100644 index 0000000..ab0aaba --- /dev/null +++ b/hb_server/apps/wf/migrations/0006_auto_20210825_1542.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.6 on 2021-08-25 07:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wf', '0005_auto_20210823_1548'), + ] + + operations = [ + migrations.AddField( + model_name='state', + name='state_fields', + field=models.JSONField(default=dict, help_text='json格式字典存储,包括读写属性1:只读,2:必填,3:可选. 示例:{"created_at":1,"title":2, "sn":1}, 内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称', verbose_name='表单字段'), + ), + migrations.AlterField( + model_name='state', + name='type', + field=models.IntegerField(choices=[(0, '普通类型'), (1, 1), (2, 2)], default=0, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理,即没有对应的transition)', verbose_name='状态类型'), + ), + ] diff --git a/hb_server/apps/wf/models.py b/hb_server/apps/wf/models.py index 0ecbd67..84ac4e2 100644 --- a/hb_server/apps/wf/models.py +++ b/hb_server/apps/wf/models.py @@ -26,20 +26,28 @@ class State(CommonAModel): STATE_TYPE_START = 1 STATE_TYPE_END = 2 type_choices = ( - (0, '普通类型'), - (1, STATE_TYPE_START), - (2, STATE_TYPE_END) + (0, '普通'), + (STATE_TYPE_START, '开始'), + (STATE_TYPE_END, '结束') ) + PARTICIPANT_TYPE_PERSONAL = 1 + PARTICIPANT_TYPE_MULTI = 2 + PARTICIPANT_TYPE_DEPT = 3 + PARTICIPANT_TYPE_ROLE = 4 + PARTICIPANT_TYPE_VARIABLE = 5 + PARTICIPANT_TYPE_ROBOT = 6 + PARTICIPANT_TYPE_FIELD = 7 + PARTICIPANT_TYPE_PARENT_FIELD = 8 type2_choices = ( (0, '无处理人'), - (1, '个人'), - (2, '多人'), - (3, '部门'), - (4, '角色'), - (5, '变量'), - (6, '脚本'), - (7, '工单的字段'), - (8, '父工单的字段') + (PARTICIPANT_TYPE_PERSONAL, '个人'), + (PARTICIPANT_TYPE_MULTI, '多人'), + (PARTICIPANT_TYPE_DEPT, '部门'), + (PARTICIPANT_TYPE_ROLE, '角色'), + (PARTICIPANT_TYPE_VARIABLE, '变量'), + (PARTICIPANT_TYPE_ROBOT, '脚本'), + (PARTICIPANT_TYPE_FIELD, '工单的字段'), + (PARTICIPANT_TYPE_PARENT_FIELD, '父工单的字段') ) name = models.CharField('名称', max_length=50) workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流') @@ -48,7 +56,7 @@ class State(CommonAModel): 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=type2_choices, default=1, blank=True, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5,参与人填creator') - + state_fields = models.JSONField('表单字段', default=dict, help_text='json格式字典存储,包括读写属性1:只读,2:必填,3:可选. 示例:{"created_at":1,"title":2, "sn":1}, 内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称') # json格式存储,包括读写属性1:只读,2:必填,3:可选,4:不显示, 字典的字典 class Transition(CommonAModel): """ @@ -116,6 +124,9 @@ class Ticket(CommonAModel): in_add_node = models.BooleanField('加签状态中', default=False, help_text='是否处于加签状态下') add_node_man = models.CharField('加签人', max_length=50, default='', blank=True, help_text='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效') + participant_type = models.IntegerField('当前处理人类型', default=0, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色', choices=State.type2_choices) + participant = models.CharField('当前处理人', max_length=1000, default='', blank=True, help_text='可以为空(无处理人的情况,如结束状态)、username\多个username(以,隔开)\部门id\角色id\脚本文件名等') + class TicketFlow(BaseModel): """ 工单流转日志 diff --git a/hb_server/apps/wf/serializers.py b/hb_server/apps/wf/serializers.py index dc4a2f6..dc7ed0a 100644 --- a/hb_server/apps/wf/serializers.py +++ b/hb_server/apps/wf/serializers.py @@ -1,29 +1,30 @@ -from rest_framework.serializers import ModelSerializer, SerializerMethodField, CharField +import rest_framework +from rest_framework import serializers from .models import State, Ticket, Workflow, Transition, CustomField -class WorkflowSerializer(ModelSerializer): +class WorkflowSerializer(serializers.ModelSerializer): class Meta: model = Workflow fields = '__all__' -class StateSerializer(ModelSerializer): +class StateSerializer(serializers.ModelSerializer): class Meta: model = State fields = '__all__' -class WorkflowSimpleSerializer(ModelSerializer): +class WorkflowSimpleSerializer(serializers.ModelSerializer): class Meta: model = Workflow fields = ['id', 'name'] -class StateSimpleSerializer(ModelSerializer): +class StateSimpleSerializer(serializers.ModelSerializer): class Meta: model = State fields = ['id', 'name'] -class TransitionSerializer(ModelSerializer): +class TransitionSerializer(serializers.ModelSerializer): source_state_ = StateSimpleSerializer(source='source_state', read_only=True) destination_state_ = StateSimpleSerializer(source='destination_state', read_only=True) class Meta: @@ -36,18 +37,19 @@ class TransitionSerializer(ModelSerializer): return queryset -class CustomFieldSerializer(ModelSerializer): +class CustomFieldSerializer(serializers.ModelSerializer): class Meta: model = CustomField fields = '__all__' -class TicketCreateSerializer(ModelSerializer): +class TicketCreateSerializer(serializers.ModelSerializer): + transition = serializers.IntegerField(label='流转ID') class Meta: model=Ticket - fields=['title','workflow','ticket_data'] - -class TicketSerializer(ModelSerializer): + fields=['title','workflow','ticket_data', 'transition'] + +class TicketSerializer(serializers.ModelSerializer): workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True) state_ = StateSimpleSerializer(source='state', read_only=True) diff --git a/hb_server/apps/wf/services.py b/hb_server/apps/wf/services.py index 6031bbd..7cd41ee 100644 --- a/hb_server/apps/wf/services.py +++ b/hb_server/apps/wf/services.py @@ -1,31 +1,38 @@ -from apps.wf.models import State, Ticket, Transition, Workflow +from apps.wf.models import CustomField, State, Ticket, Transition, Workflow from rest_framework.exceptions import APIException class WfService(object): @staticmethod - def get_wf_states(wf:Workflow): + def get_worlflow_states(workflow:Workflow): """ 获取工作流状态列表 """ - return State.objects.filter(workflow=wf, is_deleted=False).order_by('sort') + return State.objects.filter(workflow=workflow, is_deleted=False).order_by('sort') @staticmethod - def get_wf_transitions(wf:Workflow): + def get_workflow_transitions(workflow:Workflow): """ 获取工作流流转列表 """ - return Transition.objects.filter(workflow=wf, is_deleted=False) + return Transition.objects.filter(workflow=workflow, is_deleted=False) @staticmethod - def get_wf_start_state(wf:Workflow): + def get_workflow_start_state(workflow:Workflow): """ 获取工作流初始状态 """ try: - wf_state_obj = State.objects.get(workflow=wf, type=State.STATE_TYPE_START, is_deleted=False) + wf_state_obj = State.objects.get(workflow=workflow, type=State.STATE_TYPE_START, is_deleted=False) return wf_state_obj except: - raise Exception('工作流初始状态配置错误') + raise Exception('工作流状态配置错误') + @staticmethod + def get_workflow_custom_fields(workflow:Workflow): + """ + 获取工单字段 + """ + return CustomField.objects.filter(is_deleted=False, workflow=workflow).order_by('sort') + @classmethod def get_ticket_transitions(cls, ticket:Ticket): """ @@ -41,14 +48,57 @@ class WfService(object): return Transition.objects.filter(is_deleted=False, source_state=state).all() @classmethod - def get_ticket_next_state(cls, ticket:Ticket)->object: - transitions = Transition.objects.filter(source_state=ticket.state, is_deleted=False) - count = transitions.count() - if count == 0: - raise Exception('未配置流转条件') - elif count == 1: - return transitions.first() - else: - for i in transitions: - pass + def get_ticket_steps(cls, ticket:Ticket): + steps = cls.get_worlflow_states(ticket.workflow) + for i in steps: + if ticket.state.is_hidden and ticket.state != i: + steps.remove(i) + return steps + + @classmethod + def get_ticket_transitions(cls, ticket:Ticket): + """ + 获取工单可执行的操作 + """ + return cls.get_state_transitions(ticket.state) + + @classmethod + def get_transition_by_args(cls, kwargs:dict): + """ + 查询并获取流转 + """ + kwargs['is_deleted'] = False + return Transition.objects.filter(**kwargs).all() + + @classmethod + def get_next_state_id_by_transition_and_ticket_info(cls, ticket:Ticket, transition: Transition, workflow:Workflow = None)->object: + """ + 获取下个节点状态 + """ + if ticket: # 如果是新建工单 + source_state = ticket.state + else: + source_state = cls.get_workflow_start_state(workflow) + if transition.source_state != source_state: + raise APIException('流转错误') + destination_state = transition.destination_state + if transition.condition_expression: + pass + return destination_state + + @classmethod + def get_ticket_state_participant_info(cls, state:State, ticket:Ticket, ticket_data:dict): + """ + 获取工单目标状态实际的处理人 + """ + if state.type == State.STATE_TYPE_START: + """ + 回到初始状态 + """ + elif state.type == State.STATE_TYPE_END: + """ + 到达结束状态 + """ + + diff --git a/hb_server/apps/wf/views.py b/hb_server/apps/wf/views.py index 4c68021..2a7d877 100644 --- a/hb_server/apps/wf/views.py +++ b/hb_server/apps/wf/views.py @@ -4,10 +4,11 @@ from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModel from apps.wf.serializers import CustomFieldSerializer, StateSerializer, TicketCreateSerializer, TicketSerializer, TransitionSerializer, WorkflowSerializer from django.shortcuts import render from rest_framework.viewsets import GenericViewSet, ModelViewSet -from rest_framework.decorators import action +from rest_framework.decorators import action, api_view from apps.wf.models import CustomField, Ticket, Workflow, State, Transition from apps.system.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin, OptimizationMixin from apps.wf.services import WfService +from rest_framework.exceptions import APIException # Create your views here. class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet): @@ -26,7 +27,7 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet): 工作流下的状态节点 """ wf = self.get_object() - serializer = self.serializer_class(instance=WfService.get_wf_states(wf), many=True) + 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) @@ -35,7 +36,7 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet): 工作流下的流转规则 """ wf = self.get_object() - serializer = self.serializer_class(instance=WfService.get_wf_transitions(wf), many=True) + 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) @@ -54,9 +55,12 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet): """ ret={} wf = self.get_object() - start_state = WfService.get_wf_start_state(wf) + start_state = WfService.get_workflow_start_state(wf) transitions = WfService.get_state_transitions(start_state) ret['workflow'] = pk + ret['transitions'] = TransitionSerializer(instance=transitions, many=True).data + field_list = CustomFieldSerializer(instance=WfService.get_workflow_custom_fields(wf), many=True).data + ret['field_list'] = field_list return Response(ret) class StateViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet): @@ -97,4 +101,35 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin return super().get_serializer_class() def create(self, request, *args, **kwargs): - return super().create(request, *args, **kwargs) \ No newline at end of file + """ + 新建工单 + """ + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + start_state = WfService.get_workflow_start_state(serializer.data['workflow']) + transition = Transition.objects.get(pk=serializer.data['transition']) + ticket_data = serializer.data['ticket_data'] + if transition.field_require_check: + for key, value in start_state.state_fields.items(): #校验必填项 + if value == 2: + if key not in ticket_data or not ticket_data[key]: + raise APIException('字段{}必填'.format(key)) + next_state = WfService.get_next_state_id_by_transition_and_ticket_info(ticket=None, transition=transition, workflow=serializer.data['workflow']) + + @action(methods=['get'], detail=True, perms_map={'get':'*'}) + def flowsteps(self, request, pk=None): + """ + 工单流转step, 用于显示当前状态的step图(线性结构) + """ + ticket = self.get_object() + steps = WfService.get_ticket_steps(ticket) + return Response(StateSerializer(instance=steps, many=True).data) + + @action(methods=['get'], detail=True, perms_map={'get':'*'}) + def transitions(self, request, pk=None): + """ + 获取工单可执行的操作 + """ + ticket = self.get_object() + transitions = WfService.get_ticket_transitions(ticket) + return Response(TransitionSerializer(instance=transitions, many=True).data) \ No newline at end of file