355 lines
13 KiB
Python
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']
|