from rest_framework import serializers from rest_framework.reverse import reverse from drf_spectacular.utils import extend_schema_serializer from access.serializers.entity import BaseSerializer as EntityBaseSerializer from access.serializers.organization import OrganizationBaseSerializer from api.serializers import common from app.serializers.user import UserBaseSerializer from core import exceptions as centurion_exception from core import fields as centurion_field from core.fields.badge import BadgeField from core.models.ticket_base import TicketBase from core.serializers.ticket_category import TicketCategoryBaseSerializer from project_management.serializers.project import ProjectBaseSerializer from project_management.serializers.project_milestone import ProjectMilestoneBaseSerializer @extend_schema_serializer(component_name = 'TicketBaseBaseSerializer') class BaseSerializer(serializers.ModelSerializer): """Base Ticket Model""" display_name = serializers.SerializerMethodField('get_display_name') def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('get_url') def get_url(self, item) -> str: return item.get_url( request = self.context['view'].request ) class Meta: model = TicketBase fields = [ 'id', 'display_name', 'url', ] read_only_fields = [ 'id', 'display_name', 'url', ] @extend_schema_serializer(component_name = 'TicketBaseModelSerializer') class ModelSerializer( common.CommonModelSerializer, BaseSerializer ): """Ticket Base Model""" _urls = serializers.SerializerMethodField('get_url') def get_url(self, item) -> dict: ticket_type = str(item.ticket_type) url_dict: dict = { '_self': item.get_url( request = self._context['view'].request ), 'comments': reverse('v2:_api_v2_ticket_comment_base-list', request=self._context['view'].request, kwargs={'ticket_id': item.pk}), 'linked_items': reverse("v2:_api_v2_ticket_linked_item-list", request=self._context['view'].request, kwargs={'ticket_id': item.pk}), } if item.project: url_dict.update({ 'project': reverse("v2:_api_v2_project-list", request=self._context['view'].request, kwargs={}), }) if item.category: url_dict.update({ 'ticketcategory': reverse( 'v2:_api_v2_ticket_category-list', request=self._context['view'].request, kwargs={}, ) + '?' + ticket_type + '=true', }) url_dict.update({ 'related_tickets': reverse("v2:_api_v2_ticket_related-list", request=self._context['view'].request, kwargs={'ticket_id': item.pk}), }) return url_dict description = centurion_field.MarkdownField( required = True, style_class = 'large' ) impact_badge = BadgeField(label='Impact') organization = common.OrganizationField( required = True, write_only = True, ) priority_badge = BadgeField( label = 'Priority', read_only = True, ) status_badge = BadgeField( label = 'Status', read_only = True, ) ticket_duration = serializers.IntegerField( help_text = 'Total time spent on ticket', label = 'Time Spent', read_only = True, ) ticket_estimation = serializers.IntegerField( help_text = 'Time estimation to complete the ticket', label = 'Time estimation', read_only = True, ) urgency_badge = BadgeField( label = 'Urgency', read_only = True, ) class Meta: model = TicketBase fields = [ 'id', 'display_name', 'organization', 'external_system', 'external_ref', 'parent_ticket', 'ticket_type', 'status', 'status_badge', 'category', 'title', 'description', 'ticket_duration', 'ticket_estimation', 'project', 'milestone', 'urgency', 'urgency_badge', 'impact', 'impact_badge', 'priority', 'priority_badge', 'opened_by', 'subscribed_to', 'assigned_to', 'planned_start_date', 'planned_finish_date', 'real_start_date', 'real_finish_date', 'is_deleted', 'is_solved', 'date_solved', 'is_closed', 'date_closed', 'created', 'modified', '_urls', ] read_only_fields = [ 'id', 'display_name', 'created', 'modified', '_urls', ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.context.get('view', None) is not None: read_only_fields = [ 'id', 'display_name', 'created', 'modified', '_urls', ] if not self.context['view']._has_import: read_only_fields += [ 'external_system', 'external_ref', 'ticket_type', ] self.Meta.read_only_fields = read_only_fields def validate_field_milestone( self, attrs, raise_exception = False ) -> bool: milestone = attrs.get('milestone', None) project = attrs.get('project', None) if milestone is not None: if project is None: raise centurion_exception.ValidationError( detail = { 'milestone': 'Milestones require a project' }, code = 'milestone_requires_project', ) elif project.id != milestone.project.id: del attrs['milestone'] raise centurion_exception.ValidationError( detail = { 'milestone': 'Milestone must be from the same project' }, code = 'milestone_same_project', ) return attrs def validate_field_external_system( self, attrs, raise_exception = False ) -> bool: external_system = attrs.get('external_system', None) external_ref = attrs.get('external_ref', None) if external_system is None and external_ref is not None: raise centurion_exception.ValidationError( detail = { 'external_system': 'External System is required when an External Ref is defined' }, code = 'external_system_missing', ) elif external_system is not None and external_ref is None: raise centurion_exception.ValidationError( detail = { 'external_ref': 'External Ref is required when an External System is defined' }, code = 'external_ref_missing', ) return attrs def validate(self, attrs): if getattr(self.context['view'], 'action', '') in [ 'create' ]: # Always set that the ticket was opened by user ho is making the request try: attrs['opened_by'] = self.context['request'].user except KeyError: pass attrs = self.validate_field_milestone( attrs ) attrs = self.validate_field_external_system( attrs ) attrs = super().validate( attrs ) has_import_permission = self.context['view']._has_import has_triage_permission = self.context['view']._has_triage status = int(attrs.get('status', 0)) opened_by_id = attrs.get('opened_by', 0) if opened_by_id != 0: opened_by_id = opened_by_id.id request_user_id = int(self.context['request'].user.id) if opened_by_id == 0: request_user_id = 0 if not ( has_triage_permission or has_import_permission ): if( status == TicketBase.TicketStatus.ASSIGNED or status == TicketBase.TicketStatus.ASSIGNED_PLANNING ): raise centurion_exception.ValidationError( detail = { 'status': 'You cant assign a ticket if you dont have permission triage' }, code = 'no_triage_status_assigned', ) if status == TicketBase.TicketStatus.PENDING: raise centurion_exception.ValidationError( detail = { 'status': 'You cant set a ticket to pending if you dont have permission triage' }, code = 'no_triage_status_pending', ) if( status == TicketBase.TicketStatus.SOLVED and opened_by_id != request_user_id ): raise centurion_exception.ValidationError( detail = { 'status': 'You cant solve a ticket if you dont have permission triage' }, code = 'no_triage_status_solve', ) if( status == TicketBase.TicketStatus.INVALID and opened_by_id != request_user_id ): raise centurion_exception.ValidationError( detail = { 'status': 'You cant mark a ticket as invalid if you did not raise the ticket or you dont have permission triage' }, code = 'no_triage_status_invalid', ) if status == TicketBase.TicketStatus.CLOSED: raise centurion_exception.ValidationError( detail = { 'status': 'You cant close a ticket if you dont have permission triage' }, code = 'no_triage_status_close', ) elif ( has_triage_permission or has_import_permission ): if( ( 'status' not in attrs or attrs.get('status', 0) == self.Meta.model.TicketStatus.NEW ) and 'assigned_to' in attrs ): attrs['status'] = self.Meta.model.TicketStatus.ASSIGNED return attrs def is_valid(self, raise_exception = False): is_valid = super().is_valid( raise_exception = raise_exception ) return is_valid @extend_schema_serializer(component_name = 'TicketBaseViewSerializer') class ViewSerializer(ModelSerializer): """Ticket Base View Model""" assigned_to = EntityBaseSerializer(many=True, label = 'assigned to') category = TicketCategoryBaseSerializer(label = 'category') milestone = ProjectMilestoneBaseSerializer(many=False, read_only=True) opened_by = UserBaseSerializer() organization = OrganizationBaseSerializer(many=False, read_only=True) parent_ticket = BaseSerializer() project = ProjectBaseSerializer(many=False, read_only=True) subscribed_to = EntityBaseSerializer(many=True, label = 'subscribved to')