mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-01-29 21:51:31 +00:00
feat(ticket): 工单关闭生成 Comment
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
18
apps/tickets/migrations/0003_auto_20200804_1551.py
Normal file
18
apps/tickets/migrations/0003_auto_20200804_1551.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}')
|
||||
|
||||
Reference in New Issue
Block a user