Skip to content

Commit 968ec53

Browse files
AdityaKhatrisusilnem
authored andcommitted
feat(event): add crisis severity level history if only ifrc_severity_level is updated
1 parent bc9de13 commit 968ec53

File tree

8 files changed

+199
-1
lines changed

8 files changed

+199
-1
lines changed

api/admin.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.contrib.auth.admin import UserAdmin
77
from django.contrib.auth.models import User
88
from django.contrib.gis import admin as geoadmin
9+
from django.core.exceptions import ValidationError
910
from django.http import HttpResponse, HttpResponseRedirect
1011
from django.urls import reverse
1112
from django.utils.html import format_html, format_html_join
@@ -272,6 +273,28 @@ def response_change(self, request, obj):
272273
return HttpResponseRedirect(".")
273274
return super().response_change(request, obj)
274275

276+
def save_model(self, request, obj, form, change):
277+
if change:
278+
# Fetch original object from DB to compare
279+
original = models.Event.objects.get(pk=obj.pk)
280+
281+
severity_changed = original.ifrc_severity_level != obj.ifrc_severity_level
282+
update_date_changed = original.ifrc_severity_level_update_date != obj.ifrc_severity_level_update_date
283+
284+
if severity_changed and not update_date_changed:
285+
messages.error(request, "You must update the 'IFRC Severity Level Update Date' when changing the severity level.")
286+
raise ValidationError("Cannot change severity level without updating the update date.")
287+
288+
if severity_changed and update_date_changed:
289+
models.EventSeverityLevelHistory.objects.create(
290+
event=obj,
291+
ifrc_severity_level=obj.ifrc_severity_level,
292+
ifrc_severity_level_update_date=obj.ifrc_severity_level_update_date,
293+
created_by=request.user,
294+
)
295+
296+
super().save_model(request, obj, form, change)
297+
275298
def field_reports(self, instance):
276299
if getattr(instance, "field_reports").exists():
277300
return format_html_join(
@@ -1014,6 +1037,16 @@ def has_add_permission(cls, request, obj=None):
10141037
return request.user.is_superuser
10151038

10161039

1040+
@admin.register(models.EventSeverityLevelHistory)
1041+
class EventSeverityLevelHistoryAdmin(admin.ModelAdmin):
1042+
list_select_related = True
1043+
list_display = ["event", "ifrc_severity_level", "created_by", "created_at"]
1044+
autocomplete_fields = (
1045+
"event",
1046+
"created_by",
1047+
)
1048+
1049+
10171050
@admin.register(models.Export)
10181051
class ExportTokenAdmin(admin.ModelAdmin):
10191052
pass

api/drf_views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
DistrictFilter,
4343
DistrictRMDFilter,
4444
EventFilter,
45+
EventSeverityLevelHistoryFilter,
4546
EventSnippetFilter,
4647
FieldReportFilter,
4748
GoHistoricalFilter,
@@ -83,6 +84,7 @@
8384
District,
8485
Event,
8586
EventFeaturedDocument,
87+
EventSeverityLevelHistory,
8688
Export,
8789
ExternalPartner,
8890
FieldReport,
@@ -123,6 +125,7 @@
123125
DisasterTypeSerializer,
124126
DistrictSerializer,
125127
DistrictSerializerRMD,
128+
EventSeverityLevelHistorySerializer,
126129
ExportSerializer,
127130
ExternalPartnerSerializer,
128131
FieldReportGeneratedTitleSerializer,
@@ -212,6 +215,12 @@ def get_queryset(self):
212215
)
213216

214217

218+
class EventSeverityLevelHistoryViewSet(viewsets.ReadOnlyModelViewSet):
219+
queryset = EventSeverityLevelHistory.objects.select_related("event", "created_by").order_by("-created_at")
220+
serializer_class = EventSeverityLevelHistorySerializer
221+
filter_class = EventSeverityLevelHistoryFilter
222+
223+
215224
class DeployedERUFilter(rest_filters.FilterSet):
216225
eru_type = rest_filters.NumberFilter(field_name="eru__type", lookup_expr="exact")
217226

api/filter_set.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
CountrySupportingPartner,
1717
District,
1818
Event,
19+
EventSeverityLevelHistory,
1920
FieldReport,
2021
Region,
2122
RegionKeyFigure,
@@ -40,6 +41,19 @@ class Meta:
4041
fields = ()
4142

4243

44+
class EventSeverityLevelHistoryFilter(filters.FilterSet):
45+
# NOTE: Adding this fixed N + 1 for some reason
46+
# Getting this issue: NO SCROLL CURSOR WITH HOLD FOR SELECT
47+
event = filters.NumberFilter(field_name="event", lookup_expr="exact")
48+
49+
class Meta:
50+
model = EventSeverityLevelHistory
51+
fields = {
52+
"id": ("exact", "in"),
53+
"event": ("exact", "in"),
54+
}
55+
56+
4357
class CountryFilter(filters.FilterSet):
4458
region = filters.NumberFilter(field_name="region", lookup_expr="exact")
4559
record_type = filters.NumberFilter(field_name="record_type", lookup_expr="exact")
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Generated by Django 4.2.19 on 2025-08-01 08:59
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
("api", "0219_alter_appealfilter_value"),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name="event",
18+
name="ifrc_severity_level_update_date",
19+
field=models.DateTimeField(blank=True, null=True, verbose_name="ifrc severity level update date"),
20+
),
21+
migrations.CreateModel(
22+
name="EventSeverityLevelHistory",
23+
fields=[
24+
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
25+
(
26+
"ifrc_severity_level",
27+
models.IntegerField(
28+
choices=[(0, "Yellow"), (1, "Orange"), (2, "Red")], default=0, verbose_name="IFRC Severity level"
29+
),
30+
),
31+
(
32+
"ifrc_severity_level_update_date",
33+
models.DateTimeField(blank=True, null=True, verbose_name="ifrc severity level update date"),
34+
),
35+
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="created at")),
36+
(
37+
"created_by",
38+
models.ForeignKey(
39+
null=True,
40+
on_delete=django.db.models.deletion.SET_NULL,
41+
related_name="created_by_ifrc_severity_level",
42+
to=settings.AUTH_USER_MODEL,
43+
verbose_name="created by",
44+
),
45+
),
46+
("event", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="api.event", verbose_name="event")),
47+
],
48+
),
49+
]

api/models.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,8 @@ def snippet_image_path(instance, filename):
737737
return "emergencies/%s/%s" % (instance.id, filename)
738738

739739

740+
# NOTE: If ever in future we need to create an api to update the event table
741+
# we also need to make sure to add appropriate signal to create ifrc severity level event history
740742
@reversion.register()
741743
class Event(models.Model):
742744
"""A disaster, which could cover multiple countries"""
@@ -789,6 +791,9 @@ class Event(models.Model):
789791
num_displaced = models.IntegerField(verbose_name=_("number of displaced"), null=True, blank=True)
790792

791793
ifrc_severity_level = models.IntegerField(choices=AlertLevel.choices, default=0, verbose_name=_("IFRC Severity level"))
794+
ifrc_severity_level_update_date = models.DateTimeField(
795+
verbose_name=_("ifrc severity level update date"), null=True, blank=True
796+
)
792797
glide = models.CharField(verbose_name=_("glide"), max_length=18, blank=True)
793798

794799
created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True)
@@ -888,6 +893,29 @@ def __str__(self):
888893
return self.name
889894

890895

896+
class EventSeverityLevelHistory(models.Model):
897+
# FIXME: Enum value shouldn't contain 0 or 1
898+
ifrc_severity_level = models.IntegerField(choices=AlertLevel.choices, default=0, verbose_name=_("IFRC Severity level"))
899+
ifrc_severity_level_update_date = models.DateTimeField(
900+
verbose_name=_("ifrc severity level update date"),
901+
null=True,
902+
blank=True,
903+
)
904+
created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True)
905+
event = models.ForeignKey(
906+
Event,
907+
verbose_name=_("event"),
908+
on_delete=models.CASCADE,
909+
)
910+
created_by = models.ForeignKey(
911+
settings.AUTH_USER_MODEL,
912+
verbose_name=_("created by"),
913+
on_delete=models.SET_NULL,
914+
null=True,
915+
related_name="created_by_ifrc_severity_level",
916+
)
917+
918+
891919
@reversion.register()
892920
class EventFeaturedDocument(models.Model):
893921
event = models.ForeignKey(

api/serializers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
EventContact,
5050
EventFeaturedDocument,
5151
EventLink,
52+
EventSeverityLevelHistory,
5253
Export,
5354
ExternalPartner,
5455
FieldReport,
@@ -119,6 +120,12 @@ class Meta:
119120
)
120121

121122

123+
class EventSeverityLevelHistorySerializer(ModelSerializer):
124+
class Meta:
125+
model = EventSeverityLevelHistory
126+
fields = "__all__"
127+
128+
122129
class RegionGeoSerializer(ModelSerializer):
123130
bbox = serializers.SerializerMethodField()
124131

@@ -1032,6 +1039,7 @@ class Meta:
10321039
"num_affected",
10331040
"ifrc_severity_level",
10341041
"ifrc_severity_level_display",
1042+
"ifrc_severity_level_update_date",
10351043
"glide",
10361044
"disaster_start_date",
10371045
"created_at",
@@ -1080,6 +1088,7 @@ class Meta:
10801088
"num_affected",
10811089
"ifrc_severity_level",
10821090
"ifrc_severity_level_display",
1091+
"ifrc_severity_level_update_date",
10831092
"glide",
10841093
"disaster_start_date",
10851094
"created_at",
@@ -1127,6 +1136,7 @@ class Meta:
11271136
"num_affected",
11281137
"ifrc_severity_level",
11291138
"ifrc_severity_level_display",
1139+
"ifrc_severity_level_update_date",
11301140
"glide",
11311141
"disaster_start_date",
11321142
"created_at",
@@ -1171,6 +1181,7 @@ class Meta:
11711181
"num_affected",
11721182
"ifrc_severity_level",
11731183
"ifrc_severity_level_display",
1184+
"ifrc_severity_level_update_date",
11741185
"glide",
11751186
"disaster_start_date",
11761187
"created_at",
@@ -1341,6 +1352,7 @@ class Meta:
13411352
"tab_one_title",
13421353
"ifrc_severity_level",
13431354
"ifrc_severity_level_display",
1355+
"ifrc_severity_level_update_date",
13441356
"parent_event",
13451357
"glide",
13461358
"featured_documents",

api/test_models.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from unittest.mock import patch
22

3+
from django.contrib.admin.sites import AdminSite
34
from django.contrib.auth.models import User
4-
from django.test import TestCase
5+
from django.test import RequestFactory, TestCase
6+
from django.utils import timezone
57
from rest_framework.test import APITestCase
68

79
import api.models as models
10+
from api.admin import EventAdmin
811
from api.factories import country as countryFactory
912
from api.factories import event as eventFactory
1013
from api.factories import field_report as fieldReportFactory
14+
from api.factories.region import RegionFactory
1115
from main.mock import erp_request_side_effect_mock
1216

1317

@@ -128,3 +132,49 @@ def setUp(self):
128132
def test_profile_create(self):
129133
obj = models.Profile.objects.get(user__username="test1")
130134
self.assertEqual(obj.department, "testdepartment")
135+
136+
137+
class EventSeverityLevelHistoryTest(TestCase):
138+
139+
fixtures = ["DisasterTypes"]
140+
141+
def setUp(self):
142+
self.user = User.objects.create(username="username", first_name="pat", last_name="smith", password="password")
143+
self.region = RegionFactory()
144+
self.country = countryFactory.CountryFactory()
145+
self.dtype = models.DisasterType.objects.get(pk=1)
146+
147+
self.event = eventFactory.EventFactory.create(
148+
dtype=self.dtype,
149+
ifrc_severity_level=models.AlertLevel.YELLOW,
150+
)
151+
self.event.regions.set([self.region])
152+
self.event.countries.set([self.country])
153+
self.event._current_user = self.user
154+
self.admin = EventAdmin(models.Event, AdminSite())
155+
self.factory = RequestFactory()
156+
157+
def test_ifrc_severity_level_history_created_from_admin(self):
158+
# Simulate admin request
159+
request = self.factory.post("/")
160+
request.user = self.user
161+
162+
# Update the instance
163+
self.event.ifrc_severity_level = models.AlertLevel.RED
164+
self.event.ifrc_severity_level_update_date = timezone.now()
165+
166+
# Trigger admin save_model (where your logic lives)
167+
self.admin.save_model(request, self.event, None, change=True)
168+
169+
history = models.EventSeverityLevelHistory.objects.filter(event=self.event)
170+
self.assertEqual(history.count(), 1)
171+
self.assertEqual(history[0].ifrc_severity_level, models.AlertLevel.RED)
172+
self.assertEqual(history[0].created_by, self.user)
173+
174+
def test_no_history_created_if_severity_level_not_changed(self):
175+
# Save the event with the current severity level (no change)
176+
self.event.ifrc_severity_level = models.AlertLevel.YELLOW # same as initial value
177+
self.event.save()
178+
179+
history = models.EventSeverityLevelHistory.objects.filter(event=self.event)
180+
self.assertEqual(history.count(), 0)

main/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@
9898
router.register(r"eru-readiness", deployment_views.ERUReadinessViewSet, basename="eru_readiness")
9999
router.register(r"eru-readiness-type", deployment_views.ERUReadinessTypeViewset, basename="eru_readiness_type")
100100
router.register(r"event", api_views.EventViewset, basename="event")
101+
router.register(
102+
r"event-severity-level-history", api_views.EventSeverityLevelHistoryViewSet, basename="event_severity_level_history"
103+
)
101104
router.register(r"go-historical", api_views.GoHistoricalViewSet, basename="go_historical")
102105
router.register(r"featured_event_deployments", api_views.EventDeploymentsViewset, basename="featured_event_deployments")
103106
router.register(r"field-report", api_views.FieldReportViewset, basename="field_report")

0 commit comments

Comments
 (0)