From bba831ca8122741fed768147331cc27bc7a40cce Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Fri, 17 Mar 2023 18:04:39 +0000 Subject: [PATCH 01/10] Add model + API for getting / updating mobile app settings --- .../migrations/0003_mobileappusersettings.py | 30 ++++++++++++ engine/apps/mobile_app/models.py | 28 +++++++++++ engine/apps/mobile_app/serializers.py | 17 +++++++ .../mobile_app/tests/test_user_settings.py | 49 +++++++++++++++++++ engine/apps/mobile_app/urls.py | 3 +- engine/apps/mobile_app/views.py | 16 +++++- engine/conftest.py | 24 ++++++++- 7 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 engine/apps/mobile_app/migrations/0003_mobileappusersettings.py create mode 100644 engine/apps/mobile_app/serializers.py create mode 100644 engine/apps/mobile_app/tests/test_user_settings.py diff --git a/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py b/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py new file mode 100644 index 0000000000..7be893817c --- /dev/null +++ b/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.18 on 2023-03-17 17:58 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_management', '0009_organization_cluster_slug'), + ('mobile_app', '0002_alter_mobileappauthtoken_user'), + ] + + operations = [ + migrations.CreateModel( + name='MobileAppUserSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('default_notification_sound_name', models.CharField(default='default', max_length=100)), + ('default_notification_volume_type', models.CharField(choices=[('constant', 'Constant'), ('intensifying', 'Intensifying')], default='constant', max_length=50)), + ('default_notification_volume', models.FloatField(default=0.6, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), + ('default_notification_volume_override', models.BooleanField(default=False)), + ('critical_notification_sound_name', models.CharField(default='default', max_length=100)), + ('critical_notification_volume_type', models.CharField(choices=[('constant', 'Constant'), ('intensifying', 'Intensifying')], default='constant', max_length=50)), + ('critical_notification_volume', models.FloatField(default=0.6, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='user_management.user')), + ], + ), + ] diff --git a/engine/apps/mobile_app/models.py b/engine/apps/mobile_app/models.py index 08d2c1f295..401d7fbd93 100644 --- a/engine/apps/mobile_app/models.py +++ b/engine/apps/mobile_app/models.py @@ -1,6 +1,7 @@ from typing import Tuple from django.conf import settings +from django.core import validators from django.db import models from django.utils import timezone @@ -68,3 +69,30 @@ def create_auth_token(cls, user: User, organization: Organization) -> Tuple["Mob organization=organization, ) return instance, token_string + + +class MobileAppUserSettings(models.Model): + class VolumeType(models.TextChoices): + CONSTANT = "constant" + INTENSIFYING = "intensifying" + + user = models.OneToOneField(to=User, null=False, on_delete=models.CASCADE) + + # Push notification settings for default notifications + default_notification_sound_name = models.CharField(max_length=100, default="default") + default_notification_volume_type = models.CharField( + max_length=50, choices=VolumeType.choices, default=VolumeType.CONSTANT + ) + default_notification_volume = models.FloatField( + validators=[validators.MinValueValidator(0.0), validators.MaxValueValidator(1.0)], default=0.6 + ) + default_notification_volume_override = models.BooleanField(default=False) + + # Push notification settings for critical notifications + critical_notification_sound_name = models.CharField(max_length=100, default="default") + critical_notification_volume_type = models.CharField( + max_length=50, choices=VolumeType.choices, default=VolumeType.CONSTANT + ) + critical_notification_volume = models.FloatField( + validators=[validators.MinValueValidator(0.0), validators.MaxValueValidator(1.0)], default=0.6 + ) diff --git a/engine/apps/mobile_app/serializers.py b/engine/apps/mobile_app/serializers.py new file mode 100644 index 0000000000..a683ed514d --- /dev/null +++ b/engine/apps/mobile_app/serializers.py @@ -0,0 +1,17 @@ +from rest_framework import serializers + +from apps.mobile_app.models import MobileAppUserSettings + + +class MobileAppUserSettingsSerializer(serializers.ModelSerializer): + class Meta: + model = MobileAppUserSettings + fields = ( + "default_notification_sound_name", + "default_notification_volume_type", + "default_notification_volume", + "default_notification_volume_override", + "critical_notification_sound_name", + "critical_notification_volume_type", + "critical_notification_volume", + ) diff --git a/engine/apps/mobile_app/tests/test_user_settings.py b/engine/apps/mobile_app/tests/test_user_settings.py new file mode 100644 index 0000000000..f91c063b89 --- /dev/null +++ b/engine/apps/mobile_app/tests/test_user_settings.py @@ -0,0 +1,49 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient + + +@pytest.mark.django_db +def test_user_settings_get(make_organization_and_user_with_mobile_app_auth_token): + organization, user, auth_token = make_organization_and_user_with_mobile_app_auth_token() + + client = APIClient() + url = reverse("mobile_app:user_settings") + + response = client.get(url, HTTP_AUTHORIZATION=auth_token) + assert response.status_code == status.HTTP_200_OK + + # Check the default values are correct + assert response.json() == { + "default_notification_sound_name": "default", + "default_notification_volume_type": "constant", + "default_notification_volume": 0.6, + "default_notification_volume_override": False, + "critical_notification_sound_name": "default", + "critical_notification_volume_type": "constant", + "critical_notification_volume": 0.6, + } + + +@pytest.mark.django_db +def test_user_settings_put(make_organization_and_user_with_mobile_app_auth_token): + organization, user, auth_token = make_organization_and_user_with_mobile_app_auth_token() + + client = APIClient() + url = reverse("mobile_app:user_settings") + data = { + "default_notification_sound_name": "test_default", + "default_notification_volume_type": "intensifying", + "default_notification_volume": 1, + "default_notification_volume_override": True, + "critical_notification_sound_name": "test_critical", + "critical_notification_volume_type": "intensifying", + "critical_notification_volume": 1, + } + + response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=auth_token) + assert response.status_code == status.HTTP_200_OK + + # Check the values are updated correctly + assert response.json() == data diff --git a/engine/apps/mobile_app/urls.py b/engine/apps/mobile_app/urls.py index 2f0433d911..5d4898e92c 100644 --- a/engine/apps/mobile_app/urls.py +++ b/engine/apps/mobile_app/urls.py @@ -1,5 +1,5 @@ from apps.mobile_app.fcm_relay import FCMRelayView -from apps.mobile_app.views import FCMDeviceAuthorizedViewSet, MobileAppAuthTokenAPIView +from apps.mobile_app.views import FCMDeviceAuthorizedViewSet, MobileAppAuthTokenAPIView, MobileAppUserSettingsAPIView from common.api_helpers.optional_slash_router import OptionalSlashRouter, optional_slash_path app_name = "mobile_app" @@ -10,6 +10,7 @@ urlpatterns = [ *router.urls, optional_slash_path("auth_token", MobileAppAuthTokenAPIView.as_view(), name="auth_token"), + optional_slash_path("user_settings", MobileAppUserSettingsAPIView.as_view(), name="user_settings"), ] urlpatterns += [ diff --git a/engine/apps/mobile_app/views.py b/engine/apps/mobile_app/views.py index 1625744098..035b68fabe 100644 --- a/engine/apps/mobile_app/views.py +++ b/engine/apps/mobile_app/views.py @@ -1,11 +1,13 @@ from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet as BaseFCMDeviceAuthorizedViewSet -from rest_framework import status +from rest_framework import generics, status from rest_framework.exceptions import NotFound +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from apps.mobile_app.auth import MobileAppAuthTokenAuthentication, MobileAppVerificationTokenAuthentication -from apps.mobile_app.models import MobileAppAuthToken +from apps.mobile_app.models import MobileAppAuthToken, MobileAppUserSettings +from apps.mobile_app.serializers import MobileAppUserSettingsSerializer class FCMDeviceAuthorizedViewSet(BaseFCMDeviceAuthorizedViewSet): @@ -50,3 +52,13 @@ def delete(self, request): raise NotFound return Response(status=status.HTTP_204_NO_CONTENT) + + +class MobileAppUserSettingsAPIView(generics.RetrieveUpdateAPIView): + authentication_classes = (MobileAppAuthTokenAuthentication,) + permission_classes = (IsAuthenticated,) + serializer_class = MobileAppUserSettingsSerializer + + def get_object(self): + mobile_app_settings, _ = MobileAppUserSettings.objects.get_or_create(user=self.request.user) + return mobile_app_settings diff --git a/engine/conftest.py b/engine/conftest.py index b4795dedb7..115ae7c652 100644 --- a/engine/conftest.py +++ b/engine/conftest.py @@ -55,7 +55,7 @@ ) from apps.email.tests.factories import EmailMessageFactory from apps.heartbeat.tests.factories import IntegrationHeartBeatFactory -from apps.mobile_app.models import MobileAppVerificationToken +from apps.mobile_app.models import MobileAppAuthToken, MobileAppVerificationToken from apps.schedules.tests.factories import ( CustomOnCallShiftFactory, OnCallScheduleCalendarFactory, @@ -185,6 +185,14 @@ def _make_mobile_app_verification_token_for_user(user, organization): return _make_mobile_app_verification_token_for_user +@pytest.fixture +def make_mobile_app_auth_token_for_user(): + def _make_mobile_app_auth_token_for_user(user, organization): + return MobileAppAuthToken.create_auth_token(user, organization) + + return _make_mobile_app_auth_token_for_user + + @pytest.fixture def make_public_api_token(): def _make_public_api_token(user, organization, name="test_api_token"): @@ -676,6 +684,20 @@ def _make_organization_and_user_with_mobile_app_verification_token( return _make_organization_and_user_with_mobile_app_verification_token +@pytest.fixture() +def make_organization_and_user_with_mobile_app_auth_token( + make_organization_and_user, make_mobile_app_auth_token_for_user +): + def _make_organization_and_user_with_mobile_app_auth_token( + role: typing.Optional[LegacyAccessControlRole] = None, + ): + organization, user = make_organization_and_user(role) + _, token = make_mobile_app_auth_token_for_user(user, organization) + return organization, user, token + + return _make_organization_and_user_with_mobile_app_auth_token + + @pytest.fixture() def mock_send_user_notification_signal(monkeypatch): def mocked_send_signal(*args, **kwargs): From 55b24b9f6c6f267f443bf67f860ae8416abad44a Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Mon, 20 Mar 2023 13:18:15 +0000 Subject: [PATCH 02/10] Pass user settings to FCM + add tests --- engine/apps/mobile_app/models.py | 3 + engine/apps/mobile_app/tasks.py | 126 +++++++++++------- .../apps/mobile_app/tests/test_notify_user.py | 60 ++++++++- 3 files changed, 139 insertions(+), 50 deletions(-) diff --git a/engine/apps/mobile_app/models.py b/engine/apps/mobile_app/models.py index 401d7fbd93..0e7b880162 100644 --- a/engine/apps/mobile_app/models.py +++ b/engine/apps/mobile_app/models.py @@ -83,6 +83,9 @@ class VolumeType(models.TextChoices): default_notification_volume_type = models.CharField( max_length=50, choices=VolumeType.choices, default=VolumeType.CONSTANT ) + + # APNS only allows to specify volume for critical notifications, + # so "default_notification_volume" and "default_notification_volume_override" are only used on Android default_notification_volume = models.FloatField( validators=[validators.MinValueValidator(0.0), validators.MaxValueValidator(1.0)], default=0.6 ) diff --git a/engine/apps/mobile_app/tasks.py b/engine/apps/mobile_app/tasks.py index 8b18f7e519..a53e0f162a 100644 --- a/engine/apps/mobile_app/tasks.py +++ b/engine/apps/mobile_app/tasks.py @@ -67,6 +67,58 @@ def _create_error_log_record(): logger.error(f"Error while sending a mobile push notification: user {user_pk} has no device set up") return + message = _get_fcm_message(alert_group, user, device_to_notify.registration_id, critical) + logger.debug(f"Sending push notification with message: {message};") + + if settings.IS_OPEN_SOURCE: + # FCM relay uses cloud connection to send push notifications + from apps.oss_installation.models import CloudConnector + + if not CloudConnector.objects.exists(): + _create_error_log_record() + logger.error(f"Error while sending a mobile push notification: not connected to cloud") + return + + try: + response = send_push_notification_to_fcm_relay(message) + logger.debug(f"FCM relay response: {response}") + except HTTPError as e: + if status.HTTP_400_BAD_REQUEST <= e.response.status_code < status.HTTP_500_INTERNAL_SERVER_ERROR: + # do not retry on HTTP client errors (4xx errors) + _create_error_log_record() + logger.error( + f"Error while sending a mobile push notification: HTTP client error {e.response.status_code}" + ) + return + else: + raise + else: + # https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream + response = device_to_notify.send_message(message) + logger.debug(f"FCM response: {response}") + + if isinstance(response, FirebaseError): + raise response + + +def send_push_notification_to_fcm_relay(message): + """ + Send push notification to FCM relay on cloud instance: apps.mobile_app.fcm_relay.FCMRelayView + """ + url = create_engine_url("mobile_app/v1/fcm_relay", override_base=settings.GRAFANA_CLOUD_ONCALL_API_URL) + + response = requests.post( + url, headers={"Authorization": live_settings.GRAFANA_CLOUD_ONCALL_TOKEN}, json=json.loads(str(message)) + ) + response.raise_for_status() + + return response + + +def _get_fcm_message(alert_group, user, registration_id, critical): + # avoid circular import + from apps.mobile_app.models import MobileAppUserSettings + thread_id = f"{alert_group.channel.organization.public_primary_key}:{alert_group.public_primary_key}" number_of_alerts = alert_group.alerts.count() @@ -87,8 +139,18 @@ def _create_error_log_record(): alert_body = f"Status: {status_verbose}, alerts: {alerts_count_str}" + mobile_app_user_settings, _ = MobileAppUserSettings.objects.get_or_create(user=user) + + # APNS only allows to specify volume for critical notifications + apns_volume = mobile_app_user_settings.critical_notification_volume if critical else None + apns_sound_name = ( + mobile_app_user_settings.critical_notification_sound_name + if critical + else mobile_app_user_settings.default_notification_sound_name + ) + message = Message( - token=device_to_notify.registration_id, + token=registration_id, data={ # from the docs.. # A dictionary of data fields (optional). All keys and values in the dictionary must be strings @@ -103,6 +165,16 @@ def _create_error_log_record(): "subtitle": alert_subtitle, "body": alert_body, "thread_id": thread_id, + # Pass user settings, so the Android app can use them to play the correct sound and volume + "default_notification_sound_name": mobile_app_user_settings.default_notification_sound_name, + "default_notification_volume_type": mobile_app_user_settings.default_notification_volume_type, + "default_notification_volume": str(mobile_app_user_settings.default_notification_volume), + "default_notification_volume_override": json.dumps( + mobile_app_user_settings.default_notification_volume_override + ), + "critical_notification_sound_name": mobile_app_user_settings.critical_notification_sound_name, + "critical_notification_volume_type": mobile_app_user_settings.critical_notification_volume_type, + "critical_notification_volume": str(mobile_app_user_settings.critical_notification_volume), }, apns=APNSConfig( payload=APNSPayload( @@ -111,9 +183,9 @@ def _create_error_log_record(): badge=number_of_alerts, alert=ApsAlert(title=alert_title, subtitle=alert_subtitle, body=alert_body), sound=CriticalSound( - critical=1 if critical else 0, - name="ambulance.aiff" if critical else "bingbong.aiff", - volume=1, + critical=critical, + name=apns_sound_name, + volume=apns_volume, ), custom_data={ "interruption-level": "critical" if critical else "time-sensitive", @@ -123,48 +195,4 @@ def _create_error_log_record(): ), ) - logger.debug(f"Sending push notification with message: {message}; thread-id: {thread_id};") - - if settings.IS_OPEN_SOURCE: - # FCM relay uses cloud connection to send push notifications - from apps.oss_installation.models import CloudConnector - - if not CloudConnector.objects.exists(): - _create_error_log_record() - logger.error(f"Error while sending a mobile push notification: not connected to cloud") - return - - try: - response = send_push_notification_to_fcm_relay(message) - logger.debug(f"FCM relay response: {response}") - except HTTPError as e: - if status.HTTP_400_BAD_REQUEST <= e.response.status_code < status.HTTP_500_INTERNAL_SERVER_ERROR: - # do not retry on HTTP client errors (4xx errors) - _create_error_log_record() - logger.error( - f"Error while sending a mobile push notification: HTTP client error {e.response.status_code}" - ) - return - else: - raise - else: - # https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream - response = device_to_notify.send_message(message) - logger.debug(f"FCM response: {response}") - - if isinstance(response, FirebaseError): - raise response - - -def send_push_notification_to_fcm_relay(message): - """ - Send push notification to FCM relay on cloud instance: apps.mobile_app.fcm_relay.FCMRelayView - """ - url = create_engine_url("mobile_app/v1/fcm_relay", override_base=settings.GRAFANA_CLOUD_ONCALL_API_URL) - - response = requests.post( - url, headers={"Authorization": live_settings.GRAFANA_CLOUD_ONCALL_TOKEN}, json=json.loads(str(message)) - ) - response.raise_for_status() - - return response + return message diff --git a/engine/apps/mobile_app/tests/test_notify_user.py b/engine/apps/mobile_app/tests/test_notify_user.py index 8b64eda303..37d0eb63bd 100644 --- a/engine/apps/mobile_app/tests/test_notify_user.py +++ b/engine/apps/mobile_app/tests/test_notify_user.py @@ -5,7 +5,7 @@ from firebase_admin.exceptions import FirebaseError from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord -from apps.mobile_app.tasks import notify_user_async +from apps.mobile_app.tasks import _get_fcm_message, notify_user_async from apps.oss_installation.models import CloudConnector MOBILE_APP_BACKEND_ID = 5 @@ -209,3 +209,61 @@ def test_notify_user_retry( notification_policy_pk=notification_policy.pk, critical=False, ) + + +@pytest.mark.django_db +def test_fcm_message_user_settings( + make_organization_and_user, make_alert_receive_channel, make_alert_group, make_alert +): + organization, user = make_organization_and_user() + device = FCMDevice.objects.create(user=user, registration_id="test_device_id") + + alert_receive_channel = make_alert_receive_channel(organization=organization) + alert_group = make_alert_group(alert_receive_channel) + make_alert(alert_group=alert_group, raw_request_data={}) + + message = _get_fcm_message(alert_group, user, device.registration_id, critical=False) + + # Check user settings are passed to FCM message + assert message.data["default_notification_sound_name"] == "default" + assert message.data["default_notification_volume_type"] == "constant" + assert message.data["default_notification_volume_override"] == "false" + assert message.data["default_notification_volume"] == "0.6" + assert message.data["critical_notification_sound_name"] == "default" + assert message.data["critical_notification_volume_type"] == "constant" + assert message.data["critical_notification_volume"] == "0.6" + + # Check APNS notification sound is set correctly + apns_sound = message.apns.payload.aps.sound + assert apns_sound.critical is False + assert apns_sound.name == "default" + assert apns_sound.volume is None # APNS doesn't allow to specify volume for non-critical notifications + + +@pytest.mark.django_db +def test_fcm_message_user_settings_critical( + make_organization_and_user, make_alert_receive_channel, make_alert_group, make_alert +): + organization, user = make_organization_and_user() + device = FCMDevice.objects.create(user=user, registration_id="test_device_id") + + alert_receive_channel = make_alert_receive_channel(organization=organization) + alert_group = make_alert_group(alert_receive_channel) + make_alert(alert_group=alert_group, raw_request_data={}) + + message = _get_fcm_message(alert_group, user, device.registration_id, critical=True) + + # Check user settings are passed to FCM message + assert message.data["default_notification_sound_name"] == "default" + assert message.data["default_notification_volume_type"] == "constant" + assert message.data["default_notification_volume_override"] == "false" + assert message.data["default_notification_volume"] == "0.6" + assert message.data["critical_notification_sound_name"] == "default" + assert message.data["critical_notification_volume_type"] == "constant" + assert message.data["critical_notification_volume"] == "0.6" + + # Check APNS notification sound is set correctly + apns_sound = message.apns.payload.aps.sound + assert apns_sound.critical is True + assert apns_sound.name == "default" + assert apns_sound.volume == 0.6 From ebf029324ecd6787ee71ccb63ca1e3ccc77b950a Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Mon, 20 Mar 2023 14:21:22 +0000 Subject: [PATCH 03/10] Add TODOs --- engine/apps/mobile_app/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/apps/mobile_app/models.py b/engine/apps/mobile_app/models.py index 0e7b880162..a92917839f 100644 --- a/engine/apps/mobile_app/models.py +++ b/engine/apps/mobile_app/models.py @@ -79,7 +79,7 @@ class VolumeType(models.TextChoices): user = models.OneToOneField(to=User, null=False, on_delete=models.CASCADE) # Push notification settings for default notifications - default_notification_sound_name = models.CharField(max_length=100, default="default") + default_notification_sound_name = models.CharField(max_length=100, default="default") # TODO: check sound name default_notification_volume_type = models.CharField( max_length=50, choices=VolumeType.choices, default=VolumeType.CONSTANT ) @@ -92,7 +92,7 @@ class VolumeType(models.TextChoices): default_notification_volume_override = models.BooleanField(default=False) # Push notification settings for critical notifications - critical_notification_sound_name = models.CharField(max_length=100, default="default") + critical_notification_sound_name = models.CharField(max_length=100, default="default") # TODO: check sound name critical_notification_volume_type = models.CharField( max_length=50, choices=VolumeType.choices, default=VolumeType.CONSTANT ) From 992385c8dca7fd207acd77f1fae5f9750aa83958 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Mon, 20 Mar 2023 16:00:42 +0000 Subject: [PATCH 04/10] Add `critical_notification_override_dnd` --- .../migrations/0003_mobileappusersettings.py | 3 ++- engine/apps/mobile_app/models.py | 4 +++ engine/apps/mobile_app/serializers.py | 1 + engine/apps/mobile_app/tasks.py | 6 ++++- .../apps/mobile_app/tests/test_notify_user.py | 26 +++++++++++++++++++ .../mobile_app/tests/test_user_settings.py | 2 ++ 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py b/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py index 7be893817c..7c97ddbed4 100644 --- a/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py +++ b/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.18 on 2023-03-17 17:58 +# Generated by Django 3.2.18 on 2023-03-20 15:50 import django.core.validators from django.db import migrations, models @@ -24,6 +24,7 @@ class Migration(migrations.Migration): ('critical_notification_sound_name', models.CharField(default='default', max_length=100)), ('critical_notification_volume_type', models.CharField(choices=[('constant', 'Constant'), ('intensifying', 'Intensifying')], default='constant', max_length=50)), ('critical_notification_volume', models.FloatField(default=0.6, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), + ('critical_notification_override_dnd', models.BooleanField(default=True)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='user_management.user')), ], ), diff --git a/engine/apps/mobile_app/models.py b/engine/apps/mobile_app/models.py index a92917839f..0c67f5db3b 100644 --- a/engine/apps/mobile_app/models.py +++ b/engine/apps/mobile_app/models.py @@ -99,3 +99,7 @@ class VolumeType(models.TextChoices): critical_notification_volume = models.FloatField( validators=[validators.MinValueValidator(0.0), validators.MaxValueValidator(1.0)], default=0.6 ) + + # For the "Mobile push critical" step it's possible to make notifications non-critical + # if "override DND" setting is disabled in the app + critical_notification_override_dnd = models.BooleanField(default=True) diff --git a/engine/apps/mobile_app/serializers.py b/engine/apps/mobile_app/serializers.py index a683ed514d..391226e62c 100644 --- a/engine/apps/mobile_app/serializers.py +++ b/engine/apps/mobile_app/serializers.py @@ -14,4 +14,5 @@ class Meta: "critical_notification_sound_name", "critical_notification_volume_type", "critical_notification_volume", + "critical_notification_override_dnd", ) diff --git a/engine/apps/mobile_app/tasks.py b/engine/apps/mobile_app/tasks.py index a53e0f162a..50e05cc550 100644 --- a/engine/apps/mobile_app/tasks.py +++ b/engine/apps/mobile_app/tasks.py @@ -175,6 +175,9 @@ def _get_fcm_message(alert_group, user, registration_id, critical): "critical_notification_sound_name": mobile_app_user_settings.critical_notification_sound_name, "critical_notification_volume_type": mobile_app_user_settings.critical_notification_volume_type, "critical_notification_volume": str(mobile_app_user_settings.critical_notification_volume), + "critical_notification_override_dnd": json.dumps( + mobile_app_user_settings.critical_notification_override_dnd + ), }, apns=APNSConfig( payload=APNSPayload( @@ -183,7 +186,8 @@ def _get_fcm_message(alert_group, user, registration_id, critical): badge=number_of_alerts, alert=ApsAlert(title=alert_title, subtitle=alert_subtitle, body=alert_body), sound=CriticalSound( - critical=critical, + # The notification shouldn't be critical if the user has disabled "override DND" setting + critical=(critical and mobile_app_user_settings.critical_notification_override_dnd), name=apns_sound_name, volume=apns_volume, ), diff --git a/engine/apps/mobile_app/tests/test_notify_user.py b/engine/apps/mobile_app/tests/test_notify_user.py index 37d0eb63bd..cdec8fccb4 100644 --- a/engine/apps/mobile_app/tests/test_notify_user.py +++ b/engine/apps/mobile_app/tests/test_notify_user.py @@ -5,6 +5,7 @@ from firebase_admin.exceptions import FirebaseError from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord +from apps.mobile_app.models import MobileAppUserSettings from apps.mobile_app.tasks import _get_fcm_message, notify_user_async from apps.oss_installation.models import CloudConnector @@ -232,6 +233,7 @@ def test_fcm_message_user_settings( assert message.data["critical_notification_sound_name"] == "default" assert message.data["critical_notification_volume_type"] == "constant" assert message.data["critical_notification_volume"] == "0.6" + assert message.data["critical_notification_override_dnd"] == "true" # Check APNS notification sound is set correctly apns_sound = message.apns.payload.aps.sound @@ -261,9 +263,33 @@ def test_fcm_message_user_settings_critical( assert message.data["critical_notification_sound_name"] == "default" assert message.data["critical_notification_volume_type"] == "constant" assert message.data["critical_notification_volume"] == "0.6" + assert message.data["critical_notification_override_dnd"] == "true" # Check APNS notification sound is set correctly apns_sound = message.apns.payload.aps.sound assert apns_sound.critical is True assert apns_sound.name == "default" assert apns_sound.volume == 0.6 + + +@pytest.mark.django_db +def test_fcm_message_user_settings_critical_override_dnd_disabled( + make_organization_and_user, make_alert_receive_channel, make_alert_group, make_alert +): + organization, user = make_organization_and_user() + device = FCMDevice.objects.create(user=user, registration_id="test_device_id") + + alert_receive_channel = make_alert_receive_channel(organization=organization) + alert_group = make_alert_group(alert_receive_channel) + make_alert(alert_group=alert_group, raw_request_data={}) + + # Disable critical notification override DND + MobileAppUserSettings.objects.create(user=user, critical_notification_override_dnd=False) + message = _get_fcm_message(alert_group, user, device.registration_id, critical=True) + + # Check user settings are passed to FCM message + assert message.data["critical_notification_override_dnd"] == "false" + + # Check APNS notification sound is set correctly + apns_sound = message.apns.payload.aps.sound + assert apns_sound.critical is False diff --git a/engine/apps/mobile_app/tests/test_user_settings.py b/engine/apps/mobile_app/tests/test_user_settings.py index f91c063b89..85d11bb461 100644 --- a/engine/apps/mobile_app/tests/test_user_settings.py +++ b/engine/apps/mobile_app/tests/test_user_settings.py @@ -23,6 +23,7 @@ def test_user_settings_get(make_organization_and_user_with_mobile_app_auth_token "critical_notification_sound_name": "default", "critical_notification_volume_type": "constant", "critical_notification_volume": 0.6, + "critical_notification_override_dnd": True, } @@ -40,6 +41,7 @@ def test_user_settings_put(make_organization_and_user_with_mobile_app_auth_token "critical_notification_sound_name": "test_critical", "critical_notification_volume_type": "intensifying", "critical_notification_volume": 1, + "critical_notification_override_dnd": False, } response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=auth_token) From 06ca743a366d0a33dcca2d7bed06894fe4ae325a Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 21 Mar 2023 14:15:50 +0000 Subject: [PATCH 05/10] Tweak default values + add `.aiff` when sending to APNS --- engine/apps/mobile_app/models.py | 8 ++++---- engine/apps/mobile_app/tasks.py | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/engine/apps/mobile_app/models.py b/engine/apps/mobile_app/models.py index 0c67f5db3b..63b9baf7dc 100644 --- a/engine/apps/mobile_app/models.py +++ b/engine/apps/mobile_app/models.py @@ -79,7 +79,7 @@ class VolumeType(models.TextChoices): user = models.OneToOneField(to=User, null=False, on_delete=models.CASCADE) # Push notification settings for default notifications - default_notification_sound_name = models.CharField(max_length=100, default="default") # TODO: check sound name + default_notification_sound_name = models.CharField(max_length=100, default="default_sound") default_notification_volume_type = models.CharField( max_length=50, choices=VolumeType.choices, default=VolumeType.CONSTANT ) @@ -87,17 +87,17 @@ class VolumeType(models.TextChoices): # APNS only allows to specify volume for critical notifications, # so "default_notification_volume" and "default_notification_volume_override" are only used on Android default_notification_volume = models.FloatField( - validators=[validators.MinValueValidator(0.0), validators.MaxValueValidator(1.0)], default=0.6 + validators=[validators.MinValueValidator(0.0), validators.MaxValueValidator(1.0)], default=0.8 ) default_notification_volume_override = models.BooleanField(default=False) # Push notification settings for critical notifications - critical_notification_sound_name = models.CharField(max_length=100, default="default") # TODO: check sound name + critical_notification_sound_name = models.CharField(max_length=100, default="default_sound") critical_notification_volume_type = models.CharField( max_length=50, choices=VolumeType.choices, default=VolumeType.CONSTANT ) critical_notification_volume = models.FloatField( - validators=[validators.MinValueValidator(0.0), validators.MaxValueValidator(1.0)], default=0.6 + validators=[validators.MinValueValidator(0.0), validators.MaxValueValidator(1.0)], default=0.8 ) # For the "Mobile push critical" step it's possible to make notifications non-critical diff --git a/engine/apps/mobile_app/tasks.py b/engine/apps/mobile_app/tasks.py index 50e05cc550..d5599ec1b3 100644 --- a/engine/apps/mobile_app/tasks.py +++ b/engine/apps/mobile_app/tasks.py @@ -147,9 +147,9 @@ def _get_fcm_message(alert_group, user, registration_id, critical): mobile_app_user_settings.critical_notification_sound_name if critical else mobile_app_user_settings.default_notification_sound_name - ) + ) + ".aiff" # iOS app expects the filename to have .aiff extension - message = Message( + return Message( token=registration_id, data={ # from the docs.. @@ -198,5 +198,3 @@ def _get_fcm_message(alert_group, user, registration_id, critical): ), ), ) - - return message From 63aac4f3bd71079acf6ee326fd19d475c4d35430 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 21 Mar 2023 14:33:37 +0000 Subject: [PATCH 06/10] Fix tests --- engine/apps/mobile_app/tests/test_notify_user.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/engine/apps/mobile_app/tests/test_notify_user.py b/engine/apps/mobile_app/tests/test_notify_user.py index cdec8fccb4..11cb92b247 100644 --- a/engine/apps/mobile_app/tests/test_notify_user.py +++ b/engine/apps/mobile_app/tests/test_notify_user.py @@ -226,13 +226,13 @@ def test_fcm_message_user_settings( message = _get_fcm_message(alert_group, user, device.registration_id, critical=False) # Check user settings are passed to FCM message - assert message.data["default_notification_sound_name"] == "default" + assert message.data["default_notification_sound_name"] == "default_sound" assert message.data["default_notification_volume_type"] == "constant" assert message.data["default_notification_volume_override"] == "false" - assert message.data["default_notification_volume"] == "0.6" - assert message.data["critical_notification_sound_name"] == "default" + assert message.data["default_notification_volume"] == "0.8" + assert message.data["critical_notification_sound_name"] == "default_sound" assert message.data["critical_notification_volume_type"] == "constant" - assert message.data["critical_notification_volume"] == "0.6" + assert message.data["critical_notification_volume"] == "0.8" assert message.data["critical_notification_override_dnd"] == "true" # Check APNS notification sound is set correctly @@ -256,13 +256,13 @@ def test_fcm_message_user_settings_critical( message = _get_fcm_message(alert_group, user, device.registration_id, critical=True) # Check user settings are passed to FCM message - assert message.data["default_notification_sound_name"] == "default" + assert message.data["default_notification_sound_name"] == "default_sound" assert message.data["default_notification_volume_type"] == "constant" assert message.data["default_notification_volume_override"] == "false" - assert message.data["default_notification_volume"] == "0.6" - assert message.data["critical_notification_sound_name"] == "default" + assert message.data["default_notification_volume"] == "0.8" + assert message.data["critical_notification_sound_name"] == "default_sound" assert message.data["critical_notification_volume_type"] == "constant" - assert message.data["critical_notification_volume"] == "0.6" + assert message.data["critical_notification_volume"] == "0.8" assert message.data["critical_notification_override_dnd"] == "true" # Check APNS notification sound is set correctly From 40eb71fdf932b4e7fbb21b8362e683b1ee2f2264 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 21 Mar 2023 14:41:20 +0000 Subject: [PATCH 07/10] Fix tests --- engine/apps/mobile_app/tests/test_notify_user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/engine/apps/mobile_app/tests/test_notify_user.py b/engine/apps/mobile_app/tests/test_notify_user.py index 11cb92b247..743f6c6777 100644 --- a/engine/apps/mobile_app/tests/test_notify_user.py +++ b/engine/apps/mobile_app/tests/test_notify_user.py @@ -238,7 +238,7 @@ def test_fcm_message_user_settings( # Check APNS notification sound is set correctly apns_sound = message.apns.payload.aps.sound assert apns_sound.critical is False - assert apns_sound.name == "default" + assert apns_sound.name == "default_sound.aiff" assert apns_sound.volume is None # APNS doesn't allow to specify volume for non-critical notifications @@ -268,8 +268,8 @@ def test_fcm_message_user_settings_critical( # Check APNS notification sound is set correctly apns_sound = message.apns.payload.aps.sound assert apns_sound.critical is True - assert apns_sound.name == "default" - assert apns_sound.volume == 0.6 + assert apns_sound.name == "default_sound.aiff" + assert apns_sound.volume == 0.8 @pytest.mark.django_db From 6b981fcbbb1807e793d0275c0230b9cd2f53d5b6 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 21 Mar 2023 14:47:08 +0000 Subject: [PATCH 08/10] Update migration --- .../migrations/0003_mobileappusersettings.py | 10 +++++----- engine/apps/mobile_app/tests/test_user_settings.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py b/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py index 7c97ddbed4..0a237a8c71 100644 --- a/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py +++ b/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.18 on 2023-03-20 15:50 +# Generated by Django 3.2.18 on 2023-03-21 14:46 import django.core.validators from django.db import migrations, models @@ -17,13 +17,13 @@ class Migration(migrations.Migration): name='MobileAppUserSettings', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('default_notification_sound_name', models.CharField(default='default', max_length=100)), + ('default_notification_sound_name', models.CharField(default='default_sound', max_length=100)), ('default_notification_volume_type', models.CharField(choices=[('constant', 'Constant'), ('intensifying', 'Intensifying')], default='constant', max_length=50)), - ('default_notification_volume', models.FloatField(default=0.6, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), + ('default_notification_volume', models.FloatField(default=0.8, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), ('default_notification_volume_override', models.BooleanField(default=False)), - ('critical_notification_sound_name', models.CharField(default='default', max_length=100)), + ('critical_notification_sound_name', models.CharField(default='default_sound', max_length=100)), ('critical_notification_volume_type', models.CharField(choices=[('constant', 'Constant'), ('intensifying', 'Intensifying')], default='constant', max_length=50)), - ('critical_notification_volume', models.FloatField(default=0.6, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), + ('critical_notification_volume', models.FloatField(default=0.8, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), ('critical_notification_override_dnd', models.BooleanField(default=True)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='user_management.user')), ], diff --git a/engine/apps/mobile_app/tests/test_user_settings.py b/engine/apps/mobile_app/tests/test_user_settings.py index 85d11bb461..9121cbfae1 100644 --- a/engine/apps/mobile_app/tests/test_user_settings.py +++ b/engine/apps/mobile_app/tests/test_user_settings.py @@ -16,13 +16,13 @@ def test_user_settings_get(make_organization_and_user_with_mobile_app_auth_token # Check the default values are correct assert response.json() == { - "default_notification_sound_name": "default", + "default_notification_sound_name": "default_sound", "default_notification_volume_type": "constant", - "default_notification_volume": 0.6, + "default_notification_volume": 0.8, "default_notification_volume_override": False, - "critical_notification_sound_name": "default", + "critical_notification_sound_name": "default_sound", "critical_notification_volume_type": "constant", - "critical_notification_volume": 0.6, + "critical_notification_volume": 0.8, "critical_notification_override_dnd": True, } From a9b2d20e89f271cbeb533f270ba23e18cf312244 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 21 Mar 2023 16:03:21 +0000 Subject: [PATCH 09/10] `default_sound_important` + .mp3 for android notifications --- .../migrations/0003_mobileappusersettings.py | 4 ++-- engine/apps/mobile_app/models.py | 6 +++++- engine/apps/mobile_app/tasks.py | 12 +++++++++--- engine/apps/mobile_app/tests/test_notify_user.py | 10 +++++----- engine/apps/mobile_app/tests/test_user_settings.py | 2 +- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py b/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py index 0a237a8c71..935642d4c6 100644 --- a/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py +++ b/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.18 on 2023-03-21 14:46 +# Generated by Django 3.2.18 on 2023-03-21 15:53 import django.core.validators from django.db import migrations, models @@ -21,7 +21,7 @@ class Migration(migrations.Migration): ('default_notification_volume_type', models.CharField(choices=[('constant', 'Constant'), ('intensifying', 'Intensifying')], default='constant', max_length=50)), ('default_notification_volume', models.FloatField(default=0.8, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), ('default_notification_volume_override', models.BooleanField(default=False)), - ('critical_notification_sound_name', models.CharField(default='default_sound', max_length=100)), + ('critical_notification_sound_name', models.CharField(default='default_sound_important', max_length=100)), ('critical_notification_volume_type', models.CharField(choices=[('constant', 'Constant'), ('intensifying', 'Intensifying')], default='constant', max_length=50)), ('critical_notification_volume', models.FloatField(default=0.8, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), ('critical_notification_override_dnd', models.BooleanField(default=True)), diff --git a/engine/apps/mobile_app/models.py b/engine/apps/mobile_app/models.py index 63b9baf7dc..6533c62ace 100644 --- a/engine/apps/mobile_app/models.py +++ b/engine/apps/mobile_app/models.py @@ -72,6 +72,10 @@ def create_auth_token(cls, user: User, organization: Organization) -> Tuple["Mob class MobileAppUserSettings(models.Model): + # Sound names are stored without extension, extension is added when sending push notifications + IOS_SOUND_NAME_EXTENSION = ".aiff" + ANDROID_SOUND_NAME_EXTENSION = ".mp3" + class VolumeType(models.TextChoices): CONSTANT = "constant" INTENSIFYING = "intensifying" @@ -92,7 +96,7 @@ class VolumeType(models.TextChoices): default_notification_volume_override = models.BooleanField(default=False) # Push notification settings for critical notifications - critical_notification_sound_name = models.CharField(max_length=100, default="default_sound") + critical_notification_sound_name = models.CharField(max_length=100, default="default_sound_important") critical_notification_volume_type = models.CharField( max_length=50, choices=VolumeType.choices, default=VolumeType.CONSTANT ) diff --git a/engine/apps/mobile_app/tasks.py b/engine/apps/mobile_app/tasks.py index d5599ec1b3..5d51fd1607 100644 --- a/engine/apps/mobile_app/tasks.py +++ b/engine/apps/mobile_app/tasks.py @@ -147,7 +147,7 @@ def _get_fcm_message(alert_group, user, registration_id, critical): mobile_app_user_settings.critical_notification_sound_name if critical else mobile_app_user_settings.default_notification_sound_name - ) + ".aiff" # iOS app expects the filename to have .aiff extension + ) + MobileAppUserSettings.IOS_SOUND_NAME_EXTENSION # iOS app expects the filename to have an extension return Message( token=registration_id, @@ -166,13 +166,19 @@ def _get_fcm_message(alert_group, user, registration_id, critical): "body": alert_body, "thread_id": thread_id, # Pass user settings, so the Android app can use them to play the correct sound and volume - "default_notification_sound_name": mobile_app_user_settings.default_notification_sound_name, + "default_notification_sound_name": ( + mobile_app_user_settings.default_notification_sound_name + + MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION + ), "default_notification_volume_type": mobile_app_user_settings.default_notification_volume_type, "default_notification_volume": str(mobile_app_user_settings.default_notification_volume), "default_notification_volume_override": json.dumps( mobile_app_user_settings.default_notification_volume_override ), - "critical_notification_sound_name": mobile_app_user_settings.critical_notification_sound_name, + "critical_notification_sound_name": ( + mobile_app_user_settings.critical_notification_sound_name + + MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION + ), "critical_notification_volume_type": mobile_app_user_settings.critical_notification_volume_type, "critical_notification_volume": str(mobile_app_user_settings.critical_notification_volume), "critical_notification_override_dnd": json.dumps( diff --git a/engine/apps/mobile_app/tests/test_notify_user.py b/engine/apps/mobile_app/tests/test_notify_user.py index 743f6c6777..6b5748e234 100644 --- a/engine/apps/mobile_app/tests/test_notify_user.py +++ b/engine/apps/mobile_app/tests/test_notify_user.py @@ -226,11 +226,11 @@ def test_fcm_message_user_settings( message = _get_fcm_message(alert_group, user, device.registration_id, critical=False) # Check user settings are passed to FCM message - assert message.data["default_notification_sound_name"] == "default_sound" + assert message.data["default_notification_sound_name"] == "default_sound.mp3" assert message.data["default_notification_volume_type"] == "constant" assert message.data["default_notification_volume_override"] == "false" assert message.data["default_notification_volume"] == "0.8" - assert message.data["critical_notification_sound_name"] == "default_sound" + assert message.data["critical_notification_sound_name"] == "default_sound_important.mp3" assert message.data["critical_notification_volume_type"] == "constant" assert message.data["critical_notification_volume"] == "0.8" assert message.data["critical_notification_override_dnd"] == "true" @@ -256,11 +256,11 @@ def test_fcm_message_user_settings_critical( message = _get_fcm_message(alert_group, user, device.registration_id, critical=True) # Check user settings are passed to FCM message - assert message.data["default_notification_sound_name"] == "default_sound" + assert message.data["default_notification_sound_name"] == "default_sound.mp3" assert message.data["default_notification_volume_type"] == "constant" assert message.data["default_notification_volume_override"] == "false" assert message.data["default_notification_volume"] == "0.8" - assert message.data["critical_notification_sound_name"] == "default_sound" + assert message.data["critical_notification_sound_name"] == "default_sound_important.mp3" assert message.data["critical_notification_volume_type"] == "constant" assert message.data["critical_notification_volume"] == "0.8" assert message.data["critical_notification_override_dnd"] == "true" @@ -268,7 +268,7 @@ def test_fcm_message_user_settings_critical( # Check APNS notification sound is set correctly apns_sound = message.apns.payload.aps.sound assert apns_sound.critical is True - assert apns_sound.name == "default_sound.aiff" + assert apns_sound.name == "default_sound_important.aiff" assert apns_sound.volume == 0.8 diff --git a/engine/apps/mobile_app/tests/test_user_settings.py b/engine/apps/mobile_app/tests/test_user_settings.py index 9121cbfae1..6ba7293d3c 100644 --- a/engine/apps/mobile_app/tests/test_user_settings.py +++ b/engine/apps/mobile_app/tests/test_user_settings.py @@ -20,7 +20,7 @@ def test_user_settings_get(make_organization_and_user_with_mobile_app_auth_token "default_notification_volume_type": "constant", "default_notification_volume": 0.8, "default_notification_volume_override": False, - "critical_notification_sound_name": "default_sound", + "critical_notification_sound_name": "default_sound_important", "critical_notification_volume_type": "constant", "critical_notification_volume": 0.8, "critical_notification_override_dnd": True, From 56fdba0e5876831440f5efb94622cb53dca28e60 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 21 Mar 2023 16:07:21 +0000 Subject: [PATCH 10/10] critical -> important --- .../migrations/0003_mobileappusersettings.py | 8 +++---- engine/apps/mobile_app/models.py | 12 +++++----- engine/apps/mobile_app/serializers.py | 8 +++---- engine/apps/mobile_app/tasks.py | 18 +++++++-------- .../apps/mobile_app/tests/test_notify_user.py | 22 +++++++++---------- .../mobile_app/tests/test_user_settings.py | 16 +++++++------- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py b/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py index 935642d4c6..0f9946cfbf 100644 --- a/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py +++ b/engine/apps/mobile_app/migrations/0003_mobileappusersettings.py @@ -21,10 +21,10 @@ class Migration(migrations.Migration): ('default_notification_volume_type', models.CharField(choices=[('constant', 'Constant'), ('intensifying', 'Intensifying')], default='constant', max_length=50)), ('default_notification_volume', models.FloatField(default=0.8, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), ('default_notification_volume_override', models.BooleanField(default=False)), - ('critical_notification_sound_name', models.CharField(default='default_sound_important', max_length=100)), - ('critical_notification_volume_type', models.CharField(choices=[('constant', 'Constant'), ('intensifying', 'Intensifying')], default='constant', max_length=50)), - ('critical_notification_volume', models.FloatField(default=0.8, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), - ('critical_notification_override_dnd', models.BooleanField(default=True)), + ('important_notification_sound_name', models.CharField(default='default_sound_important', max_length=100)), + ('important_notification_volume_type', models.CharField(choices=[('constant', 'Constant'), ('intensifying', 'Intensifying')], default='constant', max_length=50)), + ('important_notification_volume', models.FloatField(default=0.8, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])), + ('important_notification_override_dnd', models.BooleanField(default=True)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='user_management.user')), ], ), diff --git a/engine/apps/mobile_app/models.py b/engine/apps/mobile_app/models.py index 6533c62ace..92d0e7f83c 100644 --- a/engine/apps/mobile_app/models.py +++ b/engine/apps/mobile_app/models.py @@ -95,15 +95,15 @@ class VolumeType(models.TextChoices): ) default_notification_volume_override = models.BooleanField(default=False) - # Push notification settings for critical notifications - critical_notification_sound_name = models.CharField(max_length=100, default="default_sound_important") - critical_notification_volume_type = models.CharField( + # Push notification settings for important notifications + important_notification_sound_name = models.CharField(max_length=100, default="default_sound_important") + important_notification_volume_type = models.CharField( max_length=50, choices=VolumeType.choices, default=VolumeType.CONSTANT ) - critical_notification_volume = models.FloatField( + important_notification_volume = models.FloatField( validators=[validators.MinValueValidator(0.0), validators.MaxValueValidator(1.0)], default=0.8 ) - # For the "Mobile push critical" step it's possible to make notifications non-critical + # For the "Mobile push important" step it's possible to make notifications non-critical # if "override DND" setting is disabled in the app - critical_notification_override_dnd = models.BooleanField(default=True) + important_notification_override_dnd = models.BooleanField(default=True) diff --git a/engine/apps/mobile_app/serializers.py b/engine/apps/mobile_app/serializers.py index 391226e62c..1338ecdc97 100644 --- a/engine/apps/mobile_app/serializers.py +++ b/engine/apps/mobile_app/serializers.py @@ -11,8 +11,8 @@ class Meta: "default_notification_volume_type", "default_notification_volume", "default_notification_volume_override", - "critical_notification_sound_name", - "critical_notification_volume_type", - "critical_notification_volume", - "critical_notification_override_dnd", + "important_notification_sound_name", + "important_notification_volume_type", + "important_notification_volume", + "important_notification_override_dnd", ) diff --git a/engine/apps/mobile_app/tasks.py b/engine/apps/mobile_app/tasks.py index 5d51fd1607..dec51193d6 100644 --- a/engine/apps/mobile_app/tasks.py +++ b/engine/apps/mobile_app/tasks.py @@ -142,9 +142,9 @@ def _get_fcm_message(alert_group, user, registration_id, critical): mobile_app_user_settings, _ = MobileAppUserSettings.objects.get_or_create(user=user) # APNS only allows to specify volume for critical notifications - apns_volume = mobile_app_user_settings.critical_notification_volume if critical else None + apns_volume = mobile_app_user_settings.important_notification_volume if critical else None apns_sound_name = ( - mobile_app_user_settings.critical_notification_sound_name + mobile_app_user_settings.important_notification_sound_name if critical else mobile_app_user_settings.default_notification_sound_name ) + MobileAppUserSettings.IOS_SOUND_NAME_EXTENSION # iOS app expects the filename to have an extension @@ -175,14 +175,14 @@ def _get_fcm_message(alert_group, user, registration_id, critical): "default_notification_volume_override": json.dumps( mobile_app_user_settings.default_notification_volume_override ), - "critical_notification_sound_name": ( - mobile_app_user_settings.critical_notification_sound_name + "important_notification_sound_name": ( + mobile_app_user_settings.important_notification_sound_name + MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION ), - "critical_notification_volume_type": mobile_app_user_settings.critical_notification_volume_type, - "critical_notification_volume": str(mobile_app_user_settings.critical_notification_volume), - "critical_notification_override_dnd": json.dumps( - mobile_app_user_settings.critical_notification_override_dnd + "important_notification_volume_type": mobile_app_user_settings.important_notification_volume_type, + "important_notification_volume": str(mobile_app_user_settings.important_notification_volume), + "important_notification_override_dnd": json.dumps( + mobile_app_user_settings.important_notification_override_dnd ), }, apns=APNSConfig( @@ -193,7 +193,7 @@ def _get_fcm_message(alert_group, user, registration_id, critical): alert=ApsAlert(title=alert_title, subtitle=alert_subtitle, body=alert_body), sound=CriticalSound( # The notification shouldn't be critical if the user has disabled "override DND" setting - critical=(critical and mobile_app_user_settings.critical_notification_override_dnd), + critical=(critical and mobile_app_user_settings.important_notification_override_dnd), name=apns_sound_name, volume=apns_volume, ), diff --git a/engine/apps/mobile_app/tests/test_notify_user.py b/engine/apps/mobile_app/tests/test_notify_user.py index 6b5748e234..8e3d548758 100644 --- a/engine/apps/mobile_app/tests/test_notify_user.py +++ b/engine/apps/mobile_app/tests/test_notify_user.py @@ -230,10 +230,10 @@ def test_fcm_message_user_settings( assert message.data["default_notification_volume_type"] == "constant" assert message.data["default_notification_volume_override"] == "false" assert message.data["default_notification_volume"] == "0.8" - assert message.data["critical_notification_sound_name"] == "default_sound_important.mp3" - assert message.data["critical_notification_volume_type"] == "constant" - assert message.data["critical_notification_volume"] == "0.8" - assert message.data["critical_notification_override_dnd"] == "true" + assert message.data["important_notification_sound_name"] == "default_sound_important.mp3" + assert message.data["important_notification_volume_type"] == "constant" + assert message.data["important_notification_volume"] == "0.8" + assert message.data["important_notification_override_dnd"] == "true" # Check APNS notification sound is set correctly apns_sound = message.apns.payload.aps.sound @@ -260,10 +260,10 @@ def test_fcm_message_user_settings_critical( assert message.data["default_notification_volume_type"] == "constant" assert message.data["default_notification_volume_override"] == "false" assert message.data["default_notification_volume"] == "0.8" - assert message.data["critical_notification_sound_name"] == "default_sound_important.mp3" - assert message.data["critical_notification_volume_type"] == "constant" - assert message.data["critical_notification_volume"] == "0.8" - assert message.data["critical_notification_override_dnd"] == "true" + assert message.data["important_notification_sound_name"] == "default_sound_important.mp3" + assert message.data["important_notification_volume_type"] == "constant" + assert message.data["important_notification_volume"] == "0.8" + assert message.data["important_notification_override_dnd"] == "true" # Check APNS notification sound is set correctly apns_sound = message.apns.payload.aps.sound @@ -283,12 +283,12 @@ def test_fcm_message_user_settings_critical_override_dnd_disabled( alert_group = make_alert_group(alert_receive_channel) make_alert(alert_group=alert_group, raw_request_data={}) - # Disable critical notification override DND - MobileAppUserSettings.objects.create(user=user, critical_notification_override_dnd=False) + # Disable important notification override DND + MobileAppUserSettings.objects.create(user=user, important_notification_override_dnd=False) message = _get_fcm_message(alert_group, user, device.registration_id, critical=True) # Check user settings are passed to FCM message - assert message.data["critical_notification_override_dnd"] == "false" + assert message.data["important_notification_override_dnd"] == "false" # Check APNS notification sound is set correctly apns_sound = message.apns.payload.aps.sound diff --git a/engine/apps/mobile_app/tests/test_user_settings.py b/engine/apps/mobile_app/tests/test_user_settings.py index 6ba7293d3c..de14d9dfc8 100644 --- a/engine/apps/mobile_app/tests/test_user_settings.py +++ b/engine/apps/mobile_app/tests/test_user_settings.py @@ -20,10 +20,10 @@ def test_user_settings_get(make_organization_and_user_with_mobile_app_auth_token "default_notification_volume_type": "constant", "default_notification_volume": 0.8, "default_notification_volume_override": False, - "critical_notification_sound_name": "default_sound_important", - "critical_notification_volume_type": "constant", - "critical_notification_volume": 0.8, - "critical_notification_override_dnd": True, + "important_notification_sound_name": "default_sound_important", + "important_notification_volume_type": "constant", + "important_notification_volume": 0.8, + "important_notification_override_dnd": True, } @@ -38,10 +38,10 @@ def test_user_settings_put(make_organization_and_user_with_mobile_app_auth_token "default_notification_volume_type": "intensifying", "default_notification_volume": 1, "default_notification_volume_override": True, - "critical_notification_sound_name": "test_critical", - "critical_notification_volume_type": "intensifying", - "critical_notification_volume": 1, - "critical_notification_override_dnd": False, + "important_notification_sound_name": "test_important", + "important_notification_volume_type": "intensifying", + "important_notification_volume": 1, + "important_notification_override_dnd": False, } response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=auth_token)