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_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_MANUAL = 'manual'
|
||||
|
@ -133,24 +150,6 @@ class SystemUser(BaseUser):
|
|||
def login_mode_display(self):
|
||||
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):
|
||||
if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]:
|
||||
return True
|
||||
|
@ -163,7 +162,7 @@ class SystemUser(BaseUser):
|
|||
|
||||
@property
|
||||
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):
|
||||
if username is None and self.username_same_with_user:
|
||||
|
@ -172,7 +171,7 @@ class SystemUser(BaseUser):
|
|||
|
||||
@property
|
||||
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):
|
||||
super()._merge_auth(other)
|
||||
|
@ -205,6 +204,18 @@ class SystemUser(BaseUser):
|
|||
assets = Asset.objects.filter(id__in=assets_ids)
|
||||
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:
|
||||
ordering = ['name']
|
||||
unique_together = [('name', 'org_id')]
|
||||
|
|
|
@ -45,5 +45,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
|
|||
ticket = self.get_ticket()
|
||||
if ticket:
|
||||
request.session.pop('auth_ticket_id', '')
|
||||
ticket.perform_status('closed', request.user)
|
||||
ticket.close(processor=request.user)
|
||||
return Response('', status=200)
|
||||
|
|
|
@ -187,12 +187,12 @@ class AuthMixin:
|
|||
if not ticket_id:
|
||||
ticket = None
|
||||
else:
|
||||
ticket = Ticket.origin_objects.get(pk=ticket_id)
|
||||
ticket = Ticket.all().filter(id=ticket_id).first()
|
||||
return ticket
|
||||
|
||||
def get_ticket_or_create(self, confirm_setting):
|
||||
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)
|
||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||
return ticket
|
||||
|
@ -201,12 +201,16 @@ class AuthMixin:
|
|||
ticket = self.get_ticket()
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmOtherError('', "Not found")
|
||||
if ticket.status == ticket.STATUS.OPEN:
|
||||
if ticket.status_open:
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
elif ticket.action == ticket.ACTION.APPROVE:
|
||||
elif ticket.is_approved:
|
||||
self.request.session["auth_confirm"] = "1"
|
||||
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(
|
||||
ticket.id, ticket.get_action_display()
|
||||
)
|
||||
|
|
|
@ -49,29 +49,37 @@ class LoginConfirmSetting(CommonModelMixin):
|
|||
def get_user_confirm_setting(cls, user):
|
||||
return get_object_or_none(cls, user=user)
|
||||
|
||||
def create_confirm_ticket(self, request=None):
|
||||
from tickets.models import Ticket
|
||||
title = _('Login confirm') + ' {}'.format(self.user)
|
||||
@staticmethod
|
||||
def construct_confirm_ticket_meta(request=None):
|
||||
if request:
|
||||
remote_addr = 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
|
||||
)
|
||||
login_ip = get_request_ip(request)
|
||||
else:
|
||||
body = ''
|
||||
reviewer = self.reviewers.all()
|
||||
ticket = Ticket.objects.create(
|
||||
user=self.user, title=title, body=body,
|
||||
type=Ticket.TYPE.LOGIN_CONFIRM,
|
||||
)
|
||||
ticket.assignees.set(reviewer)
|
||||
login_ip = ''
|
||||
login_ip = login_ip or '0.0.0.0'
|
||||
login_city = get_ip_city(login_ip)
|
||||
login_datetime = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
ticket_meta = {
|
||||
'apply_login_ip': login_ip,
|
||||
'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
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
@ -19,7 +19,6 @@ from django.conf import settings
|
|||
from django.urls import reverse_lazy
|
||||
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 users.utils import (
|
||||
redirect_user_first_login_or_index
|
||||
|
@ -181,6 +180,7 @@ class UserLoginWaitConfirmView(TemplateView):
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
from tickets.models import Ticket
|
||||
from tickets.const import TICKET_DETAIL_URL
|
||||
ticket_id = self.request.session.get("auth_ticket_id")
|
||||
if not ticket_id:
|
||||
ticket = None
|
||||
|
@ -189,7 +189,7 @@ class UserLoginWaitConfirmView(TemplateView):
|
|||
context = super().get_context_data(**kwargs)
|
||||
if ticket:
|
||||
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/>
|
||||
Don't close this page""").format(ticket.assignees_display)
|
||||
else:
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
|
||||
TICKET_DETAIL = '/ui/#/tickets/tickets/{id}'
|
|
@ -7,7 +7,7 @@ import six
|
|||
|
||||
__all__ = [
|
||||
'StringIDField', 'StringManyToManyField', 'ChoiceDisplayField',
|
||||
'CustomMetaDictField'
|
||||
'CustomMetaDictField', 'ReadableHiddenField',
|
||||
]
|
||||
|
||||
|
||||
|
@ -44,6 +44,17 @@ class DictField(serializers.DictField):
|
|||
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):
|
||||
"""
|
||||
In use:
|
||||
|
|
|
@ -60,7 +60,7 @@ class AssetPermissionForm(OrgModelForm):
|
|||
# 过滤系统用户
|
||||
system_users_field = self.fields.get('system_users')
|
||||
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):
|
||||
|
|
|
@ -25,7 +25,7 @@ class DatabaseAppPermissionCreateUpdateForm(OrgModelForm):
|
|||
# 过滤系统用户
|
||||
system_users_field = self.fields.get('system_users')
|
||||
system_users_field.queryset = SystemUser.objects.filter(
|
||||
protocol__in=SystemUser.application_category_protocols
|
||||
protocol__in=SystemUser.APPLICATION_CATEGORY_DB_PROTOCOLS
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -68,6 +68,11 @@ class Action:
|
|||
choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i]
|
||||
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
|
||||
def choices_to_value(cls, value):
|
||||
if not isinstance(value, list):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
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 -*-
|
||||
#
|
||||
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 -*-
|
||||
#
|
||||
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 .models import Ticket, Comment
|
||||
from .utils import (
|
||||
send_new_ticket_mail_to_assignees,
|
||||
send_ticket_action_mail_to_user
|
||||
send_ticket_applied_mail_to_assignees,
|
||||
send_ticket_processed_mail_to_applicant
|
||||
)
|
||||
from . import const
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Ticket.assignees.through)
|
||||
def on_ticket_assignees_set(sender, instance=None, action=None,
|
||||
reverse=False, model=None,
|
||||
pk_set=None, **kwargs):
|
||||
if action == 'post_add':
|
||||
logger.debug('New ticket create, send mail: {}'.format(instance.id))
|
||||
assignees = model.objects.filter(pk__in=pk_set)
|
||||
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(pre_save, sender=Ticket)
|
||||
def on_ticket_pre_save(sender, instance=None, **kwargs):
|
||||
if instance.is_applied:
|
||||
instance.applicant_display = str(instance.applicant)
|
||||
if instance.is_processed:
|
||||
instance.processor_display = str(instance.processor)
|
||||
instance.status = const.TicketStatusChoices.closed.value
|
||||
|
||||
|
||||
@receiver(post_save, sender=Ticket)
|
||||
def on_ticket_status_change(sender, instance=None, created=False, **kwargs):
|
||||
if created or instance.status == "open":
|
||||
def on_ticket_processed(sender, instance=None, created=False, **kwargs):
|
||||
if not instance.is_processed:
|
||||
return
|
||||
logger.debug('Ticket changed, send mail: {}'.format(instance.id))
|
||||
send_ticket_action_mail_to_user(instance)
|
||||
logger.debug('Ticket is processed, send mail: {}'.format(instance.id))
|
||||
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)
|
||||
def on_ticket_create(sender, instance=None, **kwargs):
|
||||
instance.user_display = str(instance.user)
|
||||
if instance.assignee:
|
||||
instance.assignee_display = str(instance.assignee)
|
||||
@receiver(m2m_changed, sender=Ticket.assignees.through)
|
||||
def on_ticket_assignees_changed(sender, instance=None, action=None, reverse=False, model=None, pk_set=None, **kwargs):
|
||||
if reverse:
|
||||
return
|
||||
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)
|
||||
def on_comment_create(sender, instance=None, **kwargs):
|
||||
def on_comment_create(sender, instance=None, created=False, **kwargs):
|
||||
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'
|
||||
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/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
]
|
||||
router.register('assignees', api.AssigneeViewSet, 'assignee')
|
||||
router.register('comments', api.CommentViewSet, 'comment')
|
||||
|
||||
urlpatterns = []
|
||||
urlpatterns += router.urls
|
||||
|
|
|
@ -4,55 +4,67 @@ from urllib.parse import urljoin
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.const.front_urls import TICKET_DETAIL
|
||||
from common.utils import get_logger
|
||||
from common.tasks import send_mail_async
|
||||
from . import const
|
||||
|
||||
logger = get_logger(__name__)
|
||||
from tickets.models import Ticket
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def send_new_ticket_mail_to_assignees(ticket: Ticket, assignees):
|
||||
recipient_list = [user.email for user in assignees]
|
||||
user = ticket.user
|
||||
if not recipient_list:
|
||||
logger.error("Ticket not has assignees: {}".format(ticket.id))
|
||||
def send_ticket_applied_mail_to_assignees(ticket, assignees):
|
||||
if not assignees:
|
||||
logger.debug("Not found assignees, ticket: {}({}), assignees: {}".format(
|
||||
ticket, str(ticket.id), assignees)
|
||||
)
|
||||
return
|
||||
subject = '{}: {}'.format(_("New ticket"), ticket.title)
|
||||
|
||||
# 这里要设置前端地址,因为要直接跳转到页面
|
||||
detail_url = urljoin(settings.SITE_URL, TICKET_DETAIL.format(id=ticket.id))
|
||||
message = _("""
|
||||
<div>
|
||||
subject = _('New Ticket: {} ({})'.format(ticket.title, ticket.get_type_display()))
|
||||
ticket_detail_url = urljoin(
|
||||
settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(ticket.id))
|
||||
)
|
||||
message = _(
|
||||
"""<div>
|
||||
<p>Your has a new ticket</p>
|
||||
<div>
|
||||
{body}
|
||||
<b>Ticket:</b>
|
||||
<br/>
|
||||
<a href={url}>click here to review</a>
|
||||
{body}
|
||||
<br/>
|
||||
<a href={ticket_detail_url}>click here to review</a>
|
||||
</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)
|
||||
|
||||
|
||||
def send_ticket_action_mail_to_user(ticket):
|
||||
if not ticket.user:
|
||||
logger.error("Ticket not has user: {}".format(ticket.id))
|
||||
def send_ticket_processed_mail_to_applicant(ticket):
|
||||
if not ticket.applicant:
|
||||
logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id))
|
||||
return
|
||||
user = ticket.user
|
||||
recipient_list = [user.email]
|
||||
subject = '{}: {}'.format(_("Ticket has been reply"), ticket.title)
|
||||
message = _("""
|
||||
subject = _('Ticket has processed: {} ({})').format(ticket.title, ticket.get_type_display())
|
||||
message = _(
|
||||
"""
|
||||
<div>
|
||||
<p>Your ticket has been replay</p>
|
||||
<p>Your ticket has been processed</p>
|
||||
<div>
|
||||
<b>Title:</b> {ticket.title}
|
||||
<b>Ticket:</b>
|
||||
<br/>
|
||||
<b>Assignee:</b> {ticket.assignee_display}
|
||||
<br/>
|
||||
<b>Status:</b> {ticket.status_display}
|
||||
{body}
|
||||
<br/>
|
||||
</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)
|
||||
|
|
|
@ -329,6 +329,28 @@ class RoleMixin:
|
|||
return
|
||||
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:
|
||||
CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}"
|
||||
|
|
Loading…
Reference in New Issue