Skip to content

Commit 088414c

Browse files
authored
Add multi-stack support for mobile app (#3500)
# What this PR does Allow creating multiple mobile devices with same `registration_id` for different users (multi-stack support) ## Which issue(s) this PR fixes #3452 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
1 parent 1ac39c2 commit 088414c

15 files changed

+356
-24
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
### Added
11+
12+
- Add backend for multi-stack support for mobile-app @Ferril ([#3500](https://github.com/grafana/oncall/pull/3500))
13+
814
## v1.3.78 (2023-12-12)
915

1016
### Changed

engine/apps/mobile_app/demo_push.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from apps.mobile_app.exceptions import DeviceNotSet
1010
from apps.mobile_app.types import FCMMessageData, MessageType, Platform
11-
from apps.mobile_app.utils import construct_fcm_message, send_push_notification
11+
from apps.mobile_app.utils import add_stack_slug_to_message_title, construct_fcm_message, send_push_notification
1212
from apps.user_management.models import User
1313

1414
if typing.TYPE_CHECKING:
@@ -47,7 +47,8 @@ def _get_test_escalation_fcm_message(user: User, device_to_notify: "FCMDevice",
4747
apns_sound_name = mobile_app_user_settings.get_notification_sound_name(message_type, Platform.IOS)
4848

4949
fcm_message_data: FCMMessageData = {
50-
"title": get_test_push_title(critical),
50+
"title": add_stack_slug_to_message_title(get_test_push_title(critical), user.organization),
51+
"orgName": user.organization.stack_slug,
5152
# Pass user settings, so the Android app can use them to play the correct sound and volume
5253
"default_notification_sound_name": mobile_app_user_settings.get_notification_sound_name(
5354
MessageType.DEFAULT, Platform.ANDROID

engine/apps/mobile_app/serializers.py

+37
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import typing
22

3+
from fcm_django.api.rest_framework import FCMDeviceSerializer as BaseFCMDeviceSerializer
34
from rest_framework import serializers
5+
from rest_framework.serializers import ValidationError
46

57
from apps.mobile_app.models import MobileAppUserSettings
68
from common.api_helpers.custom_fields import TimeZoneField
@@ -43,3 +45,38 @@ def validate_going_oncall_notification_timing(
4345
if option not in notification_timing_options:
4446
raise serializers.ValidationError(detail="invalid timing options")
4547
return going_oncall_notification_timing
48+
49+
50+
class FCMDeviceSerializer(BaseFCMDeviceSerializer):
51+
def validate(self, attrs):
52+
"""
53+
Overrides `validate` method from BaseFCMDeviceSerializer to allow different users have same device
54+
`registration_id` (multi-stack support).
55+
Removed deactivating devices with the same `registration_id` during validation.
56+
"""
57+
devices = None
58+
request_method = None
59+
request = self.context["request"]
60+
61+
if self.initial_data.get("registration_id", None):
62+
request_method = "update" if self.instance else "create"
63+
else:
64+
if request.method in ["PUT", "PATCH"]:
65+
request_method = "update"
66+
elif request.method == "POST":
67+
request_method = "create"
68+
69+
Device = self.Meta.model
70+
# unique together with registration_id and user
71+
user = request.user
72+
registration_id = attrs.get("registration_id")
73+
74+
if request_method == "update":
75+
if registration_id:
76+
devices = Device.objects.filter(registration_id=registration_id, user=user).exclude(id=self.instance.id)
77+
elif request_method == "create":
78+
devices = Device.objects.filter(user=user, registration_id=registration_id)
79+
80+
if devices:
81+
raise ValidationError({"registration_id": "This field must be unique per us."})
82+
return attrs

engine/apps/mobile_app/tasks/going_oncall_notification.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from firebase_admin.messaging import APNSPayload, Aps, ApsAlert, CriticalSound, Message
1313

1414
from apps.mobile_app.types import FCMMessageData, MessageType, Platform
15-
from apps.mobile_app.utils import MAX_RETRIES, construct_fcm_message, send_push_notification
15+
from apps.mobile_app.utils import (
16+
MAX_RETRIES,
17+
add_stack_slug_to_message_title,
18+
construct_fcm_message,
19+
send_push_notification,
20+
)
1621
from apps.schedules.models.on_call_schedule import OnCallSchedule, ScheduleEvent
1722
from apps.user_management.models import User
1823
from common.cache import ensure_cache_key_allocates_to_the_same_hash_slot
@@ -72,8 +77,9 @@ def _get_fcm_message(
7277
notification_subtitle = _get_notification_subtitle(schedule, schedule_event, mobile_app_user_settings)
7378

7479
data: FCMMessageData = {
75-
"title": notification_title,
80+
"title": add_stack_slug_to_message_title(notification_title, user.organization),
7681
"subtitle": notification_subtitle,
82+
"orgName": user.organization.stack_slug,
7783
"info_notification_sound_name": mobile_app_user_settings.get_notification_sound_name(
7884
MessageType.INFO, Platform.ANDROID
7985
),

engine/apps/mobile_app/tasks/new_alert_group.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
from apps.alerts.models import AlertGroup
99
from apps.mobile_app.alert_rendering import get_push_notification_subtitle
1010
from apps.mobile_app.types import FCMMessageData, MessageType, Platform
11-
from apps.mobile_app.utils import MAX_RETRIES, construct_fcm_message, send_push_notification
11+
from apps.mobile_app.utils import (
12+
MAX_RETRIES,
13+
add_stack_slug_to_message_title,
14+
construct_fcm_message,
15+
send_push_notification,
16+
)
1217
from apps.user_management.models import User
1318
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
1419

@@ -41,7 +46,7 @@ def _get_fcm_message(alert_group: AlertGroup, user: User, device_to_notify: "FCM
4146
apns_sound_name = mobile_app_user_settings.get_notification_sound_name(message_type, Platform.IOS)
4247

4348
fcm_message_data: FCMMessageData = {
44-
"title": alert_title,
49+
"title": add_stack_slug_to_message_title(alert_title, alert_group.channel.organization),
4550
"subtitle": alert_subtitle,
4651
"orgId": alert_group.channel.organization.public_primary_key,
4752
"orgName": alert_group.channel.organization.stack_slug,

engine/apps/mobile_app/tasks/new_shift_swap_request.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
from firebase_admin.messaging import APNSPayload, Aps, ApsAlert, CriticalSound, Message
1111

1212
from apps.mobile_app.types import FCMMessageData, MessageType, Platform
13-
from apps.mobile_app.utils import MAX_RETRIES, construct_fcm_message, send_push_notification
13+
from apps.mobile_app.utils import (
14+
MAX_RETRIES,
15+
add_stack_slug_to_message_title,
16+
construct_fcm_message,
17+
send_push_notification,
18+
)
1419
from apps.schedules.models import ShiftSwapRequest
1520
from apps.user_management.models import User
1621
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
@@ -116,8 +121,9 @@ def _get_fcm_message(
116121
route = f"/schedules/{shift_swap_request.schedule.public_primary_key}/ssrs/{shift_swap_request.public_primary_key}"
117122

118123
data: FCMMessageData = {
119-
"title": notification_title,
124+
"title": add_stack_slug_to_message_title(notification_title, user.organization),
120125
"subtitle": notification_subtitle,
126+
"orgName": user.organization.stack_slug,
121127
"route": route,
122128
"info_notification_sound_name": mobile_app_user_settings.get_notification_sound_name(
123129
MessageType.INFO, Platform.ANDROID

engine/apps/mobile_app/tests/tasks/test_going_oncall_notification.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
conditionally_send_going_oncall_push_notifications_for_schedule,
1919
)
2020
from apps.mobile_app.types import MessageType, Platform
21+
from apps.mobile_app.utils import add_stack_slug_to_message_title
2122
from apps.schedules.models import OnCallScheduleCalendar, OnCallScheduleICal, OnCallScheduleWeb
2223
from apps.schedules.models.on_call_schedule import ScheduleEvent
2324

@@ -182,6 +183,13 @@ def test_get_fcm_message(
182183
make_user_for_organization,
183184
make_schedule,
184185
):
186+
organization = make_organization()
187+
user_tz = "Europe/Amsterdam"
188+
user = make_user_for_organization(organization)
189+
user_pk = user.public_primary_key
190+
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb)
191+
notification_thread_id = f"{schedule.public_primary_key}:{user_pk}:going-oncall"
192+
185193
mock_fcm_message = "mncvmnvcmnvcnmvcmncvmn"
186194
mock_notification_title = "asdfasdf"
187195
mock_notification_subtitle = "9:06\u202fAM - 9:06\u202fAM\nSchedule XYZ"
@@ -192,13 +200,6 @@ def test_get_fcm_message(
192200
mock_get_notification_title.return_value = mock_notification_title
193201
mock_get_notification_subtitle.return_value = mock_notification_subtitle
194202

195-
organization = make_organization()
196-
user_tz = "Europe/Amsterdam"
197-
user = make_user_for_organization(organization)
198-
user_pk = user.public_primary_key
199-
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb)
200-
notification_thread_id = f"{schedule.public_primary_key}:{user_pk}:going-oncall"
201-
202203
schedule_event = _create_schedule_event(
203204
timezone.now(),
204205
timezone.now(),
@@ -214,8 +215,9 @@ def test_get_fcm_message(
214215
maus = MobileAppUserSettings.objects.create(user=user, time_zone=user_tz)
215216

216217
data = {
217-
"title": mock_notification_title,
218+
"title": add_stack_slug_to_message_title(mock_notification_title, organization),
218219
"subtitle": mock_notification_subtitle,
220+
"orgName": organization.stack_slug,
219221
"info_notification_sound_name": maus.get_notification_sound_name(MessageType.INFO, Platform.ANDROID),
220222
"info_notification_volume_type": maus.info_notification_volume_type,
221223
"info_notification_volume": str(maus.info_notification_volume),

engine/apps/mobile_app/tests/tasks/test_new_shift_swap_request.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
notify_shift_swap_requests,
2020
notify_user_about_shift_swap_request,
2121
)
22+
from apps.mobile_app.utils import add_stack_slug_to_message_title
2223
from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb, ShiftSwapRequest
2324
from apps.user_management.models import User
2425
from apps.user_management.models.user import default_working_hours
@@ -268,7 +269,7 @@ def test_notify_user_about_shift_swap_request(
268269

269270
message: Message = mock_send_push_notification.call_args.args[1]
270271
assert message.data["type"] == "oncall.info"
271-
assert message.data["title"] == "New shift swap request"
272+
assert message.data["title"] == add_stack_slug_to_message_title("New shift swap request", organization)
272273
assert message.data["subtitle"] == "John Doe, Test Schedule"
273274
assert (
274275
message.data["route"]
@@ -467,7 +468,9 @@ def test_notify_beneficiary_about_taken_shift_swap_request(
467468

468469
message: Message = mock_send_push_notification.call_args.args[1]
469470
assert message.data["type"] == "oncall.info"
470-
assert message.data["title"] == "Your shift swap request has been taken"
471+
assert message.data["title"] == add_stack_slug_to_message_title(
472+
"Your shift swap request has been taken", organization
473+
)
471474
assert message.data["subtitle"] == schedule_name
472475
assert (
473476
message.data["route"]

engine/apps/mobile_app/tests/test_demo_push.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from apps.mobile_app.demo_push import _get_test_escalation_fcm_message, get_test_push_title
44
from apps.mobile_app.models import FCMDevice, MobileAppUserSettings
5+
from apps.mobile_app.utils import add_stack_slug_to_message_title
56

67

78
@pytest.mark.django_db
@@ -33,7 +34,7 @@ def test_test_escalation_fcm_message_user_settings(
3334
# Check expected test push content
3435
assert message.apns.payload.aps.badge is None
3536
assert message.apns.payload.aps.alert.title == get_test_push_title(critical=False)
36-
assert message.data["title"] == get_test_push_title(critical=False)
37+
assert message.data["title"] == add_stack_slug_to_message_title(get_test_push_title(critical=False), organization)
3738
assert message.data["type"] == "oncall.message"
3839

3940

@@ -67,7 +68,7 @@ def test_escalation_fcm_message_user_settings_critical(
6768
# Check expected test push content
6869
assert message.apns.payload.aps.badge is None
6970
assert message.apns.payload.aps.alert.title == get_test_push_title(critical=True)
70-
assert message.data["title"] == get_test_push_title(critical=True)
71+
assert message.data["title"] == add_stack_slug_to_message_title(get_test_push_title(critical=True), organization)
7172
assert message.data["type"] == "oncall.critical_message"
7273

7374

@@ -93,4 +94,4 @@ def test_escalation_fcm_message_user_settings_critical_override_dnd_disabled(
9394
# Check expected test push content
9495
assert message.apns.payload.aps.badge is None
9596
assert message.apns.payload.aps.alert.title == get_test_push_title(critical=True)
96-
assert message.data["title"] == get_test_push_title(critical=True)
97+
assert message.data["title"] == add_stack_slug_to_message_title(get_test_push_title(critical=True), organization)

0 commit comments

Comments
 (0)