Skip to content

Commit ad81de0

Browse files
Merge pull request #2040 from IFRCGo/fix/surge-alert
Surge Alert Status and Ordering based on status and opens fields
2 parents 7225782 + 55455b3 commit ad81de0

File tree

9 files changed

+183
-5
lines changed

9 files changed

+183
-5
lines changed

deploy/helm/ifrcgo-helm/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ cronjobs:
111111
schedule: '0 9 * * *'
112112
- command: 'ingest_country_plan_file'
113113
schedule: '1 0 * * *'
114+
- command: 'update_surge_alert_status'
115+
schedule: '1 */12 * * *'
114116

115117
elasticsearch:
116118
enabled: true

main/enums.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from flash_update import enums as flash_update_enums
66
from deployments import enums as deployments_enums
77
from per import enums as per_enums
8+
from notifications import enums as notifications_enums
89

910

1011
apps_enum_register = [
@@ -13,6 +14,7 @@
1314
('flash_update', flash_update_enums.enum_register),
1415
('deployments', deployments_enums.enum_register),
1516
('per', per_enums.enum_register),
17+
('notifications', notifications_enums.enum_register),
1618
]
1719

1820

notifications/drf_views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class SurgeAlertFilter(filters.FilterSet):
3333
help_text='Molnix_tag names, comma separated',
3434
widget=CSVWidget,
3535
)
36+
status = filters.NumberFilter(field_name='status', lookup_expr='exact')
3637

3738
class Meta:
3839
model = SurgeAlert
@@ -62,7 +63,7 @@ class SurgeAlertViewset(viewsets.ReadOnlyModelViewSet):
6263
prefetch_related('molnix_tags', 'molnix_tags__groups').\
6364
select_related('event', 'country').all()
6465
filterset_class = SurgeAlertFilter
65-
ordering_fields = ('created_at', 'atype', 'category', 'event', 'is_stood_down',)
66+
ordering_fields = ('created_at', 'atype', 'category', 'event', 'is_stood_down', 'status', 'opens')
6667
search_fields = ('operation', 'message', 'event__name',) # for /docs
6768

6869
def get_serializer_class(self):

notifications/enums.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from . import models
2+
3+
enum_register = {
4+
'surge_alert_status': models.SurgeAlertStatus,
5+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import logging
2+
from django.db import models
3+
4+
from django.core.management.base import BaseCommand
5+
from django.utils import timezone
6+
from notifications.models import SurgeAlert, SurgeAlertStatus
7+
8+
logger = logging.getLogger(__name__)
9+
10+
class Command(BaseCommand):
11+
'''
12+
Updating the Alert Status according:
13+
If the alert status is marked as stood_down, then the status is Stood Down.
14+
If the closing timestamp (closes) is earlier than the current date, the status is displayed as Closed. Otherwise, it is displayed as Open.
15+
'''
16+
help = 'Update alert status'
17+
18+
def handle(self, *args, **options):
19+
now = timezone.now()
20+
try:
21+
SurgeAlert.objects.update(
22+
status=models.Case(
23+
models.When(is_stood_down=True, then=models.Value(SurgeAlertStatus.STOOD_DOWN)),
24+
models.When(closes__lt=now, then=models.Value(SurgeAlertStatus.CLOSED)),
25+
models.When(closes__gte=now, then=models.Value(SurgeAlertStatus.OPEN)),
26+
default=models.F('status'),
27+
output_field=models.IntegerField()
28+
)
29+
)
30+
except Exception as e:
31+
logger.error('Error while updating alerts status', exc_info=True)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.23 on 2024-02-13 09:43
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('notifications', '0013_auto_20230410_0720'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='surgealert',
15+
name='status',
16+
field=models.IntegerField(choices=[(0, 'Open'), (1, 'Stood Down'), (2, 'Closed')], default=0, verbose_name='alert status'),
17+
),
18+
]

notifications/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ class SurgeAlertCategory(models.IntegerChoices):
2424
STAND_DOWN = 4, _('stand down')
2525

2626

27+
class SurgeAlertStatus(models.IntegerChoices):
28+
'''
29+
Note: Ordering value should be in order of Open, Stood Down, Closed to supported custom defined ordering logic
30+
'''
31+
OPEN = 0, _('Open')
32+
STOOD_DOWN = 1, _('Stood Down')
33+
CLOSED = 2, _('Closed')
34+
35+
2736
class SurgeAlert(models.Model):
2837

2938
atype = models.IntegerField(choices=SurgeAlertType.choices, verbose_name=_('alert type'), default=0)
@@ -54,17 +63,29 @@ class SurgeAlert(models.Model):
5463

5564
# Don't set `auto_now_add` so we can modify it on save
5665
created_at = models.DateTimeField(verbose_name=_('created at'))
66+
status = models.IntegerField(choices=SurgeAlertStatus.choices, verbose_name=_('alert status'), default=SurgeAlertStatus.OPEN)
5767

5868
class Meta:
5969
ordering = ['-created_at']
6070
verbose_name = _('Surge Alert')
6171
verbose_name_plural = _('Surge Alerts')
6272

6373
def save(self, *args, **kwargs):
74+
"""
75+
If the alert status is marked as stood_down, then the status is Stood Down.
76+
If the closing timestamp (closes) is earlier than the current date, the status is displayed as Closed.
77+
Otherwise, it is displayed as Open.
78+
"""
6479
# On save, if `created` is not set, make it the current time
6580
if (not self.id and not self.created_at) or (self.created_at > timezone.now()):
6681
self.created_at = timezone.now()
6782
self.is_stood_down = self.molnix_status == 'unfilled'
83+
if self.is_stood_down:
84+
self.status = SurgeAlertStatus.STOOD_DOWN
85+
elif self.closes and self.closes < timezone.now():
86+
self.status = SurgeAlertStatus.CLOSED
87+
else:
88+
self.status = SurgeAlertStatus.OPEN
6889
return super(SurgeAlert, self).save(*args, **kwargs)
6990

7091
def __str__(self):

notifications/serializers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ class SurgeAlertSerializer(ModelSerializer):
1111
event = SurgeEventSerializer()
1212
country = MiniCountrySerializer()
1313
atype_display = serializers.CharField(source='get_atype_display', read_only=True)
14+
status_display = serializers.CharField(source='get_status_display', read_only=True)
1415
category_display = serializers.CharField(source='get_category_display', read_only=True)
1516
molnix_tags = MolnixTagSerializer(many=True, read_only=True)
1617

1718
class Meta:
1819
model = SurgeAlert
1920
fields = (
20-
'operation', 'country', 'message', 'deployment_needed', 'is_private', 'event', 'created_at', 'id',
21+
'operation','country', 'message', 'deployment_needed', 'is_private', 'event', 'created_at', 'id',
2122
'atype', 'atype_display', 'category', 'category_display', 'molnix_id', 'molnix_tags',
22-
'molnix_status', 'opens', 'closes', 'start', 'end', 'is_active', 'is_stood_down'
23+
'molnix_status', 'opens', 'closes', 'start', 'end', 'is_active', 'is_stood_down','status', 'status_display'
2324
)
2425

2526

notifications/tests.py

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from datetime import datetime, timezone
1+
from datetime import datetime, timedelta
2+
from django.utils import timezone
3+
from unittest.mock import patch
24
from django.conf import settings
35
from modeltranslation.utils import build_localized_fieldname
46

@@ -10,8 +12,9 @@
1012
from api.factories.country import CountryFactory
1113
from deployments.factories.molnix_tag import MolnixTagFactory
1214

13-
from notifications.models import SurgeAlert, SurgeAlertType
15+
from notifications.models import SurgeAlert, SurgeAlertStatus, SurgeAlertType
1416
from notifications.factories import SurgeAlertFactory
17+
from django.core.management import call_command
1518

1619

1720
class NotificationTestCase(APITestCase):
@@ -136,3 +139,97 @@ def _to_csv(*items):
136139
molnix_tag_names=_to_csv('OP-6700', 'L-FRA', 'AMER'),
137140
))
138141
self.assertEqual(response['count'], 2)
142+
143+
def test_surge_alert_status(self):
144+
region_1, region_2 = RegionFactory.create_batch(2)
145+
country_1 = CountryFactory.create(iso3='ATL', region=region_1)
146+
country_2 = CountryFactory.create(iso3='NPT', region=region_2)
147+
148+
molnix_tag_1 = MolnixTagFactory.create(name='OP-6700')
149+
molnix_tag_2 = MolnixTagFactory.create(name='L-FRA')
150+
molnix_tag_3 = MolnixTagFactory.create(name='AMER')
151+
152+
alert1 = SurgeAlertFactory.create(
153+
message='CEA Coordinator, Floods, Atlantis',
154+
country=country_1,
155+
molnix_tags=[molnix_tag_1, molnix_tag_2],
156+
opens=timezone.now() - timedelta(days=2),
157+
closes=timezone.now() + timedelta(days=5)
158+
)
159+
alert2 = SurgeAlertFactory.create(
160+
message='WASH Coordinator, Earthquake, Neptunus',
161+
country=country_2,
162+
molnix_tags=[molnix_tag_1, molnix_tag_3],
163+
opens=timezone.now() - timedelta(days=2),
164+
closes=timezone.now() - timedelta(days=1)
165+
)
166+
alert3 = SurgeAlertFactory.create(
167+
message='New One',
168+
country=country_2,
169+
molnix_tags=[molnix_tag_1, molnix_tag_3],
170+
molnix_status='unfilled',
171+
)
172+
173+
self.assertEqual(alert1.status, SurgeAlertStatus.OPEN)
174+
self.assertEqual(alert2.status, SurgeAlertStatus.CLOSED)
175+
self.assertEqual(alert3.status, SurgeAlertStatus.STOOD_DOWN)
176+
177+
def _fetch(filters):
178+
return self.client.get('/api/v2/surge_alert/', filters).json()
179+
180+
response = _fetch(dict({
181+
'status': SurgeAlertStatus.OPEN
182+
}))
183+
184+
self.assertEqual(response['count'], 1)
185+
self.assertEqual(response['results'][0]['status'], SurgeAlertStatus.OPEN)
186+
187+
response = _fetch(dict({
188+
'status': SurgeAlertStatus.CLOSED
189+
}))
190+
self.assertEqual(response['count'], 1)
191+
self.assertEqual(response['results'][0]['status'], SurgeAlertStatus.CLOSED)
192+
193+
response = _fetch(dict({
194+
'status': SurgeAlertStatus.STOOD_DOWN
195+
}))
196+
self.assertEqual(response['count'], 1)
197+
self.assertEqual(response['results'][0]['status'], SurgeAlertStatus.STOOD_DOWN)
198+
199+
200+
class SurgeAlertTestCase(APITestCase):
201+
def test_update_alert_status_command(self):
202+
region_1, region_2 = RegionFactory.create_batch(2)
203+
country_1 = CountryFactory.create(iso3='NPP', region=region_1)
204+
country_2 = CountryFactory.create(iso3='CTT', region=region_2)
205+
206+
molnix_tag_1 = MolnixTagFactory.create(name='OP-6700')
207+
molnix_tag_2 = MolnixTagFactory.create(name='L-FRA')
208+
molnix_tag_3 = MolnixTagFactory.create(name='AMER')
209+
210+
alert1 = SurgeAlertFactory.create(
211+
message='CEA Coordinator, Floods, Atlantis',
212+
country=country_1,
213+
molnix_tags=[molnix_tag_1, molnix_tag_2],
214+
opens=timezone.now() - timedelta(days=2),
215+
closes=timezone.now() + timedelta(seconds=5)
216+
)
217+
alert2 = SurgeAlertFactory.create(
218+
message='WASH Coordinator, Earthquake, Neptunus',
219+
country=country_2,
220+
molnix_tags=[molnix_tag_1, molnix_tag_3],
221+
opens=timezone.now() - timedelta(days=2),
222+
closes=timezone.now() - timedelta(days=1)
223+
)
224+
self.assertEqual(alert1.status, SurgeAlertStatus.OPEN)
225+
self.assertEqual(alert2.status, SurgeAlertStatus.CLOSED)
226+
227+
with patch('django.utils.timezone.now') as mock_now:
228+
mock_now.return_value = datetime.now() + timedelta(days=1)
229+
call_command('update_surge_alert_status')
230+
231+
alert1.refresh_from_db()
232+
alert2.refresh_from_db()
233+
234+
self.assertEqual(alert1.status, SurgeAlertStatus.CLOSED)
235+
self.assertEqual(alert2.status, SurgeAlertStatus.CLOSED)

0 commit comments

Comments
 (0)