Skip to content

Commit bc2af82

Browse files
committed
feat(notifications): add alert subscription api
1 parent 14be734 commit bc2af82

File tree

10 files changed

+409
-6
lines changed

10 files changed

+409
-6
lines changed

main/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
router.register(r"situation_report_type", api_views.SituationReportTypeViewset, basename="situation_report_type")
154154
router.register(r"subscription", notification_views.SubscriptionViewset, basename="subscription")
155155
router.register(r"surge_alert", notification_views.SurgeAlertViewset, basename="surge_alert")
156+
router.register(r"alert-subscription", notification_views.AlertSubscriptionViewSet, basename="alert_subscription")
156157
router.register(r"user", api_views.UserViewset, basename="user")
157158
router.register(r"flash-update", flash_views.FlashUpdateViewSet, basename="flash_update")
158159
router.register(r"flash-update-file", flash_views.FlashUpdateFileViewSet, basename="flash_update_file")

notifications/admin.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,29 @@ def has_delete_permission(self, request, obj=None):
6666
admin.site.register(models.NotificationGUID, NotificationGUIDAdmin)
6767
admin.site.register(models.Subscription, SubscriptionAdmin)
6868
admin.site.register(models.SurgeAlert, SurgeAlertAdmin)
69+
70+
71+
@admin.register(models.HazardType)
72+
class AlertTypeAdmin(admin.ModelAdmin):
73+
list_display = ("type",)
74+
75+
76+
@admin.register(models.AlertSubscription)
77+
class AlertSubscriptionAdmin(admin.ModelAdmin):
78+
list_select_related = True
79+
list_display = ("user", "created_at")
80+
autocomplete_fields = ("user",)
81+
82+
def get_queryset(self, request):
83+
return (
84+
super()
85+
.get_queryset(request)
86+
.select_related(
87+
"user",
88+
)
89+
.prefetch_related(
90+
"countries",
91+
"regions",
92+
"hazard_types",
93+
)
94+
)

notifications/drf_views.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
# from datetime import datetime, timedelta, timezone
22
# from django.db.models import Q
3+
from django.db.models.query import QuerySet
34
from django_filters import rest_framework as filters
45
from django_filters.widgets import CSVWidget
5-
from rest_framework import viewsets
6+
from rest_framework import mixins, viewsets
67
from rest_framework.authentication import TokenAuthentication
78
from rest_framework.permissions import IsAuthenticated
89

910
from deployments.models import MolnixTag
1011
from main.filters import CharInFilter
1112
from main.permissions import DenyGuestUserPermission
13+
from notifications.filter_set import AlertSubscriptionFilterSet
1214

13-
from .models import Subscription, SurgeAlert
15+
from .models import AlertSubscription, Subscription, SurgeAlert
1416
from .serializers import ( # UnauthenticatedSurgeAlertSerializer,
17+
AlertSubscriptionSerialize,
1518
SubscriptionSerializer,
1619
SurgeAlertCsvSerializer,
1720
SurgeAlertSerializer,
@@ -110,3 +113,34 @@ class SubscriptionViewset(viewsets.ModelViewSet):
110113

111114
def get_queryset(self):
112115
return Subscription.objects.filter(user=self.request.user)
116+
117+
118+
class AlertSubscriptionViewSet(
119+
viewsets.GenericViewSet,
120+
mixins.ListModelMixin,
121+
mixins.RetrieveModelMixin,
122+
mixins.CreateModelMixin,
123+
mixins.UpdateModelMixin,
124+
):
125+
queryset = AlertSubscription.objects.all()
126+
serializer_class = AlertSubscriptionSerialize
127+
filterset_class = AlertSubscriptionFilterSet
128+
lookup_field = "id"
129+
permission_classes = [
130+
IsAuthenticated,
131+
DenyGuestUserPermission,
132+
]
133+
134+
def get_queryset(self) -> QuerySet[AlertSubscription]:
135+
return (
136+
super()
137+
.get_queryset()
138+
.select_related(
139+
"user",
140+
)
141+
.prefetch_related(
142+
"countries",
143+
"regions",
144+
"hazard_types",
145+
)
146+
)

notifications/enums.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22

33
enum_register = {
44
"surge_alert_status": models.SurgeAlertStatus,
5+
"alert_source": models.AlertSubscription.AlertSource,
6+
"hazard_type": models.HazardType.Type,
7+
"alert_per_day": models.AlertSubscription.AlertPerDay,
58
}

notifications/factories.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import factory
22
from factory import fuzzy
33

4-
from .models import SurgeAlert, SurgeAlertStatus
4+
from deployments.factories.user import UserFactory
5+
6+
from .models import AlertSubscription, HazardType, SurgeAlert, SurgeAlertStatus
57

68

79
class SurgeAlertFactory(factory.django.DjangoModelFactory):
@@ -21,3 +23,39 @@ def molnix_tags(self, create, extracted, **_):
2123
if extracted:
2224
for item in extracted:
2325
self.molnix_tags.add(item)
26+
27+
28+
class AlertSubscriptionFactory(factory.django.DjangoModelFactory):
29+
class Meta:
30+
model = AlertSubscription
31+
32+
user = factory.SubFactory(UserFactory)
33+
34+
@factory.post_generation
35+
def countries(self, create, extracted, **kwargs):
36+
if not create:
37+
return
38+
if extracted:
39+
for country in extracted:
40+
self.countries.add(country)
41+
42+
@factory.post_generation
43+
def regions(self, create, extracted, **kwargs):
44+
if not create:
45+
return
46+
if extracted:
47+
for region in extracted:
48+
self.regions.add(region)
49+
50+
@factory.post_generation
51+
def hazard_types(self, create, extracted, **kwargs):
52+
if not create:
53+
return
54+
if extracted:
55+
for alert_type in extracted:
56+
self.hazard_types.add(alert_type)
57+
58+
59+
class HazardTypeFactory(factory.django.DjangoModelFactory):
60+
class Meta:
61+
model = HazardType

notifications/filter_set.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import django_filters as filters
2+
3+
from api.models import Country, Region
4+
from notifications.models import AlertSubscription
5+
6+
7+
class AlertSubscriptionFilterSet(filters.FilterSet):
8+
country = filters.ModelMultipleChoiceFilter(field_name="countries", queryset=Country.objects.all())
9+
region = filters.ModelMultipleChoiceFilter(field_name="regions", queryset=Region.objects.all())
10+
alert_source = filters.NumberFilter(field_name="alert_source", label="Alert Source")
11+
hazard_type = filters.NumberFilter(field_name="hazard_types__type", label="Hazard Type")
12+
alert_per_day = filters.ChoiceFilter(choices=AlertSubscription.AlertPerDay.choices, label="Alert Per Day")
13+
14+
class Meta:
15+
model = AlertSubscription
16+
fields = {
17+
"countries__iso3": ("exact",),
18+
"alert_per_day": ("exact",),
19+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Generated by Django 4.2.19 on 2025-12-03 10:45
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('api', '0226_nsdinitiativescategory_and_more'),
13+
('notifications', '0015_rename_molnix_status_surgealert_molnix_status_old'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='HazardType',
19+
fields=[
20+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21+
('type', models.IntegerField(choices=[(100, 'Earthquake'), (200, 'Flood'), (300, 'Cyclone')], unique=True, verbose_name='Hazard Type')),
22+
],
23+
options={
24+
'verbose_name': 'Hazard Type',
25+
'verbose_name_plural': 'Hazard Types',
26+
},
27+
),
28+
migrations.CreateModel(
29+
name='AlertSubscription',
30+
fields=[
31+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
32+
('alert_source', models.IntegerField(choices=[(100, 'Montandon')], default=100, verbose_name='Alert source')),
33+
('alert_per_day', models.IntegerField(choices=[(100, 'Five'), (200, 'Ten'), (300, 'Twenty'), (400, 'Fifty'), (500, 'Unlimited')], default=100, help_text='Maximum number of alerts sent to the user per day.', verbose_name='Alerts Per Day')),
34+
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
35+
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
36+
('countries', models.ManyToManyField(related_name='alert_subscriptions_by_country', to='api.country', verbose_name='Countries')),
37+
('hazard_types', models.ManyToManyField(help_text='Types of hazard the user is subscribed to.', related_name='+', to='notifications.hazardtype', verbose_name='Hazard Types')),
38+
('regions', models.ManyToManyField(blank=True, related_name='alert_subscriptions_by_region', to='api.region', verbose_name='Regions')),
39+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='alert_subscriptions', to=settings.AUTH_USER_MODEL, verbose_name='User')),
40+
],
41+
options={
42+
'verbose_name': 'Alert Subscription',
43+
'verbose_name_plural': 'Alert Subscriptions',
44+
'ordering': ['-id'],
45+
},
46+
),
47+
]

notifications/models.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,97 @@ class NotificationGUID(models.Model):
320320
)
321321
email_type = models.CharField(max_length=600, null=True, blank=True)
322322
to_list = models.TextField(null=True, blank=True)
323+
324+
325+
class HazardType(models.Model):
326+
"""Model representing a hazard category."""
327+
328+
class Type(models.IntegerChoices):
329+
EARTHQUAKE = 100, _("Earthquake")
330+
FLOOD = 200, _("Flood")
331+
CYCLONE = 300, _("Cyclone")
332+
333+
type = models.IntegerField(
334+
choices=Type.choices,
335+
unique=True,
336+
verbose_name=_("Hazard Type"),
337+
)
338+
339+
class Meta:
340+
verbose_name = _("Hazard Type")
341+
verbose_name_plural = _("Hazard Types")
342+
343+
def __str__(self):
344+
return self.get_type_display()
345+
346+
347+
class AlertSubscription(models.Model):
348+
class AlertSource(models.IntegerChoices):
349+
MONTANDON = 100, _("Montandon")
350+
"""Alerts provided by the Montandon platform."""
351+
352+
class AlertPerDay(models.IntegerChoices):
353+
"""Enum representing the maximum number of alerts per day."""
354+
355+
FIVE = 100, _("Five")
356+
"""Receive up to 5 alerts per day."""
357+
358+
TEN = 200, _("Ten")
359+
"""Receive up to 10 alerts per day."""
360+
361+
TWENTY = 300, _("Twenty")
362+
"""Receive up to 20 alerts per day."""
363+
364+
FIFTY = 400, _("Fifty")
365+
"""Receive up to 50 alerts per day."""
366+
367+
UNLIMITED = 500, _("Unlimited")
368+
"""No daily alert limit."""
369+
370+
user = models.ForeignKey(
371+
settings.AUTH_USER_MODEL,
372+
verbose_name=_("User"),
373+
on_delete=models.CASCADE,
374+
related_name="alert_subscriptions",
375+
)
376+
countries = models.ManyToManyField(
377+
Country,
378+
related_name="alert_subscriptions_by_country",
379+
verbose_name=_("Countries"),
380+
)
381+
regions = models.ManyToManyField(
382+
Region,
383+
related_name="alert_subscriptions_by_region",
384+
blank=True,
385+
verbose_name=_("Regions"),
386+
)
387+
alert_source = models.IntegerField(
388+
choices=AlertSource.choices,
389+
default=AlertSource.MONTANDON,
390+
verbose_name=_("Alert source"),
391+
)
392+
393+
hazard_types = models.ManyToManyField(
394+
HazardType,
395+
related_name="+",
396+
verbose_name=_("Hazard Types"),
397+
help_text=_("Types of hazard the user is subscribed to."),
398+
)
399+
alert_per_day = models.IntegerField(
400+
choices=AlertPerDay.choices,
401+
default=AlertPerDay.FIVE,
402+
verbose_name=_("Alerts Per Day"),
403+
help_text=_("Maximum number of alerts sent to the user per day."),
404+
)
405+
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
406+
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
407+
# Typing
408+
id = int
409+
410+
class Meta:
411+
ordering = ["-id"]
412+
verbose_name = _("Alert Subscription")
413+
verbose_name_plural = _("Alert Subscriptions")
414+
415+
def __str__(self):
416+
return f"Alert subscription for {self.user}"

notifications/serializers.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
from api.serializers import (
44
MiniCountrySerializer,
55
MiniEventSerializer,
6+
MiniRegionSerialzier,
67
SurgeEventSerializer,
8+
UserNameSerializer,
79
)
810
from deployments.serializers import MolnixTagSerializer
911
from lang.serializers import ModelSerializer
1012

11-
from .models import Subscription, SurgeAlert
13+
from .models import AlertSubscription, HazardType, Subscription, SurgeAlert
1214

1315

1416
class SurgeAlertSerializer(ModelSerializer):
@@ -258,3 +260,42 @@ class Meta:
258260
"rtype",
259261
"rtype_display",
260262
)
263+
264+
265+
class HazardTypeSerializer(ModelSerializer):
266+
267+
type_display = serializers.CharField(source="get_alert_type_display", read_only=True)
268+
269+
class Meta:
270+
model = HazardType
271+
fields = (
272+
"id",
273+
"type",
274+
"type_display",
275+
)
276+
277+
278+
class AlertSubscriptionSerialize(ModelSerializer):
279+
user_detail = UserNameSerializer(source="user", read_only=True)
280+
countries_detail = MiniCountrySerializer(source="countries", many=True, read_only=True)
281+
regions_detail = MiniRegionSerialzier(source="regions", many=True, read_only=True)
282+
hazard_types_detail = HazardTypeSerializer(source="hazard_types", many=True, read_only=True)
283+
alert_per_day_display = serializers.CharField(source="get_alert_per_day_display", read_only=True)
284+
285+
class Meta:
286+
model = AlertSubscription
287+
fields = (
288+
"id",
289+
"user",
290+
"countries",
291+
"regions",
292+
"hazard_types",
293+
"alert_per_day",
294+
"user_detail",
295+
"countries_detail",
296+
"regions_detail",
297+
"hazard_types_detail",
298+
"alert_per_day_display",
299+
"created_at",
300+
"updated_at",
301+
)

0 commit comments

Comments
 (0)