Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(maintenance): message on deletion and prevent sooner ending #428

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions admin_cohort/services/maintenance.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import logging
from datetime import timedelta
from typing import Union, Optional
@@ -22,6 +23,7 @@


class WSMaintenanceInfo(BaseModel):
id: int
subject: Optional[str]
maintenance_start: str
maintenance_end: str
@@ -36,6 +38,7 @@ class WSMaintenance(WebSocketMessage):

def maintenance_phase_to_info(maintenance: MaintenancePhase) -> WSMaintenanceInfo:
return WSMaintenanceInfo(
id=maintenance.id,
subject=maintenance.subject,
maintenance_start=maintenance.start_datetime.isoformat(),
maintenance_end=maintenance.end_datetime.isoformat(),
@@ -58,20 +61,41 @@ def get_maintenance_with_event():
return event_to_start, event_to_end

@staticmethod
def send_maintenance_notification(maintenance_info: WSMaintenanceInfo):
def send_deleted_maintenance_notification(maintenance_info: MaintenancePhase):
now = timezone.now()
if maintenance_info.end_datetime >= now >= maintenance_info.start_datetime:
deleted_maintenance = maintenance_phase_to_info(maintenance_info)
maintenance_service.send_maintenance_notification(deleted_maintenance, force_active_state=False)

@staticmethod
def send_maintenance_notification(maintenance_info: WSMaintenanceInfo, force_active_state: Optional[bool] = None):
"""
Send a maintenance notification to all clients.
Except if there is a current maintenance active and the message is an end maintenance message.
"""
now = timezone.now()
start_time = dateutil.parser.parse(maintenance_info.maintenance_start)
end_time = dateutil.parser.parse(maintenance_info.maintenance_end)
maintenance_info.active = start_time < now < end_time
maintenance_info.active = force_active_state if force_active_state is not None else start_time < now < end_time
logging.info(f"Sending maintenance notification: {maintenance_info}")
WebsocketManager.send_to_client("__all__", WSMaintenance(type=WebSocketMessageType.MAINTENANCE, info=maintenance_info))
current_maintenances = MaintenancePhase.objects.filter(start_datetime__lte=now, end_datetime__gte=now).order_by('-end_datetime').all()
current_active_maintenances = [cur for cur in
current_maintenances
if cur.id != maintenance_info.id]
if maintenance_info.active or not current_active_maintenances:
WebsocketManager.send_to_client("__all__", WSMaintenance(type=WebSocketMessageType.MAINTENANCE, info=maintenance_info))

@staticmethod
def get_next_maintenance() -> Union[MaintenancePhase, None]:
now = timezone.now()
current = MaintenancePhase.objects.filter(start_datetime__lte=now, end_datetime__gte=now) \
def get_current_maintenance(now: Optional[datetime] = None) -> Optional[MaintenancePhase]:
ref_now = now or timezone.now()
return MaintenancePhase.objects.filter(start_datetime__lte=ref_now, end_datetime__gte=ref_now) \
.order_by('-end_datetime') \
.first()

@staticmethod
def get_next_maintenance() -> Union[MaintenancePhase, None]:
now = timezone.now()
current = MaintenanceService.get_current_maintenance(now)
if current:
return current
next_maintenance = MaintenancePhase.objects.filter(start_datetime__gte=now) \
3 changes: 3 additions & 0 deletions admin_cohort/tasks.py
Original file line number Diff line number Diff line change
@@ -11,6 +11,9 @@ def maintenance_notifier_checker():
maintenance_notifier.s(maintenance_phase_to_info(event).model_dump()).apply_async(eta=event.start_datetime)
for event in event_to_end:
maintenance_notifier.s(maintenance_phase_to_info(event).model_dump()).apply_async(eta=event.end_datetime)
current_maintenance = MaintenanceService.get_current_maintenance()
if current_maintenance:
maintenance_notifier.s(maintenance_phase_to_info(current_maintenance).model_dump()).apply_async()


@shared_task
62 changes: 62 additions & 0 deletions admin_cohort/tests/test_maintenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from datetime import timedelta
from unittest.mock import patch, MagicMock

from django.test import TestCase
from django.utils import timezone

from admin_cohort.services.maintenance import MaintenanceService, WSMaintenanceInfo


class TestMaintenanceService(TestCase):
@patch('admin_cohort.services.maintenance.maintenance_phase_to_info')
@patch('admin_cohort.services.maintenance.MaintenanceService.send_maintenance_notification')
def test_send_deleted_maintenance_notification_within_time_range(self, mock_send_notification, mock_phase_to_info):
now = timezone.now()
maintenance_info = MagicMock()
maintenance_info.start_datetime = now - timedelta(minutes=5)
maintenance_info.end_datetime = now + timedelta(minutes=5)
mock_phase_to_info.return_value = WSMaintenanceInfo(id=1, maintenance_start=str(maintenance_info.start_datetime),
maintenance_end=str(maintenance_info.end_datetime), active=True, type='test',
subject='test', message='test')

MaintenanceService.send_deleted_maintenance_notification(maintenance_info)
mock_send_notification.assert_called_once()

@patch('admin_cohort.services.maintenance.maintenance_phase_to_info')
@patch('admin_cohort.services.maintenance.MaintenanceService.send_maintenance_notification')
def test_send_deleted_maintenance_notification_outside_time_range(self, mock_send_notification, mock_phase_to_info):
now = timezone.now()
maintenance_info = MagicMock()
maintenance_info.start_datetime = now - timedelta(minutes=10)
maintenance_info.end_datetime = now - timedelta(minutes=5)

MaintenanceService.send_deleted_maintenance_notification(maintenance_info)
mock_send_notification.assert_not_called()

@patch('admin_cohort.services.maintenance.dateutil.parser.parse')
@patch('admin_cohort.services.maintenance.MaintenancePhase.objects.filter')
@patch('admin_cohort.services.maintenance.WebsocketManager.send_to_client')
def test_send_maintenance_notification_active(self, mock_send_to_client, mock_filter, mock_parse):
now = timezone.now()
maintenance_info = WSMaintenanceInfo(id=1, maintenance_start=str(now - timedelta(minutes=5)),
maintenance_end=str(now + timedelta(minutes=5)), active=True, type='test',
subject='test', message='test')
mock_parse.side_effect = [now - timedelta(minutes=5), now + timedelta(minutes=5)]
mock_filter.return_value.exclude.return_value = []

MaintenanceService.send_maintenance_notification(maintenance_info)
mock_send_to_client.assert_called_once()

@patch('admin_cohort.services.maintenance.dateutil.parser.parse')
@patch('admin_cohort.services.maintenance.MaintenancePhase.objects.filter')
@patch('admin_cohort.services.maintenance.WebsocketManager.send_to_client')
def test_send_maintenance_notification_inactive_with_current_active(self, mock_send_to_client, mock_filter, mock_parse):
now = timezone.now()
maintenance_info = WSMaintenanceInfo(id=1, maintenance_start=str(now - timedelta(minutes=10)),
maintenance_end=str(now - timedelta(minutes=5)), active=False, type='test',
subject='test', message='test')
mock_parse.side_effect = [now - timedelta(minutes=10), now - timedelta(minutes=5)]
mock_filter.return_value.order_by.return_value.all.return_value = [MagicMock()]

MaintenanceService.send_maintenance_notification(maintenance_info)
mock_send_to_client.assert_not_called()
4 changes: 4 additions & 0 deletions admin_cohort/views/maintenance_phase.py
Original file line number Diff line number Diff line change
@@ -37,3 +37,7 @@ def next(self, request, *args, **kwargs):
q = maintenance_service.get_next_maintenance()
d = self.get_serializer(q).data if q is not None else {}
return Response(d)

def destroy(self, request, *args, **kwargs):
maintenance_service.send_deleted_maintenance_notification(self.get_object())
return super().destroy(request, *args, **kwargs)