Files
ETB/ETB-API/sla_oncall/views/sla.py
Iliyan Angelov 6b247e5b9f Updates
2025-09-19 11:58:53 +03:00

355 lines
13 KiB
Python

"""
Views for SLA & On-Call Management API
"""
from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from django.utils import timezone
from datetime import datetime, timedelta
from sla_oncall.models import (
BusinessHours,
SLADefinition,
EscalationPolicy,
OnCallRotation,
OnCallAssignment,
SLAInstance,
EscalationInstance,
NotificationTemplate,
)
from sla_oncall.serializers.sla import (
BusinessHoursSerializer,
SLADefinitionSerializer,
EscalationPolicySerializer,
OnCallRotationSerializer,
OnCallAssignmentSerializer,
SLAInstanceSerializer,
EscalationInstanceSerializer,
NotificationTemplateSerializer,
SLAInstanceCreateSerializer,
OnCallAssignmentCreateSerializer,
)
class BusinessHoursViewSet(viewsets.ModelViewSet):
"""ViewSet for BusinessHours management"""
queryset = BusinessHours.objects.all()
serializer_class = BusinessHoursSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['is_active', 'is_default', 'timezone']
search_fields = ['name', 'description']
ordering_fields = ['name', 'created_at']
ordering = ['name']
@action(detail=True, methods=['post'])
def test_business_hours(self, request, pk=None):
"""Test if a given time is within business hours"""
business_hours = self.get_object()
test_time = request.data.get('test_time')
if test_time:
try:
test_datetime = datetime.fromisoformat(test_time.replace('Z', '+00:00'))
is_business_hours = business_hours.is_business_hours(test_datetime)
except ValueError:
return Response(
{'error': 'Invalid datetime format. Use ISO format.'},
status=status.HTTP_400_BAD_REQUEST
)
else:
is_business_hours = business_hours.is_business_hours()
return Response({
'is_business_hours': is_business_hours,
'test_time': test_time or timezone.now().isoformat()
})
class SLADefinitionViewSet(viewsets.ModelViewSet):
"""ViewSet for SLA Definition management"""
queryset = SLADefinition.objects.all()
serializer_class = SLADefinitionSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['sla_type', 'is_active', 'is_default', 'business_hours_only']
search_fields = ['name', 'description']
ordering_fields = ['name', 'created_at', 'target_duration_minutes']
ordering = ['name']
@action(detail=True, methods=['post'])
def test_applicability(self, request, pk=None):
"""Test if SLA definition applies to a given incident"""
sla_definition = self.get_object()
incident_data = request.data
# Create a mock incident object for testing
class MockIncident:
def __init__(self, data):
self.category = data.get('category')
self.severity = data.get('severity')
self.priority = data.get('priority')
mock_incident = MockIncident(incident_data)
applies = sla_definition.applies_to_incident(mock_incident)
return Response({
'applies': applies,
'incident_data': incident_data
})
class EscalationPolicyViewSet(viewsets.ModelViewSet):
"""ViewSet for Escalation Policy management"""
queryset = EscalationPolicy.objects.all()
serializer_class = EscalationPolicySerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['escalation_type', 'trigger_condition', 'is_active']
search_fields = ['name', 'description']
ordering_fields = ['name', 'created_at']
ordering = ['name']
class OnCallRotationViewSet(viewsets.ModelViewSet):
"""ViewSet for On-Call Rotation management"""
queryset = OnCallRotation.objects.all()
serializer_class = OnCallRotationSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['rotation_type', 'status', 'external_system']
search_fields = ['name', 'team_name', 'description']
ordering_fields = ['name', 'created_at']
ordering = ['name']
@action(detail=True, methods=['get'])
def current_oncall(self, request, pk=None):
"""Get current on-call person for this rotation"""
rotation = self.get_object()
current_assignment = rotation.get_current_oncall()
if current_assignment:
serializer = OnCallAssignmentSerializer(current_assignment)
return Response(serializer.data)
else:
return Response({'message': 'No one is currently on-call'})
@action(detail=True, methods=['get'])
def upcoming_assignments(self, request, pk=None):
"""Get upcoming on-call assignments"""
rotation = self.get_object()
days_ahead = int(request.query_params.get('days', 30))
future_time = timezone.now() + timedelta(days=days_ahead)
upcoming = rotation.assignments.filter(
start_time__lte=future_time,
start_time__gte=timezone.now()
).order_by('start_time')
serializer = OnCallAssignmentSerializer(upcoming, many=True)
return Response(serializer.data)
class OnCallAssignmentViewSet(viewsets.ModelViewSet):
"""ViewSet for On-Call Assignment management"""
queryset = OnCallAssignment.objects.all()
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['rotation', 'user', 'status']
search_fields = ['user__username', 'user__email', 'rotation__name']
ordering_fields = ['start_time', 'end_time', 'created_at']
ordering = ['-start_time']
def get_serializer_class(self):
"""Return appropriate serializer based on action"""
if self.action == 'create':
return OnCallAssignmentCreateSerializer
return OnCallAssignmentSerializer
@action(detail=True, methods=['post'])
def handoff(self, request, pk=None):
"""Perform on-call handoff"""
assignment = self.get_object()
handoff_notes = request.data.get('handoff_notes', '')
handed_off_from = request.user
assignment.handoff_notes = handoff_notes
assignment.handed_off_from = handed_off_from
assignment.handoff_time = timezone.now()
assignment.save()
return Response({'message': 'Handoff completed successfully'})
@action(detail=True, methods=['post'])
def activate(self, request, pk=None):
"""Activate on-call assignment"""
assignment = self.get_object()
if assignment.status != 'SCHEDULED':
return Response(
{'error': 'Only scheduled assignments can be activated'},
status=status.HTTP_400_BAD_REQUEST
)
assignment.status = 'ACTIVE'
assignment.save()
return Response({'message': 'Assignment activated'})
@action(detail=True, methods=['post'])
def complete(self, request, pk=None):
"""Complete on-call assignment"""
assignment = self.get_object()
if assignment.status != 'ACTIVE':
return Response(
{'error': 'Only active assignments can be completed'},
status=status.HTTP_400_BAD_REQUEST
)
assignment.status = 'COMPLETED'
assignment.save()
return Response({'message': 'Assignment completed'})
class SLAInstanceViewSet(viewsets.ModelViewSet):
"""ViewSet for SLA Instance management"""
queryset = SLAInstance.objects.all()
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'escalation_triggered', 'sla_definition']
search_fields = ['incident__title', 'sla_definition__name']
ordering_fields = ['created_at', 'target_time', 'started_at']
ordering = ['-created_at']
def get_serializer_class(self):
"""Return appropriate serializer based on action"""
if self.action == 'create':
return SLAInstanceCreateSerializer
return SLAInstanceSerializer
@action(detail=False, methods=['get'])
def breached(self, request):
"""Get all breached SLA instances"""
breached_slas = self.queryset.filter(
status='BREACHED'
).order_by('-breached_at')
serializer = self.get_serializer(breached_slas, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def at_risk(self, request):
"""Get SLA instances at risk of breaching (within 15 minutes)"""
warning_time = timezone.now() + timedelta(minutes=15)
at_risk_slas = self.queryset.filter(
status='ACTIVE',
target_time__lte=warning_time
).order_by('target_time')
serializer = self.get_serializer(at_risk_slas, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def mark_met(self, request, pk=None):
"""Mark SLA as met"""
sla_instance = self.get_object()
if sla_instance.status != 'ACTIVE':
return Response(
{'error': 'Only active SLA instances can be marked as met'},
status=status.HTTP_400_BAD_REQUEST
)
sla_instance.status = 'MET'
sla_instance.met_at = timezone.now()
sla_instance.save()
return Response({'message': 'SLA marked as met'})
@action(detail=True, methods=['post'])
def mark_breached(self, request, pk=None):
"""Mark SLA as breached"""
sla_instance = self.get_object()
if sla_instance.status != 'ACTIVE':
return Response(
{'error': 'Only active SLA instances can be marked as breached'},
status=status.HTTP_400_BAD_REQUEST
)
sla_instance.status = 'BREACHED'
sla_instance.breached_at = timezone.now()
sla_instance.save()
return Response({'message': 'SLA marked as breached'})
class EscalationInstanceViewSet(viewsets.ModelViewSet):
"""ViewSet for Escalation Instance management"""
queryset = EscalationInstance.objects.all()
serializer_class = EscalationInstanceSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'escalation_level', 'escalation_policy']
search_fields = ['incident__title', 'escalation_policy__name']
ordering_fields = ['created_at', 'triggered_at']
ordering = ['-created_at']
@action(detail=True, methods=['post'])
def acknowledge(self, request, pk=None):
"""Acknowledge escalation"""
escalation = self.get_object()
if escalation.status not in ['TRIGGERED', 'PENDING']:
return Response(
{'error': 'Only pending or triggered escalations can be acknowledged'},
status=status.HTTP_400_BAD_REQUEST
)
escalation.status = 'ACKNOWLEDGED'
escalation.acknowledged_at = timezone.now()
escalation.save()
return Response({'message': 'Escalation acknowledged'})
@action(detail=True, methods=['post'])
def resolve(self, request, pk=None):
"""Resolve escalation"""
escalation = self.get_object()
if escalation.status not in ['ACKNOWLEDGED', 'TRIGGERED']:
return Response(
{'error': 'Only acknowledged or triggered escalations can be resolved'},
status=status.HTTP_400_BAD_REQUEST
)
escalation.status = 'RESOLVED'
escalation.resolved_at = timezone.now()
escalation.save()
return Response({'message': 'Escalation resolved'})
class NotificationTemplateViewSet(viewsets.ModelViewSet):
"""ViewSet for Notification Template management"""
queryset = NotificationTemplate.objects.all()
serializer_class = NotificationTemplateSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['template_type', 'channel_type', 'is_active', 'is_default']
search_fields = ['name', 'subject_template']
ordering_fields = ['name', 'created_at']
ordering = ['template_type', 'channel_type', 'name']