feat(ticket): 工单关闭生成 Comment

This commit is contained in:
xinwen
2020-08-04 16:13:16 +08:00
committed by 老广
parent c3c5801d2e
commit f6a4253936
12 changed files with 180 additions and 125 deletions

View File

@@ -1,4 +1,3 @@
from django.db.transaction import atomic
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from rest_framework.decorators import action
@@ -26,7 +25,7 @@ from ..permissions import IsAssignee
class RequestAssetPermTicketViewSet(JMSModelViewSet):
queryset = Ticket.origin_objects.filter(type=Ticket.TYPE_REQUEST_ASSET_PERM)
queryset = Ticket.origin_objects.filter(type=Ticket.TYPE.REQUEST_ASSET_PERM)
serializer_classes = {
'default': serializers.RequestAssetPermTicketSerializer,
'approve': EmptySerializer,
@@ -38,10 +37,10 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet):
search_fields = ['user_display', 'title']
def _check_can_set_action(self, instance, action):
if instance.status == instance.STATUS_CLOSED:
raise TicketClosed(detail=_('Ticket closed'))
if instance.status == instance.STATUS.CLOSED:
raise TicketClosed
if instance.action == action:
action_display = dict(instance.ACTION_CHOICES).get(action)
action_display = instance.ACTION.get(action)
raise TicketActionAlready(detail=_('Ticket has %s') % action_display)
@action(detail=False, methods=[GET], permission_classes=[IsValidUser])
@@ -72,7 +71,7 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet):
@action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser])
def reject(self, request, *args, **kwargs):
instance = self.get_object()
action = instance.ACTION_REJECT
action = instance.ACTION.REJECT
self._check_can_set_action(instance, action)
instance.perform_action(action, request.user, self._get_extra_comment(instance))
return Response()
@@ -80,7 +79,7 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet):
@action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser])
def approve(self, request, *args, **kwargs):
instance = self.get_object()
action = instance.ACTION_APPROVE
action = instance.ACTION.APPROVE
self._check_can_set_action(instance, action)
meta = instance.meta
@@ -100,10 +99,10 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet):
if system_user is None:
raise ConfirmedSystemUserChanged(detail=_('Confirmed system-user changed'))
self._create_asset_permission(instance, assets, system_user, request.user)
self._create_asset_permission(instance, assets, system_user)
return Response({'detail': _('Succeed')})
def _create_asset_permission(self, instance: Ticket, assets, system_user, user):
def _create_asset_permission(self, instance: Ticket, assets, system_user):
meta = instance.meta
request = self.request
@@ -120,13 +119,12 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet):
if date_expired:
ap_kwargs['date_expired'] = date_expired
with atomic():
instance.perform_action(instance.ACTION_APPROVE,
request.user,
self._get_extra_comment(instance))
ap = AssetPermission.objects.create(**ap_kwargs)
ap.system_users.add(system_user)
ap.assets.add(*assets)
ap.users.add(user)
instance.perform_action(instance.ACTION.APPROVE,
request.user,
self._get_extra_comment(instance))
ap = AssetPermission.objects.create(**ap_kwargs)
ap.system_users.add(system_user)
ap.assets.add(*assets)
ap.users.add(instance.user)
return ap

View File

@@ -1,3 +1,5 @@
from django.utils.translation import gettext_lazy as _
from common.exceptions import JMSException
@@ -18,12 +20,19 @@ class ConfirmedSystemUserChanged(JMSException):
class TicketClosed(JMSException):
pass
default_detail = _('Ticket closed')
default_code = 'ticket_closed'
class TicketActionAlready(JMSException):
pass
class OrgIdRequiredException(JMSException):
pass
class OnlyTicketAssigneeCanOperate(JMSException):
default_detail = _('Only assignee can operate ticket')
default_code = 'can_not_operate'
class TicketCanNotOperate(JMSException):
default_detail = _('Ticket can not be operated')
default_code = 'ticket_can_not_be_operated'

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.10 on 2020-08-04 07:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0002_auto_20200728_1146'),
]
operations = [
migrations.AlterField(
model_name='ticket',
name='assignee_display',
field=models.CharField(blank=True, default='', max_length=128, null=True, verbose_name='Assignee display name'),
),
]

View File

@@ -5,6 +5,7 @@ from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from common.db.models import ChoiceSet
from common.mixins.models import CommonModelMixin
from common.fields.model import JsonDictTextField
from orgs.mixins.models import OrgModelMixin
@@ -13,26 +14,19 @@ __all__ = ['Ticket', 'Comment']
class Ticket(OrgModelMixin, CommonModelMixin):
STATUS_OPEN = 'open'
STATUS_CLOSED = 'closed'
STATUS_CHOICES = (
(STATUS_OPEN, _("Open")),
(STATUS_CLOSED, _("Closed"))
)
TYPE_GENERAL = 'general'
TYPE_LOGIN_CONFIRM = 'login_confirm'
TYPE_REQUEST_ASSET_PERM = 'request_asset'
TYPE_CHOICES = (
(TYPE_GENERAL, _("General")),
(TYPE_LOGIN_CONFIRM, _("Login confirm")),
(TYPE_REQUEST_ASSET_PERM, _('Request asset permission'))
)
ACTION_APPROVE = 'approve'
ACTION_REJECT = 'reject'
ACTION_CHOICES = (
(ACTION_APPROVE, _('Approve')),
(ACTION_REJECT, _('Reject')),
)
class STATUS(ChoiceSet):
OPEN = 'open', _("Open")
CLOSED = 'closed', _("Closed")
class TYPE(ChoiceSet):
GENERAL = 'general', _("General")
LOGIN_CONFIRM = 'login_confirm', _("Login confirm")
REQUEST_ASSET_PERM = 'request_asset', _('Request asset permission')
class ACTION(ChoiceSet):
APPROVE = 'approve', _('Approve')
REJECT = 'reject', _('Reject')
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_requested', verbose_name=_("User"))
user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
@@ -40,12 +34,12 @@ class Ticket(OrgModelMixin, CommonModelMixin):
body = models.TextField(verbose_name=_("Body"))
meta = JsonDictTextField(verbose_name=_("Meta"), default='{}')
assignee = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_handled', verbose_name=_("Assignee"))
assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name"))
assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name"), default='')
assignees = models.ManyToManyField('users.User', related_name='%(class)s_assigned', verbose_name=_("Assignees"))
assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True)
type = models.CharField(max_length=16, choices=TYPE_CHOICES, default=TYPE_GENERAL, verbose_name=_("Type"))
status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='open')
action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True)
type = models.CharField(max_length=16, choices=TYPE.choices, default=TYPE.GENERAL, verbose_name=_("Type"))
status = models.CharField(choices=STATUS.choices, max_length=16, default='open')
action = models.CharField(choices=ACTION.choices, max_length=16, default='', blank=True)
origin_objects = models.Manager()
@@ -69,30 +63,38 @@ class Ticket(OrgModelMixin, CommonModelMixin):
return self.get_action_display()
def create_status_comment(self, status, user):
if status == self.STATUS_CLOSED:
if status == self.STATUS.CLOSED:
action = _("Close")
else:
action = _("Open")
body = _('{} {} this ticket').format(self.user, action)
self.comments.create(user=user, body=body)
def perform_status(self, status, user):
if self.status == status:
return
def perform_status(self, status, user, extra_comment=None):
self.create_comment(
self.STATUS.get(status),
user,
extra_comment
)
self.status = status
self.assignee = user
self.assignees_display = str(user)
self.save()
def create_action_comment(self, action, user, extra_comment=None):
action_display = dict(self.ACTION_CHOICES).get(action)
def create_comment(self, action_display, user, extra_comment=None):
body = '{} {} {}'.format(user, action_display, _("this ticket"))
if extra_comment is not None:
body += extra_comment
self.comments.create(body=body, user=user, user_display=str(user))
def perform_action(self, action, user, extra_comment=None):
self.create_action_comment(action, user, extra_comment)
self.create_comment(
self.ACTION.get(action),
user,
extra_comment
)
self.action = action
self.status = self.STATUS_CLOSED
self.status = self.STATUS.CLOSED
self.assignee = user
self.assignees_display = str(user)
self.save()

View File

@@ -17,7 +17,7 @@ from ..models import Ticket
class RequestAssetPermTicketSerializer(serializers.ModelSerializer):
ips = serializers.ListField(child=serializers.IPAddressField(), source='meta.ips',
default=list, label=_('IP group'))
hostname = serializers.CharField(max_length=256, source='meta.hostname', default=None,
hostname = serializers.CharField(max_length=256, source='meta.hostname', default='',
allow_blank=True, label=_('Hostname'))
system_user = serializers.CharField(max_length=256, source='meta.system_user', default='',
allow_blank=True, label=_('System user'))
@@ -135,7 +135,7 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer):
def _create_body(self, validated_data):
meta = validated_data['meta']
type = dict(Ticket.TYPE_CHOICES).get(validated_data.get('type', ''))
type = Ticket.TYPE.get(validated_data.get('type', ''))
date_start = dt_parser(meta.get('date_start')).strftime(settings.DATETIME_DISPLAY_FORMAT)
date_expired = dt_parser(meta.get('date_expired')).strftime(settings.DATETIME_DISPLAY_FORMAT)
@@ -159,7 +159,7 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer):
def create(self, validated_data):
# `type` 与 `user` 用户不可提交,
validated_data['type'] = self.Meta.model.TYPE_REQUEST_ASSET_PERM
validated_data['type'] = self.Meta.model.TYPE.REQUEST_ASSET_PERM
validated_data['user'] = self.context['request'].user
# `confirmed` 相关字段只能审批人修改,所以创建时直接清理掉
self._pop_confirmed_fields()

View File

@@ -3,14 +3,18 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from .. import models
from ..exceptions import (
TicketClosed, OnlyTicketAssigneeCanOperate,
TicketCanNotOperate
)
from ..models import Ticket, Comment
__all__ = ['TicketSerializer', 'CommentSerializer']
class TicketSerializer(serializers.ModelSerializer):
class Meta:
model = models.Ticket
model = Ticket
fields = [
'id', 'user', 'user_display', 'title', 'body',
'assignees', 'assignees_display', 'assignee', 'assignee_display',
@@ -32,17 +36,33 @@ class TicketSerializer(serializers.ModelSerializer):
return super().create(validated_data)
def update(self, instance, validated_data):
action = validated_data.get("action")
user = self.context["request"].user
action = validated_data.get('action')
user = self.context['request'].user
if instance.type not in (Ticket.TYPE.GENERAL,
Ticket.TYPE.LOGIN_CONFIRM):
# 暂时的兼容操作吧,后期重构工单
raise TicketCanNotOperate
if instance.status == instance.STATUS.CLOSED:
raise TicketClosed
if action:
if user not in instance.assignees.all():
raise OnlyTicketAssigneeCanOperate
# 有 `action` 时忽略 `status`
validated_data.pop('status', None)
instance = super().update(instance, validated_data)
if not instance.status == instance.STATUS.CLOSED and action:
instance.perform_action(action, user)
else:
status = validated_data.get('status')
instance = super().update(instance, validated_data)
if status:
instance.perform_status(status, user)
if action and user not in instance.assignees.all():
error = {"action": "Only assignees can update"}
raise serializers.ValidationError(error)
if instance.status == instance.STATUS_CLOSED:
validated_data.pop('action')
instance = super().update(instance, validated_data)
if not instance.status == instance.STATUS_CLOSED and action:
instance.perform_action(action, user)
return instance
@@ -65,7 +85,7 @@ class CommentSerializer(serializers.ModelSerializer):
)
class Meta:
model = models.Comment
model = Comment
fields = [
'id', 'ticket', 'body', 'user', 'user_display',
'date_created', 'date_updated'

View File

@@ -20,7 +20,7 @@ def send_new_ticket_mail_to_assignees(ticket: Ticket, assignees):
subject = '{}: {}'.format(_("New ticket"), ticket.title)
# 这里要设置前端地址,因为要直接跳转到页面
if ticket.type == ticket.TYPE_REQUEST_ASSET_PERM:
if ticket.type == ticket.TYPE.REQUEST_ASSET_PERM:
detail_url = urljoin(settings.SITE_URL, f'/tickets/tickets/request-asset-perm/{ticket.id}')
else:
detail_url = urljoin(settings.SITE_URL, f'/tickets/tickets/{ticket.id}')