Updates
This commit is contained in:
1
ETB-API/sla_oncall/views/__init__.py
Normal file
1
ETB-API/sla_oncall/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Views for SLA & On-Call Management
|
||||
BIN
ETB-API/sla_oncall/views/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/views/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ETB-API/sla_oncall/views/__pycache__/sla.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/views/__pycache__/sla.cpython-312.pyc
Normal file
Binary file not shown.
354
ETB-API/sla_oncall/views/sla.py
Normal file
354
ETB-API/sla_oncall/views/sla.py
Normal file
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
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']
|
||||
Reference in New Issue
Block a user