diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 5ab9153a..3c31b0e0 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -43,6 +43,7 @@ from core.viewsets import ( ticket_category, ticket_comment, ticket_linked_item, + related_ticket, ) @@ -119,6 +120,8 @@ router.register('config_management/group/(?P[0-9]+)/software', config_ router.register('core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') router.register('core/ticket/(?P[0-9]+)/linked_item', ticket_linked_item.ViewSet, basename='_api_v2_ticket_linked_item') +router.register('core/ticket/(?P[0-9]+)/related_ticket', related_ticket.ViewSet, basename='_api_v2_ticket_related') + router.register('itam', itam_index_v2.Index, basename='_api_v2_itam_home') router.register('itam/device', device_v2.ViewSet, basename='_api_v2_device') diff --git a/app/api/viewsets/common.py b/app/api/viewsets/common.py index 7c278986..bebe7a03 100644 --- a/app/api/viewsets/common.py +++ b/app/api/viewsets/common.py @@ -225,6 +225,19 @@ class ModelViewSet( +class ModelListRetrieveDeleteViewSet( + viewsets.mixins.ListModelMixin, + viewsets.mixins.RetrieveModelMixin, + viewsets.mixins.DestroyModelMixin, + viewsets.GenericViewSet, + ModelViewSetBase +): + """ Use for models that you wish to delete and view ONLY!""" + + pass + + + class ModelRetrieveUpdateViewSet( viewsets.mixins.RetrieveModelMixin, viewsets.mixins.UpdateModelMixin, diff --git a/app/core/models/ticket/ticket.py b/app/core/models/ticket/ticket.py index 322f3cc5..0f97559b 100644 --- a/app/core/models/ticket/ticket.py +++ b/app/core/models/ticket/ticket.py @@ -3,6 +3,8 @@ from django.db import models from django.db.models import Q, signals, Sum from django.forms import ValidationError +from rest_framework.reverse import reverse + from .ticket_enum_values import TicketValues from access.fields import AutoCreatedField, AutoLastModifiedField @@ -1103,6 +1105,11 @@ class RelatedTickets(TenancyObject): 'id' ] + verbose_name = 'Related Ticket' + + verbose_name_plural = 'Related Tickets' + + class Related(models.IntegerChoices): RELATED = '1', 'Related' @@ -1159,9 +1166,32 @@ class RelatedTickets(TenancyObject): ] - # def __str__(self): + def get_url( self, ticket_id, request = None ) -> str: + + if not ticket_id: + + return '' + + if request: + + return reverse( + "v2:_api_v2_ticket_related-detail", + request = request, + kwargs={ + 'ticket_id': ticket_id, + 'pk': self.id + } + ) + + return reverse("v2:_api_v2_ticket_related-detail", kwargs={'pk': self.id}) + + + def __str__(self): + + # return '#' + str( self.id ) + + return '#' - # return '' @property def parent_object(self): diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index 884e59da..adb472c0 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -75,6 +75,7 @@ class TicketModelSerializer(TicketBaseSerializer): ), 'comments': reverse('v2:_api_v2_ticket_' + str(item.get_ticket_type_display()).lower() + '_comments-list', request=context['view'].request, kwargs={'ticket_id': item.pk}), 'linked_items': reverse("v2:_api_v2_ticket_linked_item-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), + 'related_tickets': reverse("v2:_api_v2_ticket_related-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), } diff --git a/app/core/serializers/ticket_related.py b/app/core/serializers/ticket_related.py new file mode 100644 index 00000000..f7aa436f --- /dev/null +++ b/app/core/serializers/ticket_related.py @@ -0,0 +1,119 @@ +from rest_framework.fields import empty +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from core.serializers.ticket import TicketBaseSerializer + +from core.models.ticket.ticket import RelatedTickets + + + +class RelatedTicketBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.SerializerMethodField('get_url') + + def get_url(self, item) -> str: + + request = None + + ticket_id: int = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + + if 'ticket_id' in self._kwargs['context']['view'].kwargs: + + ticket_id = int(self._kwargs['context']['view'].kwargs['ticket_id']) + + return item.get_url( ticket_id = ticket_id,request = request ) + + + class Meta: + + model = RelatedTickets + + fields = [ + 'id', + 'display_name', + 'title', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'title', + 'url', + ] + + +class RelatedTicketModelSerializer(RelatedTicketBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + request = None + + ticket_id: int = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + + if 'ticket_id' in self._kwargs['context']['view'].kwargs: + + ticket_id = int(self._kwargs['context']['view'].kwargs['ticket_id']) + + return { + '_self': item.get_url( ticket_id = ticket_id, request = request ), + } + + + class Meta: + + model = RelatedTickets + + fields = [ + 'id', + 'display_name', + 'to_ticket_id', + 'from_ticket_id', + 'how_related', + 'organization', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'to_ticket_id', + 'from_ticket_id', + 'how_related', + 'organization', + '_urls', + ] + + + +class RelatedTicketViewSerializer(RelatedTicketModelSerializer): + + from_ticket_id = TicketBaseSerializer() + + organization = OrganizationBaseSerializer(many=False, read_only=True) + + to_ticket_id = TicketBaseSerializer() diff --git a/app/core/viewsets/related_ticket.py b/app/core/viewsets/related_ticket.py new file mode 100644 index 00000000..9e597c11 --- /dev/null +++ b/app/core/viewsets/related_ticket.py @@ -0,0 +1,78 @@ +from django.db.models import Q + +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from access.mixin import OrganizationMixin + +from api.viewsets.common import ModelListRetrieveDeleteViewSet + +from core.serializers.ticket_related import ( + RelatedTickets, + RelatedTicketModelSerializer, + RelatedTicketViewSerializer, +) + + + +@extend_schema_view( + destroy = extend_schema( + summary = 'Delete a related ticket', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all related tickets', + description='', + responses = { + 200: OpenApiResponse(description='', response=RelatedTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a related ticket', + description='', + responses = { + 200: OpenApiResponse(description='', response=RelatedTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), +) +class ViewSet(ModelListRetrieveDeleteViewSet): + + + filterset_fields = [ + 'organization', + ] + + search_fields = [ + 'name', + ] + + model = RelatedTickets + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] + + + def get_queryset(self): + + self.queryset = RelatedTickets.objects.filter( + Q(from_ticket_id_id=self.kwargs['ticket_id']) + | + Q(to_ticket_id_id=self.kwargs['ticket_id']) + ) + + return self.queryset.filter().order_by('id')