mirror of https://github.com/jumpserver/jumpserver
reactor&feat: 重构工单模块 & 支持申请应用工单 (#5352)
* reactor: 修改工单Model,添加工单迁移文件 * reactor: 修改工单Model,添加工单迁移文件 * reactor: 重构工单模块 * reactor: 重构工单模块2 * reactor: 重构工单模块3 * reactor: 重构工单模块4 * reactor: 重构工单模块5 * reactor: 重构工单模块6 * reactor: 重构工单模块7 * reactor: 重构工单模块8 * reactor: 重构工单模块9 * reactor: 重构工单模块10 * reactor: 重构工单模块11 * reactor: 重构工单模块12 * reactor: 重构工单模块13 * reactor: 重构工单模块14 * reactor: 重构工单模块15 * reactor: 重构工单模块16 * reactor: 重构工单模块17 * reactor: 重构工单模块18 * reactor: 重构工单模块19 * reactor: 重构工单模块20 * reactor: 重构工单模块21 * reactor: 重构工单模块22 * reactor: 重构工单模块23 * reactor: 重构工单模块24 * reactor: 重构工单模块25 * reactor: 重构工单模块26 * reactor: 重构工单模块27 * reactor: 重构工单模块28 * reactor: 重构工单模块29 * reactor: 重构工单模块30 * reactor: 重构工单模块31 * reactor: 重构工单模块32 * reactor: 重构工单模块33 * reactor: 重构工单模块34 * reactor: 重构工单模块35 * reactor: 重构工单模块36 * reactor: 重构工单模块37 * reactor: 重构工单模块38 * reactor: 重构工单模块39pull/5364/head
parent
9d4f1a01fd
commit
3b056ff953
|
@ -87,6 +87,23 @@ class SystemUser(BaseUser):
|
||||||
(PROTOCOL_POSTGRESQL, 'postgresql'),
|
(PROTOCOL_POSTGRESQL, 'postgresql'),
|
||||||
(PROTOCOL_K8S, 'k8s'),
|
(PROTOCOL_K8S, 'k8s'),
|
||||||
)
|
)
|
||||||
|
ASSET_CATEGORY_PROTOCOLS = [
|
||||||
|
PROTOCOL_SSH, PROTOCOL_RDP, PROTOCOL_TELNET, PROTOCOL_VNC
|
||||||
|
]
|
||||||
|
APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [
|
||||||
|
PROTOCOL_RDP
|
||||||
|
]
|
||||||
|
APPLICATION_CATEGORY_DB_PROTOCOLS = [
|
||||||
|
PROTOCOL_MYSQL, PROTOCOL_ORACLE, PROTOCOL_MARIADB, PROTOCOL_POSTGRESQL
|
||||||
|
]
|
||||||
|
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
|
||||||
|
PROTOCOL_K8S
|
||||||
|
]
|
||||||
|
APPLICATION_CATEGORY_PROTOCOLS = [
|
||||||
|
*APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS,
|
||||||
|
*APPLICATION_CATEGORY_DB_PROTOCOLS,
|
||||||
|
*APPLICATION_CATEGORY_CLOUD_PROTOCOLS
|
||||||
|
]
|
||||||
|
|
||||||
LOGIN_AUTO = 'auto'
|
LOGIN_AUTO = 'auto'
|
||||||
LOGIN_MANUAL = 'manual'
|
LOGIN_MANUAL = 'manual'
|
||||||
|
@ -133,24 +150,6 @@ class SystemUser(BaseUser):
|
||||||
def login_mode_display(self):
|
def login_mode_display(self):
|
||||||
return self.get_login_mode_display()
|
return self.get_login_mode_display()
|
||||||
|
|
||||||
@property
|
|
||||||
def db_application_protocols(self):
|
|
||||||
return [
|
|
||||||
self.PROTOCOL_MYSQL, self.PROTOCOL_ORACLE, self.PROTOCOL_MARIADB,
|
|
||||||
self.PROTOCOL_POSTGRESQL
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cloud_application_protocols(self):
|
|
||||||
return [self.PROTOCOL_K8S]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def application_category_protocols(self):
|
|
||||||
protocols = []
|
|
||||||
protocols.extend(self.db_application_protocols)
|
|
||||||
protocols.extend(self.cloud_application_protocols)
|
|
||||||
return protocols
|
|
||||||
|
|
||||||
def is_need_push(self):
|
def is_need_push(self):
|
||||||
if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]:
|
if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]:
|
||||||
return True
|
return True
|
||||||
|
@ -163,7 +162,7 @@ class SystemUser(BaseUser):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_need_test_asset_connective(self):
|
def is_need_test_asset_connective(self):
|
||||||
return self.protocol not in self.application_category_protocols
|
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
|
||||||
|
|
||||||
def has_special_auth(self, asset=None, username=None):
|
def has_special_auth(self, asset=None, username=None):
|
||||||
if username is None and self.username_same_with_user:
|
if username is None and self.username_same_with_user:
|
||||||
|
@ -172,7 +171,7 @@ class SystemUser(BaseUser):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_perm_to_asset(self):
|
def can_perm_to_asset(self):
|
||||||
return self.protocol not in self.application_category_protocols
|
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
|
||||||
|
|
||||||
def _merge_auth(self, other):
|
def _merge_auth(self, other):
|
||||||
super()._merge_auth(other)
|
super()._merge_auth(other)
|
||||||
|
@ -205,6 +204,18 @@ class SystemUser(BaseUser):
|
||||||
assets = Asset.objects.filter(id__in=assets_ids)
|
assets = Asset.objects.filter(id__in=assets_ids)
|
||||||
return assets
|
return assets
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_protocol_by_application_type(cls, application_type):
|
||||||
|
from applications.models import Category
|
||||||
|
remote_app_types = list(dict(Category.get_type_choices(Category.remote_app)).keys())
|
||||||
|
if application_type in remote_app_types:
|
||||||
|
return cls.PROTOCOL_RDP
|
||||||
|
cloud_types = list(dict(Category.get_type_choices(Category.cloud)).keys())
|
||||||
|
db_types = list(dict(Category.get_type_choices(Category.db)).keys())
|
||||||
|
other_types = [*cloud_types, *db_types]
|
||||||
|
if application_type in other_types:
|
||||||
|
return application_type
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
unique_together = [('name', 'org_id')]
|
unique_together = [('name', 'org_id')]
|
||||||
|
|
|
@ -45,5 +45,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
|
||||||
ticket = self.get_ticket()
|
ticket = self.get_ticket()
|
||||||
if ticket:
|
if ticket:
|
||||||
request.session.pop('auth_ticket_id', '')
|
request.session.pop('auth_ticket_id', '')
|
||||||
ticket.perform_status('closed', request.user)
|
ticket.close(processor=request.user)
|
||||||
return Response('', status=200)
|
return Response('', status=200)
|
||||||
|
|
|
@ -187,12 +187,12 @@ class AuthMixin:
|
||||||
if not ticket_id:
|
if not ticket_id:
|
||||||
ticket = None
|
ticket = None
|
||||||
else:
|
else:
|
||||||
ticket = Ticket.origin_objects.get(pk=ticket_id)
|
ticket = Ticket.all().filter(id=ticket_id).first()
|
||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
def get_ticket_or_create(self, confirm_setting):
|
def get_ticket_or_create(self, confirm_setting):
|
||||||
ticket = self.get_ticket()
|
ticket = self.get_ticket()
|
||||||
if not ticket or ticket.status == ticket.STATUS.CLOSED:
|
if not ticket or ticket.status_closed:
|
||||||
ticket = confirm_setting.create_confirm_ticket(self.request)
|
ticket = confirm_setting.create_confirm_ticket(self.request)
|
||||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||||
return ticket
|
return ticket
|
||||||
|
@ -201,12 +201,16 @@ class AuthMixin:
|
||||||
ticket = self.get_ticket()
|
ticket = self.get_ticket()
|
||||||
if not ticket:
|
if not ticket:
|
||||||
raise errors.LoginConfirmOtherError('', "Not found")
|
raise errors.LoginConfirmOtherError('', "Not found")
|
||||||
if ticket.status == ticket.STATUS.OPEN:
|
if ticket.status_open:
|
||||||
raise errors.LoginConfirmWaitError(ticket.id)
|
raise errors.LoginConfirmWaitError(ticket.id)
|
||||||
elif ticket.action == ticket.ACTION.APPROVE:
|
elif ticket.is_approved:
|
||||||
self.request.session["auth_confirm"] = "1"
|
self.request.session["auth_confirm"] = "1"
|
||||||
return
|
return
|
||||||
elif ticket.action == ticket.ACTION.REJECT:
|
elif ticket.is_rejected:
|
||||||
|
raise errors.LoginConfirmOtherError(
|
||||||
|
ticket.id, ticket.get_action_display()
|
||||||
|
)
|
||||||
|
elif ticket.is_closed:
|
||||||
raise errors.LoginConfirmOtherError(
|
raise errors.LoginConfirmOtherError(
|
||||||
ticket.id, ticket.get_action_display()
|
ticket.id, ticket.get_action_display()
|
||||||
)
|
)
|
||||||
|
|
|
@ -49,29 +49,37 @@ class LoginConfirmSetting(CommonModelMixin):
|
||||||
def get_user_confirm_setting(cls, user):
|
def get_user_confirm_setting(cls, user):
|
||||||
return get_object_or_none(cls, user=user)
|
return get_object_or_none(cls, user=user)
|
||||||
|
|
||||||
def create_confirm_ticket(self, request=None):
|
@staticmethod
|
||||||
from tickets.models import Ticket
|
def construct_confirm_ticket_meta(request=None):
|
||||||
title = _('Login confirm') + ' {}'.format(self.user)
|
|
||||||
if request:
|
if request:
|
||||||
remote_addr = get_request_ip(request)
|
login_ip = get_request_ip(request)
|
||||||
city = get_ip_city(remote_addr)
|
|
||||||
datetime = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
body = __("{user_key}: {username}<br>"
|
|
||||||
"IP: {ip}<br>"
|
|
||||||
"{city_key}: {city}<br>"
|
|
||||||
"{date_key}: {date}<br>").format(
|
|
||||||
user_key=__("User"), username=self.user,
|
|
||||||
ip=remote_addr, city_key=_("City"), city=city,
|
|
||||||
date_key=__("Datetime"), date=datetime
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
body = ''
|
login_ip = ''
|
||||||
reviewer = self.reviewers.all()
|
login_ip = login_ip or '0.0.0.0'
|
||||||
ticket = Ticket.objects.create(
|
login_city = get_ip_city(login_ip)
|
||||||
user=self.user, title=title, body=body,
|
login_datetime = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
type=Ticket.TYPE.LOGIN_CONFIRM,
|
ticket_meta = {
|
||||||
)
|
'apply_login_ip': login_ip,
|
||||||
ticket.assignees.set(reviewer)
|
'apply_login_city': login_city,
|
||||||
|
'apply_login_datetime': login_datetime,
|
||||||
|
}
|
||||||
|
return ticket_meta
|
||||||
|
|
||||||
|
def create_confirm_ticket(self, request=None):
|
||||||
|
from tickets import const
|
||||||
|
from tickets.models import Ticket
|
||||||
|
ticket_title = _('Login confirm') + ' {}'.format(self.user)
|
||||||
|
ticket_applicant = self.user
|
||||||
|
ticket_meta = self.construct_confirm_ticket_meta(request)
|
||||||
|
ticket_assignees = self.reviewers.all()
|
||||||
|
data = {
|
||||||
|
'title': ticket_title,
|
||||||
|
'type': const.TicketTypeChoices.login_confirm.value,
|
||||||
|
'applicant': ticket_applicant,
|
||||||
|
'meta': ticket_meta,
|
||||||
|
}
|
||||||
|
ticket = Ticket.objects.create(**data)
|
||||||
|
ticket.assignees.set(ticket_assignees)
|
||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -19,7 +19,6 @@ from django.conf import settings
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||||
|
|
||||||
from common.const.front_urls import TICKET_DETAIL
|
|
||||||
from common.utils import get_request_ip, get_object_or_none
|
from common.utils import get_request_ip, get_object_or_none
|
||||||
from users.utils import (
|
from users.utils import (
|
||||||
redirect_user_first_login_or_index
|
redirect_user_first_login_or_index
|
||||||
|
@ -181,6 +180,7 @@ class UserLoginWaitConfirmView(TemplateView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
from tickets.models import Ticket
|
from tickets.models import Ticket
|
||||||
|
from tickets.const import TICKET_DETAIL_URL
|
||||||
ticket_id = self.request.session.get("auth_ticket_id")
|
ticket_id = self.request.session.get("auth_ticket_id")
|
||||||
if not ticket_id:
|
if not ticket_id:
|
||||||
ticket = None
|
ticket = None
|
||||||
|
@ -189,7 +189,7 @@ class UserLoginWaitConfirmView(TemplateView):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
if ticket:
|
if ticket:
|
||||||
timestamp_created = datetime.datetime.timestamp(ticket.date_created)
|
timestamp_created = datetime.datetime.timestamp(ticket.date_created)
|
||||||
ticket_detail_url = TICKET_DETAIL.format(id=ticket_id)
|
ticket_detail_url = TICKET_DETAIL_URL.format(id=ticket_id)
|
||||||
msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
|
msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
|
||||||
Don't close this page""").format(ticket.assignees_display)
|
Don't close this page""").format(ticket.assignees_display)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
|
|
||||||
TICKET_DETAIL = '/ui/#/tickets/tickets/{id}'
|
|
|
@ -7,7 +7,7 @@ import six
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'StringIDField', 'StringManyToManyField', 'ChoiceDisplayField',
|
'StringIDField', 'StringManyToManyField', 'ChoiceDisplayField',
|
||||||
'CustomMetaDictField'
|
'CustomMetaDictField', 'ReadableHiddenField',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,6 +44,17 @@ class DictField(serializers.DictField):
|
||||||
return super().to_representation(value)
|
return super().to_representation(value)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadableHiddenField(serializers.HiddenField):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.write_only = False
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
if hasattr(value, 'id'):
|
||||||
|
return getattr(value, 'id')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class CustomMetaDictField(serializers.DictField):
|
class CustomMetaDictField(serializers.DictField):
|
||||||
"""
|
"""
|
||||||
In use:
|
In use:
|
||||||
|
|
|
@ -60,7 +60,7 @@ class AssetPermissionForm(OrgModelForm):
|
||||||
# 过滤系统用户
|
# 过滤系统用户
|
||||||
system_users_field = self.fields.get('system_users')
|
system_users_field = self.fields.get('system_users')
|
||||||
system_users_field.queryset = SystemUser.objects.exclude(
|
system_users_field.queryset = SystemUser.objects.exclude(
|
||||||
protocol__in=SystemUser.application_category_protocols
|
protocol__in=SystemUser.ASSET_CATEGORY_PROTOCOLS
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_nodes_initial(self, nodes):
|
def set_nodes_initial(self, nodes):
|
||||||
|
|
|
@ -25,7 +25,7 @@ class DatabaseAppPermissionCreateUpdateForm(OrgModelForm):
|
||||||
# 过滤系统用户
|
# 过滤系统用户
|
||||||
system_users_field = self.fields.get('system_users')
|
system_users_field = self.fields.get('system_users')
|
||||||
system_users_field.queryset = SystemUser.objects.filter(
|
system_users_field.queryset = SystemUser.objects.filter(
|
||||||
protocol__in=SystemUser.application_category_protocols
|
protocol__in=SystemUser.APPLICATION_CATEGORY_DB_PROTOCOLS
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -68,6 +68,11 @@ class Action:
|
||||||
choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i]
|
choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i]
|
||||||
return choices
|
return choices
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def value_to_choices_display(cls, value):
|
||||||
|
choices = cls.value_to_choices(value)
|
||||||
|
return [dict(cls.choices())[i] for i in choices]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def choices_to_value(cls, value):
|
def choices_to_value(cls, value):
|
||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from .ticket import *
|
from .ticket import *
|
||||||
from .request_asset_perm import *
|
from .assignee import *
|
||||||
|
from .comment import *
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
|
||||||
|
from rest_framework import viewsets
|
||||||
|
|
||||||
|
from users.models import User
|
||||||
|
from common.permissions import IsValidUser
|
||||||
|
from common.exceptions import JMSException
|
||||||
|
from orgs.utils import get_org_by_id
|
||||||
|
from .. import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class AssigneeViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
permission_classes = (IsValidUser,)
|
||||||
|
serializer_class = serializers.AssigneeSerializer
|
||||||
|
filter_fields = ('id', 'name', 'username', 'email', 'source')
|
||||||
|
search_fields = filter_fields
|
||||||
|
|
||||||
|
def get_org(self):
|
||||||
|
org_id = self.request.query_params.get('org_id')
|
||||||
|
org = get_org_by_id(org_id)
|
||||||
|
if not org:
|
||||||
|
raise JMSException('The organization `{}` does not exist'.format(org_id))
|
||||||
|
return org
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
org = self.get_org()
|
||||||
|
queryset = User.get_super_and_org_admins(org=org)
|
||||||
|
return queryset
|
|
@ -0,0 +1,38 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
|
||||||
|
from rest_framework import viewsets, mixins
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from common.exceptions import JMSException
|
||||||
|
from common.utils import lazyproperty
|
||||||
|
from tickets import serializers
|
||||||
|
from tickets.models import Ticket
|
||||||
|
from tickets.permissions.comment import IsAssignee, IsApplicant, IsSwagger
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['CommentViewSet']
|
||||||
|
|
||||||
|
|
||||||
|
class CommentViewSet(mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
|
serializer_class = serializers.CommentSerializer
|
||||||
|
permission_classes = (IsSwagger| IsAssignee | IsApplicant,)
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def ticket(self):
|
||||||
|
if getattr(self, 'swagger_fake_view', False):
|
||||||
|
return None
|
||||||
|
ticket_id = self.request.query_params.get('ticket_id')
|
||||||
|
try:
|
||||||
|
ticket = get_object_or_404(Ticket, pk=ticket_id)
|
||||||
|
return ticket
|
||||||
|
except Exception as e:
|
||||||
|
raise JMSException(str(e))
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
context = super().get_serializer_context()
|
||||||
|
context['ticket'] = self.ticket
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = self.ticket.comments.all()
|
||||||
|
return queryset
|
|
@ -1,157 +0,0 @@
|
||||||
import textwrap
|
|
||||||
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from rest_framework.mixins import ListModelMixin
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from orgs.models import Organization, ROLE as ORG_ROLE
|
|
||||||
from users.models.user import User
|
|
||||||
from common.const.http import POST
|
|
||||||
from common.drf.api import JMSModelViewSet, JmsGenericViewSet
|
|
||||||
from common.permissions import IsValidUser, IsObjectOwner
|
|
||||||
from common.utils.timezone import dt_parser
|
|
||||||
from common.drf.serializers import EmptySerializer
|
|
||||||
from perms.models.asset_permission import AssetPermission, Asset
|
|
||||||
from perms.models import Action
|
|
||||||
from assets.models.user import SystemUser
|
|
||||||
from ..exceptions import (
|
|
||||||
ConfirmedAssetsChanged, ConfirmedSystemUserChanged,
|
|
||||||
TicketClosed, TicketActionAlready, NotHaveConfirmedAssets,
|
|
||||||
NotHaveConfirmedSystemUser
|
|
||||||
)
|
|
||||||
from .. import serializers
|
|
||||||
from ..models import Ticket
|
|
||||||
from ..permissions import IsAssignee
|
|
||||||
|
|
||||||
|
|
||||||
class RequestAssetPermTicketViewSet(JMSModelViewSet):
|
|
||||||
queryset = Ticket.origin_objects.filter(type=Ticket.TYPE.REQUEST_ASSET_PERM)
|
|
||||||
serializer_classes = {
|
|
||||||
'default': serializers.RequestAssetPermTicketSerializer,
|
|
||||||
'approve': EmptySerializer,
|
|
||||||
'reject': EmptySerializer,
|
|
||||||
'close': EmptySerializer,
|
|
||||||
'assignees': serializers.AssigneeSerializer,
|
|
||||||
}
|
|
||||||
permission_classes = (IsValidUser,)
|
|
||||||
filter_fields = ['status', 'title', 'action', 'user_display', 'org_id']
|
|
||||||
search_fields = ['user_display', 'title']
|
|
||||||
|
|
||||||
def _check_can_set_action(self, instance, action):
|
|
||||||
if instance.status == instance.STATUS.CLOSED:
|
|
||||||
raise TicketClosed
|
|
||||||
if instance.action == action:
|
|
||||||
action_display = instance.ACTION.get(action)
|
|
||||||
raise TicketActionAlready(detail=_('Ticket has %s') % action_display)
|
|
||||||
|
|
||||||
def _get_extra_comment(self, instance):
|
|
||||||
meta = instance.meta
|
|
||||||
ips = ', '.join(meta.get('ips', []))
|
|
||||||
confirmed_assets_id = meta.get('confirmed_assets', [])
|
|
||||||
confirmed_system_users_id = meta.get('confirmed_system_users', [])
|
|
||||||
confirmed_assets = Asset.objects.filter(id__in=confirmed_assets_id)
|
|
||||||
confirmed_system_users = SystemUser.objects.filter(id__in=confirmed_system_users_id)
|
|
||||||
confirmed_assets_display = ', '.join([str(i) for i in confirmed_assets])
|
|
||||||
confirmed_system_users_display = ', '.join([str(i) for i in confirmed_system_users])
|
|
||||||
|
|
||||||
return textwrap.dedent('''
|
|
||||||
{}: {}
|
|
||||||
{}: {}
|
|
||||||
{}: {}
|
|
||||||
{}: {}
|
|
||||||
{}: {}
|
|
||||||
'''.format(
|
|
||||||
_('IP group'), ips,
|
|
||||||
_('Hostname'), meta.get('hostname', ''),
|
|
||||||
_('System user'), meta.get('system_user', ''),
|
|
||||||
_('Confirmed assets'), confirmed_assets_display,
|
|
||||||
_('Confirmed system users'), confirmed_system_users_display
|
|
||||||
))
|
|
||||||
|
|
||||||
@action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser])
|
|
||||||
def reject(self, request, *args, **kwargs):
|
|
||||||
instance = self.get_object()
|
|
||||||
action = instance.ACTION.REJECT
|
|
||||||
self._check_can_set_action(instance, action)
|
|
||||||
instance.perform_action(action, request.user, self._get_extra_comment(instance))
|
|
||||||
return Response()
|
|
||||||
|
|
||||||
@action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser])
|
|
||||||
def approve(self, request, *args, **kwargs):
|
|
||||||
instance = self.get_object()
|
|
||||||
action = instance.ACTION.APPROVE
|
|
||||||
self._check_can_set_action(instance, action)
|
|
||||||
|
|
||||||
meta = instance.meta
|
|
||||||
confirmed_assets = meta.get('confirmed_assets', [])
|
|
||||||
assets = list(Asset.objects.filter(id__in=confirmed_assets))
|
|
||||||
if not assets:
|
|
||||||
raise NotHaveConfirmedAssets(detail=_('Confirm assets first'))
|
|
||||||
|
|
||||||
if len(assets) != len(confirmed_assets):
|
|
||||||
raise ConfirmedAssetsChanged(detail=_('Confirmed assets changed'))
|
|
||||||
|
|
||||||
confirmed_system_users = meta.get('confirmed_system_users', [])
|
|
||||||
if not confirmed_system_users:
|
|
||||||
raise NotHaveConfirmedSystemUser(detail=_('Confirm system-users first'))
|
|
||||||
|
|
||||||
system_users = SystemUser.objects.filter(id__in=confirmed_system_users)
|
|
||||||
if system_users is None:
|
|
||||||
raise ConfirmedSystemUserChanged(detail=_('Confirmed system-users changed'))
|
|
||||||
|
|
||||||
instance.perform_action(instance.ACTION.APPROVE,
|
|
||||||
request.user,
|
|
||||||
self._get_extra_comment(instance))
|
|
||||||
self._create_asset_permission(instance, assets, system_users)
|
|
||||||
return Response({'detail': _('Succeed')})
|
|
||||||
|
|
||||||
@action(detail=True, methods=[POST], permission_classes=[IsAssignee | IsObjectOwner])
|
|
||||||
def close(self, request, *args, **kwargs):
|
|
||||||
instance = self.get_object()
|
|
||||||
instance.status = Ticket.STATUS.CLOSED
|
|
||||||
instance.save()
|
|
||||||
return Response({'detail': _('Succeed')})
|
|
||||||
|
|
||||||
def _create_asset_permission(self, instance: Ticket, assets, system_users):
|
|
||||||
meta = instance.meta
|
|
||||||
actions = meta.get('actions', Action.CONNECT)
|
|
||||||
|
|
||||||
ap_kwargs = {
|
|
||||||
'name': _('From request ticket: {} {}').format(instance.user_display, instance.id),
|
|
||||||
'created_by': self.request.user.username,
|
|
||||||
'comment': _('{} request assets, approved by {}').format(instance.user_display,
|
|
||||||
instance.assignee_display),
|
|
||||||
'actions': actions,
|
|
||||||
}
|
|
||||||
date_start = dt_parser(meta.get('date_start'))
|
|
||||||
date_expired = dt_parser(meta.get('date_expired'))
|
|
||||||
if date_start:
|
|
||||||
ap_kwargs['date_start'] = date_start
|
|
||||||
if date_expired:
|
|
||||||
ap_kwargs['date_expired'] = date_expired
|
|
||||||
|
|
||||||
ap = AssetPermission.objects.create(**ap_kwargs)
|
|
||||||
ap.system_users.add(*system_users)
|
|
||||||
ap.assets.add(*assets)
|
|
||||||
ap.users.add(instance.user)
|
|
||||||
|
|
||||||
return ap
|
|
||||||
|
|
||||||
|
|
||||||
class AssigneeViewSet(ListModelMixin, JmsGenericViewSet):
|
|
||||||
serializer_class = serializers.AssigneeSerializer
|
|
||||||
permission_classes = (IsValidUser,)
|
|
||||||
filter_fields = ('username', 'email', 'name', 'id', 'source')
|
|
||||||
search_fields = filter_fields
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
user = self.request.user
|
|
||||||
org_id = self.request.query_params.get('org_id', Organization.DEFAULT_ID)
|
|
||||||
|
|
||||||
q = Q(role=User.ROLE.ADMIN)
|
|
||||||
if org_id != Organization.DEFAULT_ID:
|
|
||||||
q |= Q(m2m_org_members__role=ORG_ROLE.ADMIN, orgs__id=org_id, orgs__members=user)
|
|
||||||
org_admins = User.objects.filter(q).distinct()
|
|
||||||
return org_admins
|
|
|
@ -1,44 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import viewsets
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
|
|
||||||
from common.permissions import IsValidUser
|
|
||||||
from common.utils import lazyproperty
|
|
||||||
from .. import serializers, models, mixins
|
|
||||||
|
|
||||||
|
|
||||||
class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet):
|
|
||||||
serializer_class = serializers.TicketSerializer
|
|
||||||
queryset = models.Ticket.origin_objects.all()
|
|
||||||
permission_classes = (IsValidUser,)
|
|
||||||
filter_fields = ['status', 'title', 'action', 'user_display']
|
|
||||||
search_fields = ['user_display', 'title']
|
|
||||||
|
|
||||||
|
|
||||||
class TicketCommentViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = serializers.CommentSerializer
|
|
||||||
http_method_names = ['get', 'post']
|
|
||||||
|
|
||||||
def check_permissions(self, request):
|
|
||||||
ticket = self.ticket
|
|
||||||
if request.user == ticket.user or \
|
|
||||||
request.user in ticket.assignees.all():
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
context = super().get_serializer_context()
|
|
||||||
context['ticket'] = self.ticket
|
|
||||||
return context
|
|
||||||
|
|
||||||
@lazyproperty
|
|
||||||
def ticket(self):
|
|
||||||
ticket_id = self.kwargs.get('ticket_id')
|
|
||||||
ticket = get_object_or_404(models.Ticket, pk=ticket_id)
|
|
||||||
return ticket
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = self.ticket.comments.all()
|
|
||||||
return queryset
|
|
|
@ -0,0 +1 @@
|
||||||
|
from .ticket import *
|
|
@ -0,0 +1,66 @@
|
||||||
|
from common.exceptions import JMSException
|
||||||
|
from tickets import const, serializers
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['TicketMetaSerializerViewMixin']
|
||||||
|
|
||||||
|
|
||||||
|
class TicketMetaSerializerViewMixin:
|
||||||
|
apply_asset_meta_serializer_classes = {
|
||||||
|
'apply': serializers.TicketMetaApplyAssetApplySerializer,
|
||||||
|
'approve': serializers.TicketMetaApplyAssetApproveSerializer,
|
||||||
|
}
|
||||||
|
apply_application_meta_serializer_classes = {
|
||||||
|
'apply': serializers.TicketMetaApplyApplicationApplySerializer,
|
||||||
|
'approve': serializers.TicketMetaApplyApplicationApproveSerializer,
|
||||||
|
}
|
||||||
|
login_confirm_meta_serializer_classes = {
|
||||||
|
'apply': serializers.TicketMetaLoginConfirmApplySerializer,
|
||||||
|
}
|
||||||
|
meta_serializer_classes = {
|
||||||
|
const.TicketTypeChoices.apply_asset.value: apply_asset_meta_serializer_classes,
|
||||||
|
const.TicketTypeChoices.apply_application.value: apply_application_meta_serializer_classes,
|
||||||
|
const.TicketTypeChoices.login_confirm.value: login_confirm_meta_serializer_classes,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_serializer_meta_field_class(self):
|
||||||
|
tp = self.request.query_params.get('type')
|
||||||
|
if not tp:
|
||||||
|
return None
|
||||||
|
tp_choices = const.TicketTypeChoices.types()
|
||||||
|
if tp not in tp_choices:
|
||||||
|
raise JMSException(
|
||||||
|
'Invalid query parameter `type`, select from the following options: {}'
|
||||||
|
''.format(tp_choices)
|
||||||
|
)
|
||||||
|
meta_class = self.meta_serializer_classes.get(tp, {}).get(self.action)
|
||||||
|
return meta_class
|
||||||
|
|
||||||
|
def get_serializer_meta_field(self):
|
||||||
|
if self.action not in ['apply', 'approve']:
|
||||||
|
return None
|
||||||
|
meta_class = self.get_serializer_meta_field_class()
|
||||||
|
if not meta_class:
|
||||||
|
return None
|
||||||
|
return meta_class(required=True)
|
||||||
|
|
||||||
|
def reset_view_metadata_action(self):
|
||||||
|
if self.action not in ['metadata']:
|
||||||
|
return
|
||||||
|
view_action = self.request.query_params.get('action')
|
||||||
|
if not view_action:
|
||||||
|
raise JMSException('The `metadata` methods must carry parameter `action`')
|
||||||
|
setattr(self, 'action', view_action)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
self.reset_view_metadata_action()
|
||||||
|
serializer_class = super().get_serializer_class()
|
||||||
|
if getattr(self, 'swagger_fake_view', False):
|
||||||
|
return serializer_class
|
||||||
|
meta_field = self.get_serializer_meta_field()
|
||||||
|
if not meta_field:
|
||||||
|
return serializer_class
|
||||||
|
serializer_class = type(
|
||||||
|
meta_field.__class__.__name__, (serializer_class,), {'meta': meta_field}
|
||||||
|
)
|
||||||
|
return serializer_class
|
|
@ -0,0 +1,66 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import MethodNotAllowed
|
||||||
|
|
||||||
|
from common.mixins.api import CommonApiMixin
|
||||||
|
from common.permissions import IsValidUser, IsOrgAdmin
|
||||||
|
from common.const.http import POST, PUT
|
||||||
|
from tickets import serializers
|
||||||
|
from tickets.permissions.ticket import IsAssignee, NotClosed
|
||||||
|
from tickets.models import Ticket
|
||||||
|
from tickets.api.ticket.mixin import TicketMetaSerializerViewMixin
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['TicketViewSet']
|
||||||
|
|
||||||
|
|
||||||
|
class TicketViewSet(TicketMetaSerializerViewMixin, CommonApiMixin, viewsets.ModelViewSet):
|
||||||
|
permission_classes = (IsValidUser,)
|
||||||
|
serializer_class = serializers.TicketSerializer
|
||||||
|
serializer_classes = {
|
||||||
|
'default': serializers.TicketDisplaySerializer,
|
||||||
|
'display': serializers.TicketDisplaySerializer,
|
||||||
|
'apply': serializers.TicketApplySerializer,
|
||||||
|
'approve': serializers.TicketApproveSerializer,
|
||||||
|
'reject': serializers.TicketRejectSerializer,
|
||||||
|
'close': serializers.TicketCloseSerializer,
|
||||||
|
}
|
||||||
|
filter_fields = [
|
||||||
|
'id', 'title', 'type', 'action', 'status', 'applicant', 'applicant_display', 'processor',
|
||||||
|
'processor_display', 'assignees__id'
|
||||||
|
]
|
||||||
|
search_fields = [
|
||||||
|
'title', 'action', 'type', 'status', 'applicant_display', 'processor_display'
|
||||||
|
]
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
raise MethodNotAllowed(self.action)
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
raise MethodNotAllowed(self.action)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
raise MethodNotAllowed(self.action)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = Ticket.get_user_related_tickets(self.request.user)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
@action(detail=False, methods=[POST])
|
||||||
|
def apply(self, request, *args, **kwargs):
|
||||||
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed])
|
||||||
|
def approve(self, request, *args, **kwargs):
|
||||||
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed])
|
||||||
|
def reject(self, request, *args, **kwargs):
|
||||||
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed])
|
||||||
|
def close(self, request, *args, **kwargs):
|
||||||
|
return super().update(request, *args, **kwargs)
|
|
@ -0,0 +1,27 @@
|
||||||
|
from django.db.models import TextChoices
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
TICKET_DETAIL_URL = '/ui/#/tickets/tickets/{id}'
|
||||||
|
|
||||||
|
|
||||||
|
class TicketTypeChoices(TextChoices):
|
||||||
|
general = 'general', _("General")
|
||||||
|
login_confirm = 'login_confirm', _("Login confirm")
|
||||||
|
apply_asset = 'apply_asset', _('Apply for asset')
|
||||||
|
apply_application = 'apply_application', _('Apply for application')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def types(cls):
|
||||||
|
return set(dict(cls.choices).keys())
|
||||||
|
|
||||||
|
|
||||||
|
class TicketActionChoices(TextChoices):
|
||||||
|
apply = 'apply', _('Apply')
|
||||||
|
approve = 'approve', _('Approve')
|
||||||
|
reject = 'reject', _('Reject')
|
||||||
|
close = 'close', _('Close')
|
||||||
|
|
||||||
|
|
||||||
|
class TicketStatusChoices(TextChoices):
|
||||||
|
open = 'open', _("Open")
|
||||||
|
closed = 'closed', _("Closed")
|
|
@ -1,38 +0,0 @@
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from common.exceptions import JMSException
|
|
||||||
|
|
||||||
|
|
||||||
class NotHaveConfirmedAssets(JMSException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmedAssetsChanged(JMSException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NotHaveConfirmedSystemUser(JMSException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmedSystemUserChanged(JMSException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TicketClosed(JMSException):
|
|
||||||
default_detail = _('Ticket closed')
|
|
||||||
default_code = 'ticket_closed'
|
|
||||||
|
|
||||||
|
|
||||||
class TicketActionAlready(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'
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
# Generated by Django 3.1 on 2020-12-24 10:21
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import tickets.models.ticket
|
||||||
|
|
||||||
|
TICKET_TYPE_APPLY_ASSET = 'apply_asset'
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_field_type(tp):
|
||||||
|
if tp == 'request_asset':
|
||||||
|
return TICKET_TYPE_APPLY_ASSET
|
||||||
|
return tp
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_field_meta(tp, old_meta):
|
||||||
|
if tp != TICKET_TYPE_APPLY_ASSET or not old_meta:
|
||||||
|
return old_meta
|
||||||
|
old_meta_hostname = old_meta.get('hostname')
|
||||||
|
old_meta_system_user = old_meta.get('system_user')
|
||||||
|
new_meta = {
|
||||||
|
'apply_ip_group': old_meta.get('ips', []),
|
||||||
|
'apply_hostname_group': [old_meta_hostname] if old_meta_hostname else [],
|
||||||
|
'apply_system_user_group': [old_meta_system_user] if old_meta_system_user else [],
|
||||||
|
'apply_actions': old_meta.get('actions'),
|
||||||
|
'apply_date_start': old_meta.get('date_start'),
|
||||||
|
'apply_date_expired': old_meta.get('date_expired'),
|
||||||
|
|
||||||
|
'approve_assets': old_meta.get('confirmed_assets', []),
|
||||||
|
'approve_system_users': old_meta.get('confirmed_system_users', []),
|
||||||
|
'approve_actions': old_meta.get('actions'),
|
||||||
|
'approve_date_start': old_meta.get('date_start'),
|
||||||
|
'approve_date_expired': old_meta.get('date_expired'),
|
||||||
|
}
|
||||||
|
return new_meta
|
||||||
|
|
||||||
|
|
||||||
|
ACTION_APPLY = 'apply'
|
||||||
|
ACTION_CLOSE = 'close'
|
||||||
|
STATUS_OPEN = 'open'
|
||||||
|
STATUS_CLOSED = 'closed'
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_field_action(old_action, old_status):
|
||||||
|
if old_action:
|
||||||
|
return old_action
|
||||||
|
if old_status == STATUS_OPEN:
|
||||||
|
return ACTION_APPLY
|
||||||
|
if old_status == STATUS_CLOSED:
|
||||||
|
return ACTION_CLOSE
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_tickets_fields_name(apps, schema_editor):
|
||||||
|
ticket_model = apps.get_model("tickets", "Ticket")
|
||||||
|
tickets = ticket_model.origin_objects.all()
|
||||||
|
|
||||||
|
for ticket in tickets:
|
||||||
|
ticket.applicant = ticket.user
|
||||||
|
ticket.applicant_display = ticket.user_display
|
||||||
|
ticket.processor = ticket.assignee
|
||||||
|
ticket.processor_display = ticket.assignee_display
|
||||||
|
ticket.action = migrate_field_action(ticket.action, ticket.status)
|
||||||
|
ticket.type = migrate_field_type(ticket.type)
|
||||||
|
ticket.meta = migrate_field_meta(ticket.type, ticket.meta)
|
||||||
|
ticket.meta['body'] = ticket.body
|
||||||
|
ticket.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('tickets', '0006_auto_20201023_1628'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# model ticket
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='applicant',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applied_tickets', to=settings.AUTH_USER_MODEL, verbose_name='Applicant'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='applicant_display',
|
||||||
|
field=models.CharField(default='No', max_length=256, verbose_name='Applicant display'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='processor',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='processed_tickets', to=settings.AUTH_USER_MODEL, verbose_name='Processor'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='processor_display',
|
||||||
|
field=models.CharField(blank=True, default='No', max_length=256, null=True, verbose_name='Processor display'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='assignees',
|
||||||
|
field=models.ManyToManyField(related_name='assigned_tickets', to=settings.AUTH_USER_MODEL, verbose_name='Assignees'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='assignees_display',
|
||||||
|
field=models.TextField(blank=True, default='No', verbose_name='Assignees display'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='meta',
|
||||||
|
field=models.JSONField(encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Meta'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('apply_asset', 'Apply for asset'), ('apply_application', 'Apply for application')], default='general', max_length=64, verbose_name='Type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='action',
|
||||||
|
field=models.CharField(choices=[('apply', 'Apply'), ('approve', 'Approve'), ('reject', 'Reject'), ('close', 'Close')], default='apply', max_length=16, verbose_name='Action')),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('open', 'Open'), ('closed', 'Closed')], default='open', max_length=16, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_tickets_fields_name),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='user',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='user_display',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='assignee',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='assignee_display',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='body',
|
||||||
|
),
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='ticket',
|
||||||
|
managers=[
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# model comment
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='comment',
|
||||||
|
name='user_display',
|
||||||
|
field=models.CharField(max_length=256, verbose_name='User display name'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,17 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
from django.db.models import Q
|
|
||||||
from .models import Ticket
|
|
||||||
|
|
||||||
|
|
||||||
class TicketMixin:
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
assign = self.request.GET.get('assign', None)
|
|
||||||
if assign is None:
|
|
||||||
queryset = Ticket.get_related_tickets(self.request.user, queryset)
|
|
||||||
elif assign in ['1']:
|
|
||||||
queryset = Ticket.get_assigned_tickets(self.request.user, queryset)
|
|
||||||
else:
|
|
||||||
queryset = Ticket.get_my_tickets(self.request.user, queryset)
|
|
||||||
return queryset
|
|
|
@ -1,3 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from .ticket import *
|
from .ticket import *
|
||||||
|
from .comment import *
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from common.mixins.models import CommonModelMixin
|
||||||
|
|
||||||
|
__all__ = ['Comment']
|
||||||
|
|
||||||
|
|
||||||
|
class Comment(CommonModelMixin):
|
||||||
|
ticket = models.ForeignKey(
|
||||||
|
'tickets.Ticket', on_delete=models.CASCADE, related_name='comments'
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
'users.User', on_delete=models.SET_NULL, null=True, related_name='comments',
|
||||||
|
verbose_name=_("User")
|
||||||
|
)
|
||||||
|
user_display = models.CharField(max_length=256, verbose_name=_("User display name"))
|
||||||
|
body = models.TextField(verbose_name=_("Body"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('date_created', )
|
|
@ -1,141 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
__all__ = ['Ticket', 'Comment']
|
|
||||||
|
|
||||||
|
|
||||||
class Ticket(OrgModelMixin, CommonModelMixin):
|
|
||||||
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"))
|
|
||||||
|
|
||||||
title = models.CharField(max_length=256, verbose_name=_("Title"))
|
|
||||||
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"), 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)
|
|
||||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
|
||||||
|
|
||||||
origin_objects = models.Manager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '{}: {}'.format(self.user_display, self.title)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def body_as_html(self):
|
|
||||||
return self.body.replace('\n', '<br/>')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status_display(self):
|
|
||||||
return self.get_status_display()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type_display(self):
|
|
||||||
return self.get_type_display()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_display(self):
|
|
||||||
return self.get_action_display()
|
|
||||||
|
|
||||||
def create_status_comment(self, status, user):
|
|
||||||
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, extra_comment=None):
|
|
||||||
self.create_comment(
|
|
||||||
self.STATUS.get(status),
|
|
||||||
user,
|
|
||||||
extra_comment
|
|
||||||
)
|
|
||||||
self.status = status
|
|
||||||
self.assignee = user
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
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_comment(
|
|
||||||
self.ACTION.get(action),
|
|
||||||
user,
|
|
||||||
extra_comment
|
|
||||||
)
|
|
||||||
self.action = action
|
|
||||||
self.status = self.STATUS.CLOSED
|
|
||||||
self.assignee = user
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def is_assignee(self, user):
|
|
||||||
return self.assignees.filter(id=user.id).exists()
|
|
||||||
|
|
||||||
def is_user(self, user):
|
|
||||||
return self.user == user
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_related_tickets(cls, user, queryset=None):
|
|
||||||
if queryset is None:
|
|
||||||
queryset = cls.objects.all()
|
|
||||||
queryset = queryset.filter(
|
|
||||||
Q(assignees=user) | Q(user=user)
|
|
||||||
).distinct()
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_assigned_tickets(cls, user, queryset=None):
|
|
||||||
if queryset is None:
|
|
||||||
queryset = cls.objects.all()
|
|
||||||
queryset = queryset.filter(assignees=user)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_my_tickets(cls, user, queryset=None):
|
|
||||||
if queryset is None:
|
|
||||||
queryset = cls.objects.all()
|
|
||||||
queryset = queryset.filter(user=user)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ('-date_created',)
|
|
||||||
|
|
||||||
|
|
||||||
class Comment(CommonModelMixin):
|
|
||||||
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name='comments')
|
|
||||||
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_("User"), related_name='comments')
|
|
||||||
user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
|
|
||||||
body = models.TextField(verbose_name=_("Body"))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ('date_created', )
|
|
|
@ -0,0 +1 @@
|
||||||
|
from .ticket import *
|
|
@ -0,0 +1 @@
|
||||||
|
from .ticket import TicketModelMixin
|
|
@ -0,0 +1,100 @@
|
||||||
|
from django.utils.translation import ugettext as __
|
||||||
|
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||||
|
from applications.models import Application, Category
|
||||||
|
from assets.models import SystemUser
|
||||||
|
from perms.models import ApplicationPermission
|
||||||
|
|
||||||
|
|
||||||
|
class ConstructBodyMixin:
|
||||||
|
|
||||||
|
def construct_apply_application_applied_body(self):
|
||||||
|
apply_category = self.meta['apply_category']
|
||||||
|
apply_category_display = dict(Category.choices)[apply_category]
|
||||||
|
apply_type = self.meta['apply_type']
|
||||||
|
apply_type_display = dict(Category.get_type_choices(apply_category))[apply_type]
|
||||||
|
apply_application_group = self.meta['apply_application_group']
|
||||||
|
apply_system_user_group = self.meta['apply_system_user_group']
|
||||||
|
apply_date_start = self.meta['apply_date_start']
|
||||||
|
apply_date_expired = self.meta['apply_date_expired']
|
||||||
|
applied_body = '''{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
'''.format(
|
||||||
|
__('Applied category'), apply_category_display,
|
||||||
|
__('Applied type'), apply_type_display,
|
||||||
|
__('Applied application group'), apply_application_group,
|
||||||
|
__('Applied system user group'), apply_system_user_group,
|
||||||
|
__('Applied date start'), apply_date_start,
|
||||||
|
__('Applied date expired'), apply_date_expired,
|
||||||
|
)
|
||||||
|
return applied_body
|
||||||
|
|
||||||
|
def construct_apply_application_approved_body(self):
|
||||||
|
# 审批信息
|
||||||
|
approve_applications_id = self.meta['approve_applications']
|
||||||
|
approve_system_users_id = self.meta['approve_system_users']
|
||||||
|
with tmp_to_org(self.org_id):
|
||||||
|
approve_applications = Application.objects.filter(id__in=approve_applications_id)
|
||||||
|
approve_system_users = SystemUser.objects.filter(id__in=approve_system_users_id)
|
||||||
|
approve_applications_display = [str(application) for application in approve_applications]
|
||||||
|
approve_system_users_display = [str(system_user) for system_user in approve_system_users]
|
||||||
|
approve_date_start = self.meta['approve_date_start']
|
||||||
|
approve_date_expired = self.meta['approve_date_expired']
|
||||||
|
approved_body = '''{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
'''.format(
|
||||||
|
__('Approved applications'), ', '.join(approve_applications_display),
|
||||||
|
__('Approved system users'), ', '.join(approve_system_users_display),
|
||||||
|
__('Approved date start'), approve_date_start,
|
||||||
|
__('Approved date expired'), approve_date_expired
|
||||||
|
)
|
||||||
|
return approved_body
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePermissionMixin:
|
||||||
|
|
||||||
|
def create_apply_application_permission(self):
|
||||||
|
with tmp_to_root_org():
|
||||||
|
application_permission = ApplicationPermission.objects.filter(id=self.id).first()
|
||||||
|
if application_permission:
|
||||||
|
return application_permission
|
||||||
|
|
||||||
|
apply_category = self.meta['apply_category']
|
||||||
|
apply_type = self.meta['apply_type']
|
||||||
|
approved_applications_id = self.meta['approve_applications']
|
||||||
|
approve_system_users_id = self.meta['approve_system_users']
|
||||||
|
approve_date_start = self.meta['approve_date_start']
|
||||||
|
approve_date_expired = self.meta['approve_date_expired']
|
||||||
|
permission_name = '{}({})'.format(
|
||||||
|
__('Created by ticket ({})'.format(self.title)), str(self.id)[:4]
|
||||||
|
)
|
||||||
|
permission_comment = __(
|
||||||
|
'Created by the ticket, '
|
||||||
|
'ticket title: {}, '
|
||||||
|
'ticket applicant: {}, '
|
||||||
|
'ticket processor: {}, '
|
||||||
|
'ticket ID: {}'
|
||||||
|
''.format(self.title, self.applicant_display, self.processor_display, str(self.id))
|
||||||
|
)
|
||||||
|
permissions_data = {
|
||||||
|
'id': self.id,
|
||||||
|
'name': permission_name,
|
||||||
|
'category': apply_category,
|
||||||
|
'type': apply_type,
|
||||||
|
'comment': permission_comment,
|
||||||
|
'created_by': self.processor_display,
|
||||||
|
'date_start': approve_date_start,
|
||||||
|
'date_expired': approve_date_expired,
|
||||||
|
}
|
||||||
|
with tmp_to_org(self.org_id):
|
||||||
|
application_permission = ApplicationPermission.objects.create(**permissions_data)
|
||||||
|
application_permission.users.add(self.applicant)
|
||||||
|
application_permission.applications.set(approved_applications_id)
|
||||||
|
application_permission.system_users.set(approve_system_users_id)
|
||||||
|
|
||||||
|
return application_permission
|
|
@ -0,0 +1,99 @@
|
||||||
|
from django.utils.translation import ugettext as __
|
||||||
|
|
||||||
|
from perms.models import AssetPermission, Action
|
||||||
|
from assets.models import Asset, SystemUser
|
||||||
|
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||||
|
|
||||||
|
|
||||||
|
class ConstructBodyMixin:
|
||||||
|
def construct_apply_asset_applied_body(self):
|
||||||
|
apply_ip_group = self.meta['apply_ip_group']
|
||||||
|
apply_hostname_group = self.meta['apply_hostname_group']
|
||||||
|
apply_system_user_group = self.meta['apply_system_user_group']
|
||||||
|
apply_actions = self.meta['apply_actions']
|
||||||
|
apply_actions_display = Action.value_to_choices_display(apply_actions)
|
||||||
|
apply_actions_display = [str(action_display) for action_display in apply_actions_display]
|
||||||
|
apply_date_start = self.meta['apply_date_start']
|
||||||
|
apply_date_expired = self.meta['apply_date_expired']
|
||||||
|
applied_body = '''{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {}
|
||||||
|
'''.format(
|
||||||
|
__('Applied IP group'), apply_ip_group,
|
||||||
|
__("Applied hostname group"), apply_hostname_group,
|
||||||
|
__("Applied system user group"), apply_system_user_group,
|
||||||
|
__("Applied actions"), apply_actions_display,
|
||||||
|
__('Applied date start'), apply_date_start,
|
||||||
|
__('Applied date expired'), apply_date_expired,
|
||||||
|
)
|
||||||
|
return applied_body
|
||||||
|
|
||||||
|
def construct_apply_asset_approved_body(self):
|
||||||
|
approve_assets_id = self.meta['approve_assets']
|
||||||
|
approve_system_users_id = self.meta['approve_system_users']
|
||||||
|
with tmp_to_org(self.org_id):
|
||||||
|
approve_assets = Asset.objects.filter(id__in=approve_assets_id)
|
||||||
|
approve_system_users = SystemUser.objects.filter(id__in=approve_system_users_id)
|
||||||
|
approve_assets_display = [str(asset) for asset in approve_assets]
|
||||||
|
approve_system_users_display = [str(system_user) for system_user in approve_system_users]
|
||||||
|
approve_actions = self.meta['approve_actions']
|
||||||
|
approve_actions_display = Action.value_to_choices_display(approve_actions)
|
||||||
|
approve_actions_display = [str(action_display) for action_display in approve_actions_display]
|
||||||
|
approve_date_start = self.meta['approve_date_start']
|
||||||
|
approve_date_expired = self.meta['approve_date_expired']
|
||||||
|
approved_body = '''{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {}
|
||||||
|
'''.format(
|
||||||
|
__('Approved assets'), ', '.join(approve_assets_display),
|
||||||
|
__('Approved system users'), ', '.join(approve_system_users_display),
|
||||||
|
__('Approved actions'), ', '.join(approve_actions_display),
|
||||||
|
__('Approved date start'), approve_date_start,
|
||||||
|
__('Approved date expired'), approve_date_expired,
|
||||||
|
)
|
||||||
|
return approved_body
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePermissionMixin:
|
||||||
|
def create_apply_asset_permission(self):
|
||||||
|
with tmp_to_root_org():
|
||||||
|
asset_permission = AssetPermission.objects.filter(id=self.id).first()
|
||||||
|
if asset_permission:
|
||||||
|
return asset_permission
|
||||||
|
|
||||||
|
approve_assets_id = self.meta['approve_assets']
|
||||||
|
approve_system_users_id = self.meta['approve_system_users']
|
||||||
|
approve_actions = self.meta['approve_actions']
|
||||||
|
approve_date_start = self.meta['approve_date_start']
|
||||||
|
approve_date_expired = self.meta['approve_date_expired']
|
||||||
|
permission_name = '{}({})'.format(
|
||||||
|
__('Created by ticket ({})'.format(self.title)), str(self.id)[:4]
|
||||||
|
)
|
||||||
|
permission_comment = __(
|
||||||
|
'Created by the ticket, '
|
||||||
|
'ticket title: {}, '
|
||||||
|
'ticket applicant: {}, '
|
||||||
|
'ticket processor: {}, '
|
||||||
|
'ticket ID: {}'
|
||||||
|
''.format(self.title, self.applicant_display, self.processor_display, str(self.id))
|
||||||
|
)
|
||||||
|
permission_data = {
|
||||||
|
'id': self.id,
|
||||||
|
'name': permission_name,
|
||||||
|
'comment': permission_comment,
|
||||||
|
'created_by': self.processor_display,
|
||||||
|
'actions': approve_actions,
|
||||||
|
'date_start': approve_date_start,
|
||||||
|
'date_expired': approve_date_expired,
|
||||||
|
}
|
||||||
|
with tmp_to_org(self.org_id):
|
||||||
|
asset_permission = AssetPermission.objects.create(**permission_data)
|
||||||
|
asset_permission.users.add(self.applicant)
|
||||||
|
asset_permission.assets.set(approve_assets_id)
|
||||||
|
asset_permission.system_users.set(approve_system_users_id)
|
||||||
|
|
||||||
|
return asset_permission
|
|
@ -0,0 +1,100 @@
|
||||||
|
import textwrap
|
||||||
|
from django.utils.translation import ugettext as __
|
||||||
|
|
||||||
|
|
||||||
|
class ConstructBodyMixin:
|
||||||
|
# applied body
|
||||||
|
def construct_applied_body(self):
|
||||||
|
construct_method = getattr(self, f'construct_{self.type}_applied_body', lambda: 'No')
|
||||||
|
applied_body = construct_method()
|
||||||
|
body = '''
|
||||||
|
{}:
|
||||||
|
{}
|
||||||
|
'''.format(
|
||||||
|
__('Ticket applied info'),
|
||||||
|
applied_body
|
||||||
|
)
|
||||||
|
return body
|
||||||
|
|
||||||
|
# approved body
|
||||||
|
def construct_approved_body(self):
|
||||||
|
construct_method = getattr(self, f'construct_{self.type}_approved_body', lambda: 'No')
|
||||||
|
approved_body = construct_method()
|
||||||
|
body = '''
|
||||||
|
{}:
|
||||||
|
{}
|
||||||
|
'''.format(
|
||||||
|
__('Ticket approved info'),
|
||||||
|
approved_body
|
||||||
|
)
|
||||||
|
return body
|
||||||
|
|
||||||
|
# meta body
|
||||||
|
def construct_meta_body(self):
|
||||||
|
applied_body = self.construct_applied_body()
|
||||||
|
if not self.is_approved:
|
||||||
|
return applied_body
|
||||||
|
approved_body = self.construct_approved_body()
|
||||||
|
return applied_body + approved_body
|
||||||
|
|
||||||
|
# basic body
|
||||||
|
def construct_basic_body(self):
|
||||||
|
basic_body = '''
|
||||||
|
{}:
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {}
|
||||||
|
'''.format(
|
||||||
|
__("Ticket basic info"),
|
||||||
|
__('Ticket title'), self.title,
|
||||||
|
__('Ticket type'), self.get_type_display(),
|
||||||
|
__('Ticket applicant'), self.applicant_display,
|
||||||
|
__('Ticket assignees'), self.assignees_display,
|
||||||
|
__('Ticket processor'), self.processor_display,
|
||||||
|
__('Ticket action'), self.get_action_display(),
|
||||||
|
__('Ticket status'), self.get_status_display()
|
||||||
|
)
|
||||||
|
return basic_body
|
||||||
|
|
||||||
|
@property
|
||||||
|
def body(self):
|
||||||
|
old_body = self.meta.get('body')
|
||||||
|
if old_body:
|
||||||
|
# 之前版本的body
|
||||||
|
return old_body
|
||||||
|
basic_body = self.construct_basic_body()
|
||||||
|
meta_body = self.construct_meta_body()
|
||||||
|
return basic_body + meta_body
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePermissionMixin:
|
||||||
|
# create permission
|
||||||
|
def create_permission(self):
|
||||||
|
create_method = getattr(self, f'create_{self.type}_permission', lambda: None)
|
||||||
|
create_method()
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCommentMixin:
|
||||||
|
def create_comment(self, comment_body):
|
||||||
|
comment_data = {
|
||||||
|
'body': comment_body,
|
||||||
|
'user': self.processor,
|
||||||
|
'user_display': self.processor_display
|
||||||
|
}
|
||||||
|
return self.comments.create(**comment_data)
|
||||||
|
|
||||||
|
def create_approved_comment(self):
|
||||||
|
comment_body = self.construct_approved_body()
|
||||||
|
# 页面展示需要取消缩进
|
||||||
|
comment_body = textwrap.dedent(comment_body)
|
||||||
|
self.create_comment(comment_body)
|
||||||
|
|
||||||
|
def create_action_comment(self):
|
||||||
|
comment_body = __(
|
||||||
|
'User {} {} the ticket'.format(self.processor_display, self.get_action_display())
|
||||||
|
)
|
||||||
|
self.create_comment(comment_body)
|
|
@ -0,0 +1,18 @@
|
||||||
|
from django.utils.translation import ugettext as __
|
||||||
|
|
||||||
|
|
||||||
|
class ConstructBodyMixin:
|
||||||
|
|
||||||
|
def construct_login_confirm_applied_body(self):
|
||||||
|
apply_login_ip = self.meta['apply_login_ip']
|
||||||
|
apply_login_city = self.meta['apply_login_city']
|
||||||
|
apply_login_datetime = self.meta['apply_login_datetime']
|
||||||
|
applied_body = '''{}: {},
|
||||||
|
{}: {},
|
||||||
|
{}: {}
|
||||||
|
'''.format(
|
||||||
|
__("Applied login IP"), apply_login_ip,
|
||||||
|
__("Applied login city"), apply_login_city,
|
||||||
|
__("Applied login datetime"), apply_login_datetime,
|
||||||
|
)
|
||||||
|
return applied_body
|
|
@ -0,0 +1,32 @@
|
||||||
|
from . import base, apply_asset, apply_application, login_confirm
|
||||||
|
|
||||||
|
__all__ = ['TicketModelMixin']
|
||||||
|
|
||||||
|
|
||||||
|
class TicketConstructBodyMixin(
|
||||||
|
base.ConstructBodyMixin,
|
||||||
|
apply_asset.ConstructBodyMixin,
|
||||||
|
apply_application.ConstructBodyMixin,
|
||||||
|
login_confirm.ConstructBodyMixin
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TicketCreatePermissionMixin(
|
||||||
|
base.CreatePermissionMixin,
|
||||||
|
apply_asset.CreatePermissionMixin,
|
||||||
|
apply_application.CreatePermissionMixin
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TicketCreateCommentMixin(
|
||||||
|
base.CreateCommentMixin
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TicketModelMixin(
|
||||||
|
TicketConstructBodyMixin, TicketCreatePermissionMixin, TicketCreateCommentMixin
|
||||||
|
):
|
||||||
|
pass
|
|
@ -0,0 +1,159 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from common.mixins.models import CommonModelMixin
|
||||||
|
from orgs.mixins.models import OrgModelMixin
|
||||||
|
from orgs.utils import tmp_to_root_org, tmp_to_org
|
||||||
|
from tickets import const
|
||||||
|
from .mixin import TicketModelMixin
|
||||||
|
|
||||||
|
__all__ = ['Ticket']
|
||||||
|
|
||||||
|
|
||||||
|
class ModelJSONFieldEncoder(json.JSONEncoder):
|
||||||
|
""" 解决一些类型的字段不能序列化的问题 """
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, datetime):
|
||||||
|
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
|
||||||
|
if isinstance(obj, uuid.UUID):
|
||||||
|
return str(obj)
|
||||||
|
else:
|
||||||
|
return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class Ticket(TicketModelMixin, CommonModelMixin, OrgModelMixin):
|
||||||
|
title = models.CharField(max_length=256, verbose_name=_("Title"))
|
||||||
|
type = models.CharField(
|
||||||
|
max_length=64, choices=const.TicketTypeChoices.choices,
|
||||||
|
default=const.TicketTypeChoices.general.value, verbose_name=_("Type")
|
||||||
|
)
|
||||||
|
meta = models.JSONField(encoder=ModelJSONFieldEncoder, verbose_name=_("Meta"))
|
||||||
|
action = models.CharField(
|
||||||
|
choices=const.TicketActionChoices.choices, max_length=16,
|
||||||
|
default=const.TicketActionChoices.apply.value, verbose_name=_("Action")
|
||||||
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=16, choices=const.TicketStatusChoices.choices,
|
||||||
|
default=const.TicketStatusChoices.open.value, verbose_name=_("Status")
|
||||||
|
)
|
||||||
|
# 申请人
|
||||||
|
applicant = models.ForeignKey(
|
||||||
|
'users.User', related_name='applied_tickets', on_delete=models.SET_NULL, null=True,
|
||||||
|
verbose_name=_("Applicant")
|
||||||
|
)
|
||||||
|
applicant_display = models.CharField(
|
||||||
|
max_length=256, default='No', verbose_name=_("Applicant display")
|
||||||
|
)
|
||||||
|
# 处理人
|
||||||
|
processor = models.ForeignKey(
|
||||||
|
'users.User', related_name='processed_tickets', on_delete=models.SET_NULL, null=True,
|
||||||
|
verbose_name=_("Processor")
|
||||||
|
)
|
||||||
|
processor_display = models.CharField(
|
||||||
|
max_length=256, blank=True, null=True, default='No', verbose_name=_("Processor display")
|
||||||
|
)
|
||||||
|
# 受理人列表
|
||||||
|
assignees = models.ManyToManyField(
|
||||||
|
'users.User', related_name='assigned_tickets', verbose_name=_("Assignees")
|
||||||
|
)
|
||||||
|
assignees_display = models.TextField(
|
||||||
|
blank=True, default='No', verbose_name=_("Assignees display")
|
||||||
|
)
|
||||||
|
# 评论
|
||||||
|
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('-date_created',)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{}({})'.format(self.title, self.applicant_display)
|
||||||
|
|
||||||
|
|
||||||
|
def has_assignee(self, assignee):
|
||||||
|
return self.assignees.filter(id=assignee.id).exists()
|
||||||
|
|
||||||
|
# status
|
||||||
|
@property
|
||||||
|
def status_closed(self):
|
||||||
|
return self.status == const.TicketStatusChoices.closed.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_open(self):
|
||||||
|
return self.status == const.TicketStatusChoices.open.value
|
||||||
|
|
||||||
|
# action
|
||||||
|
@property
|
||||||
|
def is_applied(self):
|
||||||
|
return self.action == const.TicketActionChoices.apply.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_approved(self):
|
||||||
|
return self.action == const.TicketActionChoices.approve.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_rejected(self):
|
||||||
|
return self.action == const.TicketActionChoices.reject.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
return self.action == const.TicketActionChoices.close.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_processed(self):
|
||||||
|
return self.is_approved or self.is_rejected or self.is_closed
|
||||||
|
|
||||||
|
# perform action
|
||||||
|
def close(self, processor):
|
||||||
|
self.processor = processor
|
||||||
|
self.action = const.TicketActionChoices.close.value
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
# tickets
|
||||||
|
@classmethod
|
||||||
|
def all(cls):
|
||||||
|
with tmp_to_root_org():
|
||||||
|
return Ticket.objects.all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_user_related_tickets(cls, user):
|
||||||
|
queries = None
|
||||||
|
tickets = cls.all()
|
||||||
|
if user.is_superuser:
|
||||||
|
pass
|
||||||
|
elif user.is_super_auditor:
|
||||||
|
pass
|
||||||
|
elif user.is_org_admin:
|
||||||
|
admin_orgs_id = [
|
||||||
|
str(org_id) for org_id in user.admin_orgs.values_list('id', flat=True)
|
||||||
|
]
|
||||||
|
assigned_tickets_id = [
|
||||||
|
str(ticket_id) for ticket_id in user.assigned_tickets.values_list('id', flat=True)
|
||||||
|
]
|
||||||
|
queries = Q(applicant=user)
|
||||||
|
queries |= Q(processor=user)
|
||||||
|
queries |= Q(org_id__in=admin_orgs_id)
|
||||||
|
queries |= Q(id__in=assigned_tickets_id)
|
||||||
|
elif user.is_org_auditor:
|
||||||
|
audit_orgs_id = [
|
||||||
|
str(org_id) for org_id in user.audit_orgs.values_list('id', flat=True)
|
||||||
|
]
|
||||||
|
queries = Q(org_id__in=audit_orgs_id)
|
||||||
|
elif user.is_common_user:
|
||||||
|
queries = Q(applicant=user)
|
||||||
|
else:
|
||||||
|
tickets = cls.objects.none()
|
||||||
|
if queries:
|
||||||
|
tickets = tickets.filter(queries)
|
||||||
|
return tickets.distinct()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
with tmp_to_org(self.org_id):
|
||||||
|
# 确保保存的org_id的是自身的值
|
||||||
|
return super().save(*args, **kwargs)
|
|
@ -1,9 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework.permissions import BasePermission
|
|
||||||
|
|
||||||
|
|
||||||
class IsAssignee(BasePermission):
|
|
||||||
def has_object_permission(self, request, view, obj):
|
|
||||||
return obj.is_assignee(request.user)
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
|
||||||
|
class IsSwagger(permissions.BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return getattr(view, 'swagger_fake_view', False)
|
||||||
|
|
||||||
|
|
||||||
|
class IsApplicant(permissions.BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return request.user == view.ticket.applicant
|
||||||
|
|
||||||
|
|
||||||
|
class IsAssignee(permissions.BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return view.ticket.has_assignee(request.user)
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
|
||||||
|
class IsAssignee(permissions.BasePermission):
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return obj.has_assignee(request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class NotClosed(permissions.BasePermission):
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return not obj.status_closed
|
|
@ -1,4 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from .ticket import *
|
from .ticket import *
|
||||||
from .request_asset_perm import *
|
from .assignee import *
|
||||||
|
from .comment import *
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
__all__ = ['AssigneeSerializer']
|
||||||
|
|
||||||
|
|
||||||
|
class AssigneeSerializer(serializers.Serializer):
|
||||||
|
id = serializers.UUIDField()
|
||||||
|
name = serializers.CharField()
|
||||||
|
username = serializers.CharField()
|
|
@ -0,0 +1,29 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
from ..models import Comment
|
||||||
|
from common.fields.serializer import ReadableHiddenField
|
||||||
|
|
||||||
|
__all__ = ['CommentSerializer']
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentTicket(object):
|
||||||
|
ticket = None
|
||||||
|
|
||||||
|
def set_context(self, serializer_field):
|
||||||
|
self.ticket = serializer_field.context['ticket']
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
return self.ticket
|
||||||
|
|
||||||
|
|
||||||
|
class CommentSerializer(serializers.ModelSerializer):
|
||||||
|
ticket = ReadableHiddenField(default=CurrentTicket())
|
||||||
|
user = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = [
|
||||||
|
'id', 'ticket', 'body', 'user', 'user_display', 'date_created', 'date_updated'
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
'user_display', 'date_created', 'date_updated'
|
||||||
|
]
|
|
@ -1,241 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from common.utils.timezone import dt_parser, dt_formater
|
|
||||||
from orgs.utils import tmp_to_root_org
|
|
||||||
from orgs.models import Organization, ROLE as ORG_ROLE
|
|
||||||
from assets.models import Asset, SystemUser
|
|
||||||
from users.models.user import User
|
|
||||||
from perms.serializers import ActionsField
|
|
||||||
from perms.models import Action
|
|
||||||
from ..models import Ticket
|
|
||||||
|
|
||||||
|
|
||||||
class RequestAssetPermTicketSerializer(serializers.ModelSerializer):
|
|
||||||
actions = ActionsField(source='meta.actions', choices=Action.DB_CHOICES,
|
|
||||||
default=Action.CONNECT)
|
|
||||||
ips = serializers.ListField(child=serializers.IPAddressField(), source='meta.ips',
|
|
||||||
default=list, label=_('IP group'))
|
|
||||||
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'))
|
|
||||||
date_start = serializers.DateTimeField(source='meta.date_start', allow_null=True,
|
|
||||||
required=False, label=_('Date start'))
|
|
||||||
date_expired = serializers.DateTimeField(source='meta.date_expired', allow_null=True,
|
|
||||||
required=False, label=_('Date expired'))
|
|
||||||
confirmed_assets = serializers.ListField(child=serializers.UUIDField(),
|
|
||||||
source='meta.confirmed_assets',
|
|
||||||
default=list, required=False,
|
|
||||||
label=_('Confirmed assets'))
|
|
||||||
confirmed_system_users = serializers.ListField(child=serializers.UUIDField(),
|
|
||||||
source='meta.confirmed_system_users',
|
|
||||||
default=list, required=False,
|
|
||||||
label=_('Confirmed system user'))
|
|
||||||
assets_waitlist_url = serializers.SerializerMethodField()
|
|
||||||
system_users_waitlist_url = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Ticket
|
|
||||||
mini_fields = ['id', 'title']
|
|
||||||
small_fields = [
|
|
||||||
'status', 'action', 'date_created', 'date_updated', 'system_users_waitlist_url',
|
|
||||||
'type', 'type_display', 'action_display', 'ips', 'confirmed_assets',
|
|
||||||
'date_start', 'date_expired', 'confirmed_system_users', 'hostname',
|
|
||||||
'assets_waitlist_url', 'system_user', 'org_id', 'actions', 'comment'
|
|
||||||
]
|
|
||||||
m2m_fields = [
|
|
||||||
'user', 'user_display', 'assignees', 'assignees_display',
|
|
||||||
'assignee', 'assignee_display'
|
|
||||||
]
|
|
||||||
|
|
||||||
fields = mini_fields + small_fields + m2m_fields
|
|
||||||
read_only_fields = [
|
|
||||||
'user_display', 'assignees_display', 'type', 'user', 'status',
|
|
||||||
'date_created', 'date_updated', 'action', 'id', 'assignee',
|
|
||||||
'assignee_display',
|
|
||||||
]
|
|
||||||
extra_kwargs = {
|
|
||||||
'status': {'label': _('Status')},
|
|
||||||
'action': {'label': _('Action')},
|
|
||||||
'user_display': {'label': _('User')},
|
|
||||||
'org_id': {'required': True}
|
|
||||||
}
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
org_id = attrs.get('org_id')
|
|
||||||
assignees = attrs.get('assignees')
|
|
||||||
|
|
||||||
instance = self.instance
|
|
||||||
if instance is not None:
|
|
||||||
if org_id and not assignees:
|
|
||||||
assignees = list(instance.assignees.all())
|
|
||||||
elif assignees and not org_id:
|
|
||||||
org_id = instance.org_id
|
|
||||||
elif assignees and org_id:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
user = self.context['request'].user
|
|
||||||
org = Organization.get_instance(org_id)
|
|
||||||
if org is None:
|
|
||||||
raise serializers.ValidationError(_('Invalid `org_id`'))
|
|
||||||
|
|
||||||
q = Q(role=User.ROLE.ADMIN)
|
|
||||||
if not org.is_default():
|
|
||||||
q |= Q(m2m_org_members__role=ORG_ROLE.ADMIN, orgs__id=org_id, orgs__members=user)
|
|
||||||
|
|
||||||
q &= Q(id__in=[assignee.id for assignee in assignees])
|
|
||||||
count = User.objects.filter(q).distinct().count()
|
|
||||||
if count != len(assignees):
|
|
||||||
raise serializers.ValidationError(_('Field `assignees` must be organization admin or superuser'))
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def get_system_users_waitlist_url(self, instance: Ticket):
|
|
||||||
if not self._is_assignee(instance):
|
|
||||||
return None
|
|
||||||
return reverse('api-assets:system-user-list')
|
|
||||||
|
|
||||||
def get_assets_waitlist_url(self, instance: Ticket):
|
|
||||||
if not self._is_assignee(instance):
|
|
||||||
return None
|
|
||||||
|
|
||||||
asset_api = reverse('api-assets:asset-list')
|
|
||||||
query = ''
|
|
||||||
|
|
||||||
meta = instance.meta
|
|
||||||
hostname = meta.get('hostname')
|
|
||||||
if hostname:
|
|
||||||
query = '?search=%s' % hostname
|
|
||||||
|
|
||||||
return asset_api + query
|
|
||||||
|
|
||||||
def _recommend_assets(self, data, instance):
|
|
||||||
confirmed_assets = data.get('confirmed_assets')
|
|
||||||
if not confirmed_assets and self._is_assignee(instance):
|
|
||||||
ips = data.get('ips')
|
|
||||||
hostname = data.get('hostname')
|
|
||||||
limit = 5
|
|
||||||
|
|
||||||
q = Q(id=None)
|
|
||||||
if ips:
|
|
||||||
limit = len(ips) + 2
|
|
||||||
q |= Q(ip__in=ips)
|
|
||||||
if hostname:
|
|
||||||
q |= Q(hostname__icontains=hostname)
|
|
||||||
|
|
||||||
recomand_assets_id = Asset.objects.filter(q)[:limit].values_list('id', flat=True)
|
|
||||||
data['confirmed_assets'] = [str(i) for i in recomand_assets_id]
|
|
||||||
|
|
||||||
def _recommend_system_users(self, data, instance):
|
|
||||||
confirmed_system_users = data.get('confirmed_system_users')
|
|
||||||
system_user = data.get('system_user')
|
|
||||||
|
|
||||||
if all((not confirmed_system_users, self._is_assignee(instance), system_user)):
|
|
||||||
recomand_system_users_id = SystemUser.objects.filter(
|
|
||||||
name__icontains=system_user
|
|
||||||
)[:3].values_list('id', flat=True)
|
|
||||||
data['confirmed_system_users'] = [str(i) for i in recomand_system_users_id]
|
|
||||||
|
|
||||||
def to_representation(self, instance):
|
|
||||||
data = super().to_representation(instance)
|
|
||||||
self._recommend_assets(data, instance)
|
|
||||||
self._recommend_system_users(data, instance)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _create_body(self, validated_data):
|
|
||||||
meta = validated_data['meta']
|
|
||||||
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)
|
|
||||||
|
|
||||||
validated_data['body'] = _('''
|
|
||||||
Type: {type}<br>
|
|
||||||
User: {username}<br>
|
|
||||||
Ip group: {ips}<br>
|
|
||||||
Hostname: {hostname}<br>
|
|
||||||
System user: {system_user}<br>
|
|
||||||
Date start: {date_start}<br>
|
|
||||||
Date expired: {date_expired}<br>
|
|
||||||
''').format(
|
|
||||||
type=type,
|
|
||||||
username=validated_data.get('user', ''),
|
|
||||||
ips=', '.join(meta.get('ips', [])),
|
|
||||||
hostname=meta.get('hostname', ''),
|
|
||||||
system_user=meta.get('system_user', ''),
|
|
||||||
date_start=date_start,
|
|
||||||
date_expired=date_expired
|
|
||||||
)
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
# `type` 与 `user` 用户不可提交,
|
|
||||||
validated_data['type'] = self.Meta.model.TYPE.REQUEST_ASSET_PERM
|
|
||||||
validated_data['user'] = self.context['request'].user
|
|
||||||
# `confirmed` 相关字段只能审批人修改,所以创建时直接清理掉
|
|
||||||
self._pop_confirmed_fields()
|
|
||||||
self._create_body(validated_data)
|
|
||||||
return super().create(validated_data)
|
|
||||||
|
|
||||||
def save(self, **kwargs):
|
|
||||||
"""
|
|
||||||
做了一些数据转换
|
|
||||||
"""
|
|
||||||
meta = self.validated_data.get('meta', {})
|
|
||||||
|
|
||||||
org_id = self.validated_data.get('org_id')
|
|
||||||
if org_id is not None and org_id == Organization.DEFAULT_ID:
|
|
||||||
self.validated_data['org_id'] = ''
|
|
||||||
|
|
||||||
# 时间的转换,好烦😭,可能有更好的办法吧
|
|
||||||
date_start = meta.get('date_start')
|
|
||||||
if date_start:
|
|
||||||
meta['date_start'] = dt_formater(date_start)
|
|
||||||
|
|
||||||
date_expired = meta.get('date_expired')
|
|
||||||
if date_expired:
|
|
||||||
meta['date_expired'] = dt_formater(date_expired)
|
|
||||||
|
|
||||||
# UUID 的转换
|
|
||||||
confirmed_system_users = meta.get('confirmed_system_users')
|
|
||||||
if confirmed_system_users:
|
|
||||||
meta['confirmed_system_users'] = [str(system_user) for system_user in confirmed_system_users]
|
|
||||||
|
|
||||||
confirmed_assets = meta.get('confirmed_assets')
|
|
||||||
if confirmed_assets:
|
|
||||||
meta['confirmed_assets'] = [str(asset) for asset in confirmed_assets]
|
|
||||||
|
|
||||||
with tmp_to_root_org():
|
|
||||||
return super().save(**kwargs)
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
new_meta = validated_data['meta']
|
|
||||||
if not self._is_assignee(instance):
|
|
||||||
self._pop_confirmed_fields()
|
|
||||||
|
|
||||||
# Json 字段保存的坑😭
|
|
||||||
old_meta = instance.meta
|
|
||||||
meta = {}
|
|
||||||
meta.update(old_meta)
|
|
||||||
meta.update(new_meta)
|
|
||||||
validated_data['meta'] = meta
|
|
||||||
|
|
||||||
return super().update(instance, validated_data)
|
|
||||||
|
|
||||||
def _pop_confirmed_fields(self):
|
|
||||||
meta = self.validated_data['meta']
|
|
||||||
meta.pop('confirmed_assets', None)
|
|
||||||
meta.pop('confirmed_system_users', None)
|
|
||||||
|
|
||||||
def _is_assignee(self, obj: Ticket):
|
|
||||||
user = self.context['request'].user
|
|
||||||
return obj.is_assignee(user)
|
|
||||||
|
|
||||||
|
|
||||||
class AssigneeSerializer(serializers.Serializer):
|
|
||||||
id = serializers.UUIDField()
|
|
||||||
name = serializers.CharField()
|
|
||||||
username = serializers.CharField()
|
|
|
@ -1,95 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from ..exceptions import (
|
|
||||||
TicketClosed, OnlyTicketAssigneeCanOperate,
|
|
||||||
TicketCanNotOperate
|
|
||||||
)
|
|
||||||
from ..models import Ticket, Comment
|
|
||||||
|
|
||||||
__all__ = ['TicketSerializer', 'CommentSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class TicketSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Ticket
|
|
||||||
fields = [
|
|
||||||
'id', 'user', 'user_display', 'title', 'body',
|
|
||||||
'assignees', 'assignees_display', 'assignee', 'assignee_display',
|
|
||||||
'status', 'action', 'date_created', 'date_updated',
|
|
||||||
'type', 'type_display', 'action_display',
|
|
||||||
]
|
|
||||||
read_only_fields = [
|
|
||||||
'user_display', 'assignees_display',
|
|
||||||
'date_created', 'date_updated',
|
|
||||||
]
|
|
||||||
extra_kwargs = {
|
|
||||||
'status': {'label': _('Status')},
|
|
||||||
'action': {'label': _('Action')},
|
|
||||||
'user_display': {'label': _('User')}
|
|
||||||
}
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
validated_data.pop('action')
|
|
||||||
return super().create(validated_data)
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
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)
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class CurrentTicket(object):
|
|
||||||
ticket = None
|
|
||||||
|
|
||||||
def set_context(self, serializer_field):
|
|
||||||
self.ticket = serializer_field.context['ticket']
|
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
return self.ticket
|
|
||||||
|
|
||||||
|
|
||||||
class CommentSerializer(serializers.ModelSerializer):
|
|
||||||
user = serializers.HiddenField(
|
|
||||||
default=serializers.CurrentUserDefault(),
|
|
||||||
)
|
|
||||||
ticket = serializers.HiddenField(
|
|
||||||
default=CurrentTicket()
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Comment
|
|
||||||
fields = [
|
|
||||||
'id', 'ticket', 'body', 'user', 'user_display',
|
|
||||||
'date_created', 'date_updated'
|
|
||||||
]
|
|
||||||
read_only_fields = [
|
|
||||||
'user_display', 'date_created', 'date_updated'
|
|
||||||
]
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .ticket import *
|
||||||
|
from .meta import *
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .apply_asset import *
|
||||||
|
from .apply_application import *
|
||||||
|
from .login_confirm import *
|
|
@ -0,0 +1,93 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from applications.models import Category, Application
|
||||||
|
from assets.models import SystemUser
|
||||||
|
from .base import BaseTicketMetaSerializer, BaseTicketMetaApproveSerializerMixin
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'TicketMetaApplyApplicationApplySerializer',
|
||||||
|
'TicketMetaApplyApplicationApproveSerializer',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TicketMetaApplyApplicationSerializer(BaseTicketMetaSerializer):
|
||||||
|
# 申请信息
|
||||||
|
apply_category = serializers.ChoiceField(
|
||||||
|
choices=Category.choices, required=True, label=_('Category')
|
||||||
|
)
|
||||||
|
apply_type = serializers.ChoiceField(
|
||||||
|
choices=Category.get_all_type_choices(), required=True, label=_('Type')
|
||||||
|
)
|
||||||
|
apply_application_group = serializers.ListField(
|
||||||
|
child=serializers.CharField(), default=list, label=_('Application group')
|
||||||
|
)
|
||||||
|
apply_system_user_group = serializers.ListField(
|
||||||
|
child=serializers.CharField(), default=list, label=_('System user group')
|
||||||
|
)
|
||||||
|
apply_date_start = serializers.DateTimeField(
|
||||||
|
required=True, label=_('Date start')
|
||||||
|
)
|
||||||
|
apply_date_expired = serializers.DateTimeField(
|
||||||
|
required=True, label=_('Date expired')
|
||||||
|
)
|
||||||
|
# 审批信息
|
||||||
|
approve_applications = serializers.ListField(
|
||||||
|
child=serializers.UUIDField(), required=True,
|
||||||
|
label=_('Approve applications')
|
||||||
|
)
|
||||||
|
approve_system_users = serializers.ListField(
|
||||||
|
child=serializers.UUIDField(), required=True,
|
||||||
|
label=_('Approve system users')
|
||||||
|
)
|
||||||
|
approve_date_start = serializers.DateTimeField(
|
||||||
|
required=True, label=_('Date start')
|
||||||
|
)
|
||||||
|
approve_date_expired = serializers.DateTimeField(
|
||||||
|
required=True, label=_('Date expired')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TicketMetaApplyApplicationApplySerializer(TicketMetaApplyApplicationSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
'apply_category', 'apply_type',
|
||||||
|
'apply_application_group', 'apply_system_user_group',
|
||||||
|
'apply_date_start', 'apply_date_expired'
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate_apply_type(self, tp):
|
||||||
|
category = self.root.initial_data['meta'].get('apply_category')
|
||||||
|
if not category:
|
||||||
|
return tp
|
||||||
|
valid_type_types = list((dict(Category.get_type_choices(category)).keys()))
|
||||||
|
if tp in valid_type_types:
|
||||||
|
return tp
|
||||||
|
error = _('Type `{}` is not a valid choice `({}){}`'.format(tp, category, valid_type_types))
|
||||||
|
raise serializers.ValidationError(error)
|
||||||
|
|
||||||
|
|
||||||
|
class TicketMetaApplyApplicationApproveSerializer(BaseTicketMetaApproveSerializerMixin,
|
||||||
|
TicketMetaApplyApplicationSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = {
|
||||||
|
'approve_applications', 'approve_system_users',
|
||||||
|
'approve_date_start', 'approve_date_expired'
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_approve_applications(self, approve_applications):
|
||||||
|
application_type = self.root.instance.meta['apply_type']
|
||||||
|
queries = {'type': application_type}
|
||||||
|
applications_id = self.filter_approve_resources(
|
||||||
|
resource_model=Application, resources_id=approve_applications, queries=queries
|
||||||
|
)
|
||||||
|
return applications_id
|
||||||
|
|
||||||
|
def validate_approve_system_users(self, approve_system_users):
|
||||||
|
application_type = self.root.instance.meta['apply_type']
|
||||||
|
protocol = SystemUser.get_protocol_by_application_type(application_type)
|
||||||
|
queries = {'protocol': protocol}
|
||||||
|
system_users_id = self.filter_approve_system_users(approve_system_users, queries)
|
||||||
|
return system_users_id
|
|
@ -0,0 +1,80 @@
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
from perms.serializers import ActionsField
|
||||||
|
from perms.models import Action
|
||||||
|
from assets.models import Asset, SystemUser
|
||||||
|
from .base import BaseTicketMetaSerializer, BaseTicketMetaApproveSerializerMixin
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'TicketMetaApplyAssetApplySerializer',
|
||||||
|
'TicketMetaApplyAssetApproveSerializer',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TicketMetaApplyAssetSerializer(BaseTicketMetaSerializer):
|
||||||
|
# 申请信息
|
||||||
|
apply_ip_group = serializers.ListField(
|
||||||
|
child=serializers.IPAddressField(), default=list, label=_('IP group')
|
||||||
|
)
|
||||||
|
apply_hostname_group = serializers.ListField(
|
||||||
|
child=serializers.CharField(), default=list, label=_('Hostname group')
|
||||||
|
)
|
||||||
|
apply_system_user_group = serializers.ListField(
|
||||||
|
child=serializers.CharField(), default=list, label=_('System user group')
|
||||||
|
)
|
||||||
|
apply_actions = ActionsField(
|
||||||
|
choices=Action.DB_CHOICES, default=Action.ALL
|
||||||
|
)
|
||||||
|
apply_date_start = serializers.DateTimeField(
|
||||||
|
required=True, label=_('Date start')
|
||||||
|
)
|
||||||
|
apply_date_expired = serializers.DateTimeField(
|
||||||
|
required=True, label=_('Date expired')
|
||||||
|
)
|
||||||
|
# 审批信息
|
||||||
|
approve_assets = serializers.ListField(
|
||||||
|
required=True, child=serializers.UUIDField(), label=_('Approve assets')
|
||||||
|
)
|
||||||
|
approve_system_users = serializers.ListField(
|
||||||
|
required=True, child=serializers.UUIDField(), label=_('Approve system users')
|
||||||
|
)
|
||||||
|
approve_actions = ActionsField(
|
||||||
|
required=False, choices=Action.DB_CHOICES, default=Action.ALL
|
||||||
|
)
|
||||||
|
approve_date_start = serializers.DateTimeField(
|
||||||
|
required=True, label=_('Date start')
|
||||||
|
)
|
||||||
|
approve_date_expired = serializers.DateTimeField(
|
||||||
|
required=True, label=_('Date expired')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TicketMetaApplyAssetApplySerializer(TicketMetaApplyAssetSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
'apply_ip_group', 'apply_hostname_group',
|
||||||
|
'apply_system_user_group', 'apply_actions',
|
||||||
|
'apply_date_start', 'apply_date_expired'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TicketMetaApplyAssetApproveSerializer(BaseTicketMetaApproveSerializerMixin,
|
||||||
|
TicketMetaApplyAssetSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
'approve_assets', 'approve_system_users',
|
||||||
|
'approve_actions', 'approve_date_start',
|
||||||
|
'approve_date_expired'
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate_approve_assets(self, approve_assets):
|
||||||
|
assets_id = self.filter_approve_resources(resource_model=Asset, resources_id=approve_assets)
|
||||||
|
return assets_id
|
||||||
|
|
||||||
|
def validate_approve_system_users(self, approve_system_users):
|
||||||
|
queries = {'protocol__in': SystemUser.ASSET_CATEGORY_PROTOCOLS}
|
||||||
|
system_users_id = self.filter_approve_system_users(approve_system_users, queries)
|
||||||
|
return system_users_id
|
|
@ -0,0 +1,58 @@
|
||||||
|
from collections import OrderedDict
|
||||||
|
from rest_framework import serializers
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from orgs.utils import tmp_to_org
|
||||||
|
from assets.models import SystemUser
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTicketMetaSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
def get_fields(self):
|
||||||
|
fields = super().get_fields()
|
||||||
|
required_fields = self.Meta.fields
|
||||||
|
if required_fields == '__all__':
|
||||||
|
return fields
|
||||||
|
|
||||||
|
fields = OrderedDict({
|
||||||
|
field_name: fields.pop(field_name) for field_name in set(required_fields)
|
||||||
|
if field_name in fields.keys()
|
||||||
|
})
|
||||||
|
return fields
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTicketMetaApproveSerializerMixin:
|
||||||
|
|
||||||
|
def _filter_approve_resources_by_org(self, model, resources_id):
|
||||||
|
with tmp_to_org(self.root.instance.org_id):
|
||||||
|
org_resources = model.objects.filter(id__in=resources_id)
|
||||||
|
if not org_resources:
|
||||||
|
error = _('None of the approved `{}` belong to Organization `{}`'
|
||||||
|
''.format(model.__name__, self.root.instance.org_name))
|
||||||
|
raise serializers.ValidationError(error)
|
||||||
|
return org_resources
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter_approve_resources_by_queries(model, resources, queries=None):
|
||||||
|
if queries:
|
||||||
|
resources = resources.filter(**queries)
|
||||||
|
if not resources:
|
||||||
|
error = _('None of the approved `{}` does not comply with the filtering rules `{}`'
|
||||||
|
''.format(model.__name__, queries))
|
||||||
|
raise serializers.ValidationError(error)
|
||||||
|
return resources
|
||||||
|
|
||||||
|
def filter_approve_resources(self, resource_model, resources_id, queries=None):
|
||||||
|
resources = self._filter_approve_resources_by_org(resource_model, resources_id)
|
||||||
|
resources = self._filter_approve_resources_by_queries(resource_model, resources, queries)
|
||||||
|
resources_id = list(resources.values_list('id', flat=True))
|
||||||
|
return resources_id
|
||||||
|
|
||||||
|
def filter_approve_system_users(self, system_users_id, queries=None):
|
||||||
|
system_users_id = self.filter_approve_resources(
|
||||||
|
resource_model=SystemUser, resources_id=system_users_id, queries=queries
|
||||||
|
)
|
||||||
|
return system_users_id
|
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from .base import BaseTicketMetaSerializer
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'TicketMetaLoginConfirmApplySerializer',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TicketMetaLoginConfirmSerializer(BaseTicketMetaSerializer):
|
||||||
|
apply_login_ip = serializers.IPAddressField(
|
||||||
|
required=True, label=_('Login ip')
|
||||||
|
)
|
||||||
|
apply_login_city = serializers.CharField(
|
||||||
|
required=True, max_length=64, label=_('Login city')
|
||||||
|
)
|
||||||
|
apply_login_datetime = serializers.DateTimeField(
|
||||||
|
required=True, label=_('Login datetime')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TicketMetaLoginConfirmApplySerializer(TicketMetaLoginConfirmSerializer):
|
||||||
|
pass
|
|
@ -0,0 +1,139 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
from common.fields.serializer import ReadableHiddenField
|
||||||
|
from orgs.utils import get_org_by_id
|
||||||
|
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||||
|
from users.models import User
|
||||||
|
from tickets import const
|
||||||
|
from tickets.models import Ticket
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'TicketSerializer', 'TicketDisplaySerializer',
|
||||||
|
'TicketApplySerializer', 'TicketApproveSerializer',
|
||||||
|
'TicketRejectSerializer', 'TicketCloseSerializer',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TicketSerializer(OrgResourceModelSerializerMixin):
|
||||||
|
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
|
||||||
|
status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status'))
|
||||||
|
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Ticket
|
||||||
|
fields = [
|
||||||
|
'id', 'title', 'type', 'type_display',
|
||||||
|
'meta', 'action', 'action_display', 'status', 'status_display',
|
||||||
|
'applicant', 'applicant_display', 'processor', 'processor_display',
|
||||||
|
'assignees', 'assignees_display',
|
||||||
|
'date_created', 'date_updated',
|
||||||
|
'org_id', 'org_name',
|
||||||
|
'body'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TicketDisplaySerializer(TicketSerializer):
|
||||||
|
|
||||||
|
class Meta(TicketSerializer.Meta):
|
||||||
|
read_only_fields = TicketSerializer.Meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
class TicketActionSerializer(TicketSerializer):
|
||||||
|
action = ReadableHiddenField(default=const.TicketActionChoices.apply.value)
|
||||||
|
|
||||||
|
class Meta(TicketSerializer.Meta):
|
||||||
|
required_fields = ['action']
|
||||||
|
read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields))
|
||||||
|
|
||||||
|
|
||||||
|
class TicketApplySerializer(TicketActionSerializer):
|
||||||
|
applicant = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||||
|
org_id = serializers.CharField(
|
||||||
|
max_length=36, allow_blank=True, required=True, label=_("Organization")
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(TicketActionSerializer.Meta):
|
||||||
|
required_fields = TicketActionSerializer.Meta.required_fields + [
|
||||||
|
'id', 'title', 'type', 'applicant', 'meta', 'assignees', 'org_id'
|
||||||
|
]
|
||||||
|
read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields))
|
||||||
|
extra_kwargs = {
|
||||||
|
'type': {'required': True}
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_type(self, tp):
|
||||||
|
request_type = self.context['request'].query_params.get('type')
|
||||||
|
if tp != request_type:
|
||||||
|
error = _(
|
||||||
|
'The `type` in the submission data (`{}`) is different from the type '
|
||||||
|
'in the request url (`{}`)'.format(tp, request_type)
|
||||||
|
)
|
||||||
|
raise serializers.ValidationError(error)
|
||||||
|
return tp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_org_id(org_id):
|
||||||
|
org = get_org_by_id(org_id)
|
||||||
|
if not org:
|
||||||
|
error = _('The organization `{}` does not exist'.format(org_id))
|
||||||
|
raise serializers.ValidationError(error)
|
||||||
|
return org_id
|
||||||
|
|
||||||
|
def validate_assignees(self, assignees):
|
||||||
|
org_id = self.initial_data.get('org_id')
|
||||||
|
self.validate_org_id(org_id)
|
||||||
|
org = get_org_by_id(org_id)
|
||||||
|
admins = User.get_super_and_org_admins(org)
|
||||||
|
valid_assignees = list(set(assignees) & set(admins))
|
||||||
|
if not valid_assignees:
|
||||||
|
error = _('None of the assignees belong to Organization `{}` admins'.format(org.name))
|
||||||
|
raise serializers.ValidationError(error)
|
||||||
|
return valid_assignees
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_action(action):
|
||||||
|
return const.TicketActionChoices.apply.value
|
||||||
|
|
||||||
|
|
||||||
|
class TicketProcessSerializer(TicketActionSerializer):
|
||||||
|
processor = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||||
|
|
||||||
|
class Meta(TicketActionSerializer.Meta):
|
||||||
|
required_fields = TicketActionSerializer.Meta.required_fields + ['processor']
|
||||||
|
read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields))
|
||||||
|
|
||||||
|
|
||||||
|
class TicketApproveSerializer(TicketProcessSerializer):
|
||||||
|
|
||||||
|
class Meta(TicketProcessSerializer.Meta):
|
||||||
|
required_fields = TicketProcessSerializer.Meta.required_fields + ['meta']
|
||||||
|
read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields))
|
||||||
|
extra_kwargs = {
|
||||||
|
'meta': {'read_only': True}
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_meta(self, meta):
|
||||||
|
meta.update(self.instance.meta)
|
||||||
|
return meta
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_action(action):
|
||||||
|
return const.TicketActionChoices.approve.value
|
||||||
|
|
||||||
|
|
||||||
|
class TicketRejectSerializer(TicketProcessSerializer):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_action(action):
|
||||||
|
return const.TicketActionChoices.reject.value
|
||||||
|
|
||||||
|
|
||||||
|
class TicketCloseSerializer(TicketProcessSerializer):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_action(action):
|
||||||
|
return const.TicketActionChoices.close.value
|
||||||
|
|
||||||
|
|
|
@ -6,44 +6,55 @@ from django.db.models.signals import m2m_changed, post_save, pre_save
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from .models import Ticket, Comment
|
from .models import Ticket, Comment
|
||||||
from .utils import (
|
from .utils import (
|
||||||
send_new_ticket_mail_to_assignees,
|
send_ticket_applied_mail_to_assignees,
|
||||||
send_ticket_action_mail_to_user
|
send_ticket_processed_mail_to_applicant
|
||||||
)
|
)
|
||||||
|
from . import const
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=Ticket.assignees.through)
|
@receiver(pre_save, sender=Ticket)
|
||||||
def on_ticket_assignees_set(sender, instance=None, action=None,
|
def on_ticket_pre_save(sender, instance=None, **kwargs):
|
||||||
reverse=False, model=None,
|
if instance.is_applied:
|
||||||
pk_set=None, **kwargs):
|
instance.applicant_display = str(instance.applicant)
|
||||||
if action == 'post_add':
|
if instance.is_processed:
|
||||||
logger.debug('New ticket create, send mail: {}'.format(instance.id))
|
instance.processor_display = str(instance.processor)
|
||||||
assignees = model.objects.filter(pk__in=pk_set)
|
instance.status = const.TicketStatusChoices.closed.value
|
||||||
send_new_ticket_mail_to_assignees(instance, assignees)
|
|
||||||
if action.startswith('post') and not reverse:
|
|
||||||
instance.assignees_display = ', '.join([
|
|
||||||
str(u) for u in instance.assignees.all()
|
|
||||||
])
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Ticket)
|
@receiver(post_save, sender=Ticket)
|
||||||
def on_ticket_status_change(sender, instance=None, created=False, **kwargs):
|
def on_ticket_processed(sender, instance=None, created=False, **kwargs):
|
||||||
if created or instance.status == "open":
|
if not instance.is_processed:
|
||||||
return
|
return
|
||||||
logger.debug('Ticket changed, send mail: {}'.format(instance.id))
|
logger.debug('Ticket is processed, send mail: {}'.format(instance.id))
|
||||||
send_ticket_action_mail_to_user(instance)
|
instance.create_action_comment()
|
||||||
|
if instance.is_approved:
|
||||||
|
instance.create_permission()
|
||||||
|
instance.create_approved_comment()
|
||||||
|
send_ticket_processed_mail_to_applicant(instance)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=Ticket)
|
@receiver(m2m_changed, sender=Ticket.assignees.through)
|
||||||
def on_ticket_create(sender, instance=None, **kwargs):
|
def on_ticket_assignees_changed(sender, instance=None, action=None, reverse=False, model=None, pk_set=None, **kwargs):
|
||||||
instance.user_display = str(instance.user)
|
if reverse:
|
||||||
if instance.assignee:
|
return
|
||||||
instance.assignee_display = str(instance.assignee)
|
if action != 'post_add':
|
||||||
|
return
|
||||||
|
ticket = instance
|
||||||
|
assignees_display = [str(assignee) for assignee in ticket.assignees.all()]
|
||||||
|
logger.debug(
|
||||||
|
'Receives ticket and assignees changed signal, ticket: {}, assignees: {}'
|
||||||
|
''.format(ticket.title, assignees_display)
|
||||||
|
)
|
||||||
|
ticket.assignees_display = ', '.join(assignees_display)
|
||||||
|
ticket.save()
|
||||||
|
logger.debug('Send applied email to assignees: {}'.format(assignees_display))
|
||||||
|
assignees = model.objects.filter(pk__in=pk_set)
|
||||||
|
send_ticket_applied_mail_to_assignees(ticket, assignees)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=Comment)
|
@receiver(pre_save, sender=Comment)
|
||||||
def on_comment_create(sender, instance=None, **kwargs):
|
def on_comment_create(sender, instance=None, created=False, **kwargs):
|
||||||
instance.user_display = str(instance.user)
|
instance.user_display = str(instance.user)
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
import datetime
|
|
||||||
|
|
||||||
from common.utils.timezone import now
|
|
||||||
from django.urls import reverse
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
from orgs.models import Organization, OrganizationMember, ROLE as ORG_ROLE
|
|
||||||
from orgs.utils import set_current_org
|
|
||||||
from users.models.user import User
|
|
||||||
from assets.models import Asset, AdminUser, SystemUser
|
|
||||||
|
|
||||||
|
|
||||||
class TicketTest(APITestCase):
|
|
||||||
def setUp(self):
|
|
||||||
Organization.objects.bulk_create([
|
|
||||||
Organization(name='org-01'),
|
|
||||||
Organization(name='org-02'),
|
|
||||||
Organization(name='org-03'),
|
|
||||||
])
|
|
||||||
org_01, org_02, org_03 = Organization.objects.all()
|
|
||||||
self.org_01, self.org_02, self.org_03 = org_01, org_02, org_03
|
|
||||||
|
|
||||||
set_current_org(org_01)
|
|
||||||
|
|
||||||
AdminUser.objects.bulk_create([
|
|
||||||
AdminUser(name='au-01', username='au-01'),
|
|
||||||
AdminUser(name='au-02', username='au-02'),
|
|
||||||
AdminUser(name='au-03', username='au-03'),
|
|
||||||
])
|
|
||||||
|
|
||||||
SystemUser.objects.bulk_create([
|
|
||||||
SystemUser(name='su-01', username='su-01'),
|
|
||||||
SystemUser(name='su-02', username='su-02'),
|
|
||||||
SystemUser(name='su-03', username='su-03'),
|
|
||||||
])
|
|
||||||
|
|
||||||
admin_users = AdminUser.objects.all()
|
|
||||||
Asset.objects.bulk_create([
|
|
||||||
Asset(hostname='asset-01', ip='192.168.1.1', public_ip='192.168.1.1', admin_user=admin_users[0]),
|
|
||||||
Asset(hostname='asset-02', ip='192.168.1.2', public_ip='192.168.1.2', admin_user=admin_users[0]),
|
|
||||||
Asset(hostname='asset-03', ip='192.168.1.3', public_ip='192.168.1.3', admin_user=admin_users[0]),
|
|
||||||
])
|
|
||||||
|
|
||||||
new_user = User.objects.create
|
|
||||||
new_org_member = OrganizationMember.objects.create
|
|
||||||
|
|
||||||
u = new_user(name='user-01', username='user-01', email='user-01@jms.com')
|
|
||||||
new_org_member(org=org_01, user=u, role=ORG_ROLE.USER)
|
|
||||||
new_org_member(org=org_02, user=u, role=ORG_ROLE.USER)
|
|
||||||
self.user_01 = u
|
|
||||||
|
|
||||||
u = new_user(name='org-admin-01', username='org-admin-01', email='org-admin-01@jms.com')
|
|
||||||
new_org_member(org=org_01, user=u, role=ORG_ROLE.ADMIN)
|
|
||||||
self.org_admin_01 = u
|
|
||||||
|
|
||||||
u = new_user(name='org-admin-02', username='org-admin-02', email='org-admin-02@jms.com')
|
|
||||||
new_org_member(org=org_02, user=u, role=ORG_ROLE.ADMIN)
|
|
||||||
self.org_admin_02 = u
|
|
||||||
|
|
||||||
def test_create_request_asset_perm(self):
|
|
||||||
url = reverse('api-tickets:ticket-request-asset-perm')
|
|
||||||
ticket_url = reverse('api-tickets:ticket')
|
|
||||||
|
|
||||||
self.client.force_login(self.user_01)
|
|
||||||
|
|
||||||
date_start = now()
|
|
||||||
date_expired = date_start + datetime.timedelta(days=7)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"title": "request-01",
|
|
||||||
"ips": [
|
|
||||||
"192.168.1.1"
|
|
||||||
],
|
|
||||||
"date_start": date_start,
|
|
||||||
"date_expired": date_expired,
|
|
||||||
"hostname": "",
|
|
||||||
"system_user": "",
|
|
||||||
"org_id": self.org_01.id,
|
|
||||||
"assignees": [
|
|
||||||
str(self.org_admin_01.id),
|
|
||||||
str(self.org_admin_02.id),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client.post(data)
|
|
||||||
|
|
||||||
self.client.force_login(self.org_admin_01)
|
|
||||||
res = self.client.get(ticket_url, params={'assgin': 1})
|
|
|
@ -7,13 +7,9 @@ from .. import api
|
||||||
app_name = 'tickets'
|
app_name = 'tickets'
|
||||||
router = BulkRouter()
|
router = BulkRouter()
|
||||||
|
|
||||||
router.register('tickets/request-asset-perm/assignees', api.AssigneeViewSet, 'ticket-request-asset-perm-assignee')
|
|
||||||
router.register('tickets/request-asset-perm', api.RequestAssetPermTicketViewSet, 'ticket-request-asset-perm')
|
|
||||||
router.register('tickets', api.TicketViewSet, 'ticket')
|
router.register('tickets', api.TicketViewSet, 'ticket')
|
||||||
router.register('tickets/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment')
|
router.register('assignees', api.AssigneeViewSet, 'assignee')
|
||||||
|
router.register('comments', api.CommentViewSet, 'comment')
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
]
|
|
||||||
|
|
||||||
|
urlpatterns = []
|
||||||
urlpatterns += router.urls
|
urlpatterns += router.urls
|
||||||
|
|
|
@ -4,55 +4,67 @@ from urllib.parse import urljoin
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from common.const.front_urls import TICKET_DETAIL
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.tasks import send_mail_async
|
from common.tasks import send_mail_async
|
||||||
|
from . import const
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__file__)
|
||||||
from tickets.models import Ticket
|
|
||||||
|
|
||||||
|
|
||||||
def send_new_ticket_mail_to_assignees(ticket: Ticket, assignees):
|
def send_ticket_applied_mail_to_assignees(ticket, assignees):
|
||||||
recipient_list = [user.email for user in assignees]
|
if not assignees:
|
||||||
user = ticket.user
|
logger.debug("Not found assignees, ticket: {}({}), assignees: {}".format(
|
||||||
if not recipient_list:
|
ticket, str(ticket.id), assignees)
|
||||||
logger.error("Ticket not has assignees: {}".format(ticket.id))
|
)
|
||||||
return
|
return
|
||||||
subject = '{}: {}'.format(_("New ticket"), ticket.title)
|
|
||||||
|
|
||||||
# 这里要设置前端地址,因为要直接跳转到页面
|
subject = _('New Ticket: {} ({})'.format(ticket.title, ticket.get_type_display()))
|
||||||
detail_url = urljoin(settings.SITE_URL, TICKET_DETAIL.format(id=ticket.id))
|
ticket_detail_url = urljoin(
|
||||||
message = _("""
|
settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(ticket.id))
|
||||||
<div>
|
)
|
||||||
|
message = _(
|
||||||
|
"""<div>
|
||||||
<p>Your has a new ticket</p>
|
<p>Your has a new ticket</p>
|
||||||
<div>
|
<div>
|
||||||
{body}
|
<b>Ticket:</b>
|
||||||
<br/>
|
<br/>
|
||||||
<a href={url}>click here to review</a>
|
{body}
|
||||||
|
<br/>
|
||||||
|
<a href={ticket_detail_url}>click here to review</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
""").format(body=ticket.body, user=user, url=detail_url)
|
""".format(
|
||||||
|
body=ticket.body.replace('\n', '<br/>'),
|
||||||
|
ticket_detail_url=ticket_detail_url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if settings.DEBUG:
|
||||||
|
logger.debug(message)
|
||||||
|
recipient_list = [assignee.email for assignee in assignees]
|
||||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||||
|
|
||||||
|
|
||||||
def send_ticket_action_mail_to_user(ticket):
|
def send_ticket_processed_mail_to_applicant(ticket):
|
||||||
if not ticket.user:
|
if not ticket.applicant:
|
||||||
logger.error("Ticket not has user: {}".format(ticket.id))
|
logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id))
|
||||||
return
|
return
|
||||||
user = ticket.user
|
subject = _('Ticket has processed: {} ({})').format(ticket.title, ticket.get_type_display())
|
||||||
recipient_list = [user.email]
|
message = _(
|
||||||
subject = '{}: {}'.format(_("Ticket has been reply"), ticket.title)
|
"""
|
||||||
message = _("""
|
|
||||||
<div>
|
<div>
|
||||||
<p>Your ticket has been replay</p>
|
<p>Your ticket has been processed</p>
|
||||||
<div>
|
<div>
|
||||||
<b>Title:</b> {ticket.title}
|
<b>Ticket:</b>
|
||||||
<br/>
|
<br/>
|
||||||
<b>Assignee:</b> {ticket.assignee_display}
|
{body}
|
||||||
<br/>
|
|
||||||
<b>Status:</b> {ticket.status_display}
|
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
""").format(ticket=ticket)
|
""".format(
|
||||||
|
body=ticket.body.replace('\n', '<br/>'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if settings.DEBUG:
|
||||||
|
logger.debug(message)
|
||||||
|
recipient_list = [ticket.applicant.email]
|
||||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||||
|
|
|
@ -329,6 +329,28 @@ class RoleMixin:
|
||||||
return
|
return
|
||||||
OrganizationMember.objects.remove_users(current_org, [self])
|
OrganizationMember.objects.remove_users(current_org, [self])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_super_admins(cls):
|
||||||
|
return cls.objects.filter(role=cls.ROLE.ADMIN)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_org_admins(cls, org=None):
|
||||||
|
from orgs.models import Organization
|
||||||
|
if not isinstance(org, Organization):
|
||||||
|
org = current_org
|
||||||
|
org_admins = org.admins
|
||||||
|
return org_admins
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_super_and_org_admins(cls, org=None):
|
||||||
|
super_admins = cls.get_super_admins()
|
||||||
|
super_admins_id = list(super_admins.values_list('id', flat=True))
|
||||||
|
org_admins = cls.get_org_admins(org)
|
||||||
|
org_admins_id = list(org_admins.values_list('id', flat=True))
|
||||||
|
admins_id = set(org_admins_id + super_admins_id)
|
||||||
|
admins = User.objects.filter(id__in=admins_id)
|
||||||
|
return admins
|
||||||
|
|
||||||
|
|
||||||
class TokenMixin:
|
class TokenMixin:
|
||||||
CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}"
|
CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}"
|
||||||
|
|
Loading…
Reference in New Issue