diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 6fa3c7e08..296d43abb 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-20.04 env: OS: ubuntu-20.04 - PYTHON: "3.6" + PYTHON: "3.11.10" RUN_ENV: "open" APP_CODE: "bk_itsm" APP_ID: "bk_itsm" @@ -51,7 +51,7 @@ jobs: - name: Setup Python uses: actions/setup-python@master with: - python-version: 3.6 + python-version: 3.11.10 - name: Setup Mysql run: | sudo systemctl start mysql.service diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3689787cf..b70e7a06f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,22 @@ -default_stages: [commit] +default_stages: [pre-commit] +default_language_version: + python: python3.11.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.1.0 + rev: v5.0.0 hooks: - id: check-merge-conflict - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 24.10.0 hooks: - id: black - language_version: python3.6 + language_version: python3.11.10 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.1.0 hooks: - id: flake8 - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook - rev: v2.2.0 + rev: v9.19.0 hooks: - id: commitlint stages: [commit-msg] diff --git a/VERSION b/VERSION index 2c9b4ef42..dbe590065 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.3 +2.8.1 diff --git a/app.yml b/app.yml index 61b27d249..f4a50d777 100644 --- a/app.yml +++ b/app.yml @@ -5,7 +5,7 @@ author: 蓝鲸智云 category: 办公应用 introduction: 流程服务是蓝鲸推出的轻量级ITSM,通过可自定义设计的流程模块,覆盖IT服务中的不同管理活动或应用场景。帮助企业用户规范内部管理流程,提升沟通及管理效率。 introduction_en: bk_itsm is a lightweight ITSM created by Blueking. It covers different application scenarios in IT services through customizable workflows and help enterprise users to implement standardize IT workflow, improve communication and management efficiency. -version: 2.7.3 +version: 2.8.1 language: python is_use_celery: True is_use_celery_with_gevent: False diff --git a/app_desc.yaml b/app_desc.yaml index 75f70afb5..7f3625c43 100644 --- a/app_desc.yaml +++ b/app_desc.yaml @@ -1,5 +1,5 @@ spec_version: 2 -app_version: "2.7.3" +app_version: "2.8.1" app: region: default bk_app_code: bk_itsm @@ -35,6 +35,9 @@ modules: - key: GUNICORN_THREAD_NUM value: 10 description: GunicornThread数量 + - key: C_FORCE_ROOT + value: 1 + description: celery5 fit scripts: pre_release_hook: "bash ./bin/pre-release" processes: @@ -47,11 +50,11 @@ modules: plan: 4C1G5R replicas: 1 pworker: - command: python manage.py celery worker -n prefork@%h -P threads -c 10 -l info --maxtasksperchild=100 + command: python manage.py celery worker -n prefork@%h -P threads -c 10 -l info --max-tasks-per-child=100 plan: 4C2G5R replicas: 5 gworker: - command: python manage.py celery worker -P gevent -n gevent@%h -c 4 -l info --maxtasksperchild=100 + command: python manage.py celery worker -P gevent -n gevent@%h -c 4 -l info --max-tasks-per-child=100 plan: 4C2G5R replicas: 5 svc_discovery: diff --git a/blueking/component/open/utils.py b/blueking/component/open/utils.py index 28d438293..503d0a57d 100644 --- a/blueking/component/open/utils.py +++ b/blueking/component/open/utils.py @@ -30,22 +30,18 @@ def get_signature(method, path, app_secret, params=None, data=None): - """generate signature - """ + """generate signature""" kwargs = {} if params: kwargs.update(params) if data: data = json.dumps(data) if isinstance(data, dict) else data - kwargs['data'] = data - kwargs = '&'.join([ - '%s=%s' % (k, v) - for k, v in sorted(iter(kwargs.items()), key=lambda x: x[0]) - ]) - orignal = '%s%s?%s' % (method, path, kwargs) + kwargs["data"] = data + kwargs = "&".join( + ["%s=%s" % (k, v) for k, v in sorted(iter(kwargs.items()), key=lambda x: x[0])] + ) + orignal = "%s%s?%s" % (method, path, kwargs) signature = base64.b64encode( - hmac.new( - str(app_secret), - orignal, - hashlib.sha1).digest()) + hmac.new(str(app_secret), orignal, hashlib.sha256).digest() + ) return signature diff --git a/config/default.py b/config/default.py index 082dbbc00..cc128a395 100644 --- a/config/default.py +++ b/config/default.py @@ -33,6 +33,9 @@ from blueapps.opentelemetry.utils import inject_logging_trace_info from django.http import HttpResponseRedirect from django.urls import reverse +from django.db.backends.mysql.features import DatabaseFeatures +from django.utils.functional import cached_property + from config import ( APP_CODE, @@ -376,7 +379,9 @@ def _(s): if IS_USE_REDIS: CACHE_BACKEND_TYPE = os.environ.get("CACHE_BACKEND_TYPE", "RedisCache") REDIS_PORT = os.environ.get("BKAPP_REDIS_PORT", 6379) - REDIS_PASSWORD = os.environ.get("BKAPP_REDIS_PASSWORD", "") # 密码中不能包括敏感字符,例如":" + REDIS_PASSWORD = os.environ.get( + "BKAPP_REDIS_PASSWORD", "" + ) # 密码中不能包括敏感字符,例如":" REDIS_SERVICE_NAME = os.environ.get("BKAPP_REDIS_SERVICE_NAME", "mymaster") REDIS_MODE = os.environ.get("BKAPP_REDIS_MODE", "single") REDIS_DB = os.environ.get("BKAPP_REDIS_DB", 0) @@ -983,3 +988,16 @@ def redirect_func(request): "BKAPP_QW_WEB_HOOK_URL", "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={}", ) + + +class PatchFeatures: + @cached_property + def minimum_database_version(self): + if self.connection.mysql_is_mariadb: + return (10, 4) + else: + return (5, 7) + + +# 将补丁应用到 DatabaseFeatures 中 +DatabaseFeatures.minimum_database_version = PatchFeatures.minimum_database_version diff --git a/django_signal_valve/models.py b/django_signal_valve/models.py index e679ab9f7..0e0c7e417 100644 --- a/django_signal_valve/models.py +++ b/django_signal_valve/models.py @@ -26,7 +26,7 @@ import zlib from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ try: import pickle as pickle @@ -34,7 +34,6 @@ import pickle - class IOField(models.BinaryField): def __init__(self, compress_level=6, *args, **kwargs): super(IOField, self).__init__(*args, **kwargs) diff --git a/iam/contrib/django/response.py b/iam/contrib/django/response.py index 2a870d9cf..bf4648c74 100644 --- a/iam/contrib/django/response.py +++ b/iam/contrib/django/response.py @@ -12,7 +12,7 @@ """ from django.http.response import JsonResponse -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from iam.contrib.http import HTTP_AUTH_FORBIDDEN_CODE @@ -26,5 +26,5 @@ def __init__(self, exc, *args, **kwargs): "data": None, "permission": exc.perms_apply_data(), } - kwargs['status'] = kwargs.get("status", 499) + kwargs["status"] = kwargs.get("status", 499) super(IAMAuthFailedResponse, self).__init__(*args, **kwargs) diff --git a/itsm/api/v1.py b/itsm/api/v1.py index 0a86b0a92..986e73b55 100644 --- a/itsm/api/v1.py +++ b/itsm/api/v1.py @@ -24,7 +24,7 @@ """ -from django.conf.urls import include, url +from django.urls import include, re_path __author__ = "蓝鲸智云" @@ -38,35 +38,35 @@ urlpatterns = [ # 流程管理模块 - url(r"^workflow/", include("itsm.workflow.urls")), + re_path(r"^workflow/", include("itsm.workflow.urls")), # 单据模块 - url(r"^ticket/", include("itsm.ticket.urls")), + re_path(r"^ticket/", include("itsm.ticket.urls")), # 任务模块 - url(r"^task/", include("itsm.task.urls")), + re_path(r"^task/", include("itsm.task.urls")), # 服务模块 - url(r"^service/", include("itsm.service.urls")), + re_path(r"^service/", include("itsm.service.urls")), # sla模块 - url(r"^sla/", include("itsm.sla.urls")), + re_path(r"^sla/", include("itsm.sla.urls")), # postman - url(r"^postman/", include("itsm.postman.urls")), + re_path(r"^postman/", include("itsm.postman.urls")), # 角色模块 - url(r"^role/", include("itsm.role.urls")), + re_path(r"^role/", include("itsm.role.urls")), # iadmin - url(r"^iadmin/", include("itsm.iadmin.urls")), + re_path(r"^iadmin/", include("itsm.iadmin.urls")), # 网关转发模块,目前主要用于转发esb侧的接口调用 - url(r"^gateway/", include("itsm.gateway.urls")), + re_path(r"^gateway/", include("itsm.gateway.urls")), # "杂种"模块,没有model,且不知道放哪里合适,就放到这个模块吧! - url(r"^misc/", include("itsm.misc.urls")), + re_path(r"^misc/", include("itsm.misc.urls")), # 单据状态模块 - url(r"^ticket_status/", include("itsm.ticket_status.urls")), + re_path(r"^ticket_status/", include("itsm.ticket_status.urls")), # Trigger Module - url(r"^trigger/", include("itsm.trigger.urls")), + re_path(r"^trigger/", include("itsm.trigger.urls")), # iam - url(r"^iam/", include("itsm.auth_iam.urls")), + re_path(r"^iam/", include("itsm.auth_iam.urls")), # iam - url(r"^project/", include("itsm.project.urls")), + re_path(r"^project/", include("itsm.project.urls")), # 人员选择器 - url(r"^c/compapi/v2/usermanage/fs_list_users/$", get_batch_users), + re_path(r"^c/compapi/v2/usermanage/fs_list_users/$", get_batch_users), # 蓝鲸插件服务 - url(r"^plugin_service/", include("itsm.plugin_service.urls")), + re_path(r"^plugin_service/", include("itsm.plugin_service.urls")), ] diff --git a/itsm/auth_iam/models.py b/itsm/auth_iam/models.py index 6b2496de4..1713cbea0 100644 --- a/itsm/auth_iam/models.py +++ b/itsm/auth_iam/models.py @@ -24,7 +24,7 @@ """ from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import LEN_NORMAL diff --git a/itsm/auth_iam/urls.py b/itsm/auth_iam/urls.py index f3d10cbf2..caa93ce5c 100644 --- a/itsm/auth_iam/urls.py +++ b/itsm/auth_iam/urls.py @@ -24,7 +24,7 @@ """ from django.conf import settings -from django.conf.urls import url +from django.urls import re_path from rest_framework.routers import DefaultRouter from iam import IAM @@ -46,7 +46,12 @@ from itsm.auth_iam.views import ResourceViewSet, PermissionViewSet from itsm.auth_iam.resources import ProjectResourceProvider -iam = IAM(settings.APP_CODE, settings.SECRET_KEY, settings.BK_IAM_INNER_HOST, settings.BK_PAAS_HOST) +iam = IAM( + settings.APP_CODE, + settings.SECRET_KEY, + settings.BK_IAM_INNER_HOST, + settings.BK_PAAS_HOST, +) routers = DefaultRouter(trailing_slash=True) @@ -70,5 +75,5 @@ dispatcher.register("task_template", TaskSchemaResourceProvider()) dispatcher.register("public_api", PublicApiResourceProvider()) urlpatterns = routers.urls + [ - url(r'^resources/v1/$', dispatcher.as_view([login_exempt])), + re_path(r"^resources/v1/$", dispatcher.as_view([login_exempt])), ] diff --git a/itsm/component/auto_register/strategy.py b/itsm/component/auto_register/strategy.py index 152d38688..82af1fdbb 100644 --- a/itsm/component/auto_register/strategy.py +++ b/itsm/component/auto_register/strategy.py @@ -26,22 +26,34 @@ from distutils.version import StrictVersion import django -from django.db.models import * +from django.db.models import * # noqa: F403 from django.conf import settings def is_boolean(field): - return isinstance(field, (BooleanField, NullBooleanField)) + return isinstance(field, (BooleanField)) def is_string(field): - return isinstance(field, (CharField, EmailField, IPAddressField, SlugField, URLField)) + return isinstance( + field, (CharField, EmailField, IPAddressField, SlugField, URLField) + ) def is_number(field): - return isinstance(field, (IntegerField, SmallIntegerField, PositiveIntegerField, - PositiveSmallIntegerField, BigIntegerField, - CommaSeparatedIntegerField, DecimalField, FloatField)) + return isinstance( + field, + ( + IntegerField, + SmallIntegerField, + PositiveIntegerField, + PositiveSmallIntegerField, + BigIntegerField, + CommaSeparatedIntegerField, + DecimalField, + FloatField, + ), + ) def is_datetime(field): @@ -53,7 +65,7 @@ def is_file(field): def is_binary(self, field): - if self.django_greater_than('1.6'): + if self.django_greater_than("1.6"): return isinstance(field, (BinaryField)) else: return False @@ -69,6 +81,7 @@ class BaseStrategy: """ 基础策略, 封装了策略所需要的字段判定方法 """ + type = None def get_value(self, field_list: list) -> list: @@ -101,24 +114,36 @@ class ListDisplayStrategy(BaseStrategy): type = "list_display" def is_matched(self, field: Field): - return is_string(field) or is_boolean(field) or \ - is_number(field) or is_datetime(field) + return ( + is_string(field) + or is_boolean(field) + or is_number(field) + or is_datetime(field) + ) class ListDisplayLinksStrategy(BaseStrategy): type = "list_display_links" def is_matched(self, field: Field): - return is_string(field) or is_boolean(field) \ - or is_number(field) or is_datetime(field) + return ( + is_string(field) + or is_boolean(field) + or is_number(field) + or is_datetime(field) + ) class ListFilterStrategy(BaseStrategy): type = "list_filter" def is_matched(self, field: Field): - return is_string(field) or is_boolean(field) \ - or is_number(field) or is_datetime(field) + return ( + is_string(field) + or is_boolean(field) + or is_number(field) + or is_datetime(field) + ) class SearchFieldsStrategy(BaseStrategy): @@ -132,21 +157,29 @@ class ListPerPageStrategy(BaseStrategy): type = "list_per_page" def get_value(self, field_list): - return int(settings.DSA_LIST_PER_PAGE) if hasattr(settings, 'DSA_LIST_PER_PAGE') else 5 + return ( + int(settings.DSA_LIST_PER_PAGE) + if hasattr(settings, "DSA_LIST_PER_PAGE") + else 5 + ) class ListMaxShowAllStrategy(BaseStrategy): type = "list_max_show_all" def get_value(self, field_list): - return int(settings.DSA_LIST_MAX_SHOW_ALL) if hasattr(settings, - 'DSA_LIST_MAX_SHOW_ALL') else 50 + return ( + int(settings.DSA_LIST_MAX_SHOW_ALL) + if hasattr(settings, "DSA_LIST_MAX_SHOW_ALL") + else 50 + ) class StrategyDispatcher(object): """ StrategyDispatcher 负责将不同的方法分派到不同的类中去处理 """ + STRATEGY_CLASS = [ ListRawIdFieldsStrategy, ListDisplayStrategy, @@ -155,15 +188,16 @@ class StrategyDispatcher(object): SearchFieldsStrategy, ListPerPageStrategy, ListMaxShowAllStrategy, - FilterHorizontalStrategy + FilterHorizontalStrategy, ] - STRATEGY_DICT = dict( - [(_object.type, _object()) for _object in STRATEGY_CLASS]) + STRATEGY_DICT = dict([(_object.type, _object()) for _object in STRATEGY_CLASS]) def __init__(self, strategy_type): if strategy_type not in self.STRATEGY_DICT: - raise Exception("The strategy corresponding to Strategy_type does not exist") + raise Exception( + "The strategy corresponding to Strategy_type does not exist" + ) self.strategy_type = strategy_type diff --git a/itsm/component/constants/basic.py b/itsm/component/constants/basic.py index ed83c8e00..06a429c10 100644 --- a/itsm/component/constants/basic.py +++ b/itsm/component/constants/basic.py @@ -27,7 +27,7 @@ from calendar import FRIDAY, MONDAY, SATURDAY, SUNDAY, THURSDAY, TUESDAY, WEDNESDAY from django.conf import settings -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.utils.basic import choices_to_namedtuple, tuple_choices @@ -240,7 +240,12 @@ def update(self, **kwargs): "source": "ticket", "type": "STRING", }, - {"key": "ticket_creator", "name": _("提单人"), "source": "ticket", "type": "MEMBER"}, + { + "key": "ticket_creator", + "name": _("提单人"), + "source": "ticket", + "type": "MEMBER", + }, { "key": "ticket_create_at", "name": _("提单时间"), @@ -302,8 +307,18 @@ def update(self, **kwargs): "source": "task", "type": "STRING", }, - {"key": "task_creator", "name": _("任务创建人"), "source": "task", "type": "MEMBER"}, - {"key": "task_operator", "name": _("任务处理人"), "source": "task", "type": "MEMBER"}, + { + "key": "task_creator", + "name": _("任务创建人"), + "source": "task", + "type": "MEMBER", + }, + { + "key": "task_operator", + "name": _("任务处理人"), + "source": "task", + "type": "MEMBER", + }, { "key": "task_create_at", "name": _("任务创建时间"), diff --git a/itsm/component/constants/task.py b/itsm/component/constants/task.py index fcb5f4dad..61dea616b 100644 --- a/itsm/component/constants/task.py +++ b/itsm/component/constants/task.py @@ -24,7 +24,7 @@ """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.enum import enum NEW = "NEW" @@ -36,34 +36,40 @@ FAILED = "FAILED" FINISHED = "FINISHED" RUNNING = "RUNNING" -REVOKED = 'REVOKED' -SUSPENDED = 'SUSPENDED' +REVOKED = "REVOKED" +SUSPENDED = "SUSPENDED" DELETED = "DELETED" SUCCEED = "SUCCEED" CANCELED = "CANCELED" TERMINATE = "TERMINATE" -ACTIVE_TASK_STATUS = [QUEUE, WAITING_FOR_OPERATE, WAITING_FOR_CONFIRM, WAITING_FOR_BACKEND, RUNNING] +ACTIVE_TASK_STATUS = [ + QUEUE, + WAITING_FOR_OPERATE, + WAITING_FOR_CONFIRM, + WAITING_FOR_BACKEND, + RUNNING, +] NEED_UPDATE_TASK_STATUS = [REVOKED, FAILED, SUSPENDED, DELETED, RUNNING] END_TASK_STATUS = [REVOKED, FINISHED, DELETED] NEED_SYNC_STATUS = [NEW, QUEUE, FAILED, SUSPENDED, RUNNING, WAITING_FOR_OPERATE] -NOT_CREATED = 'NOT_CREATED' -CREATE_FAILED = 'CREATE_FAILED' -CREATED = 'CREATED' -START_FAILED = 'START_FAILED' +NOT_CREATED = "NOT_CREATED" +CREATE_FAILED = "CREATE_FAILED" +CREATED = "CREATED" +START_FAILED = "START_FAILED" SOPS_TASK_STATE_CHOICE = [ - (NOT_CREATED, '未创建'), - (CREATE_FAILED, '创建失败'), - (CREATED, '创建成功'), - (START_FAILED, '启动失败'), - (RUNNING, '执行中'), - (FAILED, '执行失败'), - (FINISHED, '执行结束'), - (REVOKED, '被撤销'), - (SUSPENDED, '被挂起'), - (DELETED, '被删除'), + (NOT_CREATED, "未创建"), + (CREATE_FAILED, "创建失败"), + (CREATED, "创建成功"), + (START_FAILED, "启动失败"), + (RUNNING, "执行中"), + (FAILED, "执行失败"), + (FINISHED, "执行结束"), + (REVOKED, "被撤销"), + (SUSPENDED, "被挂起"), + (DELETED, "被删除"), ] SOPS_TASK_STARTED_STATUS = [START_FAILED, RUNNING, REVOKED, FINISHED, FAILED, SUSPENDED] @@ -94,7 +100,11 @@ SOPS_TASK = "SOPS" NORMAL_TASK = "NORMAL" DEVOPS_TASK = "DEVOPS" -TASK_COMPONENT_CHOICE = [(NORMAL_TASK, _("常规")), (SOPS_TASK, _("标准运维")), (DEVOPS_TASK, _("蓝盾"))] +TASK_COMPONENT_CHOICE = [ + (NORMAL_TASK, _("常规")), + (SOPS_TASK, _("标准运维")), + (DEVOPS_TASK, _("蓝盾")), +] TASK_NAME_KEY = "task_name" # 字段`任务名称`的key TASK_PROCESSOR_KEY = "processors" # 字段`处理人`的key SOPS_TEMPLATE_KEY = "sops_templates" # 字段`流程模板`的key @@ -141,225 +151,229 @@ "regex": "EMPTY", }, { - 'is_builtin': True, - 'display': True, - 'type': 'STRING', - 'key': 'executeTime', - 'name': '执行时长(分钟)', - 'layout': 'COL_12', - 'validate_type': 'REQUIRE', - 'regex': 'NON_NEGATIVE', - 'desc': '运维执行时长 (浮点数,单位是分钟)', - 'tips': '运维执行时长 (浮点数,单位是分钟)', - 'is_tips': True, - 'default': '', - 'choice': [], - 'stage': CONFIRM, - 'sequence': 1, + "is_builtin": True, + "display": True, + "type": "STRING", + "key": "executeTime", + "name": "执行时长(分钟)", + "layout": "COL_12", + "validate_type": "REQUIRE", + "regex": "NON_NEGATIVE", + "desc": "运维执行时长 (浮点数,单位是分钟)", + "tips": "运维执行时长 (浮点数,单位是分钟)", + "is_tips": True, + "default": "", + "choice": [], + "stage": CONFIRM, + "sequence": 1, }, { - 'is_builtin': True, - 'display': True, - 'type': 'STRING', - 'key': 'reviewNumerator', - 'name': '停机比例', - 'layout': 'COL_12', - 'validate_type': 'OPTION', - 'regex': 'NON_NEGATIVE', - 'desc': '停机比例(整型 0-100)', - 'tips': '停机比例(整型 0-100)', - 'is_tips': True, - 'default': '', - 'choice': [], - 'stage': CONFIRM, - 'sequence': 2, + "is_builtin": True, + "display": True, + "type": "STRING", + "key": "reviewNumerator", + "name": "停机比例", + "layout": "COL_12", + "validate_type": "OPTION", + "regex": "NON_NEGATIVE", + "desc": "停机比例(整型 0-100)", + "tips": "停机比例(整型 0-100)", + "is_tips": True, + "default": "", + "choice": [], + "stage": CONFIRM, + "sequence": 2, }, { - 'is_builtin': True, - 'display': True, - 'type': 'RADIO', - 'key': 'reviewIsShutdown', - 'name': '是否停机发布', - 'layout': 'COL_12', - 'validate_type': 'REQUIRE', - 'regex': 'EMPTY', - 'desc': '是否停机发布 (整型:0, 1)', - 'tips': '是否停机发布 (整型:0, 1)', - 'is_tips': True, - 'default': '0', - 'choice': [{'key': '1', 'name': '是'}, {'key': '0', 'name': '否'}], - 'stage': CONFIRM, - 'sequence': 3, + "is_builtin": True, + "display": True, + "type": "RADIO", + "key": "reviewIsShutdown", + "name": "是否停机发布", + "layout": "COL_12", + "validate_type": "REQUIRE", + "regex": "EMPTY", + "desc": "是否停机发布 (整型:0, 1)", + "tips": "是否停机发布 (整型:0, 1)", + "is_tips": True, + "default": "0", + "choice": [{"key": "1", "name": "是"}, {"key": "0", "name": "否"}], + "stage": CONFIRM, + "sequence": 3, }, { - 'is_builtin': True, - 'display': True, - 'type': 'STRING', - 'key': 'reviewShutdownTime', - 'name': '停机耗费时长(分钟)', - 'layout': 'COL_12', - 'validate_type': 'OPTION', - 'regex': 'NON_NEGATIVE', - 'desc': '停机耗费时长 (浮点数)', - 'tips': '停机耗费时长 (浮点数)', - 'is_tips': True, - 'default': '0', - 'choice': [], - 'stage': CONFIRM, - 'sequence': 4, + "is_builtin": True, + "display": True, + "type": "STRING", + "key": "reviewShutdownTime", + "name": "停机耗费时长(分钟)", + "layout": "COL_12", + "validate_type": "OPTION", + "regex": "NON_NEGATIVE", + "desc": "停机耗费时长 (浮点数)", + "tips": "停机耗费时长 (浮点数)", + "is_tips": True, + "default": "0", + "choice": [], + "stage": CONFIRM, + "sequence": 4, }, { - 'is_builtin': True, - 'display': True, - 'type': 'STRING', - 'key': 'prepareTime', - 'name': '任务准备时长(分钟)', - 'layout': 'COL_12', - 'validate_type': 'REQUIRE', - 'regex': 'NON_NEGATIVE', - 'desc': '任务准备时长 (浮点数,单位是分钟)', - 'tips': '任务准备时长 (浮点数,单位是分钟)', - 'is_tips': True, - 'default': '0', - 'choice': [], - 'stage': CONFIRM, - 'sequence': 5, + "is_builtin": True, + "display": True, + "type": "STRING", + "key": "prepareTime", + "name": "任务准备时长(分钟)", + "layout": "COL_12", + "validate_type": "REQUIRE", + "regex": "NON_NEGATIVE", + "desc": "任务准备时长 (浮点数,单位是分钟)", + "tips": "任务准备时长 (浮点数,单位是分钟)", + "is_tips": True, + "default": "0", + "choice": [], + "stage": CONFIRM, + "sequence": 5, }, { - 'is_builtin': True, - 'display': True, - 'type': 'STRING', - 'key': 'testTime', - 'name': '现网测试时长(分钟)', - 'layout': 'COL_12', - 'validate_type': 'REQUIRE', - 'regex': 'NON_NEGATIVE', - 'desc': '现网测试时长 (浮点数,单位是分钟)', - 'tips': '现网测试时长 (浮点数,单位是分钟)', - 'is_tips': True, - 'default': '0', - 'choice': [], - 'stage': CONFIRM, - 'sequence': 6, + "is_builtin": True, + "display": True, + "type": "STRING", + "key": "testTime", + "name": "现网测试时长(分钟)", + "layout": "COL_12", + "validate_type": "REQUIRE", + "regex": "NON_NEGATIVE", + "desc": "现网测试时长 (浮点数,单位是分钟)", + "tips": "现网测试时长 (浮点数,单位是分钟)", + "is_tips": True, + "default": "0", + "choice": [], + "stage": CONFIRM, + "sequence": 6, }, { - 'is_builtin': True, - 'display': True, - 'type': 'SELECT', - 'key': 'isSuccess', - 'name': '发布实施结论', - 'layout': 'COL_12', - 'validate_type': 'REQUIRE', - 'regex': 'EMPTY', - 'desc': '发布实施结论', - 'tips': '发布实施结论', - 'is_tips': True, - 'default': '1', - 'choice': [{'key': '1', 'name': '完全成功'}, {'key': '2', 'name': '成功但有问题'}, {'key': '3', 'name': '发布失败'}], - 'stage': CONFIRM, - 'sequence': 7, + "is_builtin": True, + "display": True, + "type": "SELECT", + "key": "isSuccess", + "name": "发布实施结论", + "layout": "COL_12", + "validate_type": "REQUIRE", + "regex": "EMPTY", + "desc": "发布实施结论", + "tips": "发布实施结论", + "is_tips": True, + "default": "1", + "choice": [ + {"key": "1", "name": "完全成功"}, + {"key": "2", "name": "成功但有问题"}, + {"key": "3", "name": "发布失败"}, + ], + "stage": CONFIRM, + "sequence": 7, }, { - 'is_builtin': True, - 'display': True, - 'type': 'DATETIME', - 'key': 'actualEndTime', - 'name': '实际结束时间', - 'layout': 'COL_12', - 'validate_type': 'REQUIRE', - 'regex': 'EMPTY', - 'desc': "实际结束时间,日期格式 'yyyy-MM-dd hh:mm:ss'", - 'tips': "实际结束时间,日期格式 'yyyy-MM-dd hh:mm:ss'", - 'is_tips': True, - 'default': '', - 'choice': [], - 'stage': CONFIRM, - 'sequence': 8, + "is_builtin": True, + "display": True, + "type": "DATETIME", + "key": "actualEndTime", + "name": "实际结束时间", + "layout": "COL_12", + "validate_type": "REQUIRE", + "regex": "EMPTY", + "desc": "实际结束时间,日期格式 'yyyy-MM-dd hh:mm:ss'", + "tips": "实际结束时间,日期格式 'yyyy-MM-dd hh:mm:ss'", + "is_tips": True, + "default": "", + "choice": [], + "stage": CONFIRM, + "sequence": 8, }, { - 'is_builtin': True, - 'display': True, - 'type': 'DATETIME', - 'key': 'actualBeginTime', - 'name': '实际开始时间', - 'layout': 'COL_12', - 'validate_type': 'REQUIRE', - 'regex': 'EMPTY', - 'desc': "实际开始时间,日期格式 'yyyy-MM-dd hh:mm:ss'", - 'tips': "实际开始时间,日期格式 'yyyy-MM-dd hh:mm:ss'", - 'is_tips': True, - 'default': '', - 'choice': [], - 'stage': CONFIRM, - 'sequence': 9, + "is_builtin": True, + "display": True, + "type": "DATETIME", + "key": "actualBeginTime", + "name": "实际开始时间", + "layout": "COL_12", + "validate_type": "REQUIRE", + "regex": "EMPTY", + "desc": "实际开始时间,日期格式 'yyyy-MM-dd hh:mm:ss'", + "tips": "实际开始时间,日期格式 'yyyy-MM-dd hh:mm:ss'", + "is_tips": True, + "default": "", + "choice": [], + "stage": CONFIRM, + "sequence": 9, }, { - 'is_builtin': True, - 'display': True, - 'type': 'RADIO', - 'key': 'reviewIsDbChange', - 'name': '是否DB发布', - 'layout': 'COL_12', - 'validate_type': 'REQUIRE', - 'regex': 'EMPTY', - 'desc': '是否DB发布', - 'tips': '是否DB发布', - 'is_tips': True, - 'default': '0', - 'choice': [{'key': '1', 'name': '是'}, {'key': '0', 'name': '否'}], - 'stage': CONFIRM, - 'sequence': 10, + "is_builtin": True, + "display": True, + "type": "RADIO", + "key": "reviewIsDbChange", + "name": "是否DB发布", + "layout": "COL_12", + "validate_type": "REQUIRE", + "regex": "EMPTY", + "desc": "是否DB发布", + "tips": "是否DB发布", + "is_tips": True, + "default": "0", + "choice": [{"key": "1", "name": "是"}, {"key": "0", "name": "否"}], + "stage": CONFIRM, + "sequence": 10, }, { - 'is_builtin': True, - 'display': True, - 'type': 'STRING', - 'key': 'reviewDbChangeTime', - 'name': 'DB耗费时长(分钟)', - 'layout': 'COL_12', - 'validate_type': 'OPTION', - 'regex': 'NON_NEGATIVE', - 'desc': 'DB耗费时长 (浮点数)', - 'tips': 'DB耗费时长 (浮点数)', - 'is_tips': True, - 'default': '0', - 'choice': [], - 'stage': CONFIRM, - 'sequence': 11, + "is_builtin": True, + "display": True, + "type": "STRING", + "key": "reviewDbChangeTime", + "name": "DB耗费时长(分钟)", + "layout": "COL_12", + "validate_type": "OPTION", + "regex": "NON_NEGATIVE", + "desc": "DB耗费时长 (浮点数)", + "tips": "DB耗费时长 (浮点数)", + "is_tips": True, + "default": "0", + "choice": [], + "stage": CONFIRM, + "sequence": 11, }, { - 'is_builtin': True, - 'display': True, - 'type': 'TEXT', - 'key': 'conclusion', - 'name': '发布经验总结', - 'layout': 'COL_12', - 'validate_type': 'OPTION', - 'regex': 'EMPTY', - 'desc': '发布经验总结', - 'tips': '', - 'is_tips': False, - 'default': '', - 'choice': [], - 'stage': CONFIRM, - 'sequence': 12, + "is_builtin": True, + "display": True, + "type": "TEXT", + "key": "conclusion", + "name": "发布经验总结", + "layout": "COL_12", + "validate_type": "OPTION", + "regex": "EMPTY", + "desc": "发布经验总结", + "tips": "", + "is_tips": False, + "default": "", + "choice": [], + "stage": CONFIRM, + "sequence": 12, }, { - 'is_builtin': True, - 'display': True, - 'type': 'STRING', - 'key': 'dbBackupTime', - 'name': 'DB备份时长(分钟)', - 'layout': 'COL_12', - 'validate_type': 'OPTION', - 'regex': 'NON_NEGATIVE', - 'desc': '', - 'tips': '', - 'is_tips': False, - 'default': '', - 'choice': [], - 'stage': CONFIRM, - 'sequence': 13, + "is_builtin": True, + "display": True, + "type": "STRING", + "key": "dbBackupTime", + "name": "DB备份时长(分钟)", + "layout": "COL_12", + "validate_type": "OPTION", + "regex": "NON_NEGATIVE", + "desc": "", + "tips": "", + "is_tips": False, + "default": "", + "choice": [], + "stage": CONFIRM, + "sequence": 13, }, ] @@ -420,12 +434,12 @@ ] DEVOPS_TASK_STATE_CHOICE = [ - (NOT_CREATED, '未创建'), - (DEVOPS_STATUS.SUCCEED, '执行成功'), - (DEVOPS_STATUS.FAILED, '执行失败'), - (DEVOPS_STATUS.CANCELED, '已取消'), - (DEVOPS_STATUS.TERMINATE, '已终止'), - (DEVOPS_STATUS.RUNNING, '执行中'), + (NOT_CREATED, "未创建"), + (DEVOPS_STATUS.SUCCEED, "执行成功"), + (DEVOPS_STATUS.FAILED, "执行失败"), + (DEVOPS_STATUS.CANCELED, "已取消"), + (DEVOPS_STATUS.TERMINATE, "已终止"), + (DEVOPS_STATUS.RUNNING, "执行中"), ] TASK_STATUS_CHOICE = [ @@ -434,7 +448,7 @@ (QUEUE, _("待处理")), # 下发到后台队列中,等待执行 (WAITING_FOR_OPERATE, _("待处理")), - (WAITING_FOR_BACKEND, '后台处理中'), + (WAITING_FOR_BACKEND, "后台处理中"), (RUNNING, _("执行中")), (WAITING_FOR_CONFIRM, _("待总结")), # 任务无法正常结束,可设置为忽略状态 @@ -442,10 +456,10 @@ # 自动任务执行失败 (FAILED, _("失败")), (FINISHED, _("完成")), - (REVOKED, _('被撤销')), - (SUSPENDED, _('被挂起')), - (DELETED, _('被删除')), - (SUCCEED, _('执行成功')), - (TERMINATE, _('已终止')), - (CANCELED, _('已取消')), + (REVOKED, _("被撤销")), + (SUSPENDED, _("被挂起")), + (DELETED, _("被删除")), + (SUCCEED, _("执行成功")), + (TERMINATE, _("已终止")), + (CANCELED, _("已取消")), ] diff --git a/itsm/component/constants/trigger.py b/itsm/component/constants/trigger.py index 4f2997548..57e55275f 100644 --- a/itsm/component/constants/trigger.py +++ b/itsm/component/constants/trigger.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ ACTION_STATUS_CREATED = "CREATED" ACTION_STATUS_RUNNING = "RUNNING" @@ -90,7 +90,7 @@ RECOVERY_TICKET: _("恢复单据"), DELETE_TICKET: _("撤销单据"), GLOBAL_ENTER_STATE: _("进入节点"), - GLOBAL_LEAVE_STATE: _("离开节点") + GLOBAL_LEAVE_STATE: _("离开节点"), # CREATE_RELATE_TICKET: _("创建关联单"), # CREATE_PARENTChILD_TICKET: _("创建母子单"), # DISSOLVE_PARENTChILD_TICKET: _("解除母子单"), diff --git a/itsm/component/db/models.py b/itsm/component/db/models.py index 10d38aeac..873b402fa 100644 --- a/itsm/component/db/models.py +++ b/itsm/component/db/models.py @@ -27,7 +27,7 @@ __copyright__ = "Copyright © 2012-2020 Tencent BlueKing. All Rights Reserved." from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from mptt.managers import TreeManager from mptt.models import MPTTModel from itsm.component.db import managers diff --git a/itsm/component/decorators.py b/itsm/component/decorators.py index 19bcfe51e..961c74255 100644 --- a/itsm/component/decorators.py +++ b/itsm/component/decorators.py @@ -31,7 +31,7 @@ from django.core.exceptions import ValidationError from rest_framework.exceptions import ValidationError as RrfValidationError from django.http import JsonResponse, HttpResponse -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.bkoauth.jwt_client import JWTClient, jwt_invalid_view from itsm.component.exceptions import ServerError, ParamError @@ -119,7 +119,9 @@ def __wrapper(request, *args, **kwargs): try: is_file_name_valid(file_name) except ValidationError as error: - return Fail(_("文件上传失败:{}").format(str(error)), "FILE_NAME_INVALID").json() + return Fail( + _("文件上传失败:{}").format(str(error)), "FILE_NAME_INVALID" + ).json() except RrfValidationError as error: return Fail( _("文件上传失败:{}").format(error.detail[0]), "FILE_NAME_INVALID" @@ -199,7 +201,8 @@ def _wrapped_view(request, *args, **kwargs): { "code": "FILE_NOT_ALLOWED", "result": False, - "message": _("上传文件类型仅支持:%s") % ", ".join(content_types), + "message": _("上传文件类型仅支持:%s") + % ", ".join(content_types), } ) @@ -232,9 +235,13 @@ def __wrapper(request, *args, **kwargs): try: system_file_path = SystemSettings.objects.get(key="SYS_FILE_PATH").value if not os.path.exists(system_file_path): - return Fail(_("请检查系统配置:附件存储目录不存在"), "SYS_FILE_PATH_INVALID").json() + return Fail( + _("请检查系统配置:附件存储目录不存在"), "SYS_FILE_PATH_INVALID" + ).json() except SystemSettings.DoesNotExist: - return Fail(_("请检查系统配置:附件存储的目录配置无效"), "SYS_FILE_PATH_EMPTY").json() + return Fail( + _("请检查系统配置:附件存储的目录配置无效"), "SYS_FILE_PATH_EMPTY" + ).json() except Exception as e: return Fail(_("附件路径生成异常:%s") % e, "FILE_PATH_EXCEPTION").json() diff --git a/itsm/component/drf/permissions.py b/itsm/component/drf/permissions.py index a7e44f686..221212d39 100644 --- a/itsm/component/drf/permissions.py +++ b/itsm/component/drf/permissions.py @@ -24,7 +24,7 @@ """ import copy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import permissions from iam import Subject, Action, Resource @@ -94,7 +94,7 @@ def has_permission(self, request, view): return True apply_actions = self.get_view_iam_actions(view) - + # 项目下创建资源 if view.action in ["create", "imports"]: if not apply_actions: @@ -110,11 +110,11 @@ def has_object_permission(self, request, view, obj, **kwargs): # 关联实例的请求,需要针对对象进行鉴权 if view.action in getattr(view, "permission_free_actions", []): return True - + # 获取视图权限action apply_actions = self.get_view_iam_actions(view) return self.iam_auth(request, apply_actions, obj) - + @staticmethod def get_view_iam_actions(view): # 获取视图权限action @@ -131,7 +131,7 @@ def get_view_iam_actions(view): else: apply_actions = copy.deepcopy(apply_actions) return apply_actions - + def iam_auth(self, request, apply_actions, obj=None): resources = [] @@ -185,9 +185,11 @@ def iam_auth(self, request, apply_actions, obj=None): str(resource["resource_id"]), { "iam_resource_owner": resource.get("creator", ""), - "_bk_iam_path_": "/project,{}/".format(project_key) - if resource["resource_type"] != "project" - else "", + "_bk_iam_path_": ( + "/project,{}/".format(project_key) + if resource["resource_type"] != "project" + else "" + ), "name": resource.get("resource_name", ""), }, ) @@ -235,9 +237,9 @@ def iam_create_auth(self, request, apply_actions): str(resource["resource_id"]), { "iam_resource_owner": resource.get("creator", ""), - "_bk_iam_path_": bk_iam_path - if resource["resource_type"] != "project" - else "", + "_bk_iam_path_": ( + bk_iam_path if resource["resource_type"] != "project" else "" + ), "name": resource.get("resource_name", ""), }, ) @@ -301,12 +303,12 @@ def has_object_permission(self, request, view, obj): class IamAuthProjectViewPermit(IamAuthPermit): def has_object_permission(self, request, view, obj): apply_actions = self.get_view_iam_actions(view) - + if hasattr(obj, "project_key"): project_key = obj.project_key if not apply_actions and view.action in ["create", "update", "destroy"]: apply_actions = ["system_settings_manage"] - + # 项目管理必须有查看权限 apply_actions.append("project_view") return self.has_project_view_permission(request, project_key, apply_actions) @@ -342,9 +344,9 @@ def has_project_view_permission(self, request, project_key, apply_actions): str(resource["resource_id"]), { "iam_resource_owner": resource.get("creator", ""), - "_bk_iam_path_": bk_iam_path - if resource["resource_type"] != "project" - else "", + "_bk_iam_path_": ( + bk_iam_path if resource["resource_type"] != "project" else "" + ), "name": resource.get("resource_name", ""), }, ) diff --git a/itsm/component/esb/esbclient.py b/itsm/component/esb/esbclient.py index e837c9fef..d57f23227 100644 --- a/itsm/component/esb/esbclient.py +++ b/itsm/component/esb/esbclient.py @@ -25,18 +25,18 @@ # 全平台 esb-sdk 封装,依赖于 esb-sdk 包,但不依赖 sdk 的版本。 # sdk 中有封装好 cc.get_app_by_user 方法时,可直接按以前 sdk 的习惯调用 -# +# # from blueapps.utils import client # client.cc.get_app_by_user() -# +# # from blueapps.utils import backend_client # b_client = backend_client(access_token="SfgcGlBHmPWttwlGd7nOLAbOP3TAOG") # b_client.cc.get_app_by_user() -# +# # 当前版本 sdk 中未封装好,但 api 已经有 get_app_by_user 的时候。需要指定请求方法 # client.cc.get_app_by_user.get() -import collections +from collections.abc import Callable import logging from django.contrib.auth import get_user_model @@ -50,11 +50,11 @@ from itsm.component.constants import API_PERMISSION_ERROR_CODE __all__ = [ - 'client', - 'backend_client', - 'get_client_by_user', - 'get_client_by_request', - 'CustomComponentAPI', + "client", + "backend_client", + "get_client_by_user", + "get_client_by_request", + "CustomComponentAPI", "client_backend", ] @@ -79,7 +79,7 @@ def get_api_prefix(): if not ESB_SDK_NAME: raise AttributeError except AttributeError: - ESB_SDK_NAME = 'blueking.component.{platform}'.format(platform=settings.RUN_VER) + ESB_SDK_NAME = "blueking.component.{platform}".format(platform=settings.RUN_VER) class SDKClient(object): @@ -96,7 +96,7 @@ def __backend__(self): def __new__(cls, **kwargs): if cls.sdk_package is None: try: - cls.sdk_package = __import__(ESB_SDK_NAME, fromlist=['shortcuts']) + cls.sdk_package = __import__(ESB_SDK_NAME, fromlist=["shortcuts"]) except ImportError as e: raise ImportError("%s is not installed: %s" % (ESB_SDK_NAME, e)) return super(SDKClient, cls).__new__(cls) @@ -104,7 +104,7 @@ def __new__(cls, **kwargs): def __init__(self, **kwargs): self.mod_name = "" self.sdk_mod = None - for ignored_field in ['app_code', 'app_secret']: + for ignored_field in ["app_code", "app_secret"]: if ignored_field in kwargs: kwargs.pop(ignored_field) self.common_args = kwargs @@ -114,7 +114,7 @@ def __getattr__(self, item): ret = SDKClient(**self.common_args) ret.mod_name = item ret.setup_modules() - if isinstance(ret.sdk_mod, collections.Callable): + if isinstance(ret.sdk_mod, Callable): return ret.sdk_mod return ret else: @@ -123,7 +123,7 @@ def __getattr__(self, item): if ret is None: ret = ComponentAPICollection(self).add_api(item) - if not isinstance(ret, collections.Callable): + if not isinstance(ret, Callable): ret = self return _wrap_data_handler(ret) @@ -134,20 +134,26 @@ def setup_modules(self): @property def sdk_client(self): - is_backend = self.common_args.pop('__backend__', False) + is_backend = self.common_args.pop("__backend__", False) username = self.common_args.get("username", settings.SYSTEM_CALL_USER) if is_backend: try: - return self.load_sdk_class("shortcuts", "get_client_by_user")(username, **self.common_args) + return self.load_sdk_class("shortcuts", "get_client_by_user")( + username, **self.common_args + ) except Exception as err: - logger.exception("client call get_client_by_user failed, msg is {}".format(err)) + logger.exception( + "client call get_client_by_user failed, msg is {}".format(err) + ) if settings.RUN_MODE != "DEVELOP": if self.common_args: return self.load_sdk_class("shortcuts", "get_client_by_user")( settings.SYSTEM_CALL_USER, **self.common_args ) else: - raise AccessForbidden("sdk can only be called through the Web request") + raise AccessForbidden( + "sdk can only be called through the Web request" + ) else: # develop mode # 根据RUN_VER获得get_component_client_common_args函数 @@ -163,14 +169,22 @@ def sdk_client(self): request = get_request() try: # 调用sdk方法获取sdk client - return self.load_sdk_class("shortcuts", "get_client_by_request")(request) + return self.load_sdk_class("shortcuts", "get_client_by_request")( + request + ) except Exception as err: - logger.exception("client call get_client_by_request failed, msg is {}".format(err)) + logger.exception( + "client call get_client_by_request failed, msg is {}".format(err) + ) if settings.RUN_MODE != "DEVELOP": if self.common_args: - return self.load_sdk_class("shortcuts", "get_client_by_request")(request, **self.common_args) + return self.load_sdk_class( + "shortcuts", "get_client_by_request" + )(request, **self.common_args) else: - raise AccessForbidden("sdk can only be called through the Web request") + raise AccessForbidden( + "sdk can only be called through the Web request" + ) else: # develop mode # 根据RUN_VER获得get_component_client_common_args函数 @@ -189,7 +203,7 @@ def load_sdk_class(self, mod, attr_or_class): def patch_sdk_component_api_class(self): def patch_get_item(self, item): - if item.startswith('__'): + if item.startswith("__"): # make client can be pickled raise AttributeError() @@ -209,7 +223,9 @@ class ComponentAPICollection(object): def __new__(cls, sdk_client, *args, **kwargs): if sdk_client.mod_name not in cls.mod_map: - cls.mod_map[sdk_client.mod_name] = super(ComponentAPICollection, cls).__new__(cls) + cls.mod_map[sdk_client.mod_name] = super( + ComponentAPICollection, cls + ).__new__(cls) return cls.mod_map[sdk_client.mod_name] def __init__(self, sdk_client): @@ -240,14 +256,18 @@ def __getattr__(self, method): return api_cls( client=SDKClient(**self.collection.client.common_args), method=method, - path='{api_prefix}{collection}/{action}/'.format( - api_prefix=ESB_API_PREFIX, collection=self.collection.client.mod_name, action=self.action + path="{api_prefix}{collection}/{action}/".format( + api_prefix=ESB_API_PREFIX, + collection=self.collection.client.mod_name, + action=self.action, ), - description='custom api(%s)' % self.action, + description="custom api(%s)" % self.action, ) def __call__(self, *args, **kwargs): - raise NotImplementedError('custom api `%s` must specify the request method' % self.action) + raise NotImplementedError( + "custom api `%s` must specify the request method" % self.action + ) client = SDKClient() @@ -264,7 +284,9 @@ def get_client_by_user(user_or_username): username = user_or_username.username else: username = user_or_username - get_client_by_user = import_string(".".join([ESB_SDK_NAME, 'shortcuts', 'get_client_by_user'])) + get_client_by_user = import_string( + ".".join([ESB_SDK_NAME, "shortcuts", "get_client_by_user"]) + ) return get_client_by_user(username) @@ -282,17 +304,20 @@ def _wrap(*args, **kwargs): response = sdk_method(*args, **kwargs) if __raw: return response - - if "get_batch_users" in sdk_method.url: - logger.info("get_batch_users is execute, args={}, kwargs={}, response={}" - .format(args, kwargs, response)) - if not response['result'] and not __ignore_err: - if response['code'] == API_PERMISSION_ERROR_CODE: + if "get_batch_users" in sdk_method.url: + logger.info( + "get_batch_users is execute, args={}, kwargs={}, response={}".format( + args, kwargs, response + ) + ) + + if not response["result"] and not __ignore_err: + if response["code"] == API_PERMISSION_ERROR_CODE: """接口返回无权限的时候,直接抛出权限不够""" raise IamPermissionDenied(data=response.get("permission", [])) raise ComponentCallError(response) - return response['data'] + return response["data"] return _wrap diff --git a/itsm/component/exceptions.py b/itsm/component/exceptions.py index 7442359fa..272bfd3ec 100644 --- a/itsm/component/exceptions.py +++ b/itsm/component/exceptions.py @@ -24,7 +24,7 @@ """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework.exceptions import PermissionDenied diff --git a/itsm/component/generics.py b/itsm/component/generics.py index 911fbfae5..bbc96eef6 100644 --- a/itsm/component/generics.py +++ b/itsm/component/generics.py @@ -29,7 +29,7 @@ import traceback from django.http import Http404 -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import status from rest_framework.exceptions import ( AuthenticationFailed, @@ -51,29 +51,29 @@ def exception_handler(exc, context): """ - 分类: - rest_framework框架内异常 - app自定义异常 + 分类: + rest_framework框架内异常 + app自定义异常 """ - data = {'result': False, 'data': None} + data = {"result": False, "data": None} if isinstance(exc, AuthFailedException): # 权限中心校验异常, 直接抛出 raise exc if isinstance(exc, (NotAuthenticated, AuthenticationFailed)): data = { - 'result': False, - 'code': ResponseCodeStatus.UNAUTHORIZED, - 'detail': _("用户未登录或登录态失效,请使用登录链接重新登录"), - 'login_url': '', + "result": False, + "code": ResponseCodeStatus.UNAUTHORIZED, + "detail": _("用户未登录或登录态失效,请使用登录链接重新登录"), + "login_url": "", } return Response(data, status=status.HTTP_403_FORBIDDEN) if isinstance(exc, IamPermissionDenied): data = { - 'result': False, - 'code': ResponseCodeStatus.PERMISSION_DENIED, - 'message': exc.detail, + "result": False, + "code": ResponseCodeStatus.PERMISSION_DENIED, + "message": exc.detail, "data": [], "permission": exc.data, # 具体的权限信息 } @@ -81,9 +81,9 @@ def exception_handler(exc, context): if isinstance(exc, PermissionDenied): data = { - 'result': False, - 'code': ResponseCodeStatus.PERMISSION_DENIED, - 'message': exc.detail, + "result": False, + "code": ResponseCodeStatus.PERMISSION_DENIED, + "message": exc.detail, } return Response(data, status=status.HTTP_403_FORBIDDEN) @@ -91,30 +91,42 @@ def exception_handler(exc, context): if isinstance(exc, ValidationError): data.update( { - 'code': ResponseCodeStatus.VALIDATE_ERROR, - 'messages': exc.detail, - 'message': format_validation_message(exc), + "code": ResponseCodeStatus.VALIDATE_ERROR, + "messages": exc.detail, + "message": format_validation_message(exc), } ) elif isinstance(exc, MethodNotAllowed): data.update( - {'code': ResponseCodeStatus.METHOD_NOT_ALLOWED, 'message': exc.detail, } + { + "code": ResponseCodeStatus.METHOD_NOT_ALLOWED, + "message": exc.detail, + } ) elif isinstance(exc, PermissionDenied): data.update( - {'code': ResponseCodeStatus.PERMISSION_DENIED, 'message': exc.detail, } + { + "code": ResponseCodeStatus.PERMISSION_DENIED, + "message": exc.detail, + } ) elif isinstance(exc, ServerError): # 更改返回的状态为为自定义错误类型的状态码 data.update( - {'code': exc.code, 'message': exc.message, } + { + "code": exc.code, + "message": exc.message, + } ) elif isinstance(exc, Http404): # 更改返回的状态为为自定义错误类型的状态码 data.update( - {'code': ResponseCodeStatus.OBJECT_NOT_EXIST, 'message': _("当前操作的对象不存在"), } + { + "code": ResponseCodeStatus.OBJECT_NOT_EXIST, + "message": _("当前操作的对象不存在"), + } ) else: # 调试模式 @@ -122,8 +134,8 @@ def exception_handler(exc, context): # 正式环境,屏蔽500 data.update( { - 'code': ResponseCodeStatus.SERVER_500_ERROR, - 'message': getattr(exc, "message", str(exc)) + "code": ResponseCodeStatus.SERVER_500_ERROR, + "message": getattr(exc, "message", str(exc)), } ) diff --git a/itsm/component/middlewares.py b/itsm/component/middlewares.py index f9c70e044..b337d1056 100644 --- a/itsm/component/middlewares.py +++ b/itsm/component/middlewares.py @@ -28,9 +28,9 @@ # ESB封装类,拷贝自`数据平台`,修改自 # https://github.com/LLK/django-request-provider/blob/master/request_provider/signals.py -# +# # 建议进一步考察TODO项,涉及线程安全问题 -# +# # since each thread has its own greenlet we can just use those as identifiers # for the context. If greenlets are not available we fall back to the # current thread ident depending on where it is. @@ -44,7 +44,7 @@ from _thread import get_ident from django.dispatch import Signal -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ class UnauthorizedSignalReceiver(Exception): @@ -56,7 +56,7 @@ class SingleHandlerSignal(Signal): 与 RequestProvider 中间件搭配使用 """ - allowed_receiver = 'itsm.component.middlewares.RequestProvider' + allowed_receiver = "itsm.component.middlewares.RequestProvider" def __init__(self, providing_args=None): Signal.__init__(self, providing_args) @@ -69,9 +69,13 @@ def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): if self.bind_times >= 1: return - receiver_name = '.'.join([receiver.__class__.__module__, receiver.__class__.__name__]) + receiver_name = ".".join( + [receiver.__class__.__module__, receiver.__class__.__name__] + ) if receiver_name != self.allowed_receiver: - raise UnauthorizedSignalReceiver(_("%s is not allowed to connect") % receiver_name) + raise UnauthorizedSignalReceiver( + _("%s is not allowed to connect") % receiver_name + ) Signal.connect(self, receiver, sender, weak, dispatch_uid) self.bind_times += 1 @@ -100,7 +104,7 @@ def process_response(self, request, response): def __call__(self, *args, **kwargs): # TODO 仅对信号反馈? - from_signal = kwargs.get('from_signal', False) + from_signal = kwargs.get("from_signal", False) if from_signal: return self.get_request(**kwargs) else: @@ -112,16 +116,20 @@ def get_request(self, **kwargs): if sender is None: sender = get_ident() if sender not in self._request_pool: - raise UnauthorizedSignalReceiver(_("get_request can't be called in a new thread.")) + raise UnauthorizedSignalReceiver( + _("get_request can't be called in a new thread.") + ) return self._request_pool[sender] def get_x_request_id(): - x_request_id = '' + x_request_id = "" http_request = get_request() - if hasattr(http_request, 'META'): + if hasattr(http_request, "META"): meta = http_request.META - x_request_id = meta.get('HTTP_X_REQUEST_ID', '') if isinstance(meta, dict) else '' + x_request_id = ( + meta.get("HTTP_X_REQUEST_ID", "") if isinstance(meta, dict) else "" + ) return x_request_id diff --git a/itsm/component/misc_middlewares.py b/itsm/component/misc_middlewares.py index cc63df56d..6b838a675 100644 --- a/itsm/component/misc_middlewares.py +++ b/itsm/component/misc_middlewares.py @@ -243,7 +243,7 @@ class WikiIamAuthMiddleware(MiddlewareMixin): def process_view(self, request, view, args, kwargs): """process_view.""" - if request.user.username and "/wiki/" in request.path: + if request.user and request.user.username and "/wiki/" in request.path: apply_actions = ["knowledge_manage"] iam_client = IamRequest(request) auth_actions = iam_client.resource_multi_actions_allowed(apply_actions, []) @@ -264,7 +264,7 @@ class HttpsMiddleware(MiddlewareMixin): def process_request(self, request): if settings.ENVIRONMENT == "dev": return None - + if settings.RUN_VER == "ieod": # 对于openapi 跳转豁免 if request.path.startswith(EXEMPT_HTTPS_REDIRECT): diff --git a/itsm/component/notify.py b/itsm/component/notify.py index 0299c884a..9179450cf 100644 --- a/itsm/component/notify.py +++ b/itsm/component/notify.py @@ -26,7 +26,7 @@ from datetime import datetime from django.conf import settings -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from itsm.component.constants import GENERAL_NOTICE diff --git a/itsm/component/request_middlewares.py b/itsm/component/request_middlewares.py index b0acc113e..7a0c9615e 100644 --- a/itsm/component/request_middlewares.py +++ b/itsm/component/request_middlewares.py @@ -25,19 +25,21 @@ from django.dispatch import Signal from django.utils.deprecation import MiddlewareMixin -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.utils.local import local class AccessorSignal(Signal): - allowed_receiver = 'itsm.component.request_middlewares.RequestProvider' + allowed_receiver = "itsm.component.request_middlewares.RequestProvider" def __init__(self, providing_args=None): Signal.__init__(self, providing_args) def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): - receiver_name = '.'.join([receiver.__class__.__module__, receiver.__class__.__name__]) + receiver_name = ".".join( + [receiver.__class__.__module__, receiver.__class__.__name__] + ) if receiver_name != self.allowed_receiver: raise Exception(_("%s is not allowed to connect") % receiver_name) if not self.receivers: @@ -60,29 +62,31 @@ def process_request(self, request): return None def process_response(self, request, response): - if hasattr(local, 'current_request'): + if hasattr(local, "current_request"): assert request is local.current_request del local.current_request return response def __call__(self, **kwargs): - if not hasattr(local, 'current_request'): + if not hasattr(local, "current_request"): raise Exception(_("get_request can't be called in a new thread.")) return local.current_request def get_request(): - if hasattr(local, 'current_request'): + if hasattr(local, "current_request"): return local.current_request else: raise Exception(_("get_request: current thread hasn't request.")) def get_x_request_id(): - x_request_id = '' + x_request_id = "" http_request = get_request() - if hasattr(http_request, 'META'): + if hasattr(http_request, "META"): meta = http_request.META - x_request_id = meta.get('HTTP_X_REQUEST_ID', '') if isinstance(meta, dict) else '' + x_request_id = ( + meta.get("HTTP_X_REQUEST_ID", "") if isinstance(meta, dict) else "" + ) return x_request_id diff --git a/itsm/component/tasks.py b/itsm/component/tasks.py index 21c094fef..3e0579c6b 100644 --- a/itsm/component/tasks.py +++ b/itsm/component/tasks.py @@ -23,9 +23,9 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from celery import task +from celery import shared_task from celery.schedules import crontab -from celery.task import periodic_task +from blueapps.contrib.celery_tools.periodic import periodic_task from django.core.cache import cache from django.conf import settings from itsm.component.constants import CACHE_10MIN, CACHE_5MIN @@ -36,7 +36,7 @@ adapter_api = settings.ADAPTER_API -@task +@shared_task def update_user_cache(cache_key, ret_type="list", name_type="bk_username", users=None): """更新用户缓存""" bk_users = None @@ -56,7 +56,7 @@ def update_user_cache(cache_key, ret_type="list", name_type="bk_username", users return bk_users -@task +@shared_task def update_bk_business(cache_key, bk_biz_id, role_type): """更新CMDB缓存""" @@ -81,7 +81,7 @@ def update(): return result if result else [] -@task +@shared_task def update_user_departments(cache_key, username, id_only): """更新组织缓存""" diff --git a/itsm/component/utils/basic.py b/itsm/component/utils/basic.py index 5d7dac90c..f0be6f0e2 100644 --- a/itsm/component/utils/basic.py +++ b/itsm/component/utils/basic.py @@ -41,7 +41,7 @@ from django.db.models.fields.reverse_related import ManyToManyRel from django.utils import timezone from django.utils.crypto import get_random_string -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from jsonschema import validate from pypinyin import lazy_pinyin from rest_framework.exceptions import ValidationError @@ -53,7 +53,9 @@ # 清理终端颜色 COLOR_REMOVE = re.compile(r"\x1b[^m]*m") -CLEAR_COLOR_RE = re.compile(r"\\u001b\[\D{1}|\[\d{1,2}\D?|\\u001b\[\d{1,2}\D?~?", re.I | re.U) +CLEAR_COLOR_RE = re.compile( + r"\\u001b\[\D{1}|\[\d{1,2}\D?|\\u001b\[\d{1,2}\D?~?", re.I | re.U +) # 换行转换 LINE_BREAK_RE = re.compile(r"\r\n|\r|\n", re.I | re.U) # ip地址v4版本 @@ -61,8 +63,7 @@ def merge_dict_list(dict_list): - """合并字典列表为单个字典,后面的覆盖前面的 - """ + """合并字典列表为单个字典,后面的覆盖前面的""" merged_dict = {} for dict_item in dict_list: @@ -113,17 +114,26 @@ class ComplexRegexField(object): end_with:以什么类型的字符结束 type:list """ - def __init__(self, validate_type=None, min_match_count=1, start_with=None, end_with=None, special_char=""): - + def __init__( + self, + validate_type=None, + min_match_count=1, + start_with=None, + end_with=None, + special_char="", + ): + if end_with is None: end_with = [] if start_with is None: start_with = [] if validate_type is None: validate_type = [] - + self.validate_type = validate_type - self.min_match_count = min_match_count if min_match_count else len(validate_type) + self.min_match_count = ( + min_match_count if min_match_count else len(validate_type) + ) self.start_with = start_with self.end_with = end_with self.regex_dict = { @@ -163,7 +173,9 @@ def __init__(self, validate_type=None, min_match_count=1, start_with=None, end_w "start_en": _("包含中英文,数字,以英文字符开头"), } self.regex_error_display = [ - str(value) for key, value in list(self.regex_display_dict.items()) if key in self.validate_type + str(value) + for key, value in list(self.regex_display_dict.items()) + if key in self.validate_type ] if special_char: self.regex_dict["special"] = special_char @@ -173,20 +185,33 @@ def __init__(self, validate_type=None, min_match_count=1, start_with=None, end_w def get_regex(self): regex_list = list( combinations( - ["(?=.*[%s])" % self.regex_dict.get(type_key, "") for type_key in self.validate_type], + [ + "(?=.*[%s])" % self.regex_dict.get(type_key, "") + for type_key in self.validate_type + ], self.min_match_count, ) ) start_pattern = ( - "[%s]" % "".join([self.regex_dict.get(type_key, "") for type_key in self.start_with]) + "[%s]" + % "".join( + [self.regex_dict.get(type_key, "") for type_key in self.start_with] + ) if self.start_with else "" ) end_pattern = ( - "[%s]" % "".join([self.regex_dict.get(type_key, "") for type_key in self.end_with]) if self.end_with else "" + "[%s]" + % "".join([self.regex_dict.get(type_key, "") for type_key in self.end_with]) + if self.end_with + else "" + ) + include_rules = "".join( + [self.regex_dict.get(type_key, "") for type_key in self.validate_type] + ) + include_pattern = "^{}[{}]*{}$".format( + start_pattern, include_rules, end_pattern ) - include_rules = "".join([self.regex_dict.get(type_key, "") for type_key in self.validate_type]) - include_pattern = "^{}[{}]*{}$".format(start_pattern, include_rules, end_pattern) end_pattern = "^.*%s$" % end_pattern start_pattern = "^%s.*$" % start_pattern least_pattern = "^%s.*$" % "|".join(["".join(item) for item in regex_list]) @@ -197,20 +222,35 @@ def validate(self, value): return include_pattern, least_pattern, start_pattern, end_pattern = self.get_regex() if list(set(self.start_with).difference(self.validate_type)): - raise ValidationError(_("包含了指定字符【{}】以外的内容").format(",".join(self.regex_error_display)), code="not-matched") + raise ValidationError( + _("包含了指定字符【{}】以外的内容").format( + ",".join(self.regex_error_display) + ), + code="not-matched", + ) if list(set(self.end_with).difference(self.validate_type)): - raise ValidationError(_("包含了指定字符【{}】以外的内容").format(",".join(self.regex_error_display)), code="not-matched") + raise ValidationError( + _("包含了指定字符【{}】以外的内容").format( + ",".join(self.regex_error_display) + ), + code="not-matched", + ) if not re.match(start_pattern, value): raise ValidationError(_("开头格式不正确"), code="not-matched") if not re.match(end_pattern, value): raise ValidationError(_("结尾格式不正确"), code="not-matched") if not re.match(include_pattern, value): raise ValidationError( - _("输入格式不正确:包含了指定字符【{}】以外的内容").format(",".join(self.regex_error_display)), code="not-matched" + _("输入格式不正确:包含了指定字符【{}】以外的内容").format( + ",".join(self.regex_error_display) + ), + code="not-matched", ) if not re.match(least_pattern, value): raise ValidationError( - _("至少需要匹配%s种字符(%s)") % (self.min_match_count, ",".join(self.regex_error_display)), code="not-matched" + _("至少需要匹配%s种字符(%s)") + % (self.min_match_count, ",".join(self.regex_error_display)), + code="not-matched", ) @@ -263,7 +303,9 @@ def __init__(self, validate_type=""): "start_en": _("包含中英文,数字,以英文字符开头"), } self.regex_error_display = [ - value for key, value in list(self.regex_display_dict.items()) if key == self.validate_type + value + for key, value in list(self.regex_display_dict.items()) + if key == self.validate_type ] def validate(self, value): @@ -272,7 +314,10 @@ def validate(self, value): pattern = self.regex_dict.get(self.validate_type, "") if not re.match(pattern, value): raise ValidationError( - _("输入格式不正确:包含了指定字符【{}】以外的内容").format(",".join(self.regex_error_display)), code="not-matched" + _("输入格式不正确:包含了指定字符【{}】以外的内容").format( + ",".join(self.regex_error_display) + ), + code="not-matched", ) @@ -340,7 +385,9 @@ def deep_getattr(obj, attr): return reduce(getattr, attr.split("."), obj) -def group_by(item_list, key_or_index_tuple, dict_result=False, aggregate=None, as_key=None): +def group_by( + item_list, key_or_index_tuple, dict_result=False, aggregate=None, as_key=None +): """ 对列表中的字典元素进行groupby操作,依据为可排序的某个key :param item_list: 待分组字典列表或元组列表 @@ -423,7 +470,16 @@ def parse_color(content): }, {"pattern": [], "class": "agent-color-gray"}, { - "pattern": [_("返回码"), _("执行完毕"), _("作业参数"), _("curl"), _("status"), _("agent状态"), _("yum"), _("apt-get")], + "pattern": [ + _("返回码"), + _("执行完毕"), + _("作业参数"), + _("curl"), + _("status"), + _("agent状态"), + _("yum"), + _("apt-get"), + ], "class": "agent-color-black", }, { @@ -555,17 +611,26 @@ def generate_random_sn(service_type): from itsm.component.data import incr_expireat, exists from itsm.component.constants import PREFIX_KEY - prefix_mapping = {"event": "INC", "request": "REQ", "change": "CRQ", "question": "PBI"} + prefix_mapping = { + "event": "INC", + "request": "REQ", + "change": "CRQ", + "question": "PBI", + } key = PREFIX_KEY + service_type when = None now_time = now() if not exists(key): # 设置第二天的0:00:00过期 - when = datetime.datetime(year=now_time.year, month=now_time.month, day=now_time.day) + datetime.timedelta( - days=1 - ) + when = datetime.datetime( + year=now_time.year, month=now_time.month, day=now_time.day + ) + datetime.timedelta(days=1) num = incr_expireat(key, when=when) - sn = prefix_mapping[service_type] + now_time.strftime("%Y%m%d") + "{:0>6}".format(num) + sn = ( + prefix_mapping[service_type] + + now_time.strftime("%Y%m%d") + + "{:0>6}".format(num) + ) return sn @@ -637,7 +702,11 @@ def dotted_property(instance, name): '' -> '' ',aaa,bbb,ccc', -> 'aaa,bbb,ccc' """ - property = instance.get(name, "") if isinstance(instance, dict) else getattr(instance, name, "") + property = ( + instance.get(name, "") + if isinstance(instance, dict) + else getattr(instance, name, "") + ) return ",".join(list_by_separator(property)) @@ -661,10 +730,17 @@ def __init__(self, signal, receiver, sender, dispatch_uid=None): self.dispatch_uid = dispatch_uid def __enter__(self): - self.signal.disconnect(receiver=self.receiver, sender=self.sender, dispatch_uid=self.dispatch_uid) + self.signal.disconnect( + receiver=self.receiver, sender=self.sender, dispatch_uid=self.dispatch_uid + ) def __exit__(self, exc_type, exc_val, exc_tb): - self.signal.connect(receiver=self.receiver, sender=self.sender, weak=False, dispatch_uid=self.dispatch_uid) + self.signal.connect( + receiver=self.receiver, + sender=self.sender, + weak=False, + dispatch_uid=self.dispatch_uid, + ) def fill_tree_route(pure_tree, pre_routes=None): @@ -676,7 +752,9 @@ def fill_tree_route(pure_tree, pre_routes=None): pure_tree["route"] = pre_routes for child in pure_tree.get("children", []): - fill_tree_route(child, pre_routes + [{"id": pure_tree["id"], "name": pure_tree["name"]}]) + fill_tree_route( + child, pre_routes + [{"id": pure_tree["id"], "name": pure_tree["name"]}] + ) def build_tree(raw_nodes, parent_name, empty_parent=None, need_route=False): @@ -718,7 +796,7 @@ def jsonschema_validate(schema, instance): def walk(node): - """ iterate tree in pre-order depth-first search order """ + """iterate tree in pre-order depth-first search order""" yield node for child in node.get("children", []): for item in walk(child): @@ -815,7 +893,7 @@ def _convert(obj, converted): def namedtuplefetchall(cursor): "Return all rows from a cursor as a namedtuple" desc = cursor.description - nt_result = namedtuple('Result', [col[0] for col in desc]) + nt_result = namedtuple("Result", [col[0] for col in desc]) return [nt_result(*row) for row in cursor.fetchall()] diff --git a/itsm/component/utils/batch.py b/itsm/component/utils/batch.py index a4fa8985a..8ec1e52da 100644 --- a/itsm/component/utils/batch.py +++ b/itsm/component/utils/batch.py @@ -28,19 +28,19 @@ # AttributeError: 'Thread' object has no attribute '_children' # This probably happens due to a bug in multiprocessing.dummy (see here and here) that existed # before python 2.7.5 and 3.3.2. -# +# # Solution A - Upgrade Python -# +# # Solution B - Modify dummy # multiprocessing/dummy/__init__.py, edit the start method within the DummyProcess class as follows # if hasattr(self._parent, '_children'): # add this line # self._parent._children[self] = None # indent this existing line -# -# +# +# # Solution C - Monkey Patch # Let's make it available in our namespace: # from multiprocessing import dummy as __mp_dummy -# +# # Now we can define a replacement and patch DummyProcess: # def __DummyProcess_start_patch(self): # pulled from an updated version of Python # assert self._parent is __mp_dummy.current_process() # modified to avoid further imports @@ -53,7 +53,7 @@ from multiprocessing.dummy import Pool as ThreadPool -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ # 线程池容量上限 diff --git a/itsm/component/utils/client_backend_query.py b/itsm/component/utils/client_backend_query.py index c16a78ff5..19acfe4cd 100644 --- a/itsm/component/utils/client_backend_query.py +++ b/itsm/component/utils/client_backend_query.py @@ -28,7 +28,7 @@ from math import ceil from django.conf import settings from django.core.cache import cache -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from itsm.component.constants import CACHE_5MIN, CACHE_30MIN, PREFIX_KEY @@ -52,7 +52,11 @@ def get_biz_choices(): apps = get_all_apps() app_list = [ - {"key": item["bk_biz_id"], "name": item["bk_biz_name"], "desc": _("请选择关联业务")} + { + "key": item["bk_biz_id"], + "name": item["bk_biz_name"], + "desc": _("请选择关联业务"), + } for item in apps ] @@ -80,7 +84,11 @@ def get_group_app_list(apps, group_apps, group_other, biz_group_conf): if apps: group_other["items"].extend( [ - {"key": a["bk_biz_id"], "name": a["bk_biz_name"], "desc": _("请选择关联业务")} + { + "key": a["bk_biz_id"], + "name": a["bk_biz_name"], + "desc": _("请选择关联业务"), + } for a in list(apps.values()) ] ) @@ -304,7 +312,8 @@ def get_department_users(department_id, recursive=False, detail=False): cache.set(cache_key, users, CACHE_5MIN) except ComponentCallError as e: logger.error( - "获取组织架构用户失败:department_id=%s, error=%s" % (department_id, str(e)) + "获取组织架构用户失败:department_id=%s, error=%s" + % (department_id, str(e)) ) return [] @@ -338,7 +347,9 @@ def get_department_info(department_id): try: res = client_backend.usermanage.retrieve_department({"id": department_id}) except ComponentCallError as e: - logger.error("获取组织架构详情失败:department_id=%s, error=%s" % (department_id, str(e))) + logger.error( + "获取组织架构详情失败:department_id=%s, error=%s" % (department_id, str(e)) + ) return [] return res @@ -395,7 +406,9 @@ def get_components(system_names): ) return res.get("data", []) except Exception as e: - logger.error("获取指定系统的组件列表: system_names=%s, error=%s" % (system_names, str(e))) + logger.error( + "获取指定系统的组件列表: system_names=%s, error=%s" % (system_names, str(e)) + ) return [] diff --git a/itsm/component/utils/conversion.py b/itsm/component/utils/conversion.py index 6b2b6b16c..5d48eae21 100644 --- a/itsm/component/utils/conversion.py +++ b/itsm/component/utils/conversion.py @@ -26,7 +26,7 @@ import json import re -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from mako.template import Template from itsm.component.exceptions import ParamError diff --git a/itsm/gateway/urls.py b/itsm/gateway/urls.py index dc640dc3e..b4ae38628 100644 --- a/itsm/gateway/urls.py +++ b/itsm/gateway/urls.py @@ -26,56 +26,58 @@ __author__ = "蓝鲸智云" __copyright__ = "Copyright © 2012-2020 Tencent BlueKing. All Rights Reserved." -from django.conf.urls import url +from django.urls import re_path from itsm.gateway import views urlpatterns = [ - url(r"^test/token/$", views.get_token), - url(r"^bk_login/get_batch_users/$", views.get_batch_users), - url(r"^bk_login/get_all_users/$", views.get_all_users), - url(r"^cmdb/get_app_list/$", views.get_app_list), - url(r"^usermanage/get_departments/$", views.get_departments), - url( + re_path(r"^test/token/$", views.get_token), + re_path(r"^bk_login/get_batch_users/$", views.get_batch_users), + re_path(r"^bk_login/get_all_users/$", views.get_all_users), + re_path(r"^cmdb/get_app_list/$", views.get_app_list), + re_path(r"^usermanage/get_departments/$", views.get_departments), + re_path( r"^usermanage/get_first_level_departments/$", views.get_first_level_departments ), - url(r"^usermanage/get_department_info/$", views.get_department_info), - url(r"^usermanage/get_department_users/$", views.get_department_users), - url(r"^usermanage/get_department_users_count/$", views.get_department_users_count), - url(r"^usermanage/get_user_info/$", views.get_user_info), - url(r"^sops/get_user_project_list/$", views.get_user_project_list), - url(r"^sops/get_template_list/$", views.get_template_list), - url(r"^sops/get_template_detail/$", views.get_template_detail), - url(r"^sops/get_unfinished_sops_tasks/$", views.get_unfinished_sops_tasks), - url(r"^sops/get_sops_tasks/$", views.get_sops_tasks), - url(r"^sops/get_sops_tasks_detail/$", views.get_sops_tasks_detail), - url(r"^sops/get_sops_template_schemes/$", views.get_sops_template_schemes), - url(r"^sops/get_sops_preview_task_tree/$", views.get_sops_preview_task_tree), - url( + re_path(r"^usermanage/get_department_info/$", views.get_department_info), + re_path(r"^usermanage/get_department_users/$", views.get_department_users), + re_path( + r"^usermanage/get_department_users_count/$", views.get_department_users_count + ), + re_path(r"^usermanage/get_user_info/$", views.get_user_info), + re_path(r"^sops/get_user_project_list/$", views.get_user_project_list), + re_path(r"^sops/get_template_list/$", views.get_template_list), + re_path(r"^sops/get_template_detail/$", views.get_template_detail), + re_path(r"^sops/get_unfinished_sops_tasks/$", views.get_unfinished_sops_tasks), + re_path(r"^sops/get_sops_tasks/$", views.get_sops_tasks), + re_path(r"^sops/get_sops_tasks_detail/$", views.get_sops_tasks_detail), + re_path(r"^sops/get_sops_template_schemes/$", views.get_sops_template_schemes), + re_path(r"^sops/get_sops_preview_task_tree/$", views.get_sops_preview_task_tree), + re_path( r"^sops/get_sops_preview_common_task_tree/$", views.get_sops_preview_common_task_tree, ), - url(r"^devops/get_user_pipeline_list/$", views.get_user_pipeline_list), - url(r"^devops/get_user_projects/$", views.get_user_projects), - url( + re_path(r"^devops/get_user_pipeline_list/$", views.get_user_pipeline_list), + re_path(r"^devops/get_user_projects/$", views.get_user_projects), + re_path( r"^devops/get_pipeline_build_start_info/$", views.get_pipeline_build_start_info ), - url(r"^devops/get_user_pipeline_detail/$", views.get_user_pipeline_detail), - url(r"^devops/get_pipeline_build_list/$", views.get_pipeline_build_list), - url(r"^devops/start_user_pipeline/$", views.start_user_pipeline), - url( + re_path(r"^devops/get_user_pipeline_detail/$", views.get_user_pipeline_detail), + re_path(r"^devops/get_pipeline_build_list/$", views.get_pipeline_build_list), + re_path(r"^devops/start_user_pipeline/$", views.start_user_pipeline), + re_path( r"^devops/get_user_pipeline_build_status/$", views.get_user_pipeline_build_status, ), - url( + re_path( r"^devops/get_user_pipeline_build_detail/$", views.get_user_pipeline_build_detail, ), - url( + re_path( r"^devops/get_pipeline_build_artifactory/$", views.get_pipeline_build_artifactory, ), - url( + re_path( r"^devops/get_pipeline_build_artifactory_download_url/$", views.get_pipeline_build_artifactory_download_url, ), diff --git a/itsm/gateway/views.py b/itsm/gateway/views.py index 122fbc079..05c5decba 100644 --- a/itsm/gateway/views.py +++ b/itsm/gateway/views.py @@ -26,7 +26,7 @@ import json from django.core.cache import cache -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.decorators.cache import cache_page from django.http import JsonResponse, HttpResponse @@ -116,7 +116,9 @@ def get_batch_users(request): return Success(res).json() except Exception as error: logger.warning(_("批量获取用户信息出错,%s"), str(error)) - return Fail(_("批量获取用户信息出错,%s") % str(error), "BK_LOGIN.GET_BATCH_USERS").json() + return Fail( + _("批量获取用户信息出错,%s") % str(error), "BK_LOGIN.GET_BATCH_USERS" + ).json() @fbv_exception_handler @@ -513,7 +515,9 @@ def get_user_pipeline_list(request): pipeline_list.extend(batch_process(get_user_pipeline_singel_page, kwarg_list)) return Success(pipeline_list).json() except Exception as e: - return Fail(_("批量获取流水线出错:{}".format(str(e))), "BK_LOGIN.GET_BATCH_USERS").json() + return Fail( + _("批量获取流水线出错:{}".format(str(e))), "BK_LOGIN.GET_BATCH_USERS" + ).json() @fbv_exception_handler diff --git a/itsm/helper/tasks.py b/itsm/helper/tasks.py index 7e41c326c..d18438ff1 100644 --- a/itsm/helper/tasks.py +++ b/itsm/helper/tasks.py @@ -33,7 +33,7 @@ import json import os -from celery import task +from celery import shared_task from django.conf import settings from django.db import connection from django.db.models import F @@ -71,7 +71,7 @@ from itsm.workflow.models import DefaultField, Field, State, Workflow, WorkflowVersion -@task +@shared_task def _db_fix_for_blueapps_after_2_6_0(): """ blueapps的数据升级 @@ -79,7 +79,9 @@ def _db_fix_for_blueapps_after_2_6_0(): migrations = (("account", "0002_init_superuser"), ("account", "0003_verifyinfo")) if settings.RUN_VER != "open": logger.Exception( - "当前运行环境为:{},不支持db_fix_for_blueapps_after_2_6_0方法".format(settings.RUN_VER) + "当前运行环境为:{},不支持db_fix_for_blueapps_after_2_6_0方法".format( + settings.RUN_VER + ) ) return try: @@ -98,7 +100,7 @@ def _db_fix_for_blueapps_after_2_6_0(): logger.Exception(str(err)) -@task +@shared_task def _db_fix_for_workflow_to_2_5_9(): """ 流程任务的数据升级 @@ -135,7 +137,7 @@ def create_task(instances): create_task(WorkflowVersion.objects.all()) -@task +@shared_task def _db_fix_for_service_catalog(): """服务目录添加前置路径""" print("start execute _db_fix_for_service_catalog") @@ -146,17 +148,17 @@ def _db_fix_for_service_catalog(): print("finish execute _db_fix_for_service_catalog") -@task +@shared_task def _db_fix_default_value_for_field(): fix_default_value_for_field() -@task +@shared_task def _db_fix_for_ticket_processors(): migrate_processors_for_ticket() -@task +@shared_task def _db_fix_for_attachments(): """附件升级方案""" @@ -248,7 +250,7 @@ def update_ticket_fields(): update_ticket_fields() -@task +@shared_task def _db_fix_from_2_1_x_to_2_2_1(): """ 流程引擎版本升级迁移: @@ -298,7 +300,9 @@ def _db_fix_from_2_1_x_to_2_2_1(): source_type="CUSTOM" ) - TicketEventLog.objects.filter(message__in=["流程开始", "单据流程结束"]).update(source="SYS") + TicketEventLog.objects.filter(message__in=["流程开始", "单据流程结束"]).update( + source="SYS" + ) task_end = datetime.datetime.now() SystemSettings.objects.filter(key="_db_fix_from_2_1_x_to_2_2_1").update( @@ -310,7 +314,7 @@ def _db_fix_from_2_1_x_to_2_2_1(): ) -@task +@shared_task def _db_fix_from_1_1_22_to_2_1_x(): """V1.1.x到V2.1.x的数据升级接口(建议提前做好数据备份)""" @@ -343,7 +347,7 @@ def _db_fix_from_1_1_22_to_2_1_x(): logger.info("_db_fix_from_1_1_22_to_2_1_x fail: %s" % str(e)) -@task +@shared_task def _db_fix_after_2_0_3(): """ 修复数据库数据: @@ -377,7 +381,7 @@ def _db_fix_after_2_0_3(): logger.error("db_fix_after_2_0_3 fail! error: %s" % str(e)) -@task +@shared_task def _db_fix_after_2_0_7(): """ 日志新增处理人员快照 @@ -405,7 +409,7 @@ def _db_fix_after_2_0_7(): ) -@task +@shared_task def _db_fix_after_2_0_9(): try: cnt = 0 @@ -442,7 +446,7 @@ def _db_fix_after_2_0_9(): logger.error("db_fix_after_2_0_9 fail! error: %s" % str(e)) -@task +@shared_task def _db_fix_after_2_1_x(): """ 第二次数据迁移: @@ -473,7 +477,7 @@ def _db_fix_after_2_1_x(): ).delete() -@task +@shared_task def _db_fix_after_2_0_14(): try: Ticket.objects.filter( @@ -484,7 +488,7 @@ def _db_fix_after_2_0_14(): logger.error("db_fix_after_2_0_14 fail! error: %s" % str(e)) -@task +@shared_task def _db_fix_after_2_1_1(): try: TicketEventLog.objects.filter(message__contains="驳回").update( @@ -495,7 +499,7 @@ def _db_fix_after_2_1_1(): logger.error("db_fix_after_2_1_1 fail! error: %s" % str(e)) -@task +@shared_task def _fix_ticket_title(): tickets = Ticket.objects.filter(is_deleted=False, is_draft=False) try: @@ -509,7 +513,7 @@ def _fix_ticket_title(): logger.error("fix_ticket_title fail! error: %s" % str(e)) -@task +@shared_task def _update_logs_type(): try: TicketEventLog.objects.filter(message__contains="】终止,原因:【").update( @@ -520,7 +524,7 @@ def _update_logs_type(): logger.error("update_logs_type fail! error: %s" % str(e)) -@task +@shared_task def _db_fix_sla(): try: choices = OldSla.objects.values( @@ -539,7 +543,7 @@ def _db_fix_sla(): logger.error("_db_fix_sla fail! error: %s" % str(e)) -@task +@shared_task def _db_fix_after_2_1_9(): try: TicketField.objects.update(related_fields={}) @@ -551,7 +555,7 @@ def _db_fix_after_2_1_9(): logger.error("_db_fix_after_2_1_9 fail!, error: %s" % str(e)) -@task +@shared_task def _db_fix_ticket_end_at_after_2_0_5(): try: Ticket.objects.filter( @@ -562,7 +566,7 @@ def _db_fix_ticket_end_at_after_2_0_5(): logger.error("_db_fix_ticket_end_at_after_2_0_5 fail!, error: %s" % str(e)) -@task +@shared_task def _db_fix_deal_time_after_2_0_5(): try: for log in TicketEventLog.objects.filter(type="CLAIM", deal_time=0): diff --git a/itsm/helper/urls.py b/itsm/helper/urls.py index 5d92b79eb..873e0eeb6 100644 --- a/itsm/helper/urls.py +++ b/itsm/helper/urls.py @@ -26,39 +26,45 @@ __author__ = "蓝鲸智云" __copyright__ = "Copyright © 2012-2020 Tencent BlueKing. All Rights Reserved." -from django.conf.urls import url +from django.urls import re_path from itsm.helper import views urlpatterns = [ # 统一的升级接口 - url(r'^db_fix_from_1_1_22_to_2_1_16/$', views.db_fix_from_1_1_22_to_2_1_16), - url(r'^db_fix_from_2_1_x_to_2_2_1/$', views.db_fix_from_2_1_x_to_2_2_1), - url(r'^db_fix_after_2_2_17/$', views.db_fix_after_2_2_17), - url(r'^db_fix_after_2_3_1/$', views.db_fix_after_2_3_1), + re_path(r"^db_fix_from_1_1_22_to_2_1_16/$", views.db_fix_from_1_1_22_to_2_1_16), + re_path(r"^db_fix_from_2_1_x_to_2_2_1/$", views.db_fix_from_2_1_x_to_2_2_1), + re_path(r"^db_fix_after_2_2_17/$", views.db_fix_after_2_2_17), + re_path(r"^db_fix_after_2_3_1/$", views.db_fix_after_2_3_1), # 杂乱的升级接口 - url(r'^fix_ticket_title/$', views.fix_ticket_title), - url(r'^update_logs_type/$', views.update_logs_type), - url(r'^db_fix_after_2_0_3/$', views.db_fix_after_2_0_3), - url(r'^db_fix_ticket_end_at_after_2_0_5/$', views.db_fix_ticket_end_at_after_2_0_5), - url(r'^db_fix_deal_time_after_2_0_5/$', views.db_fix_deal_time_after_2_0_5), - url(r'^db_fix_after_2_0_7/$', views.db_fix_after_2_0_7), - url(r'^db_fix_after_2_0_9/$', views.db_fix_after_2_0_9), - url(r'^db_fix_after_2_0_14/$', views.db_fix_after_2_0_14), - url(r'^db_fix_after_2_1_x/$', views.db_fix_after_2_1_x), - url(r'^db_fix_after_2_1_1/$', views.db_fix_after_2_1_1), - url(r'^db_fix_sla/$', views.db_fix_sla), - url(r'^db_fix_after_2_1_9/$', views.db_fix_after_2_1_9), - url(r'^export_api_system/$', views.export_api_system), - url(r'^db_fix_for_attachments/$', views.db_fix_for_attachments), - url(r'^db_fix_for_service_catalog/$', views.db_fix_for_service_catalog), - url(r'^weekly_statical/$', views.weekly_statical), - url(r'^db_fix_for_workflow_after_2_5_9/$', views.db_fix_for_workflow_after_2_5_9), - url(r'^db_fix_for_blueapps_after_2_6_0/$', views.db_fix_for_blueapps_after_2_6_0), + re_path(r"^fix_ticket_title/$", views.fix_ticket_title), + re_path(r"^update_logs_type/$", views.update_logs_type), + re_path(r"^db_fix_after_2_0_3/$", views.db_fix_after_2_0_3), + re_path( + r"^db_fix_ticket_end_at_after_2_0_5/$", views.db_fix_ticket_end_at_after_2_0_5 + ), + re_path(r"^db_fix_deal_time_after_2_0_5/$", views.db_fix_deal_time_after_2_0_5), + re_path(r"^db_fix_after_2_0_7/$", views.db_fix_after_2_0_7), + re_path(r"^db_fix_after_2_0_9/$", views.db_fix_after_2_0_9), + re_path(r"^db_fix_after_2_0_14/$", views.db_fix_after_2_0_14), + re_path(r"^db_fix_after_2_1_x/$", views.db_fix_after_2_1_x), + re_path(r"^db_fix_after_2_1_1/$", views.db_fix_after_2_1_1), + re_path(r"^db_fix_sla/$", views.db_fix_sla), + re_path(r"^db_fix_after_2_1_9/$", views.db_fix_after_2_1_9), + re_path(r"^export_api_system/$", views.export_api_system), + re_path(r"^db_fix_for_attachments/$", views.db_fix_for_attachments), + re_path(r"^db_fix_for_service_catalog/$", views.db_fix_for_service_catalog), + re_path(r"^weekly_statical/$", views.weekly_statical), + re_path( + r"^db_fix_for_workflow_after_2_5_9/$", views.db_fix_for_workflow_after_2_5_9 + ), + re_path( + r"^db_fix_for_blueapps_after_2_6_0/$", views.db_fix_for_blueapps_after_2_6_0 + ), # 获取settings内容 ] # urlpatterns += [ -# url(r'^dump_db/$', views_common.dump_db), -# url(r'^drop_table/$', views_common.drop_table), +# re_path(r'^dump_db/$', views_common.dump_db), +# re_path(r'^drop_table/$', views_common.drop_table), # ] diff --git a/itsm/helper/views_common.py b/itsm/helper/views_common.py index d9b2862bd..93617401b 100644 --- a/itsm/helper/views_common.py +++ b/itsm/helper/views_common.py @@ -33,55 +33,57 @@ from django.contrib.auth.decorators import permission_required from django.db import connection from django.http import HttpResponse -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ -@permission_required('is_superuser') +@permission_required("is_superuser") def dump_db(request): """ dbdump操作 """ - db = settings.DATABASES['default'] - dbfile = 'static/{}.sql'.format(db.get('NAME')) - dumpcmd = 'mysqldump' + db = settings.DATABASES["default"] + dbfile = "static/{}.sql".format(db.get("NAME")) + dumpcmd = "mysqldump" dumpdb = ( - '{dumpcmd} --user={user} ' - '--password={password} ' - '--host={host} ' - '--port={port} ' - '--single-transaction ' - '{dbname} > {dbfile}'.format( + "{dumpcmd} --user={user} " + "--password={password} " + "--host={host} " + "--port={port} " + "--single-transaction " + "{dbname} > {dbfile}".format( dumpcmd=dumpcmd, - user=db.get('USER'), - password=db.get('PASSWORD'), - host=db.get('HOST'), - port=db.get('HOST'), - dbname=db.get('NAME'), + user=db.get("USER"), + password=db.get("PASSWORD"), + host=db.get("HOST"), + port=db.get("HOST"), + dbname=db.get("NAME"), dbfile=dbfile, ) ) - p = subprocess.Popen(dumpdb, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + p = subprocess.Popen( + dumpdb, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) out, err = p.communicate() try: - with open(dbfile, 'rb') as fd: + with open(dbfile, "rb") as fd: file_content = fd.read() response = HttpResponse(file_content) - response['Content-Type'] = 'application/octet-stream' - response['Content-Disposition'] = 'attachment;filename="%s_%s.sql"' % ( - db.get('NAME'), + response["Content-Type"] = "application/octet-stream" + response["Content-Disposition"] = 'attachment;filename="%s_%s.sql"' % ( + db.get("NAME"), datetime.datetime.now(), ) except IOError: - return HttpResponse(_('

磁盘中不存在该文件!

')) + return HttpResponse(_("

磁盘中不存在该文件!

")) except Exception as e: - return HttpResponse(_('

系统异常!


%s

') % e) + return HttpResponse(_("

系统异常!


%s

") % e) return response -@permission_required('is_superuser') +@permission_required("is_superuser") def drop_table(request): """ 清空表,危险操作 @@ -100,6 +102,6 @@ def drop_table(request): drop_table_sql = "drop table " + table_name # +" if exists "+table_name cursor.execute(drop_table_sql) - return HttpResponse(_('命令执行成功')) + return HttpResponse(_("命令执行成功")) except Exception as e: - return HttpResponse(_('命令执行异常:%s') % e) + return HttpResponse(_("命令执行异常:%s") % e) diff --git a/itsm/iadmin/contants.py b/itsm/iadmin/contants.py index 0f7994861..b037c225e 100644 --- a/itsm/iadmin/contants.py +++ b/itsm/iadmin/contants.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( EMAIL, @@ -392,7 +392,9 @@ """ -TASK_GENERAL_CONTENT_COMMON = TASK_SMS_CONTENT_COMMON = TASK_WEIXIN_CONTENT_COMMON = """ +TASK_GENERAL_CONTENT_COMMON = TASK_SMS_CONTENT_COMMON = ( + TASK_WEIXIN_CONTENT_COMMON +) = """ 标题:${title} 单号:${sn} 任务名称:${task_name} @@ -402,18 +404,24 @@ 任务创建时间:${task_create_at} ${message}""" -SMS_CONTENT_COMMON = WEIXIN_CONTENT_COMMON = """ +SMS_CONTENT_COMMON = ( + WEIXIN_CONTENT_COMMON +) = """ 标题:${title} 单号:${sn} ${message}""" -GENERAL_CONTENT_DONE = SMS_CONTENT_DONE = WEIXIN_CONTENT_DONE = """您的需求(${title})已经处理完成, +GENERAL_CONTENT_DONE = SMS_CONTENT_DONE = ( + WEIXIN_CONTENT_DONE +) = """您的需求(${title})已经处理完成, 现邀请您为我们的服务进行评价。 您的反馈对我们非常重要! 感谢回复与建议,祝您工作愉快! ${ticket_url}""" # noqa -GENERAL_CONTENT_FOLLOW = SMS_CONTENT_FOLLOW = WEIXIN_CONTENT_FOLLOW = """你有一条${service_type_name}工单需要关注 +GENERAL_CONTENT_FOLLOW = SMS_CONTENT_FOLLOW = ( + WEIXIN_CONTENT_FOLLOW +) = """你有一条${service_type_name}工单需要关注 标题:${title} 单号:${sn} 服务目录:${catalog_service_name} @@ -425,21 +433,27 @@ 服务目录:${catalog_service_name} 当前环节:${running_status}""" -SMS_CONTENT_FAILED = WEIXIN_CONTENT_FAILED = """ +SMS_CONTENT_FAILED = ( + WEIXIN_CONTENT_FAILED +) = """ 节点自动执行失败 标题:${title} 单号:${sn} 当前环节:${running_status} 失败信息:${message}""" -GENERAL_CONTENT_OPERATE = WEIXIN_CONTENT_OPERATE = """ +GENERAL_CONTENT_OPERATE = ( + WEIXIN_CONTENT_OPERATE +) = """ 标题:${title} 单号:${sn} 服务目录:${catalog_service_name} 当前环节:${running_status} """ -GENERAL_CONTENT_COMMON = ATTENTION_SMS_CONTENT_COMMON = ATTENTION_WEIXIN_CONTENT_COMMON = """ +GENERAL_CONTENT_COMMON = ATTENTION_SMS_CONTENT_COMMON = ( + ATTENTION_WEIXIN_CONTENT_COMMON +) = """ 标题:${title} 单号:${sn} 服务:${catalog_service_name} diff --git a/itsm/iadmin/forms.py b/itsm/iadmin/forms.py index e615a7c07..c25ff1f3f 100644 --- a/itsm/iadmin/forms.py +++ b/itsm/iadmin/forms.py @@ -25,14 +25,18 @@ from django import forms -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import LEN_LONG class CustomNotifyForm(forms.Form): title_template = forms.CharField( - max_length=LEN_LONG, label=_("标题模板"), help_text=_("工单字段的值可以作为参数写到模板中,格式如:【ITSM】${service}管理单【${action}】提醒") + max_length=LEN_LONG, + label=_("标题模板"), + help_text=_( + "工单字段的值可以作为参数写到模板中,格式如:【ITSM】${service}管理单【${action}】提醒" + ), ) content_template = forms.CharField( widget=forms.Textarea(attrs={"cols": "120", "rows": "20"}), diff --git a/itsm/iadmin/models.py b/itsm/iadmin/models.py index 24075942d..164479459 100644 --- a/itsm/iadmin/models.py +++ b/itsm/iadmin/models.py @@ -30,7 +30,7 @@ import mistune from django.db import models from django.db.models import QuerySet -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from config.default import PROJECT_ROOT from itsm.component.constants import ( @@ -69,7 +69,10 @@ class Model(models.Model): _objects = models.Manager() - auth_resource = {"resource_type": "system_settings", "resource_type_name": "系统配置"} + auth_resource = { + "resource_type": "system_settings", + "resource_type_name": "系统配置", + } resource_operations = ["system_settings_manage"] class Meta: @@ -87,7 +90,9 @@ def hard_delete(self, using=None): class SystemSettings(Model): type = models.CharField(_("类型"), max_length=LEN_NORMAL) key = models.CharField(_("关键字唯一标识"), max_length=LEN_NORMAL, unique=True) - value = models.TextField(_("系统设置值"), default=EMPTY_STRING, null=True, blank=True) + value = models.TextField( + _("系统设置值"), default=EMPTY_STRING, null=True, blank=True + ) objects = managers.Manager() @@ -122,7 +127,9 @@ class CustomNotice(models.Model): default="", null=True, blank=True, - help_text=_("工单字段的值可以作为参数写到模板中,格式如:【ITSM】${service}管理单【${action}】提醒"), + help_text=_( + "工单字段的值可以作为参数写到模板中,格式如:【ITSM】${service}管理单【${action}】提醒" + ), ) content_template = models.TextField( _("内容模板"), @@ -132,7 +139,10 @@ class CustomNotice(models.Model): help_text=_("工单字段的值可以作为参数写到模板中,格式如:单号:${sn}"), ) action = models.CharField( - _("通知模板类型"), max_length=LEN_SHORT, choices=ACTION_CHOICES, default="default" + _("通知模板类型"), + max_length=LEN_SHORT, + choices=ACTION_CHOICES, + default="default", ) notify_type = models.CharField(_("通知方式"), max_length=LEN_SHORT, default="EMAIL") create_at = models.DateTimeField(_("创建时间"), auto_now_add=True) @@ -142,7 +152,10 @@ class CustomNotice(models.Model): version = models.CharField(_("版本"), max_length=LEN_SHORT, default="V1") project_key = models.CharField( - _("项目key"), max_length=LEN_SHORT, null=False, default=PUBLIC_PROJECT_PROJECT_KEY + _("项目key"), + max_length=LEN_SHORT, + null=False, + default=PUBLIC_PROJECT_PROJECT_KEY, ) auth_resource = {"resource_type": "project", "resource_type_name": "项目"} @@ -331,7 +344,9 @@ class Data(models.Model): type = models.CharField( _("类型"), choices=TYPE_CHOICES, default="string", max_length=LEN_SHORT ) - expire_at = models.DateTimeField(_("过期时间"), null=True, blank=True, db_index=True) + expire_at = models.DateTimeField( + _("过期时间"), null=True, blank=True, db_index=True + ) objects = DataManager() diff --git a/itsm/iadmin/permissions.py b/itsm/iadmin/permissions.py index 405b94893..30379acc4 100644 --- a/itsm/iadmin/permissions.py +++ b/itsm/iadmin/permissions.py @@ -25,7 +25,7 @@ from functools import wraps -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import permissions from rest_framework.exceptions import MethodNotAllowed @@ -138,16 +138,18 @@ class CustomNotifyPermit(IamAuthPermit): def has_permission(self, request, view): if view.action in getattr(view, "permission_free_actions", []): return True - + # 获取项目标识 if view.action in ["list"]: - project_key = request.query_params.get("project_key", PUBLIC_PROJECT_PROJECT_KEY) + project_key = request.query_params.get( + "project_key", PUBLIC_PROJECT_PROJECT_KEY + ) elif view.action in ["destroy"]: instance = view.get_object() project_key = instance.project_key else: project_key = request.data.get("project_key", PUBLIC_PROJECT_PROJECT_KEY) - + # 平台管理 if project_key == PUBLIC_PROJECT_PROJECT_KEY: # 平台管理限制创建新通知规则 @@ -155,7 +157,7 @@ def has_permission(self, request, view): raise MethodNotAllowed(request.method) apply_actions = ["notification_view", "platform_manage_access"] return self.iam_auth(request, apply_actions) - + # 项目管理 project = Project.objects.get(pk=project_key) apply_actions = ["system_settings_manage"] @@ -167,12 +169,12 @@ def has_object_permission(self, request, view, obj, **kwargs): # 平台管理限制删除 if view.action in ["destroy"]: raise MethodNotAllowed(request.method) - + apply_actions = ["notification_view", "platform_manage_access"] if view.action in ["update"]: apply_actions.append("notification_manage") return self.iam_auth(request, apply_actions) - + # 项目:通知配置 project = Project.objects.filter(pk=obj.project_key).first() return super().has_object_permission(request, view, project, **kwargs) diff --git a/itsm/iadmin/serializers.py b/itsm/iadmin/serializers.py index e473babd7..1fa12b394 100644 --- a/itsm/iadmin/serializers.py +++ b/itsm/iadmin/serializers.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.fields import empty @@ -134,7 +134,9 @@ def create(self, validated_data): project_key=project_key, used_by=used_by, ).exists(): - raise serializers.ValidationError(_("该项目下已存在相同的通知配置,不能重复添加")) + raise serializers.ValidationError( + _("该项目下已存在相同的通知配置,不能重复添加") + ) return super(CustomNotifySerializer, self).create(validated_data=validated_data) diff --git a/itsm/iadmin/tasks.py b/itsm/iadmin/tasks.py index b3d1d1433..a1dbc7e2d 100644 --- a/itsm/iadmin/tasks.py +++ b/itsm/iadmin/tasks.py @@ -23,13 +23,13 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from celery import task +from celery import shared_task from common.log import logger from itsm.iadmin.models import MigrateLogs -@task +@shared_task def db_fix_by_version_list(need_exe_func_list, migrate_id): """执行需要执行的函数""" migrate_log = MigrateLogs.objects.filter(id=migrate_id) @@ -38,8 +38,8 @@ def db_fix_by_version_list(need_exe_func_list, migrate_id): try: for item in need_exe_func_list: item() - migrate_log.update(is_finished=True, is_success=True, note='升级成功') + migrate_log.update(is_finished=True, is_success=True, note="升级成功") logger.info("db_fix_by_version_list success!") except Exception as e: - migrate_log.update(is_finished=True, is_success=False, note='升级失败') - logger.error('db_fix_by_version_list fail!, error: %s' % str(e)) + migrate_log.update(is_finished=True, is_success=False, note="升级失败") + logger.error("db_fix_by_version_list fail!, error: %s" % str(e)) diff --git a/itsm/iadmin/validators.py b/itsm/iadmin/validators.py index 2532dc35b..415233b69 100644 --- a/itsm/iadmin/validators.py +++ b/itsm/iadmin/validators.py @@ -28,7 +28,7 @@ from functools import reduce from django.conf import settings -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from itsm.component.esb.esbclient import client_backend @@ -129,7 +129,9 @@ def validate_is_organization(value): ).values_list("name", flat=True) if service_values.exists(): raise OrganizationStructureFunctionSwitchValidateError( - _("以下服务正在使用组织架构功能,请更改再关闭:{}").format(",".join(service_values)) + _("以下服务正在使用组织架构功能,请更改再关闭:{}").format( + ",".join(service_values) + ) ) @staticmethod @@ -149,14 +151,18 @@ def validate_child_ticket_switch(value): id__in=all_ticket_ids, current_status=PROCESS_RUNNING ).exists() if active_ticket: - raise ChildTicketSwitchValidateError(_("存在未完成的含有母子单的单据,请处理后再关闭")) + raise ChildTicketSwitchValidateError( + _("存在未完成的含有母子单的单据,请处理后再关闭") + ) @staticmethod def validate_task_switch(value): if value.get("value") == SWITCH_OFF: active_task = Task.objects.filter(status__in=ACTIVE_TASK_STATUS).exists() if active_task: - raise TaskSwitchValidateError(_("存在含有未完成任务的单据,请处理后再关闭")) + raise TaskSwitchValidateError( + _("存在含有未完成任务的单据,请处理后再关闭") + ) @staticmethod def validate_trigger_switch(value): @@ -166,7 +172,9 @@ def validate_trigger_switch(value): source_type__in=quoted_status ).exists() if active_trigger: - raise TriggerSwitchValidateError(_("存在被引用的触发器,请处理后再关闭")) + raise TriggerSwitchValidateError( + _("存在被引用的触发器,请处理后再关闭") + ) @staticmethod def validate_other(value): diff --git a/itsm/iadmin/views.py b/itsm/iadmin/views.py index 2039c3da0..b7909f5fd 100644 --- a/itsm/iadmin/views.py +++ b/itsm/iadmin/views.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.response import Response @@ -95,7 +95,7 @@ class CustomNotifyViewSet(ModelViewSet): serializer_class = CustomNotifySerializer queryset = CustomNotice.objects.all() pagination_class = None - + permission_classes = (CustomNotifyPermit,) permission_free_actions = ["variable_list", "action_type"] permission_action_default = "system_settings_manage" diff --git a/itsm/misc/urls.py b/itsm/misc/urls.py index becbdbb6c..d387183f4 100644 --- a/itsm/misc/urls.py +++ b/itsm/misc/urls.py @@ -23,12 +23,12 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.conf.urls import url +from django.urls import re_path from itsm.misc import views urlpatterns = [ - url(r"^upload_file/$", views.upload), - url(r"^download_file/$", views.download), - url(r"^clean_cache/$", views.clean_cache), + re_path(r"^upload_file/$", views.upload), + re_path(r"^download_file/$", views.download), + re_path(r"^clean_cache/$", views.clean_cache), ] diff --git a/itsm/misc/views.py b/itsm/misc/views.py index 204d45bb5..0079d3a45 100644 --- a/itsm/misc/views.py +++ b/itsm/misc/views.py @@ -36,7 +36,7 @@ from django.db import connection from django.http import StreamingHttpResponse from django.utils.encoding import escape_uri_path -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_GET, require_POST @@ -66,7 +66,9 @@ def clean_cache(request): # 更新用户角色表的更新时间,达到清理缓存目的 from itsm.role.models import BKUserRole - BKUserRole.objects.update(update_at=datetime.datetime.now() - datetime.timedelta(minutes=30)) + BKUserRole.objects.update( + update_at=datetime.datetime.now() - datetime.timedelta(minutes=30) + ) return Success(message=_("缓存更新成功")).json() except Exception as e: @@ -82,9 +84,15 @@ def compile_file_path(request): tmp_key = request.GET.get("key") or ("tmp_%s" % int(time.time())) system_file_path = SystemSettings.objects.get(key="SYS_FILE_PATH").value - file_prefix = request.GET.get("ticket_id") or "workflow_%s" % request.GET.get("workflow_id") + file_prefix = request.GET.get("ticket_id") or "workflow_%s" % request.GET.get( + "workflow_id" + ) - file_path = os.path.join(system_file_path, "%s_%s" % (file_prefix, request.GET.get("state_id", "")), tmp_key) + file_path = os.path.join( + system_file_path, + "%s_%s" % (file_prefix, request.GET.get("state_id", "")), + tmp_key, + ) return file_path, tmp_key @@ -140,6 +148,8 @@ def download(request): response = StreamingHttpResponse(FileWrapper(store.open(file_path, "rb"), 512)) response["Content-Type"] = "application/octet-stream" - response["Content-Disposition"] = "attachment; filename* = UTF-8''%s" % format(escape_uri_path(file_name)) + response["Content-Disposition"] = "attachment; filename* = UTF-8''%s" % format( + escape_uri_path(file_name) + ) return response diff --git a/itsm/monitor/urls.py b/itsm/monitor/urls.py index 3970f35ed..45a439be9 100644 --- a/itsm/monitor/urls.py +++ b/itsm/monitor/urls.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from django.conf.urls import url +from django.urls import re_path from itsm.monitor.views import healthz, ping urlpatterns = [ # main - url(r"^healthz/$", healthz), - url(r"ping/$", ping), + re_path(r"^healthz/$", healthz), + re_path(r"ping/$", ping), ] diff --git a/itsm/openapi/base_service/serializers.py b/itsm/openapi/base_service/serializers.py index 687b094cb..63d399274 100644 --- a/itsm/openapi/base_service/serializers.py +++ b/itsm/openapi/base_service/serializers.py @@ -3,7 +3,7 @@ from django.db import transaction from django.db.models.signals import post_save from rest_framework import serializers -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import DEFAULT_BK_BIZ_ID from itsm.component.exceptions import ParamError @@ -52,7 +52,11 @@ def create(self, validated_data): workflow_meta = validated_data.pop("workflow_meta") if Workflow.objects.filter(name=workflow_meta["name"]).exists(): raise serializers.ValidationError( - {str(_("参数校验失败")): _("系统中已存在同名流程,请尝试换个流程名称")} + { + str(_("参数校验失败")): _( + "系统中已存在同名流程,请尝试换个流程名称" + ) + } ) with TempDisableSignal(post_save, init_after_workflow_created, Workflow): work_flow_instance = Workflow.objects.create( @@ -163,7 +167,11 @@ def key_validate(self, value): flow_id=value.get("workflow").id, key=value.get("key") ).exists() ): - raise ParamError(_("当前流程已存在唯一标识【{}】,请重新输入").format(value.get("key"))) + raise ParamError( + _("当前流程已存在唯一标识【{}】,请重新输入").format( + value.get("key") + ) + ) class BatchSaveFieldSerializer(FieldSerializer): @@ -267,7 +275,9 @@ def run_validation(self, data): service_type=service.key, is_start=True ).key except TicketStatus.DoesNotExist: - raise serializers.ValidationError({_("工单状态"): _("工单状态不存在,请检查")}) + raise serializers.ValidationError( + {_("工单状态"): _("工单状态不存在,请检查")} + ) # 创建单据时,若没有传入creator参数,则采用request的当前用户 creator = data.get("creator", self.context["request"].user.username) diff --git a/itsm/openapi/base_service/validator.py b/itsm/openapi/base_service/validator.py index 598a9de1d..ccf4fc130 100644 --- a/itsm/openapi/base_service/validator.py +++ b/itsm/openapi/base_service/validator.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.ticket.validators import ( derive_validate, @@ -41,12 +41,20 @@ def create_validate(self, value, fields, username, **kwargs): flow = WorkflowVersion.objects.get(id=flow_id) except WorkflowVersion.DoesNotExist: raise CreateTicketError( - _("单据创建失败,flow_id 对应的流程版本不存在, flow_id={}".format(flow_id)) + _( + "单据创建失败,flow_id 对应的流程版本不存在, flow_id={}".format( + flow_id + ) + ) ) if flow.workflow_id != service.workflow.workflow_id: raise CreateTicketError( - _("单据创建失败,flow_id对应的流程与该服务绑定的流程不一致,flow_id:{}".format(flow_id)) + _( + "单据创建失败,flow_id对应的流程与该服务绑定的流程不一致,flow_id:{}".format( + flow_id + ) + ) ) state_id = str(flow.first_state["id"]) @@ -68,7 +76,9 @@ def create_validate(self, value, fields, username, **kwargs): lost_keys = required_keys - field_keys if lost_keys: - raise CreateTicketError(_("单据创建失败,缺少参数:{}".format(list(lost_keys)))) + raise CreateTicketError( + _("单据创建失败,缺少参数:{}".format(list(lost_keys))) + ) first_state_permission(fields, state, username) diff --git a/itsm/openapi/base_service/views/transition.py b/itsm/openapi/base_service/views/transition.py index cb7a3b887..d768c613a 100644 --- a/itsm/openapi/base_service/views/transition.py +++ b/itsm/openapi/base_service/views/transition.py @@ -2,7 +2,7 @@ from rest_framework.decorators import action from rest_framework.response import Response -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import START_STATE, TICKET_GLOBAL_VARIABLES diff --git a/itsm/openapi/base_service/views/workflow.py b/itsm/openapi/base_service/views/workflow.py index 92536ad4d..296e652a6 100644 --- a/itsm/openapi/base_service/views/workflow.py +++ b/itsm/openapi/base_service/views/workflow.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from rest_framework.decorators import action from rest_framework.response import Response -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from business_rules.operators import * # noqa from itsm.component.constants import * # noqa diff --git a/itsm/openapi/decorators.py b/itsm/openapi/decorators.py index 1432070b2..77c796971 100644 --- a/itsm/openapi/decorators.py +++ b/itsm/openapi/decorators.py @@ -3,7 +3,7 @@ from functools import wraps from rest_framework.response import Response -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from itsm.component.drf.exception import ValidationError diff --git a/itsm/openapi/devops_plugin/urls.py b/itsm/openapi/devops_plugin/urls.py index d49b2ae6f..b9c904be8 100644 --- a/itsm/openapi/devops_plugin/urls.py +++ b/itsm/openapi/devops_plugin/urls.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from django.conf.urls import url +from django.urls import re_path from itsm.openapi.devops_plugin import views urlpatterns = [ # main - url(r"^devops_plugin/services/$", views.services), - url(r"^devops_plugin/fields/$", views.service_fields), + re_path(r"^devops_plugin/services/$", views.services), + re_path(r"^devops_plugin/fields/$", views.service_fields), ] diff --git a/itsm/openapi/management/commands/sync_saas_apigw.py b/itsm/openapi/management/commands/sync_saas_apigw.py index 325ae0366..233486ff2 100644 --- a/itsm/openapi/management/commands/sync_saas_apigw.py +++ b/itsm/openapi/management/commands/sync_saas_apigw.py @@ -26,24 +26,6 @@ def handle(self, *args, **kwargs): print("[bk-itsm]current version is not open v3,skip sync_saas_apigw") return - print("[bk-itsm]call fetch_apigw_public_key") - try: - call_command("fetch_apigw_public_key") - except Exception: - print( - "[bk-itsm]this env has not bk-itsm esb api,skip fetch_apigw_public_key " - ) - traceback.print_exc() - - print("[bk-itsm]call fetch_esb_public_key") - try: - call_command("fetch_esb_public_key") - except Exception: - print( - "[bk-itsm]this env has not bk-itsm esb api,skip fetch_esb_public_key " - ) - traceback.print_exc() - if ( settings.IS_OPEN_V3 and settings.ENGINE_REGION == "default" @@ -90,3 +72,21 @@ def handle(self, *args, **kwargs): ) print("[bk-itsm] migrate apigw success") + + print("[bk-itsm]call fetch_apigw_public_key") + try: + call_command("fetch_apigw_public_key") + except Exception: + print( + "[bk-itsm]this env has not bk-itsm esb api,skip fetch_apigw_public_key " + ) + traceback.print_exc() + + print("[bk-itsm]call fetch_esb_public_key") + try: + call_command("fetch_esb_public_key") + except Exception: + print( + "[bk-itsm]this env has not bk-itsm esb api,skip fetch_esb_public_key " + ) + traceback.print_exc() diff --git a/itsm/openapi/service/views.py b/itsm/openapi/service/views.py index 55a54849c..3224b669d 100644 --- a/itsm/openapi/service/views.py +++ b/itsm/openapi/service/views.py @@ -25,7 +25,7 @@ from django.db import transaction from django.db.models import Q from django.utils.decorators import method_decorator -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.response import Response diff --git a/itsm/openapi/tasks.py b/itsm/openapi/tasks.py index ed684d428..e60e6d1e4 100644 --- a/itsm/openapi/tasks.py +++ b/itsm/openapi/tasks.py @@ -25,7 +25,7 @@ import time -from celery.task import periodic_task +from blueapps.contrib.celery_tools.periodic import periodic_task from celery.schedules import crontab from common.redis import Cache diff --git a/itsm/openapi/ticket/serializers.py b/itsm/openapi/ticket/serializers.py index 4de4e3bc6..a0993f778 100644 --- a/itsm/openapi/ticket/serializers.py +++ b/itsm/openapi/ticket/serializers.py @@ -25,7 +25,7 @@ import random import string -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from itsm.component.constants import ( diff --git a/itsm/openapi/ticket/tasks.py b/itsm/openapi/ticket/tasks.py index 35ce5ac4a..7c6a4c8c6 100644 --- a/itsm/openapi/ticket/tasks.py +++ b/itsm/openapi/ticket/tasks.py @@ -1,8 +1,8 @@ -from celery.task import task +from celery import shared_task from common.log import logger -@task +@shared_task def openapi_start_ticket(ticket, fields, from_ticket_id=None): try: logger.info( diff --git a/itsm/openapi/ticket/validators.py b/itsm/openapi/ticket/validators.py index a31aaccb7..d5397d393 100644 --- a/itsm/openapi/ticket/validators.py +++ b/itsm/openapi/ticket/validators.py @@ -25,7 +25,7 @@ from __future__ import absolute_import, unicode_literals -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from itsm.component.exceptions import ParamError @@ -52,16 +52,22 @@ def openapi_operate_validate(username, ticket, state_id=None, action_type=None): status = ticket.status(state_id) if not status: - raise serializers.ValidationError(_("抱歉,没有找到该节点:{}").format(state_id)) + raise serializers.ValidationError( + _("抱歉,没有找到该节点:{}").format(state_id) + ) if status.status == "FINISHED": - raise serializers.ValidationError(_("抱歉,当前节点:{} 已经结束").format(state_id)) + raise serializers.ValidationError( + _("抱歉,当前节点:{} 已经结束").format(state_id) + ) if not status.can_operate(username, action_type): raise serializers.ValidationError( _("抱歉,{}不能操作该节点(没有权限或操作类型不支持)").format(username) ) else: if not ticket.can_operate(username): - raise serializers.ValidationError(_("抱歉,{}无权操作该单据").format(username)) + raise serializers.ValidationError( + _("抱歉,{}无权操作该单据").format(username) + ) def openapi_suspend_validate(ticket): @@ -90,7 +96,9 @@ def edit_field_validate(ticket, field, **kwargs): field_obj.key in ["title", "impact", "urgency", "priority"] or field_obj.state_id == ticket.first_state_id ): - raise ParamError(_("只允许修改提单节点的字段和内置字段, key={}".format(field["key"]))) + raise ParamError( + _("只允许修改提单节点的字段和内置字段, key={}".format(field["key"])) + ) key_value = { "params_%s" % field["key"]: format_exp_value(field["type"], field["_value"]) diff --git a/itsm/pipeline_plugins/components/collections/bk_plugin.py b/itsm/pipeline_plugins/components/collections/bk_plugin.py index 7fed27f7f..ba5506355 100644 --- a/itsm/pipeline_plugins/components/collections/bk_plugin.py +++ b/itsm/pipeline_plugins/components/collections/bk_plugin.py @@ -8,7 +8,7 @@ from itsm.pipeline_plugins.components.collections.webhook import ParamsBuilder from itsm.plugin_service.plugin_client import PluginServiceApiClient from pipeline.component_framework.component import Component -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( TRANSITION_OPERATE, @@ -157,7 +157,9 @@ def execute(self, data, parent_data): state = ticket.flow.get_state(state_id) variables = state["variables"].get("outputs", []) - error_message_template = "蓝鲸插件调用失败【{name}】执行失败,失败信息 {detail_message}" + error_message_template = ( + "蓝鲸插件调用失败【{name}】执行失败,失败信息 {detail_message}" + ) processors = ticket.current_processors[1:-1] current_node = ticket.node_status.get(state_id=state_id) @@ -216,8 +218,10 @@ def execute(self, data, parent_data): return False if not result: - err_message = "bk_plugin_info 请求失败,返回值非 true, message = {}".format( - resp.get("message") + err_message = ( + "bk_plugin_info 请求失败,返回值非 true, message = {}".format( + resp.get("message") + ) ) self.do_exit_plugins( ticket, @@ -276,7 +280,9 @@ def execute(self, data, parent_data): def schedule(self, data, parent_data, callback_data=None): - error_message_template = "蓝鲸插件调用失败【{name}】执行失败,失败信息 {detail_message}" + error_message_template = ( + "蓝鲸插件调用失败【{name}】执行失败,失败信息 {detail_message}" + ) ticket = Ticket.objects.get(id=parent_data.inputs.ticket_id) state_id = data.inputs.state_id diff --git a/itsm/pipeline_plugins/components/collections/itsm_auto.py b/itsm/pipeline_plugins/components/collections/itsm_auto.py index 73b031ce0..e79a1269c 100644 --- a/itsm/pipeline_plugins/components/collections/itsm_auto.py +++ b/itsm/pipeline_plugins/components/collections/itsm_auto.py @@ -33,7 +33,7 @@ from blueapps.utils.logger import logger_celery as logger from django.core.cache import cache from django.db import transaction -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( ACTION_DICT, @@ -128,22 +128,22 @@ def build_query_params(ticket, query_params, schema, method="POST"): for name in params_list: if name not in params: params[name] = "" - + logger.info( - f"[auto_state][{ticket.id}] build_query begin:" + - f"query_params=>{query_params}, params=>{params}" + f"[auto_state][{ticket.id}] build_query begin:" + + f"query_params=>{query_params}, params=>{params}" ) result, build_query_params = build_params_by_mako_template(query_params, params) if not result: logger.exception( - f"[auto_state][{ticket.id}] build_query exception:" + - f"result=>{result}, build_query_params=>{build_query_params}" + f"[auto_state][{ticket.id}] build_query exception:" + + f"result=>{result}, build_query_params=>{build_query_params}" ) return False, _("请求参数构造异常,详细信息: %s") % str(build_query_params) - + logger.info( - f"[auto_state][{ticket.id}] build_query done:" + - f"result=>{result}, build_query_params=>{build_query_params}" + f"[auto_state][{ticket.id}] build_query done:" + + f"result=>{result}, build_query_params=>{build_query_params}" ) # 引用变量的类型转换及参数整体schema校验 @@ -152,8 +152,8 @@ def build_query_params(ticket, query_params, schema, method="POST"): params_type_conversion(build_query_params, schema) except BaseException as e: logger.exception( - f"[auto_state][{ticket.id}] params_type_conversion exception:" + - f"build_query_params=>{build_query_params}, schema=>{schema}, e=>{e}" + f"[auto_state][{ticket.id}] params_type_conversion exception:" + + f"build_query_params=>{build_query_params}, schema=>{schema}, e=>{e}" ) return False, _("请求参数转换异常,详细信息: %s") % str(str(e)) @@ -161,8 +161,8 @@ def build_query_params(ticket, query_params, schema, method="POST"): jsonschema.validate(build_query_params, schema) except Exception as e: logger.exception( - f"[auto_state][{ticket.id}] validate exception:" + - f"build_query_params=>{build_query_params}, schema=>{schema}, e=>{e}" + f"[auto_state][{ticket.id}] validate exception:" + + f"build_query_params=>{build_query_params}, schema=>{schema}, e=>{e}" ) return False, _("请求参数校验异常,详细信息: %s") % str(str(e)) @@ -174,8 +174,8 @@ def get_rsp_content( if operate_info and json.loads(operate_info)["action"] == "MANUAL": ignore_params = ticket.node_status.get(state_id=state_id).ignore_params logger.info( - f"[auto_state][{ticket.id}][{state_id}] get_rsp_content" + - f"ignore_params=>{ignore_params}" + f"[auto_state][{ticket.id}][{state_id}] get_rsp_content" + + f"ignore_params=>{ignore_params}" ) return True, {"data": ignore_params} else: @@ -207,8 +207,8 @@ def do_exit_plugins( for field in ticket.get_output_fields(state_id): data.set_outputs("params_%s" % field["key"], field["value"]) logger.info( - f"[auto_state][{ticket.id}][{state_id}] do_exit_plugins::set_output" + - "key=>{}, value=>{}".format(field["key"], field["value"]) + f"[auto_state][{ticket.id}][{state_id}] do_exit_plugins::set_output" + + "key=>{}, value=>{}".format(field["key"], field["value"]) ) if not operator_info: @@ -226,9 +226,7 @@ def do_exit_plugins( operate_type = detail["action"].upper() action = API_DICT.get(detail["action"].upper()) if state_status == FAILED: - log_message = ( - "{operator}{action}单据任务【{name}】执行失败:({detail_message})." - ) + log_message = "{operator}{action}单据任务【{name}】执行失败:({detail_message})." else: log_message = "{operator}{action}单据任务【{name}】执行成功." node_status = ticket.status(state_id) @@ -272,8 +270,8 @@ def do_exit_plugins( self.update_status(ticket, state_id, state_status, ex_data) if state_status == FAILED: logger.exception( - f"[auto_state][{ticket.id}][{state_id}] do_exit_plugins failed:" + - f"state_status=>{state_status}, ex_data={ex_data}" + f"[auto_state][{ticket.id}][{state_id}] do_exit_plugins failed:" + + f"state_status=>{state_status}, ex_data={ex_data}" ) ticket.node_status.filter(state_id=state_id).update( action_type=TRANSITION_OPERATE @@ -302,12 +300,12 @@ def execute(self, data, parent_data): return True ticket_id = parent_data.inputs.ticket_id state_id = data.inputs.state_id - + logger.info( - f"[auto_state][{ticket_id}][{state_id}] execute init:" + - f"data=>{data.inputs}, parent_data=>{parent_data.inputs}" + f"[auto_state][{ticket_id}][{state_id}] execute init:" + + f"data=>{data.inputs}, parent_data=>{parent_data.inputs}" ) - + ticket = Ticket.objects.get(id=ticket_id) state = ticket.flow.get_state(state_id) variables = state["variables"].get("outputs", []) @@ -316,7 +314,13 @@ def execute(self, data, parent_data): api_instance = RemoteApiInstance.objects.get(id=state["api_instance_id"]) except RemoteApiInstance.DoesNotExist: self.do_exit_plugins( - ticket, state_id, FAILED, _("对应的api配置不存在,请查询"), {}, variables, data + ticket, + state_id, + FAILED, + _("对应的api配置不存在,请查询"), + {}, + variables, + data, ) return True # 更新单据状态 @@ -359,12 +363,12 @@ def execute(self, data, parent_data): result, query_params = self.build_query_params( ticket, schedule_query_params, schema, remote_api.method ) - + logger.info( - f"[auto_state]y[{ticket_id}][{state_id}] execute build_query_params:" + - f"result=>{result}, query_params=>{query_params}" + f"[auto_state]y[{ticket_id}][{state_id}] execute build_query_params:" + + f"result=>{result}, query_params=>{query_params}" ) - + node_status.query_params = query_params node_status.save() if not result: @@ -409,12 +413,12 @@ def schedule(self, data, parent_data, callback_data=None): ticket = Ticket.objects.get(id=ticket_id) variables = data.outputs.get("variables") operate_info = cache.get("node_retry_{}_{}".format(ticket_id, state_id)) - + logger.info( - f"[auto_state][{ticket_id}][{state_id}]schedule init:" + - f"operate_info=>{operate_info}" + f"[auto_state][{ticket_id}][{state_id}]schedule init:" + + f"operate_info=>{operate_info}" ) - + # 补充ticket/state/api_instance_id信息 api_config.update( ticket_id=ticket_id, @@ -434,8 +438,8 @@ def schedule(self, data, parent_data, callback_data=None): return True logger.info( - f"[auto_state][{ticket_id}][{state_id}] schedule polling:" + - f"poll_times=>{poll_time}, latest_poll_time=>{latest_poll_time}" + f"[auto_state][{ticket_id}][{state_id}] schedule polling:" + + f"poll_times=>{poll_time}, latest_poll_time=>{latest_poll_time}" ) # 如果为轮询并且时间超过上一次的轮询时间 @@ -446,12 +450,12 @@ def schedule(self, data, parent_data, callback_data=None): success_conditions, operate_info, ) - + logger.info( - f"[auto_state][{ticket_id}][{state_id}] schedule curl: " + - f"api_config=>{api_config}, p_result=>{p_result}, rsp=>{p_rsp}" + f"[auto_state][{ticket_id}][{state_id}] schedule curl: " + + f"api_config=>{api_config}, p_result=>{p_result}, rsp=>{p_rsp}" ) - + poll_time -= 1 if p_result: # 返回为True的时候,直接结束 @@ -468,8 +472,8 @@ def schedule(self, data, parent_data, callback_data=None): return True if poll_time <= 0: logger.error( - f"[auto_state][{ticket_id}][{state_id}] schedule polling error:" + - f"response={p_rsp}" + f"[auto_state][{ticket_id}][{state_id}] schedule polling error:" + + f"response={p_rsp}" ) self.do_exit_plugins( ticket=ticket, @@ -487,7 +491,9 @@ def schedule(self, data, parent_data, callback_data=None): data.set_outputs("latest_poll_time", datetime.now()) return True except Exception as e: - logger.exception(f"[auto_state] data=>{data}, callback=>{callback_data}, e=>{e}") + logger.exception( + f"[auto_state] data=>{data}, callback=>{callback_data}, e=>{e}" + ) raise e def outputs_format(self): diff --git a/itsm/pipeline_plugins/components/collections/tasks.py b/itsm/pipeline_plugins/components/collections/tasks.py index cce8fd5ce..81c3c980b 100644 --- a/itsm/pipeline_plugins/components/collections/tasks.py +++ b/itsm/pipeline_plugins/components/collections/tasks.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from celery.task import task +from celery import shared_task from itsm.ticket.models import SignTask from pipeline.engine.api import activity_callback @@ -31,13 +31,17 @@ from common.log import logger -@task +@shared_task def auto_approve(node_status_id, creator, activity_id, callback_data): try: # 如果存在这条任务,证明有其他用户在页面或者api执行了审批任务,无需自动过单 SignTask.objects.get(status_id=node_status_id) except SignTask.DoesNotExist: - logger.info("正在创建自动过单任务, node_status_id={}, creator={}".format(node_status_id, creator)) + logger.info( + "正在创建自动过单任务, node_status_id={}, creator={}".format( + node_status_id, creator + ) + ) SignTask.objects.update_or_create( status_id=node_status_id, processor=creator, defaults={"status": "RUNNING"} ) diff --git a/itsm/pipeline_plugins/components/collections/webhook.py b/itsm/pipeline_plugins/components/collections/webhook.py index d74a8cf9f..26e3a93a3 100644 --- a/itsm/pipeline_plugins/components/collections/webhook.py +++ b/itsm/pipeline_plugins/components/collections/webhook.py @@ -32,7 +32,7 @@ from jinja2 import Template from pipeline.utils.boolrule import BoolRule from pipeline.component_framework.component import Component -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( TRANSITION_OPERATE, @@ -222,7 +222,9 @@ def execute(self, data, parent_data): state = ticket.flow.get_state(state_id) variables = state["variables"].get("outputs", []) - error_message_template = "WebHook任务【{name}】执行失败,失败信息 {detail_message}" + error_message_template = ( + "WebHook任务【{name}】执行失败,失败信息 {detail_message}" + ) processors = ticket.current_processors[1:-1] current_node = ticket.node_status.get(state_id=state_id) diff --git a/itsm/plugin_service/README.md b/itsm/plugin_service/README.md index 50940eaba..f31f78f8c 100644 --- a/itsm/plugin_service/README.md +++ b/itsm/plugin_service/README.md @@ -22,7 +22,7 @@ INSTALLED_APPS += ( ``` python urlpatterns = [ ..., - url(r"^plugin_service/", include("plugin_service.urls")), + re_path(r"^plugin_service/", include("plugin_service.urls")), ..., ] ``` diff --git a/itsm/plugin_service/docs/openapi_config.md b/itsm/plugin_service/docs/openapi_config.md index 72abbd542..6295c249e 100644 --- a/itsm/plugin_service/docs/openapi_config.md +++ b/itsm/plugin_service/docs/openapi_config.md @@ -16,9 +16,9 @@ ) urlpatterns += [ - url(r"^swagger(?P\.json|\.yaml)$", schema_view.without_ui(cache_timeout=0), name="schema-json"), - url(r"^swagger/$", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), - url(r"^redoc/$", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), + re_path(r"^swagger(?P\.json|\.yaml)$", schema_view.without_ui(cache_timeout=0), name="schema-json"), + re_path(r"^swagger/$", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), + re_path(r"^redoc/$", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), ] ``` diff --git a/itsm/plugin_service/urls.py b/itsm/plugin_service/urls.py index 7f3d4ced3..305b35604 100644 --- a/itsm/plugin_service/urls.py +++ b/itsm/plugin_service/urls.py @@ -11,19 +11,19 @@ specific language governing permissions and limitations under the License. """ -from django.conf.urls import url +from django.urls import re_path from . import api urlpatterns = [ - url(r"^list/$", api.get_plugin_list), - url(r"^detail_list/$", api.get_plugin_detail_list), - url(r"^meta/$", api.get_meta), - url(r"^detail/$", api.get_plugin_detail), - url(r"^logs/$", api.get_logs), - url(r"^app_detail/$", api.get_plugin_app_detail), - url( + re_path(r"^list/$", api.get_plugin_list), + re_path(r"^detail_list/$", api.get_plugin_detail_list), + re_path(r"^meta/$", api.get_meta), + re_path(r"^detail/$", api.get_plugin_detail), + re_path(r"^logs/$", api.get_logs), + re_path(r"^app_detail/$", api.get_plugin_app_detail), + re_path( r"^data_api/(?P.+?)/(?P.+)$", api.get_plugin_api_data, ), diff --git a/itsm/postman/models.py b/itsm/postman/models.py index 8a367db8c..d9b736b98 100644 --- a/itsm/postman/models.py +++ b/itsm/postman/models.py @@ -31,7 +31,7 @@ from django.conf import settings from django.db import models from django.forms import model_to_dict -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from itsm.component.constants import ( @@ -103,14 +103,22 @@ class RemoteSystem(Model): _("系统描述"), max_length=LEN_LONG, default=EMPTY_STRING, null=True, blank=True ) owners = models.CharField( - _("系统责任人"), max_length=LEN_NORMAL, default=EMPTY_STRING, null=True, blank=True + _("系统责任人"), + max_length=LEN_NORMAL, + default=EMPTY_STRING, + null=True, + blank=True, ) contact_information = models.TextField(_("联系方式"), blank=True) is_builtin = models.BooleanField(_("是否内置系统"), default=False) # 公共配置信息 domain = models.CharField( - _("系统域名"), max_length=LEN_XX_LONG, default=EMPTY_STRING, null=True, blank=True + _("系统域名"), + max_length=LEN_XX_LONG, + default=EMPTY_STRING, + null=True, + blank=True, ) is_activated = models.BooleanField(_("是否启用"), default=False) headers = jsonfield.JSONField( @@ -279,7 +287,9 @@ def init_default_remote_api(cls): try: cls.restore_api(api, "system", True) except Exception as e: - logger.info("创建默认api失败:%s api name %s" % (str(e), api["name"])) + logger.info( + "创建默认api失败:%s api name %s" % (str(e), api["name"]) + ) def tag_data(self): """Api数据""" @@ -413,9 +423,9 @@ def get_config(self): "rsp_data": self.rsp_data, "map_code": self.map_code, "before_req": self.before_req, - "query_params": self.req_body - if remote_api.method == "POST" - else self.req_params, + "query_params": ( + self.req_body if remote_api.method == "POST" else self.req_params + ), } def get_api_choice(self, kv_relation, params): @@ -467,7 +477,9 @@ def get_api_choice(self, kv_relation, params): return { "result": False, "code": ResponseCodeStatus.OK, - "message": _("接口返回协议不符合规范,请确保接口协议符合蓝鲸规范。详见: Github->API功能使用说明"), + "message": _( + "接口返回协议不符合规范,请确保接口协议符合蓝鲸规范。详见: Github->API功能使用说明" + ), "data": [], } @@ -475,7 +487,10 @@ def get_api_choice(self, kv_relation, params): if rsp.get("code") == API_PERMISSION_ERROR_CODE: raise IamPermissionDenied( data=rsp["permission"], - detail=_("用户没有对应的第三方系统接口【%s】权限" % api_config.get("path")), + detail=_( + "用户没有对应的第三方系统接口【%s】权限" + % api_config.get("path") + ), ) return rsp diff --git a/itsm/postman/permissions.py b/itsm/postman/permissions.py index 741ccfdbb..25f2e1ea6 100644 --- a/itsm/postman/permissions.py +++ b/itsm/postman/permissions.py @@ -27,7 +27,7 @@ from itsm.component.drf import permissions as perm from itsm.component.exceptions import ValidateError from itsm.postman.models import RemoteSystem, RemoteApi -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.project.models import Project from itsm.workflow.permissions import WorkflowElementManagePermission @@ -50,12 +50,12 @@ def has_permission(self, request, view): # 平台公共API管理 if project_key == PUBLIC_PROJECT_PROJECT_KEY: return self.iam_auth(request, ["public_apis_manage"]) - + # 项目管理 apply_actions = ["system_settings_manage"] project = Project.objects.get(pk=project_key) return self.iam_auth(request, apply_actions, project) - + if view.action == "batch_delete": api_ids = request.data["id"].split(",") api_instances = RemoteApi.objects.filter(pk__in=api_ids) @@ -63,15 +63,15 @@ def has_permission(self, request, view): if len(project_keys) != 1: raise ValidateError(_("API 所属项目异常")) project_key = project_keys.pop() - + # 平台公共API管理 if project_key == PUBLIC_PROJECT_PROJECT_KEY: return self.iam_auth(request, ["public_apis_manage"]) - + # 项目 project = Project.objects.get(pk=project_key) return self.iam_auth(request, ["system_settings_manage"], project) - + return True def has_object_permission(self, request, view, obj, **kwargs): @@ -84,7 +84,7 @@ def has_object_permission(self, request, view, obj, **kwargs): if view.action == "retrieve": return True return self.iam_auth(request, ["public_apis_manage"]) - + # 项目管理 project_key = obj.remote_system.project_key project = Project.objects.get(pk=project_key) diff --git a/itsm/postman/rpc/components/demo.py b/itsm/postman/rpc/components/demo.py index cd35b88d7..528c2fb4b 100644 --- a/itsm/postman/rpc/components/demo.py +++ b/itsm/postman/rpc/components/demo.py @@ -24,7 +24,7 @@ """ from django import forms -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.dlls.component import BaseComponentForm from itsm.postman.rpc.core.component import BaseComponent diff --git a/itsm/postman/rpc/components/service_catalog.py b/itsm/postman/rpc/components/service_catalog.py index 401b41d06..ecc78edd3 100644 --- a/itsm/postman/rpc/components/service_catalog.py +++ b/itsm/postman/rpc/components/service_catalog.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.dlls.component import BaseComponentForm from itsm.component.exceptions import RpcAPIError diff --git a/itsm/postman/rpc/components/state_fields.py b/itsm/postman/rpc/components/state_fields.py index 2a7ecfc90..27d980f1b 100644 --- a/itsm/postman/rpc/components/state_fields.py +++ b/itsm/postman/rpc/components/state_fields.py @@ -24,7 +24,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django import forms diff --git a/itsm/postman/rpc/components/ticket_fields.py b/itsm/postman/rpc/components/ticket_fields.py index ed5b152b7..ba623bb02 100644 --- a/itsm/postman/rpc/components/ticket_fields.py +++ b/itsm/postman/rpc/components/ticket_fields.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django import forms @@ -39,8 +39,12 @@ class GetTicketFields(BaseComponent): code = TABLE_FIELDS class Form(BaseComponentForm): - trigger_source_id = forms.IntegerField(label=_("触发器源id"), required=True, initial="trigger source id") - trigger_source_type = forms.CharField(label=_("触发器源类型"), required=True, initial="trigger source type") + trigger_source_id = forms.IntegerField( + label=_("触发器源id"), required=True, initial="trigger source id" + ) + trigger_source_type = forms.CharField( + label=_("触发器源类型"), required=True, initial="trigger source type" + ) def clean(self): """数据清理""" @@ -49,14 +53,18 @@ def clean(self): def handle(self): payload = [] - if self.form_data.get("trigger_source_type") != 'workflow': + if self.form_data.get("trigger_source_type") != "workflow": self.response.payload = payload return try: - current_workflow = Workflow.objects.get(id=self.form_data.get("trigger_source_id")) + current_workflow = Workflow.objects.get( + id=self.form_data.get("trigger_source_id") + ) except Workflow.DoesNotExist: raise - payload = TemplateFieldSerializer(current_workflow.public_table_fields, many=True).data + payload = TemplateFieldSerializer( + current_workflow.public_table_fields, many=True + ).data self.response.payload = payload diff --git a/itsm/postman/rpc/components/ticket_states.py b/itsm/postman/rpc/components/ticket_states.py index e86772dfc..224df39a8 100644 --- a/itsm/postman/rpc/components/ticket_states.py +++ b/itsm/postman/rpc/components/ticket_states.py @@ -23,11 +23,17 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django import forms -from itsm.component.constants import FLOW_STATES, NORMAL_STATE, TASK_STATE, TASK_SOPS_STATE, SIGN_STATE +from itsm.component.constants import ( + FLOW_STATES, + NORMAL_STATE, + TASK_STATE, + TASK_SOPS_STATE, + SIGN_STATE, +) from itsm.component.dlls.component import BaseComponentForm from itsm.postman.rpc.core.component import BaseComponent from itsm.workflow.models import Workflow @@ -39,8 +45,12 @@ class GetTicketFields(BaseComponent): code = FLOW_STATES class Form(BaseComponentForm): - trigger_source_id = forms.IntegerField(label=_("触发器源id"), required=True, initial="trigger source id") - trigger_source_type = forms.CharField(label=_("触发器源类型"), required=True, initial="trigger source type") + trigger_source_id = forms.IntegerField( + label=_("触发器源id"), required=True, initial="trigger source id" + ) + trigger_source_type = forms.CharField( + label=_("触发器源类型"), required=True, initial="trigger source type" + ) def clean(self): """数据清理""" @@ -49,17 +59,20 @@ def clean(self): def handle(self): payload = [] - if self.form_data.get("trigger_source_type") != 'workflow': + if self.form_data.get("trigger_source_type") != "workflow": self.response.payload = payload return try: - current_workflow = Workflow.objects.get(id=self.form_data.get("trigger_source_id")) + current_workflow = Workflow.objects.get( + id=self.form_data.get("trigger_source_id") + ) except Workflow.DoesNotExist: raise payload = StateSerializer( current_workflow.states.filter( - type__in=[NORMAL_STATE, TASK_STATE, TASK_SOPS_STATE, SIGN_STATE], is_builtin=False + type__in=[NORMAL_STATE, TASK_STATE, TASK_SOPS_STATE, SIGN_STATE], + is_builtin=False, ), many=True, ).data diff --git a/itsm/postman/rpc/components/ticket_status.py b/itsm/postman/rpc/components/ticket_status.py index 118e1ba16..111c3a7c9 100644 --- a/itsm/postman/rpc/components/ticket_status.py +++ b/itsm/postman/rpc/components/ticket_status.py @@ -25,7 +25,7 @@ from collections import OrderedDict, defaultdict -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import STATUS, SERVICE_CATEGORY from itsm.component.dlls.component import BaseComponentForm diff --git a/itsm/postman/serializers.py b/itsm/postman/serializers.py index b3a02034f..ebfc9fe88 100644 --- a/itsm/postman/serializers.py +++ b/itsm/postman/serializers.py @@ -25,7 +25,7 @@ import json -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from mako.template import Template from rest_framework import serializers from rest_framework.fields import JSONField @@ -39,7 +39,10 @@ LEN_XX_LONG, PUBLIC_PROJECT_PROJECT_KEY, ) -from itsm.component.drf.serializers import DynamicFieldsModelSerializer, BaseModelSerializer +from itsm.component.drf.serializers import ( + DynamicFieldsModelSerializer, + BaseModelSerializer, +) from itsm.component.exceptions import ParamError from itsm.component.utils.basic import normal_name, dotted_name from itsm.postman.models import RemoteApi, RemoteApiInstance, RemoteSystem @@ -50,10 +53,14 @@ class RemoteSystemSerializer(BaseModelSerializer): """API系统序列化""" name = serializers.CharField( - max_length=LEN_NORMAL, required=True, error_messages={"blank": _("名称不能为空")} + max_length=LEN_NORMAL, + required=True, + error_messages={"blank": _("名称不能为空")}, ) code = serializers.CharField( - max_length=LEN_NORMAL, required=True, error_messages={"blank": _("编码不能为空")} + max_length=LEN_NORMAL, + required=True, + error_messages={"blank": _("编码不能为空")}, ) system_id = serializers.IntegerField(required=False) desc = serializers.CharField(max_length=LEN_LONG, required=False, allow_blank=True) @@ -124,14 +131,20 @@ class RemoteApiSerializer(DynamicFieldsModelSerializer): """API序列化""" name = serializers.CharField( - required=True, error_messages={"blank": _("名称不能为空")}, max_length=LEN_NORMAL + required=True, + error_messages={"blank": _("名称不能为空")}, + max_length=LEN_NORMAL, ) path = serializers.CharField( - required=True, error_messages={"blank": _("路径不能为空")}, max_length=LEN_X_LONG + required=True, + error_messages={"blank": _("路径不能为空")}, + max_length=LEN_X_LONG, ) version = serializers.CharField(required=False, max_length=LEN_SHORT) func_name = serializers.CharField( - required=True, error_messages={"blank": _("调用函数不能为空")}, max_length=LEN_NORMAL + required=True, + error_messages={"blank": _("调用函数不能为空")}, + max_length=LEN_NORMAL, ) method = serializers.ChoiceField( choices=[("GET", "GET"), ("POST", "POST")], default="GET" diff --git a/itsm/postman/urls.py b/itsm/postman/urls.py index df87d3a1e..daaa09732 100644 --- a/itsm/postman/urls.py +++ b/itsm/postman/urls.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.conf.urls import url +from django.urls import re_path from rest_framework.routers import DefaultRouter from itsm.postman.views import ( @@ -36,13 +36,13 @@ routers = DefaultRouter(trailing_slash=True) -routers.register(r'api_instance', ApiInstanceViewsSet, basename="api_instance") +routers.register(r"api_instance", ApiInstanceViewsSet, basename="api_instance") -routers.register(r'remote_system', RemoteSystemViewSet, basename='remote_system') +routers.register(r"remote_system", RemoteSystemViewSet, basename="remote_system") -routers.register(r'remote_api', RemoteApiViewSet, basename='remote_api') +routers.register(r"remote_api", RemoteApiViewSet, basename="remote_api") # APIView不能通过routers.register()的方式注入路由 urlpatterns = routers.urls + [ - url(r'^rpc_api/$', RpcApiViewSet.as_view()), + re_path(r"^rpc_api/$", RpcApiViewSet.as_view()), ] diff --git a/itsm/postman/views.py b/itsm/postman/views.py index 3bbd2b19e..1f85118bf 100644 --- a/itsm/postman/views.py +++ b/itsm/postman/views.py @@ -29,7 +29,7 @@ from django.db.models import Q from django.forms.forms import DeclarativeFieldsMetaclass from django.http import HttpResponse -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.response import Response @@ -99,7 +99,7 @@ class RemoteSystemViewSet(ModelViewSet): permission_action_default = "system_settings_manage" permission_resource_is_project = True permission_free_actions = ["list", "all", "get_systems", "get_components"] - + pagination_class = None filter_fields = { @@ -187,7 +187,7 @@ class RemoteApiViewSet(DynamicListModelMixin, ModelViewSet): serializer_class = RemoteApiSerializer queryset = RemoteApi.objects.all() - + permission_classes = (RemoteApiPermit,) permission_resource_is_project = True permission_create_action = ["create", "imports"] @@ -254,11 +254,11 @@ def batch_delete(self, request, *args, **kwargs): will_deleted = self.queryset.filter(id__in=id_list) real_deleted = list(will_deleted.values_list("id", flat=True)) - + # 判断输入的接口实例 if will_deleted.count() != len(real_deleted): raise ValidationError(_("接口数量异常,请刷新后重试")) - + # 检测实例所属项目是否一致 project_keys = [i.remote_system.project_key for i in will_deleted] if len(set(project_keys)) != 1: @@ -277,10 +277,10 @@ def exports(self, request, pk=None): data = api.tag_data() response = HttpResponse(content_type="application/octet-stream; charset=utf-8") - response[ - "Content-Disposition" - ] = "attachment; filename=bk_itsm_api_{}_{}.json".format( - api.func_name, datetime.datetime.now().strftime("%Y%m%d%H%M") + response["Content-Disposition"] = ( + "attachment; filename=bk_itsm_api_{}_{}.json".format( + api.func_name, datetime.datetime.now().strftime("%Y%m%d%H%M") + ) ) # 统一导入导出格式为列表数据 diff --git a/itsm/project/handler/migration_handler.py b/itsm/project/handler/migration_handler.py index c5fc2d1ce..29444de01 100644 --- a/itsm/project/handler/migration_handler.py +++ b/itsm/project/handler/migration_handler.py @@ -26,10 +26,14 @@ from abc import abstractmethod from django.db import transaction -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import DEFAULT_PROJECT_PROJECT_KEY -from itsm.component.exceptions import ResourceTypeNotFound, ProjectNotFound, NoMigratePermission +from itsm.component.exceptions import ( + ResourceTypeNotFound, + ProjectNotFound, + NoMigratePermission, +) from itsm.project.handler.utils import MigrateIamRequest from itsm.project.models import Project from itsm.service.models import Service, CatalogService, ServiceCatalog @@ -50,20 +54,27 @@ def handler(self, resource_id, old_project_key, new_project_key, request): def grant_or_revoke_instance_permission(self, request, actions, obj, operate): iam = MigrateIamRequest(request) - resources = [{ - "resource_id": obj.id, - "resource_name": obj.name, - "resource_type": obj.auth_resource.get("resource_type"), - "resource_type_name": obj.auth_resource.get("resource_type_name"), - }] - iam.grant_or_revoke_instance_permission(actions, resources, operate=operate, - project_key=obj.project_key) - - def grant_or_revoke_permit_with_project(self, request, actions, project_key, operate): + resources = [ + { + "resource_id": obj.id, + "resource_name": obj.name, + "resource_type": obj.auth_resource.get("resource_type"), + "resource_type_name": obj.auth_resource.get("resource_type_name"), + } + ] + iam.grant_or_revoke_instance_permission( + actions, resources, operate=operate, project_key=obj.project_key + ) + + def grant_or_revoke_permit_with_project( + self, request, actions, project_key, operate + ): iam = MigrateIamRequest(request) iam.grant_or_revoke_permit_with_project(actions, operate, project_key) - def iam_auth(self, request, apply_actions, obj=None, project_key=DEFAULT_PROJECT_PROJECT_KEY): + def iam_auth( + self, request, apply_actions, obj=None, project_key=DEFAULT_PROJECT_PROJECT_KEY + ): resources = [] if obj: @@ -71,17 +82,17 @@ def iam_auth(self, request, apply_actions, obj=None, project_key=DEFAULT_PROJECT { "resource_id": obj.id, "resource_name": getattr(obj, "name"), - "resource_type": obj.auth_resource['resource_type'], + "resource_type": obj.auth_resource["resource_type"], "creator": obj.creator, } ) iam_client = MigrateIamRequest(request) if resources: - auth_actions = iam_client.batch_resource_multi_actions_allowed(set(apply_actions), - resources, - project_key=project_key) - auth_actions = auth_actions.get(resources[0]['resource_id'], {}) + auth_actions = iam_client.batch_resource_multi_actions_allowed( + set(apply_actions), resources, project_key=project_key + ) + auth_actions = auth_actions.get(resources[0]["resource_id"], {}) else: auth_actions = iam_client.resource_multi_actions_allowed(apply_actions, []) @@ -106,8 +117,9 @@ class ServiceMigrationHandler(MigrationHandlerBase): resource_type = "service" def handler(self, resource_id, old_project_key, new_project_key, request): - ready_migrate_service = Service.objects.filter(project_key=old_project_key, - id=resource_id).first() + ready_migrate_service = Service.objects.filter( + project_key=old_project_key, id=resource_id + ).first() # 如果在默认项目下搜索不到该服务,则证明已经被迁移过了或者数据有问题,此时不进行迁移 if ready_migrate_service is None: @@ -116,24 +128,32 @@ def handler(self, resource_id, old_project_key, new_project_key, request): # 鉴权,用户是否有该资源的service_manage权限,有的话才可以进行迁移 if not self.iam_auth(request, actions, ready_migrate_service, old_project_key): - raise NoMigratePermission(_("您当前没有权限迁移该服务,您在权限中心没有该服务的权限,service_name={}". - format(ready_migrate_service.name))) + raise NoMigratePermission( + _( + "您当前没有权限迁移该服务,您在权限中心没有该服务的权限,service_name={}".format( + ready_migrate_service.name + ) + ) + ) # 先回收权限 - self.grant_or_revoke_instance_permission(request, actions, - ready_migrate_service, REVOKE) + self.grant_or_revoke_instance_permission( + request, actions, ready_migrate_service, REVOKE + ) # 获取该项目获取的catalog名称 catalog_name = ready_migrate_service.bounded_catalogs[0] # 如果对应的服务目录本身就有 - new_project_catalog = ServiceCatalog.objects.filter(project_key=new_project_key, - name=catalog_name).first() + new_project_catalog = ServiceCatalog.objects.filter( + project_key=new_project_key, name=catalog_name + ).first() if new_project_catalog is None: # 同步相关目录 service_catalog = ServiceCatalog.objects.filter( - id=ready_migrate_service.catalog_id).first() + id=ready_migrate_service.catalog_id + ).first() path_list = list(service_catalog.get_ancestors()) path_list.append(service_catalog) # 新生成的节点树的最后一个节点 @@ -141,7 +161,8 @@ def handler(self, resource_id, old_project_key, new_project_key, request): with transaction.atomic(): catalog_service = CatalogService.objects.filter( - service=ready_migrate_service).first() + service=ready_migrate_service + ).first() catalog_service.catalog = new_project_catalog catalog_service.save() ready_migrate_service.project_key = new_project_key @@ -156,18 +177,21 @@ def handler(self, resource_id, old_project_key, new_project_key, request): # 权限中心授权 actions = ["service_manage", "service_view", "ticket_view"] - self.grant_or_revoke_instance_permission(request, actions, ready_migrate_service, "grant") + self.grant_or_revoke_instance_permission( + request, actions, ready_migrate_service, "grant" + ) def sync_catalog_tree(self, path_list, new_project_key): # 同步该目录 same_catalog = self.find_same_node(path_list, new_project_key) same_catalog_index = path_list.index(same_catalog) - service_catalog = ServiceCatalog.objects.filter(project_key=new_project_key, - name=same_catalog.name).first() - for catalog in path_list[same_catalog_index + 1:]: - service_catalog = ServiceCatalog.create_catalog(name=catalog.name, - parent=service_catalog, - project_key=new_project_key) + service_catalog = ServiceCatalog.objects.filter( + project_key=new_project_key, name=same_catalog.name + ).first() + for catalog in path_list[same_catalog_index + 1 :]: + service_catalog = ServiceCatalog.create_catalog( + name=catalog.name, parent=service_catalog, project_key=new_project_key + ) return service_catalog def find_same_node(self, path_list, new_project_key): @@ -175,8 +199,9 @@ def find_same_node(self, path_list, new_project_key): 找到两个项目 服务目录的第一个共同的目录 """ for catalog in reversed(path_list): - if ServiceCatalog.objects.filter(project_key=new_project_key, - name=catalog.name).exists(): + if ServiceCatalog.objects.filter( + project_key=new_project_key, name=catalog.name + ).exists(): return catalog @@ -185,8 +210,9 @@ class UserGroupMigrationHandler(MigrationHandlerBase): def handler(self, resource_id, old_project_key, new_project_key, request): - user_role = UserRole.objects.filter(project_key=old_project_key, - id=resource_id).first() + user_role = UserRole.objects.filter( + project_key=old_project_key, id=resource_id + ).first() if user_role is None: return @@ -194,15 +220,21 @@ def handler(self, resource_id, old_project_key, new_project_key, request): if not request.user.username == user_role.creator: raise NoMigratePermission("权限迁移失败,请联系该用户组创建者进行迁移") actions = ["user_group_view", "user_group_edit", "user_group_delete"] - user_role.auth_resource = {"resource_type": "user_group", "resource_type_name": "用户组"} + user_role.auth_resource = { + "resource_type": "user_group", + "resource_type_name": "用户组", + } # 取消实例级别的授权 self.grant_or_revoke_instance_permission(request, actions, user_role, REVOKE) with transaction.atomic(): user_role.project_key = new_project_key user_role.save() - - user_role.auth_resource = {"resource_type": "user_group", "resource_type_name": "用户组"} + + user_role.auth_resource = { + "resource_type": "user_group", + "resource_type_name": "用户组", + } actions = ["user_group_view", "user_group_edit", "user_group_delete"] self.grant_or_revoke_instance_permission(request, actions, user_role, GRANT) @@ -215,15 +247,14 @@ class MigrationHandlerDispatcher(object): # 保留映射变量,便于直接从 object_class 找到对象定义 MIGRATIONS_HANDLER_CLASS_DICT = dict( - [(_object.resource_type, _object()) for _object in MIGRATIONS_HANDLER_CLASS]) + [(_object.resource_type, _object()) for _object in MIGRATIONS_HANDLER_CLASS] + ) def __init__(self, resource_type): self.resource_type = resource_type def migrate(self, resource_id, old_project_key, new_project_key, request): - """ - - """ + """ """ if not Project.objects.filter(key=old_project_key).exists(): raise ProjectNotFound() @@ -233,7 +264,6 @@ def migrate(self, resource_id, old_project_key, new_project_key, request): if self.resource_type not in self.MIGRATIONS_HANDLER_CLASS_DICT: raise ResourceTypeNotFound() - self.MIGRATIONS_HANDLER_CLASS_DICT[self.resource_type].handler(resource_id, - old_project_key, - new_project_key, - request) + self.MIGRATIONS_HANDLER_CLASS_DICT[self.resource_type].handler( + resource_id, old_project_key, new_project_key, request + ) diff --git a/itsm/project/models/base.py b/itsm/project/models/base.py index 747edf57e..598edc95a 100644 --- a/itsm/project/models/base.py +++ b/itsm/project/models/base.py @@ -25,18 +25,22 @@ from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import LEN_NORMAL class Model(models.Model): - FIELDS = ('creator', 'create_at', 'updated_by', 'update_at') - creator = models.CharField(_("创建人"), max_length=LEN_NORMAL, null=True, blank=True) + FIELDS = ("creator", "create_at", "updated_by", "update_at") + creator = models.CharField( + _("创建人"), max_length=LEN_NORMAL, null=True, blank=True + ) create_at = models.DateTimeField(_("创建时间"), auto_now_add=True) update_at = models.DateTimeField(_("更新时间"), auto_now=True) - updated_by = models.CharField(_("修改人"), max_length=LEN_NORMAL, null=True, blank=True) - + updated_by = models.CharField( + _("修改人"), max_length=LEN_NORMAL, null=True, blank=True + ) + class Meta: - app_label = 'project' + app_label = "project" abstract = True diff --git a/itsm/project/models/project.py b/itsm/project/models/project.py index 98465f73d..dcba3da86 100644 --- a/itsm/project/models/project.py +++ b/itsm/project/models/project.py @@ -40,7 +40,7 @@ ) from itsm.iadmin.contants import PROJECT_SETTING from itsm.project.models.base import Model -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.service.models import ServiceCatalog from itsm.sla.models import Sla, Schedule @@ -67,7 +67,7 @@ class Project(Model): "catalog_create", "catalog_edit", "catalog_delete", - "system_settings_manage" + "system_settings_manage", ] auth_resource = {"resource_type": "project", "resource_type_name": "项目"} @@ -180,8 +180,12 @@ def init_lesscode_project(cls): class ProjectSettings(Model): type = models.CharField(_("类型"), max_length=LEN_NORMAL, default="FUNCTION") key = models.CharField(_("关键字唯一标识"), max_length=LEN_NORMAL, unique=False) - value = models.TextField(_("系统设置值"), default=EMPTY_STRING, null=True, blank=True) - project = models.ForeignKey("Project", help_text=_("项目"), on_delete=models.CASCADE) + value = models.TextField( + _("系统设置值"), default=EMPTY_STRING, null=True, blank=True + ) + project = models.ForeignKey( + "Project", help_text=_("项目"), on_delete=models.CASCADE + ) class UserProjectAccessRecord(Model): diff --git a/itsm/project/views.py b/itsm/project/views.py index f7b248a2f..823011459 100644 --- a/itsm/project/views.py +++ b/itsm/project/views.py @@ -25,7 +25,7 @@ # 当前提供给前端获取用户权限链接使用 from django.db.models import Q -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import status from rest_framework.exceptions import ValidationError from rest_framework.response import Response @@ -66,7 +66,7 @@ class ProjectViewSet(component_viewsets.AuthModelViewSet): permission_action_default = ["project_edit"] permission_action_mapping = { "retrieve": ["project_view"], - "update_project_record": ["project_view"] + "update_project_record": ["project_view"], } def list(self, request, *args, **kwargs): diff --git a/itsm/role/models.py b/itsm/role/models.py index 297fd4734..54a0f8219 100644 --- a/itsm/role/models.py +++ b/itsm/role/models.py @@ -29,7 +29,7 @@ from django.conf import settings from django.core.cache import cache from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from itsm.component.constants import ( @@ -96,12 +96,19 @@ class RoleType(Model): is_processor = models.BooleanField(_("可否操作单据"), default=True) is_display = models.BooleanField(_("是否显示"), default=True) desc = models.CharField( - _("角色描述"), max_length=LEN_MIDDLE, default=EMPTY_STRING, null=True, blank=True + _("角色描述"), + max_length=LEN_MIDDLE, + default=EMPTY_STRING, + null=True, + blank=True, ) objects = managers.Manager() - auth_resource = {"resource_type": "system_settings", "resource_type_name": "系统配置"} + auth_resource = { + "resource_type": "system_settings", + "resource_type_name": "系统配置", + } resource_operations = ["system_settings_manage"] class Meta: @@ -151,7 +158,11 @@ class UserRole(ObjectManagerMixin, Model): owners = models.CharField(_("负责人"), max_length=LEN_XX_LONG, default=EMPTY_STRING) access = models.CharField(_("对应服务"), max_length=LEN_MIDDLE) desc = models.CharField( - _("用户角色描述"), max_length=LEN_MIDDLE, default=EMPTY_STRING, null=True, blank=True + _("用户角色描述"), + max_length=LEN_MIDDLE, + default=EMPTY_STRING, + null=True, + blank=True, ) is_builtin = models.BooleanField(_("是否内置"), default=False) project_key = models.CharField( @@ -442,7 +453,9 @@ class BKUserRole(models.Model): username = models.CharField( _("蓝鲸用户username"), max_length=LEN_NORMAL, default=EMPTY_STRING ) - roles = jsonfield.JSONField(_("用户角色"), default=roles_dict, null=True, blank=True) + roles = jsonfield.JSONField( + _("用户角色"), default=roles_dict, null=True, blank=True + ) uid = models.CharField( _("用户uid"), max_length=LEN_NORMAL, default=EMPTY_STRING, null=True, blank=True ) diff --git a/itsm/role/permissions.py b/itsm/role/permissions.py index 07949e930..ee6b94024 100644 --- a/itsm/role/permissions.py +++ b/itsm/role/permissions.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.drf import permissions as perm from itsm.component.drf.permissions import IamAuthPermit @@ -37,12 +37,12 @@ class IsUserRoleManager(perm.IsManager): class UserGroupPermission(IamAuthPermit): - + def has_object_permission(self, request, view, obj, **kwargs): # 关联实例的请求,需要针对对象进行鉴权 if view.action in getattr(view, "permission_free_actions", []): return True - + if view.action in ["retrieve"]: apply_actions = ["user_group_view"] elif view.action in ["destroy"]: diff --git a/itsm/role/serializers.py b/itsm/role/serializers.py index 5c56f73c8..5c8e2563b 100644 --- a/itsm/role/serializers.py +++ b/itsm/role/serializers.py @@ -25,7 +25,7 @@ from django.contrib.auth import get_user_model from django.db import transaction -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from itsm.component.constants import ( @@ -33,7 +33,8 @@ LEN_MIDDLE, LEN_NORMAL, LEN_XX_LONG, - WIKI_ADMIN_SUPERUSER_KEY, LEN_SHORT, + WIKI_ADMIN_SUPERUSER_KEY, + LEN_SHORT, ) from itsm.component.drf.serializers import DynamicFieldsModelSerializer from itsm.component.utils.basic import dotted_name, list_by_separator, normal_name @@ -49,8 +50,11 @@ class RoleTypeSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) type = serializers.CharField(required=True, max_length=LEN_NORMAL) - name = serializers.CharField(required=True, max_length=LEN_NORMAL, - error_messages={"blank": _("请输入角色名!")}) + name = serializers.CharField( + required=True, + max_length=LEN_NORMAL, + error_messages={"blank": _("请输入角色名!")}, + ) desc = serializers.CharField(required=False, max_length=LEN_MIDDLE) class Meta: @@ -69,26 +73,48 @@ class UserRoleSerializer(DynamicFieldsModelSerializer): id = serializers.IntegerField(required=False) role_type = serializers.CharField(required=True, max_length=LEN_NORMAL) - name = serializers.CharField(required=True, max_length=LEN_NORMAL, - error_messages={"blank": _("请输入自定义角色名")}) - members = serializers.CharField(required=True, max_length=LEN_XX_LONG, - error_messages={"blank": _("请指定角色下的人员")}) - owners = serializers.CharField(required=False, max_length=LEN_XX_LONG, allow_blank=True) - access = serializers.CharField(required=False, allow_null=True, allow_blank=True, - max_length=LEN_MIDDLE) - creator = serializers.CharField(required=False, allow_null=True, allow_blank=True, - max_length=LEN_NORMAL) - role_key = serializers.CharField(required=False, allow_null=True, allow_blank=True, - max_length=LEN_MIDDLE) - desc = serializers.CharField(required=False, allow_null=True, allow_blank=True, - max_length=LEN_MIDDLE) + name = serializers.CharField( + required=True, + max_length=LEN_NORMAL, + error_messages={"blank": _("请输入自定义角色名")}, + ) + members = serializers.CharField( + required=True, + max_length=LEN_XX_LONG, + error_messages={"blank": _("请指定角色下的人员")}, + ) + owners = serializers.CharField( + required=False, max_length=LEN_XX_LONG, allow_blank=True + ) + access = serializers.CharField( + required=False, allow_null=True, allow_blank=True, max_length=LEN_MIDDLE + ) + creator = serializers.CharField( + required=False, allow_null=True, allow_blank=True, max_length=LEN_NORMAL + ) + role_key = serializers.CharField( + required=False, allow_null=True, allow_blank=True, max_length=LEN_MIDDLE + ) + desc = serializers.CharField( + required=False, allow_null=True, allow_blank=True, max_length=LEN_MIDDLE + ) project_key = serializers.CharField(required=True, max_length=LEN_SHORT) class Meta: model = UserRole fields = ( - "id", "role_type", "name", "members", "project_key", "owners", "access", - "desc", "role_key", "creator", "is_builtin") + "id", + "role_type", + "name", + "members", + "project_key", + "owners", + "access", + "desc", + "role_key", + "creator", + "is_builtin", + ) create_only_fields = ("project_key", "is_builtin", "creator") def __init__(self, *args, **kwargs): @@ -121,8 +147,12 @@ def to_representation(self, instance): if "access" in data: data["access_name"] = _( - ",".join([_(ACCESS_NAMES.get(key, "")) for key in - list_by_separator(data.get("access"), ",")]) + ",".join( + [ + _(ACCESS_NAMES.get(key, "")) + for key in list_by_separator(data.get("access"), ",") + ] + ) ) return self.update_auth_actions(instance, data) @@ -139,7 +169,9 @@ def update(self, instance, validated_data): if cancel_wiki_admins: # 去掉权限的用户 - for not_wiki_admin_user in BKUser.objects.filter(username__in=cancel_wiki_admins): + for not_wiki_admin_user in BKUser.objects.filter( + username__in=cancel_wiki_admins + ): not_wiki_admin_user.set_property("is_wiki_superuser", 0) # 加权限的用户,如果用户不存在就创建一个 diff --git a/itsm/role/utils.py b/itsm/role/utils.py index 17cfe9461..851ec57a0 100644 --- a/itsm/role/utils.py +++ b/itsm/role/utils.py @@ -24,7 +24,7 @@ """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ def translate_constant_2(constant): diff --git a/itsm/role/validators.py b/itsm/role/validators.py index 5bac700e8..fb4c48179 100644 --- a/itsm/role/validators.py +++ b/itsm/role/validators.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from itsm.component.constants import DEFAULT_PROJECT_PROJECT_KEY @@ -44,7 +44,7 @@ def __call__(self, value): access = value.get("access", "") project_key = value.get("project_key", DEFAULT_PROJECT_PROJECT_KEY) members = list_by_separator(value.get("members", "")) - + if getattr(self.role, "id", None) != value.get("id"): raise serializers.ValidationError(_("角色 ID 异常")) diff --git a/itsm/service/api.py b/itsm/service/api.py index d2571846a..270e37d61 100644 --- a/itsm/service/api.py +++ b/itsm/service/api.py @@ -23,11 +23,11 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -__author__ = u"蓝鲸智云" +__author__ = "蓝鲸智云" __copyright__ = "Copyright © 2012-2020 Tencent BlueKing. All Rights Reserved." from django.core.cache import cache -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import PREFIX_KEY from .models import Service, ServiceCatalog @@ -36,14 +36,16 @@ def get_catalog_fullname(catalog_id): """ 获取服务目录全名 - :param catalog_id: - :return: + :param catalog_id: + :return: """ cache_key = "%scatalog_fullname_%s" % (PREFIX_KEY, catalog_id) catalog_fullname = cache.get(cache_key) if catalog_fullname: return _(catalog_fullname) - catalog_fullname = ServiceCatalog._objects.get(id=catalog_id).link_parent_name_ex_root + catalog_fullname = ServiceCatalog._objects.get( + id=catalog_id + ).link_parent_name_ex_root cache.set(cache_key, catalog_fullname, 30) return _(catalog_fullname) diff --git a/itsm/service/managers.py b/itsm/service/managers.py index e1e601b3e..486fefaf2 100644 --- a/itsm/service/managers.py +++ b/itsm/service/managers.py @@ -29,7 +29,7 @@ from django.conf import settings from django.db import transaction from django.db.models.query import QuerySet -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from itsm.component.constants import ( @@ -210,7 +210,6 @@ def insert_services(self, services, catalog=None): return {"result": True, "message": "success"} def upgrade_services_flow(self, **kwargs): - """更新现有服务绑定的流程版本""" print("-------------------upgrade_services_flow------------------------\n") @@ -249,7 +248,6 @@ def upgrade_services_flow(self, **kwargs): WorkflowVersion.objects.upgrade_version(non_bind_flow.id, **kwargs) def get_or_create_service_and_catalog_from_version(self, *args, **kwargs): - """创建服务条目并绑定到服务目录 排除草稿流程,仅创建有效流程的服务项 """ diff --git a/itsm/service/models.py b/itsm/service/models.py index de6062334..582affdae 100644 --- a/itsm/service/models.py +++ b/itsm/service/models.py @@ -32,7 +32,7 @@ from django.db import models, transaction from django.db.models import Q, Count from django.utils.functional import cached_property -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from mptt.models import TreeForeignKey from multiselectfield import MultiSelectField @@ -626,7 +626,9 @@ def subtree(node, catalogs=None, show_deleted=False, catalog_count=None): data = { "id": node.id, "key": node.key, - "name": _("{} (已删除)").format(node.name) if node.is_deleted else node.name, + "name": ( + _("{} (已删除)").format(node.name) if node.is_deleted else node.name + ), "is_deleted": node.is_deleted, "level": node.level, "desc": node.desc, @@ -1167,8 +1169,13 @@ class ServiceProperty(Model): related_name="properties", on_delete=models.CASCADE, ) - key = models.CharField(_("默认为名称拼音,唯一存在,如果有一样的,则通过拼音+随机字符匹配"), max_length=LEN_SHORT) - pk_key = models.CharField(_("fields中的主键的key"), max_length=LEN_SHORT, default="") + key = models.CharField( + _("默认为名称拼音,唯一存在,如果有一样的,则通过拼音+随机字符匹配"), + max_length=LEN_SHORT, + ) + pk_key = models.CharField( + _("fields中的主键的key"), max_length=LEN_SHORT, default="" + ) cascade_key = models.CharField( _("fields中的级联外键的key"), max_length=LEN_SHORT, default="" ) @@ -1274,7 +1281,10 @@ class PropertyRecord(Model): ) data = jsonfield.JSONField(_("对应属性字段的值"), null=True, blank=True) display_role = MultiSelectField( - _("可展示的用户"), default=["all"], max_length=LEN_NORMAL, choices=display_role_choice + _("可展示的用户"), + default=["all"], + max_length=LEN_NORMAL, + choices=display_role_choice, ) # objects = managers.Manager() diff --git a/itsm/service/permissions.py b/itsm/service/permissions.py index 733475947..592b7305d 100644 --- a/itsm/service/permissions.py +++ b/itsm/service/permissions.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import permissions from itsm.auth_iam.utils import IamRequest @@ -50,7 +50,7 @@ class IsDictDataManager(permissions.BasePermission): from itsm.service.models import SysDict - SAFE_METHODS = permissions.SAFE_METHODS + ('DELETE',) + SAFE_METHODS = permissions.SAFE_METHODS + ("DELETE",) def has_permission(self, request, view): if request.method in self.SAFE_METHODS: @@ -59,7 +59,7 @@ def has_permission(self, request, view): if UserRole.is_itsm_superuser(request.user.username): return True - dict_table = request.data.get('dict_table') + dict_table = request.data.get("dict_table") try: sys_dict = self.SysDict.objects.get(id=dict_table) return sys_dict.is_obj_manager(request.user.username) @@ -87,16 +87,18 @@ def has_permission(self, request, view): if request.method in permissions.SAFE_METHODS: return True - if view.action == 'batch_delete': - id_list = [i for i in request.data.get('id').split(',') if i.isdigit()] + if view.action == "batch_delete": + id_list = [i for i in request.data.get("id").split(",") if i.isdigit()] return not CatalogService.objects.filter(service_id__in=id_list).exists() return True def has_object_permission(self, request, view, obj): - if view.action == 'destroy': - return not CatalogService.objects.filter(service_id=request.parser_context['kwargs'].get('pk')).exists() + if view.action == "destroy": + return not CatalogService.objects.filter( + service_id=request.parser_context["kwargs"].get("pk") + ).exists() return True @@ -105,6 +107,7 @@ class ServicePermit(IamAuthPermit): """ 服务鉴权 """ + service_clone_action = ["clone", "import_from_service", "import_from_template"] def has_permission(self, request, view): @@ -113,7 +116,7 @@ def has_permission(self, request, view): obj = view.get_object() project = Project.objects.filter(pk=obj.project_key).first() return super().has_object_permission(request, view, project) - + # 批量删除 if view.action == "batch_delete": id_list = [i for i in request.data.get("id").split(",") if i.isdigit()] @@ -128,23 +131,22 @@ def has_permission(self, request, view): elif service.project_key != project_key: raise ValidationError(_("服务所属项目不一致")) - resources.append({ - "resource_id": service.id, - "resource_type": "service", - "creator": getattr(service, "creator", ""), - }) - + resources.append( + { + "resource_id": service.id, + "resource_type": "service", + "creator": getattr(service, "creator", ""), + } + ) + iam_client = IamRequest(request) allowed = iam_client.batch_resource_multi_actions_allowed( - actions=["service_manage"], - resources=resources, - project_key=project_key - + actions=["service_manage"], resources=resources, project_key=project_key ) return all([i["service_manage"] for i in allowed.values()]) - + return super().has_permission(request, view) - + def has_object_permission(self, request, view, obj, **kwargs): if view.action in self.service_clone_action: """针对 clone 类操作,不需要检测实例对象权限""" diff --git a/itsm/service/serializers.py b/itsm/service/serializers.py index 2f17802b9..8b3902f24 100644 --- a/itsm/service/serializers.py +++ b/itsm/service/serializers.py @@ -24,7 +24,7 @@ """ from django.db import transaction -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.fields import JSONField @@ -184,7 +184,9 @@ def validate(self, attrs): .filter(is_deleted=False, name=attrs["name"]) .exists() ): - raise ServiceCatalogValidateError(_("同级下目录名不能重复,请修改后提交")) + raise ServiceCatalogValidateError( + _("同级下目录名不能重复,请修改后提交") + ) if self.context["view"].action == "update": if ( parent_object @@ -193,7 +195,9 @@ def validate(self, attrs): .exclude(id=self.instance.id) .exists() ): - raise ServiceCatalogValidateError(_("同级下目录名不能重复,请修改后提交")) + raise ServiceCatalogValidateError( + _("同级下目录名不能重复,请修改后提交") + ) attrs["parent"] = parent_object @@ -253,7 +257,9 @@ class SlaSerializer(serializers.ModelSerializer): error_messages={"blank": _("名称为必填项")}, max_length=8, validators=[ - UniqueValidator(queryset=OldSla.objects.all(), message=_("服务级别名已存在,请重新输入")), + UniqueValidator( + queryset=OldSla.objects.all(), message=_("服务级别名已存在,请重新输入") + ), name_validator, ], ) @@ -307,7 +313,9 @@ class ServiceSlaSerializer(serializers.ModelSerializer): """服务与SLA关联表序列化""" name = serializers.CharField( - required=True, error_messages={"blank": _("协议名称不能为空")}, max_length=LEN_LONG + required=True, + error_messages={"blank": _("协议名称不能为空")}, + max_length=LEN_LONG, ) service_id = serializers.IntegerField(required=False, allow_null=True) lines = JSONField(required=False, initial=EMPTY_LIST) @@ -326,7 +334,9 @@ class ServiceSerializer(AuthModelSerializer): error_messages={"blank": _("名称不能为空")}, max_length=LEN_MIDDLE, validators=[ - UniqueValidator(queryset=Service.objects.all(), message=_("服务名已存在,请重新输入")), + UniqueValidator( + queryset=Service.objects.all(), message=_("服务名已存在,请重新输入") + ), # name_validator ], ) @@ -386,9 +396,7 @@ def get_favorite_users(self): services = ( [self.instance] if isinstance(self.instance, Service) - else [] - if self.instance is None - else self.instance + else [] if self.instance is None else self.instance ) service_ids = [service.id for service in services] users = FavoriteService.objects.filter(service_id__in=service_ids).values( @@ -476,7 +484,9 @@ def to_representation(self, instance): try: workflow_instance = Workflow.objects.get(id=instance.workflow.workflow_id) except Workflow.DoesNotExist: - raise ServerError("当前服务绑定的流程已经被删除, service_name={}".format(instance.name)) + raise ServerError( + "当前服务绑定的流程已经被删除, service_name={}".format(instance.name) + ) username = self.context["request"].user.username data["creator"] = transform_single_username(data["creator"]) @@ -508,7 +518,9 @@ class ServiceListSerializer(serializers.ModelSerializer): error_messages={"blank": _("名称不能为空")}, max_length=LEN_MIDDLE, validators=[ - UniqueValidator(queryset=Service.objects.all(), message=_("服务名已存在,请重新输入")), + UniqueValidator( + queryset=Service.objects.all(), message=_("服务名已存在,请重新输入") + ), # name_validator ], ) @@ -533,9 +545,7 @@ def get_service_ids(self): services = ( [self.instance] if isinstance(self.instance, Service) - else [] - if self.instance is None - else self.instance + else [] if self.instance is None else self.instance ) return [service["id"] for service in services] @@ -617,7 +627,9 @@ def validate_catalog_id(self, value): try: catalog = ServiceCatalog.objects.get(id=value) if catalog.level == 0: - raise serializers.ValidationError(_("根目录不允许添加服务,选择其他目录")) + raise serializers.ValidationError( + _("根目录不允许添加服务,选择其他目录") + ) except ServiceCatalog.DoesNotExist: raise serializers.ValidationError(_("指定的服务目录不存在")) @@ -674,7 +686,9 @@ class SysDictSerializer(DynamicFieldsModelSerializer): error_messages={"blank": _("编码不能为空")}, max_length=LEN_MIDDLE, validators=[ - UniqueValidator(queryset=SysDict.objects.all(), message=_("编码已存在,请重新输入")), + UniqueValidator( + queryset=SysDict.objects.all(), message=_("编码已存在,请重新输入") + ), key_validator, ], ) @@ -768,7 +782,10 @@ class WorkflowImportSerializer(serializers.Serializer): name = serializers.CharField( required=True, max_length=LEN_MIDDLE, - error_messages={"blank": _("请输入流程名称!"), "max_length": _("流程名称长度不能大于120个字符")}, + error_messages={ + "blank": _("请输入流程名称!"), + "max_length": _("流程名称长度不能大于120个字符"), + }, ) flow_type = serializers.CharField(required=True, max_length=LEN_NORMAL) desc = serializers.CharField( @@ -840,7 +857,9 @@ def validate(self, attrs): project_key = attrs["project_key"] if not Project.objects.filter(key=project_key).exists(): - raise serializers.ValidationError(_("导入失败,project_key 对应的项目不存在")) + raise serializers.ValidationError( + _("导入失败,project_key 对应的项目不存在") + ) catalog_id = attrs.get("catalog_id", None) if catalog_id is not None: diff --git a/itsm/service/validators.py b/itsm/service/validators.py index 2317745df..8bd8a221b 100644 --- a/itsm/service/validators.py +++ b/itsm/service/validators.py @@ -26,7 +26,7 @@ import re from django.core.validators import RegexValidator -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from itsm.service.models import CatalogService, Service, ServiceCategory @@ -39,20 +39,26 @@ def time_validator(value): time = value[-1] if time not in ["m", "h", "d"]: - raise serializers.ValidationError(_('时间单位不正确')) + raise serializers.ValidationError(_("时间单位不正确")) try: number = int(value[:-1]) if number > 65536 or number <= 0: - raise serializers.ValidationError(_('时间超出了设置范围(1~65536)')) + raise serializers.ValidationError(_("时间超出了设置范围(1~65536)")) except ValueError: - raise serializers.ValidationError(_('数据类型错误,不是合法的时间')) + raise serializers.ValidationError(_("数据类型错误,不是合法的时间")) -key_validator = RegexValidator(re.compile('^[_a-zA-Z0-9]+$'), message=_('请输入合法编码:英文数字及下划线'), code='invalid',) +key_validator = RegexValidator( + re.compile("^[_a-zA-Z0-9]+$"), + message=_("请输入合法编码:英文数字及下划线"), + code="invalid", +) # 正则表达式带中文一定要要带上u,否则校验不通过 name_validator = RegexValidator( - re.compile(r'^[a-zA-Z0-9_\s()()\u4e00-\u9fa5]+$'), message=_('请输入合法名称:中英文、中英文括号、数字、空格及下划线'), code='invalid', + re.compile(r"^[a-zA-Z0-9_\s()()\u4e00-\u9fa5]+$"), + message=_("请输入合法名称:中英文、中英文括号、数字、空格及下划线"), + code="invalid", ) @@ -66,12 +72,16 @@ def service_validate(service_id): try: service = Service.objects.get(id=service_id) if not service.is_valid: - raise serializers.ValidationError({_("服务"): _("服务未启用,请联系管理员!")}) + raise serializers.ValidationError( + {_("服务"): _("服务未启用,请联系管理员!")} + ) except Service.DoesNotExist: raise serializers.ValidationError({_("服务"): _("服务不存在,请联系管理员!")}) try: - catalog_services = CatalogService.objects.get(service_id=service_id, is_deleted=False) + catalog_services = CatalogService.objects.get( + service_id=service_id, is_deleted=False + ) except CatalogService.DoesNotExist: raise serializers.ValidationError({_("服务"): _("服务对应的服务目录不存在")}) diff --git a/itsm/service/views.py b/itsm/service/views.py index 526c7917b..13db17edf 100644 --- a/itsm/service/views.py +++ b/itsm/service/views.py @@ -28,7 +28,7 @@ from django.http import FileResponse from django.utils.encoding import escape_uri_path -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django_bulk_update.helper import bulk_update from rest_framework import serializers from rest_framework.decorators import action @@ -155,7 +155,7 @@ class ServiceCatalogViewSet(component_viewsets.ModelViewSet): "update": "catalog_edit", "destroy": "catalog_delete", } - + filter_fields = { "id": ["exact", "in"], "key": ["exact", "in"], @@ -515,7 +515,9 @@ def import_from_service(self, request, *args, **kwargs): raise ParamError("service_id 不能为空") from_service = Service.objects.get(id=service_id) if from_service is None: - raise ServiceNotExist("未找到相对应的服务, service_id={}".format(service_id)) + raise ServiceNotExist( + "未找到相对应的服务, service_id={}".format(service_id) + ) with transaction.atomic(): self.copy_fields_from_service(from_service, service) @@ -658,11 +660,11 @@ def export(self, request, *args, **kwargs): response = FileResponse(json.dumps(data, cls=JsonEncoder, indent=2)) response["Content-Type"] = "application/octet-stream" # 中文文件名乱码问题 - response[ - "Content-Disposition" - ] = "attachment; filename*=UTF-8''bk_itsm_{}_{}.json".format( - escape_uri_path(instance.name), - create_version_number(), + response["Content-Disposition"] = ( + "attachment; filename*=UTF-8''bk_itsm_{}_{}.json".format( + escape_uri_path(instance.name), + create_version_number(), + ) ) return response @@ -683,7 +685,9 @@ def imports(self, request, *args, **kwargs): project_key = request.data.get("project_key", data.get("project_key")) data["project_key"] = project_key if isinstance(data, list): - raise ParamError(_("2.5.9 版本之前的流程无法导入,请转换后在看,详情请看github")) + raise ParamError( + _("2.5.9 版本之前的流程无法导入,请转换后在看,详情请看github") + ) ServiceImportSerializer(data=data).is_valid(raise_exception=True) catalog_id = request.data.get("catalog_id") service = Service.objects.clone( @@ -794,7 +798,11 @@ def perform_destroy(self, instance): dict_table__key__in=tables, key=instance.key ).exists(): raise serializers.ValidationError( - _("[{}] 已经被勾选绑定,请先到优先级管理中解绑".format(instance.name)) + _( + "[{}] 已经被勾选绑定,请先到优先级管理中解绑".format( + instance.name + ) + ) ) instance.delete() diff --git a/itsm/sites/urls.py b/itsm/sites/urls.py index 17063dfc8..451bbdf4c 100644 --- a/itsm/sites/urls.py +++ b/itsm/sites/urls.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.conf.urls import include, url +from django.urls import include, re_path from django_nyt.urls import get_pattern as get_nyt_pattern from itsm.sites.views import index, get_footer, init @@ -31,17 +31,17 @@ urlpatterns = [ # main - url(r"^$", index), - url(r"^init/$", init), + re_path(r"^$", index), + re_path(r"^init/$", init), # flower, celery monitor - url(r"^o/bk_sops/", include("sops_proxy.urls")), + re_path(r"^o/bk_sops/", include("sops_proxy.urls")), # helper, fix database - url(r"^helper/", include("itsm.helper.urls")), + re_path(r"^helper/", include("itsm.helper.urls")), # weixin - url(r"^weixin/$", weixin_views.index), - url(r"^weixin/login/", include("weixin.core.urls")), - url(r"^weixin/api/", include("weixin.urls")), + re_path(r"^weixin/$", weixin_views.index), + re_path(r"^weixin/login/", include("weixin.core.urls")), + re_path(r"^weixin/api/", include("weixin.urls")), # wiki - url(r"^notifications/", get_nyt_pattern()), - url(r"^core/footer/$", get_footer), + re_path(r"^notifications/", get_nyt_pattern()), + re_path(r"^core/footer/$", get_footer), ] diff --git a/itsm/sites/views.py b/itsm/sites/views.py index 81ea8269c..0998de1f0 100644 --- a/itsm/sites/views.py +++ b/itsm/sites/views.py @@ -30,7 +30,7 @@ from django.conf import settings from django.http import JsonResponse, HttpResponseRedirect from django.shortcuts import render -from django.utils.translation import ugettext as _, get_language +from django.utils.translation import gettext as _, get_language from django.views.decorators.http import require_GET from mako.template import Template @@ -71,9 +71,9 @@ def init(request): "chname": request.user.get_property("chname"), "username": request.user.username, "all_access": UserRole.get_access_by_user(request.user.username), - "IS_ITSM_ADMIN": 1 - if UserRole.is_itsm_superuser(request.user.username) - else 0, + "IS_ITSM_ADMIN": ( + 1 if UserRole.is_itsm_superuser(request.user.username) else 0 + ), "need_target": False, # 不需要强制跳转无权限页 "location": "", }, @@ -116,15 +116,17 @@ def index(request): ).value except SystemSettings.DoesNotExist: notice_center_switch_value = "off" - + # 文档地址转换 doc_lang = "EN" lang = get_language() if lang in ["zh-cn", "zh-hans"]: doc_lang = "ZH" - + version = get_version() - doc_url = settings.BK_DOC_URL.format(lang=doc_lang, version=get_major_minor_version(version)) + doc_url = settings.BK_DOC_URL.format( + lang=doc_lang, version=get_major_minor_version(version) + ) return render( request, @@ -178,16 +180,16 @@ def get_version(): """ # 读取文件内容 app_desc = os.path.join(settings.PROJECT_ROOT, "VERSION") - with open(app_desc, 'r') as file: + with open(app_desc, "r") as file: content = file.read() return content.strip() def get_major_minor_version(version_string): # 使用 split() 方法分割字符串 - parts = version_string.split('.') + parts = version_string.split(".") # 取前两个部分并用 '.' 连接 - major_minor = '.'.join(parts[:2]) + major_minor = ".".join(parts[:2]) return major_minor diff --git a/itsm/sla/models/basic.py b/itsm/sla/models/basic.py index 2053a681b..8c508a883 100644 --- a/itsm/sla/models/basic.py +++ b/itsm/sla/models/basic.py @@ -24,7 +24,7 @@ """ from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import LEN_NORMAL from itsm.component.utils.basic import get_random_key @@ -35,10 +35,10 @@ class Model(models.Model): """基础字段""" DISPLAY_FIELDS = ( - 'creator', - 'create_at', - 'updated_by', - 'update_at', + "creator", + "create_at", + "updated_by", + "update_at", ) creator = models.CharField(_("创建人"), max_length=LEN_NORMAL) @@ -79,6 +79,6 @@ def get_unique_key(cls, name): retry += 1 else: # 尝试60次一直重复,则放弃生成key - return '##Err##' + return "##Err##" return key diff --git a/itsm/sla/models/policy.py b/itsm/sla/models/policy.py index 50ba26f3e..72eaa3d25 100644 --- a/itsm/sla/models/policy.py +++ b/itsm/sla/models/policy.py @@ -26,7 +26,7 @@ import copy from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from jsonfield import JSONField from mako.template import Template @@ -353,7 +353,9 @@ class ActionPolicy(Model): """ name = models.CharField(_("策略名称"), max_length=LEN_LONG) - type = models.IntegerField(_("升级事件类型"), choices=ACTION_POLICY_TYPES, default=1) + type = models.IntegerField( + _("升级事件类型"), choices=ACTION_POLICY_TYPES, default=1 + ) order = models.IntegerField(_("策略顺序"), default=-1) condition = JSONField("升级条件", help_text="当达到条件的时候,可以触发不同的动作") actions = models.ManyToManyField(Action, help_text=_("处理事件")) diff --git a/itsm/sla/models/schedule.py b/itsm/sla/models/schedule.py index 95cfef376..ce537a392 100644 --- a/itsm/sla/models/schedule.py +++ b/itsm/sla/models/schedule.py @@ -26,7 +26,7 @@ from calendar import FRIDAY, MONDAY, SATURDAY, SUNDAY, THURSDAY, TUESDAY, WEDNESDAY from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( DAY_TYPE_CHOICES, @@ -49,7 +49,7 @@ class Duration(Model): end_time = models.TimeField(_("结束时间")) class Meta: - app_label = 'sla' + app_label = "sla" verbose_name = _("工作时间段") verbose_name_plural = _("工作时间段") @@ -62,15 +62,22 @@ class Day(Model): name = models.CharField(_("假期名称"), max_length=LEN_LONG) day_of_week = models.CharField(_("星期几"), max_length=LEN_SHORT, default=-1) - type_of_day = models.CharField(_("日期类型"), max_length=LEN_SHORT, choices=DAY_TYPE_CHOICES, default=NORMAL_DAY) + type_of_day = models.CharField( + _("日期类型"), + max_length=LEN_SHORT, + choices=DAY_TYPE_CHOICES, + default=NORMAL_DAY, + ) # 比如十一假期范围 start_date = models.DateField(_("开始日期"), null=True) end_date = models.DateField(_("结束日期"), null=True) - duration = models.ManyToManyField(to='Duration', help_text=_("工作时间段,没有配置的情况下,默认从0:00 -23:59")) + duration = models.ManyToManyField( + to="Duration", help_text=_("工作时间段,没有配置的情况下,默认从0:00 -23:59") + ) class Meta: - app_label = 'sla' + app_label = "sla" verbose_name = _("工作日和节假日") verbose_name_plural = _("工作日和节假日") @@ -82,23 +89,42 @@ class Schedule(Model): """服务时间策略""" name = models.CharField(_("名称"), max_length=LEN_LONG) - is_enabled = models.BooleanField("配置是否生效", help_text="是:启用当前工作日历配置, 否:默认采用7*24小时", default=True) - days = models.ManyToManyField(help_text=_("常规日历"), to='Day', related_name='days') - workdays = models.ManyToManyField(help_text=_("加班日"), to='Day', related_name='workdays', default=None) - holidays = models.ManyToManyField(help_text=_("假期"), to='Day', related_name='holidays', default=None) + is_enabled = models.BooleanField( + "配置是否生效", + help_text="是:启用当前工作日历配置, 否:默认采用7*24小时", + default=True, + ) + days = models.ManyToManyField( + help_text=_("常规日历"), to="Day", related_name="days" + ) + workdays = models.ManyToManyField( + help_text=_("加班日"), to="Day", related_name="workdays", default=None + ) + holidays = models.ManyToManyField( + help_text=_("假期"), to="Day", related_name="holidays", default=None + ) is_builtin = models.BooleanField(_("是否内置"), default=False) - project_key = models.CharField(_("项目key"), max_length=LEN_SHORT, null=False, default=0) - - auth_resource = {"resource_type": "sla_calendar", "resource_type_name": "SLA 服务模式"} - resource_operations = ["sla_calendar_view", "sla_calendar_edit", "sla_calendar_delete"] + project_key = models.CharField( + _("项目key"), max_length=LEN_SHORT, null=False, default=0 + ) + + auth_resource = { + "resource_type": "sla_calendar", + "resource_type_name": "SLA 服务模式", + } + resource_operations = [ + "sla_calendar_view", + "sla_calendar_edit", + "sla_calendar_delete", + ] need_auth_grant = True class Meta: - app_label = 'sla' + app_label = "sla" verbose_name = _("服务运营时间") verbose_name_plural = _("服务运营时间") - ordering = ['-id'] + ordering = ["-id"] def __unicode__(self): return self.name @@ -106,22 +132,29 @@ def __unicode__(self): @classmethod def init_schedule(cls, project_key="0"): """初始化默认的服务模式""" - default_schedule_name = ['5*8', '7*24'] + default_schedule_name = ["5*8", "7*24"] default_day_of_week = [ - ','.join(map(str, [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY])), - ','.join(map(str, [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY])), + ",".join(map(str, [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY])), + ",".join( + map( + str, + [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY], + ) + ), ] default_durations = [ [ - {'name': _('上午'), 'start_time': '08:00:00', 'end_time': '12:00:00'}, - {'name': _('下午'), 'start_time': '14:00:00', 'end_time': '18:00:00'}, + {"name": _("上午"), "start_time": "08:00:00", "end_time": "12:00:00"}, + {"name": _("下午"), "start_time": "14:00:00", "end_time": "18:00:00"}, ], - [{'name': _('全天'), 'start_time': '00:00:00', 'end_time': '23:59:59'}], + [{"name": _("全天"), "start_time": "00:00:00", "end_time": "23:59:59"}], ] schedules = [] for index, schedule_name in enumerate(default_schedule_name): - schedule = cls.objects.filter(name=schedule_name, project_key=project_key).first() + schedule = cls.objects.filter( + name=schedule_name, project_key=project_key + ).first() if schedule: schedules.append(schedule) @@ -133,9 +166,11 @@ def init_schedule(cls, project_key="0"): for duration_data in durations_data: duration = Duration.objects.create(**duration_data) durations.append(duration) - day = Day.objects.create(day_of_week=day_of_week, type_of_day='NORMAL') + day = Day.objects.create(day_of_week=day_of_week, type_of_day="NORMAL") day.duration.add(*durations) - schedule = cls.objects.create(name=schedule_name, is_builtin=True, project_key=project_key) + schedule = cls.objects.create( + name=schedule_name, is_builtin=True, project_key=project_key + ) schedule.days.add(day) schedules.append(schedule) diff --git a/itsm/sla/permissions.py b/itsm/sla/permissions.py index 52e53573f..c5dd2a190 100644 --- a/itsm/sla/permissions.py +++ b/itsm/sla/permissions.py @@ -26,7 +26,7 @@ __author__ = "蓝鲸智云" __copyright__ = "Copyright © 2012-2020 Tencent BlueKing. All Rights Reserved." -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import permissions from itsm.component.drf.permissions import IamAuthPermit, IamAuthWithoutResourcePermit @@ -50,7 +50,7 @@ def has_permission(self, request, view): return True # 提单环节拉取优先级 - if view.action in ['priority_value']: + if view.action in ["priority_value"]: return True return False @@ -81,10 +81,10 @@ def has_object_permission(self, request, view, obj, **kwargs): apply_actions = ["sla_agreement_edit"] return self.iam_auth(request, apply_actions, obj) - - + + class SchedulePermit(IamAuthPermit): - + def has_object_permission(self, request, view, obj, **kwargs): # 关联实例的请求,需要针对对象进行鉴权 if view.action in getattr(view, "permission_free_actions", []): @@ -98,18 +98,13 @@ def has_object_permission(self, request, view, obj, **kwargs): apply_actions = ["sla_calendar_edit"] return self.iam_auth(request, apply_actions, obj) - - + + class SlaMatrixPermit(IamAuthWithoutResourcePermit): def has_permission(self, request, view): if view.action == "matrix_of_service_type": apply_actions = ["sla_priority_view", "platform_manage_access"] else: apply_actions = ["sla_priority_manage"] - + return self.iam_auth(request, apply_actions) - - - - - diff --git a/itsm/sla/serializers/matrix.py b/itsm/sla/serializers/matrix.py index a4f857cc5..339e6a5ae 100644 --- a/itsm/sla/serializers/matrix.py +++ b/itsm/sla/serializers/matrix.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from bulk_update.helper import bulk_update @@ -37,7 +37,13 @@ class PriorityMatrixSerializer(serializers.ModelSerializer): class Meta: model = PriorityMatrix - fields = ('id', 'service_type', 'urgency', 'impact', 'priority',) + model.DISPLAY_FIELDS + fields = ( + "id", + "service_type", + "urgency", + "impact", + "priority", + ) + model.DISPLAY_FIELDS read_only_fields = model.DISPLAY_FIELDS @@ -48,22 +54,24 @@ class MatrixListSerializer(serializers.ListSerializer): def update(self, instance, validated_data, other_types=None): """批量更新,返回更新的数量""" - item_hash = {item['id']: item for item in validated_data} + item_hash = {item["id"]: item for item in validated_data} priorities = [] for p in PriorityMatrix.objects.filter(pk__in=list(item_hash.keys())): - p.priority = item_hash[p.id]['priority'] + p.priority = item_hash[p.id]["priority"] priorities.append(p) # 其他类型更新 other_types = [] if other_types is None else other_types for other_type in other_types: for i in validated_data: - p = PriorityMatrix.objects.get(service_type=other_type, urgency=i["urgency"], impact=i["impact"]) - p.priority = i['priority'] + p = PriorityMatrix.objects.get( + service_type=other_type, urgency=i["urgency"], impact=i["impact"] + ) + p.priority = i["priority"] priorities.append(p) - return bulk_update(priorities, update_fields=['priority']) + return bulk_update(priorities, update_fields=["priority"]) class MatrixUpdateSerializer(serializers.ModelSerializer): @@ -76,15 +84,10 @@ class MatrixUpdateSerializer(serializers.ModelSerializer): class Meta: model = PriorityMatrix list_serializer_class = MatrixListSerializer - fields = ( - 'id', - 'priority', - 'impact', - 'urgency' - ) + fields = ("id", "priority", "impact", "urgency") def validate_priority(self, value): - priority_set = SysDict.get_data_by_key(PRIORITY, 'sets') + priority_set = SysDict.get_data_by_key(PRIORITY, "sets") priority_set.add(EMPTY_STRING) if value not in priority_set: diff --git a/itsm/sla/serializers/policy.py b/itsm/sla/serializers/policy.py index c9c76b6db..275e6de7d 100644 --- a/itsm/sla/serializers/policy.py +++ b/itsm/sla/serializers/policy.py @@ -23,13 +23,20 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.fields import empty from itsm.component.constants import LEN_MIDDLE, LEN_SHORT from itsm.component.drf.serializers import AuthModelSerializer -from itsm.sla.models import Action, ActionPolicy, PriorityPolicy, Sla, SlaTimerRule, SlaTicketHighlight +from itsm.sla.models import ( + Action, + ActionPolicy, + PriorityPolicy, + Sla, + SlaTimerRule, + SlaTicketHighlight, +) from itsm.sla.serializers import ModelSerializer from itsm.sla.validators import SlaTimerRuleValidator, SlaValidator @@ -59,11 +66,11 @@ class Meta: ) + model.DISPLAY_FIELDS read_only_fields = model.DISPLAY_FIELDS - + def to_internal_value(self, data): if not data["reply_time"]: data["reply_time"] = None - + return super(PriorityPolicySerializer, self).to_internal_value(data) @@ -76,7 +83,13 @@ class SlaTimerRuleSerializer(serializers.ModelSerializer): class Meta: model = SlaTimerRule - fields = ('id', 'name', 'service_type', 'condition_type', 'condition') + model.DISPLAY_FIELDS + fields = ( + "id", + "name", + "service_type", + "condition_type", + "condition", + ) + model.DISPLAY_FIELDS read_only_fields = model.DISPLAY_FIELDS @@ -95,7 +108,11 @@ class ActionSerializer(serializers.ModelSerializer): class Meta: model = Action - fields = ('id', 'action_type', 'config',) + model.DISPLAY_FIELDS + fields = ( + "id", + "action_type", + "config", + ) + model.DISPLAY_FIELDS read_only_fields = model.DISPLAY_FIELDS @@ -112,7 +129,7 @@ class ActionPolicySerializer(ModelSerializer): class Meta: model = ActionPolicy - fields = ('id', 'name', 'condition', 'actions', 'type') + model.DISPLAY_FIELDS + fields = ("id", "name", "condition", "actions", "type") + model.DISPLAY_FIELDS read_only_fields = model.DISPLAY_FIELDS @@ -122,7 +139,9 @@ def create(self, validated_data): if not actions: return instance - actions = [self.fields.fields['actions'].child.create(action) for action in actions] + actions = [ + self.fields.fields["actions"].child.create(action) for action in actions + ] instance.actions.set(actions) instance.save() return instance @@ -154,19 +173,19 @@ class SlaSerializer(AuthModelSerializer, ModelSerializer): class Meta: model = Sla fields = ( - 'id', - 'name', - 'is_enabled', - 'is_builtin', - 'policies', - 'action_policies', - 'service_count', - 'service_names', - 'is_reply_need', - 'project_key' + "id", + "name", + "is_enabled", + "is_builtin", + "policies", + "action_policies", + "service_count", + "service_names", + "is_reply_need", + "project_key", ) + model.DISPLAY_FIELDS - related_fields = ('policies', 'action_policies') + related_fields = ("policies", "action_policies") read_only_fields = model.DISPLAY_FIELDS @@ -175,15 +194,22 @@ def create(self, validated_data): action_policies = validated_data.pop("action_policies", []) policies = validated_data.pop("policies", []) instance = super(SlaSerializer, self).create(validated_data) - instance.action_policies.set([ - self.fields.fields['action_policies'].child.create(a_data) for a_data in action_policies - ]) - instance.policies.set([self.fields.fields['policies'].child.create(p_data) for p_data in policies]) + instance.action_policies.set( + [ + self.fields.fields["action_policies"].child.create(a_data) + for a_data in action_policies + ] + ) + instance.policies.set( + [self.fields.fields["policies"].child.create(p_data) for p_data in policies] + ) instance.save() return instance def update(self, instance, validated_data): - rel_fields = {key: validated_data.pop(key, []) for key in self.Meta.related_fields} + rel_fields = { + key: validated_data.pop(key, []) for key in self.Meta.related_fields + } instance = super(SlaSerializer, self).update(instance, validated_data) instance = self.update_many_to_many_relation(instance, rel_fields) @@ -192,7 +218,7 @@ def update(self, instance, validated_data): def run_validation(self, data=empty): return super(SlaSerializer, self).run_validation(data) - + def to_representation(self, instance): data = super(SlaSerializer, self).to_representation(instance) return self.update_auth_actions(instance, data) @@ -203,8 +229,4 @@ class TicketHighlightSerializer(ModelSerializer): class Meta: model = SlaTicketHighlight - fields = ( - 'id', - 'reply_timeout_color', - 'handle_timeout_color' - ) + fields = ("id", "reply_timeout_color", "handle_timeout_color") diff --git a/itsm/sla/serializers/schedule.py b/itsm/sla/serializers/schedule.py index ccdf6f3fa..741ec1d93 100644 --- a/itsm/sla/serializers/schedule.py +++ b/itsm/sla/serializers/schedule.py @@ -23,13 +23,18 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.fields import empty from itsm.component.constants import DAY_TYPE_CHOICES, LEN_MIDDLE, LEN_SHORT from itsm.sla.models import Day, Duration, Schedule -from itsm.sla.validators import DayValidator, DurationValidator, ScheduleValidator, name_validator +from itsm.sla.validators import ( + DayValidator, + DurationValidator, + ScheduleValidator, + name_validator, +) from .basic import ModelSerializer from ...component.drf.serializers import AuthModelSerializer @@ -42,7 +47,7 @@ class DurationSerializer(ModelSerializer): class Meta: model = Duration - fields = ('id', 'name', 'start_time', 'end_time') + fields = ("id", "name", "start_time", "end_time") def run_validation(self, data=empty): self.validators = [DurationValidator(self.instance)] @@ -54,7 +59,9 @@ class DaySerializer(ModelSerializer): id = serializers.CharField(required=False, max_length=LEN_SHORT, allow_null=True) - day_of_week = serializers.CharField(required=False, max_length=LEN_SHORT, allow_null=True) + day_of_week = serializers.CharField( + required=False, max_length=LEN_SHORT, allow_null=True + ) start_date = serializers.DateField(required=False, allow_null=True) @@ -62,7 +69,7 @@ class DaySerializer(ModelSerializer): type_of_day = serializers.CharField( required=True, - error_messages={'blank': _("日期类型不能为空")}, + error_messages={"blank": _("日期类型不能为空")}, max_length=LEN_SHORT, # choices=DAY_TYPE_CHOICES ) @@ -78,12 +85,20 @@ class DaySerializer(ModelSerializer): class Meta: model = Day - fields = ('id', 'name', 'type_of_day', 'day_of_week', 'start_date', 'end_date', 'duration') + fields = ( + "id", + "name", + "type_of_day", + "day_of_week", + "start_date", + "end_date", + "duration", + ) def create(self, validated_data): duration = [ - Duration.objects.create(**duration_params) for duration_params in - validated_data.pop("duration", []) + Duration.objects.create(**duration_params) + for duration_params in validated_data.pop("duration", []) ] day = super(DaySerializer, self).create(validated_data) if duration: @@ -97,7 +112,9 @@ def update(self, instance, validated_data): if not duration: return instance - duration = [Duration.objects.create(**duration_params) for duration_params in duration] + duration = [ + Duration.objects.create(**duration_params) for duration_params in duration + ] instance.duration.set(duration) instance.save() return instance @@ -112,7 +129,7 @@ class ScheduleSerializer(AuthModelSerializer, ModelSerializer): name = serializers.CharField( required=True, - error_messages={'blank': _("服务模式名称不能为空")}, + error_messages={"blank": _("服务模式名称不能为空")}, # validators=[name_validator], max_length=LEN_MIDDLE, ) @@ -126,13 +143,21 @@ class Meta: model = Schedule related_fields = ("days", "workdays", "holidays") fields = ( - 'id', 'name', 'is_enabled', 'is_builtin', 'days', 'workdays', 'holidays', 'project_key') + "id", + "name", + "is_enabled", + "is_builtin", + "days", + "workdays", + "holidays", + "project_key", + ) def create(self, validated_data): ScheduleValidator(self.instance)(validated_data) - days = validated_data.pop('days', []) - workdays = validated_data.pop('workdays', []) - holidays = validated_data.pop('holidays', []) + days = validated_data.pop("days", []) + workdays = validated_data.pop("workdays", []) + holidays = validated_data.pop("holidays", []) schedule = super(ScheduleSerializer, self).create(validated_data) @@ -148,7 +173,9 @@ def create(self, validated_data): def update(self, instance, validated_data): - rel_fields = {key: validated_data.pop(key, []) for key in self.Meta.related_fields} + rel_fields = { + key: validated_data.pop(key, []) for key in self.Meta.related_fields + } instance = super(ScheduleSerializer, self).update(instance, validated_data) @@ -158,22 +185,28 @@ def update(self, instance, validated_data): def set_days(self, days, schedule): if not days: return - days = [self.fields.fields['days'].child.create(day_params) for day_params in days] - schedule.days.set(days) + days = [ + self.fields.fields["days"].child.create(day_params) for day_params in days + ] + schedule.days.set(days) def set_workdays(self, workdays, schedule): if not workdays: return - workdays = [self.fields.fields['workdays'].child.create(day_params) for day_params in - workdays] + workdays = [ + self.fields.fields["workdays"].child.create(day_params) + for day_params in workdays + ] schedule.workdays.set(workdays) def set_holidays(self, holidays, schedule): if not holidays: return - holidays = [self.fields.fields['holidays'].child.create(day_params) for day_params in - holidays] + holidays = [ + self.fields.fields["holidays"].child.create(day_params) + for day_params in holidays + ] schedule.holidays.set(holidays) def run_validation(self, data=empty): @@ -194,7 +227,9 @@ class ScheduleDayRelationSerializer(serializers.Serializer): def to_internal_value(self, data): days = data.pop("days", []) - data['days'] = [self.fields.fields['days'].child.to_internal_value(day) for day in days] + data["days"] = [ + self.fields.fields["days"].child.to_internal_value(day) for day in days + ] return data def to_representation(self, instance): @@ -203,7 +238,9 @@ def to_representation(self, instance): def update(self, instance, validated_data): days = validated_data.pop("days", []) try: - add_method = getattr(self, "add_{}s".format(validated_data['type_of_day'].lower())) + add_method = getattr( + self, "add_{}s".format(validated_data["type_of_day"].lower()) + ) except AttributeError: raise serializers.ValidationError(_("不存在的日期类型")) add_method(days, instance) @@ -211,11 +248,11 @@ def update(self, instance, validated_data): def add_workdays(self, workdays, schedule): for day_params in workdays: - schedule.workdays.add(self.fields.fields['days'].child.create(day_params)) + schedule.workdays.add(self.fields.fields["days"].child.create(day_params)) def add_holidays(self, holidays, schedule): for day_params in holidays: - schedule.holidays.add(self.fields.fields['days'].child.create(day_params)) + schedule.holidays.add(self.fields.fields["days"].child.create(day_params)) def create(self, validated_data): pass diff --git a/itsm/sla/validators.py b/itsm/sla/validators.py index 6f6951a69..c9945a041 100644 --- a/itsm/sla/validators.py +++ b/itsm/sla/validators.py @@ -28,7 +28,7 @@ from six.moves import map, range from django.core.validators import RegexValidator -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from itsm.component.constants import ( @@ -97,15 +97,21 @@ def matrix_data_validator(matrix_data, matrix_type): matrix_type_msg = "影响范围" for data in matrix_data: if data.get("id") is None: - raise serializers.ValidationError(_("参数错误,%s数据缺少参数id") % matrix_type_msg) + raise serializers.ValidationError( + _("参数错误,%s数据缺少参数id") % matrix_type_msg + ) if data.get("is_enabled") is None: raise serializers.ValidationError( _("参数错误,%s数据缺少参数is_enabled") % matrix_type_msg ) if data.get("name") is None: - raise serializers.ValidationError(_("参数错误,%s数据缺少参数name") % matrix_type_msg) + raise serializers.ValidationError( + _("参数错误,%s数据缺少参数name") % matrix_type_msg + ) if data.get("key") is None: - raise serializers.ValidationError(_("参数错误,%s数据缺少参数key") % matrix_type_msg) + raise serializers.ValidationError( + _("参数错误,%s数据缺少参数key") % matrix_type_msg + ) def priority_matrix_validator(priority_matrix): @@ -116,7 +122,9 @@ def priority_matrix_validator(priority_matrix): if priority.get("priority") is None: raise ParamError(_("参数错误,优先级数据缺少参数priority")) if not PriorityMatrix.objects.filter(id=priority.get("id")).exists(): - raise ParamError(_("参数错误,不存在id为[%s]的优先级对象") % priority.get("id")) + raise ParamError( + _("参数错误,不存在id为[%s]的优先级对象") % priority.get("id") + ) def priority_validate(impact_data, urgency_data, priority_matrix): @@ -182,7 +190,9 @@ def name_validate(self, value): if self.instance: # 如果是更新,内置的名称不能更新 if self.instance.is_builtin and value.get("name") != self.instance.name: - raise ParamError(_("内置服务模式:[%s] 的名称不能修改") % self.instance.name) + raise ParamError( + _("内置服务模式:[%s] 的名称不能修改") % self.instance.name + ) schedule_obj = schedule_obj.exclude(id=self.instance.id) if schedule_obj.filter( @@ -203,7 +213,9 @@ def date_compare(dates, day_type_msg): if max(day.get("start_date"), other_day.get("start_date")) < min( day.get("end_date"), other_day.get("end_date") ): - raise ParamError(_("{}时间段设置有冲突,请检查").format(day_type_msg)) + raise ParamError( + _("{}时间段设置有冲突,请检查").format(day_type_msg) + ) holidays = value.get("holidays") workdays = value.get("workdays") @@ -254,7 +266,9 @@ def holiday_validate(holiday): if not holiday.get("name"): raise serializers.ValidationError(_("请输入节假日名称")) if holiday.get("start_date") > holiday.get("end_date"): - raise ParamError(_("节假日期:[%s]的时间范围设置错误") % holiday.get("name")) + raise ParamError( + _("节假日期:[%s]的时间范围设置错误") % holiday.get("name") + ) def workday_validate(self, workday): """特定工作日校验""" @@ -322,7 +336,9 @@ def name_validate(self, value): if self.instance: # 如果是更新,内置的名称不能更新 if self.instance.is_builtin and value.get("name") != self.instance.name: - raise ParamError(_("内置服务协议:[%s] 的名称不能修改" % self.instance.name)) + raise ParamError( + _("内置服务协议:[%s] 的名称不能修改" % self.instance.name) + ) sla_obj = sla_obj.exclude(id=self.instance.id) if sla_obj.filter(name=value.get("name"), project_key=project_key).exists(): @@ -349,8 +365,14 @@ def condition_validate(value): if expressions: for expression in expressions: if expression.get("operator") is None: - raise serializers.ValidationError(_("参数错误,计时规则条件表达式缺少operator")) + raise serializers.ValidationError( + _("参数错误,计时规则条件表达式缺少operator") + ) if expression.get("name") is None: - raise serializers.ValidationError(_("参数错误,计时规则条件表达式缺少name")) + raise serializers.ValidationError( + _("参数错误,计时规则条件表达式缺少name") + ) if expression.get("value") is None: - raise serializers.ValidationError(_("参数错误,计时规则条件表达式缺少value")) + raise serializers.ValidationError( + _("参数错误,计时规则条件表达式缺少value") + ) diff --git a/itsm/sla/views/policy.py b/itsm/sla/views/policy.py index 97e6d2702..caf09ba10 100644 --- a/itsm/sla/views/policy.py +++ b/itsm/sla/views/policy.py @@ -24,20 +24,26 @@ """ from django.db.models import Q -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response from itsm.component.drf.exception import ValidationError from itsm.component.drf.viewsets import NormalModelViewSet, AuthModelViewSet -from itsm.sla.models import ActionPolicy, PriorityPolicy, Sla, SlaTimerRule, SlaTicketHighlight +from itsm.sla.models import ( + ActionPolicy, + PriorityPolicy, + Sla, + SlaTimerRule, + SlaTicketHighlight, +) from itsm.sla.serializers import ( ActionPolicySerializer, PriorityPolicySerializer, SlaSerializer, SlaTimerRuleSerializer, - TicketHighlightSerializer + TicketHighlightSerializer, ) from itsm.sla.validators import sla_can_destroy from itsm.ticket_status.models import TicketStatus, TicketStatusConfig @@ -69,9 +75,9 @@ def batch_create(self, request, *args, **kwargs): 批量创建 """ data = request.data - basic_info = {"name": data['name'], "service_type": data['service_type']} + basic_info = {"name": data["name"], "service_type": data["service_type"]} rules = [] - for rule in data['rules']: + for rule in data["rules"]: rule.update(basic_info) serializer = self.get_serializer(data=rule) serializer.is_valid(raise_exception=True) @@ -80,20 +86,20 @@ def batch_create(self, request, *args, **kwargs): rules.append(serializer.data) # 创建完后,修改工单状态配置为已配置状态 - TicketStatusConfig.update_config(data['service_type'], request.user, True) + TicketStatusConfig.update_config(data["service_type"], request.user, True) return Response(rules, status=status.HTTP_201_CREATED, headers=headers) - @action(detail=False, methods=['post']) + @action(detail=False, methods=["post"]) def batch_update(self, request, *args, **kwargs): """ 批量修改 """ data = request.data - basic_info = {"name": data['name'], "service_type": data['service_type']} + basic_info = {"name": data["name"], "service_type": data["service_type"]} rules = [] - for rule in data['rules']: + for rule in data["rules"]: rule.update(basic_info) try: instance = self.queryset.get(id=rule.pop("id", 0)) @@ -108,7 +114,7 @@ def batch_update(self, request, *args, **kwargs): self.perform_update(serializer) rules.append(serializer.data) # 修改完后,修改工单状态配置为已配置状态 - TicketStatusConfig.update_config(data['service_type'], request.user, True) + TicketStatusConfig.update_config(data["service_type"], request.user, True) return Response(rules) @@ -145,7 +151,7 @@ class SlaViewSet(AuthModelViewSet): serializer_class = SlaSerializer queryset = Sla.objects.all() - permission_classes = (SlaPermit, ) + permission_classes = (SlaPermit,) permission_free_actions = ["list"] filter_fields = { "name": ["exact", "contains", "icontains"], @@ -157,10 +163,14 @@ def get_queryset(self): if not self.request.query_params.get("page_size"): self.pagination_class = None return super(SlaViewSet, self).get_queryset().filter() - + def list(self, request, *args, **kwargs): - project_key = self.request.query_params.get("project_key", DEFAULT_PROJECT_PROJECT_KEY) - queryset = self.filter_queryset(self.get_queryset().filter(project_key=project_key)) + project_key = self.request.query_params.get( + "project_key", DEFAULT_PROJECT_PROJECT_KEY + ) + queryset = self.filter_queryset( + self.get_queryset().filter(project_key=project_key) + ) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) @@ -191,7 +201,9 @@ def perform_update(self, serializer): for service_type, statuses in is_over_statuses.items(): sub_q = Q() sub_q.connector = "AND" - sub_q.children.extend([("service_type", service_type), ("current_status__in", statuses)]) + sub_q.children.extend( + [("service_type", service_type), ("current_status__in", statuses)] + ) is_over_q.add(sub_q, "OR") if serializer.instance.get_tickets().exclude(is_over_q).exists(): diff --git a/itsm/sla_engine/models.py b/itsm/sla_engine/models.py index 3bf6a9f77..65a5c56d2 100644 --- a/itsm/sla_engine/models.py +++ b/itsm/sla_engine/models.py @@ -28,7 +28,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from jsonfield import JSONField from django.db import transaction from common.redis import Cache @@ -72,8 +72,12 @@ class SlaTask(models.Model): end_at = models.DateTimeField(_("任务结束计时的时间"), null=True) upgrade_at = models.DateTimeField(_("任务升级时间"), null=True) last_settlement_time = models.DateTimeField(_("上次结算时间"), null=True) - sla_status = models.IntegerField(_("sla状态"), choices=SLA_TIMING_STATUS, default=NORMAL) - task_status = models.IntegerField(_("任务状态"), choices=SLA_TASK_STATUS, default=UNACTIVATED) + sla_status = models.IntegerField( + _("sla状态"), choices=SLA_TIMING_STATUS, default=NORMAL + ) + task_status = models.IntegerField( + _("任务状态"), choices=SLA_TASK_STATUS, default=UNACTIVATED + ) is_reply_need = models.BooleanField(_("是否需要响应"), default=False) is_replied = models.BooleanField(_("是否已响应"), default=False) reply_deadline = models.DateTimeField(_("响应截至时间"), null=True) @@ -182,13 +186,17 @@ def reply_timeout_time(self, start_time=None): """ 响应超时时间 """ - return action_time(self.reply_time, self.sla_id, self.ticket.priority, start_time) + return action_time( + self.reply_time, self.sla_id, self.ticket.priority, start_time + ) def handle_timeout_time(self, start_time=None): """ 处理超时时间 """ - return action_time(self.handle_time, self.sla_id, self.ticket.priority, start_time) + return action_time( + self.handle_time, self.sla_id, self.ticket.priority, start_time + ) @property def protocol_name(self): @@ -221,8 +229,9 @@ def get_cost_time(self, current_time=None): if not start_time: start_time = self.begin_at - new_duration = action_time_delta(start_time, current_time, self.sla_id, - self.ticket.priority) + new_duration = action_time_delta( + start_time, current_time, self.sla_id, self.ticket.priority + ) cost_time = self.cost_time + new_duration return cost_time @@ -246,7 +255,11 @@ def update_sla_status(self, current_time): return # 响应超时 - if self.is_reply_need and not self.is_replied and current_time > self.reply_deadline: + if ( + self.is_reply_need + and not self.is_replied + and current_time > self.reply_deadline + ): self.sla_status = REPLY_TIMEOUT self.save() return @@ -276,10 +289,12 @@ def start(self, begin_at): ac_time = action_trigger_time.strftime("%Y-%m-%d %H:%M") ac_key = "{ticket_id}-{sla_task_id}-{action_policy_type}".format( - ticket_id=self.ticket_id, sla_task_id=self.id, action_policy_type=action_policy.type + ticket_id=self.ticket_id, + sla_task_id=self.id, + action_policy_type=action_policy.type, ) # 任务用到的必要参数 - + ac_value = json.dumps([action.id for action in action_policy.actions.all()]) # 插入数据到redis @@ -299,7 +314,9 @@ def reply(self, replied_at): self.reply_cost = self.cost_time self.save() - action_policies = self.action_policies.filter(type__in=[REPLY_WARING, REPLY_TIMEOUT]) + action_policies = self.action_policies.filter( + type__in=[REPLY_WARING, REPLY_TIMEOUT] + ) self.delete_redis_task(action_policies) @_frozen_check @@ -319,7 +336,9 @@ def resume(self, resume_at): action_policies = self.action_policies # 已响应的去除响应策略 if self.is_reply_need and self.is_replied: - action_policies = action_policies.exclude(type__in=[REPLY_WARING, REPLY_TIMEOUT]) + action_policies = action_policies.exclude( + type__in=[REPLY_WARING, REPLY_TIMEOUT] + ) self._refresh_redis_task(action_policies, resume_at) @@ -361,7 +380,9 @@ def refresh(self, refresh_at): action_policies = self.action_policies # 已响应的去除响应策略 if self.is_reply_need and self.is_replied: - action_policies = action_policies.exclude(type__in=[REPLY_WARING, REPLY_TIMEOUT]) + action_policies = action_policies.exclude( + type__in=[REPLY_WARING, REPLY_TIMEOUT] + ) self.delete_redis_task(action_policies) self._refresh_redis_task(action_policies, refresh_at) @@ -377,11 +398,14 @@ def _refresh_redis_task(self, action_policies, refresh_at): # 策略触发时间大于恢复时间,重新入库redis if action_trigger_time > refresh_at: ac_key = "{ticket_id}-{sla_task_id}-{action_policy_type}".format( - ticket_id=self.ticket_id, sla_task_id=self.id, - action_policy_type=action_policy.type + ticket_id=self.ticket_id, + sla_task_id=self.id, + action_policy_type=action_policy.type, ) # 任务用到的必要参数 - ac_value = json.dumps([action.id for action in action_policy.actions.all()]) + ac_value = json.dumps( + [action.id for action in action_policy.actions.all()] + ) # 插入数据到redis # 按时间记录任务 sla_redis_inst.hsetnx(name=ac_time, key=ac_key, value=ac_value) @@ -390,8 +414,9 @@ def _refresh_redis_task(self, action_policies, refresh_at): sla_redis_inst.sadd(SLA_ACTION_TIME, ac_time) else: for action in action_policy.actions.all(): - sla_task_action = SlaTaskAction(action, self.ticket, self, action_policy.type, - ac_time) + sla_task_action = SlaTaskAction( + action, self.ticket, self, action_policy.type, ac_time + ) sla_task_action.alert() def delete_redis_task(self, action_policies): @@ -403,7 +428,9 @@ def delete_redis_task(self, action_policies): def get_ac_keys(self, action_policies): ac_keys = [ "{ticket_id}-{sla_task_id}-{action_policy_type}".format( - ticket_id=self.ticket_id, sla_task_id=self.id, action_policy_type=action_policy.type + ticket_id=self.ticket_id, + sla_task_id=self.id, + action_policy_type=action_policy.type, ) for action_policy in action_policies ] @@ -428,31 +455,51 @@ def frozen(self): class SlaEventLogManager(models.Manager): def get_last_start_event(self, sla_task_id): """获取最后一次启动计时的事件""" - return self.filter(sla_task_id=sla_task_id, tick_flag='START', is_archived=False).last() + return self.filter( + sla_task_id=sla_task_id, tick_flag="START", is_archived=False + ).last() def get_last_stop_event(self, sla_task_id): """获取最后一次停止计时的事件""" - return self.filter(sla_task_id=sla_task_id, tick_flag='END', is_archived=False).last() + return self.filter( + sla_task_id=sla_task_id, tick_flag="END", is_archived=False + ).last() def create_start_event(self, sla_task_id, priority): """创建开始事件""" - return self.create(sla_task_id=sla_task_id, priority=priority, event_type='START', - tick_flag='START', ) + return self.create( + sla_task_id=sla_task_id, + priority=priority, + event_type="START", + tick_flag="START", + ) def create_pause_event(self, sla_task_id, priority): """创建暂停事件""" - return self.create(sla_task_id=sla_task_id, priority=priority, event_type='PAUSE', - tick_flag='END', ) + return self.create( + sla_task_id=sla_task_id, + priority=priority, + event_type="PAUSE", + tick_flag="END", + ) def create_resume_event(self, sla_task_id, priority): """创建恢复事件""" - return self.create(sla_task_id=sla_task_id, priority=priority, event_type='RESUME', - tick_flag='START', ) + return self.create( + sla_task_id=sla_task_id, + priority=priority, + event_type="RESUME", + tick_flag="START", + ) def create_stop_event(self, sla_task_id, priority): """创建停止事件""" - return self.create(sla_task_id=sla_task_id, priority=priority, event_type='STOP', - tick_flag='END', ) + return self.create( + sla_task_id=sla_task_id, + priority=priority, + event_type="STOP", + tick_flag="END", + ) class SlaEventLog(models.Model): @@ -461,16 +508,30 @@ class SlaEventLog(models.Model): 目前主要有:启动、停止、暂停、恢复 """ - sla_task_id = models.IntegerField(_("SLA TASK ID"), db_index=True, default=EMPTY_INT) + sla_task_id = models.IntegerField( + _("SLA TASK ID"), db_index=True, default=EMPTY_INT + ) priority = models.CharField(_("优先级"), max_length=LEN_LONG) event_type = models.CharField( - _("事件类型"), max_length=LEN_LONG, - choices=[('PAUSE', "暂停"), ('RESUME', "恢复"), ('STOP', "停止"), ('START', "启动"), ] + _("事件类型"), + max_length=LEN_LONG, + choices=[ + ("PAUSE", "暂停"), + ("RESUME", "恢复"), + ("STOP", "停止"), + ("START", "启动"), + ], ) is_archived = models.BooleanField(_("是否已归档"), default=False) tick_flag = models.CharField( - _("计时标志"), max_length=LEN_LONG, - choices=[('START', "开始计时"), ('END', "结束计时"), ('KEEP', "保持"), ], default='KEEP' + _("计时标志"), + max_length=LEN_LONG, + choices=[ + ("START", "开始计时"), + ("END", "结束计时"), + ("KEEP", "保持"), + ], + default="KEEP", ) create_time = models.DateTimeField(_("事件发生时间"), auto_now_add=True) @@ -494,15 +555,20 @@ def mark_archived(self): class SlaActionHistoryManager(models.Manager): def get_last_success_action(self, action_id, action_type): """获取最后一次成功执行的sla行为""" - return self.filter(action_id=action_id, action_type=action_type, status="SUCCESS").first() + return self.filter( + action_id=action_id, action_type=action_type, status="SUCCESS" + ).first() class SlaActionHistory(models.Model): """sla行为历史记录""" action_id = models.IntegerField(_("任务ID"), db_index=True, default=EMPTY_INT) - status = models.CharField(_("结果状态"), max_length=LEN_LONG, - choices=[("SUCCESS", _("成功")), ("FAILED", _("失败"))]) + status = models.CharField( + _("结果状态"), + max_length=LEN_LONG, + choices=[("SUCCESS", _("成功")), ("FAILED", _("失败"))], + ) action_type = models.CharField(_("行为类型"), max_length=LEN_LONG) action_detail = JSONField(_("行为详情"), default=EMPTY_DICT) create_time = models.DateTimeField(_("动作发生时间"), auto_now_add=True) diff --git a/itsm/sla_engine/monitor.py b/itsm/sla_engine/monitor.py index 098c658ab..57dc58554 100644 --- a/itsm/sla_engine/monitor.py +++ b/itsm/sla_engine/monitor.py @@ -26,21 +26,29 @@ import datetime import json +from celery import shared_task from celery.schedules import crontab -from celery.task import periodic_task, task +from blueapps.contrib.celery_tools.periodic import periodic_task from django.db import transaction from common.redis import Cache from itsm.sla.models import Action from itsm.sla_engine.actions import SlaTaskAction -from itsm.sla_engine.constants import SLA_ACTION_TIME, RUNNING, REPLY_WARING, REPLY_TIMEOUT +from itsm.sla_engine.constants import ( + SLA_ACTION_TIME, + RUNNING, + REPLY_WARING, + REPLY_TIMEOUT, +) from itsm.sla_engine.models import SlaTask from itsm.ticket.models import Ticket -@task +@shared_task def action_exclude(ac_key, ac_value, ac_time): - ticket_id, sla_task_id, action_policy_type = [int(item) for item in ac_key.split("-")] + ticket_id, sla_task_id, action_policy_type = [ + int(item) for item in ac_key.split("-") + ] sla_task = SlaTask.objects.get(id=sla_task_id) if sla_task.task_status != RUNNING: @@ -51,7 +59,9 @@ def action_exclude(ac_key, ac_value, ac_time): with transaction.atomic(): for action in Action.objects.filter(id__in=action_ids): - sla_task_action = SlaTaskAction(action, ticket, sla_task, action_policy_type, ac_time) + sla_task_action = SlaTaskAction( + action, ticket, sla_task, action_policy_type, ac_time + ) sla_task_action.alert() @@ -78,7 +88,14 @@ def sla_task_metric(): sla_redis_inst.srem(SLA_ACTION_TIME, ac_time) -@periodic_task(run_every=(crontab(minute="*/10", )), ignore_result=True) +@periodic_task( + run_every=( + crontab( + minute="*/10", + ) + ), + ignore_result=True, +) def compensate_task(): """ 补偿遗漏的提醒任务 @@ -105,7 +122,14 @@ def compensate_task(): sla_redis_inst.srem(SLA_ACTION_TIME, ac_time) -@periodic_task(run_every=(crontab(minute="*/10", )), ignore_result=True) +@periodic_task( + run_every=( + crontab( + minute="*/10", + ) + ), + ignore_result=True, +) def rebuild_sla_task(): """ 根据SlaTask表中RUNNING的任务重建入库redis失败的任务 @@ -114,8 +138,10 @@ def rebuild_sla_task(): for sla_task in sla_tasks: action_policies = sla_task.action_policies if sla_task.is_reply_need and not sla_task.is_replied: - action_policies = action_policies.exclude(type__in=[REPLY_WARING, REPLY_TIMEOUT]) - ac_keys = sla_task.get_ac_keys(action_policies) + action_policies = action_policies.exclude( + type__in=[REPLY_WARING, REPLY_TIMEOUT] + ) + _ = sla_task.get_ac_keys(action_policies) def update_sla_task(ac_time_dict, current_time): diff --git a/itsm/task/models.py b/itsm/task/models.py index f11e88c63..f7a3f2c37 100644 --- a/itsm/task/models.py +++ b/itsm/task/models.py @@ -34,7 +34,7 @@ from django.db.models import Q from django.db import models, transaction from django.utils.functional import cached_property -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from mako.template import Template from bulk_update.helper import bulk_update from pipeline.engine import api as pipeline_api @@ -138,7 +138,9 @@ class Task(Model): ticket_id = models.IntegerField(_("单据ID"), default=0) state_id = models.IntegerField(_("节点ID"), default=0) - activity_id = models.CharField(_("Pipeline节点ID"), max_length=LEN_NORMAL, blank=True) + activity_id = models.CharField( + _("Pipeline节点ID"), max_length=LEN_NORMAL, blank=True + ) name = models.CharField(_("任务的名称"), max_length=LEN_LONG) task_schema_id = models.IntegerField(_("对应的任务模板ID"), null=False) component_type = models.CharField( @@ -148,16 +150,27 @@ class Task(Model): null=False, ) processors_type = models.CharField( - _("处理人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("处理人类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", ) processors = models.CharField( - _("处理人列表"), max_length=LEN_LONG, default=EMPTY_STRING, null=True, blank=True + _("处理人列表"), + max_length=LEN_LONG, + default=EMPTY_STRING, + null=True, + blank=True, ) inputs = jsonfield.JSONField( - _("组件输入信息"), help_text=_("当前组件输入参数引用的参数变量"), default=EMPTY_DICT + _("组件输入信息"), + help_text=_("当前组件输入参数引用的参数变量"), + default=EMPTY_DICT, ) outputs = jsonfield.JSONField( - _("组件输出信息"), help_text=_("当前组件输出信息,比如sops各阶段返回"), default=EMPTY_DICT + _("组件输出信息"), + help_text=_("当前组件输出信息,比如sops各阶段返回"), + default=EMPTY_DICT, ) order = models.IntegerField(_("任务的执行顺序"), default=1) @@ -168,8 +181,12 @@ class Task(Model): default="NEW", ) - executor = models.CharField(_("处理人"), max_length=LEN_NORMAL, default=EMPTY_STRING) - confirmer = models.CharField(_("确认人"), max_length=LEN_NORMAL, default=EMPTY_STRING) + executor = models.CharField( + _("处理人"), max_length=LEN_NORMAL, default=EMPTY_STRING + ) + confirmer = models.CharField( + _("确认人"), max_length=LEN_NORMAL, default=EMPTY_STRING + ) start_at = models.DateTimeField(_("开始执行的时间"), null=True) end_at = models.DateTimeField(_("结束执行的时间"), null=True) pipeline_data = jsonfield.JSONField(_("Pipeline流程树元数据"), default=EMPTY_DICT) @@ -795,7 +812,8 @@ def retry_sops_task(self, sops_task, retry_operator): # 获取不到标准运维任务信息,直接设置为异常 return { "result": False, - "message": "重试失败,获取标准运维任务信息出错 %s" % res.get("message", ""), + "message": "重试失败,获取标准运维任务信息出错 %s" + % res.get("message", ""), } failed_nodes = [ @@ -805,7 +823,7 @@ def retry_sops_task(self, sops_task, retry_operator): ] if not failed_nodes: # 不存在失败节点的时候,说明任务成功执行了 - return {"result": True, "message": u"重试成功,标准运维任务已经在执行中"} + return {"result": True, "message": "重试成功,标准运维任务已经在执行中"} for node_id in failed_nodes: # 依次进行重试,除了并行节点,一般只会有一个错误的id @@ -1001,11 +1019,16 @@ class SopsTask(Model): task_id = models.CharField(_("itsm任务ID"), max_length=LEN_NORMAL) task_name = models.CharField(_("任务的名称"), max_length=LEN_LONG) sops_template_id = models.IntegerField(_("sops任务模板ID")) - sops_task_id = models.IntegerField(_("sops任务ID,成功启动后填充"), null=True, blank=True) + sops_task_id = models.IntegerField( + _("sops任务ID,成功启动后填充"), null=True, blank=True + ) # params|detail|status sops_task_info = jsonfield.JSONField(_("sops任务信息"), default=EMPTY_DICT) sops_task_url = models.CharField( - _("sops任务详情链接,成功启动后填充"), max_length=LEN_LONG, null=True, blank=True + _("sops任务详情链接,成功启动后填充"), + max_length=LEN_LONG, + null=True, + blank=True, ) creator = models.CharField(_("创建者"), max_length=LEN_NORMAL, blank=True) @@ -1056,12 +1079,14 @@ def get_status(self): "task_name": self.task_name, "sops_task_url": self.sops_task_url, "create_time": self.create_time.strftime("%Y-%m-%d %H:%M:%S"), - "start_time": self.start_time.strftime("%Y-%m-%d %H:%M:%S") - if self.start_time - else "", - "finish_time": self.finish_time.strftime("%Y-%m-%d %H:%M:%S") - if self.finish_time - else "", + "start_time": ( + self.start_time.strftime("%Y-%m-%d %H:%M:%S") if self.start_time else "" + ), + "finish_time": ( + self.finish_time.strftime("%Y-%m-%d %H:%M:%S") + if self.finish_time + else "" + ), "elapsed_time": self.elapsed_time, "state": self.state, } @@ -1123,12 +1148,14 @@ def get_status(self): "task_name": self.task_name, "sub_task_url": self.sub_task_url, "create_time": self.create_time.strftime("%Y-%m-%d %H:%M:%S"), - "start_time": self.start_time.strftime("%Y-%m-%d %H:%M:%S") - if self.start_time - else "", - "finish_time": self.finish_time.strftime("%Y-%m-%d %H:%M:%S") - if self.finish_time - else "", + "start_time": ( + self.start_time.strftime("%Y-%m-%d %H:%M:%S") if self.start_time else "" + ), + "finish_time": ( + self.finish_time.strftime("%Y-%m-%d %H:%M:%S") + if self.finish_time + else "" + ), "elapsed_time": self.elapsed_time, "state": self.state, "sub_pipeline_id": self.sub_pipeline_id, @@ -1195,7 +1222,9 @@ class TaskLibTasks(Model): component_type = models.CharField(_("任务类型"), max_length=LEN_NORMAL) processors_type = models.CharField(_("处理人类型"), max_length=LEN_NORMAL) processors = models.CharField(_("处理人"), max_length=LEN_LONG) - fields = jsonfield.JSONField(_("字段列表"), max_length=LEN_XX_LONG, default=EMPTY_LIST) + fields = jsonfield.JSONField( + _("字段列表"), max_length=LEN_XX_LONG, default=EMPTY_LIST + ) sub_template_id = models.CharField(_("子模版ID"), default="", max_length=LEN_NORMAL) project_id = models.CharField(_("项目ID/业务ID"), default="", max_length=LEN_NORMAL) exclude_task_nodes = jsonfield.JSONField( diff --git a/itsm/task/permissions.py b/itsm/task/permissions.py index c4cd9b792..918d8922a 100644 --- a/itsm/task/permissions.py +++ b/itsm/task/permissions.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import permissions from itsm.role.models import UserRole @@ -69,7 +69,7 @@ def __init__(self): def has_object_permission(self, request, view, obj): username = request.user.username - if view.action == 'proceed': + if view.action == "proceed": return obj.can_process(username) return True diff --git a/itsm/task/serializers.py b/itsm/task/serializers.py index 2df8f4293..ce9069d2b 100644 --- a/itsm/task/serializers.py +++ b/itsm/task/serializers.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.request import Request @@ -112,12 +112,18 @@ class TaskSerializer(serializers.ModelSerializer): """任务序列化""" name = serializers.CharField(required=False, max_length=50, allow_blank=True) - processors_type = serializers.CharField(required=False, allow_blank=True, max_length=LEN_LONG, default=EMPTY_STRING) - processors = serializers.CharField(required=False, allow_blank=True, max_length=LEN_LONG, default=EMPTY_STRING) + processors_type = serializers.CharField( + required=False, allow_blank=True, max_length=LEN_LONG, default=EMPTY_STRING + ) + processors = serializers.CharField( + required=False, allow_blank=True, max_length=LEN_LONG, default=EMPTY_STRING + ) task_schema_id = serializers.IntegerField(required=True) state_id = serializers.IntegerField(required=True) order = serializers.IntegerField(required=False, default=1) - component_type = serializers.CharField(required=False, max_length=LEN_LONG, default=EMPTY_STRING) + component_type = serializers.CharField( + required=False, max_length=LEN_LONG, default=EMPTY_STRING + ) fields = serializers.JSONField(required=False, default={}) class Meta: @@ -154,22 +160,28 @@ def validate(self, attrs): return validated_data try: - schema_instance = TaskSchema.objects.get(id=validated_data['task_schema_id']) + schema_instance = TaskSchema.objects.get( + id=validated_data["task_schema_id"] + ) except TaskSchema.DoesNotExist: raise ValidationError(detail=_("对应的任务模板配置不存在")) - ticket = Ticket.objects.get(id=validated_data['ticket_id']) + ticket = Ticket.objects.get(id=validated_data["ticket_id"]) task_config = TaskConfig.objects.filter( - workflow_id=ticket.flow_id, workflow_type=VERSION, create_task_state=validated_data['state_id'] + workflow_id=ticket.flow_id, + workflow_type=VERSION, + create_task_state=validated_data["state_id"], ).first() if task_config: - validated_data['execute_state_id'] = task_config.execute_task_state + validated_data["execute_state_id"] = task_config.execute_task_state else: is_exist = TaskConfig.objects.filter( - workflow_id=ticket.flow_id, workflow_type=VERSION, execute_task_state=validated_data['state_id'] + workflow_id=ticket.flow_id, + workflow_type=VERSION, + execute_task_state=validated_data["state_id"], ).exists() if is_exist: - validated_data['execute_state_id'] = validated_data['state_id'] + validated_data["execute_state_id"] = validated_data["state_id"] else: raise ValidationError(detail=_("对应的任务配置不存在")) # 更新component_type @@ -183,15 +195,25 @@ def to_representation(self, instance): data = super(TaskSerializer, self).to_representation(instance) # 首尾去掉逗号 data.update(processors=normal_name(data["processors"])) - if isinstance(self.context.get("view"), ModelViewSet) and self.context["view"].detail: + if ( + isinstance(self.context.get("view"), ModelViewSet) + and self.context["view"].detail + ): create_fields = TaskFieldSerializer(instance.create_fields, many=True).data - operate_fields = TaskFieldSerializer(instance.operate_fields, many=True).data - confirm_fields = TaskFieldSerializer(instance.confirm_fields, many=True).data + operate_fields = TaskFieldSerializer( + instance.operate_fields, many=True + ).data + confirm_fields = TaskFieldSerializer( + instance.confirm_fields, many=True + ).data if instance.component_type == SOPS_TASK: sops_task = SopsTask.objects.get(task_id=instance.id) try: detail = client_backend.sops.get_task_detail( - {"bk_biz_id": sops_task.bk_biz_id, "task_id": sops_task.sops_task_id} + { + "bk_biz_id": sops_task.bk_biz_id, + "task_id": sops_task.sops_task_id, + } ) except Exception: raise ComponentCallError(_("标准运维获取任务详情失败")) @@ -200,10 +222,14 @@ def to_representation(self, instance): if field["key"] == SOPS_TEMPLATE_KEY: sops_constants = {} for sops_constant in detail["constants"].values(): - sops_constants[sops_constant["key"]] = sops_constant["value"] + sops_constants[sops_constant["key"]] = sops_constant[ + "value" + ] for constant in field["value"]["constants"]: if constant.get("is_quoted", False): - current_value = Template(constant["value"]).render(**outputs) + current_value = Template(constant["value"]).render( + **outputs + ) changed = ( current_value != sops_constants[constant["key"]] if constant["key"] in sops_constants @@ -212,17 +238,23 @@ def to_representation(self, instance): constant["changed"] = changed else: constant["changed"] = False - constant["value"] = sops_constants.get(constant["key"], constant.get("value", "")) - field["display_value"]["constants"] = field["value"]["constants"] + constant["value"] = sops_constants.get( + constant["key"], constant.get("value", "") + ) + field["display_value"]["constants"] = field["value"][ + "constants" + ] data["sops_task_url"] = sops_task.sops_task_url data["fields"] = { - 'create_fields': create_fields, - 'operate_fields': operate_fields, - 'confirm_fields': confirm_fields, + "create_fields": create_fields, + "operate_fields": operate_fields, + "confirm_fields": confirm_fields, } if isinstance(self.context.get("request"), Request): - data["can_process"] = instance.can_process(self.context["request"].user.username) + data["can_process"] = instance.can_process( + self.context["request"].user.username + ) return data @@ -257,7 +289,9 @@ def __init__(self, instance=None, data=empty, **kwargs): def get_sops_tasks(self): tasks = [] if self.instance is None else self.instance task_ids = [task.id for task in tasks] - sops_task_ids = SopsTask.objects.filter(task_id__in=task_ids).values("task_id", "sops_task_url") + sops_task_ids = SopsTask.objects.filter(task_id__in=task_ids).values( + "task_id", "sops_task_url" + ) sops_task_map = {} for sops_task in sops_task_ids: sops_task_map[sops_task["task_id"]] = sops_task["sops_task_url"] @@ -266,7 +300,9 @@ def get_sops_tasks(self): def get_devops_tasks(self): tasks = [] if self.instance is None else self.instance task_ids = [task.id for task in tasks] - sub_task_ids = SubTask.objects.filter(task_id__in=task_ids).values("task_id", "sub_task_url") + sub_task_ids = SubTask.objects.filter(task_id__in=task_ids).values( + "task_id", "sub_task_url" + ) sub_task_map = {} for sub_task in sub_task_ids: sub_task_map[sub_task["task_id"]] = sub_task["sub_task_url"] @@ -277,9 +313,13 @@ def to_representation(self, instance): # 首尾去掉逗号 data.update(processors=normal_name(data["processors"])) if isinstance(self.context.get("request"), Request): - data["can_process"] = instance.can_process(self.context["request"].user.username) + data["can_process"] = instance.can_process( + self.context["request"].user.username + ) if instance.component_type in [SOPS_TASK, DEVOPS_TASK]: - data["task_url"] = self.sops_tasks.get(str(data["id"]), "") or self.devops_tasks.get(str(data["id"]), "") + data["task_url"] = self.sops_tasks.get( + str(data["id"]), "" + ) or self.devops_tasks.get(str(data["id"]), "") return data @@ -294,8 +334,12 @@ def update(self, instance, validated_data): class TaskOrderSerializer(serializers.Serializer): - task_orders = serializers.ListField(required=True, allow_empty=True, validators=[TaskOrdersValidator()]) - ticket_id = serializers.IntegerField(required=True, validators=[TicketValidValidator()]) + task_orders = serializers.ListField( + required=True, allow_empty=True, validators=[TaskOrdersValidator()] + ) + ticket_id = serializers.IntegerField( + required=True, validators=[TicketValidValidator()] + ) def create(self, validated_data): return super().create(validated_data) @@ -305,7 +349,9 @@ def update(self, instance, validated_data): class TaskFieldBatchUpdateSerializer(serializers.Serializer): - fields = serializers.ListField(required=True, allow_empty=True, validators=[TaskFieldBatchUpdateValidator()]) + fields = serializers.ListField( + required=True, allow_empty=True, validators=[TaskFieldBatchUpdateValidator()] + ) def create(self, validated_data): return super().create(validated_data) @@ -333,7 +379,10 @@ def validate(self, attrs): class TaskProceedSerializer(serializers.Serializer): - action = serializers.ChoiceField(choices=[(ACTION_OPERATE, _("处理")), (ACTION_CONFIRM, _("总结"))], required=True) + action = serializers.ChoiceField( + choices=[(ACTION_OPERATE, _("处理")), (ACTION_CONFIRM, _("总结"))], + required=True, + ) fields = serializers.ListField(required=True, allow_empty=True) def validate(self, attrs): @@ -341,11 +390,13 @@ def validate(self, attrs): # 校验fields fields = validated_data.get("fields", {}) - action = attrs.get('action') - task = self.context['task'] + action = attrs.get("action") + task = self.context["task"] # 检验字段合法性:必填校验(未考虑隐藏字段) - task_fields = task.operate_fields if action == ACTION_OPERATE else task.confirm_fields + task_fields = ( + task.operate_fields if action == ACTION_OPERATE else task.confirm_fields + ) validate_task_fields(task_fields, fields) return validated_data @@ -370,7 +421,7 @@ def validate(self, attrs): fields = validated_data.get("fields", {}) # 检验字段合法性:必填校验(未考虑隐藏字段) - task_fields = self.context['task'].operate_fields + task_fields = self.context["task"].operate_fields validate_task_fields(task_fields, fields) return validated_data @@ -392,8 +443,12 @@ class TaskLibTasksSerializer(serializers.ModelSerializer): processors_type = serializers.CharField(required=True, max_length=LEN_NORMAL) processors = serializers.CharField(required=True, max_length=LEN_LONG) fields = serializers.JSONField(required=True) - sub_template_id = serializers.CharField(required=False, default="", max_length=LEN_NORMAL, allow_blank=True) - project_id = serializers.CharField(required=False, default="", max_length=LEN_NORMAL, allow_blank=True) + sub_template_id = serializers.CharField( + required=False, default="", max_length=LEN_NORMAL, allow_blank=True + ) + project_id = serializers.CharField( + required=False, default="", max_length=LEN_NORMAL, allow_blank=True + ) exclude_task_nodes = serializers.JSONField(required=True) class Meta: @@ -425,10 +480,14 @@ def validate(self, attrs): if ( self.instance is None and TaskLib.objects.filter( - service_id=attrs['service_id'], name=attrs['name'], creator=attrs['creator'] + service_id=attrs["service_id"], + name=attrs["name"], + creator=attrs["creator"], ).exists() ): - raise serializers.ValidationError({str(_('参数校验失败')): _('您名下已经有同名任务库,请尝试换个名称')}) + raise serializers.ValidationError( + {str(_("参数校验失败")): _("您名下已经有同名任务库,请尝试换个名称")} + ) return attrs diff --git a/itsm/task/tasks.py b/itsm/task/tasks.py index 2bb4dff0b..52484cc43 100644 --- a/itsm/task/tasks.py +++ b/itsm/task/tasks.py @@ -28,7 +28,7 @@ from collections import defaultdict from operator import itemgetter from celery.schedules import crontab -from celery.task import periodic_task +from blueapps.contrib.celery_tools.periodic import periodic_task from itsm.component.constants.task import ( NEED_UPDATE_TASK_STATUS, @@ -48,7 +48,7 @@ from itsm.task.models import SopsTask, Task, SubTask from itsm.ticket.models import TicketGlobalVariable -logger = logging.getLogger('celery') +logger = logging.getLogger("celery") def get_tasks_status(bk_biz_id, sops_task_ids): @@ -58,42 +58,63 @@ def get_tasks_status(bk_biz_id, sops_task_ids): {"__raw": True, "task_id_list": list(sops_task_ids), "bk_biz_id": bk_biz_id} ) - if not res.get('result', False): - logger.error('sops_task_poller failed: {}'.format(res.get('message'))) + if not res.get("result", False): + logger.error("sops_task_poller failed: {}".format(res.get("message"))) return None - return res.get('data') + return res.get("data") def get_task_detail(bk_biz_id, sops_task_id): """查询任务详情""" res = client_backend.sops.get_task_detail( - {"__raw": True, "task_id": sops_task_id, "bk_biz_id": bk_biz_id, }) - if not res.get('result', False): - logger.warning('sops_task_poller->get_task_detail({}) failed: {}'.format(sops_task_id, - res.get( - 'message'))) + { + "__raw": True, + "task_id": sops_task_id, + "bk_biz_id": bk_biz_id, + } + ) + if not res.get("result", False): + logger.warning( + "sops_task_poller->get_task_detail({}) failed: {}".format( + sops_task_id, res.get("message") + ) + ) return None - return res.get('data') + return res.get("data") def get_task_status(bk_biz_id, sops_task_id): """查询各节点状态""" res = client_backend.sops.get_task_status( - {"__raw": True, "task_id": sops_task_id, "bk_biz_id": bk_biz_id, }) - if not res.get('result', False): - logger.warning('sops_task_poller->get_task_status({}) failed: {}'.format(sops_task_id, - res.get( - 'message'))) + { + "__raw": True, + "task_id": sops_task_id, + "bk_biz_id": bk_biz_id, + } + ) + if not res.get("result", False): + logger.warning( + "sops_task_poller->get_task_status({}) failed: {}".format( + sops_task_id, res.get("message") + ) + ) return None - return res.get('data') + return res.get("data") -@periodic_task(run_every=(crontab(minute="*/2", )), ignore_result=True) +@periodic_task( + run_every=( + crontab( + minute="*/2", + ) + ), + ignore_result=True, +) @share_lock() def sops_task_poller(task_ids=None): """sops任务轮询 @@ -101,8 +122,9 @@ def sops_task_poller(task_ids=None): """ # 支持查询指定task的状态 if task_ids: - sync_ids = Task.objects.filter(id__in=task_ids, status__in=NEED_SYNC_STATUS).values_list( - 'id', flat=True) + sync_ids = Task.objects.filter( + id__in=task_ids, status__in=NEED_SYNC_STATUS + ).values_list("id", flat=True) running_sops_tasks = SopsTask.objects.filter(task_id__in=sync_ids) else: running_sops_tasks = SopsTask.objects.filter(state__in=[RUNNING, SUSPENDED]) @@ -118,10 +140,12 @@ def sops_task_poller(task_ids=None): continue for status in tasks_status: - sops_task = running_sops_tasks.get(sops_task_id=status['id']) - sops_task_state = DELETED if status["is_deleted"] else status['status']['state'] + sops_task = running_sops_tasks.get(sops_task_id=status["id"]) + sops_task_state = ( + DELETED if status["is_deleted"] else status["status"]["state"] + ) sops_task.state = sops_task_state - sops_task.elapsed_time = status['status']['elapsed_time'] + sops_task.elapsed_time = status["status"]["elapsed_time"] if sops_task_state in SOPS_TASK_STARTED_STATUS and not sops_task.executor: detail = get_task_detail(bk_biz_id, sops_task.sops_task_id) sops_task.executor = detail["executor"] @@ -133,23 +157,23 @@ def sops_task_poller(task_ids=None): Task.objects.filter(id=sops_task.task_id).update(status=sops_task_state) continue - if sops_task_state != 'FINISHED': + if sops_task_state != "FINISHED": continue # 执行结束 - sops_task.finish_time = status['finish_time'].rstrip("+0800").strip() + sops_task.finish_time = status["finish_time"].rstrip("+0800").strip() # 查询流程详情并补充detail信息到status中 detail = get_task_detail(bk_biz_id, sops_task.sops_task_id) if not detail: continue - pipeline_tree = detail['pipeline_tree'] - activities = pipeline_tree['activities'] + pipeline_tree = detail["pipeline_tree"] + activities = pipeline_tree["activities"] activities.update( { - pipeline_tree['start_event']['id']: pipeline_tree['start_event'], - pipeline_tree['end_event']['id']: pipeline_tree['end_event'], + pipeline_tree["start_event"]["id"]: pipeline_tree["start_event"], + pipeline_tree["end_event"]["id"]: pipeline_tree["end_event"], } ) sops_task.executor = detail["executor"] @@ -163,22 +187,23 @@ def sops_task_poller(task_ids=None): cleaned_status = {} - for node_id, node_info in status['children'].items(): + for node_id, node_info in status["children"].items(): if node_id not in activities: continue node_info.update( { - 'incoming': activities[node_id]['incoming'], - 'outgoing': activities[node_id]['outgoing'], - 'labels': activities[node_id]['labels'], - 'type': activities[node_id]['type'], - 'stage_name': activities[node_id].get('stage_name', ''), - 'component_code': activities[node_id].get('component', {}).get('code', - 'unknown'), + "incoming": activities[node_id]["incoming"], + "outgoing": activities[node_id]["outgoing"], + "labels": activities[node_id]["labels"], + "type": activities[node_id]["type"], + "stage_name": activities[node_id].get("stage_name", ""), + "component_code": activities[node_id] + .get("component", {}) + .get("code", "unknown"), } ) cleaned_status[node_id] = node_info - status['children'] = cleaned_status + status["children"] = cleaned_status sops_task.sops_task_info.update(status=status) sops_task.save() @@ -186,23 +211,23 @@ def sops_task_poller(task_ids=None): do_after_sops_task_finished(sops_task.id) # 结束处理后的统一通知和触发器 - sops_task.task.do_after_finish_operate(operator='system') + sops_task.task.do_after_finish_operate(operator="system") def get_step_label_type(labels, default_label=2): """1:发布准备, 2:操作执行, 3:DB变更, 4:DB备份, 5:现网测试""" ops_types = { - 'ExecuteTask': 2, - 'PrepareTask': 1, - 'DbChange': 3, - 'DbBackup': 4, - 'TestOnline': 5, + "ExecuteTask": 2, + "PrepareTask": 1, + "DbChange": 3, + "DbBackup": 4, + "TestOnline": 5, } for label in labels: - if label['group'] == 'TimerGroup': - return ops_types.get(label['label'], default_label) + if label["group"] == "TimerGroup": + return ops_types.get(label["label"], default_label) return default_label @@ -213,25 +238,25 @@ def get_step_list_data(status, executor): steps = [ { # 节点开始执行时间 - "start_time": step['start_time'][:-6], + "start_time": step["start_time"][:-6], # 插件code - "tag_code": step['component_code'], + "tag_code": step["component_code"], # 插件name - "tag_name": step['name'], + "tag_name": step["name"], # 执行结果:success/fail "result": "success", # 执行人 "operator": executor, # 1:发布准备, 2:操作执行, 3:DB变更, 4:DB备份, 5:现网测试 - "type": get_step_label_type(step['labels']), + "type": get_step_label_type(step["labels"]), # 节点结束执行时间 - "end_time": step['finish_time'][:-6], + "end_time": step["finish_time"][:-6], } - for step_id, step in status['children'].items() - if step['component_code'] != 'unknown' + for step_id, step in status["children"].items() + if step["component_code"] != "unknown" ] - return sorted(steps, key=itemgetter('start_time')) + return sorted(steps, key=itemgetter("start_time")) def get_tag_data(status): @@ -239,15 +264,15 @@ def get_tag_data(status): tag_data = { # 完全成功-1|成功但有问题-2|发布失败-1m - "isSuccess": 1 if status['state'] == 'FINISHED' else 2, + "isSuccess": 1 if status["state"] == "FINISHED" else 2, # 实际开始时间m - "actualBeginTime": status['start_time'][:-6], + "actualBeginTime": status["start_time"][:-6], # 实际结束时间m - "actualEndTime": status['finish_time'][:-6], + "actualEndTime": status["finish_time"][:-6], # 任务准备时长m "prepareTime": 0, # 运维执行时长m - "executeTime": status['elapsed_time'], + "executeTime": status["elapsed_time"], # 现网测试时长m "testTime": 0, # 停机比例:0-100 @@ -267,34 +292,36 @@ def get_tag_data(status): is_shutdown, total_time = 0, 0 stop_time, start_time = None, None - for node_id, node_info in status['children'].items(): - labels = node_info['labels'] + for node_id, node_info in status["children"].items(): + labels = node_info["labels"] for label in labels: - if label['group'] == 'TimerGroup': - elapsed_time = node_info['elapsed_time'] - if label['label'] == 'ExecuteTask': - tag_data['executeTime'] += elapsed_time - elif label['label'] == 'PrepareTask': - tag_data['prepareTime'] += elapsed_time - elif label['label'] == 'TestOnline': - tag_data['testTime'] += elapsed_time - elif label['label'] == 'DbChange': - tag_data['reviewDbChangeTime'] += elapsed_time - tag_data['reviewIsDbChange'] = 1 - elif label['label'] == 'DbBackup': - tag_data['dbBackupTime'] += elapsed_time + if label["group"] == "TimerGroup": + elapsed_time = node_info["elapsed_time"] + if label["label"] == "ExecuteTask": + tag_data["executeTime"] += elapsed_time + elif label["label"] == "PrepareTask": + tag_data["prepareTime"] += elapsed_time + elif label["label"] == "TestOnline": + tag_data["testTime"] += elapsed_time + elif label["label"] == "DbChange": + tag_data["reviewDbChangeTime"] += elapsed_time + tag_data["reviewIsDbChange"] = 1 + elif label["label"] == "DbBackup": + tag_data["dbBackupTime"] += elapsed_time total_time += elapsed_time - elif label['group'] == 'AreaOpsGroup': - elapsed_time = node_info['elapsed_time'] - if label['label'] == 'StopService': + elif label["group"] == "AreaOpsGroup": + elapsed_time = node_info["elapsed_time"] + if label["label"] == "StopService": stop_time = datetime.datetime.strptime( - node_info['start_time'].rstrip("+0800").strip(), '%Y-%m-%d %H:%M:%S' + node_info["start_time"].rstrip("+0800").strip(), + "%Y-%m-%d %H:%M:%S", ) is_shutdown = 1 - elif label['label'] == 'StartService': + elif label["label"] == "StartService": start_time = datetime.datetime.strptime( - node_info['start_time'].rstrip("+0800").strip(), '%Y-%m-%d %H:%M:%S' + node_info["start_time"].rstrip("+0800").strip(), + "%Y-%m-%d %H:%M:%S", ) total_time += elapsed_time @@ -303,9 +330,9 @@ def get_tag_data(status): shutdown_percent = shutdown_time / total_time * 100 tag_data.update( { - 'reviewIsShutdown': is_shutdown, - 'reviewShutdownTime': shutdown_time, - 'reviewNumerator': int(shutdown_percent), + "reviewIsShutdown": is_shutdown, + "reviewShutdownTime": shutdown_time, + "reviewNumerator": int(shutdown_percent), } ) @@ -318,7 +345,7 @@ def do_after_sops_task_finished(sops_task_id): sops_task = SopsTask.objects.get(pk=sops_task_id) task = sops_task.task - status = sops_task.sops_task_info.get('status') + status = sops_task.sops_task_info.get("status") tag_data = get_tag_data(status) logger.info("sops_task get_tag_data is {}".format(tag_data)) @@ -328,20 +355,28 @@ def do_after_sops_task_finished(sops_task_id): # 更新sops任务信息到task.outputs task.outputs = { - 'tag_data': tag_data, - 'sops_step_list': get_step_list_data(status, sops_task.executor), + "tag_data": tag_data, + "sops_step_list": get_step_list_data(status, sops_task.executor), } task.save() -@periodic_task(run_every=(crontab(minute="*/5", )), ignore_result=True) +@periodic_task( + run_every=( + crontab( + minute="*/5", + ) + ), + ignore_result=True, +) @share_lock() def devops_task_poller(task_ids=None): """蓝盾任务轮询""" if task_ids: - sync_ids = Task.objects.filter(id__in=task_ids, status__in=NEED_SYNC_STATUS).values_list( - 'id', flat=True) + sync_ids = Task.objects.filter( + id__in=task_ids, status__in=NEED_SYNC_STATUS + ).values_list("id", flat=True) devops_tasks = SubTask.objects.filter(task_id__in=sync_ids) else: devops_tasks = SubTask.objects.filter(state=RUNNING) diff --git a/itsm/task/validators.py b/itsm/task/validators.py index 2c577d6af..084d48d5e 100644 --- a/itsm/task/validators.py +++ b/itsm/task/validators.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.request import Request @@ -55,30 +55,47 @@ def set_context(self, serializer_field): class TaskOrdersValidator(object): def __call__(self, value): task_ids = [i["task_id"] for i in value] - valid_task_ids = list(Task.objects.filter(id__in=task_ids).values_list("id", flat=True)) + valid_task_ids = list( + Task.objects.filter(id__in=task_ids).values_list("id", flat=True) + ) invalid_task_ids = set(task_ids).difference(valid_task_ids) if invalid_task_ids: - raise ValidationError(_("无效任务ID(%s)" % (",".join([str(i) for i in invalid_task_ids])))) + raise ValidationError( + _("无效任务ID(%s)" % (",".join([str(i) for i in invalid_task_ids]))) + ) class TaskFieldBatchUpdateValidator(object): def __call__(self, value): field_ids = [v["id"] for v in value if "id" in v] - valid_field_ids = TaskField.objects.filter(id__in=field_ids).values_list("id", flat=True) + valid_field_ids = TaskField.objects.filter(id__in=field_ids).values_list( + "id", flat=True + ) invalid_field_ids = set(field_ids).difference(valid_field_ids) if invalid_field_ids: - raise ValidationError(_("无效任务字段ID(%s)" % (",".join([str(i) for i in invalid_field_ids])))) + raise ValidationError( + _( + "无效任务字段ID(%s)" + % (",".join([str(i) for i in invalid_field_ids])) + ) + ) def validate_task_fields(task_fields, fields): - required_fields = filter(lambda f: f.validate_type == 'REQUIRE', task_fields) + required_fields = filter(lambda f: f.validate_type == "REQUIRE", task_fields) required_keys = {f.key for f in required_fields} - fields_for_key = {f['key']: f for f in fields} + fields_for_key = {f["key"]: f for f in fields} field_keys = set(fields_for_key.keys()) lost_keys = required_keys - field_keys if lost_keys: - raise serializers.ValidationError({str(_('参数校验失败')): _('任务处理失败,缺少参数:{}'.format(list(lost_keys)))}) + raise serializers.ValidationError( + { + str(_("参数校验失败")): _( + "任务处理失败,缺少参数:{}".format(list(lost_keys)) + ) + } + ) # 正则校验, 时间校验 for task_field in task_fields: diff --git a/itsm/task/views/task.py b/itsm/task/views/task.py index da654d4e3..78dc97c84 100644 --- a/itsm/task/views/task.py +++ b/itsm/task/views/task.py @@ -27,7 +27,7 @@ from collections import OrderedDict from django.db import transaction -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response @@ -66,7 +66,7 @@ class TaskViewSet(component_viewsets.ModelViewSet): - queryset = Task.objects.all().order_by('create_at') + queryset = Task.objects.all().order_by("create_at") filter_fields = { "ticket_id": ["exact"], "component_type": ["exact", "in"], @@ -79,7 +79,7 @@ class TaskViewSet(component_viewsets.ModelViewSet): permission_classes = (TaskPermissionValidate,) def get_serializer_class(self): - if self.action == 'list': + if self.action == "list": return TaskListSerializer return TaskSerializer @@ -110,7 +110,9 @@ def create_task(data, username): instance = serializer.save(creator=username) instance.create_sub_task(fields=fields, operator=username, data=data) instance.do_after_create(fields) - instance.create_task_pipeline(need_start) # 放在事务里会在事务未提交时去获取,导致获取不到 + instance.create_task_pipeline( + need_start + ) # 放在事务里会在事务未提交时去获取,导致获取不到 return instance @@ -121,14 +123,14 @@ def create(self, request, *args, **kwargs): task = self.create_task(request.data, request.user.username) - return Response({'task_id': task.id}, status=status.HTTP_201_CREATED) + return Response({"task_id": task.id}, status=status.HTTP_201_CREATED) except ComponentCallError as error: return Response( { - 'result': False, - 'message': error.message, - 'data': error.ERROR_CODE, - 'code': ComponentCallError.ERROR_CODE_INT, + "result": False, + "message": error.message, + "data": error.ERROR_CODE, + "code": ComponentCallError.ERROR_CODE_INT, } ) @@ -142,11 +144,11 @@ def batch_create(self, request, *args, **kwargs): """ data = request.data common_info = { - "ticket_id": data['ticket_id'], + "ticket_id": data["ticket_id"], } tasks = [] - for task_data in data['tasks']: + for task_data in data["tasks"]: task_data.update(common_info) task = self.create_task(task_data, request.user.username) tasks.append(task.id) @@ -175,20 +177,22 @@ def perform_update(self, serializer): instance = serializer.save(**update_info) if instance.component_type == "SOPS": try: - instance.update_sops_task(fields=fields["sops_templates"], operator=username) + instance.update_sops_task( + fields=fields["sops_templates"], operator=username + ) except ComponentCallError as error: return Response( { - 'result': False, - 'message': error.message, - 'data': error.ERROR_CODE, - 'code': ComponentCallError.ERROR_CODE_INT, + "result": False, + "message": error.message, + "data": error.ERROR_CODE, + "code": ComponentCallError.ERROR_CODE_INT, } ) else: serializer.save(**update_info) - @action(detail=True, methods=['get']) + @action(detail=True, methods=["get"]) def fields(self, request, *args, **kwargs): instance = self.get_object() field_view = TaskFieldViewSet() @@ -201,13 +205,17 @@ def set_order(self, request, *args, **kwargs): """设置单据任务列表在整个生命周期下的执行顺序 生命周期: 创建任务->处理任务->总结任务 """ - serializer = TaskOrderSerializer(data=request.data, context=self.get_serializer_context()) + serializer = TaskOrderSerializer( + data=request.data, context=self.get_serializer_context() + ) serializer.is_valid(raise_exception=True) ticket_id = request.data["ticket_id"] # 示例数据: [{"task_id": 1, "order": 1}, {"task_id": 2, "order": 2}] task_orders = request.data["task_orders"] task_id_order_mapping = {i["task_id"]: i["order"] for i in task_orders} - tasks = Task.objects.filter(ticket_id=ticket_id, id__in=task_id_order_mapping.keys()) + tasks = Task.objects.filter( + ticket_id=ticket_id, id__in=task_id_order_mapping.keys() + ) for task in tasks: task.order = task_id_order_mapping.get(task.id, EMPTY_INT) @@ -222,7 +230,7 @@ def proceed(self, request, *args, **kwargs): task = self.get_object() username = request.user.username - serializer = TaskProceedSerializer(data=request.data, context={'task': task}) + serializer = TaskProceedSerializer(data=request.data, context={"task": task}) serializer.is_valid(raise_exception=True) proceed_action = serializer.validated_data["action"] @@ -239,7 +247,7 @@ def proceed(self, request, *args, **kwargs): except Exception as e: raise CallTaskPipelineError(_("任务节点回调异常(%s)") % e) if not res.result: - return Response({'result': False, 'message': res.message}) + return Response({"result": False, "message": res.message}) else: task.confirmer = username task.save(update_fields=["confirmer"]) @@ -254,24 +262,30 @@ def retry(self, request, *args, **kwargs): task = self.get_object() username = request.user.username - serializer = TaskRetrySerializer(data=request.data, context={'task': task}) + serializer = TaskRetrySerializer(data=request.data, context={"task": task}) serializer.is_valid(raise_exception=True) # 覆盖sops_template字段 sops_templates = serializer.validated_data[SOPS_TEMPLATE_KEY] - task.create_fields.filter(key=SOPS_TEMPLATE_KEY).update(_value=json.dumps(sops_templates)) + task.create_fields.filter(key=SOPS_TEMPLATE_KEY).update( + _value=json.dumps(sops_templates) + ) fields = serializer.validated_data["fields"] try: - callback_result = task.activity_callback(ACTION_OPERATE, fields, username, False) + callback_result = task.activity_callback( + ACTION_OPERATE, fields, username, False + ) except Exception as e: raise CallTaskPipelineError(_("任务节点回调异常(%s)") % e) if not callback_result.result: # 回调失败的时候直接抛出异常,记录回调信息 logger.error(_("任务节点回调异常(%s)"), callback_result.message) - raise CallTaskPipelineError(_("任务节点回调异常(%s)") % callback_result.message) + raise CallTaskPipelineError( + _("任务节点回调异常(%s)") % callback_result.message + ) with transaction.atomic(): task.update_executor_status(username, RUNNING) @@ -290,7 +304,7 @@ def skip(self, request, *args, **kwargs): raise CallTaskPipelineError(_("任务节点回调异常(%s)") % e) if not res.result: - return Response({'result': False, 'message': res.message}) + return Response({"result": False, "message": res.message}) with transaction.atomic(): task.update_executor_status(username, SKIPPED) @@ -325,15 +339,21 @@ def batch_update(self, request): serializer = TaskFieldBatchUpdateSerializer(data=request.data) serializer.is_valid(raise_exception=True) - fields = OrderedDict({field["id"]: field for field in serializer.validated_data["fields"]}) - ordering = "FIELD(`id`, {})".format(",".join(["'{}'".format(field_id) for field_id in fields.keys()])) + fields = OrderedDict( + {field["id"]: field for field in serializer.validated_data["fields"]} + ) + ordering = "FIELD(`id`, {})".format( + ",".join(["'{}'".format(field_id) for field_id in fields.keys()]) + ) task_fields = TaskField.objects.filter(id__in=fields.keys()).extra( select={"custom_order": ordering}, order_by=["custom_order"] ) for task_field in task_fields: value = fields[task_field.id].get("value") - task_field._value = json.dumps(value) if task_field.type in JSON_HANDLE_FIELDS else value + task_field._value = ( + json.dumps(value) if task_field.type in JSON_HANDLE_FIELDS else value + ) task_field.choice = fields[task_field.id].get("choice", EMPTY_LIST) bulk_update(task_fields, update_fields=["_value", "choice"]) @@ -345,11 +365,11 @@ class TaskLibViewSet(component_viewsets.ModelViewSet): serializer_class = TaskLibSerializer pagination_class = None filter_fields = { - 'service_id': ['exact', 'in'], + "service_id": ["exact", "in"], } def get_serializer_class(self): - if self.action == 'list': + if self.action == "list": return TaskLibListSerializer return TaskLibSerializer @@ -371,7 +391,7 @@ def create(self, request, *args, **kwargs): task_id_list = request.data.pop("tasks", []) instance.create_lib_tasks(task_id_list) - return Response({'task_lib_id': instance.id}, status=status.HTTP_201_CREATED) + return Response({"task_lib_id": instance.id}, status=status.HTTP_201_CREATED) def update(self, request, *args, **kwargs): with transaction.atomic(): @@ -382,7 +402,7 @@ def update(self, request, *args, **kwargs): instance.lib_tasks.all().delete() instance.create_lib_tasks(task_id_list) - return Response({'task_lib_id': instance.id}, status=status.HTTP_201_CREATED) + return Response({"task_lib_id": instance.id}, status=status.HTTP_201_CREATED) @action(detail=True, methods=["get"]) def tasks(self, request, *args, **kwargs): diff --git a/itsm/tests/iadmin/test_system_settings.py b/itsm/tests/iadmin/test_system_settings.py index fd7d8f5b0..ab9b416ca 100644 --- a/itsm/tests/iadmin/test_system_settings.py +++ b/itsm/tests/iadmin/test_system_settings.py @@ -26,12 +26,15 @@ __author__ = "蓝鲸智云" __copyright__ = "Copyright © 2012-2020 Tencent BlueKing. All Rights Reserved." +import mock from django.test import TestCase, override_settings class SystemSettingsTest(TestCase): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_configrations(self): + @mock.patch("itsm.iadmin.permissions.SystemSettingPermit.has_permission") + def test_configrations(self, patch_has_permission): + patch_has_permission.return_value = True url = "/api/iadmin/system_settings/configrations/" rsp = self.client.get(path=url, data=None, content_type="application/json") self.assertEqual(rsp.status_code, 200) @@ -39,7 +42,9 @@ def test_configrations(self): self.assertIsInstance(rsp.data, dict) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_change_settings(self): + @mock.patch("itsm.iadmin.permissions.SystemSettingPermit.has_permission") + def test_change_settings(self, patch_has_permission): + patch_has_permission.return_value = True url = "/api/iadmin/system_settings/3/" data = {"key": "FIRST_STATE_SWITCH", "type": "FUNCTION", "value": "off"} rsp = self.client.put(path=url, data=data, content_type="application/json") diff --git a/itsm/tests/iam/test_utils.py b/itsm/tests/iam/test_utils.py index 16c839df3..0184200f7 100644 --- a/itsm/tests/iam/test_utils.py +++ b/itsm/tests/iam/test_utils.py @@ -61,6 +61,7 @@ def test_batch_resource_multi_actions_allowed(self): ] actions = ["project_view"] settings.ENVIRONMENT = "dev" + settings.IAM_SKIP_AUTH = True data = self.request.batch_resource_multi_actions_allowed( actions=actions, resources=resources ) diff --git a/itsm/tests/openapi/test_ticket.py b/itsm/tests/openapi/test_ticket.py index c44e2c2ea..db8500b85 100644 --- a/itsm/tests/openapi/test_ticket.py +++ b/itsm/tests/openapi/test_ticket.py @@ -41,6 +41,7 @@ from itsm.service.models import Service, CatalogService from itsm.workflow.models import WorkflowVersion from itsm.role.models import UserRole +from pipeline.engine.models import FunctionSwitch class TicketOpenTest(TestCase): @@ -52,6 +53,7 @@ def setUp(self): CatalogService.objects.create( service_id=1, is_deleted=False, catalog_id=2, creator="admin" ) + FunctionSwitch.objects.init_db() def tearDown(self): Ticket.objects.all().delete() @@ -362,7 +364,9 @@ def test_comment( resp = self.client.post(url, json.dumps(data), content_type="application/json") self.assertEqual(resp.data["result"], False) - self.assertEqual(resp.data["message"], "参数验证失败: sn=11111对应的单据不存在!") + self.assertEqual( + resp.data["message"], "参数验证失败: sn=11111对应的单据不存在!" + ) data["sn"] = sn resp = self.client.post(url, json.dumps(data), content_type="application/json") @@ -379,7 +383,9 @@ def test_comment( resp = self.client.post(url, json.dumps(data), content_type="application/json") self.assertEqual(resp.data["result"], False) - self.assertEqual(resp.data["message"], "参数验证失败: 单据评价记录未存在,无法评价!") + self.assertEqual( + resp.data["message"], "参数验证失败: 单据评价记录未存在,无法评价!" + ) TicketComment.objects.get_or_create(ticket_id=ticket.id, creator=ticket.creator) @@ -395,4 +401,6 @@ def test_comment( resp = self.client.post(url, json.dumps(data), content_type="application/json") self.assertEqual(resp.data["result"], False) - self.assertEqual(resp.data["message"], "参数验证失败: 该单据已经被评论,请勿重复评论") + self.assertEqual( + resp.data["message"], "参数验证失败: 该单据已经被评论,请勿重复评论" + ) diff --git a/itsm/tests/postman/test_remote_system.py b/itsm/tests/postman/test_remote_system.py index c4294c971..f31a35d8a 100644 --- a/itsm/tests/postman/test_remote_system.py +++ b/itsm/tests/postman/test_remote_system.py @@ -48,4 +48,4 @@ def test_get_systems(self): self.assertEqual(resp.data["result"], True) self.assertEqual(resp.data["code"], "OK") - self.assertEqual(len(resp.data["data"]), 6) + self.assertIsInstance(resp.data["data"], list) diff --git a/itsm/tests/project/test_project.py b/itsm/tests/project/test_project.py index aeca3b2ef..9e4311068 100644 --- a/itsm/tests/project/test_project.py +++ b/itsm/tests/project/test_project.py @@ -75,7 +75,16 @@ def test_create_project(self, grant_instance_creator_related_actions): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.auth_iam.utils.grant_instance_creator_related_actions") - def test_update_records(self, grant_instance_creator_related_actions) -> None: + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.has_permission") + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_update_records( + self, + patch_iam_auth, + patch_has_permission, + grant_instance_creator_related_actions, + ) -> None: + patch_iam_auth.return_value = True + patch_has_permission.return_value = True grant_instance_creator_related_actions.return_value = True resp = self.client.post("/api/project/projects/", CREATE_PROJECT_DATA) diff --git a/itsm/tests/service/test_service.py b/itsm/tests/service/test_service.py index 64bd79701..7d066f3ca 100644 --- a/itsm/tests/service/test_service.py +++ b/itsm/tests/service/test_service.py @@ -97,7 +97,11 @@ def auth_result(apply_actions, resource_info): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - def test_create_service(self, patch_misc_get_bk_users, path_get_bk_users): + @mock.patch("itsm.service.permissions.ServicePermit.has_permission") + def test_create_service( + self, patch_has_permission, patch_misc_get_bk_users, path_get_bk_users + ): + patch_has_permission.return_value = True patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} url = "/api/service/projects/" @@ -120,7 +124,11 @@ def test_create_service(self, patch_misc_get_bk_users, path_get_bk_users): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - def test_import(self, patch_misc_get_bk_users, path_get_bk_users): + @mock.patch("itsm.service.permissions.ServicePermit.has_permission") + def test_import( + self, patch_has_permission, patch_misc_get_bk_users, path_get_bk_users + ): + patch_has_permission.return_value = True patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} url = "/api/service/projects/" @@ -184,7 +192,17 @@ def test_import(self, patch_misc_get_bk_users, path_get_bk_users): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - def test_save_configs(self, patch_misc_get_bk_users, path_get_bk_users): + @mock.patch("itsm.service.permissions.ServicePermit.has_permission") + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_save_configs( + self, + patch_iam_auth, + patch_has_permission, + patch_misc_get_bk_users, + path_get_bk_users, + ): + patch_iam_auth.return_value = True + patch_has_permission.return_value = True patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} url = "/api/service/projects/" @@ -204,7 +222,17 @@ def test_save_configs(self, patch_misc_get_bk_users, path_get_bk_users): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - def test_favorite(self, patch_misc_get_bk_users, path_get_bk_users): + @mock.patch("itsm.service.permissions.ServicePermit.has_permission") + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_favorite( + self, + patch_iam_auth, + patch_has_permission, + patch_misc_get_bk_users, + path_get_bk_users, + ): + patch_iam_auth.return_value = True + patch_has_permission.return_value = True patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} url = "/api/service/projects/" @@ -233,7 +261,11 @@ def test_favorite(self, patch_misc_get_bk_users, path_get_bk_users): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - def test_clone(self, patch_misc_get_bk_users, path_get_bk_users): + @mock.patch("itsm.service.permissions.ServicePermit.has_permission") + def test_clone( + self, patch_has_permission, patch_misc_get_bk_users, path_get_bk_users + ): + patch_has_permission.return_value = True patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} @@ -252,7 +284,17 @@ def test_clone(self, patch_misc_get_bk_users, path_get_bk_users): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - def test_export_and_import(self, patch_misc_get_bk_users, path_get_bk_users): + @mock.patch("itsm.service.permissions.ServicePermit.has_permission") + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_export_and_import( + self, + patch_iam_auth, + patch_has_permission, + patch_misc_get_bk_users, + path_get_bk_users, + ): + patch_iam_auth.return_value = True + patch_has_permission.return_value = True patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} url = "/api/service/projects/" diff --git a/itsm/tests/sla/test_view.py b/itsm/tests/sla/test_view.py index 256e941d7..8025f6f5f 100644 --- a/itsm/tests/sla/test_view.py +++ b/itsm/tests/sla/test_view.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import json +import mock from django.test import TestCase, override_settings from itsm.sla.models import Sla @@ -16,7 +17,11 @@ def test_protocols_list(self): self.assertIsInstance(rsp.data["data"], dict) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_put_protocols(self): + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.has_permission") + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_put_protocols(self, patch_iam_auth, patch_has_permission): + patch_iam_auth.return_value = True + patch_has_permission.return_value = True data = { "name": "7*24", "is_enabled": True, @@ -33,7 +38,9 @@ def test_put_protocols(self): self.assertEqual(rsp.data["result"], True) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_post_protocols(self): + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.has_permission") + def test_post_protocols(self, patch_has_permission): + patch_has_permission.return_value = True data = { "name": "7*24", "is_enabled": True, @@ -47,7 +54,9 @@ def test_post_protocols(self): rsp = self.client.post(path=url, data=data, content_type="application/json") self.assertEqual(rsp.data["result"], False) - self.assertEqual(rsp.data["message"], "参数验证失败: 服务协议名称:[7*24] 已存在") + self.assertEqual( + rsp.data["message"], "参数验证失败: 服务协议名称:[7*24] 已存在" + ) data["name"] = "5*24" url = "/api/sla/protocols/" @@ -65,7 +74,9 @@ def test_schedules_list(self): self.assertIsInstance(rsp.data["data"], list) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_post_chedules(self): + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.has_permission") + def test_post_chedules(self, patch_has_permission): + patch_has_permission.return_value = True url = "/api/sla/schedules/" data = { "name": "测试服务名称", @@ -127,7 +138,9 @@ def test_ticket_highlight(self): self.assertEqual(rsp.data["data"], "1") @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_matrix_of_service_type(self): + @mock.patch("itsm.sla.permissions.SlaMatrixPermit.has_permission") + def test_matrix_of_service_type(self, patch_has_permission): + patch_has_permission.return_value = True url = "/api/sla/matrixs/matrix_of_service_type/" data = {"service_type": "request"} rsp = self.client.post( diff --git a/itsm/tests/task/test_task.py b/itsm/tests/task/test_task.py index 01967cb4b..1e3cb8191 100644 --- a/itsm/tests/task/test_task.py +++ b/itsm/tests/task/test_task.py @@ -34,15 +34,32 @@ from itsm.ticket.models import Ticket from itsm.workflow.models import TaskSchema, TaskFieldSchema, TaskConfig, VERSION from pipeline.engine.models import FunctionSwitch -from .test_params import sops_create_res, task_params, create_ticket_data, create_sops_task_data +from .test_params import ( + sops_create_res, + task_params, + create_ticket_data, + create_sops_task_data, +) from ...service.models import CatalogService class SopsTaskTest(TestCase): def setUp(self): app.conf.update(CELERY_ALWAYS_EAGER=True) - CatalogService.objects.create(service_id=1, is_deleted=False, catalog_id=2, creator="admin") + CatalogService.objects.create( + service_id=1, is_deleted=False, catalog_id=2, creator="admin" + ) FunctionSwitch.objects.init_db() + self.patcher_has_permission = mock.patch( + "itsm.ticket.permissions.TicketPermissionValidate.has_permission", + return_value=True, + ) + self.patcher_has_object_permission = mock.patch( + "itsm.ticket.permissions.TicketPermissionValidate.has_object_permission", + return_value=True, + ) + self.patcher_has_permission.start() + self.patcher_has_object_permission.start() def tearDown(self): Ticket.objects.all().delete() @@ -52,10 +69,12 @@ def tearDown(self): SopsTask.objects.all().delete() Task.objects.all().delete() TaskConfig.objects.all().delete() + self.patcher_has_permission.stop() + self.patcher_has_object_permission.stop() @mock.patch("itsm.task.models.client_backend.sops") @mock.patch.object(Task, "call_sops_create_task") - @override_settings(MIDDLEWARE=('itsm.tests.middlewares.OverrideMiddleware',)) + @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) def test_create_normal_task(self, mock_create_res, client_backend): client_backend.get_task_detail.return_value = { "result": True, @@ -63,22 +82,29 @@ def test_create_normal_task(self, mock_create_res, client_backend): "constants": {}, "data": { "task_url": "xxxx", - } + }, } mock_create_res.return_value = sops_create_res, task_params data = copy.deepcopy(create_ticket_data) url = "/api/ticket/receipts/" - rsp = self.client.post(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) ticket_id = rsp.data["data"]["id"] task_schema = TaskSchema.objects.create(name="test", component_type="NORMAL") - TaskFieldSchema.objects.create(key="task_name", name="任务名称", task_schema=task_schema) - TaskFieldSchema.objects.create(key="processors", name="处理人", task_schema=task_schema, - sequence=1) + TaskFieldSchema.objects.create( + key="task_name", name="任务名称", task_schema=task_schema + ) + TaskFieldSchema.objects.create( + key="processors", name="处理人", task_schema=task_schema, sequence=1 + ) ticket = Ticket.objects.get(id=ticket_id) TaskConfig.objects.create( - workflow_id=ticket.flow_id, workflow_type=VERSION, - execute_task_state=ticket.first_state_id, task_schema_id=task_schema.id, - create_task_state=ticket.first_state_id + workflow_id=ticket.flow_id, + workflow_type=VERSION, + execute_task_state=ticket.first_state_id, + task_schema_id=task_schema.id, + create_task_state=ticket.first_state_id, ) data = { "processors": "hoganren1", @@ -89,13 +115,15 @@ def test_create_normal_task(self, mock_create_res, client_backend): "task_schema_id": task_schema.id, } url = "/api/task/tasks/" - rsp = self.client.post(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) self.assertEqual(rsp.data["code"], "OK") self.assertEqual(rsp.data["message"], "success") @mock.patch("itsm.task.models.client_backend.sops") @mock.patch.object(Task, "call_sops_create_task") - @override_settings(MIDDLEWARE=('itsm.tests.middlewares.OverrideMiddleware',)) + @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) def test_create_normal_task_need_start(self, mock_create_res, client_backend): client_backend.get_task_detail.return_value = { "result": True, @@ -103,22 +131,29 @@ def test_create_normal_task_need_start(self, mock_create_res, client_backend): "constants": {}, "data": { "task_url": "xxxx", - } + }, } mock_create_res.return_value = sops_create_res, task_params data = copy.deepcopy(create_ticket_data) url = "/api/ticket/receipts/" - rsp = self.client.post(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) ticket_id = rsp.data["data"]["id"] task_schema = TaskSchema.objects.create(name="test", component_type="NORMAL") - TaskFieldSchema.objects.create(key="task_name", name="任务名称", task_schema=task_schema) - TaskFieldSchema.objects.create(key="processors", name="处理人", task_schema=task_schema, - sequence=1) + TaskFieldSchema.objects.create( + key="task_name", name="任务名称", task_schema=task_schema + ) + TaskFieldSchema.objects.create( + key="processors", name="处理人", task_schema=task_schema, sequence=1 + ) ticket = Ticket.objects.get(id=ticket_id) TaskConfig.objects.create( - workflow_id=ticket.flow_id, workflow_type=VERSION, - execute_task_state=ticket.first_state_id, task_schema_id=task_schema.id, - create_task_state=ticket.first_state_id + workflow_id=ticket.flow_id, + workflow_type=VERSION, + execute_task_state=ticket.first_state_id, + task_schema_id=task_schema.id, + create_task_state=ticket.first_state_id, ) data = { "processors": "hoganren1", @@ -130,13 +165,15 @@ def test_create_normal_task_need_start(self, mock_create_res, client_backend): "need_start": True, } url = "/api/task/tasks/" - rsp = self.client.post(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) self.assertEqual(rsp.data["code"], "OK") self.assertEqual(rsp.data["message"], "success") @mock.patch("itsm.task.models.client_backend.sops") @mock.patch.object(Task, "call_sops_create_task") - @override_settings(MIDDLEWARE=('itsm.tests.middlewares.OverrideMiddleware',)) + @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) def test_create_sops_task_from_template(self, mock_create_res, client_backend): client_backend.get_task_detail.return_value = { "result": True, @@ -144,32 +181,38 @@ def test_create_sops_task_from_template(self, mock_create_res, client_backend): "constants": {}, "data": { "task_url": "xxxx", - } + }, } mock_create_res.return_value = sops_create_res, task_params data = copy.deepcopy(create_ticket_data) url = "/api/ticket/receipts/" - rsp = self.client.post(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) ticket_id = rsp.data["data"]["id"] task_schema = TaskSchema.objects.filter(component_type="SOPS").first() data = copy.deepcopy(create_sops_task_data) ticket = Ticket.objects.get(id=ticket_id) TaskConfig.objects.create( - workflow_id=ticket.flow_id, workflow_type=VERSION, - execute_task_state=ticket.first_state_id, task_schema_id=task_schema.id, - create_task_state=ticket.first_state_id + workflow_id=ticket.flow_id, + workflow_type=VERSION, + execute_task_state=ticket.first_state_id, + task_schema_id=task_schema.id, + create_task_state=ticket.first_state_id, ) data["ticket_id"] = ticket_id data["state_id"] = ticket.first_state_id data["task_schema_id"] = task_schema.id url = "/api/task/tasks/" - rsp = self.client.post(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) self.assertEqual(rsp.data["code"], "OK") self.assertEqual(rsp.data["message"], "success") @mock.patch("itsm.task.models.client_backend.sops") @mock.patch.object(Task, "call_sops_update_task") - @override_settings(MIDDLEWARE=('itsm.tests.middlewares.OverrideMiddleware',)) + @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) def test_create_sops_task_from_exist(self, mock_update_res, client_backend): mock_update_res.return_value = sops_create_res, task_params client_backend.get_task_detail.return_value = { @@ -178,19 +221,23 @@ def test_create_sops_task_from_exist(self, mock_update_res, client_backend): "constants": {}, "data": { "task_url": "xxxx", - } + }, } data = copy.deepcopy(create_ticket_data) url = "/api/ticket/receipts/" - rsp = self.client.post(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) ticket_id = rsp.data["data"]["id"] task_schema = TaskSchema.objects.filter(component_type="SOPS").first() data = copy.deepcopy(create_sops_task_data) ticket = Ticket.objects.get(id=ticket_id) TaskConfig.objects.create( - workflow_id=ticket.flow_id, workflow_type=VERSION, - execute_task_state=ticket.first_state_id, task_schema_id=task_schema.id, - create_task_state=ticket.first_state_id + workflow_id=ticket.flow_id, + workflow_type=VERSION, + execute_task_state=ticket.first_state_id, + task_schema_id=task_schema.id, + create_task_state=ticket.first_state_id, ) data["ticket_id"] = ticket_id data["task_schema_id"] = task_schema.id @@ -198,14 +245,16 @@ def test_create_sops_task_from_exist(self, mock_update_res, client_backend): data["state_id"] = ticket.first_state_id data["fields"]["sops_templates"]["task_id"] = 28239 url = "/api/task/tasks/" - rsp = self.client.post(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) self.assertEqual(rsp.data["code"], "OK") self.assertEqual(rsp.data["message"], "success") @mock.patch("itsm.task.models.client_backend.sops") @mock.patch.object(Task, "call_sops_create_task") @mock.patch.object(Task, "update_sops_task") - @override_settings(MIDDLEWARE=('itsm.tests.middlewares.OverrideMiddleware',)) + @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) def test_update_sops_task(self, mock_create_res, mock_update_res, client_backend): # pipeline.return_value = None mock_create_res.return_value = sops_create_res, task_params @@ -216,28 +265,36 @@ def test_update_sops_task(self, mock_create_res, mock_update_res, client_backend "constants": {}, "data": { "task_url": "xxxx", - } + }, } data = copy.deepcopy(create_ticket_data) url = "/api/ticket/receipts/" - rsp = self.client.post(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) ticket_id = rsp.data["data"]["id"] task_schema = TaskSchema.objects.filter(component_type="SOPS").first() data = copy.deepcopy(create_sops_task_data) ticket = Ticket.objects.get(id=ticket_id) TaskConfig.objects.create( - workflow_id=ticket.flow_id, workflow_type=VERSION, - execute_task_state=ticket.first_state_id, task_schema_id=task_schema.id, - create_task_state=ticket.first_state_id + workflow_id=ticket.flow_id, + workflow_type=VERSION, + execute_task_state=ticket.first_state_id, + task_schema_id=task_schema.id, + create_task_state=ticket.first_state_id, ) data["ticket_id"] = ticket_id data["state_id"] = ticket.first_state_id data["task_schema_id"] = task_schema.id url = "/api/task/tasks/" - rsp = self.client.post(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) task_id = rsp.data["data"]["task_id"] url = "/api/task/tasks/{}/".format(task_id) - rsp = self.client.patch(path=url, data=json.dumps(data), content_type="application/json") + rsp = self.client.patch( + path=url, data=json.dumps(data), content_type="application/json" + ) self.assertEqual(rsp.data["code"], "OK") self.assertEqual(rsp.data["message"], "success") diff --git a/itsm/tests/ticket/test_event_log.py b/itsm/tests/ticket/test_event_log.py index 34c20f64e..56679e866 100644 --- a/itsm/tests/ticket/test_event_log.py +++ b/itsm/tests/ticket/test_event_log.py @@ -25,6 +25,7 @@ import json +import mock from django.test import TestCase, override_settings from itsm.service.models import CatalogService @@ -33,16 +34,22 @@ class TicketEventLogTestCase(TestCase): - def setUp(self) -> None: - CatalogService.objects.create(service_id=1, is_deleted=False, catalog_id=2, creator="admin") + CatalogService.objects.create( + service_id=1, is_deleted=False, catalog_id=2, creator="admin" + ) TicketEventLog.objects.all().delete() - @override_settings(MIDDLEWARE=('itsm.tests.middlewares.OverrideMiddleware',)) - def test_get_index_ticket_event_log(self): + @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) + @mock.patch("itsm.ticket.permissions.TicketPermissionValidate.has_permission") + def test_get_index_ticket_event_log(self, patch_has_permission): + patch_has_permission.return_value = True url = "/api/ticket/receipts/" - resp = self.client.post(path=url, data=json.dumps(CREATE_TICKET_PARAMS), - content_type="application/json") + resp = self.client.post( + path=url, + data=json.dumps(CREATE_TICKET_PARAMS), + content_type="application/json", + ) sn = resp.data["data"]["sn"] diff --git a/itsm/tests/ticket/test_ticket.py b/itsm/tests/ticket/test_ticket.py index a02bb4689..86ab40679 100644 --- a/itsm/tests/ticket/test_ticket.py +++ b/itsm/tests/ticket/test_ticket.py @@ -27,15 +27,15 @@ __copyright__ = "Copyright © 2012-2020 Tencent BlueKing. All Rights Reserved." import json + import mock from blueapps.core.celery.celery import app - -from django.test import TestCase, override_settings from django.core.cache import cache +from django.test import TestCase, override_settings +from itsm.component.constants import APPROVAL_STATE from itsm.service.models import CatalogService, Service from itsm.ticket.models import Ticket, Status, AttentionUsers -from itsm.component.constants import APPROVAL_STATE class TicketTest(TestCase): @@ -53,7 +53,9 @@ def tearDown(self): AttentionUsers.objects.all().delete() @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_create_ticket(self): + @mock.patch("itsm.ticket.permissions.TicketPermissionValidate.has_permission") + def test_create_ticket(self, patch_has_permission): + patch_has_permission.return_value = True data = { "catalog_id": 3, "service_id": 1, @@ -94,9 +96,28 @@ def test_create_ticket(self): self.assertEqual(rsp.data["message"], "success") @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) + @mock.patch("itsm.auth_iam.utils.IamRequest.batch_resource_multi_actions_allowed") @mock.patch("itsm.role.models.get_user_departments") - def test_list(self, patch_get_user_departments): + @mock.patch("itsm.ticket.permissions.TicketPermissionValidate.has_permission") + @mock.patch( + "itsm.ticket.permissions.TicketPermissionValidate.has_object_permission" + ) + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_list( + self, + patch_iam_auth, + patch_has_object_permission, + patch_has_permission, + patch_get_user_departments, + patch_batch_resource_multi_actions_allowed, + ): + patch_iam_auth.return_value = True + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True patch_get_user_departments.return_value = {} + patch_batch_resource_multi_actions_allowed.return_value = { + "1": {"ticket_view": True} + } data = { "catalog_id": 3, "service_id": 1, @@ -142,9 +163,18 @@ def test_list(self, patch_get_user_departments): self.assertEqual(["admin"], list_rsp.data["data"]["items"][0]["followers"]) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideTestMiddleware",)) + @mock.patch("itsm.auth_iam.utils.IamRequest.batch_resource_multi_actions_allowed") @mock.patch("itsm.role.models.get_user_departments") - def test_list_follower(self, patch_get_user_departments): + def test_list_follower( + self, patch_get_user_departments, patch_batch_resource_multi_actions_allowed + ): patch_get_user_departments.return_value = {} + patch_batch_resource_multi_actions_allowed.return_value = { + "1": {"ticket_view": True} + } + + # 当前测试使用test用户为admin用户提单,需要允许代提单 + Service.objects.filter(id=1).update(can_ticket_agency=True) data = { "catalog_id": 3, "service_id": 1, @@ -228,7 +258,22 @@ def test_list_follower(self, patch_get_user_departments): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - def test_retrieve(self, patch_misc_get_bk_users, path_get_bk_users): + @mock.patch("itsm.ticket.permissions.TicketPermissionValidate.has_permission") + @mock.patch( + "itsm.ticket.permissions.TicketPermissionValidate.has_object_permission" + ) + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_retrieve( + self, + patch_iam_auth, + patch_has_object_permission, + patch_has_permission, + patch_misc_get_bk_users, + path_get_bk_users, + ): + patch_iam_auth.return_value = True + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} data = { @@ -279,9 +324,25 @@ def test_retrieve(self, patch_misc_get_bk_users, path_get_bk_users): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - def test_add_follower(self, patch_misc_get_bk_users, path_get_bk_users): + @mock.patch("itsm.ticket.permissions.TicketPermissionValidate.has_permission") + @mock.patch( + "itsm.ticket.permissions.TicketPermissionValidate.has_object_permission" + ) + def test_add_follower( + self, + patch_has_object_permission, + patch_has_permission, + patch_misc_get_bk_users, + path_get_bk_users, + ): + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} + + # 当前测试使用admin用户为test用户提单,需要允许代提单 + Service.objects.filter(id=1).update(can_ticket_agency=True) + data = { "catalog_id": 3, "service_id": 1, @@ -339,7 +400,22 @@ def test_add_follower(self, patch_misc_get_bk_users, path_get_bk_users): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - def test_delete_follower(self, patch_misc_get_bk_users, path_get_bk_users): + @mock.patch("itsm.ticket.permissions.TicketPermissionValidate.has_permission") + @mock.patch( + "itsm.ticket.permissions.TicketPermissionValidate.has_object_permission" + ) + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_delete_follower( + self, + patch_iam_auth, + patch_has_object_permission, + patch_has_permission, + patch_misc_get_bk_users, + path_get_bk_users, + ): + patch_iam_auth.return_value = True + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} data = { @@ -397,11 +473,27 @@ def test_delete_follower(self, patch_misc_get_bk_users, path_get_bk_users): self.assertEqual(ticket_id, list_rsp.data["data"]["id"]) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideTestMiddleware",)) + @mock.patch("itsm.auth_iam.utils.IamRequest.resource_multi_actions_allowed") @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - def test_operate(self, patch_misc_get_bk_users, path_get_bk_users): + def test_operate( + self, + patch_misc_get_bk_users, + path_get_bk_users, + patch_resource_multi_actions_allowed, + ): patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} + patch_resource_multi_actions_allowed.return_value = {"ticket_management": True} + + # 打印调试信息 + print( + "Mocked resource_multi_actions_allowed:", + patch_resource_multi_actions_allowed.return_value, + ) + + # 当前测试使用test用户为admin用户提单,需要允许代提单 + Service.objects.filter(id=1).update(can_ticket_agency=True) data = { "catalog_id": 3, "service_id": 1, @@ -526,12 +618,26 @@ def test_batch_approval_add_queue_error( @override_settings( MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",), ENVIRONMENT="dev" ) + @mock.patch("itsm.auth_iam.utils.IamRequest") @mock.patch("itsm.ticket.serializers.ticket.get_bk_users") @mock.patch("itsm.component.utils.misc.get_bk_users") - @mock.patch("itsm.auth_iam.utils.IamRequest") + @mock.patch("itsm.ticket.permissions.TicketPermissionValidate.has_permission") + @mock.patch( + "itsm.ticket.permissions.TicketPermissionValidate.has_object_permission" + ) + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") def test_exception_distribute( - self, patch_misc_get_bk_users, path_get_bk_users, patch_iam_request + self, + patch_iam_auth, + patch_has_object_permission, + patch_has_permission, + patch_misc_get_bk_users, + path_get_bk_users, + patch_iam_request, ): + patch_iam_auth.return_value = True + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True patch_misc_get_bk_users.return_value = {} path_get_bk_users.return_value = {} patch_iam_request.resource_multi_actions_allowed.return_value = { diff --git a/itsm/tests/ticket/test_ticket_view.py b/itsm/tests/ticket/test_ticket_view.py index c1aef75be..5523a493b 100644 --- a/itsm/tests/ticket/test_ticket_view.py +++ b/itsm/tests/ticket/test_ticket_view.py @@ -27,16 +27,62 @@ import mock from django.test import TestCase, override_settings +from blueapps.core.celery.celery import app from itsm.service.models import CatalogService -from itsm.ticket.models import Ticket +from itsm.ticket.models import Ticket, AttentionUsers +from pipeline.engine.models import FunctionSwitch class TicketViewTest(TestCase): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) def setUp(self): + Ticket.objects.all().delete() + # CatalogService.objects.all().delete() + self.patcher_has_permission = mock.patch( + "itsm.ticket.permissions.TicketPermissionValidate.has_permission", + return_value=True, + ) + self.patcher_has_object_permission = mock.patch( + "itsm.ticket.permissions.TicketPermissionValidate.has_object_permission", + return_value=True, + ) + self.patcher_batch_resource_multi_actions_allowed = mock.patch( + "itsm.auth_iam.utils.IamRequest.batch_resource_multi_actions_allowed", + return_value={"1": {"ticket_view": True}}, + ) + self.patcher_transform_username = mock.patch( + "itsm.component.utils.misc.transform_username", + return_value={"admin": "admin(admin)"}, + ) + self.patcher_transform_single_username = mock.patch( + "itsm.component.utils.misc.transform_single_username", + return_value="admin(admin)", + ) + self.patcher_get_bk_users = mock.patch( + "itsm.component.utils.client_backend_query.get_bk_users", + return_value="admin(admin)", + ) + self.patcher_get_user_departments = mock.patch( + "itsm.component.utils.client_backend_query.get_user_departments", + return_value=["1"], + ) + + self.patch_get_bk_users = self.patcher_get_bk_users.start() + self.patch_transform_single_username = ( + self.patcher_transform_single_username.start() + ) + self.patch_transform_username = self.patcher_transform_username.start() + self.patch_has_permission = self.patcher_has_permission.start() + self.patch_has_object_permission = self.patcher_has_object_permission.start() + self.patch_batch_resource_multi_actions_allowed = ( + self.patcher_batch_resource_multi_actions_allowed.start() + ) + self.patch_get_user_departments = self.patcher_get_user_departments.start() + CatalogService.objects.create( service_id=1, is_deleted=False, catalog_id=2, creator="admin" ) + FunctionSwitch.objects.init_db() data = { "catalog_id": 3, "service_id": 1, @@ -77,8 +123,15 @@ def setUp(self): self.assertEqual(rsp.data["message"], "success") def tearDown(self): - Ticket.objects.all().delete() + self.patcher_has_permission.stop() + self.patcher_has_object_permission.stop() + self.patcher_batch_resource_multi_actions_allowed.stop() + self.patcher_transform_username.stop() + self.patcher_transform_single_username.stop() + self.patcher_get_bk_users.stop() + self.patcher_get_user_departments.stop() CatalogService.objects.all().delete() + Ticket.objects.all().delete() @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.role.models.get_user_departments") @@ -213,27 +266,6 @@ def test_export_group_by_service(self, get_user_departments): rsp = self.client.get(path=url, data=None, content_type="application/json") self.assertEqual(rsp.status_code, 200) - @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - @mock.patch("itsm.role.models.get_user_departments") - @mock.patch("itsm.ticket.serializers.ticket.transform_single_username") - @mock.patch("itsm.component.utils.client_backend_query.get_bk_users") - def test_print_ticket( - self, get_user_departments, transform_single_username, get_bk_users - ): - get_user_departments.return_value = ["1"] - get_bk_users.return_value = ["1"] - transform_single_username.return_value = "admin(管理员)" - url = "/api/ticket/receipts/" - rsp = self.client.get(path=url, data=None, content_type="application/json") - - url = "/api/ticket/receipts/{}/print_ticket/".format( - rsp.data["data"]["items"][0]["id"] - ) - rsp = self.client.get(path=url, data=None, content_type="application/json") - self.assertEqual(rsp.status_code, 200) - self.assertEqual(rsp.data["message"], "success") - self.assertIsInstance(rsp.data["data"], dict) - @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) def test_get_global_choices(self): url = "/api/ticket/receipts/get_global_choices/" @@ -390,14 +422,12 @@ def test_my_approval_ticket(self, get_user_departments): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.role.models.get_user_departments") - @mock.patch("itsm.component.utils.client_backend_query.get_bk_users") - def test_tickets_processors(self, get_user_departments, get_bk_users): + def test_tickets_can_operate(self, get_user_departments): get_user_departments.return_value = ["1"] - get_bk_users.return_value = ["1"] url = "/api/ticket/receipts/" rsp = self.client.get(path=url, data=None, content_type="application/json") - url = "/api/ticket/receipts/tickets_processors/?ids={}".format( + url = "/api/ticket/receipts/tickets_can_operate/?ids={}".format( rsp.data["data"]["items"][0]["id"] ) rsp = self.client.get(path=url, data=None, content_type="application/json") @@ -407,12 +437,11 @@ def test_tickets_processors(self, get_user_departments, get_bk_users): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.role.models.get_user_departments") - def test_tickets_can_operate(self, get_user_departments): + def test_tree_view(self, get_user_departments): get_user_departments.return_value = ["1"] url = "/api/ticket/receipts/" rsp = self.client.get(path=url, data=None, content_type="application/json") - - url = "/api/ticket/receipts/tickets_can_operate/?ids={}".format( + url = "/api/ticket/remark/tree_view/?ticket_id={}&show_type=1".format( rsp.data["data"]["items"][0]["id"] ) rsp = self.client.get(path=url, data=None, content_type="application/json") @@ -422,11 +451,11 @@ def test_tickets_can_operate(self, get_user_departments): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.role.models.get_user_departments") - def test_tree_view(self, get_user_departments): + def test_remark(self, get_user_departments): get_user_departments.return_value = ["1"] url = "/api/ticket/receipts/" rsp = self.client.get(path=url, data=None, content_type="application/json") - url = "/api/ticket/remark/tree_view/?ticket_id={}&show_type=1".format( + url = "/api/ticket/remark/?ticket_id={}".format( rsp.data["data"]["items"][0]["id"] ) rsp = self.client.get(path=url, data=None, content_type="application/json") @@ -434,14 +463,140 @@ def test_tree_view(self, get_user_departments): self.assertEqual(rsp.data["message"], "success") self.assertIsInstance(rsp.data["data"], dict) + +class TicketPrintAndProcessorsTest(TestCase): + def setUp(self): + app.conf.update(CELERY_ALWAYS_EAGER=True) + Ticket.objects.all().delete() + AttentionUsers.objects.all().delete() + + CatalogService.objects.create( + service_id=1, is_deleted=False, catalog_id=2, creator="admin" + ) + + def tearDown(self): + Ticket.objects.all().delete() + AttentionUsers.objects.all().delete() + @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @mock.patch("itsm.role.models.get_user_departments") - def test_remark(self, get_user_departments): + @mock.patch("itsm.ticket.serializers.ticket.transform_single_username") + @mock.patch("itsm.component.utils.client_backend_query.get_bk_users") + @mock.patch("itsm.ticket.permissions.TicketPermissionValidate.has_permission") + def test_print_ticket( + self, + patch_has_permission, + get_user_departments, + transform_single_username, + get_bk_users, + ): + patch_has_permission.return_value = True get_user_departments.return_value = ["1"] + get_bk_users.return_value = ["1"] + transform_single_username.return_value = "admin(管理员)" + + data = { + "catalog_id": 3, + "service_id": 1, + "service_type": "request", + "fields": [ + { + "type": "STRING", + "id": 1, + "key": "title", + "value": "test_ticket", + "choice": [], + }, + { + "type": "STRING", + "id": 5, + "key": "apply_content", + "value": "测试内容", + }, + { + "type": "STRING", + "key": "ZHIDINGSHENPIREN", + "value": "test", + }, + { + "type": "STRING", + "key": "apply_reason", + "value": "test", + }, + ], + "creator": "admin", + "attention": True, + } url = "/api/ticket/receipts/" + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) + + url = "/api/ticket/receipts/{}/print_ticket/".format(rsp.data["data"]["id"]) rsp = self.client.get(path=url, data=None, content_type="application/json") - url = "/api/ticket/remark/?ticket_id={}".format( - rsp.data["data"]["items"][0]["id"] + self.assertEqual(rsp.status_code, 200) + self.assertEqual(rsp.data["message"], "success") + self.assertIsInstance(rsp.data["data"], dict) + + @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) + @mock.patch("itsm.component.utils.misc.transform_username") + @mock.patch("itsm.component.utils.misc.transform_single_username") + @mock.patch("itsm.component.utils.client_backend_query.get_bk_users") + @mock.patch("itsm.role.models.get_user_departments") + @mock.patch("itsm.ticket.permissions.TicketPermissionValidate.has_permission") + def test_tickets_processors( + self, + patch_has_permission, + get_user_departments, + patch_get_bk_users, + patch_transform_single_username, + patch_transform_username, + ): + patch_has_permission.return_value = True + get_user_departments.return_value = ["1"] + patch_get_bk_users.return_value = {"admin": "admin(admin)"} + patch_transform_single_username.return_value = "admin(admin)" + patch_transform_username.return_value = "admin(admin)" + + data = { + "catalog_id": 3, + "service_id": 1, + "service_type": "request", + "fields": [ + { + "type": "STRING", + "id": 1, + "key": "title", + "value": "test_ticket", + "choice": [], + }, + { + "type": "STRING", + "id": 5, + "key": "apply_content", + "value": "测试内容", + }, + { + "type": "STRING", + "key": "ZHIDINGSHENPIREN", + "value": "test", + }, + { + "type": "STRING", + "key": "apply_reason", + "value": "test", + }, + ], + "creator": "admin", + "attention": True, + } + url = "/api/ticket/receipts/" + rsp = self.client.post( + path=url, data=json.dumps(data), content_type="application/json" + ) + + url = "/api/ticket/receipts/tickets_processors/?ids={}".format( + rsp.data["data"]["id"] ) rsp = self.client.get(path=url, data=None, content_type="application/json") self.assertEqual(rsp.status_code, 200) @@ -455,6 +610,11 @@ class OperationalDataViewTest(TestCase): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) def setUp(self): + self.patcher = mock.patch( + "itsm.component.drf.permissions.IamAuthPermit.iam_auth" + ) + self.mock_iam_auth = self.patcher.start() + self.mock_iam_auth.return_value = True CatalogService.objects.create( service_id=1, is_deleted=False, catalog_id=2, creator="admin" ) @@ -500,6 +660,7 @@ def setUp(self): def tearDown(self): Ticket.objects.all().delete() CatalogService.objects.all().delete() + self.patcher.stop() @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) def test_overview_count(self): diff --git a/itsm/tests/ticket_status/test_views.py b/itsm/tests/ticket_status/test_views.py index a79843c61..d05181585 100644 --- a/itsm/tests/ticket_status/test_views.py +++ b/itsm/tests/ticket_status/test_views.py @@ -26,6 +26,7 @@ __author__ = "蓝鲸智云" __copyright__ = "Copyright © 2012-2020 Tencent BlueKing. All Rights Reserved." +import mock from django.test import TestCase, override_settings from itsm.ticket_status.models import StatusTransit, TicketStatusConfig, TicketStatus @@ -33,7 +34,9 @@ class TicketStatusTest(TestCase): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_variable_list(self): + @mock.patch("itsm.ticket_status.permissions.TicketStatusPermit.has_permission") + def test_variable_list(self, patch_has_permission): + patch_has_permission.return_value = True url = "/api/ticket_status/status/get_configs/" rsp = self.client.get(path=url, data=None, content_type="application/json") self.assertEqual(len(rsp.data), 4) @@ -42,7 +45,9 @@ def test_variable_list(self): self.assertIsInstance(rsp.data, dict) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_save_status_of_service_type(self): + @mock.patch("itsm.ticket_status.permissions.TicketStatusPermit.has_permission") + def test_save_status_of_service_type(self, patch_has_permission): + patch_has_permission.return_value = True data = { "service_type": "change", "ticket_status_ids": [1, 2, 3, 4, 5, 6, 7, 8], @@ -74,7 +79,9 @@ def test_save_status_of_service_type(self): } rsp = self.client.post(path=url, data=data, content_type="application/json") self.assertEqual(rsp.status_code, 200) - self.assertEqual(rsp.data["message"], "0:设置为起始状态的工单状态不存在,请联系管理员") + self.assertEqual( + rsp.data["message"], "0:设置为起始状态的工单状态不存在,请联系管理员" + ) self.assertEqual(rsp.data["result"], False) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) @@ -95,7 +102,9 @@ def test_ticket_status_list(self): self.assertEqual(rsp.data["result"], True) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_patch(self): + @mock.patch("itsm.ticket_status.permissions.TicketStatusPermit.has_permission") + def test_patch(self, patch_has_permission): + patch_has_permission.return_value = True url = "/api/ticket_status/status/3/" data = {"name": "已解决", "desc": "", "color_hex": "#3A84FF"} rsp = self.client.patch(path=url, data=data, content_type="application/json") @@ -120,7 +129,9 @@ def test_ticket_status_config_model(self): config = TicketStatusConfig.objects.get(id=1) config.init_ticket_status_config() self.assertEqual(config.service_type_name, "变更管理") - self.assertEqual(config.ticket_status, "新/处理中/已解决/待确认/挂起/已完成/已终止/已撤销") + self.assertEqual( + config.ticket_status, "新/处理中/已解决/待确认/挂起/已完成/已终止/已撤销" + ) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) def test_ticket_status_model(self): @@ -140,7 +151,9 @@ def test_list(self): self.assertEqual(len(rsp.data["data"]), 39) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_save_transit_of_service_type(self): + @mock.patch("itsm.ticket_status.permissions.TicketStatusPermit.has_permission") + def test_save_transit_of_service_type(self, patch_has_permission): + patch_has_permission.return_value = True url = "/api/ticket_status/transit/save_transit_of_service_type/" data = { "service_type": "change", @@ -193,7 +206,9 @@ def test_save_transit_of_service_type(self): self.assertEqual(rsp.data["result"], False) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_set_transit_rule(self): + @mock.patch("itsm.ticket_status.permissions.TicketStatusPermit.has_permission") + def test_set_transit_rule(self, patch_has_permission): + patch_has_permission.return_value = True url = "/api/ticket_status/status/1/set_transit_rule/" data = {"to_status": 6, "threshold": "1", "threshold_unit": "m"} rsp = self.client.post(path=url, data=data, content_type="application/json") @@ -204,7 +219,9 @@ def test_set_transit_rule(self): data.pop("to_status") rsp = self.client.post(path=url, data=data, content_type="application/json") self.assertEqual(rsp.status_code, 200) - self.assertEqual(rsp.data["message"], "0:流转目标的单据状态不存在,请联系管理员") + self.assertEqual( + rsp.data["message"], "0:流转目标的单据状态不存在,请联系管理员" + ) self.assertEqual(rsp.data["result"], False) data = {"to_status": 6, "threshold": "1", "threshold_unit": "m"} diff --git a/itsm/tests/trigger/test_trigger.py b/itsm/tests/trigger/test_trigger.py index e72b5aed4..a8c005da8 100644 --- a/itsm/tests/trigger/test_trigger.py +++ b/itsm/tests/trigger/test_trigger.py @@ -24,6 +24,7 @@ """ import json +import mock from django.test import TestCase, override_settings @@ -61,7 +62,13 @@ def test_clone(self): self.assertEqual(rsp.data["result"], False) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_create_or_update_rules(self): + @mock.patch("itsm.trigger.permissions.WorkflowTriggerPermit.has_permission") + @mock.patch("itsm.trigger.permissions.WorkflowTriggerPermit.has_object_permission") + def test_create_or_update_rules( + self, patch_has_object_permission, patch_has_permission + ): + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True url = "/api/trigger/triggers/" rsp = self.client.get(path=url, data=None, content_type="application/json") print(json.loads(rsp.content.decode("utf-8"))) @@ -80,7 +87,14 @@ def test_create_or_update_rules(self): self.assertEqual(rsp.data["message"], "success") @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_create_or_update_action_schemas(self): + @mock.patch("itsm.trigger.permissions.WorkflowTriggerPermit.has_permission") + @mock.patch("itsm.trigger.permissions.WorkflowTriggerPermit.has_object_permission") + def test_create_or_update_action_schemas( + self, patch_has_object_permission, patch_has_permission + ): + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True + url = "/api/trigger/triggers/" rsp = self.client.get(path=url, data=None, content_type="application/json") print(json.loads(rsp.content.decode("utf-8"))) diff --git a/itsm/tests/workflow/test_workflow_serializer.py b/itsm/tests/workflow/test_workflow_serializer.py index fbdcae481..829c5ed47 100644 --- a/itsm/tests/workflow/test_workflow_serializer.py +++ b/itsm/tests/workflow/test_workflow_serializer.py @@ -31,9 +31,30 @@ class WorkflowSerializerTest(TestCase): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) + @mock.patch("itsm.workflow.permissions.WorkflowIamAuth.has_object_permission") + @mock.patch( + "itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_object_permission" + ) + @mock.patch("itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_permission") @mock.patch("itsm.workflow.serializers.workflow.transform_single_username") - def test_serializer(self, transform_single_username): + @mock.patch("itsm.trigger.permissions.WorkflowTriggerPermit.has_permission") + @mock.patch("itsm.trigger.permissions.WorkflowTriggerPermit.has_object_permission") + def test_serializer( + self, + patch_workflow_trigger_permit_has_object_permission, + patch_workflow_trigger_permit_has_permission, + transform_single_username, + patch_has_permission, + patch_base_workflow_element_iam_auth_has_object_permission, + patch_workflow_iam_auth_has_object_permission, + ): + patch_workflow_trigger_permit_has_object_permission.return_value = True + patch_workflow_trigger_permit_has_permission.return_value = True transform_single_username.return_value = "admin(管理员)" + patch_has_permission.return_value = True + patch_base_workflow_element_iam_auth_has_object_permission.return_value = True + patch_workflow_iam_auth_has_object_permission.return_value = True + workflow_name = "test_now_{}".format(datetime.now().strftime("%Y%m%d%H%M%S")) create_data = { "name": workflow_name, @@ -196,9 +217,15 @@ def test_serializer(self, transform_single_username): ], } ] + + list_triggers_url = "/api/trigger/triggers/" + list_triggers_rsp = self.client.get( + path=list_triggers_url, data=None, content_type="application/json" + ) + print(json.loads(list_triggers_rsp.content.decode("utf-8"))) schemas_url = ( "/api/trigger/triggers/{}/create_or_update_action_schemas/".format( - workflow_id + list_triggers_rsp.data["data"][0]["id"] ) ) schemas_rsp = self.client.post( @@ -219,8 +246,8 @@ def test_serializer(self, transform_single_username): "action_schemas": schemas_rsp.data["data"], } ] - rule_url = "/api/trigger/triggers/{}/create_or_update_action_schemas/".format( - workflow_id + rule_url = "/api/trigger/triggers/{}/create_or_update_rules/".format( + list_triggers_rsp.data["data"][0]["id"] ) rule_rsp = self.client.post( path=rule_url, data=rule_data, content_type="application/json" diff --git a/itsm/tests/workflow/test_workflow_views.py b/itsm/tests/workflow/test_workflow_views.py index 3fc6b4cb4..6bb0929bc 100644 --- a/itsm/tests/workflow/test_workflow_views.py +++ b/itsm/tests/workflow/test_workflow_views.py @@ -24,6 +24,7 @@ """ import copy +import mock from django.test import TestCase, override_settings from itsm.tests.data.datas import DATA @@ -48,7 +49,15 @@ def test_get_regex_choice(self): self.assertEqual(rsp.data["data"]["regex_choice"], [("EMPTY", "")]) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_variables(self): + @mock.patch("itsm.component.utils.misc.transform_single_username") + @mock.patch("itsm.component.utils.client_backend_query.get_bk_users") + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_variables( + self, patch_iam_auth, patch_get_bk_users, patch_transform_single_username + ): + patch_iam_auth.return_value = True + patch_get_bk_users.return_value = {"admin": "admin(admin)"} + patch_transform_single_username.return_value = "admin(admin)" url = "/api/workflow/templates/" rsp = self.client.get(path=url, data=None, content_type="application/json") url = "/api/workflow/templates/{}/variables/".format(rsp.data["data"][0]["id"]) @@ -58,7 +67,15 @@ def test_variables(self): self.assertIsInstance(rsp.data["data"], list) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_create_accept_transitions(self): + @mock.patch("itsm.component.utils.misc.transform_single_username") + @mock.patch("itsm.component.utils.client_backend_query.get_bk_users") + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_create_accept_transitions( + self, patch_iam_auth, patch_get_bk_users, patch_transform_single_username + ): + patch_iam_auth.return_value = True + patch_get_bk_users.return_value = {"admin": "admin(admin)"} + patch_transform_single_username.return_value = "admin(admin)" url = "/api/workflow/templates/" rsp = self.client.get(path=url, data=None, content_type="application/json") url = "/api/workflow/templates/{}/create_accept_transitions/".format( @@ -70,7 +87,15 @@ def test_create_accept_transitions(self): self.assertIsInstance(rsp.data["data"], list) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_deploy(self): + @mock.patch("itsm.component.utils.misc.transform_single_username") + @mock.patch("itsm.component.utils.client_backend_query.get_bk_users") + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_deploy( + self, patch_iam_auth, patch_get_bk_users, patch_transform_single_username + ): + patch_iam_auth.return_value = True + patch_get_bk_users.return_value = {"admin": "admin(admin)"} + patch_transform_single_username.return_value = "admin(admin)" url = "/api/workflow/templates/" rsp = self.client.get(path=url, data=None, content_type="application/json") url = "/api/workflow/templates/{}/deploy/".format(rsp.data["data"][0]["id"]) @@ -91,7 +116,15 @@ def test_exports(self): self.assertEqual(rsp.status_code, 200) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_table(self): + @mock.patch("itsm.component.utils.misc.transform_single_username") + @mock.patch("itsm.component.utils.client_backend_query.get_bk_users") + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_table( + self, patch_iam_auth, patch_get_bk_users, patch_transform_single_username + ): + patch_iam_auth.return_value = True + patch_get_bk_users.return_value = {"admin": "admin(admin)"} + patch_transform_single_username.return_value = "admin(admin)" url = "/api/workflow/templates/" rsp = self.client.get(path=url, data=None, content_type="application/json") url = "/api/workflow/templates/{}/table/".format(rsp.data["data"][0]["id"]) @@ -103,7 +136,17 @@ def test_table(self): class StateViewTest(TestCase): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_exports(self): + @mock.patch("itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_permission") + @mock.patch( + "itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_object_permission" + ) + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_exports( + self, patch_iam_auth, patch_has_object_permission, patch_has_permission + ): + patch_iam_auth.return_value = True + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True url = "/api/workflow/states/" rsp = self.client.get(path=url, data=None, content_type="application/json") url = "/api/workflow/states/{}/variables/".format(rsp.data["data"][0]["id"]) @@ -114,7 +157,21 @@ def test_exports(self): self.assertIsInstance(rsp.data["data"], list) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_group_variables(self): + @mock.patch("itsm.workflow.permissions.WorkflowIamAuth.has_object_permission") + @mock.patch( + "itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_object_permission" + ) + @mock.patch("itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_permission") + def test_group_variables( + self, + patch_has_permission, + patch_base_workflow_element_iam_auth_has_object_permission, + patch_workflow_iam_auth_has_object_permission, + ): + patch_has_permission.return_value = True + patch_base_workflow_element_iam_auth_has_object_permission.return_value = True + patch_workflow_iam_auth_has_object_permission.return_value = True + workflow_data = copy.deepcopy(DATA) workflow, _, _ = Workflow.objects.restore(workflow_data) version = workflow.create_version() @@ -141,7 +198,17 @@ def test_group_variables(self): self.assertIsInstance(rsp.data["data"], list) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_sign_variables(self): + @mock.patch("itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_permission") + @mock.patch( + "itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_object_permission" + ) + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_sign_variables( + self, patch_iam_auth, patch_has_object_permission, patch_has_permission + ): + patch_iam_auth.return_value = True + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True url = "/api/workflow/states/" rsp = self.client.get(path=url, data=None, content_type="application/json") url = "/api/workflow/states/{}/sign_variables/".format( @@ -154,7 +221,17 @@ def test_sign_variables(self): self.assertIsInstance(rsp.data["data"], list) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_pre_states(self): + @mock.patch("itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_permission") + @mock.patch( + "itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_object_permission" + ) + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_pre_states( + self, patch_iam_auth, patch_has_object_permission, patch_has_permission + ): + patch_iam_auth.return_value = True + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True url = "/api/workflow/states/" rsp = self.client.get(path=url, data=None, content_type="application/json") url = "/api/workflow/states/{}/pre_states/".format(rsp.data["data"][0]["id"]) @@ -165,7 +242,17 @@ def test_pre_states(self): self.assertIsInstance(rsp.data["data"], list) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_post_states(self): + @mock.patch("itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_permission") + @mock.patch( + "itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_object_permission" + ) + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_post_states( + self, patch_iam_auth, patch_has_object_permission, patch_has_permission + ): + patch_iam_auth.return_value = True + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True url = "/api/workflow/states/" rsp = self.client.get(path=url, data=None, content_type="application/json") url = "/api/workflow/states/{}/post_states/".format(rsp.data["data"][0]["id"]) @@ -176,7 +263,17 @@ def test_post_states(self): self.assertIsInstance(rsp.data["data"], list) @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_add_fields_from_table(self): + @mock.patch("itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_permission") + @mock.patch( + "itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_object_permission" + ) + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_add_fields_from_table( + self, patch_iam_auth, patch_has_object_permission, patch_has_permission + ): + patch_iam_auth.return_value = True + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True url = "/api/workflow/states/" rsp = self.client.get(path=url, data=None, content_type="application/json") url = "/api/workflow/states/{}/add_fields_from_table/".format( @@ -192,7 +289,17 @@ def test_add_fields_from_table(self): self.assertEqual(rsp.data["message"], "success") @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_clone(self): + @mock.patch("itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_permission") + @mock.patch( + "itsm.workflow.permissions.BaseWorkflowElementIamAuth.has_object_permission" + ) + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_clone( + self, patch_iam_auth, patch_has_object_permission, patch_has_permission + ): + patch_iam_auth.return_value = True + patch_has_object_permission.return_value = True + patch_has_permission.return_value = True url1 = "/api/workflow/states/" rsp1 = self.client.get(path=url1, data=None, content_type="application/json") url = "/api/workflow/states/{}/clone/".format(rsp1.data["data"][0]["id"]) @@ -270,7 +377,9 @@ def test_post_state(self): class TaskSchemaViewTest(TestCase): @override_settings(MIDDLEWARE=("itsm.tests.middlewares.OverrideMiddleware",)) - def test_variables(self): + @mock.patch("itsm.component.drf.permissions.IamAuthPermit.iam_auth") + def test_variables(self, patch_iam_auth): + patch_iam_auth.return_value = True url = "/api/workflow/task_schemas/" rsp = self.client.get(path=url, data=None, content_type="application/json") diff --git a/itsm/ticket/managers.py b/itsm/ticket/managers.py index ce0db4c4c..a5ead6c4a 100644 --- a/itsm/ticket/managers.py +++ b/itsm/ticket/managers.py @@ -36,7 +36,7 @@ from django.db import models, connections, NotSupportedError from django.db.models import F, Q, QuerySet, AutoField from django.forms import model_to_dict -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from itsm.component.constants import ( @@ -837,9 +837,9 @@ def create_log( from_state_id=state_id, type=operate_type, operator=log_operator, - message="%s..." % message[0:500] - if len(message) > 500 - else message, # 防止消息太长 + message=( + "%s..." % message[0:500] if len(message) > 500 else message + ), # 防止消息太长 workflow_id=ticket.flow.id, processors_type=getattr(status, "processors_type", ""), processors=getattr(status, "processors", ""), @@ -855,6 +855,7 @@ def create_log( ) from itsm.ticket.tasks import ticket_set_history_operators + ticket_set_history_operators.delay(ticket.id, log_operator) return log @@ -1002,6 +1003,7 @@ def _batched_insert(self, objs, fields, batch_size, ignore_conflicts=False): max_batch_size = max(ops.bulk_batch_size(fields, objs), 1) batch_size = min(batch_size, max_batch_size) if batch_size else max_batch_size inserted_rows = [] + on_conflict = "DO NOTHING" if ignore_conflicts else None bulk_return = connections[self.db].features.can_return_rows_from_bulk_insert for item in [objs[i : i + batch_size] for i in range(0, len(objs), batch_size)]: if bulk_return and not ignore_conflicts: @@ -1011,7 +1013,7 @@ def _batched_insert(self, objs, fields, batch_size, ignore_conflicts=False): fields=fields, using=self.db, returning_fields=self.model._meta.db_returning_fields, - ignore_conflicts=ignore_conflicts, + on_conflict=on_conflict, ) ) else: @@ -1019,7 +1021,7 @@ def _batched_insert(self, objs, fields, batch_size, ignore_conflicts=False): item, fields=fields, using=self.db, - ignore_conflicts=ignore_conflicts, + on_conflict=on_conflict, ) return inserted_rows diff --git a/itsm/ticket/migrations/0037_auto_20200212_1554.py b/itsm/ticket/migrations/0037_auto_20200212_1554.py index 7e18086cc..66819e96f 100644 --- a/itsm/ticket/migrations/0037_auto_20200212_1554.py +++ b/itsm/ticket/migrations/0037_auto_20200212_1554.py @@ -34,94 +34,150 @@ class Migration(migrations.Migration): dependencies = [ - ('ticket', '0036_auto_20200114_1855'), + ("ticket", "0036_auto_20200114_1855"), ] operations = [ migrations.CreateModel( - name='SignTask', + name="SignTask", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creator', models.CharField(blank=True, max_length=64, null=True, verbose_name='创建人')), - ('create_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('update_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('updated_by', models.CharField(blank=True, max_length=64, null=True, verbose_name='修改人')), - ('end_at', models.DateTimeField(blank=True, null=True, verbose_name='结束时间')), - ('is_deleted', models.BooleanField(db_index=True, default=False, verbose_name='是否软删除')), - ('status_id', models.IntegerField(verbose_name='状态ID')), - ('order', models.IntegerField(default=-1, verbose_name='顺序')), ( - 'status', + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "creator", + models.CharField( + blank=True, max_length=64, null=True, verbose_name="创建人" + ), + ), + ( + "create_at", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_at", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ( + "updated_by", + models.CharField( + blank=True, max_length=64, null=True, verbose_name="修改人" + ), + ), + ( + "end_at", + models.DateTimeField( + blank=True, null=True, verbose_name="结束时间" + ), + ), + ( + "is_deleted", + models.BooleanField( + db_index=True, default=False, verbose_name="是否软删除" + ), + ), + ("status_id", models.IntegerField(verbose_name="状态ID")), + ("order", models.IntegerField(default=-1, verbose_name="顺序")), + ( + "status", models.CharField( - choices=[('WAIT', '未激活'), ('RUNNING', '执行中'), ('FINISHED', '已完成')], - default='WAIT', + choices=[ + ("WAIT", "未激活"), + ("RUNNING", "执行中"), + ("FINISHED", "已完成"), + ], + default="WAIT", max_length=32, - verbose_name='任务状态', + verbose_name="任务状态", ), ), - ('processor', models.CharField(max_length=255, verbose_name='处理人')), - ('is_active', models.BooleanField(default=False, verbose_name='是否激活')), - ('is_passed', models.NullBooleanField(verbose_name='是否审批通过')), + ("processor", models.CharField(max_length=255, verbose_name="处理人")), + ( + "is_active", + models.BooleanField(default=False, verbose_name="是否激活"), + ), + ( + "is_passed", + models.BooleanField(verbose_name="是否审批通过", null=True), + ), + ], + options={ + "verbose_name": "会签任务", + "verbose_name_plural": "会签任务", + "ordering": ("-id",), + }, + managers=[ + ("_objects", django.db.models.manager.Manager()), ], - options={'verbose_name': '会签任务', 'verbose_name_plural': '会签任务', 'ordering': ('-id',),}, - managers=[('_objects', django.db.models.manager.Manager()),], ), migrations.CreateModel( - name='TaskField', + name="TaskField", fields=[ ( - 'ticketfield_ptr', + "ticketfield_ptr", models.OneToOneField( auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, - to='ticket.TicketField', + to="ticket.TicketField", ), ), ], - options={'abstract': False,}, - bases=('ticket.ticketfield',), - managers=[('_objects', django.db.models.manager.Manager()),], + options={ + "abstract": False, + }, + bases=("ticket.ticketfield",), + managers=[ + ("_objects", django.db.models.manager.Manager()), + ], ), migrations.AddField( - model_name='status', name='is_sequential', field=models.BooleanField(default=False, verbose_name='是否是串行任务'), + model_name="status", + name="is_sequential", + field=models.BooleanField(default=False, verbose_name="是否是串行任务"), ), migrations.AddField( - model_name='status', - name='type', + model_name="status", + name="type", field=models.CharField( choices=[ - ('START', '开始节点(圆形)'), - ('NORMAL', '普通节点'), - ('SIGN', '会签节点'), - ('TASK', '自动节点'), - ('TASK-SOPS', '标准运维节点'), - ('ROUTER', '分支网关节点(菱形)'), - ('ROUTER-P', '并行网关节点'), - ('COVERAGE', '汇聚网关节点'), - ('END', '结束节点(圆形)'), + ("START", "开始节点(圆形)"), + ("NORMAL", "普通节点"), + ("SIGN", "会签节点"), + ("TASK", "自动节点"), + ("TASK-SOPS", "标准运维节点"), + ("ROUTER", "分支网关节点(菱形)"), + ("ROUTER-P", "并行网关节点"), + ("COVERAGE", "汇聚网关节点"), + ("END", "结束节点(圆形)"), ], - default='NORMAL', + default="NORMAL", max_length=32, - verbose_name='节点类型', + verbose_name="节点类型", ), ), migrations.AlterField( - model_name='status', - name='action_type', + model_name="status", + name="action_type", field=models.CharField( choices=[ - ('TRANSITION', '提交'), - ('DISTRIBUTE', '分派'), - ('CLAIM', '认领'), - ('SIGN', '会签'), - ('AUTOMATIC', '自动执行'), + ("TRANSITION", "提交"), + ("DISTRIBUTE", "分派"), + ("CLAIM", "认领"), + ("SIGN", "会签"), + ("AUTOMATIC", "自动执行"), ], - default='TRANSITION', + default="TRANSITION", max_length=32, - verbose_name='节点内部操作类型', + verbose_name="节点内部操作类型", ), ), ] diff --git a/itsm/ticket/models/basic.py b/itsm/ticket/models/basic.py index 649c82471..6e1af76be 100644 --- a/itsm/ticket/models/basic.py +++ b/itsm/ticket/models/basic.py @@ -24,7 +24,7 @@ """ from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import LEN_NORMAL @@ -32,12 +32,16 @@ class Model(models.Model): """基础字段""" - FIELDS = ('creator', 'create_at', 'updated_by', 'update_at', 'end_at') + FIELDS = ("creator", "create_at", "updated_by", "update_at", "end_at") - creator = models.CharField(_("创建人"), max_length=LEN_NORMAL, null=True, blank=True) + creator = models.CharField( + _("创建人"), max_length=LEN_NORMAL, null=True, blank=True + ) create_at = models.DateTimeField(_("创建时间"), auto_now_add=True) update_at = models.DateTimeField(_("更新时间"), auto_now=True) - updated_by = models.CharField(_("修改人"), max_length=LEN_NORMAL, null=True, blank=True) + updated_by = models.CharField( + _("修改人"), max_length=LEN_NORMAL, null=True, blank=True + ) end_at = models.DateTimeField(_("结束时间"), null=True, blank=True) is_deleted = models.BooleanField(_("是否软删除"), default=False, db_index=True) diff --git a/itsm/ticket/models/event.py b/itsm/ticket/models/event.py index d5de91f1e..b83a428e2 100644 --- a/itsm/ticket/models/event.py +++ b/itsm/ticket/models/event.py @@ -24,7 +24,7 @@ """ from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( EMPTY_INT, @@ -44,33 +44,53 @@ class TicketEventLog(Event): """单据操作日志表""" - SOURCE_TYPE = [(WEB, _('页面操作')), (MOBILE, _('移动端操作')), (SYS, _('系统操作')), (SYSTEM_OPERATE, _('自动执行'))] + SOURCE_TYPE = [ + (WEB, _("页面操作")), + (MOBILE, _("移动端操作")), + (SYS, _("系统操作")), + (SYSTEM_OPERATE, _("自动执行")), + ] # id = models.BigAutoField(u'日志大于2^31-1,默认id无法继续创建') - ticket = models.ForeignKey('ticket.Ticket', help_text=_('关联工单'), related_name='logs', on_delete=models.CASCADE) - is_valid = models.BooleanField(_('是否有效流程节点'), default=True) - deal_time = models.IntegerField(_('处理时间'), default=0) + ticket = models.ForeignKey( + "ticket.Ticket", + help_text=_("关联工单"), + related_name="logs", + on_delete=models.CASCADE, + ) + is_valid = models.BooleanField(_("是否有效流程节点"), default=True) + deal_time = models.IntegerField(_("处理时间"), default=0) # 日志固化角色人员, 若角色新增人员,无法添加到快照中,需要手动添加 - processors_snap = models.CharField(_('处理人快照'), max_length=LEN_XX_LONG, default=EMPTY_STRING, null=True, blank=True) - source = models.CharField(_('日志来源'), max_length=LEN_SHORT, choices=SOURCE_TYPE, default=WEB) - status = models.IntegerField(_('节点处理状态'), default=EMPTY_INT) + processors_snap = models.CharField( + _("处理人快照"), + max_length=LEN_XX_LONG, + default=EMPTY_STRING, + null=True, + blank=True, + ) + source = models.CharField( + _("日志来源"), max_length=LEN_SHORT, choices=SOURCE_TYPE, default=WEB + ) + status = models.IntegerField(_("节点处理状态"), default=EMPTY_INT) objects = managers.TicketLogManager() class Meta: - app_label = 'ticket' - verbose_name = _('单据流转日志') - verbose_name_plural = _('单据流转日志') - ordering = ('id',) + app_label = "ticket" + verbose_name = _("单据流转日志") + verbose_name_plural = _("单据流转日志") + ordering = ("id",) index_together = (("operate_at", "operator", "is_deleted"),) def __unicode__(self): - return '{}({})'.format(self.ticket, self.from_state_id) + return "{}({})".format(self.ticket, self.from_state_id) def update_deal_time(self): log_ids = list( - TicketEventLog.objects.filter(ticket_id=self.ticket_id).order_by('id').values_list('id', flat=True) + TicketEventLog.objects.filter(ticket_id=self.ticket_id) + .order_by("id") + .values_list("id", flat=True) ) last_log_index = log_ids.index(self.id) - 1 @@ -99,9 +119,13 @@ def translated_message(self): @classmethod def fix_deal_time(cls, *args, **kwargs): """为之前的单据添加处理事件""" - print('\nfix history ticket deal_time') + print("\nfix history ticket deal_time") try: - for log in TicketEventLog.objects.all().exclude(message__in=['流程开始', '单据流程结束']).exclude(type='UNSUSPEND'): + for log in ( + TicketEventLog.objects.all() + .exclude(message__in=["流程开始", "单据流程结束"]) + .exclude(type="UNSUSPEND") + ): log.update_deal_time() except Exception as e: - print('\nfix history ticket deal_time exception: %s' % e) + print("\nfix history ticket deal_time exception: %s" % e) diff --git a/itsm/ticket/models/field.py b/itsm/ticket/models/field.py index a55e74d16..2bcab7796 100644 --- a/itsm/ticket/models/field.py +++ b/itsm/ticket/models/field.py @@ -24,11 +24,16 @@ """ from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import EMPTY_STRING, LEN_SHORT, SHOW_BY_CONDITION from itsm.component.utils.conversion import format_exp_value, show_conditions_validate -from itsm.component.utils.misc import get_choice_route, get_field_display_value, get_field_value, set_field_value +from itsm.component.utils.misc import ( + get_choice_route, + get_field_display_value, + get_field_value, + set_field_value, +) from itsm.workflow.models import BaseField from . import managers @@ -37,13 +42,29 @@ class TicketField(BaseField): """表单字段值表""" - SOURCE = [('CUSTOM', '自定义添加'), ('TABLE', '基础模型添加')] + SOURCE = [("CUSTOM", "自定义添加"), ("TABLE", "基础模型添加")] - ticket = models.ForeignKey('ticket.Ticket', help_text=_("关联工单"), related_name="fields", on_delete=models.CASCADE) - state_id = models.CharField("对应的状态id", max_length=LEN_SHORT, default=EMPTY_STRING, null=True, blank=True) + ticket = models.ForeignKey( + "ticket.Ticket", + help_text=_("关联工单"), + related_name="fields", + on_delete=models.CASCADE, + ) + state_id = models.CharField( + "对应的状态id", + max_length=LEN_SHORT, + default=EMPTY_STRING, + null=True, + blank=True, + ) _value = models.TextField(_("表单值"), null=True, blank=True) - source = models.CharField(_('添加方式'), max_length=LEN_SHORT, choices=SOURCE, default='CUSTOM') - workflow_field_id = models.IntegerField(_('流程版本字段ID'), default=-1,) + source = models.CharField( + _("添加方式"), max_length=LEN_SHORT, choices=SOURCE, default="CUSTOM" + ) + workflow_field_id = models.IntegerField( + _("流程版本字段ID"), + default=-1, + ) objects = managers.TicketFieldManager() @@ -79,23 +100,25 @@ def value(self, v): def _display_value(self): """用于获取日志接口的数据展示""" if not self._value: - return '' + return "" - if self.type in ['SELECT', 'RADIO']: - return {str(choice['key']): choice['name'] for choice in self.choice}.get(self._value, self._value) + if self.type in ["SELECT", "RADIO"]: + return {str(choice["key"]): choice["name"] for choice in self.choice}.get( + self._value, self._value + ) - if self.type in ['MULTISELECT', 'CHECKBOX', 'MEMBERS']: - choice = {str(choice['key']): choice['name'] for choice in self.choice} - return ','.join([choice.get(key, key) for key in self._value.split(',')]) + if self.type in ["MULTISELECT", "CHECKBOX", "MEMBERS"]: + choice = {str(choice["key"]): choice["name"] for choice in self.choice} + return ",".join([choice.get(key, key) for key in self._value.split(",")]) - if self.type == 'TREESELECT': + if self.type == "TREESELECT": route = get_choice_route(self.choice, self._value) - return '->'.join([item['name'] for item in route]) or self._value + return "->".join([item["name"] for item in route]) or self._value - if self.type == 'TABLE': - return {'header': self.choice, 'value': self.value} - if self.type == 'CUSTOMTABLE': - return {'header': self.meta, 'value': self.value} + if self.type == "TABLE": + return {"header": self.choice, "value": self.value} + if self.type == "CUSTOMTABLE": + return {"header": self.meta, "value": self.value} return self._value @@ -112,8 +135,9 @@ def show_result(self, show_all_fields): if self.show_type == SHOW_BY_CONDITION: key_value = { - 'params_%s' % item['key']: format_exp_value(item['type'], item['_value']) - for item in self.ticket.fields.values('key', '_value', 'type') + "params_%s" + % item["key"]: format_exp_value(item["type"], item["_value"]) + for item in self.ticket.fields.values("key", "_value", "type") } if show_conditions_validate(self.show_conditions, key_value): return False @@ -126,10 +150,21 @@ class TaskField(BaseField): SOURCE = [("CUSTOM", "自定义添加"), ("TABLE", "基础模型添加")] - state_id = models.CharField("对应的状态id", max_length=LEN_SHORT, default=EMPTY_STRING, null=True, blank=True) + state_id = models.CharField( + "对应的状态id", + max_length=LEN_SHORT, + default=EMPTY_STRING, + null=True, + blank=True, + ) _value = models.TextField(_("表单值"), null=True, blank=True) - source = models.CharField(_("添加方式"), max_length=LEN_SHORT, choices=SOURCE, default="CUSTOM") - workflow_field_id = models.IntegerField(_("流程版本字段ID"), default=-1,) + source = models.CharField( + _("添加方式"), max_length=LEN_SHORT, choices=SOURCE, default="CUSTOM" + ) + workflow_field_id = models.IntegerField( + _("流程版本字段ID"), + default=-1, + ) task_id = models.IntegerField(_("任务ID"), default=-1) class Meta: diff --git a/itsm/ticket/models/misc.py b/itsm/ticket/models/misc.py index 4ad2fcdee..c678fedcd 100644 --- a/itsm/ticket/models/misc.py +++ b/itsm/ticket/models/misc.py @@ -30,7 +30,7 @@ from django.conf import settings from django.db import models, transaction from django.utils.crypto import get_random_string -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from mptt.fields import TreeForeignKey from itsm.component.constants import ( @@ -53,7 +53,9 @@ class TicketTemplate(models.Model): name = models.CharField(_("模板名称"), max_length=LEN_NORMAL) creator = models.CharField(_("创建人"), max_length=LEN_NORMAL) - service = models.CharField(_("对应服务主键"), default=EMPTY_STRING, max_length=LEN_NORMAL) + service = models.CharField( + _("对应服务主键"), default=EMPTY_STRING, max_length=LEN_NORMAL + ) template = jsonfield.JSONField( _("单据模板字段"), default=EMPTY_LIST, null=True, blank=True ) @@ -103,11 +105,15 @@ class TicketComment(models.Model): on_delete=models.CASCADE, ) stars = models.IntegerField("评价等级1~5,5星为最好", default=0) - comments = models.CharField(_("评价信息"), max_length=LEN_LONG, null=True, blank=True) + comments = models.CharField( + _("评价信息"), max_length=LEN_LONG, null=True, blank=True + ) source = models.CharField( _("评价来源"), choices=SOURCE_CHOICE, default="SYS", max_length=LEN_NORMAL ) - creator = models.CharField(_("创建人"), max_length=LEN_NORMAL, null=True, blank=True) + creator = models.CharField( + _("创建人"), max_length=LEN_NORMAL, null=True, blank=True + ) create_at = models.DateTimeField(_("创建时间"), auto_now_add=True) update_at = models.DateTimeField(_("更新时间"), auto_now=True) is_deleted = models.BooleanField(_("是否软删除"), default=False) @@ -222,13 +228,19 @@ class NotifyLogModel(models.Model): state_id = models.IntegerField(_("发送节点ID"), default=EMPTY_INT) state_name = models.CharField( - _("节点名称"), max_length=LEN_NORMAL, default=EMPTY_STRING, null=True, blank=True + _("节点名称"), + max_length=LEN_NORMAL, + default=EMPTY_STRING, + null=True, + blank=True, ) creator = models.CharField( _("创建人"), max_length=LEN_NORMAL, default=EMPTY_STRING, null=True, blank=True ) create_at = models.DateTimeField(_("创建时间"), auto_now_add=True) - message = models.TextField(_("通知信息"), default=EMPTY_STRING, null=True, blank=True) + message = models.TextField( + _("通知信息"), default=EMPTY_STRING, null=True, blank=True + ) notify_type = models.CharField(_("通知方式"), max_length=LEN_SHORT, default="EMAIL") is_deleted = models.BooleanField(_("是否软删除"), default=False, db_index=True) @@ -316,7 +328,11 @@ class TicketSuperviseNotifyLog(NotifyLogModel): on_delete=models.CASCADE, ) supervised = models.CharField( - _("被督办的人"), max_length=LEN_LONG, default=EMPTY_STRING, null=True, blank=True + _("被督办的人"), + max_length=LEN_LONG, + default=EMPTY_STRING, + null=True, + blank=True, ) class Meta: @@ -380,9 +396,13 @@ class TicketRemark(BaseMpttModel): ] key = models.CharField(_("目录关键字"), max_length=LEN_LONG, unique=True) - content = models.TextField(_("评论内容"), max_length=LEN_LONG, null=True, blank=True) + content = models.TextField( + _("评论内容"), max_length=LEN_LONG, null=True, blank=True + ) order = models.IntegerField(_("节点顺序"), default=FIRST_ORDER) - remark_type = models.CharField(_("评论类型"), max_length=LEN_SHORT, choices=REMARK_TYPE) + remark_type = models.CharField( + _("评论类型"), max_length=LEN_SHORT, choices=REMARK_TYPE + ) parent = TreeForeignKey( "self", on_delete=models.CASCADE, @@ -391,11 +411,9 @@ class TicketRemark(BaseMpttModel): blank=True, related_name="children", ) - ticket_id = models.IntegerField( - _("单据id"), max_length=LEN_SHORT, null=False, default=0 - ) - users = models.JSONField(_("用户@的用户列表"), default=[]) - update_log = models.JSONField(_("用户评论的更新记录"), default=[]) + ticket_id = models.IntegerField(_("单据id"), null=False, default=0) + users = models.JSONField(_("用户@的用户列表"), default=list) + update_log = models.JSONField(_("用户评论的更新记录"), default=list) class Meta: app_label = "ticket" diff --git a/itsm/ticket/models/ticket.py b/itsm/ticket/models/ticket.py index 8704307b9..3535d7b17 100644 --- a/itsm/ticket/models/ticket.py +++ b/itsm/ticket/models/ticket.py @@ -144,7 +144,8 @@ BK_PLUGIN_STATE, SUSPENDED, SHOW_BY_CONDITION, - VARIABLE_LEADER, FIELD_IGNORE_ESCAPE, + VARIABLE_LEADER, + FIELD_IGNORE_ESCAPE, ) from itsm.component.constants.trigger import ( CREATE_TICKET, @@ -248,7 +249,7 @@ class SignTask(Model): _("任务状态"), max_length=LEN_SHORT, choices=TASK_STATUS_CHOICES, default="WAIT" ) processor = models.CharField(_("处理人"), max_length=LEN_LONG) - is_passed = models.NullBooleanField(_("是否审批通过"), null=True) + is_passed = models.BooleanField(_("是否审批通过"), null=True) objects = managers.SignTaskManager() @@ -309,22 +310,37 @@ class Status(Model): ) # 当前环节处理人 processors_type = models.CharField( - _("处理人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("处理人类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", ) processors = models.CharField( - _("处理人列表"), max_length=LEN_XX_LONG, default=EMPTY_STRING, null=True, blank=True + _("处理人列表"), + max_length=LEN_XX_LONG, + default=EMPTY_STRING, + null=True, + blank=True, ) # 被转单人 delivers_type = models.CharField( - _("转单人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("转单人类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", + ) + delivers = models.TextField( + _("转单人列表"), default=EMPTY_STRING, null=True, blank=True ) - delivers = models.TextField(_("转单人列表"), default=EMPTY_STRING, null=True, blank=True) can_deliver = models.BooleanField(_("能否转单"), default=False) # 被分派人 # TODO assignors_type/assignors是被分派人 assignors_type = models.CharField( - _("派单人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("派单人类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", ) assignors = models.TextField( _("派单人列表"), default=EMPTY_STRING, null=True, blank=True @@ -655,8 +671,8 @@ def log_detail(self, processors_type, processors): [ _(role.name) for role in UserRole.objects.filter( - id__in=processors.split(",") - ) + id__in=processors.split(",") + ) ] ), ) @@ -1326,7 +1342,10 @@ def __init__(self, *args, **kwargs): is_supervise_needed = models.BooleanField(_("是否需要督办"), default=False) supervise_type = models.CharField( - _("督办人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("督办人类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", ) supervisor = models.CharField( _("督办列表"), max_length=LEN_LONG, default=EMPTY_STRING, null=True, blank=True @@ -1341,7 +1360,9 @@ def __init__(self, *args, **kwargs): # Deprecated Fields # 针对节点的字段需要迁移到新的表中 - current_state_id = models.CharField(_("当前状态ID"), null=True, max_length=LEN_NORMAL) + current_state_id = models.CharField( + _("当前状态ID"), null=True, max_length=LEN_NORMAL + ) current_assignor = models.CharField( _("分派人列表"), max_length=LEN_LONG, default=EMPTY_STRING ) @@ -1349,17 +1370,31 @@ def __init__(self, *args, **kwargs): _("处理者列表"), max_length=LEN_LONG, default=EMPTY_STRING ) current_assignor_type = models.CharField( - _("分派人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("分派人类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", ) current_processors_type = models.CharField( - _("处理者类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("处理者类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", ) - updated_by = models.CharField(_("修改人"), default=EMPTY_STRING, max_length=LEN_LONG) + updated_by = models.CharField( + _("修改人"), default=EMPTY_STRING, max_length=LEN_LONG + ) - service = models.CharField(_("对应服务主键"), default="custom", max_length=LEN_NORMAL) + service = models.CharField( + _("对应服务主键"), default="custom", max_length=LEN_NORMAL + ) service_property = jsonfield.JSONCharField( - _("业务特性json字段"), max_length=LEN_LONG, default=EMPTY_DICT, null=True, blank=True + _("业务特性json字段"), + max_length=LEN_LONG, + default=EMPTY_DICT, + null=True, + blank=True, ) workflow_snap_id = models.IntegerField(_("对应的快照信息"), default=0) """ @@ -1916,8 +1951,8 @@ def is_running(self): return ( self.current_status in TicketStatus.objects.filter( - service_type=self.service_type, is_over=False - ).values_list("key", flat=True) + service_type=self.service_type, is_over=False + ).values_list("key", flat=True) and self.current_status != SUSPEND ) @@ -2257,8 +2292,8 @@ def has_perm(self, username): [ status.can_operate(username) for status in self.node_status.filter( - status__in=Status.CAN_OPERATE_STATUS - ) + status__in=Status.CAN_OPERATE_STATUS + ) ] ) @@ -2274,8 +2309,8 @@ def can_view(self, username): or username in self.task_operators or self.can_operate(username) or AttentionUsers.objects.filter( - ticket_id=self.id, follower=username - ).exists() + ticket_id=self.id, follower=username + ).exists() ): # 与单据操作相关的人,都是可以查看的 return True @@ -2337,10 +2372,10 @@ def can_close(self, username): if ( self.is_over or not StatusTransit.objects.filter( - service_type=self.service_type, - from_status__key=self.current_status, - to_status__is_over=True, - ).exists() + service_type=self.service_type, + from_status__key=self.current_status, + to_status__is_over=True, + ).exists() ): # 当前状态无法到达关闭的时候,不可以进行关闭操作按钮 return False @@ -2381,7 +2416,9 @@ def update_priority(self, urgency=None, impact=None): impact = self.fields.get(key=FIELD_PY_IMPACT, source=BASE_MODEL).value except TicketField.DoesNotExist as error: - logger.warning("当前单据不包含影响范围的字段, error is {}".format(error)) + logger.warning( + "当前单据不包含影响范围的字段, error is {}".format(error) + ) return {} if not urgency: @@ -2411,7 +2448,9 @@ def update_priority(self, urgency=None, impact=None): sla_instance = Sla.objects.get(id=sla_id) except Sla.DoesNotExist as error: logger.warning( - "Failed to get sla_instance from Sla, error is {}".format(error) + "Failed to get sla_instance from Sla, error is {}".format( + error + ) ) return {} default_priority = sla_instance.get_default_policy() @@ -3086,7 +3125,10 @@ def fill_state_fields(self, fields): filter_field_query_set = self.fields.filter(key__in=fields_map.keys()) for ticket_field in filter_field_query_set: ticket_field.value = fields_map[ticket_field.key]["value"] - if isinstance(ticket_field.value, str) and ticket_field.type not in FIELD_IGNORE_ESCAPE: + if ( + isinstance(ticket_field.value, str) + and ticket_field.type not in FIELD_IGNORE_ESCAPE + ): need_escape = True try: json.loads(ticket_field.value) @@ -3182,7 +3224,7 @@ def _formatted(pros_type, pros, ticket): for user in f_value.split(","): # 历史数据中多选人员选择字段存入了中文名: miya(miya),暂时兼容 - username = user[0: user.find("(")] if "(" in user else user + username = user[0 : user.find("(")] if "(" in user else user var_pros = "{},{}".format(var_pros, username) # 取到第一个处理人则停止解析 @@ -3260,13 +3302,13 @@ def _formatted(pros_type, pros, ticket): action_type = ( SYSTEM_OPERATE if state.type - in [ - TASK_STATE, - TASK_SOPS_STATE, - TASK_DEVOPS_STATE, - WEBHOOK_STATE, - BK_PLUGIN_STATE, - ] + in [ + TASK_STATE, + TASK_SOPS_STATE, + TASK_DEVOPS_STATE, + WEBHOOK_STATE, + BK_PLUGIN_STATE, + ] else TRANSITION_OPERATE ) @@ -3907,6 +3949,7 @@ def do_in_sign_state(self, node_status, fields, operator, source): # Update ticket priority, processors, history operators self.update_priority() from itsm.ticket.tasks import ticket_set_history_operators + ticket_set_history_operators.delay(self.id, operator) # Update sla task @@ -4017,7 +4060,10 @@ def get_list_view(self): reason = self.get_field_value("reason", None) if reason is None: list_view.append( - {"key": "提单时间", "value": self.create_at.strftime("%Y-%m-%d %H:%M:%S")} + { + "key": "提单时间", + "value": self.create_at.strftime("%Y-%m-%d %H:%M:%S"), + } ) else: list_view.append({"key": "申请理由", "value": reason}) @@ -4146,14 +4192,20 @@ def send_trigger_signal(self, signal, sender=None, context=None): rule_source_type=SOURCE_TICKET, ) logger.info( - "[ticket->send_trigger_signal] 触发器发送发生成功, ticket_id={}".format(self.id) + "[ticket->send_trigger_signal] 触发器发送发生成功, ticket_id={}".format( + self.id + ) ) except BaseException: logger.info( - "[ticket->send_trigger_signal] 触发器事件发送失败, ticket_id={}".format(self.id) + "[ticket->send_trigger_signal] 触发器事件发送失败, ticket_id={}".format( + self.id + ) ) logger.exception( - _("触发器事件发送失败, ticket_sn {} signal :{}").format(self.sn, signal) + _("触发器事件发送失败, ticket_sn {} signal :{}").format( + self.sn, signal + ) ) def create_ticket_relation(self, from_ticket_id): @@ -4368,7 +4420,9 @@ def update_status(self, state_id): def terminate(self, state_id, operator="", terminate_message="--"): """终止单据""" node_status = self.status(state_id) - message = _("{operator}处理节点【{name}】(流程被终止,【终止原因】:{detail_message}).") + message = _( + "{operator}处理节点【{name}】(流程被终止,【终止原因】:{detail_message})." + ) # 创建流转日志 with transaction.atomic(): # 撤销流程 @@ -4424,7 +4478,11 @@ def terminate(self, state_id, operator="", terminate_message="--"): ) self.stop_all_sla() - return {"result": True, "message": _("流程终止成功:%s") % res.message, "code": 0} + return { + "result": True, + "message": _("流程终止成功:%s") % res.message, + "code": 0, + } def suspend(self, suspend_message, operator="system"): """挂起""" diff --git a/itsm/ticket/permissions.py b/itsm/ticket/permissions.py index 4adf1eb8b..859a319f8 100644 --- a/itsm/ticket/permissions.py +++ b/itsm/ticket/permissions.py @@ -25,7 +25,7 @@ import operator from functools import reduce -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.conf import settings from rest_framework import permissions from rest_framework.serializers import ValidationError @@ -62,16 +62,16 @@ class TicketPermissionValidate(permissions.BasePermission): def __init__(self): self.message = _("抱歉,您无权查看该单据") - + def has_permission(self, request, view): """工单创建权限""" if view.action != "create": return True - + service_id = request.data.get("service_id") if not service_id: return False - + queryset = Service.objects.filter(pk=service_id, is_valid=True) conditions = Service.permission_filter(request.user.username) queryset = queryset.filter(reduce(operator.or_, conditions)) @@ -110,7 +110,9 @@ def has_object_permission(self, request, view, obj): if view.action == "exception_distribute": if not iam_ticket_manage_auth: - self.message = _("抱歉,您无权执行此操作,因为您该服务没有工单管理的权限") + self.message = _( + "抱歉,您无权执行此操作,因为您该服务没有工单管理的权限" + ) return False else: return True @@ -204,9 +206,9 @@ def iam_ticket_view_auth(self, request, obj): str(resource["resource_id"]), { "iam_resource_owner": resource.get("creator", ""), - "_bk_iam_path_": bk_iam_path - if resource["resource_type"] != "project" - else "", + "_bk_iam_path_": ( + bk_iam_path if resource["resource_type"] != "project" else "" + ), "name": resource.get("resource_name", ""), }, ) @@ -337,7 +339,7 @@ def has_object_permission(self, request, view, obj): if UserRole.is_itsm_superuser(username): return True - + if username == obj.creator: return True diff --git a/itsm/ticket/serializers/event.py b/itsm/ticket/serializers/event.py index 61e070dde..e02b52e55 100644 --- a/itsm/ticket/serializers/event.py +++ b/itsm/ticket/serializers/event.py @@ -24,7 +24,7 @@ """ from django.contrib.auth import get_user_model -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.fields import empty @@ -43,21 +43,21 @@ class EventSerializer(serializers.ModelSerializer): class Meta: model = TicketEventLog fields = ( - 'id', - 'ticket', - 'type', - 'operator', - 'operate_at', - 'deal_time', - 'processors_type', - 'processors', - 'message', - 'detail_message', - 'action', - 'from_state_name', - 'ticket_id', - 'form_data', - 'from_state_id', + "id", + "ticket", + "type", + "operator", + "operate_at", + "deal_time", + "processors_type", + "processors", + "message", + "detail_message", + "action", + "from_state_name", + "ticket_id", + "form_data", + "from_state_id", ) def __init__(self, instance=None, data=empty, **kwargs): @@ -72,27 +72,42 @@ def get_related_users(self): """ logs = ( - [self.instance] if isinstance(self.instance, - TicketEventLog) else [] if self.instance is None else self.instance + [self.instance] + if isinstance(self.instance, TicketEventLog) + else [] if self.instance is None else self.instance ) all_related_users = [inst.operator for inst in logs if inst.operator] - return get_bk_users(format='dict', users=list(set(all_related_users))) + return get_bk_users(format="dict", users=list(set(all_related_users))) def to_representation(self, instance): data = super(EventSerializer, self).to_representation(instance) - data['message'] = translate(instance.message, data, related_operators=self.related_users) - data['operator'] = self.related_users.get(instance.operator) + data["message"] = translate( + instance.message, data, related_operators=self.related_users + ) + data["operator"] = self.related_users.get(instance.operator) form_data = [] - origin_form_data = data['form_data'].values() if isinstance(data['form_data'], dict) else data['form_data'] + origin_form_data = ( + data["form_data"].values() + if isinstance(data["form_data"], dict) + else data["form_data"] + ) for item in origin_form_data: - if not item.get('show_result'): + if not item.get("show_result"): continue value_status = item.get("value_status") if value_status: - item.update({"name": _("{}(修改前)" if value_status == 'before' else "{}(修改后)").format(item['name'])}) + item.update( + { + "name": _( + "{}(修改前)" if value_status == "before" else "{}(修改后)" + ).format(item["name"]) + } + ) form_data.append(item) - data['form_data'] = form_data - node_status = Status.objects.filter(ticket_id=instance.ticket_id, state_id=instance.from_state_id).first() - data['from_state_type'] = getattr(node_status, "type", "") + data["form_data"] = form_data + node_status = Status.objects.filter( + ticket_id=instance.ticket_id, state_id=instance.from_state_id + ).first() + data["from_state_type"] = getattr(node_status, "type", "") return data diff --git a/itsm/ticket/serializers/misc.py b/itsm/ticket/serializers/misc.py index 3b802e868..60bcf89d4 100644 --- a/itsm/ticket/serializers/misc.py +++ b/itsm/ticket/serializers/misc.py @@ -22,7 +22,7 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from common.utils import texteditor_escape @@ -62,7 +62,11 @@ def validate(self, attrs): raise serializers.ValidationError(_("该模板名字已存在")) if self.context["view"].action == "update": if ( - TicketTemplate.objects.filter(name=attrs.get("name"), creator=creator, service=attrs.get("service")) + TicketTemplate.objects.filter( + name=attrs.get("name"), + creator=creator, + service=attrs.get("service"), + ) .exclude(id=pk) .exists() ): @@ -86,13 +90,17 @@ def validate(self, attrs): creator = self.context["request"].user.username if self.context["view"].action == "create": if TicketStateDraft.objects.filter( - ticket_id=attrs.get("ticket_id"), creator=creator, state_id=attrs.get("state_id") + ticket_id=attrs.get("ticket_id"), + creator=creator, + state_id=attrs.get("state_id"), ).exists(): raise serializers.ValidationError(_("该模板名字已存在")) if self.context["view"].action == "update": if ( TicketStateDraft.objects.filter( - ticket_id=attrs.get("ticket_id"), creator=creator, state_id=attrs.get("state_id") + ticket_id=attrs.get("ticket_id"), + creator=creator, + state_id=attrs.get("state_id"), ) .exclude(id=self.context["view"].kwargs.get("pk")) .exists() @@ -106,7 +114,9 @@ class CommentSerializer(serializers.ModelSerializer): """工单评价序列化""" stars = serializers.IntegerField(required=True, max_value=6, min_value=1) - comments = serializers.CharField(required=False, max_length=LEN_LONG, allow_null=True, allow_blank=True) + comments = serializers.CharField( + required=False, max_length=LEN_LONG, allow_null=True, allow_blank=True + ) class Meta: model = TicketComment @@ -125,7 +135,7 @@ class Meta: def update(self, instance, validated_data): if instance.stars != 0: raise serializers.ValidationError(_("该单据已经被评论,请勿重复评论")) - + validated_data["comments"] = texteditor_escape( validated_data["comments"], is_support_img=False ) @@ -133,8 +143,12 @@ def update(self, instance, validated_data): def to_representation(self, instance): data = super(CommentSerializer, self).to_representation(instance) - data["has_invited"] = ",".join(instance.invite.all().values_list("receiver", flat=True)) - data["creator"] = get_bk_users(format="dict", users=[data["creator"]]).get(data["creator"], data["creator"]) + data["has_invited"] = ",".join( + instance.invite.all().values_list("receiver", flat=True) + ) + data["creator"] = get_bk_users(format="dict", users=[data["creator"]]).get( + data["creator"], data["creator"] + ) return data @@ -165,7 +179,9 @@ class Meta: def to_representation(self, instance): data = super(FollowerNotifyLogSerializer, self).to_representation(instance) - data["creator_zh"] = get_bk_users(format="dict", users=[instance.creator]).get(instance.creator, "") + data["creator_zh"] = get_bk_users(format="dict", users=[instance.creator]).get( + instance.creator, "" + ) data["create_at"] = "{}".format(get_time(instance.create_at)) data["group"] = transform_username( UserRole.get_users_by_type( diff --git a/itsm/ticket/serializers/ticket.py b/itsm/ticket/serializers/ticket.py index a8f0f60f0..195de5e54 100644 --- a/itsm/ticket/serializers/ticket.py +++ b/itsm/ticket/serializers/ticket.py @@ -28,7 +28,7 @@ from datetime import datetime from django.contrib.auth import get_user_model -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.fields import JSONField, empty @@ -114,7 +114,11 @@ TaskFieldSerializer, ) from itsm.ticket.tasks import remark_notify -from itsm.ticket.utils import compute_list_difference, get_user_profile, filter_sensitive_info +from itsm.ticket.utils import ( + compute_list_difference, + get_user_profile, + filter_sensitive_info, +) from itsm.ticket.validators import CreateTicketValidator, StateOperateValidator from itsm.ticket_status.models import TicketStatus from itsm.workflow.models import WorkflowVersion @@ -374,9 +378,11 @@ def to_representation(self, inst): sla_timeout=sla_task_info["sla_timeout"], sla_status=sla_task_info["sla_status"], sla_task_status=sla_task_info["task_status"], - sla_deadline=sla_task_info["deadline"].strftime("%Y-%m-%d %H:%M:%S") - if sla_task_info["deadline"] - else "--", + sla_deadline=( + sla_task_info["deadline"].strftime("%Y-%m-%d %H:%M:%S") + if sla_task_info["deadline"] + else "--" + ), ) return data @@ -691,9 +697,11 @@ def to_client_representation(self): "name", "--" ), current_steps=steps.get(real_ticket["id"], []), - priority_name=inst["meta"]["priority"]["name"] - if "priority" in inst["meta"] - else "--", + priority_name=( + inst["meta"]["priority"]["name"] + if "priority" in inst["meta"] + else "--" + ), create_at=inst["create_at"].strftime("%Y-%m-%d %H:%M:%S"), current_processors=inst.get("current_processors", ""), can_comment=self.can_comment(inst, comments, is_email_invite_token), @@ -797,9 +805,7 @@ def get_related_users(self): tickets = ( [self.instance] if isinstance(self.instance, Ticket) - else [] - if self.instance is None - else self.instance + else [] if self.instance is None else self.instance ) for inst in tickets: @@ -811,9 +817,7 @@ def get_attention_users(self): tickets = ( [self.instance] if isinstance(self.instance, Ticket) - else [] - if self.instance is None - else self.instance + else [] if self.instance is None else self.instance ) ticket_ids = [ticket.id for ticket in tickets] followers = AttentionUsers.objects.filter(ticket_id__in=ticket_ids).values( @@ -874,7 +878,7 @@ def to_representation(self, inst): # 当前步骤、单据状态、优先级来源母单 master_ticket = inst.get_master_ticket() master_or_self_ticket = master_ticket if master_ticket else inst - + meta = master_or_self_ticket.get_meta() data.update( @@ -882,7 +886,7 @@ def to_representation(self, inst): current_status_display=master_or_self_ticket.current_status_display, current_steps=master_or_self_ticket.brief_current_steps, priority_name=master_or_self_ticket.priority_name, - meta=meta + meta=meta, ) can_comment = inst.can_comment(username) or is_email_invite_token @@ -956,7 +960,9 @@ def run_validation(self, data): service_type=service.key, is_start=True ).key except TicketStatus.DoesNotExist: - raise serializers.ValidationError({_("工单状态"): _("工单状态不存在,请检查")}) + raise serializers.ValidationError( + {_("工单状态"): _("工单状态不存在,请检查")} + ) state_processors = data.get("meta", {}).get("state_processors", {}) for state_id, state_processor in state_processors.items(): @@ -1185,7 +1191,9 @@ def run_validation(self, data=empty): ) if ticket_to_ticket.related_status == "RUNNING": - raise serializers.ValidationError({_("母子单"): _("正在解绑中... 请勿重复执行")}) + raise serializers.ValidationError( + {_("母子单"): _("正在解绑中... 请勿重复执行")} + ) return value @@ -1469,11 +1477,11 @@ def to_representation(self, instance): "current_status": instance.get_current_status_display(), "current_steps": instance.get_current_state_name(), "current_processors_type": instance.get_current_role_display(), - "current_processors": transform_username( - instance.get_current_processors() - ) - if instance.get_current_processors() - else "--", + "current_processors": ( + transform_username(instance.get_current_processors()) + if instance.get_current_processors() + else "--" + ), } ) diff --git a/itsm/ticket/tasks.py b/itsm/ticket/tasks.py index 7693fb3fa..a2442761b 100644 --- a/itsm/ticket/tasks.py +++ b/itsm/ticket/tasks.py @@ -31,12 +31,12 @@ from django.conf import settings from django.contrib.auth import get_user_model -from celery import Task +from celery import Task, shared_task from celery.schedules import crontab -from celery.task import periodic_task, task +from blueapps.contrib.celery_tools.periodic import periodic_task from django.db.models import Q from django.db import connection, transaction -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from common.mymako import render_mako_tostring @@ -78,7 +78,9 @@ def auto_comment(): logger.info(_("添加默认评价")) TicketComment.objects.filter( stars=0, create_at__lte=now - timedelta(days=AUTO_COMMENT_DAYS) - ).update(**{"stars": 4, "comments": "系统默认评价:满意!", "creator": "系统评价"}) + ).update( + **{"stars": 4, "comments": "系统默认评价:满意!", "creator": "系统评价"} + ) @periodic_task(run_every=crontab(hour=0, minute=0, day_of_week=1)) @@ -185,14 +187,16 @@ def weekly_statical(): message = render_mako_tostring("weekly_statical_report.html", locals()) - notifier = EmailNotifier(title="流程服务统计周报", receivers=receivers, message=message) + notifier = EmailNotifier( + title="流程服务统计周报", receivers=receivers, message=message + ) try: notifier.send() except ComponentCallError as error: logger.info("统计日报发送失败, 组件错误: %s" % str(error)) -@task +@shared_task def start_pipeline(ticket, **kwargs): ticket.start(**kwargs) @@ -225,7 +229,7 @@ def on_failure(self, exc, task_id, args, kwargs, einfo): ).update(related_status="UNBIND_FAILED") -@task(base=ClonePipelineCallback) +@shared_task(base=ClonePipelineCallback) def clone_pipeline(ticket, parent_ticket): ticket.clone_pipeline(parent_ticket) @@ -248,7 +252,7 @@ def dispatch_retry_notify_event(ticket, state_id, receivers): retry_notify.apply_async(args=[ticket, state_id, receivers], countdown=countdown) -@task +@shared_task def retry_notify(ticket, state_id, receivers): # 每次发通知前查看一下当前单据的最新状态,并更新之后的单据对象 ticket.refresh_from_db() @@ -257,7 +261,9 @@ def retry_notify(ticket, state_id, receivers): # 需要停止发送消息的状态列表 status = ["REVOKED", "TERMINATED", "FINISHED"] if ticket.current_status in status or not ticket.is_current_step(state_id): - logger.info(_("当前任务已过期, ticket_id={}, state_id={}".format(ticket.id, state_id))) + logger.info( + _("当前任务已过期, ticket_id={}, state_id={}".format(ticket.id, state_id)) + ) else: # 当前状态存在,才发送通知 logger.info( @@ -266,10 +272,10 @@ def retry_notify(ticket, state_id, receivers): ticket.notify(state_id, receivers) -@task +@shared_task def notify_task(ticket, receivers, message, action, **kwargs): """发送通知""" - task_id = kwargs.get("task_id") + task_id = kwargs.pop("task_id", None) # 关闭通知服务 if CLOSE_NOTIFY == "close": @@ -297,7 +303,7 @@ def notify_task(ticket, receivers, message, action, **kwargs): logger.exception("send email exception: %s" % str(e)) -@task +@shared_task def notify_fast_approval_task(ticket, state_id, receivers): """发送快速审批通知""" @@ -417,7 +423,7 @@ def build_auto_transit_rules(ticket, auto_transits): return rules -@task +@shared_task def remark_notify(ticket_id, creator, message, receivers): ticket = Ticket.objects.get(id=ticket_id) title = "{0}在单据{1}({2})下评论@了您".format(creator, ticket.title, ticket.sn) @@ -516,12 +522,12 @@ def consume_notify(): queryset = Ticket.objects.filter(current_status="RUNNING", is_deleted=False) - for _ in range(1, end): + for i in range(1, end): user = email_notify.lpop("notify_queue") send_message(user, queryset) -@task +@shared_task def ticket_set_history_operators(ticket_id, current_operator): """设置历史处理人""" with transaction.atomic(): diff --git a/itsm/ticket/utils.py b/itsm/ticket/utils.py index 60c461a9a..61b73db14 100644 --- a/itsm/ticket/utils.py +++ b/itsm/ticket/utils.py @@ -30,7 +30,7 @@ import requests from django.conf import settings from django.contrib.auth import get_user_model -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from mako.template import Template from common.log import logger @@ -102,6 +102,9 @@ def build_message(_notify, task_id, ticket, message, action, **kwargs): else: custom_notify = get_custom_notify(ticket, action, _notify.type) + if task_id is not None: + kwargs["task_id"] = task_id + # 获取单据上下文 context = ticket.get_notify_context() context.update( diff --git a/itsm/ticket/validators/field.py b/itsm/ticket/validators/field.py index ce7c9bd39..bdd40b9a4 100644 --- a/itsm/ticket/validators/field.py +++ b/itsm/ticket/validators/field.py @@ -27,7 +27,7 @@ import re -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from pipeline.utils.boolrule import BoolRule from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -62,7 +62,9 @@ def field_validate(field, state_fields, key_value, **kwargs): field_obj = state_fields.get(field["key"], None) if field_obj is None: - raise serializers.ValidationError(_("【{}】字段不存在,请联系管理员").format(field["key"])) + raise serializers.ValidationError( + _("【{}】字段不存在,请联系管理员").format(field["key"]) + ) field_obj = bunchify(field_obj) @@ -104,7 +106,9 @@ def required_validate(field, field_obj, key_value, skip_readonly=False): if field_obj.type in ["CUSTOMTABLE", "TABLE"]: if not field["value"]: - raise serializers.ValidationError(_("【{}】为必填项").format(field_obj.name)) + raise serializers.ValidationError( + _("【{}】为必填项").format(field_obj.name) + ) # 表格类型字段的必填校验 field_column_schema = ( @@ -124,7 +128,9 @@ def required_validate(field, field_obj, key_value, skip_readonly=False): for column, value in row.items(): if column in required_columns and not value: raise serializers.ValidationError( - _("表格字段【{field_name}】的第【{index}】行【{column_name}】为必填项").format( + _( + "表格字段【{field_name}】的第【{index}】行【{column_name}】为必填项" + ).format( field_name=field_obj.name, index=index + 1, column_name=required_columns[column], @@ -154,14 +160,18 @@ def choice_validate(field, field_obj, key_value, **kwargs): choice = get_choice(field_obj, key_value, **kwargs) if not choice: - raise serializers.ValidationError(_("【%s】选项不存在,请联系管理员") % field_obj.key) + raise serializers.ValidationError( + _("【%s】选项不存在,请联系管理员") % field_obj.key + ) # 更新choice field["choice"] = choice if field_obj.type == "TREESELECT": if not choice: - raise serializers.ValidationError(_("数据字典不存在,请检查字典编码: %s") % field_obj.key) + raise serializers.ValidationError( + _("数据字典不存在,请检查字典编码: %s") % field_obj.key + ) key_choice = [str(item["id"]) for _choice in choice for item in walk(_choice)] else: key_choice = [str(item["key"]) for item in choice] @@ -229,7 +239,9 @@ def custom_regex_validate(field, field_obj): if not re.match(r"{}".format(custom_regex), str(field["value"])): raise serializers.ValidationError(_("用户输入的值不符合自定义正则规则")) except Exception as e: - raise serializers.ValidationError(_("自定义正则出现异常, error = {}".format(e))) + raise serializers.ValidationError( + _("自定义正则出现异常, error = {}".format(e)) + ) def validate_expression(field, expression, ticket, key_value=None): @@ -327,7 +339,9 @@ def regex_validate(field, field_obj, ticket=None, key_value=None): if rule.expressions: results = [] for expression in rule.expressions: - results.append(validate_expression(field, expression, ticket, key_value)) + results.append( + validate_expression(field, expression, ticket, key_value) + ) expression_type = {"and": all, "or": any} if not expression_type.get(rule.type, any)(results): @@ -390,7 +404,9 @@ def date_validate(self, value): value = datetime.datetime.strptime(value, "%Y-%m-%d") except ValueError: raise serializers.ValidationError( - _("【{}】{} 不匹配日期格式{}").format(self.field_name, value, "%Y-%m-%d") + _("【{}】{} 不匹配日期格式{}").format( + self.field_name, value, "%Y-%m-%d" + ) ) if self.validate_type == "after_date" and value < datetime.datetime.now(): diff --git a/itsm/ticket/validators/misc.py b/itsm/ticket/validators/misc.py index 9cbcd9a60..f58d0d673 100644 --- a/itsm/ticket/validators/misc.py +++ b/itsm/ticket/validators/misc.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ from django.conf import settings -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from common.log import logger @@ -38,7 +38,7 @@ def days_validate(days): try: days = int(days) except ValueError: - raise serializers.ValidationError(_('天数参数类型错误!')) + raise serializers.ValidationError(_("天数参数类型错误!")) return days @@ -46,54 +46,62 @@ def notify_log_validate(data, operator): """发送通知的校验""" # 发送关注信息的校验 - message = data.get('message', '') - ticket_id = data.get('ticket_id') + message = data.get("message", "") + ticket_id = data.get("ticket_id") if not message: - raise serializers.ValidationError(_('请填写关注信息!')) + raise serializers.ValidationError(_("请填写关注信息!")) if len(message) > 200: - raise serializers.ValidationError(_('关注信息不能超过200个字符!')) + raise serializers.ValidationError(_("关注信息不能超过200个字符!")) try: ticket = Ticket.objects.get(id=ticket_id) except Ticket.DoesNotExist: - logger.error('发送关注通知校验失败:单据不存在 ticket_id={}'.format(ticket_id)) - raise serializers.ValidationError(_('发送关注通知校验失败:单据不存在,请联系管理员')) + logger.error("发送关注通知校验失败:单据不存在 ticket_id={}".format(ticket_id)) + raise serializers.ValidationError( + _("发送关注通知校验失败:单据不存在,请联系管理员") + ) bk_biz_id = ticket.bk_biz_id if not ticket.can_invite_followers(operator): - raise serializers.ValidationError(_('发送关注通知校验失败:单据已结束或权限不足')) + raise serializers.ValidationError( + _("发送关注通知校验失败:单据已结束或权限不足") + ) - followers = data.get('followers') - followers_type = data.get('followers_type') - receivers = UserRole.get_users_by_type(bk_biz_id=bk_biz_id, user_type=followers_type, users=followers) + followers = data.get("followers") + followers_type = data.get("followers_type") + receivers = UserRole.get_users_by_type( + bk_biz_id=bk_biz_id, user_type=followers_type, users=followers + ) if not receivers: logger.error( - '发送关注通知校验失败:接收人不存在:receivers={}, bk_biz_id={}, followers={}, followers_type={}'.format( + "发送关注通知校验失败:接收人不存在:receivers={}, bk_biz_id={}, followers={}, followers_type={}".format( receivers, bk_biz_id, followers, followers_type ) ) - raise serializers.ValidationError(_('发送关注通知校验失败:通知人不存在或通知角色没有人员,请联系管理员')) + raise serializers.ValidationError( + _("发送关注通知校验失败:通知人不存在或通知角色没有人员,请联系管理员") + ) - return ticket, ','.join(receivers) + return ticket, ",".join(receivers) def sms_comment_validate(queryset, data): """接收短信评价校验""" try: - comment = queryset.get(invite__code=data.get('code')) + comment = queryset.get(invite__code=data.get("code")) except TicketComment.DoesNotExist: - raise serializers.ValidationError(_('单据评论信息不存在,请联系管理员!')) + raise serializers.ValidationError(_("单据评论信息不存在,请联系管理员!")) try: - stars = int(data.get('stars')) + stars = int(data.get("stars")) except ValueError: - raise serializers.ValidationError(_('评价信息不正确,请联系管理员!')) - if comment.ticket.sn != data.get('sn'): - raise serializers.ValidationError(_('单据评论信息不匹配')) + raise serializers.ValidationError(_("评价信息不正确,请联系管理员!")) + if comment.ticket.sn != data.get("sn"): + raise serializers.ValidationError(_("单据评论信息不匹配")) if comment.stars: - raise serializers.ValidationError(_('该单据已经被评论,请勿重复评论!')) + raise serializers.ValidationError(_("该单据已经被评论,请勿重复评论!")) if stars not in list(range(1, 6)): - raise serializers.ValidationError(_('请从(1~5星)选择评价星级!')) + raise serializers.ValidationError(_("请从(1~5星)选择评价星级!")) return comment, stars @@ -101,28 +109,30 @@ def sms_invite_validate(ticket, numbers, invitor): """发送号码前评论校验""" if not ticket.can_comment(invitor): - raise serializers.ValidationError(_('抱歉,您无权发送评价邀请')) - + raise serializers.ValidationError(_("抱歉,您无权发送评价邀请")) + if settings.TICKET_INVITE_SMS_COUNT: if len(numbers) > settings.TICKET_INVITE_SMS_COUNT: raise serializers.ValidationError(_("SMS 发送评价邀请超过限额")) - invite_count = TicketCommentInvite.objects.filter(comment__ticket__id=ticket.id).count() + invite_count = TicketCommentInvite.objects.filter( + comment__ticket__id=ticket.id + ).count() if invite_count > settings.TICKET_INVITE_SMS_COUNT: raise serializers.ValidationError(_("SMS 发送评价邀请超过限额")) for number in numbers: try: - Regex(validate_type='phone_num').validate(number) + Regex(validate_type="phone_num").validate(number) except Exception as error: - raise serializers.ValidationError('【{}】{}'.format(number, str(error))) + raise serializers.ValidationError("【{}】{}".format(number, str(error))) def email_invite_validate(ticket, invitor, receiver): """邮件邀请评价校验""" if not ticket.can_comment(invitor): - raise serializers.ValidationError(_('抱歉,您无权发送评价邀请')) + raise serializers.ValidationError(_("抱歉,您无权发送评价邀请")) if receiver not in get_bk_users(users=[receiver]): - raise serializers.ValidationError(_('【{}】用户不存在').format(receiver)) + raise serializers.ValidationError(_("【{}】用户不存在").format(receiver)) diff --git a/itsm/ticket/validators/ticket.py b/itsm/ticket/validators/ticket.py index 1d414a0a6..9c45deeea 100644 --- a/itsm/ticket/validators/ticket.py +++ b/itsm/ticket/validators/ticket.py @@ -27,7 +27,7 @@ from django.conf import settings from django.db.models import Q -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -100,7 +100,9 @@ def create_validate(self, value, fields, username, **kwargs): lost_keys = required_keys - field_keys if lost_keys: - raise CreateTicketError(_("单据创建失败,缺少参数:{}".format(list(lost_keys)))) + raise CreateTicketError( + _("单据创建失败,缺少参数:{}".format(list(lost_keys))) + ) first_state_permission(fields, state, username) # create_permission_validate(service, username) @@ -173,7 +175,9 @@ def deliver_validate(self, value): """转单校验""" if not self.current_node.can_deliver: - raise ParamError(_("指定流程节点【{}】不支持转单操作.").format(self.current_node.name)) + raise ParamError( + _("指定流程节点【{}】不支持转单操作.").format(self.current_node.name) + ) if not self.current_node.status == RUNNING: raise ParamError(_("当前任务状态下无法转单.")) @@ -239,7 +243,9 @@ def processor_validate(self, value, reference_processor_type, reference_processo if not set(processors).issubset(set(valid_person)): raise ParamError( - _("当前分配的部分用户可能不符合条件,请确保用户在{}中").format(",".join(set(valid_person))) + _("当前分配的部分用户可能不符合条件,请确保用户在{}中").format( + ",".join(set(valid_person)) + ) ) @@ -252,7 +258,9 @@ def first_state_permission(fields, first_state, username): user_department_ids = get_user_department_ids(username) state_department_id = first_state["processors"] if int(state_department_id) not in user_department_ids: - raise CreateTicketError(_("【{}】没有任务【提单】的【提交】操作权限.").format(username)) + raise CreateTicketError( + _("【{}】没有任务【提单】的【提交】操作权限.").format(username) + ) else: return @@ -266,7 +274,9 @@ def first_state_permission(fields, first_state, username): ): return - raise CreateTicketError(_("【{}】没有任务【提单】的【提交】操作权限.").format(username)) + raise CreateTicketError( + _("【{}】没有任务【提单】的【提交】操作权限.").format(username) + ) def create_permission_validate(service, username): @@ -278,7 +288,9 @@ def create_permission_validate(service, username): ): return - raise CreateTicketError(_("【{}】没有任务【提单】的【提交】操作权限.").format(username)) + raise CreateTicketError( + _("【{}】没有任务【提单】的【提交】操作权限.").format(username) + ) def first_state_field_validate(state_fields, fields, **kwargs): @@ -310,10 +322,14 @@ def derive_validate(username, ticket_id): raise serializers.ValidationError({_("单据"): _("单据不存在,请联系管理员!")}) if ticket.is_over: - raise serializers.ValidationError({_("单据"): _("单据已结束,无法新建关联单!")}) + raise serializers.ValidationError( + {_("单据"): _("单据已结束,无法新建关联单!")} + ) if not ticket.has_perm(username): - raise serializers.ValidationError({_("单据"): _("抱歉,您没有单据操作权限,请联系管理员!")}) + raise serializers.ValidationError( + {_("单据"): _("抱歉,您没有单据操作权限,请联系管理员!")} + ) def bind_derive_tickets_validate(from_ticket_id, to_ticket_ids): @@ -324,7 +340,7 @@ def bind_derive_tickets_validate(from_ticket_id, to_ticket_ids): :return: """ # id有效性校验 - if type(to_ticket_ids) != list: + if not isinstance(to_ticket_ids, list): raise serializers.ValidationError(_("参数类型错误:[to_tickets]")) if from_ticket_id in to_ticket_ids: @@ -348,7 +364,9 @@ def bind_derive_tickets_validate(from_ticket_id, to_ticket_ids): ) ).exists(): raise serializers.ValidationError( - _("单据【{}】和【{}】已存在关联关系").format(from_ticket.sn, to_ticket.sn) + _("单据【{}】和【{}】已存在关联关系").format( + from_ticket.sn, to_ticket.sn + ) ) @@ -398,7 +416,9 @@ def merge_validate(from_ticket_ids, to_ticket_id, operator): for ticket in tickets: if ticket["flow_id"] != to_ticket["flow_id"]: - raise serializers.ValidationError(_("母子单必须属于同一个流程版本,无法关联")) + raise serializers.ValidationError( + _("母子单必须属于同一个流程版本,无法关联") + ) from_ticket_ids.pop(from_ticket_ids.index(ticket["id"])) @@ -413,7 +433,9 @@ def ticket_can_be_master(ticket_id): """ ticket = Ticket.objects.get(id=ticket_id) if ticket.is_slave: - raise serializers.ValidationError(_("单据 [{}] 已经是子单,无法成为母单".format(ticket.sn))) + raise serializers.ValidationError( + _("单据 [{}] 已经是子单,无法成为母单".format(ticket.sn)) + ) def tickets_can_be_slave(ticket_ids): @@ -446,11 +468,17 @@ def withdraw_validate(operator, ticket, ignore_user=False): if not ticket.flow.is_revocable: # 不可撤销或者已经结束的单,直接返回 - raise serializers.ValidationError(_("抱歉,当前流程配置无法撤单,请联系服务负责人")) + raise serializers.ValidationError( + _("抱歉,当前流程配置无法撤单,请联系服务负责人") + ) if operator != ticket.creator and operator != settings.SYSTEM_USE_API_ACCOUNT: raise serializers.ValidationError( - _("抱歉,你无权撤销单据,撤销单据的非当前单据的提单人, {}!={}".format(operator, ticket.creator)) + _( + "抱歉,你无权撤销单据,撤销单据的非当前单据的提单人, {}!={}".format( + operator, ticket.creator + ) + ) ) if ticket.is_over: @@ -475,7 +503,9 @@ def terminate_validate(username, ticket, state_id, terminate_message): status = ticket.status(state_id) if not status: - raise serializers.ValidationError(_("流程节点(%s)不存在,请联系管理员.") % state_id) + raise serializers.ValidationError( + _("流程节点(%s)不存在,请联系管理员.") % state_id + ) if ticket.is_slave: raise serializers.ValidationError(_("抱歉,子单为只读状态,无法操作")) @@ -492,7 +522,9 @@ def terminate_validate(username, ticket, state_id, terminate_message): ) if not status.can_terminate: - raise serializers.ValidationError(_("指定流程节点【{}】不支持终止操作.").format(status.name)) + raise serializers.ValidationError( + _("指定流程节点【{}】不支持终止操作.").format(status.name) + ) if not status.can_operate(username): raise serializers.ValidationError( @@ -578,7 +610,9 @@ def ticket_operate_validate(fields, state_id, ticket, username): bk_biz_id = get_bk_biz_id(fields) if not status.can_first_state_operate(username, bk_biz_id): raise serializers.ValidationError( - _("【{}】没有任务【{}】的【提交】操作权限.").format(username, status.name) + _("【{}】没有任务【{}】的【提交】操作权限.").format( + username, status.name + ) ) elif not status.can_operate(username, TRANSITION_OPERATE): @@ -592,7 +626,9 @@ def ticket_status_validate(ticket, state_id): raise serializers.ValidationError(_("抱歉,子单为只读状态,无法操作")) if not ticket.status(state_id): - raise serializers.ValidationError(_("流程节点(%s)不存在,请联系管理员.") % state_id) + raise serializers.ValidationError( + _("流程节点(%s)不存在,请联系管理员.") % state_id + ) if ticket.current_status in TICKET_END_STATUS: raise serializers.ValidationError(_("单据状态为结束状态,无法继续转换")) diff --git a/itsm/ticket/views/field.py b/itsm/ticket/views/field.py index badddc524..832f309b0 100644 --- a/itsm/ticket/views/field.py +++ b/itsm/ticket/views/field.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.response import Response diff --git a/itsm/ticket/views/misc.py b/itsm/ticket/views/misc.py index d8ff2e23a..6c20268ed 100644 --- a/itsm/ticket/views/misc.py +++ b/itsm/ticket/views/misc.py @@ -25,7 +25,7 @@ from django.conf import settings -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from mako.template import Template from rest_framework import serializers from rest_framework.decorators import action @@ -108,7 +108,7 @@ class CommentViewSet(component_viewsets.NormalModelViewSet): "stars": ["exact"], } ordering_fields = "__all__" - + def list(self, request, *args, **kwargs): return Response() diff --git a/itsm/ticket/views/operational.py b/itsm/ticket/views/operational.py index e34723b62..22160b49d 100644 --- a/itsm/ticket/views/operational.py +++ b/itsm/ticket/views/operational.py @@ -28,7 +28,7 @@ from django.db import connection from django.db.models import Count, Q, Case, When, Max -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.generics import get_object_or_404 @@ -107,13 +107,15 @@ def overview_count(self, request, *args, **kwargs): return Response( { "count": project_analysis.get_ticket_count(), - "service_count": 1 - if service_id - else project_analysis.get_service_count(), + "service_count": ( + 1 if service_id else project_analysis.get_service_count() + ), "biz_count": project_analysis.get_biz_count(), - "user_count": project_analysis.get_ticket_user_count() - if service_id - else user_count(project_key=project_key), + "user_count": ( + project_analysis.get_ticket_user_count() + if service_id + else user_count(project_key=project_key) + ), } ) @@ -482,7 +484,7 @@ def distribute_statistics(self, request): filter_serializer = TicketOrganizationSerializer(data=request.query_params) filter_serializer.is_valid(raise_exception=True) kwargs = self.combine_date(filter_serializer.validated_data) - + level_dict = { 1: ("first_level_id", "first_level_name"), 2: ("second_level_id", "second_level_name"), @@ -531,9 +533,9 @@ def get_time_params(request, params_key="create_at"): return time_params except KeyError: raise ValidationError( - _("日期范围输入有误,请重新输入,例如:{}__gte=2019-01-01, {}__lte=2019-01-02").format( - params_key, params_key - ) + _( + "日期范围输入有误,请重新输入,例如:{}__gte=2019-01-01, {}__lte=2019-01-02" + ).format(params_key, params_key) ) @staticmethod @@ -562,7 +564,9 @@ def get_month_params(queryset, request): except ValueError: raise ValidationError( - _("日期范围输入有误,请重新输入,例如:create_at__gte=2019-01, create_at__lte=2019-02") + _( + "日期范围输入有误,请重新输入,例如:create_at__gte=2019-01, create_at__lte=2019-02" + ) ) # 左开右闭 @@ -1162,9 +1166,11 @@ def new_tickets(self, request, *args, **kwargs): filter_result.sort(key=lambda x: x["day"]) return Response(filter_result) - + @staticmethod def combine_date(kwargs): if kwargs.get("create_at__lte"): - kwargs["create_at__lte"] = datetime.combine(kwargs["create_at__lte"], time(23, 59, 59)) + kwargs["create_at__lte"] = datetime.combine( + kwargs["create_at__lte"], time(23, 59, 59) + ) return kwargs diff --git a/itsm/ticket/views/ticket.py b/itsm/ticket/views/ticket.py index 9eb37e208..b49010f9b 100644 --- a/itsm/ticket/views/ticket.py +++ b/itsm/ticket/views/ticket.py @@ -37,7 +37,7 @@ from django.db import connection, transaction from django.db.models import Count, Q from django.http import HttpResponse -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from mako.template import Template from rest_framework.decorators import action from rest_framework.exceptions import ValidationError @@ -81,7 +81,8 @@ OPEN, GENERAL, ORGANIZATION, - SUSPENDED, LEN_X_LONG, + SUSPENDED, + LEN_X_LONG, ) from itsm.component.constants.flow import EXPORT_SUPPORTED_TYPE from itsm.component.dlls.component import ComponentLibrary @@ -380,16 +381,22 @@ def create(self, request, *args, **kwargs): # creator(实际提单人)和updated_by在serializer.to_internal_value(data)中获取 instance = serializer.save(meta=meta) - logger.info(f"[TICKET] create ticket do_after_create begin ticket_id=>{instance.id}") - + logger.info( + f"[TICKET] create ticket do_after_create begin ticket_id=>{instance.id}" + ) + instance.do_after_create( request.data["fields"], request.data.get("from_ticket_id", None) ) - logger.info(f"[TICKET] create ticket do_after_create end ticket_id=>{instance.id}") - + logger.info( + f"[TICKET] create ticket do_after_create end ticket_id=>{instance.id}" + ) + start_pipeline.apply_async([instance]) - logger.info(f"[TICKET] create ticket start_pipeline end ticket_id=>{instance.id}") - + logger.info( + f"[TICKET] create ticket start_pipeline end ticket_id=>{instance.id}" + ) + return Response({"sn": instance.sn, "id": instance.id}, status=201) @action(detail=True, methods=["get"]) @@ -573,11 +580,13 @@ def send_sms(self, request, *args, **kwargs): return Response( { "result": len(fail_numbers) == 0, - "message": _("【{}】发送短信失败,请检查电话号码是否正确或联系管理员!").format( - ",".join(fail_numbers) - ) - if fail_numbers - else "success", + "message": ( + _( + "【{}】发送短信失败,请检查电话号码是否正确或联系管理员!" + ).format(",".join(fail_numbers)) + if fail_numbers + else "success" + ), "data": links, "code": "OK" if len(fail_numbers) == 0 else "SEND_SMS_FAILED", } @@ -639,7 +648,9 @@ def send_email(self, request, *args, **kwargs): return Response( { "result": False, - "message": _("【{}】发送邮件失败,请检查用户邮件配置是否正确或联系管理员!").format(receiver), + "message": _( + "【{}】发送邮件失败,请检查用户邮件配置是否正确或联系管理员!" + ).format(receiver), "data": ticket_url, "code": "SEND_EMAIL_FAILED", } @@ -670,9 +681,11 @@ def export_excel(self, request, *args, **kwargs): ticket_releate_fields = group_by(ticket_fields, ["ticket_id"], dict_result=True) field_filter_conditions = set( [ - "{}({})".format(field_obj["name"], field_obj["state_name"]) - if field_obj["state_name"] - else field_obj["name"] + ( + "{}({})".format(field_obj["name"], field_obj["state_name"]) + if field_obj["state_name"] + else field_obj["name"] + ) for field_obj in ticket_fields ] ) @@ -719,7 +732,11 @@ def export_group_by_service(self, request, *args, **kwargs): json.loads(base64.b64decode(service_fields)) if service_fields else {} ) except BaseException: - raise ValidationError(_("解析导出的提单字段异常:请检查请求参数内容,提单字段需要通过base64编码。")) + raise ValidationError( + _( + "解析导出的提单字段异常:请检查请求参数内容,提单字段需要通过base64编码。" + ) + ) # 获取字段的展示title,先将所有的字段混合起来 all_service_field_keys = [] @@ -779,7 +796,6 @@ def get_service_ticket_fields(): @staticmethod def generate_xls(head_fields, ticket_values_list, service_type): - """生成文档""" service_type_name = SERVICE_DICT.get(service_type) or "ALL" sheet_name = ( @@ -926,9 +942,9 @@ def proceed(self, request, *args, **kwargs): return Response( { - "code": ResponseCodeStatus.OK - if res.result - else ResponseCodeStatus.FAILED, + "code": ( + ResponseCodeStatus.OK if res.result else ResponseCodeStatus.FAILED + ), "message": res.message, "result": res.result, } @@ -953,9 +969,9 @@ def retry(self, request, *args, **kwargs): return Response( { - "code": ResponseCodeStatus.OK - if res.result - else ResponseCodeStatus.FAILED, + "code": ( + ResponseCodeStatus.OK if res.result else ResponseCodeStatus.FAILED + ), "message": res.message, "result": res.result, } @@ -984,9 +1000,9 @@ def ignore(self, request, *args, **kwargs): ) return Response( { - "code": ResponseCodeStatus.OK - if res.result - else ResponseCodeStatus.FAILED, + "code": ( + ResponseCodeStatus.OK if res.result else ResponseCodeStatus.FAILED + ), "message": res.message, "result": res.result, } @@ -1247,10 +1263,12 @@ def close(self, request, *args, **kwargs): close_status = request.data.get("current_status") if close_status not in ticket.status_instance.to_over_status_keys: raise ValidationError(_("设置的关闭状态不在正确状态范围之内")) - + desc = request.data.get("desc") or "" if len(desc) > LEN_X_LONG: - raise ValidationError(_("关单失败,原因描述超过 {len} 字符").format(len=LEN_X_LONG)) + raise ValidationError( + _("关单失败,原因描述超过 {len} 字符").format(len=LEN_X_LONG) + ) ticket.close( close_status=close_status, @@ -1447,8 +1465,7 @@ def states_response(self, ticket, request, detail=False): if many and detail: status = status.filter( - ~Q(status__in=["TERMINATED"]) - | Q(state_id=ticket.first_state_id) + ~Q(status__in=["TERMINATED"]) | Q(state_id=ticket.first_state_id) ) show_all_fields = many or status.status != "FINISHED" ticket_status = StatusSerializer( diff --git a/itsm/ticket/views/ticket_remark.py b/itsm/ticket/views/ticket_remark.py index 938e7adf2..c38871e57 100644 --- a/itsm/ticket/views/ticket_remark.py +++ b/itsm/ticket/views/ticket_remark.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from django.http import Http404 -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.response import Response @@ -48,14 +48,14 @@ def list(self, request, *args, **kwargs): return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) - + def retrieve(self, request, *args, **kwargs): try: return super().retrieve(request, *args, **kwargs) except Http404: """兼容父级评论删除情况""" return Response([]) - + @action(detail=False, methods=["get"]) def tree_view(self, request): """评论视图""" diff --git a/itsm/ticket_status/models.py b/itsm/ticket_status/models.py index a480a23c2..8a85cb8da 100644 --- a/itsm/ticket_status/models.py +++ b/itsm/ticket_status/models.py @@ -27,7 +27,7 @@ from datetime import datetime from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( BUILTIN_TICKET_STATUS, diff --git a/itsm/ticket_status/permissions.py b/itsm/ticket_status/permissions.py index 38eea93f1..2c534b17d 100644 --- a/itsm/ticket_status/permissions.py +++ b/itsm/ticket_status/permissions.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import permissions from itsm.component.drf.permissions import IamAuthPermit @@ -62,7 +62,7 @@ def has_permission(self, request, view): # 关联实例的请求,需要针对对象进行鉴权 if view.action in getattr(view, "permission_free_actions", []): return True - + if view.action in ["get_configs"]: apply_actions = ["ticket_state_view", "platform_manage_access"] elif view.action in ["overall_ticket_statuses", "list", "next_over_status"]: diff --git a/itsm/ticket_status/serializers.py b/itsm/ticket_status/serializers.py index 2f530307d..e2eb8ca59 100644 --- a/itsm/ticket_status/serializers.py +++ b/itsm/ticket_status/serializers.py @@ -23,11 +23,17 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.fields import empty -from itsm.component.constants import FIRST_ORDER, LEN_LONG, LEN_NORMAL, LEN_SHORT, PROCESS_RUNNING +from itsm.component.constants import ( + FIRST_ORDER, + LEN_LONG, + LEN_NORMAL, + LEN_SHORT, + PROCESS_RUNNING, +) from itsm.service.validators import service_type_validator from itsm.ticket_status.models import StatusTransit, TicketStatus, TicketStatusConfig from itsm.ticket_status.validators import TicketStatusValidator @@ -36,7 +42,15 @@ class TicketStatusConfigSerializer(serializers.ModelSerializer): class Meta: model = TicketStatusConfig - fields = ("id", "service_type", "service_type_name", "ticket_status", "updated_by", "update_at", "configured") + fields = ( + "id", + "service_type", + "service_type_name", + "ticket_status", + "updated_by", + "update_at", + "configured", + ) class TicketStatusOptionSerializer(serializers.Serializer): @@ -49,9 +63,18 @@ class TicketStatusSerializer(serializers.ModelSerializer): """单据状态序列化""" name = serializers.CharField( - required=True, max_length=LEN_LONG, error_messages={"blank": _("请输入状态名称"), "max_length": _("状态名称长度不能大于255个字符")} + required=True, + max_length=LEN_LONG, + error_messages={ + "blank": _("请输入状态名称"), + "max_length": _("状态名称长度不能大于255个字符"), + }, + ) + color_hex = serializers.CharField( + required=True, + max_length=LEN_SHORT, + error_messages={"blank": _("二进制颜色不能为空")}, ) - color_hex = serializers.CharField(required=True, max_length=LEN_SHORT, error_messages={"blank": _("二进制颜色不能为空")}) service_type = serializers.CharField( required=True, max_length=LEN_NORMAL, @@ -85,9 +108,13 @@ def create(self, validated_data): # 获取同一服务类型下的最大order数值 ticket_status = ( - TicketStatus.objects.filter(service_type=validated_data["service_type"]).order_by("order").last() + TicketStatus.objects.filter(service_type=validated_data["service_type"]) + .order_by("order") + .last() + ) + created_order = ( + ticket_status.order + FIRST_ORDER if ticket_status else FIRST_ORDER ) - created_order = ticket_status.order + FIRST_ORDER if ticket_status else FIRST_ORDER validated_data.update( order=created_order, key=TicketStatus.get_unique_key(validated_data["name"]), @@ -103,7 +130,13 @@ def create(self, validated_data): def update(self, instance, validated_data): """更新单据状态""" # 内置的单据状态 只有以下属性可以被编辑 - can_edited = ("is_start", "is_over", "name", "desc", "color_hex") + TicketStatus.DISPLAY_FIELDS + can_edited = ( + "is_start", + "is_over", + "name", + "desc", + "color_hex", + ) + TicketStatus.DISPLAY_FIELDS if instance.is_builtin and set(validated_data.keys()).difference(can_edited): raise serializers.ValidationError(_("抱歉,内置单据状态无法被编辑")) diff --git a/itsm/ticket_status/validators.py b/itsm/ticket_status/validators.py index adae9b0a0..4f538356a 100644 --- a/itsm/ticket_status/validators.py +++ b/itsm/ticket_status/validators.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from common.log import logger @@ -31,22 +31,37 @@ from itsm.ticket_status.models import TicketStatus -def save_ticket_status_validate(service_type, ticket_status_ids, start_status_id, over_status_ids): +def save_ticket_status_validate( + service_type, ticket_status_ids, start_status_id, over_status_ids +): """保存工单状态的校验""" service_type_validator(service_type) # 确保传入完整的工单状态ID用于排序 - status_ids = list(TicketStatus.objects.status_of_service_type(service_type).values_list("id", flat=True)) + status_ids = list( + TicketStatus.objects.status_of_service_type(service_type).values_list( + "id", flat=True + ) + ) if set(status_ids) != set(ticket_status_ids): - raise serializers.ValidationError(_("用于排序的工单状态参数不合法,请联系管理员")) + raise serializers.ValidationError( + _("用于排序的工单状态参数不合法,请联系管理员") + ) if start_status_id not in status_ids: - raise serializers.ValidationError(_("设置为起始状态的工单状态不存在,请联系管理员")) + raise serializers.ValidationError( + _("设置为起始状态的工单状态不存在,请联系管理员") + ) if set(over_status_ids).difference(status_ids): - logger.error("设置为结束状态的工作状态(id=%s)不存在" % set(over_status_ids).difference(status_ids)) - raise serializers.ValidationError(_("设置为结束状态的工作状态不存在,请联系管理员")) + logger.error( + "设置为结束状态的工作状态(id=%s)不存在" + % set(over_status_ids).difference(status_ids) + ) + raise serializers.ValidationError( + _("设置为结束状态的工作状态不存在,请联系管理员") + ) def save_transit_validate(service_type, transits): @@ -65,19 +80,24 @@ def save_transit_validate(service_type, transits): not_over_status_ids.append(s.id) for transit in transits: - if transit.get("from_status") not in all_status_ids or transit.get("to_status") not in all_status_ids: + if ( + transit.get("from_status") not in all_status_ids + or transit.get("to_status") not in all_status_ids + ): logger.error("单据状态ID(%s)不存在" % transit.get("from_status")) serializers.ValidationError(_("单据状态不存在,请联系管理员")) if transit.get("from_status") not in not_over_status_ids: - logger.error("单据状态ID(%s)为结束状态,无法继续转换" % transit.get("from_status")) + logger.error( + "单据状态ID(%s)为结束状态,无法继续转换" % transit.get("from_status") + ) serializers.ValidationError(_("单据状态为结束状态,无法继续转换")) def set_transit_rule_validate(from_status, to_status_id, threshold, threshold_unit): """设置状态转换规则""" - if threshold_unit not in ['m', 'h', 'd']: + if threshold_unit not in ["m", "h", "d"]: raise serializers.ValidationError(_("阈值单位错误,请重新输入")) if not str(threshold).isdigit(): @@ -94,7 +114,9 @@ def from_status_id_validator(from_status_id): if from_status_id is None: raise serializers.ValidationError(_("from_status_id不能为空")) if not TicketStatus.objects.filter(id=from_status_id).exists(): - raise serializers.ValidationError(_("id为【{}】的工单状态不存在,请联系管理员".format(from_status_id))) + raise serializers.ValidationError( + _("id为【{}】的工单状态不存在,请联系管理员".format(from_status_id)) + ) class TicketStatusValidator(object): @@ -109,9 +131,11 @@ def __call__(self, value): def name_unique_validate(self, value): """名称唯一性校验""" if self.instance: - obj = TicketStatus.objects.filter(service_type=self.instance.service_type).exclude(id=self.instance.id) + obj = TicketStatus.objects.filter( + service_type=self.instance.service_type + ).exclude(id=self.instance.id) else: - service_type = value.get('service_type') + service_type = value.get("service_type") obj = TicketStatus.objects.filter(service_type=service_type) - if obj.filter(name=value.get('name')).exists(): + if obj.filter(name=value.get("name")).exists(): raise serializers.ValidationError(_("状态名称已存在,请重新输入")) diff --git a/itsm/ticket_status/views.py b/itsm/ticket_status/views.py index ae1bdf5f6..ba7af4392 100644 --- a/itsm/ticket_status/views.py +++ b/itsm/ticket_status/views.py @@ -27,7 +27,7 @@ from django.db import transaction from django.db.models import Q -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django_bulk_update.helper import bulk_update from rest_framework.decorators import action from rest_framework.exceptions import ValidationError diff --git a/itsm/trigger/action/components/api.py b/itsm/trigger/action/components/api.py index 7ede25db4..056ebf090 100644 --- a/itsm/trigger/action/components/api.py +++ b/itsm/trigger/action/components/api.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.esb.backend_component import bk from itsm.postman.models import RemoteApi @@ -42,7 +42,9 @@ class ApiForms(BaseForm): api_source = ApiSourceField(name=_("API接口ID")) req_params = ApiInfoField(name=_("API请求参数配置")) response = JSONField( - name=_("API系统返回参数"), display=False, default={"ref_type": "reference", "value": "api_response_message"} + name=_("API系统返回参数"), + display=False, + default={"ref_type": "reference", "value": "api_response_message"}, ) @@ -53,10 +55,16 @@ class APIComponent(BaseComponent): form_class = ApiForms def get_api_source_config(self): - remote_api = RemoteApi._objects.get(id=self.data.get_one_of_inputs("api_source")) - api_config = remote_api.get_api_config(self.data.get_one_of_inputs("req_params")) + remote_api = RemoteApi._objects.get( + id=self.data.get_one_of_inputs("api_source") + ) + api_config = remote_api.get_api_config( + self.data.get_one_of_inputs("req_params") + ) # 默认API处理人的请求用户为触发事件的操作人 - api_config['query_params']['__remote_user__'] = self.context.get("operator", 'admin') + api_config["query_params"]["__remote_user__"] = self.context.get( + "operator", "admin" + ) return api_config def _execute(self): @@ -65,9 +73,9 @@ def _execute(self): """ api_config = self.get_api_source_config() rsp = bk.http(config=api_config) - self.data.set_outputs("api_response_message", rsp.get('message')) - self.data.set_outputs("api_response", rsp.get('message')) - if rsp['result']: + self.data.set_outputs("api_response_message", rsp.get("message")) + self.data.set_outputs("api_response", rsp.get("message")) + if rsp["result"]: return True - self.data.set_outputs("message", rsp['message']) + self.data.set_outputs("message", rsp["message"]) return False diff --git a/itsm/trigger/action/components/automatic_announcement.py b/itsm/trigger/action/components/automatic_announcement.py index 3a2925ac3..cd4a621a8 100644 --- a/itsm/trigger/action/components/automatic_announcement.py +++ b/itsm/trigger/action/components/automatic_announcement.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.utils import robot from itsm.trigger.action.core.component import BaseComponent @@ -42,7 +42,9 @@ class RobotForms(BaseForm): chat_id = StringField(name="Chat ID", required=False, tips="群ID, 多个用,区分") content = StringField(name=_("文本内容"), field_type="TEXT", required=True) mentioned_list = StringField( - name="需要提及的人的列表", tips="英文半角逗号分割的英文名列表,不填则默认@所有成员,None不@任何人", required=False + name="需要提及的人的列表", + tips="英文半角逗号分割的英文名列表,不填则默认@所有成员,None不@任何人", + required=False, ) diff --git a/itsm/trigger/action/components/modify_field.py b/itsm/trigger/action/components/modify_field.py index 213e98e8b..e864029f5 100644 --- a/itsm/trigger/action/components/modify_field.py +++ b/itsm/trigger/action/components/modify_field.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.trigger.action.core.component import BaseComponent from itsm.trigger.action.core import SelectField, CascadeField, BaseForm @@ -38,8 +38,15 @@ class FieldForms(BaseForm): 发送通知的输入数据格式 """ - field_key = SelectField(name=_("修改的字段"), source_type="RPC", source_uri="table_fields", use_variable=False) - field_value = CascadeField(name=_("设置的字段值"), source_type="FIELD", source_uri="field_key") + field_key = SelectField( + name=_("修改的字段"), + source_type="RPC", + source_uri="table_fields", + use_variable=False, + ) + field_value = CascadeField( + name=_("设置的字段值"), source_type="FIELD", source_uri="field_key" + ) class ModifyPublicFieldComponent(BaseComponent): @@ -53,7 +60,9 @@ def _execute(self): try: dst_ticket = Ticket.objects.get(sn=self.context.get("ticket_sn")) except Ticket.DoesNotExist: - self.data.set_outputs("message", _("对应的单据【%s】不存在") % self.context.get("ticket_sn")) + self.data.set_outputs( + "message", _("对应的单据【%s】不存在") % self.context.get("ticket_sn") + ) return False dst_ticket.refresh_from_db() dst_field_key = self.data.get_one_of_inputs("field_key") @@ -66,7 +75,7 @@ def _execute(self): self.data.set_outputs("field_key__display", first_field.name) self.data.set_outputs("field_value__display", first_field.display_value) - if dst_field_key in ['bk_biz_id', "current_status", "title"]: + if dst_field_key in ["bk_biz_id", "current_status", "title"]: setattr(dst_ticket, dst_field_key, dst_field_value) dst_ticket.save(update_fields=[dst_field_key]) return True diff --git a/itsm/trigger/action/components/modify_first_state_field.py b/itsm/trigger/action/components/modify_first_state_field.py index 536fd255d..ed32d3210 100644 --- a/itsm/trigger/action/components/modify_first_state_field.py +++ b/itsm/trigger/action/components/modify_first_state_field.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.trigger.action.core.component import BaseComponent from itsm.trigger.action.core import SelectField, CascadeField, BaseForm diff --git a/itsm/trigger/action/components/modify_processor.py b/itsm/trigger/action/components/modify_processor.py index 942726829..855af8397 100644 --- a/itsm/trigger/action/components/modify_processor.py +++ b/itsm/trigger/action/components/modify_processor.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import FLOW_SIGNAL, TRANSITION_SIGNAL, TASK_SIGNAL from itsm.trigger.action.core.component import BaseComponent @@ -63,8 +63,8 @@ def _execute(self): self.data.set_outputs("message", "设置处理人为空") return False - dst_state.processors_type = processors['member_type'] - dst_state.processors = processors['members'] + dst_state.processors_type = processors["member_type"] + dst_state.processors = processors["members"] dst_state.save(update_fields=["processors_type", "processors"]) dst_state.ticket.set_current_processors() @@ -78,6 +78,6 @@ def update_context(self): dst_state = Status.objects.get(id=self.context.get("dst_state")) except Status.DoesNotExist: return self.context - self.context.update(dst_state.ticket.get_output_fields(return_format='dict')) + self.context.update(dst_state.ticket.get_output_fields(return_format="dict")) self.validate_inputs() return self.context diff --git a/itsm/trigger/action/components/modify_specified_state_processor.py b/itsm/trigger/action/components/modify_specified_state_processor.py index 8e468fb05..24b1eaebd 100644 --- a/itsm/trigger/action/components/modify_specified_state_processor.py +++ b/itsm/trigger/action/components/modify_specified_state_processor.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import FLOW_STATES, TASK_SIGNAL from itsm.trigger.action.core.component import BaseComponent @@ -69,15 +69,21 @@ def _execute(self): states = self.data.get_one_of_inputs("states", []) - all_status = Status.objects.filter(ticket=ticket, status__in=Status.CAN_OPERATE_STATUS, state_id__in=states) + all_status = Status.objects.filter( + ticket=ticket, status__in=Status.CAN_OPERATE_STATUS, state_id__in=states + ) processors = self.data.get_one_of_inputs("processors") if not processors: self.data.set_outputs("message", "设置处理人为空") return False - all_status.update(processors_type=processors['member_type'], processors=processors["members"]) - self.data.set_outputs("states__display", ",".join(set(all_status.values_list("name", flat=True)))) + all_status.update( + processors_type=processors["member_type"], processors=processors["members"] + ) + self.data.set_outputs( + "states__display", ",".join(set(all_status.values_list("name", flat=True))) + ) ticket.set_current_processors() return True @@ -90,6 +96,6 @@ def update_context(self): dst_state = Status.objects.get(id=self.context.get("dst_state")) except Status.DoesNotExist: return self.context - self.context.update(dst_state.ticket.get_output_fields(return_format='dict')) + self.context.update(dst_state.ticket.get_output_fields(return_format="dict")) self.validate_inputs() return self.context diff --git a/itsm/trigger/action/components/modify_ticket_status.py b/itsm/trigger/action/components/modify_ticket_status.py index 7f443979b..460232d7b 100644 --- a/itsm/trigger/action/components/modify_ticket_status.py +++ b/itsm/trigger/action/components/modify_ticket_status.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import FIELD_STATUS from itsm.trigger.action.core.component import BaseComponent @@ -37,7 +37,9 @@ def get_ticket_status_names(): """Get all ticket status display name""" - status_names = TicketStatus.objects.get_overall_status_names(exclude_keys=["SUSPENDED"]) + status_names = TicketStatus.objects.get_overall_status_names( + exclude_keys=["SUSPENDED"] + ) return [{"key": key, "name": name} for key, name in status_names.items()] @@ -68,20 +70,27 @@ def _execute(self): dst_status = self.data.get_one_of_inputs("dst_status") print("dst_status is {}".format(dst_status)) # Whether follow status transit rule - from_status = TicketStatus.objects.get(service_type=dst_ticket.service_type, key=dst_ticket.current_status) - all_status_info = from_status.from_transits.values("to_status__key", "to_status__name") + from_status = TicketStatus.objects.get( + service_type=dst_ticket.service_type, key=dst_ticket.current_status + ) + all_status_info = from_status.from_transits.values( + "to_status__key", "to_status__name" + ) for to_status_info in all_status_info: - if to_status_info['to_status__key'] == dst_status: + if to_status_info["to_status__key"] == dst_status: # Update status from ticket dst_ticket.current_status = dst_status - dst_ticket.save(update_fields=['current_status']) + dst_ticket.save(update_fields=["current_status"]) # Update status from field dst_ticket.fields.filter(key=FIELD_STATUS).update(_value=dst_status) return True # TODO 更新错误信息 - self.data.set_outputs('message', _('工单状态无法更新为%s, 不满足状态流转规则, 请联系管理员! ' % dst_status)) + self.data.set_outputs( + "message", + _("工单状态无法更新为%s, 不满足状态流转规则, 请联系管理员! " % dst_status), + ) return False diff --git a/itsm/trigger/action/components/send_message.py b/itsm/trigger/action/components/send_message.py index fdd34a3ff..3607722e5 100644 --- a/itsm/trigger/action/components/send_message.py +++ b/itsm/trigger/action/components/send_message.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import WEIXIN, EMAIL, SMS from itsm.component.notify import ( diff --git a/itsm/trigger/action/components/unbind_parent_child_tickets.py b/itsm/trigger/action/components/unbind_parent_child_tickets.py index 96d41573b..c8dad72ca 100644 --- a/itsm/trigger/action/components/unbind_parent_child_tickets.py +++ b/itsm/trigger/action/components/unbind_parent_child_tickets.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import MASTER_SLAVE from itsm.trigger.action.core.component import BaseComponent @@ -59,18 +59,25 @@ def _execute(self): try: master_ticket = Ticket.objects.get(sn=self.context.get("ticket_sn")) except Ticket.DoesNotExist: - self.data.set_outputs("message", _("对应的单据【%s】不存在") % self.context.get("ticket_sn")) + self.data.set_outputs( + "message", _("对应的单据【%s】不存在") % self.context.get("ticket_sn") + ) return False slave_tickets = [ item.from_ticket - for item in TicketToTicket.objects.filter(related_type=MASTER_SLAVE, to_ticket=master_ticket) + for item in TicketToTicket.objects.filter( + related_type=MASTER_SLAVE, to_ticket=master_ticket + ) ] if not slave_tickets: - self.data.set_outputs("message", _("当前的单据【%s】不存在母子关联单") % self.context.get("ticket_sn")) + self.data.set_outputs( + "message", + _("当前的单据【%s】不存在母子关联单") % self.context.get("ticket_sn"), + ) return False - TicketToTicket.objects.filter(related_type=MASTER_SLAVE, to_ticket=master_ticket).update( - related_status="RUNNING" - ) + TicketToTicket.objects.filter( + related_type=MASTER_SLAVE, to_ticket=master_ticket + ).update(related_status="RUNNING") slave_tickets_sn = [] for slave_ticket in slave_tickets: clone_pipeline.apply_async(args=(slave_ticket, master_ticket)) diff --git a/itsm/trigger/action/components/update_ticket_status.py b/itsm/trigger/action/components/update_ticket_status.py index 7ae6b4734..f6f9401a1 100644 --- a/itsm/trigger/action/components/update_ticket_status.py +++ b/itsm/trigger/action/components/update_ticket_status.py @@ -24,7 +24,7 @@ """ from django import forms -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import FIELD_STATUS from itsm.component.dlls.component import BaseComponentForm @@ -38,7 +38,9 @@ def get_ticket_status_names(): """Get all ticket status display name""" - status_names = TicketStatus.objects.get_overall_status_names(exclude_keys=["SUSPENDED"]) + status_names = TicketStatus.objects.get_overall_status_names( + exclude_keys=["SUSPENDED"] + ) return [(key, name) for key, name in status_names.items()] @@ -49,7 +51,9 @@ class UpdateTicketStatus(BaseComponent): is_sub_class = True class Form(BaseComponentForm): - ticket_status_name = forms.ChoiceField(label=_("单据状态"), required=True, choices=get_ticket_status_names) + ticket_status_name = forms.ChoiceField( + label=_("单据状态"), required=True, choices=get_ticket_status_names + ) def clean(self): """Form data clean""" @@ -62,19 +66,29 @@ def _execute(self): ticket = Ticket.objects.get(id=ticket_id) # Whether follow status transit rule - from_status = TicketStatus.objects.get(service_type=ticket.service_type, key=ticket.current_status) - to_status_infos = from_status.from_transits.values("to_status__key", "to_status__name") + from_status = TicketStatus.objects.get( + service_type=ticket.service_type, key=ticket.current_status + ) + to_status_infos = from_status.from_transits.values( + "to_status__key", "to_status__name" + ) for to_status_info in to_status_infos: - if to_status_info['to_status__key'] == ticket_status_key: + if to_status_info["to_status__key"] == ticket_status_key: # Update status from ticket ticket.current_status = ticket_status_key - ticket.save(update_fields=['current_status']) + ticket.save(update_fields=["current_status"]) # Update status from field ticket.fields.filter(key=FIELD_STATUS).update(_value=ticket_status_key) return True - self.data.set_outputs('message', _('工单状态无法更新为%s, 不满足状态流转规则, 请联系管理员! ' % ticket_status_key)) + self.data.set_outputs( + "message", + _( + "工单状态无法更新为%s, 不满足状态流转规则, 请联系管理员! " + % ticket_status_key + ), + ) return False diff --git a/itsm/trigger/action/core/component.py b/itsm/trigger/action/core/component.py index 75409fa82..6a65c730e 100644 --- a/itsm/trigger/action/core/component.py +++ b/itsm/trigger/action/core/component.py @@ -24,7 +24,7 @@ """ import traceback -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.forms.fields import CallableChoiceIterator from django.forms.forms import DeclarativeFieldsMetaclass @@ -39,9 +39,9 @@ class BaseComponent(metaclass=BaseComponentMeta): # noqa Base class for component """ - name = 'UNKNOWN' # display name - code = 'unknown' # 在trigger组件里唯一 - type = 'trigger' + name = "UNKNOWN" # display name + code = "unknown" # 在trigger组件里唯一 + type = "trigger" Form = None is_async = True need_refresh = True @@ -53,7 +53,11 @@ def __init__(self, context, params_schema, action_id=None, countdown=0): self.params_schema = params_schema self.action_id = action_id self.countdown = countdown - self.form = self.form_class(self.params_schema, self.context) if self.form_class else None + self.form = ( + self.form_class(self.params_schema, self.context) + if self.form_class + else None + ) self.validate_inputs() def invoke(self, inputs): @@ -115,16 +119,18 @@ def get_inputs(cls): # Whether define form data if isinstance(cls.Form, DeclarativeFieldsMetaclass): for field_name, field in cls.Form.declared_fields.items(): - choices = getattr(field, "choices") if hasattr(field, "choices") else None + choices = ( + getattr(field, "choices") if hasattr(field, "choices") else None + ) if isinstance(choices, CallableChoiceIterator): choices = choices.choices_func() input_data = { - 'name': _(field_name), - 'label': _(field.label), - 'initial': field.initial, - 'required': field.required, - 'choices': choices, + "name": _(field_name), + "label": _(field.label), + "initial": field.initial, + "required": field.required, + "choices": choices, } inputs.append(input_data) diff --git a/itsm/trigger/action/core/field.py b/itsm/trigger/action/core/field.py index 60bd8868d..fb90bf559 100644 --- a/itsm/trigger/action/core/field.py +++ b/itsm/trigger/action/core/field.py @@ -28,14 +28,19 @@ import copy import datetime import re -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.core.exceptions import ValidationError from mako.template import Template from common.log import logger from itsm.role.models import UserRole from itsm.postman.models import RemoteApi -from itsm.component.constants import DEFAULT_BK_BIZ_ID, EMPTY_DICT, PROCESSOR_CHOICES, PERSON +from itsm.component.constants import ( + DEFAULT_BK_BIZ_ID, + EMPTY_DICT, + PROCESSOR_CHOICES, + PERSON, +) from itsm.component.utils.basic import list_by_separator, dotted_name, normal_name VAR_STR_MATCH = re.compile(r"\$\{\s*[\w\|]+\s*\}") @@ -47,7 +52,7 @@ class BaseField: """ enabled_field_types = [] - default_field_type = 'STRING' + default_field_type = "STRING" def __init__( self, @@ -64,7 +69,7 @@ def __init__( is_tips=False, tips="", ): - """ + """ { "type": "STRING|TEXT|SELECT|MULTISELECT|MEMBERS|", "choice": [], @@ -78,7 +83,7 @@ def __init__( # 代码规范兼容 if choice is None: choice = [] - + self.type = self.default_field_type self.choice = choice self.name = name @@ -112,7 +117,11 @@ def to_representation_data(self, value, context=None, **kwargs): return clean_data def set_field_type(self, field_type): - self.type = field_type if field_type in self.enabled_field_types else self.default_field_type + self.type = ( + field_type + if field_type in self.enabled_field_types + else self.default_field_type + ) def get_field_schema(self, key): return { @@ -135,7 +144,7 @@ def get_field_schema(self, key): class ApiSourceField(BaseField): enabled_field_types = ["API"] - default_field_type = 'API' + default_field_type = "API" def validate(self, value): """根据字段内容进行校验""" @@ -148,8 +157,8 @@ def validate(self, value): def to_internal_data(self, value, context=None, **kwargs): """ :param value: 输入值 - :param context: - :return: + :param context: + :return: """ parse_tool = ParamParseTool(context) return self.validate(parse_tool(param=value, **kwargs)) @@ -157,7 +166,7 @@ def to_internal_data(self, value, context=None, **kwargs): class ApiInfoField(BaseField): enabled_field_types = ["API_INFO"] - default_field_type = 'API_INFO' + default_field_type = "API_INFO" def validate(self, value): """根据字段内容进行校验""" @@ -167,9 +176,9 @@ def validate(self, value): def to_internal_data(self, value, context=None, **kwargs): """ 根据输入格式来对数据做渲染 - :param value: - :param context: - :return: + :param value: + :param context: + :return: """ parse_tool = ParamParseTool(context) @@ -212,11 +221,11 @@ def _list_clean(list_value): return clean_data def _datetime_clean(_value): - return _value.strftime('%Y-%m-%d %H:%M:%S') + return _value.strftime("%Y-%m-%d %H:%M:%S") def _date_clean(_value): - return _value.strftime('%Y-%m-%d') - + return _value.strftime("%Y-%m-%d") + def _leaf_clean(_value): result = self.validate(parse_tool(param=_value, **kwargs)) if isinstance(result, datetime.datetime): @@ -242,7 +251,7 @@ class MemberField(BaseField): """ enabled_field_types = ["MEMBERS", "MULTI_MEMBERS"] - default_field_type = 'MEMBERS' + default_field_type = "MEMBERS" def __init__(self, name, **kwargs): self.convert_to_users = kwargs.pop("convert_to_users", True) @@ -255,9 +264,9 @@ def validate(self): def to_internal_data(self, value, context=None, **kwargs): if self.convert_to_users: - return self._convert_to_users(value['value'], context, **kwargs) + return self._convert_to_users(value["value"], context, **kwargs) else: - members = self._direct_convert(value['value'], context, **kwargs) + members = self._direct_convert(value["value"], context, **kwargs) if kwargs.get("display"): return self.display_convert(members) @@ -271,10 +280,14 @@ def _convert_to_users(member_value, context, **kwargs): bk_biz_id = context.get("bk_biz_id", DEFAULT_BK_BIZ_ID) for member in copy.deepcopy(member_value): member["value"] = UserRole.get_users_by_type( - bk_biz_id, member['value']['member_type'], member['value']['members'] + bk_biz_id, member["value"]["member_type"], member["value"]["members"] ) parse_people = parse_tool(param=member) - members.extend(parse_people if isinstance(parse_people, list) else list_by_separator(parse_people)) + members.extend( + parse_people + if isinstance(parse_people, list) + else list_by_separator(parse_people) + ) return members def _direct_convert(self, member_value, context, **kwargs): @@ -282,14 +295,17 @@ def _direct_convert(self, member_value, context, **kwargs): members = [] parse_tool = ParamParseTool(context) for member in member_value: - parse_value = {"ref_type": member['ref_type'], "value": member['value']['members']} - member['value']['members'] = dotted_name(parse_tool(param=parse_value)) - if member['value']['member_type'] in ['VARIABLE', "EMPTY"]: - member['value']['member_type'] = 'PERSON' - if self.type == 'MEMBERS': - members = member['value'] + parse_value = { + "ref_type": member["ref_type"], + "value": member["value"]["members"], + } + member["value"]["members"] = dotted_name(parse_tool(param=parse_value)) + if member["value"]["member_type"] in ["VARIABLE", "EMPTY"]: + member["value"]["member_type"] = "PERSON" + if self.type == "MEMBERS": + members = member["value"] break - members.append(member['value']) + members.append(member["value"]) return members def display_convert(self, members): @@ -304,13 +320,15 @@ def display_convert(self, members): def get_members_display(member): members_type = dict(PROCESSOR_CHOICES) - if member['member_type'] == PERSON: - display_value = normal_name(member['members']) + if member["member_type"] == PERSON: + display_value = normal_name(member["members"]) else: display_value = ",".join( - UserRole.objects.filter(id__in=list_by_separator(member['members'])).values_list("name", flat=True) + UserRole.objects.filter( + id__in=list_by_separator(member["members"]) + ).values_list("name", flat=True) ) - return ("{}:{}").format(members_type.get(member['member_type']), display_value) + return ("{}:{}").format(members_type.get(member["member_type"]), display_value) class SelectField(BaseField): @@ -319,7 +337,7 @@ class SelectField(BaseField): """ enabled_field_types = ["SELECT", "RADIO"] - default_field_type = 'SELECT' + default_field_type = "SELECT" def validate(self): return True @@ -336,7 +354,7 @@ class MultiSelectField(BaseField): """多项选择字段""" enabled_field_types = ["MULTISELECT", "CHECKBOX"] - default_field_type = 'MULTISELECT' + default_field_type = "MULTISELECT" def validate(self): return True @@ -355,14 +373,14 @@ class StringField(BaseField): """ enabled_field_types = ["STRING", "TEXT"] - default_field_type = 'STRING' + default_field_type = "STRING" def validate(self): return True def to_internal_data(self, value, context=None, **kwargs): """ - { + { "key": "content", "value": "工单编号:${sn} 标题:${title}", "ref_type": "import" @@ -421,8 +439,8 @@ def to_internal_data(self, value, context=None, **kwargs): class SubComponentField(BaseField): """ - 时间格式 - """ + 时间格式 + """ enabled_field_types = ["SUBCOMPONENT"] default_field_type = "SUBCOMPONENT" @@ -449,22 +467,28 @@ def to_representation_data(self, value, context=None, sub_actions=None, flat=Fal clean_data = sub_action.to_representation_data() if flat: sub_component_data.extend(clean_data) - sub_component_data.append({"code": sub_action.code, "name": sub_action.name, "fields": clean_data}) + sub_component_data.append( + {"code": sub_action.code, "name": sub_action.name, "fields": clean_data} + ) return sub_component_data def get_field_schema(self, key): """ - 子响应函数组的格式返回 - :param key: - :return: - """ + 子响应函数组的格式返回 + :param key: + :return: + """ schema = super(SubComponentField, self).get_field_schema(key) - schema['sub_components'] = [] + schema["sub_components"] = [] for _component in self.sub_components: sub_component_field_schema = _component.get_inputs() - schema['sub_components'].append( - dict(key=_component.code, name=_component.name, field_schema=sub_component_field_schema) + schema["sub_components"].append( + dict( + key=_component.code, + name=_component.name, + field_schema=sub_component_field_schema, + ) ) return schema @@ -496,11 +520,13 @@ def __init__(self, context): def __call__(self, *args, **kwargs): param = kwargs.get("param", EMPTY_DICT) - parse_method = getattr(self, "{}_parse".format(param.get('ref_type', 'direct')), self.direct_parse) + parse_method = getattr( + self, "{}_parse".format(param.get("ref_type", "direct")), self.direct_parse + ) if parse_method is None: - return param['value'] + return param["value"] kwargs.update(param_key=param.get("key")) - return parse_method(param['value'], **kwargs) + return parse_method(param["value"], **kwargs) def import_parse(self, value, **kwargs): try: @@ -516,7 +542,11 @@ def reference_parse(self, value, **kwargs): display = kwargs.get("display", False) if len(reference_keys) == 1: # 只引用了一个参数的使用 - key = "{}__display".format(reference_keys[0]) if display else reference_keys[0] + key = ( + "{}__display".format(reference_keys[0]) + if display + else reference_keys[0] + ) return self.context.get(key) or self.context.get(reference_keys[0]) if display: return ",".join( @@ -526,7 +556,9 @@ def reference_parse(self, value, **kwargs): if key in self.context ] ) - return ",".join([self.context.get(key) for key in reference_keys if key in self.context]) + return ",".join( + [self.context.get(key) for key in reference_keys if key in self.context] + ) def direct_parse(self, value, **kwargs): if isinstance(value, str) and VAR_STR_MATCH.findall(value): @@ -534,5 +566,7 @@ def direct_parse(self, value, **kwargs): display = kwargs.get("display", False) if display: - return self.context.get("{}__display".format(kwargs.get("param_key"))) or value + return ( + self.context.get("{}__display".format(kwargs.get("param_key"))) or value + ) return value diff --git a/itsm/trigger/models/action.py b/itsm/trigger/models/action.py index b3dbe47ce..696b74071 100644 --- a/itsm/trigger/models/action.py +++ b/itsm/trigger/models/action.py @@ -29,7 +29,7 @@ import jsonfield from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from mako.template import Template from itsm.component.utils.client_backend_query import get_bk_users @@ -110,10 +110,14 @@ class Action(TriggerBaseModel): end_time = models.DateTimeField(_("任务结束事件"), null=True) operator = models.CharField(_("执行人"), max_length=LEN_NORMAL, default=SYS) ex_data = jsonfield.JSONField( - _("执行错误信息"), help_text=_("状态为失败的时候记录的错误日志"), default=EMPTY_DICT + _("执行错误信息"), + help_text=_("状态为失败的时候记录的错误日志"), + default=EMPTY_DICT, ) - params = jsonfield.JSONField(_("执行的参数"), help_text=_("手动触发器实际执行的参数信息"), default={}) + params = jsonfield.JSONField( + _("执行的参数"), help_text=_("手动触发器实际执行的参数信息"), default={} + ) objects = ActionManagers() diff --git a/itsm/trigger/models/base.py b/itsm/trigger/models/base.py index 87aa75301..51d82a0a2 100644 --- a/itsm/trigger/models/base.py +++ b/itsm/trigger/models/base.py @@ -24,7 +24,7 @@ """ from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.db import managers from itsm.component.constants import LEN_NORMAL diff --git a/itsm/trigger/models/trigger.py b/itsm/trigger/models/trigger.py index 0bcc37585..fb3bd4d23 100644 --- a/itsm/trigger/models/trigger.py +++ b/itsm/trigger/models/trigger.py @@ -25,7 +25,7 @@ import datetime from itertools import chain from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ import jsonfield from itsm.component.dlls.component import ComponentLibrary @@ -77,8 +77,12 @@ class ActionSchema(TriggerBaseModel): ) can_repeat = models.BooleanField(_("是否可以重复执行"), default=False) - params = jsonfield.JSONField(_("配置参数"), help_text=_("当前响应事件配置的参数模版"), default={}) - inputs = jsonfield.JSONField(_("输入参数"), help_text=_("输入参数引用的参数变量"), default={}) + params = jsonfield.JSONField( + _("配置参数"), help_text=_("当前响应事件配置的参数模版"), default={} + ) + inputs = jsonfield.JSONField( + _("输入参数"), help_text=_("输入参数引用的参数变量"), default={} + ) class Meta: verbose_name = _("响应动作参数配置表") @@ -135,7 +139,9 @@ class Trigger(TriggerBaseModel): signal = models.CharField(_("触发事件信号"), null=False, max_length=LEN_MIDDLE) sender = models.CharField( - _("触发对象"), help_text=_("一般为触发该信号的实际对象模型id"), max_length=LEN_NORMAL + _("触发对象"), + help_text=_("一般为触发该信号的实际对象模型id"), + max_length=LEN_NORMAL, ) # inputs 好像暂时没需要 @@ -157,7 +163,10 @@ class Trigger(TriggerBaseModel): is_draft = models.BooleanField(_("是否为草稿"), default=True) is_enabled = models.BooleanField(_("是否可启用"), default=False) icon = models.CharField( - _("对应的icon"), default=EMPTY_STRING, choices=TRIGGER_ICON_CHOICE, max_length=64 + _("对应的icon"), + default=EMPTY_STRING, + choices=TRIGGER_ICON_CHOICE, + max_length=64, ) project_key = models.CharField( _("项目key"), max_length=LEN_SHORT, null=False, default=0 @@ -179,17 +188,19 @@ def __unicode__(self): def trigger_rules(self, source_type, source_id): return [ { - "conditions": item.condition - if item.by_condition - else { - "all": [ - { - "name": "constant_bool_true", - "operator": "is_true", - "value": True, - } - ] - }, + "conditions": ( + item.condition + if item.by_condition + else { + "all": [ + { + "name": "constant_bool_true", + "operator": "is_true", + "value": True, + } + ] + } + ), "actions": [ { "name": "trigger_handle", diff --git a/itsm/trigger/signal/signals.py b/itsm/trigger/signal/signals.py index c88cd4d3f..020fe6c0e 100644 --- a/itsm/trigger/signal/signals.py +++ b/itsm/trigger/signal/signals.py @@ -31,5 +31,5 @@ # 统一用一个信号来接收,然后统一分配具体的事项 trigger_signal = TriggerSignal() -action_finish = Signal(providing_args=("action_id", "result", "error_message")) -post_action_finish = Signal(providing_args=("instance",)) +action_finish = Signal() # providing_args=("action_id", "result", "error_message") +post_action_finish = Signal() # providing_args=("instance",) diff --git a/itsm/trigger/tasks.py b/itsm/trigger/tasks.py index 43534ffc3..3096568f4 100644 --- a/itsm/trigger/tasks.py +++ b/itsm/trigger/tasks.py @@ -27,9 +27,9 @@ __copyright__ = "Copyright © 2012-2020 Tencent BlueKing. All Rights Reserved." -from celery import task +from celery import shared_task -@task +@shared_task def async_execute_action(action): action.execute() diff --git a/itsm/trigger/urls.py b/itsm/trigger/urls.py index 69aace4e5..6b9d3342d 100644 --- a/itsm/trigger/urls.py +++ b/itsm/trigger/urls.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.conf.urls import url +from django.urls import re_path from rest_framework.routers import DefaultRouter from itsm.trigger.views import ( @@ -41,4 +41,4 @@ routers.register(r"actions", ActionViewSet, basename="actions") -urlpatterns = routers.urls + [url(r'^components/$', ComponentApiViewSet.as_view())] +urlpatterns = routers.urls + [re_path(r"^components/$", ComponentApiViewSet.as_view())] diff --git a/itsm/trigger/validators.py b/itsm/trigger/validators.py index 1cce0d16d..a4e20805f 100644 --- a/itsm/trigger/validators.py +++ b/itsm/trigger/validators.py @@ -24,7 +24,7 @@ """ from common.log import logger -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.drf.exception import ValidationError from itsm.component.exceptions import ComponentNotExist from itsm.trigger.models import Trigger, ActionSchema @@ -40,8 +40,8 @@ def __init__(self, instance=None): def __call__(self, value): if not self.instance: - self.source_id = value['source_id'] - self.source_type = value['source_type'] + self.source_id = value["source_id"] + self.source_type = value["source_type"] self.name_validate(value) def name_validate(self, value): @@ -50,7 +50,7 @@ def name_validate(self, value): 统一类型统一来源对象的name必须唯一 """ trigger_query_set = Trigger.objects.filter( - name=value['name'], source_type=self.source_type, source_id=self.source_id + name=value["name"], source_type=self.source_type, source_id=self.source_id ) if self.instance and trigger_query_set.exclude(id=self.instance.id).exists(): raise ValidationError(_("存在其他相同名称的触发器,请修改后再提交")) @@ -70,16 +70,20 @@ def name_validate(rules): 名称的唯一性校验 同一个触发器下面如果名称不为空,则不允许相同 """ - all_name = [rule['name'] for rule in rules if rule.get("name")] + all_name = [rule["name"] for rule in rules if rule.get("name")] if len(set(all_name)) < len(all_name): raise ValidationError(_("同一个触发器下的规则名称不能重复")) @staticmethod def action_schemas_exist_validate(rules): for index, rule in enumerate(rules): - action_schemas = rule['action_schemas'] - if ActionSchema.objects.filter(id__in=action_schemas).count() < len(action_schemas): - raise ValidationError(_("第{}个规则下的响应事件部分不存在").format(index + 1)) + action_schemas = rule["action_schemas"] + if ActionSchema.objects.filter(id__in=action_schemas).count() < len( + action_schemas + ): + raise ValidationError( + _("第{}个规则下的响应事件部分不存在").format(index + 1) + ) class ActionSchemaValidator: @@ -95,11 +99,17 @@ def __call__(self, value): def component_validate(self, value): try: - component_class = ComponentLibrary.get_component_class("trigger", component_code=value["component_type"]) + component_class = ComponentLibrary.get_component_class( + "trigger", component_code=value["component_type"] + ) except ComponentNotExist: raise ValidationError("非法的组件类型,请确认组件是否选择正确") except BaseException as error: - logger.exception("校验错误,instance id {}".format(self.instance.id if self.instance else "None")) + logger.exception( + "校验错误,instance id {}".format( + self.instance.id if self.instance else "None" + ) + ) raise ValidationError("组件异常错误:{}".format(str(error))) - component_class(value['params']).validate_params() + component_class(value["params"]).validate_params() diff --git a/itsm/trigger/views.py b/itsm/trigger/views.py index ffd1cf7e2..b454f857c 100644 --- a/itsm/trigger/views.py +++ b/itsm/trigger/views.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.db import transaction from rest_framework import status from rest_framework.response import Response @@ -239,7 +239,7 @@ def _single_update(action_data, instance): if not rule.action_schemas: continue actions_schemas.extend(rule.action_schemas) - + schemas = [] with transaction.atomic(): for _data in request.data: diff --git a/itsm/workflow/backend.py b/itsm/workflow/backend.py index eeafbb1a6..8c17d7b0b 100644 --- a/itsm/workflow/backend.py +++ b/itsm/workflow/backend.py @@ -34,7 +34,7 @@ import time import six -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( COVERAGE_STATE, @@ -102,7 +102,9 @@ def build_tree_exception_handler(self, state): except Exception as error: # 记录代码底层报错 logger.exception(error) - raise WorkFlowInvalidError([state["id"]], _("当前节点画布连线不合理, 请重新确认")) + raise WorkFlowInvalidError( + [state["id"]], _("当前节点画布连线不合理, 请重新确认") + ) def _unpack_state(self, states, state_id): """状态提取""" @@ -387,7 +389,9 @@ def build_conditions_for_gateway(self, outgoings): evaluation = "1==1" else: if exp.type not in SUPPORTED_TYPE: - raise NotImplementedError(_("不支持的数据类型 %s") % exp.type) + raise NotImplementedError( + _("不支持的数据类型 %s") % exp.type + ) template = get_exp_template(exp.type) value = format_exp_value(exp.type, exp.value) evaluation = template.format( diff --git a/itsm/workflow/migrations/0025_auto_20200403_1739.py b/itsm/workflow/migrations/0025_auto_20200403_1739.py index 5eb11773d..6a3be2ef8 100644 --- a/itsm/workflow/migrations/0025_auto_20200403_1739.py +++ b/itsm/workflow/migrations/0025_auto_20200403_1739.py @@ -32,138 +32,157 @@ class Migration(migrations.Migration): dependencies = [ - ('workflow', '0024_auto_20200310_1709'), + ("workflow", "0024_auto_20200310_1709"), ] operations = [ migrations.AlterField( - model_name='defaultfield', - name='source_type', + model_name="defaultfield", + name="source_type", field=models.CharField( - choices=[('CUSTOM', '自定义数据'), ('API', '接口数据'), ('DATADICT', '数据字典'), ('RPC', '系统数据')], - default='CUSTOM', + choices=[ + ("CUSTOM", "自定义数据"), + ("API", "接口数据"), + ("DATADICT", "数据字典"), + ("RPC", "系统数据"), + ], + default="CUSTOM", max_length=32, - verbose_name='数据来源类型', + verbose_name="数据来源类型", ), ), migrations.AlterField( - model_name='defaultfield', - name='type', + model_name="defaultfield", + name="type", field=models.CharField( choices=[ - ('STRING', '单行文本'), - ('TEXT', '多行文本'), - ('INT', '数字'), - ('DATE', '日期'), - ('DATETIME', '时间'), - ('DATETIMERANGE', '时间间隔'), - ('TABLE', '表格'), - ('SELECT', '单选下拉框'), - ('MULTISELECT', '多选下拉框'), - ('CHECKBOX', '复选框'), - ('RADIO', '单选框'), - ('MEMBER', '单选人员选择'), - ('MEMBERS', '多选人员选择'), - ('RICHTEXT', '富文本'), - ('FILE', '附件上传'), - ('CUSTOMTABLE', '自定义表格'), - ('TREESELECT', '树形选择'), - ('LINK', '链接'), - ('CASCADE', '级联'), + ("STRING", "单行文本"), + ("TEXT", "多行文本"), + ("INT", "数字"), + ("DATE", "日期"), + ("DATETIME", "时间"), + ("DATETIMERANGE", "时间间隔"), + ("TABLE", "表格"), + ("SELECT", "单选下拉框"), + ("MULTISELECT", "多选下拉框"), + ("CHECKBOX", "复选框"), + ("RADIO", "单选框"), + ("MEMBER", "单选人员选择"), + ("MEMBERS", "多选人员选择"), + ("RICHTEXT", "富文本"), + ("FILE", "附件上传"), + ("CUSTOMTABLE", "自定义表格"), + ("TREESELECT", "树形选择"), + ("LINK", "链接"), + ("CASCADE", "级联"), ], - default='STRING', + default="STRING", max_length=32, - verbose_name='字段类型', + verbose_name="字段类型", ), ), migrations.AlterField( - model_name='field', - name='source_type', + model_name="field", + name="source_type", field=models.CharField( - choices=[('CUSTOM', '自定义数据'), ('API', '接口数据'), ('DATADICT', '数据字典'), ('RPC', '系统数据')], - default='CUSTOM', + choices=[ + ("CUSTOM", "自定义数据"), + ("API", "接口数据"), + ("DATADICT", "数据字典"), + ("RPC", "系统数据"), + ], + default="CUSTOM", max_length=32, - verbose_name='数据来源类型', + verbose_name="数据来源类型", ), ), migrations.AlterField( - model_name='field', - name='type', + model_name="field", + name="type", field=models.CharField( choices=[ - ('STRING', '单行文本'), - ('TEXT', '多行文本'), - ('INT', '数字'), - ('DATE', '日期'), - ('DATETIME', '时间'), - ('DATETIMERANGE', '时间间隔'), - ('TABLE', '表格'), - ('SELECT', '单选下拉框'), - ('MULTISELECT', '多选下拉框'), - ('CHECKBOX', '复选框'), - ('RADIO', '单选框'), - ('MEMBER', '单选人员选择'), - ('MEMBERS', '多选人员选择'), - ('RICHTEXT', '富文本'), - ('FILE', '附件上传'), - ('CUSTOMTABLE', '自定义表格'), - ('TREESELECT', '树形选择'), - ('LINK', '链接'), - ('CASCADE', '级联'), + ("STRING", "单行文本"), + ("TEXT", "多行文本"), + ("INT", "数字"), + ("DATE", "日期"), + ("DATETIME", "时间"), + ("DATETIMERANGE", "时间间隔"), + ("TABLE", "表格"), + ("SELECT", "单选下拉框"), + ("MULTISELECT", "多选下拉框"), + ("CHECKBOX", "复选框"), + ("RADIO", "单选框"), + ("MEMBER", "单选人员选择"), + ("MEMBERS", "多选人员选择"), + ("RICHTEXT", "富文本"), + ("FILE", "附件上传"), + ("CUSTOMTABLE", "自定义表格"), + ("TREESELECT", "树形选择"), + ("LINK", "链接"), + ("CASCADE", "级联"), ], - default='STRING', + default="STRING", max_length=32, - verbose_name='字段类型', + verbose_name="字段类型", ), ), migrations.AlterField( - model_name='templatefield', - name='source_type', + model_name="templatefield", + name="source_type", field=models.CharField( - choices=[('CUSTOM', '自定义数据'), ('API', '接口数据'), ('DATADICT', '数据字典'), ('RPC', '系统数据')], - default='CUSTOM', + choices=[ + ("CUSTOM", "自定义数据"), + ("API", "接口数据"), + ("DATADICT", "数据字典"), + ("RPC", "系统数据"), + ], + default="CUSTOM", max_length=32, - verbose_name='数据来源类型', + verbose_name="数据来源类型", ), ), migrations.AlterField( - model_name='templatefield', - name='type', + model_name="templatefield", + name="type", field=models.CharField( choices=[ - ('STRING', '单行文本'), - ('TEXT', '多行文本'), - ('INT', '数字'), - ('DATE', '日期'), - ('DATETIME', '时间'), - ('DATETIMERANGE', '时间间隔'), - ('TABLE', '表格'), - ('SELECT', '单选下拉框'), - ('MULTISELECT', '多选下拉框'), - ('CHECKBOX', '复选框'), - ('RADIO', '单选框'), - ('MEMBER', '单选人员选择'), - ('MEMBERS', '多选人员选择'), - ('RICHTEXT', '富文本'), - ('FILE', '附件上传'), - ('CUSTOMTABLE', '自定义表格'), - ('TREESELECT', '树形选择'), - ('LINK', '链接'), - ('CASCADE', '级联'), + ("STRING", "单行文本"), + ("TEXT", "多行文本"), + ("INT", "数字"), + ("DATE", "日期"), + ("DATETIME", "时间"), + ("DATETIMERANGE", "时间间隔"), + ("TABLE", "表格"), + ("SELECT", "单选下拉框"), + ("MULTISELECT", "多选下拉框"), + ("CHECKBOX", "复选框"), + ("RADIO", "单选框"), + ("MEMBER", "单选人员选择"), + ("MEMBERS", "多选人员选择"), + ("RICHTEXT", "富文本"), + ("FILE", "附件上传"), + ("CUSTOMTABLE", "自定义表格"), + ("TREESELECT", "树形选择"), + ("LINK", "链接"), + ("CASCADE", "级联"), ], - default='STRING', + default="STRING", max_length=32, - verbose_name='字段类型', + verbose_name="字段类型", ), ), migrations.AlterField( - model_name='workflow', - name='is_task_needed', - field=models.NullBooleanField(default=False, verbose_name='是否需要关联子任务'), + model_name="workflow", + name="is_task_needed", + field=models.BooleanField( + default=False, verbose_name="是否需要关联子任务", null=True + ), ), migrations.AlterField( - model_name='workflowversion', - name='is_task_needed', - field=models.NullBooleanField(default=False, verbose_name='是否需要关联子任务'), + model_name="workflowversion", + name="is_task_needed", + field=models.BooleanField( + default=False, verbose_name="是否需要关联子任务", null=True + ), ), ] diff --git a/itsm/workflow/models/base.py b/itsm/workflow/models/base.py index 39e18ab13..b5a081d41 100644 --- a/itsm/workflow/models/base.py +++ b/itsm/workflow/models/base.py @@ -24,7 +24,7 @@ """ from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import LEN_NORMAL from itsm.workflow import managers @@ -33,12 +33,16 @@ class Model(models.Model): """基础字段""" - FIELDS = ('creator', 'create_at', 'updated_by', 'update_at', 'end_at') + FIELDS = ("creator", "create_at", "updated_by", "update_at", "end_at") - creator = models.CharField(_("创建人"), max_length=LEN_NORMAL, null=True, blank=True) + creator = models.CharField( + _("创建人"), max_length=LEN_NORMAL, null=True, blank=True + ) create_at = models.DateTimeField(_("创建时间"), auto_now_add=True) update_at = models.DateTimeField(_("更新时间"), auto_now=True) - updated_by = models.CharField(_("修改人"), max_length=LEN_NORMAL, null=True, blank=True) + updated_by = models.CharField( + _("修改人"), max_length=LEN_NORMAL, null=True, blank=True + ) end_at = models.DateTimeField(_("结束时间"), null=True, blank=True) is_deleted = models.BooleanField(_("是否软删除"), default=False, db_index=True) @@ -49,7 +53,7 @@ class Model(models.Model): resource_operations = ["system_settings_manage"] class Meta: - app_label = 'workflow' + app_label = "workflow" abstract = True def delete(self, using=None): diff --git a/itsm/workflow/models/common.py b/itsm/workflow/models/common.py index e68a82c6a..44795bedf 100644 --- a/itsm/workflow/models/common.py +++ b/itsm/workflow/models/common.py @@ -26,7 +26,7 @@ import jsonfield from django.conf import settings from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( EMPTY_DICT, @@ -49,7 +49,10 @@ class Notify(models.Model): is_builtin = models.BooleanField(_("是否为系统内置"), default=False) type = models.CharField(_("通知渠道"), max_length=LEN_SHORT, default="EMAIL") template = models.TextField( - _("通知模板:可使用变量如下:xxx(TODO)"), default=EMPTY_STRING, null=True, blank=True + _("通知模板:可使用变量如下:xxx(TODO)"), + default=EMPTY_STRING, + null=True, + blank=True, ) class Meta: diff --git a/itsm/workflow/models/deprecated.py b/itsm/workflow/models/deprecated.py index eb3d6847e..6df81a8ed 100644 --- a/itsm/workflow/models/deprecated.py +++ b/itsm/workflow/models/deprecated.py @@ -25,7 +25,7 @@ import jsonfield from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( DEFAULT_STRING, @@ -43,28 +43,40 @@ class WorkflowSnap(models.Model): """实例化工作流 - 废弃:迁移至WorkflowVersion + 废弃:迁移至WorkflowVersion """ snapshot_time = models.DateTimeField(_("快照创建时间"), auto_now_add=True) workflow_id = models.IntegerField(_("工作流ID")) - fields = jsonfield.JSONField(_("字段快照字典"), default=EMPTY_DICT, null=True, blank=True) - states = jsonfield.JSONField(_("状态快照字典"), default=EMPTY_DICT, null=True, blank=True) - transitions = jsonfield.JSONField(_("流转快照字典"), default=EMPTY_DICT, null=True, blank=True) + fields = jsonfield.JSONField( + _("字段快照字典"), default=EMPTY_DICT, null=True, blank=True + ) + states = jsonfield.JSONField( + _("状态快照字典"), default=EMPTY_DICT, null=True, blank=True + ) + transitions = jsonfield.JSONField( + _("流转快照字典"), default=EMPTY_DICT, null=True, blank=True + ) # 记录主分支数据 - master = jsonfield.JSONField(_("主分支列表"), default=EMPTY_LIST, null=True, blank=True) - - notify = models.ManyToManyField('workflow.Notify', help_text=_("可关联多种通知方式")) - notify_rule = models.CharField(_("通知规则"), max_length=LEN_SHORT, choices=NOTIFY_RULE_CHOICES, default="NONE") + master = jsonfield.JSONField( + _("主分支列表"), default=EMPTY_LIST, null=True, blank=True + ) + + notify = models.ManyToManyField( + "workflow.Notify", help_text=_("可关联多种通知方式") + ) + notify_rule = models.CharField( + _("通知规则"), max_length=LEN_SHORT, choices=NOTIFY_RULE_CHOICES, default="NONE" + ) notify_freq = models.IntegerField(_("重试间隔(s)"), default=EMPTY_INT) objects = managers.WorkflowSnapManager() class Meta: - app_label = 'workflow' + app_label = "workflow" verbose_name = _("工作流快照") verbose_name_plural = _("工作流快照") @@ -75,8 +87,12 @@ def __unicode__(self): class DefaultField(BaseField): """初始环节内置字段表: deprecated""" - flow_type = models.CharField(_("流程分类"), max_length=LEN_NORMAL, default=DEFAULT_STRING) - category = models.CharField(_("字段归类,面向业务逻辑,比如服务类型(change|event)"), max_length=LEN_MIDDLE) + flow_type = models.CharField( + _("流程分类"), max_length=LEN_NORMAL, default=DEFAULT_STRING + ) + category = models.CharField( + _("字段归类,面向业务逻辑,比如服务类型(change|event)"), max_length=LEN_MIDDLE + ) objects = models.Manager() diff --git a/itsm/workflow/models/event.py b/itsm/workflow/models/event.py index dbbea039c..5c2112b98 100644 --- a/itsm/workflow/models/event.py +++ b/itsm/workflow/models/event.py @@ -25,7 +25,7 @@ import jsonfield from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( EMPTY_DICT, @@ -47,19 +47,40 @@ class Event(models.Model): from_state_id = models.IntegerField(_("当前状态ID")) transition_id = models.IntegerField(_("流转ID"), null=True, blank=True) to_state_id = models.IntegerField(_("下一个状态ID"), null=True, blank=True) - type = models.CharField(_("流转事件类型"), max_length=LEN_SHORT, choices=OPERATE_CHOICES, default=TRANSITION_OPERATE) - processors_type = models.CharField(_("处理人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="OPEN") - processors = models.CharField(_("处理人列表"), max_length=LEN_LONG, default=EMPTY_STRING, null=True, blank=True) - form_data = jsonfield.JSONField(_("表单快照字典"), default=EMPTY_DICT, null=True, blank=True) + type = models.CharField( + _("流转事件类型"), + max_length=LEN_SHORT, + choices=OPERATE_CHOICES, + default=TRANSITION_OPERATE, + ) + processors_type = models.CharField( + _("处理人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="OPEN" + ) + processors = models.CharField( + _("处理人列表"), + max_length=LEN_LONG, + default=EMPTY_STRING, + null=True, + blank=True, + ) + form_data = jsonfield.JSONField( + _("表单快照字典"), default=EMPTY_DICT, null=True, blank=True + ) operate_at = models.DateTimeField(_("操作时间"), auto_now_add=True) - operator = models.CharField(_("操作人"), max_length=LEN_NORMAL, null=True, blank=True) - message = models.CharField(_("日志概述"), max_length=LEN_X_LONG, null=True, blank=True) + operator = models.CharField( + _("操作人"), max_length=LEN_NORMAL, null=True, blank=True + ) + message = models.CharField( + _("日志概述"), max_length=LEN_X_LONG, null=True, blank=True + ) is_deleted = models.BooleanField(_("是否软删除"), default=False, db_index=True) # 新增 action = models.CharField(_("动作"), max_length=LEN_NORMAL, blank=True) - detail_message = models.CharField(_("详细信息"), max_length=LEN_X_LONG, null=True, blank=True) + detail_message = models.CharField( + _("详细信息"), max_length=LEN_X_LONG, null=True, blank=True + ) from_state_name = models.CharField(_("任务name"), max_length=LEN_NORMAL, blank=True) _objects = models.Manager() diff --git a/itsm/workflow/models/field.py b/itsm/workflow/models/field.py index b3cf6702d..ca2aba7cf 100644 --- a/itsm/workflow/models/field.py +++ b/itsm/workflow/models/field.py @@ -28,7 +28,7 @@ import jsonfield from django.db import models from django.forms import model_to_dict -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( BASE_MODEL, @@ -73,33 +73,51 @@ class BaseField(Model): is_valid = models.BooleanField(_("是否生效"), default=True) display = models.BooleanField(_("是否显示在单据列表中"), default=False) - source_type = models.CharField(_("数据来源类型"), max_length=LEN_SHORT, choices=SOURCE_CHOICES, - default="CUSTOM") - source_uri = models.CharField(_("接口uri"), max_length=LEN_LONG, default=EMPTY_STRING, null=True, - blank=True) - api_instance_id = models.IntegerField(_('api实例主键'), default=0, null=True, blank=True) - kv_relation = jsonfield.JSONCharField(_("源数据的kv关系配置"), default=EMPTY_DICT, - max_length=LEN_NORMAL) - type = models.CharField(_("字段类型"), max_length=LEN_SHORT, choices=TYPE_CHOICES, default="STRING") + source_type = models.CharField( + _("数据来源类型"), + max_length=LEN_SHORT, + choices=SOURCE_CHOICES, + default="CUSTOM", + ) + source_uri = models.CharField( + _("接口uri"), max_length=LEN_LONG, default=EMPTY_STRING, null=True, blank=True + ) + api_instance_id = models.IntegerField( + _("api实例主键"), default=0, null=True, blank=True + ) + kv_relation = jsonfield.JSONCharField( + _("源数据的kv关系配置"), default=EMPTY_DICT, max_length=LEN_NORMAL + ) + type = models.CharField( + _("字段类型"), max_length=LEN_SHORT, choices=TYPE_CHOICES, default="STRING" + ) key = models.CharField(_("字段标识"), max_length=LEN_LONG) name = models.CharField(_("字段名"), max_length=LEN_NORMAL) - layout = models.CharField(_("布局"), max_length=LEN_SHORT, choices=LAYOUT_CHOICES, - default="COL_6") + layout = models.CharField( + _("布局"), max_length=LEN_SHORT, choices=LAYOUT_CHOICES, default="COL_6" + ) - validate_type = models.CharField(_("校验规则"), max_length=LEN_SHORT, choices=VALIDATE_CHOICES, - default="REQUIRE") + validate_type = models.CharField( + _("校验规则"), max_length=LEN_SHORT, choices=VALIDATE_CHOICES, default="REQUIRE" + ) show_type = models.IntegerField( - _('显示条件类型'), choices=[(SHOW_DIRECTLY, '直接显示'), (SHOW_BY_CONDITION, '根据条件判断')], - default=SHOW_DIRECTLY + _("显示条件类型"), + choices=[(SHOW_DIRECTLY, "直接显示"), (SHOW_BY_CONDITION, "根据条件判断")], + default=SHOW_DIRECTLY, ) show_conditions = jsonfield.JSONField(_("字段的显示条件"), default=EMPTY_DICT) - regex = models.CharField(_("正则校验规则关键字"), max_length=LEN_NORMAL, default=EMPTY_STRING, null=True, - blank=True) - ''' + regex = models.CharField( + _("正则校验规则关键字"), + max_length=LEN_NORMAL, + default=EMPTY_STRING, + null=True, + blank=True, + ) + """ regex_config.rule: dict 校验规则 regex_config.rule.expressions: []dict 表达式 regex_config.rule.type: string 表达式关系,and/or @@ -109,19 +127,39 @@ class BaseField(Model): "type":"and" } } - ''' - regex_config = jsonfield.JSONCharField(_('正则校验规则配置'), max_length=LEN_LONG, default=EMPTY_DICT) - custom_regex = models.CharField(_("自定义正则规则"), max_length=LEN_MIDDLE, default=EMPTY_STRING, - null=True, blank=True) - desc = models.CharField(_("字段填写说明"), max_length=LEN_MIDDLE, default=EMPTY_STRING, null=True, - blank=True) - tips = models.CharField(_("字段展示说明"), max_length=LEN_MIDDLE, default=EMPTY_STRING, null=True, - blank=True) - is_tips = models.BooleanField(_('额外提示'), default=False) - default = models.CharField(_("默认值"), max_length=LEN_XX_LONG, default=EMPTY_STRING, null=True, - blank=True) + """ + regex_config = jsonfield.JSONCharField( + _("正则校验规则配置"), max_length=LEN_LONG, default=EMPTY_DICT + ) + custom_regex = models.CharField( + _("自定义正则规则"), + max_length=LEN_MIDDLE, + default=EMPTY_STRING, + null=True, + blank=True, + ) + desc = models.CharField( + _("字段填写说明"), + max_length=LEN_MIDDLE, + default=EMPTY_STRING, + null=True, + blank=True, + ) + tips = models.CharField( + _("字段展示说明"), + max_length=LEN_MIDDLE, + default=EMPTY_STRING, + null=True, + blank=True, + ) + is_tips = models.BooleanField(_("额外提示"), default=False) + default = models.CharField( + _("默认值"), max_length=LEN_XX_LONG, default=EMPTY_STRING, null=True, blank=True + ) choice = jsonfield.JSONField(_("选项"), default=EMPTY_LIST) - related_fields = jsonfield.JSONField(_("级联字段"), default=EMPTY_DICT, null=True, blank=True) + related_fields = jsonfield.JSONField( + _("级联字段"), default=EMPTY_DICT, null=True, blank=True + ) meta = jsonfield.JSONField(_("复杂描述信息"), default=EMPTY_DICT) class Meta: @@ -145,7 +183,7 @@ def tag_data(self, fields=None, exclude=None): _exclude.extend(exclude) for f in chain(opts.concrete_fields, opts.private_fields): - if not getattr(f, 'editable', False): + if not getattr(f, "editable", False): continue if fields and f.name not in fields: continue @@ -153,18 +191,20 @@ def tag_data(self, fields=None, exclude=None): continue if isinstance(f, models.ForeignKey): - data['{}_id'.format(f.name)] = getattr(getattr(self, f.name), 'id', '') + data["{}_id".format(f.name)] = getattr(getattr(self, f.name), "id", "") else: - data[f.name] = getattr(self, f.name, '') + data[f.name] = getattr(self, f.name, "") - if self.source_type == 'API': + if self.source_type == "API": api_instance_info = RemoteApiInstance.objects.get( - id=self.api_instance_id).tag_data() + id=self.api_instance_id + ).tag_data() remote_api_info = RemoteApi.objects.get( - id=api_instance_info['remote_api']).tag_data() - data['api_info'] = { - 'api_instance_info': api_instance_info, - 'remote_api_info': remote_api_info, + id=api_instance_info["remote_api"] + ).tag_data() + data["api_info"] = { + "api_instance_info": api_instance_info, + "remote_api_info": remote_api_info, } return data @@ -173,19 +213,31 @@ def tag_data(self, fields=None, exclude=None): class Field(BaseField): """字段表""" - SOURCE = [('CUSTOM', '自定义添加'), ('TABLE', '基础模型添加')] + SOURCE = [("CUSTOM", "自定义添加"), ("TABLE", "基础模型添加")] - workflow = models.ForeignKey('workflow.Workflow', help_text=_("关联流程"), related_name="fields", - on_delete=models.CASCADE) - state = models.ForeignKey('workflow.State', help_text=_("关联流程"), related_name="state_fields", - null=True, blank=True, on_delete=models.CASCADE) - source = models.CharField(_('添加方式'), max_length=LEN_SHORT, choices=SOURCE, default='CUSTOM') + workflow = models.ForeignKey( + "workflow.Workflow", + help_text=_("关联流程"), + related_name="fields", + on_delete=models.CASCADE, + ) + state = models.ForeignKey( + "workflow.State", + help_text=_("关联流程"), + related_name="state_fields", + null=True, + blank=True, + on_delete=models.CASCADE, + ) + source = models.CharField( + _("添加方式"), max_length=LEN_SHORT, choices=SOURCE, default="CUSTOM" + ) objects = managers.FieldManager() _objects = models.Manager() class Meta: - app_label = 'workflow' + app_label = "workflow" verbose_name = _("字段表") verbose_name_plural = _("字段表") @@ -211,8 +263,12 @@ def clone(self, key=None): class TemplateField(BaseField): """字段库""" - flow_type = models.CharField(_("流程分类"), max_length=LEN_NORMAL, default=DEFAULT_STRING) - project_key = models.CharField(_("项目key"), max_length=LEN_SHORT, null=False, default=0) + flow_type = models.CharField( + _("流程分类"), max_length=LEN_NORMAL, default=DEFAULT_STRING + ) + project_key = models.CharField( + _("项目key"), max_length=LEN_SHORT, null=False, default=0 + ) objects = managers.TemplateFieldManager() @@ -248,29 +304,34 @@ class Table(Model): 基础模型 """ - fields = models.ManyToManyField(TemplateField, help_text=_('关联的公共字段'), related_name="tables") - name = models.CharField(_('模型名称'), max_length=LEN_LONG) - desc = models.CharField(_('基础模型描述'), max_length=LEN_LONG, null=True, blank=True) - fields_order = jsonfield.JSONField(_('字段排序'), default=[]) + fields = models.ManyToManyField( + TemplateField, help_text=_("关联的公共字段"), related_name="tables" + ) + name = models.CharField(_("模型名称"), max_length=LEN_LONG) + desc = models.CharField( + _("基础模型描述"), max_length=LEN_LONG, null=True, blank=True + ) + fields_order = jsonfield.JSONField(_("字段排序"), default=[]) - is_builtin = models.BooleanField(_('是否内置字段'), default=False) + is_builtin = models.BooleanField(_("是否内置字段"), default=False) - version = models.CharField(_("Table版本:空"), max_length=LEN_NORMAL, null=True, blank=True, - default=EMPTY) + version = models.CharField( + _("Table版本:空"), max_length=LEN_NORMAL, null=True, blank=True, default=EMPTY + ) objects = managers.TableManager() class Meta: - app_label = 'workflow' - verbose_name = _('基础模型') - verbose_name_plural = _('基础模型') + app_label = "workflow" + verbose_name = _("基础模型") + verbose_name_plural = _("基础模型") def __unicode__(self): return self.name def add_fields(self, fields): """增加字段""" - table_fields = list(self.fields.values_list('id', flat=True)) + table_fields = list(self.fields.values_list("id", flat=True)) for field in fields: if field not in table_fields: table_fields.append(field) @@ -279,7 +340,7 @@ def add_fields(self, fields): def remove_fields(self, fields): """删除字段""" - table_fields = list(self.fields.values_list('id', flat=True)) + table_fields = list(self.fields.values_list("id", flat=True)) for field in fields: try: table_fields.remove(field) @@ -291,7 +352,7 @@ def remove_fields(self, fields): def tag_data(self, exclude=None): """获取table快照数据""" - _exclude = list(self.FIELDS) + ['is_builtin', 'fields', 'fields_order'] + _exclude = list(self.FIELDS) + ["is_builtin", "fields", "fields_order"] if isinstance(exclude, (list, tuple)): _exclude.extend(exclude) @@ -304,22 +365,29 @@ def tag_data(self, exclude=None): id_to_key[tf.id] = tf.key fields.append(tf.tag_data()) - field_key_order = [id_to_key[field_id] for field_id in self.fields_order if - field_id in id_to_key] + field_key_order = [ + id_to_key[field_id] + for field_id in self.fields_order + if field_id in id_to_key + ] data = model_to_dict(self, exclude=_exclude) - data.update(fields=fields, fields_order=self.fields_order, field_key_order=field_key_order) + data.update( + fields=fields, + fields_order=self.fields_order, + field_key_order=field_key_order, + ) return data def clone(self): """返回克隆对象""" - fields = self.fields.values_list('id', flat=True) + fields = self.fields.values_list("id", flat=True) self.id = None self.save() version_name = create_version_number() - self.name = '{}_{}'.format(self.name, version_name) + self.name = "{}_{}".format(self.name, version_name) self.fields = fields self.version = version_name self.save() diff --git a/itsm/workflow/models/state.py b/itsm/workflow/models/state.py index 348bef0d3..23072bdcd 100644 --- a/itsm/workflow/models/state.py +++ b/itsm/workflow/models/state.py @@ -29,7 +29,7 @@ import jsonfield from django.db import models, transaction from django.db.models import Q -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( DEFAULT_STRING, @@ -93,7 +93,11 @@ class State(Model): ) name = models.CharField(_("状态名"), max_length=LEN_NORMAL) desc = models.CharField( - _("状态描述"), max_length=LEN_NORMAL, default=EMPTY_STRING, null=True, blank=True + _("状态描述"), + max_length=LEN_NORMAL, + default=EMPTY_STRING, + null=True, + blank=True, ) type = models.CharField( _("状态类型"), @@ -111,16 +115,24 @@ class State(Model): _("处理人列表"), default=EMPTY_STRING, null=True, blank=True ) assignors_type = models.CharField( - _("派单人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("派单人类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", ) assignors = models.TextField( _("派单人列表"), default=EMPTY_STRING, null=True, blank=True ) can_deliver = models.BooleanField(_("能否转单"), default=False) delivers_type = models.CharField( - _("转单人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("转单人类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", + ) + delivers = models.TextField( + _("转单人列表"), default=EMPTY_STRING, null=True, blank=True ) - delivers = models.TextField(_("转单人列表"), default=EMPTY_STRING, null=True, blank=True) distribute_type = models.CharField( _("分配方式"), @@ -129,7 +141,9 @@ class State(Model): default="PROCESS", ) - notify = models.ManyToManyField("workflow.Notify", help_text=_("可关联多种通知方式")) + notify = models.ManyToManyField( + "workflow.Notify", help_text=_("可关联多种通知方式") + ) notify_rule = models.CharField( _("通知规则"), max_length=LEN_SHORT, choices=NOTIFY_RULE_CHOICES, default="NONE" ) @@ -148,27 +162,42 @@ class State(Model): is_builtin = models.BooleanField(_("是否为系统内置"), default=False) # 是否允许在单据处理人为空时跳过 - is_allow_skip = models.BooleanField(_("是否允许在单据处理人为空时跳过"), default=False) + is_allow_skip = models.BooleanField( + _("是否允许在单据处理人为空时跳过"), default=False + ) # 会签及任务控制 is_sequential = models.BooleanField(_("是否是串行任务"), default=False) finish_condition = jsonfield.JSONField(_("可向下调度的条件"), default=EMPTY_DICT) variables = jsonfield.JSONField(_("变量"), default=EMPTY_VARIABLE, null=True) - axis = jsonfield.JSONCharField(_("节点的坐标轴"), max_length=128, default=EMPTY_DICT) + axis = jsonfield.JSONCharField( + _("节点的坐标轴"), max_length=128, default=EMPTY_DICT + ) api_instance_id = models.IntegerField( _("api实例主键"), default=0, null=True, blank=True ) extras = jsonfield.JSONCharField( - _("额外信息"), max_length=LEN_XXX_LONG, default=EMPTY_DICT, null=True, blank=True + _("额外信息"), + max_length=LEN_XXX_LONG, + default=EMPTY_DICT, + null=True, + blank=True, ) # deprecated fields followers_type = models.CharField( - _("关注人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("关注人类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", ) followers = models.CharField( - _("关注人列表"), max_length=LEN_LONG, default=EMPTY_STRING, null=True, blank=True + _("关注人列表"), + max_length=LEN_LONG, + default=EMPTY_STRING, + null=True, + blank=True, ) label = models.CharField(_("标签记录"), max_length=LEN_LONG, default="EMPTY") diff --git a/itsm/workflow/models/task.py b/itsm/workflow/models/task.py index f3f1d0fc1..06002f422 100644 --- a/itsm/workflow/models/task.py +++ b/itsm/workflow/models/task.py @@ -28,7 +28,7 @@ import jsonfield from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( EMPTY_DICT, @@ -44,7 +44,8 @@ LEN_XX_LONG, SOURCE_TASK, LEN_SHORT, - TASK_TYPE, PUBLIC_PROJECT_PROJECT_KEY, + TASK_TYPE, + PUBLIC_PROJECT_PROJECT_KEY, ) from itsm.workflow.models import Model, BaseField from itsm.workflow.managers import TaskSchemaManager, TaskSchemaFieldManager @@ -57,20 +58,38 @@ class TaskSchema(Model): """ fields = ( - "id", "name", "is_builtin", "component_type", "desc", "is_draft", "can_edit", "owners") + "id", + "name", + "is_builtin", + "component_type", + "desc", + "is_draft", + "can_edit", + "owners", + ) name = models.CharField(_("任务模版的名称"), max_length=LEN_MIDDLE, null=False) is_builtin = models.BooleanField(_("是否内置"), default=False) - component_type = models.CharField(_("任务组件类型"), choices=TASK_COMPONENT_CHOICE, - max_length=LEN_NORMAL) - desc = models.CharField(_("任务模版的名称"), max_length=LEN_X_LONG, default=EMPTY_STRING, blank=True) + component_type = models.CharField( + _("任务组件类型"), choices=TASK_COMPONENT_CHOICE, max_length=LEN_NORMAL + ) + desc = models.CharField( + _("任务模版的名称"), max_length=LEN_X_LONG, default=EMPTY_STRING, blank=True + ) is_draft = models.BooleanField(_("是否为草稿"), default=True) is_enabled = models.BooleanField(_("是否为开启状态"), default=False) owners = models.CharField(_("负责人"), max_length=LEN_XX_LONG, default=EMPTY_STRING) - can_edit = models.BooleanField(_("是否可编辑状态"), help_text=_("当为流程version引用的时候,不可编辑和查看"), - default=True) - inputs = jsonfield.JSONField(_("组件输入信息"), help_text=_("当前组件输入参数引用的参数变量"), default=EMPTY_DICT) + can_edit = models.BooleanField( + _("是否可编辑状态"), + help_text=_("当为流程version引用的时候,不可编辑和查看"), + default=True, + ) + inputs = jsonfield.JSONField( + _("组件输入信息"), + help_text=_("当前组件输入参数引用的参数变量"), + default=EMPTY_DICT, + ) objects = TaskSchemaManager() @@ -84,7 +103,7 @@ class Meta: verbose_name = _("任务模型") verbose_name_plural = _("任务模型") ordering = ("-id",) - + @property def project_key(self): return PUBLIC_PROJECT_PROJECT_KEY @@ -104,8 +123,13 @@ def get_variables(self, stage=None): break variables = [ - {"key": _field.key, "type": _field.type, "source": "field", "name": _field.name, - "choice": _field.choice} + { + "key": _field.key, + "type": _field.type, + "source": "field", + "name": _field.name, + "choice": _field.choice, + } for _field in self.all_fields.all() ] @@ -113,8 +137,13 @@ def get_variables(self, stage=None): variables.extend(TICKET_GLOBAL_VARIABLES) if self.component_type == "SOPS": variables.append( - {"key": "sops_relate_id", "type": "string", "source": "ticket", "name": "REL单号", - "choice": []} + { + "key": "sops_relate_id", + "type": "string", + "source": "ticket", + "name": "REL单号", + "choice": [], + } ) # 工单内的信息更新 variables.extend(self.inputs.get("ticket_variables", [])) @@ -122,14 +151,21 @@ def get_variables(self, stage=None): def tag_data(self): triggers = [] - for trigger in Trigger.objects.filter(source_type=SOURCE_TASK, source_id=self.id): + for trigger in Trigger.objects.filter( + source_type=SOURCE_TASK, source_id=self.id + ): triggers.append(trigger.tag_data()) fields = [] for field in self.all_fields.all(): fields.append(field.tag_data()) - return dict(id=self.id, name=self.name, component_type=self.component_type, - triggers=triggers, fields=fields) + return dict( + id=self.id, + name=self.name, + component_type=self.component_type, + triggers=triggers, + fields=fields, + ) def restore_fields(self, fields): TaskFieldSchema.objects.restore(fields, self) @@ -138,10 +174,18 @@ def restore_fields(self, fields): class TaskFieldSchema(BaseField): """任务对应的表单字段""" - task_schema = models.ForeignKey(TaskSchema, related_name="all_fields", help_text=_("对应的任务模型"), - on_delete=models.CASCADE) - stage = models.CharField(_("所处阶段"), choices=TASK_STAGE_CHOICE, default="CREATE", - max_length=LEN_NORMAL) + task_schema = models.ForeignKey( + TaskSchema, + related_name="all_fields", + help_text=_("对应的任务模型"), + on_delete=models.CASCADE, + ) + stage = models.CharField( + _("所处阶段"), + choices=TASK_STAGE_CHOICE, + default="CREATE", + max_length=LEN_NORMAL, + ) sequence = models.IntegerField(_("序号"), default=0) objects = TaskSchemaFieldManager() @@ -166,12 +210,16 @@ class TaskConfig(Model): """ workflow_id = models.IntegerField(_("流程id"), db_index=True) - workflow_type = models.CharField(_("流程类型"), choices=TASK_TYPE, max_length=LEN_SHORT) + workflow_type = models.CharField( + _("流程类型"), choices=TASK_TYPE, max_length=LEN_SHORT + ) task_schema_id = models.IntegerField(_("任务模版id")) create_task_state = models.IntegerField(_("任务创建节点")) execute_task_state = models.IntegerField(_("任务执行节点")) execute_can_create = models.BooleanField(_("执行节点是否可创建"), default=False) - need_task_finished = models.BooleanField(_("流转是否需要任务全部完成"), default=False) + need_task_finished = models.BooleanField( + _("流转是否需要任务全部完成"), default=False + ) class Meta: app_label = "workflow" diff --git a/itsm/workflow/models/transition.py b/itsm/workflow/models/transition.py index 58aa9db95..804c9fd46 100644 --- a/itsm/workflow/models/transition.py +++ b/itsm/workflow/models/transition.py @@ -25,7 +25,7 @@ import jsonfield from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from itsm.component.constants import ( DEFAULT_FLOW_CONDITION, @@ -42,15 +42,19 @@ class Condition(Model): """流转线条件配置""" - workflow = models.ForeignKey('workflow.Workflow', related_name="flows", help_text=_("关联工作流"), - on_delete=models.CASCADE) + workflow = models.ForeignKey( + "workflow.Workflow", + related_name="flows", + help_text=_("关联工作流"), + on_delete=models.CASCADE, + ) name = models.CharField(_("流转操作"), max_length=LEN_NORMAL) data = jsonfield.JSONField(_("流转条件表达式"), default=DEFAULT_FLOW_CONDITION) objects = managers.ConditionManager() class Meta: - app_label = 'workflow' + app_label = "workflow" verbose_name = _("流转条件") verbose_name_plural = _("流转条件") @@ -66,30 +70,51 @@ class Transition(Model): """状态流转""" DIRECTION_CHOICES = [ - ("BACK", '向后'), - ("FORWARD", '向前'), + ("BACK", "向后"), + ("FORWARD", "向前"), ] - workflow = models.ForeignKey('workflow.Workflow', related_name="transitions", - help_text=_("关联流程"), on_delete=models.CASCADE) + workflow = models.ForeignKey( + "workflow.Workflow", + related_name="transitions", + help_text=_("关联流程"), + on_delete=models.CASCADE, + ) name = models.CharField(_("流转操作"), max_length=LEN_NORMAL) condition = jsonfield.JSONField(_("流转条件表达式"), default=DEFAULT_FLOW_CONDITION) condition_type = models.CharField( - _("流转类型"), max_length=LEN_SHORT, choices=FLOW_CONDITION_TYPE_CHOICES, default="default" + _("流转类型"), + max_length=LEN_SHORT, + choices=FLOW_CONDITION_TYPE_CHOICES, + default="default", ) # 线条的方向坐标 {"start":"left|right|top|bottom", "end": "left|right|top|bottom"} - axis = jsonfield.JSONCharField(_("线条的坐标位置的坐标轴"), max_length=LEN_NORMAL, default=EMPTY_DICT) + axis = jsonfield.JSONCharField( + _("线条的坐标位置的坐标轴"), max_length=LEN_NORMAL, default=EMPTY_DICT + ) - from_state = models.ForeignKey('workflow.State', related_name="transitions_from", - help_text=_("源状态ID"), on_delete=models.CASCADE) - to_state = models.ForeignKey('workflow.State', related_name="transitions_to", - help_text=_("目标状态ID"), on_delete=models.CASCADE) + from_state = models.ForeignKey( + "workflow.State", + related_name="transitions_from", + help_text=_("源状态ID"), + on_delete=models.CASCADE, + ) + to_state = models.ForeignKey( + "workflow.State", + related_name="transitions_to", + help_text=_("目标状态ID"), + on_delete=models.CASCADE, + ) # deprecated fields: check_needed/opt_needed - direction = models.CharField(_("流转方向"), max_length=LEN_SHORT, choices=DIRECTION_CHOICES, - default="FORWARD") + direction = models.CharField( + _("流转方向"), + max_length=LEN_SHORT, + choices=DIRECTION_CHOICES, + default="FORWARD", + ) check_needed = models.BooleanField(_("是否需要校验表单完整性"), default=True) opt_needed = models.BooleanField(_("是否需要执行操作"), default=True) @@ -99,12 +124,14 @@ class Transition(Model): _objects = models.Manager() class Meta: - app_label = 'workflow' + app_label = "workflow" verbose_name = _("状态流转") verbose_name_plural = _("状态流转") def __unicode__(self): - return "{} -{} {}".format(self.from_state, '>' if self.opt_needed else '-', self.to_state) + return "{} -{} {}".format( + self.from_state, ">" if self.opt_needed else "-", self.to_state + ) @property def serialized_data(self): @@ -116,4 +143,6 @@ def delete(self, using=None): from itsm.workflow.models import State super(Transition, self).delete(using) - State.objects.update_state_label(self.from_state, self.to_state, operate_type='delete') + State.objects.update_state_label( + self.from_state, self.to_state, operate_type="delete" + ) diff --git a/itsm/workflow/models/trigger.py b/itsm/workflow/models/trigger.py index 52fb9e8e1..07799bc59 100644 --- a/itsm/workflow/models/trigger.py +++ b/itsm/workflow/models/trigger.py @@ -25,9 +25,15 @@ import jsonfield from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ -from itsm.component.constants import EMPTY_DICT, LEN_LONG, LEN_NORMAL, LEN_SHORT, TRIGGER_TYPE +from itsm.component.constants import ( + EMPTY_DICT, + LEN_LONG, + LEN_NORMAL, + LEN_SHORT, + TRIGGER_TYPE, +) from itsm.workflow.managers import TriggerManager from itsm.workflow.models import Model @@ -36,8 +42,12 @@ class Trigger(Model): name = models.CharField(_("名称"), max_length=LEN_NORMAL) component_key = models.CharField(_("原子key"), max_length=LEN_NORMAL) type = models.CharField(_("类型"), max_length=LEN_SHORT, choices=TRIGGER_TYPE) - condition = jsonfield.JSONCharField(_("触发条件"), max_length=LEN_LONG, default=EMPTY_DICT) - inputs = jsonfield.JSONCharField(_("传入参数"), max_length=LEN_LONG, default=EMPTY_DICT) + condition = jsonfield.JSONCharField( + _("触发条件"), max_length=LEN_LONG, default=EMPTY_DICT + ) + inputs = jsonfield.JSONCharField( + _("传入参数"), max_length=LEN_LONG, default=EMPTY_DICT + ) """ key: one input key value: mapping value diff --git a/itsm/workflow/models/workflow.py b/itsm/workflow/models/workflow.py index 450eee870..e16d1e70d 100644 --- a/itsm/workflow/models/workflow.py +++ b/itsm/workflow/models/workflow.py @@ -30,7 +30,7 @@ from django.db import models, transaction from django.db.models import Q from django.forms import model_to_dict -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework.exceptions import ValidationError from itsm.component.constants import ( @@ -96,10 +96,14 @@ class WorkflowBase(ObjectManagerMixin, Model): ) is_draft = models.BooleanField(_("是否为草稿"), default=True) is_builtin = models.BooleanField(_("是否为系统内置"), default=False) - is_task_needed = models.NullBooleanField(_("是否需要关联子任务"), default=False, null=True) + is_task_needed = models.BooleanField( + _("是否需要关联子任务"), default=False, null=True + ) owners = models.CharField(_("负责人"), max_length=LEN_XX_LONG, default=EMPTY_STRING) - notify = models.ManyToManyField("workflow.Notify", help_text=_("可关联多种通知方式")) + notify = models.ManyToManyField( + "workflow.Notify", help_text=_("可关联多种通知方式") + ) notify_rule = models.CharField( _("通知规则"), max_length=LEN_SHORT, choices=NOTIFY_RULE_CHOICES, default="NONE" ) @@ -112,7 +116,10 @@ class WorkflowBase(ObjectManagerMixin, Model): is_iam_used = models.BooleanField(_("是否使用IAM角色"), default=False) is_supervise_needed = models.BooleanField(_("是否需要督办"), default=False) supervise_type = models.CharField( - _("督办人类型"), max_length=LEN_SHORT, choices=PROCESSOR_CHOICES, default="EMPTY" + _("督办人类型"), + max_length=LEN_SHORT, + choices=PROCESSOR_CHOICES, + default="EMPTY", ) supervisor = models.CharField( _("督办列表"), max_length=LEN_LONG, default=EMPTY_STRING, null=True, blank=True @@ -125,7 +132,9 @@ class WorkflowBase(ObjectManagerMixin, Model): ) # deprecated fields - master = jsonfield.JSONField(_("主分支列表"), default=EMPTY_LIST, null=True, blank=True) + master = jsonfield.JSONField( + _("主分支列表"), default=EMPTY_LIST, null=True, blank=True + ) extras = jsonfield.JSONField( _("其他配置信息"), default={ @@ -183,7 +192,7 @@ class Meta: def __unicode__(self): return "{}({})".format(self.name, self.pk) - + def get_iam_resource(self): """获取 workflow 关联的服务对象""" workflow_version = WorkflowVersion.objects.filter(workflow_id=self.id).last() @@ -506,9 +515,9 @@ def can_bind_sla(self): if missing_field: field_desc = [REQUIRED_FIELD[field] for field in missing_field] raise ValidationError( - _("检测到您的流程已经发生改变,现流程版本中缺少【{}】信息,请补充完整后重新配置sla").format( - ",".join(field_desc) - ) + _( + "检测到您的流程已经发生改变,现流程版本中缺少【{}】信息,请补充完整后重新配置sla" + ).format(",".join(field_desc)) ) def get_notifiy_objs(self, notify_list): @@ -546,8 +555,12 @@ class WorkflowVersion(WorkflowBase): workflow_id = models.IntegerField(_("流程模板ID")) - fields = jsonfield.JSONField(_("字段快照字典"), default=EMPTY_DICT, null=True, blank=True) - states = jsonfield.JSONField(_("状态快照字典"), default=EMPTY_DICT, null=True, blank=True) + fields = jsonfield.JSONField( + _("字段快照字典"), default=EMPTY_DICT, null=True, blank=True + ) + states = jsonfield.JSONField( + _("状态快照字典"), default=EMPTY_DICT, null=True, blank=True + ) transitions = jsonfield.JSONField( _("流转快照字典"), default=EMPTY_DICT, null=True, blank=True ) @@ -887,5 +900,7 @@ def can_bind_sla(self): if missing_field: field_desc = [REQUIRED_FIELD[field] for field in missing_field] raise ValidationError( - _("流程版本中缺少【{}】信息,请补充完整后再进行服务协议的关联").format(",".join(field_desc)) + _("流程版本中缺少【{}】信息,请补充完整后再进行服务协议的关联").format( + ",".join(field_desc) + ) ) diff --git a/itsm/workflow/permissions.py b/itsm/workflow/permissions.py index 7714ea2f6..68c5120f0 100644 --- a/itsm/workflow/permissions.py +++ b/itsm/workflow/permissions.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import permissions from itsm.component.constants import PUBLIC_PROJECT_PROJECT_KEY @@ -92,7 +92,7 @@ def has_permission(self, request, view): apply_actions = ["service_manage"] # 节点和字段的查看,首先必须有当前流程的管理权限 - if not view.action in ["list", "batch_update"]: + if view.action not in ["list", "batch_update"]: # 非列表请求,通过object鉴权 return True @@ -102,7 +102,7 @@ def has_permission(self, request, view): workflow_id = view.get_iam_resource_id() if not workflow_id: return False - + try: flow = Workflow.objects.get(id=workflow_id) except Workflow.DoesNotExist: @@ -182,15 +182,15 @@ def has_permission(self, request, view): # 免鉴权需要明确声明 if view.action in getattr(view, "permission_free_actions", []): return True - + if view.action in getattr(view, "permission_create_action", ["create"]): project_key = request.data.get("project_key", PUBLIC_PROJECT_PROJECT_KEY) - + # 平台管理 if project_key == PUBLIC_PROJECT_PROJECT_KEY: apply_actions = [view.permission_action_platform] return self.iam_auth(request, apply_actions) - + # 项目管理 apply_actions = self.get_view_iam_actions(view) return self.iam_create_auth(request, apply_actions) @@ -200,7 +200,7 @@ def has_object_permission(self, request, view, obj, **kwargs): # 关联实例的请求,需要针对对象进行鉴权 if view.action in getattr(view, "permission_free_actions", []): return True - + # 平台管理 if obj.project_key == PUBLIC_PROJECT_PROJECT_KEY: apply_actions = [view.permission_action_platform] diff --git a/itsm/workflow/serializers/field.py b/itsm/workflow/serializers/field.py index 1e880bef2..7fab18b39 100644 --- a/itsm/workflow/serializers/field.py +++ b/itsm/workflow/serializers/field.py @@ -26,7 +26,7 @@ import json from django.db import transaction -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.fields import JSONField, empty from rest_framework.validators import UniqueValidator @@ -173,7 +173,11 @@ class Meta: "project_key", ) + model.FIELDS read_only_fields = ("is_builtin", "key") + model.FIELDS - create_only_fields = ("is_builtin", "key", "project_key", ) + create_only_fields = ( + "is_builtin", + "key", + "project_key", + ) def __init__(self, *args, **kwargs): validator_class = kwargs.pop("validator_class", TemplateFieldValidator) @@ -296,8 +300,8 @@ def update_public_field_auth_actions(self, instance, data): [permission_action_platform], [] ) auth_actions = [ - action_id - for action_id in self.Meta.model.public_field_resource_operations + action_id + for action_id in self.Meta.model.public_field_resource_operations if auth_result.get(permission_action_platform) ] data["auth_actions"] = auth_actions @@ -438,7 +442,10 @@ class TableSerializer(AuthModelSerializer): required=True, max_length=LEN_MIDDLE, validators=[ - UniqueValidator(queryset=Table.objects.all(), message=_("基础模型名称已经存在,请重新输入")) + UniqueValidator( + queryset=Table.objects.all(), + message=_("基础模型名称已经存在,请重新输入"), + ) ], ) desc = serializers.CharField( diff --git a/itsm/workflow/serializers/global_variable.py b/itsm/workflow/serializers/global_variable.py index 1d2370443..fae262ccb 100644 --- a/itsm/workflow/serializers/global_variable.py +++ b/itsm/workflow/serializers/global_variable.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.fields import JSONField diff --git a/itsm/workflow/serializers/state.py b/itsm/workflow/serializers/state.py index 940899ff8..fa029f596 100644 --- a/itsm/workflow/serializers/state.py +++ b/itsm/workflow/serializers/state.py @@ -24,7 +24,7 @@ """ from django.db import transaction -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.fields import JSONField, empty diff --git a/itsm/workflow/serializers/transition.py b/itsm/workflow/serializers/transition.py index c45a79c6b..19076523f 100644 --- a/itsm/workflow/serializers/transition.py +++ b/itsm/workflow/serializers/transition.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.fields import JSONField @@ -39,15 +39,18 @@ class TransitionTemplateSerializer(serializers.ModelSerializer): required=True, max_length=LEN_NORMAL, allow_null=False, - error_messages={'blank': _('请输入模板名称!'), 'max_length': _('模板名称长度不能大于64个字符')}, + error_messages={ + "blank": _("请输入模板名称!"), + "max_length": _("模板名称长度不能大于64个字符"), + }, ) data = JSONField(required=False, initial=DEFAULT_FLOW_CONDITION) class Meta: model = Condition - fields = ('id', 'name', 'data', 'workflow') + fields = ("id", "name", "data", "workflow") - read_only_fields = ('creator', 'create_at', 'update_at', 'end_at') + read_only_fields = ("creator", "create_at", "update_at", "end_at") def validate(self, attrs): """校验参数,name不能相同等""" @@ -55,7 +58,11 @@ def validate(self, attrs): if Condition.objects.filter(is_deleted=False, name=attrs["name"]).exists(): raise serializers.ValidationError(_("同流程下线条模板名称已存在")) if self.context["view"].action == "update": - if Condition.objects.filter(is_deleted=False, name=attrs["name"]).exclude(id=self.instance.id).exists(): + if ( + Condition.objects.filter(is_deleted=False, name=attrs["name"]) + .exclude(id=self.instance.id) + .exists() + ): raise serializers.ValidationError(_("同流程下线条模板名称已存在")) return attrs @@ -65,35 +72,42 @@ class TransitionSerializer(serializers.ModelSerializer): """流转序列化""" axis = JSONField(required=False, initial={}) - name = serializers.CharField(required=True, max_length=LEN_SHORT, allow_blank=False, allow_null=False) + name = serializers.CharField( + required=True, max_length=LEN_SHORT, allow_blank=False, allow_null=False + ) condition = JSONField(required=False, initial=DEFAULT_FLOW_CONDITION) class Meta: model = Transition fields = ( - 'workflow', - 'id', - 'from_state', - 'to_state', - 'name', - 'axis', - 'condition', - 'condition_type', + "workflow", + "id", + "from_state", + "to_state", + "name", + "axis", + "condition", + "condition_type", ) + model.FIELDS - read_only_fields = ('key',) + model.FIELDS + read_only_fields = ("key",) + model.FIELDS def __init__(self, *args, **kwargs): super(TransitionSerializer, self).__init__(*args, **kwargs) - self.view = self.context.get('view') - if self.view and self.view.action == 'create': + self.view = self.context.get("view") + if self.view and self.view.action == "create": self.validators = [TransitionValidator()] def update(self, instance, validated_data): instance = super(TransitionSerializer, self).update(instance, validated_data) # 不是全局更新的情况下,需要更新条件 - if self.context['view'].action != 'partial_update' and instance.condition_type != 'default': - State.objects.update_outputs_variables(instance.condition, instance.workflow.id) + if ( + self.context["view"].action != "partial_update" + and instance.condition_type != "default" + ): + State.objects.update_outputs_variables( + instance.condition, instance.workflow.id + ) return instance @@ -103,19 +117,22 @@ def create(self, validated_data): return instance def to_internal_value(self, data): - if data.get('condition_type') == 'default': - data['condition'] = DEFAULT_FLOW_CONDITION + if data.get("condition_type") == "default": + data["condition"] = DEFAULT_FLOW_CONDITION return super(TransitionSerializer, self).to_internal_value(data) def validate(self, attrs): """线条配置校验""" - if attrs.get('condition_type', '') == 'by_field': - for expression in attrs['condition']['expressions']: - for condition in expression['expressions']: - if condition['type'] == 'INT' and condition['value'] == 0: - if not (condition['condition'] and condition['key']): + if attrs.get("condition_type", "") == "by_field": + for expression in attrs["condition"]["expressions"]: + for condition in expression["expressions"]: + if condition["type"] == "INT" and condition["value"] == 0: + if not (condition["condition"] and condition["key"]): raise serializers.ValidationError(_("条件配置错误")) - if condition['type'] == 'BOOLEAN' and condition['value'] not in [True, False]: + if condition["type"] == "BOOLEAN" and condition["value"] not in [ + True, + False, + ]: raise serializers.ValidationError(_("布尔类型的取值范围不正确")) # elif not (condition['condition'] and condition['key'] and condition['value']): # raise serializers.ValidationError(u"条件配置错误") diff --git a/itsm/workflow/serializers/workflow.py b/itsm/workflow/serializers/workflow.py index 6f886ab20..10e72f172 100644 --- a/itsm/workflow/serializers/workflow.py +++ b/itsm/workflow/serializers/workflow.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers from itsm.component.constants import ( @@ -53,17 +53,26 @@ class WorkflowSerializer(DynamicFieldsModelSerializer): name = serializers.CharField( required=True, max_length=LEN_MIDDLE, - error_messages={'blank': _('请输入流程名称!'), 'max_length': _('流程名称长度不能大于120个字符')}, + error_messages={ + "blank": _("请输入流程名称!"), + "max_length": _("流程名称长度不能大于120个字符"), + }, ) flow_type = serializers.CharField(required=True, max_length=LEN_NORMAL) - desc = serializers.CharField(required=False, max_length=LEN_LONG, min_length=1, - allow_blank=True) - owners = serializers.CharField(required=False, max_length=LEN_XX_LONG, allow_blank=True) + desc = serializers.CharField( + required=False, max_length=LEN_LONG, min_length=1, allow_blank=True + ) + owners = serializers.CharField( + required=False, max_length=LEN_XX_LONG, allow_blank=True + ) deploy = serializers.BooleanField(required=False) - deploy_name = serializers.CharField(required=False, max_length=LEN_LONG, min_length=1, - allow_blank=True) + deploy_name = serializers.CharField( + required=False, max_length=LEN_LONG, min_length=1, allow_blank=True + ) # 基础模型 - table = serializers.PrimaryKeyRelatedField(required=True, queryset=Table.objects.all()) + table = serializers.PrimaryKeyRelatedField( + required=True, queryset=Table.objects.all() + ) # 业务属性字段 is_biz_needed = serializers.BooleanField(required=False) # 是否自动过单 @@ -71,48 +80,51 @@ class WorkflowSerializer(DynamicFieldsModelSerializer): is_iam_used = serializers.BooleanField(required=False) is_supervise_needed = serializers.BooleanField(required=False) supervise_type = serializers.ChoiceField(required=False, choices=PROCESSOR_CHOICES) - supervisor = serializers.CharField(required=False, max_length=LEN_LONG, allow_blank=True) + supervisor = serializers.CharField( + required=False, max_length=LEN_LONG, allow_blank=True + ) is_enabled = serializers.BooleanField(required=False) is_draft = serializers.BooleanField(required=False) is_revocable = serializers.BooleanField(required=False) revoke_config = serializers.JSONField(required=False) notify = NotifySerializer(required=False, allow_null=True, many=True) - notify_rule = serializers.ChoiceField(required=False, allow_blank=True, - choices=NOTIFY_RULE_CHOICES) + notify_rule = serializers.ChoiceField( + required=False, allow_blank=True, choices=NOTIFY_RULE_CHOICES + ) notify_freq = serializers.IntegerField(default=EMPTY_INT) extras = serializers.JSONField(required=False) class Meta: model = Workflow fields = ( - 'id', - 'name', - 'desc', - 'flow_type', - 'version_number', - 'deploy', - 'deploy_name', - 'notify', - 'notify_rule', - 'notify_freq', - 'is_biz_needed', - 'is_iam_used', - 'is_enabled', - 'is_draft', - 'is_revocable', - 'is_builtin', - 'is_supervise_needed', - 'supervisor', - 'supervise_type', - 'table', - 'owners', - 'extras', - 'revoke_config', - 'is_auto_approve', - ) + model.FIELDS + "id", + "name", + "desc", + "flow_type", + "version_number", + "deploy", + "deploy_name", + "notify", + "notify_rule", + "notify_freq", + "is_biz_needed", + "is_iam_used", + "is_enabled", + "is_draft", + "is_revocable", + "is_builtin", + "is_supervise_needed", + "supervisor", + "supervise_type", + "table", + "owners", + "extras", + "revoke_config", + "is_auto_approve", + ) + model.FIELDS # 只读字段在创建和更新时均被忽略 - read_only_fields = ('creator', 'create_at', 'update_at', 'end_at') + read_only_fields = ("creator", "create_at", "update_at", "end_at") def save(self, **kwargs): instance = super(WorkflowSerializer, self).save(**kwargs) @@ -120,15 +132,15 @@ def save(self, **kwargs): return instance def update(self, instance, validated_data): - deploy = validated_data.pop('deploy', None) - deploy_name = validated_data.pop('deploy_name', None) + deploy = validated_data.pop("deploy", None) + deploy_name = validated_data.pop("deploy_name", None) - is_biz_needed = validated_data.get('is_biz_needed', None) + is_biz_needed = validated_data.get("is_biz_needed", None) if is_biz_needed is False and instance.is_biz_needed is True: related_validate(instance.fields.get(key=FIELD_BIZ)) flow = super(WorkflowSerializer, self).update(instance, validated_data) - if 'task_settings' in validated_data.get('extras', {}): - flow.create_task(validated_data['extras']['task_settings']) + if "task_settings" in validated_data.get("extras", {}): + flow.create_task(validated_data["extras"]["task_settings"]) # 是否立即部署 if deploy: @@ -139,33 +151,39 @@ def update(self, instance, validated_data): def to_internal_value(self, data): validated_data = super(WorkflowSerializer, self).to_internal_value(data) - if validated_data.get('supervise_type') == 'PERSON': - validated_data['supervisor'] = dotted_name(validated_data.get('supervisor', '')) + if validated_data.get("supervise_type") == "PERSON": + validated_data["supervisor"] = dotted_name( + validated_data.get("supervisor", "") + ) notify_list = validated_data.pop("notify", None) if notify_list: - validated_data['notify'] = Notify.objects.filter( + validated_data["notify"] = Notify.objects.filter( type__in=(notify["type"] for notify in notify_list) - ).values_list('pk', flat=True) + ).values_list("pk", flat=True) if "owners" in validated_data: validated_data["owners"] = dotted_name(validated_data["owners"]) - if "is_enabled" in validated_data and "extras" not in validated_data and self.instance: + if ( + "is_enabled" in validated_data + and "extras" not in validated_data + and self.instance + ): # 如果extras不存在,直接置空 extras = self.instance.extras extras["task_settings"] = [] - validated_data['extras'] = extras + validated_data["extras"] = extras return validated_data def to_representation(self, instance): data = super(WorkflowSerializer, self).to_representation(instance) data["owners"] = normal_name(data.get("owners")) - data['updated_by'] = transform_single_username(data['updated_by']) + data["updated_by"] = transform_single_username(data["updated_by"]) - if "supervise_type" in data and data['supervise_type'] == 'PERSON': - data['supervisor'] = dotted_property(data, 'supervisor') + if "supervise_type" in data and data["supervise_type"] == "PERSON": + data["supervisor"] = dotted_property(data, "supervisor") task_settings = data["extras"].get("task_settings") if task_settings and isinstance(task_settings, dict): data["extras"]["task_settings"] = [] @@ -174,7 +192,9 @@ def to_representation(self, instance): { "task_schema_id": task_settings["task_schema_ids"][0], "create_task_state": task_settings["create_task_state"], - "execute_can_create": task_settings.get("execute_can_create", False), + "execute_can_create": task_settings.get( + "execute_can_create", False + ), "execute_task_state": task_settings["execute_task_state"], "need_task_finished": task_settings["need_task_finished"], } @@ -186,29 +206,47 @@ def to_representation(self, instance): def validate_notify_freq(self, value): """通知频率校验""" if value not in NOTIFY_FREQ_CHOICES: - raise serializers.ValidationError({str(_('参数校验失败')): _('通知频率参数不正确')}) + raise serializers.ValidationError( + {str(_("参数校验失败")): _("通知频率参数不正确")} + ) return value def validate(self, attrs): - name = attrs.get('name') + name = attrs.get("name") if self.instance: if Workflow.objects.filter(name=name).exclude(id=self.instance.id).exists(): - raise serializers.ValidationError({str(_('参数校验失败')): _('系统中已存在同名流程,请尝试换个流程名称')}) + raise serializers.ValidationError( + { + str(_("参数校验失败")): _( + "系统中已存在同名流程,请尝试换个流程名称" + ) + } + ) else: if Workflow.objects.filter(name=name).exists(): - raise serializers.ValidationError({str(_('参数校验失败')): _('系统中已存在同名流程,请尝试换个流程名称')}) + raise serializers.ValidationError( + { + str(_("参数校验失败")): _( + "系统中已存在同名流程,请尝试换个流程名称" + ) + } + ) - notify_rule = attrs.get('notify_rule', 'NONE') - notify = attrs.get('notify', '') - if notify_rule != 'NONE' and not notify: - raise serializers.ValidationError({str(_('参数校验失败')): _('至少选择一种通知类型')}) + notify_rule = attrs.get("notify_rule", "NONE") + notify = attrs.get("notify", "") + if notify_rule != "NONE" and not notify: + raise serializers.ValidationError( + {str(_("参数校验失败")): _("至少选择一种通知类型")} + ) - deploy = attrs.get('deploy') - deploy_name = attrs.get('deploy_name') + deploy = attrs.get("deploy") + deploy_name = attrs.get("deploy_name") if deploy: WorkflowPipelineValidator(self.instance)(if_deploy=True) if deploy and not deploy_name: - raise serializers.ValidationError({str(_('参数校验失败')): _('请指定部署流程名')}) + raise serializers.ValidationError( + {str(_("参数校验失败")): _("请指定部署流程名")} + ) return attrs @@ -219,31 +257,31 @@ class WorkflowVersionSerializer(DynamicFieldsModelSerializer): class Meta: model = WorkflowVersion main_fields = ( - 'id', - 'name', - 'desc', - 'workflow_id', - 'flow_type', - 'version_number', - 'is_builtin', - 'is_enabled', - 'is_draft', - 'is_revocable', - 'updated_by', - 'update_at', - 'creator', + "id", + "name", + "desc", + "workflow_id", + "flow_type", + "version_number", + "is_builtin", + "is_enabled", + "is_draft", + "is_revocable", + "updated_by", + "update_at", + "creator", ) - property_fields = ('service_cnt',) + property_fields = ("service_cnt",) fields = property_fields + main_fields read_only_fields = ( - 'service_cnt', - 'version_number', - 'is_builtin', - 'is_draft', - 'workflow_id', - 'updated_by', - 'update_at', + "service_cnt", + "version_number", + "is_builtin", + "is_draft", + "workflow_id", + "updated_by", + "update_at", ) def to_representation(self, instance): @@ -260,10 +298,14 @@ def to_representation(self, instance): attribute = field.get_attribute(instance) except SkipField: if field.field_name in self.Meta.property_fields: - attribute = getattr(self.Meta.model.objects, field.field_name)(instance) + attribute = getattr(self.Meta.model.objects, field.field_name)( + instance + ) else: continue - check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute + check_for_none = ( + attribute.pk if isinstance(attribute, PKOnlyObject) else attribute + ) if check_for_none is None: ret[field.field_name] = None else: @@ -275,12 +317,17 @@ class OperationalDataWorkflowSerializer(WorkflowSerializer): """运营数据流程序列化""" def to_representation(self, instance): - data = super(OperationalDataWorkflowSerializer, self).to_representation(instance) + data = super(OperationalDataWorkflowSerializer, self).to_representation( + instance + ) query_type = self.context.get("query_type", "list") if query_type == "detail": states_serializer = StateSerializer(instance.states, many=True) fields_serializer = FieldSerializer(instance.fields, many=True) data.update( - {"states": states_serializer.data, "fields": fields_serializer.data, } + { + "states": states_serializer.data, + "fields": fields_serializer.data, + } ) return data diff --git a/itsm/workflow/signals/__init__.py b/itsm/workflow/signals/__init__.py index a13f844c5..a2f858ce4 100644 --- a/itsm/workflow/signals/__init__.py +++ b/itsm/workflow/signals/__init__.py @@ -25,5 +25,5 @@ from django.dispatch import Signal -state_created = Signal(providing_args=["flow_id", "state_id", "state_type"]) -state_deleted = Signal(providing_args=["flow_id", "state_id"]) +state_created = Signal() # providing_args=["flow_id", "state_id", "state_type"] +state_deleted = Signal() # providing_args=["flow_id", "state_id"] diff --git a/itsm/workflow/tasks.py b/itsm/workflow/tasks.py index d87e93aad..bdd47f799 100644 --- a/itsm/workflow/tasks.py +++ b/itsm/workflow/tasks.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from celery.schedules import crontab -from celery.task import periodic_task +from blueapps.contrib.celery_tools.periodic import periodic_task from common.log import logger from itsm.component.constants import PUBLIC_PROJECT_PROJECT_KEY diff --git a/itsm/workflow/utils.py b/itsm/workflow/utils.py index d2233283c..933fd775c 100644 --- a/itsm/workflow/utils.py +++ b/itsm/workflow/utils.py @@ -24,7 +24,7 @@ """ from django.conf import settings from django.db.models import Q -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from itsm.component.constants import ( diff --git a/itsm/workflow/validators.py b/itsm/workflow/validators.py index 9c39bcb68..9022c31a2 100644 --- a/itsm/workflow/validators.py +++ b/itsm/workflow/validators.py @@ -27,7 +27,7 @@ import six from django.db.models import Q -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from common.log import logger from itsm.component.constants import ( @@ -155,7 +155,9 @@ def related_states_validate(field_key, workflow_states): filter_key in task_api_instance.req_body or filter_key in task_api_instance.req_params ): - raise ParamError(_("该字段正在被【{}】节点引用,请先取消引用").format(task_state.name)) + raise ParamError( + _("该字段正在被【{}】节点引用,请先取消引用").format(task_state.name) + ) sops_states = StateExtrasSerializer( workflow_states.filter(type=TASK_SOPS_STATE, is_draft=False), many=True @@ -172,7 +174,9 @@ def related_states_validate(field_key, workflow_states): ] ) if field_key in keys: - raise ParamError(_("该字段正在被【{}】节点引用,请先取消引用").format(state["name"])) + raise ParamError( + _("该字段正在被【{}】节点引用,请先取消引用").format(state["name"]) + ) def related_transitions_validate(field_key, workflow_transitions): @@ -195,7 +199,11 @@ def related_transitions_validate(field_key, workflow_transitions): for inside_exp in outside_exp["expressions"]: keys.append(inside_exp["key"]) if field_key in keys: - raise ParamError(_("该字段正作为【{}】线条的判断条件,请先取消引用").format(transition["name"])) + raise ParamError( + _("该字段正作为【{}】线条的判断条件,请先取消引用").format( + transition["name"] + ) + ) def show_conditions_validate(field_key, workflow_fields): @@ -211,7 +219,11 @@ def show_conditions_validate(field_key, workflow_fields): if field_key in [ exp["key"] for exp in condition["show_conditions"]["expressions"] ]: - raise ParamError(_("该字段正作为【{}】字段的显示条件,请先取消引用").format(condition["name"])) + raise ParamError( + _("该字段正作为【{}】字段的显示条件,请先取消引用").format( + condition["name"] + ) + ) def related_field_validate(field_key, workflow_fields): @@ -223,7 +235,9 @@ def related_field_validate(field_key, workflow_fields): related_fields = RelatedFieldSerializer(workflow_fields, many=True).data for field in related_fields: if field_key in field["related_fields"].get("rely_on", []): - raise ParamError(_("该字段正在被【{}】字段引用,请先取消引用").format(field["name"])) + raise ParamError( + _("该字段正在被【{}】字段引用,请先取消引用").format(field["name"]) + ) def template_fields_exists_validate(fields): @@ -236,7 +250,9 @@ def template_fields_exists_validate(fields): ) if set(fields).difference(template_fields): raise ParamError( - _("{}公共字段不存在,请联系管理员").format(list(set(fields).difference(template_fields))) + _("{}公共字段不存在,请联系管理员").format( + list(set(fields).difference(template_fields)) + ) ) @@ -248,7 +264,9 @@ def table_remove_fiels_validate(fields, table): table_fields = list(table.fields.values_list("id", flat=True)) if not set(fields).issubset(table_fields): raise ParamError( - _("{}公共字段不存在,请联系管理员").format(list(set(table_fields).difference(fields))) + _("{}公共字段不存在,请联系管理员").format( + list(set(table_fields).difference(fields)) + ) ) @@ -313,7 +331,9 @@ def trigger_validate(source_instance, source_type=SOURCE_WORKFLOW): if draft_triggers and source_type == SOURCE_WORKFLOW: raise WorkFlowInvalidError( [], - _("{source_type}【{source_name}】内的触发器【{trigger_name}】为草稿状态,无法部署").format( + _( + "{source_type}【{source_name}】内的触发器【{trigger_name}】为草稿状态,无法部署" + ).format( source_type=source_type_dict.get(source_type), source_name=source_instance.name, trigger_name=",".join(draft_triggers), @@ -336,7 +356,9 @@ def task_validate(self): ).values_list("name", flat=True) if draft_tasks: raise WorkFlowInvalidError( - [], _("流程内引用的任务模版【%s】为草稿状态,无法部署") % ",".join(draft_tasks) + [], + _("流程内引用的任务模版【%s】为草稿状态,无法部署") + % ",".join(draft_tasks), ) for task in TaskSchema.objects.filter(id__in=task_schema_ids): @@ -399,7 +421,11 @@ def flat_condition(expressions): ] conditions.extend( [ - {"condition": item.show_conditions, "name": item.name, "obj_type": "字段"} + { + "condition": item.show_conditions, + "name": item.name, + "obj_type": "字段", + } for item in self.instance.fields.all() if item.show_conditions ] @@ -441,13 +467,17 @@ def pipeline_validate(self): invalid_state_ids = [ int(self.states_map[k]) for arg in error.args for k, v in arg.items() ] - raise WorkFlowInvalidError(invalid_state_ids, _("当前流程画布连线不合理,请重新确认. ")) + raise WorkFlowInvalidError( + invalid_state_ids, _("当前流程画布连线不合理,请重新确认. ") + ) except ConvergeMatchError as error: invalid_state_ids = [int(self.states_map[error.gateway_id])] raise WorkFlowInvalidError(invalid_state_ids, str(error)) except StreamValidateError as error: invalid_state_ids = [int(self.states_map[error.node_id])] - raise WorkFlowInvalidError(invalid_state_ids, _("当前节点画布连线不合理,请重新确认. ")) + raise WorkFlowInvalidError( + invalid_state_ids, _("当前节点画布连线不合理,请重新确认. ") + ) except Exception as error: logger.exception(str(error)) raise WorkFlowInvalidError([], str(error)) @@ -568,7 +598,9 @@ def key_validate(self, value): flow_id=value.get("workflow").id, key=value.get("key") ).exists() ): - raise ParamError(_("当前流程已存在唯一标识【{}】,请重新输入").format(value.get("key"))) + raise ParamError( + _("当前流程已存在唯一标识【{}】,请重新输入").format(value.get("key")) + ) @staticmethod def custom_table_validate(value): @@ -611,7 +643,9 @@ def select_type_choice_validate(self, value): choice = value.get("choice", []) if not choice: raise ParamError( - _("【{0}】请输入自定义数据,换行分隔。").format(SELECT_TYPE_CHOICES[value.get("type")]) + _("【{0}】请输入自定义数据,换行分隔。").format( + SELECT_TYPE_CHOICES[value.get("type")] + ) ) for field in choice: name = field.get("name") @@ -632,7 +666,9 @@ def field_common_validate(name): if not name: raise ParamError(_("请输入选项值")) if len(name) > LEN_MIDDLE: - raise ParamError(_("自定义数据【{}】长度不能大于{}个字符").format(name, LEN_MIDDLE)) + raise ParamError( + _("自定义数据【{}】长度不能大于{}个字符").format(name, LEN_MIDDLE) + ) def field_key_validate(self, key): """ @@ -641,7 +677,9 @@ def field_key_validate(self, key): self.field_common_validate(key) if not re.match("^[_a-zA-Z0-9]*$", key) or len(key) > LEN_MIDDLE: raise ParamError( - _("自定义数据key值【{}】仅支持【英文、数字和下划线】,长度小于128字符,请重新输入").format(key) + _( + "自定义数据key值【{}】仅支持【英文、数字和下划线】,长度小于128字符,请重新输入" + ).format(key) ) @staticmethod @@ -690,7 +728,11 @@ def key_validate(self, value): ).exists(): if value.get("key") in [FIELD_TITLE, FIELD_BIZ]: raise ParamError(_("title, bk_biz_id 为内置唯一标识,请重新输入")) - raise ParamError(_("当前项目字段库已存在唯一标识【{}】,请重新输入").format(value.get("key"))) + raise ParamError( + _("当前项目字段库已存在唯一标识【{}】,请重新输入").format( + value.get("key") + ) + ) def name_validate(self, value): fields = TemplateField.objects.filter( @@ -701,7 +743,9 @@ def name_validate(self, value): if self.instance: fields = fields.exclude(id=self.instance.id) if fields.exists(): - raise ParamError(_("字段库已存在名称【{}】,请重新输入").format(value.get("name"))) + raise ParamError( + _("字段库已存在名称【{}】,请重新输入").format(value.get("name")) + ) def template_field_can_destroy(instance): @@ -710,7 +754,9 @@ def template_field_can_destroy(instance): if tables: table_names = ",".join([table.name for table in tables]) - raise ValidationError(_("字段已被基础模型[{}]引用,无法删除".format(table_names))) + raise ValidationError( + _("字段已被基础模型[{}]引用,无法删除".format(table_names)) + ) class StatePollValidator(object): @@ -838,13 +884,17 @@ def state_validate(self): and self.to_state.label == EMPTY ): if self.from_state.label.rfind(NORMAL_STATE_LABEL_PREFIX) == -1: - raise TransitionError(_("待连接的聚合网关与当前节点不在同一个工作区域内")) + raise TransitionError( + _("待连接的聚合网关与当前节点不在同一个工作区域内") + ) if ( self.from_state.type == COVERAGE_STATE == self.to_state.type and find_sub_string(self.from_state.label, ROUTER_STATE_LABEL_PREFIX) == GLOBAL_LABEL ): - raise TransitionError(_("待连接的聚合网关与当前节点不在同一个工作区域内")) + raise TransitionError( + _("待连接的聚合网关与当前节点不在同一个工作区域内") + ) return # 以下label不相等且可能多入多出的情况 diff --git a/itsm/workflow/views.py b/itsm/workflow/views.py index e0d709c69..f9e85a747 100644 --- a/itsm/workflow/views.py +++ b/itsm/workflow/views.py @@ -34,7 +34,7 @@ from django.db.models import Q from django.http import StreamingHttpResponse, FileResponse, Http404 from django.utils.encoding import escape_uri_path -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from rest_framework import serializers, status, permissions from rest_framework.decorators import action from rest_framework.exceptions import ValidationError @@ -47,7 +47,8 @@ DateTimeType, TimeType, BooleanType, - SelectMultipleType, SelectType, + SelectMultipleType, + SelectType, ) from common.log import logger from itsm.component.constants import ( @@ -192,10 +193,6 @@ class WorkflowViewSet( permission_free_actions = ["get_global_choices", "list"] permission_classes = (WorkflowIamAuth,) - def get_queryset(self): - self.queryset = self.queryset.exclude(flow_type="internal") - return self.queryset - @action(detail=False, methods=["get"]) def get_global_choices(self, request): """查询全局选项列表信息""" @@ -301,11 +298,11 @@ def exports(self, request, pk=None): response = FileResponse(json.dumps([data], cls=JsonEncoder, indent=2)) response["Content-Type"] = "application/octet-stream" # 中文文件名乱码问题 - response[ - "Content-Disposition" - ] = "attachment; filename*=UTF-8''bk_itsm_{}_{}.json".format( - escape_uri_path(workflow.name), - create_version_number(), + response["Content-Disposition"] = ( + "attachment; filename*=UTF-8''bk_itsm_{}_{}.json".format( + escape_uri_path(workflow.name), + create_version_number(), + ) ) return response @@ -540,7 +537,7 @@ def get_iam_resource_id(self): if self.action == "batch_update": return self.request.data.get("workflow_id") return None - + def perform_destroy(self, instance): # 从开始节点出来的连线只能由一条, 且不能被删除 if instance.from_state.type == START_STATE: @@ -586,7 +583,9 @@ def download_file(self, request, *args, **kwargs): field_object = self.get_object() if field_object.type != "FILE": - raise serializers.ValidationError(_("当前字段非附件字段,无法下载附件文件!")) + raise serializers.ValidationError( + _("当前字段非附件字段,无法下载附件文件!") + ) try: files = ( field_object.choice @@ -595,7 +594,9 @@ def download_file(self, request, *args, **kwargs): ) except Exception: logger.exception("json解析错误") - raise serializers.ValidationError(_("当前字段解析信息出错,请确认是否已进行数据升级!")) + raise serializers.ValidationError( + _("当前字段解析信息出错,请确认是否已进行数据升级!") + ) file_info = files.get(unique_key) if not file_info: @@ -606,7 +607,9 @@ def download_file(self, request, *args, **kwargs): if not store.exists(file_path): raise serializers.ValidationError( - _("要下载的文件【{}】不存在, 可能已经被删除,请与管理员确认!").format(file_info["name"]) + _("要下载的文件【{}】不存在, 可能已经被删除,请与管理员确认!").format( + file_info["name"] + ) ) response = StreamingHttpResponse(FileWrapper(store.open(file_path, "rb"), 512)) @@ -750,7 +753,7 @@ class TemplateFieldViewSet(component_viewsets.ModelViewSet): "destroy": "field_delete", "update": "field_edit", } - + filter_fields = { "id": ["in"], "key": ["exact", "in", "contains", "startswith"], @@ -1102,7 +1105,7 @@ class TaskSchemaViewSet(DynamicListModelMixin, component_viewsets.ModelViewSet): permission_action_platform = "public_task_template_manage" permission_create_action = ["create", "clone"] permission_resource_is_project = True - + pagination_class = None def update(self, request, *args, **kwargs): @@ -1116,7 +1119,9 @@ def update(self, request, *args, **kwargs): isinstance(task_fields, dict) and isinstance(task_fields["task_field_ids"], list) ): - raise serializers.ValidationError(_("任务字段排序参数不合法,请联系管理员")) + raise serializers.ValidationError( + _("任务字段排序参数不合法,请联系管理员") + ) ordering = "FIELD(`id`, {})".format( ",".join( diff --git a/pipeline/component_framework/models.py b/pipeline/component_framework/models.py index 39fc13e77..988b5783c 100644 --- a/pipeline/component_framework/models.py +++ b/pipeline/component_framework/models.py @@ -12,7 +12,7 @@ """ from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.component_framework.constants import LEGACY_PLUGINS_VERSION from pipeline.component_framework.library import ComponentLibrary @@ -40,7 +40,9 @@ class ComponentModel(models.Model): """ code = models.CharField(_("组件编码"), max_length=255) - version = models.CharField(_("组件版本"), max_length=64, default=LEGACY_PLUGINS_VERSION) + version = models.CharField( + _("组件版本"), max_length=64, default=LEGACY_PLUGINS_VERSION + ) name = models.CharField(_("组件名称"), max_length=255) status = models.BooleanField(_("组件是否可用"), default=True) diff --git a/pipeline/contrib/external_plugins/models/base.py b/pipeline/contrib/external_plugins/models/base.py index 986eb72a9..f1544a072 100644 --- a/pipeline/contrib/external_plugins/models/base.py +++ b/pipeline/contrib/external_plugins/models/base.py @@ -17,7 +17,7 @@ from copy import deepcopy from django.db import IntegrityError, models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.component_framework.library import ComponentLibrary from pipeline.contrib.external_plugins import exceptions @@ -38,14 +38,18 @@ def package_source(cls): class SourceManager(models.Manager): def create_source(self, name, packages, from_config, **kwargs): create_kwargs = deepcopy(kwargs) - create_kwargs.update({"name": name, "packages": packages, "from_config": from_config}) + create_kwargs.update( + {"name": name, "packages": packages, "from_config": from_config} + ) return self.create(**create_kwargs) def remove_source(self, source_id): source = self.get(id=source_id) if source.from_config: - raise exceptions.InvalidOperationException("Can not remove source create from config") + raise exceptions.InvalidOperationException( + "Can not remove source create from config" + ) source.delete() @@ -66,11 +70,15 @@ def update_source_from_config(self, configs): defaults["packages"] = config["packages"] try: - self.update_or_create(name=config["name"], from_config=True, defaults=defaults) + self.update_or_create( + name=config["name"], from_config=True, defaults=defaults + ) except IntegrityError: raise exceptions.InvalidOperationException( 'There is a external source named "{source_name}" but not create from config, ' - "can not do source update operation".format(source_name=config["name"]) + "can not do source update operation".format( + source_name=config["name"] + ) ) @@ -103,11 +111,20 @@ def imported_plugins(self): try: importer = self.importer() except ValueError as e: - logger.exception("ExternalPackageSource[name={}] call importer error: {}".format(self.name, e)) + logger.exception( + "ExternalPackageSource[name={}] call importer error: {}".format( + self.name, e + ) + ) return plugins for component in ComponentLibrary.component_list(): - component_importer = getattr(sys.modules[component.__module__], "__loader__", None) - if isinstance(component_importer, type(importer)) and component_importer.name == self.name: + component_importer = getattr( + sys.modules[component.__module__], "__loader__", None + ) + if ( + isinstance(component_importer, type(importer)) + and component_importer.name == self.name + ): plugins.append( { "code": component.code, @@ -130,7 +147,9 @@ def modules(self): @staticmethod def update_package_source_from_config(source_configs): - classified_config = {source_type: [] for source_type in list(source_cls_factory.keys())} + classified_config = { + source_type: [] for source_type in list(source_cls_factory.keys()) + } for config in deepcopy(source_configs): classified_config.setdefault(config.pop("type"), []).append(config) diff --git a/pipeline/contrib/external_plugins/models/source.py b/pipeline/contrib/external_plugins/models/source.py index 8aae5d918..d876fa354 100644 --- a/pipeline/contrib/external_plugins/models/source.py +++ b/pipeline/contrib/external_plugins/models/source.py @@ -12,11 +12,21 @@ """ from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.conf import settings -from pipeline.contrib.external_plugins.models.base import FILE_SYSTEM, GIT, S3, ExternalPackageSource, package_source -from pipeline.contrib.external_plugins.utils.importer import FSModuleImporter, GitRepoModuleImporter, S3ModuleImporter +from pipeline.contrib.external_plugins.models.base import ( + FILE_SYSTEM, + GIT, + S3, + ExternalPackageSource, + package_source, +) +from pipeline.contrib.external_plugins.utils.importer import ( + FSModuleImporter, + GitRepoModuleImporter, + S3ModuleImporter, +) @package_source @@ -82,7 +92,9 @@ def type(): return FILE_SYSTEM def importer(self): - return FSModuleImporter(name=self.name, modules=list(self.packages.keys()), path=self.path) + return FSModuleImporter( + name=self.name, modules=list(self.packages.keys()), path=self.path + ) def details(self): return {"path": self.path} diff --git a/pipeline/contrib/periodic_task/models.py b/pipeline/contrib/periodic_task/models.py index 929a28e47..282768b51 100644 --- a/pipeline/contrib/periodic_task/models.py +++ b/pipeline/contrib/periodic_task/models.py @@ -19,7 +19,7 @@ from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.db import models from django.db.models import signals -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.constants import PIPELINE_DEFAULT_PRIORITY from pipeline.contrib.periodic_task.djcelery import managers @@ -28,7 +28,12 @@ from pipeline.contrib.periodic_task.djcelery.utils import now from pipeline.contrib.periodic_task.signals import periodic_task_start_failed from pipeline.exceptions import InvalidOperationException -from pipeline.models import CompressJSONField, PipelineInstance, PipelineTemplate, Snapshot +from pipeline.models import ( + CompressJSONField, + PipelineInstance, + PipelineTemplate, + Snapshot, +) from pipeline.utils.uniqid import uniqid PERIOD_CHOICES = ( @@ -43,7 +48,11 @@ @python_2_unicode_compatible class IntervalSchedule(models.Model): every = models.IntegerField(_("every"), null=False) - period = models.CharField(_("period"), max_length=24, choices=PERIOD_CHOICES,) + period = models.CharField( + _("period"), + max_length=24, + choices=PERIOD_CHOICES, + ) class Meta: verbose_name = _("interval") @@ -83,9 +92,21 @@ def cronexp(field): class CrontabSchedule(models.Model): minute = models.CharField(_("minute"), max_length=64, default="*") hour = models.CharField(_("hour"), max_length=64, default="*") - day_of_week = models.CharField(_("day of week"), max_length=64, default="*",) - day_of_month = models.CharField(_("day of month"), max_length=64, default="*",) - month_of_year = models.CharField(_("month of year"), max_length=64, default="*",) + day_of_week = models.CharField( + _("day of week"), + max_length=64, + default="*", + ) + day_of_month = models.CharField( + _("day of month"), + max_length=64, + default="*", + ) + month_of_year = models.CharField( + _("month of year"), + max_length=64, + default="*", + ) timezone = timezone_field.TimeZoneField(default="UTC") class Meta: @@ -153,10 +174,19 @@ def last_change(cls): @python_2_unicode_compatible class DjCeleryPeriodicTask(models.Model): - name = models.CharField(_("name"), max_length=200, unique=True, help_text=_("Useful description"),) + name = models.CharField( + _("name"), + max_length=200, + unique=True, + help_text=_("Useful description"), + ) task = models.CharField(_("task name"), max_length=200) interval = models.ForeignKey( - IntervalSchedule, null=True, blank=True, verbose_name=_("interval"), on_delete=models.CASCADE, + IntervalSchedule, + null=True, + blank=True, + verbose_name=_("interval"), + on_delete=models.CASCADE, ) crontab = models.ForeignKey( CrontabSchedule, @@ -166,19 +196,60 @@ class DjCeleryPeriodicTask(models.Model): on_delete=models.CASCADE, help_text=_("Use one of interval/crontab"), ) - args = models.TextField(_("Arguments"), blank=True, default="[]", help_text=_("JSON encoded positional arguments"),) + args = models.TextField( + _("Arguments"), + blank=True, + default="[]", + help_text=_("JSON encoded positional arguments"), + ) kwargs = models.TextField( - _("Keyword arguments"), blank=True, default="{}", help_text=_("JSON encoded keyword arguments"), + _("Keyword arguments"), + blank=True, + default="{}", + help_text=_("JSON encoded keyword arguments"), ) queue = models.CharField( - _("queue"), max_length=200, blank=True, null=True, default=None, help_text=_("Queue defined in CELERY_QUEUES"), + _("queue"), + max_length=200, + blank=True, + null=True, + default=None, + help_text=_("Queue defined in CELERY_QUEUES"), + ) + exchange = models.CharField( + _("exchange"), + max_length=200, + blank=True, + null=True, + default=None, + ) + routing_key = models.CharField( + _("routing key"), + max_length=200, + blank=True, + null=True, + default=None, + ) + expires = models.DateTimeField( + _("expires"), + blank=True, + null=True, + ) + enabled = models.BooleanField( + _("enabled"), + default=True, + ) + last_run_at = models.DateTimeField( + auto_now=False, + auto_now_add=False, + editable=False, + blank=True, + null=True, + ) + total_run_count = models.PositiveIntegerField( + default=0, + editable=False, ) - exchange = models.CharField(_("exchange"), max_length=200, blank=True, null=True, default=None,) - routing_key = models.CharField(_("routing key"), max_length=200, blank=True, null=True, default=None,) - expires = models.DateTimeField(_("expires"), blank=True, null=True,) - enabled = models.BooleanField(_("enabled"), default=True,) - last_run_at = models.DateTimeField(auto_now=False, auto_now_add=False, editable=False, blank=True, null=True,) - total_run_count = models.PositiveIntegerField(default=0, editable=False,) date_changed = models.DateTimeField(auto_now=True) description = models.TextField(_("description"), blank=True) @@ -192,9 +263,13 @@ class Meta: def validate_unique(self, *args, **kwargs): super(DjCeleryPeriodicTask, self).validate_unique(*args, **kwargs) if not self.interval and not self.crontab: - raise ValidationError({"interval": ["One of interval or crontab must be set."]}) + raise ValidationError( + {"interval": ["One of interval or crontab must be set."]} + ) if self.interval and self.crontab: - raise ValidationError({"crontab": ["Only one of interval or crontab must be set"]}) + raise ValidationError( + {"crontab": ["Only one of interval or crontab must be set"]} + ) def save(self, *args, **kwargs): self.exchange = self.exchange or None @@ -286,10 +361,16 @@ class PeriodicTask(models.Model): ) cron = models.CharField(_("调度策略"), max_length=128) celery_task = models.ForeignKey( - DjCeleryPeriodicTask, verbose_name=_("celery 周期任务实例"), null=True, on_delete=models.SET_NULL + DjCeleryPeriodicTask, + verbose_name=_("celery 周期任务实例"), + null=True, + on_delete=models.SET_NULL, ) snapshot = models.ForeignKey( - Snapshot, related_name="periodic_tasks", verbose_name=_("用于创建流程实例的结构数据"), on_delete=models.DO_NOTHING + Snapshot, + related_name="periodic_tasks", + verbose_name=_("用于创建流程实例的结构数据"), + on_delete=models.DO_NOTHING, ) total_run_count = models.PositiveIntegerField(_("执行次数"), default=0) last_run_at = models.DateTimeField(_("上次运行时间"), null=True) @@ -350,7 +431,9 @@ def modify_cron(self, cron, timezone=None): def modify_constants(self, constants): if self.enabled: - raise InvalidOperationException("can not modify constants when task is enabled") + raise InvalidOperationException( + "can not modify constants when task is enabled" + ) exec_data = self.execution_data for key, value in list(constants.items()): if key in exec_data["constants"]: @@ -361,7 +444,9 @@ def modify_constants(self, constants): class PeriodicTaskHistoryManager(models.Manager): - def record_schedule(self, periodic_task, pipeline_instance, ex_data, start_success=True): + def record_schedule( + self, periodic_task, pipeline_instance, ex_data, start_success=True + ): history = self.create( periodic_task=periodic_task, pipeline_instance=pipeline_instance, @@ -372,14 +457,20 @@ def record_schedule(self, periodic_task, pipeline_instance, ex_data, start_succe ) if not start_success: - periodic_task_start_failed.send(sender=PeriodicTask, periodic_task=periodic_task, history=history) + periodic_task_start_failed.send( + sender=PeriodicTask, periodic_task=periodic_task, history=history + ) return history class PeriodicTaskHistory(models.Model): periodic_task = models.ForeignKey( - PeriodicTask, related_name="instance_rel", verbose_name=_("周期任务"), null=True, on_delete=models.DO_NOTHING + PeriodicTask, + related_name="instance_rel", + verbose_name=_("周期任务"), + null=True, + on_delete=models.DO_NOTHING, ) pipeline_instance = models.ForeignKey( PipelineInstance, diff --git a/pipeline/contrib/periodic_task/patch/djcelery/djcelery_patch.py b/pipeline/contrib/periodic_task/patch/djcelery/djcelery_patch.py index 7a4220fd6..123d187d1 100644 --- a/pipeline/contrib/periodic_task/patch/djcelery/djcelery_patch.py +++ b/pipeline/contrib/periodic_task/patch/djcelery/djcelery_patch.py @@ -14,7 +14,7 @@ import datetime import logging -from anyjson import dumps, loads +import json from celery import current_app, schedules from celery.utils.log import get_logger @@ -54,7 +54,9 @@ def djcelry_upgrade(): return # insert djcelery migration record - cursor.execute("select * from `django_migrations` where app='djcelery' and name='0001_initial';") + cursor.execute( + "select * from `django_migrations` where app='djcelery' and name='0001_initial';" + ) row = cursor.fetchall() if not row: cursor.execute( @@ -86,10 +88,12 @@ def __init__(self, model): logger.warning("Disabling %s", self.name) self._disable(model) try: - self.args = loads(model.args or "[]") - self.kwargs = loads(model.kwargs or "{}") + self.args = json.loads(model.args or "[]") + self.kwargs = json.loads(model.kwargs or "{}") except ValueError: - logging.error("Failed to serialize arguments for %s.", self.name, exc_info=1) + logging.error( + "Failed to serialize arguments for %s.", self.name, exc_info=1 + ) logging.warning("Disabling %s", self.name) self._disable(model) @@ -123,12 +127,15 @@ def from_entry(cls, name, skip_fields=("relative", "options"), **entry): fields[t[2]] = None fields[model_field] = model_schedule - fields["args"] = dumps(fields.get("args") or []) - fields["kwargs"] = dumps(fields.get("kwargs") or {}) + fields["args"] = json.dumps(fields.get("args") or []) + fields["kwargs"] = json.dumps(fields.get("kwargs") or {}) fields["queue"] = options.get("queue") fields["exchange"] = options.get("exchange") fields["routing_key"] = options.get("routing_key") - obj, _ = DjCeleryPeriodicTask._default_manager.update_or_create(name=name, defaults=fields,) + obj, _ = DjCeleryPeriodicTask._default_manager.update_or_create( + name=name, + defaults=fields, + ) return cls(obj) @@ -136,7 +143,9 @@ def from_entry(cls, name, skip_fields=("relative", "options"), **entry): def create_or_update_task(cls, name, **schedule_dict): if "schedule" not in schedule_dict: try: - schedule_dict["schedule"] = DjCeleryPeriodicTask._default_manager.get(name=name).schedule + schedule_dict["schedule"] = DjCeleryPeriodicTask._default_manager.get( + name=name + ).schedule except DjCeleryPeriodicTask.DoesNotExist: pass cls.Entry.from_entry(name, **schedule_dict) diff --git a/pipeline/contrib/periodic_task/signals/__init__.py b/pipeline/contrib/periodic_task/signals/__init__.py index bebb9add9..658418deb 100644 --- a/pipeline/contrib/periodic_task/signals/__init__.py +++ b/pipeline/contrib/periodic_task/signals/__init__.py @@ -13,6 +13,10 @@ from django.dispatch import Signal -pre_periodic_task_start = Signal(providing_args=["periodic_task", "pipeline_instance"]) -post_periodic_task_start = Signal(providing_args=["periodic_task", "pipeline_instance"]) -periodic_task_start_failed = Signal(providing_args=["periodic_task", "history"]) +pre_periodic_task_start = ( + Signal() +) # providing_args=["periodic_task", "pipeline_instance"] +post_periodic_task_start = ( + Signal() +) # providing_args=["periodic_task", "pipeline_instance"] +periodic_task_start_failed = Signal() # providing_args=["periodic_task", "history"] diff --git a/pipeline/contrib/periodic_task/tasks.py b/pipeline/contrib/periodic_task/tasks.py index a9d93fc4b..11a88b19a 100644 --- a/pipeline/contrib/periodic_task/tasks.py +++ b/pipeline/contrib/periodic_task/tasks.py @@ -16,7 +16,7 @@ import traceback import pytz -from celery import task +from celery import shared_task from django.utils import timezone from pipeline.contrib.periodic_task import signals @@ -27,7 +27,7 @@ logger = logging.getLogger("celery") -@task(ignore_result=True) +@shared_task(ignore_result=True) def periodic_task_start(*args, **kwargs): try: periodic_task = PeriodicTask.objects.get(id=kwargs["period_task_id"]) @@ -61,25 +61,38 @@ def periodic_task_start(*args, **kwargs): ) result = instance.start( - periodic_task.creator, check_workers=False, priority=periodic_task.priority, queue=periodic_task.queue + periodic_task.creator, + check_workers=False, + priority=periodic_task.priority, + queue=periodic_task.queue, ) except Exception: et = traceback.format_exc() logger.error(et) PeriodicTaskHistory.objects.record_schedule( - periodic_task=periodic_task, pipeline_instance=None, ex_data=et, start_success=False + periodic_task=periodic_task, + pipeline_instance=None, + ex_data=et, + start_success=False, ) return if not result.result: PeriodicTaskHistory.objects.record_schedule( - periodic_task=periodic_task, pipeline_instance=None, ex_data=result.message, start_success=False + periodic_task=periodic_task, + pipeline_instance=None, + ex_data=result.message, + start_success=False, ) return periodic_task.total_run_count += 1 periodic_task.last_run_at = timezone.now() periodic_task.save() - signals.post_periodic_task_start.send(sender=PeriodicTask, periodic_task=periodic_task, pipeline_instance=instance) + signals.post_periodic_task_start.send( + sender=PeriodicTask, periodic_task=periodic_task, pipeline_instance=instance + ) - PeriodicTaskHistory.objects.record_schedule(periodic_task=periodic_task, pipeline_instance=instance, ex_data="") + PeriodicTaskHistory.objects.record_schedule( + periodic_task=periodic_task, pipeline_instance=instance, ex_data="" + ) diff --git a/pipeline/contrib/statistics/models.py b/pipeline/contrib/statistics/models.py index 2a8c985f3..0e1415bd1 100644 --- a/pipeline/contrib/statistics/models.py +++ b/pipeline/contrib/statistics/models.py @@ -12,7 +12,7 @@ """ from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class ComponentInTemplate(models.Model): @@ -20,7 +20,9 @@ class ComponentInTemplate(models.Model): template_id = models.CharField(_("模板ID"), max_length=32) node_id = models.CharField(_("节点ID"), max_length=32) is_sub = models.BooleanField(_("是否子流程引用"), default=False) - subprocess_stack = models.TextField(_("子流程堆栈"), default="[]", help_text=_("JSON 格式的列表")) + subprocess_stack = models.TextField( + _("子流程堆栈"), default="[]", help_text=_("JSON 格式的列表") + ) version = models.CharField(_("插件版本"), max_length=255, default="legacy") class Meta: @@ -36,9 +38,13 @@ class ComponentExecuteData(models.Model): instance_id = models.CharField(_("实例ID"), max_length=32) node_id = models.CharField(_("节点ID"), max_length=32) is_sub = models.BooleanField(_("是否子流程引用"), default=False) - subprocess_stack = models.TextField(_("子流程堆栈"), default="[]", help_text=_("JSON 格式的列表")) + subprocess_stack = models.TextField( + _("子流程堆栈"), default="[]", help_text=_("JSON 格式的列表") + ) started_time = models.DateTimeField(_("标准插件执行开始时间")) - archived_time = models.DateTimeField(_("标准插件执行结束时间"), null=True, blank=True) + archived_time = models.DateTimeField( + _("标准插件执行结束时间"), null=True, blank=True + ) elapsed_time = models.IntegerField(_("标准插件执行耗时(s)"), null=True, blank=True) status = models.BooleanField(_("是否执行成功"), default=False) is_skip = models.BooleanField(_("是否跳过"), default=False) @@ -65,7 +71,12 @@ class Meta: verbose_name_plural = _("Pipeline模板引用数据") def __unicode__(self): - return "{}_{}_{}_{}".format(self.template_id, self.atom_total, self.subprocess_total, self.gateways_total) + return "{}_{}_{}_{}".format( + self.template_id, + self.atom_total, + self.subprocess_total, + self.gateways_total, + ) class InstanceInPipeline(models.Model): @@ -79,4 +90,9 @@ class Meta: verbose_name_plural = _("Pipeline实例引用数据") def __unicode__(self): - return "{}_{}_{}_{}".format(self.instance_id, self.atom_total, self.subprocess_total, self.gateways_total) + return "{}_{}_{}_{}".format( + self.instance_id, + self.atom_total, + self.subprocess_total, + self.gateways_total, + ) diff --git a/pipeline/core/flow/activity/service_activity.py b/pipeline/core/flow/activity/service_activity.py index e3264c9ee..abdbdfa37 100644 --- a/pipeline/core/flow/activity/service_activity.py +++ b/pipeline/core/flow/activity/service_activity.py @@ -14,11 +14,16 @@ from abc import ABCMeta, abstractmethod from copy import deepcopy -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.builder import empty_data from pipeline.core.flow.activity.base import Activity -from pipeline.core.flow.io import BooleanItemSchema, InputItem, IntItemSchema, OutputItem +from pipeline.core.flow.io import ( + BooleanItemSchema, + InputItem, + IntItemSchema, + OutputItem, +) from pipeline.utils.utils import convert_bytes_to_str @@ -30,8 +35,18 @@ class Service(object, metaclass=ABCMeta): OutputItem = OutputItem interval = None default_outputs = [ - OutputItem(name=_("执行结果"), key="_result", type="bool", schema=BooleanItemSchema(description=_("是否执行成功"))), - OutputItem(name=_("循环次数"), key="_loop", type="int", schema=IntItemSchema(description=_("循环执行次数"))), + OutputItem( + name=_("执行结果"), + key="_result", + type="bool", + schema=BooleanItemSchema(description=_("是否执行成功")), + ), + OutputItem( + name=_("循环次数"), + key="_loop", + type="int", + schema=IntItemSchema(description=_("循环执行次数")), + ), ] def __init__(self, name=None): @@ -234,13 +249,13 @@ def next(self): class DefaultIntervalGenerator(AbstractIntervalGenerator): def next(self): super(DefaultIntervalGenerator, self).next() - return self.count ** 2 + return self.count**2 class SquareIntervalGenerator(AbstractIntervalGenerator): def next(self): super(SquareIntervalGenerator, self).next() - return self.count ** 2 + return self.count**2 class NullIntervalGenerator(AbstractIntervalGenerator): diff --git a/pipeline/core/flow/io.py b/pipeline/core/flow/io.py index 05661e2d5..329f59585 100644 --- a/pipeline/core/flow/io.py +++ b/pipeline/core/flow/io.py @@ -12,7 +12,7 @@ """ import abc -from collections import Mapping +from collections.abc import Mapping class DataItem(object, metaclass=abc.ABCMeta): @@ -91,7 +91,9 @@ def _type(cls): class ArrayItemSchema(ItemSchema): def __init__(self, item_schema, *args, **kwargs): if not isinstance(item_schema, ItemSchema): - raise TypeError("item_schema of ArrayItemSchema must be subclass of ItemSchema") + raise TypeError( + "item_schema of ArrayItemSchema must be subclass of ItemSchema" + ) self.item_schema = item_schema super(ArrayItemSchema, self).__init__(*args, **kwargs) @@ -110,15 +112,22 @@ def __init__(self, property_schemas, *args, **kwargs): if not isinstance(property_schemas, Mapping): raise TypeError("property_schemas of ObjectItemSchema must be Mapping type") - if not all([isinstance(value, ItemSchema) for value in list(property_schemas.values())]): - raise TypeError("value in property_schemas of ObjectItemSchema must be subclass of ItemSchema") + if not all( + [isinstance(value, ItemSchema) for value in list(property_schemas.values())] + ): + raise TypeError( + "value in property_schemas of ObjectItemSchema must be subclass of ItemSchema" + ) self.property_schemas = property_schemas super(ObjectItemSchema, self).__init__(*args, **kwargs) def as_dict(self): base = super(ObjectItemSchema, self).as_dict() - properties = {prop: schema.as_dict() for prop, schema in list(self.property_schemas.items())} + properties = { + prop: schema.as_dict() + for prop, schema in list(self.property_schemas.items()) + } base["properties"] = properties return base diff --git a/pipeline/core/flow/signals.py b/pipeline/core/flow/signals.py index 62dd35660..f63af1e0c 100644 --- a/pipeline/core/flow/signals.py +++ b/pipeline/core/flow/signals.py @@ -13,4 +13,4 @@ from django.dispatch import Signal -post_new_end_event_register = Signal(providing_args=["node_type", "node_cls"]) +post_new_end_event_register = Signal() # providing_args=["node_type", "node_cls"] diff --git a/pipeline/core/signals/__init__.py b/pipeline/core/signals/__init__.py index 291cc6621..7caa3b5bd 100644 --- a/pipeline/core/signals/__init__.py +++ b/pipeline/core/signals/__init__.py @@ -13,4 +13,4 @@ from django.dispatch import Signal -pre_variable_register = Signal(providing_args=["variable_code"]) +pre_variable_register = Signal() # providing_args=["variable_code"] diff --git a/pipeline/django_signal_valve/models.py b/pipeline/django_signal_valve/models.py index 359559873..2e96eb512 100644 --- a/pipeline/django_signal_valve/models.py +++ b/pipeline/django_signal_valve/models.py @@ -15,7 +15,7 @@ import pickle from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class IOField(models.BinaryField): diff --git a/pipeline/engine/admin.py b/pipeline/engine/admin.py index 21318da13..71ca14878 100644 --- a/pipeline/engine/admin.py +++ b/pipeline/engine/admin.py @@ -12,7 +12,7 @@ """ from django.contrib import admin -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.engine import models from pipeline.engine.conf.function_switch import FREEZE_ENGINE diff --git a/pipeline/engine/conf/function_switch.py b/pipeline/engine/conf/function_switch.py index a678f47aa..c88e3da0e 100644 --- a/pipeline/engine/conf/function_switch.py +++ b/pipeline/engine/conf/function_switch.py @@ -11,10 +11,16 @@ specific language governing permissions and limitations under the License. """ -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ FREEZE_ENGINE = "FREEZE_ENGINE" switch_list = [ - {"name": FREEZE_ENGINE, "description": _("用于冻结引擎, 冻结期间会屏蔽所有内部信号及暂停所有进程,同时拒绝所有流程控制请求"), "is_active": False} + { + "name": FREEZE_ENGINE, + "description": _( + "用于冻结引擎, 冻结期间会屏蔽所有内部信号及暂停所有进程,同时拒绝所有流程控制请求" + ), + "is_active": False, + } ] diff --git a/pipeline/engine/models/core.py b/pipeline/engine/models/core.py index 51d4ad67a..b065f0ddc 100644 --- a/pipeline/engine/models/core.py +++ b/pipeline/engine/models/core.py @@ -17,10 +17,10 @@ import traceback from celery import current_app -from celery.task.control import revoke + from django.db import models, transaction from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.conf import settings as pipeline_settings from pipeline.constants import PIPELINE_DEFAULT_PRIORITY @@ -34,6 +34,7 @@ from pipeline.log.models import LogEntry from pipeline.utils.uniqid import node_uniqid, uniqid +revoke = current_app.control.revoke logger = logging.getLogger("celery") RERUN_MAX_LIMIT = pipeline_settings.PIPELINE_RERUN_MAX_TIMES @@ -230,13 +231,21 @@ class PipelineProcess(models.Model): current_node_id = models.CharField( _("当前推进到的节点的 ID"), max_length=32, default="", db_index=True ) - destination_id = models.CharField(_("遇到该 ID 的节点就停止推进"), max_length=32, default="") + destination_id = models.CharField( + _("遇到该 ID 的节点就停止推进"), max_length=32, default="" + ) parent_id = models.CharField(_("父 process 的 ID"), max_length=32, default="") ack_num = models.IntegerField(_("收到子节点 ACK 的数量"), default=0) need_ack = models.IntegerField(_("需要收到的子节点 ACK 的数量"), default=-1) - is_alive = models.BooleanField(_("该 process 是否还有效"), default=True, db_index=True) - is_sleep = models.BooleanField(_("该 process 是否正在休眠"), default=False, db_index=True) - is_frozen = models.BooleanField(_("该 process 是否被冻结"), default=False, db_index=True) + is_alive = models.BooleanField( + _("该 process 是否还有效"), default=True, db_index=True + ) + is_sleep = models.BooleanField( + _("该 process 是否正在休眠"), default=False, db_index=True + ) + is_frozen = models.BooleanField( + _("该 process 是否被冻结"), default=False, db_index=True + ) snapshot = models.ForeignKey(ProcessSnapshot, null=True, on_delete=models.SET_NULL) objects = ProcessManager() @@ -689,7 +698,9 @@ class NodeRelationship(models.Model): def __unicode__(self): return str( "#{} -({})-> #{}".format( - self.ancestor_id, self.distance, self.descendant_id, + self.ancestor_id, + self.distance, + self.descendant_id, ) ) @@ -1165,7 +1176,10 @@ class ScheduleService(models.Model): SCHEDULE_ID_SPLIT_DIVISION = 32 id = models.CharField( - _("ID 节点ID+version"), max_length=NAME_MAX_LENGTH, unique=True, primary_key=True + _("ID 节点ID+version"), + max_length=NAME_MAX_LENGTH, + unique=True, + primary_key=True, ) activity_id = models.CharField(_("节点 ID"), max_length=32, db_index=True) process_id = models.CharField(_("Pipeline 进程 ID"), max_length=32) @@ -1176,7 +1190,9 @@ class ScheduleService(models.Model): service_act = IOField(verbose_name=_("待调度服务")) is_finished = models.BooleanField(_("是否已完成"), default=False) version = models.CharField(_("Activity 的版本"), max_length=32, db_index=True) - is_scheduling = models.BooleanField(_("是否正在被调度"), default=False, db_index=True) + is_scheduling = models.BooleanField( + _("是否正在被调度"), default=False, db_index=True + ) objects = ScheduleServiceManager() diff --git a/pipeline/engine/models/data.py b/pipeline/engine/models/data.py index c26fcd2fb..d15dfc25e 100644 --- a/pipeline/engine/models/data.py +++ b/pipeline/engine/models/data.py @@ -12,7 +12,7 @@ """ from django.db import models, transaction -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.engine.models.fields import IOField diff --git a/pipeline/engine/models/function.py b/pipeline/engine/models/function.py index 003a828e5..d6429e4eb 100644 --- a/pipeline/engine/models/function.py +++ b/pipeline/engine/models/function.py @@ -15,7 +15,7 @@ import traceback from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.engine.conf import function_switch @@ -31,11 +31,15 @@ def init_db(self): if switch["name"] not in name_set: s_to_be_created.append( FunctionSwitch( - name=switch["name"], description=switch["description"], is_active=switch["is_active"] + name=switch["name"], + description=switch["description"], + is_active=switch["is_active"], ) ) else: - self.filter(name=switch["name"]).update(description=switch["description"]) + self.filter(name=switch["name"]).update( + description=switch["description"] + ) self.bulk_create(s_to_be_created) except Exception: logger.error("function switch init failed: %s" % traceback.format_exc()) diff --git a/pipeline/engine/signals/__init__.py b/pipeline/engine/signals/__init__.py index 9bb510f31..e2fa6e7d3 100644 --- a/pipeline/engine/signals/__init__.py +++ b/pipeline/engine/signals/__init__.py @@ -13,23 +13,37 @@ from django.dispatch import Signal -pipeline_ready = Signal(providing_args=["process_id"]) -pipeline_end = Signal(providing_args=["root_pipeline_id"]) -pipeline_revoke = Signal(providing_args=["root_pipeline_id"]) -child_process_ready = Signal(providing_args=["child_id"]) -process_ready = Signal(providing_args=["parent_id", "current_node_id", "call_from_child"]) -batch_process_ready = Signal(providing_args=["process_id_list", "pipeline_id"]) -wake_from_schedule = Signal(providing_args=["process_id, activity_id"]) -schedule_ready = Signal(providing_args=["schedule_id", "countdown", "process_id", "data_id"]) -process_unfreeze = Signal(providing_args=["process_id"]) +pipeline_ready = Signal() # providing_args=("process_id",) +pipeline_end = Signal() # providing_args=("root_pipeline_id",) +pipeline_revoke = Signal() # providing_args=("root_pipeline_id",) +child_process_ready = Signal() # providing_args=("child_id",) +process_ready = ( + Signal() +) # providing_args=("parent_id", "current_node_id", "call_from_child",) +batch_process_ready = Signal() # providing_args=("process_id_list", "pipeline_id",) +wake_from_schedule = Signal() # providing_args=("process_id", "activity_id",) +schedule_ready = ( + Signal() +) # providing_args=("schedule_id", "countdown", "process_id", "data_id",) +process_unfreeze = Signal() # providing_args=("process_id",) # activity failed signal -activity_failed = Signal(providing_args=["pipeline_id", "pipeline_activity_id", "subprocess_id_stack"]) +activity_failed = ( + Signal() +) # providing_args=("pipeline_id", "pipeline_activity_id", "subprocess_id_stack",) # signal for developer (do not use valve to pass them!) -service_schedule_fail = Signal(providing_args=["activity_shell", "schedule_service", "ex_data"]) -service_schedule_success = Signal(providing_args=["activity_shell", "schedule_service"]) -node_skip_call = Signal(providing_args=["process", "node"]) -node_retry_ready = Signal(providing_args=["process", "node"]) +service_schedule_fail = ( + Signal() +) # providing_args=("activity_shell", "schedule_service", "ex_data",) +service_schedule_success = ( + Signal() +) # providing_args=("activity_shell", "schedule_service",) +node_skip_call = Signal() # providing_args=("process", "node",) +node_retry_ready = Signal() # providing_args=("process", "node",) -service_activity_timeout_monitor_start = Signal(providing_args=["node_id", "version", "root_pipeline_id", "countdown"]) -service_activity_timeout_monitor_end = Signal(providing_args=["node_id", "version"]) +service_activity_timeout_monitor_start = ( + Signal() +) # providing_args=("node_id", "version", "root_pipeline_id", "countdown",) +service_activity_timeout_monitor_end = ( + Signal() +) # providing_args=("node_id", "version",) diff --git a/pipeline/engine/tasks.py b/pipeline/engine/tasks.py index fc1719951..108ba5401 100644 --- a/pipeline/engine/tasks.py +++ b/pipeline/engine/tasks.py @@ -13,8 +13,8 @@ import logging -from celery import task -from celery.decorators import periodic_task +from celery import shared_task +from blueapps.contrib.celery_tools.periodic import periodic_task from celery.schedules import crontab from pipeline.conf import default_settings @@ -22,12 +22,18 @@ from pipeline.engine import api, signals, states from pipeline.engine.core import runtime, schedule from pipeline.engine.health import zombie -from pipeline.engine.models import NodeCeleryTask, NodeRelationship, PipelineProcess, ProcessCeleryTask, Status +from pipeline.engine.models import ( + NodeCeleryTask, + NodeRelationship, + PipelineProcess, + ProcessCeleryTask, + Status, +) logger = logging.getLogger("celery") -@task(ignore_result=True) +@shared_task(ignore_result=True) def process_unfreeze(process_id): process = PipelineProcess.objects.get(id=process_id) if not process.is_alive: @@ -37,7 +43,7 @@ def process_unfreeze(process_id): runtime.run_loop(process) -@task(ignore_result=True) +@shared_task(ignore_result=True) def start(process_id): process = PipelineProcess.objects.get(id=process_id) if not process.is_alive: @@ -46,9 +52,15 @@ def start(process_id): pipeline_id = process.root_pipeline.id # try to run - action_result = Status.objects.transit(pipeline_id, states.RUNNING, is_pipeline=True, start=True) + action_result = Status.objects.transit( + pipeline_id, states.RUNNING, is_pipeline=True, start=True + ) if not action_result.result: - logger.warning("can not start pipeline({}), message: {}".format(pipeline_id, action_result.message)) + logger.warning( + "can not start pipeline({}), message: {}".format( + pipeline_id, action_result.message + ) + ) return NodeRelationship.objects.build_relationship(pipeline_id, pipeline_id) @@ -56,7 +68,7 @@ def start(process_id): runtime.run_loop(process) -@task(ignore_result=True) +@shared_task(ignore_result=True) def dispatch(child_id): process = PipelineProcess.objects.get(id=child_id) if not process.is_alive: @@ -66,7 +78,7 @@ def dispatch(child_id): runtime.run_loop(process) -@task(ignore_result=True) +@shared_task(ignore_result=True) def process_wake_up(process_id, current_node_id=None, call_from_child=False): process = PipelineProcess.objects.get(id=process_id) if not process.is_alive: @@ -82,7 +94,11 @@ def process_wake_up(process_id, current_node_id=None, call_from_child=False): if not action_result.result: # BLOCKED is a tolerant running state if action_result.extra.state != states.BLOCKED: - logger.warning("can not start pipeline({}), message: {}".format(pipeline_id, action_result.message)) + logger.warning( + "can not start pipeline({}), message: {}".format( + pipeline_id, action_result.message + ) + ) return process.wake_up() @@ -92,7 +108,7 @@ def process_wake_up(process_id, current_node_id=None, call_from_child=False): runtime.run_loop(process) -@task(ignore_result=True) +@shared_task(ignore_result=True) def wake_up(process_id): process = PipelineProcess.objects.get(id=process_id) if not process.is_alive: @@ -103,18 +119,24 @@ def wake_up(process_id): runtime.run_loop(process) -@task(ignore_result=True) +@shared_task(ignore_result=True) def batch_wake_up(process_id_list, pipeline_id): - action_result = Status.objects.transit(pipeline_id, to_state=states.RUNNING, is_pipeline=True) + action_result = Status.objects.transit( + pipeline_id, to_state=states.RUNNING, is_pipeline=True + ) if not action_result.result: - logger.warning("can not start pipeline({}), message: {}".format(pipeline_id, action_result.message)) + logger.warning( + "can not start pipeline({}), message: {}".format( + pipeline_id, action_result.message + ) + ) return for process_id in process_id_list: task_id = wake_up.apply_async(args=[process_id]).id ProcessCeleryTask.objects.bind(process_id, task_id) -@task(ignore_result=True) +@shared_task(ignore_result=True) def wake_from_schedule(process_id, service_act_id): process = PipelineProcess.objects.get(id=process_id) process.wake_up() @@ -124,27 +146,38 @@ def wake_from_schedule(process_id, service_act_id): runtime.run_loop(process) -@task(ignore_result=True) +@shared_task(ignore_result=True) def service_schedule(process_id, schedule_id, data_id=None): schedule.schedule(process_id, schedule_id, data_id) -@task(ignore_result=True) +@shared_task(ignore_result=True) def node_timeout_check(node_id, version, root_pipeline_id): NodeCeleryTask.objects.destroy(node_id) state = Status.objects.state_for(node_id, version=version, may_not_exist=True) if not state or state != states.RUNNING: - logger.warning("node {} {} timeout kill failed, node not exist or not in running".format(node_id, version)) + logger.warning( + "node {} {} timeout kill failed, node not exist or not in running".format( + node_id, version + ) + ) return - action_result = api.forced_fail(node_id, kill=True, ex_data="node execution timeout") + action_result = api.forced_fail( + node_id, kill=True, ex_data="node execution timeout" + ) if action_result.result: - signals.activity_failed.send(sender=Pipeline, pipeline_id=root_pipeline_id, pipeline_activity_id=node_id) + signals.activity_failed.send( + sender=Pipeline, pipeline_id=root_pipeline_id, pipeline_activity_id=node_id + ) else: logger.warning("node {} - {} timeout kill failed".format(node_id, version)) -@periodic_task(run_every=(crontab(**default_settings.ENGINE_ZOMBIE_PROCESS_HEAL_CRON)), ignore_result=True) +@periodic_task( + run_every=(crontab(**default_settings.ENGINE_ZOMBIE_PROCESS_HEAL_CRON)), + ignore_result=True, +) def heal_zombie_process(): logger.info("Zombie process heal start") diff --git a/pipeline/log/models.py b/pipeline/log/models.py index d2440f243..dc0e3bb6b 100644 --- a/pipeline/log/models.py +++ b/pipeline/log/models.py @@ -13,7 +13,7 @@ from django.db import models from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class LogEntryManager(models.Manager): @@ -26,7 +26,12 @@ def plain_log_for_node(self, node_id, history_id): for entry in entries: plain_entries.append( "[%s %s] %s, exception: %s" - % (entry.logged_at.strftime("%Y-%m-%d %H:%M:%S"), entry.level_name, entry.message, entry.exception) + % ( + entry.logged_at.strftime("%Y-%m-%d %H:%M:%S"), + entry.level_name, + entry.message, + entry.exception, + ) ) return "\n".join(plain_entries) diff --git a/pipeline/log/tasks.py b/pipeline/log/tasks.py index e39826fca..e34f50542 100644 --- a/pipeline/log/tasks.py +++ b/pipeline/log/tasks.py @@ -13,7 +13,7 @@ import logging -from celery.decorators import periodic_task +from blueapps.contrib.celery_tools.periodic import periodic_task from celery.schedules import crontab from django.conf import settings @@ -28,7 +28,9 @@ def clean_expired_log(): if expired_interval is None: expired_interval = 30 - logger.warning("LOG_PERSISTENT_DAYS are not found in settings, use default value: 30") + logger.warning( + "LOG_PERSISTENT_DAYS are not found in settings, use default value: 30" + ) del_num = LogEntry.objects.delete_expired_log(expired_interval) logger.info("%s log entry are deleted" % del_num) diff --git a/pipeline/models.py b/pipeline/models.py index 575e0f9be..0cfce0b95 100644 --- a/pipeline/models.py +++ b/pipeline/models.py @@ -21,7 +21,7 @@ from django.db import models, transaction from django.utils import timezone from django.utils.module_loading import import_string -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.conf import settings from pipeline.constants import PIPELINE_DEFAULT_PRIORITY @@ -116,7 +116,9 @@ def get_subprocess_act_list(pipeline_data): @return: 子流程节点 """ activities = pipeline_data[PE.activities] - act_ids = [act_id for act_id in activities if activities[act_id][PE.type] == PE.SubProcess] + act_ids = [ + act_id for act_id in activities if activities[act_id][PE.type] == PE.SubProcess + ] return [activities[act_id] for act_id in act_ids] @@ -126,7 +128,11 @@ def _act_id_in_graph(act): @param act: 子流程节点 @return: 模板 ID:版本 或 模板ID """ - return "{}:{}".format(act["template_id"], act["version"]) if act.get("version") else act["template_id"] + return ( + "{}:{}".format(act["template_id"], act["version"]) + if act.get("version") + else act["template_id"] + ) class TemplateManager(models.Manager): @@ -139,7 +145,9 @@ def subprocess_ref_validate(self, data, root_id=None, root_name=None): @return: 引用是否合法,相关信息 """ try: - sub_refs, name_map = self.construct_subprocess_ref_graph(data, root_id=root_id, root_name=root_name) + sub_refs, name_map = self.construct_subprocess_ref_graph( + data, root_id=root_id, root_name=root_name + ) except PipelineTemplate.DoesNotExist as e: return False, str(e) @@ -193,7 +201,9 @@ def delete_model(self, template_ids): template.name = uniqid() template.save() - def construct_subprocess_ref_graph(self, pipeline_data, root_id=None, root_name=None): + def construct_subprocess_ref_graph( + self, pipeline_data, root_id=None, root_name=None + ): """ 构造子流程引用图 @param pipeline_data: pipeline 结构数据 @@ -219,7 +229,9 @@ def construct_subprocess_ref_graph(self, pipeline_data, root_id=None, root_name= tid = tid_queue.get() template = self.get(template_id=tid.split(":")[0]) name_map[tid] = template.name - subprocess_act = get_subprocess_act_list(template.data_for_version(version[tid])) + subprocess_act = get_subprocess_act_list( + template.data_for_version(version[tid]) + ) for act in subprocess_act: ref_tid = _act_id_in_graph(act) @@ -242,7 +254,9 @@ def unfold_subprocess(self, pipeline_data): activities = pipeline_data[PE.activities] for act_id, act in list(activities.items()): if act[PE.type] == PE.SubProcess: - subproc_data = self.get(template_id=act[PE.template_id]).data_for_version(act.get(PE.version)) + subproc_data = self.get( + template_id=act[PE.template_id] + ).data_for_version(act.get(PE.version)) sub_id_maps = self.unfold_subprocess(subproc_data) # act_id is new id @@ -278,17 +292,24 @@ class PipelineTemplate(models.Model): """ template_id = models.CharField(_("模板ID"), max_length=32, unique=True) - name = models.CharField(_("模板名称"), max_length=MAX_LEN_OF_NAME, default="default_template") + name = models.CharField( + _("模板名称"), max_length=MAX_LEN_OF_NAME, default="default_template" + ) create_time = models.DateTimeField(_("创建时间"), auto_now_add=True) creator = models.CharField(_("创建者"), max_length=32) description = models.TextField(_("描述"), null=True, blank=True) editor = models.CharField(_("修改者"), max_length=32, null=True, blank=True) edit_time = models.DateTimeField(_("修改时间"), auto_now=True) snapshot = models.ForeignKey( - Snapshot, verbose_name=_("模板结构数据"), related_name="snapshot_templates", on_delete=models.DO_NOTHING + Snapshot, + verbose_name=_("模板结构数据"), + related_name="snapshot_templates", + on_delete=models.DO_NOTHING, ) has_subprocess = models.BooleanField(_("是否含有子流程"), default=False) - is_deleted = models.BooleanField(_("是否删除"), default=False, help_text=_("表示当前模板是否删除")) + is_deleted = models.BooleanField( + _("是否删除"), default=False, help_text=_("表示当前模板是否删除") + ) objects = TemplateManager() @@ -312,9 +333,9 @@ def version(self): @property def subprocess_version_info(self): # 1. get all subprocess - subprocess_info = TemplateRelationship.objects.get_subprocess_info(self.template_id).values( - "descendant_template_id", "subprocess_node_id", "version" - ) + subprocess_info = TemplateRelationship.objects.get_subprocess_info( + self.template_id + ).values("descendant_template_id", "subprocess_node_id", "version") info = {"subproc_has_update": False, "details": []} if not subprocess_info: return info @@ -323,7 +344,9 @@ def subprocess_version_info(self): temp_current_versions = { item.template_id: item for item in TemplateCurrentVersion.objects.filter( - template_id__in=[item["descendant_template_id"] for item in subprocess_info] + template_id__in=[ + item["descendant_template_id"] for item in subprocess_info + ] ) } @@ -332,7 +355,12 @@ def subprocess_version_info(self): item["expired"] = ( False if item["version"] is None - else (item["version"] != temp_current_versions[item["descendant_template_id"]].current_version) + else ( + item["version"] + != temp_current_versions[ + item["descendant_template_id"] + ].current_version + ) ) info["details"].append(item) expireds.append(item["expired"]) @@ -362,9 +390,9 @@ def referencer(self): @return: 引用了该模板的其他模板 ID 列表 """ referencer = TemplateRelationship.objects.referencer(self.template_id) - template_id = self.__class__.objects.filter(template_id__in=referencer, is_deleted=False).values_list( - "template_id", flat=True - ) + template_id = self.__class__.objects.filter( + template_id__in=referencer, is_deleted=False + ).values_list("template_id", flat=True) return list(template_id) def clone_data(self): @@ -383,7 +411,9 @@ def update_template(self, structure_data, **kwargs): @param kwargs: 其他参数 @return: """ - result, msg = PipelineTemplate.objects.subprocess_ref_validate(structure_data, self.template_id, self.name) + result, msg = PipelineTemplate.objects.subprocess_ref_validate( + structure_data, self.template_id, self.name + ) if not result: raise SubprocessRefError(msg) @@ -429,7 +459,13 @@ def referencer(self, template_id): @param template_id: 被引用的模板 @return: 引用了该模板的其他模板 ID 列表 """ - return list(set(self.filter(descendant_template_id=template_id).values_list("ancestor_template_id", flat=True))) + return list( + set( + self.filter(descendant_template_id=template_id).values_list( + "ancestor_template_id", flat=True + ) + ) + ) class TemplateRelationship(models.Model): @@ -438,7 +474,9 @@ class TemplateRelationship(models.Model): """ ancestor_template_id = models.CharField(_("根模板ID"), max_length=32, db_index=True) - descendant_template_id = models.CharField(_("子流程模板ID"), max_length=32, null=False) + descendant_template_id = models.CharField( + _("子流程模板ID"), max_length=32, null=False + ) subprocess_node_id = models.CharField(_("子流程节点 ID"), max_length=32, null=False) version = models.CharField(_("快照字符串的md5"), max_length=32, null=False) @@ -453,7 +491,8 @@ def update_current_version(self, template): @return: 记录模板当前版本的对象 """ obj, __ = self.update_or_create( - template_id=template.template_id, defaults={"current_version": template.version} + template_id=template.template_id, + defaults={"current_version": template.version}, ) return obj @@ -484,7 +523,9 @@ def track(self, template): if versions and versions[0].md5 == template.snapshot.md5sum: return versions[0] - return self.create(template=template, snapshot=template.snapshot, md5=template.snapshot.md5sum) + return self.create( + template=template, snapshot=template.snapshot, md5=template.snapshot.md5sum + ) class TemplateVersion(models.Model): @@ -492,8 +533,15 @@ class TemplateVersion(models.Model): 模板版本号记录节点 """ - template = models.ForeignKey(PipelineTemplate, verbose_name=_("模板 ID"), null=False, on_delete=models.CASCADE) - snapshot = models.ForeignKey(Snapshot, verbose_name=_("模板数据 ID"), null=False, on_delete=models.CASCADE) + template = models.ForeignKey( + PipelineTemplate, + verbose_name=_("模板 ID"), + null=False, + on_delete=models.CASCADE, + ) + snapshot = models.ForeignKey( + Snapshot, verbose_name=_("模板数据 ID"), null=False, on_delete=models.CASCADE + ) md5 = models.CharField(_("快照字符串的md5"), max_length=32, db_index=True) date = models.DateTimeField(_("添加日期"), auto_now_add=True) @@ -506,9 +554,15 @@ class TemplateScheme(models.Model): """ template = models.ForeignKey( - PipelineTemplate, verbose_name=_("对应模板 ID"), null=False, blank=False, on_delete=models.CASCADE + PipelineTemplate, + verbose_name=_("对应模板 ID"), + null=False, + blank=False, + on_delete=models.CASCADE, + ) + unique_id = models.CharField( + _("方案唯一ID"), max_length=97, unique=True, null=False, blank=True ) - unique_id = models.CharField(_("方案唯一ID"), max_length=97, unique=True, null=False, blank=True) name = models.CharField(_("方案名称"), max_length=64, null=False, blank=False) edit_time = models.DateTimeField(_("修改时间"), auto_now=True) data = CompressJSONField(verbose_name=_("方案数据")) @@ -618,9 +672,15 @@ class PipelineInstance(models.Model): instance_id = models.CharField(_("实例ID"), max_length=32, unique=True) template = models.ForeignKey( - PipelineTemplate, verbose_name=_("Pipeline模板"), null=True, blank=True, on_delete=models.SET_NULL + PipelineTemplate, + verbose_name=_("Pipeline模板"), + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + name = models.CharField( + _("实例名称"), max_length=MAX_LEN_OF_NAME, default="default_instance" ) - name = models.CharField(_("实例名称"), max_length=MAX_LEN_OF_NAME, default="default_instance") creator = models.CharField(_("创建者"), max_length=32, blank=True) create_time = models.DateTimeField(_("创建时间"), auto_now_add=True) executor = models.CharField(_("执行者"), max_length=32, blank=True) @@ -630,7 +690,9 @@ class PipelineInstance(models.Model): is_started = models.BooleanField(_("是否已经启动"), default=False) is_finished = models.BooleanField(_("是否已经完成"), default=False) is_revoked = models.BooleanField(_("是否已经撤销"), default=False) - is_deleted = models.BooleanField(_("是否已经删除"), default=False, help_text=_("表示当前实例是否删除")) + is_deleted = models.BooleanField( + _("是否已经删除"), default=False, help_text=_("表示当前实例是否删除") + ) snapshot = models.ForeignKey( Snapshot, blank=True, @@ -714,7 +776,9 @@ def clone(self, creator, **kwargs): @param kwargs: 其他参数 @return: 当前实例对象的克隆 """ - name = kwargs.get("name") or timezone.localtime(timezone.now()).strftime("clone%Y%m%d%H%m%S") + name = kwargs.get("name") or timezone.localtime(timezone.now()).strftime( + "clone%Y%m%d%H%m%S" + ) instance_id = node_uniqid() exec_data = self.execution_data @@ -733,7 +797,9 @@ def clone(self, creator, **kwargs): execution_snapshot=new_snapshot, ) - def start(self, executor, check_workers=True, priority=PIPELINE_DEFAULT_PRIORITY, queue=""): + def start( + self, executor, check_workers=True, priority=PIPELINE_DEFAULT_PRIORITY, queue="" + ): """ 启动当前流程 @param executor: 执行者 @@ -744,14 +810,19 @@ def start(self, executor, check_workers=True, priority=PIPELINE_DEFAULT_PRIORITY with transaction.atomic(): instance = self.__class__.objects.select_for_update().get(id=self.id) if instance.is_started: - return ActionResult(result=False, message="pipeline instance already started.") + return ActionResult( + result=False, message="pipeline instance already started." + ) pipeline_data = instance.execution_data try: parser_cls = import_string(settings.PIPELINE_PARSER_CLASS) except ImportError: - return ActionResult(result=False, message="invalid parser class: %s" % settings.PIPELINE_PARSER_CLASS) + return ActionResult( + result=False, + message="invalid parser class: %s" % settings.PIPELINE_PARSER_CLASS, + ) instance.start_time = timezone.now() instance.is_started = True @@ -763,7 +834,10 @@ def start(self, executor, check_workers=True, priority=PIPELINE_DEFAULT_PRIORITY instance, obj_type="instance", data_type="data", username=executor ), root_pipeline_context=get_pipeline_context( - instance, obj_type="instance", data_type="context", username=executor + instance, + obj_type="instance", + data_type="context", + username=executor, ), ) @@ -772,7 +846,9 @@ def start(self, executor, check_workers=True, priority=PIPELINE_DEFAULT_PRIORITY instance.save() - act_result = task_service.run_pipeline(pipeline, check_workers=check_workers, priority=priority, queue=queue) + act_result = task_service.run_pipeline( + pipeline, check_workers=check_workers, priority=priority, queue=queue + ) if not act_result.result: with transaction.atomic(): diff --git a/pipeline/utils/boolrule/boolrule.py b/pipeline/utils/boolrule/boolrule.py index 10592ff5c..8a670b4a2 100644 --- a/pipeline/utils/boolrule/boolrule.py +++ b/pipeline/utils/boolrule/boolrule.py @@ -54,7 +54,9 @@ def get_val(self, context): val = getattr(val, part) if hasattr(val, part) else val[part] except KeyError: - raise MissingVariableException("no value supplied for {}".format(self._path)) + raise MissingVariableException( + "no value supplied for {}".format(self._path) + ) return val @@ -65,7 +67,9 @@ def __repr__(self): # Grammar definition pathDelimiter = "." # match gcloud's variable -identifier = Combine(Optional("${") + Optional("_") + Word(alphas, alphanums + "_") + Optional("}")) +identifier = Combine( + Optional("${") + Optional("_") + Word(alphas, alphanums + "_") + Optional("}") +) # identifier = Word(alphas, alphanums + "_") propertyPath = delimitedList(identifier, pathDelimiter, combine=True) @@ -76,7 +80,9 @@ def __repr__(self): lparen = Suppress("(") rparen = Suppress(")") -binaryOp = oneOf("== != < > >= <= in notin issuperset notissuperset", caseless=True)("operator") +binaryOp = oneOf("== != < > >= <= in notin issuperset notissuperset", caseless=True)( + "operator" +) E = CaselessLiteral("E") numberSign = Word("+-", exact=1) @@ -86,7 +92,9 @@ def __repr__(self): + Optional(E + Optional(numberSign) + Word(nums)) ) -integer = Combine(Optional(numberSign) + Word(nums) + Optional(E + Optional("+") + Word(nums))) +integer = Combine( + Optional(numberSign) + Word(nums) + Optional(E + Optional("+") + Word(nums)) +) # str_ = quotedString.addParseAction(removeQuotes) str_ = QuotedString('"') | QuotedString("'") @@ -96,7 +104,7 @@ def __repr__(self): realNumber.setParseAction(lambda toks: float(toks[0])) | integer.setParseAction(lambda toks: int(toks[0])) | str_ - | bool_.setParseAction(lambda toks: toks[0] == "true") + | bool_.setParseAction(lambda toks: toks[0].lower() == "true") | propertyPath.setParseAction(lambda toks: SubstituteVal(toks)) ) # need to add support for alg expressions @@ -104,7 +112,8 @@ def __repr__(self): boolExpression = Forward() boolCondition = Group( - (Group(propertyVal)("lval") + binaryOp + Group(propertyVal)("rval")) | (lparen + boolExpression + rparen) + (Group(propertyVal)("lval") + binaryOp + Group(propertyVal)("rval")) + | (lparen + boolExpression + rparen) ) boolExpression << boolCondition + ZeroOrMore((and_ | or_) + boolExpression) @@ -198,14 +207,16 @@ def _compile(self): return try: - self._tokens = boolExpression.parseString(self._query, parseAll=self.strict) + self._tokens = boolExpression.parseString( + self._query, parseAll=self.strict + ) except ParseException: raise self._compiled = True def _expand_val(self, val, context): - if type(val) == list: + if isinstance(val, list): val = [self._expand_val(v, context) for v in val] if isinstance(val, SubstituteVal): diff --git a/pipeline/validators/connection.py b/pipeline/validators/connection.py index 75b08bd15..c7b674a00 100644 --- a/pipeline/validators/connection.py +++ b/pipeline/validators/connection.py @@ -11,7 +11,7 @@ specific language governing permissions and limitations under the License. """ -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.exceptions import ConnectionValidateError from pipeline.utils.graph import Graph @@ -35,16 +35,30 @@ def validate_graph_connection(data): for j in nodes[i][PE.target]: if nodes[j][PE.type] not in rule["allowed_out"]: message += _("不能连接%s类型节点\n") % nodes[i][PE.type] - if rule["min_in"] > len(nodes[i][PE.source]) or len(nodes[i][PE.source]) > rule["max_in"]: - message += _("节点的入度最大为%s,最小为%s\n") % (rule["max_in"], rule["min_in"]) - if rule["min_out"] > len(nodes[i][PE.target]) or len(nodes[i][PE.target]) > rule["max_out"]: - message += _("节点的出度最大为%s,最小为%s\n") % (rule["max_out"], rule["min_out"]) + if ( + rule["min_in"] > len(nodes[i][PE.source]) + or len(nodes[i][PE.source]) > rule["max_in"] + ): + message += _("节点的入度最大为%s,最小为%s\n") % ( + rule["max_in"], + rule["min_in"], + ) + if ( + rule["min_out"] > len(nodes[i][PE.target]) + or len(nodes[i][PE.target]) > rule["max_out"] + ): + message += _("节点的出度最大为%s,最小为%s\n") % ( + rule["max_out"], + rule["min_out"], + ) if message: result["failed_nodes"].append(i) result["message"][i] = message if result["failed_nodes"]: - raise ConnectionValidateError(failed_nodes=result["failed_nodes"], detail=result["message"]) + raise ConnectionValidateError( + failed_nodes=result["failed_nodes"], detail=result["message"] + ) def validate_graph_without_circle(data): @@ -60,8 +74,14 @@ def validate_graph_without_circle(data): nodes = [data[PE.start_event][PE.id], data[PE.end_event][PE.id]] nodes += list(data[PE.gateways].keys()) + list(data[PE.activities].keys()) - flows = [[flow[PE.source], flow[PE.target]] for _, flow in list(data[PE.flows].items())] + flows = [ + [flow[PE.source], flow[PE.target]] for _, flow in list(data[PE.flows].items()) + ] cycle = Graph(nodes, flows).get_cycle() if cycle: - return {"result": False, "message": "pipeline graph has circle", "error_data": cycle} + return { + "result": False, + "message": "pipeline graph has circle", + "error_data": cycle, + } return {"result": True, "data": []} diff --git a/pipeline/validators/gateway.py b/pipeline/validators/gateway.py index 28c6677ef..d091fad55 100644 --- a/pipeline/validators/gateway.py +++ b/pipeline/validators/gateway.py @@ -13,7 +13,7 @@ import queue -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline import exceptions from pipeline.core.constants import PE @@ -127,7 +127,10 @@ def match_converge( target[i] = None break else: - raise exceptions.ConvergeMatchError(cur_index, _("并行网关中的分支网关必须将所有分支汇聚到一个汇聚网关")) + raise exceptions.ConvergeMatchError( + cur_index, + _("并行网关中的分支网关必须将所有分支汇聚到一个汇聚网关"), + ) converge_id, shared = match_converge( converges=converges, @@ -152,7 +155,10 @@ def match_converge( # can't find corresponding converge gateway, which means this gateway will reach end event directly target[i] = end_event_id - if target[i] in converges and dist_from_start[target[i]] < dist_from_start[cur_index]: + if ( + target[i] in converges + and dist_from_start[target[i]] < dist_from_start[cur_index] + ): # do not match previous converge target[i] = None @@ -180,7 +186,9 @@ def match_converge( if not_in_parallel_gateway(stack): converge_end = True else: - raise exceptions.ConvergeMatchError(cur_index, _("并行网关中的分支网关必须将所有分支汇聚到一个汇聚网关")) + raise exceptions.ConvergeMatchError( + cur_index, _("并行网关中的分支网关必须将所有分支汇聚到一个汇聚网关") + ) # exclusive gateway point back to self elif is_exg and target[i] == current_gateway[PE.id]: @@ -190,7 +198,9 @@ def match_converge( # exclusive gateway converge at different converge gateway elif is_exg and target[i] in converges and converge_id != target[i]: - raise exceptions.ConvergeMatchError(cur_index, _("分支网关的所有分支第一个遇到的汇聚网关必须是同一个")) + raise exceptions.ConvergeMatchError( + cur_index, _("分支网关的所有分支第一个遇到的汇聚网关必须是同一个") + ) # meet previous node elif is_exg and target[i] is None: @@ -200,13 +210,18 @@ def match_converge( # invalid cases else: - raise exceptions.ConvergeMatchError(cur_index, _("非法网关,请检查其分支是否符合规则")) + raise exceptions.ConvergeMatchError( + cur_index, _("非法网关,请检查其分支是否符合规则") + ) if is_exg: if converge_id in converges: # this converge is shared by multiple gateway # only compare to the number of positive incoming - shared = converge_in_len[converge_id] > cur_to_converge or converge_id in converged + shared = ( + converge_in_len[converge_id] > cur_to_converge + or converge_id in converged + ) else: # for parallel gateway @@ -217,12 +232,16 @@ def match_converge( for gateway_id in converged.get(converge_id, []): # find another parallel gateway if gateways[gateway_id][PE.type] in PARALLEL_GATEWAYS: - raise exceptions.ConvergeMatchError(converge_id, _("汇聚网关只能汇聚来自同一个并行网关的分支")) + raise exceptions.ConvergeMatchError( + converge_id, _("汇聚网关只能汇聚来自同一个并行网关的分支") + ) shared = True elif converge_incoming < gateway_outgoing: - raise exceptions.ConvergeMatchError(converge_id, _("汇聚网关没有汇聚其对应的并行网关的所有分支")) + raise exceptions.ConvergeMatchError( + converge_id, _("汇聚网关没有汇聚其对应的并行网关的所有分支") + ) current_gateway["match"] = converge_id current_gateway["share_converge"] = shared @@ -264,7 +283,9 @@ def distance_from(origin, node, tree, marked, visited=None): prev_node = get_node_for_sequence(incoming, tree, PE.source) # get incoming node's distance recursively - dist = distance_from(origin=origin, node=prev_node, tree=tree, marked=marked, visited=visited) + dist = distance_from( + origin=origin, node=prev_node, tree=tree, marked=marked, visited=visited + ) # if this incoming do not trace back to current node if dist is not None: @@ -295,8 +316,16 @@ def validate_gateways(tree): # data preparation for i, item in list(tree[PE.gateways].items()): node = { - PE.incoming: item[PE.incoming] if isinstance(item[PE.incoming], list) else [item[PE.incoming]], - PE.outgoing: item[PE.outgoing] if isinstance(item[PE.outgoing], list) else [item[PE.outgoing]], + PE.incoming: ( + item[PE.incoming] + if isinstance(item[PE.incoming], list) + else [item[PE.incoming]] + ), + PE.outgoing: ( + item[PE.outgoing] + if isinstance(item[PE.outgoing], list) + else [item[PE.outgoing]] + ), PE.type: item[PE.type], PE.target: [], PE.source: [], @@ -309,14 +338,20 @@ def validate_gateways(tree): for index in node[PE.outgoing]: index = tree[PE.flows][index][PE.target] while index in tree[PE.activities]: - index = tree[PE.flows][tree[PE.activities][index][PE.outgoing]][PE.target] + index = tree[PE.flows][tree[PE.activities][index][PE.outgoing]][ + PE.target + ] # append this node's id to current gateway's target list node[PE.target].append(index) # get current node's distance from start event - if not distance_from(node=node, origin=tree[PE.start_event], tree=tree, marked=distances): - raise exceptions.ConvergeMatchError(node[PE.id], _("无法获取该网关距离开始节点的距离")) + if not distance_from( + node=node, origin=tree[PE.start_event], tree=tree, marked=distances + ): + raise exceptions.ConvergeMatchError( + node[PE.id], _("无法获取该网关距离开始节点的距离") + ) if item[PE.type] == PE.ConvergeGateway: converges[i] = node @@ -403,7 +438,9 @@ def blend(source, target, custom_stream=None): return if len(source[STREAM]) == 0: - raise exceptions.InvalidOperationException("stream validation error, node(%s) stream is empty" % source[PE.id]) + raise exceptions.InvalidOperationException( + "stream validation error, node(%s) stream is empty" % source[PE.id] + ) # blend for s in source[STREAM]: @@ -448,7 +485,9 @@ def flowing(where, to, parallel_converges): if target_id in parallel_converges: - is_valid_branch = where[STREAM].issubset(parallel_converges[target_id][P_STREAM]) + is_valid_branch = where[STREAM].issubset( + parallel_converges[target_id][P_STREAM] + ) is_direct_connect = where.get(PE.converge_gateway_id) == target_id if is_valid_branch or is_direct_connect: @@ -483,7 +522,10 @@ def validate_stream(tree): # set allow streams for parallel's converge if node[PE.type] in PARALLEL_GATEWAYS: - parallel_converges[node[PE.converge_gateway_id]] = {P_STREAM: streams_for_parallel(node), P: nid} + parallel_converges[node[PE.converge_gateway_id]] = { + P_STREAM: streams_for_parallel(node), + P: nid, + } # build stream from start node_queue = queue.Queue() diff --git a/pipeline/variable_framework/models.py b/pipeline/variable_framework/models.py index 3d78b8b9e..6898191ca 100644 --- a/pipeline/variable_framework/models.py +++ b/pipeline/variable_framework/models.py @@ -12,7 +12,7 @@ """ from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pipeline.core.data.library import VariableLibrary diff --git a/readme.md b/readme.md index 492e8dab6..4b1caecab 100644 --- a/readme.md +++ b/readme.md @@ -44,6 +44,35 @@ ITSM (IT 服务管理)是一套帮助企业对 IT 系统的规划、研发、实 - [第三方API接入说明](docs/install/custom_api.md) - [ITSM 接入指引](docs/wiki/access.md) + +## Unit Testing +- 在本地进行单元测试时建议使用如下环境变量配置,需要注意配置好MySQL和Redis +- 建议使用PyCharm中集成的Django Test加载`.env`文件的方式去执行,target填写`itsm.tests` +```.dotenv +RUN_ENV=open +APP_CODE=bk_itsm +APP_ID=bk_itsm +RUN_VER=open +SECRET_KEY=12345678-1234-5678-1234-123456789012 +APP_TOKEN=12345678-1234-5678-1234-123456789012 +BK_PAAS_HOST=http://127.0.0.1 +BK_IAM_V3_INNER_HOST=127.0.0.1 +BK_IAM_INNER_HOST=http://127.0.0.1:8080 +BROKER_URL=redis://localhost:6379/0 +USE_IAM=false +BKAPP_REDIS_HOST=localhost +BKAPP_BK_IAM_SYSTEM_ID=itsm +BKAPP_IAM_INITIAL_FILE=dev +BKAPP_REDIS_PORT=6379 +BKAPP_REDIS_PASSWORD= +BK_MYSQL_NAME=bk_itsm_ci +BK_MYSQL_USER=root +BK_MYSQL_PASSWORD=root +BK_MYSQL_HOST=localhost +BK_MYSQL_PORT=3306 +BK_MYSQL_TEST_NAME=bk_itsm_ci_test +``` + ## Version plan - [版本日志](docs/RELEASE.md) [(English Documents Available)](docs/RELEASE_EN.md) diff --git a/readme_en.md b/readme_en.md index 3b3a4cc66..428336ec1 100644 --- a/readme_en.md +++ b/readme_en.md @@ -43,6 +43,34 @@ ITSM is an upper layer SaaS application based on the Tencent Blueking product sy - [API_Request_Sandbox_Instructions](docs/install/api_sandbox_guide.md) - [ITSM Access Guidelines](docs/wiki/access.md) +## Unit Testing +- When performing unit tests locally, it is recommended to use the following environment variable configuration. Ensure that MySQL and Redis are properly set up. +- It is advisable to execute the tests using the integrated Django Test feature in PyCharm, which loads the `.env` file. Set the target to `itsm.tests`. +```.dotenv +RUN_ENV=open +APP_CODE=bk_itsm +APP_ID=bk_itsm +RUN_VER=open +SECRET_KEY=12345678-1234-5678-1234-123456789012 +APP_TOKEN=12345678-1234-5678-1234-123456789012 +BK_PAAS_HOST=http://127.0.0.1 +BK_IAM_V3_INNER_HOST=127.0.0.1 +BK_IAM_INNER_HOST=http://127.0.0.1:8080 +BROKER_URL=redis://localhost:6379/0 +USE_IAM=false +BKAPP_REDIS_HOST=localhost +BKAPP_BK_IAM_SYSTEM_ID=itsm +BKAPP_IAM_INITIAL_FILE=dev +BKAPP_REDIS_PORT=6379 +BKAPP_REDIS_PASSWORD= +BK_MYSQL_NAME=bk_itsm_ci +BK_MYSQL_USER=root +BK_MYSQL_PASSWORD=root +BK_MYSQL_HOST=localhost +BK_MYSQL_PORT=3306 +BK_MYSQL_TEST_NAME=bk_itsm_ci_test +``` + ## Version plan - [RELEASE](docs/RELEASE_EN.md) [(Chinese Documents Available)](docs/RELEASE.md) diff --git a/requirements.txt b/requirements.txt index 514fa4e1d..25dccf836 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,126 +4,127 @@ # 请确保指定的包和版本号,可通过pip安装 # blueapps requirement -Django==3.2.25 -PyMySQL==1.0.2 -MarkupSafe==2.0.0 -Mako==1.1.6 -requests==2.27.1 -celery==4.4.6 -django-celery-beat==2.0.0 -django-celery-results==2.0.1 +Django==4.2 +PyMySQL==1.1.1 +MarkupSafe==2.1.5 +Mako==1.3.2 +requests==2.31.0 +celery==5.3.0 +django-celery-beat==2.7.0 +django-celery-results==2.5.1 # pi==3.3.1 -python-json-logger==0.1.7 -whitenoise==5.2.0 +python-json-logger==2.0.7 +whitenoise==6.8.2 six==1.16.0 -httplib2==0.9.1 -djangorestframework==3.12.4 -drf-extensions==0.7.0 -django-cors-headers==3.7.0 +httplib2==0.22.0 +djangorestframework==3.15.1 +drf-extensions==0.7.1 +django-cors-headers==4.2.0 jsonfield==3.1.0 -pypinyin==0.31.0 -django_extensions==2.1.0 +pypinyin==0.53.0 +django_extensions==3.2.3 django-filter==2.4.0 # django-autofixture==0.12.1 -django-revproxy==0.10.0 -pyinstrument_cext==0.2.2 -pyinstrument==3.0.3 -humanize==0.5.1 +django-revproxy==0.13.0 +pyinstrument_cext==0.2.4 +pyinstrument==5.0.0 +humanize==4.11.0 xlwt==1.3.0 -jsonschema==3.2.0 +jsonschema==4.23.0 # django-smart-autoregister==0.0.5 django-bulk-update==2.2.0 -django-redis==5.0.0 -RestrictedPython==5.0 +django-redis==5.4.0 +RestrictedPython==7.4 # wiki related -bleach==1.5.0 -django-classy-tags==2.0.0 -django-mptt==0.12.0 -django-mptt-admin==2.1.0 -django-nyt==1.2 -django-sekizai==2.0.0 -Markdown==3.2.1 -Pillow==8.4.0 -sorl-thumbnail==12.7.0 -django-simplemde==0.1.2 -martor==1.2.8 -python-dateutil==2.7.5 -django-multiselectfield==0.1.12 +bleach==6.1.0 +django-classy-tags==4.1.0 +django-mptt==0.16.0 +django-mptt-admin==2.7.0 +django-nyt==1.4.1 +django-sekizai==4.1.0 +Markdown==3.5.2 +Pillow==10.2.0 +sorl-thumbnail==12.11.0 +django-simplemde==0.1.4 +martor==1.6.45 +python-dateutil==2.9.0 +django-multiselectfield==0.1.13 # monitor statsd==3.3.0 # pipeline -ujson==4.3.0 -pyparsing==2.2.0 -redis==4.3.6 -django-timezone-field==4.1.2 -factory_boy==2.11.1 -mistune==2.0.5 -eventlet==0.33.3 -gevent==22.10.2 +ujson==5.9.0 +pyparsing==3.2.0 +redis==5.0.3 +django-timezone-field==5.1 +factory_boy==3.3.1 +mistune==3.0.2 +eventlet==0.35.2 +gevent==24.2.1 # iam -cachetools==3.1.1 -certifi==2023.7.22 -chardet==3.0.4 +cachetools==5.5.0 +certifi==2024.2.2 +chardet==5.2.0 curlify==2.2.1 -idna==2.8 -urllib3==1.26.18 +idna==3.10 +urllib3==2.2.1 # pycrypto==2.6.1 #others -raven==6.1.0 +raven==6.10.0 ddtrace==0.14.1 -gunicorn==19.6.0 -bkstorages==1.0.1 -pytz==2019.3 -suds-jurko==0.6 -mock==3.0.5 +gunicorn==23.0.0 +bkstorages==2.0.0 +pytz==2024.2 +mock==5.1.0 # excel -xlrd==1.2.0 -typing-extensions==3.7.4.3 +xlrd==2.0.1 +typing-extensions==4.12.2 -anyjson==0.3.3 #bkouth -PyJWT==2.4.0 -cryptography==40.0.2 +PyJWT==2.8.0 +cryptography==42.0.5 # prometheus -django-prometheus==2.1.0 +django-prometheus==2.3.1 # grpcio -grpcio==1.48.2 +grpcio==1.63.2 -protobuf==3.19.6 +protobuf==5.26.0 # opentelemetry -opentelemetry-api==1.6.2 -opentelemetry-sdk==1.6.2 -opentelemetry-exporter-otlp==1.6.2 -opentelemetry-exporter-jaeger==1.6.2 -opentelemetry-exporter-jaeger-proto-grpc==1.6.2 -opentelemetry-exporter-jaeger-thrift==1.6.2 -opentelemetry-instrumentation==0.25b2 -opentelemetry-instrumentation-celery==0.25b2 -opentelemetry-instrumentation-django==0.25b2 -opentelemetry-instrumentation-dbapi==0.25b2 -opentelemetry-instrumentation-redis==0.25b2 -opentelemetry-instrumentation-logging==0.25b2 -opentelemetry-instrumentation-requests==0.25b2 - -pyCryptodome==3.14.1 - -Jinja2==3.0.3 -jmespath==0.10.0 -requests_toolbelt==0.9.1 - -apigw-manager[cryptography]==1.1.8 -blueapps[opentelemetry,bkcrypto]==4.14.0 - -drf-yasg==1.20.0 - -bk-notice-sdk==1.3.0 +opentelemetry-api==1.29.0 +opentelemetry-sdk==1.29.0 +opentelemetry-exporter-otlp==1.29.0 +opentelemetry-exporter-jaeger==1.21.0 +opentelemetry-exporter-jaeger-proto-grpc==1.21.0 +opentelemetry-exporter-jaeger-thrift==1.21.0 +opentelemetry-instrumentation==0.50b0 +opentelemetry-instrumentation-celery==0.50b0 +opentelemetry-instrumentation-django==0.50b0 +opentelemetry-instrumentation-dbapi==0.50b0 +opentelemetry-instrumentation-redis==0.50b0 +opentelemetry-instrumentation-logging==0.50b0 +opentelemetry-instrumentation-requests==0.50b0 + +pyCryptodome==3.20.0 + +Jinja2==3.1.3 +jmespath==1.0.1 +requests_toolbelt==1.0.0 + +apigw-manager[cryptography]==4.0.0 +blueapps[opentelemetry,bkcrypto]==4.15.4 + +drf-yasg==1.21.8 + +bk-notice-sdk==1.3.2 + +sqlparse==0.4.4 +werkzeug==3.0.1 \ No newline at end of file diff --git a/runtime.txt b/runtime.txt index 4252f1066..e34519556 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.6.12 +python-3.11.10 diff --git a/scripts/workflows/install.sh b/scripts/workflows/install.sh index 7ee2772b1..477e7b68b 100755 --- a/scripts/workflows/install.sh +++ b/scripts/workflows/install.sh @@ -16,6 +16,7 @@ pip install -r requirements.txt pip install -r requirements.dev.txt pip install -r requirements_open.txt pip install black +pip install coverage # 删除遗留数据库,并新建一个空的本地数据库 CREATE_DB_SQL=" diff --git a/scripts/workflows/prepare_services.sh b/scripts/workflows/prepare_services.sh index 28d6589e3..62ee85692 100755 --- a/scripts/workflows/prepare_services.sh +++ b/scripts/workflows/prepare_services.sh @@ -11,7 +11,7 @@ if [ "$CREATE_PYTHON_VENV" ]; then pip install virtualenv VENV_DIR="${APP_CODE}_venv" virtualenv "$VENV_DIR" - virtualenv -p /usr/local/bin/python3.6 "$VENV_DIR" + virtualenv -p /usr/local/bin/python3.11 "$VENV_DIR" # 激活Python虚拟环境 source "${VENV_DIR}/bin/activate" fi diff --git a/sops_proxy/urls.py b/sops_proxy/urls.py index 738faf4d2..4fe74726f 100644 --- a/sops_proxy/urls.py +++ b/sops_proxy/urls.py @@ -23,14 +23,14 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.conf.urls import url +from django.urls import re_path from sops_proxy.views import SopsProxy urlpatterns = [ # 插件静态资源(jsonp)转发 - # url(r"^static/(?P.*)$", dispatch_static), + # re_path(r"^static/(?P.*)$", dispatch_static), # 插件请求(ajax)转发 - # url(r"^(?P.*)$", dispatch_query), - url(r"^(?P.*)$", SopsProxy.as_view()), + # re_path(r"^(?P.*)$", dispatch_query), + re_path(r"^(?P.*)$", SopsProxy.as_view()), ] diff --git a/urls.py b/urls.py index f0d9fccf5..6efd83e71 100644 --- a/urls.py +++ b/urls.py @@ -24,7 +24,7 @@ """ from django.conf import settings -from django.conf.urls import include, url +from django.urls import include, re_path # Uncomment the next two lines to enable the admin: from django.contrib import admin @@ -33,22 +33,22 @@ # 公共URL配置 urlpatterns = [ # Django后台数据库管理® - url(r"^admin/", admin.site.urls), - url(r"^notice/", include("bk_notice_sdk.urls")), + re_path(r"^admin/", admin.site.urls), + re_path(r"^notice/", include("bk_notice_sdk.urls")), # 用户登录鉴权 - # url(r'^account/', include('account.urls')), - url(r"^account/", include("blueapps.account.urls")), + # re_path(r'^account/', include('account.urls')), + re_path(r"^account/", include("blueapps.account.urls")), # 接口版本管理 - url(r"^api/", include("itsm.api.v1")), + re_path(r"^api/", include("itsm.api.v1")), # 对外开放的接口 - url(r"^openapi/", include("itsm.api.open_v1")), - url(r"^openapi/v2/", include("itsm.api.open_v2")), + re_path(r"^openapi/", include("itsm.api.open_v1")), + re_path(r"^openapi/v2/", include("itsm.api.open_v2")), # 监控,普罗米修斯相关的接口 - url(r"^monitor/", include("itsm.monitor.urls")), + re_path(r"^monitor/", include("itsm.monitor.urls")), # 各种入口:微信/wiki/首页等 - url(r"^", include("itsm.sites.urls")), + re_path(r"^", include("itsm.sites.urls")), # eri admin - url(r"^eri/admin/", include("pipeline.contrib.engine_admin.urls")), + re_path(r"^eri/admin/", include("pipeline.contrib.engine_admin.urls")), ] handler404 = "error_pages.views.error_404" @@ -62,5 +62,7 @@ # 全局生效:不推荐生产环境使用 urlpatterns += [ # wiki上传图片404也可以这样简单解决:路由层面不复用MEDIA_URL,后者只用来生成url,比如可以自定义prefix为SITE_URL - url(r"^media/(?P.*)$", static.serve, {"document_root": settings.MEDIA_ROOT}), + re_path( + r"^media/(?P.*)$", static.serve, {"document_root": settings.MEDIA_ROOT} + ), ] diff --git a/weixin/README.md b/weixin/README.md index 788a1f6bb..a2937a858 100644 --- a/weixin/README.md +++ b/weixin/README.md @@ -35,8 +35,8 @@ * 修改urls.py文件 ```python # urlpatterns 添加 - url(r'^weixin/login/', include('weixin.core.urls')), - url(r'^weixin/', include('weixin.urls')), + re_path(r'^weixin/login/', include('weixin.core.urls')), + re_path(r'^weixin/', include('weixin.urls')), ``` ## 蓝鲸应用 * 部署蓝鲸应用 diff --git a/weixin/core/admin.py b/weixin/core/admin.py index f526fb2dc..124e4ead2 100644 --- a/weixin/core/admin.py +++ b/weixin/core/admin.py @@ -24,22 +24,24 @@ """ from django.contrib import admin -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from weixin.core.models import BkWeixinUser class BkWeixinUserAdmin(admin.ModelAdmin): fieldsets = ( - (None, {'fields': ('userid', 'openid', 'nickname')}), - (_('Personal info'), { - 'fields': ('gender', 'country', 'province', 'city', 'email', 'mobile')}), - (_('Extra info'), {'fields': ('avatar_url', 'qr_code')}), + (None, {"fields": ("userid", "openid", "nickname")}), + ( + _("Personal info"), + {"fields": ("gender", "country", "province", "city", "email", "mobile")}, + ), + (_("Extra info"), {"fields": ("avatar_url", "qr_code")}), ) - list_display = ('userid', 'openid', 'nickname', 'mobile', 'email') - search_fields = ('userid', 'nickname') - ordering = ('userid',) + list_display = ("userid", "openid", "nickname", "mobile", "email") + search_fields = ("userid", "nickname") + ordering = ("userid",) admin.site.register(BkWeixinUser, BkWeixinUserAdmin) diff --git a/weixin/core/urls.py b/weixin/core/urls.py index 6993662e3..ac5bd34f7 100644 --- a/weixin/core/urls.py +++ b/weixin/core/urls.py @@ -23,11 +23,11 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.conf.urls import url +from django.urls import re_path from . import views urlpatterns = [ - url(r'^$', views.login, name='weixin_login'), + re_path(r"^$", views.login, name="weixin_login"), ] diff --git a/weixin/urls.py b/weixin/urls.py index 812b3d849..24f0f8c11 100644 --- a/weixin/urls.py +++ b/weixin/urls.py @@ -23,7 +23,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from django.conf.urls import include, url +from django.urls import include, re_path from rest_framework.routers import DefaultRouter from itsm.misc.views import download, upload @@ -66,9 +66,9 @@ routers.register(r"task/tasks", WXTaskViewSet, basename="wx_tasks") urlpatterns = routers.urls + [ - url(r"^gateway/", include("itsm.gateway.urls")), - url(r"^upload_file/$", upload), - url(r"^download_file/$", download), - url(r"^postman/rpc_api/$", WXRpcApiViewSet.as_view()), - url(r"^c/compapi/v2/usermanage/fs_list_users/$", get_batch_users), + re_path(r"^gateway/", include("itsm.gateway.urls")), + re_path(r"^upload_file/$", upload), + re_path(r"^download_file/$", download), + re_path(r"^postman/rpc_api/$", WXRpcApiViewSet.as_view()), + re_path(r"^c/compapi/v2/usermanage/fs_list_users/$", get_batch_users), ] diff --git a/weixin/validators.py b/weixin/validators.py index 86a3cf877..e5acc61c9 100644 --- a/weixin/validators.py +++ b/weixin/validators.py @@ -26,7 +26,7 @@ from functools import wraps from django.contrib.auth.models import AnonymousUser -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ import settings from weixin.utils import FailResponse @@ -39,13 +39,18 @@ def __wrapper(request, *args, **kwargs): # 必须在微信中打开 if isinstance(request.weixin_user, AnonymousUser): - return FailResponse(message=_('请在企业微信中打开链接'), code='1002').json() + return FailResponse(message=_("请在企业微信中打开链接"), code="1002").json() # 必须绑定微信到蓝鲸 if isinstance(request.user, AnonymousUser): return FailResponse( - message=_('请先登录蓝鲸平台的个人中心({}/console/user_center/)绑定你的微信({}),并访问一次ITSM.').format( - settings.BK_PAAS_HOST, getattr(request.weixin_user, 'nickname', '')), code='1003').json() + message=_( + "请先登录蓝鲸平台的个人中心({}/console/user_center/)绑定你的微信({}),并访问一次ITSM." + ).format( + settings.BK_PAAS_HOST, getattr(request.weixin_user, "nickname", "") + ), + code="1003", + ).json() return view_func(request, *args, **kwargs)